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:
CRTkafa 2026-10-22 02:27:34 +08:00
parent 2598c4953a
commit 9bd17c401d
21 changed files with 218 additions and 13 deletions

54
cli/lib/bypass.ts Normal file
View 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;
}

View file

@ -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.',
];

View file

@ -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' },