switchroom 0.14.33 → 0.14.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11219,6 +11219,7 @@ var profileFields = {
11219
11219
  }).optional()
11220
11220
  }).optional(),
11221
11221
  schedule: exports_external.array(ScheduleEntrySchema).optional(),
11222
+ secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).optional().describe("Operator-granted STANDING vault keys this agent may read via the " + "broker — independent of any cron or MCP server. Use when an agent " + "needs a credential both interactively and in its own (agent-managed) " + "schedules, so the grant lives with the agent rather than welded to a " + "specific cron's `secrets[]`. OPERATOR-SET ONLY: agents cannot edit " + "switchroom.yaml or self-grant (reference/vision.md outcome 2 — 'you " + "hold the leash; only your tap grants it'). Exact key names. Cascades " + "UNION across defaults -> profile -> agent (see docs/configuration.md)."),
11222
11223
  reactions: ReactionsSchema,
11223
11224
  model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only").optional(),
11224
11225
  thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
@@ -11287,6 +11288,7 @@ var AgentSchema = exports_external.object({
11287
11288
  tools: AgentToolsSchema,
11288
11289
  memory: AgentMemorySchema,
11289
11290
  schedule: exports_external.array(ScheduleEntrySchema).default([]),
11291
+ secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).optional(),
11290
11292
  reactions: ReactionsSchema,
11291
11293
  model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only (no spaces or shell specials)").optional().describe("Claude model override (e.g., 'claude-sonnet-4-6')"),
11292
11294
  thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "Per-agent override wins over defaults.thinking_effort. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
@@ -11756,6 +11758,12 @@ function mergeAgentConfig(defaultsIn, agentIn) {
11756
11758
  deny: dedupe([...dDeny, ...aDeny])
11757
11759
  };
11758
11760
  }
11761
+ if (defaults.secrets || merged.secrets) {
11762
+ merged.secrets = dedupe([
11763
+ ...defaults.secrets ?? [],
11764
+ ...merged.secrets ?? []
11765
+ ]);
11766
+ }
11759
11767
  if (defaults.soul || merged.soul) {
11760
11768
  const base = defaults.soul ?? {};
11761
11769
  const override = merged.soul ?? {};
@@ -11219,6 +11219,7 @@ var profileFields = {
11219
11219
  }).optional()
11220
11220
  }).optional(),
11221
11221
  schedule: exports_external.array(ScheduleEntrySchema).optional(),
11222
+ secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).optional().describe("Operator-granted STANDING vault keys this agent may read via the " + "broker — independent of any cron or MCP server. Use when an agent " + "needs a credential both interactively and in its own (agent-managed) " + "schedules, so the grant lives with the agent rather than welded to a " + "specific cron's `secrets[]`. OPERATOR-SET ONLY: agents cannot edit " + "switchroom.yaml or self-grant (reference/vision.md outcome 2 — 'you " + "hold the leash; only your tap grants it'). Exact key names. Cascades " + "UNION across defaults -> profile -> agent (see docs/configuration.md)."),
11222
11223
  reactions: ReactionsSchema,
11223
11224
  model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only").optional(),
11224
11225
  thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
@@ -11287,6 +11288,7 @@ var AgentSchema = exports_external.object({
11287
11288
  tools: AgentToolsSchema,
11288
11289
  memory: AgentMemorySchema,
11289
11290
  schedule: exports_external.array(ScheduleEntrySchema).default([]),
11291
+ secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).optional(),
11290
11292
  reactions: ReactionsSchema,
11291
11293
  model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only (no spaces or shell specials)").optional().describe("Claude model override (e.g., 'claude-sonnet-4-6')"),
11292
11294
  thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "Per-agent override wins over defaults.thinking_effort. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
@@ -11756,6 +11758,12 @@ function mergeAgentConfig(defaultsIn, agentIn) {
11756
11758
  deny: dedupe([...dDeny, ...aDeny])
11757
11759
  };
11758
11760
  }
11761
+ if (defaults.secrets || merged.secrets) {
11762
+ merged.secrets = dedupe([
11763
+ ...defaults.secrets ?? [],
11764
+ ...merged.secrets ?? []
11765
+ ]);
11766
+ }
11759
11767
  if (defaults.soul || merged.soul) {
11760
11768
  const base = defaults.soul ?? {};
11761
11769
  const override = merged.soul ?? {};
@@ -10969,7 +10969,8 @@ var init_protocol = __esm(() => {
10969
10969
  description: exports_external.string().optional(),
10970
10970
  write_keys: exports_external.array(exports_external.string().min(1)).optional(),
10971
10971
  passphrase: exports_external.string().optional(),
10972
- attest_via_posture: exports_external.boolean().optional()
10972
+ attest_via_posture: exports_external.boolean().optional(),
10973
+ decision_id: exports_external.string().optional()
10973
10974
  });
10974
10975
  ListGrantsRequestSchema = exports_external.object({
10975
10976
  v: exports_external.literal(1),
@@ -11966,6 +11967,7 @@ var profileFields = {
11966
11967
  }).optional()
11967
11968
  }).optional(),
11968
11969
  schedule: exports_external.array(ScheduleEntrySchema).optional(),
11970
+ secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).optional().describe("Operator-granted STANDING vault keys this agent may read via the " + "broker \u2014 independent of any cron or MCP server. Use when an agent " + "needs a credential both interactively and in its own (agent-managed) " + "schedules, so the grant lives with the agent rather than welded to a " + "specific cron's `secrets[]`. OPERATOR-SET ONLY: agents cannot edit " + "switchroom.yaml or self-grant (reference/vision.md outcome 2 \u2014 'you " + "hold the leash; only your tap grants it'). Exact key names. Cascades " + "UNION across defaults -> profile -> agent (see docs/configuration.md)."),
11969
11971
  reactions: ReactionsSchema,
11970
11972
  model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only").optional(),
11971
11973
  thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
@@ -12034,6 +12036,7 @@ var AgentSchema = exports_external.object({
12034
12036
  tools: AgentToolsSchema,
12035
12037
  memory: AgentMemorySchema,
12036
12038
  schedule: exports_external.array(ScheduleEntrySchema).default([]),
12039
+ secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).optional(),
12037
12040
  reactions: ReactionsSchema,
12038
12041
  model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only (no spaces or shell specials)").optional().describe("Claude model override (e.g., 'claude-sonnet-4-6')"),
12039
12042
  thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "Per-agent override wins over defaults.thinking_effort. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
@@ -12505,6 +12508,12 @@ function mergeAgentConfig(defaultsIn, agentIn) {
12505
12508
  deny: dedupe([...dDeny, ...aDeny])
12506
12509
  };
12507
12510
  }
