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,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UAT scenario for #1115 — `vault.broker.approvalAuth: telegram-id`
|
|
3
|
+
* single-factor approve path.
|
|
4
|
+
*
|
|
5
|
+
* **What this exercises that unit tests cannot.** The schema test, the
|
|
6
|
+
* resolver fuzz, and the source-text contracts all live inside the
|
|
7
|
+
* gateway process. They prove the wiring is shaped right. They do NOT
|
|
8
|
+
* prove that:
|
|
9
|
+
* - the *live* Telegram callback for the Approve button is routed
|
|
10
|
+
* to `handleVaultRequestAccessCallback`,
|
|
11
|
+
* - the gateway-cached `AUTO_UNLOCK_PASSPHRASE` is what the broker
|
|
12
|
+
* actually accepts,
|
|
13
|
+
* - the broker mints a real grant token end-to-end with no
|
|
14
|
+
* passphrase prompt visible in chat,
|
|
15
|
+
* - the success card carries the single-factor footer
|
|
16
|
+
* (`Approver verified by Telegram identity`).
|
|
17
|
+
*
|
|
18
|
+
* This scenario closes that gap by round-tripping a real Telegram tap
|
|
19
|
+
* against a real broker on a host configured with
|
|
20
|
+
* `vault.broker.approvalAuth: telegram-id`.
|
|
21
|
+
*
|
|
22
|
+
* Sibling: `vault-request-access-end-to-end-dm.test.ts` exercises the
|
|
23
|
+
* same agent-initiated path under the DEFAULT `passphrase` posture
|
|
24
|
+
* (two-factor). The two scenarios together pin both rungs of the
|
|
25
|
+
* posture matrix.
|
|
26
|
+
*
|
|
27
|
+
* **Skipped by default.** To unskip:
|
|
28
|
+
*
|
|
29
|
+
* 1. Standard UAT preflight (`uat/SETUP.md` §5-6) — test-harness agent
|
|
30
|
+
* live, driver session auth'd, env vars set.
|
|
31
|
+
*
|
|
32
|
+
* 2. **Host posture flipped to single-factor.** Edit `switchroom.yaml`:
|
|
33
|
+
*
|
|
34
|
+
* vault:
|
|
35
|
+
* broker:
|
|
36
|
+
* autoUnlock: true
|
|
37
|
+
* approvalAuth: telegram-id
|
|
38
|
+
*
|
|
39
|
+
* Then `switchroom update` (or `apply` + restart gateway). The
|
|
40
|
+
* scenario refuses to run if `switchroom doctor` reports the
|
|
41
|
+
* passphrase posture — running it under passphrase mode would
|
|
42
|
+
* block on a passphrase prompt the scenario doesn't send.
|
|
43
|
+
*
|
|
44
|
+
* 3. **Sacrificial vault key.** Same convention as the sibling:
|
|
45
|
+
*
|
|
46
|
+
* TMPF=$(mktemp) && printf '%s' 'sentinel-1115-value' > "$TMPF" && \
|
|
47
|
+
* switchroom vault set uat/req-access-target-tid --file "$TMPF" \
|
|
48
|
+
* --format string ; shred -u "$TMPF"
|
|
49
|
+
*
|
|
50
|
+
* 4. Remove `describe.skip` below.
|
|
51
|
+
*
|
|
52
|
+
* Why skipped: (a) mutates host vault state (mints a 30-day grant on
|
|
53
|
+
* test-harness), (b) requires the operator to flip the live posture
|
|
54
|
+
* — opt-in only. Cleanup is operator-side
|
|
55
|
+
* (`switchroom vault revoke <grant-id>` after the run, then revert
|
|
56
|
+
* the posture if desired).
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
import { describe, expect, it } from "vitest";
|
|
60
|
+
import { spinUp } from "../harness.js";
|
|
61
|
+
|
|
62
|
+
const SENTINEL_VALUE = "sentinel-1115-value";
|
|
63
|
+
const TARGET_KEY = "uat/req-access-target-tid";
|
|
64
|
+
|
|
65
|
+
describe.skip("uat: vault_request_access — telegram-id (single-factor) posture (#1115)", () => {
|
|
66
|
+
it(
|
|
67
|
+
"agent calls tool → operator taps Approve → silent mint, no passphrase prompt, single-factor footer present",
|
|
68
|
+
async () => {
|
|
69
|
+
const sc = await spinUp({ agent: "test-harness" });
|
|
70
|
+
try {
|
|
71
|
+
// 1. Trigger the agent-side MCP tool call. The agent's reply
|
|
72
|
+
// is what emits the approval card.
|
|
73
|
+
await sc.sendDM(
|
|
74
|
+
`Please call your vault_request_access MCP tool with ` +
|
|
75
|
+
`key="${TARGET_KEY}", scope="read", reason="UAT regression for #1115 ` +
|
|
76
|
+
`(telegram-id single-factor posture)". Then attempt to read the key ` +
|
|
77
|
+
`once the operator confirms.`,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// 2. Wait for the bot's approval card. Anchor on the headline
|
|
81
|
+
// emoji + tool-specific copy — same as the passphrase
|
|
82
|
+
// sibling scenario, since the card itself is identical
|
|
83
|
+
// regardless of posture (it's the Approve-tap behaviour
|
|
84
|
+
// that diverges).
|
|
85
|
+
const card = await sc.expectMessage(/🔐.*wants vault access/, {
|
|
86
|
+
from: "bot",
|
|
87
|
+
timeout: 60_000,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// 3. Confirm the inline keyboard has the Approve button and
|
|
91
|
+
// locate its callback_data.
|
|
92
|
+
const kb = await sc.driver.getKeyboard(sc.botUserId, card.messageId);
|
|
93
|
+
expect(kb).not.toBeNull();
|
|
94
|
+
const approveButton = kb!
|
|
95
|
+
.flat()
|
|
96
|
+
.find((b) => b.callbackData !== undefined && /approve/i.test(b.text));
|
|
97
|
+
expect(approveButton, "card should have an [✅ Approve] button").toBeDefined();
|
|
98
|
+
|
|
99
|
+
// 4. Tap Approve. Under `telegram-id` posture this MUST take
|
|
100
|
+
// us straight to the "Approved by @ … — minting…" state —
|
|
101
|
+
// no passphrase prompt should appear at any point.
|
|
102
|
+
await sc.driver.pressButton(
|
|
103
|
+
sc.botUserId,
|
|
104
|
+
card.messageId,
|
|
105
|
+
approveButton!.callbackData!,
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// 5. Expect the immediate "minting" edit (with operator
|
|
109
|
+
// @username) — the load-bearing UX signal that the
|
|
110
|
+
// passphrase prompt was skipped.
|
|
111
|
+
await sc.expectMessage(/Approved by @.*minting/i, {
|
|
112
|
+
from: "bot",
|
|
113
|
+
timeout: 15_000,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// 6. Expect the success card with the SINGLE-FACTOR footer —
|
|
117
|
+
// `performVaultAccessApproval` annotates the card with
|
|
118
|
+
// "Approver verified by Telegram identity (broker
|
|
119
|
+
// auto-unlocked at startup)" only under telegram-id mode.
|
|
120
|
+
// If posture flipped silently to passphrase between steps
|
|
121
|
+
// 1 and 5, the footer wouldn't say this and the regex
|
|
122
|
+
// misses → the scenario fails with a posture-state
|
|
123
|
+
// diagnosis.
|
|
124
|
+
const granted = await sc.expectMessage(
|
|
125
|
+
/Granted.*read access[\s\S]*Approver verified by Telegram identity/i,
|
|
126
|
+
{ from: "bot", timeout: 30_000 },
|
|
127
|
+
);
|
|
128
|
+
expect(granted.text).toMatch(/broker auto-unlocked at startup/i);
|
|
129
|
+
|
|
130
|
+
// 7. **Negative invariant** — *implicit*. The scenario sends
|
|
131
|
+
// no passphrase between steps 4 (tap) and 6 (Granted). If
|
|
132
|
+
// the gateway had actually fallen back to the passphrase
|
|
133
|
+
// branch (`Reply with your passphrase` prompt), the flow
|
|
134
|
+
// would stall waiting for an operator reply and the
|
|
135
|
+
// `expectMessage(/Granted .../)` in step 6 would time out.
|
|
136
|
+
// The single-factor-footer regex in step 6 is the
|
|
137
|
+
// explicit positive gate that we landed in the
|
|
138
|
+
// telegram-id branch and not, say, an unlocked-cache
|
|
139
|
+
// shortcut.
|
|
140
|
+
|
|
141
|
+
// 8. Load-bearing functional assertion: the freshly-minted
|
|
142
|
+
// grant actually works. Mirrors the sibling scenario's
|
|
143
|
+
// final assertion so a future regression that breaks
|
|
144
|
+
// the token-write path is caught here too.
|
|
145
|
+
await sc.sendDM(
|
|
146
|
+
`Now run: switchroom vault get ${TARGET_KEY} — and tell me ` +
|
|
147
|
+
`exactly what the command printed (including any error markers).`,
|
|
148
|
+
);
|
|
149
|
+
const replyAfterGet = await sc.expectMessage(
|
|
150
|
+
new RegExp(SENTINEL_VALUE),
|
|
151
|
+
{ from: "bot", timeout: 60_000 },
|
|
152
|
+
);
|
|
153
|
+
expect(replyAfterGet.text).toContain(SENTINEL_VALUE);
|
|
154
|
+
expect(replyAfterGet.text).not.toMatch(/VAULT-BROKER-DENIED/);
|
|
155
|
+
} finally {
|
|
156
|
+
await sc.tearDown();
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
300_000,
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
it(
|
|
163
|
+
"doctor reports the single-factor posture so operators can verify the host before merging",
|
|
164
|
+
async () => {
|
|
165
|
+
// This second-tier check exists to flush out the failure mode
|
|
166
|
+
// where the YAML lands `approvalAuth: telegram-id` but the
|
|
167
|
+
// gateway either didn't reload or the schema-validation gate
|
|
168
|
+
// silently rejected it. Without this check, scenario 1 still
|
|
169
|
+
// passes when the gateway has reverted to passphrase posture
|
|
170
|
+
// (the operator would just be looking at a separate gateway
|
|
171
|
+
// process state and we'd never know).
|
|
172
|
+
const sc = await spinUp({ agent: "test-harness" });
|
|
173
|
+
try {
|
|
174
|
+
await sc.sendDM(
|
|
175
|
+
`Run: switchroom doctor — and quote exactly the line containing ` +
|
|
176
|
+
`"Approval auth:".`,
|
|
177
|
+
);
|
|
178
|
+
const doctorReply = await sc.expectMessage(
|
|
179
|
+
/Approval auth:\s*telegram-id/i,
|
|
180
|
+
{ from: "bot", timeout: 60_000 },
|
|
181
|
+
);
|
|
182
|
+
expect(doctorReply.text).toMatch(
|
|
183
|
+
/single-factor.*Telegram account security/i,
|
|
184
|
+
);
|
|
185
|
+
} finally {
|
|
186
|
+
await sc.tearDown();
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
120_000,
|
|
190
|
+
);
|
|
191
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vault UX scenario — operator DMs `/vault audit`, taps `[Allow]`
|
|
3
|
+
* on a recent denial, agent re-attempts the vault read and succeeds.
|
|
4
|
+
*
|
|
5
|
+
* Part of: https://github.com/switchroom/switchroom/issues/866
|
|
6
|
+
* Exercises: #969 P2b one-tap allow flow.
|
|
7
|
+
*
|
|
8
|
+
* **Gated by operator setup.** To unskip:
|
|
9
|
+
*
|
|
10
|
+
* 1. **Driver must be admin on `test-harness`.** `agent add
|
|
11
|
+
* --allow-from $DRIVER_UID` already includes the driver in
|
|
12
|
+
* `access.json:allowFrom`. Confirm the `/vault` commands also
|
|
13
|
+
* work — they may require an explicit `admin_chat_id` setting
|
|
14
|
+
* in the agent's switchroom.yaml. Look for
|
|
15
|
+
* "VAULT-AUDIT-FORBIDDEN" or similar in the gateway log when
|
|
16
|
+
* the driver DMs `/vault audit`.
|
|
17
|
+
*
|
|
18
|
+
* 2. **Sacrificial vault keys for the test.** The scenario writes
|
|
19
|
+
* and reads `uat-test-denial` under a deliberately empty
|
|
20
|
+
* `--allow` scope so the agent's first read fails with
|
|
21
|
+
* VAULT-BROKER-DENIED — that denial is what shows up in
|
|
22
|
+
* `/vault audit`. Pre-create the key on the host:
|
|
23
|
+
*
|
|
24
|
+
* ```bash
|
|
25
|
+
* TMPF=$(mktemp) && printf '%s' 'sentinel-uat-value' > "$TMPF" && \
|
|
26
|
+
* switchroom vault set uat-test-denial --file "$TMPF" \
|
|
27
|
+
* --format string ; shred -u "$TMPF"
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* The scenario then triggers a denial, taps Allow (scopes the
|
|
31
|
+
* key to `test-harness`), and asserts the agent can now read it.
|
|
32
|
+
* Cleanup at the end is operator-side: re-narrow the scope or
|
|
33
|
+
* remove the key.
|
|
34
|
+
*
|
|
35
|
+
* 3. Remove the `describe.skip` below.
|
|
36
|
+
*
|
|
37
|
+
* Why skipped by default: the scenario mutates host vault state
|
|
38
|
+
* (broker ACL) — opt-in only.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import { describe, expect, it } from "vitest";
|
|
42
|
+
import { spinUp } from "../harness.js";
|
|
43
|
+
|
|
44
|
+
describe.skip("uat: /vault audit one-tap allow", () => {
|
|
45
|
+
it("driver taps [Allow] on a recent denial; agent's next read succeeds", async () => {
|
|
46
|
+
const sc = await spinUp({ agent: "test-harness" });
|
|
47
|
+
try {
|
|
48
|
+
// 1. Trigger a denial by asking the agent to read a key the
|
|
49
|
+
// agent isn't yet scoped for.
|
|
50
|
+
await sc.sendDM(
|
|
51
|
+
"Please run `switchroom vault get uat-test-denial` and tell me the value.",
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// The bot's denial trace finishes the turn — wait for the
|
|
55
|
+
// turn-end message, then proceed to /vault audit.
|
|
56
|
+
await sc.expectMessage(/VAULT-BROKER-DENIED|denied|cannot/i, {
|
|
57
|
+
from: "bot",
|
|
58
|
+
timeout: 60_000,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// 2. DM /vault audit. The bot replies with a recent-denials
|
|
62
|
+
// summary + inline [Allow] / [Deny] buttons per denied key.
|
|
63
|
+
await sc.sendDM("/vault audit");
|
|
64
|
+
const auditCard = await sc.expectMessage(/Recent denials|uat-test-denial/, {
|
|
65
|
+
from: "bot",
|
|
66
|
+
timeout: 30_000,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// 3. Find and press the [Allow] button.
|
|
70
|
+
const kb = await sc.driver.getKeyboard(sc.botUserId, auditCard.messageId);
|
|
71
|
+
expect(kb).not.toBeNull();
|
|
72
|
+
const allowButton = kb!
|
|
73
|
+
.flat()
|
|
74
|
+
.find(
|
|
75
|
+
(b) =>
|
|
76
|
+
b.callbackData !== undefined &&
|
|
77
|
+
/allow/i.test(b.text) &&
|
|
78
|
+
b.callbackData.includes("uat-test-denial"),
|
|
79
|
+
);
|
|
80
|
+
expect(allowButton).toBeDefined();
|
|
81
|
+
await sc.driver.pressButton(
|
|
82
|
+
sc.botUserId,
|
|
83
|
+
auditCard.messageId,
|
|
84
|
+
allowButton!.callbackData!,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// 4. Confirmation comes back via card edit; assert it lands.
|
|
88
|
+
// (The gateway typically edits the audit card in-place to
|
|
89
|
+
// show "allowed" status.)
|
|
90
|
+
await sc.expectMessage(/allowed|✓|scope updated/i, {
|
|
91
|
+
from: "bot",
|
|
92
|
+
timeout: 15_000,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// 5. Re-attempt the read. Should now succeed.
|
|
96
|
+
await sc.sendDM(
|
|
97
|
+
"Try `switchroom vault get uat-test-denial` again now.",
|
|
98
|
+
);
|
|
99
|
+
const success = await sc.expectMessage(/sentinel-uat-value/, {
|
|
100
|
+
from: "bot",
|
|
101
|
+
timeout: 60_000,
|
|
102
|
+
});
|
|
103
|
+
expect(success.text).toContain("sentinel-uat-value");
|
|
104
|
+
} finally {
|
|
105
|
+
await sc.tearDown();
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-end UAT scenario for #1052 — agent auto-resumes its task
|
|
3
|
+
* after operator approves a vault_request_access card.
|
|
4
|
+
*
|
|
5
|
+
* Pre-fix: agent fired vault_request_access → ended its turn → operator
|
|
6
|
+
* approved later → grant minted → agent did nothing further until
|
|
7
|
+
* operator messaged again. Operator had to manually nudge the agent
|
|
8
|
+
* to resume work the agent itself had flagged.
|
|
9
|
+
*
|
|
10
|
+
* Fix: gateway injects a synthetic InboundMessage after successful
|
|
11
|
+
* mint (via the existing inject_inbound IPC primitive cron uses).
|
|
12
|
+
* Agent's bridge receives the channel event, starts a new turn, and
|
|
13
|
+
* resumes the task.
|
|
14
|
+
*
|
|
15
|
+
* Load-bearing assertion: after the operator's passphrase reply +
|
|
16
|
+
* "Granted" card edit, the DRIVER sees a NEW bot turn (a substantive
|
|
17
|
+
* reply that uses the just-granted key) WITHOUT the driver sending
|
|
18
|
+
* any further message.
|
|
19
|
+
*
|
|
20
|
+
* **Skipped by default.** To unskip:
|
|
21
|
+
*
|
|
22
|
+
* 1. Standard UAT preflight (`uat/SETUP.md` §5-6).
|
|
23
|
+
* 2. `TELEGRAM_UAT_VAULT_PASSPHRASE` set in env.
|
|
24
|
+
* 3. Pre-create a sacrificial vault key:
|
|
25
|
+
*
|
|
26
|
+
* ```bash
|
|
27
|
+
* TMPF=$(mktemp) && printf '%s' 'sentinel-1052-value' > "$TMPF" && \
|
|
28
|
+
* switchroom vault set uat/auto-resume-key --file "$TMPF" \
|
|
29
|
+
* --format string ; shred -u "$TMPF"
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* 4. Remove `describe.skip` below.
|
|
33
|
+
*
|
|
34
|
+
* Why skipped: mutates vault state. Cleanup is operator-side post-run.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import { describe, expect, it } from "vitest";
|
|
38
|
+
import { spinUp } from "../harness.js";
|
|
39
|
+
|
|
40
|
+
const KEY = "uat/auto-resume-key";
|
|
41
|
+
const SENTINEL = "sentinel-1052-value";
|
|
42
|
+
|
|
43
|
+
describe.skip("uat: agent auto-resumes after vault grant approval (#1052)", () => {
|
|
44
|
+
it(
|
|
45
|
+
"agent fires card, operator approves, agent emits new turn WITHOUT a nudge",
|
|
46
|
+
async () => {
|
|
47
|
+
const passphrase = process.env.TELEGRAM_UAT_VAULT_PASSPHRASE;
|
|
48
|
+
if (!passphrase) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
"TELEGRAM_UAT_VAULT_PASSPHRASE must be set in env (see uat/SETUP.md).",
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
const sc = await spinUp({ agent: "test-harness" });
|
|
54
|
+
try {
|
|
55
|
+
// 1. Ask the agent to fetch the key — it'll hit DENIED first,
|
|
56
|
+
// fire vault_request_access, then end its turn.
|
|
57
|
+
await sc.sendDM(
|
|
58
|
+
`Please run \`switchroom vault get ${KEY}\`. If you get ` +
|
|
59
|
+
`VAULT-BROKER-DENIED, call your vault_request_access MCP tool ` +
|
|
60
|
+
`for that key (read, 30d, reason "UAT for #1052 auto-resume"), ` +
|
|
61
|
+
`END YOUR TURN cleanly, and when the operator approves you should ` +
|
|
62
|
+
`automatically resume by re-running the vault get and reporting ` +
|
|
63
|
+
`the value.`,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// 2. Wait for the approval card.
|
|
67
|
+
const card = await sc.expectMessage(/🔐.*wants vault access/, {
|
|
68
|
+
from: "bot",
|
|
69
|
+
timeout: 120_000,
|
|
70
|
+
});
|
|
71
|
+
const kb = await sc.driver.getKeyboard(sc.botUserId, card.messageId);
|
|
72
|
+
const approveButton = kb!
|
|
73
|
+
.flat()
|
|
74
|
+
.find((b) => b.callbackData !== undefined && /approve/i.test(b.text));
|
|
75
|
+
expect(approveButton).toBeDefined();
|
|
76
|
+
|
|
77
|
+
// 3. Tap Approve. Triggers passphrase prompt.
|
|
78
|
+
await sc.driver.pressButton(sc.botUserId, card.messageId, approveButton!.callbackData!);
|
|
79
|
+
await sc.expectMessage(/Vault is locked.*Reply with your passphrase/, {
|
|
80
|
+
from: "bot",
|
|
81
|
+
timeout: 15_000,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// 4. Send passphrase. Card edits to "Granted ...".
|
|
85
|
+
const lastDriverMsg = await sc.sendDM(passphrase);
|
|
86
|
+
const lastDriverMsgId = lastDriverMsg.messageId;
|
|
87
|
+
await sc.expectMessage(/Granted.*read access/, {
|
|
88
|
+
from: "bot",
|
|
89
|
+
timeout: 30_000,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// 5. THE LOAD-BEARING #1052 ASSERTION: the agent should
|
|
93
|
+
// auto-resume WITHOUT the driver sending another message.
|
|
94
|
+
// We wait for a substantive bot reply containing the
|
|
95
|
+
// sentinel value, OR matching the auto-resume channel
|
|
96
|
+
// source marker the gateway stamps.
|
|
97
|
+
//
|
|
98
|
+
// Pre-fix: this assertion times out (no synthetic
|
|
99
|
+
// injection → agent's bridge never received a new turn
|
|
100
|
+
// → agent stayed idle).
|
|
101
|
+
//
|
|
102
|
+
// Post-fix: the gateway's ipcServer.sendToAgent fires a
|
|
103
|
+
// synthetic inbound with meta.source="vault_grant_approved".
|
|
104
|
+
// The agent's bridge starts a new turn, re-runs vault get,
|
|
105
|
+
// and reports the value.
|
|
106
|
+
const autoResumeReply = await sc.expectMessage(
|
|
107
|
+
new RegExp(SENTINEL),
|
|
108
|
+
{ from: "bot", timeout: 180_000 },
|
|
109
|
+
);
|
|
110
|
+
expect(autoResumeReply.text).toContain(SENTINEL);
|
|
111
|
+
expect(
|
|
112
|
+
autoResumeReply.messageId,
|
|
113
|
+
"auto-resume reply must be a NEW message, not the granted-card edit",
|
|
114
|
+
).toBeGreaterThan(lastDriverMsgId);
|
|
115
|
+
} finally {
|
|
116
|
+
await sc.tearDown();
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
420_000,
|
|
120
|
+
);
|
|
121
|
+
});
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-end UAT scenario for the #1051 fix — concurrent
|
|
3
|
+
* vault_request_access cards must BOTH end up readable by the agent.
|
|
4
|
+
*
|
|
5
|
+
* #1051 had two failure modes:
|
|
6
|
+
* (a) `.vault-token` overwrite — each new grant strands the prior
|
|
7
|
+
* (agent can read only the most-recently-approved key).
|
|
8
|
+
* (b) pending-op race — second Approve tap before first passphrase
|
|
9
|
+
* reply orphans the first stage entirely (no second grant
|
|
10
|
+
* even minted).
|
|
11
|
+
*
|
|
12
|
+
* Both fixed by the gateway-side grant-union path: list existing
|
|
13
|
+
* grants → union keys → mint a consolidated grant. PLUS the
|
|
14
|
+
* pending-op shape extended to a queue so concurrent taps don't
|
|
15
|
+
* overwrite.
|
|
16
|
+
*
|
|
17
|
+
* This scenario covers (a) — the most-likely real-world repro
|
|
18
|
+
* (gymbro fires two cards back-to-back; operator approves both
|
|
19
|
+
* sequentially). Covering (b) cleanly needs precise tap-timing
|
|
20
|
+
* that's harder to script — the static-source test
|
|
21
|
+
* `telegram-plugin/tests/vault-grant-union.test.ts` pins it at the
|
|
22
|
+
* code level instead.
|
|
23
|
+
*
|
|
24
|
+
* **Skipped by default.** To unskip:
|
|
25
|
+
*
|
|
26
|
+
* 1. Standard UAT preflight (`uat/SETUP.md` §5-6).
|
|
27
|
+
* 2. `TELEGRAM_UAT_VAULT_PASSPHRASE` set in env.
|
|
28
|
+
* 3. Pre-create two sacrificial vault keys:
|
|
29
|
+
*
|
|
30
|
+
* ```bash
|
|
31
|
+
* for k in uat/concurrent-a uat/concurrent-b ; do
|
|
32
|
+
* TMPF=$(mktemp); printf '%s' "sentinel-$(basename "$k")" > "$TMPF"
|
|
33
|
+
* switchroom vault set "$k" --file "$TMPF" --format string
|
|
34
|
+
* shred -u "$TMPF"
|
|
35
|
+
* done
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* 4. Remove `describe.skip` below.
|
|
39
|
+
*
|
|
40
|
+
* Why skipped: mutates vault state (mints a grant covering both keys
|
|
41
|
+
* on test-harness). Cleanup is operator-side post-run.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import { describe, expect, it } from "vitest";
|
|
45
|
+
import { spinUp } from "../harness.js";
|
|
46
|
+
|
|
47
|
+
const KEY_A = "uat/concurrent-a";
|
|
48
|
+
const KEY_B = "uat/concurrent-b";
|
|
49
|
+
const SENTINEL_A = "sentinel-concurrent-a";
|
|
50
|
+
const SENTINEL_B = "sentinel-concurrent-b";
|
|
51
|
+
|
|
52
|
+
describe.skip("uat: concurrent vault_request_access approvals — both grants survive (#1051)", () => {
|
|
53
|
+
it(
|
|
54
|
+
"agent fires two cards back-to-back, operator approves both → agent can read both keys",
|
|
55
|
+
async () => {
|
|
56
|
+
const passphrase = process.env.TELEGRAM_UAT_VAULT_PASSPHRASE;
|
|
57
|
+
if (!passphrase) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
"TELEGRAM_UAT_VAULT_PASSPHRASE must be set in env (see uat/SETUP.md).",
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
const sc = await spinUp({ agent: "test-harness" });
|
|
63
|
+
try {
|
|
64
|
+
// 1. Tell the agent to fire TWO vault_request_access calls
|
|
65
|
+
// in the same turn for two distinct keys. The natural way
|
|
66
|
+
// to express this is one prompt that describes both
|
|
67
|
+
// needs; the agent then makes both tool calls.
|
|
68
|
+
await sc.sendDM(
|
|
69
|
+
`Please call your vault_request_access MCP tool TWICE — ` +
|
|
70
|
+
`once for key="${KEY_A}" and once for key="${KEY_B}" — ` +
|
|
71
|
+
`both scope="read", both reason="UAT for #1051 concurrent". ` +
|
|
72
|
+
`Then attempt to read BOTH keys via switchroom vault get and ` +
|
|
73
|
+
`print the values in your reply.`,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
// 2. Wait for the FIRST approval card.
|
|
77
|
+
const cardA = await sc.expectMessage(
|
|
78
|
+
new RegExp(`🔐.*wants vault access[\\s\\S]*${KEY_A.replace("/", "\\/")}`),
|
|
79
|
+
{ from: "bot", timeout: 90_000 },
|
|
80
|
+
);
|
|
81
|
+
const cardB = await sc.expectMessage(
|
|
82
|
+
new RegExp(`🔐.*wants vault access[\\s\\S]*${KEY_B.replace("/", "\\/")}`),
|
|
83
|
+
{ from: "bot", timeout: 30_000 },
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// 3. Tap Approve on card A.
|
|
87
|
+
const kbA = await sc.driver.getKeyboard(sc.botUserId, cardA.messageId);
|
|
88
|
+
const approveA = kbA!
|
|
89
|
+
.flat()
|
|
90
|
+
.find((b) => b.callbackData !== undefined && /approve/i.test(b.text));
|
|
91
|
+
expect(approveA).toBeDefined();
|
|
92
|
+
await sc.driver.pressButton(sc.botUserId, cardA.messageId, approveA!.callbackData!);
|
|
93
|
+
|
|
94
|
+
// Wait for the passphrase prompt on card A.
|
|
95
|
+
await sc.expectMessage(/Vault is locked.*Reply with your passphrase/, {
|
|
96
|
+
from: "bot",
|
|
97
|
+
timeout: 15_000,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// 4. Tap Approve on card B BEFORE typing the passphrase.
|
|
101
|
+
// This exercises the pending-op queueing path (bug B).
|
|
102
|
+
// The card should edit to "Queued behind an earlier card."
|
|
103
|
+
const kbB = await sc.driver.getKeyboard(sc.botUserId, cardB.messageId);
|
|
104
|
+
const approveB = kbB!
|
|
105
|
+
.flat()
|
|
106
|
+
.find((b) => b.callbackData !== undefined && /approve/i.test(b.text));
|
|
107
|
+
expect(approveB).toBeDefined();
|
|
108
|
+
await sc.driver.pressButton(sc.botUserId, cardB.messageId, approveB!.callbackData!);
|
|
109
|
+
await sc.expectMessage(/Queued behind an earlier card|Queued.*one passphrase/i, {
|
|
110
|
+
from: "bot",
|
|
111
|
+
timeout: 15_000,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// 5. Send passphrase. Gateway drains the queue, mints a
|
|
115
|
+
// unioned grant for {KEY_A, KEY_B}, writes a single
|
|
116
|
+
// .vault-token. BOTH cards edit to "Granted".
|
|
117
|
+
await sc.sendDM(passphrase);
|
|
118
|
+
await sc.expectMessage(new RegExp(`Granted[\\s\\S]*${KEY_A.replace("/", "\\/")}`), {
|
|
119
|
+
from: "bot",
|
|
120
|
+
timeout: 30_000,
|
|
121
|
+
});
|
|
122
|
+
await sc.expectMessage(new RegExp(`Granted[\\s\\S]*${KEY_B.replace("/", "\\/")}`), {
|
|
123
|
+
from: "bot",
|
|
124
|
+
timeout: 30_000,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// 6. Ask the agent to read BOTH keys. The load-bearing
|
|
128
|
+
// assertion: pre-fix, the agent could only read ONE
|
|
129
|
+
// (.vault-token had been overwritten with the second
|
|
130
|
+
// grant's token, which only covered KEY_B). Post-fix,
|
|
131
|
+
// the agent has a single token whose grant covers
|
|
132
|
+
// BOTH keys, so BOTH gets succeed.
|
|
133
|
+
await sc.sendDM(
|
|
134
|
+
`Now run: switchroom vault get ${KEY_A} && switchroom vault get ${KEY_B} ` +
|
|
135
|
+
`— and paste the output verbatim. Include any error markers if either fails.`,
|
|
136
|
+
);
|
|
137
|
+
const finalReply = await sc.expectMessage(
|
|
138
|
+
new RegExp(SENTINEL_A),
|
|
139
|
+
{ from: "bot", timeout: 90_000 },
|
|
140
|
+
);
|
|
141
|
+
expect(
|
|
142
|
+
finalReply.text,
|
|
143
|
+
`agent must read KEY_A — pre-fix this was the SECOND grant's key and would have succeeded; the union now covers it`,
|
|
144
|
+
).toContain(SENTINEL_A);
|
|
145
|
+
expect(
|
|
146
|
+
finalReply.text,
|
|
147
|
+
`agent must read KEY_B — pre-fix this would have FAILED with VAULT-BROKER-DENIED because .vault-token was overwritten`,
|
|
148
|
+
).toContain(SENTINEL_B);
|
|
149
|
+
expect(
|
|
150
|
+
finalReply.text,
|
|
151
|
+
`neither key should denied`,
|
|
152
|
+
).not.toMatch(/VAULT-BROKER-DENIED/);
|
|
153
|
+
} finally {
|
|
154
|
+
await sc.tearDown();
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
420_000, // 7 min — covers two cards rendering + two approves +
|
|
158
|
+
// passphrase round-trip + drain queue + two
|
|
159
|
+
// mints + two vault gets.
|
|
160
|
+
);
|
|
161
|
+
});
|