switchroom 0.14.27 → 0.14.28
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/dist/cli/switchroom.js +20 -4
- package/dist/host-control/main.js +2 -2
- package/package.json +1 -1
- package/telegram-plugin/bridge/bridge.ts +15 -0
- package/telegram-plugin/card-format.ts +7 -4
- package/telegram-plugin/dist/bridge/bridge.js +14 -0
- package/telegram-plugin/dist/gateway/gateway.js +2131 -1729
- package/telegram-plugin/dist/server.js +14 -0
- package/telegram-plugin/gateway/gateway.ts +457 -12
- package/telegram-plugin/history.ts +16 -4
- package/telegram-plugin/permission-title.ts +48 -0
- package/telegram-plugin/secret-detect/patterns.ts +8 -0
- package/telegram-plugin/secret-detect/redact.ts +76 -0
- package/telegram-plugin/tests/card-format.test.ts +16 -0
- package/telegram-plugin/tests/gateway-outbound-redact.test.ts +80 -0
- package/telegram-plugin/tests/gateway-request-secret.test.ts +78 -0
- package/telegram-plugin/tests/history.test.ts +59 -0
- package/telegram-plugin/tests/permission-title.test.ts +68 -0
- package/telegram-plugin/tests/permission-verdict-resume-guard.test.ts +35 -0
- package/telegram-plugin/tests/secret-detect-sanctum.test.ts +115 -0
- package/telegram-plugin/tests/worker-activity-feed.test.ts +15 -0
- package/telegram-plugin/uat/scenarios/jtbd-request-secret-dm.test.ts +101 -0
- package/telegram-plugin/worker-activity-feed.ts +5 -2
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-end UAT for the agent-initiated `request_secret` flow (#2045).
|
|
3
|
+
*
|
|
4
|
+
* The upstream fix behind the 2026-06-01 incident: an agent must never ask
|
|
5
|
+
* the user to paste a secret into chat. Instead it calls `request_secret`,
|
|
6
|
+
* the operator taps [Provide securely] and sends the value once, and the
|
|
7
|
+
* gateway deletes the message + writes it straight to the vault — the raw
|
|
8
|
+
* value never lands in history/logs and is never returned to the agent.
|
|
9
|
+
*
|
|
10
|
+
* This round-trips through real Telegram + real broker + real agent and
|
|
11
|
+
* asserts the FINAL state: (a) the secure card renders with the right
|
|
12
|
+
* button, (b) after the operator provides the value the bot confirms a
|
|
13
|
+
* vault save, (c) the value actually landed in the vault (host-side read),
|
|
14
|
+
* and (d) the raw value never reappears in a bot message.
|
|
15
|
+
*
|
|
16
|
+
* **Skipped by default.** To unskip:
|
|
17
|
+
*
|
|
18
|
+
* 1. Standard UAT preflight (`uat/SETUP.md` §5-6) — test-harness agent live
|
|
19
|
+
* (running build WITH request_secret, #2045), driver session auth'd,
|
|
20
|
+
* env vars set.
|
|
21
|
+
* 2. `SWITCHROOM_VAULT_PASSPHRASE` in env (read-back asserts the value
|
|
22
|
+
* landed; under telegram-id approval mode the save itself is
|
|
23
|
+
* posture-attested and needs no passphrase).
|
|
24
|
+
* 3. Remove `describe.skip`.
|
|
25
|
+
*
|
|
26
|
+
* Cleanup is operator-side: `switchroom vault rm uat/req-secret-target`.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { execFileSync } from "node:child_process";
|
|
30
|
+
import { describe, expect, it } from "vitest";
|
|
31
|
+
import { spinUp } from "../harness.js";
|
|
32
|
+
|
|
33
|
+
const TARGET_KEY = "uat/req-secret-target";
|
|
34
|
+
// Built at runtime so the source never holds a contiguous secret literal.
|
|
35
|
+
const PROVIDED_VALUE = "sentinel-2045-" + "Ab3xK9pQ".repeat(2);
|
|
36
|
+
|
|
37
|
+
function hostVaultGet(key: string): string {
|
|
38
|
+
try {
|
|
39
|
+
return execFileSync("switchroom", ["vault", "get", key], {
|
|
40
|
+
encoding: "utf-8",
|
|
41
|
+
env: { ...process.env },
|
|
42
|
+
}).trim();
|
|
43
|
+
} catch {
|
|
44
|
+
return "";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe.skip("uat: request_secret end-to-end (#2045)", () => {
|
|
49
|
+
it(
|
|
50
|
+
"agent calls request_secret → operator provides → value vaulted, never echoed",
|
|
51
|
+
async () => {
|
|
52
|
+
const sc = await spinUp({ agent: "test-harness" });
|
|
53
|
+
try {
|
|
54
|
+
// 1. Prompt the agent to call request_secret for a key it lacks.
|
|
55
|
+
// (We don't fire the card from the driver — the point is to
|
|
56
|
+
// cover the agent → gateway → capture → vault path.)
|
|
57
|
+
await sc.sendDM(
|
|
58
|
+
`I need an API token but it is NOT in your vault. Use your ` +
|
|
59
|
+
`request_secret MCP tool with key="${TARGET_KEY}", ` +
|
|
60
|
+
`reason="UAT for #2045" to ask me for it securely. Do NOT ask me ` +
|
|
61
|
+
`to paste it as a normal message.`,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// 2. The secure card (request_secret renders "needs a secret").
|
|
65
|
+
const card = await sc.expectMessage(/needs a secret/i, {
|
|
66
|
+
from: "bot",
|
|
67
|
+
timeout: 60_000,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// 3. It must carry a [Provide securely] button.
|
|
71
|
+
const kb = await sc.driver.getKeyboard(sc.botUserId, card.messageId);
|
|
72
|
+
expect(kb).not.toBeNull();
|
|
73
|
+
const provide = kb!
|
|
74
|
+
.flat()
|
|
75
|
+
.find((b) => b.callbackData !== undefined && /provide/i.test(b.text));
|
|
76
|
+
expect(provide, "card should have a [Provide securely] button").toBeDefined();
|
|
77
|
+
|
|
78
|
+
// 4. Tap Provide → the card arms capture + prompts for the value.
|
|
79
|
+
await sc.driver.pressButton(sc.botUserId, card.messageId, provide!.callbackData!);
|
|
80
|
+
await sc.expectMessage(/Send the value for/i, { from: "bot", timeout: 15_000 });
|
|
81
|
+
|
|
82
|
+
// 5. Send the value. The gateway deletes it + writes to the vault.
|
|
83
|
+
await sc.sendDM(PROVIDED_VALUE);
|
|
84
|
+
const confirm = await sc.expectMessage(/saved as/i, {
|
|
85
|
+
from: "bot",
|
|
86
|
+
timeout: 30_000,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// 6. The confirmation references the key, NOT the raw value.
|
|
90
|
+
expect(confirm.text).toContain(`vault:${TARGET_KEY}`);
|
|
91
|
+
expect(confirm.text).not.toContain(PROVIDED_VALUE);
|
|
92
|
+
|
|
93
|
+
// 7. Load-bearing: the value actually landed in the vault.
|
|
94
|
+
expect(hostVaultGet(TARGET_KEY)).toBe(PROVIDED_VALUE);
|
|
95
|
+
} finally {
|
|
96
|
+
await sc.tearDown();
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
300_000,
|
|
100
|
+
);
|
|
101
|
+
});
|
|
@@ -147,7 +147,10 @@ export function renderWorkerActivity(v: WorkerActivityView): string {
|
|
|
147
147
|
const finished = v.state === 'done' || v.state === 'failed'
|
|
148
148
|
|
|
149
149
|
const steps = (v.narrativeLines ?? [])
|
|
150
|
-
.
|
|
150
|
+
// A narrative entry can itself be multi-line (e.g. a worker's final
|
|
151
|
+
// "Done.\n\n## Summary\n…"). Collapse to one visual line so a step
|
|
152
|
+
// slot stays single-line after the per-line markdown strip.
|
|
153
|
+
.map((s) => stripMarkdown(s).replace(/\s+/g, ' ').trim())
|
|
151
154
|
.filter((s) => s.length > 0)
|
|
152
155
|
.map((s) => escapeHtml(truncate(s, STEP_MAX)))
|
|
153
156
|
|
|
@@ -172,7 +175,7 @@ export function renderWorkerActivity(v: WorkerActivityView): string {
|
|
|
172
175
|
} else {
|
|
173
176
|
// Back-compat for direct render callers that pass only latestSummary;
|
|
174
177
|
// the manager always supplies narrativeLines.
|
|
175
|
-
const summary = stripMarkdown(v.latestSummary)
|
|
178
|
+
const summary = stripMarkdown(v.latestSummary).replace(/\s+/g, ' ').trim()
|
|
176
179
|
if (summary.length > 0) {
|
|
177
180
|
lines.push(`<b>→ ${escapeHtml(truncate(summary, STEP_MAX))}</b>`)
|
|
178
181
|
} else {
|