12511
+ if (defaults.secrets || merged.secrets) {
12512
+ merged.secrets = dedupe([
12513
+ ...defaults.secrets ?? [],
12514
+ ...merged.secrets ?? []
12515
+ ]);
12516
+ }
12508
12517
  if (defaults.soul || merged.soul) {
12509
12518
  const base = defaults.soul ?? {};
12510
12519
  const override = merged.soul ?? {};
@@ -13783,6 +13783,7 @@ var init_schema = __esm(() => {
13783
13783
  }).optional()
13784
13784
  }).optional(),
13785
13785
  schedule: exports_external.array(ScheduleEntrySchema).optional(),
13786
+ secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).optional().describe("Operator-granted STANDING vault keys this agent may read via the " + "broker \u2014 independent of any cron or MCP server. Use when an agent " + "needs a credential both interactively and in its own (agent-managed) " + "schedules, so the grant lives with the agent rather than welded to a " + "specific cron's `secrets[]`. OPERATOR-SET ONLY: agents cannot edit " + "switchroom.yaml or self-grant (reference/vision.md outcome 2 \u2014 'you " + "hold the leash; only your tap grants it'). Exact key names. Cascades " + "UNION across defaults -> profile -> agent (see docs/configuration.md)."),
13786
13787
  reactions: ReactionsSchema,
13787
13788
  model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only").optional(),
13788
13789
  thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
@@ -13851,6 +13852,7 @@ var init_schema = __esm(() => {
13851
13852
  tools: AgentToolsSchema,
13852
13853
  memory: AgentMemorySchema,
13853
13854
  schedule: exports_external.array(ScheduleEntrySchema).default([]),
13855
+ secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).optional(),
13854
13856
  reactions: ReactionsSchema,
13855
13857
  model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only (no spaces or shell specials)").optional().describe("Claude model override (e.g., 'claude-sonnet-4-6')"),
13856
13858
  thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "Per-agent override wins over defaults.thinking_effort. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
@@ -14372,6 +14374,12 @@ function mergeAgentConfig(defaultsIn, agentIn) {
14372
14374
  deny: dedupe([...dDeny, ...aDeny])
14373
14375
  };
14374
14376
  }
