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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue