@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.
- package/bun.lock +40 -40
- package/bunfig.toml +3 -0
- package/docs/architecture/memory.md +1 -1
- package/node_modules/@vellumai/ces-contracts/src/rpc.ts +42 -0
- package/openapi.yaml +184 -69
- package/package.json +41 -41
- package/scripts/generate-openapi.ts +1 -2
- package/src/__tests__/acp-session.test.ts +43 -0
- package/src/__tests__/app-builder-tool-scripts.test.ts +1 -0
- package/src/__tests__/app-executors.test.ts +1 -0
- package/src/__tests__/app-source-watcher.test.ts +37 -11
- package/src/__tests__/approval-routes-http.test.ts +178 -1
- package/src/__tests__/browser-fill-credential.test.ts +229 -94
- package/src/__tests__/browser-manager.test.ts +40 -27
- package/src/__tests__/catalog-files.test.ts +862 -0
- package/src/__tests__/channel-approvals.test.ts +53 -0
- package/src/__tests__/config-managed-gemini-defaults.test.ts +326 -0
- package/src/__tests__/config-schema-cmd.test.ts +2 -2
- package/src/__tests__/config-schema.test.ts +125 -48
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +23 -0
- package/src/__tests__/context-overflow-approval.test.ts +16 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop.test.ts +1 -1
- package/src/__tests__/conversation-analysis-routes.test.ts +2 -2
- package/src/__tests__/conversation-attachments.test.ts +80 -4
- package/src/__tests__/conversation-confirmation-signals.test.ts +155 -0
- package/src/__tests__/conversation-fork-crud.test.ts +17 -0
- package/src/__tests__/conversation-history-web-search.test.ts +1 -0
- package/src/__tests__/conversation-host-access-routes.test.ts +229 -0
- package/src/__tests__/conversation-inject-context.test.ts +103 -0
- package/src/__tests__/conversation-queue.test.ts +45 -2
- package/src/__tests__/conversation-routes-disk-view.test.ts +5 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +16 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +269 -46
- package/src/__tests__/conversation-starter-routes.test.ts +126 -0
- package/src/__tests__/conversation-starters-cadence.test.ts +161 -0
- package/src/__tests__/conversation-store.test.ts +195 -0
- package/src/__tests__/conversation-workspace-cache-state.test.ts +193 -0
- package/src/__tests__/credential-execution-approval-bridge.test.ts +32 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/credential-vault-unit.test.ts +4 -4
- package/src/__tests__/credential-vault.test.ts +152 -13
- package/src/__tests__/credentials-cli.test.ts +2 -2
- package/src/__tests__/date-context.test.ts +4 -4
- package/src/__tests__/embedding-managed-proxy-selection.test.ts +256 -0
- package/src/__tests__/extension-id-sync-guard.test.ts +155 -0
- package/src/__tests__/fixtures/mock-chrome-extension.ts +375 -0
- package/src/__tests__/gateway-only-guard.test.ts +3 -0
- package/src/__tests__/gemini-provider.test.ts +2 -2
- package/src/__tests__/guardian-routing-invariants.test.ts +70 -2
- package/src/__tests__/headless-browser-interactions.test.ts +707 -371
- package/src/__tests__/headless-browser-navigate.test.ts +389 -47
- package/src/__tests__/headless-browser-read-tools.test.ts +266 -103
- package/src/__tests__/headless-browser-snapshot.test.ts +240 -77
- package/src/__tests__/host-bash-proxy.test.ts +150 -1
- package/src/__tests__/host-browser-e2e-cloud.test.ts +462 -0
- package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +286 -0
- package/src/__tests__/host-browser-e2e-self-hosted.test.ts +374 -0
- package/src/__tests__/host-browser-event-routes.test.ts +350 -0
- package/src/__tests__/host-browser-proxy.test.ts +444 -0
- package/src/__tests__/host-browser-routes.test.ts +198 -0
- package/src/__tests__/host-browser-ws-events-e2e.test.ts +320 -0
- package/src/__tests__/host-cu-proxy.test.ts +171 -1
- package/src/__tests__/host-file-proxy.test.ts +185 -1
- package/src/__tests__/host-file-read-tool.test.ts +52 -0
- package/src/__tests__/host-proxy-interface.test.ts +165 -0
- package/src/__tests__/host-shell-tool.test.ts +1 -11
- package/src/__tests__/http-user-message-parity.test.ts +1 -0
- package/src/__tests__/integration-status.test.ts +6 -7
- package/src/__tests__/list-messages-tool-merge.test.ts +37 -12
- package/src/__tests__/mcp-client-auth.test.ts +40 -4
- package/src/__tests__/mcp-health-check.test.ts +10 -3
- package/src/__tests__/migration-cross-version-compatibility.test.ts +3 -1
- package/src/__tests__/migration-export-http.test.ts +61 -2
- package/src/__tests__/migration-export-streaming.test.ts +66 -0
- package/src/__tests__/migration-import-commit-http.test.ts +101 -1
- package/src/__tests__/native-host-marker-sync-guard.test.ts +157 -0
- package/src/__tests__/oauth-apps-routes.test.ts +17 -12
- package/src/__tests__/oauth-cli.test.ts +707 -60
- package/src/__tests__/oauth-connect-orchestrator.test.ts +116 -24
- package/src/__tests__/oauth-provider-seed-logos.test.ts +23 -0
- package/src/__tests__/oauth-provider-serializer.test.ts +146 -10
- package/src/__tests__/oauth-provider-visibility.test.ts +19 -21
- package/src/__tests__/oauth-providers-routes.test.ts +50 -14
- package/src/__tests__/oauth-store.test.ts +1386 -182
- package/src/__tests__/oauth2-gateway-transport.test.ts +211 -20
- package/src/__tests__/onboarding-template-contract.test.ts +75 -57
- package/src/__tests__/openai-provider.test.ts +2 -2
- package/src/__tests__/outlook-categories.test.ts +1 -1
- package/src/__tests__/outlook-client-automation.test.ts +1 -1
- package/src/__tests__/outlook-compose-tools.test.ts +1 -1
- package/src/__tests__/outlook-email-watcher.test.ts +1 -1
- package/src/__tests__/outlook-follow-up.test.ts +1 -1
- package/src/__tests__/outlook-messaging-provider.test.ts +2 -2
- package/src/__tests__/outlook-trash.test.ts +1 -1
- package/src/__tests__/outlook-unsubscribe.test.ts +1 -1
- package/src/__tests__/permission-checker-host-gate.test.ts +74 -14
- package/src/__tests__/permission-mode.test.ts +28 -56
- package/src/__tests__/platform-callback-registration.test.ts +19 -0
- package/src/__tests__/post-turn-tool-result-truncation.test.ts +296 -0
- package/src/__tests__/proxy-approval-callback.test.ts +18 -0
- package/src/__tests__/require-fresh-approval.test.ts +40 -1
- package/src/__tests__/sanitize-config-for-transfer.test.ts +132 -0
- package/src/__tests__/schedule-routes.test.ts +162 -0
- package/src/__tests__/secret-detection-handler.test.ts +84 -0
- package/src/__tests__/secret-ingress-http.test.ts +1 -0
- package/src/__tests__/send-endpoint-busy.test.ts +3 -0
- package/src/__tests__/set-permission-mode.test.ts +13 -250
- package/src/__tests__/skills-file-content-endpoint.test.ts +670 -0
- package/src/__tests__/skills-files-catalog-fallback.test.ts +450 -0
- package/src/__tests__/slack-channel-config.test.ts +12 -15
- package/src/__tests__/subagent-detail.test.ts +44 -2
- package/src/__tests__/subagent-disposal.test.ts +1 -0
- package/src/__tests__/subagent-fork-notifications.test.ts +291 -0
- package/src/__tests__/subagent-fork-spawn.test.ts +384 -0
- package/src/__tests__/subagent-manager-notify.test.ts +1 -0
- package/src/__tests__/subagent-notify-parent.test.ts +1 -0
- package/src/__tests__/subagent-spawn-tool-fork.test.ts +411 -0
- package/src/__tests__/subagent-tools.test.ts +1 -0
- package/src/__tests__/subagent-types.test.ts +1 -0
- package/src/__tests__/system-prompt-ask-mode.test.ts +27 -71
- package/src/__tests__/system-prompt.test.ts +72 -1
- package/src/__tests__/task-scheduler.test.ts +32 -6
- package/src/__tests__/telegram-config.test.ts +10 -13
- package/src/__tests__/terminal-tools.test.ts +9 -0
- package/src/__tests__/tool-approval-handler.test.ts +73 -0
- package/src/__tests__/tool-side-effects-slack-dm.test.ts +22 -0
- package/src/__tests__/top-level-renderer.test.ts +73 -1
- package/src/__tests__/transport-hints-queue.test.ts +14 -29
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +109 -0
- package/src/__tests__/v2-consent-policy.test.ts +103 -0
- package/src/acp/client-handler.ts +30 -4
- package/src/agent/loop.ts +12 -6
- package/src/approvals/guardian-request-resolvers.ts +21 -15
- package/src/browser-session/__tests__/manager.test.ts +297 -0
- package/src/browser-session/backends/cdp-inspect.ts +30 -0
- package/src/browser-session/backends/extension.ts +26 -0
- package/src/browser-session/backends/local.ts +24 -0
- package/src/browser-session/events.ts +164 -0
- package/src/browser-session/index.ts +27 -0
- package/src/browser-session/manager.ts +159 -0
- package/src/browser-session/types.ts +28 -0
- package/src/channels/__tests__/types.test.ts +134 -0
- package/src/channels/types.ts +53 -3
- package/src/cli/commands/browser-relay.ts +339 -409
- package/src/cli/commands/credentials.ts +3 -3
- package/src/cli/commands/email.ts +18 -13
- package/src/cli/commands/mcp.ts +16 -4
- package/src/cli/commands/oauth/__tests__/connect.test.ts +44 -44
- package/src/cli/commands/oauth/__tests__/disconnect.test.ts +21 -21
- package/src/cli/commands/oauth/__tests__/mode.test.ts +17 -17
- package/src/cli/commands/oauth/__tests__/ping.test.ts +16 -16
- package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +31 -33
- package/src/cli/commands/oauth/__tests__/providers-register.test.ts +329 -0
- package/src/cli/commands/oauth/__tests__/providers-update.test.ts +116 -12
- package/src/cli/commands/oauth/__tests__/status.test.ts +10 -10
- package/src/cli/commands/oauth/__tests__/token.test.ts +7 -7
- package/src/cli/commands/oauth/apps.ts +7 -4
- package/src/cli/commands/oauth/connect.ts +6 -3
- package/src/cli/commands/oauth/disconnect.ts +1 -1
- package/src/cli/commands/oauth/providers.ts +200 -36
- package/src/cli/commands/oauth/shared.ts +5 -5
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +259 -0
- package/src/cli/commands/platform/index.ts +107 -10
- package/src/cli/commands/usage.ts +10 -9
- package/src/cli/lib/daemon-credential-client.ts +4 -0
- package/src/cli/program.ts +1 -1
- package/src/config/bundled-skills/app-builder/SKILL.md +26 -249
- package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +105 -0
- package/src/config/bundled-skills/app-builder/references/INTERACTION_HOOKS.md +56 -0
- package/src/config/bundled-skills/app-builder/references/WIDGETS.md +125 -0
- package/src/config/bundled-skills/contacts/SKILL.md +3 -0
- package/src/config/bundled-skills/document/SKILL.md +4 -0
- package/src/config/bundled-skills/gmail/SKILL.md +1 -1
- package/src/config/bundled-skills/outlook/SKILL.md +7 -0
- package/src/config/bundled-skills/subagent/SKILL.md +21 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +8 -4
- package/src/config/bundled-skills/tasks/SKILL.md +5 -0
- package/src/config/env-registry.ts +14 -0
- package/src/config/env.ts +21 -0
- package/src/config/feature-flag-registry.json +44 -5
- package/src/config/loader.ts +56 -1
- package/src/config/sanitize-for-transfer.ts +47 -0
- package/src/config/schema.ts +46 -5
- package/src/config/schemas/host-browser.ts +66 -0
- package/src/config/schemas/memory-lifecycle.ts +1 -1
- package/src/config/schemas/memory-retrieval.ts +103 -0
- package/src/config/schemas/security.ts +0 -6
- package/src/config/schemas/services.ts +8 -0
- package/src/config/types.ts +0 -1
- package/src/context/post-turn-tool-result-truncation.ts +176 -0
- package/src/context/window-manager.ts +19 -1
- package/src/credential-execution/approval-bridge.ts +49 -15
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +186 -0
- package/src/daemon/app-source-watcher.ts +35 -0
- package/src/daemon/context-overflow-approval.ts +5 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +17 -2
- package/src/daemon/conversation-agent-loop.ts +58 -24
- package/src/daemon/conversation-attachments.ts +40 -0
- package/src/daemon/conversation-process.ts +48 -1
- package/src/daemon/conversation-runtime-assembly.ts +118 -36
- package/src/daemon/conversation-surfaces.ts +37 -36
- package/src/daemon/conversation-tool-setup.ts +74 -8
- package/src/daemon/conversation-workspace.ts +12 -0
- package/src/daemon/conversation.ts +226 -8
- package/src/daemon/date-context.ts +10 -10
- package/src/daemon/first-greeting.ts +3 -2
- package/src/daemon/handlers/conversations.ts +9 -140
- package/src/daemon/handlers/shared.ts +58 -0
- package/src/daemon/handlers/skills.ts +232 -37
- package/src/daemon/host-bash-proxy.ts +48 -13
- package/src/daemon/host-browser-proxy.ts +191 -0
- package/src/daemon/host-cu-proxy.ts +36 -11
- package/src/daemon/host-file-proxy.ts +57 -9
- package/src/daemon/lifecycle.ts +65 -11
- package/src/daemon/message-protocol.ts +7 -0
- package/src/daemon/message-types/conversations.ts +55 -13
- package/src/daemon/message-types/host-browser.ts +100 -0
- package/src/daemon/message-types/messages.ts +5 -5
- package/src/daemon/message-types/skills.ts +10 -0
- package/src/daemon/message-types/subagents.ts +2 -0
- package/src/daemon/server.ts +92 -12
- package/src/daemon/tool-side-effects.ts +6 -0
- package/src/daemon/transport-hints.ts +5 -24
- package/src/inbound/platform-callback-registration.ts +18 -17
- package/src/mcp/client.ts +59 -24
- package/src/memory/app-store.ts +31 -1
- package/src/memory/conversation-crud.ts +23 -0
- package/src/memory/conversation-starters-cadence.ts +76 -0
- package/src/memory/conversation-title-service.ts +5 -2
- package/src/memory/db-init.ts +12 -0
- package/src/memory/embedding-backend.test.ts +75 -0
- package/src/memory/embedding-backend.ts +131 -5
- package/src/memory/embedding-gemini.test.ts +54 -0
- package/src/memory/embedding-gemini.ts +20 -9
- package/src/memory/embedding-local.ts +176 -17
- package/src/memory/graph/consolidation.ts +10 -23
- package/src/memory/graph/extraction-job.ts +15 -0
- package/src/memory/graph/retriever.ts +40 -22
- package/src/memory/graph/store.test.ts +7 -3
- package/src/memory/graph/store.ts +47 -12
- package/src/memory/llm-usage-store.ts +45 -4
- package/src/memory/migrations/213-oauth-providers-scope-separator.ts +13 -0
- package/src/memory/migrations/214-oauth-providers-refresh-url.ts +11 -0
- package/src/memory/migrations/215-oauth-providers-revoke.ts +14 -0
- package/src/memory/migrations/216-oauth-providers-token-auth-method.ts +30 -0
- package/src/memory/migrations/217-conversation-host-access.ts +40 -0
- package/src/memory/migrations/218-oauth-providers-logo-url.ts +11 -0
- package/src/memory/migrations/index.ts +6 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/conversations.ts +1 -0
- package/src/memory/schema/oauth.ts +18 -13
- package/src/oauth/AGENTS.md +76 -0
- package/src/oauth/__tests__/identity-verifier.test.ts +24 -19
- package/src/oauth/__tests__/seed-providers-managed.test.ts +32 -0
- package/src/oauth/byo-connection.test.ts +8 -8
- package/src/oauth/byo-connection.ts +7 -7
- package/src/oauth/connect-orchestrator.ts +23 -21
- package/src/oauth/connect-types.ts +3 -3
- package/src/oauth/connection-resolver.test.ts +17 -4
- package/src/oauth/connection-resolver.ts +16 -16
- package/src/oauth/connection.ts +1 -1
- package/src/oauth/manual-token-connection.ts +13 -13
- package/src/oauth/oauth-store.ts +214 -100
- package/src/oauth/platform-connection.test.ts +3 -3
- package/src/oauth/platform-connection.ts +4 -4
- package/src/oauth/provider-serializer.ts +31 -5
- package/src/oauth/revoke.ts +76 -0
- package/src/oauth/seed-providers.ts +126 -87
- package/src/oauth/token-persistence.ts +1 -1
- package/src/permissions/permission-mode.ts +4 -11
- package/src/permissions/prompter.ts +13 -1
- package/src/permissions/v2-consent-policy.ts +87 -0
- package/src/prompts/system-prompt.ts +18 -21
- package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +3 -65
- package/src/prompts/templates/BOOTSTRAP.md +59 -105
- package/src/providers/anthropic/client.ts +1 -0
- package/src/providers/types.ts +1 -1
- package/src/runtime/AGENTS.md +23 -0
- package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +715 -0
- package/src/runtime/__tests__/capability-tokens.test.ts +258 -0
- package/src/runtime/__tests__/chrome-extension-registry.test.ts +518 -0
- package/src/runtime/assistant-event-hub.ts +2 -2
- package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -0
- package/src/runtime/auth/__tests__/middleware.test.ts +116 -1
- package/src/runtime/auth/__tests__/route-policy.test.ts +8 -0
- package/src/runtime/auth/middleware.ts +98 -0
- package/src/runtime/auth/route-policy.ts +6 -7
- package/src/runtime/capability-tokens.ts +414 -0
- package/src/runtime/channel-approvals.ts +18 -5
- package/src/runtime/chrome-extension-registry.ts +332 -0
- package/src/runtime/confirmation-request-guardian-bridge.ts +6 -0
- package/src/runtime/guardian-decision-types.ts +7 -0
- package/src/runtime/http-server.ts +425 -70
- package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +172 -0
- package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +276 -0
- package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +162 -0
- package/src/runtime/migrations/migration-transport.ts +6 -0
- package/src/runtime/migrations/migration-wizard.ts +22 -2
- package/src/runtime/migrations/rebind-secrets-screen.ts +76 -15
- package/src/runtime/migrations/vbundle-builder.ts +145 -38
- package/src/runtime/migrations/vbundle-import-analyzer.ts +19 -0
- package/src/runtime/migrations/vbundle-importer.ts +55 -5
- package/src/runtime/pending-interactions.ts +29 -13
- package/src/runtime/routes/approval-routes.ts +90 -16
- package/src/runtime/routes/browser-cdp-routes.ts +229 -0
- package/src/runtime/routes/browser-extension-pair-routes.ts +497 -0
- package/src/runtime/routes/conversation-analysis-routes.ts +2 -1
- package/src/runtime/routes/conversation-management-routes.ts +108 -0
- package/src/runtime/routes/conversation-routes.ts +301 -27
- package/src/runtime/routes/conversation-starter-routes.ts +78 -16
- package/src/runtime/routes/guardian-action-routes.ts +24 -13
- package/src/runtime/routes/host-browser-routes.ts +279 -0
- package/src/runtime/routes/host-file-routes.ts +9 -1
- package/src/runtime/routes/identity-routes.ts +259 -16
- package/src/runtime/routes/log-export-routes.ts +42 -22
- package/src/runtime/routes/memory-item-routes.ts +1 -7
- package/src/runtime/routes/migration-routes.ts +87 -2
- package/src/runtime/routes/oauth-apps.ts +15 -17
- package/src/runtime/routes/oauth-providers.ts +4 -0
- package/src/runtime/routes/schedule-routes.ts +24 -11
- package/src/runtime/routes/settings-routes.ts +9 -97
- package/src/runtime/routes/skills-routes.ts +52 -2
- package/src/runtime/routes/subagents-routes.ts +14 -10
- package/src/runtime/routes/usage-routes.ts +8 -7
- package/src/runtime/routes/workspace-routes.test.ts +22 -0
- package/src/runtime/routes/workspace-routes.ts +8 -1
- package/src/runtime/routes/workspace-utils.ts +2 -0
- package/src/schedule/scheduler.ts +7 -5
- package/src/security/ces-credential-client.ts +20 -0
- package/src/security/ces-rpc-credential-backend.ts +17 -0
- package/src/security/credential-backend.ts +5 -0
- package/src/security/oauth2.ts +42 -25
- package/src/security/secure-keys.ts +118 -25
- package/src/security/token-manager.ts +23 -10
- package/src/skills/catalog-files.ts +492 -0
- package/src/subagent/manager.ts +131 -26
- package/src/subagent/types.ts +19 -0
- package/src/tools/apps/executors.ts +11 -2
- package/src/tools/browser/__tests__/auth-detector.test.ts +202 -108
- package/src/tools/browser/auth-detector.ts +43 -12
- package/src/tools/browser/browser-execution.ts +645 -340
- package/src/tools/browser/browser-manager.ts +36 -12
- package/src/tools/browser/cdp-client/__tests__/accessibility-snapshot.test.ts +318 -0
- package/src/tools/browser/cdp-client/__tests__/cdp-dom-helpers.test.ts +1175 -0
- package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +870 -0
- package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +330 -0
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +377 -0
- package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-nested-frames.json +64 -0
- package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-simple.json +69 -0
- package/src/tools/browser/cdp-client/__tests__/local-cdp-client.test.ts +310 -0
- package/src/tools/browser/cdp-client/__tests__/types.test.ts +96 -0
- package/src/tools/browser/cdp-client/accessibility-snapshot.ts +387 -0
- package/src/tools/browser/cdp-client/cdp-dom-helpers.ts +695 -0
- package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +743 -0
- package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +580 -0
- package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +578 -0
- package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +579 -0
- package/src/tools/browser/cdp-client/cdp-inspect-client.ts +635 -0
- package/src/tools/browser/cdp-client/errors.ts +34 -0
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +125 -0
- package/src/tools/browser/cdp-client/factory.ts +204 -0
- package/src/tools/browser/cdp-client/index.ts +14 -0
- package/src/tools/browser/cdp-client/local-cdp-client.ts +187 -0
- package/src/tools/browser/cdp-client/types.ts +52 -0
- package/src/tools/filesystem/edit.ts +1 -1
- package/src/tools/filesystem/list.ts +1 -1
- package/src/tools/filesystem/read.ts +1 -1
- package/src/tools/filesystem/write.ts +2 -1
- package/src/tools/host-filesystem/edit.ts +1 -1
- package/src/tools/host-filesystem/read.ts +12 -15
- package/src/tools/host-filesystem/write.ts +1 -1
- package/src/tools/host-terminal/host-shell.ts +21 -16
- package/src/tools/permission-checker.ts +77 -82
- package/src/tools/registry.ts +0 -2
- package/src/tools/secret-detection-handler.ts +34 -0
- package/src/tools/shared/filesystem/image-read.ts +61 -40
- package/src/tools/subagent/spawn.ts +47 -3
- package/src/tools/subagent/status.ts +2 -0
- package/src/tools/system/register.ts +2 -16
- package/src/tools/terminal/safe-env.ts +7 -0
- package/src/tools/terminal/shell.ts +21 -16
- package/src/tools/tool-approval-handler.ts +48 -2
- package/src/tools/types.ts +2 -0
- package/src/util/platform.ts +14 -19
- package/src/workspace/top-level-renderer.ts +19 -1
- package/src/__tests__/chrome-cdp.test.ts +0 -419
- package/src/__tests__/permission-mode-sse.test.ts +0 -418
- package/src/__tests__/permission-mode-store.test.ts +0 -277
- package/src/browser-extension-relay/protocol.ts +0 -63
- package/src/browser-extension-relay/server.ts +0 -203
- package/src/config/schemas/sandbox.ts +0 -14
- package/src/permissions/permission-mode-store.ts +0 -180
- package/src/tools/browser/chrome-cdp.ts +0 -239
- 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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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(
|
|
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
|
|
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
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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:
|
|
369
|
+
maxCores: Math.ceil(getContainerCpuCores()),
|
|
127
370
|
};
|
|
128
371
|
}
|
|
129
372
|
|