14377
+ if (defaults.secrets || merged.secrets) {
14378
+ merged.secrets = dedupe([
14379
+ ...defaults.secrets ?? [],
14380
+ ...merged.secrets ?? []
14381
+ ]);
14382
+ }
14375
14383
  if (defaults.soul || merged.soul) {
14376
14384
  const base = defaults.soul ?? {};
14377
14385
  const override = merged.soul ?? {};
@@ -22004,7 +22012,8 @@ var init_protocol = __esm(() => {
22004
22012
  description: exports_external.string().optional(),
22005
22013
  write_keys: exports_external.array(exports_external.string().min(1)).optional(),
22006
22014
  passphrase: exports_external.string().optional(),
22007
- attest_via_posture: exports_external.boolean().optional()
22015
+ attest_via_posture: exports_external.boolean().optional(),
22016
+ decision_id: exports_external.string().optional()
22008
22017
  });
22009
22018
  ListGrantsRequestSchema = exports_external.object({
22010
22019
  v: exports_external.literal(1),
@@ -27941,11 +27950,21 @@ function checkAclByAgent(config, agentName, key) {
27941
27950
  return { allow: true };
27942
27951
  }
27943
27952
  }
27953
+ const cfgSecrets = config;
27954
+ const profileSecrets = profileName != null && profileName.length > 0 ? cfgSecrets.profiles?.[profileName]?.secrets : undefined;
27955
+ const standingSecrets = [
27956
+ ...Array.isArray(cfgSecrets.defaults?.secrets) ? cfgSecrets.defaults.secrets : [],
27957
+ ...Array.isArray(profileSecrets) ? profileSecrets : [],
27958
+ ...Array.isArray(agentConfig.secrets) ? agentConfig.secrets : []
27959
+ ];
27960
+ if (standingSecrets.includes(key)) {
27961
+ return { allow: true };
27962
+ }
27944
27963
  const schedule = agentConfig.schedule ?? [];
27945
27964
  if (schedule.length === 0) {
27946
27965
  return {
27947
27966
  allow: false,
27948
- reason: `agent '${agentName}' has no schedule entries declaring 'secrets' and no mcp_servers.*.secrets[] declaring '${key}'; nothing is broker-accessible`
27967
+ reason: `agent '${agentName}' has no schedule entries declaring 'secrets', no mcp_servers.*.secrets[], and no agents.${agentName}.secrets[] standing grant declaring '${key}'; nothing is broker-accessible`
27949
27968
  };
27950
27969
  }
27951
27970
  for (const entry of schedule) {
@@ -49420,8 +49439,8 @@ var {
49420
49439
  } = import__.default;
49421
49440
 
49422
49441
  // src/build-info.ts
49423
- var VERSION = "0.14.33";
49424
- var COMMIT_SHA = "0b73633c";
49442
+ var VERSION = "0.14.35";
49443
+ var COMMIT_SHA = "7ac06aea";
49425
49444
 
49426
49445
  // src/cli/agent.ts
49427
49446
  init_source();
@@ -50323,6 +50342,22 @@ If you genuinely need WebFetch back for one agent (e.g. a workflow
50323
50342
  that depends on its specific output shape), set \`mcp_servers.webkite:
50324
50343
  false\` in that agent's switchroom.yaml block \u2014 webkite goes away
50325
50344
  and the native tools come back together.`;
50345
+ var VAULT_GUIDANCE = `## Secrets in the vault \u2014 discover, don't guess
50346
+
50347
+ Your API keys and credentials live in the Switchroom vault. For normal
50348
+ MCP-tool work they're injected by the launchers and you never touch the
50349
+ raw values. When a task needs a secret directly (a direct API call an
50350
+ MCP tool has no verb for), read it with \`switchroom vault get <key>\`.
50351
+
50352
+ **Never guess a key name.** Run \`switchroom vault list\` to see the
50353
+ exact keys you already hold \u2014 they're usually namespaced \`<you>/...\`
50354
+ (e.g. \`marko/postiz-api-key\`, not \`postiz/api-key\`). Use the real
50355
+ name from that list.
50356
+
50357
+ Only call the \`vault_request_access\` MCP tool \u2014 which pings the
50358
+ operator for a Telegram approval \u2014 for a key you've **confirmed via
50359
+ \`vault list\` you don't already have.** Requesting access to a guessed
50360
+ or already-held key wastes the operator's tap and fails.`;
50326
50361
  function renderFleetInvariants() {
50327
50362
  return [
50328
50363
  "<!--",
@@ -50347,6 +50382,8 @@ function renderFleetInvariants() {
50347
50382
  MEMORY_GUIDANCE,
50348
50383
  "",
50349
50384
  WEB_FETCH_GUIDANCE,
50385
+ "",
50386
+ VAULT_GUIDANCE,
50350
50387
  ""
50351
50388
  ].join(`
50352
50389
  `);
@@ -58988,6 +59025,26 @@ function scrubSqlite(path, values, dryRun) {
58988
59025
  // src/cli/vault.ts
58989
59026
  init_client();
58990
59027
 
59028
+ // src/cli/vault-key-suggest.ts
59029
+ function tokenize(key) {
59030
+ return key.toLowerCase().split(/[/\-_.]+/).filter((t) => t.length > 0);
59031
+ }
59032
+ function suggestVaultKeys(requested, available, max = 3) {
59033
+ const want = new Set(tokenize(requested));
59034
+ if (want.size === 0)
59035
+ return [];
59036
+ const scored = available.map((key) => {
59037
+ const have = new Set(tokenize(key));
59038
+ let shared = 0;
59039
+ for (const t of have)
59040
+ if (want.has(t))
59041
+ shared++;
59042
+ const diff = want.size + have.size - 2 * shared;
59043
+ return { key, shared, diff };
59044
+ }).filter((c) => c.shared > 0 && c.key !== requested).sort((a, b) => b.shared - a.shared || a.diff - b.diff);
59045
+ return scored.slice(0, max).map((c) => c.key);
59046
+ }
59047
+
58991
59048
  // src/cli/vault-broker.ts
58992
59049
  init_loader();
58993
59050
  init_loader();
@@ -61285,9 +61342,20 @@ function migrateApprovalSchema(db) {
61285
61342
  approver_set_canonical TEXT NOT NULL,
61286
61343
  last_used_at INTEGER,
61287
61344
  revoked_at INTEGER,
61288
- revoke_reason TEXT
61345
+ revoke_reason TEXT,
61346
+ -- Provenance of the operator-authorization (RFC vault-approval-hard-
61347
+ -- boundary). 'agent': recorded on a per-agent socket \u2014 claude shares
61348
+ -- that socket, so it is FORGEABLE and must NOT be trusted as proof an
61349
+ -- operator tapped. 'operator': recorded via the host-side verifier on a
61350
+ -- claude-unreachable channel \u2014 the only value the broker's mint gate
61351
+ -- trusts. Default 'agent' (fail-closed for the new gate).
61352
+ origin TEXT NOT NULL DEFAULT 'agent'
61289
61353
  )
61290
61354
  `);
61355
+ const decisionCols = db.query("PRAGMA table_info(approval_decisions)").all();
61356
+ if (!decisionCols.some((c) => c.name === "origin")) {
61357
+ db.run(`ALTER TABLE approval_decisions ADD COLUMN origin TEXT NOT NULL DEFAULT 'agent'`);
61358
+ }
61291
61359
  db.run(`
61292
61360
  CREATE INDEX IF NOT EXISTS approval_decisions_lookup
61293
61361
  ON approval_decisions(agent_unit, scope, action)
@@ -61389,7 +61457,8 @@ function rowToDecision(row) {
61389
61457
  approver_set_canonical: row.approver_set_canonical,
61390
61458
  last_used_at: row.last_used_at ?? null,
61391
61459
  revoked_at: row.revoked_at ?? null,
61392
- revoke_reason: row.revoke_reason ?? null
61460
+ revoke_reason: row.revoke_reason ?? null,
61461
+ origin: row.origin === "operator" ? "operator" : "agent"
61393
61462
  };
61394
61463
  }
61395
61464
  var MAX_PENDING_PER_AGENT = 2;
@@ -61555,8 +61624,8 @@ function recordDecision(db, input, now = Date.now()) {
61555
61624
  db.run(`INSERT INTO approval_decisions
61556
61625
  (id, agent_unit, scope, action, decision,
61557
61626
  ttl_expires_at, granted_at, granted_by_user_id,
61558
- approver_set_canonical, last_used_at, revoked_at, revoke_reason)
61559
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, NULL)`, [
61627
+ approver_set_canonical, last_used_at, revoked_at, revoke_reason, origin)
61628
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, NULL, ?)`, [
61560
61629
  id,
61561
61630
  input.nonce.agent_unit,
61562
61631
  input.nonce.scope,
@@ -61565,7 +61634,8 @@ function recordDecision(db, input, now = Date.now()) {
61565
61634
  ttl_expires_at,
61566
61635
  now,
61567
61636
  input.granted_by_user_id,
61568
- canonical
61637
+ canonical,
61638
+ input.origin ?? "agent"
61569
61639
  ]);
61570
61640
  db.run(`UPDATE approval_nonces SET decision_id = ? WHERE request_id = ?`, [id, input.nonce.request_id]);
61571
61641
  const granted = input.decision === "allow_once" || input.decision === "allow_always" || input.decision === "allow_ttl";
@@ -61619,6 +61689,13 @@ function listDecisions(db, filter, now = Date.now()) {
61619
61689
  const rows = db.query(sql).all(...params);
61620
61690
  return rows.map(rowToDecision);
61621
61691
  }
61692
+ function getDecision(db, id) {
61693
+ const row = db.query(`SELECT * FROM approval_decisions WHERE id = ?`).get(id);
61694
+ return row ? rowToDecision(row) : null;
61695
+ }
61696
+ function isOperatorVerifiedDecision(dec, agent_unit, now = Date.now()) {
61697
+ return dec !== null && dec.origin === "operator" && dec.agent_unit === agent_unit && dec.revoked_at === null && (dec.decision === "allow_once" || dec.decision === "allow_always" || dec.decision === "allow_ttl") && (dec.ttl_expires_at === null || dec.ttl_expires_at > now);
61698
+ }
61622
61699
  function getNonce(db, request_id) {
61623
61700
  const row = db.query(`SELECT * FROM approval_nonces WHERE request_id = ?`).get(request_id);
61624
61701
  return row ? nonceFromRow(row) : null;
@@ -62634,6 +62711,22 @@ class VaultBroker {
62634
62711
  socket.write(encodeResponse(errorResponse("LOCKED", "Broker is locked")));
62635
62712
  return;
62636
62713
  }
62714
+ if (req.op === "mint_grant" && process.env.SWITCHROOM_REQUIRE_OPERATOR_APPROVAL_MINT === "1") {
62715
+ const decisionId = req.decision_id;
62716
+ const dec = decisionId != null && decisionId !== "" ? getDecision(this.grantsDb, decisionId) : null;
62717
+ if (!isOperatorVerifiedDecision(dec, agentName ?? "")) {
62718
+ writeAudit({
62719
+ ts: new Date().toISOString(),
62720
+ op: req.op,
62721
+ caller: auditCaller,
62722
+ pid: auditPid,
62723
+ cgroup: auditCgroup,
62724
+ result: "denied:posture-mint-needs-operator-verified-decision"
62725
+ });
62726
+ socket.write(encodeResponse(errorResponse("DENIED", "posture-attested mint requires an operator-verified approval (host-side tap); none referenced. See RFC vault-approval-hard-boundary.")));
62727
+ return;
62728
+ }
62729
+ }
62637
62730
  mintPostureAttested = true;
62638
62731
  writeAudit({
62639
62732
  ts: new Date().toISOString(),
@@ -64404,14 +64497,19 @@ function refuseSandboxDirectAccess(verbHint) {
64404
64497
  `);
64405
64498
  process.exit(VAULT_EXIT_SANDBOX_CONTEXT);
64406
64499
  }
64407
- function recoveryHint(situation, key) {
64500
+ function recoveryHint(situation, key, suggestions) {
64408
64501
  if (!isSandboxContext()) {
64409
64502
  const keyArg = key ? ` ${key}` : "";
64410
64503
  return `Hint: run 'switchroom vault get --no-broker${keyArg}' for interactive (non-cron) access.`;
64411
64504
  }
64412
64505
  switch (situation) {
64413
- case "denied":
64414
- return `Hint: this agent has no grant for ${key ? `'${key}'` : "this key"}. ` + `Call the \`vault_request_access\` MCP tool ` + `(key=${key ? `'${key}'` : "'<key>'"}, scope='read') ` + `to ask the operator for access via a Telegram approval card. ` + `Do NOT retry with --no-broker \u2014 the vault file is not mounted ` + `into agent containers.`;
64506
+ case "denied": {
64507
+ const named = key ? `'${key}'` : "this key";
64508
+ if (suggestions && suggestions.length > 0) {
64509
+ return `Hint: you have no grant for ${named} \u2014 that key may not even exist. ` + `You ALREADY have access to similar keys: ${suggestions.join(", ")}. ` + `Try \`switchroom vault get <one-of-those>\` instead. ` + `Run \`switchroom vault list\` to see every key you hold. ` + `Only call the \`vault_request_access\` MCP tool for a key you've ` + `confirmed (via \`vault list\`) you don't already have. ` + `Do NOT retry with --no-broker \u2014 the vault file is not mounted ` + `into agent containers.`;
64510
+ }
64511
+ return `Hint: this agent has no grant for ${named}. ` + `FIRST run \`switchroom vault list\` to see the keys you already hold \u2014 ` + `the real name often differs from a guessed one (it's usually ` + `namespaced \`<you>/...\`). ` + `If you genuinely lack it, call the \`vault_request_access\` MCP tool ` + `(key=${key ? `'${key}'` : "'<key>'"}, scope='read') ` + `to ask the operator for access via a Telegram approval card. ` + `Do NOT retry with --no-broker \u2014 the vault file is not mounted ` + `into agent containers.`;
64512
+ }
64415
64513
  case "locked":
64416
64514
  return `Hint: the broker is locked. Ask the operator to unlock it ` + `(\`/vault unlock\` in this chat, or ` + `\`switchroom vault broker unlock\` on the host). ` + `Do NOT retry with --no-broker \u2014 the vault file is not mounted ` + `into agent containers.`;
64417
64515
  case "unreachable":
@@ -64829,8 +64927,14 @@ Push passphrase to broker for future requests? [Y/n]: `);
64829
64927
  if (process.stdin.isTTY) {
64830
64928
  console.error(source_default.yellow(`broker denied request (${result.code}): ${result.msg}. ` + `Falling back to direct vault access.`));
64831
64929
  } else {
64930
+ let suggestions = [];
64931
+ try {
64932
+ const myKeys = await listViaBroker(brokerOpts);
64933
+ if (myKeys && key)
64934
+ suggestions = suggestVaultKeys(key, myKeys);
64935
+ } catch {}
64832
64936
  process.stderr.write(`VAULT-BROKER-DENIED [${result.code}]: ${result.msg}
64833
- ` + `${recoveryHint("denied", key)}
64937
+ ` + `${recoveryHint("denied", key, suggestions)}
64834
64938
  `);
64835
64939
  writeVaultDeniedEnvelope(key, result.code, result.msg);
64836
64940
  process.exit(2);
@@ -75544,7 +75648,7 @@ var DEFAULT_MEMORY_SEARCH_MAX_RESULTS = 6;
75544
75648
  var DEFAULT_MEMORY_SEARCH_SNIPPET_CHARS = 220;
75545
75649
  var MEMORY_SEARCH_MAX_INDEXED_CHARS = 2000000;
75546
75650
  var MEMORY_SEARCH_MAX_FILE_SIZE = 512 * 1024;
75547
- function tokenize(text) {
75651
+ function tokenize2(text) {
75548
75652
  return text.toLowerCase().split(/[^a-z0-9]+/u).filter((t) => t.length > 1 && t.length < 40);
75549
75653
  }
75550
75654
  async function listMarkdownFiles(workspaceDir, maxDepth = 3) {
@@ -75596,7 +75700,7 @@ async function loadIndex(workspaceDir) {
75596
75700
  if (content.length === 0)
75597
75701
  continue;
75598
75702
  totalChars += content.length;
75599
- const terms = tokenize(content);
75703
+ const terms = tokenize2(content);
75600
75704
  const freq = new Map;
75601
75705
  for (const t of terms) {
75602
75706
  freq.set(t, (freq.get(t) ?? 0) + 1);
@@ -75669,7 +75773,7 @@ function computeIdf(index, queryTerms) {
75669
75773
  async function searchWorkspaceMemory(params) {
75670
75774
  const maxResults = params.maxResults ?? DEFAULT_MEMORY_SEARCH_MAX_RESULTS;
75671
75775
  const snippetChars = params.snippetChars ?? DEFAULT_MEMORY_SEARCH_SNIPPET_CHARS;
75672
- const queryTerms = Array.from(new Set(tokenize(params.query)));
75776
+ const queryTerms = Array.from(new Set(tokenize2(params.query)));
75673
75777
  if (queryTerms.length === 0) {
75674
75778
  return { query: params.query, indexedFiles: 0, totalMatches: 0, hits: [] };
75675
75779
  }
@@ -13954,6 +13954,7 @@ var profileFields = {
13954
13954
  }).optional()
13955
13955
  }).optional(),
13956
13956
  schedule: exports_external.array(ScheduleEntrySchema).optional(),
13957
+ secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).optional().describe("Operator-granted STANDING vault keys this agent may read via the " + "broker — independent of any cron or MCP server. Use when an agent " + "needs a credential both interactively and in its own (agent-managed) " + "schedules, so the grant lives with the agent rather than welded to a " + "specific cron's `secrets[]`. OPERATOR-SET ONLY: agents cannot edit " + "switchroom.yaml or self-grant (reference/vision.md outcome 2 — 'you " + "hold the leash; only your tap grants it'). Exact key names. Cascades " + "UNION across defaults -> profile -> agent (see docs/configuration.md)."),
13957
13958
  reactions: ReactionsSchema,
13958
13959
  model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only").optional(),
13959
13960
  thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
@@ -14022,6 +14023,7 @@ var AgentSchema = exports_external.object({
14022
14023
  tools: AgentToolsSchema,
14023
14024
  memory: AgentMemorySchema,
14024
14025
  schedule: exports_external.array(ScheduleEntrySchema).default([]),
14026
+ secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).optional(),
14025
14027
  reactions: ReactionsSchema,
14026
14028
  model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only (no spaces or shell specials)").optional().describe("Claude model override (e.g., 'claude-sonnet-4-6')"),
14027
14029
  thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "Per-agent override wins over defaults.thinking_effort. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
@@ -14501,6 +14503,12 @@ function mergeAgentConfig(defaultsIn, agentIn) {
14501
14503
  deny: dedupe([...dDeny, ...aDeny])
14502
14504
  };
14503
14505
  }
14506
+ if (defaults.secrets || merged.secrets) {
14507
+ merged.secrets = dedupe([
14508
+ ...defaults.secrets ?? [],
14509
+ ...merged.secrets ?? []
14510
+ ]);
14511
+ }
14504
14512
  if (defaults.soul || merged.soul) {
14505
14513
  const base = defaults.soul ?? {};
14506
14514
  const override = merged.soul ?? {};
@@ -4106,6 +4106,12 @@ function mergeAgentConfig(defaultsIn, agentIn) {
4106
4106
  deny: dedupe([...dDeny, ...aDeny])
4107
4107
  };
4108
4108
  }
4109
+ if (defaults.secrets || merged.secrets) {
4110
+ merged.secrets = dedupe([
4111
+ ...defaults.secrets ?? [],
4112
+ ...merged.secrets ?? []
4113
+ ]);
4114
+ }
4109
4115
  if (defaults.soul || merged.soul) {
4110
4116
  const base = defaults.soul ?? {};
4111
4117
  const override = merged.soul ?? {};
@@ -11534,6 +11540,7 @@ var init_schema = __esm(() => {
11534
11540
  }).optional()
11535
11541
  }).optional(),
11536
11542
  schedule: exports_external.array(ScheduleEntrySchema).optional(),
11543
+ secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).optional().describe("Operator-granted STANDING vault keys this agent may read via the " + "broker — independent of any cron or MCP server. Use when an agent " + "needs a credential both interactively and in its own (agent-managed) " + "schedules, so the grant lives with the agent rather than welded to a " + "specific cron's `secrets[]`. OPERATOR-SET ONLY: agents cannot edit " + "switchroom.yaml or self-grant (reference/vision.md outcome 2 — 'you " + "hold the leash; only your tap grants it'). Exact key names. Cascades " + "UNION across defaults -> profile -> agent (see docs/configuration.md)."),
11537
11544
  reactions: ReactionsSchema,
11538
11545
  model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only").optional(),
11539
11546
  thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
@@ -11602,6 +11609,7 @@ var init_schema = __esm(() => {
11602
11609
  tools: AgentToolsSchema,
11603
11610
  memory: AgentMemorySchema,
11604
11611
  schedule: exports_external.array(ScheduleEntrySchema).default([]),
11612
+ secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).optional(),
11605
11613
  reactions: ReactionsSchema,
11606
11614
  model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only (no spaces or shell specials)").optional().describe("Claude model override (e.g., 'claude-sonnet-4-6')"),
11607
11615
  thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "Per-agent override wins over defaults.thinking_effort. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
@@ -12272,7 +12280,8 @@ var MintGrantRequestSchema = exports_external.object({
12272
12280
  description: exports_external.string().optional(),
12273
12281
  write_keys: exports_external.array(exports_external.string().min(1)).optional(),
12274
12282
  passphrase: exports_external.string().optional(),
12275
- attest_via_posture: exports_external.boolean().optional()
12283
+ attest_via_posture: exports_external.boolean().optional(),
12284
+ decision_id: exports_external.string().optional()
12276
12285
  });
12277
12286
  var ListGrantsRequestSchema = exports_external.object({
12278
12287
  v: exports_external.literal(1),
@@ -12587,9 +12596,20 @@ function migrateApprovalSchema(db) {
12587
12596
  approver_set_canonical TEXT NOT NULL,
12588
12597
  last_used_at INTEGER,
12589
12598
  revoked_at INTEGER,
12590
- revoke_reason TEXT
12599
+ revoke_reason TEXT,
12600
+ -- Provenance of the operator-authorization (RFC vault-approval-hard-
12601
+ -- boundary). 'agent': recorded on a per-agent socket — claude shares
12602
+ -- that socket, so it is FORGEABLE and must NOT be trusted as proof an
12603
+ -- operator tapped. 'operator': recorded via the host-side verifier on a
12604
+ -- claude-unreachable channel — the only value the broker's mint gate
12605
+ -- trusts. Default 'agent' (fail-closed for the new gate).
12606
+ origin TEXT NOT NULL DEFAULT 'agent'
12591
12607
  )
12592
12608
  `);
12609
+ const decisionCols = db.query("PRAGMA table_info(approval_decisions)").all();
12610
+ if (!decisionCols.some((c) => c.name === "origin")) {
12611
+ db.run(`ALTER TABLE approval_decisions ADD COLUMN origin TEXT NOT NULL DEFAULT 'agent'`);
12612
+ }
12593
12613
  db.run(`
12594
12614
  CREATE INDEX IF NOT EXISTS approval_decisions_lookup
12595
12615
  ON approval_decisions(agent_unit, scope, action)
@@ -12665,7 +12685,8 @@ function rowToDecision(row) {
12665
12685
  approver_set_canonical: row.approver_set_canonical,
12666
12686
  last_used_at: row.last_used_at ?? null,
12667
12687
  revoked_at: row.revoked_at ?? null,
12668
- revoke_reason: row.revoke_reason ?? null
12688
+ revoke_reason: row.revoke_reason ?? null,
12689
+ origin: row.origin === "operator" ? "operator" : "agent"
12669
12690
  };
12670
12691
  }
12671
12692
  var MAX_PENDING_PER_AGENT = 2;
@@ -12831,8 +12852,8 @@ function recordDecision(db, input, now = Date.now()) {
12831
12852
  db.run(`INSERT INTO approval_decisions
12832
12853
  (id, agent_unit, scope, action, decision,
12833
12854
  ttl_expires_at, granted_at, granted_by_user_id,
12834
- approver_set_canonical, last_used_at, revoked_at, revoke_reason)
12835
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, NULL)`, [
12855
+ approver_set_canonical, last_used_at, revoked_at, revoke_reason, origin)
12856
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, NULL, ?)`, [
12836
12857
  id,
12837
12858
  input.nonce.agent_unit,
12838
12859
  input.nonce.scope,
@@ -12841,7 +12862,8 @@ function recordDecision(db, input, now = Date.now()) {
12841
12862
  ttl_expires_at,
12842
12863
  now,
12843
12864
  input.granted_by_user_id,
12844
- canonical
12865
+ canonical,
12866
+ input.origin ?? "agent"
12845
12867
  ]);
12846
12868
  db.run(`UPDATE approval_nonces SET decision_id = ? WHERE request_id = ?`, [id, input.nonce.request_id]);
12847
12869
  const granted = input.decision === "allow_once" || input.decision === "allow_always" || input.decision === "allow_ttl";
@@ -12869,7 +12891,8 @@ function consumeAndRecord(db, input, now = Date.now()) {
12869
12891
  decision: input.decision,
12870
12892
  approver_set: input.approver_set,
12871
12893
  granted_by_user_id: input.granted_by_user_id,
12872
- ttl_ms: input.ttl_ms
12894
+ ttl_ms: input.ttl_ms,
12895
+ origin: input.origin
12873
12896
  }, now);
12874
12897
  return { consumed: true, decision_id, nonce };
12875
12898
  });
@@ -140,6 +140,12 @@ function mergeAgentConfig(defaultsIn, agentIn) {
140
140
  deny: dedupe([...dDeny, ...aDeny])
141
141
  };
142
142
  }
143
+ if (defaults.secrets || merged.secrets) {
144
+ merged.secrets = dedupe([
145
+ ...defaults.secrets ?? [],
146
+ ...merged.secrets ?? []
147
+ ]);
148
+ }
143
149
  if (defaults.soul || merged.soul) {
144
150
  const base = defaults.soul ?? {};
145
151
  const override = merged.soul ?? {};
@@ -11534,6 +11540,7 @@ var init_schema = __esm(() => {
11534
11540
  }).optional()
11535
11541
  }).optional(),
11536
11542
  schedule: exports_external.array(ScheduleEntrySchema).optional(),
11543
+ secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).optional().describe("Operator-granted STANDING vault keys this agent may read via the " + "broker — independent of any cron or MCP server. Use when an agent " + "needs a credential both interactively and in its own (agent-managed) " + "schedules, so the grant lives with the agent rather than welded to a " + "specific cron's `secrets[]`. OPERATOR-SET ONLY: agents cannot edit " + "switchroom.yaml or self-grant (reference/vision.md outcome 2 — 'you " + "hold the leash; only your tap grants it'). Exact key names. Cascades " + "UNION across defaults -> profile -> agent (see docs/configuration.md)."),
11537
11544
  reactions: ReactionsSchema,
