@vellumai/assistant 0.6.2 → 0.6.3

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 (396) hide show
  1. package/bun.lock +40 -40
  2. package/bunfig.toml +3 -0
  3. package/docs/architecture/memory.md +1 -1
  4. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +42 -0
  5. package/openapi.yaml +184 -69
  6. package/package.json +41 -41
  7. package/scripts/generate-openapi.ts +1 -2
  8. package/src/__tests__/acp-session.test.ts +43 -0
  9. package/src/__tests__/app-builder-tool-scripts.test.ts +1 -0
  10. package/src/__tests__/app-executors.test.ts +1 -0
  11. package/src/__tests__/app-source-watcher.test.ts +37 -11
  12. package/src/__tests__/approval-routes-http.test.ts +178 -1
  13. package/src/__tests__/browser-fill-credential.test.ts +229 -94
  14. package/src/__tests__/browser-manager.test.ts +40 -27
  15. package/src/__tests__/catalog-files.test.ts +862 -0
  16. package/src/__tests__/channel-approvals.test.ts +53 -0
  17. package/src/__tests__/config-managed-gemini-defaults.test.ts +326 -0
  18. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  19. package/src/__tests__/config-schema.test.ts +125 -48
  20. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +23 -0
  21. package/src/__tests__/context-overflow-approval.test.ts +16 -1
  22. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -1
  23. package/src/__tests__/conversation-agent-loop.test.ts +1 -1
  24. package/src/__tests__/conversation-analysis-routes.test.ts +2 -2
  25. package/src/__tests__/conversation-attachments.test.ts +80 -4
  26. package/src/__tests__/conversation-confirmation-signals.test.ts +155 -0
  27. package/src/__tests__/conversation-fork-crud.test.ts +17 -0
  28. package/src/__tests__/conversation-history-web-search.test.ts +1 -0
  29. package/src/__tests__/conversation-host-access-routes.test.ts +229 -0
  30. package/src/__tests__/conversation-inject-context.test.ts +103 -0
  31. package/src/__tests__/conversation-queue.test.ts +45 -2
  32. package/src/__tests__/conversation-routes-disk-view.test.ts +5 -0
  33. package/src/__tests__/conversation-routes-guardian-reply.test.ts +16 -0
  34. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  35. package/src/__tests__/conversation-runtime-assembly.test.ts +269 -46
  36. package/src/__tests__/conversation-starter-routes.test.ts +126 -0
  37. package/src/__tests__/conversation-starters-cadence.test.ts +161 -0
  38. package/src/__tests__/conversation-store.test.ts +195 -0
  39. package/src/__tests__/conversation-workspace-cache-state.test.ts +193 -0
  40. package/src/__tests__/credential-execution-approval-bridge.test.ts +32 -1
  41. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  42. package/src/__tests__/credential-vault-unit.test.ts +4 -4
  43. package/src/__tests__/credential-vault.test.ts +152 -13
  44. package/src/__tests__/credentials-cli.test.ts +2 -2
  45. package/src/__tests__/date-context.test.ts +4 -4
  46. package/src/__tests__/embedding-managed-proxy-selection.test.ts +256 -0
  47. package/src/__tests__/extension-id-sync-guard.test.ts +155 -0
  48. package/src/__tests__/fixtures/mock-chrome-extension.ts +375 -0
  49. package/src/__tests__/gateway-only-guard.test.ts +3 -0
  50. package/src/__tests__/gemini-provider.test.ts +2 -2
  51. package/src/__tests__/guardian-routing-invariants.test.ts +70 -2
  52. package/src/__tests__/headless-browser-interactions.test.ts +707 -371
  53. package/src/__tests__/headless-browser-navigate.test.ts +389 -47
  54. package/src/__tests__/headless-browser-read-tools.test.ts +266 -103
  55. package/src/__tests__/headless-browser-snapshot.test.ts +240 -77
  56. package/src/__tests__/host-bash-proxy.test.ts +150 -1
  57. package/src/__tests__/host-browser-e2e-cloud.test.ts +462 -0
  58. package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +286 -0
  59. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +374 -0
  60. package/src/__tests__/host-browser-event-routes.test.ts +350 -0
  61. package/src/__tests__/host-browser-proxy.test.ts +444 -0
  62. package/src/__tests__/host-browser-routes.test.ts +198 -0
  63. package/src/__tests__/host-browser-ws-events-e2e.test.ts +320 -0
  64. package/src/__tests__/host-cu-proxy.test.ts +171 -1
  65. package/src/__tests__/host-file-proxy.test.ts +185 -1
  66. package/src/__tests__/host-file-read-tool.test.ts +52 -0
  67. package/src/__tests__/host-proxy-interface.test.ts +165 -0
  68. package/src/__tests__/host-shell-tool.test.ts +1 -11
  69. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  70. package/src/__tests__/integration-status.test.ts +6 -7
  71. package/src/__tests__/list-messages-tool-merge.test.ts +37 -12
  72. package/src/__tests__/mcp-client-auth.test.ts +40 -4
  73. package/src/__tests__/mcp-health-check.test.ts +10 -3
  74. package/src/__tests__/migration-cross-version-compatibility.test.ts +3 -1
  75. package/src/__tests__/migration-export-http.test.ts +61 -2
  76. package/src/__tests__/migration-export-streaming.test.ts +66 -0
  77. package/src/__tests__/migration-import-commit-http.test.ts +101 -1
  78. package/src/__tests__/native-host-marker-sync-guard.test.ts +157 -0
  79. package/src/__tests__/oauth-apps-routes.test.ts +17 -12
  80. package/src/__tests__/oauth-cli.test.ts +707 -60
  81. package/src/__tests__/oauth-connect-orchestrator.test.ts +116 -24
  82. package/src/__tests__/oauth-provider-seed-logos.test.ts +23 -0
  83. package/src/__tests__/oauth-provider-serializer.test.ts +146 -10
  84. package/src/__tests__/oauth-provider-visibility.test.ts +19 -21
  85. package/src/__tests__/oauth-providers-routes.test.ts +50 -14
  86. package/src/__tests__/oauth-store.test.ts +1386 -182
  87. package/src/__tests__/oauth2-gateway-transport.test.ts +211 -20
  88. package/src/__tests__/onboarding-template-contract.test.ts +75 -57
  89. package/src/__tests__/openai-provider.test.ts +2 -2
  90. package/src/__tests__/outlook-categories.test.ts +1 -1
  91. package/src/__tests__/outlook-client-automation.test.ts +1 -1
  92. package/src/__tests__/outlook-compose-tools.test.ts +1 -1
  93. package/src/__tests__/outlook-email-watcher.test.ts +1 -1
  94. package/src/__tests__/outlook-follow-up.test.ts +1 -1
  95. package/src/__tests__/outlook-messaging-provider.test.ts +2 -2
  96. package/src/__tests__/outlook-trash.test.ts +1 -1
  97. package/src/__tests__/outlook-unsubscribe.test.ts +1 -1
  98. package/src/__tests__/permission-checker-host-gate.test.ts +74 -14
  99. package/src/__tests__/permission-mode.test.ts +28 -56
  100. package/src/__tests__/platform-callback-registration.test.ts +19 -0
  101. package/src/__tests__/post-turn-tool-result-truncation.test.ts +296 -0
  102. package/src/__tests__/proxy-approval-callback.test.ts +18 -0
  103. package/src/__tests__/require-fresh-approval.test.ts +40 -1
  104. package/src/__tests__/sanitize-config-for-transfer.test.ts +132 -0
  105. package/src/__tests__/schedule-routes.test.ts +162 -0
  106. package/src/__tests__/secret-detection-handler.test.ts +84 -0
  107. package/src/__tests__/secret-ingress-http.test.ts +1 -0
  108. package/src/__tests__/send-endpoint-busy.test.ts +3 -0
  109. package/src/__tests__/set-permission-mode.test.ts +13 -250
  110. package/src/__tests__/skills-file-content-endpoint.test.ts +670 -0
  111. package/src/__tests__/skills-files-catalog-fallback.test.ts +450 -0
  112. package/src/__tests__/slack-channel-config.test.ts +12 -15
  113. package/src/__tests__/subagent-detail.test.ts +44 -2
  114. package/src/__tests__/subagent-disposal.test.ts +1 -0
  115. package/src/__tests__/subagent-fork-notifications.test.ts +291 -0
  116. package/src/__tests__/subagent-fork-spawn.test.ts +384 -0
  117. package/src/__tests__/subagent-manager-notify.test.ts +1 -0
  118. package/src/__tests__/subagent-notify-parent.test.ts +1 -0
  119. package/src/__tests__/subagent-spawn-tool-fork.test.ts +411 -0
  120. package/src/__tests__/subagent-tools.test.ts +1 -0
  121. package/src/__tests__/subagent-types.test.ts +1 -0
  122. package/src/__tests__/system-prompt-ask-mode.test.ts +27 -71
  123. package/src/__tests__/system-prompt.test.ts +72 -1
  124. package/src/__tests__/task-scheduler.test.ts +32 -6
  125. package/src/__tests__/telegram-config.test.ts +10 -13
  126. package/src/__tests__/terminal-tools.test.ts +9 -0
  127. package/src/__tests__/tool-approval-handler.test.ts +73 -0
  128. package/src/__tests__/tool-side-effects-slack-dm.test.ts +22 -0
  129. package/src/__tests__/top-level-renderer.test.ts +73 -1
  130. package/src/__tests__/transport-hints-queue.test.ts +14 -29
  131. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +109 -0
  132. package/src/__tests__/v2-consent-policy.test.ts +103 -0
  133. package/src/acp/client-handler.ts +30 -4
  134. package/src/agent/loop.ts +12 -6
  135. package/src/approvals/guardian-request-resolvers.ts +21 -15
  136. package/src/browser-session/__tests__/manager.test.ts +297 -0
  137. package/src/browser-session/backends/cdp-inspect.ts +30 -0
  138. package/src/browser-session/backends/extension.ts +26 -0
  139. package/src/browser-session/backends/local.ts +24 -0
  140. package/src/browser-session/events.ts +164 -0
  141. package/src/browser-session/index.ts +27 -0
  142. package/src/browser-session/manager.ts +159 -0
  143. package/src/browser-session/types.ts +28 -0
  144. package/src/channels/__tests__/types.test.ts +134 -0
  145. package/src/channels/types.ts +53 -3
  146. package/src/cli/commands/browser-relay.ts +339 -409
  147. package/src/cli/commands/credentials.ts +3 -3
  148. package/src/cli/commands/email.ts +18 -13
  149. package/src/cli/commands/mcp.ts +16 -4
  150. package/src/cli/commands/oauth/__tests__/connect.test.ts +44 -44
  151. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +21 -21
  152. package/src/cli/commands/oauth/__tests__/mode.test.ts +17 -17
  153. package/src/cli/commands/oauth/__tests__/ping.test.ts +16 -16
  154. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +31 -33
  155. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +329 -0
  156. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +116 -12
  157. package/src/cli/commands/oauth/__tests__/status.test.ts +10 -10
  158. package/src/cli/commands/oauth/__tests__/token.test.ts +7 -7
  159. package/src/cli/commands/oauth/apps.ts +7 -4
  160. package/src/cli/commands/oauth/connect.ts +6 -3
  161. package/src/cli/commands/oauth/disconnect.ts +1 -1
  162. package/src/cli/commands/oauth/providers.ts +200 -36
  163. package/src/cli/commands/oauth/shared.ts +5 -5
  164. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +259 -0
  165. package/src/cli/commands/platform/index.ts +107 -10
  166. package/src/cli/commands/usage.ts +10 -9
  167. package/src/cli/lib/daemon-credential-client.ts +4 -0
  168. package/src/cli/program.ts +1 -1
  169. package/src/config/bundled-skills/app-builder/SKILL.md +26 -249
  170. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +105 -0
  171. package/src/config/bundled-skills/app-builder/references/INTERACTION_HOOKS.md +56 -0
  172. package/src/config/bundled-skills/app-builder/references/WIDGETS.md +125 -0
  173. package/src/config/bundled-skills/contacts/SKILL.md +3 -0
  174. package/src/config/bundled-skills/document/SKILL.md +4 -0
  175. package/src/config/bundled-skills/gmail/SKILL.md +1 -1
  176. package/src/config/bundled-skills/outlook/SKILL.md +7 -0
  177. package/src/config/bundled-skills/subagent/SKILL.md +21 -0
  178. package/src/config/bundled-skills/subagent/TOOLS.json +8 -4
  179. package/src/config/bundled-skills/tasks/SKILL.md +5 -0
  180. package/src/config/env-registry.ts +14 -0
  181. package/src/config/env.ts +21 -0
  182. package/src/config/feature-flag-registry.json +44 -5
  183. package/src/config/loader.ts +56 -1
  184. package/src/config/sanitize-for-transfer.ts +47 -0
  185. package/src/config/schema.ts +46 -5
  186. package/src/config/schemas/host-browser.ts +66 -0
  187. package/src/config/schemas/memory-lifecycle.ts +1 -1
  188. package/src/config/schemas/memory-retrieval.ts +103 -0
  189. package/src/config/schemas/security.ts +0 -6
  190. package/src/config/schemas/services.ts +8 -0
  191. package/src/config/types.ts +0 -1
  192. package/src/context/post-turn-tool-result-truncation.ts +176 -0
  193. package/src/context/window-manager.ts +19 -1
  194. package/src/credential-execution/approval-bridge.ts +49 -15
  195. package/src/daemon/__tests__/conversation-tool-setup.test.ts +186 -0
  196. package/src/daemon/app-source-watcher.ts +35 -0
  197. package/src/daemon/context-overflow-approval.ts +5 -0
  198. package/src/daemon/conversation-agent-loop-handlers.ts +17 -2
  199. package/src/daemon/conversation-agent-loop.ts +58 -24
  200. package/src/daemon/conversation-attachments.ts +40 -0
  201. package/src/daemon/conversation-process.ts +48 -1
  202. package/src/daemon/conversation-runtime-assembly.ts +118 -36
  203. package/src/daemon/conversation-surfaces.ts +37 -36
  204. package/src/daemon/conversation-tool-setup.ts +74 -8
  205. package/src/daemon/conversation-workspace.ts +12 -0
  206. package/src/daemon/conversation.ts +226 -8
  207. package/src/daemon/date-context.ts +10 -10
  208. package/src/daemon/first-greeting.ts +3 -2
  209. package/src/daemon/handlers/conversations.ts +9 -140
  210. package/src/daemon/handlers/shared.ts +58 -0
  211. package/src/daemon/handlers/skills.ts +232 -37
  212. package/src/daemon/host-bash-proxy.ts +48 -13
  213. package/src/daemon/host-browser-proxy.ts +191 -0
  214. package/src/daemon/host-cu-proxy.ts +36 -11
  215. package/src/daemon/host-file-proxy.ts +57 -9
  216. package/src/daemon/lifecycle.ts +65 -11
  217. package/src/daemon/message-protocol.ts +7 -0
  218. package/src/daemon/message-types/conversations.ts +55 -13
  219. package/src/daemon/message-types/host-browser.ts +100 -0
  220. package/src/daemon/message-types/messages.ts +5 -5
  221. package/src/daemon/message-types/skills.ts +10 -0
  222. package/src/daemon/message-types/subagents.ts +2 -0
  223. package/src/daemon/server.ts +92 -12
  224. package/src/daemon/tool-side-effects.ts +6 -0
  225. package/src/daemon/transport-hints.ts +5 -24
  226. package/src/inbound/platform-callback-registration.ts +18 -17
  227. package/src/mcp/client.ts +59 -24
  228. package/src/memory/app-store.ts +31 -1
  229. package/src/memory/conversation-crud.ts +23 -0
  230. package/src/memory/conversation-starters-cadence.ts +76 -0
  231. package/src/memory/conversation-title-service.ts +5 -2
  232. package/src/memory/db-init.ts +12 -0
  233. package/src/memory/embedding-backend.test.ts +75 -0
  234. package/src/memory/embedding-backend.ts +131 -5
  235. package/src/memory/embedding-gemini.test.ts +54 -0
  236. package/src/memory/embedding-gemini.ts +20 -9
  237. package/src/memory/embedding-local.ts +176 -17
  238. package/src/memory/graph/consolidation.ts +10 -23
  239. package/src/memory/graph/extraction-job.ts +15 -0
  240. package/src/memory/graph/retriever.ts +40 -22
  241. package/src/memory/graph/store.test.ts +7 -3
  242. package/src/memory/graph/store.ts +47 -12
  243. package/src/memory/llm-usage-store.ts +45 -4
  244. package/src/memory/migrations/213-oauth-providers-scope-separator.ts +13 -0
  245. package/src/memory/migrations/214-oauth-providers-refresh-url.ts +11 -0
  246. package/src/memory/migrations/215-oauth-providers-revoke.ts +14 -0
  247. package/src/memory/migrations/216-oauth-providers-token-auth-method.ts +30 -0
  248. package/src/memory/migrations/217-conversation-host-access.ts +40 -0
  249. package/src/memory/migrations/218-oauth-providers-logo-url.ts +11 -0
  250. package/src/memory/migrations/index.ts +6 -0
  251. package/src/memory/migrations/registry.ts +8 -0
  252. package/src/memory/schema/conversations.ts +1 -0
  253. package/src/memory/schema/oauth.ts +18 -13
  254. package/src/oauth/AGENTS.md +76 -0
  255. package/src/oauth/__tests__/identity-verifier.test.ts +24 -19
  256. package/src/oauth/__tests__/seed-providers-managed.test.ts +32 -0
  257. package/src/oauth/byo-connection.test.ts +8 -8
  258. package/src/oauth/byo-connection.ts +7 -7
  259. package/src/oauth/connect-orchestrator.ts +23 -21
  260. package/src/oauth/connect-types.ts +3 -3
  261. package/src/oauth/connection-resolver.test.ts +17 -4
  262. package/src/oauth/connection-resolver.ts +16 -16
  263. package/src/oauth/connection.ts +1 -1
  264. package/src/oauth/manual-token-connection.ts +13 -13
  265. package/src/oauth/oauth-store.ts +214 -100
  266. package/src/oauth/platform-connection.test.ts +3 -3
  267. package/src/oauth/platform-connection.ts +4 -4
  268. package/src/oauth/provider-serializer.ts +31 -5
  269. package/src/oauth/revoke.ts +76 -0
  270. package/src/oauth/seed-providers.ts +126 -87
  271. package/src/oauth/token-persistence.ts +1 -1
  272. package/src/permissions/permission-mode.ts +4 -11
  273. package/src/permissions/prompter.ts +13 -1
  274. package/src/permissions/v2-consent-policy.ts +87 -0
  275. package/src/prompts/system-prompt.ts +18 -21
  276. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +3 -65
  277. package/src/prompts/templates/BOOTSTRAP.md +59 -105
  278. package/src/providers/anthropic/client.ts +1 -0
  279. package/src/providers/types.ts +1 -1
  280. package/src/runtime/AGENTS.md +23 -0
  281. package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +715 -0
  282. package/src/runtime/__tests__/capability-tokens.test.ts +258 -0
  283. package/src/runtime/__tests__/chrome-extension-registry.test.ts +518 -0
  284. package/src/runtime/assistant-event-hub.ts +2 -2
  285. package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -0
  286. package/src/runtime/auth/__tests__/middleware.test.ts +116 -1
  287. package/src/runtime/auth/__tests__/route-policy.test.ts +8 -0
  288. package/src/runtime/auth/middleware.ts +98 -0
  289. package/src/runtime/auth/route-policy.ts +6 -7
  290. package/src/runtime/capability-tokens.ts +414 -0
  291. package/src/runtime/channel-approvals.ts +18 -5
  292. package/src/runtime/chrome-extension-registry.ts +332 -0
  293. package/src/runtime/confirmation-request-guardian-bridge.ts +6 -0
  294. package/src/runtime/guardian-decision-types.ts +7 -0
  295. package/src/runtime/http-server.ts +425 -70
  296. package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +172 -0
  297. package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +276 -0
  298. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +162 -0
  299. package/src/runtime/migrations/migration-transport.ts +6 -0
  300. package/src/runtime/migrations/migration-wizard.ts +22 -2
  301. package/src/runtime/migrations/rebind-secrets-screen.ts +76 -15
  302. package/src/runtime/migrations/vbundle-builder.ts +145 -38
  303. package/src/runtime/migrations/vbundle-import-analyzer.ts +19 -0
  304. package/src/runtime/migrations/vbundle-importer.ts +55 -5
  305. package/src/runtime/pending-interactions.ts +29 -13
  306. package/src/runtime/routes/approval-routes.ts +90 -16
  307. package/src/runtime/routes/browser-cdp-routes.ts +229 -0
  308. package/src/runtime/routes/browser-extension-pair-routes.ts +497 -0
  309. package/src/runtime/routes/conversation-analysis-routes.ts +2 -1
  310. package/src/runtime/routes/conversation-management-routes.ts +108 -0
  311. package/src/runtime/routes/conversation-routes.ts +301 -27
  312. package/src/runtime/routes/conversation-starter-routes.ts +78 -16
  313. package/src/runtime/routes/guardian-action-routes.ts +24 -13
  314. package/src/runtime/routes/host-browser-routes.ts +279 -0
  315. package/src/runtime/routes/host-file-routes.ts +9 -1
  316. package/src/runtime/routes/identity-routes.ts +259 -16
  317. package/src/runtime/routes/log-export-routes.ts +42 -22
  318. package/src/runtime/routes/memory-item-routes.ts +1 -7
  319. package/src/runtime/routes/migration-routes.ts +87 -2
  320. package/src/runtime/routes/oauth-apps.ts +15 -17
  321. package/src/runtime/routes/oauth-providers.ts +4 -0
  322. package/src/runtime/routes/schedule-routes.ts +24 -11
  323. package/src/runtime/routes/settings-routes.ts +9 -97
  324. package/src/runtime/routes/skills-routes.ts +52 -2
  325. package/src/runtime/routes/subagents-routes.ts +14 -10
  326. package/src/runtime/routes/usage-routes.ts +8 -7
  327. package/src/runtime/routes/workspace-routes.test.ts +22 -0
  328. package/src/runtime/routes/workspace-routes.ts +8 -1
  329. package/src/runtime/routes/workspace-utils.ts +2 -0
  330. package/src/schedule/scheduler.ts +7 -5
  331. package/src/security/ces-credential-client.ts +20 -0
  332. package/src/security/ces-rpc-credential-backend.ts +17 -0
  333. package/src/security/credential-backend.ts +5 -0
  334. package/src/security/oauth2.ts +42 -25
  335. package/src/security/secure-keys.ts +118 -25
  336. package/src/security/token-manager.ts +23 -10
  337. package/src/skills/catalog-files.ts +492 -0
  338. package/src/subagent/manager.ts +131 -26
  339. package/src/subagent/types.ts +19 -0
  340. package/src/tools/apps/executors.ts +11 -2
  341. package/src/tools/browser/__tests__/auth-detector.test.ts +202 -108
  342. package/src/tools/browser/auth-detector.ts +43 -12
  343. package/src/tools/browser/browser-execution.ts +645 -340
  344. package/src/tools/browser/browser-manager.ts +36 -12
  345. package/src/tools/browser/cdp-client/__tests__/accessibility-snapshot.test.ts +318 -0
  346. package/src/tools/browser/cdp-client/__tests__/cdp-dom-helpers.test.ts +1175 -0
  347. package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +870 -0
  348. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +330 -0
  349. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +377 -0
  350. package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-nested-frames.json +64 -0
  351. package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-simple.json +69 -0
  352. package/src/tools/browser/cdp-client/__tests__/local-cdp-client.test.ts +310 -0
  353. package/src/tools/browser/cdp-client/__tests__/types.test.ts +96 -0
  354. package/src/tools/browser/cdp-client/accessibility-snapshot.ts +387 -0
  355. package/src/tools/browser/cdp-client/cdp-dom-helpers.ts +695 -0
  356. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +743 -0
  357. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +580 -0
  358. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +578 -0
  359. package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +579 -0
  360. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +635 -0
  361. package/src/tools/browser/cdp-client/errors.ts +34 -0
  362. package/src/tools/browser/cdp-client/extension-cdp-client.ts +125 -0
  363. package/src/tools/browser/cdp-client/factory.ts +204 -0
  364. package/src/tools/browser/cdp-client/index.ts +14 -0
  365. package/src/tools/browser/cdp-client/local-cdp-client.ts +187 -0
  366. package/src/tools/browser/cdp-client/types.ts +52 -0
  367. package/src/tools/filesystem/edit.ts +1 -1
  368. package/src/tools/filesystem/list.ts +1 -1
  369. package/src/tools/filesystem/read.ts +1 -1
  370. package/src/tools/filesystem/write.ts +2 -1
  371. package/src/tools/host-filesystem/edit.ts +1 -1
  372. package/src/tools/host-filesystem/read.ts +12 -15
  373. package/src/tools/host-filesystem/write.ts +1 -1
  374. package/src/tools/host-terminal/host-shell.ts +21 -16
  375. package/src/tools/permission-checker.ts +77 -82
  376. package/src/tools/registry.ts +0 -2
  377. package/src/tools/secret-detection-handler.ts +34 -0
  378. package/src/tools/shared/filesystem/image-read.ts +61 -40
  379. package/src/tools/subagent/spawn.ts +47 -3
  380. package/src/tools/subagent/status.ts +2 -0
  381. package/src/tools/system/register.ts +2 -16
  382. package/src/tools/terminal/safe-env.ts +7 -0
  383. package/src/tools/terminal/shell.ts +21 -16
  384. package/src/tools/tool-approval-handler.ts +48 -2
  385. package/src/tools/types.ts +2 -0
  386. package/src/util/platform.ts +14 -19
  387. package/src/workspace/top-level-renderer.ts +19 -1
  388. package/src/__tests__/chrome-cdp.test.ts +0 -419
  389. package/src/__tests__/permission-mode-sse.test.ts +0 -418
  390. package/src/__tests__/permission-mode-store.test.ts +0 -277
  391. package/src/browser-extension-relay/protocol.ts +0 -63
  392. package/src/browser-extension-relay/server.ts +0 -203
  393. package/src/config/schemas/sandbox.ts +0 -14
  394. package/src/permissions/permission-mode-store.ts +0 -180
  395. package/src/tools/browser/chrome-cdp.ts +0 -239
  396. package/src/tools/system/set-permission-mode.ts +0 -103
