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.
- package/README.md +107 -371
- package/dist/cli/switchroom.js +397 -245
- package/dist/host-control/main.js +1 -1
- package/dist/vault/approvals/kernel-server.js +20 -0
- package/dist/vault/broker/server.js +111 -1
- package/package.json +1 -1
- package/telegram-plugin/dist/gateway/gateway.js +208 -26
- package/telegram-plugin/gateway/approval-callback.ts +24 -13
- package/telegram-plugin/gateway/gateway.ts +149 -17
- package/telegram-plugin/gateway/pending-permission-decisions.ts +112 -0
- package/telegram-plugin/gateway/vault-grant-inbound-builders.ts +117 -0
- package/telegram-plugin/tests/pending-permission-decisions.test.ts +73 -0
- package/telegram-plugin/tests/vault-save-inbound-builders.test.ts +96 -0
|
@@ -15877,7 +15877,7 @@ class HostdServer {
|
|
|
15877
15877
|
const PROBES = [
|
|
15878
15878
|
{
|
|
15879
15879
|
name: "auth",
|
|
15880
|
-
cmd:
|
|
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.
|
|
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
|
@@ -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.
|
|
46625
|
-
var COMMIT_SHA = "
|
|
46626
|
-
var COMMIT_DATE = "2026-05-
|
|
46627
|
-
var LATEST_PR =
|
|
46628
|
-
var COMMITS_AHEAD_OF_TAG =
|
|
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
|
-
|
|
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
|
-
|
|
51125
|
-
const delivered = ipcServer.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
54352
|
-
|
|
54353
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|