11538
11545
  model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only").optional(),
11539
11546
  thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
@@ -11602,6 +11609,7 @@ var init_schema = __esm(() => {
11602
11609
  tools: AgentToolsSchema,
11603
11610
  memory: AgentMemorySchema,
11604
11611
  schedule: exports_external.array(ScheduleEntrySchema).default([]),
11612
+ secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).optional(),
11605
11613
  reactions: ReactionsSchema,
11606
11614
  model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only (no spaces or shell specials)").optional().describe("Claude model override (e.g., 'claude-sonnet-4-6')"),
11607
11615
  thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "Per-agent override wins over defaults.thinking_effort. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
@@ -13288,11 +13296,21 @@ function checkAclByAgent(config, agentName, key) {
13288
13296
  return { allow: true };
13289
13297
  }
13290
13298
  }
13299
+ const cfgSecrets = config;
13300
+ const profileSecrets = profileName != null && profileName.length > 0 ? cfgSecrets.profiles?.[profileName]?.secrets : undefined;
13301
+ const standingSecrets = [
13302
+ ...Array.isArray(cfgSecrets.defaults?.secrets) ? cfgSecrets.defaults.secrets : [],
13303
+ ...Array.isArray(profileSecrets) ? profileSecrets : [],
13304
+ ...Array.isArray(agentConfig.secrets) ? agentConfig.secrets : []
13305
+ ];
13306
+ if (standingSecrets.includes(key)) {
13307
+ return { allow: true };
13308
+ }
13291
13309
  const schedule = agentConfig.schedule ?? [];
