switchroom 0.19.35 → 0.19.37

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.
Files changed (35) hide show
  1. package/dist/cli/skill-validate-pretool.mjs +15 -2
  2. package/dist/cli/switchroom.js +678 -444
  3. package/dist/host-control/main.js +149 -3
  4. package/package.json +4 -2
  5. package/profiles/_base/start.sh.hbs +37 -12
  6. package/telegram-plugin/bridge/bridge.ts +4 -4
  7. package/telegram-plugin/dist/bridge/bridge.js +4 -4
  8. package/telegram-plugin/dist/gateway/gateway.js +317 -95
  9. package/telegram-plugin/dist/server.js +4 -4
  10. package/telegram-plugin/format.ts +100 -29
  11. package/telegram-plugin/gateway/gateway.ts +22 -29
  12. package/telegram-plugin/gateway/ipc-server.ts +18 -15
  13. package/telegram-plugin/gateway/model-command.ts +34 -0
  14. package/telegram-plugin/gateway/outbound-send-path.ts +12 -1
  15. package/telegram-plugin/operator-events.ts +40 -16
  16. package/telegram-plugin/render/unsupported-token-guard.ts +10 -6
  17. package/telegram-plugin/secret-detect/db-uri.ts +90 -0
  18. package/telegram-plugin/secret-detect/index.ts +24 -1
  19. package/telegram-plugin/secret-detect/inert-values.ts +147 -0
  20. package/telegram-plugin/secret-detect/kv-scanner.ts +108 -0
  21. package/telegram-plugin/secret-detect/patterns.ts +24 -4
  22. package/telegram-plugin/tests/format-consistency.test.ts +111 -0
  23. package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +40 -2
  24. package/telegram-plugin/tests/ipc-server-validate-operator.test.ts +20 -11
  25. package/telegram-plugin/tests/mcp-instructions-budget.test.ts +8 -1
  26. package/telegram-plugin/tests/outbound-send-path.test.ts +1 -1
  27. package/telegram-plugin/tests/render/unsupported-token-guard.test.ts +36 -5
  28. package/telegram-plugin/tests/secret-detect-cross-engine.test.ts +263 -0
  29. package/telegram-plugin/tests/secret-detect-write-path.test.ts +403 -0
  30. package/telegram-plugin/tests/turn-flush-safety.test.ts +2 -2
  31. package/vendor/hindsight-memory/scripts/lib/client.py +58 -0
  32. package/vendor/hindsight-memory/scripts/lib/secret_patterns.json +431 -0
  33. package/vendor/hindsight-memory/scripts/lib/secret_redact.py +563 -0
  34. package/vendor/hindsight-memory/scripts/lib/secret_redaction_vectors.json +397 -0
  35. package/vendor/hindsight-memory/scripts/tests/test_secret_redact.py +522 -0
@@ -13,6 +13,7 @@ from pathlib import Path
13
13
  from typing import Optional
14
14
 
15
15
  from .retain_split import part_document_id, part_metadata, split_retain_content
16
+ from .secret_redact import redact, redact_metadata, redact_retain_fields
16
17
 
17
18
  DEFAULT_TIMEOUT = 15 # seconds
18
19
  HEALTH_CHECK_RETRIES = 3
@@ -80,8 +81,50 @@ class HindsightClient:
80
81
  headers["Authorization"] = f"Bearer {self.api_token}"
81
82
  return headers
82
83
 