@@ -0,0 +1,279 @@
1
+ /**
2
+ * Route handler for host browser result submissions.
3
+ *
4
+ * Resolves pending host browser proxy requests by requestId when the desktop
5
+ * client returns CDP results via HTTP.
6
+ */
7
+ import { z } from "zod";
8
+
9
+ import {
10
+ markTargetInvalidated,
11
+ publishCdpEvent,
12
+ } from "../../browser-session/events.js";
13
+ import { requireBoundGuardian } from "../auth/require-bound-guardian.js";
14
+ import type { AuthContext } from "../auth/types.js";
15
+ import { httpError } from "../http-errors.js";
16
+ import type { RouteDefinition } from "../http-router.js";
17
+ import * as pendingInteractions from "../pending-interactions.js";
18
+
19
+ /**
20
+ * Result of attempting to resolve a host browser result frame. Used by both
21
+ * the HTTP endpoint and the WS relay path so they share the same validation
22
+ * and resolution semantics.
23
+ *
24
+ * Success → the pending interaction was consumed and the conversation (or
25
+ * CLI shim callback) was notified.
26
+ *
27
+ * Error variants mirror the HTTP status codes the `/v1/host-browser-result`
28
+ * endpoint returns, so the caller can log/translate them consistently.
29
+ */
30
+ export type HostBrowserResultResolution =
31
+ | { ok: true }
32
+ | {
33
+ ok: false;
34
+ code: "BAD_REQUEST" | "NOT_FOUND" | "CONFLICT";
35
+ status: 400 | 404 | 409;
36
+ message: string;
37
+ };
38
+
39
+ /**
40
+ * Shared resolver used by both the HTTP route handler and the WS
41
+ * `host_browser_result` frame handler. Looks up the pending interaction
42
+ * by requestId, validates its kind, and forwards the response to the
43
+ * owning conversation (or CLI shim callback).
44
+ *
45
+ * This function does NOT perform auth — callers are expected to have
46
+ * already authenticated the caller (the HTTP route uses
47
+ * `requireBoundGuardian`, the WS path relies on the JWT check performed
48
+ * at WebSocket upgrade time).
49
+ */
50
+ export function resolveHostBrowserResultByRequestId(frame: {
51
+ requestId?: unknown;
52
+ content?: unknown;
53
+ isError?: unknown;
54
+ }): HostBrowserResultResolution {
55
+ const { requestId, content, isError } = frame;
56
+
57
+ if (!requestId || typeof requestId !== "string") {
58
+ return {
59
+ ok: false,
60
+ code: "BAD_REQUEST",
61
+ status: 400,
62
+ message: "requestId is required",
63
+ };
64
+ }
65
+
66
+ // Peek first (non-destructive) so we can validate the interaction kind
67
+ // without accidentally consuming a confirmation or secret interaction.
68
+ const peeked = pendingInteractions.get(requestId);
69
+ if (!peeked) {
70
+ return {
71
+ ok: false,
72
+ code: "NOT_FOUND",
73
+ status: 404,
74
+ message: "No pending interaction found for this requestId",
75
+ };
76
+ }
77
+
78
+ if (peeked.kind !== "host_browser") {
79
+ return {
80
+ ok: false,
81
+ code: "CONFLICT",
82
+ status: 409,
83
+ message: `Pending interaction is of kind "${peeked.kind}", expected "host_browser"`,
84
+ };
85
+ }
86
+
87
+ // Validation passed — consume the pending interaction.
88
+ const interaction = pendingInteractions.resolve(requestId)!;
89
+
90
+ const normalizedContent = typeof content === "string" ? content : "";
91
+ const normalizedIsError = typeof isError === "boolean" ? isError : false;
92
+
93
+ // CLI shim path: pending interactions registered by /v1/browser-cdp carry
94
+ // a directBrowserResolve callback and are not bound to a Conversation.
95
+ // Resolve them in place without touching the conversation object.
96
+ if (interaction.directBrowserResolve) {
97
+ interaction.directBrowserResolve({
98
+ content: normalizedContent,
99
+ isError: normalizedIsError,
100
+ });
101
+ return { ok: true };
102
+ }
103
+
104
+ // The host_browser kind always has a conversation attached at register time
105
+ // (HostBrowserProxy.request wires it through), so this guard exists so a
106
+ // future refactor of pending-interactions can change the type without
107
+ // silently breaking the host_browser path. Prefer an explicit error over an
108
+ // optional-chain no-op that would leave the proxy request unresolved.
109
+ if (!interaction.conversation) {
110
+ return {
111
+ ok: false,
112
+ code: "BAD_REQUEST",
113
+ status: 400,
114
+ message:
115
+ "host_browser pending interaction has no associated conversation",
116
+ };
117
+ }
118
+
119
+ interaction.conversation.resolveHostBrowser(requestId, {
120
+ content: normalizedContent,
121
+ isError: normalizedIsError,
122
+ });
123
+
124
+ return { ok: true };
125
+ }
126
+
127
+ /**
128
+ * Result of attempting to resolve a `host_browser_event` frame. The
129
+ * event surface never fails — every well-formed frame is published
130
+ * to the runtime-side CDP event bus — so the resolution is either
131
+ * `ok: true` or a `BAD_REQUEST` when the frame is malformed. Kept as
132
+ * a discriminated union (rather than `void`) so the WS dispatcher
133
+ * can log rejected frames with a consistent shape.
134
+ */
135
+ export type HostBrowserEventResolution =
136
+ | { ok: true }
137
+ | {
138
+ ok: false;
139
+ code: "BAD_REQUEST";
140
+ status: 400;
141
+ message: string;
142
+ };
143
+
144
+ /**
145
+ * Shared resolver for `host_browser_event` envelopes. Publishes the
146
+ * event into the module-level browser-session event bus where
147
+ * runtime-side consumers can subscribe. Validation is intentionally
148
+ * minimal — the frame is opaque to the bus and the bus's listeners
149
+ * are the ones that care about params shape.
150
+ */
151
+ export function resolveHostBrowserEvent(frame: {
152
+ method?: unknown;
153
+ params?: unknown;
154
+ cdpSessionId?: unknown;
155
+ }): HostBrowserEventResolution {
156
+ const { method, params, cdpSessionId } = frame;
157
+
158
+ if (!method || typeof method !== "string") {
159
+ return {
160
+ ok: false,
161
+ code: "BAD_REQUEST",
162
+ status: 400,
163
+ message: "method is required",
164
+ };
165
+ }
166
+
167
+ publishCdpEvent({
168
+ method,
169
+ params,
170
+ cdpSessionId:
171
+ typeof cdpSessionId === "string" && cdpSessionId.length > 0
172
+ ? cdpSessionId
173
+ : undefined,
174
+ });
175
+
176
+ return { ok: true };
177
+ }
178
+
179
+ /**
180
+ * Result of attempting to resolve a `host_browser_session_invalidated`
181
+ * frame. Mirrors {@link HostBrowserEventResolution} — publishing into
182
+ * the invalidated-target registry never fails, so the resolution is
183
+ * either `ok: true` or a `BAD_REQUEST` on a malformed frame.
184
+ */
185
+ export type HostBrowserSessionInvalidatedResolution =
186
+ | { ok: true }
187
+ | {
188
+ ok: false;
189
+ code: "BAD_REQUEST";
190
+ status: 400;
191
+ message: string;
192
+ };
193
+
194
+ /**
195
+ * Shared resolver for `host_browser_session_invalidated` envelopes.
196
+ * Marks the target as invalidated in the runtime-side registry; the
197
+ * next `BrowserSessionManager.send()` against that target will
198
+ * evict the stale session and force the owning tool to reattach.
199
+ *
200
+ * A frame without a `targetId` is tolerated (some detach notifications
201
+ * carry only a `reason`) but logged at info because it is less
202
+ * actionable than a targeted invalidation — the caller can still
203
+ * subscribe to the event bus to observe the signal.
204
+ */
205
+ export function resolveHostBrowserSessionInvalidated(frame: {
206
+ targetId?: unknown;
207
+ reason?: unknown;
208
+ }): HostBrowserSessionInvalidatedResolution {
209
+ const { targetId, reason } = frame;
210
+
211
+ if (targetId !== undefined && typeof targetId !== "string") {
212
+ return {
213
+ ok: false,
214
+ code: "BAD_REQUEST",
215
+ status: 400,
216
+ message: "targetId must be a string when present",
217
+ };
218
+ }
219
+
220
+ if (typeof targetId === "string" && targetId.length > 0) {
221
+ markTargetInvalidated(
222
+ targetId,
223
+ typeof reason === "string" ? reason : undefined,
224
+ );
225
+ }
226
+
227
+ return { ok: true };
228
+ }
229
+
230
+ /**
231
+ * POST /v1/host-browser-result — resolve a pending host browser request by requestId.
232
+ * Requires AuthContext with guardian-bound actor.
233
+ */
234
+ export async function handleHostBrowserResult(
235
+ req: Request,
236
+ authContext: AuthContext,
237
+ ): Promise<Response> {
238
+ const authError = requireBoundGuardian(authContext);
239
+ if (authError) return authError;
240
+
241
+ const body = (await req.json()) as {
242
+ requestId?: string;
243
+ content?: string;
244
+ isError?: boolean;
245
+ };
246
+
247
+ const resolution = resolveHostBrowserResultByRequestId(body);
248
+ if (!resolution.ok) {
249
+ return httpError(resolution.code, resolution.message, resolution.status);
250
+ }
251
+
252
+ return Response.json({ accepted: true });
253
+ }
254
+
255
+ // ---------------------------------------------------------------------------
256
+ // Route definitions
257
+ // ---------------------------------------------------------------------------
258
+
259
+ export function hostBrowserRouteDefinitions(): RouteDefinition[] {
260
+ return [
261
+ {
262
+ endpoint: "host-browser-result",
263
+ method: "POST",
264
+ summary: "Submit host browser result",
265
+ description: "Resolve a pending host browser request by requestId.",
266
+ tags: ["host"],
267
+ requestBody: z.object({
268
+ requestId: z.string().describe("Pending browser request ID"),
269
+ content: z.string().optional(),
270
+ isError: z.boolean().optional(),
271
+ }),
272
+ responseBody: z.object({
273
+ accepted: z.boolean(),
274
+ }),
275
+ handler: async ({ req, authContext }) =>
276
+ handleHostBrowserResult(req, authContext),
277
+ },
278
+ ];
279
+ }
@@ -27,9 +27,10 @@ export async function handleHostFileResult(
27
27
  requestId?: string;
28
28
  content?: string;
29
29
  isError?: boolean;
30
+ imageData?: string;
30
31
  };