13292
13310
  if (schedule.length === 0) {
13293
13311
  return {
13294
13312
  allow: false,
13295
- reason: `agent '${agentName}' has no schedule entries declaring 'secrets' and no mcp_servers.*.secrets[] declaring '${key}'; nothing is broker-accessible`
13313
+ reason: `agent '${agentName}' has no schedule entries declaring 'secrets', no mcp_servers.*.secrets[], and no agents.${agentName}.secrets[] standing grant declaring '${key}'; nothing is broker-accessible`
13296
13314
  };
13297
13315
  }
13298
13316
  for (const entry of schedule) {
@@ -13377,7 +13395,8 @@ var MintGrantRequestSchema = exports_external.object({
13377
13395
  description: exports_external.string().optional(),
13378
13396
  write_keys: exports_external.array(exports_external.string().min(1)).optional(),
13379
13397
  passphrase: exports_external.string().optional(),
13380
- attest_via_posture: exports_external.boolean().optional()
13398
+ attest_via_posture: exports_external.boolean().optional(),
13399
+ decision_id: exports_external.string().optional()
13381
13400
  });
13382
13401
  var ListGrantsRequestSchema = exports_external.object({
13383
13402
  v: exports_external.literal(1),
@@ -15714,9 +15733,20 @@ function migrateApprovalSchema(db) {
15714
15733
  approver_set_canonical TEXT NOT NULL,
15715
15734
  last_used_at INTEGER,
15716
15735
  revoked_at INTEGER,
15717
- revoke_reason TEXT
15736
+ revoke_reason TEXT,
15737
+ -- Provenance of the operator-authorization (RFC vault-approval-hard-
15738
+ -- boundary). 'agent': recorded on a per-agent socket — claude shares
15739
+ -- that socket, so it is FORGEABLE and must NOT be trusted as proof an
15740
+ -- operator tapped. 'operator': recorded via the host-side verifier on a
15741
+ -- claude-unreachable channel — the only value the broker's mint gate
15742
+ -- trusts. Default 'agent' (fail-closed for the new gate).
15743
+ origin TEXT NOT NULL DEFAULT 'agent'
15718
15744
  )
15719
15745
  `);
15746
+ const decisionCols = db.query("PRAGMA table_info(approval_decisions)").all();
15747
+ if (!decisionCols.some((c) => c.name === "origin")) {
15748
+ db.run(`ALTER TABLE approval_decisions ADD COLUMN origin TEXT NOT NULL DEFAULT 'agent'`);
15749
+ }
15720
15750
  db.run(`
15721
15751
  CREATE INDEX IF NOT EXISTS approval_decisions_lookup
15722
15752
  ON approval_decisions(agent_unit, scope, action)
@@ -15818,7 +15848,8 @@ function rowToDecision(row) {
15818
15848
  approver_set_canonical: row.approver_set_canonical,
15819
15849
  last_used_at: row.last_used_at ?? null,
15820
15850
  revoked_at: row.revoked_at ?? null,
15821
- revoke_reason: row.revoke_reason ?? null
15851
+ revoke_reason: row.revoke_reason ?? null,
15852
+ origin: row.origin === "operator" ? "operator" : "agent"
15822
15853
  };
15823
15854
  }
15824
15855
  var MAX_PENDING_PER_AGENT = 2;
@@ -15984,8 +16015,8 @@ function recordDecision(db, input, now = Date.now()) {
15984
16015
  db.run(`INSERT INTO approval_decisions
15985
16016
  (id, agent_unit, scope, action, decision,
15986
16017
  ttl_expires_at, granted_at, granted_by_user_id,
15987
- approver_set_canonical, last_used_at, revoked_at, revoke_reason)
15988
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, NULL)`, [
16018
+ approver_set_canonical, last_used_at, revoked_at, revoke_reason, origin)
16019
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, NULL, ?)`, [
15989
16020
  id,
15990
16021
  input.nonce.agent_unit,
15991
16022
  input.nonce.scope,
@@ -15994,7 +16025,8 @@ function recordDecision(db, input, now = Date.now()) {
15994
16025
  ttl_expires_at,
15995
16026
  now,
15996
16027
  input.granted_by_user_id,
15997
- canonical
16028
+ canonical,
16029
+ input.origin ?? "agent"
15998
16030
  ]);
15999
16031
  db.run(`UPDATE approval_nonces SET decision_id = ? WHERE request_id = ?`, [id, input.nonce.request_id]);
16000
16032
  const granted = input.decision === "allow_once" || input.decision === "allow_always" || input.decision === "allow_ttl";
@@ -16048,6 +16080,13 @@ function listDecisions(db, filter, now = Date.now()) {
16048
16080
  const rows = db.query(sql).all(...params);
16049
16081
  return rows.map(rowToDecision);
16050
16082
  }
16083
+ function getDecision(db, id) {
16084
+ const row = db.query(`SELECT * FROM approval_decisions WHERE id = ?`).get(id);
16085
+ return row ? rowToDecision(row) : null;
16086
+ }
16087
+ function isOperatorVerifiedDecision(dec, agent_unit, now = Date.now()) {
16088
+ return dec !== null && dec.origin === "operator" && dec.agent_unit === agent_unit && dec.revoked_at === null && (dec.decision === "allow_once" || dec.decision === "allow_always" || dec.decision === "allow_ttl") && (dec.ttl_expires_at === null || dec.ttl_expires_at > now);
16089
+ }
16051
16090
  function getNonce(db, request_id) {
16052
16091
  const row = db.query(`SELECT * FROM approval_nonces WHERE request_id = ?`).get(request_id);
16053
16092
  return row ? nonceFromRow(row) : null;
@@ -17063,6 +17102,22 @@ class VaultBroker {
17063
17102
  socket.write(encodeResponse(errorResponse("LOCKED", "Broker is locked")));
17064
17103
  return;
17065
17104
  }
17105
+ if (req.op === "mint_grant" && process.env.SWITCHROOM_REQUIRE_OPERATOR_APPROVAL_MINT === "1") {
17106
+ const decisionId = req.decision_id;
17107
+ const dec = decisionId != null && decisionId !== "" ? getDecision(this.grantsDb, decisionId) : null;
17108
+ if (!isOperatorVerifiedDecision(dec, agentName ?? "")) {
17109
+ writeAudit({
17110
+ ts: new Date().toISOString(),
17111
+ op: req.op,
17112
+ caller: auditCaller,
17113
+ pid: auditPid,
17114
+ cgroup: auditCgroup,
17115
+ result: "denied:posture-mint-needs-operator-verified-decision"
17116
+ });
17117
+ socket.write(encodeResponse(errorResponse("DENIED", "posture-attested mint requires an operator-verified approval (host-side tap); none referenced. See RFC vault-approval-hard-boundary.")));
17118
+ return;
17119
+ }
17120
+ }
17066
17121
  mintPostureAttested = true;
17067
17122
  writeAudit({
17068
17123
  ts: new Date().toISOString(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.14.33",
3
+ "version": "0.14.35",
4
4
  "description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,7 +25,7 @@
25
25
  "pretest": "npm run build",
26
26
  "test": "vitest run && bun test telegram-plugin/tests/history.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/silent-reply-guard.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.test.ts telegram-plugin/registry/api-registry.test.ts telegram-plugin/registry/turns-schema.test.ts telegram-plugin/tests/idle-footer-wiring.test.ts telegram-plugin/tests/subagent-tracker-hooks.test.ts telegram-plugin/tests/resolve-calling-subagent.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts",
27
27
  "test:vitest": "vitest run",
28
- "test:bun": "bun test src/watchdog/state.test.ts src/watchdog/policy.test.ts src/vault/grants.test.ts src/vault/write-grants.test.ts src/vault/broker/server-grants.test.ts src/vault/broker/server-write-grants.test.ts src/vault/broker/server-mint-grant-passphrase-attest.test.ts src/vault/broker/server-passphrase-attest.test.ts src/vault/broker/server-mint-grant-posture-attest.test.ts src/vault/broker/client-token.test.ts src/vault/broker/server-unlock.test.ts src/vault/broker/auto-unlock.test.ts src/vault/broker/drift-detection.test.ts tests/vault-broker-passphrase.test.ts src/cli/vault-get-broker.test.ts src/vault/resolver-via-broker.test.ts src/vault/broker/scope.test.ts src/vault/broker/server.test.ts src/drive/disconnect.test.ts src/drive/grants.test.ts src/drive/oauth.test.ts src/drive/onboarding.test.ts src/drive/reconciler.test.ts src/drive/vault-slots.test.ts src/drive/wrapper.test.ts src/vault/approvals/kernel.test.ts src/vault/approvals/schema-idempotent.test.ts src/vault/broker/server-approvals.test.ts telegram-plugin/tests/boot-probes.test.ts telegram-plugin/tests/boot-version-string.test.ts telegram-plugin/tests/history.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/silent-reply-guard.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.test.ts telegram-plugin/tests/resolve-calling-subagent.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/uat/load-env.test.ts telegram-plugin/uat/feed-matcher.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts",
28
+ "test:bun": "bun test src/watchdog/state.test.ts src/watchdog/policy.test.ts src/vault/grants.test.ts src/vault/write-grants.test.ts src/vault/broker/server-grants.test.ts src/vault/broker/server-write-grants.test.ts src/vault/broker/server-mint-grant-passphrase-attest.test.ts src/vault/broker/server-passphrase-attest.test.ts src/vault/broker/server-mint-grant-posture-attest.test.ts src/vault/broker/client-token.test.ts src/vault/broker/server-unlock.test.ts src/vault/broker/auto-unlock.test.ts src/vault/broker/drift-detection.test.ts tests/vault-broker-passphrase.test.ts src/cli/vault-get-broker.test.ts src/vault/resolver-via-broker.test.ts src/vault/broker/scope.test.ts src/vault/broker/server.test.ts src/drive/disconnect.test.ts src/drive/grants.test.ts src/drive/oauth.test.ts src/drive/onboarding.test.ts src/drive/reconciler.test.ts src/drive/vault-slots.test.ts src/drive/wrapper.test.ts src/vault/approvals/kernel.test.ts src/vault/approvals/approval-origin.test.ts src/vault/approvals/schema-idempotent.test.ts src/vault/broker/server-approvals.test.ts telegram-plugin/tests/boot-probes.test.ts telegram-plugin/tests/boot-version-string.test.ts telegram-plugin/tests/history.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/silent-reply-guard.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.test.ts telegram-plugin/tests/resolve-calling-subagent.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/uat/load-env.test.ts telegram-plugin/uat/feed-matcher.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts",
29
29
  "test:watch": "vitest",
30
30
  "lint": "tsc --noEmit && node scripts/check-plugin-references.mjs && bash scripts/check-bot-api-wrapping.sh && node scripts/check-bun-test-imports.mjs && node scripts/check-no-pii-secrets.mjs && node scripts/check-vault-test-hermeticity.mjs && node scripts/check-no-broadcast-delivery.mjs",
31
31
  "lint:tsc": "tsc --noEmit",
@@ -23925,6 +23925,7 @@ var init_schema = __esm(() => {
23925
23925
  }).optional()
23926
23926
  }).optional(),
23927
23927
  schedule: exports_external.array(ScheduleEntrySchema).optional(),
23928
+ secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).optional().describe("Operator-granted STANDING vault keys this agent may read via the " + "broker \u2014 independent of any cron or MCP server. Use when an agent " + "needs a credential both interactively and in its own (agent-managed) " + "schedules, so the grant lives with the agent rather than welded to a " + "specific cron's `secrets[]`. OPERATOR-SET ONLY: agents cannot edit " + "switchroom.yaml or self-grant (reference/vision.md outcome 2 \u2014 'you " + "hold the leash; only your tap grants it'). Exact key names. Cascades " + "UNION across defaults -> profile -> agent (see docs/configuration.md)."),
23928
23929
  reactions: ReactionsSchema,
23929
23930
  model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only").optional(),
23930
23931
  thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
@@ -23993,6 +23994,7 @@ var init_schema = __esm(() => {
23993
23994
  tools: AgentToolsSchema,
23994
23995
  memory: AgentMemorySchema,
23995
23996
  schedule: exports_external.array(ScheduleEntrySchema).default([]),
23997
+ secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).optional(),
23996
23998
  reactions: ReactionsSchema,
23997
23999
  model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only (no spaces or shell specials)").optional().describe("Claude model override (e.g., 'claude-sonnet-4-6')"),
23998
24000
  thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "Per-agent override wins over defaults.thinking_effort. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
@@ -24472,6 +24474,12 @@ function mergeAgentConfig(defaultsIn, agentIn) {
24472
24474
  deny: dedupe([...dDeny, ...aDeny])
24473
24475
  };
24474
24476
  }
24477
+ if (defaults.secrets || merged.secrets) {
24478
+ merged.secrets = dedupe([
24479
+ ...defaults.secrets ?? [],
24480
+ ...merged.secrets ?? []
24481
+ ]);
24482
+ }
24475
24483
  if (defaults.soul || merged.soul) {
24476
24484
  const base = defaults.soul ?? {};
24477
24485
  const override = merged.soul ?? {};
@@ -27725,7 +27733,8 @@ var init_protocol2 = __esm(() => {
27725
27733
  description: exports_external.string().optional(),
27726
27734
  write_keys: exports_external.array(exports_external.string().min(1)).optional(),
27727
27735
  passphrase: exports_external.string().optional(),
27728
- attest_via_posture: exports_external.boolean().optional()
27736
+ attest_via_posture: exports_external.boolean().optional(),
27737
+ decision_id: exports_external.string().optional()
27729
27738
  });
27730
27739
  ListGrantsRequestSchema = exports_external.object({
27731
27740
  v: exports_external.literal(1),
@@ -44481,6 +44490,12 @@ function mergeAgentConfig2(defaultsIn, agentIn) {
44481
44490
  deny: dedupe2([...dDeny, ...aDeny])
44482
44491
  };
44483
44492
  }
44493
+ if (defaults.secrets || merged.secrets) {
44494
+ merged.secrets = dedupe2([
44495
+ ...defaults.secrets ?? [],
44496
+ ...merged.secrets ?? []
44497
+ ]);
44498
+ }
44484
44499
  if (defaults.soul || merged.soul) {
44485
44500
  const base = defaults.soul ?? {};
44486
44501
  const override = merged.soul ?? {};
@@ -51766,10 +51781,10 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
51766
51781
  }
51767
51782
 
51768
51783
  // ../src/build-info.ts
51769
- var VERSION = "0.14.33";
51770
- var COMMIT_SHA = "0b73633c";
51771
- var COMMIT_DATE = "2026-06-01T12:35:17Z";
51772
- var LATEST_PR = 2066;
51784
+ var VERSION = "0.14.35";
51785
+ var COMMIT_SHA = "7ac06aea";
51786
+ var COMMIT_DATE = "2026-06-01T21:48:46Z";
51787
+ var LATEST_PR = 2072;
51773
51788
  var COMMITS_AHEAD_OF_TAG = 0;
51774
51789
 
51775
51790
  // gateway/boot-version.ts