@vellumai/assistant 0.12.2 → 0.12.3-staging.1

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 (298) hide show
  1. package/.env.example +2 -0
  2. package/AGENTS.md +1 -1
  3. package/docs/architecture/integrations.md +7 -0
  4. package/docs/architecture/memory.md +17 -2
  5. package/docs/credential-execution-service.md +1 -1
  6. package/docs/desktop-browser-cli.md +7 -3
  7. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
  8. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
  9. package/node_modules/@vellumai/environments/src/shell.test.ts +21 -0
  10. package/node_modules/@vellumai/environments/src/shell.ts +24 -0
  11. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
  12. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
  13. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +8 -2
  14. package/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
  15. package/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
  16. package/openapi.yaml +54 -5
  17. package/package.json +1 -1
  18. package/scripts/smoke-desktop-browser-cli.ts +1 -0
  19. package/scripts/sync-llm-catalog.ts +3 -0
  20. package/scripts/voice-ttft-spike.ts +2 -2
  21. package/src/__tests__/agent-loop.test.ts +259 -0
  22. package/src/__tests__/agent-wake-delegation-prompt.test.ts +64 -2
  23. package/src/__tests__/anthropic-provider.test.ts +55 -0
  24. package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
  25. package/src/__tests__/attachments-store.test.ts +22 -3
  26. package/src/__tests__/channel-approval.test.ts +9 -14
  27. package/src/__tests__/channel-reply-delivery.test.ts +56 -0
  28. package/src/__tests__/chat-credential-redaction.test.ts +23 -0
  29. package/src/__tests__/computer-use-screenshot-attachments.test.ts +411 -0
  30. package/src/__tests__/computer-use-screenshot-selection.test.ts +50 -0
  31. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
  32. package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -0
  33. package/src/__tests__/conversation-agent-loop.test.ts +257 -6
  34. package/src/__tests__/conversation-attachments.test.ts +106 -0
  35. package/src/__tests__/conversation-fork-crud.test.ts +191 -0
  36. package/src/__tests__/conversation-rendered-delegation-state.test.ts +276 -0
  37. package/src/__tests__/credential-execution-client.test.ts +70 -53
  38. package/src/__tests__/db-conversation-tool-surface.test.ts +221 -0
  39. package/src/__tests__/events-tail-route.test.ts +33 -0
  40. package/src/__tests__/history-repair.test.ts +53 -0
  41. package/src/__tests__/inference-profile-session-handler.test.ts +26 -0
  42. package/src/__tests__/list-messages-tool-merge.test.ts +85 -1
  43. package/src/__tests__/llm-catalog-parity.test.ts +27 -4
  44. package/src/__tests__/oauth-apps-routes.test.ts +1 -0
  45. package/src/__tests__/oauth-commands-routes.test.ts +114 -101
  46. package/src/__tests__/oauth-connect-orchestrator.test.ts +2 -0
  47. package/src/__tests__/oauth-provider-serializer.test.ts +1 -0
  48. package/src/__tests__/oauth-providers-routes.test.ts +2 -0
  49. package/src/__tests__/persist-media-references.test.ts +50 -0
  50. package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
  51. package/src/__tests__/run-conversation-turn-persistence.test.ts +138 -1
  52. package/src/__tests__/schedule-routes.test.ts +20 -0
  53. package/src/__tests__/scheduler-result-notification.test.ts +23 -4
  54. package/src/__tests__/script-proxy-certs.test.ts +1 -1
  55. package/src/__tests__/secret-routes-platform-proxy.test.ts +86 -31
  56. package/src/__tests__/secret-routes-scrub.test.ts +22 -27
  57. package/src/__tests__/secret-scanner.test.ts +20 -0
  58. package/src/__tests__/secure-keys-managed-failover.test.ts +8 -0
  59. package/src/__tests__/secure-keys.test.ts +7 -3
  60. package/src/__tests__/server-tool-pairing.test.ts +107 -0
  61. package/src/__tests__/skills.test.ts +5 -4
  62. package/src/__tests__/subagent-tool-gate-mode.test.ts +226 -0
  63. package/src/__tests__/terminal-tools.test.ts +8 -0
  64. package/src/__tests__/tool-result-follow-up.test.ts +99 -0
  65. package/src/__tests__/tool-result-metadata-plumbing.test.ts +63 -0
  66. package/src/__tests__/unicode.test.ts +36 -0
  67. package/src/agent/loop.ts +37 -12
  68. package/src/agent/tool-result-follow-up.ts +70 -0
  69. package/src/api/attachment-provenance.test.ts +68 -0
  70. package/src/api/computer-use-tool.test.ts +55 -0
  71. package/src/api/computer-use-tool.ts +27 -0
  72. package/src/api/events/assistant-outbound-attachment.ts +3 -0
  73. package/src/api/events/desktop-activity-changed.ts +10 -0
  74. package/src/api/events/question-request.ts +1 -0
  75. package/src/api/index.ts +11 -0
  76. package/src/api/responses/conversation-message.ts +3 -0
  77. package/src/approvals/approval-primitive.ts +5 -2
  78. package/src/approvals/scoped-approval-grants.ts +6 -2
  79. package/src/browser/virtual-desktop-target.ts +1 -2
  80. package/src/cli/commands/__tests__/cli-test-harness.ts +21 -3
  81. package/src/cli/commands/__tests__/plugins.test.ts +20 -1
  82. package/src/cli/commands/__tests__/schedules.test.ts +14 -0
  83. package/src/cli/commands/bash.help.ts +4 -3
  84. package/src/cli/commands/browser.help.ts +5 -1
  85. package/src/cli/commands/credentials.help.ts +3 -3
  86. package/src/cli/commands/oauth/index.help.ts +10 -0
  87. package/src/cli/commands/oauth/providers.ts +12 -0
  88. package/src/cli/commands/oauth/request.test.ts +179 -2
  89. package/src/cli/commands/oauth/request.ts +41 -10
  90. package/src/cli/commands/plugins.ts +12 -5
  91. package/src/cli/commands/schedules.ts +2 -0
  92. package/src/cli/lib/__tests__/inspect-plugin.test.ts +54 -0
  93. package/src/cli/lib/__tests__/install-from-github.test.ts +41 -0
  94. package/src/cli/lib/__tests__/local-plugin-upgrade.test.ts +133 -51
  95. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +8 -2
  96. package/src/cli/lib/bundled-marketplace.json +855 -0
  97. package/src/cli/lib/bundled-plugin-packages.json +783 -1
  98. package/src/cli/lib/inspect-plugin.ts +11 -4
  99. package/src/cli/lib/upgrade-plugin.ts +19 -11
  100. package/src/cli/output.ts +18 -4
  101. package/src/config/__tests__/default-provider.test.ts +4 -0
  102. package/src/config/bundled-skills/schedule/SKILL.md +6 -6
  103. package/src/config/loader.ts +9 -17
  104. package/src/config/platform-identity.ts +8 -8
  105. package/src/config/profile-text-generation.test.ts +51 -0
  106. package/src/config/profile-text-generation.ts +51 -0
  107. package/src/config/schemas/__tests__/memory-v3.test.ts +12 -0
  108. package/src/config/schemas/llm.ts +7 -2
  109. package/src/config/schemas/mcp.ts +5 -1
  110. package/src/config/schemas/memory-v3.ts +12 -0
  111. package/src/credential-execution/ces-runtime.ts +2 -2
  112. package/src/credential-execution/executable-discovery.ts +36 -134
  113. package/src/credential-execution/process-manager.test.ts +20 -24
  114. package/src/credential-execution/process-manager.ts +24 -19
  115. package/src/daemon/__tests__/conversation-tool-setup.test.ts +43 -0
  116. package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +36 -0
  117. package/src/daemon/assistant-attachments.ts +20 -12
  118. package/src/daemon/chat-credential-redaction.ts +16 -1
  119. package/src/daemon/conversation-agent-loop-handlers.ts +74 -3
  120. package/src/daemon/conversation-agent-loop.ts +12 -0
  121. package/src/daemon/conversation-attachments.ts +93 -12
  122. package/src/daemon/conversation-tool-setup.ts +71 -3
  123. package/src/daemon/conversation-turn-finalize.ts +26 -13
  124. package/src/daemon/conversation.ts +72 -17
  125. package/src/daemon/daemon-control.ts +3 -7
  126. package/src/daemon/lifecycle.ts +4 -4
  127. package/src/daemon/mcp-reload-service.ts +1 -1
  128. package/src/daemon/message-types/shared.ts +1 -0
  129. package/src/daemon/orphan-reaper.ts +4 -3
  130. package/src/daemon/persist-media-references.ts +20 -3
  131. package/src/daemon/tool-setup-types.ts +6 -0
  132. package/src/daemon/wake-conversation-ops.ts +50 -15
  133. package/src/desktop/desktop-automation-lease.test.ts +230 -0
  134. package/src/desktop/desktop-automation-lease.ts +90 -6
  135. package/src/desktop/desktop-help.ts +15 -0
  136. package/src/desktop/desktop-stream-bridge.test.ts +65 -10
  137. package/src/desktop/desktop-stream-bridge.ts +3 -3
  138. package/src/desktop/desktop-wallpaper-renderer.ts +132 -0
  139. package/src/desktop/desktop-wallpaper-worker.ts +17 -0
  140. package/src/desktop/desktop-wallpaper.test.ts +48 -7
  141. package/src/desktop/desktop-wallpaper.ts +34 -102
  142. package/src/desktop/virtual-desktop-feature.ts +1 -1
  143. package/src/mcp/__tests__/manager-tool-caps.test.ts +111 -0
  144. package/src/mcp/__tests__/startup.test.ts +30 -7
  145. package/src/mcp/__tests__/tool-caps.test.ts +107 -0
  146. package/src/mcp/manager.ts +168 -101
  147. package/src/mcp/startup.ts +33 -13
  148. package/src/mcp/tool-caps.ts +176 -0
  149. package/src/messaging/provider-message-metadata.ts +3 -3
  150. package/src/notifications/__tests__/copy-composer.test.ts +70 -0
  151. package/src/notifications/copy-composer.ts +11 -3
  152. package/src/notifications/schedule-result-producer.ts +4 -6
  153. package/src/oauth/AGENTS.md +2 -0
  154. package/src/oauth/__tests__/identity-verifier.test.ts +36 -1
  155. package/src/oauth/identity-verifier.ts +24 -0
  156. package/src/oauth/oauth-store.ts +11 -1
  157. package/src/oauth/provider-serializer.ts +1 -0
  158. package/src/oauth/seed-providers.ts +12 -0
  159. package/src/permissions/confirmation-guardian-request.test.ts +16 -0
  160. package/src/permissions/confirmation-guardian-request.ts +2 -3
  161. package/src/permissions/question-prompter.test.ts +31 -0
  162. package/src/permissions/question-prompter.ts +2 -0
  163. package/src/persistence/attachments-store.ts +84 -44
  164. package/src/persistence/conversation-crud.ts +111 -20
  165. package/src/persistence/conversation-plugin-facade.ts +30 -0
  166. package/src/persistence/conversation-tool-surface.ts +135 -0
  167. package/src/persistence/conversation-types.test.ts +32 -0
  168. package/src/persistence/conversation-types.ts +26 -4
  169. package/src/persistence/migrations/378-create-conversation-tool-surfaces.test.ts +78 -0
  170. package/src/persistence/migrations/378-create-conversation-tool-surfaces.ts +29 -0
  171. package/src/persistence/migrations/379-oauth-providers-response-ok-field.test.ts +92 -0
  172. package/src/persistence/migrations/379-oauth-providers-response-ok-field.ts +32 -0
  173. package/src/persistence/migrations/380-conversation-tool-surfaces-delegate-independent-tasks.test.ts +90 -0
  174. package/src/persistence/migrations/380-conversation-tool-surfaces-delegate-independent-tasks.ts +34 -0
  175. package/src/persistence/schema/conversation-tool-surfaces.ts +32 -0
  176. package/src/persistence/schema/index.ts +1 -0
  177. package/src/persistence/schema/oauth.ts +1 -0
  178. package/src/persistence/steps.ts +24 -3
  179. package/src/plugin-api/conversation-turn.ts +31 -7
  180. package/src/plugin-api/index.ts +11 -1
  181. package/src/plugin-api/plugin-channel-turn-trust.test.ts +133 -0
  182. package/src/plugin-api/plugin-channel-turn-trust.ts +71 -0
  183. package/src/plugins/defaults/memory/AGENTS.md +39 -5
  184. package/src/plugins/defaults/memory/__tests__/buffer-file.test.ts +365 -0
  185. package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +43 -0
  186. package/src/plugins/defaults/memory/__tests__/conversation-memory-purge.test.ts +1 -0
  187. package/src/plugins/defaults/memory/__tests__/db-memory-attach.test.ts +2 -0
  188. package/src/plugins/defaults/memory/__tests__/fixtures/buffer-appender.ts +17 -0
  189. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +74 -0
  190. package/src/plugins/defaults/memory/__tests__/memory-run-evidence.test.ts +161 -0
  191. package/src/plugins/defaults/memory/__tests__/relocated-memory-test-rows.ts +10 -0
  192. package/src/plugins/defaults/memory/buffer-file.ts +354 -0
  193. package/src/plugins/defaults/memory/buffer-format.ts +40 -0
  194. package/src/plugins/defaults/memory/context-search/agent-runner.ts +1 -2
  195. package/src/plugins/defaults/memory/context-search/format.ts +2 -1
  196. package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +2 -1
  197. package/src/plugins/defaults/memory/context-search/sources/workspace.ts +2 -1
  198. package/src/plugins/defaults/memory/conversation-memory-purge.ts +4 -0
  199. package/src/plugins/defaults/memory/graph/capability-seed.ts +1 -2
  200. package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -42
  201. package/src/plugins/defaults/memory/host-utils.ts +0 -10
  202. package/src/plugins/defaults/memory/injectors.ts +4 -3
  203. package/src/plugins/defaults/memory/memory-retrospective-job.ts +72 -183
  204. package/src/plugins/defaults/memory/memory-run-evidence.ts +213 -0
  205. package/src/plugins/defaults/memory/src/memory-item-routes.test.ts +1 -1
  206. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +407 -99
  207. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +10 -0
  208. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +107 -7
  209. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +307 -86
  210. package/src/plugins/defaults/memory/substrate/consolidation-tool-surface.ts +34 -0
  211. package/src/plugins/defaults/memory/substrate/page-index.ts +2 -1
  212. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +89 -49
  213. package/src/plugins/defaults/memory/substrate/sweep-job.ts +1 -1
  214. package/src/plugins/defaults/memory/tools.ts +1 -1
  215. package/src/plugins/defaults/memory/v1/graph/consolidation.ts +2 -2
  216. package/src/plugins/defaults/memory/v1/graph/extraction.ts +2 -1
  217. package/src/plugins/defaults/memory/v1/graph/retriever.ts +1 -1
  218. package/src/plugins/defaults/memory/v2/__tests__/migration.test.ts +5 -0
  219. package/src/plugins/defaults/memory/v2/__tests__/reranker.test.ts +5 -2
  220. package/src/plugins/defaults/memory/v2/reranker.ts +2 -1
  221. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +81 -1
  222. package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +87 -0
  223. package/src/plugins/defaults/memory/v3/__tests__/plugin-schema.test.ts +12 -0
  224. package/src/plugins/defaults/memory/v3/__tests__/pool-log-store.test.ts +183 -4
  225. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +103 -2
  226. package/src/plugins/defaults/memory/v3/card.ts +2 -1
  227. package/src/plugins/defaults/memory/v3/injector.ts +212 -178
  228. package/src/plugins/defaults/memory/v3/orchestrate.ts +112 -25
  229. package/src/plugins/defaults/memory/v3/plugin-schema.ts +54 -4
  230. package/src/plugins/defaults/memory/v3/pool-log-store.ts +253 -1
  231. package/src/plugins/defaults/memory/v3/pool-select.test.ts +35 -2
  232. package/src/plugins/defaults/memory/v3/pool-select.ts +31 -19
  233. package/src/plugins/defaults/memory/v3/sections.ts +2 -1
  234. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +41 -14
  235. package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +4 -1
  236. package/src/plugins/defaults/tool-result-truncate/terminal.ts +1 -46
  237. package/src/prompts/__tests__/parallel-tasks-section.test.ts +25 -0
  238. package/src/prompts/delegation-gate.ts +57 -0
  239. package/src/prompts/system-prompt.ts +14 -32
  240. package/src/providers/inference/adapter-factory.ts +6 -0
  241. package/src/providers/jev/client.test.ts +260 -0
  242. package/src/providers/jev/client.ts +518 -0
  243. package/src/providers/model-catalog.ts +58 -3
  244. package/src/providers/server-tool-pairing.ts +16 -7
  245. package/src/runtime/AGENTS.md +2 -2
  246. package/src/runtime/__tests__/agent-wake.test.ts +94 -1
  247. package/src/runtime/agent-wake.ts +31 -4
  248. package/src/runtime/guardian-action-service.ts +2 -17
  249. package/src/runtime/guardian-reply-router.ts +1 -8
  250. package/src/runtime/http-server.ts +2 -2
  251. package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +89 -0
  252. package/src/runtime/migrations/vbundle-import-policy.ts +33 -11
  253. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +24 -2
  254. package/src/runtime/routes/channel-route-shared.ts +1 -9
  255. package/src/runtime/routes/conversation-routes.ts +29 -4
  256. package/src/runtime/routes/desktop-setup-routes.test.ts +2 -2
  257. package/src/runtime/routes/desktop-setup-routes.ts +8 -5
  258. package/src/runtime/routes/guardian-approval-interception.ts +24 -0
  259. package/src/runtime/routes/inbound-message-handler.ts +2 -3
  260. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +1 -1
  261. package/src/runtime/routes/inbound-stages/background-dispatch.ts +7 -4
  262. package/src/runtime/routes/inference-profile-session-handler.ts +11 -1
  263. package/src/runtime/routes/inference-profiles-routes.ts +9 -0
  264. package/src/runtime/routes/oauth-commands-routes.ts +57 -54
  265. package/src/runtime/routes/oauth-providers.ts +4 -0
  266. package/src/runtime/routes/oauth-request-hints.test.ts +246 -0
  267. package/src/runtime/routes/oauth-request-hints.ts +149 -0
  268. package/src/runtime/routes/schedule-routes.ts +2 -0
  269. package/src/runtime/routes/secret-routes.ts +99 -119
  270. package/src/schedule/__tests__/worker-mcp-bootstrap.test.ts +107 -0
  271. package/src/schedule/__tests__/worker-mcp-readiness.test.ts +173 -0
  272. package/src/schedule/__tests__/worker-mcp-tools.test.ts +13 -47
  273. package/src/schedule/run-script.ts +2 -2
  274. package/src/schedule/scheduler.ts +47 -11
  275. package/src/schedule/tool-surface-readiness.ts +65 -0
  276. package/src/schedule/worker-mcp.ts +85 -0
  277. package/src/schedule/worker.ts +9 -31
  278. package/src/security/secure-keys.ts +60 -48
  279. package/src/tools/ask-question/ask-question-tool.test.ts +167 -5
  280. package/src/tools/ask-question/ask-question-tool.ts +119 -23
  281. package/src/tools/browser/browser-execution.ts +7 -1
  282. package/src/tools/host-terminal/host-shell.ts +12 -6
  283. package/src/tools/shared/filesystem/file-ops-service.ts +1 -31
  284. package/src/tools/shared/shell-output.test.ts +10 -0
  285. package/src/tools/shared/shell-output.ts +14 -2
  286. package/src/tools/skills/sandbox-runner.ts +13 -2
  287. package/src/tools/skills/scaffold-managed.ts +2 -1
  288. package/src/tools/terminal/__tests__/safe-env.test.ts +33 -4
  289. package/src/tools/terminal/__tests__/sanitized-bash.test.ts +0 -14
  290. package/src/tools/terminal/safe-env.ts +35 -24
  291. package/src/tools/terminal/sanitized-bash.ts +15 -2
  292. package/src/tools/terminal/shell-launch.test.ts +162 -0
  293. package/src/tools/terminal/shell.test.ts +29 -0
  294. package/src/tools/terminal/shell.ts +13 -7
  295. package/src/util/browser-human-verification.ts +14 -0
  296. package/src/util/host-process.test.ts +17 -1
  297. package/src/util/host-process.ts +24 -0
  298. package/src/util/unicode.ts +29 -0
