@vellumai/assistant 0.5.0 → 0.5.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/ARCHITECTURE.md +54 -54
- package/docs/architecture/integrations.md +62 -67
- package/docs/credential-execution-service.md +3 -3
- package/package.json +1 -1
- package/src/__tests__/agent-loop.test.ts +111 -0
- package/src/__tests__/always-loaded-tools-guard.test.ts +3 -4
- package/src/__tests__/app-builder-tool-scripts.test.ts +13 -151
- package/src/__tests__/app-dir-path-guard.test.ts +78 -0
- package/src/__tests__/app-executors.test.ts +1 -291
- package/src/__tests__/app-git-history.test.ts +4 -4
- package/src/__tests__/app-routes-csp.test.ts +1 -0
- package/src/__tests__/app-store-dir-names.test.ts +426 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +7 -9
- package/src/__tests__/attachments-store.test.ts +169 -21
- package/src/__tests__/attachments.test.ts +115 -1
- package/src/__tests__/btw-routes.test.ts +1 -0
- package/src/__tests__/canonical-guardian-store.test.ts +38 -0
- package/src/__tests__/channel-reply-delivery.test.ts +55 -0
- package/src/__tests__/checker.test.ts +54 -0
- package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
- package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
- package/src/__tests__/compaction.benchmark.test.ts +2 -1
- package/src/__tests__/config-schema-cmd.test.ts +68 -21
- package/src/__tests__/config-schema.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +149 -5
- package/src/__tests__/conversation-agent-loop.test.ts +290 -2
- package/src/__tests__/conversation-attachments.test.ts +17 -19
- package/src/__tests__/conversation-disk-view-integration.test.ts +277 -0
- package/src/__tests__/conversation-disk-view.test.ts +810 -0
- package/src/__tests__/conversation-error.test.ts +1 -1
- package/src/__tests__/conversation-fork-crud.test.ts +551 -0
- package/src/__tests__/conversation-fork-route.test.ts +386 -0
- package/src/__tests__/conversation-history-web-search.test.ts +1 -1
- package/src/__tests__/conversation-key-store-disk-view.test.ts +130 -0
- package/src/__tests__/conversation-media-retry.test.ts +8 -2
- package/src/__tests__/conversation-queue.test.ts +36 -1
- package/src/__tests__/conversation-routes-disk-view.test.ts +439 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +2 -2
- package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -7
- package/src/__tests__/conversation-runtime-assembly.test.ts +17 -2
- package/src/__tests__/conversation-skill-tools.test.ts +4 -9
- package/src/__tests__/conversation-slash-commands.test.ts +149 -0
- package/src/__tests__/conversation-store.test.ts +24 -21
- package/src/__tests__/conversation-surfaces-state-update.test.ts +246 -0
- package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -0
- package/src/__tests__/conversation-title-service.test.ts +137 -0
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +25 -315
- package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +1 -0
- package/src/__tests__/conversation-tool-setup-side-effect-flag.test.ts +1 -0
- package/src/__tests__/conversation-workspace-cache-state.test.ts +44 -2
- package/src/__tests__/conversation-workspace-injection.test.ts +11 -0
- package/src/__tests__/credential-execution-feature-gates.test.ts +3 -3
- package/src/__tests__/credential-security-invariants.test.ts +3 -0
- package/src/__tests__/credential-vault-unit.test.ts +5 -10
- package/src/__tests__/cu-unified-flow.test.ts +1 -0
- package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +241 -0
- package/src/__tests__/db-llm-request-log-provider-migration.test.ts +214 -0
- package/src/__tests__/diagnostics-export.test.ts +70 -1
- package/src/__tests__/filesystem-tools.test.ts +4 -2
- package/src/__tests__/first-greeting.test.ts +80 -0
- package/src/__tests__/gateway-only-guard.test.ts +1 -0
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +3 -7
- package/src/__tests__/history-repair.test.ts +103 -10
- package/src/__tests__/http-conversation-lineage.test.ts +251 -0
- package/src/__tests__/image-source-path-reinject.test.ts +136 -0
- package/src/__tests__/llm-context-normalization.test.ts +1116 -0
- package/src/__tests__/llm-context-route-provider.test.ts +217 -0
- package/src/__tests__/llm-request-log-turn-query.test.ts +270 -0
- package/src/__tests__/media-generate-image.test.ts +47 -94
- package/src/__tests__/memory-lifecycle-e2e.test.ts +3 -1
- package/src/__tests__/memory-recall-quality.test.ts +5 -5
- package/src/__tests__/migration-cross-version-compatibility.test.ts +4 -1
- package/src/__tests__/migration-export-http.test.ts +3 -1
- package/src/__tests__/migration-import-commit-http.test.ts +18 -4
- package/src/__tests__/migration-import-preflight-http.test.ts +1 -3
- package/src/__tests__/mime-builder.test.ts +3 -2
- package/src/__tests__/non-member-access-request.test.ts +12 -1
- package/src/__tests__/notification-decision-identity.test.ts +52 -0
- package/src/__tests__/oauth-apps-routes.test.ts +103 -0
- package/src/__tests__/oauth-store.test.ts +115 -0
- package/src/__tests__/provider-error-scenarios.test.ts +1 -3
- package/src/__tests__/provider-failover-actual-provider.test.ts +66 -0
- package/src/__tests__/recording-handler.test.ts +17 -0
- package/src/__tests__/registry.test.ts +3 -8
- package/src/__tests__/relay-server.test.ts +1 -1
- package/src/__tests__/runtime-attachment-metadata.test.ts +7 -3
- package/src/__tests__/schema-transforms.test.ts +165 -5
- package/src/__tests__/server-history-render.test.ts +2 -2
- package/src/__tests__/skill-feature-flags-integration.test.ts +18 -17
- package/src/__tests__/skill-feature-flags.test.ts +13 -13
- package/src/__tests__/skill-load-feature-flag.test.ts +4 -4
- package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
- package/src/__tests__/slack-inbound-verification.test.ts +2 -2
- package/src/__tests__/starter-task-flow.test.ts +1 -0
- package/src/__tests__/suggestion-routes.test.ts +443 -0
- package/src/__tests__/swarm-conversation-integration.test.ts +1 -0
- package/src/__tests__/swarm-recursion.test.ts +1 -0
- package/src/__tests__/swarm-tool.test.ts +1 -0
- package/src/__tests__/system-prompt.test.ts +8 -0
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -0
- package/src/__tests__/tool-preview-lifecycle.test.ts +32 -5
- package/src/__tests__/top-level-renderer.test.ts +22 -0
- package/src/__tests__/turn-boundary-resolution.test.ts +243 -0
- package/src/__tests__/web-fetch.test.ts +6 -2
- package/src/__tests__/workspace-migration-006-services-config.test.ts +335 -0
- package/src/__tests__/workspace-migration-007-web-search-provider-rename.test.ts +312 -0
- package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +278 -0
- package/src/__tests__/workspace-migration-010-app-dir-rename.test.ts +275 -0
- package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +77 -0
- package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +401 -0
- package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +328 -0
- package/src/__tests__/workspace-migration-seed-device-id.test.ts +6 -10
- package/src/agent/attachments.ts +27 -1
- package/src/agent/loop.ts +29 -1
- package/src/avatar/traits-png-sync.ts +80 -25
- package/src/bundler/app-bundler.ts +4 -4
- package/src/calls/call-domain.ts +1 -0
- package/src/calls/voice-session-bridge.ts +1 -0
- package/src/cli/commands/auth.ts +92 -0
- package/src/cli/commands/avatar.ts +7 -6
- package/src/cli/commands/config.ts +2 -0
- package/src/cli/commands/oauth/providers.ts +29 -0
- package/src/cli/program.ts +12 -0
- package/src/cli.ts +15 -48
- package/src/config/bundled-skills/app-builder/SKILL.md +103 -28
- package/src/config/bundled-skills/app-builder/TOOLS.json +5 -199
- package/src/config/bundled-skills/app-builder/tools/{app-query.ts → app-refresh.ts} +2 -2
- package/src/config/bundled-skills/contacts/tools/google-contacts.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +6 -9
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +4 -6
- package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +4 -6
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -3
- package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +2 -3
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
- package/src/config/bundled-skills/image-studio/SKILL.md +2 -2
- package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +45 -72
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/shared.ts +1 -1
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -3
- package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
- package/src/config/bundled-skills/slack/tools/shared.ts +19 -4
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +2 -3
- package/src/config/bundled-skills/transcribe/SKILL.md +1 -1
- package/src/config/bundled-skills/transcribe/TOOLS.json +2 -6
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +19 -83
- package/src/config/bundled-tool-registry.ts +2 -14
- package/src/config/feature-flag-registry.json +16 -0
- package/src/config/loader.ts +64 -0
- package/src/config/raw-config-utils.ts +30 -0
- package/src/config/schema-utils.ts +28 -7
- package/src/config/schema.ts +8 -0
- package/src/config/schemas/elevenlabs.ts +18 -0
- package/src/config/schemas/memory-lifecycle.ts +4 -2
- package/src/config/schemas/memory-storage.ts +1 -1
- package/src/config/schemas/services.ts +8 -6
- package/src/contacts/contact-store.ts +13 -6
- package/src/contacts/contacts-write.ts +0 -1
- package/src/context/window-manager.ts +13 -2
- package/src/daemon/conversation-agent-loop-handlers.ts +46 -42
- package/src/daemon/conversation-agent-loop.ts +56 -19
- package/src/daemon/conversation-attachments.ts +18 -36
- package/src/daemon/conversation-error.ts +2 -1
- package/src/daemon/conversation-history.ts +18 -4
- package/src/daemon/conversation-lifecycle.ts +39 -15
- package/src/daemon/conversation-messaging.ts +70 -26
- package/src/daemon/conversation-process.ts +58 -34
- package/src/daemon/conversation-runtime-assembly.ts +21 -38
- package/src/daemon/conversation-slash.ts +121 -256
- package/src/daemon/conversation-surfaces.ts +143 -20
- package/src/daemon/conversation-tool-setup.ts +0 -6
- package/src/daemon/conversation-workspace.ts +21 -1
- package/src/daemon/conversation.ts +51 -29
- package/src/daemon/first-greeting.ts +35 -0
- package/src/daemon/handlers/config-embeddings.ts +148 -0
- package/src/daemon/handlers/config-model.ts +71 -26
- package/src/daemon/handlers/conversations.ts +0 -23
- package/src/daemon/handlers/recording.ts +26 -21
- package/src/daemon/history-repair.ts +28 -8
- package/src/daemon/host-cu-proxy.ts +2 -2
- package/src/daemon/lifecycle.ts +106 -64
- package/src/daemon/message-protocol.ts +3 -0
- package/src/daemon/message-types/conversations.ts +19 -0
- package/src/daemon/message-types/messages.ts +1 -0
- package/src/daemon/message-types/shared.ts +2 -0
- package/src/daemon/message-types/surfaces.ts +2 -0
- package/src/daemon/message-types/upgrades.ts +23 -0
- package/src/daemon/server.ts +83 -12
- package/src/daemon/shutdown-handlers.ts +8 -5
- package/src/daemon/startup-error.ts +9 -0
- package/src/daemon/tool-side-effects.ts +11 -28
- package/src/events/tool-permission-telemetry-listener.ts +1 -3
- package/src/instrument.ts +0 -4
- package/src/media/app-icon-generator.ts +2 -2
- package/src/memory/app-git-service.ts +28 -16
- package/src/memory/app-store.ts +230 -41
- package/src/memory/attachments-store.ts +558 -130
- package/src/memory/conversation-attention-store.ts +70 -0
- package/src/memory/conversation-crud.ts +442 -3
- package/src/memory/conversation-directories.ts +125 -0
- package/src/memory/conversation-disk-view.ts +390 -0
- package/src/memory/conversation-key-store.ts +17 -5
- package/src/memory/conversation-queries.ts +5 -1
- package/src/memory/conversation-title-service.ts +21 -49
- package/src/memory/db-init.ts +28 -0
- package/src/memory/embedding-backend.ts +42 -53
- package/src/memory/embedding-gemini.test.ts +4 -4
- package/src/memory/embedding-local.ts +1 -3
- package/src/memory/embedding-ollama.ts +1 -3
- package/src/memory/embedding-openai.ts +1 -3
- package/src/memory/indexer.ts +9 -7
- package/src/memory/items-extractor.ts +42 -13
- package/src/memory/job-handlers/conversation-starters.ts +6 -1
- package/src/memory/job-handlers/embedding.test.ts +1 -4
- package/src/memory/llm-request-log-store.ts +100 -1
- package/src/memory/migrations/102-alter-table-columns.ts +5 -0
- package/src/memory/migrations/146-schedule-oneshot-routing.ts +3 -3
- package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +66 -70
- package/src/memory/migrations/148-drop-reminders-table.ts +5 -9
- package/src/memory/migrations/160-drop-loopback-port-column.ts +1 -3
- package/src/memory/migrations/174-rename-thread-starters-table.ts +0 -7
- package/src/memory/migrations/178-oauth-providers-managed-service-config-key.ts +15 -0
- package/src/memory/migrations/179-llm-request-log-message-id.ts +16 -0
- package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +66 -0
- package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +46 -0
- package/src/memory/migrations/182-oauth-providers-display-metadata.ts +20 -0
- package/src/memory/migrations/183-add-conversation-fork-lineage.ts +22 -0
- package/src/memory/migrations/184-llm-request-log-provider.ts +12 -0
- package/src/memory/migrations/index.ts +7 -0
- package/src/memory/migrations/registry.ts +13 -0
- package/src/memory/retriever.test.ts +601 -2
- package/src/memory/retriever.ts +85 -9
- package/src/memory/schema/conversations.ts +6 -0
- package/src/memory/schema/infrastructure.ts +13 -7
- package/src/memory/schema/oauth.ts +6 -0
- package/src/messaging/providers/gmail/mime-builder.ts +3 -1
- package/src/notifications/copy-composer.ts +26 -0
- package/src/notifications/decision-engine.ts +14 -1
- package/src/notifications/emit-signal.ts +1 -1
- package/src/notifications/signal.ts +36 -0
- package/src/oauth/byo-connection.test.ts +1 -45
- package/src/oauth/byo-connection.ts +2 -8
- package/src/oauth/connect-orchestrator.ts +15 -11
- package/src/oauth/connection-resolver.test.ts +191 -0
- package/src/oauth/connection-resolver.ts +66 -38
- package/src/oauth/connection.ts +0 -1
- package/src/oauth/oauth-store.ts +97 -47
- package/src/oauth/platform-connection.test.ts +0 -1
- package/src/oauth/platform-connection.ts +11 -3
- package/src/oauth/seed-providers.ts +78 -3
- package/src/oauth/token-persistence.ts +16 -10
- package/src/permissions/checker.ts +62 -19
- package/src/prompts/system-prompt.ts +2 -0
- package/src/prompts/templates/BOOTSTRAP.md +2 -0
- package/src/providers/anthropic/client.ts +8 -1
- package/src/providers/failover.ts +4 -1
- package/src/providers/gemini/client.ts +50 -0
- package/src/providers/model-catalog.ts +92 -0
- package/src/providers/model-intents.ts +29 -20
- package/src/providers/openai/client.ts +49 -0
- package/src/providers/types.ts +2 -0
- package/src/runtime/access-request-helper.ts +16 -7
- package/src/runtime/auth/credential-service.ts +3 -1
- package/src/runtime/auth/route-policy.ts +14 -1
- package/src/runtime/btw-sidechain.ts +101 -0
- package/src/runtime/channel-reply-delivery.ts +17 -1
- package/src/runtime/http-router.ts +3 -1
- package/src/runtime/http-server.ts +196 -141
- package/src/runtime/http-types.ts +1 -0
- package/src/runtime/migrations/vbundle-builder.ts +5 -1
- package/src/runtime/routes/access-request-decision.ts +41 -0
- package/src/runtime/routes/app-management-routes.ts +6 -3
- package/src/runtime/routes/app-routes.ts +7 -3
- package/src/runtime/routes/approval-routes.ts +1 -0
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +34 -2
- package/src/runtime/routes/attachment-routes.ts +45 -15
- package/src/runtime/routes/btw-routes.ts +21 -61
- package/src/runtime/routes/conversation-management-routes.ts +68 -0
- package/src/runtime/routes/conversation-query-routes.ts +180 -10
- package/src/runtime/routes/conversation-routes.ts +222 -28
- package/src/runtime/routes/conversation-starter-routes.ts +9 -11
- package/src/runtime/routes/diagnostics-routes.ts +1 -0
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +2 -2
- package/src/runtime/routes/llm-context-normalization.ts +1199 -0
- package/src/runtime/routes/log-export-routes.ts +3 -0
- package/src/runtime/routes/memory-item-routes.test.ts +34 -0
- package/src/runtime/routes/memory-item-routes.ts +4 -0
- package/src/runtime/routes/migration-routes.ts +4 -1
- package/src/runtime/routes/oauth-apps.ts +291 -0
- package/src/runtime/routes/secret-routes.ts +28 -1
- package/src/runtime/routes/settings-routes.ts +14 -0
- package/src/runtime/routes/trace-event-routes.ts +4 -1
- package/src/schedule/schedule-store.ts +9 -21
- package/src/security/secure-keys.ts +21 -0
- package/src/signals/bash.ts +1 -1
- package/src/swarm/backend-claude-code.ts +3 -6
- package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
- package/src/telemetry/usage-telemetry-reporter.ts +3 -1
- package/src/tools/AGENTS.md +6 -10
- package/src/tools/apps/executors.ts +17 -232
- package/src/tools/claude-code/claude-code.ts +2 -3
- package/src/tools/credentials/vault.ts +7 -12
- package/src/tools/host-filesystem/read.ts +13 -10
- package/src/tools/network/__tests__/web-search.test.ts +4 -2
- package/src/tools/schedule/list.ts +2 -7
- package/src/tools/schema-transforms.ts +5 -0
- package/src/tools/shared/filesystem/format-diff.ts +4 -21
- package/src/tools/skills/execute.ts +1 -1
- package/src/tools/tool-manifest.ts +0 -6
- package/src/tools/ui-surface/definitions.ts +2 -2
- package/src/util/device-id.ts +28 -5
- package/src/util/platform.ts +6 -0
- package/src/util/pricing.ts +1 -0
- package/src/util/retry.ts +1 -3
- package/src/workspace/migrations/002-backfill-installation-id.ts +23 -12
- package/src/workspace/migrations/003-seed-device-id.ts +3 -4
- package/src/workspace/migrations/006-services-config.ts +5 -0
- package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +12 -0
- package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +10 -0
- package/src/workspace/migrations/010-app-dir-rename.ts +223 -0
- package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +64 -0
- package/src/workspace/migrations/013-repair-conversation-disk-view.ts +11 -0
- package/src/workspace/migrations/rebuild-conversation-disk-view.ts +186 -0
- package/src/workspace/migrations/registry.ts +10 -0
- package/src/workspace/top-level-renderer.ts +12 -0
- package/src/__tests__/asset-materialize-tool.test.ts +0 -523
- package/src/__tests__/asset-search-tool.test.ts +0 -536
- package/src/__tests__/fixtures/media-reuse-fixtures.ts +0 -56
- package/src/__tests__/media-reuse-story.e2e.test.ts +0 -762
- package/src/__tests__/media-visibility-policy.test.ts +0 -190
- package/src/config/bundled-skills/app-builder/tools/app-file-edit.ts +0 -14
- package/src/config/bundled-skills/app-builder/tools/app-file-list.ts +0 -13
- package/src/config/bundled-skills/app-builder/tools/app-file-read.ts +0 -21
- package/src/config/bundled-skills/app-builder/tools/app-file-write.ts +0 -14
- package/src/config/bundled-skills/app-builder/tools/app-list.ts +0 -13
- package/src/config/bundled-skills/app-builder/tools/app-update.ts +0 -23
- package/src/daemon/media-visibility-policy.ts +0 -59
- package/src/tools/assets/materialize.ts +0 -248
- package/src/tools/assets/search.ts +0 -400
|
@@ -1,762 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Story E2E Test: "selfie yesterday -> generated image today"
|
|
3
|
-
*
|
|
4
|
-
* Exercises the full media-reuse user story end-to-end:
|
|
5
|
-
*
|
|
6
|
-
* 1. A fal.ai credential is stored with an injection template.
|
|
7
|
-
* 2. User uploads a selfie in Conversation A (standard conversation).
|
|
8
|
-
* 3. In Conversation B (standard), the agent uses asset_search to find the selfie,
|
|
9
|
-
* then asset_materialize to write it to disk.
|
|
10
|
-
* 4. A proxied bash command calls the provider API through a real proxy
|
|
11
|
-
* session with credential injection against a local mock endpoint.
|
|
12
|
-
* 5. The generated result is saved back.
|
|
13
|
-
*
|
|
14
|
-
* Also verifies that private-conversation isolation blocks cross-conversation media access.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
18
|
-
import * as http from "node:http";
|
|
19
|
-
import { tmpdir } from "node:os";
|
|
20
|
-
import { join } from "node:path";
|
|
21
|
-
import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
22
|
-
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
// Test directory and mocks (must precede any source imports)
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
|
-
|
|
27
|
-
const testDir = mkdtempSync(join(tmpdir(), "media-reuse-story-e2e-"));
|
|
28
|
-
const sandboxDir = join(testDir, "sandbox");
|
|
29
|
-
|
|
30
|
-
mock.module("../util/platform.js", () => ({
|
|
31
|
-
getDataDir: () => testDir,
|
|
32
|
-
isMacOS: () => process.platform === "darwin",
|
|
33
|
-
isLinux: () => process.platform === "linux",
|
|
34
|
-
isWindows: () => process.platform === "win32",
|
|
35
|
-
getPidPath: () => join(testDir, "test.pid"),
|
|
36
|
-
getDbPath: () => join(testDir, "test.db"),
|
|
37
|
-
getLogPath: () => join(testDir, "test.log"),
|
|
38
|
-
ensureDataDir: () => {},
|
|
39
|
-
getRootDir: () => testDir,
|
|
40
|
-
}));
|
|
41
|
-
|
|
42
|
-
mock.module("../util/logger.js", () => ({
|
|
43
|
-
getLogger: () =>
|
|
44
|
-
new Proxy({} as Record<string, unknown>, {
|
|
45
|
-
get: () => () => {},
|
|
46
|
-
}),
|
|
47
|
-
}));
|
|
48
|
-
|
|
49
|
-
mock.module("../config/loader.js", () => ({
|
|
50
|
-
getConfig: () => ({
|
|
51
|
-
ui: {},
|
|
52
|
-
|
|
53
|
-
model: "test",
|
|
54
|
-
provider: "test",
|
|
55
|
-
memory: { enabled: false },
|
|
56
|
-
rateLimit: { maxRequestsPerMinute: 0 },
|
|
57
|
-
timeouts: { shellDefaultTimeoutSec: 30, shellMaxTimeoutSec: 60 },
|
|
58
|
-
sandbox: { enabled: false, backend: "native" },
|
|
59
|
-
}),
|
|
60
|
-
}));
|
|
61
|
-
|
|
62
|
-
// Credential resolver and secure key mocks — must be set up before
|
|
63
|
-
// session-manager is imported so the proxy uses our test data.
|
|
64
|
-
import { credentialKey } from "../security/credential-key.js";
|
|
65
|
-
import type { ResolvedCredential } from "../tools/credentials/resolve.js";
|
|
66
|
-
|
|
67
|
-
let resolveByIdResults = new Map<string, ResolvedCredential | undefined>();
|
|
68
|
-
let secureKeyValues = new Map<string, string | undefined>();
|
|
69
|
-
|
|
70
|
-
mock.module("../tools/credentials/resolve.js", () => ({
|
|
71
|
-
resolveById: (credentialId: string) => resolveByIdResults.get(credentialId),
|
|
72
|
-
resolveByServiceField: () => undefined,
|
|
73
|
-
resolveForDomain: () => [],
|
|
74
|
-
}));
|
|
75
|
-
|
|
76
|
-
mock.module("../tools/credentials/metadata-store.js", () => ({
|
|
77
|
-
listCredentialMetadata: () => [],
|
|
78
|
-
}));
|
|
79
|
-
|
|
80
|
-
mock.module("../security/secure-keys.js", () => ({
|
|
81
|
-
getSecureKeyAsync: async (account: string) => secureKeyValues.get(account),
|
|
82
|
-
listSecureKeysAsync: async () => [],
|
|
83
|
-
_resetBackend: () => {},
|
|
84
|
-
}));
|
|
85
|
-
|
|
86
|
-
// Stub ensureLocalCA / certs so tests never run openssl
|
|
87
|
-
mock.module("../tools/network/script-proxy/certs.js", () => ({
|
|
88
|
-
ensureLocalCA: async () => {},
|
|
89
|
-
issueLeafCert: async () => ({ cert: "", key: "" }),
|
|
90
|
-
getCAPath: (dataDir: string) => `${dataDir}/proxy-ca/ca.pem`,
|
|
91
|
-
}));
|
|
92
|
-
|
|
93
|
-
// ---------------------------------------------------------------------------
|
|
94
|
-
// Source imports (after mocks)
|
|
95
|
-
// ---------------------------------------------------------------------------
|
|
96
|
-
|
|
97
|
-
import { mkdirSync } from "node:fs";
|
|
98
|
-
|
|
99
|
-
import {
|
|
100
|
-
type AttachmentContext,
|
|
101
|
-
filterVisibleAttachments,
|
|
102
|
-
isAttachmentVisible,
|
|
103
|
-
} from "../daemon/media-visibility-policy.js";
|
|
104
|
-
import {
|
|
105
|
-
linkAttachmentToMessage,
|
|
106
|
-
uploadAttachment,
|
|
107
|
-
} from "../memory/attachments-store.js";
|
|
108
|
-
import { addMessage, createConversation } from "../memory/conversation-crud.js";
|
|
109
|
-
import { getDb, initializeDb, resetDb } from "../memory/db.js";
|
|
110
|
-
import { assetMaterializeTool } from "../tools/assets/materialize.js";
|
|
111
|
-
import { assetSearchTool, searchAttachments } from "../tools/assets/search.js";
|
|
112
|
-
import type { CredentialInjectionTemplate } from "../tools/credentials/policy-types.js";
|
|
113
|
-
import { stopAllSessions } from "../tools/network/script-proxy/index.js";
|
|
114
|
-
import { shellTool } from "../tools/terminal/shell.js";
|
|
115
|
-
import type { ToolContext } from "../tools/types.js";
|
|
116
|
-
import {
|
|
117
|
-
FAKE_SELFIE_ATTACHMENT,
|
|
118
|
-
fakeAllowOnce,
|
|
119
|
-
fakeDeny,
|
|
120
|
-
TINY_PNG_BASE64,
|
|
121
|
-
} from "./fixtures/media-reuse-fixtures.js";
|
|
122
|
-
|
|
123
|
-
initializeDb();
|
|
124
|
-
mkdirSync(sandboxDir, { recursive: true });
|
|
125
|
-
|
|
126
|
-
afterAll(async () => {
|
|
127
|
-
resetDb();
|
|
128
|
-
await stopAllSessions();
|
|
129
|
-
resolveByIdResults = new Map();
|
|
130
|
-
secureKeyValues = new Map();
|
|
131
|
-
try {
|
|
132
|
-
rmSync(testDir, { recursive: true });
|
|
133
|
-
} catch {
|
|
134
|
-
/* best effort */
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
// ---------------------------------------------------------------------------
|
|
139
|
-
// Helpers
|
|
140
|
-
// ---------------------------------------------------------------------------
|
|
141
|
-
|
|
142
|
-
/** Resolved credential factory matching the pattern in script-proxy-injection-runtime.test.ts */
|
|
143
|
-
function makeResolved(
|
|
144
|
-
credentialId: string,
|
|
145
|
-
templates: CredentialInjectionTemplate[],
|
|
146
|
-
service = "test-service",
|
|
147
|
-
field = "api-key",
|
|
148
|
-
): ResolvedCredential {
|
|
149
|
-
return {
|
|
150
|
-
credentialId,
|
|
151
|
-
service,
|
|
152
|
-
field,
|
|
153
|
-
storageKey: credentialKey(service, field),
|
|
154
|
-
injectionTemplates: templates,
|
|
155
|
-
metadata: {
|
|
156
|
-
credentialId,
|
|
157
|
-
service,
|
|
158
|
-
field,
|
|
159
|
-
allowedTools: [],
|
|
160
|
-
allowedDomains: [],
|
|
161
|
-
createdAt: Date.now(),
|
|
162
|
-
updatedAt: Date.now(),
|
|
163
|
-
injectionTemplates: templates,
|
|
164
|
-
},
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Start a local HTTP echo server that captures received headers and returns
|
|
170
|
-
* them as JSON. Returns the server and its port.
|
|
171
|
-
*/
|
|
172
|
-
function startEchoServer(): Promise<{ server: http.Server; port: number }> {
|
|
173
|
-
return new Promise((resolve) => {
|
|
174
|
-
const server = http.createServer((req, res) => {
|
|
175
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
176
|
-
res.end(JSON.stringify({ headers: req.headers, url: req.url }));
|
|
177
|
-
});
|
|
178
|
-
server.listen(0, "127.0.0.1", () => {
|
|
179
|
-
const addr = server.address() as { port: number };
|
|
180
|
-
resolve({ server, port: addr.port });
|
|
181
|
-
});
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function resetTables() {
|
|
186
|
-
const db = getDb();
|
|
187
|
-
db.run("DELETE FROM message_attachments");
|
|
188
|
-
db.run("DELETE FROM attachments");
|
|
189
|
-
db.run("DELETE FROM messages");
|
|
190
|
-
db.run("DELETE FROM conversations");
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// ---------------------------------------------------------------------------
|
|
194
|
-
// Story E2E: selfie yesterday, generated image today
|
|
195
|
-
// ---------------------------------------------------------------------------
|
|
196
|
-
|
|
197
|
-
describe("Story E2E: selfie yesterday -> generated image today", () => {
|
|
198
|
-
// Shared state across the story steps
|
|
199
|
-
let conversationA: ReturnType<typeof createConversation>;
|
|
200
|
-
let conversationB: ReturnType<typeof createConversation>;
|
|
201
|
-
let selfieId: string;
|
|
202
|
-
let selfieAttachment: ReturnType<typeof uploadAttachment>;
|
|
203
|
-
|
|
204
|
-
beforeEach(async () => {
|
|
205
|
-
resetTables();
|
|
206
|
-
// Clear sandbox so stale files from prior tests don't mask regressions
|
|
207
|
-
rmSync(sandboxDir, { recursive: true, force: true });
|
|
208
|
-
mkdirSync(sandboxDir, { recursive: true });
|
|
209
|
-
|
|
210
|
-
// -- Step 1: Credential with injection template (simulated) --
|
|
211
|
-
// In a real flow, the user stores a fal.ai credential via credential_store.
|
|
212
|
-
// Here we only need to verify the injection template structure is valid --
|
|
213
|
-
// the actual credential broker is tested in dedicated tests. We construct
|
|
214
|
-
// the template to verify the data shape used downstream.
|
|
215
|
-
const falInjectionTemplate: CredentialInjectionTemplate = {
|
|
216
|
-
hostPattern: "*.fal.ai",
|
|
217
|
-
injectionType: "header",
|
|
218
|
-
headerName: "Authorization",
|
|
219
|
-
valuePrefix: "Key ",
|
|
220
|
-
};
|
|
221
|
-
// Sanity check the template shape
|
|
222
|
-
expect(falInjectionTemplate.hostPattern).toBe("*.fal.ai");
|
|
223
|
-
expect(falInjectionTemplate.injectionType).toBe("header");
|
|
224
|
-
expect(falInjectionTemplate.headerName).toBe("Authorization");
|
|
225
|
-
expect(falInjectionTemplate.valuePrefix).toBe("Key ");
|
|
226
|
-
|
|
227
|
-
// -- Step 2: Selfie uploaded in Conversation A (standard) --
|
|
228
|
-
conversationA = createConversation({
|
|
229
|
-
title: "Conversation A — selfie upload",
|
|
230
|
-
});
|
|
231
|
-
selfieAttachment = uploadAttachment(
|
|
232
|
-
"selfie.png",
|
|
233
|
-
"image/png",
|
|
234
|
-
TINY_PNG_BASE64,
|
|
235
|
-
);
|
|
236
|
-
selfieId = selfieAttachment.id;
|
|
237
|
-
|
|
238
|
-
const msgA = await addMessage(
|
|
239
|
-
conversationA.id,
|
|
240
|
-
"user",
|
|
241
|
-
"Here is my selfie from yesterday",
|
|
242
|
-
);
|
|
243
|
-
linkAttachmentToMessage(msgA.id, selfieId, 0);
|
|
244
|
-
|
|
245
|
-
// Backdate the selfie to "yesterday" for realism
|
|
246
|
-
const yesterday = Date.now() - 24 * 60 * 60 * 1000 - 5000;
|
|
247
|
-
const db = getDb();
|
|
248
|
-
db.run(
|
|
249
|
-
`UPDATE attachments SET created_at = ${yesterday} WHERE id = '${selfieId}'`,
|
|
250
|
-
);
|
|
251
|
-
|
|
252
|
-
// -- Step 3: Conversation B is a new standard conversation --
|
|
253
|
-
conversationB = createConversation({
|
|
254
|
-
title: "Conversation B — generate image",
|
|
255
|
-
});
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
test("asset_search discovers the selfie from Conversation B (cross-conversation)", async () => {
|
|
259
|
-
const context: ToolContext = {
|
|
260
|
-
workingDir: sandboxDir,
|
|
261
|
-
conversationId: conversationB.id,
|
|
262
|
-
trustClass: "guardian",
|
|
263
|
-
};
|
|
264
|
-
|
|
265
|
-
const result = await assetSearchTool.execute(
|
|
266
|
-
{ mime_type: "image/*", filename: "selfie" },
|
|
267
|
-
context,
|
|
268
|
-
);
|
|
269
|
-
|
|
270
|
-
expect(result.isError).toBe(false);
|
|
271
|
-
expect(result.content).toContain("selfie.png");
|
|
272
|
-
expect(result.content).toContain(selfieId);
|
|
273
|
-
expect(result.content).toContain("Found 1 asset(s)");
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
test("asset_search with recency last_7_days finds the selfie uploaded yesterday", async () => {
|
|
277
|
-
const context: ToolContext = {
|
|
278
|
-
workingDir: sandboxDir,
|
|
279
|
-
conversationId: conversationB.id,
|
|
280
|
-
trustClass: "guardian",
|
|
281
|
-
};
|
|
282
|
-
|
|
283
|
-
const result = await assetSearchTool.execute(
|
|
284
|
-
{ mime_type: "image/*", recency: "last_7_days" },
|
|
285
|
-
context,
|
|
286
|
-
);
|
|
287
|
-
|
|
288
|
-
expect(result.isError).toBe(false);
|
|
289
|
-
expect(result.content).toContain("selfie.png");
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
test("asset_materialize writes the selfie to disk in Conversation B sandbox", async () => {
|
|
293
|
-
const context: ToolContext = {
|
|
294
|
-
workingDir: sandboxDir,
|
|
295
|
-
conversationId: conversationB.id,
|
|
296
|
-
trustClass: "guardian",
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
const result = await assetMaterializeTool.execute(
|
|
300
|
-
{ attachment_id: selfieId, destination_path: "inputs/selfie.png" },
|
|
301
|
-
context,
|
|
302
|
-
);
|
|
303
|
-
|
|
304
|
-
expect(result.isError).toBe(false);
|
|
305
|
-
expect(result.content).toContain("Materialized");
|
|
306
|
-
expect(result.content).toContain("selfie.png");
|
|
307
|
-
expect(result.content).toContain("image/png");
|
|
308
|
-
|
|
309
|
-
// Verify the file actually exists on disk
|
|
310
|
-
const materializedPath = join(sandboxDir, "inputs", "selfie.png");
|
|
311
|
-
expect(existsSync(materializedPath)).toBe(true);
|
|
312
|
-
|
|
313
|
-
// Verify the content matches the original base64-decoded data
|
|
314
|
-
const writtenBytes = readFileSync(materializedPath);
|
|
315
|
-
const expectedBytes = Buffer.from(TINY_PNG_BASE64, "base64");
|
|
316
|
-
expect(Buffer.compare(writtenBytes, expectedBytes)).toBe(0);
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
test("full story: search -> materialize -> proxied provider call -> output saved", async () => {
|
|
320
|
-
const contextB: ToolContext = {
|
|
321
|
-
workingDir: sandboxDir,
|
|
322
|
-
conversationId: conversationB.id,
|
|
323
|
-
trustClass: "guardian",
|
|
324
|
-
};
|
|
325
|
-
|
|
326
|
-
// Step 3a: Search for the selfie
|
|
327
|
-
const searchResult = await assetSearchTool.execute(
|
|
328
|
-
{ mime_type: "image/*", filename: "selfie" },
|
|
329
|
-
contextB,
|
|
330
|
-
);
|
|
331
|
-
expect(searchResult.isError).toBe(false);
|
|
332
|
-
expect(searchResult.content).toContain(selfieId);
|
|
333
|
-
|
|
334
|
-
// Step 3b: Materialize the selfie to disk
|
|
335
|
-
const materializeResult = await assetMaterializeTool.execute(
|
|
336
|
-
{ attachment_id: selfieId, destination_path: "inputs/selfie.png" },
|
|
337
|
-
contextB,
|
|
338
|
-
);
|
|
339
|
-
expect(materializeResult.isError).toBe(false);
|
|
340
|
-
const inputPath = join(sandboxDir, "inputs", "selfie.png");
|
|
341
|
-
expect(existsSync(inputPath)).toBe(true);
|
|
342
|
-
|
|
343
|
-
// Step 4: Invoke the bash tool with network_mode='proxied', just as the
|
|
344
|
-
// agent loop would. The shell tool internally creates a proxy session,
|
|
345
|
-
// injects proxy env vars, spawns curl, and the proxy injects credentials.
|
|
346
|
-
const echo = await startEchoServer();
|
|
347
|
-
try {
|
|
348
|
-
const tpl: CredentialInjectionTemplate = {
|
|
349
|
-
hostPattern: "127.0.0.1",
|
|
350
|
-
injectionType: "header",
|
|
351
|
-
headerName: "Authorization",
|
|
352
|
-
valuePrefix: "Key ",
|
|
353
|
-
};
|
|
354
|
-
const resolved = makeResolved("cred-story", [tpl]);
|
|
355
|
-
resolveByIdResults.set("cred-story", resolved);
|
|
356
|
-
secureKeyValues.set(
|
|
357
|
-
credentialKey("test-service", "api-key"),
|
|
358
|
-
"fal_test_secret",
|
|
359
|
-
);
|
|
360
|
-
|
|
361
|
-
// Drive the proxy step through the bash tool — the actual integration path.
|
|
362
|
-
// -x "$HTTP_PROXY" forces curl to use the proxy explicitly (macOS curl
|
|
363
|
-
// ignores the uppercase HTTP_PROXY env var). --noproxy "" overrides the
|
|
364
|
-
// NO_PROXY list which excludes 127.0.0.1 by default.
|
|
365
|
-
const bashResult = await shellTool.execute(
|
|
366
|
-
{
|
|
367
|
-
command: `curl -s -x "$HTTP_PROXY" --noproxy "" http://127.0.0.1:${echo.port}/v1/generate`,
|
|
368
|
-
network_mode: "proxied",
|
|
369
|
-
credential_ids: ["cred-story"],
|
|
370
|
-
},
|
|
371
|
-
contextB,
|
|
372
|
-
);
|
|
373
|
-
expect(bashResult.isError).toBeFalsy();
|
|
374
|
-
|
|
375
|
-
// The echo server returns JSON with the headers it received.
|
|
376
|
-
// When exit code is 0 and there's no stderr, formatShellOutput
|
|
377
|
-
// returns raw stdout as the content — so JSON.parse works directly.
|
|
378
|
-
const echoResponse = JSON.parse(bashResult.content);
|
|
379
|
-
expect(echoResponse.headers.authorization).toBe("Key fal_test_secret");
|
|
380
|
-
|
|
381
|
-
await stopAllSessions();
|
|
382
|
-
resolveByIdResults.delete("cred-story");
|
|
383
|
-
secureKeyValues.delete(credentialKey("test-service", "api-key"));
|
|
384
|
-
} finally {
|
|
385
|
-
echo.server.close();
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// Step 5: Save the generated result back as an attachment.
|
|
389
|
-
// Use different content than the selfie to avoid content-hash deduplication
|
|
390
|
-
// in the attachment store (same hash = returns existing row).
|
|
391
|
-
const generatedImageBase64 = Buffer.from(
|
|
392
|
-
"generated-portrait-data-unique",
|
|
393
|
-
).toString("base64");
|
|
394
|
-
const outputAttachment = uploadAttachment(
|
|
395
|
-
"generated-portrait.png",
|
|
396
|
-
"image/png",
|
|
397
|
-
generatedImageBase64,
|
|
398
|
-
);
|
|
399
|
-
|
|
400
|
-
const msgB = await addMessage(
|
|
401
|
-
conversationB.id,
|
|
402
|
-
"assistant",
|
|
403
|
-
"Here is your generated portrait!",
|
|
404
|
-
);
|
|
405
|
-
linkAttachmentToMessage(msgB.id, outputAttachment.id, 0);
|
|
406
|
-
|
|
407
|
-
// Verify the output attachment exists in the DB via raw search
|
|
408
|
-
const rawResults = searchAttachments({ filename: "generated-portrait" });
|
|
409
|
-
expect(rawResults.length).toBe(1);
|
|
410
|
-
expect(rawResults[0].originalFilename).toBe("generated-portrait.png");
|
|
411
|
-
expect(rawResults[0].id).toBe(outputAttachment.id);
|
|
412
|
-
|
|
413
|
-
// Verify it's also findable via the tool (with visibility filtering)
|
|
414
|
-
const outputSearchResult = await assetSearchTool.execute(
|
|
415
|
-
{ filename: "generated-portrait" },
|
|
416
|
-
contextB,
|
|
417
|
-
);
|
|
418
|
-
expect(outputSearchResult.isError).toBe(false);
|
|
419
|
-
expect(outputSearchResult.content).toContain("generated-portrait.png");
|
|
420
|
-
expect(outputSearchResult.content).toContain(outputAttachment.id);
|
|
421
|
-
});
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
// ---------------------------------------------------------------------------
|
|
425
|
-
// Credential injection template validation
|
|
426
|
-
// ---------------------------------------------------------------------------
|
|
427
|
-
|
|
428
|
-
describe("Credential injection template structure", () => {
|
|
429
|
-
test("fal.ai header injection template has all required fields", () => {
|
|
430
|
-
const template: CredentialInjectionTemplate = {
|
|
431
|
-
hostPattern: "*.fal.ai",
|
|
432
|
-
injectionType: "header",
|
|
433
|
-
headerName: "Authorization",
|
|
434
|
-
valuePrefix: "Key ",
|
|
435
|
-
};
|
|
436
|
-
|
|
437
|
-
expect(template.hostPattern).toBe("*.fal.ai");
|
|
438
|
-
expect(template.injectionType).toBe("header");
|
|
439
|
-
expect(template.headerName).toBe("Authorization");
|
|
440
|
-
expect(template.valuePrefix).toBe("Key ");
|
|
441
|
-
expect(template.queryParamName).toBeUndefined();
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
test("query-param injection template shape is valid", () => {
|
|
445
|
-
const template: CredentialInjectionTemplate = {
|
|
446
|
-
hostPattern: "api.example.com",
|
|
447
|
-
injectionType: "query",
|
|
448
|
-
queryParamName: "api_key",
|
|
449
|
-
};
|
|
450
|
-
|
|
451
|
-
expect(template.injectionType).toBe("query");
|
|
452
|
-
expect(template.queryParamName).toBe("api_key");
|
|
453
|
-
expect(template.headerName).toBeUndefined();
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
test("host pattern matching for fal.ai subdomains", async () => {
|
|
457
|
-
// The proxy uses minimatch for host patterns. Verify the pattern shape.
|
|
458
|
-
const { minimatch } = await import("minimatch");
|
|
459
|
-
const pattern = "*.fal.ai";
|
|
460
|
-
expect(minimatch("api.fal.ai", pattern)).toBe(true);
|
|
461
|
-
expect(minimatch("v1.fal.ai", pattern)).toBe(true);
|
|
462
|
-
expect(minimatch("fal.ai", pattern)).toBe(false); // No subdomain
|
|
463
|
-
expect(minimatch("evil.fal.ai.attacker.com", pattern)).toBe(false);
|
|
464
|
-
});
|
|
465
|
-
});
|
|
466
|
-
|
|
467
|
-
// ---------------------------------------------------------------------------
|
|
468
|
-
// Proxied bash approval is per-invocation (not persistent)
|
|
469
|
-
// ---------------------------------------------------------------------------
|
|
470
|
-
|
|
471
|
-
describe("Proxied bash activation requires per-invocation approval", () => {
|
|
472
|
-
test("one-time allow decision has no pattern or scope (cannot create persistent rule)", () => {
|
|
473
|
-
const approval = fakeAllowOnce();
|
|
474
|
-
expect(approval.decision).toBe("allow");
|
|
475
|
-
expect(approval.pattern).toBeUndefined();
|
|
476
|
-
expect(approval.scope).toBeUndefined();
|
|
477
|
-
});
|
|
478
|
-
|
|
479
|
-
test("deny decision also has no pattern or scope", () => {
|
|
480
|
-
const denial = fakeDeny();
|
|
481
|
-
expect(denial.decision).toBe("deny");
|
|
482
|
-
expect(denial.pattern).toBeUndefined();
|
|
483
|
-
expect(denial.scope).toBeUndefined();
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
test("consecutive approval checks produce independent decisions", () => {
|
|
487
|
-
// Simulate multiple invocations — each must be independently approved
|
|
488
|
-
const decisions = [fakeAllowOnce(), fakeAllowOnce(), fakeDeny()];
|
|
489
|
-
expect(decisions[0].decision).toBe("allow");
|
|
490
|
-
expect(decisions[1].decision).toBe("allow");
|
|
491
|
-
expect(decisions[2].decision).toBe("deny");
|
|
492
|
-
// No decision carries over from a previous one
|
|
493
|
-
for (const d of decisions) {
|
|
494
|
-
expect(d.pattern).toBeUndefined();
|
|
495
|
-
}
|
|
496
|
-
});
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
// ---------------------------------------------------------------------------
|
|
500
|
-
// Private-conversation variant: cross-conversation blocking
|
|
501
|
-
// ---------------------------------------------------------------------------
|
|
502
|
-
|
|
503
|
-
describe("Private-conversation variant: cross-conversation media blocking", () => {
|
|
504
|
-
beforeEach(() => {
|
|
505
|
-
resetTables();
|
|
506
|
-
rmSync(sandboxDir, { recursive: true, force: true });
|
|
507
|
-
mkdirSync(sandboxDir, { recursive: true });
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
test("selfie in private conversation A is NOT discoverable via search from Conversation B", async () => {
|
|
511
|
-
// Upload selfie in a private conversation
|
|
512
|
-
const privateConversation = createConversation({
|
|
513
|
-
title: "Private selfie conversation",
|
|
514
|
-
conversationType: "private",
|
|
515
|
-
});
|
|
516
|
-
const selfie = uploadAttachment(
|
|
517
|
-
"private-selfie.png",
|
|
518
|
-
"image/png",
|
|
519
|
-
TINY_PNG_BASE64,
|
|
520
|
-
);
|
|
521
|
-
const msg = await addMessage(
|
|
522
|
-
privateConversation.id,
|
|
523
|
-
"user",
|
|
524
|
-
"My private selfie",
|
|
525
|
-
);
|
|
526
|
-
linkAttachmentToMessage(msg.id, selfie.id, 0);
|
|
527
|
-
|
|
528
|
-
// Search from a standard conversation
|
|
529
|
-
const standardConversation = createConversation({
|
|
530
|
-
title: "Standard conversation B",
|
|
531
|
-
});
|
|
532
|
-
const context: ToolContext = {
|
|
533
|
-
workingDir: sandboxDir,
|
|
534
|
-
conversationId: standardConversation.id,
|
|
535
|
-
trustClass: "guardian",
|
|
536
|
-
};
|
|
537
|
-
|
|
538
|
-
const result = await assetSearchTool.execute(
|
|
539
|
-
{ filename: "private-selfie" },
|
|
540
|
-
context,
|
|
541
|
-
);
|
|
542
|
-
|
|
543
|
-
expect(result.isError).toBe(false);
|
|
544
|
-
expect(result.content).toContain("No assets found");
|
|
545
|
-
});
|
|
546
|
-
|
|
547
|
-
test("selfie in private conversation A is NOT materializable from Conversation B", async () => {
|
|
548
|
-
const privateConversation = createConversation({
|
|
549
|
-
title: "Private selfie conversation",
|
|
550
|
-
conversationType: "private",
|
|
551
|
-
});
|
|
552
|
-
const base64 = Buffer.from("private image data").toString("base64");
|
|
553
|
-
const selfie = uploadAttachment("private-selfie.png", "image/png", base64);
|
|
554
|
-
const msg = await addMessage(
|
|
555
|
-
privateConversation.id,
|
|
556
|
-
"user",
|
|
557
|
-
"My private selfie",
|
|
558
|
-
);
|
|
559
|
-
linkAttachmentToMessage(msg.id, selfie.id, 0);
|
|
560
|
-
|
|
561
|
-
// Try to materialize from a standard conversation
|
|
562
|
-
const standardConversation = createConversation({
|
|
563
|
-
title: "Standard conversation B",
|
|
564
|
-
});
|
|
565
|
-
const context: ToolContext = {
|
|
566
|
-
workingDir: sandboxDir,
|
|
567
|
-
conversationId: standardConversation.id,
|
|
568
|
-
trustClass: "guardian",
|
|
569
|
-
};
|
|
570
|
-
|
|
571
|
-
const result = await assetMaterializeTool.execute(
|
|
572
|
-
{ attachment_id: selfie.id, destination_path: "stolen-selfie.png" },
|
|
573
|
-
context,
|
|
574
|
-
);
|
|
575
|
-
|
|
576
|
-
expect(result.isError).toBe(true);
|
|
577
|
-
expect(result.content).toContain("private conversation");
|
|
578
|
-
expect(result.content).toContain("cannot be accessed");
|
|
579
|
-
});
|
|
580
|
-
|
|
581
|
-
test("selfie in private conversation IS accessible from the same private conversation", async () => {
|
|
582
|
-
const privateConversation = createConversation({
|
|
583
|
-
title: "Private selfie conversation",
|
|
584
|
-
conversationType: "private",
|
|
585
|
-
});
|
|
586
|
-
const selfie = uploadAttachment(
|
|
587
|
-
"private-selfie.png",
|
|
588
|
-
"image/png",
|
|
589
|
-
TINY_PNG_BASE64,
|
|
590
|
-
);
|
|
591
|
-
const msg = await addMessage(
|
|
592
|
-
privateConversation.id,
|
|
593
|
-
"user",
|
|
594
|
-
"My private selfie",
|
|
595
|
-
);
|
|
596
|
-
linkAttachmentToMessage(msg.id, selfie.id, 0);
|
|
597
|
-
|
|
598
|
-
// Search from the same private conversation
|
|
599
|
-
const context: ToolContext = {
|
|
600
|
-
workingDir: sandboxDir,
|
|
601
|
-
conversationId: privateConversation.id,
|
|
602
|
-
trustClass: "guardian",
|
|
603
|
-
};
|
|
604
|
-
|
|
605
|
-
const searchResult = await assetSearchTool.execute(
|
|
606
|
-
{ filename: "private-selfie" },
|
|
607
|
-
context,
|
|
608
|
-
);
|
|
609
|
-
expect(searchResult.isError).toBe(false);
|
|
610
|
-
expect(searchResult.content).toContain("private-selfie.png");
|
|
611
|
-
|
|
612
|
-
// Materialize from the same private conversation
|
|
613
|
-
const materializeResult = await assetMaterializeTool.execute(
|
|
614
|
-
{ attachment_id: selfie.id, destination_path: "my-selfie.png" },
|
|
615
|
-
context,
|
|
616
|
-
);
|
|
617
|
-
expect(materializeResult.isError).toBe(false);
|
|
618
|
-
expect(materializeResult.content).toContain("Materialized");
|
|
619
|
-
});
|
|
620
|
-
|
|
621
|
-
test("selfie in private conversation A is NOT accessible from private conversation B", async () => {
|
|
622
|
-
const privateConversationA = createConversation({
|
|
623
|
-
title: "Private conversation A",
|
|
624
|
-
conversationType: "private",
|
|
625
|
-
});
|
|
626
|
-
const selfie = uploadAttachment(
|
|
627
|
-
"conversation-a-selfie.png",
|
|
628
|
-
"image/png",
|
|
629
|
-
TINY_PNG_BASE64,
|
|
630
|
-
);
|
|
631
|
-
const msgA = await addMessage(
|
|
632
|
-
privateConversationA.id,
|
|
633
|
-
"user",
|
|
634
|
-
"Selfie in conversation A",
|
|
635
|
-
);
|
|
636
|
-
linkAttachmentToMessage(msgA.id, selfie.id, 0);
|
|
637
|
-
|
|
638
|
-
// Search from a different private conversation
|
|
639
|
-
const privateConversationB = createConversation({
|
|
640
|
-
title: "Private conversation B",
|
|
641
|
-
conversationType: "private",
|
|
642
|
-
});
|
|
643
|
-
const context: ToolContext = {
|
|
644
|
-
workingDir: sandboxDir,
|
|
645
|
-
conversationId: privateConversationB.id,
|
|
646
|
-
trustClass: "guardian",
|
|
647
|
-
};
|
|
648
|
-
|
|
649
|
-
const searchResult = await assetSearchTool.execute(
|
|
650
|
-
{ filename: "conversation-a-selfie" },
|
|
651
|
-
context,
|
|
652
|
-
);
|
|
653
|
-
expect(searchResult.isError).toBe(false);
|
|
654
|
-
expect(searchResult.content).toContain("No assets found");
|
|
655
|
-
|
|
656
|
-
// Also verify materialize is blocked
|
|
657
|
-
const materializeResult = await assetMaterializeTool.execute(
|
|
658
|
-
{ attachment_id: selfie.id, destination_path: "cross-conversation.png" },
|
|
659
|
-
context,
|
|
660
|
-
);
|
|
661
|
-
expect(materializeResult.isError).toBe(true);
|
|
662
|
-
expect(materializeResult.content).toContain("private conversation");
|
|
663
|
-
});
|
|
664
|
-
|
|
665
|
-
test("visibility policy unit check: private attachment blocked from standard context", () => {
|
|
666
|
-
const privateAttachment: AttachmentContext = {
|
|
667
|
-
conversationId: "conv-private-001",
|
|
668
|
-
isPrivate: true,
|
|
669
|
-
};
|
|
670
|
-
const standardContext: AttachmentContext = {
|
|
671
|
-
conversationId: "conv-standard-001",
|
|
672
|
-
isPrivate: false,
|
|
673
|
-
};
|
|
674
|
-
|
|
675
|
-
expect(isAttachmentVisible(privateAttachment, standardContext)).toBe(false);
|
|
676
|
-
});
|
|
677
|
-
|
|
678
|
-
test("visibility policy unit check: standard attachment visible from any context", () => {
|
|
679
|
-
const standardAttachment: AttachmentContext = {
|
|
680
|
-
conversationId: "conv-standard-001",
|
|
681
|
-
isPrivate: false,
|
|
682
|
-
};
|
|
683
|
-
const otherContext: AttachmentContext = {
|
|
684
|
-
conversationId: "conv-other",
|
|
685
|
-
isPrivate: false,
|
|
686
|
-
};
|
|
687
|
-
const privateContext: AttachmentContext = {
|
|
688
|
-
conversationId: "conv-private-001",
|
|
689
|
-
isPrivate: true,
|
|
690
|
-
};
|
|
691
|
-
|
|
692
|
-
expect(isAttachmentVisible(standardAttachment, otherContext)).toBe(true);
|
|
693
|
-
expect(isAttachmentVisible(standardAttachment, privateContext)).toBe(true);
|
|
694
|
-
});
|
|
695
|
-
|
|
696
|
-
test("filterVisibleAttachments correctly partitions mixed standard/private attachments", () => {
|
|
697
|
-
interface TestItem {
|
|
698
|
-
id: string;
|
|
699
|
-
conversationId: string;
|
|
700
|
-
isPrivate: boolean;
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
const items: TestItem[] = [
|
|
704
|
-
{ id: "std-1", conversationId: "conv-std", isPrivate: false },
|
|
705
|
-
{ id: "priv-a", conversationId: "conv-priv-a", isPrivate: true },
|
|
706
|
-
{ id: "priv-b", conversationId: "conv-priv-b", isPrivate: true },
|
|
707
|
-
];
|
|
708
|
-
|
|
709
|
-
const getCtx = (item: TestItem): AttachmentContext => ({
|
|
710
|
-
conversationId: item.conversationId,
|
|
711
|
-
isPrivate: item.isPrivate,
|
|
712
|
-
});
|
|
713
|
-
|
|
714
|
-
// From a standard context, only standard attachments are visible
|
|
715
|
-
const fromStandard = filterVisibleAttachments(
|
|
716
|
-
items,
|
|
717
|
-
{ conversationId: "conv-other", isPrivate: false },
|
|
718
|
-
getCtx,
|
|
719
|
-
);
|
|
720
|
-
expect(fromStandard.map((i) => i.id)).toEqual(["std-1"]);
|
|
721
|
-
|
|
722
|
-
// From private conversation A, standard + A's private attachment are visible
|
|
723
|
-
const fromPrivA = filterVisibleAttachments(
|
|
724
|
-
items,
|
|
725
|
-
{ conversationId: "conv-priv-a", isPrivate: true },
|
|
726
|
-
getCtx,
|
|
727
|
-
);
|
|
728
|
-
expect(fromPrivA.map((i) => i.id)).toEqual(["std-1", "priv-a"]);
|
|
729
|
-
|
|
730
|
-
// From private conversation B, standard + B's private attachment are visible
|
|
731
|
-
const fromPrivB = filterVisibleAttachments(
|
|
732
|
-
items,
|
|
733
|
-
{ conversationId: "conv-priv-b", isPrivate: true },
|
|
734
|
-
getCtx,
|
|
735
|
-
);
|
|
736
|
-
expect(fromPrivB.map((i) => i.id)).toEqual(["std-1", "priv-b"]);
|
|
737
|
-
});
|
|
738
|
-
});
|
|
739
|
-
|
|
740
|
-
// ---------------------------------------------------------------------------
|
|
741
|
-
// Fixture data integrity checks
|
|
742
|
-
// ---------------------------------------------------------------------------
|
|
743
|
-
|
|
744
|
-
describe("Fixture data integrity", () => {
|
|
745
|
-
test("FAKE_SELFIE_ATTACHMENT has consistent metadata", () => {
|
|
746
|
-
expect(FAKE_SELFIE_ATTACHMENT.originalFilename).toBe("selfie.png");
|
|
747
|
-
expect(FAKE_SELFIE_ATTACHMENT.mimeType).toBe("image/png");
|
|
748
|
-
expect(FAKE_SELFIE_ATTACHMENT.kind).toBe("image");
|
|
749
|
-
expect(FAKE_SELFIE_ATTACHMENT.sizeBytes).toBe(
|
|
750
|
-
Buffer.from(TINY_PNG_BASE64, "base64").length,
|
|
751
|
-
);
|
|
752
|
-
});
|
|
753
|
-
|
|
754
|
-
test("TINY_PNG_BASE64 decodes to valid PNG header bytes", () => {
|
|
755
|
-
const bytes = Buffer.from(TINY_PNG_BASE64, "base64");
|
|
756
|
-
// PNG magic bytes: 137 80 78 71 13 10 26 10
|
|
757
|
-
expect(bytes[0]).toBe(0x89);
|
|
758
|
-
expect(bytes[1]).toBe(0x50); // P
|
|
759
|
-
expect(bytes[2]).toBe(0x4e); // N
|
|
760
|
-
expect(bytes[3]).toBe(0x47); // G
|
|
761
|
-
});
|
|
762
|
-
});
|