@vellumai/assistant 0.4.49 → 0.4.51
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 +24 -33
- package/README.md +3 -3
- package/docs/architecture/integrations.md +2 -2
- package/docs/architecture/keychain-broker.md +6 -6
- package/docs/architecture/memory.md +180 -119
- package/knip.json +32 -0
- package/package.json +3 -2
- package/src/__tests__/agent-loop.test.ts +3 -1
- package/src/__tests__/anthropic-provider.test.ts +114 -23
- package/src/__tests__/approval-cascade.test.ts +1 -15
- package/src/__tests__/approval-routes-http.test.ts +2 -0
- package/src/__tests__/assistant-feature-flag-guard.test.ts +0 -23
- package/src/__tests__/btw-routes.test.ts +61 -5
- package/src/__tests__/canonical-guardian-store.test.ts +95 -0
- package/src/__tests__/checker.test.ts +13 -0
- package/src/__tests__/config-schema.test.ts +1 -68
- package/src/__tests__/config-watcher.test.ts +8 -0
- package/src/__tests__/context-memory-e2e.test.ts +11 -100
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +8 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
- package/src/__tests__/credential-security-e2e.test.ts +1 -0
- package/src/__tests__/credential-security-invariants.test.ts +8 -7
- package/src/__tests__/credential-vault-unit.test.ts +23 -18
- package/src/__tests__/credential-vault.test.ts +30 -18
- package/src/__tests__/credentials-cli.test.ts +257 -82
- package/src/__tests__/cu-unified-flow.test.ts +532 -0
- package/src/__tests__/date-context.test.ts +93 -77
- package/src/__tests__/deterministic-verification-control-plane.test.ts +64 -0
- package/src/__tests__/guardian-routing-invariants.test.ts +93 -0
- package/src/__tests__/history-repair.test.ts +245 -0
- package/src/__tests__/host-cu-proxy.test.ts +165 -3
- package/src/__tests__/http-user-message-parity.test.ts +1 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +36 -7
- package/src/__tests__/integration-status.test.ts +31 -30
- package/src/__tests__/invite-redemption-service.test.ts +166 -13
- package/src/__tests__/invite-routes-http.test.ts +166 -5
- package/src/__tests__/keychain-broker-client.test.ts +4 -4
- package/src/__tests__/list-messages-attachments.test.ts +193 -0
- package/src/__tests__/memory-context-benchmark.benchmark.test.ts +56 -18
- package/src/__tests__/memory-lifecycle-e2e.test.ts +244 -387
- package/src/__tests__/memory-recall-quality.test.ts +244 -407
- package/src/__tests__/memory-regressions.experimental.test.ts +126 -101
- package/src/__tests__/memory-regressions.test.ts +477 -2841
- package/src/__tests__/memory-retrieval.benchmark.test.ts +33 -150
- package/src/__tests__/memory-upsert-concurrency.test.ts +5 -244
- package/src/__tests__/mime-builder.test.ts +28 -0
- package/src/__tests__/native-web-search.test.ts +1 -0
- package/src/__tests__/oauth-cli.test.ts +824 -31
- package/src/__tests__/oauth-provider-profiles.test.ts +1 -1
- package/src/__tests__/oauth-store.test.ts +363 -17
- package/src/__tests__/qdrant-collection-migration.test.ts +53 -8
- package/src/__tests__/registry.test.ts +0 -1
- package/src/__tests__/relay-server.test.ts +55 -1
- package/src/__tests__/schedule-tools.test.ts +32 -0
- package/src/__tests__/script-proxy-certs.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +1 -0
- package/src/__tests__/secret-routes-managed-proxy.test.ts +183 -0
- package/src/__tests__/secure-keys.test.ts +78 -18
- package/src/__tests__/send-endpoint-busy.test.ts +3 -0
- package/src/__tests__/server-history-render.test.ts +2 -2
- package/src/__tests__/session-abort-tool-results.test.ts +1 -14
- package/src/__tests__/session-agent-loop-overflow.test.ts +1583 -0
- package/src/__tests__/session-agent-loop.test.ts +19 -15
- package/src/__tests__/session-confirmation-signals.test.ts +1 -15
- package/src/__tests__/session-error.test.ts +124 -2
- package/src/__tests__/session-history-web-search.test.ts +918 -0
- package/src/__tests__/session-pre-run-repair.test.ts +1 -14
- package/src/__tests__/session-provider-retry-repair.test.ts +25 -28
- package/src/__tests__/session-queue.test.ts +37 -27
- package/src/__tests__/session-runtime-assembly.test.ts +54 -0
- package/src/__tests__/session-slash-known.test.ts +1 -15
- package/src/__tests__/session-slash-queue.test.ts +1 -15
- package/src/__tests__/session-slash-unknown.test.ts +1 -15
- package/src/__tests__/session-workspace-cache-state.test.ts +3 -33
- package/src/__tests__/session-workspace-injection.test.ts +3 -37
- package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -37
- package/src/__tests__/skills-install-extract.test.ts +93 -0
- package/src/__tests__/skills.test.ts +2 -2
- package/src/__tests__/skillssh-registry.test.ts +451 -0
- package/src/__tests__/slack-channel-config.test.ts +10 -8
- package/src/__tests__/trust-store.test.ts +15 -0
- package/src/__tests__/twilio-config.test.ts +11 -10
- package/src/__tests__/twilio-provider.test.ts +9 -4
- package/src/__tests__/voice-invite-redemption.test.ts +85 -5
- package/src/agent/ax-tree-compaction.test.ts +51 -0
- package/src/agent/loop.ts +39 -12
- package/src/approvals/AGENTS.md +1 -1
- package/src/approvals/guardian-request-resolvers.ts +14 -2
- package/src/bundler/compiler-tools.ts +66 -2
- package/src/calls/call-domain.ts +134 -3
- package/src/calls/call-store.ts +6 -0
- package/src/calls/relay-server.ts +44 -6
- package/src/calls/relay-setup-router.ts +17 -1
- package/src/calls/twilio-config.ts +5 -4
- package/src/calls/twilio-provider.ts +14 -9
- package/src/calls/twilio-rest.ts +10 -7
- package/src/calls/types.ts +3 -1
- package/src/cli/commands/config.ts +14 -9
- package/src/cli/commands/contacts.ts +3 -0
- package/src/cli/commands/credentials.ts +170 -174
- package/src/cli/commands/doctor.ts +11 -8
- package/src/cli/commands/keys.ts +9 -9
- package/src/cli/commands/mcp.ts +46 -59
- package/src/cli/commands/memory.ts +16 -165
- package/src/cli/commands/oauth/apps.ts +68 -10
- package/src/cli/commands/oauth/connections.ts +475 -105
- package/src/cli/commands/oauth/index.ts +3 -3
- package/src/cli/commands/oauth/providers.ts +18 -4
- package/src/cli/commands/sessions.ts +5 -2
- package/src/cli/commands/skills.ts +173 -1
- package/src/cli/http-client.ts +0 -20
- package/src/cli/main-screen.tsx +2 -2
- package/src/cli/program.ts +5 -6
- package/src/cli.ts +20 -22
- package/src/config/__tests__/feature-flag-registry-bundled.test.ts +39 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +1 -1
- package/src/config/bundled-skills/computer-use/tools/computer-use-observe.ts +12 -0
- package/src/config/bundled-skills/contacts/SKILL.md +35 -11
- package/src/config/bundled-skills/contacts/tools/google-contacts.ts +1 -1
- package/src/config/bundled-skills/gmail/SKILL.md +1 -1
- package/src/config/bundled-skills/gmail/TOOLS.json +52 -0
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +13 -3
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +9 -2
- package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +9 -2
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +5 -1
- package/src/config/bundled-skills/google-calendar/TOOLS.json +20 -0
- package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +8 -2
- package/src/config/bundled-skills/messaging/SKILL.md +1 -1
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-read.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-search.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/shared.ts +7 -5
- package/src/config/bundled-skills/slack/tools/shared.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +1 -1
- package/src/config/bundled-tool-registry.ts +2 -5
- package/src/config/loader.ts +6 -42
- package/src/config/schema.ts +1 -12
- package/src/config/schemas/memory-lifecycle.ts +0 -9
- package/src/config/schemas/memory-processing.ts +0 -180
- package/src/config/schemas/memory-retrieval.ts +32 -104
- package/src/config/schemas/memory.ts +0 -10
- package/src/config/types.ts +0 -4
- package/src/contacts/contact-store.ts +39 -2
- package/src/contacts/contacts-write.ts +9 -0
- package/src/context/window-manager.ts +4 -1
- package/src/daemon/config-watcher.ts +55 -2
- package/src/daemon/daemon-control.ts +1 -1
- package/src/daemon/date-context.ts +114 -31
- package/src/daemon/handlers/config-ingress.ts +2 -2
- package/src/daemon/handlers/config-slack-channel.ts +59 -39
- package/src/daemon/handlers/config-telegram.ts +23 -14
- package/src/daemon/handlers/session-history.ts +1 -358
- package/src/daemon/handlers/sessions.ts +18 -13
- package/src/daemon/handlers/shared.ts +3 -17
- package/src/daemon/handlers/skills.ts +20 -1
- package/src/daemon/history-repair.ts +72 -8
- package/src/daemon/host-cu-proxy.ts +55 -26
- package/src/daemon/lifecycle.ts +39 -4
- package/src/daemon/mcp-reload-service.ts +2 -2
- package/src/daemon/message-types/computer-use.ts +1 -12
- package/src/daemon/message-types/memory.ts +4 -16
- package/src/daemon/message-types/messages.ts +1 -0
- package/src/daemon/message-types/sessions.ts +4 -42
- package/src/daemon/server.ts +6 -1
- package/src/daemon/session-agent-loop-handlers.ts +38 -0
- package/src/daemon/session-agent-loop.ts +334 -48
- package/src/daemon/session-error.ts +89 -6
- package/src/daemon/session-history.ts +17 -7
- package/src/daemon/session-media-retry.ts +6 -2
- package/src/daemon/session-memory.ts +69 -149
- package/src/daemon/session-process.ts +10 -1
- package/src/daemon/session-runtime-assembly.ts +49 -19
- package/src/daemon/session-slash.ts +3 -5
- package/src/daemon/session-surfaces.ts +4 -1
- package/src/daemon/session-tool-setup.ts +7 -1
- package/src/daemon/session.ts +12 -2
- package/src/email/providers/index.ts +2 -2
- package/src/instrument.ts +61 -1
- package/src/media/avatar-router.ts +1 -1
- package/src/memory/admin.ts +2 -191
- package/src/memory/canonical-guardian-store.ts +38 -2
- package/src/memory/conversation-crud.ts +0 -33
- package/src/memory/conversation-queries.ts +25 -83
- package/src/memory/db-init.ts +32 -0
- package/src/memory/embedding-backend.ts +84 -8
- package/src/memory/embedding-types.ts +9 -1
- package/src/memory/indexer.ts +7 -46
- package/src/memory/invite-store.ts +19 -0
- package/src/memory/items-extractor.ts +274 -76
- package/src/memory/job-handlers/backfill.ts +2 -127
- package/src/memory/job-handlers/cleanup.ts +2 -16
- package/src/memory/job-handlers/extraction.ts +2 -138
- package/src/memory/job-handlers/index-maintenance.ts +1 -6
- package/src/memory/job-handlers/summarization.ts +3 -148
- package/src/memory/job-utils.ts +21 -59
- package/src/memory/jobs-store.ts +1 -159
- package/src/memory/jobs-worker.ts +9 -52
- package/src/memory/migrations/104-core-indexes.ts +3 -3
- package/src/memory/migrations/149-oauth-tables.ts +2 -0
- package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +98 -0
- package/src/memory/migrations/151-oauth-providers-ping-url.ts +11 -0
- package/src/memory/migrations/152-memory-item-supersession.ts +44 -0
- package/src/memory/migrations/153-drop-entity-tables.ts +15 -0
- package/src/memory/migrations/154-drop-fts.ts +20 -0
- package/src/memory/migrations/155-drop-conflicts.ts +7 -0
- package/src/memory/migrations/156-call-session-invite-metadata.ts +24 -0
- package/src/memory/migrations/157-invite-contact-id.ts +104 -0
- package/src/memory/migrations/index.ts +8 -0
- package/src/memory/migrations/registry.ts +6 -0
- package/src/memory/qdrant-client.ts +148 -51
- package/src/memory/raw-query.ts +1 -1
- package/src/memory/retriever.test.ts +294 -273
- package/src/memory/retriever.ts +421 -645
- package/src/memory/schema/calls.ts +2 -0
- package/src/memory/schema/contacts.ts +1 -0
- package/src/memory/schema/memory-core.ts +3 -48
- package/src/memory/schema/oauth.ts +2 -0
- package/src/memory/search/formatting.ts +263 -176
- package/src/memory/search/lexical.ts +1 -254
- package/src/memory/search/ranking.ts +0 -455
- package/src/memory/search/semantic.ts +100 -14
- package/src/memory/search/staleness.ts +47 -0
- package/src/memory/search/tier-classifier.ts +21 -0
- package/src/memory/search/types.ts +15 -77
- package/src/memory/task-memory-cleanup.ts +4 -6
- package/src/messaging/provider.ts +1 -1
- package/src/messaging/providers/gmail/adapter.ts +1 -1
- package/src/messaging/providers/gmail/mime-builder.ts +17 -7
- package/src/messaging/providers/telegram-bot/adapter.ts +17 -8
- package/src/messaging/providers/whatsapp/adapter.ts +13 -9
- package/src/messaging/registry.ts +9 -5
- package/src/oauth/byo-connection.test.ts +40 -25
- package/src/oauth/connect-orchestrator.ts +4 -10
- package/src/oauth/connection-resolver.ts +20 -6
- package/src/oauth/manual-token-connection.ts +5 -5
- package/src/oauth/oauth-store.ts +183 -31
- package/src/oauth/platform-connection.test.ts +1 -1
- package/src/oauth/provider-behaviors.ts +503 -4
- package/src/oauth/seed-providers.ts +214 -8
- package/src/oauth/token-persistence.ts +31 -16
- package/src/permissions/defaults.ts +1 -0
- package/src/permissions/trust-store.ts +23 -1
- package/src/playbooks/playbook-compiler.ts +1 -1
- package/src/prompts/system-prompt.ts +18 -2
- package/src/providers/anthropic/client.ts +56 -126
- package/src/providers/types.ts +7 -1
- package/src/runtime/AGENTS.md +9 -0
- package/src/runtime/auth/route-policy.ts +6 -3
- package/src/runtime/channel-readiness-service.ts +48 -40
- package/src/runtime/guardian-reply-router.ts +24 -22
- package/src/runtime/http-server.ts +2 -2
- package/src/runtime/http-types.ts +2 -0
- package/src/runtime/invite-redemption-service.ts +72 -12
- package/src/runtime/invite-service.ts +43 -0
- package/src/runtime/middleware/twilio-validation.ts +1 -1
- package/src/runtime/pending-interactions.ts +2 -2
- package/src/runtime/routes/brain-graph-routes.ts +10 -90
- package/src/runtime/routes/btw-routes.ts +10 -5
- package/src/runtime/routes/conversation-routes.ts +56 -11
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -12
- package/src/runtime/routes/integrations/slack/channel.ts +2 -2
- package/src/runtime/routes/integrations/telegram.ts +2 -2
- package/src/runtime/routes/integrations/twilio.ts +17 -17
- package/src/runtime/routes/invite-routes.ts +29 -4
- package/src/runtime/routes/memory-item-routes.test.ts +754 -0
- package/src/runtime/routes/memory-item-routes.ts +503 -0
- package/src/runtime/routes/secret-routes.ts +17 -0
- package/src/runtime/routes/session-management-routes.ts +3 -3
- package/src/runtime/routes/settings-routes.ts +3 -3
- package/src/runtime/routes/trust-rules-routes.ts +14 -0
- package/src/runtime/routes/workspace-routes.ts +9 -4
- package/src/runtime/routes/workspace-utils.ts +8 -2
- package/src/schedule/integration-status.ts +26 -19
- package/src/security/keychain-broker-client.ts +17 -4
- package/src/security/oauth2.ts +6 -7
- package/src/security/secure-keys.ts +44 -19
- package/src/security/token-manager.ts +46 -39
- package/src/services/vercel-deploy.ts +0 -24
- package/src/signals/confirm.ts +78 -0
- package/src/signals/mcp-reload.ts +18 -0
- package/src/skills/catalog-install.ts +74 -18
- package/src/skills/skillssh-registry.ts +503 -0
- package/src/tools/assets/search.ts +5 -1
- package/src/tools/computer-use/definitions.ts +0 -10
- package/src/tools/computer-use/registry.ts +1 -1
- package/src/tools/credentials/vault.ts +22 -7
- package/src/tools/memory/definitions.ts +4 -13
- package/src/tools/memory/handlers.test.ts +83 -103
- package/src/tools/memory/handlers.ts +50 -85
- package/src/tools/network/script-proxy/session-manager.ts +8 -8
- package/src/tools/schedule/create.ts +10 -3
- package/src/tools/schedule/update.ts +8 -1
- package/src/tools/skills/load.ts +25 -2
- package/src/watcher/provider-types.ts +1 -1
- package/src/watcher/providers/github.ts +1 -1
- package/src/watcher/providers/gmail.ts +3 -3
- package/src/watcher/providers/google-calendar.ts +3 -3
- package/src/watcher/providers/linear.ts +1 -1
- package/src/__tests__/clarification-resolver.test.ts +0 -193
- package/src/__tests__/conflict-intent-tokenization.test.ts +0 -160
- package/src/__tests__/conflict-policy.test.ts +0 -269
- package/src/__tests__/conflict-store.test.ts +0 -372
- package/src/__tests__/contradiction-checker.test.ts +0 -361
- package/src/__tests__/entity-extractor.test.ts +0 -211
- package/src/__tests__/entity-search.test.ts +0 -1117
- package/src/__tests__/profile-compiler.test.ts +0 -392
- package/src/__tests__/session-conflict-gate.test.ts +0 -1228
- package/src/__tests__/session-profile-injection.test.ts +0 -557
- package/src/config/bundled-skills/knowledge-graph/SKILL.md +0 -25
- package/src/config/bundled-skills/knowledge-graph/TOOLS.json +0 -66
- package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +0 -211
- package/src/daemon/session-conflict-gate.ts +0 -167
- package/src/daemon/session-dynamic-profile.ts +0 -77
- package/src/memory/clarification-resolver.ts +0 -417
- package/src/memory/conflict-intent.ts +0 -205
- package/src/memory/conflict-policy.ts +0 -127
- package/src/memory/conflict-store.ts +0 -410
- package/src/memory/contradiction-checker.ts +0 -508
- package/src/memory/entity-extractor.ts +0 -535
- package/src/memory/format-recall.ts +0 -47
- package/src/memory/fts-reconciler.ts +0 -165
- package/src/memory/job-handlers/conflict.ts +0 -200
- package/src/memory/profile-compiler.ts +0 -195
- package/src/memory/recall-cache.ts +0 -117
- package/src/memory/search/entity.ts +0 -535
- package/src/memory/search/query-expansion.test.ts +0 -70
- package/src/memory/search/query-expansion.ts +0 -118
- package/src/runtime/routes/mcp-routes.ts +0 -20
|
@@ -189,6 +189,7 @@ import {
|
|
|
189
189
|
RelayConnection,
|
|
190
190
|
} from "../calls/relay-server.js";
|
|
191
191
|
import { setVoiceBridgeDeps } from "../calls/voice-session-bridge.js";
|
|
192
|
+
import { upsertContact } from "../contacts/contact-store.js";
|
|
192
193
|
import {
|
|
193
194
|
createGuardianBinding,
|
|
194
195
|
upsertContactChannel,
|
|
@@ -289,6 +290,11 @@ function resetTables() {
|
|
|
289
290
|
ensuredConvIds = new Set();
|
|
290
291
|
}
|
|
291
292
|
|
|
293
|
+
/** Create a throwaway contact and return its ID, for use as the invite's contactId. */
|
|
294
|
+
function createTargetContact(displayName = "Test Contact"): string {
|
|
295
|
+
return upsertContact({ displayName, role: "contact" }).id;
|
|
296
|
+
}
|
|
297
|
+
|
|
292
298
|
function addTrustedVoiceContact(phoneNumber: string): void {
|
|
293
299
|
upsertContactChannel({
|
|
294
300
|
sourceChannel: "phone",
|
|
@@ -2053,6 +2059,7 @@ describe("relay-server", () => {
|
|
|
2053
2059
|
const codeHash = hashVoiceCode(code);
|
|
2054
2060
|
createInvite({
|
|
2055
2061
|
sourceChannel: "phone",
|
|
2062
|
+
contactId: createTargetContact(),
|
|
2056
2063
|
maxUses: 1,
|
|
2057
2064
|
expectedExternalUserId: "+15558887777",
|
|
2058
2065
|
voiceCodeHash: codeHash,
|
|
@@ -2126,6 +2133,7 @@ describe("relay-server", () => {
|
|
|
2126
2133
|
const codeHash = hashVoiceCode(code);
|
|
2127
2134
|
createInvite({
|
|
2128
2135
|
sourceChannel: "phone",
|
|
2136
|
+
contactId: createTargetContact(),
|
|
2129
2137
|
maxUses: 1,
|
|
2130
2138
|
expectedExternalUserId: "+15558886666",
|
|
2131
2139
|
voiceCodeHash: codeHash,
|
|
@@ -4013,6 +4021,7 @@ describe("relay-server", () => {
|
|
|
4013
4021
|
const codeHash = hashVoiceCode(code);
|
|
4014
4022
|
createInvite({
|
|
4015
4023
|
sourceChannel: "phone",
|
|
4024
|
+
contactId: createTargetContact(),
|
|
4016
4025
|
maxUses: 1,
|
|
4017
4026
|
expectedExternalUserId: "+15557776666",
|
|
4018
4027
|
voiceCodeHash: codeHash,
|
|
@@ -4055,7 +4064,7 @@ describe("relay-server", () => {
|
|
|
4055
4064
|
.filter((m) => m.type === "text");
|
|
4056
4065
|
expect(
|
|
4057
4066
|
textMessages.some((m) =>
|
|
4058
|
-
(m.token ?? "").includes("
|
|
4067
|
+
(m.token ?? "").includes("verified that you are Eve"),
|
|
4059
4068
|
),
|
|
4060
4069
|
).toBe(true);
|
|
4061
4070
|
|
|
@@ -4079,6 +4088,51 @@ describe("relay-server", () => {
|
|
|
4079
4088
|
relay.destroy();
|
|
4080
4089
|
});
|
|
4081
4090
|
|
|
4091
|
+
test("outbound invite prompt uses assistant introduction", async () => {
|
|
4092
|
+
ensureConversation("conv-outbound-invite-origin");
|
|
4093
|
+
ensureConversation("conv-outbound-invite");
|
|
4094
|
+
const session = createCallSession({
|
|
4095
|
+
conversationId: "conv-outbound-invite",
|
|
4096
|
+
provider: "twilio",
|
|
4097
|
+
fromNumber: "+15551111111",
|
|
4098
|
+
toNumber: "+15558887777",
|
|
4099
|
+
callMode: "invite",
|
|
4100
|
+
inviteFriendName: "Grace",
|
|
4101
|
+
inviteGuardianName: "Hank",
|
|
4102
|
+
initiatedFromConversationId: "conv-outbound-invite-origin",
|
|
4103
|
+
});
|
|
4104
|
+
|
|
4105
|
+
mockAssistantName = "Vellum";
|
|
4106
|
+
|
|
4107
|
+
const { ws, relay } = createMockWs(session.id);
|
|
4108
|
+
|
|
4109
|
+
await relay.handleMessage(
|
|
4110
|
+
JSON.stringify({
|
|
4111
|
+
type: "setup",
|
|
4112
|
+
callSid: "CA_outbound_invite",
|
|
4113
|
+
from: "+15551111111",
|
|
4114
|
+
to: "+15558887777",
|
|
4115
|
+
}),
|
|
4116
|
+
);
|
|
4117
|
+
|
|
4118
|
+
// Should be in verification-pending for invite redemption
|
|
4119
|
+
expect(relay.getConnectionState()).toBe("verification_pending");
|
|
4120
|
+
|
|
4121
|
+
// The prompt should use the outbound assistant introduction
|
|
4122
|
+
const textMessages = ws.sentMessages
|
|
4123
|
+
.map((raw) => JSON.parse(raw) as { type: string; token?: string })
|
|
4124
|
+
.filter((m) => m.type === "text");
|
|
4125
|
+
expect(
|
|
4126
|
+
textMessages.some(
|
|
4127
|
+
(m) =>
|
|
4128
|
+
(m.token ?? "").includes("this is Vellum") &&
|
|
4129
|
+
(m.token ?? "").includes("Hank's assistant"),
|
|
4130
|
+
),
|
|
4131
|
+
).toBe(true);
|
|
4132
|
+
|
|
4133
|
+
relay.destroy();
|
|
4134
|
+
});
|
|
4135
|
+
|
|
4082
4136
|
// ── resolveGuardianLabel resolution priority ─────────────────────────
|
|
4083
4137
|
|
|
4084
4138
|
test("guardian label: USER.md name takes precedence over Contact.displayName", async () => {
|
|
@@ -61,6 +61,11 @@ const ctx: ToolContext = {
|
|
|
61
61
|
trustClass: "guardian",
|
|
62
62
|
};
|
|
63
63
|
|
|
64
|
+
const trustedCtx: ToolContext = {
|
|
65
|
+
...ctx,
|
|
66
|
+
trustClass: "trusted_contact",
|
|
67
|
+
};
|
|
68
|
+
|
|
64
69
|
// ── schedule_create ─────────────────────────────────────────────────
|
|
65
70
|
|
|
66
71
|
describe("schedule_create tool", () => {
|
|
@@ -169,6 +174,20 @@ describe("schedule_create tool", () => {
|
|
|
169
174
|
expect(result.isError).toBe(true);
|
|
170
175
|
expect(result.content).toContain("Invalid cron expression");
|
|
171
176
|
});
|
|
177
|
+
|
|
178
|
+
test("rejects non-guardian actors", async () => {
|
|
179
|
+
const result = await executeScheduleCreate(
|
|
180
|
+
{
|
|
181
|
+
name: "Blocked schedule",
|
|
182
|
+
expression: "0 9 * * *",
|
|
183
|
+
message: "test",
|
|
184
|
+
},
|
|
185
|
+
trustedCtx,
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
expect(result.isError).toBe(true);
|
|
189
|
+
expect(result.content).toContain("restricted to guardian actors");
|
|
190
|
+
});
|
|
172
191
|
});
|
|
173
192
|
|
|
174
193
|
// ── schedule_create with fire_at (one-shot) ──────────────────────────
|
|
@@ -680,6 +699,19 @@ describe("schedule_update tool", () => {
|
|
|
680
699
|
expect(result.isError).toBe(true);
|
|
681
700
|
expect(result.content).toContain("Invalid cron expression");
|
|
682
701
|
});
|
|
702
|
+
|
|
703
|
+
test("rejects non-guardian actors", async () => {
|
|
704
|
+
const result = await executeScheduleUpdate(
|
|
705
|
+
{
|
|
706
|
+
job_id: "nonexistent-id",
|
|
707
|
+
message: "injected",
|
|
708
|
+
},
|
|
709
|
+
trustedCtx,
|
|
710
|
+
);
|
|
711
|
+
|
|
712
|
+
expect(result.isError).toBe(true);
|
|
713
|
+
expect(result.content).toContain("restricted to guardian actors");
|
|
714
|
+
});
|
|
683
715
|
});
|
|
684
716
|
|
|
685
717
|
// ── schedule_update with mode and routing ────────────────────────────
|
|
@@ -43,6 +43,7 @@ mock.module("../security/secure-keys.js", () => {
|
|
|
43
43
|
};
|
|
44
44
|
return {
|
|
45
45
|
getSecureKey: (key: string) => storedKeys.get(key) ?? null,
|
|
46
|
+
getSecureKeyAsync: async (key: string) => storedKeys.get(key) ?? undefined,
|
|
46
47
|
setSecureKey: syncSet,
|
|
47
48
|
setSecureKeyAsync: async (key: string, value: string) =>
|
|
48
49
|
syncSet(key, value),
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { credentialKey } from "../security/credential-key.js";
|
|
4
|
+
|
|
5
|
+
let lastGeminiConstructorOpts: Record<string, unknown> | null = null;
|
|
6
|
+
let secureKeyStore: Record<string, string | undefined> = {};
|
|
7
|
+
const metadataUpserts: Array<{ service: string; field: string }> = [];
|
|
8
|
+
const metadataDeletes: Array<{ service: string; field: string }> = [];
|
|
9
|
+
|
|
10
|
+
const PLATFORM_BASE_URL = "https://platform.example.com";
|
|
11
|
+
const ASSISTANT_API_KEY_PATH = credentialKey("vellum", "assistant_api_key");
|
|
12
|
+
const MANAGED_PROVIDERS = [
|
|
13
|
+
"anthropic",
|
|
14
|
+
"openai",
|
|
15
|
+
"gemini",
|
|
16
|
+
"fireworks",
|
|
17
|
+
"openrouter",
|
|
18
|
+
] as const;
|
|
19
|
+
|
|
20
|
+
const mockConfig = {
|
|
21
|
+
apiKeys: {},
|
|
22
|
+
provider: "anthropic",
|
|
23
|
+
model: "test-model",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
mock.module("@google/genai", () => ({
|
|
27
|
+
GoogleGenAI: class MockGoogleGenAI {
|
|
28
|
+
constructor(opts: Record<string, unknown>) {
|
|
29
|
+
lastGeminiConstructorOpts = opts;
|
|
30
|
+
}
|
|
31
|
+
models = {
|
|
32
|
+
generateContentStream: async () => ({
|
|
33
|
+
[Symbol.asyncIterator]: async function* () {
|
|
34
|
+
/* no chunks */
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
ApiError: class FakeApiError extends Error {
|
|
40
|
+
status: number;
|
|
41
|
+
constructor(status: number, message: string) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.status = status;
|
|
44
|
+
this.name = "ApiError";
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
mock.module("../config/env.js", () => ({
|
|
50
|
+
getPlatformBaseUrl: () => PLATFORM_BASE_URL,
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
mock.module("../config/loader.js", () => ({
|
|
54
|
+
API_KEY_PROVIDERS: [
|
|
55
|
+
"anthropic",
|
|
56
|
+
"openai",
|
|
57
|
+
"gemini",
|
|
58
|
+
"fireworks",
|
|
59
|
+
"openrouter",
|
|
60
|
+
],
|
|
61
|
+
getConfig: () => mockConfig,
|
|
62
|
+
invalidateConfigCache: () => {},
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
mock.module("../logfire.js", () => ({
|
|
66
|
+
wrapWithLogfire: (provider: unknown) => provider,
|
|
67
|
+
}));
|
|
68
|
+
|
|
69
|
+
mock.module("../security/secure-keys.js", () => ({
|
|
70
|
+
getSecureKey: (key: string) => secureKeyStore[key],
|
|
71
|
+
getSecureKeyAsync: async (key: string) => secureKeyStore[key],
|
|
72
|
+
setSecureKeyAsync: async (key: string, value: string) => {
|
|
73
|
+
secureKeyStore[key] = value;
|
|
74
|
+
return true;
|
|
75
|
+
},
|
|
76
|
+
deleteSecureKeyAsync: async (key: string) => {
|
|
77
|
+
delete secureKeyStore[key];
|
|
78
|
+
return "deleted";
|
|
79
|
+
},
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
mock.module("../tools/credentials/metadata-store.js", () => ({
|
|
83
|
+
assertMetadataWritable: () => {},
|
|
84
|
+
upsertCredentialMetadata: (service: string, field: string) => {
|
|
85
|
+
metadataUpserts.push({ service, field });
|
|
86
|
+
},
|
|
87
|
+
deleteCredentialMetadata: (service: string, field: string) => {
|
|
88
|
+
metadataDeletes.push({ service, field });
|
|
89
|
+
},
|
|
90
|
+
}));
|
|
91
|
+
|
|
92
|
+
mock.module("../util/logger.js", () => ({
|
|
93
|
+
getLogger: () =>
|
|
94
|
+
new Proxy({} as Record<string, unknown>, {
|
|
95
|
+
get: () => () => {},
|
|
96
|
+
}),
|
|
97
|
+
}));
|
|
98
|
+
|
|
99
|
+
import {
|
|
100
|
+
getProviderRoutingSource,
|
|
101
|
+
initializeProviders,
|
|
102
|
+
listProviders,
|
|
103
|
+
} from "../providers/registry.js";
|
|
104
|
+
import {
|
|
105
|
+
handleAddSecret,
|
|
106
|
+
handleDeleteSecret,
|
|
107
|
+
} from "../runtime/routes/secret-routes.js";
|
|
108
|
+
|
|
109
|
+
function makeAddCredentialRequest(name: string, value: string): Request {
|
|
110
|
+
return new Request("http://localhost/v1/secrets", {
|
|
111
|
+
method: "POST",
|
|
112
|
+
headers: { "Content-Type": "application/json" },
|
|
113
|
+
body: JSON.stringify({
|
|
114
|
+
type: "credential",
|
|
115
|
+
name,
|
|
116
|
+
value,
|
|
117
|
+
}),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function makeDeleteCredentialRequest(name: string): Request {
|
|
122
|
+
return new Request("http://localhost/v1/secrets", {
|
|
123
|
+
method: "DELETE",
|
|
124
|
+
headers: { "Content-Type": "application/json" },
|
|
125
|
+
body: JSON.stringify({
|
|
126
|
+
type: "credential",
|
|
127
|
+
name,
|
|
128
|
+
}),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
describe("secret routes managed proxy registry sync", () => {
|
|
133
|
+
beforeEach(() => {
|
|
134
|
+
secureKeyStore = {};
|
|
135
|
+
metadataUpserts.length = 0;
|
|
136
|
+
metadataDeletes.length = 0;
|
|
137
|
+
lastGeminiConstructorOpts = null;
|
|
138
|
+
initializeProviders(mockConfig);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("adding vellum:assistant_api_key bootstraps managed providers immediately", async () => {
|
|
142
|
+
expect(listProviders()).toEqual([]);
|
|
143
|
+
|
|
144
|
+
const res = await handleAddSecret(
|
|
145
|
+
makeAddCredentialRequest("vellum:assistant_api_key", "ast-managed-key"),
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
expect(res.status).toBe(201);
|
|
149
|
+
expect(secureKeyStore[ASSISTANT_API_KEY_PATH]).toBe("ast-managed-key");
|
|
150
|
+
expect(metadataUpserts).toEqual([
|
|
151
|
+
{ service: "vellum", field: "assistant_api_key" },
|
|
152
|
+
]);
|
|
153
|
+
|
|
154
|
+
const providers = listProviders();
|
|
155
|
+
expect(providers).toHaveLength(MANAGED_PROVIDERS.length);
|
|
156
|
+
for (const provider of MANAGED_PROVIDERS) {
|
|
157
|
+
expect(providers).toContain(provider);
|
|
158
|
+
expect(getProviderRoutingSource(provider)).toBe("managed-proxy");
|
|
159
|
+
}
|
|
160
|
+
expect(lastGeminiConstructorOpts).toBeDefined();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("deleting vellum:assistant_api_key clears managed providers immediately", async () => {
|
|
164
|
+
secureKeyStore[ASSISTANT_API_KEY_PATH] = "ast-managed-key";
|
|
165
|
+
initializeProviders(mockConfig);
|
|
166
|
+
|
|
167
|
+
for (const provider of MANAGED_PROVIDERS) {
|
|
168
|
+
expect(listProviders()).toContain(provider);
|
|
169
|
+
expect(getProviderRoutingSource(provider)).toBe("managed-proxy");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const res = await handleDeleteSecret(
|
|
173
|
+
makeDeleteCredentialRequest("vellum:assistant_api_key"),
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
expect(res.status).toBe(200);
|
|
177
|
+
expect(secureKeyStore[ASSISTANT_API_KEY_PATH]).toBeUndefined();
|
|
178
|
+
expect(metadataDeletes).toEqual([
|
|
179
|
+
{ service: "vellum", field: "assistant_api_key" },
|
|
180
|
+
]);
|
|
181
|
+
expect(listProviders()).toEqual([]);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
@@ -33,12 +33,14 @@ let mockBrokerStore: Map<string, string> = new Map();
|
|
|
33
33
|
let mockBrokerGetError = false;
|
|
34
34
|
let mockBrokerSetError = false;
|
|
35
35
|
let mockBrokerDelError = false;
|
|
36
|
+
let mockBrokerGetCalled = false;
|
|
36
37
|
|
|
37
38
|
mock.module("../security/keychain-broker-client.js", () => ({
|
|
38
39
|
createBrokerClient: () => ({
|
|
39
40
|
isAvailable: () => mockBrokerAvailable,
|
|
40
41
|
ping: async () => (mockBrokerAvailable ? { pong: true } : null),
|
|
41
42
|
get: async (account: string) => {
|
|
43
|
+
mockBrokerGetCalled = true;
|
|
42
44
|
// null = broker error (fall back to encrypted store)
|
|
43
45
|
if (mockBrokerGetError) return null;
|
|
44
46
|
const value = mockBrokerStore.get(account);
|
|
@@ -46,9 +48,14 @@ mock.module("../security/keychain-broker-client.js", () => ({
|
|
|
46
48
|
return { found: false };
|
|
47
49
|
},
|
|
48
50
|
set: async (account: string, value: string) => {
|
|
49
|
-
if (mockBrokerSetError)
|
|
51
|
+
if (mockBrokerSetError)
|
|
52
|
+
return {
|
|
53
|
+
status: "rejected" as const,
|
|
54
|
+
code: "KEYCHAIN_ERROR",
|
|
55
|
+
message: "mock error",
|
|
56
|
+
};
|
|
50
57
|
mockBrokerStore.set(account, value);
|
|
51
|
-
return
|
|
58
|
+
return { status: "ok" as const };
|
|
52
59
|
},
|
|
53
60
|
del: async (account: string) => {
|
|
54
61
|
if (mockBrokerDelError) return false;
|
|
@@ -95,6 +102,7 @@ describe("secure-keys", () => {
|
|
|
95
102
|
mockBrokerGetError = false;
|
|
96
103
|
mockBrokerSetError = false;
|
|
97
104
|
mockBrokerDelError = false;
|
|
105
|
+
mockBrokerGetCalled = false;
|
|
98
106
|
|
|
99
107
|
if (existsSync(TEST_DIR)) {
|
|
100
108
|
rmSync(TEST_DIR, { recursive: true });
|
|
@@ -202,20 +210,22 @@ describe("secure-keys", () => {
|
|
|
202
210
|
// Async variants — broker available path
|
|
203
211
|
// -----------------------------------------------------------------------
|
|
204
212
|
describe("async variants with broker available", () => {
|
|
205
|
-
test("getSecureKeyAsync returns
|
|
213
|
+
test("getSecureKeyAsync returns encrypted store value when both stores have key", async () => {
|
|
206
214
|
mockBrokerAvailable = true;
|
|
207
215
|
mockBrokerStore.set("api-key", "broker-value");
|
|
208
216
|
setSecureKey("api-key", "encrypted-value");
|
|
209
|
-
|
|
217
|
+
// Encrypted store is checked first — broker is never called
|
|
218
|
+
expect(await getSecureKeyAsync("api-key")).toBe("encrypted-value");
|
|
219
|
+
expect(mockBrokerGetCalled).toBe(false);
|
|
210
220
|
});
|
|
211
221
|
|
|
212
|
-
test("getSecureKeyAsync
|
|
222
|
+
test("getSecureKeyAsync returns encrypted store value without calling broker", async () => {
|
|
213
223
|
mockBrokerAvailable = true;
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
// was unavailable or via sync setSecureKey), so we must fall back.
|
|
224
|
+
// Only encrypted store has the key — broker has nothing.
|
|
225
|
+
// Encrypted store is checked first, so broker.get() is never called.
|
|
217
226
|
setSecureKey("api-key", "encrypted-value");
|
|
218
227
|
expect(await getSecureKeyAsync("api-key")).toBe("encrypted-value");
|
|
228
|
+
expect(mockBrokerGetCalled).toBe(false);
|
|
219
229
|
});
|
|
220
230
|
|
|
221
231
|
test("getSecureKeyAsync returns undefined when neither broker nor encrypted store has key", async () => {
|
|
@@ -224,11 +234,14 @@ describe("secure-keys", () => {
|
|
|
224
234
|
expect(await getSecureKeyAsync("missing-key")).toBeUndefined();
|
|
225
235
|
});
|
|
226
236
|
|
|
227
|
-
test("getSecureKeyAsync
|
|
237
|
+
test("getSecureKeyAsync returns encrypted store value even when broker would error", async () => {
|
|
228
238
|
mockBrokerAvailable = true;
|
|
229
239
|
mockBrokerGetError = true;
|
|
240
|
+
// Encrypted store hit short-circuits — broker is never called, so
|
|
241
|
+
// the broker error flag is irrelevant.
|
|
230
242
|
setSecureKey("api-key", "encrypted-value");
|
|
231
243
|
expect(await getSecureKeyAsync("api-key")).toBe("encrypted-value");
|
|
244
|
+
expect(mockBrokerGetCalled).toBe(false);
|
|
232
245
|
});
|
|
233
246
|
|
|
234
247
|
test("setSecureKeyAsync writes to broker and encrypted store", async () => {
|
|
@@ -303,10 +316,56 @@ describe("secure-keys", () => {
|
|
|
303
316
|
});
|
|
304
317
|
|
|
305
318
|
// -----------------------------------------------------------------------
|
|
306
|
-
//
|
|
319
|
+
// Encrypted-store-first read order
|
|
320
|
+
// -----------------------------------------------------------------------
|
|
321
|
+
describe("encrypted-store-first read order", () => {
|
|
322
|
+
test("returns value from encrypted store without calling broker", async () => {
|
|
323
|
+
mockBrokerAvailable = true;
|
|
324
|
+
setSecureKey("test-account", "test-secret");
|
|
325
|
+
mockBrokerStore.set("test-account", "broker-secret");
|
|
326
|
+
|
|
327
|
+
const result = await getSecureKeyAsync("test-account");
|
|
328
|
+
expect(result).toBe("test-secret");
|
|
329
|
+
// Broker should never have been called — encrypted store hit
|
|
330
|
+
// short-circuits the lookup.
|
|
331
|
+
expect(mockBrokerGetCalled).toBe(false);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test("falls back to broker when encrypted store returns undefined", async () => {
|
|
335
|
+
mockBrokerAvailable = true;
|
|
336
|
+
// Encrypted store has nothing for this key
|
|
337
|
+
mockBrokerStore.set("test-account", "broker-secret");
|
|
338
|
+
|
|
339
|
+
const result = await getSecureKeyAsync("test-account");
|
|
340
|
+
expect(result).toBe("broker-secret");
|
|
341
|
+
// Broker should have been called as fallback
|
|
342
|
+
expect(mockBrokerGetCalled).toBe(true);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("returns undefined when neither store has the key", async () => {
|
|
346
|
+
mockBrokerAvailable = true;
|
|
347
|
+
// Neither encrypted store nor broker has the key
|
|
348
|
+
|
|
349
|
+
const result = await getSecureKeyAsync("test-account");
|
|
350
|
+
expect(result).toBeUndefined();
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test("returns undefined without broker call when broker unavailable and encrypted store empty", async () => {
|
|
354
|
+
// Broker is unavailable (default state), encrypted store is empty
|
|
355
|
+
mockBrokerAvailable = false;
|
|
356
|
+
|
|
357
|
+
const result = await getSecureKeyAsync("test-account");
|
|
358
|
+
expect(result).toBeUndefined();
|
|
359
|
+
// Broker.get() should not have been called since broker is unavailable
|
|
360
|
+
expect(mockBrokerGetCalled).toBe(false);
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// -----------------------------------------------------------------------
|
|
365
|
+
// Stale-value prevention — encrypted-store-first reads avoid stale broker data
|
|
307
366
|
// -----------------------------------------------------------------------
|
|
308
367
|
describe("stale-value prevention", () => {
|
|
309
|
-
test("setSecureKeyAsync updates
|
|
368
|
+
test("setSecureKeyAsync updates both stores so encrypted-store-first read returns new value", async () => {
|
|
310
369
|
mockBrokerAvailable = true;
|
|
311
370
|
// Simulate broker holding an old value
|
|
312
371
|
mockBrokerStore.set("api-key", "old-broker-value");
|
|
@@ -316,7 +375,7 @@ describe("secure-keys", () => {
|
|
|
316
375
|
const ok = await setSecureKeyAsync("api-key", "new-value");
|
|
317
376
|
expect(ok).toBe(true);
|
|
318
377
|
|
|
319
|
-
//
|
|
378
|
+
// Encrypted-store-first read returns the new value
|
|
320
379
|
const value = await getSecureKeyAsync("api-key");
|
|
321
380
|
expect(value).toBe("new-value");
|
|
322
381
|
// Both stores should agree
|
|
@@ -324,7 +383,7 @@ describe("secure-keys", () => {
|
|
|
324
383
|
expect(getSecureKey("api-key")).toBe("new-value");
|
|
325
384
|
});
|
|
326
385
|
|
|
327
|
-
test("deleteSecureKeyAsync removes from
|
|
386
|
+
test("deleteSecureKeyAsync removes from both stores so read returns undefined", async () => {
|
|
328
387
|
mockBrokerAvailable = true;
|
|
329
388
|
mockBrokerStore.set("api-key", "old-broker-value");
|
|
330
389
|
setSecureKey("api-key", "old-encrypted-value");
|
|
@@ -333,22 +392,23 @@ describe("secure-keys", () => {
|
|
|
333
392
|
const result = await deleteSecureKeyAsync("api-key");
|
|
334
393
|
expect(result).toBe("deleted");
|
|
335
394
|
|
|
336
|
-
//
|
|
395
|
+
// Neither store has the key — returns undefined
|
|
337
396
|
const value = await getSecureKeyAsync("api-key");
|
|
338
397
|
expect(value).toBeUndefined();
|
|
339
398
|
});
|
|
340
399
|
|
|
341
|
-
test("sync setSecureKey
|
|
400
|
+
test("sync setSecureKey updates encrypted store — encrypted-store-first read returns fresh value", async () => {
|
|
342
401
|
mockBrokerAvailable = true;
|
|
343
402
|
mockBrokerStore.set("api-key", "old-broker-value");
|
|
344
403
|
|
|
345
404
|
// Sync write only updates encrypted store, NOT broker
|
|
346
405
|
setSecureKey("api-key", "new-encrypted-value");
|
|
347
406
|
|
|
348
|
-
//
|
|
407
|
+
// Encrypted-store-first read returns the fresh encrypted value,
|
|
408
|
+
// bypassing the stale broker entirely.
|
|
349
409
|
const value = await getSecureKeyAsync("api-key");
|
|
350
|
-
expect(value).toBe("
|
|
351
|
-
|
|
410
|
+
expect(value).toBe("new-encrypted-value");
|
|
411
|
+
expect(mockBrokerGetCalled).toBe(false);
|
|
352
412
|
});
|
|
353
413
|
|
|
354
414
|
test("setSecureKeyAsync failure leaves both stores unchanged", async () => {
|
|
@@ -118,6 +118,7 @@ function makeCompletingSession(): Session {
|
|
|
118
118
|
setHostBashProxy: () => {},
|
|
119
119
|
setHostFileProxy: () => {},
|
|
120
120
|
setHostCuProxy: () => {},
|
|
121
|
+
addPreactivatedSkillId: () => {},
|
|
121
122
|
hasAnyPendingConfirmation: () => false,
|
|
122
123
|
hasPendingConfirmation: () => false,
|
|
123
124
|
denyAllPendingConfirmations: () => {},
|
|
@@ -175,6 +176,7 @@ function makeHangingSession(): Session {
|
|
|
175
176
|
setHostBashProxy: () => {},
|
|
176
177
|
setHostFileProxy: () => {},
|
|
177
178
|
setHostCuProxy: () => {},
|
|
179
|
+
addPreactivatedSkillId: () => {},
|
|
178
180
|
hasAnyPendingConfirmation: () => false,
|
|
179
181
|
hasPendingConfirmation: () => false,
|
|
180
182
|
denyAllPendingConfirmations: () => {},
|
|
@@ -260,6 +262,7 @@ function makePendingApprovalSession(
|
|
|
260
262
|
setHostBashProxy: () => {},
|
|
261
263
|
setHostFileProxy: () => {},
|
|
262
264
|
setHostCuProxy: () => {},
|
|
265
|
+
addPreactivatedSkillId: () => {},
|
|
263
266
|
hasAnyPendingConfirmation: () => pending.size > 0,
|
|
264
267
|
hasPendingConfirmation: (candidateRequestId: string) =>
|
|
265
268
|
pending.has(candidateRequestId),
|
|
@@ -78,7 +78,7 @@ describe("renderHistoryContent", () => {
|
|
|
78
78
|
);
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
-
test("
|
|
81
|
+
test("skips image attachment placeholder text (images sent as separate attachments)", () => {
|
|
82
82
|
const output = renderHistoryContent([
|
|
83
83
|
{
|
|
84
84
|
type: "image",
|
|
@@ -90,7 +90,7 @@ describe("renderHistoryContent", () => {
|
|
|
90
90
|
},
|
|
91
91
|
]);
|
|
92
92
|
|
|
93
|
-
expect(output.text).
|
|
93
|
+
expect(output.text).toBe("");
|
|
94
94
|
});
|
|
95
95
|
|
|
96
96
|
test("appends attachment lines after text content", () => {
|
|
@@ -75,18 +75,6 @@ mock.module("../security/secret-allowlist.js", () => ({
|
|
|
75
75
|
resetAllowlist: () => {},
|
|
76
76
|
}));
|
|
77
77
|
|
|
78
|
-
mock.module("../memory/admin.js", () => ({
|
|
79
|
-
getMemoryConflictAndCleanupStats: () => ({
|
|
80
|
-
conflicts: { pending: 0, resolved: 0, oldestPendingAgeMs: null },
|
|
81
|
-
cleanup: {
|
|
82
|
-
resolvedBacklog: 0,
|
|
83
|
-
supersededBacklog: 0,
|
|
84
|
-
resolvedCompleted24h: 0,
|
|
85
|
-
supersededCompleted24h: 0,
|
|
86
|
-
},
|
|
87
|
-
}),
|
|
88
|
-
}));
|
|
89
|
-
|
|
90
78
|
// Track all messages persisted to DB
|
|
91
79
|
let persistedMessages: Array<{ role: string; content: string }> = [];
|
|
92
80
|
|
|
@@ -128,13 +116,12 @@ mock.module("../memory/retriever.js", () => ({
|
|
|
128
116
|
enabled: false,
|
|
129
117
|
degraded: false,
|
|
130
118
|
injectedText: "",
|
|
131
|
-
|
|
119
|
+
|
|
132
120
|
semanticHits: 0,
|
|
133
121
|
recencyHits: 0,
|
|
134
122
|
injectedTokens: 0,
|
|
135
123
|
latencyMs: 0,
|
|
136
124
|
}),
|
|
137
|
-
injectMemoryRecallIntoUserMessage: (msg: Message) => msg,
|
|
138
125
|
stripMemoryRecallMessages: (msgs: Message[]) => msgs,
|
|
139
126
|
}));
|
|
140
127
|
|