84
+ @staticmethod
85
+ def _is_memory_write(method: str, path: str) -> bool:
86
+ """True for a request that CREATES memory rows.
87
+
88
+ `POST .../memories` is the write; `POST .../memories/recall` is a
89
+ read that happens to be a POST, and must not be rewritten.
90
+ """
91
+ if method.upper() != "POST":
92
+ return False
93
+ base = path.split("?", 1)[0].rstrip("/")
94
+ return base.endswith("/memories")
95
+
96
+ @staticmethod
97
+ def _redact_write_body(body: Optional[dict]) -> Optional[dict]:
98
+ """Structural backstop — mask secrets in a memory-write body.
99
+
100
+ `retain()` already redacts BEFORE splitting (which is where a
101
+ secret straddling a chunk boundary is still whole). This second
102
+ pass exists so a future caller that reaches `_request` without
103
+ going through `retain()` cannot write a raw credential into a
104
+ bank: every memory row this client creates is emitted from here.
105
+ Redaction is mask-in-place and stable on already-masked text, so
106
+ running it twice is a no-op on the second pass.
107
+ """
108
+ if not isinstance(body, dict):
109
+ return body
110
+ items = body.get("items")
111
+ if not isinstance(items, list):
112
+ return body
113
+ for item in items:
114
+ if not isinstance(item, dict):
115
+ continue
116
+ if isinstance(item.get("content"), str):
117
+ item["content"] = redact(item["content"])
118
+ if isinstance(item.get("context"), str):
119
+ item["context"] = redact(item["context"])
120
+ if isinstance(item.get("metadata"), dict):
121
+ item["metadata"] = redact_metadata(item["metadata"])
122
+ return body
123
+
83
124
  def _request(self, method: str, path: str, body: Optional[dict] = None, timeout: int = DEFAULT_TIMEOUT) -> dict:
84
125
  timeout = self._resolve_timeout(timeout)
126
+ if self._is_memory_write(method, path):
127
+ body = self._redact_write_body(body)
85
128
  url = f"{self.api_url}{path}"
86
129
  data = json.dumps(body).encode() if body else None
87
130
  req = urllib.request.Request(url, data=data, headers=self._headers(), method=method)
@@ -232,6 +275,21 @@ class HindsightClient:
232
275
  ``backfill_transcripts.py`` logs and backs off. The parts already
233
276
  committed are upserted on the next attempt, not duplicated.
