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

View file

@ -25,7 +25,8 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path';
import pkg from '../package.json' with { type: 'json' };
import { runCommand, type Running } from './lib/runner';
import { listTools, TOOLS, toArgv, validateInput } from './mcp-tools';
import { listTools, OWNER_STEP, TOOLS, toArgv, validateInput } from './mcp-tools';
import { bypassOn, logOwnerAction } from './lib/bypass';
import * as rt from './lib/rt';
/** Current (modern, stateless) revision. */
@ -102,18 +103,23 @@ export async function handle(m: Msg, run: (argv: string[], id: Id) => Promise<{
case 'server/discover':
return ok({ supportedVersions: SUPPORTED, capabilities: { tools: {} }, instructions: INSTRUCTIONS, ttlMs: TOOLS_TTL_MS, cacheScope: 'public' });
case 'ping': return ok({});
case 'tools/list': return ok({ tools: listTools(), ...(era === 'modern' ? { ttlMs: TOOLS_TTL_MS, cacheScope: 'public' } : {}) });
case 'tools/list': return ok({ tools: listTools(bypassOn()), ...(era === 'modern' ? { ttlMs: TOOLS_TTL_MS, cacheScope: 'public' } : {}) });
case 'tools/call': {
const name = String(m.params?.name ?? '');
const tool = TOOLS.find(t => t.name === name);
const tool = TOOLS.find(t => t.name === name) ?? (name === OWNER_STEP.name && bypassOn() ? OWNER_STEP : undefined);
if (!tool) return err(-32602, `Unknown tool: ${name}`);
const input = (m.params?.arguments ?? {}) as Record<string, unknown>;
const problems = validateInput(tool, input);
if (problems.length) return ok({ content: [{ type: 'text', text: `Invalid arguments: ${problems.join('; ')}` }], isError: true });
const { argv: raw, dryRun } = toArgv(tool, input);
const argv = textToFile(raw);
// owner_step runs the real command with --yes; its dry run is the command's own --dry
const argv = tool === OWNER_STEP && !dryRun ? [...textToFile(raw), '--yes'] : textToFile(raw);
const r = await run(argv, m.id);
let out = clip(r.out, argv.includes('--json'));
if (tool === OWNER_STEP) {
const file = logOwnerAction(process.env.STOREBOUND_PROJECT || process.cwd(), { command: `storebound ${argv.join(' ')}`, dry: dryRun, exit: r.code, summary: r.out.trim().split('\n').slice(-1)[0] });
out += `\n(bypass mode: logged to ${file})`;
}
if (dryRun) out += '\n\n(dry run: nothing was changed. Ask the owner before calling again with dry_run: false.)';
const structured = { exitCode: r.code, dryRun, command: `storebound ${argv.join(' ')}`, output: out };
return ok({ content: [{ type: 'text', text: `$ storebound ${argv.join(' ')}\n${out}\n[exit ${r.code}]` }], structuredContent: structured, isError: r.code !== 0 });