@vellumai/assistant 0.6.1 → 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/docker-entrypoint.sh +12 -2
- package/docs/architecture/memory.md +1 -1
- package/node_modules/@vellumai/ces-contracts/src/handles.ts +7 -9
- 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__/assistant-event-hub.test.ts +30 -0
- 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__/checker.test.ts +104 -170
- package/src/__tests__/cli-command-risk-guard.test.ts +1 -1
- 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 +21 -6
- 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 +169 -0
- package/src/__tests__/conversation-attachments.test.ts +80 -4
- package/src/__tests__/conversation-confirmation-signals.test.ts +155 -0
- package/src/__tests__/conversation-directories-parse.test.ts +105 -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 -3
- 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__/init-feature-flag-overrides.test.ts +167 -0
- package/src/__tests__/inline-command-runner.test.ts +7 -5
- package/src/__tests__/integration-status.test.ts +6 -7
- package/src/__tests__/list-messages-tool-merge.test.ts +37 -12
- package/src/__tests__/log-export-workspace.test.ts +190 -0
- package/src/__tests__/managed-credential-catalog-cli.test.ts +12 -14
- 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__/navigate-settings-tab.test.ts +14 -1
- package/src/__tests__/notification-broadcaster.test.ts +65 -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 +74 -55
- 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__/pkb-autoinject.test.ts +96 -0
- 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 -3
- package/src/__tests__/sandbox-diagnostics.test.ts +1 -32
- 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-sandbox.test.ts +1 -1
- package/src/__tests__/terminal-tools.test.ts +11 -5
- package/src/__tests__/test-preload.ts +14 -0
- package/src/__tests__/tool-approval-handler.test.ts +73 -0
- package/src/__tests__/tool-domain-event-publisher.test.ts +0 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -8
- package/src/__tests__/tool-executor.test.ts +0 -1
- 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 +62 -0
- package/src/__tests__/trust-store.test.ts +4 -4
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +109 -0
- package/src/__tests__/v2-consent-policy.test.ts +103 -0
- package/src/__tests__/workspace-migration-030-seed-pkb-autoinject.test.ts +168 -0
- package/src/__tests__/workspace-policy.test.ts +2 -7
- package/src/acp/client-handler.ts +30 -4
- package/src/agent/loop.ts +12 -35
- 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 +55 -0
- package/src/cli/__tests__/run-assistant-command.ts +34 -7
- package/src/cli/__tests__/unknown-command.test.ts +33 -0
- package/src/cli/commands/browser-relay.ts +339 -409
- package/src/cli/commands/credentials.ts +3 -3
- package/src/cli/commands/default-action.ts +68 -1
- 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 +68 -41
- 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 +16 -2
- 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/__tests__/connect.test.ts +1 -1
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +1 -1
- 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 +10 -3
- package/src/config/assistant-feature-flags.ts +59 -55
- package/src/config/bundled-skills/app-builder/SKILL.md +33 -173
- 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 +12 -7
- package/src/config/bundled-skills/gmail/TOOLS.json +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -1
- package/src/config/bundled-skills/outlook/SKILL.md +7 -0
- package/src/config/bundled-skills/settings/TOOLS.json +1 -1
- package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +8 -3
- 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 +46 -7
- 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 +16 -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 -16
- package/src/credential-execution/managed-catalog.ts +3 -7
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +186 -0
- package/src/daemon/app-source-watcher.ts +35 -0
- package/src/daemon/config-watcher.ts +6 -2
- package/src/daemon/context-overflow-approval.ts +5 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +17 -2
- package/src/daemon/conversation-agent-loop.ts +74 -19
- package/src/daemon/conversation-attachments.ts +40 -1
- package/src/daemon/conversation-messaging.ts +3 -0
- package/src/daemon/conversation-process.ts +66 -3
- package/src/daemon/conversation-queue-manager.ts +8 -0
- package/src/daemon/conversation-runtime-assembly.ts +159 -20
- package/src/daemon/conversation-surfaces.ts +78 -12
- package/src/daemon/conversation-tool-setup.ts +74 -11
- package/src/daemon/conversation-workspace.ts +12 -0
- package/src/daemon/conversation.ts +227 -11
- package/src/daemon/date-context.ts +10 -10
- package/src/daemon/first-greeting.ts +3 -2
- package/src/daemon/handlers/conversations.ts +9 -139
- package/src/daemon/handlers/shared.ts +65 -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 +86 -12
- package/src/daemon/message-protocol.ts +7 -0
- package/src/daemon/message-types/conversations.ts +59 -13
- package/src/daemon/message-types/host-browser.ts +100 -0
- package/src/daemon/message-types/messages.ts +5 -6
- package/src/daemon/message-types/notifications.ts +12 -0
- package/src/daemon/message-types/settings.ts +12 -0
- package/src/daemon/message-types/skills.ts +10 -0
- package/src/daemon/message-types/subagents.ts +2 -0
- package/src/daemon/server.ts +112 -35
- package/src/daemon/tool-side-effects.ts +6 -0
- package/src/daemon/transport-hints.ts +14 -0
- package/src/inbound/platform-callback-registration.ts +18 -17
- package/src/index.ts +1 -1
- package/src/mcp/client.ts +59 -24
- package/src/memory/app-store.ts +31 -1
- package/src/memory/conversation-crud.ts +38 -10
- package/src/memory/conversation-directories.ts +39 -0
- package/src/memory/conversation-group-migration.ts +65 -5
- 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 +177 -18
- package/src/memory/graph/capability-seed.ts +3 -5
- 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/group-crud.ts +25 -9
- 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/messaging/provider.ts +1 -1
- package/src/notifications/broadcaster.ts +6 -0
- package/src/notifications/conversation-pairing.ts +12 -4
- package/src/notifications/emit-signal.ts +14 -0
- package/src/notifications/signal.ts +11 -0
- 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 +5 -5
- 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 +127 -87
- package/src/oauth/token-persistence.ts +1 -1
- package/src/permissions/checker.ts +3 -3
- package/src/permissions/defaults.ts +7 -8
- package/src/permissions/permission-mode.ts +4 -11
- package/src/permissions/prompter.ts +13 -3
- package/src/permissions/v2-consent-policy.ts +87 -0
- package/src/platform/client.ts +1 -1
- package/src/prompts/system-prompt.ts +18 -21
- package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +3 -65
- package/src/prompts/templates/BOOTSTRAP.md +59 -96
- package/src/prompts/templates/SOUL.md +11 -11
- 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 +24 -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/auth/token-service.ts +8 -0
- 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 +18 -5
- package/src/runtime/routes/conversation-management-routes.ts +108 -0
- package/src/runtime/routes/conversation-routes.ts +308 -28
- package/src/runtime/routes/conversation-starter-routes.ts +78 -16
- package/src/runtime/routes/group-routes.ts +22 -8
- 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/AGENTS.md +104 -0
- package/src/runtime/routes/log-export/__tests__/workspace-allowlist-error-contract.test.ts +103 -0
- package/src/runtime/routes/log-export/__tests__/workspace-allowlist.test.ts +716 -0
- package/src/runtime/routes/log-export/workspace-allowlist.ts +458 -0
- package/src/runtime/routes/log-export-routes.ts +60 -25
- 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/skills/inline-command-runner.ts +12 -14
- 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 -100
- package/src/tools/registry.ts +0 -2
- package/src/tools/secret-detection-handler.ts +34 -1
- package/src/tools/shared/filesystem/image-read.ts +61 -40
- package/src/tools/skills/sandbox-runner.ts +3 -6
- 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/sandbox-diagnostics.ts +4 -4
- package/src/tools/terminal/sandbox.ts +4 -1
- package/src/tools/terminal/shell.ts +24 -21
- package/src/tools/tool-approval-handler.ts +48 -2
- package/src/tools/types.ts +2 -3
- package/src/util/platform.ts +14 -19
- package/src/watcher/provider-types.ts +1 -1
- package/src/workspace/migrations/029-seed-pkb.ts +1 -0
- package/src/workspace/migrations/030-seed-pkb-autoinject.ts +73 -0
- package/src/workspace/migrations/registry.ts +2 -0
- 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
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import { afterEach, describe, expect, test } from "bun:test";
|
|
1
|
+
import { afterEach, describe, expect, jest, test } from "bun:test";
|
|
2
2
|
|
|
3
3
|
const { HostFileProxy } = await import("../daemon/host-file-proxy.js");
|
|
4
4
|
|
|
5
|
+
// Minimal PNG header
|
|
6
|
+
const PNG_HEADER = Buffer.from([
|
|
7
|
+
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49,
|
|
8
|
+
0x48, 0x44, 0x52,
|
|
9
|
+
]);
|
|
10
|
+
|
|
5
11
|
describe("HostFileProxy", () => {
|
|
6
12
|
let proxy: InstanceType<typeof HostFileProxy>;
|
|
7
13
|
let sentMessages: unknown[];
|
|
@@ -77,6 +83,39 @@ describe("HostFileProxy", () => {
|
|
|
77
83
|
expect(result.content).toContain("ENOENT");
|
|
78
84
|
});
|
|
79
85
|
|
|
86
|
+
test("rebuilds image tool results from proxied image payloads", async () => {
|
|
87
|
+
setup();
|
|
88
|
+
|
|
89
|
+
const resultPromise = proxy.request(
|
|
90
|
+
{
|
|
91
|
+
operation: "read",
|
|
92
|
+
path: "/Users/test/Desktop/screenshot.png",
|
|
93
|
+
},
|
|
94
|
+
"session-1",
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const sent = sentMessages[0] as Record<string, unknown>;
|
|
98
|
+
const requestId = sent.requestId as string;
|
|
99
|
+
|
|
100
|
+
proxy.resolve(requestId, {
|
|
101
|
+
content: "Image loaded on host",
|
|
102
|
+
isError: false,
|
|
103
|
+
imageData: PNG_HEADER.toString("base64"),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const result = await resultPromise;
|
|
107
|
+
expect(result.isError).toBe(false);
|
|
108
|
+
expect(result.content).toContain("Image loaded");
|
|
109
|
+
expect(result.content).toContain("/Users/test/Desktop/screenshot.png");
|
|
110
|
+
expect(result.contentBlocks).toHaveLength(1);
|
|
111
|
+
expect(result.contentBlocks?.[0]).toMatchObject({
|
|
112
|
+
type: "image",
|
|
113
|
+
source: {
|
|
114
|
+
media_type: "image/png",
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
80
119
|
test("handles write operations", async () => {
|
|
81
120
|
setup();
|
|
82
121
|
|
|
@@ -377,6 +416,151 @@ describe("HostFileProxy", () => {
|
|
|
377
416
|
});
|
|
378
417
|
});
|
|
379
418
|
|
|
419
|
+
describe("abort listener lifecycle", () => {
|
|
420
|
+
// Helper that wraps an AbortSignal to observe add/removeEventListener
|
|
421
|
+
// invocations without tripping over tsc's strict overload matching on
|
|
422
|
+
// AbortSignal itself.
|
|
423
|
+
type Spied = {
|
|
424
|
+
signal: AbortSignal;
|
|
425
|
+
addCalls: string[];
|
|
426
|
+
removeCalls: string[];
|
|
427
|
+
};
|
|
428
|
+
function spySignal(source: AbortSignal): Spied {
|
|
429
|
+
const addCalls: string[] = [];
|
|
430
|
+
const removeCalls: string[] = [];
|
|
431
|
+
|
|
432
|
+
const s = source as any;
|
|
433
|
+
const origAdd = source.addEventListener.bind(source);
|
|
434
|
+
const origRemove = source.removeEventListener.bind(source);
|
|
435
|
+
s.addEventListener = (
|
|
436
|
+
type: string,
|
|
437
|
+
|
|
438
|
+
...rest: any[]
|
|
439
|
+
) => {
|
|
440
|
+
addCalls.push(type);
|
|
441
|
+
|
|
442
|
+
return (origAdd as any)(type, ...rest);
|
|
443
|
+
};
|
|
444
|
+
s.removeEventListener = (
|
|
445
|
+
type: string,
|
|
446
|
+
|
|
447
|
+
...rest: any[]
|
|
448
|
+
) => {
|
|
449
|
+
removeCalls.push(type);
|
|
450
|
+
|
|
451
|
+
return (origRemove as any)(type, ...rest);
|
|
452
|
+
};
|
|
453
|
+
return { signal: source, addCalls, removeCalls };
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
test("removes abort listener from signal after resolve completes", async () => {
|
|
457
|
+
setup();
|
|
458
|
+
const controller = new AbortController();
|
|
459
|
+
const spy = spySignal(controller.signal);
|
|
460
|
+
|
|
461
|
+
const resultPromise = proxy.request(
|
|
462
|
+
{ operation: "read", path: "/tmp/test.txt" },
|
|
463
|
+
"session-1",
|
|
464
|
+
spy.signal,
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
expect(spy.addCalls).toEqual(["abort"]);
|
|
468
|
+
expect(spy.removeCalls).toEqual([]);
|
|
469
|
+
|
|
470
|
+
const requestId = (sentMessages[0] as Record<string, unknown>)
|
|
471
|
+
.requestId as string;
|
|
472
|
+
proxy.resolve(requestId, { content: "file contents", isError: false });
|
|
473
|
+
await resultPromise;
|
|
474
|
+
|
|
475
|
+
// Listener is detached after normal completion.
|
|
476
|
+
expect(spy.removeCalls).toEqual(["abort"]);
|
|
477
|
+
|
|
478
|
+
// Subsequent aborts are harmless no-ops (no side effects on the proxy).
|
|
479
|
+
controller.abort();
|
|
480
|
+
// No additional emitted envelopes from the late abort.
|
|
481
|
+
expect(sentMessages).toHaveLength(1);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
test("removes abort listener from signal on timer timeout", async () => {
|
|
485
|
+
setup();
|
|
486
|
+
|
|
487
|
+
jest.useFakeTimers();
|
|
488
|
+
try {
|
|
489
|
+
const controller = new AbortController();
|
|
490
|
+
const spy = spySignal(controller.signal);
|
|
491
|
+
|
|
492
|
+
const resultPromise = proxy.request(
|
|
493
|
+
{ operation: "read", path: "/tmp/slow.txt" },
|
|
494
|
+
"session-1",
|
|
495
|
+
spy.signal,
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
expect(spy.addCalls).toEqual(["abort"]);
|
|
499
|
+
expect(spy.removeCalls).toEqual([]);
|
|
500
|
+
|
|
501
|
+
const requestId = (sentMessages[0] as Record<string, unknown>)
|
|
502
|
+
.requestId as string;
|
|
503
|
+
expect(proxy.hasPendingRequest(requestId)).toBe(true);
|
|
504
|
+
|
|
505
|
+
// Advance past the 30s internal timeout.
|
|
506
|
+
jest.advanceTimersByTime(31 * 1000);
|
|
507
|
+
|
|
508
|
+
const result = await resultPromise;
|
|
509
|
+
expect(result.isError).toBe(true);
|
|
510
|
+
expect(result.content).toContain("Host file proxy timed out");
|
|
511
|
+
expect(proxy.hasPendingRequest(requestId)).toBe(false);
|
|
512
|
+
|
|
513
|
+
// Listener is detached after the timer fires.
|
|
514
|
+
expect(spy.removeCalls).toEqual(["abort"]);
|
|
515
|
+
|
|
516
|
+
// Subsequent aborts should be harmless — no cancel emitted.
|
|
517
|
+
controller.abort();
|
|
518
|
+
expect(sentMessages).toHaveLength(1);
|
|
519
|
+
} finally {
|
|
520
|
+
jest.useRealTimers();
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
describe("sender throws synchronously", () => {
|
|
526
|
+
test("rejects the promise, clears pending state and timer, invokes onInternalResolve", async () => {
|
|
527
|
+
const resolvedIds: string[] = [];
|
|
528
|
+
sentMessages = [];
|
|
529
|
+
sendToClient = () => {
|
|
530
|
+
throw new Error("transport down");
|
|
531
|
+
};
|
|
532
|
+
proxy = new HostFileProxy(sendToClient, (id) => resolvedIds.push(id));
|
|
533
|
+
|
|
534
|
+
const resultPromise = proxy.request(
|
|
535
|
+
{ operation: "read", path: "/tmp/test.txt" },
|
|
536
|
+
"session-1",
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
await expect(resultPromise).rejects.toThrow("transport down");
|
|
540
|
+
|
|
541
|
+
// The internal resolve should fire exactly once as part of cleanup.
|
|
542
|
+
expect(resolvedIds).toHaveLength(1);
|
|
543
|
+
|
|
544
|
+
// Issue a new request on a fresh (non-throwing) sender and verify
|
|
545
|
+
// the proxy is still functional — no stale timers or bookkeeping
|
|
546
|
+
// from the failed request.
|
|
547
|
+
sentMessages = [];
|
|
548
|
+
proxy.updateSender((msg) => sentMessages.push(msg), true);
|
|
549
|
+
const okPromise = proxy.request(
|
|
550
|
+
{ operation: "read", path: "/tmp/ok.txt" },
|
|
551
|
+
"session-1",
|
|
552
|
+
);
|
|
553
|
+
expect(sentMessages).toHaveLength(1);
|
|
554
|
+
const okRequestId = (sentMessages[0] as Record<string, unknown>)
|
|
555
|
+
.requestId as string;
|
|
556
|
+
expect(proxy.hasPendingRequest(okRequestId)).toBe(true);
|
|
557
|
+
proxy.resolve(okRequestId, { content: "ok", isError: false });
|
|
558
|
+
const okResult = await okPromise;
|
|
559
|
+
expect(okResult.content).toBe("ok");
|
|
560
|
+
expect(okResult.isError).toBe(false);
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
|
|
380
564
|
describe("onInternalResolve callback", () => {
|
|
381
565
|
test("fires on abort", async () => {
|
|
382
566
|
const resolvedIds: string[] = [];
|
|
@@ -3,6 +3,7 @@ import { tmpdir } from "node:os";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
5
5
|
|
|
6
|
+
import type { HostFileInput } from "../daemon/host-file-proxy.js";
|
|
6
7
|
import { hostFileReadTool } from "../tools/host-filesystem/read.js";
|
|
7
8
|
import type { ToolContext } from "../tools/types.js";
|
|
8
9
|
|
|
@@ -165,6 +166,57 @@ describe("host_file_read tool", () => {
|
|
|
165
166
|
});
|
|
166
167
|
|
|
167
168
|
describe("host_file_read image support", () => {
|
|
169
|
+
test("uses host proxy for image reads when available", async () => {
|
|
170
|
+
const requests: Array<{
|
|
171
|
+
input: HostFileInput;
|
|
172
|
+
conversationId: string;
|
|
173
|
+
signal?: AbortSignal;
|
|
174
|
+
}> = [];
|
|
175
|
+
const proxyContext: ToolContext = {
|
|
176
|
+
...makeContext(),
|
|
177
|
+
hostFileProxy: {
|
|
178
|
+
isAvailable: () => true,
|
|
179
|
+
request: async (input, conversationId, signal) => {
|
|
180
|
+
requests.push({ input, conversationId, signal });
|
|
181
|
+
return {
|
|
182
|
+
content: "Image loaded: /host/screenshot.png",
|
|
183
|
+
isError: false,
|
|
184
|
+
contentBlocks: [
|
|
185
|
+
{
|
|
186
|
+
type: "image",
|
|
187
|
+
source: {
|
|
188
|
+
type: "base64",
|
|
189
|
+
media_type: "image/png",
|
|
190
|
+
data: PNG_HEADER.toString("base64"),
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
};
|
|
195
|
+
},
|
|
196
|
+
} as ToolContext["hostFileProxy"],
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const result = await hostFileReadTool.execute(
|
|
200
|
+
{ path: "/host/screenshot.png" },
|
|
201
|
+
proxyContext,
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
expect(result.isError).toBe(false);
|
|
205
|
+
expect(result.contentBlocks).toHaveLength(1);
|
|
206
|
+
expect(requests).toEqual([
|
|
207
|
+
{
|
|
208
|
+
input: {
|
|
209
|
+
operation: "read",
|
|
210
|
+
path: "/host/screenshot.png",
|
|
211
|
+
offset: undefined,
|
|
212
|
+
limit: undefined,
|
|
213
|
+
},
|
|
214
|
+
conversationId: "test-conversation",
|
|
215
|
+
signal: undefined,
|
|
216
|
+
},
|
|
217
|
+
]);
|
|
218
|
+
});
|
|
219
|
+
|
|
168
220
|
test("returns image content block for .png file", async () => {
|
|
169
221
|
const dir = makeTempDir();
|
|
170
222
|
const filePath = join(dir, "screenshot.png");
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import type { HostProxyInterfaceId, InterfaceId } from "../channels/types.js";
|
|
4
|
+
import { supportsHostProxy } from "../channels/types.js";
|
|
5
|
+
import type {
|
|
6
|
+
ConversationTransportMetadata,
|
|
7
|
+
HostProxyTransportMetadata,
|
|
8
|
+
NonHostProxyTransportMetadata,
|
|
9
|
+
} from "../daemon/message-types/conversations.js";
|
|
10
|
+
import { isHostProxyTransport } from "../daemon/message-types/conversations.js";
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// supportsHostProxy — runtime behavior
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
describe("supportsHostProxy (runtime)", () => {
|
|
17
|
+
test("no-arg form returns true for host-proxy interfaces", () => {
|
|
18
|
+
expect(supportsHostProxy("macos")).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("no-arg form returns false for interfaces without host-proxy support", () => {
|
|
22
|
+
const nonHostProxyIds: InterfaceId[] = [
|
|
23
|
+
"ios",
|
|
24
|
+
"cli",
|
|
25
|
+
"telegram",
|
|
26
|
+
"phone",
|
|
27
|
+
"vellum",
|
|
28
|
+
"whatsapp",
|
|
29
|
+
"slack",
|
|
30
|
+
"email",
|
|
31
|
+
"chrome-extension",
|
|
32
|
+
];
|
|
33
|
+
for (const id of nonHostProxyIds) {
|
|
34
|
+
expect(supportsHostProxy(id)).toBe(false);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("capability form grants host_browser to chrome-extension", () => {
|
|
39
|
+
expect(supportsHostProxy("chrome-extension", "host_browser")).toBe(true);
|
|
40
|
+
expect(supportsHostProxy("chrome-extension", "host_bash")).toBe(false);
|
|
41
|
+
expect(supportsHostProxy("chrome-extension", "host_file")).toBe(false);
|
|
42
|
+
expect(supportsHostProxy("chrome-extension", "host_cu")).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("capability form grants host_bash/file/cu to macOS but not host_browser", () => {
|
|
46
|
+
expect(supportsHostProxy("macos", "host_bash")).toBe(true);
|
|
47
|
+
expect(supportsHostProxy("macos", "host_file")).toBe(true);
|
|
48
|
+
expect(supportsHostProxy("macos", "host_cu")).toBe(true);
|
|
49
|
+
expect(supportsHostProxy("macos", "host_browser")).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("capability form rejects everything for non-host-proxy interfaces", () => {
|
|
53
|
+
expect(supportsHostProxy("ios", "host_bash")).toBe(false);
|
|
54
|
+
expect(supportsHostProxy("cli", "host_file")).toBe(false);
|
|
55
|
+
expect(supportsHostProxy("telegram", "host_browser")).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// supportsHostProxy — type predicate (compile-time contract)
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
describe("supportsHostProxy (type predicate)", () => {
|
|
64
|
+
test("no-arg form narrows InterfaceId to HostProxyInterfaceId", () => {
|
|
65
|
+
const id: InterfaceId = "macos";
|
|
66
|
+
if (supportsHostProxy(id)) {
|
|
67
|
+
// Inside this branch, TypeScript narrows `id` to HostProxyInterfaceId.
|
|
68
|
+
// If the overload were wrong, this assignment would fail to type-check
|
|
69
|
+
// and the test file wouldn't compile.
|
|
70
|
+
const narrowed: HostProxyInterfaceId = id;
|
|
71
|
+
expect(narrowed).toBe("macos");
|
|
72
|
+
} else {
|
|
73
|
+
throw new Error("expected narrowing branch to be taken for macos");
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("narrowing reaches through discriminated transport union", () => {
|
|
78
|
+
// Build a value typed as the full union so TypeScript can't cheat.
|
|
79
|
+
const transport: ConversationTransportMetadata = {
|
|
80
|
+
channelId: "vellum",
|
|
81
|
+
interfaceId: "macos",
|
|
82
|
+
hostHomeDir: "/Users/alice",
|
|
83
|
+
hostUsername: "alice",
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
if (transport.interfaceId && supportsHostProxy(transport.interfaceId)) {
|
|
87
|
+
// Narrowing the discriminant narrows the union member — after this
|
|
88
|
+
// check, `transport` should be HostProxyTransportMetadata and the
|
|
89
|
+
// host-env fields are directly accessible.
|
|
90
|
+
const narrowed: HostProxyTransportMetadata = transport;
|
|
91
|
+
expect(narrowed.hostHomeDir).toBe("/Users/alice");
|
|
92
|
+
expect(narrowed.hostUsername).toBe("alice");
|
|
93
|
+
} else {
|
|
94
|
+
throw new Error("expected host-proxy branch for macos transport");
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("non-host-proxy branch narrows to NonHostProxyTransportMetadata", () => {
|
|
99
|
+
const transport: ConversationTransportMetadata = {
|
|
100
|
+
channelId: "vellum",
|
|
101
|
+
interfaceId: "ios",
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
if (transport.interfaceId && supportsHostProxy(transport.interfaceId)) {
|
|
105
|
+
throw new Error("expected non-host-proxy branch for ios transport");
|
|
106
|
+
} else {
|
|
107
|
+
// `transport` is NonHostProxyTransportMetadata here.
|
|
108
|
+
const narrowed: NonHostProxyTransportMetadata = transport;
|
|
109
|
+
expect(narrowed.interfaceId).toBe("ios");
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// isHostProxyTransport — type guard on ConversationTransportMetadata
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
describe("isHostProxyTransport", () => {
|
|
119
|
+
test("returns true for macOS transport and narrows to HostProxyTransportMetadata", () => {
|
|
120
|
+
const transport: ConversationTransportMetadata = {
|
|
121
|
+
channelId: "vellum",
|
|
122
|
+
interfaceId: "macos",
|
|
123
|
+
hostHomeDir: "/Users/alice",
|
|
124
|
+
hostUsername: "alice",
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
expect(isHostProxyTransport(transport)).toBe(true);
|
|
128
|
+
|
|
129
|
+
if (isHostProxyTransport(transport)) {
|
|
130
|
+
const narrowed: HostProxyTransportMetadata = transport;
|
|
131
|
+
expect(narrowed.hostHomeDir).toBe("/Users/alice");
|
|
132
|
+
expect(narrowed.hostUsername).toBe("alice");
|
|
133
|
+
} else {
|
|
134
|
+
throw new Error("narrowing branch not taken");
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("returns false for every non-host-proxy interface", () => {
|
|
139
|
+
const nonHostProxyIds: Array<Exclude<InterfaceId, HostProxyInterfaceId>> = [
|
|
140
|
+
"ios",
|
|
141
|
+
"cli",
|
|
142
|
+
"telegram",
|
|
143
|
+
"phone",
|
|
144
|
+
"vellum",
|
|
145
|
+
"whatsapp",
|
|
146
|
+
"slack",
|
|
147
|
+
"email",
|
|
148
|
+
"chrome-extension",
|
|
149
|
+
];
|
|
150
|
+
for (const interfaceId of nonHostProxyIds) {
|
|
151
|
+
const transport: ConversationTransportMetadata = {
|
|
152
|
+
channelId: "vellum",
|
|
153
|
+
interfaceId,
|
|
154
|
+
};
|
|
155
|
+
expect(isHostProxyTransport(transport)).toBe(false);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("returns false when interfaceId is absent", () => {
|
|
160
|
+
const transport: ConversationTransportMetadata = {
|
|
161
|
+
channelId: "vellum",
|
|
162
|
+
};
|
|
163
|
+
expect(isHostProxyTransport(transport)).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -36,7 +36,6 @@ const mockConfig = {
|
|
|
36
36
|
entropyThreshold: 4.0,
|
|
37
37
|
},
|
|
38
38
|
auditLog: { retentionDays: 0 },
|
|
39
|
-
sandbox: { enabled: true },
|
|
40
39
|
};
|
|
41
40
|
|
|
42
41
|
// Track whether wrapCommand was ever called — host_bash must never invoke it
|
|
@@ -149,10 +148,6 @@ describe("host_bash tool", () => {
|
|
|
149
148
|
const dir = mkdtempSync(join(tmpdir(), "host-shell-plain-"));
|
|
150
149
|
testDirs.push(dir);
|
|
151
150
|
|
|
152
|
-
// Verify the tool executes successfully even when sandbox is enabled in config,
|
|
153
|
-
// proving it bypasses the sandbox entirely
|
|
154
|
-
expect(mockConfig.sandbox.enabled).toBe(true);
|
|
155
|
-
|
|
156
151
|
spawnCalls.length = 0;
|
|
157
152
|
|
|
158
153
|
const result = await hostShellTool.execute(
|
|
@@ -236,10 +231,7 @@ describe("host_bash — baseline: no sandbox isolation", () => {
|
|
|
236
231
|
expect(spawnCalls[0].args[2]).toBe("ls -la /tmp");
|
|
237
232
|
});
|
|
238
233
|
|
|
239
|
-
test("
|
|
240
|
-
// The mock config has sandbox.enabled = true
|
|
241
|
-
expect(mockConfig.sandbox.enabled).toBe(true);
|
|
242
|
-
|
|
234
|
+
test("host_bash always spawns plain bash without wrapCommand", async () => {
|
|
243
235
|
const dir = mkdtempSync(join(tmpdir(), "host-shell-sandbox-cfg-"));
|
|
244
236
|
testDirs.push(dir);
|
|
245
237
|
|
|
@@ -255,9 +247,7 @@ describe("host_bash — baseline: no sandbox isolation", () => {
|
|
|
255
247
|
);
|
|
256
248
|
|
|
257
249
|
expect(result.isError).toBe(false);
|
|
258
|
-
// Must never call wrapCommand regardless of config
|
|
259
250
|
expect(wrapCommandCallCount).toBe(0);
|
|
260
|
-
// Must still spawn plain bash
|
|
261
251
|
expect(spawnCalls[0].command).toBe("bash");
|
|
262
252
|
});
|
|
263
253
|
});
|
|
@@ -178,6 +178,7 @@ function makeConversation(overrides: Record<string, unknown> = {}) {
|
|
|
178
178
|
setTrustContext: () => {},
|
|
179
179
|
updateClient: () => {},
|
|
180
180
|
setHostBashProxy: () => {},
|
|
181
|
+
setHostBrowserProxy: () => {},
|
|
181
182
|
setHostFileProxy: () => {},
|
|
182
183
|
setHostCuProxy: () => {},
|
|
183
184
|
addPreactivatedSkillId: () => {},
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for initFeatureFlagOverrides() — the async gateway fetch that
|
|
3
|
+
* pre-populates the feature flag cache before CLI program construction.
|
|
4
|
+
*/
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
clearFeatureFlagOverridesCache,
|
|
9
|
+
initFeatureFlagOverrides,
|
|
10
|
+
isAssistantFeatureFlagEnabled,
|
|
11
|
+
} from "../config/assistant-feature-flags.js";
|
|
12
|
+
import * as tokenService from "../runtime/auth/token-service.js";
|
|
13
|
+
import { getMockFetchCalls, mockFetch, resetMockFetch } from "./mock-fetch.js";
|
|
14
|
+
|
|
15
|
+
const VALID_HEX_KEY = "ab".repeat(32);
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
clearFeatureFlagOverridesCache();
|
|
19
|
+
tokenService._resetSigningKeyForTesting();
|
|
20
|
+
|
|
21
|
+
// Set up a signing key so mintEdgeRelayToken() works
|
|
22
|
+
process.env.ACTOR_TOKEN_SIGNING_KEY = VALID_HEX_KEY;
|
|
23
|
+
tokenService.initAuthSigningKey(tokenService.resolveSigningKey());
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
resetMockFetch();
|
|
28
|
+
clearFeatureFlagOverridesCache();
|
|
29
|
+
tokenService._resetSigningKeyForTesting();
|
|
30
|
+
delete process.env.ACTOR_TOKEN_SIGNING_KEY;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("initFeatureFlagOverrides", () => {
|
|
34
|
+
it("populates cache from gateway fetch response", async () => {
|
|
35
|
+
mockFetch(
|
|
36
|
+
"/v1/feature-flags",
|
|
37
|
+
{ method: "GET" },
|
|
38
|
+
{
|
|
39
|
+
body: {
|
|
40
|
+
flags: [
|
|
41
|
+
{
|
|
42
|
+
key: "foo-enabled",
|
|
43
|
+
enabled: true,
|
|
44
|
+
label: "Foo",
|
|
45
|
+
defaultEnabled: false,
|
|
46
|
+
description: "",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
key: "bar-enabled",
|
|
50
|
+
enabled: true,
|
|
51
|
+
label: "Bar",
|
|
52
|
+
defaultEnabled: true,
|
|
53
|
+
description: "",
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
status: 200,
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
await initFeatureFlagOverrides();
|
|
62
|
+
|
|
63
|
+
const config = {} as any;
|
|
64
|
+
expect(isAssistantFeatureFlagEnabled("foo-enabled", config)).toBe(true);
|
|
65
|
+
expect(isAssistantFeatureFlagEnabled("bar-enabled", config)).toBe(true);
|
|
66
|
+
|
|
67
|
+
// Verify fetch was called with correct URL and auth header
|
|
68
|
+
const calls = getMockFetchCalls();
|
|
69
|
+
expect(calls.length).toBe(1);
|
|
70
|
+
expect(calls[0].path).toContain("/v1/feature-flags");
|
|
71
|
+
const headers = calls[0].init.headers as Record<string, string> | undefined;
|
|
72
|
+
expect(headers).toHaveProperty("Authorization");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("sends a valid Bearer JWT in the Authorization header", async () => {
|
|
76
|
+
mockFetch(
|
|
77
|
+
"/v1/feature-flags",
|
|
78
|
+
{ method: "GET" },
|
|
79
|
+
{ body: { flags: [] }, status: 200 },
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
await initFeatureFlagOverrides();
|
|
83
|
+
|
|
84
|
+
const calls = getMockFetchCalls();
|
|
85
|
+
expect(calls.length).toBe(1);
|
|
86
|
+
const headers = calls[0].init.headers as Record<string, string> | undefined;
|
|
87
|
+
const authHeader = headers?.Authorization;
|
|
88
|
+
|
|
89
|
+
expect(authHeader).toBeDefined();
|
|
90
|
+
expect(authHeader).toMatch(/^Bearer /);
|
|
91
|
+
|
|
92
|
+
// Verify it's a valid JWT (three dot-separated base64url segments)
|
|
93
|
+
const token = authHeader!.replace("Bearer ", "");
|
|
94
|
+
const parts = token.split(".");
|
|
95
|
+
expect(parts.length).toBe(3);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("falls back gracefully when gateway is unreachable", async () => {
|
|
99
|
+
mockFetch("/v1/feature-flags", { method: "GET" }, { status: 500 });
|
|
100
|
+
|
|
101
|
+
// Should not throw
|
|
102
|
+
await initFeatureFlagOverrides();
|
|
103
|
+
|
|
104
|
+
// Without gateway data or file, undeclared flags default to true
|
|
105
|
+
const config = {} as any;
|
|
106
|
+
expect(isAssistantFeatureFlagEnabled("foo-enabled", config)).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("falls back gracefully on non-OK HTTP status", async () => {
|
|
110
|
+
mockFetch(
|
|
111
|
+
"/v1/feature-flags",
|
|
112
|
+
{ method: "GET" },
|
|
113
|
+
{ body: "Unauthorized", status: 401 },
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
await initFeatureFlagOverrides();
|
|
117
|
+
|
|
118
|
+
// Undeclared flags default to true without overrides
|
|
119
|
+
const config = {} as any;
|
|
120
|
+
expect(isAssistantFeatureFlagEnabled("foo-enabled", config)).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("initializes signing key lazily when not yet set", async () => {
|
|
124
|
+
// Reset signing key to simulate fresh CLI subprocess
|
|
125
|
+
tokenService._resetSigningKeyForTesting();
|
|
126
|
+
delete process.env.ACTOR_TOKEN_SIGNING_KEY;
|
|
127
|
+
|
|
128
|
+
expect(tokenService.isSigningKeyInitialized()).toBe(false);
|
|
129
|
+
|
|
130
|
+
mockFetch(
|
|
131
|
+
"/v1/feature-flags",
|
|
132
|
+
{ method: "GET" },
|
|
133
|
+
{
|
|
134
|
+
body: {
|
|
135
|
+
flags: [{ key: "expected-enabled", enabled: true }],
|
|
136
|
+
},
|
|
137
|
+
status: 200,
|
|
138
|
+
},
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
await initFeatureFlagOverrides();
|
|
142
|
+
|
|
143
|
+
// Signing key should have been initialized during the fetch
|
|
144
|
+
expect(tokenService.isSigningKeyInitialized()).toBe(true);
|
|
145
|
+
|
|
146
|
+
// And the flag should be resolved correctly
|
|
147
|
+
const config = {} as any;
|
|
148
|
+
expect(isAssistantFeatureFlagEnabled("expected-enabled", config)).toBe(
|
|
149
|
+
true,
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("does not cache empty gateway response", async () => {
|
|
154
|
+
mockFetch(
|
|
155
|
+
"/v1/feature-flags",
|
|
156
|
+
{ method: "GET" },
|
|
157
|
+
{ body: { flags: [] }, status: 200 },
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
await initFeatureFlagOverrides();
|
|
161
|
+
|
|
162
|
+
// Undeclared flags without overrides default to true (not false from
|
|
163
|
+
// a cached empty map)
|
|
164
|
+
const config = {} as any;
|
|
165
|
+
expect(isAssistantFeatureFlagEnabled("foo-enabled", config)).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -11,7 +11,6 @@ const mockConfig = {
|
|
|
11
11
|
model: "test",
|
|
12
12
|
maxTokens: 4096,
|
|
13
13
|
dataDir: "/tmp",
|
|
14
|
-
sandbox: { enabled: true },
|
|
15
14
|
timeouts: {
|
|
16
15
|
shellDefaultTimeoutSec: 120,
|
|
17
16
|
shellMaxTimeoutSec: 600,
|
|
@@ -104,20 +103,23 @@ describe("runInlineCommand", () => {
|
|
|
104
103
|
// ── Sandbox enforcement ──────────────────────────────────────────────────
|
|
105
104
|
|
|
106
105
|
describe("sandbox enforcement", () => {
|
|
107
|
-
test("always passes sandbox config with enabled=
|
|
106
|
+
test("always passes sandbox config with enabled=false", async () => {
|
|
108
107
|
lastWrapCall = null;
|
|
109
108
|
await runInlineCommand("echo sandbox-check", CWD);
|
|
110
109
|
|
|
111
110
|
expect(lastWrapCall).not.toBeNull();
|
|
112
|
-
expect(lastWrapCall!.config.enabled).toBe(
|
|
111
|
+
expect(lastWrapCall!.config.enabled).toBe(false);
|
|
113
112
|
});
|
|
114
113
|
|
|
115
|
-
test("
|
|
114
|
+
test("does not pass networkMode when sandbox is disabled", async () => {
|
|
116
115
|
lastWrapCall = null;
|
|
117
116
|
await runInlineCommand("echo network-check", CWD);
|
|
118
117
|
|
|
119
118
|
expect(lastWrapCall).not.toBeNull();
|
|
120
|
-
|
|
119
|
+
// networkMode is a no-op when sandbox is disabled (wrapCommand returns
|
|
120
|
+
// a plain bash invocation), so it is not passed. Network isolation is
|
|
121
|
+
// provided by the Docker/platform-managed container.
|
|
122
|
+
expect(lastWrapCall!.options).toBeUndefined();
|
|
121
123
|
});
|
|
122
124
|
|
|
123
125
|
test("uses the provided workingDir as cwd", async () => {
|