@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
@@ -51,19 +51,19 @@ describe("safe-env Qdrant forwarding", () => {
51
51
  });
52
52
 
53
53
  describe("safe-env CES child forwarding", () => {
54
- test("forwards CES HTTP credentials so sanitized children can resolve managed connections", () => {
54
+ test("forwards CES IPC discovery env and CES HTTP credentials", () => {
55
+ expect(SAFE_ENV_VARS).toContain("CES_BOOTSTRAP_SOCKET_DIR");
55
56
  expect(SAFE_ENV_VARS).toContain("CES_SERVICE_TOKEN");
56
57
  expect(SAFE_ENV_VARS).toContain("CES_CREDENTIAL_URL");
57
- expect(SAFE_ENV_VARS).toContain("CES_LOCAL_SOCKET");
58
58
 
59
59
  const env = buildSanitizedEnv("linux", {
60
+ CES_BOOTSTRAP_SOCKET_DIR: "/run/ces-bootstrap",
60
61
  CES_SERVICE_TOKEN: "vault-bearer",
61
62
  CES_CREDENTIAL_URL: "http://127.0.0.1:8090",
62
- CES_LOCAL_SOCKET: "/tmp/ces.sock",
63
63
  });
64
+ expect(env.CES_BOOTSTRAP_SOCKET_DIR).toBe("/run/ces-bootstrap");
64
65
  expect(env.CES_SERVICE_TOKEN).toBe("vault-bearer");
65
66
  expect(env.CES_CREDENTIAL_URL).toBe("http://127.0.0.1:8090");
66
- expect(env.CES_LOCAL_SOCKET).toBe("/tmp/ces.sock");
67
67
  });
68
68
  });
69
69
 
@@ -111,6 +111,35 @@ describe("safe-env Windows forwarding", () => {
111
111
  }
112
112
  });
113
113
 
114
+ test("prepends the Windows assistant binary directory when it sits beside the runtime", () => {
115
+ const dir = mkdtempSync(join(tmpdir(), "win-assistant-"));
116
+ writeFileSync(join(dir, "assistant.exe"), "");
117
+ try {
118
+ const env = buildSanitizedEnv(
119
+ "win32",
120
+ { Path: "C:\\Windows\\System32" },
121
+ { execPath: join(dir, "vellum-daemon.exe") },
122
+ );
123
+ expect(env.PATH).toBe(`${dir};C:\\Windows\\System32`);
124
+ } finally {
125
+ rmSync(dir, { recursive: true, force: true });
126
+ }
127
+ });
128
+
129
+ test("does not invent a Windows assistant path when the sibling binary is absent", () => {
130
+ const dir = mkdtempSync(join(tmpdir(), "win-no-assistant-"));
131
+ try {
132
+ const env = buildSanitizedEnv(
133
+ "win32",
134
+ { Path: "C:\\Windows\\System32" },
135
+ { execPath: join(dir, "vellum-daemon.exe") },
136
+ );
137
+ expect(env.PATH).toBe("C:\\Windows\\System32");
138
+ } finally {
139
+ rmSync(dir, { recursive: true, force: true });
140
+ }
141
+ });
142
+
114
143
  test("matches Windows environment names case-insensitively", () => {
115
144
  const windowsEnv = buildSanitizedEnv("win32", {
116
145
  Path: "C:\\Windows\\System32;C:\\Program Files\\Vellum",
@@ -4,7 +4,6 @@ import { runSanitizedBash } from "../sanitized-bash.js";
4
4
 
5
5
  describe("runSanitizedBash", () => {
6
6
  const priorToken = process.env.CES_SERVICE_TOKEN;
7
- const priorSocket = process.env.CES_LOCAL_SOCKET;
8
7
 
9
8
  afterEach(() => {
10
9
  if (priorToken == null) {
@@ -12,11 +11,6 @@ describe("runSanitizedBash", () => {
12
11
  } else {
13
12
  process.env.CES_SERVICE_TOKEN = priorToken;
14
13
  }
15
- if (priorSocket == null) {
16
- delete process.env.CES_LOCAL_SOCKET;
17
- } else {
18
- process.env.CES_LOCAL_SOCKET = priorSocket;
19
- }
20
14
  });
21
15
 
22
16
  test("runs a command and returns stdout", async () => {
@@ -35,14 +29,6 @@ describe("runSanitizedBash", () => {
35
29
  expect(result.stdout.trim()).toBe("vault-bearer");
36
30
  });
37
31
 
38
- test("forwards CES_LOCAL_SOCKET to the child", async () => {
39
- process.env.CES_LOCAL_SOCKET = "/tmp/ces.sock";
40
- const result = await runSanitizedBash("printenv CES_LOCAL_SOCKET", 5_000);
41
- expect(result.error).toBeUndefined();
42
- expect(result.exitCode).toBe(0);
43
- expect(result.stdout.trim()).toBe("/tmp/ces.sock");
44
- });
45
-
46
32
  test("times out a hung command", async () => {
47
33
  const result = await runSanitizedBash("sleep 5", 200);
48
34
  expect(result.error).toBeUndefined();
@@ -7,8 +7,12 @@
7
7
  */
8
8
  import { readdirSync, statSync } from "node:fs";
9
9
  import { tmpdir } from "node:os";
10
+ import { dirname, join } from "node:path";
10
11
 
11
- import { pathListDelimiter } from "@vellumai/environments/shell";
12
+ import {
13
+ pathListDelimiter,
14
+ prependUniquePathEntries,
15
+ } from "@vellumai/environments/shell";
12
16
 
13
17
  import { getGatewayInternalBaseUrl } from "../../config/env.js";
14
18
  import { getDataDir, getWorkspaceDir } from "../../util/platform.js";
@@ -55,34 +59,16 @@ export const SAFE_ENV_VARS = [
55
59
  "VELLUM_MIGRATION_EXPORT_ALLOWED_HOSTS",
56
60
  "VELLUM_MIGRATION_IMPORT_ALLOWED_HOSTS",
57
61
  "CES_MANAGED_MODE",
58
- "CES_LOCAL_SOCKET",
59
- // Child processes (bash, skill sandbox, scheduled scripts) run
60
- // `assistant oauth request` in-process and resolve managed connections
61
- // through CES HTTP (`getSecureKeyAsync` / platform client). They need
62
- // both the credential URL and the service token. CES_LOCAL_SOCKET alone
63
- // is not enough on managed pods: the socket is often absent or does not
64
- // complete the lazy RPC path those children use, so a connected account
65
- // looks disconnected from sanitized children while the daemon still
66
- // reports healthy.
67
- //
68
- // To drop these later, children must resolve the same ACTIVE managed
69
- // connections without inheriting a reusable vault bearer. That means
70
- // CES RPC over CES_LOCAL_SOCKET works on managed pods (present,
71
- // reachable, and sufficient for secure-key / platform-client reads), or
72
- // a narrower grant protocol issues a one-shot provider token.
73
- //
74
- // Before removing them again, reproduce from a sanitized child on a
75
- // platform-managed pod. After `buildSanitizedEnv()`,
76
- // `assistant oauth request --provider <key> --json <provider-url>` must
77
- // succeed for an ACTIVE connection and match a direct terminal
78
- // invocation. Unit tests that only assert the vars are absent are not
79
- // enough: also cover skill sandbox and scheduled-script children.
80
62
  "CES_CREDENTIAL_URL",
81
63
  "CES_SERVICE_TOKEN",
64
+ // Child processes (bash, skill sandbox, scheduled scripts) reach CES
65
+ // over IPC via `CES_BOOTSTRAP_SOCKET_DIR`. CES HTTP credentials are
66
+ // forwarded so children can fail over to CES HTTP while that transport
67
+ // still exists.
82
68
  // Per-instance port of the assistant-managed Qdrant sidecar, so skill and
83
69
  // bash-tool subprocesses that use the vector helpers (e.g. embed/search over
84
70
  // `@vellumai/plugin-api`) resolve the same local sidecar as the daemon
85
- // (127.0.0.1:<port>). `QDRANT_URL` is intentionally excluded it flips
71
+ // (127.0.0.1:<port>). `QDRANT_URL` is intentionally excluded: it flips
86
72
  // QdrantManager into external mode and bypasses the local managed lifecycle.
87
73
  "QDRANT_HTTP_PORT",
88
74
  "IS_CONTAINERIZED",
@@ -284,6 +270,7 @@ function appendUniquePathEntries(
284
270
  export function buildSanitizedEnv(
285
271
  hostPlatform: NodeJS.Platform = process.platform,
286
272
  sourceEnv: NodeJS.ProcessEnv = process.env,
273
+ options?: { execPath?: string },
287
274
  ): Record<string, string> {
288
275
  const env: Record<string, string> = {};
289
276
  const isKataRuntime = isKataFamilyRuntime(sourceEnv.VELLUM_SANDBOX_RUNTIME);
@@ -360,5 +347,29 @@ export function buildSanitizedEnv(
360
347
  if (!env.LC_ALL) {
361
348
  env.LC_ALL = utf8Locale;
362
349
  }
350
+ prependWindowsAssistantDir(
351
+ env,
352
+ hostPlatform,
353
+ options?.execPath ?? process.execPath,
354
+ );
363
355
  return env;
364
356
  }
357
+
358
+ function prependWindowsAssistantDir(
359
+ env: Record<string, string>,
360
+ hostPlatform: NodeJS.Platform,
361
+ execPath: string,
362
+ ): void {
363
+ if (hostPlatform !== "win32") {
364
+ return;
365
+ }
366
+ const execDir = dirname(execPath);
367
+ try {
368
+ if (!statSync(join(execDir, "assistant.exe")).isFile()) {
369
+ return;
370
+ }
371
+ } catch {
372
+ return;
373
+ }
374
+ env.PATH = prependUniquePathEntries(env.PATH, [execDir], hostPlatform);
375
+ }
@@ -11,9 +11,12 @@ import { spawn } from "node:child_process";
11
11
 
12
12
  import {
13
13
  buildShellInvocation,
14
+ buildShellSpawnFlags,
14
15
  terminateProcessTree,
16
+ watchShellProcessStart,
15
17
  } from "../../util/host-process.js";
16
18
  import { getWorkspaceDir } from "../../util/platform.js";
19
+ import { SHELL_DID_NOT_START_MESSAGE } from "../shared/shell-output.js";
17
20
  import { buildSanitizedEnv } from "./safe-env.js";
18
21
 
19
22
  export const DEFAULT_SANITIZED_BASH_TIMEOUT_MS = 30_000;
@@ -63,10 +66,10 @@ export function runSanitizedBash(
63
66
  const child = spawn(wrapped.command, wrapped.args, {
64
67
  cwd: getWorkspaceDir(),
65
68
  stdio: ["ignore", "pipe", "pipe"],
66
- detached: true,
67
69
  env: buildSanitizedEnv(),
68
- windowsHide: true,
70
+ ...buildShellSpawnFlags(),
69
71
  });
72
+ const launch = watchShellProcessStart(child);
70
73
 
71
74
  const timer = setTimeout(() => {
72
75
  timedOut = true;
@@ -83,6 +86,16 @@ export function runSanitizedBash(
83
86
 
84
87
  child.on("close", (code) => {
85
88
  clearTimeout(timer);
89
+ if (!launch.didStart()) {
90
+ finish({
91
+ stdout: "",
92
+ stderr: "",
93
+ exitCode: null,
94
+ timedOut: false,
95
+ error: SHELL_DID_NOT_START_MESSAGE,
96
+ });
97
+ return;
98
+ }
86
99
  finish({
87
100
  stdout: Buffer.concat(stdoutChunks).toString(),
88
101
  stderr: Buffer.concat(stderrChunks).toString(),
@@ -0,0 +1,162 @@
1
+ import * as realChildProcess from "node:child_process";
2
+ import { EventEmitter } from "node:events";
3
+ import { existsSync, mkdtempSync, rmSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
7
+
8
+ import { setConfig } from "../../__tests__/helpers/set-config.js";
9
+ import { SHELL_DID_NOT_START_MESSAGE } from "../shared/shell-output.js";
10
+ import type { ToolContext } from "../types.js";
11
+
12
+ const originalSpawn = realChildProcess.spawn;
13
+
14
+ type FakeChild = EventEmitter & {
15
+ pid?: number;
16
+ stdout: EventEmitter;
17
+ stderr: EventEmitter;
18
+ kill: () => boolean;
19
+ };
20
+
21
+ type SpawnImpl = (
22
+ ...args: Parameters<typeof realChildProcess.spawn>
23
+ ) => ReturnType<typeof realChildProcess.spawn>;
24
+
25
+ const spawnCalls: {
26
+ command: string;
27
+ args: unknown;
28
+ options: realChildProcess.SpawnOptions;
29
+ }[] = [];
30
+
31
+ let spawnImpl: SpawnImpl = originalSpawn as SpawnImpl;
32
+
33
+ const spawnSpy = mock((...args: Parameters<typeof realChildProcess.spawn>) => {
34
+ spawnCalls.push({
35
+ command: args[0] as string,
36
+ args: args[1],
37
+ options: (args[2] ?? {}) as realChildProcess.SpawnOptions,
38
+ });
39
+ return spawnImpl(...args);
40
+ });
41
+
42
+ mock.module("node:child_process", () => ({
43
+ ...realChildProcess,
44
+ spawn: spawnSpy,
45
+ }));
46
+
47
+ const realLogger = await import("../../util/logger.js");
48
+ mock.module("../../util/logger.js", () => ({
49
+ ...realLogger,
50
+ getLogger: () =>
51
+ new Proxy({} as Record<string, unknown>, {
52
+ get: () => () => {},
53
+ }),
54
+ }));
55
+
56
+ setConfig("timeouts", { shellDefaultTimeoutSec: 30, shellMaxTimeoutSec: 60 });
57
+
58
+ const realHub = await import("../../runtime/assistant-event-hub.js");
59
+ mock.module("../../runtime/assistant-event-hub.js", () => ({
60
+ ...realHub,
61
+ broadcastMessage: () => {},
62
+ }));
63
+
64
+ const realWake = await import("../../runtime/agent-wake.js");
65
+ mock.module("../../runtime/agent-wake.js", () => ({
66
+ ...realWake,
67
+ wakeAgentForOpportunity: async () => ({}),
68
+ }));
69
+
70
+ const { shellTool } = await import("./shell.js");
71
+
72
+ function makeContext(workingDir: string): ToolContext {
73
+ return {
74
+ workingDir,
75
+ conversationId: "conv-xyz",
76
+ trustClass: "guardian",
77
+ };
78
+ }
79
+
80
+ function fakeChild(): FakeChild {
81
+ const child = new EventEmitter() as FakeChild;
82
+ child.stdout = new EventEmitter();
83
+ child.stderr = new EventEmitter();
84
+ child.kill = () => true;
85
+ return child;
86
+ }
87
+
88
+ function closeWithoutStart(): ReturnType<typeof realChildProcess.spawn> {
89
+ const child = fakeChild();
90
+ queueMicrotask(() => {
91
+ child.emit("close", 0, null);
92
+ });
93
+ return child as unknown as ReturnType<typeof realChildProcess.spawn>;
94
+ }
95
+
96
+ function spawnError(
97
+ err: NodeJS.ErrnoException,
98
+ ): ReturnType<typeof realChildProcess.spawn> {
99
+ const child = fakeChild();
100
+ queueMicrotask(() => {
101
+ child.emit("error", err);
102
+ });
103
+ return child as unknown as ReturnType<typeof realChildProcess.spawn>;
104
+ }
105
+
106
+ describe("bash launch failures are not success", () => {
107
+ let workingDir = "";
108
+
109
+ beforeEach(() => {
110
+ spawnCalls.length = 0;
111
+ spawnImpl = originalSpawn as SpawnImpl;
112
+ workingDir = mkdtempSync(join(tmpdir(), "bash-launch-"));
113
+ });
114
+
115
+ afterEach(() => {
116
+ spawnImpl = originalSpawn as SpawnImpl;
117
+ rmSync(workingDir, { recursive: true, force: true });
118
+ });
119
+
120
+ test("a close with exit 0 and no process start is an error, not command_completed", async () => {
121
+ spawnImpl = () => closeWithoutStart();
122
+ const proof = join(workingDir, "proof.txt");
123
+
124
+ const result = await shellTool.execute(
125
+ {
126
+ command: "printf ran > proof.txt",
127
+ activity: "test",
128
+ },
129
+ makeContext(workingDir),
130
+ );
131
+
132
+ expect(result.isError).toBe(true);
133
+ expect(result.content).toBe(SHELL_DID_NOT_START_MESSAGE);
134
+ expect(result.content).not.toContain("<command_completed />");
135
+ expect(existsSync(proof)).toBe(false);
136
+ expect(spawnCalls).toHaveLength(1);
137
+ });
138
+
139
+ test("a failed spawn of the shell is an error", async () => {
140
+ spawnImpl = () => {
141
+ const err = new Error(
142
+ "spawn powershell.exe ENOENT",
143
+ ) as NodeJS.ErrnoException;
144
+ err.code = "ENOENT";
145
+ return spawnError(err);
146
+ };
147
+
148
+ const result = await shellTool.execute(
149
+ {
150
+ command: "assistant browser --help",
151
+ activity: "test",
152
+ },
153
+ makeContext(workingDir),
154
+ );
155
+
156
+ expect(result.isError).toBe(true);
157
+ expect(result.content).toContain("Error spawning command");
158
+ expect(result.content).toContain("The command was not found");
159
+ expect(result.content).not.toContain("<command_completed />");
160
+ expect(existsSync(join(workingDir, "help.txt"))).toBe(false);
161
+ });
162
+ });
@@ -7,6 +7,7 @@ import { setConfig } from "../../__tests__/helpers/set-config.js";
7
7
  import {
8
8
  MAX_OUTPUT_LENGTH,
9
9
  OUTPUT_TRUNCATED_TAG,
10
+ SHELL_DID_NOT_START_MESSAGE,
10
11
  } from "../shared/shell-output.js";
11
12
  import type { ToolContext } from "../types.js";
12
13
 
@@ -207,6 +208,34 @@ describe("foreground stdin handling", () => {
207
208
  });
208
209
  });
209
210
 
211
+ describe("foreground command execution", () => {
212
+ let tmpDir = "";
213
+
214
+ beforeEach(async () => {
215
+ tmpDir = await mkdtemp(join(tmpdir(), "bash-exec-"));
216
+ });
217
+
218
+ afterEach(async () => {
219
+ if (tmpDir) {
220
+ await rm(tmpDir, { recursive: true, force: true });
221
+ }
222
+ });
223
+
224
+ test("a command that writes a file must actually create it", async () => {
225
+ const result = await shellTool.execute(
226
+ {
227
+ command: "printf ran > proof.txt",
228
+ activity: "test",
229
+ },
230
+ { ...makeContext(), workingDir: tmpDir },
231
+ );
232
+
233
+ expect(result.isError).toBeFalsy();
234
+ expect(result.content).not.toBe(SHELL_DID_NOT_START_MESSAGE);
235
+ expect(await readFile(join(tmpDir, "proof.txt"), "utf-8")).toBe("ran");
236
+ });
237
+ });
238
+
210
239
  describe("streaming stdout cap", () => {
211
240
  let tmpDir = "";
212
241
 
@@ -11,7 +11,9 @@ import { conversationRevealNonce } from "../../runtime/reveal-nonce.js";
11
11
  import { redactSecrets } from "../../security/secret-scanner.js";
12
12
  import {
13
13
  buildShellInvocation,
14
+ buildShellSpawnFlags,
14
15
  terminateProcessTree,
16
+ watchShellProcessStart,
15
17
  } from "../../util/host-process.js";
16
18
  import { getLogger } from "../../util/logger.js";
17
19
  import { getDataDir } from "../../util/platform.js";
@@ -324,9 +326,9 @@ export const shellTool = {
324
326
  cwd: context.workingDir,
325
327
  env,
326
328
  stdio: ["ignore", "pipe", "pipe"],
327
- detached: true,
328
- windowsHide: true,
329
+ ...buildShellSpawnFlags(),
329
330
  });
331
+ const launch = watchShellProcessStart(child);
330
332
  const collector = attachBoundedStdio(child);
331
333
 
332
334
  const killTree = buildKillTree(child, {
@@ -367,11 +369,13 @@ export const shellTool = {
367
369
  timedOut,
368
370
  });
369
371
 
370
- const fmtResult = collector.format(code, timedOut, timeoutSec);
372
+ const fmtResult = collector.format(code, timedOut, timeoutSec, {
373
+ started: launch.didStart(),
374
+ });
371
375
 
372
376
  const status: BackgroundToolCompletedEvent["status"] = aborted
373
377
  ? "cancelled"
374
- : timedOut
378
+ : timedOut || fmtResult.isError
375
379
  ? "failed"
376
380
  : code === 0
377
381
  ? "completed"
@@ -529,9 +533,9 @@ export const shellTool = {
529
533
  cwd: context.workingDir,
530
534
  env,
531
535
  stdio: ["ignore", "pipe", "pipe"],
532
- detached: true,
533
- windowsHide: true,
536
+ ...buildShellSpawnFlags(),
534
537
  });
538
+ const launch = watchShellProcessStart(child);
535
539
  const collector = attachBoundedStdio(child, {
536
540
  onOutput: context.onOutput,
537
541
  });
@@ -573,7 +577,9 @@ export const shellTool = {
573
577
  timedOut,
574
578
  });
575
579
 
576
- const fmtResult = collector.format(code, timedOut, timeoutSec);
580
+ const fmtResult = collector.format(code, timedOut, timeoutSec, {
581
+ started: launch.didStart(),
582
+ });
577
583
 
578
584
  resolve({
579
585
  content: fmtResult.content,
@@ -0,0 +1,14 @@
1
+ export const HUMAN_VERIFICATION_GUIDANCE =
2
+ "CAPTCHAs and bot-detection challenges must be completed by the user. " +
3
+ "This includes drag-to-verify sliders, press-and-hold checks, verification checkboxes and image puzzles. " +
4
+ "Stop before attempting the challenge, even when its controls look easy to automate. " +
5
+ "Do not click, drag, hold, script or retry the verification yourself.";
6
+
7
+ export const DESKTOP_HELP_GUIDANCE =
8
+ HUMAN_VERIFICATION_GUIDANCE +
9
+ " In the virtual desktop, immediately call ask_question with " +
10
+ "desktopHelp: { message, doneLabel, skipLabel } in the user's language. " +
11
+ "Keep message to one short sentence explaining the needed human action. " +
12
+ "This shows an inline preview with Step In, Done and Skip. Wait for the response. " +
13
+ "After Done, inspect a fresh snapshot. If verification remains, request help again. " +
14
+ "Skip leaves the challenge unresolved. Use the same desktopHelp request for other steps that require direct user interaction.";
@@ -1,6 +1,7 @@
1
+ import { EventEmitter } from "node:events";
1
2
  import { expect, mock, test } from "bun:test";
2
3
 
3
- import { terminateProcessTree } from "./host-process.js";
4
+ import { terminateProcessTree, watchShellProcessStart } from "./host-process.js";
4
5
 
5
6
  test("Windows process trees use taskkill and fall back to the direct child", () => {
6
7
  const kill = mock(() => true);
@@ -15,3 +16,18 @@ test("Windows process trees use taskkill and fall back to the direct child", ()
15
16
  expect(kill).toHaveBeenCalledTimes(1);
16
17
  expect(kill).toHaveBeenCalledWith();
17
18
  });
19
+
20
+ test("watchShellProcessStart is false until spawn or a pid appears", () => {
21
+ const child = new EventEmitter() as EventEmitter & { pid?: number };
22
+ const launch = watchShellProcessStart(child);
23
+ expect(launch.didStart()).toBe(false);
24
+
25
+ child.emit("spawn");
26
+ expect(launch.didStart()).toBe(true);
27
+ });
28
+
29
+ test("watchShellProcessStart treats a pid assigned at construction as started", () => {
30
+ const child = new EventEmitter() as EventEmitter & { pid?: number };
31
+ child.pid = 4242;
32
+ expect(watchShellProcessStart(child).didStart()).toBe(true);
33
+ });
@@ -2,10 +2,34 @@ import { spawn } from "node:child_process";
2
2
 
3
3
  export {
4
4
  buildShellInvocation,
5
+ buildShellSpawnFlags,
5
6
  pathListDelimiter,
6
7
  prependUniquePathEntries,
7
8
  } from "@vellumai/environments/shell";
8
9
 
10
+ export interface ShellProcessStartWatch {
11
+ didStart(): boolean;
12
+ }
13
+
14
+ /**
15
+ * Record whether a spawned child actually started. A `close` with exit 0 and
16
+ * empty stdio is not proof of execution: Windows can emit that when the
17
+ * process was never created. Callers must treat `didStart() === false` as a
18
+ * launch failure.
19
+ */
20
+ export function watchShellProcessStart(child: {
21
+ pid?: number;
22
+ once(event: "spawn", listener: () => void): unknown;
23
+ }): ShellProcessStartWatch {
24
+ let spawnSeen = child.pid != null;
25
+ child.once("spawn", () => {
26
+ spawnSeen = true;
27
+ });
28
+ return {
29
+ didStart: () => spawnSeen || child.pid != null,
30
+ };
31
+ }
32
+
9
33
  export interface KillableProcess {
10
34
  pid?: number;
11
35
  kill: unknown;
@@ -67,6 +67,35 @@ export function safeStringSlice(
67
67
  return str.slice(safeStart, safeEnd);
68
68
  }
69
69
 
70
+ /**
71
+ * A `[start, end)` character window of at most `maxChars` code units that
72
+ * never splits a surrogate pair, for paging through text by offset. A split
73
+ * leaves a lone half at each edge, and each encodes to U+FFFD, so the
74
+ * character is lost from both this window and the next one paged in after
75
+ * it. `charCodeAt` reads the underlying text so a caller need not hold it as
76
+ * one string.
77
+ */
78
+ export function surrogateSafeWindow(
79
+ total: number,
80
+ charCodeAt: (index: number) => number,
81
+ requestedStart: number,
82
+ maxChars: number,
83
+ ): { start: number; end: number } {
84
+ let start = Math.max(0, Math.min(requestedStart, total));
85
+ if (start > 0 && start < total && isLowSurrogate(charCodeAt(start))) {
86
+ start -= 1;
87
+ }
88
+
89
+ let end = Math.min(total, start + maxChars);
90
+ if (end > start && end < total && isHighSurrogate(charCodeAt(end - 1))) {
91
+ // Backing off would empty a one-character window, which stalls paging on
92
+ // the same offset, so take the whole pair instead.
93
+ end = end - 1 > start ? end - 1 : Math.min(total, end + 1);
94
+ }
95
+
96
+ return { start, end };
97
+ }
98
+
70
99
  /**
71
100
  * Replace every orphaned UTF-16 surrogate in `str` with U+FFFD
72
101
  * (REPLACEMENT CHARACTER).