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,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static-source contract pin for the grant-union behavior shipped to
|
|
3
|
+
* fix #1051's silent-token-overwrite bug class.
|
|
4
|
+
*
|
|
5
|
+
* The bug: when an operator approved a vault_request_access card for
|
|
6
|
+
* key A and then later approved a second card for key B, the second
|
|
7
|
+
* mint OVERWROTE the agent's `.vault-token` file with a single-key
|
|
8
|
+
* grant. Both grants existed in the broker DB but the agent could
|
|
9
|
+
* only authenticate against the most-recent one — `vault get keyA`
|
|
10
|
+
* after the second approval returned VAULT-BROKER-DENIED.
|
|
11
|
+
*
|
|
12
|
+
* Fix: before minting, the gateway lists the agent's existing
|
|
13
|
+
* non-expired grants (using the new passphrase-attested list_grants
|
|
14
|
+
* path) and unions their keys with the new key. The fresh grant
|
|
15
|
+
* covers OLD ∪ NEW; the old grant ages out via TTL.
|
|
16
|
+
*
|
|
17
|
+
* This file pins the wiring at the call site so a future refactor
|
|
18
|
+
* can't silently drop the union step.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { describe, it, expect } from "vitest";
|
|
22
|
+
import { readFileSync } from "node:fs";
|
|
23
|
+
import { resolve } from "node:path";
|
|
24
|
+
|
|
25
|
+
const gatewaySrc = readFileSync(
|
|
26
|
+
resolve(__dirname, "..", "gateway", "gateway.ts"),
|
|
27
|
+
"utf-8",
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
function extractPerformBlock(): string {
|
|
31
|
+
const start = gatewaySrc.indexOf("async function performVaultAccessApproval");
|
|
32
|
+
if (start < 0) throw new Error("performVaultAccessApproval not found");
|
|
33
|
+
// Bound the block by the next top-level `async function` keyword.
|
|
34
|
+
const restAfter = gatewaySrc.slice(start + 1);
|
|
35
|
+
const endRel = restAfter.indexOf("\nasync function ");
|
|
36
|
+
return gatewaySrc.slice(start, start + 1 + (endRel >= 0 ? endRel : restAfter.length));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("performVaultAccessApproval unions keys with the agent's existing grant (#1051)", () => {
|
|
40
|
+
const block = extractPerformBlock();
|
|
41
|
+
|
|
42
|
+
it("calls listGrantsViaBroker BEFORE mintGrantViaBroker", () => {
|
|
43
|
+
// fails when: a refactor drops the list step. Without it the
|
|
44
|
+
// mint covers only [pending.key] and OVERWRITES the agent's
|
|
45
|
+
// .vault-token, stranding the previous approval's grant from
|
|
46
|
+
// the agent's perspective.
|
|
47
|
+
const listIdx = block.indexOf("listGrantsViaBroker(");
|
|
48
|
+
const mintIdx = block.indexOf("mintGrantViaBroker(");
|
|
49
|
+
expect(listIdx, "listGrantsViaBroker call missing in performVaultAccessApproval").toBeGreaterThan(0);
|
|
50
|
+
expect(mintIdx, "mintGrantViaBroker call missing").toBeGreaterThan(0);
|
|
51
|
+
expect(listIdx, "list MUST happen BEFORE mint").toBeLessThan(mintIdx);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("forwards an attestation to listGrantsViaBroker (#1115 follow-up: passphrase OR posture)", () => {
|
|
55
|
+
// The non-admin agent socket needs operator-attestation to call
|
|
56
|
+
// list_grants (#1051's broker-side gate widening). Pre-#1115-
|
|
57
|
+
// follow-up this had to be the operator passphrase. Post-fix the
|
|
58
|
+
// gateway threads either `{ passphrase }` or
|
|
59
|
+
// `{ attest_via_posture: true }` via `brokerAuthOpts`, depending
|
|
60
|
+
// on whether the operator typed the passphrase or the host is
|
|
61
|
+
// running telegram-id posture. The call must forward the auth
|
|
62
|
+
// opts object intact.
|
|
63
|
+
const listMatch = block.match(/listGrantsViaBroker\([^)]+\)/);
|
|
64
|
+
expect(listMatch, "listGrantsViaBroker call shape").not.toBeNull();
|
|
65
|
+
expect(listMatch![0], "attestation MUST be forwarded (brokerAuthOpts)").toMatch(/brokerAuthOpts/);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("unions existing key_allow with the new key before minting", () => {
|
|
69
|
+
// Pin the union semantics. The mint call must pass a Set-like
|
|
70
|
+
// union of existing keys + new key, not just [pending.key].
|
|
71
|
+
expect(block).toMatch(/key_allow/);
|
|
72
|
+
expect(block).toMatch(/new Set/);
|
|
73
|
+
// The Set MUST be seeded from the existing grant's keys.
|
|
74
|
+
expect(block).toMatch(/existingReadKeys|active\[0\]/);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("includes write_allow union for write-scope requests", () => {
|
|
78
|
+
// Mirror the read-side union for write scope.
|
|
79
|
+
expect(block).toMatch(/write_allow/);
|
|
80
|
+
expect(block).toMatch(/writeKeys\.add|writeKeys\.size/);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("concurrent Approve taps queue into a single pending-op (#1051)", () => {
|
|
84
|
+
// Without the queue, a second Approve tap before the operator
|
|
85
|
+
// types their passphrase OVERWRITES the first stage's pending
|
|
86
|
+
// op — the first card's grant never mints. The new shape has
|
|
87
|
+
// `items: [...]` so a second tap APPENDS, and the text-handler
|
|
88
|
+
// drains every queued stage off one passphrase entry.
|
|
89
|
+
//
|
|
90
|
+
// Anchor on the producer site inside handleVaultRequestAccessCallback.
|
|
91
|
+
const callbackHandler =
|
|
92
|
+
gatewaySrc
|
|
93
|
+
.split("async function handleVaultRequestAccessCallback")[1]
|
|
94
|
+
?.split("\nasync function ")[0] ?? "";
|
|
95
|
+
// The producer reads any existing pending op, and APPENDS to
|
|
96
|
+
// items[] rather than overwriting.
|
|
97
|
+
expect(callbackHandler).toMatch(/pendingVaultOps\.get\(pending\.chat_id\)/);
|
|
98
|
+
// `items` declared (either as `items: [...]` literal or `const items = ...`).
|
|
99
|
+
expect(callbackHandler).toMatch(/\bitems\b/);
|
|
100
|
+
expect(callbackHandler).toMatch(/passphrase-for-access-approve/);
|
|
101
|
+
|
|
102
|
+
// The consumer (text-handler) loops over items. Anchor on the
|
|
103
|
+
// unique consumer code (the `else if` branch that handles a
|
|
104
|
+
// passphrase reply), not the type-discriminator string itself —
|
|
105
|
+
// the latter appears in the PendingVaultOp type definition too,
|
|
106
|
+
// so a naive split lands on the wrong slice.
|
|
107
|
+
const textHandlerIdx = gatewaySrc.indexOf("else if (pendingVault.kind === 'passphrase-for-access-approve')");
|
|
108
|
+
expect(textHandlerIdx, "text-handler consumer branch not found").toBeGreaterThan(0);
|
|
109
|
+
const textHandler = gatewaySrc.slice(textHandlerIdx, textHandlerIdx + 3000);
|
|
110
|
+
expect(textHandler, "text-handler MUST iterate items").toMatch(/for\s*\(\s*const\s+item\s+of\s+pendingVault\.items/);
|
|
111
|
+
// Each iteration looks up the staged access (sibling-stage may
|
|
112
|
+
// have been denied / expired between tap and passphrase reply).
|
|
113
|
+
expect(textHandler).toMatch(/pendingVaultRequestAccesses\.get\(item\.stageId\)/);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("gracefully proceeds with single-key mint when listGrants fails", () => {
|
|
117
|
+
// Non-blocker: if listGrants returns unreachable/error, the
|
|
118
|
+
// gateway should STILL mint the new grant (without union) so
|
|
119
|
+
// the operator's tap-to-approve doesn't get blocked on a
|
|
120
|
+
// transient broker issue. Documented intent.
|
|
121
|
+
expect(block).toMatch(/list\.kind === 'ok'/);
|
|
122
|
+
// A comment explaining the fallback behavior must be present so
|
|
123
|
+
// a future reader knows the gracefully-degraded path is
|
|
124
|
+
// intentional.
|
|
125
|
+
// The comment is split across multiple // lines, so collapse
|
|
126
|
+
// whitespace + comment prefixes before matching.
|
|
127
|
+
const flattened = block.replace(/\n\s*\/\/\s*/g, " ");
|
|
128
|
+
expect(flattened).toMatch(/(without union|edge case|fall.?back|fail closed|degrade)/i);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract pin for #1047 — the gateway's vault-key regex was stricter
|
|
3
|
+
* than the broker's, so canonical slash-namespaced keys like
|
|
4
|
+
* `fatsecret/client_id` couldn't be requested via the in-band
|
|
5
|
+
* approval card flow. Filed by gymbro after hitting
|
|
6
|
+
* VAULT-BROKER-DENIED on `fatsecret/client_id` and being unable to
|
|
7
|
+
* call `vault_request_access` because of the schema regex.
|
|
8
|
+
*
|
|
9
|
+
* Three call sites use the regex:
|
|
10
|
+
* 1. `vault_request_save` execute (telegram-plugin/gateway/gateway.ts ~3549)
|
|
11
|
+
* 2. `vault_request_access` execute (~3633)
|
|
12
|
+
* 3. The rename-the-staged-key text-message handler (~5185)
|
|
13
|
+
*
|
|
14
|
+
* All three must accept the canonical slash-namespaced shape used by
|
|
15
|
+
* production keys: `fatsecret/client_id`, `mff/agent-private-key`,
|
|
16
|
+
* `microsoft/ken-tokens`. The broker itself has no key-shape regex
|
|
17
|
+
* (just `z.string().min(1)` in protocol.ts), so the gateway should
|
|
18
|
+
* mirror that posture — accept what the broker accepts.
|
|
19
|
+
*
|
|
20
|
+
* This is a static-source pin — the regex literal must appear in the
|
|
21
|
+
* gateway source. A runtime test would need a full bot harness;
|
|
22
|
+
* static-source mirrors the convention used elsewhere in this file
|
|
23
|
+
* tree (see jtbd-talk-from-anywhere.test.ts, vault-request-access-tool.test.ts).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { describe, it, expect } from "vitest";
|
|
27
|
+
import { readFileSync } from "node:fs";
|
|
28
|
+
import { resolve } from "node:path";
|
|
29
|
+
|
|
30
|
+
const gatewaySrc = readFileSync(
|
|
31
|
+
resolve(__dirname, "..", "gateway", "gateway.ts"),
|
|
32
|
+
"utf-8",
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
/** The exported regex literal — what the gateway actually validates against. */
|
|
36
|
+
function extractVaultKeyRegex(): RegExp {
|
|
37
|
+
// The shared constant declaration. Anchored on the `VAULT_KEY_REGEX
|
|
38
|
+
// = /` prefix and the `/` immediately before the line terminator
|
|
39
|
+
// (the regex has no flags). The source includes `/` inside the
|
|
40
|
+
// character class, so a greedy match up to end-of-line is the
|
|
41
|
+
// safe extraction strategy.
|
|
42
|
+
const m = gatewaySrc.match(/const\s+VAULT_KEY_REGEX\s*=\s*\/(.+)\/\s*$/m);
|
|
43
|
+
if (!m) throw new Error("VAULT_KEY_REGEX constant not found in gateway.ts");
|
|
44
|
+
return new RegExp(m[1]);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Either inline literal with `/`, OR a reference to VAULT_KEY_REGEX. */
|
|
48
|
+
const ACCEPTS_SLASH = /\[A-Za-z0-9_(\/|\\\/|\.)+-\]|VAULT_KEY_REGEX/;
|
|
49
|
+
|
|
50
|
+
describe("vault-key regex accepts canonical slash-namespaced keys (#1047)", () => {
|
|
51
|
+
it("vault_request_save validation includes '/' in the charclass", () => {
|
|
52
|
+
// Anchor on the unique error message so a refactor that moves
|
|
53
|
+
// the validation can still be located.
|
|
54
|
+
const ix = gatewaySrc.indexOf("vault_request_save: key must match");
|
|
55
|
+
expect(ix, "could not find vault_request_save key error message").toBeGreaterThan(0);
|
|
56
|
+
const window = gatewaySrc.slice(Math.max(0, ix - 400), ix);
|
|
57
|
+
expect(
|
|
58
|
+
window,
|
|
59
|
+
"vault_request_save validation should accept '/' (e.g. fatsecret/client_id)",
|
|
60
|
+
).toMatch(ACCEPTS_SLASH);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("vault_request_access validation includes '/' in the charclass", () => {
|
|
64
|
+
const ix = gatewaySrc.indexOf("vault_request_access: key must match");
|
|
65
|
+
expect(ix, "could not find vault_request_access key error message").toBeGreaterThan(0);
|
|
66
|
+
const window = gatewaySrc.slice(Math.max(0, ix - 400), ix);
|
|
67
|
+
expect(
|
|
68
|
+
window,
|
|
69
|
+
"vault_request_access validation should accept '/' (e.g. fatsecret/client_id)",
|
|
70
|
+
).toMatch(ACCEPTS_SLASH);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("rename-staged-key validation includes '/' in the charclass", () => {
|
|
74
|
+
// The rename handler for the [✏️ Rename] button on a
|
|
75
|
+
// vault_request_save card. If this stays strict, operators can't
|
|
76
|
+
// rename a staged key to a canonical namespaced form.
|
|
77
|
+
const ix = gatewaySrc.indexOf("Key must match");
|
|
78
|
+
expect(ix, "could not find rename validation error message").toBeGreaterThan(0);
|
|
79
|
+
const window = gatewaySrc.slice(Math.max(0, ix - 400), ix);
|
|
80
|
+
expect(
|
|
81
|
+
window,
|
|
82
|
+
"rename handler should accept '/' (e.g. fatsecret/client_id)",
|
|
83
|
+
).toMatch(ACCEPTS_SLASH);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("/vault audit one-tap Allow callback validation includes '/' in the charclass", () => {
|
|
87
|
+
// Reviewer-caught oversight on #1049: the
|
|
88
|
+
// handleVaultRecentDenialCallback handler (#969 P2b) validates
|
|
89
|
+
// the keyName parsed from the inline-button callback_data with
|
|
90
|
+
// its own copy of the key regex. Without this site updated, the
|
|
91
|
+
// exact bug from #1047 — operator opens /vault audit on a
|
|
92
|
+
// denied `fatsecret/client_id`, taps [Allow] — would surface
|
|
93
|
+
// "Invalid key name" even though the agent-initiated card flow
|
|
94
|
+
// works.
|
|
95
|
+
const ix = gatewaySrc.indexOf("'Invalid key name'");
|
|
96
|
+
expect(ix, "could not find /vault audit Invalid key name error").toBeGreaterThan(0);
|
|
97
|
+
const window = gatewaySrc.slice(Math.max(0, ix - 400), ix);
|
|
98
|
+
expect(
|
|
99
|
+
window,
|
|
100
|
+
"/vault audit one-tap allow should accept '/' (e.g. fatsecret/client_id)",
|
|
101
|
+
).toMatch(ACCEPTS_SLASH);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("user-facing rename error message names the slash as allowed", () => {
|
|
105
|
+
// The visible error text guides the operator on what's allowed.
|
|
106
|
+
// If we widened the regex but didn't update the message, the
|
|
107
|
+
// operator is told `/` is disallowed even though it isn't.
|
|
108
|
+
// The VAULT_KEY_REGEX_LABEL string is the canonical user-visible
|
|
109
|
+
// hint and includes the `/` shape.
|
|
110
|
+
const m = gatewaySrc.match(/const\s+VAULT_KEY_REGEX_LABEL\s*=\s*"([^"]+)"/);
|
|
111
|
+
expect(m, "VAULT_KEY_REGEX_LABEL constant not found").not.toBeNull();
|
|
112
|
+
expect(m![1], "label must mention '/' as allowed").toMatch(/\//);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("vault-key regex: regression guards (the fix doesn't break the original shape)", () => {
|
|
117
|
+
// Anchored to the live VAULT_KEY_REGEX constant declaration so the
|
|
118
|
+
// test runs the same regex the gateway runs. A breaking refactor
|
|
119
|
+
// (typo, accidental tightening) fails loudly here.
|
|
120
|
+
const re = extractVaultKeyRegex();
|
|
121
|
+
|
|
122
|
+
it.each([
|
|
123
|
+
["telegram_bot_token", true, "underscores + lowercase"],
|
|
124
|
+
["MY_TOKEN", true, "uppercase + underscore"],
|
|
125
|
+
["api.key", true, "dot namespace"],
|
|
126
|
+
["fatsecret/client_id", true, "slash namespace (the bug)"],
|
|
127
|
+
["fatsecret/credentials", true, "slash namespace"],
|
|
128
|
+
["mff/agent-private-key", true, "slash namespace with hyphen"],
|
|
129
|
+
["microsoft/ken-tokens", true, "slash namespace from issue"],
|
|
130
|
+
["k", true, "single char"],
|
|
131
|
+
["a".repeat(200), true, "max length"],
|
|
132
|
+
["", false, "empty rejected"],
|
|
133
|
+
["a".repeat(201), false, "over-length rejected"],
|
|
134
|
+
["key with space", false, "space rejected"],
|
|
135
|
+
['key"with"quotes', false, "quotes rejected"],
|
|
136
|
+
["key\nwith\nnewlines", false, "newlines rejected"],
|
|
137
|
+
] as const)("VAULT_KEY_REGEX: %s — should %s (%s)", (input, expected) => {
|
|
138
|
+
expect(re.test(input), `${JSON.stringify(input)} (${expected ? "accept" : "reject"})`).toBe(expected);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression for #1115 follow-up — vault-approval-posture config errors
|
|
3
|
+
* must NOT manifest as unhandled-rejection crash loops.
|
|
4
|
+
*
|
|
5
|
+
* Pre-fix (2026-05-13 overnight UAT discovery): when the operator
|
|
6
|
+
* declared `vault.broker.approvalAuth: telegram-id` in switchroom.yaml
|
|
7
|
+
* but the auto-unlock blob couldn't be read (e.g. agent UID can't
|
|
8
|
+
* access the operator's home dir), `resolveVaultApprovalPosture`
|
|
9
|
+
* threw, the startup IIFE in gateway.ts let the error propagate as an
|
|
10
|
+
* unhandled rejection, and `_switchroom_supervise` saw status=0 from
|
|
11
|
+
* the shutdown handler and respawned the gateway. 10 restarts in <60s
|
|
12
|
+
* before the supervisor's restart-cap kicked in. Each restart posted
|
|
13
|
+
* an "agent-crashed" operator-event card and the bridge was alive only
|
|
14
|
+
* in brief windows between restarts — inbound messages dropped.
|
|
15
|
+
*
|
|
16
|
+
* Post-fix: the startup catches the config-class error, writes a
|
|
17
|
+
* quarantine marker with reason `startup.config_error`, and calls
|
|
18
|
+
* `process.exit(78)` (sysexits EX_CONFIG). The supervisor short-
|
|
19
|
+
* circuits on exit 78 without restarting (`_switchroom_supervise` in
|
|
20
|
+
* `profiles/_base/start.sh.hbs`), so the operator sees ONE clean
|
|
21
|
+
* error and a quarantine file instead of a crash-loop log smear.
|
|
22
|
+
*
|
|
23
|
+
* These tests assert the contracts:
|
|
24
|
+
* 1. The reason code `startup.config_error` exists in both quarantine
|
|
25
|
+
* modules (host-side and plugin-side stay in sync).
|
|
26
|
+
* 2. The quarantine marker writer accepts the new reason and writes
|
|
27
|
+
* a parseable JSON file.
|
|
28
|
+
* 3. The reader at `src/agents/quarantine.ts` round-trips the new
|
|
29
|
+
* reason.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
33
|
+
import { mkdtempSync, readFileSync, rmSync } from 'node:fs'
|
|
34
|
+
import { tmpdir } from 'node:os'
|
|
35
|
+
import { join } from 'node:path'
|
|
36
|
+
import { writeQuarantineMarker, QUARANTINE_FILENAME } from '../gateway/quarantine.js'
|
|
37
|
+
import {
|
|
38
|
+
readQuarantineMarker,
|
|
39
|
+
type QuarantineReason,
|
|
40
|
+
} from '../../src/agents/quarantine.js'
|
|
41
|
+
|
|
42
|
+
let stateDir: string
|
|
43
|
+
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
stateDir = mkdtempSync(join(tmpdir(), 'vault-posture-quarantine-'))
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
rmSync(stateDir, { recursive: true, force: true })
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe('quarantine — startup.config_error reason', () => {
|
|
53
|
+
it('plugin-side writer accepts the new reason and writes a parseable marker', () => {
|
|
54
|
+
writeQuarantineMarker(
|
|
55
|
+
stateDir,
|
|
56
|
+
'startup.config_error',
|
|
57
|
+
'vault.broker.approvalAuth=telegram-id but blob unreadable',
|
|
58
|
+
)
|
|
59
|
+
const raw = readFileSync(join(stateDir, QUARANTINE_FILENAME), 'utf-8')
|
|
60
|
+
const parsed = JSON.parse(raw) as {
|
|
61
|
+
v: number
|
|
62
|
+
reason: string
|
|
63
|
+
ts: number
|
|
64
|
+
detail?: string
|
|
65
|
+
}
|
|
66
|
+
expect(parsed.v).toBe(1)
|
|
67
|
+
expect(parsed.reason).toBe('startup.config_error')
|
|
68
|
+
expect(typeof parsed.ts).toBe('number')
|
|
69
|
+
expect(parsed.detail).toContain('vault.broker.approvalAuth')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('host-side reader round-trips the new reason', () => {
|
|
73
|
+
writeQuarantineMarker(stateDir, 'startup.config_error', 'detail goes here')
|
|
74
|
+
const m = readQuarantineMarker(stateDir)
|
|
75
|
+
expect(m).not.toBeNull()
|
|
76
|
+
expect(m!.reason).toBe('startup.config_error')
|
|
77
|
+
expect(m!.detail).toBe('detail goes here')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('startup.unauthorized reason still round-trips (no regression)', () => {
|
|
81
|
+
writeQuarantineMarker(stateDir, 'startup.unauthorized', '401 Unauthorized')
|
|
82
|
+
const m = readQuarantineMarker(stateDir)
|
|
83
|
+
expect(m!.reason).toBe('startup.unauthorized')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('type-level: both reasons accepted by QuarantineReason union', () => {
|
|
87
|
+
const accepted: QuarantineReason[] = ['startup.unauthorized', 'startup.config_error']
|
|
88
|
+
expect(accepted).toHaveLength(2)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('marker survives in detail field — no truncation of operator-facing message', () => {
|
|
92
|
+
const longDetail =
|
|
93
|
+
'vault.broker.approvalAuth=telegram-id but reading auto-unlock blob at '
|
|
94
|
+
+ '/state/agent/home/.switchroom/vault-auto-unlock failed: ENOENT no such '
|
|
95
|
+
+ 'file or directory. Refusing to boot — silently falling back to '
|
|
96
|
+
+ 'passphrase posture would invert the operator\'s declared security '
|
|
97
|
+
+ 'posture. Either repair the auto-unlock blob (rerun `switchroom setup` '
|
|
98
|
+
+ '/ `switchroom vault auto-unlock`) or remove vault.broker.approvalAuth '
|
|
99
|
+
+ 'from switchroom.yaml.'
|
|
100
|
+
writeQuarantineMarker(stateDir, 'startup.config_error', longDetail)
|
|
101
|
+
const m = readQuarantineMarker(stateDir)
|
|
102
|
+
expect(m!.detail).toBe(longDetail)
|
|
103
|
+
})
|
|
104
|
+
})
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pin the agent-initiated vault ACL request flow shipped in #1012.
|
|
3
|
+
*
|
|
4
|
+
* Regressions guarded here:
|
|
5
|
+
* 1. `vault_request_access` MCP tool dropping from bridge.ts schema
|
|
6
|
+
* (the agent would lose the tool with no compile-time signal).
|
|
7
|
+
* 2. `vault_request_access` missing from the gateway's ALLOWED_TOOLS
|
|
8
|
+
* set (bridge would emit it but gateway would 403 with
|
|
9
|
+
* `tool not allowed`).
|
|
10
|
+
* 3. The `vra:` callback prefix losing its dispatcher branch (taps
|
|
11
|
+
* on [Approve] / [Deny] silently fall through to the trailing
|
|
12
|
+
* "unknown callback" arm).
|
|
13
|
+
* 4. The 90-day TTL ceiling and `read|write` scope shape — both are
|
|
14
|
+
* part of the threat model (agents can't request perpetual or
|
|
15
|
+
* undefined-scope grants).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describe, it, expect } from 'vitest'
|
|
19
|
+
import { readFileSync } from 'node:fs'
|
|
20
|
+
import { resolve } from 'node:path'
|
|
21
|
+
|
|
22
|
+
const bridgeSrc = readFileSync(
|
|
23
|
+
resolve(__dirname, '..', 'bridge', 'bridge.ts'),
|
|
24
|
+
'utf-8',
|
|
25
|
+
)
|
|
26
|
+
const gatewaySrc = readFileSync(
|
|
27
|
+
resolve(__dirname, '..', 'gateway', 'gateway.ts'),
|
|
28
|
+
'utf-8',
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
describe('vault_request_access (#1012)', () => {
|
|
32
|
+
it('bridge advertises the tool to MCP clients', () => {
|
|
33
|
+
// fails when: the schema is removed from TOOL_SCHEMAS — agents
|
|
34
|
+
// running the new tool will hit a generic "unknown tool" path
|
|
35
|
+
// before they ever see the gateway.
|
|
36
|
+
expect(bridgeSrc).toContain("name: 'vault_request_access'")
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('bridge schema declares the threat-model-critical fields', () => {
|
|
40
|
+
// fails when: a refactor drops `scope` (defaults to read) or
|
|
41
|
+
// `duration` (caps requested TTL at 90d). Both gates are part of
|
|
42
|
+
// the agent-can-only-request-not-mint boundary described in #1012.
|
|
43
|
+
const block = bridgeSrc.split("name: 'vault_request_access'")[1]?.split("name: '")[0] ?? ''
|
|
44
|
+
expect(block).toContain("'read'")
|
|
45
|
+
expect(block).toContain("'write'")
|
|
46
|
+
expect(block).toMatch(/duration/)
|
|
47
|
+
// Required fields: chat_id + key. Value is NOT required (this is
|
|
48
|
+
// an access request, not a save — the agent doesn't have the
|
|
49
|
+
// value, it wants permission to read it).
|
|
50
|
+
expect(block).toMatch(/required:\s*\[\s*'chat_id',\s*'key'\s*\]/)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('gateway accepts vault_request_access in ALLOWED_TOOLS', () => {
|
|
54
|
+
// fails when: the ALLOWED_TOOLS set is touched and the entry
|
|
55
|
+
// gets dropped. Bridge would forward the call and the gateway
|
|
56
|
+
// would reject with `tool not allowed`.
|
|
57
|
+
expect(gatewaySrc).toMatch(/ALLOWED_TOOLS[\s\S]*?'vault_request_access'/)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('gateway routes vault_request_access in executeToolCall', () => {
|
|
61
|
+
// fails when: the switch arm is dropped. Tool would be accepted
|
|
62
|
+
// by ALLOWED_TOOLS but fall through to the `unknown tool` branch.
|
|
63
|
+
expect(gatewaySrc).toMatch(/case\s+'vault_request_access':\s*\n\s*return\s+executeVaultRequestAccess/)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('gateway dispatches vra: callback prefix', () => {
|
|
67
|
+
// fails when: the callback_query dispatcher loses the `vra:`
|
|
68
|
+
// branch. Operator taps on [Approve] / [Deny] would fall to the
|
|
69
|
+
// catch-all "unknown callback" path and the card would stay
|
|
70
|
+
// open forever.
|
|
71
|
+
expect(gatewaySrc).toMatch(/data\.startsWith\('vra:'\)/)
|
|
72
|
+
expect(gatewaySrc).toMatch(/handleVaultRequestAccessCallback/)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('approve handler mints via broker (not direct grants.db write)', () => {
|
|
76
|
+
// fails when: someone tries to short-circuit by writing to the
|
|
77
|
+
// grants DB directly. The broker is the single point of grant
|
|
78
|
+
// issuance — bypassing it skips audit-log emission and breaks
|
|
79
|
+
// the path-as-identity ACL contract.
|
|
80
|
+
//
|
|
81
|
+
// The mint call lives in performVaultAccessApproval — the helper
|
|
82
|
+
// factored out so both the direct-approve path AND the
|
|
83
|
+
// tap-on-locked → passphrase-resume path drive identical minting
|
|
84
|
+
// (see telegram-plugin/tests/vault-request-access-unlock-resume.test.ts).
|
|
85
|
+
const mintHelperBlock =
|
|
86
|
+
gatewaySrc
|
|
87
|
+
.split('async function performVaultAccessApproval')[1]
|
|
88
|
+
?.split('async function handleVaultRequestAccessCallback')[0] ?? ''
|
|
89
|
+
expect(mintHelperBlock).toMatch(/mintGrantViaBroker/)
|
|
90
|
+
// Description string must carry the audit breadcrumb so post-hoc
|
|
91
|
+
// forensics can tell agent-initiated grants apart from
|
|
92
|
+
// operator-host-CLI grants and from /vault audit one-tap grants.
|
|
93
|
+
expect(mintHelperBlock).toMatch(/vault_request_access/)
|
|
94
|
+
expect(mintHelperBlock).toMatch(/#1012/)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('duration parser enforces the 90-day ceiling', () => {
|
|
98
|
+
// fails when: the cap is removed or widened without a corresponding
|
|
99
|
+
// doc/threat-model update. Agent-initiated grants must have a
|
|
100
|
+
// finite sunset; "never" must be refused outright.
|
|
101
|
+
const execBlock = gatewaySrc.split('async function executeVaultRequestAccess')[1]?.split('async function ')[0] ?? ''
|
|
102
|
+
expect(execBlock).toMatch(/NINETY_DAYS/)
|
|
103
|
+
expect(execBlock).toMatch(/90\s*\*\s*86400/)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('approve handler is gated on the operator allowFrom list', () => {
|
|
107
|
+
// fails when: the access check is dropped. Without this gate any
|
|
108
|
+
// chat member could approve a grant — breaks the operator-only
|
|
109
|
+
// mint authority that's load-bearing for #1012's threat model.
|
|
110
|
+
const handlerBlock = gatewaySrc.split('async function handleVaultRequestAccessCallback')[1]?.split('async function handleVaultRequestSaveCallback')[0] ?? ''
|
|
111
|
+
expect(handlerBlock).toMatch(/loadAccess\(\)/)
|
|
112
|
+
expect(handlerBlock).toMatch(/allowFrom\.includes/)
|
|
113
|
+
})
|
|
114
|
+
})
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract pin for the tap-to-unlock-and-approve flow added on top of
|
|
3
|
+
* #1012 Phase 2 (#1030 broker passphrase-attested mint_grant).
|
|
4
|
+
*
|
|
5
|
+
* Before this PR: tapping Approve on a vault_request_access card
|
|
6
|
+
* without first unlocking the vault edited the card to "🔒 Vault is
|
|
7
|
+
* locked. Run /vault unlock... then ask the agent to re-issue." The
|
|
8
|
+
* operator had to (a) clear that card, (b) /vault unlock, (c) ask
|
|
9
|
+
* the agent to re-emit the request, (d) tap Approve again. Four steps
|
|
10
|
+
* for one decision.
|
|
11
|
+
*
|
|
12
|
+
* After this PR: the cache-miss tap keeps the card open, prompts for
|
|
13
|
+
* the passphrase as the next message, captures+caches it, deletes the
|
|
14
|
+
* passphrase message from chat, then auto-resumes the mint. One tap
|
|
15
|
+
* + one reply = one grant.
|
|
16
|
+
*
|
|
17
|
+
* Mirrors the `passphrase-for-deferred` flow from #44 (deferred-secret
|
|
18
|
+
* card's "🔓 Unlock vault & save"). Same idiom, same trust posture.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { describe, it, expect } from 'vitest'
|
|
22
|
+
import { readFileSync } from 'node:fs'
|
|
23
|
+
import { resolve } from 'node:path'
|
|
24
|
+
|
|
25
|
+
const gatewaySrc = readFileSync(
|
|
26
|
+
resolve(__dirname, '..', 'gateway', 'gateway.ts'),
|
|
27
|
+
'utf-8',
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
describe('vault_request_access — tap-to-unlock-and-approve UX', () => {
|
|
31
|
+
it('declares the passphrase-for-access-approve PendingVaultOp variant', () => {
|
|
32
|
+
// fails when: the new PendingVaultOp kind is dropped. The
|
|
33
|
+
// resume flow leans on this discriminator — without it the
|
|
34
|
+
// text handler can't route the passphrase reply back to the
|
|
35
|
+
// staged request.
|
|
36
|
+
expect(gatewaySrc).toMatch(/kind:\s*['"]passphrase-for-access-approve['"]/)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('Approve on a locked vault stages a passphrase prompt instead of clearing the card', () => {
|
|
40
|
+
// fails when: the cache-miss branch reverts to the old behaviour
|
|
41
|
+
// of editing the card to "ask the agent to re-issue." That UX
|
|
42
|
+
// is the four-step workaround we just replaced.
|
|
43
|
+
//
|
|
44
|
+
// Anchor: the approve-action block inside handleVaultRequestAccessCallback.
|
|
45
|
+
const approveBlock =
|
|
46
|
+
gatewaySrc.split('if (action === \'approve\')')[1]?.split('await ctx.answerCallbackQuery({ text: \'Unknown action\'')[0] ?? ''
|
|
47
|
+
expect(approveBlock).toMatch(/pendingVaultOps\.set/)
|
|
48
|
+
expect(approveBlock).toMatch(/passphrase-for-access-approve/)
|
|
49
|
+
// Card text must invite a passphrase reply, not punt to a
|
|
50
|
+
// /vault unlock detour.
|
|
51
|
+
expect(approveBlock).toMatch(/Reply with your passphrase/i)
|
|
52
|
+
// The "ask the agent to re-issue the request card" copy belonged
|
|
53
|
+
// to the pre-fix path. Should be gone from the cache-miss branch.
|
|
54
|
+
expect(approveBlock).not.toMatch(/ask the agent to re-issue the request card/)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('passphrase intercept deletes the chat message and resumes mint', () => {
|
|
58
|
+
// fails when: the new pending-op handler stops calling
|
|
59
|
+
// deleteSensitiveMessage on the passphrase message OR stops
|
|
60
|
+
// routing into performVaultAccessApproval. Both are load-bearing:
|
|
61
|
+
// - delete: prevents the passphrase from lingering in chat history
|
|
62
|
+
// - resume: closes the "one decision" UX promise
|
|
63
|
+
//
|
|
64
|
+
// Anchor: the text-handler branch keyed on the new kind.
|
|
65
|
+
const handlerBlock =
|
|
66
|
+
gatewaySrc
|
|
67
|
+
.split("pendingVault.kind === 'passphrase-for-access-approve'")[1]
|
|
68
|
+
?.split("pendingVault.kind === 'grant-wizard'")[0] ?? ''
|
|
69
|
+
expect(handlerBlock).toMatch(/deleteSensitiveMessage/)
|
|
70
|
+
expect(handlerBlock).toMatch(/performVaultAccessApproval/)
|
|
71
|
+
// Cache the passphrase so future operations in the same chat
|
|
72
|
+
// don't re-prompt within the TTL window.
|
|
73
|
+
expect(handlerBlock).toMatch(/vaultPassphraseCache\.set/)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('expired-stage path edits the card cleanly, does not silently drop', () => {
|
|
77
|
+
// fails when: the resume path forgets to handle the edge case
|
|
78
|
+
// where the staged access entry's 10-min TTL elapsed between
|
|
79
|
+
// tap-on-locked and passphrase reply. Without this branch the
|
|
80
|
+
// operator types their passphrase, gets nothing visible back,
|
|
81
|
+
// and is confused about whether their secret leaked.
|
|
82
|
+
const handlerBlock =
|
|
83
|
+
gatewaySrc
|
|
84
|
+
.split("pendingVault.kind === 'passphrase-for-access-approve'")[1]
|
|
85
|
+
?.split("pendingVault.kind === 'grant-wizard'")[0] ?? ''
|
|
86
|
+
expect(handlerBlock).toMatch(/expired before you replied|expired/)
|
|
87
|
+
expect(handlerBlock).toMatch(/editMessageText/)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('mint failure (e.g. wrong passphrase) edits the card; does not silent-drop', () => {
|
|
91
|
+
// fails when: performVaultAccessApproval's error branch returns
|
|
92
|
+
// without editing the card. Without the edit, a wrong-passphrase
|
|
93
|
+
// attempt leaves the locked-vault prompt on screen forever and
|
|
94
|
+
// the operator can't tell whether the system saw their reply.
|
|
95
|
+
//
|
|
96
|
+
// Anchor: performVaultAccessApproval's `result.kind === 'error'`
|
|
97
|
+
// branch.
|
|
98
|
+
const mintHelper =
|
|
99
|
+
gatewaySrc.split('async function performVaultAccessApproval')[1]?.split('async function handleVaultRequestAccessCallback')[0] ?? ''
|
|
100
|
+
expect(mintHelper).toMatch(/result\.kind === 'error'/)
|
|
101
|
+
// After error: card edited AND pending entry dropped (no
|
|
102
|
+
// zombie staged request).
|
|
103
|
+
expect(mintHelper).toMatch(/editMessageText[\s\S]{0,400}mint_grant failed/)
|
|
104
|
+
expect(mintHelper).toMatch(/pendingVaultRequestAccesses\.delete/)
|
|
105
|
+
})
|
|
106
|
+
})
|