@@ -1,196 +1,98 @@
1
1
  /**
2
- * CES executable discovery and transport bootstrap.
2
+ * CES socket discovery and transport bootstrap.
3
3
  *
4
- * Provides two discovery strategies:
5
- *
6
- * 1. **Sibling mode** — Locates the CLI-launched CES sibling process via its
7
- * Unix socket (`CES_LOCAL_SOCKET`). Used by bare-metal/local instances.
8
- *
9
- * 2. **Managed mode** — Locates the bootstrap Unix socket exposed by the CES
10
- * sidecar container through a shared emptyDir volume, and returns a
11
- * connected Unix socket transport.
12
- *
13
- * Both strategies fail closed: if the socket cannot be found, the discovery
14
- * returns a structured error rather than falling back to in-process
15
- * credential handling.
4
+ * Local siblings and managed sidecars share one resolver: `ces.sock` under
5
+ * `CES_BOOTSTRAP_SOCKET_DIR` (default `/run/ces-bootstrap`). Discovery fails
6
+ * closed if the socket cannot be found.
16
7
  */
17
8
 
18
9
  import { existsSync } from "node:fs";
19
- import { join } from "node:path";
20
10
 
21
- import { getIsContainerized } from "../config/env-registry.js";
11
+ import {
12
+ isNamedPipePath,
13
+ resolveIpcEndpoint,
14
+ } from "@vellumai/ipc-server-utils";
15
+
22
16
  import { getLogger } from "../util/logger.js";
