switchroom 0.12.13 → 0.12.15
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 +627 -386
- package/dist/vault/approvals/kernel-server.js +88 -1
- package/dist/vault/broker/server.js +132 -2
- package/package.json +1 -1
- package/telegram-plugin/dist/gateway/gateway.js +289 -81
- package/telegram-plugin/gateway/approval-callback.test.ts +49 -1
- package/telegram-plugin/gateway/approval-callback.ts +85 -56
- package/telegram-plugin/gateway/gateway.ts +168 -19
- package/telegram-plugin/gateway/pending-inbound-buffer.ts +39 -0
- 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-inbound-buffer.test.ts +71 -1
- package/telegram-plugin/tests/pending-permission-decisions.test.ts +73 -0
- package/telegram-plugin/tests/vault-save-inbound-builders.test.ts +96 -0
|
@@ -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, ApprovalConsumeRecordRequestSchema, RequestSchema2, VaultEntrySchema, ErrorCode, OkEntryResponseSchema, OkKeysResponseSchema, BrokerStatus, OkStatusResponseSchema, OkLockResponseSchema, OkPutResponseSchema, OkMintGrantResponseSchema, GrantMetaSchema, OkListGrantsResponseSchema, OkRevokeGrantResponseSchema, OkApprovalRequestResponseSchema, ApprovalDecisionMetaSchema, OkApprovalLookupResponseSchema, OkApprovalConsumeResponseSchema, OkApprovalRevokeResponseSchema, OkApprovalListResponseSchema, OkApprovalRecordResponseSchema, OkApprovalConsumeRecordResponseSchema, 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"),
|
|
@@ -24774,12 +24792,22 @@ var init_protocol2 = __esm(() => {
|
|
|
24774
24792
|
granted_by_user_id: exports_external.number().int(),
|
|
24775
24793
|
ttl_ms: exports_external.number().int().positive().nullable().optional()
|
|
24776
24794
|
});
|
|
24795
|
+
ApprovalConsumeRecordRequestSchema = exports_external.object({
|
|
24796
|
+
v: exports_external.literal(1),
|
|
24797
|
+
op: exports_external.literal("approval_consume_record"),
|
|
24798
|
+
request_id: exports_external.string().regex(/^[0-9a-f]{32}$/),
|
|
24799
|
+
decision: ApprovalDecisionModeSchema,
|
|
24800
|
+
approver_set: exports_external.array(exports_external.string()),
|
|
24801
|
+
granted_by_user_id: exports_external.number().int(),
|
|
24802
|
+
ttl_ms: exports_external.number().int().positive().nullable().optional()
|
|
24803
|
+
});
|
|
24777
24804
|
RequestSchema2 = exports_external.discriminatedUnion("op", [
|
|
24778
24805
|
GetRequestSchema,
|
|
24779
24806
|
PutRequestSchema,
|
|
24780
24807
|
ListRequestSchema,
|
|
24781
24808
|
StatusRequestSchema,
|
|
24782
24809
|
LockRequestSchema,
|
|
24810
|
+
PreflightAccessRequestSchema,
|
|
24783
24811
|
MintGrantRequestSchema,
|
|
24784
24812
|
ListGrantsRequestSchema,
|
|
24785
24813
|
RevokeGrantRequestSchema,
|
|
@@ -24788,7 +24816,8 @@ var init_protocol2 = __esm(() => {
|
|
|
24788
24816
|
ApprovalConsumeRequestSchema,
|
|
24789
24817
|
ApprovalRevokeRequestSchema,
|
|
24790
24818
|
ApprovalListRequestSchema,
|
|
24791
|
-
ApprovalRecordRequestSchema
|
|
24819
|
+
ApprovalRecordRequestSchema,
|
|
24820
|
+
ApprovalConsumeRecordRequestSchema
|
|
24792
24821
|
]);
|
|
24793
24822
|
VaultEntrySchema = exports_external.union([
|
|
24794
24823
|
exports_external.object({ kind: exports_external.literal("string"), value: exports_external.string() }),
|
|
@@ -24910,6 +24939,15 @@ var init_protocol2 = __esm(() => {
|
|
|
24910
24939
|
ok: exports_external.literal(true),
|
|
24911
24940
|
decision_id: exports_external.string()
|
|
24912
24941
|
});
|
|
24942
|
+
OkApprovalConsumeRecordResponseSchema = exports_external.object({
|
|
24943
|
+
ok: exports_external.literal(true),
|
|
24944
|
+
consumed: exports_external.boolean(),
|
|
24945
|
+
decision_id: exports_external.string().optional(),
|
|
24946
|
+
agent_unit: exports_external.string().optional(),
|
|
24947
|
+
scope: exports_external.string().optional(),
|
|
24948
|
+
action: exports_external.string().optional(),
|
|
24949
|
+
why: exports_external.string().nullable().optional()
|
|
24950
|
+
});
|
|
24913
24951
|
ErrorResponseSchema2 = exports_external.object({
|
|
24914
24952
|
ok: exports_external.literal(false),
|
|
24915
24953
|
code: ErrorCode,
|
|
@@ -24920,6 +24958,7 @@ var init_protocol2 = __esm(() => {
|
|
|
24920
24958
|
OkKeysResponseSchema,
|
|
24921
24959
|
OkStatusResponseSchema,
|
|
24922
24960
|
OkLockResponseSchema,
|
|
24961
|
+
OkPreflightAccessResponseSchema,
|
|
24923
24962
|
OkPutResponseSchema,
|
|
24924
24963
|
OkMintGrantResponseSchema,
|
|
24925
24964
|
OkListGrantsResponseSchema,
|
|
@@ -24930,6 +24969,7 @@ var init_protocol2 = __esm(() => {
|
|
|
24930
24969
|
OkApprovalRevokeResponseSchema,
|
|
24931
24970
|
OkApprovalListResponseSchema,
|
|
24932
24971
|
OkApprovalRecordResponseSchema,
|
|
24972
|
+
OkApprovalConsumeRecordResponseSchema,
|
|
24933
24973
|
ErrorResponseSchema2
|
|
24934
24974
|
]);
|
|
24935
24975
|
});
|
|
@@ -29112,20 +29152,6 @@ function withKernelOpts2(opts) {
|
|
|
29112
29152
|
return opts;
|
|
29113
29153
|
return { ...opts ?? {}, socket: sock };
|
|
29114
29154
|
}
|
|
29115
|
-
async function approvalConsume2(request_id, opts) {
|
|
29116
|
-
const r = await rpcRaw({ v: 1, op: "approval_consume", request_id }, withKernelOpts2(opts));
|
|
29117
|
-
if (r.kind !== "response" || !r.resp.ok)
|
|
29118
|
-
return null;
|
|
29119
|
-
if (!("consumed" in r.resp))
|
|
29120
|
-
return null;
|
|
29121
|
-
return {
|
|
29122
|
-
consumed: r.resp.consumed,
|
|
29123
|
-
agent_unit: r.resp.agent_unit,
|
|
29124
|
-
scope: r.resp.scope,
|
|
29125
|
-
action: r.resp.action,
|
|
29126
|
-
why: r.resp.why ?? null
|
|
29127
|
-
};
|
|
29128
|
-
}
|
|
29129
29155
|
async function approvalRevoke(decision_id, actor, reason, opts) {
|
|
29130
29156
|
const r = await rpcRaw({ v: 1, op: "approval_revoke", decision_id, actor, reason }, withKernelOpts2(opts));
|
|
29131
29157
|
if (r.kind !== "response" || !r.resp.ok)
|
|
@@ -29134,10 +29160,10 @@ async function approvalRevoke(decision_id, actor, reason, opts) {
|
|
|
29134
29160
|
return null;
|
|
29135
29161
|
return r.resp.revoked;
|
|
29136
29162
|
}
|
|
29137
|
-
async function
|
|
29163
|
+
async function approvalConsumeRecord(args, opts) {
|
|
29138
29164
|
const r = await rpcRaw({
|
|
29139
29165
|
v: 1,
|
|
29140
|
-
op: "
|
|
29166
|
+
op: "approval_consume_record",
|
|
29141
29167
|
request_id: args.request_id,
|
|
29142
29168
|
decision: args.decision,
|
|
29143
29169
|
approver_set: args.approver_set,
|
|
@@ -29146,9 +29172,17 @@ async function approvalRecord2(args, opts) {
|
|
|
29146
29172
|
}, withKernelOpts2(opts));
|
|
29147
29173
|
if (r.kind !== "response" || !r.resp.ok)
|
|
29148
29174
|
return null;
|
|
29149
|
-
if (!("
|
|
29175
|
+
if (!("consumed" in r.resp))
|
|
29150
29176
|
return null;
|
|
29151
|
-
|
|
29177
|
+
const resp = r.resp;
|
|
29178
|
+
return {
|
|
29179
|
+
consumed: resp.consumed,
|
|
29180
|
+
decision_id: resp.decision_id,
|
|
29181
|
+
agent_unit: resp.agent_unit,
|
|
29182
|
+
scope: resp.scope,
|
|
29183
|
+
action: resp.action,
|
|
29184
|
+
why: resp.why ?? null
|
|
29185
|
+
};
|
|
29152
29186
|
}
|
|
29153
29187
|
async function approvalList(agent_unit, opts) {
|
|
29154
29188
|
const r = await rpcRaw({ v: 1, op: "approval_list", agent_unit }, withKernelOpts2(opts));
|
|
@@ -29595,9 +29629,26 @@ var init_approval_card = __esm(() => {
|
|
|
29595
29629
|
// gateway/approval-callback.ts
|
|
29596
29630
|
var exports_approval_callback = {};
|
|
29597
29631
|
__export(exports_approval_callback, {
|
|
29632
|
+
resolveApprovalDecision: () => resolveApprovalDecision,
|
|
29598
29633
|
handleApprovalCallback: () => handleApprovalCallback,
|
|
29599
29634
|
buildGrantedKeyboard: () => buildGrantedKeyboard
|
|
29600
29635
|
});
|
|
29636
|
+
function resolveApprovalDecision(choice) {
|
|
29637
|
+
switch (choice.kind) {
|
|
29638
|
+
case "deny":
|
|
29639
|
+
return { ok: true, decision: "deny", granted: false, ttl_ms: null, displayMode: "denied" };
|
|
29640
|
+
case "once":
|
|
29641
|
+
return { ok: true, decision: "allow_once", granted: true, ttl_ms: null, displayMode: "granted once" };
|
|
29642
|
+
case "always":
|
|
29643
|
+
return { ok: true, decision: "allow_always", granted: true, ttl_ms: null, displayMode: "granted always" };
|
|
29644
|
+
case "ttl": {
|
|
29645
|
+
const ms = ttlMsFromToken(choice.param);
|
|
29646
|
+
if (ms === null)
|
|
29647
|
+
return { ok: false, error: "bad ttl token" };
|
|
29648
|
+
return { ok: true, decision: "allow_ttl", granted: true, ttl_ms: ms, displayMode: `granted for ${choice.param}` };
|
|
29649
|
+
}
|
|
29650
|
+
}
|
|
29651
|
+
}
|
|
29601
29652
|
function buildGrantedKeyboard(scope) {
|
|
29602
29653
|
const btn = scopeToOpenInDriveButton(scope);
|
|
29603
29654
|
if (btn === null)
|
|
@@ -29610,64 +29661,37 @@ async function handleApprovalCallback(ctx, data) {
|
|
|
29610
29661
|
await ctx.answerCallbackQuery({ text: "malformed approval callback" });
|
|
29611
29662
|
return;
|
|
29612
29663
|
}
|
|
29613
|
-
const
|
|
29614
|
-
if (
|
|
29615
|
-
await ctx.answerCallbackQuery({ text:
|
|
29616
|
-
return;
|
|
29617
|
-
}
|
|
29618
|
-
if (!consumed.consumed) {
|
|
29619
|
-
await ctx.answerCallbackQuery({ text: "this prompt expired" });
|
|
29664
|
+
const resolved = resolveApprovalDecision(parsed.choice);
|
|
29665
|
+
if (!resolved.ok) {
|
|
29666
|
+
await ctx.answerCallbackQuery({ text: resolved.error });
|
|
29620
29667
|
return;
|
|
29621
29668
|
}
|
|
29622
|
-
|
|
29623
|
-
let granted;
|
|
29624
|
-
let ttl_ms = null;
|
|
29625
|
-
let displayMode;
|
|
29626
|
-
switch (parsed.choice.kind) {
|
|
29627
|
-
case "deny":
|
|
29628
|
-
decision = "deny";
|
|
29629
|
-
granted = false;
|
|
29630
|
-
displayMode = "denied";
|
|
29631
|
-
break;
|
|
29632
|
-
case "once":
|
|
29633
|
-
decision = "allow_once";
|
|
29634
|
-
granted = true;
|
|
29635
|
-
displayMode = "granted once";
|
|
29636
|
-
break;
|
|
29637
|
-
case "always":
|
|
29638
|
-
decision = "allow_always";
|
|
29639
|
-
granted = true;
|
|
29640
|
-
displayMode = "granted always";
|
|
29641
|
-
break;
|
|
29642
|
-
case "ttl": {
|
|
29643
|
-
decision = "allow_ttl";
|
|
29644
|
-
granted = true;
|
|
29645
|
-
const ms = ttlMsFromToken(parsed.choice.param);
|
|
29646
|
-
if (ms === null) {
|
|
29647
|
-
await ctx.answerCallbackQuery({ text: "bad ttl token" });
|
|
29648
|
-
return;
|
|
29649
|
-
}
|
|
29650
|
-
ttl_ms = ms;
|
|
29651
|
-
displayMode = `granted for ${parsed.choice.param}`;
|
|
29652
|
-
break;
|
|
29653
|
-
}
|
|
29654
|
-
}
|
|
29669
|
+
const { decision, granted, ttl_ms, displayMode } = resolved;
|
|
29655
29670
|
const granted_by_user_id = ctx.from?.id ?? 0;
|
|
29656
29671
|
const approver_set = [String(granted_by_user_id)];
|
|
29657
|
-
const
|
|
29672
|
+
const result = await approvalConsumeRecord({
|
|
29658
29673
|
request_id: parsed.request_id,
|
|
29659
29674
|
decision,
|
|
29660
29675
|
approver_set,
|
|
29661
29676
|
granted_by_user_id,
|
|
29662
29677
|
ttl_ms
|
|
29663
29678
|
});
|
|
29664
|
-
if (
|
|
29679
|
+
if (result === null) {
|
|
29680
|
+
await ctx.answerCallbackQuery({ text: "approval kernel unreachable" });
|
|
29681
|
+
return;
|
|
29682
|
+
}
|
|
29683
|
+
if (!result.consumed) {
|
|
29684
|
+
await ctx.answerCallbackQuery({ text: "this prompt expired" });
|
|
29685
|
+
return;
|
|
29686
|
+
}
|
|
29687
|
+
if (!result.decision_id) {
|
|
29665
29688
|
await ctx.answerCallbackQuery({ text: "kernel record failed" });
|
|
29666
29689
|
return;
|
|
29667
29690
|
}
|
|
29691
|
+
const decision_id = result.decision_id;
|
|
29668
29692
|
const icon = granted ? "\u2705" : "\uD83D\uDEAB";
|
|
29669
29693
|
const newBody = `${icon} ${displayMode}` + (granted ? ` \u00b7 /approvals revoke <code>${decision_id}</code>` : "");
|
|
29670
|
-
const postTapKeyboard = granted &&
|
|
29694
|
+
const postTapKeyboard = granted && result.scope ? buildGrantedKeyboard(result.scope) : undefined;
|
|
29671
29695
|
try {
|
|
29672
29696
|
await ctx.editMessageText(newBody, {
|
|
29673
29697
|
parse_mode: "HTML",
|
|
@@ -41280,7 +41304,7 @@ async function approvalRecord(args, opts) {
|
|
|
41280
41304
|
return null;
|
|
41281
41305
|
if (!("decision_id" in r.resp))
|
|
41282
41306
|
return null;
|
|
41283
|
-
return r.resp.decision_id;
|
|
41307
|
+
return r.resp.decision_id ?? null;
|
|
41284
41308
|
}
|
|
41285
41309
|
|
|
41286
41310
|
// quota-check.ts
|
|
@@ -43322,6 +43346,26 @@ function escapeHtml7(s) {
|
|
|
43322
43346
|
|
|
43323
43347
|
// gateway/pending-inbound-buffer.ts
|
|
43324
43348
|
var DEFAULT_PENDING_INBOUND_CAP = 32;
|
|
43349
|
+
function redeliverBufferedInbound(buffer, agent, send) {
|
|
43350
|
+
const pending = buffer.drain(agent);
|
|
43351
|
+
let redelivered = 0;
|
|
43352
|
+
let rebuffered = 0;
|
|
43353
|
+
for (const msg of pending) {
|
|
43354
|
+
let delivered = false;
|
|
43355
|
+
try {
|
|
43356
|
+
delivered = send(msg);
|
|
43357
|
+
} catch {
|
|
43358
|
+
delivered = false;
|
|
43359
|
+
}
|
|
43360
|
+
if (delivered) {
|
|
43361
|
+
redelivered++;
|
|
43362
|
+
} else {
|
|
43363
|
+
buffer.push(agent, msg);
|
|
43364
|
+
rebuffered++;
|
|
43365
|
+
}
|
|
43366
|
+
}
|
|
43367
|
+
return { drained: pending.length, redelivered, rebuffered };
|
|
43368
|
+
}
|
|
43325
43369
|
function createPendingInboundBuffer(opts = {}) {
|
|
43326
43370
|
const cap = opts.capPerAgent ?? DEFAULT_PENDING_INBOUND_CAP;
|
|
43327
43371
|
const log = opts.log ?? ((line) => process.stderr.write(line));
|
|
@@ -43366,6 +43410,52 @@ function createPendingInboundBuffer(opts = {}) {
|
|
|
43366
43410
|
};
|
|
43367
43411
|
}
|
|
43368
43412
|
|
|
43413
|
+
// gateway/pending-permission-decisions.ts
|
|
43414
|
+
var DEFAULT_PENDING_PERMISSION_CAP = 32;
|
|
43415
|
+
function createPendingPermissionBuffer(opts = {}) {
|
|
43416
|
+
const cap = opts.capPerAgent ?? DEFAULT_PENDING_PERMISSION_CAP;
|
|
43417
|
+
const log = opts.log ?? ((line) => process.stderr.write(line));
|
|
43418
|
+
const queues = new Map;
|
|
43419
|
+
return {
|
|
43420
|
+
push(agent, ev) {
|
|
43421
|
+
let q = queues.get(agent);
|
|
43422
|
+
if (q == null) {
|
|
43423
|
+
q = [];
|
|
43424
|
+
queues.set(agent, q);
|
|
43425
|
+
}
|
|
43426
|
+
let evicted = false;
|
|
43427
|
+
if (q.length >= cap) {
|
|
43428
|
+
const dropped = q.shift();
|
|
43429
|
+
evicted = true;
|
|
43430
|
+
log(`pending-permission-buffer: agent=${agent} cap=${cap} reached \u2014 ` + `dropped oldest verdict request=${dropped?.requestId ?? "-"} ` + `behavior=${dropped?.behavior ?? "-"}
|
|
43431
|
+
`);
|
|
43432
|
+
}
|
|
43433
|
+
q.push(ev);
|
|
43434
|
+
log(`pending-permission-buffer: agent=${agent} buffered request=${ev.requestId} ` + `behavior=${ev.behavior} depth_after=${q.length} evicted=${evicted}
|
|
43435
|
+
`);
|
|
43436
|
+
return !evicted;
|
|
43437
|
+
},
|
|
43438
|
+
drain(agent) {
|
|
43439
|
+
const q = queues.get(agent);
|
|
43440
|
+
if (q == null || q.length === 0)
|
|
43441
|
+
return [];
|
|
43442
|
+
queues.delete(agent);
|
|
43443
|
+
log(`pending-permission-buffer: drained agent=${agent} count=${q.length} ` + `requests=[${q.map((e) => e.requestId).join(",")}]
|
|
43444
|
+
`);
|
|
43445
|
+
return q;
|
|
43446
|
+
},
|
|
43447
|
+
depth(agent) {
|
|
43448
|
+
return queues.get(agent)?.length ?? 0;
|
|
43449
|
+
},
|
|
43450
|
+
totalDepth() {
|
|
43451
|
+
let n = 0;
|
|
43452
|
+
for (const q of queues.values())
|
|
43453
|
+
n += q.length;
|
|
43454
|
+
return n;
|
|
43455
|
+
}
|
|
43456
|
+
};
|
|
43457
|
+
}
|
|
43458
|
+
|
|
43369
43459
|
// gateway/vault-grant-inbound-builders.ts
|
|
43370
43460
|
function buildVaultGrantApprovedInbound(opts) {
|
|
43371
43461
|
const ts = opts.nowMs ?? Date.now();
|
|
@@ -43409,6 +43499,63 @@ function buildVaultGrantDeniedInbound(opts) {
|
|
|
43409
43499
|
}
|
|
43410
43500
|
};
|
|
43411
43501
|
}
|
|
43502
|
+
function buildVaultSaveCompletedInbound(opts) {
|
|
43503
|
+
const ts = opts.nowMs ?? Date.now();
|
|
43504
|
+
return {
|
|
43505
|
+
type: "inbound",
|
|
43506
|
+
chatId: opts.ctx.chat_id,
|
|
43507
|
+
messageId: ts,
|
|
43508
|
+
user: "vault-broker",
|
|
43509
|
+
userId: 0,
|
|
43510
|
+
ts,
|
|
43511
|
+
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.`,
|
|
43512
|
+
meta: {
|
|
43513
|
+
source: "vault_save_completed",
|
|
43514
|
+
agent: opts.ctx.agent,
|
|
43515
|
+
key: opts.ctx.key,
|
|
43516
|
+
stage_id: opts.stageId,
|
|
43517
|
+
operator_id: opts.operatorId
|
|
43518
|
+
}
|
|
43519
|
+
};
|
|
43520
|
+
}
|
|
43521
|
+
function buildVaultSaveFailedInbound(opts) {
|
|
43522
|
+
const ts = opts.nowMs ?? Date.now();
|
|
43523
|
+
return {
|
|
43524
|
+
type: "inbound",
|
|
43525
|
+
chatId: opts.ctx.chat_id,
|
|
43526
|
+
messageId: ts,
|
|
43527
|
+
user: "vault-broker",
|
|
43528
|
+
userId: 0,
|
|
43529
|
+
ts,
|
|
43530
|
+
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.`,
|
|
43531
|
+
meta: {
|
|
43532
|
+
source: "vault_save_failed",
|
|
43533
|
+
agent: opts.ctx.agent,
|
|
43534
|
+
key: opts.ctx.key,
|
|
43535
|
+
stage_id: opts.stageId,
|
|
43536
|
+
operator_id: opts.operatorId
|
|
43537
|
+
}
|
|
43538
|
+
};
|
|
43539
|
+
}
|
|
43540
|
+
function buildVaultSaveDiscardedInbound(opts) {
|
|
43541
|
+
const ts = opts.nowMs ?? Date.now();
|
|
43542
|
+
return {
|
|
43543
|
+
type: "inbound",
|
|
43544
|
+
chatId: opts.ctx.chat_id,
|
|
43545
|
+
messageId: ts,
|
|
43546
|
+
user: "vault-broker",
|
|
43547
|
+
userId: 0,
|
|
43548
|
+
ts,
|
|
43549
|
+
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.`,
|
|
43550
|
+
meta: {
|
|
43551
|
+
source: "vault_save_discarded",
|
|
43552
|
+
agent: opts.ctx.agent,
|
|
43553
|
+
key: opts.ctx.key,
|
|
43554
|
+
stage_id: opts.stageId,
|
|
43555
|
+
operator_id: opts.operatorId
|
|
43556
|
+
}
|
|
43557
|
+
};
|
|
43558
|
+
}
|
|
43412
43559
|
|
|
43413
43560
|
// gateway/poll-health.ts
|
|
43414
43561
|
var DEFAULT_LOG = (msg) => {
|
|
@@ -46621,11 +46768,11 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
|
|
|
46621
46768
|
}
|
|
46622
46769
|
|
|
46623
46770
|
// ../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 =
|
|
46771
|
+
var VERSION = "0.12.15";
|
|
46772
|
+
var COMMIT_SHA = "dc508a92";
|
|
46773
|
+
var COMMIT_DATE = "2026-05-19T07:24:41Z";
|
|
46774
|
+
var LATEST_PR = 1547;
|
|
46775
|
+
var COMMITS_AHEAD_OF_TAG = 22;
|
|
46629
46776
|
|
|
46630
46777
|
// gateway/boot-version.ts
|
|
46631
46778
|
function formatRelativeAgo(iso) {
|
|
@@ -47995,8 +48142,12 @@ var pendingStateReaper = setInterval(() => {
|
|
|
47995
48142
|
pendingVaultOps.delete(k);
|
|
47996
48143
|
}
|
|
47997
48144
|
for (const [k, v] of pendingPermissions) {
|
|
47998
|
-
if (now - v.startedAt > PERMISSION_TTL_MS)
|
|
48145
|
+
if (now - v.startedAt > PERMISSION_TTL_MS) {
|
|
48146
|
+
dispatchPermissionVerdict({ type: "permission", requestId: k, behavior: "deny" });
|
|
48147
|
+
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)
|
|
48148
|
+
`);
|
|
47999
48149
|
pendingPermissions.delete(k);
|
|
48150
|
+
}
|
|
48000
48151
|
}
|
|
48001
48152
|
for (const [k, v] of vaultPassphraseCache) {
|
|
48002
48153
|
if (now > v.expiresAt)
|
|
@@ -48311,11 +48462,23 @@ startTimer({
|
|
|
48311
48462
|
try {
|
|
48312
48463
|
clearSilentEndState(fbKey);
|
|
48313
48464
|
} catch {}
|
|
48314
|
-
|
|
48465
|
+
const fbSelfAgent = process.env.SWITCHROOM_AGENT_NAME ?? "";
|
|
48466
|
+
const fbRedeliver = redeliverBufferedInbound(pendingInboundBuffer, fbSelfAgent, (m) => ipcServer.sendToAgent(fbSelfAgent, m));
|
|
48467
|
+
process.stderr.write(`telegram gateway: silence-poke framework-fallback ended wedged turn chat=${fbChatId} thread=${ctx.threadId ?? "-"} silence_ms=${ctx.silenceMs} currentTurn_nulled=${turnMatchesFallback} drained_buffered=${fbRedeliver.redelivered}/${fbRedeliver.drained}${fbRedeliver.rebuffered > 0 ? ` rebuffered=${fbRedeliver.rebuffered}` : ""}
|
|
48315
48468
|
`);
|
|
48316
48469
|
}
|
|
48317
48470
|
});
|
|
48318
48471
|
var pendingInboundBuffer = createPendingInboundBuffer();
|
|
48472
|
+
var pendingPermissionBuffer = createPendingPermissionBuffer();
|
|
48473
|
+
function dispatchPermissionVerdict(ev) {
|
|
48474
|
+
const selfAgent = process.env.SWITCHROOM_AGENT_NAME ?? "";
|
|
48475
|
+
const delivered = ipcServer.sendToAgent(selfAgent, ev);
|
|
48476
|
+
if (!delivered) {
|
|
48477
|
+
pendingPermissionBuffer.push(selfAgent, ev);
|
|
48478
|
+
process.stderr.write(`telegram gateway: permission verdict buffered (bridge offline) request=${ev.requestId} behavior=${ev.behavior}
|
|
48479
|
+
`);
|
|
48480
|
+
}
|
|
48481
|
+
}
|
|
48319
48482
|
var ipcServer = createIpcServer({
|
|
48320
48483
|
socketPath: SOCKET_PATH,
|
|
48321
48484
|
onClientRegistered(client3) {
|
|
@@ -48329,6 +48492,15 @@ var ipcServer = createIpcServer({
|
|
|
48329
48492
|
client3.send(msg);
|
|
48330
48493
|
} catch (err) {
|
|
48331
48494
|
process.stderr.write(`telegram gateway: pending-inbound drain failed agent=${client3.agentName} source=${msg.meta?.source ?? "-"}: ${err.message}
|
|
48495
|
+
`);
|
|
48496
|
+
}
|
|
48497
|
+
}
|
|
48498
|
+
const pendingVerdicts = pendingPermissionBuffer.drain(client3.agentName);
|
|
48499
|
+
for (const ev of pendingVerdicts) {
|
|
48500
|
+
try {
|
|
48501
|
+
client3.send(ev);
|
|
48502
|
+
} catch (err) {
|
|
48503
|
+
process.stderr.write(`telegram gateway: pending-permission drain failed agent=${client3.agentName} request=${ev.requestId} behavior=${ev.behavior}: ${err.message}
|
|
48332
48504
|
`);
|
|
48333
48505
|
}
|
|
48334
48506
|
}
|
|
@@ -50646,7 +50818,7 @@ async function handleInbound(ctx, text, downloadImage, attachment) {
|
|
|
50646
50818
|
if (permMatch) {
|
|
50647
50819
|
const behavior = permMatch[1].toLowerCase().startsWith("y") ? "allow" : "deny";
|
|
50648
50820
|
const request_id = permMatch[2].toLowerCase();
|
|
50649
|
-
|
|
50821
|
+
dispatchPermissionVerdict({
|
|
50650
50822
|
type: "permission",
|
|
50651
50823
|
requestId: request_id,
|
|
50652
50824
|
behavior
|
|
@@ -51121,11 +51293,12 @@ ${preBlock(write.output)}`;
|
|
|
51121
51293
|
} : {}
|
|
51122
51294
|
}
|
|
51123
51295
|
};
|
|
51124
|
-
|
|
51125
|
-
const delivered = ipcServer.
|
|
51296
|
+
const selfAgent = process.env.SWITCHROOM_AGENT_NAME ?? "";
|
|
51297
|
+
const delivered = ipcServer.sendToAgent(selfAgent, inboundMsg);
|
|
51126
51298
|
if (!delivered) {
|
|
51299
|
+
pendingInboundBuffer.push(selfAgent, inboundMsg);
|
|
51127
51300
|
const threadOpts = messageThreadId != null ? { message_thread_id: messageThreadId } : {};
|
|
51128
|
-
swallowingApiCall(() => bot.api.sendMessage(chat_id, "\u23F3 Agent is restarting
|
|
51301
|
+
swallowingApiCall(() => bot.api.sendMessage(chat_id, "\u23F3 Agent is restarting \u2014 your message is queued and will be processed when it reconnects.", { ...threadOpts }), {
|
|
51129
51302
|
chat_id,
|
|
51130
51303
|
verb: "agent-restarting-notice",
|
|
51131
51304
|
...messageThreadId != null ? { threadId: messageThreadId } : {}
|
|
@@ -52298,7 +52471,7 @@ async function handlePermissionSlash(ctx, behavior) {
|
|
|
52298
52471
|
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
52472
|
return;
|
|
52300
52473
|
}
|
|
52301
|
-
|
|
52474
|
+
dispatchPermissionVerdict({ type: "permission", requestId: request_id, behavior });
|
|
52302
52475
|
pendingPermissions.delete(request_id);
|
|
52303
52476
|
process.stderr.write(`[telegram gateway] slash-${behavior} request_id=${request_id} tool=${details.tool_name} by=${senderId}
|
|
52304
52477
|
`);
|
|
@@ -52893,6 +53066,16 @@ async function handleVaultRequestSaveCallback(ctx, data) {
|
|
|
52893
53066
|
if (pending.card_message_id != null) {
|
|
52894
53067
|
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
53068
|
}
|
|
53069
|
+
const discardInbound = buildVaultSaveDiscardedInbound({
|
|
53070
|
+
ctx: { agent: pending.agent, key: pending.key, chat_id: pending.chat_id },
|
|
53071
|
+
stageId,
|
|
53072
|
+
operatorId: senderId
|
|
53073
|
+
});
|
|
53074
|
+
const dDelivered = ipcServer.sendToAgent(pending.agent, discardInbound);
|
|
53075
|
+
process.stderr.write(`telegram gateway: vault_save_discarded injection agent=${pending.agent} key=${pending.key} stage=${stageId} delivered=${dDelivered}
|
|
53076
|
+
`);
|
|
53077
|
+
if (!dDelivered)
|
|
53078
|
+
pendingInboundBuffer.push(pending.agent, discardInbound);
|
|
52896
53079
|
return;
|
|
52897
53080
|
}
|
|
52898
53081
|
if (action === "rename") {
|
|
@@ -52935,6 +53118,19 @@ async function handleVaultRequestSaveCallback(ctx, data) {
|
|
|
52935
53118
|
<i>Tap a fresh card after fixing the underlying issue.</i>`, { parse_mode: "HTML", reply_markup: { inline_keyboard: [] } }).catch(() => {});
|
|
52936
53119
|
}
|
|
52937
53120
|
pendingVaultRequestSaves.delete(stageId);
|
|
53121
|
+
const failReason = (write.output || "vault write error").split(`
|
|
53122
|
+
`)[0].slice(0, 200);
|
|
53123
|
+
const failInbound = buildVaultSaveFailedInbound({
|
|
53124
|
+
ctx: { agent: pending.agent, key: pending.key, chat_id: pending.chat_id },
|
|
53125
|
+
stageId,
|
|
53126
|
+
operatorId: senderId,
|
|
53127
|
+
reason: failReason
|
|
53128
|
+
});
|
|
53129
|
+
const fDelivered = ipcServer.sendToAgent(pending.agent, failInbound);
|
|
53130
|
+
process.stderr.write(`telegram gateway: vault_save_failed injection agent=${pending.agent} key=${pending.key} stage=${stageId} delivered=${fDelivered}
|
|
53131
|
+
`);
|
|
53132
|
+
if (!fDelivered)
|
|
53133
|
+
pendingInboundBuffer.push(pending.agent, failInbound);
|
|
52938
53134
|
return;
|
|
52939
53135
|
}
|
|
52940
53136
|
pendingVaultRequestSaves.delete(stageId);
|
|
@@ -52942,6 +53138,16 @@ async function handleVaultRequestSaveCallback(ctx, data) {
|
|
|
52942
53138
|
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
53139
|
<i>The agent can now reference this as <code>vault:${escapeHtmlForTg(pending.key)}</code>.</i>`, { parse_mode: "HTML", reply_markup: { inline_keyboard: [] } }).catch(() => {});
|
|
52944
53140
|
}
|
|
53141
|
+
const okInbound = buildVaultSaveCompletedInbound({
|
|
53142
|
+
ctx: { agent: pending.agent, key: pending.key, chat_id: pending.chat_id },
|
|
53143
|
+
stageId,
|
|
53144
|
+
operatorId: senderId
|
|
53145
|
+
});
|
|
53146
|
+
const okDelivered = ipcServer.sendToAgent(pending.agent, okInbound);
|
|
53147
|
+
process.stderr.write(`telegram gateway: vault_save_completed injection agent=${pending.agent} key=${pending.key} stage=${stageId} delivered=${okDelivered}
|
|
53148
|
+
`);
|
|
53149
|
+
if (!okDelivered)
|
|
53150
|
+
pendingInboundBuffer.push(pending.agent, okInbound);
|
|
52945
53151
|
return;
|
|
52946
53152
|
}
|
|
52947
53153
|
await ctx.answerCallbackQuery({ text: "Unknown action" }).catch(() => {});
|
|
@@ -54348,9 +54554,11 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
|
|
|
54348
54554
|
};
|
|
54349
54555
|
process.stderr.write(`telegram gateway: button_callback chatId=${cbChatId} user=${ctx.from.id} data=${JSON.stringify(agentCb.raw)} btnText=${JSON.stringify(buttonText ?? null)}
|
|
54350
54556
|
`);
|
|
54351
|
-
|
|
54352
|
-
|
|
54353
|
-
|
|
54557
|
+
const selfAgentBtn = process.env.SWITCHROOM_AGENT_NAME ?? "";
|
|
54558
|
+
const btnDelivered = ipcServer.sendToAgent(selfAgentBtn, inboundMsg);
|
|
54559
|
+
if (!btnDelivered) {
|
|
54560
|
+
pendingInboundBuffer.push(selfAgentBtn, inboundMsg);
|
|
54561
|
+
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
54562
|
chat_id: cbChatId,
|
|
54355
54563
|
verb: "button-tap-restarting-notice",
|
|
54356
54564
|
...cbThreadId != null ? { threadId: cbThreadId } : {}
|
|
@@ -54445,7 +54653,7 @@ ${prettyInput}`;
|
|
|
54445
54653
|
${editLabel}` : editLabel,
|
|
54446
54654
|
parseMode: "HTML",
|
|
54447
54655
|
synthInbound: () => {
|
|
54448
|
-
|
|
54656
|
+
dispatchPermissionVerdict({
|
|
54449
54657
|
type: "permission",
|
|
54450
54658
|
requestId: request_id,
|
|
54451
54659
|
behavior: "allow",
|
|
@@ -54466,7 +54674,7 @@ ${editLabel}` : editLabel,
|
|
|
54466
54674
|
${label}` : label,
|
|
54467
54675
|
parseMode: "HTML",
|
|
54468
54676
|
synthInbound: () => {
|
|
54469
|
-
|
|
54677
|
+
dispatchPermissionVerdict({
|
|
54470
54678
|
type: "permission",
|
|
54471
54679
|
requestId: request_id,
|
|
54472
54680
|
behavior
|
|
@@ -12,7 +12,10 @@
|
|
|
12
12
|
|
|
13
13
|
import { describe, expect, it } from "vitest";
|
|
14
14
|
import { InlineKeyboard } from "grammy";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
buildGrantedKeyboard,
|
|
17
|
+
resolveApprovalDecision,
|
|
18
|
+
} from "./approval-callback.js";
|
|
16
19
|
|
|
17
20
|
/**
|
|
18
21
|
* Helper — pull the `[{text, url}]` rows out of a grammy InlineKeyboard
|
|
@@ -102,3 +105,48 @@ describe("buildGrantedKeyboard — no button cases", () => {
|
|
|
102
105
|
expect(buildGrantedKeyboard("doc:gdrive:write:abc?evil=1")).toBeUndefined();
|
|
103
106
|
});
|
|
104
107
|
});
|
|
108
|
+
|
|
109
|
+
describe("resolveApprovalDecision — pure decision resolution (PR-5)", () => {
|
|
110
|
+
it("deny → deny / not granted", () => {
|
|
111
|
+
expect(resolveApprovalDecision({ kind: "deny" })).toEqual({
|
|
112
|
+
ok: true, decision: "deny", granted: false, ttl_ms: null, displayMode: "denied",
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("once → allow_once, no ttl", () => {
|
|
117
|
+
expect(resolveApprovalDecision({ kind: "once" })).toEqual({
|
|
118
|
+
ok: true, decision: "allow_once", granted: true, ttl_ms: null, displayMode: "granted once",
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("always → allow_always, no ttl", () => {
|
|
123
|
+
expect(resolveApprovalDecision({ kind: "always" })).toEqual({
|
|
124
|
+
ok: true, decision: "allow_always", granted: true, ttl_ms: null, displayMode: "granted always",
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("ttl with a valid token → allow_ttl with computed ms", () => {
|
|
129
|
+
expect(resolveApprovalDecision({ kind: "ttl", param: "24h" })).toEqual({
|
|
130
|
+
ok: true,
|
|
131
|
+
decision: "allow_ttl",
|
|
132
|
+
granted: true,
|
|
133
|
+
ttl_ms: 24 * 60 * 60 * 1000,
|
|
134
|
+
displayMode: "granted for 24h",
|
|
135
|
+
});
|
|
136
|
+
expect(resolveApprovalDecision({ kind: "ttl", param: "7d" })).toMatchObject({
|
|
137
|
+
ok: true, decision: "allow_ttl", ttl_ms: 7 * 24 * 60 * 60 * 1000,
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("ttl with a bad token → { ok: false } so the caller does NOT consume the nonce", () => {
|
|
142
|
+
// This is the load-bearing case: pre-PR-4 this branch ran after
|
|
143
|
+
// approvalConsume() burned the single-use nonce, wedging the agent.
|
|
144
|
+
// It must report failure WITHOUT side effects (pure fn → caller
|
|
145
|
+
// returns before approvalConsume).
|
|
146
|
+
for (const param of ["bogus", "0h", "1w", "", "h", "12"]) {
|
|
147
|
+
expect(resolveApprovalDecision({ kind: "ttl", param })).toEqual({
|
|
148
|
+
ok: false, error: "bad ttl token",
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
});
|