234
277
  """
278
+ # ── Secret redaction, BEFORE splitting ────────────────────────
279
+ #
280
+ # Every retain in this plugin (Stop-hook auto-retain, SubagentStop
281
+ # sidechain retain, the pending-backlog drainer, boot
282
+ # reconciliation, transcript backfill) funnels through this one
283
+ # method, so this is the intake chokepoint for memory content —
284
+ # the same property `retain_split` relies on.
285
+ #
286
+ # It runs BEFORE `split_retain_content` on purpose: a credential
287
+ # straddling a 3000-char part boundary is only whole here. The
288
+ # second pass in `_request` is the structural backstop for any
289
+ # caller that never reaches this method.
290
+ content, context, metadata = redact_retain_fields(
291
+ content, context, metadata
292
+ )
235
293
  parts = split_retain_content(content)
236
294
  total = len(parts)
237
295
  response = None
@@ -0,0 +1,431 @@
1
+ {
2
+ "_generated_by": "scripts/gen-secret-patterns.ts",
3
+ "_source_of_truth": "telegram-plugin/secret-detect/patterns.ts",
4
+ "_do_not_edit": "Run `bun scripts/gen-secret-patterns.ts` after changing patterns.ts. `bun lint` fails when this file drifts from the TS table.",
5
+ "patterns": [
6
+ {
7
+ "rule_id": "anthropic_api_key",
8
+ "source": "\\b(sk-ant-[A-Za-z0-9_-]{8,})\\b",
9
+ "ignore_case": false,
10
+ "multiline": false,
11
+ "capture_index": 1,
12
+ "slug_hint": "anthropic_api_key"
13
+ },
14
+ {
15
+ "rule_id": "anthropic_oauth_code",
16
+ "source": "(?:^|\\s)([A-Za-z0-9_-]{20,}#[A-Za-z0-9_-]{20,})(?=\\s|$)",
17
+ "ignore_case": false,
18
+ "multiline": true,
19
+ "capture_index": 1,
20
+ "slug_hint": "anthropic_oauth_code"
21
+ },
22
+ {
23
+ "rule_id": "openai_api_key",
24
+ "source": "\\b(sk-[A-Za-z0-9_-]{20,})\\b",
25
+ "ignore_case": false,
26
+ "multiline": false,
27
+ "capture_index": 1,
28
+ "slug_hint": "openai_api_key"
29
+ },
30
+ {
31
+ "rule_id": "github_pat_classic",
32
+ "source": "\\b(ghp_[A-Za-z0-9]{20,})\\b",
33
+ "ignore_case": false,
34
+ "multiline": false,
35
+ "capture_index": 1,
36
+ "slug_hint": "github_pat"
37
+ },
38
+ {
39
+ "rule_id": "github_pat_fine_grained",
40
+ "source": "\\b(github_pat_[A-Za-z0-9_]{20,})\\b",
41
+ "ignore_case": false,
42
+ "multiline": false,
43
+ "capture_index": 1,
44
+ "slug_hint": "github_pat"
45
+ },
46
+ {
47
+ "rule_id": "slack_token",
48
+ "source": "\\b(xox[baprs]-[A-Za-z0-9-]{10,})\\b",
49
+ "ignore_case": false,
50
+ "multiline": false,
51
+ "capture_index": 1,
52
+ "slug_hint": "slack_token"
53
+ },
54
+ {
55
+ "rule_id": "slack_app_token",
56
+ "source": "\\b(xapp-[A-Za-z0-9-]{10,})\\b",
57
+ "ignore_case": false,
58
+ "multiline": false,
59
+ "capture_index": 1,
60
+ "slug_hint": "slack_app_token"
61
+ },
62
+ {
63
+ "rule_id": "groq_api_key",
64
+ "source": "\\b(gsk_[A-Za-z0-9_-]{10,})\\b",
65
+ "ignore_case": false,
66
+ "multiline": false,
67
+ "capture_index": 1,
68
+ "slug_hint": "groq_api_key"
69
+ },
70
+ {
71
+ "rule_id": "google_api_key",
72
+ "source": "\\b(AIza[0-9A-Za-z\\-_]{20,})\\b",
73
+ "ignore_case": false,
74
+ "multiline": false,
75
+ "capture_index": 1,
76
+ "slug_hint": "google_api_key"
77
+ },
78
+ {
79
+ "rule_id": "perplexity_api_key",
80
+ "source": "\\b(pplx-[A-Za-z0-9_-]{10,})\\b",
81
+ "ignore_case": false,
82
+ "multiline": false,
83
+ "capture_index": 1,
84
+ "slug_hint": "perplexity_api_key"
85
+ },
86
+ {
87
+ "rule_id": "npm_token",
88
+ "source": "\\b(npm_[A-Za-z0-9]{10,})\\b",
89
+ "ignore_case": false,
90
+ "multiline": false,
91
+ "capture_index": 1,
92
+ "slug_hint": "npm_token"
93
+ },
94
+ {
95
+ "rule_id": "telegram_bot_token_prefixed",
96
+ "source": "\\bbot(\\d{6,}:[A-Za-z0-9_-]{20,})\\b",
97
+ "ignore_case": false,
98
+ "multiline": false,
99
+ "capture_index": 1,
100
+ "slug_hint": "telegram_bot_token"
101
+ },
102
+ {
103
+ "rule_id": "telegram_bot_token",
104
+ "source": "\\b(\\d{6,}:[A-Za-z0-9_-]{20,})\\b",
105
+ "ignore_case": false,
106
+ "multiline": false,
107
+ "capture_index": 1,
108
+ "slug_hint": "telegram_bot_token"
109
+ },
110
+ {
111
+ "rule_id": "laravel_sanctum_token",
112
+ "source": "\\b(\\d+\\|[A-Za-z0-9]{40,})\\b",
113
+ "ignore_case": false,
114
+ "multiline": false,
115
+ "capture_index": 1,
116
+ "slug_hint": "api_token"
117
+ },
118
+ {
119
+ "rule_id": "aws_access_key",
120
+ "source": "\\b(AKIA[0-9A-Z]{16})\\b",
121
+ "ignore_case": false,
122
+ "multiline": false,
123
+ "capture_index": 1,
124
+ "slug_hint": "aws_access_key"
125
+ },
126
+ {
127
+ "rule_id": "jwt",
128
+ "source": "\\b(eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,})\\b",
129
+ "ignore_case": false,
130
+ "multiline": false,
131
+ "capture_index": 1,
132
+ "slug_hint": "jwt"
133
+ },
134
+ {
135
+ "rule_id": "slack_webhook",
136
+ "source": "(https://hooks\\.slack\\.com/services/[A-Za-z0-9_/]+)",
137
+ "ignore_case": false,
138
+ "multiline": false,
139
+ "capture_index": 1,
140
+ "slug_hint": "slack_webhook"
141
+ },
142
+ {
143
+ "rule_id": "stripe_live_secret",
144
+ "source": "\\b(sk_live_[A-Za-z0-9]{24,})\\b",
145
+ "ignore_case": false,
146
+ "multiline": false,
147
+ "capture_index": 1,
148
+ "slug_hint": "stripe_key"
149
+ },
150
+ {
151
+ "rule_id": "stripe_restricted",
152
+ "source": "\\b(rk_live_[A-Za-z0-9]{24,})\\b",
153
+ "ignore_case": false,
154
+ "multiline": false,
155
+ "capture_index": 1,
156
+ "slug_hint": "stripe_key"
157
+ },
158
+ {
159
+ "rule_id": "sendgrid_api_key",
160
+ "source": "\\b(SG\\.[A-Za-z0-9_-]{22}\\.[A-Za-z0-9_-]{43})\\b",
161
+ "ignore_case": false,
162
+ "multiline": false,
163
+ "capture_index": 1,
164
+ "slug_hint": "sendgrid_key"
165
+ },
166
+ {
167
+ "rule_id": "gitlab_pat",
168
+ "source": "\\b(glpat-[A-Za-z0-9_-]{20})\\b",
169
+ "ignore_case": false,
170
+ "multiline": false,
171
+ "capture_index": 1,
172
+ "slug_hint": "gitlab_pat"
173
+ },
174
+ {
175
+ "rule_id": "huggingface_token",
176
+ "source": "\\b(hf_[A-Za-z0-9]{34,})\\b",
177
+ "ignore_case": false,
178
+ "multiline": false,
179
+ "capture_index": 1,
180
+ "slug_hint": "huggingface_token"
181
+ },
182
+ {
183
+ "rule_id": "twilio_api_key",
184
+ "source": "\\b(SK[0-9a-f]{32})\\b",
185
+ "ignore_case": false,
186
+ "multiline": false,
187
+ "capture_index": 1,
188
+ "slug_hint": "twilio_api_key"
189
+ },
190
+ {
191
+ "rule_id": "mailgun_key",
192
+ "source": "\\b(key-[0-9a-f]{32})\\b",
193
+ "ignore_case": false,
194
+ "multiline": false,
195
+ "capture_index": 1,
196
+ "slug_hint": "mailgun_key"
197
+ },
198
+ {
199
+ "rule_id": "digitalocean_pat",
200
+ "source": "\\b(dop_v1_[a-f0-9]{64})\\b",
201
+ "ignore_case": false,
202
+ "multiline": false,
203
+ "capture_index": 1,
204
+ "slug_hint": "digitalocean_token"
205
+ },
206
+ {
207
+ "rule_id": "digitalocean_oauth",
208
+ "source": "\\b(doo_v1_[a-f0-9]{64})\\b",
209
+ "ignore_case": false,
210
+ "multiline": false,
211
+ "capture_index": 1,
212
+ "slug_hint": "digitalocean_token"
213
+ },
214
+ {
215
+ "rule_id": "digitalocean_refresh",
216
+ "source": "\\b(dor_v1_[a-f0-9]{64})\\b",
217
+ "ignore_case": false,
218
+ "multiline": false,
219
+ "capture_index": 1,
220
+ "slug_hint": "digitalocean_token"
221
+ },
222
+ {
223
+ "rule_id": "doppler_token",
224
+ "source": "\\b(dp\\.(?:pt|st|ct|sa|scim|audit)\\.[A-Za-z0-9]{40,44})\\b",
225
+ "ignore_case": false,
226
+ "multiline": false,
227
+ "capture_index": 1,
228
+ "slug_hint": "doppler_token"
229
+ },
230
+ {
231
+ "rule_id": "linear_api_key",
232
+ "source": "\\b(lin_api_[A-Za-z0-9]{40})\\b",
233
+ "ignore_case": false,
234
+ "multiline": false,
235
+ "capture_index": 1,
236
+ "slug_hint": "linear_api_key"
237
+ },
238
+ {
239
+ "rule_id": "shopify_access_token",
240
+ "source": "\\b(shpat_[a-fA-F0-9]{32})\\b",
241
+ "ignore_case": false,
242
+ "multiline": false,
243
+ "capture_index": 1,
244
+ "slug_hint": "shopify_token"
245
+ },
246
+ {
247
+ "rule_id": "shopify_shared_secret",
248
+ "source": "\\b(shpss_[a-fA-F0-9]{32})\\b",
249
+ "ignore_case": false,
250
+ "multiline": false,
251
+ "capture_index": 1,
252
+ "slug_hint": "shopify_token"
253
+ },
254
+ {
255
+ "rule_id": "shopify_private_app",
256
+ "source": "\\b(shppa_[a-fA-F0-9]{32})\\b",
257
+ "ignore_case": false,
258
+ "multiline": false,
259
+ "capture_index": 1,
260
+ "slug_hint": "shopify_token"
261
+ },
262
+ {
263
+ "rule_id": "square_access_token",
264
+ "source": "\\b(sq0atp-[A-Za-z0-9_-]{22})\\b",
265
+ "ignore_case": false,
266
+ "multiline": false,
267
+ "capture_index": 1,
268
+ "slug_hint": "square_token"
269
+ },
270
+ {
271
+ "rule_id": "square_oauth_secret",
272
+ "source": "\\b(sq0csp-[A-Za-z0-9_-]{43})\\b",
273
+ "ignore_case": false,
274
+ "multiline": false,
275
+ "capture_index": 1,
276
+ "slug_hint": "square_token"
277
+ },
278
+ {
279
+ "rule_id": "newrelic_key",
280
+ "source": "\\b(NRAK-[A-Z0-9]{27})\\b",
281
+ "ignore_case": false,
282
+ "multiline": false,
283
+ "capture_index": 1,
284
+ "slug_hint": "newrelic_key"
285
+ },
286
+ {
287
+ "rule_id": "notion_token",
288
+ "source": "\\b(ntn_[A-Za-z0-9]{46})\\b",
289
+ "ignore_case": false,
290
+ "multiline": false,
291
+ "capture_index": 1,
292
+ "slug_hint": "notion_token"
293
+ },
294
+ {
295
+ "rule_id": "planetscale_password",
296
+ "source": "\\b(pscale_pw_[A-Za-z0-9_.-]{43})\\b",
297
+ "ignore_case": false,
298
+ "multiline": false,
299
+ "capture_index": 1,
300
+ "slug_hint": "planetscale_token"
301
+ },
302
+ {
303
+ "rule_id": "planetscale_token",
304
+ "source": "\\b(pscale_tkn_[A-Za-z0-9_.-]{43})\\b",
305
+ "ignore_case": false,
306
+ "multiline": false,
307
+ "capture_index": 1,
308
+ "slug_hint": "planetscale_token"
309
+ },
310
+ {
311
+ "rule_id": "supabase_service_key",
312
+ "source": "\\b(sbp_[a-f0-9]{40})\\b",
313
+ "ignore_case": false,
314
+ "multiline": false,
315
+ "capture_index": 1,
316
+ "slug_hint": "supabase_key"
317
+ },
318
+ {
319
+ "rule_id": "atlassian_token",
320
+ "source": "\\b(ATATT[A-Za-z0-9_\\-=]{20,})\\b",
321
+ "ignore_case": false,
322
+ "multiline": false,
323
+ "capture_index": 1,
324
+ "slug_hint": "atlassian_token"
325
+ },
326
+ {
327
+ "rule_id": "dropbox_token",
328
+ "source": "\\b(sl\\.[A-Za-z0-9_-]{130,})",
329
+ "ignore_case": false,
330
+ "multiline": false,
331
+ "capture_index": 1,
332
+ "slug_hint": "dropbox_token"
333
+ },
334
+ {
335
+ "rule_id": "databricks_token",
336
+ "source": "\\b(dapi[a-f0-9]{32})\\b",
337
+ "ignore_case": false,
338
+ "multiline": false,
339
+ "capture_index": 1,
340
+ "slug_hint": "databricks_token"
341
+ },
342
+ {
343
+ "rule_id": "grafana_service_account",
344
+ "source": "\\b(glsa_[A-Za-z0-9]{32}_[a-fA-F0-9]{8})\\b",
345
+ "ignore_case": false,
346
+ "multiline": false,
347
+ "capture_index": 1,
348
+ "slug_hint": "grafana_token"
349
+ },
350
+ {
351
+ "rule_id": "pypi_token",
352
+ "source": "\\b(pypi-AgEIcHlwaS[A-Za-z0-9_-]{50,})",
353
+ "ignore_case": false,
354
+ "multiline": false,
355
+ "capture_index": 1,
356
+ "slug_hint": "pypi_token"
357
+ },
358
+ {
359
+ "rule_id": "aws_temp_access_key",
360
+ "source": "\\b(ASIA[0-9A-Z]{16})\\b",
361
+ "ignore_case": false,
362
+ "multiline": false,
363
+ "capture_index": 1,
364
+ "slug_hint": "aws_access_key"
365
+ },
366
+ {
367
+ "rule_id": "gcp_oauth_token",
368
+ "source": "\\b(ya29\\.[A-Za-z0-9_-]{30,})",
369
+ "ignore_case": false,
370
+ "multiline": false,
371
+ "capture_index": 1,
372
+ "slug_hint": "gcp_oauth_token"
373
+ },
374
+ {
375
+ "rule_id": "env_key_value",
376
+ "source": "\\b([A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD))\\b\\s*[=:]\\s*([\"']?)([^\\s\"'\\\\]+)\\2",
377
+ "ignore_case": false,
378
+ "multiline": false,
379
+ "capture_index": 3,
380
+ "slug_hint": "env"
381
+ },
382
+ {
383
+ "rule_id": "json_secret_field",
384
+ "source": "\"(?:apiKey|token|secret|password|passwd|accessToken|refreshToken)\"\\s*:\\s*\"([^\"]+)\"",
385
+ "ignore_case": false,
386
+ "multiline": false,
387
+ "capture_index": 1,
388
+ "slug_hint": "json_secret"
389
+ },
390
+ {
391
+ "rule_id": "cli_flag",
392
+ "source": "--(?:api[-_]?key|hook[-_]?token|token|secret|password|passwd)\\s+([\"']?)([^\\s\"']+)\\1",
393
+ "ignore_case": false,
394
+ "multiline": false,
395
+ "capture_index": 2,
396
+ "slug_hint": "cli_flag"
397
+ },
398
+ {
399
+ "rule_id": "bearer_auth_header",
400
+ "source": "Authorization\\s*[:=]\\s*Bearer\\s+([A-Za-z0-9._\\-+=]+)",
401
+ "ignore_case": true,
402
+ "multiline": false,
403
+ "capture_index": 1,
404
+ "slug_hint": "bearer_token"
405
+ },
406
+ {
407
+ "rule_id": "bearer_loose",
408
+ "source": "\\bBearer\\s+([A-Za-z0-9._\\-+=]{18,})\\b",
409
+ "ignore_case": true,
410
+ "multiline": false,
411
+ "capture_index": 1,
412
+ "slug_hint": "bearer_token"
413
+ },
414
+ {
415
+ "rule_id": "basic_auth_header",
416
+ "source": "Authorization\\s*[:=]\\s*Basic\\s+([A-Za-z0-9+/=]{8,})",
417
+ "ignore_case": true,
418
+ "multiline": false,
419
+ "capture_index": 1,
420
+ "slug_hint": "basic_auth"
421
+ },
422
+ {
423
+ "rule_id": "pem_private_key",
424
+ "source": "-----BEGIN [A-Z ]*PRIVATE KEY-----[\\s\\S]+?-----END [A-Z ]*PRIVATE KEY-----",
425
+ "ignore_case": false,
426
+ "multiline": false,
427
+ "capture_index": 0,
428
+ "slug_hint": "pem_private_key"
429
+ }
430
+ ]
431
+ }