switchroom 0.7.15 → 0.10.0
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 +51 -59
- package/bin/run-hook.sh +27 -11
- package/bin/timezone-hook.sh +9 -7
- package/dist/agent-scheduler/index.js +410 -133
- package/dist/auth-broker/index.js +13932 -0
- package/dist/cli/switchroom.js +26937 -5601
- package/dist/host-control/main.js +12702 -0
- package/dist/vault/approvals/kernel-server.js +467 -184
- package/dist/vault/broker/server.js +1430 -724
- package/examples/minimal.yaml +63 -0
- package/examples/personal-google-workspace-mcp/.env.example +34 -0
- package/examples/personal-google-workspace-mcp/README.md +194 -0
- package/examples/personal-google-workspace-mcp/compose.yaml +66 -0
- package/examples/switchroom.yaml +220 -0
- package/package.json +7 -4
- package/profiles/_base/settings.json.hbs +20 -5
- package/profiles/_base/start.sh.hbs +16 -3
- package/profiles/_shared/agent-self-service.md.hbs +126 -0
- package/profiles/_shared/telegram-style.md.hbs +20 -90
- package/profiles/_shared/vault-protocol.md.hbs +68 -0
- package/profiles/default/CLAUDE.md +50 -96
- package/profiles/default/CLAUDE.md.hbs +36 -6
- package/profiles/default/workspace/SOUL.md.hbs +12 -5
- package/skills/buildkite-agent-infrastructure/SKILL.md +30 -11
- package/skills/buildkite-agent-runtime/SKILL.md +44 -11
- package/skills/buildkite-api/SKILL.md +31 -8
- package/skills/buildkite-cli/SKILL.md +27 -9
- package/skills/buildkite-migration/SKILL.md +22 -9
- package/skills/buildkite-pipelines/SKILL.md +26 -9
- package/skills/buildkite-secure-delivery/SKILL.md +23 -9
- package/skills/buildkite-test-engine/SKILL.md +25 -8
- package/skills/docx/SKILL.md +1 -1
- package/skills/docx/scripts/office/validators/__pycache__/__init__.cpython-313.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/base.cpython-313.pyc +0 -0
- package/skills/file-bug/SKILL.md +34 -6
- package/skills/humanizer/SKILL.md +15 -0
- package/skills/humanizer-calibrate/SKILL.md +7 -1
- package/skills/mcp-builder/SKILL.md +1 -1
- package/skills/pdf/SKILL.md +1 -1
- package/skills/pptx/SKILL.md +1 -1
- package/skills/skill-creator/SKILL.md +21 -1
- package/skills/skill-creator/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/generate_report.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/improve_description.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/run_eval.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/run_loop.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/utils.cpython-313.pyc +0 -0
- package/skills/switchroom-cli/SKILL.md +63 -64
- package/skills/switchroom-health/SKILL.md +23 -10
- package/skills/switchroom-install/SKILL.md +3 -3
- package/skills/switchroom-manage/SKILL.md +26 -19
- package/skills/switchroom-runtime/SKILL.md +191 -0
- package/skills/switchroom-status/SKILL.md +27 -2
- package/skills/telegram-test-harness/SKILL.md +3 -0
- package/skills/token-helpers/SKILL.md +24 -1
- package/skills/webapp-testing/SKILL.md +31 -1
- package/skills/xlsx/SKILL.md +1 -1
- package/telegram-plugin/admin-commands/index.ts +7 -5
- package/telegram-plugin/analytics-posthog.ts +191 -0
- package/telegram-plugin/bridge/bridge.ts +69 -0
- package/telegram-plugin/bridge/ipc-client.ts +4 -1
- package/telegram-plugin/dist/bridge/bridge.js +194 -119
- package/telegram-plugin/dist/gateway/gateway.js +23611 -19671
- package/telegram-plugin/dist/server.js +245 -189
- package/telegram-plugin/first-paint.ts +3 -24
- package/telegram-plugin/gateway/auth-add-flow.ts +326 -0
- package/telegram-plugin/gateway/auth-broker-client.ts +75 -0
- package/telegram-plugin/gateway/auth-command.ts +794 -0
- package/telegram-plugin/gateway/auth-line.ts +123 -0
- package/telegram-plugin/gateway/boot-card.ts +169 -40
- package/telegram-plugin/gateway/boot-issue-cache.ts +308 -0
- package/telegram-plugin/gateway/boot-probes.ts +166 -123
- package/telegram-plugin/gateway/boot-reason.ts +41 -7
- package/telegram-plugin/gateway/boot-version.ts +66 -0
- package/telegram-plugin/gateway/gateway.ts +3499 -1885
- package/telegram-plugin/gateway/hostd-dispatch.ts +117 -0
- package/telegram-plugin/gateway/ipc-protocol.ts +18 -0
- package/telegram-plugin/gateway/pending-inbound-buffer.ts +106 -0
- package/telegram-plugin/gateway/quarantine.ts +69 -0
- package/telegram-plugin/gateway/quota-cache.ts +9 -4
- package/telegram-plugin/gateway/reaction-trigger.ts +401 -0
- package/telegram-plugin/gateway/recent-denials.test.ts +103 -0
- package/telegram-plugin/gateway/recent-denials.ts +77 -0
- package/telegram-plugin/gateway/startup-network-retry.ts +109 -31
- package/telegram-plugin/gateway/vault-grant-inbound-builders.ts +125 -0
- package/telegram-plugin/history.ts +91 -0
- package/telegram-plugin/hooks/hooks.json +10 -0
- package/telegram-plugin/hooks/sandbox-hint-posttool.mjs +130 -0
- package/telegram-plugin/hooks/subagent-tracker-posttool.mjs +19 -2
- package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +22 -2
- package/telegram-plugin/hooks/tool-label-pretool.mjs +11 -0
- package/telegram-plugin/hooks/wedge-detect-posttool.mjs +303 -0
- package/telegram-plugin/inbound-classifier.ts +50 -0
- package/telegram-plugin/inline-keyboard-callbacks.ts +136 -0
- package/telegram-plugin/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/telegram-plugin/package.json +4 -2
- package/telegram-plugin/permission-rule.ts +51 -0
- package/telegram-plugin/permission-title.ts +56 -0
- package/telegram-plugin/quota-check.ts +19 -41
- package/telegram-plugin/registry/reaper.ts +223 -0
- package/telegram-plugin/retry-api-call.ts +80 -0
- package/telegram-plugin/runtime-metrics.ts +177 -0
- package/telegram-plugin/scripts/build.mjs +0 -1
- package/telegram-plugin/secret-detect/index.ts +24 -0
- package/telegram-plugin/secret-detect/vault-error.test.ts +64 -12
- package/telegram-plugin/secret-detect/vault-error.ts +78 -11
- package/telegram-plugin/secret-detect/vault-write.ts +14 -2
- package/telegram-plugin/server.js +41795 -0
- package/telegram-plugin/session-tail.ts +6 -1
- package/telegram-plugin/shared/bot-runtime.ts +5 -4
- package/telegram-plugin/silence-poke.ts +420 -0
- package/telegram-plugin/silent-end.ts +174 -0
- package/telegram-plugin/stream-controller.ts +13 -0
- package/telegram-plugin/stream-reply-handler.ts +7 -0
- package/telegram-plugin/subagent-watcher.ts +213 -4
- package/telegram-plugin/tests/auth-add-flow.test.ts +559 -0
- package/telegram-plugin/tests/auth-code-redact.test.ts +8 -4
- package/telegram-plugin/tests/auth-command-vernacular.test.ts +531 -0
- package/telegram-plugin/tests/boot-card-issue-dedup.test.ts +247 -0
- package/telegram-plugin/tests/boot-card-reason-to-render.test.ts +182 -0
- package/telegram-plugin/tests/boot-card-reason.test.ts +65 -2
- package/telegram-plugin/tests/boot-card-render.test.ts +146 -0
- package/telegram-plugin/tests/boot-card-silent-on-operator.test.ts +103 -0
- package/telegram-plugin/tests/boot-probes.test.ts +216 -10
- package/telegram-plugin/tests/boot-version-string.test.ts +0 -0
- package/telegram-plugin/tests/finalize-callback.test.ts +190 -0
- package/telegram-plugin/tests/gateway-message-validator.test.ts +26 -0
- package/telegram-plugin/tests/gateway-secret-detect.test.ts +12 -3
- package/telegram-plugin/tests/gateway-startup-network-retry.test.ts +104 -0
- package/telegram-plugin/tests/history-reaper.test.ts +378 -0
- package/telegram-plugin/tests/hostd-dispatch.test.ts +129 -0
- package/telegram-plugin/tests/inbound-classifier.test.ts +76 -0
- package/telegram-plugin/tests/inbound-message-types.test.ts +267 -0
- package/telegram-plugin/tests/issues-card.test.ts +49 -0
- package/telegram-plugin/tests/pending-inbound-buffer.test.ts +132 -0
- package/telegram-plugin/tests/permission-rule.test.ts +80 -1
- package/telegram-plugin/tests/permission-title.test.ts +31 -0
- package/telegram-plugin/tests/quota-check.test.ts +5 -35
- package/telegram-plugin/tests/races.test.ts +179 -0
- package/telegram-plugin/tests/reaction-trigger-flow.test.ts +353 -0
- package/telegram-plugin/tests/reaction-trigger.test.ts +397 -0
- package/telegram-plugin/tests/retry-api-call.test.ts +152 -1
- package/telegram-plugin/tests/runtime-metrics.test.ts +145 -0
- package/telegram-plugin/tests/sandbox-hint-posttool.test.ts +155 -0
- package/telegram-plugin/tests/secret-detect-delete-must-surface-failures.test.ts +133 -0
- package/telegram-plugin/tests/secret-detect-false-positives.test.ts +137 -0
- package/telegram-plugin/tests/silence-poke.test.ts +493 -0
- package/telegram-plugin/tests/silent-end.test.ts +206 -0
- package/telegram-plugin/tests/subagent-tracker-hooks.test.ts +107 -0
- package/telegram-plugin/tests/subagent-watcher-env-thresholds.test.ts +224 -0
- package/telegram-plugin/tests/subagent-watcher-stall-terminal.test.ts +316 -0
- package/telegram-plugin/tests/subagent-watcher.test.ts +263 -0
- package/telegram-plugin/tests/turn-signal-tracker.test.ts +81 -0
- package/telegram-plugin/tests/vault-approval-posture.test.ts +256 -0
- package/telegram-plugin/tests/vault-grant-auto-resume.test.ts +73 -0
- package/telegram-plugin/tests/vault-grant-inbound-builders.test.ts +226 -0
- package/telegram-plugin/tests/vault-grant-union.test.ts +130 -0
- package/telegram-plugin/tests/vault-key-regex-allows-slash.test.ts +140 -0
- package/telegram-plugin/tests/vault-posture-quarantine.test.ts +104 -0
- package/telegram-plugin/tests/vault-request-access-tool.test.ts +114 -0
- package/telegram-plugin/tests/vault-request-access-unlock-resume.test.ts +106 -0
- package/telegram-plugin/turn-signal-tracker.ts +100 -24
- package/telegram-plugin/uat/SETUP.md +210 -35
- package/telegram-plugin/uat/assertions.ts +264 -37
- package/telegram-plugin/uat/driver-info.ts +57 -0
- package/telegram-plugin/uat/driver.ts +590 -51
- package/telegram-plugin/uat/harness.ts +140 -94
- package/telegram-plugin/uat/load-env.test.ts +72 -0
- package/telegram-plugin/uat/load-env.ts +48 -0
- package/telegram-plugin/uat/login.ts +96 -53
- package/telegram-plugin/uat/runners/agent-self-sufficiency.ts +457 -0
- package/telegram-plugin/uat/runners/paraphrases.ts +231 -0
- package/telegram-plugin/uat/runners/report.ts +150 -0
- package/telegram-plugin/uat/runners/run-agent-self-sufficiency.sh +50 -0
- package/telegram-plugin/uat/runners/scorer.test.ts +196 -0
- package/telegram-plugin/uat/runners/scorer.ts +106 -0
- package/telegram-plugin/uat/runners/skill-coverage.test.ts +100 -0
- package/telegram-plugin/uat/runners/skill-coverage.ts +620 -0
- package/telegram-plugin/uat/scenarios/ask-user-button-tap-dm.test.ts +141 -0
- package/telegram-plugin/uat/scenarios/bg-sub-agent-dispatch-dm.test.ts +191 -0
- package/telegram-plugin/uat/scenarios/fuzz-extended-dm.test.ts +255 -0
- package/telegram-plugin/uat/scenarios/fuzz-human-style-dm.test.ts +275 -0
- package/telegram-plugin/uat/scenarios/fuzz-random-prompts-dm.test.ts +146 -0
- package/telegram-plugin/uat/scenarios/fuzz-status-ask-dm.test.ts +486 -0
- package/telegram-plugin/uat/scenarios/jtbd-interrupt-marker-dm.test.ts +67 -0
- package/telegram-plugin/uat/scenarios/jtbd-rapid-followup-dm.test.ts +100 -0
- package/telegram-plugin/uat/scenarios/jtbd-soft-commit-dm.test.ts +67 -0
- package/telegram-plugin/uat/scenarios/jtbd-status-query-dm.test.ts +49 -0
- package/telegram-plugin/uat/scenarios/location-inbound-dm.test.ts +65 -0
- package/telegram-plugin/uat/scenarios/midturn-silent-dm.test.ts +175 -0
- package/telegram-plugin/uat/scenarios/reactions-dm.test.ts +142 -0
- package/telegram-plugin/uat/scenarios/reactions-trigger-turn-dm.test.ts +96 -0
- package/telegram-plugin/uat/scenarios/secret-redaction-deletes-original-dm.test.ts +123 -0
- package/telegram-plugin/uat/scenarios/secret-redaction-no-false-positive-dm.test.ts +87 -0
- package/telegram-plugin/uat/scenarios/silence-poke-soft-dm.test.ts +155 -0
- package/telegram-plugin/uat/scenarios/silent-end-recovery-dm.test.ts +95 -0
- package/telegram-plugin/uat/scenarios/smoke-dm-reply.test.ts +57 -0
- package/telegram-plugin/uat/scenarios/subagent-watcher-no-rerun-dm.test.ts +135 -0
- package/telegram-plugin/uat/scenarios/vault-approval-posture-telegram-id-dm.test.ts +191 -0
- package/telegram-plugin/uat/scenarios/vault-audit-allow-dm.test.ts +108 -0
- package/telegram-plugin/uat/scenarios/vault-grant-auto-resume-dm.test.ts +121 -0
- package/telegram-plugin/uat/scenarios/vault-request-access-concurrent-dm.test.ts +161 -0
- package/telegram-plugin/uat/scenarios/vault-request-access-end-to-end-dm.test.ts +158 -0
- package/telegram-plugin/uat/scenarios/voice-inbound-dm.test.ts +65 -0
- package/telegram-plugin/vault-approval-posture.ts +42 -0
- package/telegram-plugin/welcome-text.ts +1 -0
- package/telegram-plugin/active-pins-sweep.ts +0 -204
- package/telegram-plugin/active-pins.ts +0 -146
- package/telegram-plugin/auth-dashboard.ts +0 -1104
- package/telegram-plugin/auth-slot-parser.ts +0 -497
- package/telegram-plugin/card-event-log.ts +0 -138
- package/telegram-plugin/dist/foreman/foreman.js +0 -31106
- package/telegram-plugin/docs/multi-agent-card-design.md +0 -847
- package/telegram-plugin/docs/pinned-progress-card-reliability.md +0 -144
- package/telegram-plugin/foreman/foreman-create-flow.ts +0 -202
- package/telegram-plugin/foreman/foreman-handlers.ts +0 -493
- package/telegram-plugin/foreman/foreman.ts +0 -1165
- package/telegram-plugin/foreman/setup-flow.ts +0 -345
- package/telegram-plugin/foreman/setup-state.ts +0 -239
- package/telegram-plugin/foreman/state.ts +0 -203
- package/telegram-plugin/pin-event-log.ts +0 -76
- package/telegram-plugin/progress-card-driver.ts +0 -2886
- package/telegram-plugin/progress-card-pin-manager.ts +0 -589
- package/telegram-plugin/progress-card-pin-watchdog.ts +0 -98
- package/telegram-plugin/progress-card.ts +0 -1409
- package/telegram-plugin/tests/HARNESS.md +0 -340
- package/telegram-plugin/tests/_progress-card-harness.ts +0 -109
- package/telegram-plugin/tests/active-pins-boot-reaper.test.ts +0 -211
- package/telegram-plugin/tests/active-pins-sweep.test.ts +0 -309
- package/telegram-plugin/tests/active-pins.test.ts +0 -187
- package/telegram-plugin/tests/auth-account-identity-surface.test.ts +0 -118
- package/telegram-plugin/tests/auth-dashboard-edge-cases.test.ts +0 -260
- package/telegram-plugin/tests/auth-dashboard-restart-flow.test.ts +0 -140
- package/telegram-plugin/tests/auth-dashboard-v3b.test.ts +0 -559
- package/telegram-plugin/tests/auth-dashboard.test.ts +0 -1045
- package/telegram-plugin/tests/auth-slot-commands.test.ts +0 -640
- package/telegram-plugin/tests/bg-agent-progress-card-757.test.ts +0 -201
- package/telegram-plugin/tests/boot-card-account-quota.test.ts +0 -137
- package/telegram-plugin/tests/card-event-log.test.ts +0 -145
- package/telegram-plugin/tests/first-paint.test.ts +0 -257
- package/telegram-plugin/tests/foreman-create-flow.test.ts +0 -359
- package/telegram-plugin/tests/foreman-handlers.test.ts +0 -347
- package/telegram-plugin/tests/foreman-state.test.ts +0 -164
- package/telegram-plugin/tests/foreman-write-ops.test.ts +0 -214
- package/telegram-plugin/tests/harness-ordering-invariants.test.ts +0 -243
- package/telegram-plugin/tests/pin-event-log.test.ts +0 -124
- package/telegram-plugin/tests/progress-card-api-failure-during-deferred.test.ts +0 -73
- package/telegram-plugin/tests/progress-card-close-paths-converge.test.ts +0 -272
- package/telegram-plugin/tests/progress-card-cross-turn.test.ts +0 -258
- package/telegram-plugin/tests/progress-card-delay-842.test.ts +0 -160
- package/telegram-plugin/tests/progress-card-dispose-preservepending.test.ts +0 -81
- package/telegram-plugin/tests/progress-card-draft-flag.test.ts +0 -80
- package/telegram-plugin/tests/progress-card-driver-eviction.test.ts +0 -215
- package/telegram-plugin/tests/progress-card-driver-fleet-shadow.test.ts +0 -123
- package/telegram-plugin/tests/progress-card-driver-force-complete-parent-done.test.ts +0 -76
- package/telegram-plugin/tests/progress-card-edit-timestamps-budget.test.ts +0 -62
- package/telegram-plugin/tests/progress-card-memory-bounds.test.ts +0 -84
- package/telegram-plugin/tests/progress-card-pin-failure-paths.test.ts +0 -139
- package/telegram-plugin/tests/progress-card-pin-manager.test.ts +0 -773
- package/telegram-plugin/tests/progress-card-pin-race-fast-turn.test.ts +0 -66
- package/telegram-plugin/tests/progress-card-pin-sidecar-partial-write.test.ts +0 -64
- package/telegram-plugin/tests/progress-card-pin-watchdog.test.ts +0 -190
- package/telegram-plugin/tests/progress-card-sigterm-pin-flush.test.ts +0 -146
- package/telegram-plugin/tests/real-gateway-f1-ladder-integrity.test.ts +0 -123
- package/telegram-plugin/tests/real-gateway-f2-instant-draft.test.ts +0 -82
- package/telegram-plugin/tests/real-gateway-f3-late-card.test.ts +0 -114
- package/telegram-plugin/tests/real-gateway-harness.ts +0 -699
- package/telegram-plugin/tests/real-gateway-i6-turn-flush-replay-dedup.test.ts +0 -313
- package/telegram-plugin/tests/real-gateway-ipc-lifecycle.test.ts +0 -299
- package/telegram-plugin/tests/real-gateway-spec.test.ts +0 -487
- package/telegram-plugin/tests/real-gateway.smoke.test.ts +0 -101
- package/telegram-plugin/tests/setup-flow.test.ts +0 -510
- package/telegram-plugin/tests/setup-state.test.ts +0 -146
- package/telegram-plugin/tests/sync-chat-running-subagents.test.ts +0 -116
- package/telegram-plugin/tests/turn-end-regressions.test.ts +0 -489
- package/telegram-plugin/tests/turn-flush-card-takeover.test.ts +0 -218
- package/telegram-plugin/tests/turn-flush-prose-recovery.test.ts +0 -78
- package/telegram-plugin/tests/two-zone-bg-carry-full-lifecycle.test.ts +0 -131
- package/telegram-plugin/tests/two-zone-bg-detection.test.ts +0 -120
- package/telegram-plugin/tests/two-zone-bg-done-when-all-terminal.test.ts +0 -116
- package/telegram-plugin/tests/two-zone-bg-early-turn-end.test.ts +0 -87
- package/telegram-plugin/tests/two-zone-bg-survives-next-turn.test.ts +0 -211
- package/telegram-plugin/tests/two-zone-card-cap.test.ts +0 -62
- package/telegram-plugin/tests/two-zone-card-fleet-row.test.ts +0 -101
- package/telegram-plugin/tests/two-zone-card-header-phases.test.ts +0 -78
- package/telegram-plugin/tests/two-zone-card-html-balance.test.ts +0 -110
- package/telegram-plugin/tests/two-zone-card-lifecycle.test.ts +0 -128
- package/telegram-plugin/tests/two-zone-card-sanitise.test.ts +0 -58
- package/telegram-plugin/tests/two-zone-card-snapshot.test.ts +0 -133
- package/telegram-plugin/tests/two-zone-concurrent-turns-isolation.test.ts +0 -155
- package/telegram-plugin/tests/two-zone-phasefor-precedence.test.ts +0 -117
- package/telegram-plugin/tests/two-zone-snapshot-extras.test.ts +0 -187
- package/telegram-plugin/tests/two-zone-stuck-edit-throttle.test.ts +0 -149
- package/telegram-plugin/tests/two-zone-stuck-header-escalation.test.ts +0 -101
- package/telegram-plugin/tests/two-zone-stuck-per-member.test.ts +0 -114
- package/telegram-plugin/tests/two-zone-stuck-recovery.test.ts +0 -105
- package/telegram-plugin/tests/waiting-ux-harness.ts +0 -381
- package/telegram-plugin/tests/waiting-ux.e2e.test.ts +0 -233
- package/telegram-plugin/turn-flush-prose-recovery.ts +0 -40
- package/telegram-plugin/two-zone-card.ts +0 -269
- package/telegram-plugin/uat/scenarios/smoke-clerk-reply.test.ts +0 -61
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract pins for the `vault.broker.approvalAuth` posture toggle and
|
|
3
|
+
* its #1115 follow-up — broker-mediated attestation.
|
|
4
|
+
*
|
|
5
|
+
* Posture summary:
|
|
6
|
+
* - `passphrase` (default): Approve on a grant card prompts the
|
|
7
|
+
* operator for the vault passphrase. Two-factor (Telegram ID +
|
|
8
|
+
* passphrase). Gateway holds the passphrase only briefly after
|
|
9
|
+
* operator typing.
|
|
10
|
+
* - `telegram-id` (opt-in): Approve mints immediately. The gateway
|
|
11
|
+
* signals operator-tap intent to the broker via
|
|
12
|
+
* `attest_via_posture: true` on the mint_grant call; the broker
|
|
13
|
+
* uses its OWN retained passphrase internally. Single-factor;
|
|
14
|
+
* passphrase never leaves the broker process.
|
|
15
|
+
*
|
|
16
|
+
* Load-bearing invariants pinned here:
|
|
17
|
+
* 1. The resolver returns the posture mode and nothing else — the
|
|
18
|
+
* gateway no longer holds the passphrase in memory under
|
|
19
|
+
* telegram-id (the #1115 first-cut and #1115-follow-up-v1
|
|
20
|
+
* designs did, and the reviewer flagged it as a bypass).
|
|
21
|
+
* 2. The allowlist check is the FIRST gate in every vault callback
|
|
22
|
+
* handler — no posture branching, no mint, no
|
|
23
|
+
* `pendingVaultOps.set` runs before it.
|
|
24
|
+
* 3. handleVaultDeferCallback and handleVaultRequestSaveCallback
|
|
25
|
+
* under telegram-id NO LONGER short-circuit on an in-memory
|
|
26
|
+
* passphrase — they fall through to the standard
|
|
27
|
+
* cached-passphrase path (#1115 follow-up cleanup; the original
|
|
28
|
+
* shortcut was the same bypass class as the access-approve
|
|
29
|
+
* one).
|
|
30
|
+
* 4. handleVaultRequestAccessCallback under telegram-id calls
|
|
31
|
+
* `performVaultAccessApproval` with `{ kind: 'posture' }` — the
|
|
32
|
+
* attestation type that drives `attest_via_posture: true` on
|
|
33
|
+
* mint_grant.
|
|
34
|
+
* 5. `handleVaultGrantCallback` (operator-initiated wizard) NEVER
|
|
35
|
+
* references `VAULT_APPROVAL_AUTH_MODE` — flipping posture
|
|
36
|
+
* cannot change wizard behaviour.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { describe, expect, it } from 'vitest'
|
|
40
|
+
import { readFileSync } from 'node:fs'
|
|
41
|
+
import { resolve } from 'node:path'
|
|
42
|
+
import { resolveVaultApprovalPosture } from '../vault-approval-posture.js'
|
|
43
|
+
|
|
44
|
+
const gatewaySrc = readFileSync(
|
|
45
|
+
resolve(__dirname, '..', 'gateway', 'gateway.ts'),
|
|
46
|
+
'utf-8',
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
function sliceAccessApproveBlock(): string {
|
|
50
|
+
const fn =
|
|
51
|
+
gatewaySrc.split('async function handleVaultRequestAccessCallback')[1]?.split('async function')[0] ?? ''
|
|
52
|
+
return fn.split("if (action === 'approve')")[1]?.split("await ctx.answerCallbackQuery({ text: 'Unknown action'")[0] ?? ''
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe('vault grant approval posture — module-level wiring', () => {
|
|
56
|
+
it('declares the posture mode holder; does NOT hold the passphrase in memory', () => {
|
|
57
|
+
expect(gatewaySrc).toMatch(/let VAULT_APPROVAL_AUTH_MODE:\s*['"]passphrase['"]\s*\|\s*['"]telegram-id['"]/)
|
|
58
|
+
// Regression guard: post-#1115-follow-up the gateway must NOT
|
|
59
|
+
// declare AUTO_UNLOCK_PASSPHRASE — the passphrase stays in the
|
|
60
|
+
// broker. If a future change reintroduces this variable, the
|
|
61
|
+
// reviewer's "agent can self-mint" bypass returns.
|
|
62
|
+
expect(gatewaySrc).not.toMatch(/let AUTO_UNLOCK_PASSPHRASE/)
|
|
63
|
+
expect(gatewaySrc).not.toMatch(/AUTO_UNLOCK_PASSPHRASE\s*=/)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('initialises posture from switchroom config at startup', () => {
|
|
67
|
+
expect(gatewaySrc).toMatch(/function initVaultApprovalPosture/)
|
|
68
|
+
expect(gatewaySrc).toMatch(/initVaultApprovalPosture\(\)/)
|
|
69
|
+
expect(gatewaySrc).toMatch(/resolveVaultApprovalPosture\(/)
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
describe('handleVaultRequestAccessCallback — posture branch', () => {
|
|
74
|
+
it('mints via posture attestation (NOT in-memory passphrase) when posture is telegram-id', () => {
|
|
75
|
+
const approveBlock = sliceAccessApproveBlock()
|
|
76
|
+
expect(approveBlock).toMatch(/VAULT_APPROVAL_AUTH_MODE === ['"]telegram-id['"]/)
|
|
77
|
+
// Pinned: the call shape MUST be `{ kind: 'posture' }`. If the
|
|
78
|
+
// gateway ever reverts to passing a real passphrase here, the
|
|
79
|
+
// bypass surface returns.
|
|
80
|
+
expect(approveBlock).toMatch(/performVaultAccessApproval\(ctx, pending, stageId, senderId, \{ kind: ['"]posture['"] \}\)/)
|
|
81
|
+
expect(approveBlock).toMatch(/Approved by @/)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('preserves the allowlist guard regardless of posture', () => {
|
|
85
|
+
const handlerBlock =
|
|
86
|
+
gatewaySrc.split('async function handleVaultRequestAccessCallback')[1]?.split('async function')[0] ?? ''
|
|
87
|
+
expect(handlerBlock).toMatch(/if \(!access\.allowFrom\.includes\(senderId\)\)/)
|
|
88
|
+
expect(handlerBlock).toMatch(/Not authorized/)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('passphrase-mode branch unchanged: cache lookup + prompt still present', () => {
|
|
92
|
+
const approveBlock = sliceAccessApproveBlock()
|
|
93
|
+
expect(approveBlock).toMatch(/vaultPassphraseCache\.get\(pending\.chat_id\)/)
|
|
94
|
+
expect(approveBlock).toMatch(/Reply with your passphrase/i)
|
|
95
|
+
expect(approveBlock).toMatch(/passphrase-for-access-approve/)
|
|
96
|
+
// Pinned: the queued-drain path passes the typed passphrase via
|
|
97
|
+
// the new attestation shape `{ kind: 'passphrase', passphrase }`.
|
|
98
|
+
expect(gatewaySrc).toMatch(/performVaultAccessApproval\(ctx, stagedAccess, item\.stageId, item\.senderId, \{ kind: ['"]passphrase['"], passphrase \}\)/)
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
describe('performVaultAccessApproval — broker-mediated attestation', () => {
|
|
103
|
+
it('builds brokerAuthOpts from the AccessApprovalAttestation discriminator', () => {
|
|
104
|
+
const fnBlock =
|
|
105
|
+
gatewaySrc
|
|
106
|
+
.split('async function performVaultAccessApproval')[1]
|
|
107
|
+
?.split('async function handleVaultRequestAccessCallback')[0] ?? ''
|
|
108
|
+
// Pinned: the passphrase variant feeds the broker passphrase
|
|
109
|
+
// attestation; the posture variant feeds `attest_via_posture:
|
|
110
|
+
// true`. NOT a free-form union — the discriminator is what
|
|
111
|
+
// makes the call shapes type-safe.
|
|
112
|
+
expect(fnBlock).toMatch(/attestation\.kind === ['"]passphrase['"]/)
|
|
113
|
+
expect(fnBlock).toMatch(/attest_via_posture: true/)
|
|
114
|
+
expect(fnBlock).toMatch(/passphrase: attestation\.passphrase/)
|
|
115
|
+
// Pinned: the same brokerAuthOpts threads both
|
|
116
|
+
// listGrantsViaBroker (for #1051 grant-union) AND
|
|
117
|
+
// mintGrantViaBroker. If only one is wired, the union path
|
|
118
|
+
// silently re-strands the prior token under telegram-id.
|
|
119
|
+
expect(fnBlock).toMatch(/listGrantsViaBroker\(pending\.agent, brokerAuthOpts\)/)
|
|
120
|
+
expect(fnBlock).toMatch(/\.\.\.brokerAuthOpts/)
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
describe('handleVaultRequestSaveCallback — telegram-id silent path withdrawn', () => {
|
|
125
|
+
it('NO LONGER reads an in-memory passphrase for telegram-id; falls through to cached-passphrase path', () => {
|
|
126
|
+
const fnBlock =
|
|
127
|
+
gatewaySrc
|
|
128
|
+
.split('async function handleVaultRequestSaveCallback')[1]
|
|
129
|
+
?.split('async function handleVaultDeferCallback')[0] ?? ''
|
|
130
|
+
// Regression guard: the original PR added a
|
|
131
|
+
// `VAULT_APPROVAL_AUTH_MODE === 'telegram-id'` shortcut that
|
|
132
|
+
// pulled an in-memory passphrase. That was a bypass surface.
|
|
133
|
+
// The save handler must NOT branch on the posture for an
|
|
134
|
+
// in-memory passphrase any more.
|
|
135
|
+
expect(fnBlock).not.toMatch(/AUTO_UNLOCK_PASSPHRASE/)
|
|
136
|
+
// Standard cache lookup still present.
|
|
137
|
+
expect(fnBlock).toMatch(/vaultPassphraseCache\.get\(pending\.chat_id\)/)
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
describe('handleVaultDeferCallback — telegram-id silent path withdrawn', () => {
|
|
142
|
+
it('NO LONGER reads an in-memory passphrase for telegram-id; falls through to cached-passphrase path', () => {
|
|
143
|
+
const fnBlock =
|
|
144
|
+
gatewaySrc
|
|
145
|
+
.split('async function handleVaultDeferCallback')[1]
|
|
146
|
+
?.split('\nasync function ')[0] ?? ''
|
|
147
|
+
expect(fnBlock).not.toMatch(/AUTO_UNLOCK_PASSPHRASE/)
|
|
148
|
+
// Cached-passphrase path still present.
|
|
149
|
+
const unlockBranch = fnBlock.split("if (action === 'unlock')")[1] ?? ''
|
|
150
|
+
expect(unlockBranch).toMatch(/vaultPassphraseCache\.get\(/)
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
describe('handleVaultGrantCallback (wizard) — posture cannot affect wizard', () => {
|
|
155
|
+
it('wizard handler never references VAULT_APPROVAL_AUTH_MODE — posture flips are inert here', () => {
|
|
156
|
+
const fnBlock =
|
|
157
|
+
gatewaySrc
|
|
158
|
+
.split('async function handleVaultGrantCallback')[1]
|
|
159
|
+
?.split('\nasync function ')[0] ?? ''
|
|
160
|
+
expect(fnBlock).not.toMatch(/VAULT_APPROVAL_AUTH_MODE/)
|
|
161
|
+
expect(fnBlock).not.toMatch(/AUTO_UNLOCK_PASSPHRASE/)
|
|
162
|
+
expect(fnBlock).not.toMatch(/attest_via_posture/)
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
describe('allowlist is the first gate in every vault callback handler', () => {
|
|
167
|
+
function handlerBody(name: string): string {
|
|
168
|
+
const after = gatewaySrc.split(`async function ${name}(`)[1] ?? ''
|
|
169
|
+
return after.split('\nasync function ')[0] ?? ''
|
|
170
|
+
}
|
|
171
|
+
for (const handler of [
|
|
172
|
+
'handleVaultRequestAccessCallback',
|
|
173
|
+
'handleVaultRequestSaveCallback',
|
|
174
|
+
'handleVaultDeferCallback',
|
|
175
|
+
'handleVaultGrantCallback',
|
|
176
|
+
]) {
|
|
177
|
+
it(`${handler}: allowlist check fires before any other branching`, () => {
|
|
178
|
+
const body = handlerBody(handler)
|
|
179
|
+
expect(body).not.toBe('')
|
|
180
|
+
const idx = body.indexOf('if (')
|
|
181
|
+
const firstGuard = body.slice(idx, body.indexOf('\n', body.indexOf('\n', idx) + 1))
|
|
182
|
+
expect(firstGuard).toMatch(/access\.allowFrom\.includes\(senderId\)/)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it(`${handler}: no callback side-effect appears before the allowlist check`, () => {
|
|
186
|
+
const body = handlerBody(handler)
|
|
187
|
+
const beforeAllowlist = body.split('access.allowFrom.includes(senderId)')[0] ?? body
|
|
188
|
+
for (const sentinel of [
|
|
189
|
+
'mintGrantViaBroker',
|
|
190
|
+
'performVaultAccessApproval',
|
|
191
|
+
'pendingVaultOps.set',
|
|
192
|
+
"VAULT_APPROVAL_AUTH_MODE === 'telegram-id'",
|
|
193
|
+
'attest_via_posture',
|
|
194
|
+
]) {
|
|
195
|
+
expect(
|
|
196
|
+
beforeAllowlist.includes(sentinel),
|
|
197
|
+
`${handler}: sentinel "${sentinel}" must NOT appear before the allowlist check`,
|
|
198
|
+
).toBe(false)
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
describe('resolveVaultApprovalPosture — runtime behaviour', () => {
|
|
205
|
+
it('passphrase posture when approvalAuth is absent', () => {
|
|
206
|
+
expect(resolveVaultApprovalPosture(undefined)).toEqual({ mode: 'passphrase' })
|
|
207
|
+
expect(resolveVaultApprovalPosture({})).toEqual({ mode: 'passphrase' })
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('passphrase posture when approvalAuth is explicitly passphrase', () => {
|
|
211
|
+
expect(resolveVaultApprovalPosture({ approvalAuth: 'passphrase' })).toEqual({ mode: 'passphrase' })
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('telegram-id posture when approvalAuth is telegram-id', () => {
|
|
215
|
+
expect(resolveVaultApprovalPosture({ approvalAuth: 'telegram-id' })).toEqual({ mode: 'telegram-id' })
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('defence-in-depth: unknown approvalAuth values fall back to passphrase (schema rejects them, but trust nothing)', () => {
|
|
219
|
+
expect(resolveVaultApprovalPosture({ approvalAuth: 'TELEGRAM-ID' })).toEqual({ mode: 'passphrase' })
|
|
220
|
+
expect(resolveVaultApprovalPosture({ approvalAuth: 'telegram_id' })).toEqual({ mode: 'passphrase' })
|
|
221
|
+
expect(resolveVaultApprovalPosture({ approvalAuth: '' })).toEqual({ mode: 'passphrase' })
|
|
222
|
+
expect(resolveVaultApprovalPosture({ approvalAuth: 'nonsense' })).toEqual({ mode: 'passphrase' })
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('adversarial fuzz: 200 random inputs never return a non-passphrase, non-telegram-id mode and never throw', () => {
|
|
226
|
+
const rand = mulberry32(0xdeadbeef)
|
|
227
|
+
const choices: any[] = [
|
|
228
|
+
undefined,
|
|
229
|
+
'passphrase',
|
|
230
|
+
'telegram-id',
|
|
231
|
+
'PASSPHRASE',
|
|
232
|
+
'',
|
|
233
|
+
'nonsense',
|
|
234
|
+
null,
|
|
235
|
+
0,
|
|
236
|
+
false,
|
|
237
|
+
{ nested: 'telegram-id' },
|
|
238
|
+
]
|
|
239
|
+
for (let i = 0; i < 200; i++) {
|
|
240
|
+
const broker = { approvalAuth: choices[Math.floor(rand() * choices.length)] }
|
|
241
|
+
const result = resolveVaultApprovalPosture(broker as never)
|
|
242
|
+
expect(result.mode === 'passphrase' || result.mode === 'telegram-id', `iter ${i}: mode must be one of the two literals`).toBe(true)
|
|
243
|
+
}
|
|
244
|
+
})
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
function mulberry32(seed: number): () => number {
|
|
248
|
+
let a = seed >>> 0
|
|
249
|
+
return () => {
|
|
250
|
+
a = (a + 0x6D2B79F5) >>> 0
|
|
251
|
+
let t = a
|
|
252
|
+
t = Math.imul(t ^ (t >>> 15), t | 1)
|
|
253
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
|
|
254
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract pin for #1052 — agent auto-resumes after operator approves
|
|
3
|
+
* a vault_request_access card.
|
|
4
|
+
*
|
|
5
|
+
* Pre-fix: agent called vault_request_access → tool returned ack →
|
|
6
|
+
* agent's turn ended ("waiting for approval"). Operator approved later
|
|
7
|
+
* → grant minted → BUT the agent did nothing because its turn was
|
|
8
|
+
* already over. Operator had to send a fresh message to kick the
|
|
9
|
+
* agent back into action.
|
|
10
|
+
*
|
|
11
|
+
* Fix: after successful mint (via passphrase-attestation broker call
|
|
12
|
+
* + token-write), the gateway injects a synthetic InboundMessage into
|
|
13
|
+
* the agent's bridge via `ipcServer.sendToAgent`. The bridge sees it
|
|
14
|
+
* as a normal channel event (with meta.source="vault_grant_approved"
|
|
15
|
+
* for distinct rendering) and starts a new turn. Re-uses the existing
|
|
16
|
+
* inject_inbound primitive that cron has used since #890+.
|
|
17
|
+
*
|
|
18
|
+
* This file pins the call site so a future refactor can't quietly
|
|
19
|
+
* drop the injection.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { describe, it, expect } from "vitest";
|
|
23
|
+
import { readFileSync } from "node:fs";
|
|
24
|
+
import { resolve } from "node:path";
|
|
25
|
+
|
|
26
|
+
const gatewaySrc = readFileSync(
|
|
27
|
+
resolve(__dirname, "..", "gateway", "gateway.ts"),
|
|
28
|
+
"utf-8",
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
function extractPerformBlock(): string {
|
|
32
|
+
const start = gatewaySrc.indexOf("async function performVaultAccessApproval");
|
|
33
|
+
if (start < 0) throw new Error("performVaultAccessApproval not found");
|
|
34
|
+
const restAfter = gatewaySrc.slice(start + 1);
|
|
35
|
+
const endRel = restAfter.indexOf("\nasync function ");
|
|
36
|
+
return gatewaySrc.slice(start, start + 1 + (endRel >= 0 ? endRel : restAfter.length));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("performVaultAccessApproval injects a synthetic inbound on success (#1052)", () => {
|
|
40
|
+
const block = extractPerformBlock();
|
|
41
|
+
|
|
42
|
+
it("calls ipcServer.sendToAgent AFTER successful mint + token-write", () => {
|
|
43
|
+
// fails when: the auto-resume injection gets dropped. Pre-fix
|
|
44
|
+
// operator had to message the agent again to resume the task —
|
|
45
|
+
// the injection is the load-bearing wiring.
|
|
46
|
+
expect(block, "missing ipcServer.sendToAgent call").toMatch(/ipcServer\.sendToAgent\(/);
|
|
47
|
+
// Must run AFTER the mint-success path (i.e., after the
|
|
48
|
+
// `result.kind === 'error'` early-return guard).
|
|
49
|
+
const errorReturn = block.indexOf("result.kind === 'error'");
|
|
50
|
+
const sendIdx = block.indexOf("ipcServer.sendToAgent(");
|
|
51
|
+
expect(errorReturn).toBeGreaterThan(0);
|
|
52
|
+
expect(sendIdx, "sendToAgent must come AFTER the error-return early exit").toBeGreaterThan(errorReturn);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("delegates inbound construction to buildVaultGrantApprovedInbound", () => {
|
|
56
|
+
// PR #1168 extracted the InboundMessage literals (meta.source,
|
|
57
|
+
// user, userId, meta.{agent,key,scope,grant_id,stage_id,operator_id})
|
|
58
|
+
// into `gateway/vault-grant-inbound-builders.ts`. The shape itself
|
|
59
|
+
// is now pinned by `vault-grant-inbound-builders.test.ts` against
|
|
60
|
+
// the builder directly. What this test still pins is the call-site
|
|
61
|
+
// contract: `performVaultAccessApproval` must keep wiring to the
|
|
62
|
+
// builder — a regression that inlines or replaces the builder
|
|
63
|
+
// would silently drop the meta fields downstream filters rely on.
|
|
64
|
+
expect(block).toMatch(/buildVaultGrantApprovedInbound\(/);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("logs delivery outcome to stderr for forensics", () => {
|
|
68
|
+
// Mirrors the cron inject_inbound logging at gateway.ts:2470 so
|
|
69
|
+
// ops can confirm an injection actually delivered (vs the agent's
|
|
70
|
+
// bridge being down).
|
|
71
|
+
expect(block).toMatch(/vault_grant_approved injection[\s\S]*delivered/);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pin the InboundMessage shapes the gateway synthesizes when the
|
|
3
|
+
* operator taps Approve / Deny on a `vault_request_access` card
|
|
4
|
+
* (#1052 / #1150). A regression that drops a `meta.source` field, or
|
|
5
|
+
* changes the source string, would silently break the agent's wake-
|
|
6
|
+
* up — the bridge wouldn't recognize the source, the message would
|
|
7
|
+
* render as a generic channel event, and the model wouldn't know it
|
|
8
|
+
* was an approval response.
|
|
9
|
+
*
|
|
10
|
+
* These tests are the cheap regression guard. The wire shape is
|
|
11
|
+
* load-bearing — downstream filters / dashboards / future replay
|
|
12
|
+
* tooling may anchor on individual meta fields.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { describe, it, expect } from 'vitest'
|
|
16
|
+
import {
|
|
17
|
+
buildVaultGrantApprovedInbound,
|
|
18
|
+
buildVaultGrantDeniedInbound,
|
|
19
|
+
type VaultGrantInboundContext,
|
|
20
|
+
} from '../gateway/vault-grant-inbound-builders.js'
|
|
21
|
+
|
|
22
|
+
const FIXED_NOW = 1_700_000_000_000
|
|
23
|
+
|
|
24
|
+
const CTX_READ: VaultGrantInboundContext = {
|
|
25
|
+
agent: 'gymbro',
|
|
26
|
+
key: 'fatsecret/credentials',
|
|
27
|
+
scope: 'read',
|
|
28
|
+
chat_id: '8248703757',
|
|
29
|
+
ttl_seconds: 30 * 86400,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const CTX_WRITE: VaultGrantInboundContext = {
|
|
33
|
+
...CTX_READ,
|
|
34
|
+
key: 'analytics/api-token',
|
|
35
|
+
scope: 'write',
|
|
36
|
+
ttl_seconds: 7 * 86400,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('buildVaultGrantApprovedInbound', () => {
|
|
40
|
+
it('emits the canonical envelope (type, chat_id, user, userId, ts, messageId)', () => {
|
|
41
|
+
const msg = buildVaultGrantApprovedInbound({
|
|
42
|
+
ctx: CTX_READ,
|
|
43
|
+
grantId: 'vg_a1b2c3',
|
|
44
|
+
stageId: 'stage-001',
|
|
45
|
+
operatorId: '8248703757',
|
|
46
|
+
nowMs: FIXED_NOW,
|
|
47
|
+
})
|
|
48
|
+
expect(msg.type).toBe('inbound')
|
|
49
|
+
expect(msg.chatId).toBe('8248703757')
|
|
50
|
+
expect(msg.user).toBe('vault-broker')
|
|
51
|
+
expect(msg.userId).toBe(0)
|
|
52
|
+
expect(msg.ts).toBe(FIXED_NOW)
|
|
53
|
+
// messageId is synthetic — pin that it equals ts so the gateway
|
|
54
|
+
// can produce a stable id under fake-clock tests without colliding
|
|
55
|
+
// with real Telegram messageIds.
|
|
56
|
+
expect(msg.messageId).toBe(FIXED_NOW)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('pins meta.source = "vault_grant_approved" — load-bearing for the bridge', () => {
|
|
60
|
+
const msg = buildVaultGrantApprovedInbound({
|
|
61
|
+
ctx: CTX_READ,
|
|
62
|
+
grantId: 'vg_x',
|
|
63
|
+
stageId: 's',
|
|
64
|
+
operatorId: '1',
|
|
65
|
+
})
|
|
66
|
+
expect(msg.meta?.source).toBe('vault_grant_approved')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('carries all forensic fields in meta', () => {
|
|
70
|
+
const msg = buildVaultGrantApprovedInbound({
|
|
71
|
+
ctx: CTX_READ,
|
|
72
|
+
grantId: 'vg_a1b2c3',
|
|
73
|
+
stageId: 'stage-001',
|
|
74
|
+
operatorId: '8248703757',
|
|
75
|
+
})
|
|
76
|
+
expect(msg.meta).toEqual({
|
|
77
|
+
source: 'vault_grant_approved',
|
|
78
|
+
agent: 'gymbro',
|
|
79
|
+
key: 'fatsecret/credentials',
|
|
80
|
+
scope: 'read',
|
|
81
|
+
grant_id: 'vg_a1b2c3',
|
|
82
|
+
stage_id: 'stage-001',
|
|
83
|
+
operator_id: '8248703757',
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('text names the key, scope, ttl-in-days, and grant id', () => {
|
|
88
|
+
const msg = buildVaultGrantApprovedInbound({
|
|
89
|
+
ctx: CTX_READ,
|
|
90
|
+
grantId: 'vg_a1b2c3',
|
|
91
|
+
stageId: 's',
|
|
92
|
+
operatorId: '1',
|
|
93
|
+
})
|
|
94
|
+
expect(msg.text).toContain('approved')
|
|
95
|
+
expect(msg.text).toContain('`fatsecret/credentials`')
|
|
96
|
+
expect(msg.text).toContain('scope=read')
|
|
97
|
+
expect(msg.text).toContain('30d')
|
|
98
|
+
expect(msg.text).toContain('grant=vg_a1b2c3')
|
|
99
|
+
// Instructional: tells the agent how to recover the value.
|
|
100
|
+
expect(msg.text).toContain('switchroom vault get')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('rounds ttl_seconds to days', () => {
|
|
104
|
+
const ctx7d: VaultGrantInboundContext = { ...CTX_READ, ttl_seconds: 7 * 86400 }
|
|
105
|
+
const msg = buildVaultGrantApprovedInbound({
|
|
106
|
+
ctx: ctx7d,
|
|
107
|
+
grantId: 'vg_x',
|
|
108
|
+
stageId: 's',
|
|
109
|
+
operatorId: '1',
|
|
110
|
+
})
|
|
111
|
+
expect(msg.text).toContain('7d')
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('honors write scope in text + meta', () => {
|
|
115
|
+
const msg = buildVaultGrantApprovedInbound({
|
|
116
|
+
ctx: CTX_WRITE,
|
|
117
|
+
grantId: 'vg_x',
|
|
118
|
+
stageId: 's',
|
|
119
|
+
operatorId: '1',
|
|
120
|
+
})
|
|
121
|
+
expect(msg.text).toContain('scope=write')
|
|
122
|
+
expect(msg.meta?.scope).toBe('write')
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('defaults nowMs to Date.now() when omitted', () => {
|
|
126
|
+
const before = Date.now()
|
|
127
|
+
const msg = buildVaultGrantApprovedInbound({
|
|
128
|
+
ctx: CTX_READ,
|
|
129
|
+
grantId: 'vg_x',
|
|
130
|
+
stageId: 's',
|
|
131
|
+
operatorId: '1',
|
|
132
|
+
})
|
|
133
|
+
const after = Date.now()
|
|
134
|
+
expect(msg.ts).toBeGreaterThanOrEqual(before)
|
|
135
|
+
expect(msg.ts).toBeLessThanOrEqual(after)
|
|
136
|
+
expect(msg.messageId).toBe(msg.ts)
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
describe('buildVaultGrantDeniedInbound', () => {
|
|
141
|
+
it('pins meta.source = "vault_grant_denied" — the deny-side wake-up was added in #1156', () => {
|
|
142
|
+
const msg = buildVaultGrantDeniedInbound({
|
|
143
|
+
ctx: CTX_READ,
|
|
144
|
+
stageId: 's',
|
|
145
|
+
operatorId: '1',
|
|
146
|
+
})
|
|
147
|
+
expect(msg.meta?.source).toBe('vault_grant_denied')
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('omits grant_id (denial never mints a grant)', () => {
|
|
151
|
+
const msg = buildVaultGrantDeniedInbound({
|
|
152
|
+
ctx: CTX_READ,
|
|
153
|
+
stageId: 'stage-001',
|
|
154
|
+
operatorId: '8248703757',
|
|
155
|
+
})
|
|
156
|
+
expect(msg.meta).toEqual({
|
|
157
|
+
source: 'vault_grant_denied',
|
|
158
|
+
agent: 'gymbro',
|
|
159
|
+
key: 'fatsecret/credentials',
|
|
160
|
+
scope: 'read',
|
|
161
|
+
stage_id: 'stage-001',
|
|
162
|
+
operator_id: '8248703757',
|
|
163
|
+
})
|
|
164
|
+
expect((msg.meta as { grant_id?: string }).grant_id).toBeUndefined()
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('text steers toward a fallback path', () => {
|
|
168
|
+
const msg = buildVaultGrantDeniedInbound({
|
|
169
|
+
ctx: CTX_READ,
|
|
170
|
+
stageId: 's',
|
|
171
|
+
operatorId: '1',
|
|
172
|
+
})
|
|
173
|
+
expect(msg.text).toContain('denied')
|
|
174
|
+
expect(msg.text).toContain('`fatsecret/credentials`')
|
|
175
|
+
expect(msg.text).toContain('fallback')
|
|
176
|
+
// The "DO NOT re-request" line is load-bearing UX — prevents the
|
|
177
|
+
// model from spam-tapping a fresh request immediately after a
|
|
178
|
+
// deny. Pin it.
|
|
179
|
+
expect(msg.text).toMatch(/Do NOT re-request/)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('shares envelope shape with the approve builder (type, user, chat)', () => {
|
|
183
|
+
const denied = buildVaultGrantDeniedInbound({
|
|
184
|
+
ctx: CTX_READ,
|
|
185
|
+
stageId: 's',
|
|
186
|
+
operatorId: '1',
|
|
187
|
+
nowMs: FIXED_NOW,
|
|
188
|
+
})
|
|
189
|
+
expect(denied.type).toBe('inbound')
|
|
190
|
+
expect(denied.user).toBe('vault-broker')
|
|
191
|
+
expect(denied.userId).toBe(0)
|
|
192
|
+
expect(denied.chatId).toBe('8248703757')
|
|
193
|
+
expect(denied.ts).toBe(FIXED_NOW)
|
|
194
|
+
expect(denied.messageId).toBe(FIXED_NOW)
|
|
195
|
+
})
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
describe('approve vs deny shape invariants', () => {
|
|
199
|
+
it('both emit type=inbound, user=vault-broker — bridge anchors on these', () => {
|
|
200
|
+
const approve = buildVaultGrantApprovedInbound({
|
|
201
|
+
ctx: CTX_READ, grantId: 'g', stageId: 's', operatorId: '1',
|
|
202
|
+
})
|
|
203
|
+
const deny = buildVaultGrantDeniedInbound({
|
|
204
|
+
ctx: CTX_READ, stageId: 's', operatorId: '1',
|
|
205
|
+
})
|
|
206
|
+
for (const m of [approve, deny]) {
|
|
207
|
+
expect(m.type).toBe('inbound')
|
|
208
|
+
expect(m.user).toBe('vault-broker')
|
|
209
|
+
expect(m.userId).toBe(0)
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('source strings are disjoint — no shared substring that could route both to the same handler', () => {
|
|
214
|
+
const approve = buildVaultGrantApprovedInbound({
|
|
215
|
+
ctx: CTX_READ, grantId: 'g', stageId: 's', operatorId: '1',
|
|
216
|
+
})
|
|
217
|
+
const deny = buildVaultGrantDeniedInbound({
|
|
218
|
+
ctx: CTX_READ, stageId: 's', operatorId: '1',
|
|
219
|
+
})
|
|
220
|
+
expect(approve.meta?.source).not.toBe(deny.meta?.source)
|
|
221
|
+
// Defensive: a fuzzy match like `meta.source.includes('approve')`
|
|
222
|
+
// shouldn't accidentally fire on the deny side. Pin the prefixes.
|
|
223
|
+
expect(String(approve.meta?.source)).toMatch(/^vault_grant_approved$/)
|
|
224
|
+
expect(String(deny.meta?.source)).toMatch(/^vault_grant_denied$/)
|
|
225
|
+
})
|
|
226
|
+
})
|