23
17
 
24
18
  const log = getLogger("ces-discovery");
25
19
 
26
- // ---------------------------------------------------------------------------
27
- // Well-known paths
28
- // ---------------------------------------------------------------------------
29
-
30
- /** Default directory for the bootstrap socket shared volume. */
20
+ /** Default directory for the CES Unix socket. */
31
21
  const DEFAULT_BOOTSTRAP_SOCKET_DIR = "/run/ces-bootstrap";
32
22
 
33
- /** Bootstrap socket filename. */
34
- const BOOTSTRAP_SOCKET_NAME = "ces.sock";
35
-
36
23
  /**
37
- * Resolve the CES bootstrap socket path.
24
+ * Resolve the CES socket path used in every environment.
38
25
  *
39
- * Priority:
40
- * 1. `CES_BOOTSTRAP_SOCKET_DIR` env var (directory) — appends `ces.sock`
41
- * 2. `CES_BOOTSTRAP_SOCKET` env var (full file path override)
42
- * 3. Hardcoded default: `/run/ces-bootstrap/ces.sock`
43
- *
44
- * The pod template exports `CES_BOOTSTRAP_SOCKET_DIR`; the full-path
45
- * override is kept for local testing convenience.
26
+ * Uses `CES_BOOTSTRAP_SOCKET_DIR` when set, otherwise `/run/ces-bootstrap`.
27
+ * `ces.sock` is resolved through `resolveIpcEndpoint`.
46
28
  */
