Screenshot guardrails: name every screenshot not COMPLETE on 409 ASSET_IN_POST_PROCESSING (submit, validate; stuck after 5 min in UPLOAD_COMPLETE), apple screenshots --repair-stuck replaces only those in place, part uploads without Expect: 100-continue (failed part deletes the reservation, one retry, never committed), storebound api --confirm <operationId> for agents' plain writes (never owner-only)
This commit is contained in:
parent
164182012f
commit
00756e6908
19 changed files with 475 additions and 45 deletions
|
|
@ -2,7 +2,8 @@
|
|||
//
|
||||
// read GET (and a few POSTs that only compute or query, listed below): runs right away.
|
||||
// write any other POST / PATCH / PUT / DELETE: prints the exact request; sent only with --yes in an interactive
|
||||
// terminal, never under MCP (`storebound mcp`) or the desktop app (`storebound serve`).
|
||||
// terminal, or with --yes --confirm <operationId> (equal to the resolved id) from an agent's shell without a
|
||||
// terminal; never under MCP (`storebound mcp`) or the desktop app (`storebound serve`).
|
||||
// owner writes that submit, release, delete, cancel, change prices, invite or remove testers, send review replies,
|
||||
// start or stop experiments, refund or rotate keys (AGENTS.md rule 1): a write that also needs the operation id
|
||||
// typed at the prompt. Never reachable through MCP or serve.
|
||||
|
|
@ -78,6 +79,10 @@ export interface GateInput {
|
|||
agent: boolean;
|
||||
/** `storebound api get` (the MCP api_get tool): plain GETs only */
|
||||
getOnly: boolean;
|
||||
/** --confirm <operationId>: the non-interactive confirmation for an agent in a shell (plain writes only) */
|
||||
confirm?: string;
|
||||
/** the operation id the call resolved to (what --confirm must equal) */
|
||||
opId?: string;
|
||||
}
|
||||
|
||||
export type Decision =
|
||||
|
|
@ -87,9 +92,13 @@ export type Decision =
|
|||
| { action: 'refuse'; why: string };
|
||||
|
||||
/** --yes without a terminal: refused loudly (exit 1, stderr), never a silent dry run (pitfall 77). */
|
||||
export const NOT_A_TTY = 'refused: writes through `storebound api` need an interactive terminal (stdout is not a TTY: piped or redirected). Nothing was sent. Run it without a pipe or redirect, or use the dedicated storebound command.';
|
||||
export const NOT_A_TTY = 'refused: writes through `storebound api` need an interactive terminal (stdout is not a TTY: piped or redirected). Nothing was sent. Run it without a pipe or redirect, add --confirm <operationId> (agents, plain writes only), or use the dedicated storebound command.';
|
||||
|
||||
/** What to do with one call. Pure. */
|
||||
/** --confirm never stands in for the owner's typed id on an owner-only operation. */
|
||||
export const OWNER_NO_CONFIRM = 'refused: owner-only operation: --confirm does not apply; the owner types the operation id in an interactive terminal. Nothing was sent.';
|
||||
|
||||
/** What to do with one call. Pure. A plain write without a terminal is sent only with --yes and --confirm <operationId>
|
||||
* equal to the resolved operation id (the agent path); owner-only operations always need the owner's terminal. */
|
||||
export function decide(g: GateInput): Decision {
|
||||
if (g.getOnly && g.method.toUpperCase() !== 'GET') return { action: 'refuse', why: 'api get runs GET operations only' };
|
||||
if (g.kind === 'read') return g.dry ? { action: 'dry', why: '--dry' } : { action: 'send' };
|
||||
|
|
@ -97,6 +106,12 @@ export function decide(g: GateInput): Decision {
|
|||
? 'owner-only operation: never through MCP or the desktop app; the owner runs it in a terminal'
|
||||
: 'writes through `storebound api` are not available to MCP or the desktop app; run it in a terminal' };
|
||||
if (g.dry || !g.yes) return { action: 'dry', why: g.dry ? '--dry' : 'writes are dry by default: add --yes (in a terminal) to send' };
|
||||
if (g.confirm !== undefined) {
|
||||
// an agent's typed confirmation: only for plain writes, only when it names this exact operation
|
||||
if (g.kind === 'owner') return { action: 'refuse', why: OWNER_NO_CONFIRM };
|
||||
if (!g.opId || g.confirm !== g.opId) return { action: 'refuse', why: `refused: --confirm ${g.confirm} does not equal the operation id ${g.opId ?? '?'}. Nothing was sent.` };
|
||||
return { action: 'send' };
|
||||
}
|
||||
if (!g.tty) return { action: 'refuse', why: NOT_A_TTY };
|
||||
return g.kind === 'owner' ? { action: 'confirm', typed: true } : { action: 'send' };
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue