switchroom 0.19.18 → 0.19.22
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/agent-scheduler/index.js +2 -1
- package/dist/auth-broker/index.js +56 -1
- package/dist/cli/drive-write-pretool.mjs +48 -5
- package/dist/cli/ms-365-write-pretool.mjs +40 -2
- package/dist/cli/notion-write-pretool.mjs +2 -1
- package/dist/cli/switchroom.js +5242 -2239
- package/dist/host-control/main.js +12241 -11375
- package/dist/vault/approvals/kernel-server.js +113 -7
- package/dist/vault/broker/server.js +259 -76
- package/package.json +6 -3
- package/profiles/_base/start.sh.hbs +61 -1
- package/skills/switchroom-release/SKILL.md +103 -20
- package/telegram-plugin/bridge/bridge.ts +14 -0
- package/telegram-plugin/card-format.ts +92 -3
- package/telegram-plugin/dist/bridge/bridge.js +13 -0
- package/telegram-plugin/dist/gateway/gateway.js +2356 -1159
- package/telegram-plugin/dist/server.js +13 -0
- package/telegram-plugin/edit-flood-fuse.ts +477 -0
- package/telegram-plugin/format.ts +19 -7
- package/telegram-plugin/gateway/always-allow-persist-queue.ts +97 -11
- package/telegram-plugin/gateway/boot-sweep-gate.ts +164 -0
- package/telegram-plugin/gateway/callback-query-handlers.ts +454 -81
- package/telegram-plugin/gateway/gateway.ts +66 -56
- package/telegram-plugin/gateway/inbound-interceptors.ts +27 -4
- package/telegram-plugin/gateway/missed-approvals-store.ts +66 -17
- package/telegram-plugin/gateway/narrative-lane.ts +49 -3
- package/telegram-plugin/gateway/pending-card-store.ts +46 -16
- package/telegram-plugin/gateway/scoped-grant-store.ts +39 -14
- package/telegram-plugin/gateway/status-pin-api.ts +145 -0
- package/telegram-plugin/gateway/store-file.ts +244 -0
- package/telegram-plugin/hooks/subagent-tracker-posttool.mjs +325 -45
- package/telegram-plugin/hooks/tool-label-pretool.mjs +88 -2
- package/telegram-plugin/retry-api-call.ts +15 -2
- package/telegram-plugin/send-gate.ts +1 -1
- package/telegram-plugin/status-no-truncate.ts +64 -1
- package/telegram-plugin/status-pin-driver.ts +50 -27
- package/telegram-plugin/status-pin.ts +43 -5
- package/telegram-plugin/tests/activity-card-send-gate.test.ts +275 -0
- package/telegram-plugin/tests/activity-card-wiring.test.ts +16 -7
- package/telegram-plugin/tests/boot-pin-sweep-wiring.test.ts +101 -0
- package/telegram-plugin/tests/boot-sweep-gate.test.ts +293 -0
- package/telegram-plugin/tests/boot-version-string.test.ts +0 -0
- package/telegram-plugin/tests/bridge-tool-parity.test.ts +95 -0
- package/telegram-plugin/tests/edit-flood-fuse.test.ts +431 -0
- package/telegram-plugin/tests/pinned-card-collapse.test.ts +356 -0
- package/telegram-plugin/tests/status-pin-api.test.ts +178 -0
- package/telegram-plugin/tests/status-pin-boot-recovery.test.ts +94 -11
- package/telegram-plugin/tests/status-pin.test.ts +106 -5
- package/telegram-plugin/tests/store-atomic-write.test.ts +411 -0
- package/telegram-plugin/tests/subagent-tracker-hooks.test.ts +631 -1
- package/telegram-plugin/tests/tool-activity-summary.test.ts +28 -12
- package/telegram-plugin/tests/tool-label-pretool.test.ts +94 -0
- package/telegram-plugin/tests/vault-approval-posture.test.ts +6 -1
- package/telegram-plugin/tests/vault-passphrase-retry.test.ts +666 -0
- package/telegram-plugin/tests/vault-request-access-unlock-resume.test.ts +42 -21
- package/telegram-plugin/tests/worker-feed-coalesce.test.ts +233 -1
- package/telegram-plugin/tests/worker-feed-repeat-steps.test.ts +147 -0
- package/telegram-plugin/tool-activity-summary.ts +85 -13
- package/telegram-plugin/worker-activity-feed.ts +56 -2
- package/vendor/hindsight-memory/scripts/drain_pending.py +847 -67
- package/vendor/hindsight-memory/scripts/lib/client.py +124 -0
- package/vendor/hindsight-memory/scripts/lib/pending.py +944 -33
- package/vendor/hindsight-memory/scripts/lib/retain_split.py +460 -0
- package/vendor/hindsight-memory/scripts/recall.py +74 -5
- package/vendor/hindsight-memory/scripts/session_start.py +48 -0
- package/vendor/hindsight-memory/scripts/tests/test_client_document_exists.py +470 -0
- package/vendor/hindsight-memory/scripts/tests/test_pending_drops.py +2275 -0
- package/vendor/hindsight-memory/scripts/tests/test_pending_failure_class.py +105 -0
- package/vendor/hindsight-memory/scripts/tests/test_pending_wedge.py +300 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_degraded_notice.py +365 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_envelope_strip_telemetry.py +12 -4
- package/vendor/hindsight-memory/scripts/tests/test_recall_transcript_fallback.py +27 -2
- package/vendor/hindsight-memory/scripts/tests/test_retain_split.py +438 -0
- package/vendor/hindsight-memory/scripts/tests/test_session_start_version_skew.py +204 -0
- package/vendor/hindsight-memory/tests/test_drain_pending.py +130 -8
- package/vendor/hindsight-memory/tests/test_pending.py +32 -7
|
@@ -15,14 +15,26 @@ import {
|
|
|
15
15
|
type SessionActivityHeader,
|
|
16
16
|
} from "../tool-activity-summary.js";
|
|
17
17
|
import { STATUS_ROLLING_LINES, STATUS_LINE_MAX } from "../status-no-truncate.js";
|
|
18
|
+
import { COLLAPSE_SAFE_SEPARATOR } from "../card-format.js";
|
|
19
|
+
|
|
20
|
+
/** The pinned-card collapse separator (#3666) every status-card line now
|
|
21
|
+
* carries before its hard break. Aliased for readable goldens. */
|
|
22
|
+
const NB = COLLAPSE_SAFE_SEPARATOR;
|
|
18
23
|
|
|
19
24
|
describe("describeToolUse — friendly per-tool rendering (draft-mirror)", () => {
|
|
20
25
|
it("Bash uses the model-authored description verbatim, never the command", () => {
|
|
21
26
|
expect(
|
|
22
27
|
describeToolUse("Bash", { command: "ls -la /tmp", description: "List workspace" }),
|
|
23
28
|
).toBe("List workspace");
|
|
24
|
-
// No description →
|
|
25
|
-
|
|
29
|
+
// No description → sanitised PROGRAM-ONLY derivation, never the raw
|
|
30
|
+
// command (its args routinely carry tokens and private paths). The old
|
|
31
|
+
// behavior was a constant "Running a command" for every Bash call, which
|
|
32
|
+
// froze the sub-agent step feed on one line for a whole job.
|
|
33
|
+
expect(describeToolUse("Bash", { command: "grep -r foo ." })).toBe("Running grep");
|
|
34
|
+
// Nothing safe to derive → the generic label is still the floor.
|
|
35
|
+
expect(describeToolUse("Bash", { command: "$(cat /run/secrets/token) --x" })).toBe(
|
|
36
|
+
"Running a command",
|
|
37
|
+
);
|
|
26
38
|
});
|
|
27
39
|
|
|
28
40
|
it("Read/Edit/Write render the file basename, not the full path", () => {
|
|
@@ -88,10 +100,10 @@ describe("appendActivityLine + renderActivityFeed — accumulating activity feed
|
|
|
88
100
|
"**→ Reading gateway.ts**",
|
|
89
101
|
);
|
|
90
102
|
expect(appendActivityLine(lines, "mcp__hindsight__reflect", { query: "x" })).toBe(
|
|
91
|
-
|
|
103
|
+
`~~_✓ Reading gateway.ts_~~${NB} \n**→ Searching memory**`,
|
|
92
104
|
);
|
|
93
105
|
expect(appendActivityLine(lines, "Bash", { command: "ls", description: "List workspace" })).toBe(
|
|
94
|
-
|
|
106
|
+
`~~_✓ Reading gateway.ts_~~${NB} \n~~_✓ Searching memory_~~${NB} \n**→ List workspace**`,
|
|
95
107
|
);
|
|
96
108
|
});
|
|
97
109
|
|
|
@@ -114,7 +126,7 @@ describe("appendActivityLine + renderActivityFeed — accumulating activity feed
|
|
|
114
126
|
const lines = Array.from({ length: total }, (_, i) => `Action ${i + 1}`);
|
|
115
127
|
const out = renderActivityFeed(lines)!;
|
|
116
128
|
const hidden = total - STATUS_ROLLING_LINES;
|
|
117
|
-
expect(out.startsWith(`_✓ +${hidden} earlier…_ \n`)).toBe(true);
|
|
129
|
+
expect(out.startsWith(`_✓ +${hidden} earlier…_${NB} \n`)).toBe(true);
|
|
118
130
|
// Only the last STATUS_ROLLING_LINES actions are shown; older ones collapsed.
|
|
119
131
|
const firstVisible = total - STATUS_ROLLING_LINES + 1;
|
|
120
132
|
expect(out).toContain(`~~_✓ Action ${firstVisible}_~~`);
|
|
@@ -149,7 +161,7 @@ describe("appendActivityLine + renderActivityFeed — accumulating activity feed
|
|
|
149
161
|
const lines = ["Reading a.ts", "Searching memory", "Running a command"];
|
|
150
162
|
const out = renderActivityFeed(lines, true)!;
|
|
151
163
|
expect(out).toBe(
|
|
152
|
-
|
|
164
|
+
`~~_✓ Reading a.ts_~~${NB} \n~~_✓ Searching memory_~~${NB} \n~~_✓ Running a command_~~`,
|
|
153
165
|
);
|
|
154
166
|
expect(out).not.toContain("→"); // no in-progress arrow anywhere
|
|
155
167
|
});
|
|
@@ -188,7 +200,7 @@ describe("appendActivityLine + renderActivityFeed — accumulating activity feed
|
|
|
188
200
|
describe("liveSuffix (heartbeat)", () => {
|
|
189
201
|
it("appends the suffix to the newest in-progress line only", () => {
|
|
190
202
|
expect(renderActivityFeed(["Reading a.ts", "Running a command"], false, " · 18s")).toBe(
|
|
191
|
-
|
|
203
|
+
`~~_✓ Reading a.ts_~~${NB} \n**→ Running a command · 18s**`,
|
|
192
204
|
);
|
|
193
205
|
});
|
|
194
206
|
it("single live line gets the suffix", () => {
|
|
@@ -200,7 +212,7 @@ describe("appendActivityLine + renderActivityFeed — accumulating activity feed
|
|
|
200
212
|
const out = renderActivityFeed(["Reading a.ts", "Running a command"], true, " · 18s")!;
|
|
201
213
|
expect(out).not.toContain("·");
|
|
202
214
|
expect(out).not.toContain("→");
|
|
203
|
-
expect(out).toBe(
|
|
215
|
+
expect(out).toBe(`~~_✓ Reading a.ts_~~${NB} \n~~_✓ Running a command_~~`);
|
|
204
216
|
});
|
|
205
217
|
it("default empty suffix is byte-identical to no suffix", () => {
|
|
206
218
|
expect(renderActivityFeed(["Reading a.ts"], false, "")).toBe(
|
|
@@ -215,7 +227,7 @@ describe("appendActivityLabel — precomputed label feed (tool_label path)", ()
|
|
|
215
227
|
const lines: string[] = [];
|
|
216
228
|
expect(appendActivityLabel(lines, "Searching memory")).toBe("**→ Searching memory**");
|
|
217
229
|
expect(appendActivityLabel(lines, "List workspace")).toBe(
|
|
218
|
-
|
|
230
|
+
`~~_✓ Searching memory_~~${NB} \n**→ List workspace**`,
|
|
219
231
|
);
|
|
220
232
|
// consecutive dup collapses
|
|
221
233
|
appendActivityLabel(lines, "List workspace");
|
|
@@ -410,7 +422,7 @@ describe("renderActivityFeedWithNested — foreground sub-agent nesting (Model A
|
|
|
410
422
|
it("stepCount footer appears on final=true with no children (delegates to flat render)", () => {
|
|
411
423
|
const out = renderActivityFeedWithNested(["Reading a.ts"], [], true, "", 3)!;
|
|
412
424
|
expect(out).toContain("_✓ 3 steps_");
|
|
413
|
-
expect(out).toBe(
|
|
425
|
+
expect(out).toBe(`~~_✓ Reading a.ts_~~${NB} \n_✓ 3 steps_`);
|
|
414
426
|
});
|
|
415
427
|
|
|
416
428
|
// Liveness-driven feed open (dark-turn fix): a turn that emits no tool_label
|
|
@@ -1045,7 +1057,11 @@ describe("status-card body stacks like a reply (#2669 rich-message renderer)", (
|
|
|
1045
1057
|
it("multiple step bullets stack on their own lines (no soft-break collapse)", () => {
|
|
1046
1058
|
const out = renderActivityFeed(["Reading a.ts", "Searching memory", "Running tests"], true)!;
|
|
1047
1059
|
const lines = out.split(" \n");
|
|
1048
|
-
expect(lines).toEqual([
|
|
1060
|
+
expect(lines).toEqual([
|
|
1061
|
+
`~~_✓ Reading a.ts_~~${NB}`,
|
|
1062
|
+
`~~_✓ Searching memory_~~${NB}`,
|
|
1063
|
+
"~~_✓ Running tests_~~",
|
|
1064
|
+
]);
|
|
1049
1065
|
// Not a single lone `\n` survives to collapse two bullets.
|
|
1050
1066
|
expect(out).not.toMatch(/(?<! {2})\n/);
|
|
1051
1067
|
});
|
|
@@ -1069,6 +1085,6 @@ describe("status-card body stacks like a reply (#2669 rich-message renderer)", (
|
|
|
1069
1085
|
// prose by escapeStepLine; the inline-markup-survives case is covered by
|
|
1070
1086
|
// the stackCardLines unit test, which operates on pre-rendered lines.)
|
|
1071
1087
|
const out = renderActivityFeed(["Reading a.ts", "Running tests"], false)!;
|
|
1072
|
-
expect(out).toBe(
|
|
1088
|
+
expect(out).toBe(`~~_✓ Reading a.ts_~~${NB} \n**→ Running tests**`);
|
|
1073
1089
|
});
|
|
1074
1090
|
});
|
|
@@ -68,3 +68,97 @@ describe('computeLabel — surface-tool suppression is key-agnostic', () => {
|
|
|
68
68
|
expect(computeLabel('mcp__clerk-telegram__get_recent_messages', {})).toBe('Reading chat history')
|
|
69
69
|
})
|
|
70
70
|
})
|
|
71
|
+
|
|
72
|
+
// ─── Bash label without a model-authored description ─────────────────────
|
|
73
|
+
//
|
|
74
|
+
// `description` is OPTIONAL, so a sub-agent that never writes one used to
|
|
75
|
+
// produce the constant "Running a command" for every Bash call — the worker
|
|
76
|
+
// feed then deduped every repeat away and the card froze on one line for the
|
|
77
|
+
// whole job. The fallback derives a label from the command, but ONLY through
|
|
78
|
+
// an allowlist: program basename, plus one bare subcommand for known
|
|
79
|
+
// multiplexers. Command lines carry tokens, passwords, credential URLs and
|
|
80
|
+
// private paths, and this string is rendered into a Telegram message.
|
|
81
|
+
describe('computeLabel — Bash fallback when description is omitted', () => {
|
|
82
|
+
it('prefers the model-authored description when present', () => {
|
|
83
|
+
expect(computeLabel('Bash', { command: 'git push origin main', description: 'Push branch' }))
|
|
84
|
+
.toBe('Push branch')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('derives the program name when description is missing', () => {
|
|
88
|
+
expect(computeLabel('Bash', { command: 'grep -r foo .' })).toBe('Running grep')
|
|
89
|
+
expect(computeLabel('Bash', { command: 'ls -la /var/log' })).toBe('Running ls')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('adds a bare subcommand for known multiplexers', () => {
|
|
93
|
+
expect(computeLabel('Bash', { command: 'git status --short' })).toBe('Running git status')
|
|
94
|
+
expect(computeLabel('Bash', { command: 'docker ps -a' })).toBe('Running docker ps')
|
|
95
|
+
expect(computeLabel('Bash', { command: 'npm run lint' })).toBe('Running npm run')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('produces DISTINCT labels for distinct commands (the frozen-card fix)', () => {
|
|
99
|
+
const labels = [
|
|
100
|
+
'git status', 'npm test', 'ls /tmp', 'grep -n x y', 'docker logs c',
|
|
101
|
+
].map((c) => computeLabel('Bash', { command: c }))
|
|
102
|
+
expect(new Set(labels).size).toBe(labels.length)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('skips navigation prefixes and wrappers to the real program', () => {
|
|
106
|
+
expect(computeLabel('Bash', { command: 'cd /srv/app && git pull' })).toBe('Running git pull')
|
|
107
|
+
expect(computeLabel('Bash', { command: 'sudo systemctl restart nginx' }))
|
|
108
|
+
.toBe('Running systemctl restart')
|
|
109
|
+
expect(computeLabel('Bash', { command: 'FOO=bar deploy.sh' })).toBe('Running deploy.sh')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('NEVER echoes arguments, flag values, URLs, or env assignments', () => {
|
|
113
|
+
// Token-shaped literals are assembled at runtime: GitHub Push Protection
|
|
114
|
+
// and check-no-pii-secrets both reject contiguous ones, fake or not
|
|
115
|
+
// (pattern from secret-detect-secretlint.test.ts).
|
|
116
|
+
const fakeAnthropic = 'sk-' + 'ant-oat01-FAKEVALUE'
|
|
117
|
+
const cases = [
|
|
118
|
+
'curl -H "Authorization: Bearer ' + 'sk-' + 'live-abcdef123456" https://api.example.com/v1/x',
|
|
119
|
+
'psql postgres://user:hunter2@db.internal:5432/prod -c "select 1"',
|
|
120
|
+
'aws s3 cp s3://private-bucket/key.pem . --profile prod',
|
|
121
|
+
'export TOKEN=ghp_AAAABBBBCCCCDDDD && ./run',
|
|
122
|
+
`echo "${fakeAnthropic}" > /root/.creds`,
|
|
123
|
+
'ssh deploy@10.0.0.7 -i /root/.ssh/id_ed25519',
|
|
124
|
+
'git clone https://x-access-token:ghs_SECRET@github.com/o/r.git',
|
|
125
|
+
]
|
|
126
|
+
const forbidden = /sk-|ghp_|ghs_|hunter2|Bearer|SECRET|10\.0\.0\.7|@|:\/\/|=/
|
|
127
|
+
for (const command of cases) {
|
|
128
|
+
const label = computeLabel('Bash', { command })
|
|
129
|
+
expect(label).not.toBeNull()
|
|
130
|
+
expect(label).not.toMatch(forbidden)
|
|
131
|
+
// Whatever it says, it must be short and made of the allowlisted shape.
|
|
132
|
+
expect(label).toMatch(/^Running (a command|[A-Za-z][A-Za-z0-9._+-]{0,19}( [a-z][a-z-]{1,15})?)$/)
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('never inspects a heredoc / multiline body', () => {
|
|
137
|
+
const label = computeLabel('Bash', {
|
|
138
|
+
command: 'cat <<EOF > /tmp/x\nAPI_KEY=sk-live-shouldnotleak\nEOF',
|
|
139
|
+
})
|
|
140
|
+
expect(label).toBe('Running cat')
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('falls back to the generic label when nothing safe can be derived', () => {
|
|
144
|
+
expect(computeLabel('Bash', { command: '$(cat /run/secrets/token) --x' })).toBe('Running a command')
|
|
145
|
+
expect(computeLabel('Bash', {})).toBe('Running a command')
|
|
146
|
+
expect(computeLabel('Bash', { command: ' ' })).toBe('Running a command')
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
describe('computeLabel — Bash subcommand is never a flag value', () => {
|
|
151
|
+
it('only accepts the token IMMEDIATELY after a multiplexer', () => {
|
|
152
|
+
// A flag value (a private context/host/profile name) must never surface.
|
|
153
|
+
expect(computeLabel('Bash', { command: 'docker --context prod-internal ps' }))
|
|
154
|
+
.toBe('Running docker')
|
|
155
|
+
expect(computeLabel('Bash', { command: 'git -C /srv/private-app status' }))
|
|
156
|
+
.toBe('Running git')
|
|
157
|
+
expect(computeLabel('Bash', { command: 'aws --profile customer-acme s3 ls' }))
|
|
158
|
+
.toBe('Running aws')
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('still reads the plain subcommand form', () => {
|
|
162
|
+
expect(computeLabel('Bash', { command: 'git commit -m "wip"' })).toBe('Running git commit')
|
|
163
|
+
})
|
|
164
|
+
})
|
|
@@ -101,7 +101,12 @@ describe('handleVaultRequestAccessCallback — posture branch', () => {
|
|
|
101
101
|
it('passphrase-mode branch unchanged: cache lookup + prompt still present', () => {
|
|
102
102
|
const approveBlock = sliceAccessApproveBlock()
|
|
103
103
|
expect(approveBlock).toMatch(/vaultPassphraseCache\.get\(pending\.chat_id\)/)
|
|
104
|
-
|
|
104
|
+
// #3627: the prompt COPY moved into the shared
|
|
105
|
+
// `buildAccessPassphrasePromptText` builder (so the first prompt and the
|
|
106
|
+
// wrong-passphrase re-prompt can't drift). The passphrase-mode branch
|
|
107
|
+
// must still route into it.
|
|
108
|
+
expect(approveBlock).toMatch(/sendAccessPassphrasePrompt\(/)
|
|
109
|
+
expect(gatewaySrc).toMatch(/Reply with your passphrase/i)
|
|
105
110
|
expect(approveBlock).toMatch(/passphrase-for-access-approve/)
|
|
106
111
|
// Pinned: the queued-drain path passes the typed passphrase via
|
|
107
112
|
// the new attestation shape `{ kind: 'passphrase', passphrase }`.
|