@vellumai/assistant 0.6.0 → 0.6.2
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/AGENTS.md +4 -0
- package/ARCHITECTURE.md +68 -15
- package/Dockerfile +2 -2
- package/bun.lock +6 -2
- package/docker-entrypoint.sh +42 -1
- package/docs/architecture/integrations.md +1 -1
- package/docs/architecture/memory.md +21 -24
- package/node_modules/@vellumai/ces-contracts/src/handles.ts +7 -9
- package/openapi.yaml +539 -4
- package/package.json +5 -1
- package/src/__tests__/anthropic-provider.test.ts +160 -95
- package/src/__tests__/app-dir-path-guard.test.ts +1 -0
- package/src/__tests__/app-executors.test.ts +47 -1
- package/src/__tests__/app-source-watcher.test.ts +159 -0
- package/src/__tests__/assistant-event-hub.test.ts +30 -0
- package/src/__tests__/checker.test.ts +138 -172
- package/src/__tests__/cli-command-risk-guard.test.ts +1 -1
- package/src/__tests__/config-schema.test.ts +5 -0
- package/src/__tests__/context-overflow-approval.test.ts +5 -5
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -6
- package/src/__tests__/conversation-agent-loop.test.ts +4 -51
- package/src/__tests__/conversation-analysis-routes.test.ts +169 -0
- package/src/__tests__/conversation-directories-parse.test.ts +105 -0
- package/src/__tests__/conversation-history-web-search.test.ts +1 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +653 -832
- package/src/__tests__/conversation-runtime-workspace.test.ts +1 -93
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +17 -4
- package/src/__tests__/conversation-wipe.test.ts +2 -6
- package/src/__tests__/conversation-workspace-cache-state.test.ts +6 -12
- package/src/__tests__/conversation-workspace-injection.test.ts +25 -26
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
- package/src/__tests__/copy-composer-tc-templates.test.ts +335 -0
- package/src/__tests__/credential-execution-approval-bridge.test.ts +0 -2
- package/src/__tests__/date-context.test.ts +76 -210
- package/src/__tests__/db-schedule-syntax-migration.test.ts +16 -1
- package/src/__tests__/file-list-tool.test.ts +219 -0
- package/src/__tests__/first-greeting.test.ts +1 -1
- package/src/__tests__/heartbeat-service.test.ts +180 -3
- package/src/__tests__/identity-routes.test.ts +328 -0
- package/src/__tests__/init-feature-flag-overrides.test.ts +167 -0
- package/src/__tests__/injection-block.test.ts +24 -0
- package/src/__tests__/inline-command-runner.test.ts +7 -5
- package/src/__tests__/install-skill-routing.test.ts +7 -6
- package/src/__tests__/jobs-store-qdrant-breaker.test.ts +15 -14
- package/src/__tests__/list-messages-tool-merge.test.ts +300 -0
- package/src/__tests__/llm-context-normalization.test.ts +18 -18
- package/src/__tests__/llm-context-route-provider.test.ts +101 -0
- package/src/__tests__/llm-request-log-turn-query.test.ts +162 -0
- package/src/__tests__/log-export-workspace.test.ts +257 -100
- package/src/__tests__/managed-credential-catalog-cli.test.ts +12 -14
- package/src/__tests__/mcp-abort-signal.test.ts +5 -0
- package/src/__tests__/mcp-client-auth.test.ts +5 -0
- package/src/__tests__/memory-recall-log-store.test.ts +132 -0
- package/src/__tests__/migration-export-streaming.test.ts +304 -0
- package/src/__tests__/migration-import-commit-http.test.ts +11 -10
- package/src/__tests__/mock-fetch.ts +87 -0
- package/src/__tests__/navigate-settings-tab.test.ts +14 -1
- package/src/__tests__/notification-broadcaster.test.ts +65 -0
- package/src/__tests__/notification-decision-recipient-context.test.ts +282 -0
- package/src/__tests__/onboarding-template-contract.test.ts +63 -14
- package/src/__tests__/parser.test.ts +32 -0
- package/src/__tests__/permission-checker-host-gate.test.ts +452 -0
- package/src/__tests__/permission-controls-v2-flag.test.ts +55 -0
- package/src/__tests__/permission-mode-sse.test.ts +418 -0
- package/src/__tests__/permission-mode-store.test.ts +277 -0
- package/src/__tests__/permission-mode.test.ts +101 -0
- package/src/__tests__/pkb-autoinject.test.ts +96 -0
- package/src/__tests__/platform-bash-auto-approve.test.ts +359 -0
- package/src/__tests__/profiler-routes.test.ts +502 -0
- package/src/__tests__/profiler-run-store.test.ts +441 -0
- package/src/__tests__/proxy-approval-callback.test.ts +4 -75
- package/src/__tests__/registry.test.ts +1 -1
- package/src/__tests__/require-fresh-approval.test.ts +0 -2
- package/src/__tests__/sandbox-diagnostics.test.ts +1 -32
- package/src/__tests__/sandbox-host-parity.test.ts +5 -4
- package/src/__tests__/scheduler-reuse-conversation.test.ts +368 -0
- package/src/__tests__/scrub-corrupted-image-attachments.test.ts +278 -0
- package/src/__tests__/search-skills-unified.test.ts +4 -3
- package/src/__tests__/send-endpoint-busy.test.ts +42 -3
- package/src/__tests__/set-permission-mode.test.ts +274 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +12 -0
- package/src/__tests__/skill-memory.test.ts +2 -783
- package/src/__tests__/strip-memory-injections.test.ts +187 -0
- package/src/__tests__/subagent-detail.test.ts +84 -0
- package/src/__tests__/subagent-disposal.test.ts +308 -0
- package/src/__tests__/subagent-manager-notify.test.ts +19 -10
- package/src/__tests__/subagent-notify-parent.test.ts +390 -0
- package/src/__tests__/subagent-role-registry.test.ts +108 -0
- package/src/__tests__/subagent-tool-filtering.test.ts +71 -0
- package/src/__tests__/subagent-tools.test.ts +464 -4
- package/src/__tests__/system-prompt-ask-mode.test.ts +139 -0
- package/src/__tests__/task-memory-cleanup.test.ts +12 -12
- package/src/__tests__/terminal-sandbox.test.ts +1 -1
- package/src/__tests__/terminal-tools.test.ts +16 -29
- package/src/__tests__/test-preload.ts +18 -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 +4 -27
- package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -0
- package/src/__tests__/top-level-renderer.test.ts +10 -13
- package/src/__tests__/transport-hints-queue.test.ts +77 -0
- package/src/__tests__/trust-store.test.ts +4 -4
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +116 -2
- package/src/__tests__/workspace-migration-028-recover-conversations-from-disk-view.test.ts +387 -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/agent/loop.ts +6 -29
- package/src/approvals/guardian-request-resolvers.ts +24 -0
- package/src/avatar/traits-png-sync.ts +3 -3
- package/src/channels/types.ts +5 -0
- package/src/cli/__tests__/run-assistant-command.ts +56 -0
- package/src/cli/__tests__/unknown-command.test.ts +33 -0
- package/src/cli/commands/__tests__/email-download.test.ts +245 -0
- package/src/cli/commands/__tests__/email-list.test.ts +192 -0
- package/src/cli/commands/__tests__/email-register.test.ts +186 -0
- package/src/cli/commands/__tests__/email-send.test.ts +291 -0
- package/src/cli/commands/__tests__/email-status.test.ts +181 -0
- package/src/cli/commands/__tests__/email-unregister.test.ts +139 -0
- package/src/cli/commands/__tests__/routes.test.ts +562 -0
- package/src/cli/commands/conversations.ts +1 -8
- package/src/cli/commands/default-action.ts +68 -1
- package/src/cli/commands/email.ts +584 -835
- package/src/cli/commands/memory.ts +1 -34
- package/src/cli/commands/notifications.ts +7 -2
- package/src/cli/commands/oauth/__tests__/connect.test.ts +27 -0
- package/src/cli/commands/oauth/connect.ts +25 -5
- 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/routes.ts +396 -0
- package/src/cli/commands/skills.ts +130 -20
- package/src/cli/program.ts +11 -2
- package/src/cli.ts +1 -120
- package/src/config/assistant-feature-flags.ts +59 -55
- package/src/config/bundled-skills/app-builder/SKILL.md +91 -5
- package/src/config/bundled-skills/gmail/SKILL.md +13 -8
- 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/messaging/SKILL.md +7 -0
- package/src/config/bundled-skills/schedule/SKILL.md +22 -2
- package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
- package/src/config/bundled-skills/settings/TOOLS.json +1 -1
- package/src/config/bundled-skills/settings/tools/avatar-get.ts +3 -13
- package/src/config/bundled-skills/settings/tools/avatar-remove.ts +2 -4
- package/src/config/bundled-skills/settings/tools/avatar-update.ts +5 -2
- package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +8 -3
- package/src/config/bundled-skills/slack/SKILL.md +2 -0
- package/src/config/bundled-skills/subagent/SKILL.md +43 -3
- package/src/config/bundled-skills/subagent/TOOLS.json +29 -4
- package/src/config/env-registry.ts +63 -0
- package/src/config/feature-flag-registry.json +17 -1
- package/src/config/schema.ts +8 -0
- package/src/config/schemas/filing.ts +51 -0
- package/src/config/schemas/heartbeat.ts +15 -12
- package/src/config/schemas/memory-lifecycle.ts +12 -0
- package/src/config/schemas/security.ts +14 -0
- package/src/config/schemas/services.ts +8 -0
- package/src/credential-execution/approval-bridge.ts +0 -1
- package/src/credential-execution/managed-catalog.ts +3 -7
- package/src/daemon/app-source-watcher.ts +93 -0
- package/src/daemon/config-watcher.ts +85 -3
- package/src/daemon/context-overflow-approval.ts +0 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +20 -0
- package/src/daemon/conversation-agent-loop.ts +179 -65
- package/src/daemon/conversation-attachments.ts +0 -1
- package/src/daemon/conversation-history.ts +4 -19
- package/src/daemon/conversation-lifecycle.ts +8 -14
- package/src/daemon/conversation-messaging.ts +3 -0
- package/src/daemon/conversation-process.ts +30 -8
- package/src/daemon/conversation-queue-manager.ts +8 -0
- package/src/daemon/conversation-runtime-assembly.ts +359 -308
- package/src/daemon/conversation-surfaces.ts +65 -0
- package/src/daemon/conversation-tool-setup.ts +44 -17
- package/src/daemon/conversation-workspace.ts +1 -2
- package/src/daemon/conversation.ts +19 -3
- package/src/daemon/date-context.ts +26 -53
- package/src/daemon/first-greeting.ts +1 -1
- package/src/daemon/handlers/conversations.ts +5 -7
- package/src/daemon/handlers/shared.test.ts +143 -0
- package/src/daemon/handlers/shared.ts +70 -5
- package/src/daemon/handlers/skills.ts +11 -18
- package/src/daemon/lifecycle.ts +220 -158
- package/src/daemon/message-types/conversations.ts +29 -6
- package/src/daemon/message-types/messages.ts +9 -2
- package/src/daemon/message-types/notifications.ts +12 -0
- package/src/daemon/message-types/schedules.ts +1 -0
- package/src/daemon/message-types/settings.ts +18 -0
- package/src/daemon/profiler-run-store.ts +557 -0
- package/src/daemon/server.ts +87 -10
- package/src/daemon/shutdown-handlers.ts +5 -0
- package/src/daemon/tool-side-effects.ts +23 -3
- package/src/daemon/transport-hints.ts +33 -0
- package/src/export/transcript-formatter.ts +148 -0
- package/src/filing/filing-service.ts +228 -0
- package/src/heartbeat/heartbeat-service.ts +96 -7
- package/src/index.ts +1 -1
- package/src/mcp/client.ts +6 -0
- package/src/mcp/mcp-oauth-provider.ts +149 -27
- package/src/memory/admin.ts +33 -32
- package/src/memory/app-store.ts +69 -0
- package/src/memory/conversation-bootstrap.ts +1 -1
- package/src/memory/conversation-crud.ts +151 -117
- package/src/memory/conversation-directories.ts +39 -0
- package/src/memory/conversation-group-migration.ts +66 -6
- package/src/memory/conversation-queries.ts +58 -12
- package/src/memory/conversation-title-service.ts +1 -0
- package/src/memory/db-init.ts +182 -376
- package/src/memory/embedding-local.ts +1 -1
- package/src/memory/graph/bootstrap.ts +75 -66
- package/src/memory/graph/capability-seed.ts +167 -17
- package/src/memory/graph/consolidation.ts +38 -4
- package/src/memory/graph/conversation-graph-memory.ts +133 -104
- package/src/memory/graph/extraction-job.ts +9 -4
- package/src/memory/graph/extraction.ts +66 -23
- package/src/memory/graph/graph-memory-state-store.ts +37 -0
- package/src/memory/graph/graph-search.ts +29 -15
- package/src/memory/graph/injection.ts +38 -8
- package/src/memory/graph/inspect.ts +12 -3
- package/src/memory/graph/retriever.ts +365 -262
- package/src/memory/graph/store.test.ts +48 -0
- package/src/memory/graph/store.ts +150 -11
- package/src/memory/graph/tool-handlers.ts +84 -209
- package/src/memory/graph/tools.ts +8 -52
- package/src/memory/graph/types.ts +24 -0
- package/src/memory/group-crud.ts +25 -9
- package/src/memory/job-handlers/cleanup.ts +44 -1
- package/src/memory/jobs-store.ts +70 -60
- package/src/memory/jobs-worker.ts +44 -28
- package/src/memory/llm-request-log-store.ts +96 -12
- package/src/memory/memory-recall-log-store.ts +49 -5
- package/src/memory/migrations/203-drop-memory-items-tables.ts +33 -1
- package/src/memory/migrations/206-memory-graph-node-edits.ts +19 -0
- package/src/memory/migrations/206-scrub-corrupted-image-attachments.ts +131 -0
- package/src/memory/migrations/207-conversation-graph-memory-state.ts +20 -0
- package/src/memory/migrations/208-conversations-last-message-at.ts +35 -0
- package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +85 -0
- package/src/memory/migrations/210-schedule-reuse-conversation.ts +13 -0
- package/src/memory/migrations/211-memory-recall-logs-query-context.ts +21 -0
- package/src/memory/migrations/212-llm-request-logs-created-at-index.ts +19 -0
- package/src/memory/migrations/index.ts +8 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/conversations.ts +14 -0
- package/src/memory/schema/infrastructure.ts +8 -1
- package/src/memory/schema/memory-core.ts +0 -51
- package/src/memory/schema/memory-graph.ts +15 -0
- package/src/memory/task-memory-cleanup.ts +30 -11
- 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/copy-composer.ts +86 -0
- package/src/notifications/decision-engine.ts +35 -0
- package/src/notifications/emit-signal.ts +14 -0
- package/src/notifications/signal.ts +11 -0
- package/src/oauth/platform-connection.test.ts +2 -2
- package/src/oauth/seed-providers.ts +1 -0
- package/src/permissions/checker.ts +15 -4
- package/src/permissions/defaults.ts +7 -8
- package/src/permissions/permission-mode-store.ts +180 -0
- package/src/permissions/permission-mode.ts +31 -0
- package/src/permissions/prompter.ts +0 -2
- package/src/permissions/workspace-policy.ts +9 -0
- package/src/platform/client.ts +1 -1
- package/src/prompts/system-prompt.ts +59 -7
- package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +100 -0
- package/src/prompts/templates/BOOTSTRAP.md +76 -162
- package/src/prompts/templates/HEARTBEAT.md +3 -1
- package/src/prompts/templates/SOUL.md +30 -9
- package/src/prompts/templates/UPDATES.md +8 -0
- package/src/providers/anthropic/client.ts +107 -219
- package/src/runtime/assistant-event-hub.ts +22 -0
- package/src/runtime/auth/route-policy.ts +23 -0
- package/src/runtime/auth/token-service.ts +8 -0
- package/src/runtime/http-server.ts +32 -2
- package/src/runtime/http-types.ts +12 -1
- package/src/runtime/migrations/vbundle-builder.ts +389 -3
- package/src/runtime/migrations/vbundle-importer.ts +8 -6
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +378 -0
- package/src/runtime/routes/app-management-routes.ts +1 -11
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +26 -0
- package/src/runtime/routes/archive-utils.ts +29 -0
- package/src/runtime/routes/avatar-routes.ts +2 -9
- package/src/runtime/routes/btw-routes.ts +14 -1
- package/src/runtime/routes/conversation-analysis-routes.ts +185 -0
- package/src/runtime/routes/conversation-management-routes.ts +1 -14
- package/src/runtime/routes/conversation-query-routes.ts +49 -3
- package/src/runtime/routes/conversation-routes.ts +270 -44
- package/src/runtime/routes/group-routes.ts +22 -8
- package/src/runtime/routes/heartbeat-routes.ts +4 -10
- package/src/runtime/routes/identity-routes.ts +53 -18
- package/src/runtime/routes/llm-context-normalization.ts +14 -10
- 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 +41 -278
- package/src/runtime/routes/memory-item-routes.test.ts +168 -233
- package/src/runtime/routes/migration-routes.ts +18 -7
- package/src/runtime/routes/profiler-routes.ts +350 -0
- package/src/runtime/routes/schedule-routes.ts +27 -12
- package/src/runtime/routes/settings-routes.ts +95 -8
- package/src/runtime/routes/subagents-routes.ts +28 -7
- package/src/runtime/routes/user-route-dispatcher.ts +223 -0
- package/src/runtime/routes/user-routes.ts +41 -0
- package/src/runtime/routes/workspace-routes.ts +0 -1
- package/src/schedule/schedule-store.ts +30 -0
- package/src/schedule/scheduler.ts +45 -18
- package/src/skills/catalog-install.ts +10 -2
- package/src/skills/inline-command-runner.ts +12 -14
- package/src/skills/managed-store.ts +2 -2
- package/src/skills/skill-memory.ts +1 -293
- package/src/subagent/index.ts +13 -3
- package/src/subagent/manager.ts +308 -29
- package/src/subagent/types.ts +68 -0
- package/src/tasks/task-runner.ts +4 -4
- package/src/tools/apps/executors.ts +29 -4
- package/src/tools/filesystem/list.ts +93 -0
- package/src/tools/permission-checker.ts +78 -18
- package/src/tools/registry.ts +4 -0
- package/src/tools/schedule/create.ts +3 -0
- package/src/tools/schedule/list.ts +1 -0
- package/src/tools/schedule/update.ts +6 -0
- package/src/tools/secret-detection-handler.ts +0 -1
- package/src/tools/shared/filesystem/errors.ts +5 -0
- package/src/tools/shared/filesystem/file-ops-service.ts +90 -2
- package/src/tools/shared/filesystem/types.ts +17 -0
- package/src/tools/shared/shell-output.ts +31 -2
- package/src/tools/skills/sandbox-runner.ts +3 -6
- package/src/tools/subagent/abort.ts +12 -2
- package/src/tools/subagent/message.ts +9 -2
- package/src/tools/subagent/notify-parent.ts +79 -0
- package/src/tools/subagent/read.ts +29 -8
- package/src/tools/subagent/resolve.ts +21 -0
- package/src/tools/subagent/spawn.ts +2 -0
- package/src/tools/subagent/status.ts +11 -1
- package/src/tools/system/avatar-generator.ts +3 -3
- package/src/tools/system/register.ts +23 -0
- package/src/tools/system/set-permission-mode.ts +103 -0
- package/src/tools/terminal/parser.ts +30 -5
- package/src/tools/terminal/safe-env.ts +16 -1
- package/src/tools/terminal/sandbox-diagnostics.ts +4 -4
- package/src/tools/terminal/sandbox.ts +4 -1
- package/src/tools/terminal/shell.ts +3 -5
- package/src/tools/tool-manifest.ts +6 -0
- package/src/tools/types.ts +2 -3
- package/src/util/logger.ts +1 -1
- package/src/util/platform.ts +50 -17
- package/src/watcher/provider-types.ts +1 -1
- package/src/workspace/migrations/023-move-config-files-to-workspace.ts +2 -2
- package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +2 -2
- package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +270 -0
- package/src/workspace/migrations/029-seed-pkb.ts +85 -0
- package/src/workspace/migrations/030-seed-pkb-autoinject.ts +73 -0
- package/src/workspace/migrations/registry.ts +6 -0
- package/src/workspace/top-level-renderer.ts +5 -9
- package/src/__tests__/cli-memory.test.ts +0 -377
- package/src/__tests__/clipboard.test.ts +0 -88
- package/src/cli/cli-memory.ts +0 -179
- package/src/util/clipboard.ts +0 -34
|
@@ -736,24 +736,24 @@ describe("invalidateAssistantInferredItemsForConversation", () => {
|
|
|
736
736
|
expect(isConversationFailed(otherConvId)).toBe(false);
|
|
737
737
|
});
|
|
738
738
|
|
|
739
|
-
test("cancels pending
|
|
739
|
+
test("cancels pending graph_extract jobs for the failed conversation", () => {
|
|
740
740
|
seedConversations();
|
|
741
741
|
seedMessages();
|
|
742
742
|
|
|
743
743
|
const db = getDb();
|
|
744
744
|
|
|
745
|
-
// Enqueue
|
|
746
|
-
enqueueMemoryJob("
|
|
747
|
-
|
|
745
|
+
// Enqueue graph_extract jobs for the target conversation
|
|
746
|
+
enqueueMemoryJob("graph_extract", {
|
|
747
|
+
conversationId: convId,
|
|
748
748
|
scopeId: "default",
|
|
749
749
|
});
|
|
750
|
-
enqueueMemoryJob("
|
|
751
|
-
|
|
750
|
+
enqueueMemoryJob("graph_extract", {
|
|
751
|
+
conversationId: convId,
|
|
752
752
|
scopeId: "default",
|
|
753
753
|
});
|
|
754
|
-
// Enqueue
|
|
755
|
-
enqueueMemoryJob("
|
|
756
|
-
|
|
754
|
+
// Enqueue a graph_extract job for a different conversation
|
|
755
|
+
enqueueMemoryJob("graph_extract", {
|
|
756
|
+
conversationId: otherConvId,
|
|
757
757
|
scopeId: "default",
|
|
758
758
|
});
|
|
759
759
|
|
|
@@ -761,7 +761,7 @@ describe("invalidateAssistantInferredItemsForConversation", () => {
|
|
|
761
761
|
const pendingBefore = db
|
|
762
762
|
.select()
|
|
763
763
|
.from(memoryJobs)
|
|
764
|
-
.where(eq(memoryJobs.type, "
|
|
764
|
+
.where(eq(memoryJobs.type, "graph_extract"))
|
|
765
765
|
.all();
|
|
766
766
|
expect(pendingBefore.filter((j) => j.status === "pending")).toHaveLength(3);
|
|
767
767
|
|
|
@@ -771,7 +771,7 @@ describe("invalidateAssistantInferredItemsForConversation", () => {
|
|
|
771
771
|
const allJobs = db
|
|
772
772
|
.select()
|
|
773
773
|
.from(memoryJobs)
|
|
774
|
-
.where(eq(memoryJobs.type, "
|
|
774
|
+
.where(eq(memoryJobs.type, "graph_extract"))
|
|
775
775
|
.all();
|
|
776
776
|
const failedJobs = allJobs.filter((j) => j.status === "failed");
|
|
777
777
|
const pendingJobs = allJobs.filter((j) => j.status === "pending");
|
|
@@ -785,6 +785,6 @@ describe("invalidateAssistantInferredItemsForConversation", () => {
|
|
|
785
785
|
// The job for the other conversation should remain pending
|
|
786
786
|
expect(pendingJobs).toHaveLength(1);
|
|
787
787
|
const payload = JSON.parse(pendingJobs[0].payload);
|
|
788
|
-
expect(payload.
|
|
788
|
+
expect(payload.conversationId).toBe(otherConvId);
|
|
789
789
|
});
|
|
790
790
|
});
|
|
@@ -2,7 +2,7 @@ import * as realChildProcess from "node:child_process";
|
|
|
2
2
|
import * as realFs from "node:fs";
|
|
3
3
|
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
4
4
|
|
|
5
|
-
import type { SandboxConfig } from "../
|
|
5
|
+
import type { SandboxConfig } from "../tools/terminal/sandbox.js";
|
|
6
6
|
|
|
7
7
|
let platform = "linux";
|
|
8
8
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
1
2
|
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
3
|
|
|
3
4
|
import type { ShellOutputResult } from "../tools/shared/shell-output.js";
|
|
@@ -64,9 +65,13 @@ mock.module("../tools/network/script-proxy/index.js", () => ({
|
|
|
64
65
|
|
|
65
66
|
// ── Imports (after mocks) ───────────────────────────────────────────────────
|
|
66
67
|
|
|
67
|
-
import type { SandboxConfig } from "../config/schema.js";
|
|
68
68
|
import { parse } from "../tools/terminal/parser.js";
|
|
69
|
-
import {
|
|
69
|
+
import {
|
|
70
|
+
ALWAYS_INJECTED_ENV_VARS,
|
|
71
|
+
buildSanitizedEnv,
|
|
72
|
+
SAFE_ENV_VARS,
|
|
73
|
+
} from "../tools/terminal/safe-env.js";
|
|
74
|
+
import type { SandboxConfig } from "../tools/terminal/sandbox.js";
|
|
70
75
|
import { wrapCommand } from "../tools/terminal/sandbox.js";
|
|
71
76
|
import { ToolError } from "../util/errors.js";
|
|
72
77
|
|
|
@@ -450,30 +455,7 @@ describe("buildSanitizedEnv", () => {
|
|
|
450
455
|
test("result is a plain object with no prototype-inherited secrets", () => {
|
|
451
456
|
const env = buildSanitizedEnv();
|
|
452
457
|
const keys = Object.keys(env);
|
|
453
|
-
const safeKeys = [
|
|
454
|
-
"PATH",
|
|
455
|
-
"HOME",
|
|
456
|
-
"TERM",
|
|
457
|
-
"LANG",
|
|
458
|
-
"EDITOR",
|
|
459
|
-
"SHELL",
|
|
460
|
-
"USER",
|
|
461
|
-
"TMPDIR",
|
|
462
|
-
"LC_ALL",
|
|
463
|
-
"LC_CTYPE",
|
|
464
|
-
"XDG_RUNTIME_DIR",
|
|
465
|
-
"DISPLAY",
|
|
466
|
-
"COLORTERM",
|
|
467
|
-
"TERM_PROGRAM",
|
|
468
|
-
"SSH_AUTH_SOCK",
|
|
469
|
-
"SSH_AGENT_PID",
|
|
470
|
-
"GPG_TTY",
|
|
471
|
-
"GNUPGHOME",
|
|
472
|
-
"VELLUM_DEV",
|
|
473
|
-
"INTERNAL_GATEWAY_BASE_URL",
|
|
474
|
-
"VELLUM_DATA_DIR",
|
|
475
|
-
"VELLUM_WORKSPACE_DIR",
|
|
476
|
-
];
|
|
458
|
+
const safeKeys: string[] = [...SAFE_ENV_VARS, ...ALWAYS_INJECTED_ENV_VARS];
|
|
477
459
|
for (const key of keys) {
|
|
478
460
|
expect(safeKeys).toContain(key);
|
|
479
461
|
}
|
|
@@ -722,9 +704,14 @@ describe("formatShellOutput", () => {
|
|
|
722
704
|
});
|
|
723
705
|
|
|
724
706
|
test("truncates very long output", () => {
|
|
725
|
-
const longOutput = "x".repeat(
|
|
707
|
+
const longOutput = "x".repeat(30_000);
|
|
726
708
|
const result = formatShellOutput(longOutput, "", 0, false, 120);
|
|
727
|
-
expect(result.content).toContain('
|
|
728
|
-
|
|
709
|
+
expect(result.content).toContain('limit="20K"');
|
|
710
|
+
// Extract the file="..." attribute from the truncation tag
|
|
711
|
+
const fileMatch = result.content.match(/file="([^"]+)"/);
|
|
712
|
+
expect(fileMatch).not.toBeNull();
|
|
713
|
+
const filePath = fileMatch![1];
|
|
714
|
+
expect(existsSync(filePath)).toBe(true);
|
|
715
|
+
expect(readFileSync(filePath, "utf-8")).toBe(longOutput);
|
|
729
716
|
});
|
|
730
717
|
});
|
|
@@ -22,10 +22,28 @@ const testDir = realpathSync(
|
|
|
22
22
|
mkdtempSync(join(tmpdir(), "vellum-test-workspace-")),
|
|
23
23
|
);
|
|
24
24
|
process.env.VELLUM_WORKSPACE_DIR = testDir;
|
|
25
|
+
process.env.VELLUM_PLATFORM_URL = "https://test-platform.vellum.ai";
|
|
26
|
+
process.exitCode = 0;
|
|
27
|
+
|
|
28
|
+
// Prevent tests from routing credential writes through the real CES
|
|
29
|
+
// (Credential Execution Service). Without this, setSecureKeyAsync() in
|
|
30
|
+
// containerized environments writes to the live credential store.
|
|
31
|
+
const savedIsContainerized = process.env.IS_CONTAINERIZED;
|
|
32
|
+
const savedCesCredentialUrl = process.env.CES_CREDENTIAL_URL;
|
|
33
|
+
delete process.env.IS_CONTAINERIZED;
|
|
34
|
+
delete process.env.CES_CREDENTIAL_URL;
|
|
25
35
|
|
|
26
36
|
afterAll(() => {
|
|
27
37
|
resetDb();
|
|
38
|
+
process.exitCode = 0;
|
|
28
39
|
delete process.env.VELLUM_WORKSPACE_DIR;
|
|
40
|
+
delete process.env.VELLUM_PLATFORM_URL;
|
|
41
|
+
if (savedIsContainerized !== undefined) {
|
|
42
|
+
process.env.IS_CONTAINERIZED = savedIsContainerized;
|
|
43
|
+
}
|
|
44
|
+
if (savedCesCredentialUrl !== undefined) {
|
|
45
|
+
process.env.CES_CREDENTIAL_URL = savedCesCredentialUrl;
|
|
46
|
+
}
|
|
29
47
|
try {
|
|
30
48
|
rmSync(testDir, { recursive: true, force: true });
|
|
31
49
|
} catch {
|
|
@@ -41,7 +41,6 @@ let checkerDecision: "allow" | "prompt" | "deny" = "allow";
|
|
|
41
41
|
let checkerReason = "allowed";
|
|
42
42
|
let checkerRisk = "low";
|
|
43
43
|
let promptDecision: "allow" | "always_allow" | "deny" | "always_deny" = "allow";
|
|
44
|
-
let sandboxed = false;
|
|
45
44
|
let fakeToolResult: ToolExecutionResult = { content: "ok", isError: false };
|
|
46
45
|
let toolThrow: Error | null = null;
|
|
47
46
|
|
|
@@ -151,7 +150,7 @@ mock.module("../tools/shared/filesystem/path-policy.js", () => ({
|
|
|
151
150
|
}));
|
|
152
151
|
|
|
153
152
|
mock.module("../tools/terminal/sandbox.js", () => ({
|
|
154
|
-
wrapCommand: () => ({ command: "", sandboxed }),
|
|
153
|
+
wrapCommand: () => ({ command: "", sandboxed: false }),
|
|
155
154
|
}));
|
|
156
155
|
|
|
157
156
|
import { PermissionPrompter } from "../permissions/prompter.js";
|
|
@@ -193,7 +192,6 @@ describe("ToolExecutor lifecycle events", () => {
|
|
|
193
192
|
checkerReason = "allowed";
|
|
194
193
|
checkerRisk = "low";
|
|
195
194
|
promptDecision = "allow";
|
|
196
|
-
sandboxed = false;
|
|
197
195
|
fakeToolResult = { content: "ok", isError: false };
|
|
198
196
|
toolThrow = null;
|
|
199
197
|
});
|
|
@@ -231,7 +229,6 @@ describe("ToolExecutor lifecycle events", () => {
|
|
|
231
229
|
checkerReason = "medium risk: requires approval";
|
|
232
230
|
checkerRisk = "medium";
|
|
233
231
|
promptDecision = "deny";
|
|
234
|
-
sandboxed = true;
|
|
235
232
|
|
|
236
233
|
const events: ToolLifecycleEvent[] = [];
|
|
237
234
|
const executor = new ToolExecutor(makePrompter());
|
|
@@ -256,7 +253,6 @@ describe("ToolExecutor lifecycle events", () => {
|
|
|
256
253
|
expect(promptEvent.executionTarget).toBe("sandbox");
|
|
257
254
|
expect(promptEvent.riskLevel).toBe("medium");
|
|
258
255
|
expect(promptEvent.reason).toBe("medium risk: requires approval");
|
|
259
|
-
expect(promptEvent.sandboxed).toBe(true);
|
|
260
256
|
expect(promptEvent.allowlistOptions).toEqual([
|
|
261
257
|
{ label: "exact", description: "exact", pattern: "exact" },
|
|
262
258
|
]);
|
|
@@ -276,7 +272,6 @@ describe("ToolExecutor lifecycle events", () => {
|
|
|
276
272
|
checkerDecision = "prompt";
|
|
277
273
|
checkerReason = "guardrail prompt";
|
|
278
274
|
checkerRisk = "high";
|
|
279
|
-
sandboxed = true;
|
|
280
275
|
|
|
281
276
|
const events: ToolLifecycleEvent[] = [];
|
|
282
277
|
const executor = new ToolExecutor(
|
|
@@ -580,7 +575,6 @@ describe("ToolExecutor lifecycle events", () => {
|
|
|
580
575
|
checkerReason = "Matched trust rule";
|
|
581
576
|
checkerRisk = "low";
|
|
582
577
|
promptDecision = "allow";
|
|
583
|
-
sandboxed = true;
|
|
584
578
|
|
|
585
579
|
const events: ToolLifecycleEvent[] = [];
|
|
586
580
|
const executor = new ToolExecutor(makePrompter());
|
|
@@ -602,7 +596,6 @@ describe("ToolExecutor lifecycle events", () => {
|
|
|
602
596
|
expect(promptEvent.reason).toBe(
|
|
603
597
|
"Private conversation: side-effect tools require explicit approval",
|
|
604
598
|
);
|
|
605
|
-
expect(promptEvent.sandboxed).toBe(true);
|
|
606
599
|
});
|
|
607
600
|
|
|
608
601
|
test("no permission_prompt event for read-only tool even with forcePromptSideEffects", async () => {
|
|
@@ -1949,7 +1949,6 @@ describe("ToolExecutor persistentDecisionsAllowed contract", () => {
|
|
|
1949
1949
|
_allowlistOptions: AllowlistOption[],
|
|
1950
1950
|
_scopeOptions: ScopeOption[],
|
|
1951
1951
|
_diff: unknown,
|
|
1952
|
-
_sandboxed: unknown,
|
|
1953
1952
|
_conversationId: unknown,
|
|
1954
1953
|
_executionTarget: unknown,
|
|
1955
1954
|
persistentDecisionsAllowed: boolean | undefined,
|
|
@@ -2072,7 +2071,8 @@ describe("ToolExecutor persistentDecisionsAllowed contract", () => {
|
|
|
2072
2071
|
// ---------------------------------------------------------------------------
|
|
2073
2072
|
|
|
2074
2073
|
// Import the real buildSanitizedEnv (not mocked) for baseline credential tests
|
|
2075
|
-
const { buildSanitizedEnv } =
|
|
2074
|
+
const { buildSanitizedEnv, SAFE_ENV_VARS, ALWAYS_INJECTED_ENV_VARS } =
|
|
2075
|
+
await import("../tools/terminal/safe-env.js");
|
|
2076
2076
|
|
|
2077
2077
|
describe("buildSanitizedEnv — baseline: credential exclusion", () => {
|
|
2078
2078
|
// Credential-like env vars that must never appear in the sanitized env.
|
|
@@ -2130,33 +2130,10 @@ describe("buildSanitizedEnv — baseline: credential exclusion", () => {
|
|
|
2130
2130
|
});
|
|
2131
2131
|
|
|
2132
2132
|
test("sanitized env only contains keys from the allowlist", () => {
|
|
2133
|
-
const
|
|
2134
|
-
"PATH",
|
|
2135
|
-
"HOME",
|
|
2136
|
-
"TERM",
|
|
2137
|
-
"LANG",
|
|
2138
|
-
"EDITOR",
|
|
2139
|
-
"SHELL",
|
|
2140
|
-
"USER",
|
|
2141
|
-
"TMPDIR",
|
|
2142
|
-
"LC_ALL",
|
|
2143
|
-
"LC_CTYPE",
|
|
2144
|
-
"XDG_RUNTIME_DIR",
|
|
2145
|
-
"DISPLAY",
|
|
2146
|
-
"COLORTERM",
|
|
2147
|
-
"TERM_PROGRAM",
|
|
2148
|
-
"SSH_AUTH_SOCK",
|
|
2149
|
-
"SSH_AGENT_PID",
|
|
2150
|
-
"GPG_TTY",
|
|
2151
|
-
"GNUPGHOME",
|
|
2152
|
-
"INTERNAL_GATEWAY_BASE_URL",
|
|
2153
|
-
"VELLUM_DATA_DIR",
|
|
2154
|
-
"VELLUM_WORKSPACE_DIR",
|
|
2155
|
-
];
|
|
2156
|
-
|
|
2133
|
+
const allowed: string[] = [...SAFE_ENV_VARS, ...ALWAYS_INJECTED_ENV_VARS];
|
|
2157
2134
|
const env = buildSanitizedEnv();
|
|
2158
2135
|
for (const key of Object.keys(env)) {
|
|
2159
|
-
expect(
|
|
2136
|
+
expect(allowed).toContain(key);
|
|
2160
2137
|
}
|
|
2161
2138
|
});
|
|
2162
2139
|
});
|
|
@@ -31,6 +31,7 @@ mock.module("../memory/app-store.js", () => ({
|
|
|
31
31
|
getApp: mock(() => null),
|
|
32
32
|
getAppDirPath: mock(() => ""),
|
|
33
33
|
isMultifileApp: mock(() => false),
|
|
34
|
+
resolveAppIdFromPath: mock(() => null),
|
|
34
35
|
}));
|
|
35
36
|
mock.module("../services/published-app-updater.js", () => ({
|
|
36
37
|
updatePublishedAppDeployment: mock(() => Promise.resolve()),
|
|
@@ -15,11 +15,11 @@ describe("renderWorkspaceTopLevelContext", () => {
|
|
|
15
15
|
const result = renderWorkspaceTopLevelContext(snapshot);
|
|
16
16
|
expect(result).toBe(
|
|
17
17
|
[
|
|
18
|
-
"<
|
|
18
|
+
"<workspace>",
|
|
19
19
|
"Root: /sandbox",
|
|
20
20
|
"Directories: lib, src, tests",
|
|
21
21
|
"Files: README.md, package.json",
|
|
22
|
-
"</
|
|
22
|
+
"</workspace>",
|
|
23
23
|
].join("\n"),
|
|
24
24
|
);
|
|
25
25
|
});
|
|
@@ -61,11 +61,11 @@ describe("renderWorkspaceTopLevelContext", () => {
|
|
|
61
61
|
const result = renderWorkspaceTopLevelContext(snapshot);
|
|
62
62
|
expect(result).toBe(
|
|
63
63
|
[
|
|
64
|
-
"<
|
|
64
|
+
"<workspace>",
|
|
65
65
|
"Root: /empty",
|
|
66
66
|
"Directories: ",
|
|
67
67
|
"Files: ",
|
|
68
|
-
"</
|
|
68
|
+
"</workspace>",
|
|
69
69
|
].join("\n"),
|
|
70
70
|
);
|
|
71
71
|
});
|
|
@@ -92,8 +92,8 @@ describe("renderWorkspaceTopLevelContext", () => {
|
|
|
92
92
|
};
|
|
93
93
|
|
|
94
94
|
const result = renderWorkspaceTopLevelContext(snapshot);
|
|
95
|
-
expect(result.startsWith("<
|
|
96
|
-
expect(result.endsWith("</
|
|
95
|
+
expect(result.startsWith("<workspace>")).toBe(true);
|
|
96
|
+
expect(result.endsWith("</workspace>")).toBe(true);
|
|
97
97
|
});
|
|
98
98
|
|
|
99
99
|
test("includes hidden directories", () => {
|
|
@@ -124,7 +124,7 @@ describe("renderWorkspaceTopLevelContext", () => {
|
|
|
124
124
|
expect(result).toContain("Files: a.txt, b.txt");
|
|
125
125
|
});
|
|
126
126
|
|
|
127
|
-
test("renders
|
|
127
|
+
test("renders attachment path when provided", () => {
|
|
128
128
|
const snapshot: TopLevelSnapshot = {
|
|
129
129
|
rootPath: "/sandbox",
|
|
130
130
|
directories: ["src"],
|
|
@@ -133,16 +133,13 @@ describe("renderWorkspaceTopLevelContext", () => {
|
|
|
133
133
|
};
|
|
134
134
|
|
|
135
135
|
const result = renderWorkspaceTopLevelContext(snapshot, {
|
|
136
|
-
|
|
137
|
-
currentConversationAttachmentsPath:
|
|
136
|
+
conversationAttachmentsPath:
|
|
138
137
|
"conversations/2026-03-19T12-00-00.000Z_conv-1/attachments/",
|
|
139
138
|
});
|
|
140
139
|
|
|
141
140
|
expect(result).toContain(
|
|
142
|
-
"Current conversation
|
|
143
|
-
);
|
|
144
|
-
expect(result).toContain(
|
|
145
|
-
"Attachment files: conversations/2026-03-19T12-00-00.000Z_conv-1/attachments/",
|
|
141
|
+
"Current conversation attachments: conversations/2026-03-19T12-00-00.000Z_conv-1/attachments/",
|
|
146
142
|
);
|
|
143
|
+
expect(result).not.toContain("Current conversation folder");
|
|
147
144
|
});
|
|
148
145
|
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
MacosTransportMetadata,
|
|
5
|
+
NonMacosTransportMetadata,
|
|
6
|
+
} from "../daemon/message-types/conversations.js";
|
|
7
|
+
import { buildTransportHints } from "../daemon/transport-hints.js";
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// buildTransportHints
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
describe("buildTransportHints", () => {
|
|
14
|
+
test("produces correct hints for macOS transport", () => {
|
|
15
|
+
const transport: MacosTransportMetadata = {
|
|
16
|
+
channelId: "vellum",
|
|
17
|
+
interfaceId: "macos",
|
|
18
|
+
hostHomeDir: "/Users/alice",
|
|
19
|
+
hostUsername: "alice",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const hints = buildTransportHints(transport);
|
|
23
|
+
|
|
24
|
+
expect(hints).toContain("User is messaging from interface: macos");
|
|
25
|
+
expect(hints).toContain("Host home directory: /Users/alice");
|
|
26
|
+
expect(hints).toContain("Host username: alice");
|
|
27
|
+
expect(hints).toHaveLength(3);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("produces correct hints for non-macOS transport", () => {
|
|
31
|
+
const transport: NonMacosTransportMetadata = {
|
|
32
|
+
channelId: "vellum",
|
|
33
|
+
interfaceId: "ios",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const hints = buildTransportHints(transport);
|
|
37
|
+
|
|
38
|
+
expect(hints).toContain("User is messaging from interface: ios");
|
|
39
|
+
expect(hints).toHaveLength(1);
|
|
40
|
+
// Should not include host environment hints
|
|
41
|
+
expect(hints.some((h) => h.includes("Host home directory"))).toBe(false);
|
|
42
|
+
expect(hints.some((h) => h.includes("Host username"))).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("includes client-provided hints", () => {
|
|
46
|
+
const transport: MacosTransportMetadata = {
|
|
47
|
+
channelId: "vellum",
|
|
48
|
+
interfaceId: "macos",
|
|
49
|
+
hostHomeDir: "/Users/bob",
|
|
50
|
+
hostUsername: "bob",
|
|
51
|
+
hints: ["custom hint"],
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const hints = buildTransportHints(transport);
|
|
55
|
+
|
|
56
|
+
expect(hints).toContain("User is messaging from interface: macos");
|
|
57
|
+
expect(hints).toContain("Host home directory: /Users/bob");
|
|
58
|
+
expect(hints).toContain("Host username: bob");
|
|
59
|
+
expect(hints).toContain("custom hint");
|
|
60
|
+
expect(hints).toHaveLength(4);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("handles missing optional fields on macOS transport", () => {
|
|
64
|
+
const transport: MacosTransportMetadata = {
|
|
65
|
+
channelId: "vellum",
|
|
66
|
+
interfaceId: "macos",
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const hints = buildTransportHints(transport);
|
|
70
|
+
|
|
71
|
+
expect(hints).toContain("User is messaging from interface: macos");
|
|
72
|
+
// Without hostHomeDir and hostUsername, only the interface hint is present
|
|
73
|
+
expect(hints).toHaveLength(1);
|
|
74
|
+
expect(hints.some((h) => h.includes("Host home directory"))).toBe(false);
|
|
75
|
+
expect(hints.some((h) => h.includes("Host username"))).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -909,8 +909,8 @@ describe("Trust Store", () => {
|
|
|
909
909
|
expect(match!.id).toBe("default:allow-bash-rm-bootstrap");
|
|
910
910
|
expect(match!.decision).toBe("allow");
|
|
911
911
|
expect(match!.allowHighRisk).toBe(true);
|
|
912
|
-
// Outside workspace, the bootstrap rule doesn't match —
|
|
913
|
-
//
|
|
912
|
+
// Outside workspace, the bootstrap rule doesn't match — without
|
|
913
|
+
// IS_CONTAINERIZED there is no catch-all bash allow rule either.
|
|
914
914
|
const other = findHighestPriorityRule(
|
|
915
915
|
"bash",
|
|
916
916
|
["rm BOOTSTRAP.md"],
|
|
@@ -930,8 +930,8 @@ describe("Trust Store", () => {
|
|
|
930
930
|
expect(match!.id).toBe("default:allow-bash-rm-updates");
|
|
931
931
|
expect(match!.decision).toBe("allow");
|
|
932
932
|
expect(match!.allowHighRisk).toBe(true);
|
|
933
|
-
// Outside workspace, should NOT match the updates rule —
|
|
934
|
-
//
|
|
933
|
+
// Outside workspace, should NOT match the updates rule — without
|
|
934
|
+
// IS_CONTAINERIZED there is no catch-all bash allow rule either.
|
|
935
935
|
const other = findHighestPriorityRule(
|
|
936
936
|
"bash",
|
|
937
937
|
["rm UPDATES.md"],
|
|
@@ -133,7 +133,7 @@ describe("trusted contact lifecycle notification signals", () => {
|
|
|
133
133
|
resetState();
|
|
134
134
|
});
|
|
135
135
|
|
|
136
|
-
test("guardian deny emits guardian_decision and denied signals", async () => {
|
|
136
|
+
test("guardian deny emits guardian_decision and denied signals with display names", async () => {
|
|
137
137
|
// Set up guardian binding and member record (guardians must pass ACL)
|
|
138
138
|
createGuardianBinding({
|
|
139
139
|
channel: "telegram",
|
|
@@ -148,6 +148,17 @@ describe("trusted contact lifecycle notification signals", () => {
|
|
|
148
148
|
externalChatId: "guardian-chat-789",
|
|
149
149
|
status: "active",
|
|
150
150
|
policy: "allow",
|
|
151
|
+
displayName: "Guardian Bob",
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Set up requester contact with a display name so payloads are enriched
|
|
155
|
+
upsertContactChannel({
|
|
156
|
+
sourceChannel: "telegram",
|
|
157
|
+
externalUserId: "requester-user-456",
|
|
158
|
+
externalChatId: "requester-chat-456",
|
|
159
|
+
status: "pending",
|
|
160
|
+
policy: "ask",
|
|
161
|
+
displayName: "Alice Requester",
|
|
151
162
|
});
|
|
152
163
|
|
|
153
164
|
const testRequestId = `req-deny-${Date.now()}`;
|
|
@@ -199,11 +210,15 @@ describe("trusted contact lifecycle notification signals", () => {
|
|
|
199
210
|
expect(gdPayload.decision).toBe("denied");
|
|
200
211
|
expect(gdPayload.requesterExternalUserId).toBe("requester-user-456");
|
|
201
212
|
expect(gdPayload.decidedByExternalUserId).toBe("guardian-user-789");
|
|
213
|
+
expect(gdPayload.requesterDisplayName).toBe("Alice Requester");
|
|
214
|
+
expect(gdPayload.decidedByDisplayName).toBe("Guardian Bob");
|
|
202
215
|
|
|
203
216
|
// Verify denied payload
|
|
204
217
|
const dPayload = deniedSignals[0].contextPayload as Record<string, unknown>;
|
|
205
218
|
expect(dPayload.decision).toBe("denied");
|
|
206
219
|
expect(dPayload.requesterExternalUserId).toBe("requester-user-456");
|
|
220
|
+
expect(dPayload.requesterDisplayName).toBe("Alice Requester");
|
|
221
|
+
expect(dPayload.decidedByDisplayName).toBe("Guardian Bob");
|
|
207
222
|
|
|
208
223
|
// Verify deduplication keys are distinct
|
|
209
224
|
expect(guardianDecisionSignals[0].dedupeKey).toContain(
|
|
@@ -212,7 +227,7 @@ describe("trusted contact lifecycle notification signals", () => {
|
|
|
212
227
|
expect(deniedSignals[0].dedupeKey).toContain("trusted-contact:denied:");
|
|
213
228
|
});
|
|
214
229
|
|
|
215
|
-
test("guardian approve emits guardian_decision and verification_sent signals", async () => {
|
|
230
|
+
test("guardian approve emits guardian_decision and verification_sent signals with display names", async () => {
|
|
216
231
|
// Set up guardian binding and member record (guardians must pass ACL)
|
|
217
232
|
createGuardianBinding({
|
|
218
233
|
channel: "telegram",
|
|
@@ -227,6 +242,17 @@ describe("trusted contact lifecycle notification signals", () => {
|
|
|
227
242
|
externalChatId: "guardian-chat-789",
|
|
228
243
|
status: "active",
|
|
229
244
|
policy: "allow",
|
|
245
|
+
displayName: "Guardian Bob",
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Set up requester contact with a display name
|
|
249
|
+
upsertContactChannel({
|
|
250
|
+
sourceChannel: "telegram",
|
|
251
|
+
externalUserId: "requester-user-456",
|
|
252
|
+
externalChatId: "requester-chat-456",
|
|
253
|
+
status: "pending",
|
|
254
|
+
policy: "ask",
|
|
255
|
+
displayName: "Alice Requester",
|
|
230
256
|
});
|
|
231
257
|
|
|
232
258
|
const testRequestId = `req-approve-${Date.now()}`;
|
|
@@ -276,6 +302,8 @@ describe("trusted contact lifecycle notification signals", () => {
|
|
|
276
302
|
const vsPayload = vsSignal.contextPayload as Record<string, unknown>;
|
|
277
303
|
expect(vsPayload.requesterExternalUserId).toBe("requester-user-456");
|
|
278
304
|
expect(vsPayload.verificationSessionId).toBeDefined();
|
|
305
|
+
expect(vsPayload.requesterDisplayName).toBe("Alice Requester");
|
|
306
|
+
expect(vsPayload.decidedByDisplayName).toBe("Guardian Bob");
|
|
279
307
|
expect(
|
|
280
308
|
(vsSignal.attentionHints as Record<string, unknown>).visibleInSourceNow,
|
|
281
309
|
).toBe(true);
|
|
@@ -340,6 +368,92 @@ describe("trusted contact lifecycle notification signals", () => {
|
|
|
340
368
|
// guardian_decision and denied — both keyed on approval.id
|
|
341
369
|
expect(signals.length).toBe(2);
|
|
342
370
|
});
|
|
371
|
+
|
|
372
|
+
test("display name fields fall back to null when contacts have no display name", async () => {
|
|
373
|
+
// Set up guardian binding — createGuardianBinding creates a contact with
|
|
374
|
+
// displayName defaulting to the externalUserId. We need to verify the
|
|
375
|
+
// null-fallback path for display name resolution, so we use a guardian
|
|
376
|
+
// whose external user ID does NOT have a matching contact_channels row.
|
|
377
|
+
//
|
|
378
|
+
// Strategy: create the guardian binding normally (so the sender is
|
|
379
|
+
// classified as trustClass="guardian"), then use a DIFFERENT external
|
|
380
|
+
// user ID in the approval request's guardianExternalUserId field. The
|
|
381
|
+
// decidedByExternalUserId comes from actorExternalId (the real guardian),
|
|
382
|
+
// which DOES have a contact. To truly get null, we clear the guardian
|
|
383
|
+
// contact's displayName to empty string after creation — the signal
|
|
384
|
+
// enrichment code treats empty string the same as null for display
|
|
385
|
+
// purposes.
|
|
386
|
+
createGuardianBinding({
|
|
387
|
+
channel: "telegram",
|
|
388
|
+
guardianExternalUserId: "guardian-noname-111",
|
|
389
|
+
guardianDeliveryChatId: "guardian-chat-111",
|
|
390
|
+
guardianPrincipalId: "guardian-noname-111",
|
|
391
|
+
verifiedVia: "test",
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
// Clear the guardian contact's displayName to empty string so the
|
|
395
|
+
// display name resolution returns a falsy value. createGuardianBinding
|
|
396
|
+
// defaults displayName to the externalUserId, which would be a non-empty
|
|
397
|
+
// string and defeat the purpose of this test.
|
|
398
|
+
const db = getDb();
|
|
399
|
+
db.run("UPDATE contacts SET display_name = '' WHERE role = 'guardian'");
|
|
400
|
+
|
|
401
|
+
// Do NOT create a requester contact — display name should resolve to null
|
|
402
|
+
|
|
403
|
+
const testRequestId = `req-noname-${Date.now()}`;
|
|
404
|
+
|
|
405
|
+
createApprovalRequest({
|
|
406
|
+
runId: `ingress-access-request-${Date.now()}`,
|
|
407
|
+
requestId: testRequestId,
|
|
408
|
+
conversationId: "access-req-telegram-requester-noname-222",
|
|
409
|
+
channel: "telegram",
|
|
410
|
+
requesterExternalUserId: "requester-noname-222",
|
|
411
|
+
requesterChatId: "requester-chat-222",
|
|
412
|
+
guardianExternalUserId: "guardian-noname-111",
|
|
413
|
+
guardianChatId: "guardian-chat-111",
|
|
414
|
+
toolName: "ingress_access_request",
|
|
415
|
+
riskLevel: "access_request",
|
|
416
|
+
reason: "Unknown user requesting access",
|
|
417
|
+
expiresAt: Date.now() + GUARDIAN_APPROVAL_TTL_MS,
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// Guardian denies via callback button
|
|
421
|
+
const guardianReq = buildInboundRequest({
|
|
422
|
+
conversationExternalId: "guardian-chat-111",
|
|
423
|
+
actorExternalId: "guardian-noname-111",
|
|
424
|
+
actorDisplayName: "Guardian",
|
|
425
|
+
content: "",
|
|
426
|
+
callbackData: `apr:${testRequestId}:reject`,
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
await handleChannelInbound(guardianReq, undefined, TEST_BEARER_TOKEN);
|
|
430
|
+
|
|
431
|
+
const guardianDecisionSignals = emitSignalCalls.filter(
|
|
432
|
+
(c) => c.sourceEventName === "ingress.trusted_contact.guardian_decision",
|
|
433
|
+
);
|
|
434
|
+
const deniedSignals = emitSignalCalls.filter(
|
|
435
|
+
(c) => c.sourceEventName === "ingress.trusted_contact.denied",
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
expect(guardianDecisionSignals.length).toBe(1);
|
|
439
|
+
expect(deniedSignals.length).toBe(1);
|
|
440
|
+
|
|
441
|
+
// Verify display names fall back to null/empty when contacts have no
|
|
442
|
+
// display name set.
|
|
443
|
+
const gdPayload = guardianDecisionSignals[0].contextPayload as Record<
|
|
444
|
+
string,
|
|
445
|
+
unknown
|
|
446
|
+
>;
|
|
447
|
+
expect(gdPayload.requesterDisplayName).toBeNull();
|
|
448
|
+
// Guardian contact exists but displayName was cleared to empty string.
|
|
449
|
+
// The signal enrichment resolves displayName from the contact record,
|
|
450
|
+
// so decidedByDisplayName will be "" (empty string) rather than null.
|
|
451
|
+
expect(gdPayload.decidedByDisplayName).toBe("");
|
|
452
|
+
|
|
453
|
+
const dPayload = deniedSignals[0].contextPayload as Record<string, unknown>;
|
|
454
|
+
expect(dPayload.requesterDisplayName).toBeNull();
|
|
455
|
+
expect(dPayload.decidedByDisplayName).toBe("");
|
|
456
|
+
});
|
|
343
457
|
});
|
|
344
458
|
|
|
345
459
|
// ---------------------------------------------------------------------------
|