Bypass mode (owner opt-in, default off): storebound settings bypass on|off|status and a one-time question in init; MCP owner_step (owner-only commands, dry run first), owner-only api operations with --yes --confirm, console plans may press buttons.final; every action logged to .storebound/owner-actions.log; docs/bypass.md, agent rules, tests
This commit is contained in:
parent
2598c4953a
commit
9bd17c401d
21 changed files with 218 additions and 13 deletions
54
cli/lib/bypass.ts
Normal file
54
cli/lib/bypass.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
// Bypass mode: the owner's opt-in to let an AI agent press the owner-only buttons too (Submit for review, releases and
|
||||
// rollouts, testers, review replies, prices, deletes, cancels, console final buttons). Default OFF. Stored per machine
|
||||
// in ~/.storebound/settings.json, never in a project's config or repo:
|
||||
// storebound settings bypass on | off | status
|
||||
// When ON: the MCP server lists `owner_step` (runs one owner command, dry run first), `storebound api` sends owner-only
|
||||
// operations with --yes --confirm <operationId>, console plans tell the agent it may press buttons.final. Every owner
|
||||
// action an agent runs is appended to <project>/.storebound/owner-actions.log. `bypass off` restores the old behaviour
|
||||
// at once. An AI client may still ask the owner itself for legal agreements, passwords and 2FA (its own rules).
|
||||
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { homedir } from 'node:os';
|
||||
import { dirname, join } from 'node:path';
|
||||
|
||||
export interface Settings { bypass?: { on: boolean; since: string } }
|
||||
|
||||
export const settingsFile = (env: Record<string, string | undefined> = process.env): string =>
|
||||
join(env.STOREBOUND_HOME ?? join(homedir(), '.storebound'), 'settings.json');
|
||||
|
||||
export function readSettings(env: Record<string, string | undefined> = process.env): Settings {
|
||||
try { return JSON.parse(readFileSync(settingsFile(env), 'utf8')) as Settings; } catch { return {}; }
|
||||
}
|
||||
|
||||
export function writeSettings(s: Settings, env: Record<string, string | undefined> = process.env): void {
|
||||
const f = settingsFile(env);
|
||||
mkdirSync(dirname(f), { recursive: true });
|
||||
writeFileSync(f, `${JSON.stringify(s, null, 2)}\n`);
|
||||
}
|
||||
|
||||
/** Whether the owner turned bypass mode on for this machine. */
|
||||
export const bypassOn = (env: Record<string, string | undefined> = process.env): boolean => readSettings(env).bypass?.on === true;
|
||||
|
||||
export function setBypass(on: boolean, env: Record<string, string | undefined> = process.env): Settings {
|
||||
const s = readSettings(env);
|
||||
s.bypass = { on, since: new Date().toISOString() };
|
||||
writeSettings(s, env);
|
||||
return s;
|
||||
}
|
||||
|
||||
/** Owner-only commands `owner_step` may run in bypass mode (CLI words). Everything else has its own MCP tool. */
|
||||
export const OWNER_COMMANDS = [
|
||||
'apple submit', 'apple resubmit', 'apple iap-submit', 'apple release', 'apple preorder',
|
||||
'play release', 'play rollout',
|
||||
'reviews', 'apple testflight', 'play products', 'play subscriptions', 'apple iap', 'apple subscriptions', 'prices',
|
||||
] as const;
|
||||
export type OwnerCommand = typeof OWNER_COMMANDS[number];
|
||||
|
||||
export const BYPASS_NOTICE = 'bypass mode is ON (storebound settings bypass off turns it off): the agent runs owner steps itself; each one is logged to .storebound/owner-actions.log';
|
||||
|
||||
/** One line per owner action an agent ran: when, what, the result. */
|
||||
export function logOwnerAction(root: string, entry: { command: string; dry: boolean; exit: number; summary?: string }): string {
|
||||
const f = join(root, '.storebound', 'owner-actions.log');
|
||||
if (!existsSync(dirname(f))) mkdirSync(dirname(f), { recursive: true });
|
||||
appendFileSync(f, `${new Date().toISOString()}\t${entry.dry ? 'dry' : 'RAN'}\texit ${entry.exit}\t${entry.command}${entry.summary ? `\t${entry.summary.replace(/\s+/g, ' ').slice(0, 300)}` : ''}\n`);
|
||||
return f;
|
||||
}
|
||||
|
|
@ -191,7 +191,7 @@ const DRIVING = [
|
|||
'Text fields: click the field and TYPE the text (key presses), then read the value back. Never set it programmatically (form_input, el.value = …): App Store Connect\'s New App kept its Name and SKU empty after a value set (verified 2026-10-13).',
|
||||
'Owner steps: hand the owner plan.ownerBatch in ONE message before you start (the "before" items: answers, the IARC Terms of Use box) and run without stopping until the "after" items (the final button).',
|
||||
'An answer with status ask-owner: ask the owner in chat before filling it (AGENTS.md rule 5), or leave it empty and tell the owner. Never guess.',
|
||||
'Stop before the final button (buttons.final) and tell the owner exactly what to press; then run the verify command.',
|
||||
'Stop before the final button (buttons.final) and tell the owner exactly what to press; then run the verify command. (Bypass mode on, storebound settings bypass: press buttons.final yourself after the owner\'s standing opt-in; agreements, passwords and 2FA still go to the owner.)',
|
||||
'Never sign in (never type the owner\'s own password), accept an agreement or tick a declaration: the owner does that in the browser. The one password you type is the App Review demo account from .storebound/credentials.json (reviewDemoAccount) on Play\'s Sign in details card, never printed.',
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ export const DESKTOP_COVERAGE: Record<string, Coverage> = {
|
|||
'import': { where: 'App folders / first-run guide: Import from fastlane', methods: ['host:project_import_fastlane'] },
|
||||
'install': { where: `${T}: Install for an AI tool`, methods: ['agent.install'] },
|
||||
'mcp': { not: 'the stdio server for AI tools; the window itself is the GUI over the same commands' },
|
||||
'settings': { not: 'per-machine switch for AI agents (bypass mode); in the window the owner presses owner actions in a native dialog anyway' },
|
||||
'serve': { not: 'the JSON-RPC sidecar the window runs on; nothing to click' },
|
||||
'apps': { where: 'Accounts (store apps per account, key check) and App folders (list, add, remove)', methods: ['host:store_apps_refresh', 'host:account_check', 'host:projects_list', 'host:project_add', 'host:project_remove'] },
|
||||
'desktop': { not: 'opens or installs this window from a terminal' },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue