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,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-end UAT scenario for the agent-initiated vault_request_access
|
|
3
|
+
* flow — closes the test-coverage gap that allowed #1053 to ship.
|
|
4
|
+
*
|
|
5
|
+
* #1053: an agent called `vault_request_access`, operator approved,
|
|
6
|
+
* passphrase was entered, broker minted a grant token and wrote it
|
|
7
|
+
* to `.vault-token` — BUT the agent's subsequent `vault get` still
|
|
8
|
+
* returned VAULT-BROKER-DENIED because the CLI's get path didn't
|
|
9
|
+
* forward the token. Every unit test passed (gateway, broker, CLI
|
|
10
|
+
* each looked right in isolation) but the integration was broken.
|
|
11
|
+
*
|
|
12
|
+
* The lesson the operator drew: "test and prevent these kinds of
|
|
13
|
+
* things using the full telegram test bot." This scenario is that
|
|
14
|
+
* test — it round-trips through real Telegram, real broker, real
|
|
15
|
+
* agent, asserting the final state (vault get succeeds) rather
|
|
16
|
+
* than any single component's contract.
|
|
17
|
+
*
|
|
18
|
+
* Sibling: `vault-audit-allow-dm.test.ts` exercises the OPERATOR-
|
|
19
|
+
* initiated path (operator opens /vault audit, taps Allow on a
|
|
20
|
+
* recent denial). This scenario exercises the AGENT-initiated path
|
|
21
|
+
* (agent calls vault_request_access, operator approves the card
|
|
22
|
+
* the agent's tool emitted) — a different gateway handler
|
|
23
|
+
* (handleVaultRequestAccessCallback vs handleVaultRecentDenialCallback)
|
|
24
|
+
* but the same broker token-writing + grant-validation backend.
|
|
25
|
+
*
|
|
26
|
+
* **Skipped by default.** To unskip:
|
|
27
|
+
*
|
|
28
|
+
* 1. Standard UAT preflight (`uat/SETUP.md` §5-6) — test-harness
|
|
29
|
+
* agent live, driver session auth'd, env vars set.
|
|
30
|
+
*
|
|
31
|
+
* 2. **Operator passphrase visibility.** The scenario must enter
|
|
32
|
+
* the operator's vault passphrase in chat as part of the
|
|
33
|
+
* approve flow. Set `TELEGRAM_UAT_VAULT_PASSPHRASE` in the
|
|
34
|
+
* env so the scenario can send it. The gateway deletes the
|
|
35
|
+
* passphrase message from chat history immediately after
|
|
36
|
+
* caching it (see `deleteSensitiveMessage` in gateway.ts) so
|
|
37
|
+
* no plaintext lingers.
|
|
38
|
+
*
|
|
39
|
+
* 3. **Sacrificial vault key.** Same convention as the
|
|
40
|
+
* `/vault audit` scenario — pre-create a key the harness can
|
|
41
|
+
* request. Suggested:
|
|
42
|
+
*
|
|
43
|
+
* ```bash
|
|
44
|
+
* TMPF=$(mktemp) && printf '%s' 'sentinel-1053-value' > "$TMPF" && \
|
|
45
|
+
* switchroom vault set uat/req-access-target --file "$TMPF" \
|
|
46
|
+
* --format string ; shred -u "$TMPF"
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* Slash-namespaced shape on purpose — also exercises #1047
|
|
50
|
+
* (vault-key regex allowing '/').
|
|
51
|
+
*
|
|
52
|
+
* 4. Remove `describe.skip` below.
|
|
53
|
+
*
|
|
54
|
+
* Why skipped: mutates host vault state (mints a 30-day grant on
|
|
55
|
+
* test-harness) — opt-in only. Cleanup is operator-side
|
|
56
|
+
* (`switchroom vault revoke <grant-id>` after the run).
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
import { describe, expect, it } from "vitest";
|
|
60
|
+
import { spinUp } from "../harness.js";
|
|
61
|
+
|
|
62
|
+
const SENTINEL_VALUE = "sentinel-1053-value";
|
|
63
|
+
const TARGET_KEY = "uat/req-access-target";
|
|
64
|
+
|
|
65
|
+
describe.skip("uat: vault_request_access end-to-end (#1053 regression)", () => {
|
|
66
|
+
it(
|
|
67
|
+
"agent calls tool → operator approves + enters passphrase → agent reads the value",
|
|
68
|
+
async () => {
|
|
69
|
+
const operatorPassphrase = process.env.TELEGRAM_UAT_VAULT_PASSPHRASE;
|
|
70
|
+
if (!operatorPassphrase) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
"TELEGRAM_UAT_VAULT_PASSPHRASE must be set in env for this scenario " +
|
|
73
|
+
"(see SETUP.md). The scenario sends it via DM as part of the approve " +
|
|
74
|
+
"flow; the gateway deletes the message immediately after caching.",
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
const sc = await spinUp({ agent: "test-harness" });
|
|
78
|
+
try {
|
|
79
|
+
// 1. Tell the agent to call the MCP tool. The agent's reply
|
|
80
|
+
// is what fires the approval card — we don't fire it
|
|
81
|
+
// from the driver side because the WHOLE POINT is to
|
|
82
|
+
// cover the agent → gateway → broker → token-file
|
|
83
|
+
// → agent path.
|
|
84
|
+
await sc.sendDM(
|
|
85
|
+
`Please call your vault_request_access MCP tool with ` +
|
|
86
|
+
`key="${TARGET_KEY}", scope="read", reason="UAT regression for #1053". ` +
|
|
87
|
+
`Then attempt to read the key once the operator confirms.`,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// 2. Wait for the bot's approval card. Anchor on the
|
|
91
|
+
// headline emoji + tool-specific copy.
|
|
92
|
+
const card = await sc.expectMessage(/🔐.*wants vault access/, {
|
|
93
|
+
from: "bot",
|
|
94
|
+
timeout: 60_000,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// 3. Confirm the card carries the right inline keyboard.
|
|
98
|
+
// Locate the [✅ Approve] button.
|
|
99
|
+
const kb = await sc.driver.getKeyboard(sc.botUserId, card.messageId);
|
|
100
|
+
expect(kb).not.toBeNull();
|
|
101
|
+
const approveButton = kb!
|
|
102
|
+
.flat()
|
|
103
|
+
.find((b) => b.callbackData !== undefined && /approve/i.test(b.text));
|
|
104
|
+
expect(approveButton, "card should have an [✅ Approve] button").toBeDefined();
|
|
105
|
+
|
|
106
|
+
// 4. Tap Approve. With no cached passphrase yet, the gateway
|
|
107
|
+
// edits the card to prompt for the passphrase as the
|
|
108
|
+
// next message (vault_request_access tap-to-unlock flow
|
|
109
|
+
// from #1012 Phase 2 / #1034).
|
|
110
|
+
await sc.driver.pressButton(
|
|
111
|
+
sc.botUserId,
|
|
112
|
+
card.messageId,
|
|
113
|
+
approveButton!.callbackData!,
|
|
114
|
+
);
|
|
115
|
+
await sc.expectMessage(/Vault is locked.*Reply with your passphrase/, {
|
|
116
|
+
from: "bot",
|
|
117
|
+
timeout: 15_000,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// 5. Send the passphrase. Gateway caches it, deletes the
|
|
121
|
+
// chat message via deleteSensitiveMessage, then auto-
|
|
122
|
+
// resumes the mint flow. Card edits to the "Granted"
|
|
123
|
+
// state when the broker accepts the attestation.
|
|
124
|
+
await sc.sendDM(operatorPassphrase);
|
|
125
|
+
await sc.expectMessage(/Granted.*read access/, {
|
|
126
|
+
from: "bot",
|
|
127
|
+
timeout: 30_000,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// 6. THE LOAD-BEARING ASSERTION FOR #1053: ask the agent
|
|
131
|
+
// to fetch the key. The agent's `switchroom vault get`
|
|
132
|
+
// MUST forward the freshly-minted token. If it doesn't
|
|
133
|
+
// (the pre-#1053-fix state), the broker denies on the
|
|
134
|
+
// peercred ACL and the agent reports VAULT-BROKER-DENIED.
|
|
135
|
+
// Post-fix: the get succeeds and returns the sentinel
|
|
136
|
+
// value the operator pre-staged.
|
|
137
|
+
await sc.sendDM(
|
|
138
|
+
`Now run: switchroom vault get ${TARGET_KEY} — and tell me ` +
|
|
139
|
+
`exactly what the command printed (including any error markers).`,
|
|
140
|
+
);
|
|
141
|
+
const replyAfterGet = await sc.expectMessage(
|
|
142
|
+
new RegExp(SENTINEL_VALUE),
|
|
143
|
+
{ from: "bot", timeout: 60_000 },
|
|
144
|
+
);
|
|
145
|
+
expect(replyAfterGet.text).toContain(SENTINEL_VALUE);
|
|
146
|
+
// The bot's reply MUST NOT contain the denial marker. This
|
|
147
|
+
// is the regression guard: a future bug that reintroduces
|
|
148
|
+
// the silent token-drop would surface VAULT-BROKER-DENIED
|
|
149
|
+
// alongside the value (or instead of it).
|
|
150
|
+
expect(replyAfterGet.text).not.toMatch(/VAULT-BROKER-DENIED/);
|
|
151
|
+
} finally {
|
|
152
|
+
await sc.tearDown();
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
300_000, // 5 min — covers card render + Approve tap + passphrase
|
|
156
|
+
// round-trip + grant mint + agent's next turn + vault get.
|
|
157
|
+
);
|
|
158
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Voice-inbound scenario — driver sends a voice note (OGG/Opus)
|
|
3
|
+
* to the test bot, gateway's `voice_in` skill transcribes it,
|
|
4
|
+
* bot replies with text.
|
|
5
|
+
*
|
|
6
|
+
* Part of: https://github.com/switchroom/switchroom/issues/866
|
|
7
|
+
*
|
|
8
|
+
* **Gated by fixture + bot config.** To unskip:
|
|
9
|
+
*
|
|
10
|
+
* 1. Generate a 1-second silent OGG/Opus fixture:
|
|
11
|
+
*
|
|
12
|
+
* ```bash
|
|
13
|
+
* mkdir -p telegram-plugin/uat/fixtures/voice
|
|
14
|
+
* ffmpeg -f lavfi -i anullsrc=r=48000:cl=mono -t 1 \
|
|
15
|
+
* -c:a libopus -b:a 32k \
|
|
16
|
+
* telegram-plugin/uat/fixtures/voice/silence-1s.opus
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* (Fixture is intentionally NOT committed to git — keep the repo
|
|
20
|
+
* light. Generate locally before unskipping.)
|
|
21
|
+
*
|
|
22
|
+
* 2. Verify the test-harness agent has `voice_in` configured. The
|
|
23
|
+
* default profile may not enable it; check `switchroom config
|
|
24
|
+
* show test-harness` for `channels.telegram.voice_in.enabled`.
|
|
25
|
+
*
|
|
26
|
+
* 3. Remove the `describe.skip` below.
|
|
27
|
+
*
|
|
28
|
+
* Why skipped by default: voice transcription costs money per call
|
|
29
|
+
* (OpenAI/Whisper) and slow turns are expected — keeping this off
|
|
30
|
+
* the default UAT path until someone explicitly tests voice.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import path from "node:path";
|
|
34
|
+
import { existsSync } from "node:fs";
|
|
35
|
+
import { describe, expect, it } from "vitest";
|
|
36
|
+
import { spinUp } from "../harness.js";
|
|
37
|
+
|
|
38
|
+
const FIXTURE = path.resolve(
|
|
39
|
+
__dirname,
|
|
40
|
+
"..",
|
|
41
|
+
"fixtures",
|
|
42
|
+
"voice",
|
|
43
|
+
"silence-1s.opus",
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
describe.skip("uat: voice-inbound DM round-trip", () => {
|
|
47
|
+
it("driver sends a voice note, bot transcribes + replies within 60s", async () => {
|
|
48
|
+
if (!existsSync(FIXTURE)) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`voice fixture not found at ${FIXTURE} — see scenario header to generate one`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
const sc = await spinUp({ agent: "test-harness" });
|
|
54
|
+
try {
|
|
55
|
+
await sc.driver.sendVoice(sc.botUserId, FIXTURE);
|
|
56
|
+
const reply = await sc.expectMessage(/.+/, {
|
|
57
|
+
from: "bot",
|
|
58
|
+
timeout: 60_000,
|
|
59
|
+
});
|
|
60
|
+
expect(reply.text.length).toBeGreaterThan(0);
|
|
61
|
+
} finally {
|
|
62
|
+
await sc.tearDown();
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the vault grant-card approval posture from switchroom config.
|
|
3
|
+
*
|
|
4
|
+
* Pre-#1115-follow-up this module also loaded the auto-unlock blob
|
|
5
|
+
* (file-based on legacy installs, broker-IPC `get_unlock_passphrase`
|
|
6
|
+
* on Docker) and surfaced the plaintext passphrase to the gateway so
|
|
7
|
+
* it could attest mint_grant calls. The reviewer flagged that as a
|
|
8
|
+
* bypass surface (claude in the same agent container could exfiltrate
|
|
9
|
+
* the passphrase via /proc or broker socket). Pivoted to broker-
|
|
10
|
+
* mediated attestation: the passphrase NEVER leaves the broker
|
|
11
|
+
* process; the gateway just signals operator-tap intent via
|
|
12
|
+
* `attest_via_posture: true` on mint_grant / list_grants.
|
|
13
|
+
*
|
|
14
|
+
* What this module does NOW: read `vault.broker.approvalAuth` from
|
|
15
|
+
* the operator's switchroom.yaml and tell the gateway whether to
|
|
16
|
+
* branch into the silent-mint code path. Nothing else.
|
|
17
|
+
*
|
|
18
|
+
* Behaviour:
|
|
19
|
+
* - `approvalAuth` absent / `passphrase` → passphrase posture.
|
|
20
|
+
* - `approvalAuth: telegram-id` → telegram-id posture (gateway will
|
|
21
|
+
* use attest_via_posture on broker calls).
|
|
22
|
+
* - `approvalAuth` set to anything else → passphrase posture (the
|
|
23
|
+
* schema rejects unknown values at startup; this is defence in
|
|
24
|
+
* depth in case the schema is bypassed).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export interface VaultBrokerPostureConfig {
|
|
28
|
+
approvalAuth?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ResolvedPosture {
|
|
32
|
+
mode: 'passphrase' | 'telegram-id'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function resolveVaultApprovalPosture(
|
|
36
|
+
broker: VaultBrokerPostureConfig | undefined,
|
|
37
|
+
): ResolvedPosture {
|
|
38
|
+
if (broker?.approvalAuth === 'telegram-id') {
|
|
39
|
+
return { mode: 'telegram-id' }
|
|
40
|
+
}
|
|
41
|
+
return { mode: 'passphrase' }
|
|
42
|
+
}
|
|
@@ -159,6 +159,7 @@ export function helpText(agentName: string): string {
|
|
|
159
159
|
``,
|
|
160
160
|
`<code>/start</code> — pairing instructions`,
|
|
161
161
|
`<code>/status</code> — agent, model, auth`,
|
|
162
|
+
`<code>/vault audit <agent></code> — admin: review agent's vault access + one-tap [🔓 Allow] on recent denials`,
|
|
162
163
|
`<code>/commands</code> — full command list`,
|
|
163
164
|
].join("\n");
|
|
164
165
|
}
|
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Sweep logic for the active-pins sidecar.
|
|
3
|
-
*
|
|
4
|
-
* The sidecar (see `active-pins.ts`) records every progress-card
|
|
5
|
-
* message the bot has pinned but not yet unpinned. Two lifecycle
|
|
6
|
-
* events consume it:
|
|
7
|
-
*
|
|
8
|
-
* 1. Startup — when a new bot process boots, it sweeps any entries
|
|
9
|
-
* left over from a prior session that crashed or was killed
|
|
10
|
-
* mid-turn. Without this, the pins stay on Telegram forever
|
|
11
|
-
* because the in-memory map that tracks them died with the old
|
|
12
|
-
* process.
|
|
13
|
-
*
|
|
14
|
-
* 2. Pre-restart — when the /restart, /reconcile --restart, or
|
|
15
|
-
* /update commands fire a self-restart, the bot proactively
|
|
16
|
-
* unpins any still-pinned cards before it gets SIGTERM'd. This
|
|
17
|
-
* avoids a ~1s window where the restart ack is visible in chat
|
|
18
|
-
* but the previous turn's progress card is still pinned.
|
|
19
|
-
*
|
|
20
|
-
* Both consumers call `sweepActivePins`, which is shaped as a pure
|
|
21
|
-
* function that takes the unpin callback as an argument. That keeps
|
|
22
|
-
* it testable in isolation — the tests pass a fake unpin and assert
|
|
23
|
-
* which pins were visited and whether the sidecar was cleared.
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
import { readActivePins, clearActivePins, type ActivePin } from "./active-pins.js";
|
|
27
|
-
|
|
28
|
-
export type UnpinFn = (chatId: string, messageId: number) => Promise<unknown>;
|
|
29
|
-
/**
|
|
30
|
-
* Optional pre-unpin hook. Called once per sidecar entry before the
|
|
31
|
-
* unpin fires. Used by the boot-time orphan-pin reaper (#689) to edit
|
|
32
|
-
* the message body to a "Restart interrupted this work" banner, so the
|
|
33
|
-
* user sees WHY the card stopped updating rather than silently losing
|
|
34
|
-
* the pin.
|
|
35
|
-
*
|
|
36
|
-
* Hook errors are logged and swallowed: a banner edit failing must
|
|
37
|
-
* never block the unpin (frozen card is worse than no card).
|
|
38
|
-
*/
|
|
39
|
-
export type EditBeforeUnpinFn = (pin: ActivePin) => Promise<unknown>;
|
|
40
|
-
|
|
41
|
-
export interface SweepOptions {
|
|
42
|
-
/** Upper bound on how long to wait for all unpin calls before returning. */
|
|
43
|
-
timeoutMs?: number;
|
|
44
|
-
/** Optional log hook — called with human-readable progress/error lines. */
|
|
45
|
-
log?: (msg: string) => void;
|
|
46
|
-
/**
|
|
47
|
-
* Optional per-pin edit hook fired BEFORE the unpin. Failures are
|
|
48
|
-
* caught and logged; the unpin still runs. See {@link EditBeforeUnpinFn}.
|
|
49
|
-
*/
|
|
50
|
-
editBeforeUnpin?: EditBeforeUnpinFn;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export interface SweepResult {
|
|
54
|
-
swept: ActivePin[];
|
|
55
|
-
timedOut: boolean;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Unpin every entry in the sidecar, then clear it. Bounded by
|
|
60
|
-
* `timeoutMs` (default 2s) so a slow Telegram API can't block a
|
|
61
|
-
* restart indefinitely. Unpin failures are logged and swallowed —
|
|
62
|
-
* the sidecar is cleared regardless so stale entries don't pile up
|
|
63
|
-
* on subsequent boots.
|
|
64
|
-
*/
|
|
65
|
-
export async function sweepActivePins(
|
|
66
|
-
agentDir: string,
|
|
67
|
-
unpin: UnpinFn,
|
|
68
|
-
options: SweepOptions = {},
|
|
69
|
-
): Promise<SweepResult> {
|
|
70
|
-
const log = options.log ?? (() => {});
|
|
71
|
-
const timeoutMs = options.timeoutMs ?? 2000;
|
|
72
|
-
const pins = readActivePins(agentDir);
|
|
73
|
-
if (pins.length === 0) return { swept: [], timedOut: false };
|
|
74
|
-
|
|
75
|
-
log(`sweeping ${pins.length} active pin(s)`);
|
|
76
|
-
const editBeforeUnpin = options.editBeforeUnpin;
|
|
77
|
-
const attempts = pins.map((pin) =>
|
|
78
|
-
Promise.resolve()
|
|
79
|
-
.then(async () => {
|
|
80
|
-
if (editBeforeUnpin != null) {
|
|
81
|
-
try {
|
|
82
|
-
await editBeforeUnpin(pin);
|
|
83
|
-
} catch (err) {
|
|
84
|
-
// Banner edits are best-effort — message may already be gone
|
|
85
|
-
// or the bot may have lost edit rights. Don't block unpin.
|
|
86
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
87
|
-
log(`banner edit failed for ${pin.chatId}/${pin.messageId}: ${msg}`);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
return unpin(pin.chatId, pin.messageId);
|
|
91
|
-
})
|
|
92
|
-
.catch((err: unknown) => {
|
|
93
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
94
|
-
log(`unpin failed for ${pin.chatId}/${pin.messageId}: ${msg}`);
|
|
95
|
-
}),
|
|
96
|
-
);
|
|
97
|
-
|
|
98
|
-
let timedOut = false;
|
|
99
|
-
await Promise.race([
|
|
100
|
-
Promise.allSettled(attempts),
|
|
101
|
-
new Promise<void>((resolve) =>
|
|
102
|
-
setTimeout(() => {
|
|
103
|
-
timedOut = true;
|
|
104
|
-
resolve();
|
|
105
|
-
}, timeoutMs),
|
|
106
|
-
),
|
|
107
|
-
]);
|
|
108
|
-
|
|
109
|
-
// By design: clear the sidecar on timeout even though in-flight unpins
|
|
110
|
-
// may not have landed. Telegram's unpin is idempotent, so a retried unpin
|
|
111
|
-
// on the next boot is a cheap no-op, whereas keeping the sidecar entries
|
|
112
|
-
// around would have the sweep re-fire forever whenever Telegram is slow.
|
|
113
|
-
clearActivePins(agentDir);
|
|
114
|
-
return { swept: pins, timedOut };
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* A single pinned message returned from Telegram's `getChat` API,
|
|
119
|
-
* narrowed to the fields this sweep needs. `fromId` is null when the
|
|
120
|
-
* pinned message has no `from` (e.g., anonymous channel posts) — in
|
|
121
|
-
* that case the sweep treats the pin as foreign and stops, since we
|
|
122
|
-
* can only confidently unpin messages we authored ourselves.
|
|
123
|
-
*/
|
|
124
|
-
export interface PinnedMessageInfo {
|
|
125
|
-
messageId: number;
|
|
126
|
-
fromId: number | null;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export type GetTopPinFn = (chatId: string) => Promise<PinnedMessageInfo | null>;
|
|
130
|
-
|
|
131
|
-
export interface BotAuthoredSweepResult {
|
|
132
|
-
/** One entry per chat — how many bot-authored pins were unpinned there. */
|
|
133
|
-
perChat: Record<string, number>;
|
|
134
|
-
/** Total across all chats. */
|
|
135
|
-
total: number;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Sweep bot-authored pinned messages from the given chats. Telegram's
|
|
140
|
-
* Bot API doesn't expose a "list all pinned messages" endpoint, only
|
|
141
|
-
* `getChat().pinned_message` which returns the topmost pin. This
|
|
142
|
-
* iterates that endpoint: if the top pin is authored by our bot, we
|
|
143
|
-
* unpin it and re-check — the next most recent pin bubbles up. We
|
|
144
|
-
* stop when the top pin is either missing or authored by someone
|
|
145
|
-
* else, which is the safe behavior: a user-pinned message acts as a
|
|
146
|
-
* barrier so we never interfere with pins the user made themselves.
|
|
147
|
-
*
|
|
148
|
-
* The per-chat loop is bounded by `maxPerChat` (default 32) so a
|
|
149
|
-
* chat with an unexpected pile of bot pins can't spin forever.
|
|
150
|
-
* Failures from `getChat` or `unpin` are logged and tolerated — the
|
|
151
|
-
* sweep advances to the next chat rather than aborting the boot
|
|
152
|
-
* sequence.
|
|
153
|
-
*
|
|
154
|
-
* This complements `sweepActivePins`, which only touches entries
|
|
155
|
-
* previously recorded in the sidecar. Some stale pins never land in
|
|
156
|
-
* the sidecar (e.g., if a pin write raced a crash before `addActivePin`
|
|
157
|
-
* ran, or if the sidecar file itself was lost). This function is the
|
|
158
|
-
* belt-and-suspenders backstop that picks those up on the next boot.
|
|
159
|
-
*/
|
|
160
|
-
export async function sweepBotAuthoredPins(
|
|
161
|
-
chatIds: ReadonlyArray<string>,
|
|
162
|
-
botUserId: number,
|
|
163
|
-
getTopPin: GetTopPinFn,
|
|
164
|
-
unpin: UnpinFn,
|
|
165
|
-
options: SweepOptions & { maxPerChat?: number } = {},
|
|
166
|
-
): Promise<BotAuthoredSweepResult> {
|
|
167
|
-
const log = options.log ?? (() => {});
|
|
168
|
-
const maxPerChat = options.maxPerChat ?? 32;
|
|
169
|
-
const perChat: Record<string, number> = {};
|
|
170
|
-
let total = 0;
|
|
171
|
-
|
|
172
|
-
for (const chatId of chatIds) {
|
|
173
|
-
let unpinnedHere = 0;
|
|
174
|
-
for (let i = 0; i < maxPerChat; i++) {
|
|
175
|
-
let top: PinnedMessageInfo | null;
|
|
176
|
-
try {
|
|
177
|
-
top = await getTopPin(chatId);
|
|
178
|
-
} catch (err) {
|
|
179
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
180
|
-
log(`getChat failed for ${chatId}: ${msg}`);
|
|
181
|
-
break;
|
|
182
|
-
}
|
|
183
|
-
if (top == null) break;
|
|
184
|
-
if (top.fromId !== botUserId) break;
|
|
185
|
-
try {
|
|
186
|
-
await unpin(chatId, top.messageId);
|
|
187
|
-
unpinnedHere++;
|
|
188
|
-
total++;
|
|
189
|
-
} catch (err) {
|
|
190
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
191
|
-
log(`unpin failed for ${chatId}/${top.messageId}: ${msg}`);
|
|
192
|
-
// If unpin fails, the top pin stays — another loop iteration
|
|
193
|
-
// would fetch the same one and loop forever. Break out.
|
|
194
|
-
break;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
if (unpinnedHere > 0) {
|
|
198
|
-
perChat[chatId] = unpinnedHere;
|
|
199
|
-
log(`unpinned ${unpinnedHere} bot-authored pin(s) in ${chatId}`);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return { perChat, total };
|
|
204
|
-
}
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure helpers for tracking pinned progress-card message IDs across
|
|
3
|
-
* restarts.
|
|
4
|
-
*
|
|
5
|
-
* The progress-card driver pins a per-turn card on first emit and
|
|
6
|
-
* unpins on turn complete. That lifecycle is in-memory only, so a
|
|
7
|
-
* crash or kill mid-turn leaves a pinned message with no cleanup
|
|
8
|
-
* path — on restart, the in-memory map is empty and Telegram still
|
|
9
|
-
* shows the stale pin.
|
|
10
|
-
*
|
|
11
|
-
* This module persists the set of currently-pinned cards to a
|
|
12
|
-
* `.active-pins.json` sidecar under `$AGENT_DIR`. The server adds an
|
|
13
|
-
* entry right after `pinChatMessage` succeeds, removes the entry
|
|
14
|
-
* after `unpinChatMessage`, and on startup reads the file to sweep
|
|
15
|
-
* any stale entries (best-effort unpin) before the driver starts.
|
|
16
|
-
*
|
|
17
|
-
* All helpers are filesystem-only — no Telegram side effects — so
|
|
18
|
-
* they're unit-testable in isolation, mirroring `handoff-continuity.ts`.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import { readFileSync, writeFileSync, renameSync, existsSync, unlinkSync } from "node:fs";
|
|
22
|
-
import { join } from "node:path";
|
|
23
|
-
|
|
24
|
-
export const ACTIVE_PINS_FILENAME = ".active-pins.json";
|
|
25
|
-
|
|
26
|
-
export interface ActivePin {
|
|
27
|
-
chatId: string;
|
|
28
|
-
messageId: number;
|
|
29
|
-
turnKey: string;
|
|
30
|
-
pinnedAt: number;
|
|
31
|
-
/**
|
|
32
|
-
* Per-agent identity for the pin. Optional in the on-disk shape so
|
|
33
|
-
* sidecars written before per-agent cards (#per-agent-cards) still
|
|
34
|
-
* parse cleanly — readers should treat a missing field as the parent
|
|
35
|
-
* sentinel (`__parent__`).
|
|
36
|
-
*/
|
|
37
|
-
agentId?: string;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function pinsPath(agentDir: string): string {
|
|
41
|
-
return join(agentDir, ACTIVE_PINS_FILENAME);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Read the active-pins sidecar. Missing, empty, or malformed files
|
|
46
|
-
* return an empty array — callers never have to handle parse errors.
|
|
47
|
-
* Entries that fail shape validation are dropped silently so a
|
|
48
|
-
* corrupted file can't brick the startup sweep.
|
|
49
|
-
*/
|
|
50
|
-
export function readActivePins(agentDir: string): ActivePin[] {
|
|
51
|
-
const p = pinsPath(agentDir);
|
|
52
|
-
if (!existsSync(p)) return [];
|
|
53
|
-
let raw: string;
|
|
54
|
-
try {
|
|
55
|
-
raw = readFileSync(p, "utf-8");
|
|
56
|
-
} catch {
|
|
57
|
-
return [];
|
|
58
|
-
}
|
|
59
|
-
if (raw.trim().length === 0) return [];
|
|
60
|
-
let parsed: unknown;
|
|
61
|
-
try {
|
|
62
|
-
parsed = JSON.parse(raw);
|
|
63
|
-
} catch {
|
|
64
|
-
return [];
|
|
65
|
-
}
|
|
66
|
-
if (!Array.isArray(parsed)) return [];
|
|
67
|
-
const out: ActivePin[] = [];
|
|
68
|
-
for (const item of parsed) {
|
|
69
|
-
if (
|
|
70
|
-
item != null &&
|
|
71
|
-
typeof item === "object" &&
|
|
72
|
-
typeof (item as ActivePin).chatId === "string" &&
|
|
73
|
-
typeof (item as ActivePin).messageId === "number" &&
|
|
74
|
-
typeof (item as ActivePin).turnKey === "string" &&
|
|
75
|
-
typeof (item as ActivePin).pinnedAt === "number"
|
|
76
|
-
) {
|
|
77
|
-
const aid = (item as ActivePin).agentId;
|
|
78
|
-
const entry: ActivePin = aid != null && typeof aid === "string"
|
|
79
|
-
? (item as ActivePin)
|
|
80
|
-
: { ...(item as ActivePin), agentId: undefined };
|
|
81
|
-
out.push(entry);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
return out;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Atomically overwrite the sidecar with the given list. Writing an
|
|
89
|
-
* empty list deletes the file so a fresh restart sees no state.
|
|
90
|
-
*/
|
|
91
|
-
export function writeActivePins(agentDir: string, pins: ActivePin[]): void {
|
|
92
|
-
const p = pinsPath(agentDir);
|
|
93
|
-
if (pins.length === 0) {
|
|
94
|
-
try {
|
|
95
|
-
unlinkSync(p);
|
|
96
|
-
} catch {
|
|
97
|
-
/* already gone */
|
|
98
|
-
}
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
const tmp = `${p}.tmp-${process.pid}-${Date.now()}`;
|
|
102
|
-
try {
|
|
103
|
-
writeFileSync(tmp, JSON.stringify(pins) + "\n", "utf-8");
|
|
104
|
-
renameSync(tmp, p);
|
|
105
|
-
} catch {
|
|
106
|
-
/* best-effort — failsafe cleanup is cosmetic, not safety-critical */
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Append a new pin to the sidecar. Idempotent on `(chatId, messageId)`
|
|
112
|
-
* — a duplicate add replaces the existing entry so `pinnedAt` reflects
|
|
113
|
-
* the most recent pin.
|
|
114
|
-
*/
|
|
115
|
-
export function addActivePin(agentDir: string, pin: ActivePin): void {
|
|
116
|
-
const existing = readActivePins(agentDir).filter(
|
|
117
|
-
(p) => !(p.chatId === pin.chatId && p.messageId === pin.messageId),
|
|
118
|
-
);
|
|
119
|
-
existing.push(pin);
|
|
120
|
-
writeActivePins(agentDir, existing);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Remove the pin matching `(chatId, messageId)`. No-op when the
|
|
125
|
-
* sidecar or entry is absent.
|
|
126
|
-
*/
|
|
127
|
-
export function removeActivePin(agentDir: string, chatId: string, messageId: number): void {
|
|
128
|
-
const existing = readActivePins(agentDir);
|
|
129
|
-
const next = existing.filter(
|
|
130
|
-
(p) => !(p.chatId === chatId && p.messageId === messageId),
|
|
131
|
-
);
|
|
132
|
-
if (next.length === existing.length) return;
|
|
133
|
-
writeActivePins(agentDir, next);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Delete the sidecar outright. Called after the startup sweep so the
|
|
138
|
-
* next run starts clean regardless of unpin success.
|
|
139
|
-
*/
|
|
140
|
-
export function clearActivePins(agentDir: string): void {
|
|
141
|
-
try {
|
|
142
|
-
unlinkSync(pinsPath(agentDir));
|
|
143
|
-
} catch {
|
|
144
|
-
/* already gone */
|
|
145
|
-
}
|
|
146
|
-
}
|