31
32
 
32
- const { requestId, content, isError } = body;
33
+ const { requestId, content, isError, imageData } = body;
33
34
 
34
35
  if (!requestId || typeof requestId !== "string") {
35
36
  return httpError("BAD_REQUEST", "requestId is required", 400);
@@ -60,6 +61,7 @@ export async function handleHostFileResult(
60
61
  interaction.conversation!.resolveHostFile(requestId, {
61
62
  content: content ?? "",
62
63
  isError: isError ?? false,
64
+ imageData,
63
65
  });
64
66
 
65
67
  return Response.json({ accepted: true });
@@ -87,6 +89,12 @@ export function hostFileRouteDefinitions(): RouteDefinition[] {
87
89
  .boolean()
88
90
  .describe("Whether the result is an error")
89
91
  .optional(),
92
+ imageData: z
93
+ .string()
94
+ .describe(
95
+ "Optional base64-encoded image bytes for successful image reads",
96
+ )
97
+ .optional(),
90
98
  }),
91
99
  responseBody: z.object({
92
100
  accepted: z.boolean(),
@@ -3,12 +3,13 @@
3
3
  */
4
4
 
5
5
  import { existsSync, readFileSync, statfsSync, statSync } from "node:fs";
6
- import { cpus, totalmem } from "node:os";
6
+ import { availableParallelism, cpus, totalmem } from "node:os";
7
7
  import { dirname, join } from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
9
 
10
10
  import { z } from "zod";
11
11
 
12
+ import { getCpuLimit, getIsPlatform } from "../../config/env-registry.js";
12
13
  import { parseIdentityFields } from "../../daemon/handlers/identity.js";
13
14
  import { getProfilerRuntimeStatus } from "../../daemon/profiler-run-store.js";
14
15
  import { getMaxMigrationVersion } from "../../memory/migrations/registry.js";
@@ -54,10 +55,60 @@ interface MemoryInfo {
54
55
  maxMb: number;
55
56
  }
56
57
 
57
- // Read the container memory limit from cgroups if available, falling back to host total.
58
- // cgroups v2: /sys/fs/cgroup/memory.max (returns "max" when unlimited)
59
- // cgroups v1: /sys/fs/cgroup/memory/memory.limit_in_bytes (large sentinel when unlimited)
58
+ /**
59
+ * Parse a Kubernetes-style memory string (e.g. "3Gi", "512Mi", "1G") into bytes.
60
+ * Returns null if the value is not a recognized format.
61
+ */
62
+ function parseK8sMemoryBytes(value: string): number | null {
63
+ const match = value
64
+ .trim()
65
+ .match(/^(\d+(?:\.\d+)?)\s*(Ki|Mi|Gi|Ti|Pi|Ei|k|M|G|T|P|E|m)?$/);
66
+ if (!match) return null;
67
+ const num = parseFloat(match[1]);
68
+ const unit = match[2] ?? "";
69
+ const multipliers: Record<string, number> = {
70
+ "": 1,
71
+ m: 1e-3,
72
+ k: 1e3,
73
+ M: 1e6,
74
+ G: 1e9,
75
+ T: 1e12,
76
+ P: 1e15,
77
+ E: 1e18,
78
+ Ki: 1024,
79
+ Mi: 1024 ** 2,
80
+ Gi: 1024 ** 3,
81
+ Ti: 1024 ** 4,
82
+ Pi: 1024 ** 5,
83
+ Ei: 1024 ** 6,
84
+ };
85
+ const mult = multipliers[unit];
86
+ if (mult === undefined) return null;
87
+ const bytes = Math.round(num * mult);
88
+ return bytes > 0 ? bytes : null;
89
+ }
90
+
91
+ /**
92
+ * Read the memory limit from the VELLUM_MEMORY_LIMIT env var (K8s resource format),
93
+ * then fall back to cgroups, then to os.totalmem().
94
+ *
95
+ * In platform mode the container runs under gVisor where cgroup files may report
96
+ * the node's memory rather than the container limit. VELLUM_MEMORY_LIMIT is set
97
+ * by the StatefulSet template to the exact K8s memory limit (e.g. "3Gi").
98
+ */
60
99
  function getContainerMemoryLimitBytes(): number | null {
100
+ // 1. Prefer the explicit env var set by the platform StatefulSet template.
101
+ try {
102
+ const envLimit = process.env.VELLUM_MEMORY_LIMIT;
103
+ if (envLimit) {
104
+ const parsed = parseK8sMemoryBytes(envLimit);
105
+ if (parsed !== null) return parsed;
106
+ }
107
+ } catch {
108
+ /* env var parsing failed – fall through to cgroups */
109
+ }
110
+
111
+ // 2. Try cgroups v2.
61
112
  try {
62
113
  const v2 = readFileSync("/sys/fs/cgroup/memory.max", "utf-8").trim();
63
114
  if (v2 !== "max") {
@@ -67,6 +118,8 @@ function getContainerMemoryLimitBytes(): number | null {
67
118
  } catch {
68
119
  /* not available */
69
120
  }
121
+
122
+ // 3. Try cgroups v1.
70
123
  try {
71
124
  const v1 = readFileSync(
72
125
  "/sys/fs/cgroup/memory/memory.limit_in_bytes",
@@ -81,10 +134,56 @@ function getContainerMemoryLimitBytes(): number | null {
81
134
  return null;
82
135
  }
83
136
 
137
+ /**
138
+ * Read the container's current memory usage from cgroup files.
139
+ *
140
+ * Tries cgroups v2 (`memory.current`) first, then cgroups v1
141
+ * (`memory/memory.usage_in_bytes`), mirroring the v2-then-v1 fallback used by
142
+ * `getContainerMemoryLimitBytes`. Returns null if neither file is available
143
+ * or readable.
144
+ *
145
+ * Unlike the limit lookup, no env-var override is needed: the gVisor issue
146
+ * that motivates VELLUM_MEMORY_LIMIT is specifically about the *limit* files
147
+ * exposing the host node's memory instead of the sandbox limit. The *usage*
148
+ * files (memory.current / memory.usage_in_bytes) reflect the sandbox's own
149
+ * accounting and are accurate under gVisor.
150
+ */
151
+ function getContainerMemoryUsageBytes(): number | null {
152
+ // 1. Try cgroups v2.
153
+ try {
154
+ const v2 = readFileSync("/sys/fs/cgroup/memory.current", "utf-8").trim();
155
+ const bytes = parseInt(v2, 10);
156
+ if (!isNaN(bytes) && bytes > 0) return bytes;
157
+ } catch {
158
+ /* not available */
159
+ }
160
+
161
+ // 2. Try cgroups v1.
162
+ try {
163
+ const v1 = readFileSync(
164
+ "/sys/fs/cgroup/memory/memory.usage_in_bytes",
165
+ "utf-8",
166
+ ).trim();
167
+ const bytes = parseInt(v1, 10);
168
+ if (!isNaN(bytes) && bytes > 0) return bytes;
169
+ } catch {
170
+ /* not available */
171
+ }
172
+ return null;
173
+ }
174
+
84
175
  function getMemoryInfo(): MemoryInfo {
85
176
  const bytesToMb = (b: number) => Math.round((b / (1024 * 1024)) * 100) / 100;
177
+ // In platform-managed mode the daemon shares its Node process with whatever
178
+ // the container is doing as a whole; `process.memoryUsage().rss` only sees
179
+ // this process's resident set, which understates the container footprint
180
+ // operators care about. Read the cgroup usage file directly so /v1/health
181
+ // matches what the StatefulSet's memory limit is enforced against.
182
+ const currentBytes =
183
+ (getIsPlatform() ? getContainerMemoryUsageBytes() : null) ??
184
+ process.memoryUsage().rss;
86
185
  return {
87
- currentMb: bytesToMb(process.memoryUsage().rss),
186
+ currentMb: bytesToMb(currentBytes),
88
187
  maxMb: bytesToMb(getContainerMemoryLimitBytes() ?? totalmem()),
89
188
  };
90
189
  }
@@ -94,36 +193,180 @@ interface CpuInfo {
94
193
  maxCores: number;
95
194
  }
96
195
 
196
+ /**
197
+ * Parse a Kubernetes-style CPU string (e.g. "2000m", "1", "500m") into
198
+ * fractional cores. Returns null if the value is not a recognized format.
199
+ */
200
+ function parseK8sCpuCores(value: string): number | null {
201
+ const trimmed = value.trim();
202
+ const milliMatch = trimmed.match(/^(\d+)m$/);
203
+ if (milliMatch) {
204
+ const millis = parseInt(milliMatch[1], 10);
205
+ return millis > 0 ? millis / 1000 : null;
206
+ }
207
+ if (/^\d+(\.\d+)?$/.test(trimmed)) {
208
+ const num = parseFloat(trimmed);
209
+ return !isNaN(num) && num > 0 ? num : null;
210
+ }
211
+ return null;
212
+ }
213
+
214
+ /**
215
+ * Read the container's CPU core limit.
216
+ *
217
+ * Resolution order:
218
+ * 1. VELLUM_CPU_LIMIT env var (K8s resource format, e.g. "2000m" or "2").
219
+ * In platform mode the container runs under gVisor where cgroup files may
220
+ * report the node's CPU count rather than the sandbox limit.
221
+ * 2. cgroups v2 cpu.max (quota / period → fractional cores).
222
+ * 3. cgroups v1 cpu.cfs_quota_us / cpu.cfs_period_us.
223
+ * 4. os.cpus().length as last resort.
224
+ */
225
+ function getContainerCpuCores(): number {
226
+ // 1. Prefer the explicit env var set by the platform StatefulSet template.
227
+ try {
228
+ const envLimit = getCpuLimit();
229
+ if (envLimit) {
230
+ const parsed = parseK8sCpuCores(envLimit);
231
+ if (parsed !== null) return parsed;
232
+ }
233
+ } catch {
234
+ /* env var parsing failed – fall through */
235
+ }
236
+
237
+ // 2. Try cgroups v2: /sys/fs/cgroup/cpu.max contains "$MAX $PERIOD".
238
+ try {
239
+ const raw = readFileSync("/sys/fs/cgroup/cpu.max", "utf-8").trim();
240
+ if (!raw.startsWith("max")) {
241
+ const parts = raw.split(/\s+/);
242
+ const quota = parseInt(parts[0], 10);
243
+ const period = parseInt(parts[1], 10);
244
+ if (!isNaN(quota) && !isNaN(period) && period > 0 && quota > 0) {
245
+ const cores = quota / period;
246
+ // Sanity check: if the value looks like the node's full CPU count
247
+ // and we're on a platform pod, it's likely gVisor leaking the host value.
248
+ if (cores < cpus().length * 0.9 || !getIsPlatform()) {
249
+ return cores;
250
+ }
251
+ }
252
+ }
253
+ } catch {
254
+ /* not available */
255
+ }
256
+
257
+ // 3. Try cgroups v1.
258
+ try {
259
+ const quota = parseInt(
260
+ readFileSync("/sys/fs/cgroup/cpu/cpu.cfs_quota_us", "utf-8").trim(),
261
+ 10,
262
+ );
263
+ const period = parseInt(
264
+ readFileSync("/sys/fs/cgroup/cpu/cpu.cfs_period_us", "utf-8").trim(),
265
+ 10,
266
+ );
267
+ if (!isNaN(quota) && !isNaN(period) && period > 0 && quota > 0) {
268
+ const cores = quota / period;
269
+ if (cores < cpus().length * 0.9 || !getIsPlatform()) {
270
+ return cores;
271
+ }
272
+ }
273
+ } catch {
274
+ /* not available */
275
+ }
276
+
277
+ return cpus().length || availableParallelism();
278
+ }
279
+
280
+ /**
281
+ * Read the container's CPU usage from cgroup accounting files.
282
+ *
283
+ * Returns total CPU microseconds consumed by the container since boot.
284
+ * We use the delta between two samples to compute percentage.
285
+ */
286
+ function getContainerCpuUsageUs(): number | null {
287
+ // cgroups v2: cpu.stat has a "usage_usec" line.
288
+ try {
289
+ const stat = readFileSync("/sys/fs/cgroup/cpu.stat", "utf-8");
290
+ for (const line of stat.split("\n")) {
291
+ if (line.startsWith("usage_usec")) {
292
+ const val = parseInt(line.split(/\s+/)[1], 10);
293
+ if (!isNaN(val) && val > 0) return val;
294
+ }
295
+ }
296
+ } catch {
297
+ /* not available */
298
+ }
299
+
300
+ // cgroups v1: cpuacct.usage is in nanoseconds.
301
+ try {
302
+ const ns = parseInt(
303
+ readFileSync("/sys/fs/cgroup/cpuacct/cpuacct.usage", "utf-8").trim(),
304
+ 10,
305
+ );
306
+ if (!isNaN(ns) && ns > 0) return ns / 1000; // convert ns → µs
307
+ } catch {
308
+ /* not available */
309
+ }
310
+
311
+ return null;
312
+ }
313
+
97
314
  // Track CPU usage over a rolling window so /v1/health reports near-real-time
98
315
  // utilization instead of a lifetime average (total CPU time / total uptime).
99
316
  const CPU_SAMPLE_INTERVAL_MS = 5_000;
100
- let _lastCpuUsage: NodeJS.CpuUsage = process.cpuUsage();
317
+ let _lastProcessCpuUsage: NodeJS.CpuUsage = process.cpuUsage();
318
+ let _lastCgroupCpuUs: number | null = getContainerCpuUsageUs();
101
319
  let _lastCpuTime: number = Date.now();
102
320
  let _cachedCpuPercent = 0;
103
321
 
104
322
  // Kick off the background sampler. unref() so it never prevents process exit.
105
323
  setInterval(() => {
106
324
  const now = Date.now();
107
- const newUsage = process.cpuUsage();
108
325
  const elapsedMs = now - _lastCpuTime;
109
- if (elapsedMs > 0) {
110
- const deltaCpuUs =
111
- newUsage.user -
112
- _lastCpuUsage.user +
113
- (newUsage.system - _lastCpuUsage.system);
114
- const deltaCpuMs = deltaCpuUs / 1000;
115
- const numCores = cpus().length;
326
+ if (elapsedMs <= 0) return;
327
+
328
+ const numCores = getContainerCpuCores();
329
+
330
+ // Always sample process-level CPU so the baseline stays fresh. This
331
+ // prevents a spike if the platform cgroup path later falls back to
332
+ // process.cpuUsage() after cgroup stats were previously available.
333
+ const newProcessUsage = process.cpuUsage();
334
+ const processDeltaUs =
335
+ newProcessUsage.user -
336
+ _lastProcessCpuUsage.user +
337
+ (newProcessUsage.system - _lastProcessCpuUsage.system);
338
+ _lastProcessCpuUsage = newProcessUsage;
339
+
340
+ if (getIsPlatform()) {
341
+ // In platform mode, prefer cgroup-level CPU usage so we see the full
342
+ // container footprint, not just this process.
343
+ const cgroupUs = getContainerCpuUsageUs();
344
+ if (cgroupUs !== null && _lastCgroupCpuUs !== null) {
345
+ const deltaCpuUs = cgroupUs - _lastCgroupCpuUs;
346
+ const deltaCpuMs = deltaCpuUs / 1000;
347
+ _cachedCpuPercent =
348
+ Math.round((deltaCpuMs / (elapsedMs * numCores)) * 10000) / 100;
349
+ } else {
350
+ // cgroup CPU stats unavailable (e.g. gVisor) – fall back to process-level.
351
+ const deltaCpuMs = processDeltaUs / 1000;
352
+ _cachedCpuPercent =
353
+ Math.round((deltaCpuMs / (elapsedMs * numCores)) * 10000) / 100;
354
+ }
355
+ _lastCgroupCpuUs = cgroupUs;
356
+ } else {
357
+ // Non-platform: use process.cpuUsage() (accurate for single-process mode).
358
+ const deltaCpuMs = processDeltaUs / 1000;
116
359
  _cachedCpuPercent =
117
360
  Math.round((deltaCpuMs / (elapsedMs * numCores)) * 10000) / 100;
118
361
  }
119
- _lastCpuUsage = newUsage;
362
+
120
363
  _lastCpuTime = now;
121
364
  }, CPU_SAMPLE_INTERVAL_MS).unref();
122
365
 
123
366
  function getCpuInfo(): CpuInfo {
124
367
  return {
125
368
  currentPercent: _cachedCpuPercent,
126
- maxCores: cpus().length,
369
+ maxCores: Math.ceil(getContainerCpuCores()),
127
370
  };
128
371
  }
129
372