47
- function getManagedBootstrapSocketPath(): string {
48
- const dir = process.env["CES_BOOTSTRAP_SOCKET_DIR"];
49
- if (dir) {
50
- return join(dir, BOOTSTRAP_SOCKET_NAME);
51
- }
52
- return (
53
- process.env["CES_BOOTSTRAP_SOCKET"] ??
54
- join(DEFAULT_BOOTSTRAP_SOCKET_DIR, BOOTSTRAP_SOCKET_NAME)
55
- );
29
+ function getCesSocketPath(): string {
30
+ const dir =
31
+ process.env["CES_BOOTSTRAP_SOCKET_DIR"]?.trim() ||
32
+ DEFAULT_BOOTSTRAP_SOCKET_DIR;
33
+ return resolveIpcEndpoint("ces", { workspaceDir: dir }).path;
56
34
  }
57
35
 
58
- // ---------------------------------------------------------------------------
59
- // Discovery result types
60
- // ---------------------------------------------------------------------------
61
-
62
36
  export interface ManagedDiscoverySuccess {
63
37
  mode: "managed";
64
38
  socketPath: string;
65
39
  }
66
40
 
67
- /**
68
- * A local CES running as an independent sibling process (not spawned by the
69
- * assistant), reached over a Unix socket. Transport-identical to managed; the
70
- * distinct mode records that the assistant does not own this process's
71
- * lifecycle. This is the default topology for bare-metal/local instances.
72
- */
73
- export interface SiblingDiscoverySuccess {
74
- mode: "sibling";
75
- socketPath: string;
76
- }
77
-
78
41
  export interface DiscoveryFailure {
79
42
  mode: "unavailable";
80
43
  reason: string;
81
44
  }
82
45
 
83
- export type DiscoveryResult =
84
- | ManagedDiscoverySuccess
85
- | SiblingDiscoverySuccess
86
- | DiscoveryFailure;
87
-
88
- // ---------------------------------------------------------------------------
89
- // Managed discovery
90
- // ---------------------------------------------------------------------------
46
+ export type DiscoveryResult = ManagedDiscoverySuccess | DiscoveryFailure;
91
47
 
92
48
  /**
93
- * Discover the managed CES sidecar via its bootstrap Unix socket.
49
+ * Discover CES via the shared bootstrap Unix socket.
94
50
  *
95
- * Checks that the well-known socket file exists on disk. Does NOT open a
96
- * connection the CES sidecar accepts exactly one connection and then
97
- * unlinks the socket, so a probe would consume the only slot. The actual
98
- * connection is made later by `CesProcessManager.start()`.
99
- *
100
- * The socket path is derived from `CES_BOOTSTRAP_SOCKET_DIR` (matching the
101
- * pod template), with `CES_BOOTSTRAP_SOCKET` as a full-path fallback.
51
+ * Checks that the well-known socket exists. Does not open a connection.
52
+ * CES serves a multi-connection listener, so the assistant and its child
53
+ * processes can each connect. The actual connection is made later by
54
+ * `CesProcessManager.start()`.
102
55
  */
103
56
  export function discoverManagedCes():
104
57
  | ManagedDiscoverySuccess
105
58
  | DiscoveryFailure {
106
- const socketPath = getManagedBootstrapSocketPath();
59
+ const socketPath = getCesSocketPath();
107
60
 
108
- if (!existsSync(socketPath)) {
61
+ if (!isNamedPipePath(socketPath) && !existsSync(socketPath)) {
109
62
  const reason = `CES bootstrap socket not found at ${socketPath}`;
110
63
  log.warn(reason);
111
64
  return { mode: "unavailable", reason };
112
65
  }
113
66
 
114
- log.info({ socketPath }, "Managed CES bootstrap socket found");
67
+ log.info({ socketPath }, "CES bootstrap socket found");
115
68
  return { mode: "managed", socketPath };
116
69
  }
117
70
 
118
- // ---------------------------------------------------------------------------
119
- // Sibling discovery (local bare-metal topology)
120
- // ---------------------------------------------------------------------------
121
-
122
71
  /**
123
- * Discover a CLI-launched local CES sibling via its Unix socket. The CLI sets
124
- * `CES_LOCAL_SOCKET` on both the sibling and the daemon so they agree on the
125
- * path. Does not open a connection — that happens in `CesProcessManager.start()`.
126
- */
127
- export function discoverLocalSiblingCes():
128
- | SiblingDiscoverySuccess
129
- | DiscoveryFailure {
130
- const socketPath = process.env["CES_LOCAL_SOCKET"];
131
- if (!socketPath) {
132
- const reason =
133
- "CES_LOCAL_SOCKET is not set — cannot locate the CES sibling socket. The CLI should set this during wake/hatch.";
134
- log.warn(reason);
135
- return { mode: "unavailable", reason };
136
- }
137
- if (!existsSync(socketPath)) {
138
- return {
139
- mode: "unavailable",
140
- reason: `CES sibling socket not found at ${socketPath}`,
141
- };
142
- }
143
- log.info({ socketPath }, "Local CES sibling socket found");
144
- return { mode: "sibling", socketPath };
145
- }
146
-
147
- // ---------------------------------------------------------------------------
148
- // Auto-discovery
149
- // ---------------------------------------------------------------------------
150
-
151
- /**
152
- * Automatically discover CES using the appropriate strategy for the
153
- * current deployment topology.
154
- *
155
- * - Containerized environments → managed sidecar discovery
156
- * - Non-containerized → CLI-launched sibling socket discovery
72
+ * Discover CES for the current process. Local and managed topologies use
73
+ * the same bootstrap socket path.
157
74
  */
158
75
  export function discoverCes(): DiscoveryResult {
159
- if (getIsContainerized()) {
160
- return discoverManagedCes();
161
- }
162
- return discoverLocalSiblingCes();
76
+ return discoverManagedCes();
163
77
  }
164
78
 
165
- /** How long to poll for the managed bootstrap socket before giving up. */
79
+ /** How long to poll for the CES socket before giving up. */
166
80
  const MANAGED_DISCOVERY_TIMEOUT_MS = 3_000;
167
81
 
168
- /** Delay between managed bootstrap-socket discovery attempts. */
82
+ /** Delay between CES socket discovery attempts. */
169
83
  const MANAGED_DISCOVERY_INTERVAL_MS = 100;
170
84
 
171
85
  /**
172
- * Discover CES, polling for the managed bootstrap socket with a short
173
- * backoff before failing.
174
- *
175
- * The managed CES sidecar re-binds its bootstrap socket after each assistant
176
- * session ends — it outlives any single assistant and accepts the next
177
- * connection (see `credential-executor/src/main.ts`). A reconnecting
178
- * assistant (e.g. after a container restart) can therefore probe during the
179
- * brief window before the socket is re-bound. A single existence check would
180
- * race that window and incorrectly report the sidecar as unavailable; polling
181
- * absorbs the gap.
86
+ * Discover CES, polling for the socket with a short backoff before failing.
182
87
  *
183
- * Local discovery is returned immediately a missing binary will not appear
184
- * by waiting, so there is nothing to poll for.
88
+ * CES binds its socket asynchronously. A reconnecting assistant can probe
89
+ * during the brief window before the socket is re-bound. Polling absorbs
90
+ * that gap.
185
91
  */
186
92
  export async function discoverCesWithRetry({
187
93
  timeoutMs = MANAGED_DISCOVERY_TIMEOUT_MS,
188
94
  intervalMs = MANAGED_DISCOVERY_INTERVAL_MS,
189
95
  }: { timeoutMs?: number; intervalMs?: number } = {}): Promise<DiscoveryResult> {
190
- // Both the managed bootstrap socket and the CLI-launched sibling socket
191
- // are bound asynchronously, so polling is worthwhile. A missing local
192
- // binary (the old stdio path) would not appear by waiting, but that path
193
- // is gone — the sibling is the only local topology now.
194
96
  const deadline = Date.now() + timeoutMs;
195
97
  let result = discoverCes();
196
98
  while (result.mode === "unavailable" && Date.now() < deadline) {
@@ -3,12 +3,10 @@ import { createServer, type Server } from "node:net";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import {
6
- afterAll,
7
6
  afterEach,
8
7
  beforeEach,
9
8
  describe,
10
9
  expect,
11
- mock,
12
10
  test,
13
11
  } from "bun:test";
14
12
 
@@ -21,22 +19,18 @@ const untilClosed = (predicate: () => boolean) =>
21
19
  message: "socket close never propagated to the transport",
22
20
  });
23
21
 
24
- // ---------------------------------------------------------------------------
25
- // onTransportClose tests
26
- // ---------------------------------------------------------------------------
27
-
28
- // Mock executable-discovery so start() connects to our test socket.
29
- const realDiscovery = await import("./executable-discovery.js");
30
-
31
22
  let mockSocketPath = "";
32
23
 
33
- mock.module("./executable-discovery.js", () => ({
34
- ...realDiscovery,
35
- discoverCesWithRetry: async () => ({
36
- mode: "sibling" as const,
24
+ function discoverTestSocket() {
25
+ return Promise.resolve({
26
+ mode: "managed" as const,
37
27
  socketPath: mockSocketPath,
38
- }),
39
- }));
28
+ });
29
+ }
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // onTransportClose tests
33
+ // ---------------------------------------------------------------------------
40
34
 
41
35
  describe("CesProcessManager.onTransportClose", () => {
42
36
  let tempDir: string;
@@ -67,12 +61,8 @@ describe("CesProcessManager.onTransportClose", () => {
67
61
  rmSync(tempDir, { recursive: true, force: true });
68
62
  });
69
63
 
70
- afterAll(() => {
71
- mock.module("./executable-discovery.js", () => realDiscovery);
72
- });
73
-
74
64
  test("fires handler when the transport dies (socket closed)", async () => {
75
- const pm = createCesProcessManager({});
65
+ const pm = createCesProcessManager({ discover: discoverTestSocket });
76
66
  const transport = await pm.start();
77
67
  expect(transport.isAlive()).toBe(true);
78
68
 
@@ -93,7 +83,7 @@ describe("CesProcessManager.onTransportClose", () => {
93
83
  });
94
84
 
95
85
  test("does not fire handler before the transport dies", async () => {
96
- const pm = createCesProcessManager({});
86
+ const pm = createCesProcessManager({ discover: discoverTestSocket });
97
87
  await pm.start();
98
88
 
99
89
  let closeFired = false;
@@ -109,7 +99,7 @@ describe("CesProcessManager.onTransportClose", () => {
109
99
  });
110
100
 
111
101
  test("fires handler immediately if transport is already dead", async () => {
112
- const pm = createCesProcessManager({});
102
+ const pm = createCesProcessManager({ discover: discoverTestSocket });
113
103
  const transport = await pm.start();
114
104
 
115
105
  // Kill the transport by destroying the server-side socket.
@@ -186,7 +176,10 @@ describe("CesProcessManager transport-death logging", () => {
186
176
 
187
177
  test("logs WARN when the transport dies unexpectedly (remote close)", async () => {
188
178
  const { calls, logger } = makeRecordingLogger();
189
- const pm = createCesProcessManager({ logger });
179
+ const pm = createCesProcessManager({
180
+ logger,
181
+ discover: discoverTestSocket,
182
+ });
190
183
  const transport = await pm.start();
191
184
 
192
185
  // The remote (server) closes the connection: genuinely-unexpected death.
@@ -205,7 +198,10 @@ describe("CesProcessManager transport-death logging", () => {
205
198
 
206
199
  test("does not log WARN on an intentional stop()", async () => {
207
200
  const { calls, logger } = makeRecordingLogger();
208
- const pm = createCesProcessManager({ logger });
201
+ const pm = createCesProcessManager({
202
+ logger,
203
+ discover: discoverTestSocket,
204
+ });
209
205
  await pm.start();
210
206
 
211
207
  await pm.stop();
@@ -1,17 +1,17 @@
1
1
  /**
2
2
  * CES process manager.
3
3
  *
4
- * Creates transport connections to the CES process either a CLI-launched
5
- * sibling over a Unix socket (bare-metal/local), or a managed sidecar over
6
- * its bootstrap socket (containerized). The process manager owns only the
7
- * transport connection lifecycle; the CES process itself is managed by the
8
- * CLI (sibling) or the pod infrastructure (managed).
4
+ * Creates a transport connection to the CES process over the shared bootstrap
5
+ * socket. Local CLI siblings and managed sidecars bind the same path under
6
+ * `CES_BOOTSTRAP_SOCKET_DIR`. The process manager owns only the transport
7
+ * connection lifecycle; the CES process itself is managed by the CLI
8
+ * (sibling) or the pod infrastructure (managed).
9
9
  *
10
- * Managed env contract:
11
- * - CES_BOOTSTRAP_SOCKET — Path to the bootstrap Unix socket (shared emptyDir)
12
- * - /assistant-data-ro Assistant data mounted read-only into the CES sidecar
13
- * - /ces-data CES private data directory (separate PVC)
14
- * - CES_HEALTH_PORT Health check port exposed by the CES sidecar
10
+ * Env contract:
11
+ * - CES_BOOTSTRAP_SOCKET_DIR: directory containing `ces.sock`
12
+ * - /assistant-data-ro: assistant data mounted read-only into CES
13
+ * - /ces-data: CES private data directory (separate PVC)
14
+ * - CES_HEALTH_PORT: health check port exposed by the CES sidecar
15
15
  */
16
16
 
17
17
  import { createConnection, type Socket } from "node:net";
@@ -24,7 +24,6 @@ import {
24
24
  discoverCesWithRetry,
25
25
  type DiscoveryResult,
26
26
  type ManagedDiscoverySuccess,
27
- type SiblingDiscoverySuccess,
28
27
  } from "./executable-discovery.js";
29
28
 
30
29
  const log = getLogger("ces-process-manager");
@@ -62,6 +61,12 @@ export interface CesProcessManagerConfig {
62
61
 
63
62
  /** Logger override. Defaults to the module logger; injected in tests. */
64
63
  logger?: PmLogger;
64
+
65
+ /**
66
+ * Socket discovery. Defaults to `discoverCesWithRetry`. Tests inject a
67
+ * stub that returns a known socket path.
68
+ */
69
+ discover?: typeof discoverCesWithRetry;
65
70
  }
66
71
 
67
72
  // ---------------------------------------------------------------------------
@@ -70,7 +75,7 @@ export interface CesProcessManagerConfig {
70
75
 
71
76
  export interface CesProcessManager {
72
77
  /**
73
- * Connect to the CES process (sibling socket or managed sidecar socket).
78
+ * Connect to the CES bootstrap socket.
74
79
  * Returns a CesTransport ready for use with createCesClient().
75
80
  *
76
81
  * Throws if CES is unavailable.
@@ -107,6 +112,7 @@ export function createCesProcessManager(
107
112
  config: CesProcessManagerConfig,
108
113
  ): CesProcessManager {
109
114
  const pmLog = config.logger ?? log;
115
+ const discover = config.discover ?? discoverCesWithRetry;
110
116
  let managedSocket: Socket | null = null;
111
117
  let discoveryResult: DiscoveryResult | null = null;
112
118
  let running = false;
@@ -119,16 +125,15 @@ export function createCesProcessManager(
119
125
  throw new Error("CES process manager is already running");
120
126
  }
121
127
 
122
- // Poll for the socket with a short backoff. Both the managed bootstrap
123
- // socket and the CLI-launched sibling socket are bound asynchronously,
124
- // so a reconnecting assistant can briefly race the re-bind.
125
- discoveryResult = await discoverCesWithRetry();
128
+ // Poll for the socket with a short backoff. CES binds
129
+ // asynchronously, so a reconnecting assistant can briefly race
130
+ // the re-bind.
131
+ discoveryResult = await discover();
126
132
 
127
133
  if (discoveryResult.mode === "unavailable") {
128
134
  throw new CesUnavailableError(discoveryResult.reason);
129
135
  }
130
136
 
131
- // managed sidecar or CLI-launched sibling — both connect to a socket.
132
137
  const transport = await connectManagedSocket(discoveryResult);
133
138
  currentTransport = transport;
134
139
  wireTransportClose(transport);
@@ -200,11 +205,11 @@ export function createCesProcessManager(
200
205
  }
201
206
 
202
207
  // -------------------------------------------------------------------------
203
- // Socket connection — managed sidecar or CLI-launched local sibling
208
+ // Socket connection
204
209
  // -------------------------------------------------------------------------
205
210
 
206
211
  async function connectManagedSocket(
207
- discovery: ManagedDiscoverySuccess | SiblingDiscoverySuccess,
212
+ discovery: ManagedDiscoverySuccess,
208
213
  ): Promise<SocketTransport> {
209
214
  pmLog.info(
210
215
  { socketPath: discovery.socketPath, mode: discovery.mode },
@@ -6,6 +6,7 @@
6
6
 
7
7
  import { beforeEach, describe, expect, mock, test } from "bun:test";
8
8
 
9
+ import { CONSOLIDATION_ALLOWED_TOOLS } from "../../plugins/defaults/memory/substrate/consolidation-tool-surface.js";
9
10
  import type { Conversation } from "../conversation.js";
10
11
  import type { ChannelCapabilities } from "../conversation-runtime-assembly.js";
11
12
 
@@ -779,3 +780,45 @@ describe("isToolActiveForContext — allowlist-only tools (delete_memory_page)",
779
780
  ).toBe(false);
780
781
  });
781
782
  });
783
+
784
+ describe("isToolActiveForContext — memory consolidation surface resolves onto the wire", () => {
785
+ // A background job conversation is not a subagent, so SUBAGENT_ONLY tools
786
+ // are filtered off before the allowlist applies. Every name the
787
+ // consolidation run allowlists must survive that gate, or the tool is
788
+ // silently missing from the run while the allowlist still claims it.
789
+ const consolidationCtx = () =>
790
+ makeCtx({
791
+ subagentAllowedTools: new Set(CONSOLIDATION_ALLOWED_TOOLS),
792
+ subagentToolGateMode: "wire",
793
+ });
794
+
795
+ test("every allowlisted tool is active for a background conversation", () => {
796
+ for (const name of CONSOLIDATION_ALLOWED_TOOLS) {
797
+ expect(isToolActiveForContext(name, consolidationCtx()), name).toBe(true);
798
+ }
799
+ });
800
+
801
+ test("the shell and the page-delete primitive are on the surface", () => {
802
+ expect(isToolActiveForContext("bash", consolidationCtx())).toBe(true);
803
+ expect(
804
+ isToolActiveForContext("delete_memory_page", consolidationCtx()),
805
+ ).toBe(true);
806
+ });
807
+
808
+ test("network egress and host-proxy tools stay off the wire", () => {
809
+ for (const name of [
810
+ "web_fetch",
811
+ "web_search",
812
+ "network_request",
813
+ "host_bash",
814
+ "host_file_read",
815
+ "host_file_write",
816
+ "host_file_edit",
817
+ "host_cu",
818
+ ]) {
819
+ expect(isToolActiveForContext(name, consolidationCtx()), name).toBe(
820
+ false,
821
+ );
822
+ }
823
+ });
824
+ });
@@ -128,6 +128,7 @@ async function runSettle(): Promise<Message[]> {
128
128
  ctx,
129
129
  state: {
130
130
  lastAssistantMessageId: ASSISTANT_MESSAGE_ID,
131
+ assistantMessageIdsToSync: new Set(),
131
132
  inflightWriters: new Map<string, InflightContentWriter>(),
132
133
  },
133
134
  rlog,
@@ -169,4 +170,39 @@ describe("settleTurnContent conversation deleted after the terminal SSE", () =>
169
170
  expect(diskSyncCalls).toEqual([ASSISTANT_MESSAGE_ID]);
170
171
  expect(messages).toEqual(TRUNCATED_MESSAGES);
171
172
  });
173
+
174
+ test("exports a queued attachment target only once when it is the final row", async () => {
175
+ const ctx = { conversationId: CONVERSATION_ID, messages: [] as Message[] };
176
+
177
+ await settleTurnContent({
178
+ ctx,
179
+ state: {
180
+ lastAssistantMessageId: ASSISTANT_MESSAGE_ID,
181
+ assistantMessageIdsToSync: new Set([ASSISTANT_MESSAGE_ID]),
182
+ inflightWriters: new Map<string, InflightContentWriter>(),
183
+ },
184
+ rlog,
185
+ });
186
+
187
+ expect(diskSyncCalls).toEqual([ASSISTANT_MESSAGE_ID]);
188
+ });
189
+
190
+ test("exports an earlier attachment target before the final private row", async () => {
191
+ const ctx = { conversationId: CONVERSATION_ID, messages: [] as Message[] };
192
+
193
+ await settleTurnContent({
194
+ ctx,
195
+ state: {
196
+ lastAssistantMessageId: ASSISTANT_MESSAGE_ID,
197
+ assistantMessageIdsToSync: new Set(["msg-delivered-reply"]),
198
+ inflightWriters: new Map<string, InflightContentWriter>(),
199
+ },
200
+ rlog,
201
+ });
202
+
203
+ expect(diskSyncCalls).toEqual([
204
+ "msg-delivered-reply",
205
+ ASSISTANT_MESSAGE_ID,
206
+ ]);
207
+ });
172
208
  });
@@ -109,8 +109,8 @@ export function classifyKind(mimeType: string): "image" | "video" | "document" {
109
109
  // Validation / cap enforcement
110
110
  // ---------------------------------------------------------------------------
111
111
 
112
- interface ValidatedDrafts {
113
- accepted: AssistantAttachmentDraft[];
112
+ interface ValidatedDrafts<T extends AssistantAttachmentDraft> {
113
+ accepted: T[];
114
114
  warnings: string[];
115
115
  }
116
116
 
@@ -119,10 +119,10 @@ interface ValidatedDrafts {
119
119
  *
120
120
  * - Rejects individual drafts that exceed `MAX_ASSISTANT_ATTACHMENT_BYTES`.
121
121
  */
122
- export function validateDrafts(
123
- drafts: AssistantAttachmentDraft[],
124
- ): ValidatedDrafts {
125
- const accepted: AssistantAttachmentDraft[] = [];
122
+ export function validateDrafts<T extends AssistantAttachmentDraft>(
123
+ drafts: T[],
124
+ ): ValidatedDrafts<T> {
125
+ const accepted: T[] = [];
126
126
  const warnings: string[] = [];
127
127
 
128
128
  for (const draft of drafts) {
@@ -800,6 +800,16 @@ function toolNameToFilePrefix(toolName?: string): string {
800
800
  .toLowerCase();
801
801
  }
802
802
 
803
+ /** Shared download name for an image produced by a tool result. */
804
+ export function toolImageFilename(
805
+ mediaType: string,
806
+ toolName?: string,
807
+ title?: string,
808
+ ): string {
809
+ const ext = mediaType.split("/")[1] ?? "png";
810
+ return `${title || toolNameToFilePrefix(toolName)}.${ext}`;
811
+ }
812
+
803
813
  /**
804
814
  * Convert tool content blocks (images/files from tool results) into
805
815
  * attachment drafts. Blocks that aren't image or file types are skipped.
@@ -820,12 +830,10 @@ export function contentBlocksToDrafts(
820
830
  const src = b.source as ImageBlock["source"];
821
831
  const data = src.data;
822
832
  const mimeType = src.media_type;
823
- const ext = mimeType.split("/")[1] ?? "png";
824
833
  const title = typeof b._title === "string" ? b._title : undefined;
825
- const prefix = title || toolNameToFilePrefix(toolName);
826
834
  drafts.push({
827
835
  sourceType: "tool_block",
828
- filename: `${prefix}.${ext}`,
836
+ filename: toolImageFilename(mimeType, toolName, title),
829
837
  mimeType,
830
838
  dataBase64: data,
831
839
  sizeBytes: estimateBase64Bytes(data),
@@ -918,9 +926,9 @@ export function cleanAssistantContent(content: readonly unknown[]): {
918
926
  * data from being attached twice when it appears as both a directive tag
919
927
  * (user-chosen name) and an auto-converted tool block ("tool-output.png").
920
928
  */
921
- export function deduplicateDrafts(
922
- drafts: AssistantAttachmentDraft[],
923
- ): AssistantAttachmentDraft[] {
929
+ export function deduplicateDrafts<T extends AssistantAttachmentDraft>(
930
+ drafts: T[],
931
+ ): T[] {
924
932
  const seenKeys = new Set<string>();
925
933
  const seenDirectiveHashes = new Set<string>();
926
934
  return drafts.filter((d) => {
@@ -10,7 +10,8 @@
10
10
  *
11
11
  * 1. While a turn runs, every shell-style tool command is scanned for
12
12
  * `credentials reveal` invocations and the referenced credentials
13
- * (`--service`/`--field` flags or a positional UUID) are recorded as
13
+ * (`--service`/`--field` flags, a positional UUID, or a legacy
14
+ * `service/field` path) are recorded as
14
15
  * *candidates* ({@link collectRevealRefsFromCommand}).
15
16
  * 2. At persist time, only those candidates' plaintexts are fetched from
16
17
  * the credential store — a scoped read of secrets that were already in
@@ -55,6 +56,7 @@ import {
55
56
  } from "../security/secret-scanner.js";
56
57
  import { getSecureKeyAsync } from "../security/secure-keys.js";
57
58
  import { getCredentialMetadataById } from "../tools/credentials/metadata-store.js";
59
+ import { parseServiceFieldRef } from "../tools/credentials/ref-parse.js";
58
60
  import { getLogger } from "../util/logger.js";
59
61
 
60
62
  const log = getLogger("chat-credential-redaction");
@@ -106,6 +108,12 @@ const SERVICE_FLAG_RE = new RegExp(String.raw`--service(?:=|\s+)${FLAG_VALUE}`);
106
108
  const FIELD_FLAG_RE = new RegExp(String.raw`--field(?:=|\s+)${FLAG_VALUE}`);
107
109
  const UUID_RE =
108
110
  /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i;
111
+ // The CLI's single positional argument, directly after `reveal`. A UUID is
112
+ // handled above; anything else is tried as a legacy `service/field` path via
113
+ // the shared grammar in `parseServiceFieldRef`. The route rejects that form
114
+ // as an id today, so no plaintext prints; staging the ref is defense in depth
115
+ // so a future lookup that does succeed is already proof-gated.
116
+ const POSITIONAL_RE = /\breveal\s+([^\s"'\\()<>`-][^\s"'\\()<>`]*)/;
109
117
 
110
118
  /**
111
119
  * Extract a flag's value from a segment, undoing the shell quoting the
@@ -265,6 +273,13 @@ export function collectRevealRefsFromCommand(
265
273
  const id = UUID_RE.exec(invocation)?.[0];
266
274
  if (id) {
267
275
  refs.push({ id });
276
+ continue;
277
+ }
278
+ const positional = POSITIONAL_RE.exec(invocation)?.[1];
279
+ const pathRef =
280
+ positional === undefined ? undefined : parseServiceFieldRef(positional);
281
+ if (pathRef) {
282
+ refs.push(pathRef);
268
283
  }
269
284
  // No parseable identity → no ref. The span still gets redacted at
270
285
  // persist; it just won't be revealable.