@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
|
@@ -0,0 +1,716 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the workspace allowlist module used by `POST /v1/export`.
|
|
3
|
+
*
|
|
4
|
+
* Validates that `collectWorkspaceData` honors the time + conversationId
|
|
5
|
+
* filters, enforces the workspace cap, ignores malformed conversation
|
|
6
|
+
* directory names, and never throws.
|
|
7
|
+
*
|
|
8
|
+
* The shared `test-preload.ts` sets `VELLUM_WORKSPACE_DIR` to a per-file
|
|
9
|
+
* temp directory before any test code runs, so `getConversationsDir()`
|
|
10
|
+
* already resolves under our temp workspace. We just seed the
|
|
11
|
+
* `conversations/` subdirectory before each test and tear it down
|
|
12
|
+
* afterwards.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
existsSync,
|
|
17
|
+
mkdirSync,
|
|
18
|
+
mkdtempSync,
|
|
19
|
+
readdirSync,
|
|
20
|
+
rmSync,
|
|
21
|
+
symlinkSync,
|
|
22
|
+
writeFileSync,
|
|
23
|
+
} from "node:fs";
|
|
24
|
+
import { tmpdir } from "node:os";
|
|
25
|
+
import { join } from "node:path";
|
|
26
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
27
|
+
|
|
28
|
+
import { getConversationsDir } from "../../../../util/platform.js";
|
|
29
|
+
import { collectWorkspaceData } from "../workspace-allowlist.js";
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Helpers
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
const CONV_DIRS = {
|
|
36
|
+
jan10: "2025-01-10T00-00-00.000Z_conv-jan10",
|
|
37
|
+
jan15: "2025-01-15T00-00-00.000Z_conv-jan15",
|
|
38
|
+
jan20: "2025-01-20T00-00-00.000Z_conv-jan20",
|
|
39
|
+
jan25: "2025-01-25T00-00-00.000Z_conv-jan25",
|
|
40
|
+
invalid: "not-a-valid-name",
|
|
41
|
+
jan15Attachments: "2025-01-15T00-00-00.000Z_conv-jan15-with-attachments",
|
|
42
|
+
} as const;
|
|
43
|
+
|
|
44
|
+
function seedConversations(): void {
|
|
45
|
+
const conversationsDir = getConversationsDir();
|
|
46
|
+
mkdirSync(conversationsDir, { recursive: true });
|
|
47
|
+
|
|
48
|
+
// Four canonical conversation dirs with a meta + messages file each.
|
|
49
|
+
for (const name of [
|
|
50
|
+
CONV_DIRS.jan10,
|
|
51
|
+
CONV_DIRS.jan15,
|
|
52
|
+
CONV_DIRS.jan20,
|
|
53
|
+
CONV_DIRS.jan25,
|
|
54
|
+
]) {
|
|
55
|
+
const dir = join(conversationsDir, name);
|
|
56
|
+
mkdirSync(dir, { recursive: true });
|
|
57
|
+
writeFileSync(
|
|
58
|
+
join(dir, "meta.json"),
|
|
59
|
+
JSON.stringify({ name }, null, 2),
|
|
60
|
+
"utf-8",
|
|
61
|
+
);
|
|
62
|
+
writeFileSync(
|
|
63
|
+
join(dir, "messages.jsonl"),
|
|
64
|
+
`{"role":"user","content":"hi from ${name}"}\n`,
|
|
65
|
+
"utf-8",
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Malformed dir — should be skipped because parseConversationDirName
|
|
70
|
+
// returns null for it.
|
|
71
|
+
const invalidDir = join(conversationsDir, CONV_DIRS.invalid);
|
|
72
|
+
mkdirSync(invalidDir, { recursive: true });
|
|
73
|
+
writeFileSync(join(invalidDir, "junk.txt"), "should not be copied", "utf-8");
|
|
74
|
+
|
|
75
|
+
// A separate canonical conversation dir whose id is *not* an exact match
|
|
76
|
+
// for "conv-jan15" — used to verify that the conversationId filter does
|
|
77
|
+
// exact matching, not substring matching.
|
|
78
|
+
const attachmentsDir = join(conversationsDir, CONV_DIRS.jan15Attachments);
|
|
79
|
+
mkdirSync(join(attachmentsDir, "attachments"), { recursive: true });
|
|
80
|
+
writeFileSync(
|
|
81
|
+
join(attachmentsDir, "meta.json"),
|
|
82
|
+
JSON.stringify({ name: CONV_DIRS.jan15Attachments }, null, 2),
|
|
83
|
+
"utf-8",
|
|
84
|
+
);
|
|
85
|
+
writeFileSync(
|
|
86
|
+
join(attachmentsDir, "attachments", "photo.png"),
|
|
87
|
+
"PNGDATA",
|
|
88
|
+
"utf-8",
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let staging: string;
|
|
93
|
+
|
|
94
|
+
beforeEach(() => {
|
|
95
|
+
// Fresh staging directory for each test.
|
|
96
|
+
staging = mkdtempSync(join(tmpdir(), "ws-allowlist-staging-"));
|
|
97
|
+
// Reset the workspace's conversations dir between tests.
|
|
98
|
+
const conversationsDir = getConversationsDir();
|
|
99
|
+
rmSync(conversationsDir, { recursive: true, force: true });
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
afterEach(() => {
|
|
103
|
+
try {
|
|
104
|
+
rmSync(staging, { recursive: true, force: true });
|
|
105
|
+
} catch {
|
|
106
|
+
/* best-effort cleanup */
|
|
107
|
+
}
|
|
108
|
+
// Wipe the workspace's conversations dir so test files can't bleed into
|
|
109
|
+
// each other.
|
|
110
|
+
try {
|
|
111
|
+
rmSync(getConversationsDir(), { recursive: true, force: true });
|
|
112
|
+
} catch {
|
|
113
|
+
/* best-effort cleanup */
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Tests
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
describe("collectWorkspaceData — conversations entry", () => {
|
|
122
|
+
test("copies all valid conversation dirs when no filters are set", () => {
|
|
123
|
+
seedConversations();
|
|
124
|
+
|
|
125
|
+
const result = collectWorkspaceData({ staging });
|
|
126
|
+
|
|
127
|
+
expect(result.entries).toHaveLength(1);
|
|
128
|
+
const [entry] = result.entries;
|
|
129
|
+
expect(entry.entry).toBe("conversations");
|
|
130
|
+
// Four valid + one extra canonical (jan15-with-attachments) = 5
|
|
131
|
+
expect(entry.itemCount).toBe(5);
|
|
132
|
+
expect(entry.skippedDueToCap).toBe(0);
|
|
133
|
+
expect(entry.bytes).toBeGreaterThan(0);
|
|
134
|
+
expect(result.totalBytes).toBe(entry.bytes);
|
|
135
|
+
|
|
136
|
+
const copied = readdirSync(join(staging, "workspace", "conversations"));
|
|
137
|
+
expect(copied).toContain(CONV_DIRS.jan10);
|
|
138
|
+
expect(copied).toContain(CONV_DIRS.jan15);
|
|
139
|
+
expect(copied).toContain(CONV_DIRS.jan20);
|
|
140
|
+
expect(copied).toContain(CONV_DIRS.jan25);
|
|
141
|
+
expect(copied).toContain(CONV_DIRS.jan15Attachments);
|
|
142
|
+
// Malformed dir is skipped.
|
|
143
|
+
expect(copied).not.toContain(CONV_DIRS.invalid);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("startTime filter excludes earlier conversations", () => {
|
|
147
|
+
seedConversations();
|
|
148
|
+
const startTime = Date.parse("2025-01-14T00:00:00Z");
|
|
149
|
+
|
|
150
|
+
const result = collectWorkspaceData({ staging, startTime });
|
|
151
|
+
|
|
152
|
+
const copied = readdirSync(join(staging, "workspace", "conversations"));
|
|
153
|
+
expect(copied).not.toContain(CONV_DIRS.jan10);
|
|
154
|
+
expect(copied).toContain(CONV_DIRS.jan15);
|
|
155
|
+
expect(copied).toContain(CONV_DIRS.jan20);
|
|
156
|
+
expect(copied).toContain(CONV_DIRS.jan25);
|
|
157
|
+
// jan15-with-attachments has the same timestamp as jan15 → still included.
|
|
158
|
+
expect(copied).toContain(CONV_DIRS.jan15Attachments);
|
|
159
|
+
expect(result.entries[0].itemCount).toBe(4);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("endTime filter excludes later conversations", () => {
|
|
163
|
+
seedConversations();
|
|
164
|
+
const endTime = Date.parse("2025-01-22T00:00:00Z");
|
|
165
|
+
|
|
166
|
+
const result = collectWorkspaceData({ staging, endTime });
|
|
167
|
+
|
|
168
|
+
const copied = readdirSync(join(staging, "workspace", "conversations"));
|
|
169
|
+
expect(copied).toContain(CONV_DIRS.jan10);
|
|
170
|
+
expect(copied).toContain(CONV_DIRS.jan15);
|
|
171
|
+
expect(copied).toContain(CONV_DIRS.jan20);
|
|
172
|
+
expect(copied).not.toContain(CONV_DIRS.jan25);
|
|
173
|
+
expect(copied).toContain(CONV_DIRS.jan15Attachments);
|
|
174
|
+
expect(result.entries[0].itemCount).toBe(4);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("startTime + endTime keeps only conversations inside the window", () => {
|
|
178
|
+
seedConversations();
|
|
179
|
+
const startTime = Date.parse("2025-01-14T00:00:00Z");
|
|
180
|
+
const endTime = Date.parse("2025-01-22T00:00:00Z");
|
|
181
|
+
|
|
182
|
+
const result = collectWorkspaceData({ staging, startTime, endTime });
|
|
183
|
+
|
|
184
|
+
const copied = readdirSync(join(staging, "workspace", "conversations"));
|
|
185
|
+
expect(copied).not.toContain(CONV_DIRS.jan10);
|
|
186
|
+
expect(copied).toContain(CONV_DIRS.jan15);
|
|
187
|
+
expect(copied).toContain(CONV_DIRS.jan20);
|
|
188
|
+
expect(copied).not.toContain(CONV_DIRS.jan25);
|
|
189
|
+
// jan15-with-attachments shares the Jan 15 timestamp → still included.
|
|
190
|
+
expect(copied).toContain(CONV_DIRS.jan15Attachments);
|
|
191
|
+
expect(result.entries[0].itemCount).toBe(3);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("conversationId filter matches exactly (no substrings)", () => {
|
|
195
|
+
seedConversations();
|
|
196
|
+
|
|
197
|
+
const result = collectWorkspaceData({
|
|
198
|
+
staging,
|
|
199
|
+
conversationId: "conv-jan15",
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const copied = readdirSync(join(staging, "workspace", "conversations"));
|
|
203
|
+
expect(copied).toEqual([CONV_DIRS.jan15]);
|
|
204
|
+
// Crucially, the substring-match attachments dir is NOT included.
|
|
205
|
+
expect(copied).not.toContain(CONV_DIRS.jan15Attachments);
|
|
206
|
+
expect(result.entries[0].itemCount).toBe(1);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("conversationId + time filter intersection can be empty", () => {
|
|
210
|
+
seedConversations();
|
|
211
|
+
|
|
212
|
+
const result = collectWorkspaceData({
|
|
213
|
+
staging,
|
|
214
|
+
conversationId: "conv-jan15",
|
|
215
|
+
// Window that excludes Jan 15.
|
|
216
|
+
startTime: Date.parse("2025-01-16T00:00:00Z"),
|
|
217
|
+
endTime: Date.parse("2025-01-22T00:00:00Z"),
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
expect(result.entries).toHaveLength(1);
|
|
221
|
+
expect(result.entries[0].itemCount).toBe(0);
|
|
222
|
+
expect(result.entries[0].bytes).toBe(0);
|
|
223
|
+
expect(result.totalBytes).toBe(0);
|
|
224
|
+
// No directory should have been created because nothing was copied.
|
|
225
|
+
expect(existsSync(join(staging, "workspace", "conversations"))).toBe(false);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("includes conversation when createdAt is outside window but a message ts is inside", () => {
|
|
229
|
+
// Conversation was created on Jan 10 but received a message on
|
|
230
|
+
// Jan 18. With a [Jan 14, Jan 22] window, the directory-name parse
|
|
231
|
+
// says "out of window" but the message scan should keep it.
|
|
232
|
+
const conversationsDir = getConversationsDir();
|
|
233
|
+
mkdirSync(conversationsDir, { recursive: true });
|
|
234
|
+
const dir = join(conversationsDir, CONV_DIRS.jan10);
|
|
235
|
+
mkdirSync(dir, { recursive: true });
|
|
236
|
+
writeFileSync(
|
|
237
|
+
join(dir, "meta.json"),
|
|
238
|
+
JSON.stringify({ name: CONV_DIRS.jan10 }, null, 2),
|
|
239
|
+
"utf-8",
|
|
240
|
+
);
|
|
241
|
+
writeFileSync(
|
|
242
|
+
join(dir, "messages.jsonl"),
|
|
243
|
+
[
|
|
244
|
+
'{"role":"user","ts":"2025-01-10T00:00:00.000Z","content":"created"}',
|
|
245
|
+
'{"role":"user","ts":"2025-01-18T12:00:00.000Z","content":"in window"}',
|
|
246
|
+
"",
|
|
247
|
+
].join("\n"),
|
|
248
|
+
"utf-8",
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
const result = collectWorkspaceData({
|
|
252
|
+
staging,
|
|
253
|
+
startTime: Date.parse("2025-01-14T00:00:00Z"),
|
|
254
|
+
endTime: Date.parse("2025-01-22T00:00:00Z"),
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
expect(result.entries[0].itemCount).toBe(1);
|
|
258
|
+
const copied = readdirSync(join(staging, "workspace", "conversations"));
|
|
259
|
+
expect(copied).toEqual([CONV_DIRS.jan10]);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("excludes conversation when createdAt and every message ts are outside the window", () => {
|
|
263
|
+
// Conversation created on Jan 10 with messages only before/after the
|
|
264
|
+
// [Jan 14, Jan 22] window. Both filters miss → directory must be skipped.
|
|
265
|
+
const conversationsDir = getConversationsDir();
|
|
266
|
+
mkdirSync(conversationsDir, { recursive: true });
|
|
267
|
+
const dir = join(conversationsDir, CONV_DIRS.jan10);
|
|
268
|
+
mkdirSync(dir, { recursive: true });
|
|
269
|
+
writeFileSync(
|
|
270
|
+
join(dir, "meta.json"),
|
|
271
|
+
JSON.stringify({ name: CONV_DIRS.jan10 }, null, 2),
|
|
272
|
+
"utf-8",
|
|
273
|
+
);
|
|
274
|
+
writeFileSync(
|
|
275
|
+
join(dir, "messages.jsonl"),
|
|
276
|
+
[
|
|
277
|
+
'{"role":"user","ts":"2025-01-10T00:00:00.000Z","content":"too early"}',
|
|
278
|
+
'{"role":"user","ts":"2025-01-12T00:00:00.000Z","content":"still early"}',
|
|
279
|
+
'{"role":"user","ts":"2025-01-25T00:00:00.000Z","content":"too late"}',
|
|
280
|
+
"",
|
|
281
|
+
].join("\n"),
|
|
282
|
+
"utf-8",
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
const result = collectWorkspaceData({
|
|
286
|
+
staging,
|
|
287
|
+
startTime: Date.parse("2025-01-14T00:00:00Z"),
|
|
288
|
+
endTime: Date.parse("2025-01-22T00:00:00Z"),
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
expect(result.entries[0].itemCount).toBe(0);
|
|
292
|
+
expect(existsSync(join(staging, "workspace", "conversations"))).toBe(false);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test("includes conversation when createdAt is in window even if messages.jsonl is missing", () => {
|
|
296
|
+
// Conversation created on Jan 15 with no messages.jsonl yet (e.g.
|
|
297
|
+
// brand-new conversation). The cheap createdAt check is enough; we
|
|
298
|
+
// should never even open messages.jsonl.
|
|
299
|
+
const conversationsDir = getConversationsDir();
|
|
300
|
+
mkdirSync(conversationsDir, { recursive: true });
|
|
301
|
+
const dir = join(conversationsDir, CONV_DIRS.jan15);
|
|
302
|
+
mkdirSync(dir, { recursive: true });
|
|
303
|
+
writeFileSync(
|
|
304
|
+
join(dir, "meta.json"),
|
|
305
|
+
JSON.stringify({ name: CONV_DIRS.jan15 }, null, 2),
|
|
306
|
+
"utf-8",
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
const result = collectWorkspaceData({
|
|
310
|
+
staging,
|
|
311
|
+
startTime: Date.parse("2025-01-14T00:00:00Z"),
|
|
312
|
+
endTime: Date.parse("2025-01-22T00:00:00Z"),
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
expect(result.entries[0].itemCount).toBe(1);
|
|
316
|
+
const copied = readdirSync(join(staging, "workspace", "conversations"));
|
|
317
|
+
expect(copied).toEqual([CONV_DIRS.jan15]);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test("excludes conversation when createdAt is out of window and messages.jsonl is missing", () => {
|
|
321
|
+
// Conversation created on Jan 10 (out of window) with no
|
|
322
|
+
// messages.jsonl at all. Both checks miss → skip.
|
|
323
|
+
const conversationsDir = getConversationsDir();
|
|
324
|
+
mkdirSync(conversationsDir, { recursive: true });
|
|
325
|
+
const dir = join(conversationsDir, CONV_DIRS.jan10);
|
|
326
|
+
mkdirSync(dir, { recursive: true });
|
|
327
|
+
writeFileSync(
|
|
328
|
+
join(dir, "meta.json"),
|
|
329
|
+
JSON.stringify({ name: CONV_DIRS.jan10 }, null, 2),
|
|
330
|
+
"utf-8",
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
const result = collectWorkspaceData({
|
|
334
|
+
staging,
|
|
335
|
+
startTime: Date.parse("2025-01-14T00:00:00Z"),
|
|
336
|
+
endTime: Date.parse("2025-01-22T00:00:00Z"),
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
expect(result.entries[0].itemCount).toBe(0);
|
|
340
|
+
expect(existsSync(join(staging, "workspace", "conversations"))).toBe(false);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("canonical-named symlinks are rejected before the message-window scan runs", () => {
|
|
344
|
+
// Symlink creation requires elevated permissions on Windows; skip
|
|
345
|
+
// there to avoid spurious failures in CI on Windows hosts.
|
|
346
|
+
if (process.platform === "win32") return;
|
|
347
|
+
|
|
348
|
+
// Create an external directory containing a messages.jsonl whose
|
|
349
|
+
// single message timestamp falls inside the requested window. If
|
|
350
|
+
// the message-window scan ever follows the symlink, it would
|
|
351
|
+
// mistakenly include the symlink because the in-window message
|
|
352
|
+
// would "match". The boundary guard must reject the symlink first
|
|
353
|
+
// so the scan never reads outside `conversations/`.
|
|
354
|
+
const externalTarget = mkdtempSync(
|
|
355
|
+
join(tmpdir(), "ws-allowlist-symlink-msg-"),
|
|
356
|
+
);
|
|
357
|
+
try {
|
|
358
|
+
writeFileSync(
|
|
359
|
+
join(externalTarget, "meta.json"),
|
|
360
|
+
JSON.stringify({ name: "evil" }, null, 2),
|
|
361
|
+
"utf-8",
|
|
362
|
+
);
|
|
363
|
+
writeFileSync(
|
|
364
|
+
join(externalTarget, "messages.jsonl"),
|
|
365
|
+
'{"role":"user","ts":"2025-01-18T12:00:00.000Z","content":"in window"}\n',
|
|
366
|
+
"utf-8",
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
const conversationsDir = getConversationsDir();
|
|
370
|
+
mkdirSync(conversationsDir, { recursive: true });
|
|
371
|
+
|
|
372
|
+
// Canonical name with createdAt OUTSIDE the window so the cheap
|
|
373
|
+
// check fails and the message-window fallback would normally fire.
|
|
374
|
+
const evilName = "2025-01-10T00-00-00.000Z_evil-target";
|
|
375
|
+
symlinkSync(externalTarget, join(conversationsDir, evilName), "dir");
|
|
376
|
+
|
|
377
|
+
const result = collectWorkspaceData({
|
|
378
|
+
staging,
|
|
379
|
+
startTime: Date.parse("2025-01-14T00:00:00Z"),
|
|
380
|
+
endTime: Date.parse("2025-01-22T00:00:00Z"),
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// The boundary guard must reject the symlink before the message
|
|
384
|
+
// scan ever opens the external messages.jsonl. Nothing must land
|
|
385
|
+
// in the staging directory.
|
|
386
|
+
expect(result.entries).toHaveLength(1);
|
|
387
|
+
const [entry] = result.entries;
|
|
388
|
+
expect(entry.itemCount).toBe(0);
|
|
389
|
+
expect(entry.bytes).toBe(0);
|
|
390
|
+
expect(entry.skippedDueToCap).toBe(0);
|
|
391
|
+
expect(existsSync(join(staging, "workspace", "conversations"))).toBe(
|
|
392
|
+
false,
|
|
393
|
+
);
|
|
394
|
+
} finally {
|
|
395
|
+
rmSync(externalTarget, { recursive: true, force: true });
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
test("streaming scan finds an in-window message in a large messages.jsonl", () => {
|
|
400
|
+
// Build a messages.jsonl that's large enough to span multiple
|
|
401
|
+
// 64 KB read chunks. Padding messages have an out-of-window ts; a
|
|
402
|
+
// single in-window message is buried near the end so the scan must
|
|
403
|
+
// actually traverse most of the file to find it. This exercises
|
|
404
|
+
// the streaming + UTF-8 boundary handling without ever loading
|
|
405
|
+
// the whole file into a single string.
|
|
406
|
+
const conversationsDir = getConversationsDir();
|
|
407
|
+
mkdirSync(conversationsDir, { recursive: true });
|
|
408
|
+
const dir = join(conversationsDir, CONV_DIRS.jan10);
|
|
409
|
+
mkdirSync(dir, { recursive: true });
|
|
410
|
+
writeFileSync(
|
|
411
|
+
join(dir, "meta.json"),
|
|
412
|
+
JSON.stringify({ name: CONV_DIRS.jan10 }, null, 2),
|
|
413
|
+
"utf-8",
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
const padLine = `{"role":"user","ts":"2025-01-10T00:00:00.000Z","content":"${"x".repeat(500)}"}`;
|
|
417
|
+
const matchLine =
|
|
418
|
+
'{"role":"user","ts":"2025-01-18T12:00:00.000Z","content":"hit"}';
|
|
419
|
+
// ~500 padding lines + 1 match line ≈ 250 KB, well over a single
|
|
420
|
+
// 64 KB chunk.
|
|
421
|
+
const lines: string[] = [];
|
|
422
|
+
for (let i = 0; i < 500; i++) lines.push(padLine);
|
|
423
|
+
lines.push(matchLine);
|
|
424
|
+
writeFileSync(
|
|
425
|
+
join(dir, "messages.jsonl"),
|
|
426
|
+
lines.join("\n") + "\n",
|
|
427
|
+
"utf-8",
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
const result = collectWorkspaceData({
|
|
431
|
+
staging,
|
|
432
|
+
startTime: Date.parse("2025-01-14T00:00:00Z"),
|
|
433
|
+
endTime: Date.parse("2025-01-22T00:00:00Z"),
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
expect(result.entries[0].itemCount).toBe(1);
|
|
437
|
+
const copied = readdirSync(join(staging, "workspace", "conversations"));
|
|
438
|
+
expect(copied).toEqual([CONV_DIRS.jan10]);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
test("malformed messages.jsonl lines are silently skipped during the window scan", () => {
|
|
442
|
+
// Conversation created on Jan 10 (out of window). messages.jsonl
|
|
443
|
+
// has garbage on most lines but ONE valid line whose ts is in
|
|
444
|
+
// window — that single valid line should be enough to keep the dir.
|
|
445
|
+
const conversationsDir = getConversationsDir();
|
|
446
|
+
mkdirSync(conversationsDir, { recursive: true });
|
|
447
|
+
const dir = join(conversationsDir, CONV_DIRS.jan10);
|
|
448
|
+
mkdirSync(dir, { recursive: true });
|
|
449
|
+
writeFileSync(
|
|
450
|
+
join(dir, "meta.json"),
|
|
451
|
+
JSON.stringify({ name: CONV_DIRS.jan10 }, null, 2),
|
|
452
|
+
"utf-8",
|
|
453
|
+
);
|
|
454
|
+
writeFileSync(
|
|
455
|
+
join(dir, "messages.jsonl"),
|
|
456
|
+
[
|
|
457
|
+
"not json at all",
|
|
458
|
+
'{"role":"user"}', // missing ts
|
|
459
|
+
'{"role":"user","ts":"not-a-date"}', // ts isn't parseable
|
|
460
|
+
'{"role":"user","ts":42}', // ts is wrong type
|
|
461
|
+
'{"role":"user","ts":"2025-01-18T12:00:00.000Z","content":"valid"}',
|
|
462
|
+
"",
|
|
463
|
+
].join("\n"),
|
|
464
|
+
"utf-8",
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
const result = collectWorkspaceData({
|
|
468
|
+
staging,
|
|
469
|
+
startTime: Date.parse("2025-01-14T00:00:00Z"),
|
|
470
|
+
endTime: Date.parse("2025-01-22T00:00:00Z"),
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
expect(result.entries[0].itemCount).toBe(1);
|
|
474
|
+
const copied = readdirSync(join(staging, "workspace", "conversations"));
|
|
475
|
+
expect(copied).toEqual([CONV_DIRS.jan10]);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
test("byte cap enforcement skips every conversation when too tight", () => {
|
|
479
|
+
seedConversations();
|
|
480
|
+
|
|
481
|
+
// 1 byte cap is impossible to fit any seeded dir into.
|
|
482
|
+
const result = collectWorkspaceData({ staging, maxBytes: 1 });
|
|
483
|
+
|
|
484
|
+
expect(result.entries).toHaveLength(1);
|
|
485
|
+
const [entry] = result.entries;
|
|
486
|
+
expect(entry.itemCount).toBe(0);
|
|
487
|
+
expect(entry.bytes).toBe(0);
|
|
488
|
+
expect(entry.skippedDueToCap).toBe(5);
|
|
489
|
+
expect(result.totalBytes).toBe(0);
|
|
490
|
+
expect(existsSync(join(staging, "workspace", "conversations"))).toBe(false);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
test("byte cap keeps the newest conversations first", () => {
|
|
494
|
+
// Seed three dirs with distinct, non-trivial sizes and known
|
|
495
|
+
// timestamps. Use a padding file per dir so the per-dir byte total
|
|
496
|
+
// is predictable and large enough to push us past the cap after a
|
|
497
|
+
// couple entries have been copied.
|
|
498
|
+
const conversationsDir = getConversationsDir();
|
|
499
|
+
mkdirSync(conversationsDir, { recursive: true });
|
|
500
|
+
|
|
501
|
+
const PADDING_BYTES = 4000;
|
|
502
|
+
const padding = "a".repeat(PADDING_BYTES);
|
|
503
|
+
for (const name of [CONV_DIRS.jan10, CONV_DIRS.jan15, CONV_DIRS.jan20]) {
|
|
504
|
+
const dir = join(conversationsDir, name);
|
|
505
|
+
mkdirSync(dir, { recursive: true });
|
|
506
|
+
writeFileSync(join(dir, "pad.txt"), padding, "utf-8");
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Each dir weighs ~PADDING_BYTES. A 10 KB cap fits exactly 2 dirs
|
|
510
|
+
// (but not the third).
|
|
511
|
+
const result = collectWorkspaceData({
|
|
512
|
+
staging,
|
|
513
|
+
maxBytes: PADDING_BYTES * 2 + 500,
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
expect(result.entries).toHaveLength(1);
|
|
517
|
+
const [entry] = result.entries;
|
|
518
|
+
expect(entry.itemCount).toBe(2);
|
|
519
|
+
expect(entry.skippedDueToCap).toBe(1);
|
|
520
|
+
|
|
521
|
+
// The two newest dirs (jan20 and jan15) should have been copied;
|
|
522
|
+
// jan10 (oldest) should be the one skipped due to the cap.
|
|
523
|
+
const copied = readdirSync(join(staging, "workspace", "conversations"));
|
|
524
|
+
expect(copied).toContain(CONV_DIRS.jan20);
|
|
525
|
+
expect(copied).toContain(CONV_DIRS.jan15);
|
|
526
|
+
expect(copied).not.toContain(CONV_DIRS.jan10);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
test("non-directory entries with canonical-looking names are skipped", () => {
|
|
530
|
+
// Make sure the conversations dir exists and seed one valid dir so
|
|
531
|
+
// we can confirm the function still copies legit entries alongside
|
|
532
|
+
// the bogus regular file.
|
|
533
|
+
const conversationsDir = getConversationsDir();
|
|
534
|
+
mkdirSync(conversationsDir, { recursive: true });
|
|
535
|
+
|
|
536
|
+
// Seed a real canonical conversation dir so there's something to copy.
|
|
537
|
+
const validDir = join(conversationsDir, CONV_DIRS.jan20);
|
|
538
|
+
mkdirSync(validDir, { recursive: true });
|
|
539
|
+
writeFileSync(
|
|
540
|
+
join(validDir, "meta.json"),
|
|
541
|
+
JSON.stringify({ name: CONV_DIRS.jan20 }, null, 2),
|
|
542
|
+
"utf-8",
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
// Seed a REGULAR FILE whose name matches the canonical
|
|
546
|
+
// `<ISO>_<conversationId>` pattern. Fill it with data that is big
|
|
547
|
+
// enough to exceed the cap, to prove that the non-dir guard bails
|
|
548
|
+
// before `dirSizeWithinBudget`/`cpSync` could silently copy it.
|
|
549
|
+
const bogusName = "2025-01-15T00-00-00.000Z_conv-jan15-as-file";
|
|
550
|
+
const bogusPath = join(conversationsDir, bogusName);
|
|
551
|
+
writeFileSync(bogusPath, "x".repeat(1024 * 1024), "utf-8"); // 1 MB
|
|
552
|
+
|
|
553
|
+
// Use a tight cap (larger than the valid dir but smaller than the
|
|
554
|
+
// bogus file) to prove the bogus file is skipped before copying.
|
|
555
|
+
const result = collectWorkspaceData({
|
|
556
|
+
staging,
|
|
557
|
+
maxBytes: 100 * 1024, // 100 KB
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
expect(result.entries).toHaveLength(1);
|
|
561
|
+
const [entry] = result.entries;
|
|
562
|
+
// Only the real conversation dir should have been copied.
|
|
563
|
+
expect(entry.itemCount).toBe(1);
|
|
564
|
+
// skippedDueToCap should NOT include the bogus file — it's rejected
|
|
565
|
+
// by the non-dir guard, not by the cap.
|
|
566
|
+
expect(entry.skippedDueToCap).toBe(0);
|
|
567
|
+
|
|
568
|
+
const copied = readdirSync(join(staging, "workspace", "conversations"));
|
|
569
|
+
expect(copied).toEqual([CONV_DIRS.jan20]);
|
|
570
|
+
expect(copied).not.toContain(bogusName);
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
test("missing conversations dir returns an empty entry summary", () => {
|
|
574
|
+
// Do NOT seed — workspace has no conversations/ subdir.
|
|
575
|
+
const conversationsDir = getConversationsDir();
|
|
576
|
+
rmSync(conversationsDir, { recursive: true, force: true });
|
|
577
|
+
expect(existsSync(conversationsDir)).toBe(false);
|
|
578
|
+
|
|
579
|
+
const result = collectWorkspaceData({ staging });
|
|
580
|
+
|
|
581
|
+
expect(result.entries).toHaveLength(1);
|
|
582
|
+
expect(result.entries[0]).toEqual({
|
|
583
|
+
entry: "conversations",
|
|
584
|
+
itemCount: 0,
|
|
585
|
+
bytes: 0,
|
|
586
|
+
skippedDueToCap: 0,
|
|
587
|
+
});
|
|
588
|
+
expect(result.totalBytes).toBe(0);
|
|
589
|
+
expect(existsSync(join(staging, "workspace"))).toBe(false);
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
test("recursive copy preserves nested attachments", () => {
|
|
593
|
+
seedConversations();
|
|
594
|
+
|
|
595
|
+
collectWorkspaceData({
|
|
596
|
+
staging,
|
|
597
|
+
conversationId: "conv-jan15-with-attachments",
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
const copied = readdirSync(join(staging, "workspace", "conversations"));
|
|
601
|
+
expect(copied).toEqual([CONV_DIRS.jan15Attachments]);
|
|
602
|
+
const photoPath = join(
|
|
603
|
+
staging,
|
|
604
|
+
"workspace",
|
|
605
|
+
"conversations",
|
|
606
|
+
CONV_DIRS.jan15Attachments,
|
|
607
|
+
"attachments",
|
|
608
|
+
"photo.png",
|
|
609
|
+
);
|
|
610
|
+
expect(existsSync(photoPath)).toBe(true);
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
test("skips symlinked directories to avoid infinite loops", () => {
|
|
614
|
+
// Symlink creation requires elevated permissions on Windows; skip
|
|
615
|
+
// there to avoid spurious failures in CI on Windows hosts.
|
|
616
|
+
if (process.platform === "win32") return;
|
|
617
|
+
|
|
618
|
+
// Seed a single canonical conversation directory and stick a
|
|
619
|
+
// symlink loop inside it (`loop -> .`). `dirSizeWithinBudget` uses
|
|
620
|
+
// `lstatSync` so that symlinks are skipped rather than dereferenced;
|
|
621
|
+
// without this, following the symlink would recurse infinitely and
|
|
622
|
+
// hang the export. We expect the function to return promptly and
|
|
623
|
+
// still process the conversation directory.
|
|
624
|
+
const conversationsDir = getConversationsDir();
|
|
625
|
+
mkdirSync(conversationsDir, { recursive: true });
|
|
626
|
+
|
|
627
|
+
const convDir = join(conversationsDir, CONV_DIRS.jan20);
|
|
628
|
+
mkdirSync(convDir, { recursive: true });
|
|
629
|
+
writeFileSync(
|
|
630
|
+
join(convDir, "meta.json"),
|
|
631
|
+
JSON.stringify({ name: CONV_DIRS.jan20 }, null, 2),
|
|
632
|
+
"utf-8",
|
|
633
|
+
);
|
|
634
|
+
|
|
635
|
+
// Create the loop: <conv-dir>/loop -> .
|
|
636
|
+
symlinkSync(".", join(convDir, "loop"), "dir");
|
|
637
|
+
|
|
638
|
+
const startMs = Date.now();
|
|
639
|
+
const result = collectWorkspaceData({ staging });
|
|
640
|
+
const elapsedMs = Date.now() - startMs;
|
|
641
|
+
|
|
642
|
+
// Sanity check: the call must complete quickly. The `lstatSync`
|
|
643
|
+
// guard is what keeps this from hanging — if the recursive walker
|
|
644
|
+
// were to dereference the symlink, the bun test runner would time
|
|
645
|
+
// out long before this assertion ever fired.
|
|
646
|
+
expect(elapsedMs).toBeLessThan(5000);
|
|
647
|
+
|
|
648
|
+
expect(result.entries).toHaveLength(1);
|
|
649
|
+
const [entry] = result.entries;
|
|
650
|
+
expect(entry.entry).toBe("conversations");
|
|
651
|
+
// The conversation directory should still be processed and copied;
|
|
652
|
+
// we don't care whether the symlink itself was reproduced in the
|
|
653
|
+
// copy — the key invariant is that the function completed.
|
|
654
|
+
expect(entry.itemCount).toBe(1);
|
|
655
|
+
expect(entry.skippedDueToCap).toBe(0);
|
|
656
|
+
|
|
657
|
+
const copied = readdirSync(join(staging, "workspace", "conversations"));
|
|
658
|
+
expect(copied).toContain(CONV_DIRS.jan20);
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
test("rejects top-level symlinks pointing outside the conversations dir", () => {
|
|
662
|
+
// Symlink creation requires elevated permissions on Windows; skip
|
|
663
|
+
// there to avoid spurious failures in CI on Windows hosts.
|
|
664
|
+
if (process.platform === "win32") return;
|
|
665
|
+
|
|
666
|
+
// Create a directory OUTSIDE `conversations/` that masquerades as a
|
|
667
|
+
// valid conversation dir (with a `meta.json`). The allowlist guard
|
|
668
|
+
// must not allow a symlink with a canonical name to escape the
|
|
669
|
+
// `conversations/` boundary by dereferencing into this external
|
|
670
|
+
// target.
|
|
671
|
+
const externalTarget = mkdtempSync(
|
|
672
|
+
join(tmpdir(), "ws-allowlist-external-"),
|
|
673
|
+
);
|
|
674
|
+
try {
|
|
675
|
+
writeFileSync(
|
|
676
|
+
join(externalTarget, "meta.json"),
|
|
677
|
+
JSON.stringify({ name: "evil" }, null, 2),
|
|
678
|
+
"utf-8",
|
|
679
|
+
);
|
|
680
|
+
writeFileSync(
|
|
681
|
+
join(externalTarget, "secret.txt"),
|
|
682
|
+
"should never be copied",
|
|
683
|
+
"utf-8",
|
|
684
|
+
);
|
|
685
|
+
|
|
686
|
+
// Seed the conversations dir and add a symlink with a canonical
|
|
687
|
+
// name pointing at the external target.
|
|
688
|
+
const conversationsDir = getConversationsDir();
|
|
689
|
+
mkdirSync(conversationsDir, { recursive: true });
|
|
690
|
+
|
|
691
|
+
const evilName = "2025-01-30T00-00-00.000Z_evil-target";
|
|
692
|
+
symlinkSync(externalTarget, join(conversationsDir, evilName), "dir");
|
|
693
|
+
|
|
694
|
+
const result = collectWorkspaceData({ staging });
|
|
695
|
+
|
|
696
|
+
// The symlink must be skipped by the top-level `lstatSync` guard.
|
|
697
|
+
// Nothing from the external target should land in the staging
|
|
698
|
+
// directory and the entry summary should not count it.
|
|
699
|
+
expect(result.entries).toHaveLength(1);
|
|
700
|
+
const [entry] = result.entries;
|
|
701
|
+
expect(entry.entry).toBe("conversations");
|
|
702
|
+
expect(entry.itemCount).toBe(0);
|
|
703
|
+
expect(entry.skippedDueToCap).toBe(0);
|
|
704
|
+
expect(entry.bytes).toBe(0);
|
|
705
|
+
expect(result.totalBytes).toBe(0);
|
|
706
|
+
|
|
707
|
+
// No staging directory should have been created because nothing
|
|
708
|
+
// qualified for copying.
|
|
709
|
+
expect(existsSync(join(staging, "workspace", "conversations"))).toBe(
|
|
710
|
+
false,
|
|
711
|
+
);
|
|
712
|
+
} finally {
|
|
713
|
+
rmSync(externalTarget, { recursive: true, force: true });
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
});
|