switchroom 0.12.12 → 0.12.14

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.
@@ -15877,7 +15877,7 @@ class HostdServer {
15877
15877
  const PROBES = [
15878
15878
  {
15879
15879
  name: "auth",
15880
- cmd: 'test -s "$CLAUDE_CONFIG_DIR/.credentials.json" || test -s "$HOME/.claude/.credentials.json"'
15880
+ cmd: "test -s /state/agent/.claude/.credentials.json"
15881
15881
  },
15882
15882
  {
15883
15883
  name: "scheduler",
@@ -11719,6 +11719,24 @@ var LockRequestSchema = exports_external.object({
11719
11719
  v: exports_external.literal(1),
11720
11720
  op: exports_external.literal("lock")
11721
11721
  });
11722
+ var PreflightAccessRequestSchema = exports_external.object({
11723
+ v: exports_external.literal(1),
11724
+ op: exports_external.literal("preflight_access"),
11725
+ agent: exports_external.string().min(1),
11726
+ keys: exports_external.array(exports_external.string().min(1)).min(1).max(128)
11727
+ });
11728
+ var OkPreflightAccessResponseSchema = exports_external.object({
11729
+ ok: exports_external.literal(true),
11730
+ op: exports_external.literal("preflight_access"),
11731
+ results: exports_external.array(exports_external.object({
11732
+ key: exports_external.string(),
11733
+ exists: exports_external.boolean(),
11734
+ acl_ok: exports_external.boolean(),
11735
+ acl_reason: exports_external.string().optional(),
11736
+ scope_ok: exports_external.boolean(),
11737
+ scope_reason: exports_external.string().optional()
11738
+ }))
11739
+ });
11722
11740
  var ApprovalRequestRequestSchema = exports_external.object({
11723
11741
  v: exports_external.literal(1),
11724
11742
  op: exports_external.literal("approval_request"),
@@ -11776,6 +11794,7 @@ var RequestSchema = exports_external.discriminatedUnion("op", [
11776
11794
  ListRequestSchema,
11777
11795
  StatusRequestSchema,
11778
11796
  LockRequestSchema,
11797
+ PreflightAccessRequestSchema,
11779
11798
  MintGrantRequestSchema,
11780
11799
  ListGrantsRequestSchema,
11781
11800
  RevokeGrantRequestSchema,
@@ -11916,6 +11935,7 @@ var ResponseSchema = exports_external.union([
11916
11935
  OkKeysResponseSchema,
11917
11936
  OkStatusResponseSchema,
11918
11937
  OkLockResponseSchema,
11938
+ OkPreflightAccessResponseSchema,
11919
11939
  OkPutResponseSchema,
11920
11940
  OkMintGrantResponseSchema,
11921
11941
  OkListGrantsResponseSchema,
@@ -12899,6 +12899,48 @@ function readAutoUnlockFile(filePath) {
12899
12899
  }
12900
12900
  var DEFAULT_AUTO_UNLOCK_PATH = "~/.switchroom/vault-auto-unlock";
12901
12901
 
12902
+ // src/config/google-workspace-acl.ts
12903
+ function shouldEmitGdriveMcp(agentName, agentGoogleAccount, googleAccounts) {
12904
+ if (!agentGoogleAccount)
12905
+ return false;
12906
+ const account = agentGoogleAccount.trim().toLowerCase();
12907
+ if (account.length === 0)
12908
+ return false;
12909
+ const acctEntry = googleAccounts?.[account];
12910
+ if (!acctEntry)
12911
+ return false;
12912
+ const enabledFor = acctEntry.enabled_for ?? [];
12913
+ return enabledFor.includes(agentName);
12914
+ }
12915
+ function vaultRefKey(value) {
12916
+ if (typeof value !== "string" || !value.startsWith("vault:"))
12917
+ return null;
12918
+ const key = value.slice("vault:".length).split("#")[0];
12919
+ return key.length > 0 ? key : null;
12920
+ }
12921
+ function isGoogleClientCredentialKeyForAgent(config, agentName, key) {
12922
+ if (!agentName || !key)
12923
+ return false;
12924
+ const agentConfig = config.agents?.[agentName];
12925
+ if (!agentConfig)
12926
+ return false;
12927
+ if (agentConfig.mcp_servers?.["gdrive"] === false) {
12928
+ return false;
12929
+ }
12930
+ const account = agentConfig.google_workspace?.account;
12931
+ if (!shouldEmitGdriveMcp(agentName, account, config.google_accounts)) {
12932
+ return false;
12933
+ }
12934
+ const gw = config.google_workspace;
12935
+ if (!gw)
12936
+ return false;
12937
+ for (const ref of [gw.google_client_id, gw.google_client_secret]) {
12938
+ if (vaultRefKey(ref) === key)
12939
+ return true;
12940
+ }
12941
+ return false;
12942
+ }
12943
+
12902
12944
  // src/vault/broker/acl.ts
12903
12945
  function parseCronUnit(unitName) {
12904
12946
  const m = unitName.match(/^switchroom-([a-zA-Z0-9_-]+)-cron-(\d+)\.service$/);
@@ -12986,6 +13028,9 @@ function checkAclByAgent(config, agentName, key) {
12986
13028
  if (googleSlot !== null) {
12987
13029
  return checkGoogleAccountAcl(config, agentName, googleSlot.account, key);
12988
13030
  }
13031
+ if (isGoogleClientCredentialKeyForAgent(config, agentName, key)) {
13032
+ return { allow: true };
13033
+ }
12989
13034
  const agentBot = agentConfig.bot_token;
12990
13035
  const botRef = agentBot && agentBot.length > 0 ? agentBot : config.telegram?.bot_token;
12991
13036
  if (typeof botRef === "string" && botRef.startsWith("vault:")) {
@@ -13102,6 +13147,24 @@ var LockRequestSchema = exports_external.object({
13102
13147
  v: exports_external.literal(1),
13103
13148
  op: exports_external.literal("lock")
13104
13149
  });
13150
+ var PreflightAccessRequestSchema = exports_external.object({
13151
+ v: exports_external.literal(1),
13152
+ op: exports_external.literal("preflight_access"),
13153
+ agent: exports_external.string().min(1),
13154
+ keys: exports_external.array(exports_external.string().min(1)).min(1).max(128)
13155
+ });
13156
+ var OkPreflightAccessResponseSchema = exports_external.object({
13157
+ ok: exports_external.literal(true),
13158
+ op: exports_external.literal("preflight_access"),
13159
+ results: exports_external.array(exports_external.object({
13160
+ key: exports_external.string(),
13161
+ exists: exports_external.boolean(),
13162
+ acl_ok: exports_external.boolean(),
13163
+ acl_reason: exports_external.string().optional(),
13164
+ scope_ok: exports_external.boolean(),
13165
+ scope_reason: exports_external.string().optional()
13166
+ }))
13167
+ });
13105
13168
  var ApprovalRequestRequestSchema = exports_external.object({
13106
13169
  v: exports_external.literal(1),
13107
13170
  op: exports_external.literal("approval_request"),
@@ -13159,6 +13222,7 @@ var RequestSchema = exports_external.discriminatedUnion("op", [
13159
13222
  ListRequestSchema,
13160
13223
  StatusRequestSchema,
13161
13224
  LockRequestSchema,
13225
+ PreflightAccessRequestSchema,
13162
13226
  MintGrantRequestSchema,
13163
13227
  ListGrantsRequestSchema,
13164
13228
  RevokeGrantRequestSchema,
@@ -13299,6 +13363,7 @@ var ResponseSchema = exports_external.union([
13299
13363
  OkKeysResponseSchema,
13300
13364
  OkStatusResponseSchema,
13301
13365
  OkLockResponseSchema,
13366
+ OkPreflightAccessResponseSchema,
13302
13367
  OkPutResponseSchema,
13303
13368
  OkMintGrantResponseSchema,
13304
13369
  OkListGrantsResponseSchema,
@@ -15741,7 +15806,9 @@ class VaultBroker {
15741
15806
  this._writePidFile();
15742
15807
  this._sdNotify(`READY=1
15743
15808
  `);
15744
- this._tryAutoUnlock();
15809
+ if (this.testOpts._testSecrets === undefined) {
15810
+ this._tryAutoUnlock();
15811
+ }
15745
15812
  if (process.platform !== "linux") {
15746
15813
  process.stderr.write(`[vault-broker] WARNING: running on ${process.platform} with ` + `SWITCHROOM_BROKER_ALLOW_NON_LINUX=1 — peercred ACL is disabled. ` + `Access control is socket file mode 0600 ONLY. Do not use this configuration for production secrets.
15747
15814
  `);
@@ -16052,6 +16119,49 @@ class VaultBroker {
16052
16119
  socket.write(encodeResponse({ ok: true, locked: true }));
16053
16120
  return;
16054
16121
  }
16122
+ if (req.op === "preflight_access") {
16123
+ if (!isOperator) {
16124
+ writeAudit({
16125
+ ts: new Date().toISOString(),
16126
+ op: "preflight_access",
16127
+ caller: auditCaller,
16128
+ pid: auditPid,
16129
+ cgroup: auditCgroup,
16130
+ result: "denied:operator-only"
16131
+ });
16132
+ socket.write(encodeResponse(errorResponse("DENIED", "preflight_access is operator-only")));
16133
+ return;
16134
+ }
16135
+ if (this.secrets === null) {
16136
+ socket.write(encodeResponse(errorResponse("LOCKED", "Vault is locked")));
16137
+ return;
16138
+ }
16139
+ const pfSecrets = this.secrets;
16140
+ const pfConfig = this.config;
16141
+ const results = req.keys.map((key) => {
16142
+ const exists = Object.prototype.hasOwnProperty.call(pfSecrets, key);
16143
+ const acl = pfConfig !== null ? checkAclByAgent(pfConfig, req.agent, key) : { allow: false, reason: "broker has no config loaded" };
16144
+ const scope = checkEntryScope(pfSecrets[key]?.scope, req.agent);
16145
+ return {
16146
+ key,
16147
+ exists,
16148
+ acl_ok: acl.allow,
16149
+ ...acl.allow ? {} : { acl_reason: acl.reason },
16150
+ scope_ok: scope.allow,
16151
+ ...scope.allow ? {} : { scope_reason: scope.reason }
16152
+ };
16153
+ });
16154
+ writeAudit({
16155
+ ts: new Date().toISOString(),
16156
+ op: "preflight_access",
16157
+ caller: auditCaller,
16158
+ pid: auditPid,
16159
+ cgroup: auditCgroup,
16160
+ result: `allowed:agent=${req.agent},keys=${req.keys.length}`
16161
+ });
16162
+ socket.write(encodeResponse({ ok: true, op: "preflight_access", results }));
16163
+ return;
16164
+ }
16055
16165
  if (req.op === "list") {
16056
16166
  if (this.secrets === null) {
16057
16167
  socket.write(encodeResponse(errorResponse("LOCKED", "Vault is locked")));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.12.12",
3
+ "version": "0.12.14",
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": {
@@ -24664,7 +24664,7 @@ function decodeResponse2(line) {
24664
24664
  const obj = JSON.parse(line);
24665
24665
  return ResponseSchema2.parse(obj);
24666
24666
  }
24667
- var MAX_FRAME_BYTES2, GetRequestSchema, PutRequestSchema, ListRequestSchema, MintGrantRequestSchema, ListGrantsRequestSchema, RevokeGrantRequestSchema, StatusRequestSchema, LockRequestSchema, ApprovalRequestRequestSchema, ApprovalLookupRequestSchema, ApprovalConsumeRequestSchema, ApprovalRevokeRequestSchema, ApprovalListRequestSchema, ApprovalDecisionModeSchema, ApprovalRecordRequestSchema, RequestSchema2, VaultEntrySchema, ErrorCode, OkEntryResponseSchema, OkKeysResponseSchema, BrokerStatus, OkStatusResponseSchema, OkLockResponseSchema, OkPutResponseSchema, OkMintGrantResponseSchema, GrantMetaSchema, OkListGrantsResponseSchema, OkRevokeGrantResponseSchema, OkApprovalRequestResponseSchema, ApprovalDecisionMetaSchema, OkApprovalLookupResponseSchema, OkApprovalConsumeResponseSchema, OkApprovalRevokeResponseSchema, OkApprovalListResponseSchema, OkApprovalRecordResponseSchema, ErrorResponseSchema2, ResponseSchema2;
24667
+ var MAX_FRAME_BYTES2, GetRequestSchema, PutRequestSchema, ListRequestSchema, MintGrantRequestSchema, ListGrantsRequestSchema, RevokeGrantRequestSchema, StatusRequestSchema, LockRequestSchema, PreflightAccessRequestSchema, OkPreflightAccessResponseSchema, ApprovalRequestRequestSchema, ApprovalLookupRequestSchema, ApprovalConsumeRequestSchema, ApprovalRevokeRequestSchema, ApprovalListRequestSchema, ApprovalDecisionModeSchema, ApprovalRecordRequestSchema, RequestSchema2, VaultEntrySchema, ErrorCode, OkEntryResponseSchema, OkKeysResponseSchema, BrokerStatus, OkStatusResponseSchema, OkLockResponseSchema, OkPutResponseSchema, OkMintGrantResponseSchema, GrantMetaSchema, OkListGrantsResponseSchema, OkRevokeGrantResponseSchema, OkApprovalRequestResponseSchema, ApprovalDecisionMetaSchema, OkApprovalLookupResponseSchema, OkApprovalConsumeResponseSchema, OkApprovalRevokeResponseSchema, OkApprovalListResponseSchema, OkApprovalRecordResponseSchema, ErrorResponseSchema2, ResponseSchema2;
24668
24668
  var init_protocol2 = __esm(() => {
24669
24669
  init_zod();
24670
24670
  MAX_FRAME_BYTES2 = 64 * 1024;
@@ -24723,6 +24723,24 @@ var init_protocol2 = __esm(() => {
24723
24723
  v: exports_external.literal(1),
24724
24724
  op: exports_external.literal("lock")
24725
24725
  });
24726
+ PreflightAccessRequestSchema = exports_external.object({
24727
+ v: exports_external.literal(1),
24728
+ op: exports_external.literal("preflight_access"),
24729
+ agent: exports_external.string().min(1),
24730
+ keys: exports_external.array(exports_external.string().min(1)).min(1).max(128)
24731
+ });
24732
+ OkPreflightAccessResponseSchema = exports_external.object({
24733
+ ok: exports_external.literal(true),
24734
+ op: exports_external.literal("preflight_access"),
24735
+ results: exports_external.array(exports_external.object({
24736
+ key: exports_external.string(),
24737
+ exists: exports_external.boolean(),
24738
+ acl_ok: exports_external.boolean(),
24739
+ acl_reason: exports_external.string().optional(),
24740
+ scope_ok: exports_external.boolean(),
24741
+ scope_reason: exports_external.string().optional()
24742
+ }))
24743
+ });
24726
24744
  ApprovalRequestRequestSchema = exports_external.object({
24727
24745
  v: exports_external.literal(1),
24728
24746
  op: exports_external.literal("approval_request"),
@@ -24780,6 +24798,7 @@ var init_protocol2 = __esm(() => {
24780
24798
  ListRequestSchema,
24781
24799
  StatusRequestSchema,
24782
24800
  LockRequestSchema,
24801
+ PreflightAccessRequestSchema,
24783
24802
  MintGrantRequestSchema,
24784
24803
  ListGrantsRequestSchema,
24785
24804
  RevokeGrantRequestSchema,
@@ -24920,6 +24939,7 @@ var init_protocol2 = __esm(() => {
24920
24939
  OkKeysResponseSchema,
24921
24940
  OkStatusResponseSchema,
24922
24941
  OkLockResponseSchema,
24942
+ OkPreflightAccessResponseSchema,
24923
24943
  OkPutResponseSchema,
24924
24944
  OkMintGrantResponseSchema,
24925
24945
  OkListGrantsResponseSchema,
@@ -29610,15 +29630,6 @@ async function handleApprovalCallback(ctx, data) {
29610
29630
  await ctx.answerCallbackQuery({ text: "malformed approval callback" });
29611
29631
  return;
29612
29632
  }
29613
- const consumed = await approvalConsume2(parsed.request_id);
29614
- if (consumed === null) {
29615
- await ctx.answerCallbackQuery({ text: "approval kernel unreachable" });
29616
- return;
29617
- }
29618
- if (!consumed.consumed) {
29619
- await ctx.answerCallbackQuery({ text: "this prompt expired" });
29620
- return;
29621
- }
29622
29633
  let decision;
29623
29634
  let granted;
29624
29635
  let ttl_ms = null;
@@ -29652,6 +29663,15 @@ async function handleApprovalCallback(ctx, data) {
29652
29663
  break;
29653
29664
  }
29654
29665
  }
29666
+ const consumed = await approvalConsume2(parsed.request_id);
29667
+ if (consumed === null) {
29668
+ await ctx.answerCallbackQuery({ text: "approval kernel unreachable" });
29669
+ return;
29670
+ }
29671
+ if (!consumed.consumed) {
29672
+ await ctx.answerCallbackQuery({ text: "this prompt expired" });
29673
+ return;
29674
+ }
29655
29675
  const granted_by_user_id = ctx.from?.id ?? 0;
29656
29676
  const approver_set = [String(granted_by_user_id)];
29657
29677
  const decision_id = await approvalRecord2({
@@ -43366,6 +43386,52 @@ function createPendingInboundBuffer(opts = {}) {
43366
43386
  };
43367
43387
  }
43368
43388
 
43389
+ // gateway/pending-permission-decisions.ts
43390
+ var DEFAULT_PENDING_PERMISSION_CAP = 32;
43391
+ function createPendingPermissionBuffer(opts = {}) {
43392
+ const cap = opts.capPerAgent ?? DEFAULT_PENDING_PERMISSION_CAP;
43393
+ const log = opts.log ?? ((line) => process.stderr.write(line));
43394
+ const queues = new Map;
43395
+ return {
43396
+ push(agent, ev) {
43397
+ let q = queues.get(agent);
43398
+ if (q == null) {
43399
+ q = [];
43400
+ queues.set(agent, q);
43401
+ }
43402
+ let evicted = false;
43403
+ if (q.length >= cap) {
43404
+ const dropped = q.shift();
43405
+ evicted = true;
43406
+ log(`pending-permission-buffer: agent=${agent} cap=${cap} reached \u2014 ` + `dropped oldest verdict request=${dropped?.requestId ?? "-"} ` + `behavior=${dropped?.behavior ?? "-"}
43407
+ `);
43408
+ }
43409
+ q.push(ev);
43410
+ log(`pending-permission-buffer: agent=${agent} buffered request=${ev.requestId} ` + `behavior=${ev.behavior} depth_after=${q.length} evicted=${evicted}
43411
+ `);
43412
+ return !evicted;
43413
+ },
43414
+ drain(agent) {
43415
+ const q = queues.get(agent);
43416
+ if (q == null || q.length === 0)
43417
+ return [];
43418
+ queues.delete(agent);
43419
+ log(`pending-permission-buffer: drained agent=${agent} count=${q.length} ` + `requests=[${q.map((e) => e.requestId).join(",")}]
43420
+ `);
43421
+ return q;
43422
+ },
43423
+ depth(agent) {
43424
+ return queues.get(agent)?.length ?? 0;
43425
+ },
43426
+ totalDepth() {
43427
+ let n = 0;
43428
+ for (const q of queues.values())
43429
+ n += q.length;
43430
+ return n;
43431
+ }
43432
+ };
43433
+ }
43434
+
43369
43435
  // gateway/vault-grant-inbound-builders.ts
43370
43436
  function buildVaultGrantApprovedInbound(opts) {
43371
43437
  const ts = opts.nowMs ?? Date.now();
@@ -43409,6 +43475,63 @@ function buildVaultGrantDeniedInbound(opts) {
43409
43475
  }
43410
43476
  };
43411
43477
  }
43478
+ function buildVaultSaveCompletedInbound(opts) {
43479
+ const ts = opts.nowMs ?? Date.now();
43480
+ return {
43481
+ type: "inbound",
43482
+ chatId: opts.ctx.chat_id,
43483
+ messageId: ts,
43484
+ user: "vault-broker",
43485
+ userId: 0,
43486
+ ts,
43487
+ text: `\u2705 Operator saved your secret as \`vault:${opts.ctx.key}\`. ` + `Please resume the task that was waiting on it \u2014 reference the ` + `value via the usual \`vault:${opts.ctx.key}\` path.`,
43488
+ meta: {
43489
+ source: "vault_save_completed",
43490
+ agent: opts.ctx.agent,
43491
+ key: opts.ctx.key,
43492
+ stage_id: opts.stageId,
43493
+ operator_id: opts.operatorId
43494
+ }
43495
+ };
43496
+ }
43497
+ function buildVaultSaveFailedInbound(opts) {
43498
+ const ts = opts.nowMs ?? Date.now();
43499
+ return {
43500
+ type: "inbound",
43501
+ chatId: opts.ctx.chat_id,
43502
+ messageId: ts,
43503
+ user: "vault-broker",
43504
+ userId: 0,
43505
+ ts,
43506
+ text: `\u26a0\ufe0f The operator tapped Save but the vault write for ` + `\`vault:${opts.ctx.key}\` FAILED (${opts.reason}). The secret was ` + `NOT stored \u2014 do NOT assume \`vault:${opts.ctx.key}\` resolves. ` + `Either retry the save (the operator may need to fix the underlying ` + `issue first) or pick a fallback for the original task.`,
43507
+ meta: {
43508
+ source: "vault_save_failed",
43509
+ agent: opts.ctx.agent,
43510
+ key: opts.ctx.key,
43511
+ stage_id: opts.stageId,
43512
+ operator_id: opts.operatorId
43513
+ }
43514
+ };
43515
+ }
43516
+ function buildVaultSaveDiscardedInbound(opts) {
43517
+ const ts = opts.nowMs ?? Date.now();
43518
+ return {
43519
+ type: "inbound",
43520
+ chatId: opts.ctx.chat_id,
43521
+ messageId: ts,
43522
+ user: "vault-broker",
43523
+ userId: 0,
43524
+ ts,
43525
+ text: `\uD83D\uDEAB Operator discarded your \`vault_request_save\` for ` + `\`${opts.ctx.key}\` \u2014 the secret was NOT stored and ` + `\`vault:${opts.ctx.key}\` will not resolve. Pick a fallback for ` + `the original task (ask the user, try another approach, or skip ` + `the feature). Do NOT re-request the save without asking the user.`,
43526
+ meta: {
43527
+ source: "vault_save_discarded",
43528
+ agent: opts.ctx.agent,
43529
+ key: opts.ctx.key,
43530
+ stage_id: opts.stageId,
43531
+ operator_id: opts.operatorId
43532
+ }
43533
+ };
43534
+ }
43412
43535
 
43413
43536
  // gateway/poll-health.ts
43414
43537
  var DEFAULT_LOG = (msg) => {
@@ -46621,11 +46744,11 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
46621
46744
  }
46622
46745
 
46623
46746
  // ../src/build-info.ts
46624
- var VERSION = "0.12.12";
46625
- var COMMIT_SHA = "85dea837";
46626
- var COMMIT_DATE = "2026-05-19T01:52:34Z";
46627
- var LATEST_PR = 1530;
46628
- var COMMITS_AHEAD_OF_TAG = 6;
46747
+ var VERSION = "0.12.14";
46748
+ var COMMIT_SHA = "db6d87d6";
46749
+ var COMMIT_DATE = "2026-05-19T05:51:09Z";
46750
+ var LATEST_PR = 1542;
46751
+ var COMMITS_AHEAD_OF_TAG = 17;
46629
46752
 
46630
46753
  // gateway/boot-version.ts
46631
46754
  function formatRelativeAgo(iso) {
@@ -47995,8 +48118,12 @@ var pendingStateReaper = setInterval(() => {
47995
48118
  pendingVaultOps.delete(k);
47996
48119
  }
47997
48120
  for (const [k, v] of pendingPermissions) {
47998
- if (now - v.startedAt > PERMISSION_TTL_MS)
48121
+ if (now - v.startedAt > PERMISSION_TTL_MS) {
48122
+ dispatchPermissionVerdict({ type: "permission", requestId: k, behavior: "deny" });
48123
+ process.stderr.write(`telegram gateway: permission TTL expired \u2014 auto-deny request=${k} tool=${v.tool_name} (no operator response in ${Math.round(PERMISSION_TTL_MS / 60000)}m)
48124
+ `);
47999
48125
  pendingPermissions.delete(k);
48126
+ }
48000
48127
  }
48001
48128
  for (const [k, v] of vaultPassphraseCache) {
48002
48129
  if (now > v.expiresAt)
@@ -48316,6 +48443,16 @@ startTimer({
48316
48443
  }
48317
48444
  });
48318
48445
  var pendingInboundBuffer = createPendingInboundBuffer();
48446
+ var pendingPermissionBuffer = createPendingPermissionBuffer();
48447
+ function dispatchPermissionVerdict(ev) {
48448
+ const selfAgent = process.env.SWITCHROOM_AGENT_NAME ?? "";
48449
+ const delivered = ipcServer.sendToAgent(selfAgent, ev);
48450
+ if (!delivered) {
48451
+ pendingPermissionBuffer.push(selfAgent, ev);
48452
+ process.stderr.write(`telegram gateway: permission verdict buffered (bridge offline) request=${ev.requestId} behavior=${ev.behavior}
48453
+ `);
48454
+ }
48455
+ }
48319
48456
  var ipcServer = createIpcServer({
48320
48457
  socketPath: SOCKET_PATH,
48321
48458
  onClientRegistered(client3) {
@@ -48329,6 +48466,15 @@ var ipcServer = createIpcServer({
48329
48466
  client3.send(msg);
48330
48467
  } catch (err) {
48331
48468
  process.stderr.write(`telegram gateway: pending-inbound drain failed agent=${client3.agentName} source=${msg.meta?.source ?? "-"}: ${err.message}
48469
+ `);
48470
+ }
48471
+ }
48472
+ const pendingVerdicts = pendingPermissionBuffer.drain(client3.agentName);
48473
+ for (const ev of pendingVerdicts) {
48474
+ try {
48475
+ client3.send(ev);
48476
+ } catch (err) {
48477
+ process.stderr.write(`telegram gateway: pending-permission drain failed agent=${client3.agentName} request=${ev.requestId} behavior=${ev.behavior}: ${err.message}
48332
48478
  `);
48333
48479
  }
48334
48480
  }
@@ -50646,7 +50792,7 @@ async function handleInbound(ctx, text, downloadImage, attachment) {
50646
50792
  if (permMatch) {
50647
50793
  const behavior = permMatch[1].toLowerCase().startsWith("y") ? "allow" : "deny";
50648
50794
  const request_id = permMatch[2].toLowerCase();
50649
- ipcServer.broadcast({
50795
+ dispatchPermissionVerdict({
50650
50796
  type: "permission",
50651
50797
  requestId: request_id,
50652
50798
  behavior
@@ -51121,11 +51267,12 @@ ${preBlock(write.output)}`;
51121
51267
  } : {}
51122
51268
  }
51123
51269
  };
51124
- ipcServer.broadcast(inboundMsg);
51125
- const delivered = ipcServer.clientCount() > 0;
51270
+ const selfAgent = process.env.SWITCHROOM_AGENT_NAME ?? "";
51271
+ const delivered = ipcServer.sendToAgent(selfAgent, inboundMsg);
51126
51272
  if (!delivered) {
51273
+ pendingInboundBuffer.push(selfAgent, inboundMsg);
51127
51274
  const threadOpts = messageThreadId != null ? { message_thread_id: messageThreadId } : {};
51128
- swallowingApiCall(() => bot.api.sendMessage(chat_id, "\u23F3 Agent is restarting, please wait\u2026", { ...threadOpts }), {
51275
+ swallowingApiCall(() => bot.api.sendMessage(chat_id, "\u23F3 Agent is restarting \u2014 your message is queued and will be processed when it reconnects.", { ...threadOpts }), {
51129
51276
  chat_id,
51130
51277
  verb: "agent-restarting-notice",
51131
51278
  ...messageThreadId != null ? { threadId: messageThreadId } : {}
@@ -52298,7 +52445,7 @@ async function handlePermissionSlash(ctx, behavior) {
52298
52445
  await switchroomReply(ctx, `No pending permission for id <code>${escapeHtmlForTg(request_id)}</code>. It may have already been answered or timed out.`, { html: true });
52299
52446
  return;
52300
52447
  }
52301
- ipcServer.broadcast({ type: "permission", requestId: request_id, behavior });
52448
+ dispatchPermissionVerdict({ type: "permission", requestId: request_id, behavior });
52302
52449
  pendingPermissions.delete(request_id);
52303
52450
  process.stderr.write(`[telegram gateway] slash-${behavior} request_id=${request_id} tool=${details.tool_name} by=${senderId}
52304
52451
  `);
@@ -52893,6 +53040,16 @@ async function handleVaultRequestSaveCallback(ctx, data) {
52893
53040
  if (pending.card_message_id != null) {
52894
53041
  await ctx.api.editMessageText(pending.chat_id, pending.card_message_id, `\uD83D\uDEAB <i>Discarded. The secret was not written to the vault.</i>`, { parse_mode: "HTML", reply_markup: { inline_keyboard: [] } }).catch(() => {});
52895
53042
  }
53043
+ const discardInbound = buildVaultSaveDiscardedInbound({
53044
+ ctx: { agent: pending.agent, key: pending.key, chat_id: pending.chat_id },
53045
+ stageId,
53046
+ operatorId: senderId
53047
+ });
53048
+ const dDelivered = ipcServer.sendToAgent(pending.agent, discardInbound);
53049
+ process.stderr.write(`telegram gateway: vault_save_discarded injection agent=${pending.agent} key=${pending.key} stage=${stageId} delivered=${dDelivered}
53050
+ `);
53051
+ if (!dDelivered)
53052
+ pendingInboundBuffer.push(pending.agent, discardInbound);
52896
53053
  return;
52897
53054
  }
52898
53055
  if (action === "rename") {
@@ -52935,6 +53092,19 @@ async function handleVaultRequestSaveCallback(ctx, data) {
52935
53092
  <i>Tap a fresh card after fixing the underlying issue.</i>`, { parse_mode: "HTML", reply_markup: { inline_keyboard: [] } }).catch(() => {});
52936
53093
  }
52937
53094
  pendingVaultRequestSaves.delete(stageId);
53095
+ const failReason = (write.output || "vault write error").split(`
53096
+ `)[0].slice(0, 200);
53097
+ const failInbound = buildVaultSaveFailedInbound({
53098
+ ctx: { agent: pending.agent, key: pending.key, chat_id: pending.chat_id },
53099
+ stageId,
53100
+ operatorId: senderId,
53101
+ reason: failReason
53102
+ });
53103
+ const fDelivered = ipcServer.sendToAgent(pending.agent, failInbound);
53104
+ process.stderr.write(`telegram gateway: vault_save_failed injection agent=${pending.agent} key=${pending.key} stage=${stageId} delivered=${fDelivered}
53105
+ `);
53106
+ if (!fDelivered)
53107
+ pendingInboundBuffer.push(pending.agent, failInbound);
52938
53108
  return;
52939
53109
  }
52940
53110
  pendingVaultRequestSaves.delete(stageId);
@@ -52942,6 +53112,16 @@ async function handleVaultRequestSaveCallback(ctx, data) {
52942
53112
  await ctx.api.editMessageText(pending.chat_id, pending.card_message_id, `\u2705 saved as <code>vault:${escapeHtmlForTg(pending.key)}</code> (masked: <code>${escapeHtmlForTg(maskToken2(pending.value))}</code>)
52943
53113
  <i>The agent can now reference this as <code>vault:${escapeHtmlForTg(pending.key)}</code>.</i>`, { parse_mode: "HTML", reply_markup: { inline_keyboard: [] } }).catch(() => {});
52944
53114
  }
53115
+ const okInbound = buildVaultSaveCompletedInbound({
53116
+ ctx: { agent: pending.agent, key: pending.key, chat_id: pending.chat_id },
53117
+ stageId,
53118
+ operatorId: senderId
53119
+ });
53120
+ const okDelivered = ipcServer.sendToAgent(pending.agent, okInbound);
53121
+ process.stderr.write(`telegram gateway: vault_save_completed injection agent=${pending.agent} key=${pending.key} stage=${stageId} delivered=${okDelivered}
53122
+ `);
53123
+ if (!okDelivered)
53124
+ pendingInboundBuffer.push(pending.agent, okInbound);
52945
53125
  return;
52946
53126
  }
52947
53127
  await ctx.answerCallbackQuery({ text: "Unknown action" }).catch(() => {});
@@ -54348,9 +54528,11 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
54348
54528
  };
54349
54529
  process.stderr.write(`telegram gateway: button_callback chatId=${cbChatId} user=${ctx.from.id} data=${JSON.stringify(agentCb.raw)} btnText=${JSON.stringify(buttonText ?? null)}
54350
54530
  `);
54351
- ipcServer.broadcast(inboundMsg);
54352
- if (ipcServer.clientCount() === 0) {
54353
- swallowingApiCall(() => bot.api.sendMessage(cbChatId, "\u23F3 Agent is restarting \u2014 your button tap was queued but won't be processed until it comes back.", cbThreadId != null ? { message_thread_id: cbThreadId } : {}), {
54531
+ const selfAgentBtn = process.env.SWITCHROOM_AGENT_NAME ?? "";
54532
+ const btnDelivered = ipcServer.sendToAgent(selfAgentBtn, inboundMsg);
54533
+ if (!btnDelivered) {
54534
+ pendingInboundBuffer.push(selfAgentBtn, inboundMsg);
54535
+ swallowingApiCall(() => bot.api.sendMessage(cbChatId, "\u23F3 Agent is restarting \u2014 your button tap is queued and will be processed when it comes back.", cbThreadId != null ? { message_thread_id: cbThreadId } : {}), {
54354
54536
  chat_id: cbChatId,
54355
54537
  verb: "button-tap-restarting-notice",
54356
54538
  ...cbThreadId != null ? { threadId: cbThreadId } : {}
@@ -54445,7 +54627,7 @@ ${prettyInput}`;
54445
54627
  ${editLabel}` : editLabel,
54446
54628
  parseMode: "HTML",
54447
54629
  synthInbound: () => {
54448
- ipcServer.broadcast({
54630
+ dispatchPermissionVerdict({
54449
54631
  type: "permission",
54450
54632
  requestId: request_id,
54451
54633
  behavior: "allow",
@@ -54466,7 +54648,7 @@ ${editLabel}` : editLabel,
54466
54648
  ${label}` : label,
54467
54649
  parseMode: "HTML",
54468
54650
  synthInbound: () => {
54469
- ipcServer.broadcast({
54651
+ dispatchPermissionVerdict({
54470
54652
  type: "permission",
54471
54653
  requestId: request_id,
54472
54654
  behavior
@@ -57,19 +57,18 @@ export async function handleApprovalCallback(
57
57
  return;
58
58
  }
59
59
 
60
- const consumed = await approvalConsume(parsed.request_id);
61
- if (consumed === null) {
62
- await ctx.answerCallbackQuery({ text: "approval kernel unreachable" });
63
- return;
64
- }
65
- if (!consumed.consumed) {
66
- // Single-use enforcement: someone already tapped, or the nonce
67
- // expired/unknown. Match the RFC §8.1 wording.
68
- await ctx.answerCallbackQuery({ text: "this prompt expired" });
69
- return;
70
- }
71
-
72
- // Compute decision + ttl from the choice variant.
60
+ // Compute decision + ttl from the choice variant BEFORE burning the
61
+ // single-use nonce. This block has a fallible early-return (the
62
+ // `bad ttl token` path). Pre-fix it ran AFTER approvalConsume(), so a
63
+ // malformed ttl token burned the nonce but recorded no decision — the
64
+ // agent's approval_lookup poll never saw a verdict and the turn
65
+ // wedged (pre-PR-3: forever; now bounded by PR-3's PERMISSION_TTL
66
+ // auto-deny). approvalConsume stays the atomic single-use guard; it
67
+ // simply doesn't fire until we have a valid decision to record
68
+ // immediately after. There is now NO fallible step between
69
+ // consume→record; the only residual gap is the inherent 1-RPC
70
+ // consume/record non-atomicity (backstopped by PR-3's TTL auto-deny;
71
+ // a fully atomic kernel consume+record is a tracked follow-up).
73
72
  let decision: ApprovalDecisionMode;
74
73
  let granted: boolean;
75
74
  let ttl_ms: number | null = null;
@@ -107,6 +106,18 @@ export async function handleApprovalCallback(
107
106
  }
108
107
  }
109
108
 
109
+ const consumed = await approvalConsume(parsed.request_id);
110
+ if (consumed === null) {
111
+ await ctx.answerCallbackQuery({ text: "approval kernel unreachable" });
112
+ return;
113
+ }
114
+ if (!consumed.consumed) {
115
+ // Single-use enforcement: someone already tapped, or the nonce
116
+ // expired/unknown. Match the RFC §8.1 wording.
117
+ await ctx.answerCallbackQuery({ text: "this prompt expired" });
118
+ return;
119
+ }
120
+
110
121
  const granted_by_user_id = ctx.from?.id ?? 0;
111
122
  // Approver set at decision time = the chat that received the card. We
112
123
  // store the singleton for now; the gateway-side approver-set lookup