@vellumai/assistant 0.5.6 → 0.5.7
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/.env.example +16 -2
- package/ARCHITECTURE.md +6 -75
- package/Dockerfile +1 -1
- package/README.md +0 -2
- package/bun.lock +0 -414
- package/docs/architecture/keychain-broker.md +45 -240
- package/docs/architecture/security.md +0 -17
- package/docs/credential-execution-service.md +2 -2
- package/node_modules/@vellumai/ces-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-contracts/src/rpc.ts +119 -0
- package/node_modules/@vellumai/credential-storage/package.json +1 -0
- package/node_modules/@vellumai/egress-proxy/package.json +1 -0
- package/package.json +2 -3
- package/src/__tests__/actor-token-service.test.ts +0 -114
- package/src/__tests__/assistant-feature-flags-integration.test.ts +30 -29
- package/src/__tests__/browser-skill-endstate.test.ts +6 -5
- package/src/__tests__/btw-routes.test.ts +0 -39
- package/src/__tests__/call-domain.test.ts +0 -128
- package/src/__tests__/ces-rpc-credential-backend.test.ts +199 -0
- package/src/__tests__/channel-approval-routes.test.ts +0 -5
- package/src/__tests__/channel-readiness-service.test.ts +1 -60
- package/src/__tests__/checker.test.ts +4 -2
- package/src/__tests__/cli-command-risk-guard.test.ts +112 -0
- package/src/__tests__/config-schema-cmd.test.ts +0 -1
- package/src/__tests__/config-schema.test.ts +1 -1
- package/src/__tests__/conversation-attention-telegram.test.ts +0 -5
- package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
- package/src/__tests__/conversation-skill-tools.test.ts +0 -54
- package/src/__tests__/conversation-title-service.test.ts +87 -0
- package/src/__tests__/credential-execution-feature-gates.test.ts +28 -14
- package/src/__tests__/credential-execution-managed-contract.test.ts +33 -18
- package/src/__tests__/credential-security-e2e.test.ts +0 -66
- package/src/__tests__/credential-security-invariants.test.ts +4 -45
- package/src/__tests__/credentials-cli.test.ts +78 -0
- package/src/__tests__/db-migration-rollback.test.ts +2015 -1
- package/src/__tests__/docker-signing-key-bootstrap.test.ts +34 -143
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -4
- package/src/__tests__/guardian-routing-state.test.ts +0 -5
- package/src/__tests__/host-shell-tool.test.ts +6 -7
- package/src/__tests__/http-user-message-parity.test.ts +3 -103
- package/src/__tests__/inbound-invite-redemption.test.ts +0 -4
- package/src/__tests__/inline-skill-load-permissions.test.ts +6 -8
- package/src/__tests__/intent-routing.test.ts +0 -13
- package/src/__tests__/jobs-store-qdrant-breaker.test.ts +178 -0
- package/src/__tests__/keychain-broker-client.test.ts +161 -22
- package/src/__tests__/memory-jobs-worker-backoff.test.ts +150 -0
- package/src/__tests__/migration-export-http.test.ts +2 -2
- package/src/__tests__/migration-import-commit-http.test.ts +2 -2
- package/src/__tests__/migration-import-preflight-http.test.ts +2 -2
- package/src/__tests__/migration-validate-http.test.ts +2 -2
- package/src/__tests__/non-member-access-request.test.ts +0 -5
- package/src/__tests__/notification-decision-fallback.test.ts +4 -0
- package/src/__tests__/notification-decision-identity.test.ts +4 -0
- package/src/__tests__/permission-types.test.ts +1 -0
- package/src/__tests__/provider-managed-proxy-integration.test.ts +5 -6
- package/src/__tests__/qdrant-manager.test.ts +28 -2
- package/src/__tests__/registry.test.ts +0 -6
- package/src/__tests__/runtime-attachment-metadata.test.ts +0 -4
- package/src/__tests__/secret-routes-managed-proxy.test.ts +0 -4
- package/src/__tests__/secure-keys.test.ts +83 -263
- package/src/__tests__/shell-identity.test.ts +96 -6
- package/src/__tests__/skill-feature-flags-integration.test.ts +22 -14
- package/src/__tests__/skill-feature-flags.test.ts +46 -45
- package/src/__tests__/skill-load-feature-flag.test.ts +7 -10
- package/src/__tests__/skill-load-inline-command.test.ts +8 -12
- package/src/__tests__/skill-load-inline-includes.test.ts +6 -10
- package/src/__tests__/skill-load-tool.test.ts +0 -2
- package/src/__tests__/skill-projection-feature-flag.test.ts +33 -29
- package/src/__tests__/skills.test.ts +0 -2
- package/src/__tests__/slack-inbound-verification.test.ts +0 -4
- package/src/__tests__/suggestion-routes.test.ts +1 -32
- package/src/__tests__/system-prompt.test.ts +0 -1
- package/src/__tests__/tool-executor-shell-integration.test.ts +5 -3
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -5
- package/src/__tests__/trusted-contact-multichannel.test.ts +0 -4
- package/src/__tests__/update-bulletin.test.ts +0 -2
- package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +6 -9
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -6
- package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +252 -0
- package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +218 -0
- package/src/__tests__/workspace-migration-down-functions.test.ts +1009 -0
- package/src/__tests__/workspace-migrations-runner.test.ts +114 -0
- package/src/calls/audio-store.test.ts +97 -0
- package/src/calls/audio-store.ts +205 -0
- package/src/calls/call-controller.ts +85 -7
- package/src/calls/call-domain.ts +3 -0
- package/src/calls/call-store.ts +10 -3
- package/src/calls/fish-audio-client.ts +117 -0
- package/src/calls/relay-server.ts +27 -0
- package/src/calls/twilio-routes.ts +2 -1
- package/src/calls/types.ts +1 -0
- package/src/calls/voice-ingress-preflight.ts +0 -42
- package/src/calls/voice-quality.ts +26 -5
- package/src/calls/voice-session-bridge.ts +6 -12
- package/src/cli/commands/config.ts +1 -4
- package/src/cli/commands/credentials.ts +34 -4
- package/src/cli/commands/oauth/index.ts +7 -0
- package/src/cli/commands/oauth/platform.ts +179 -0
- package/src/cli/commands/platform.ts +3 -3
- package/src/config/assistant-feature-flags.ts +186 -5
- package/src/config/bundled-skills/messaging/SKILL.md +5 -5
- package/src/config/bundled-skills/phone-calls/TOOLS.json +4 -0
- package/src/config/bundled-skills/settings/TOOLS.json +2 -2
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +42 -0
- package/src/config/bundled-tool-registry.ts +1 -11
- package/src/config/env-registry.ts +1 -1
- package/src/config/env.ts +8 -14
- package/src/config/feature-flag-registry.json +48 -8
- package/src/config/loader.ts +98 -31
- package/src/config/schema.ts +4 -13
- package/src/config/schemas/calls.ts +13 -0
- package/src/config/schemas/fish-audio.ts +39 -0
- package/src/config/schemas/security.ts +0 -4
- package/src/config/types.ts +0 -1
- package/src/contacts/contact-store.ts +39 -0
- package/src/contacts/types.ts +2 -0
- package/src/credential-execution/approval-bridge.ts +1 -0
- package/src/credential-execution/executable-discovery.ts +28 -4
- package/src/credential-execution/feature-gates.ts +16 -0
- package/src/credential-execution/process-manager.ts +38 -0
- package/src/daemon/assistant-attachments.ts +9 -0
- package/src/daemon/config-watcher.ts +5 -0
- package/src/daemon/conversation-tool-setup.ts +0 -105
- package/src/daemon/conversation.ts +10 -1
- package/src/daemon/handlers/config-vercel.ts +92 -0
- package/src/daemon/handlers/skills.ts +2 -15
- package/src/daemon/install-symlink.ts +195 -0
- package/src/daemon/lifecycle.ts +227 -51
- package/src/daemon/message-types/conversations.ts +3 -4
- package/src/daemon/message-types/diagnostics.ts +3 -22
- package/src/daemon/message-types/messages.ts +0 -2
- package/src/daemon/message-types/upgrades.ts +8 -0
- package/src/daemon/server.ts +30 -92
- package/src/events/domain-events.ts +2 -1
- package/src/inbound/platform-callback-registration.ts +3 -3
- package/src/instrument.ts +8 -5
- package/src/memory/conversation-title-service.ts +50 -1
- package/src/memory/db-init.ts +12 -0
- package/src/memory/items-extractor.ts +15 -1
- package/src/memory/job-handlers/conversation-starters.ts +4 -1
- package/src/memory/jobs-store.ts +30 -5
- package/src/memory/jobs-worker.ts +31 -7
- package/src/memory/migrations/001-job-deferrals.ts +19 -0
- package/src/memory/migrations/004-entity-relation-dedup.ts +10 -0
- package/src/memory/migrations/005-fingerprint-scope-unique.ts +76 -0
- package/src/memory/migrations/006-scope-salted-fingerprints.ts +50 -0
- package/src/memory/migrations/007-assistant-id-to-self.ts +10 -0
- package/src/memory/migrations/008-remove-assistant-id-columns.ts +34 -0
- package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +26 -0
- package/src/memory/migrations/014-backfill-inbox-thread-state.ts +10 -0
- package/src/memory/migrations/015-drop-active-search-index.ts +17 -0
- package/src/memory/migrations/019-notification-tables-schema-migration.ts +12 -0
- package/src/memory/migrations/020-rename-macos-ios-channel-to-vellum.ts +121 -0
- package/src/memory/migrations/024-embedding-vector-blob.ts +74 -0
- package/src/memory/migrations/026a-embeddings-nullable-vector-json.ts +82 -0
- package/src/memory/migrations/036-normalize-phone-identities.ts +11 -0
- package/src/memory/migrations/116-messages-fts.ts +106 -1
- package/src/memory/migrations/126-backfill-guardian-principal-id.ts +52 -0
- package/src/memory/migrations/127-guardian-principal-id-not-null.ts +77 -0
- package/src/memory/migrations/134-contacts-notes-column.ts +13 -0
- package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +20 -0
- package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -0
- package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +13 -0
- package/src/memory/migrations/141-rename-verification-table.ts +54 -0
- package/src/memory/migrations/142-rename-verification-session-id-column.ts +25 -0
- package/src/memory/migrations/143-rename-guardian-verification-values.ts +35 -0
- package/src/memory/migrations/144-rename-voice-to-phone.ts +136 -0
- package/src/memory/migrations/145-drop-accounts-table.ts +32 -0
- package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +14 -1
- package/src/memory/migrations/148-drop-reminders-table.ts +35 -1
- package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +69 -1
- package/src/memory/migrations/162-guardian-timestamps-epoch-ms.ts +290 -0
- package/src/memory/migrations/169-rename-gmail-provider-key-to-google.ts +51 -1
- package/src/memory/migrations/174-rename-thread-starters-table.ts +47 -1
- package/src/memory/migrations/176-drop-capability-card-state.ts +13 -0
- package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +16 -0
- package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +28 -1
- package/src/memory/migrations/190-call-session-skip-disclosure.ts +15 -0
- package/src/memory/migrations/191-backfill-audio-attachment-mime-types.ts +64 -0
- package/src/memory/migrations/192-contacts-user-file-column.ts +15 -0
- package/src/memory/migrations/index.ts +4 -0
- package/src/memory/migrations/registry.ts +90 -0
- package/src/memory/migrations/validate-migration-state.ts +137 -11
- package/src/memory/qdrant-circuit-breaker.ts +9 -0
- package/src/memory/qdrant-manager.ts +64 -7
- package/src/memory/schema/calls.ts +1 -0
- package/src/memory/schema/contacts.ts +1 -0
- package/src/notifications/decision-engine.ts +4 -1
- package/src/oauth/connection-resolver.ts +6 -4
- package/src/permissions/checker.ts +0 -38
- package/src/permissions/shell-identity.ts +76 -22
- package/src/permissions/types.ts +4 -2
- package/src/platform/client.ts +35 -7
- package/src/prompts/persona-resolver.ts +138 -0
- package/src/prompts/system-prompt.ts +36 -4
- package/src/prompts/templates/users/default.md +1 -0
- package/src/providers/registry.ts +27 -40
- package/src/runtime/auth/__tests__/credential-service.test.ts +0 -1
- package/src/runtime/auth/__tests__/external-assistant-id.test.ts +13 -68
- package/src/runtime/auth/external-assistant-id.ts +13 -59
- package/src/runtime/auth/route-policy.ts +15 -1
- package/src/runtime/auth/token-service.ts +43 -138
- package/src/runtime/channel-readiness-service.ts +1 -16
- package/src/runtime/http-server.ts +27 -2
- package/src/runtime/middleware/error-handler.ts +1 -9
- package/src/runtime/routes/audio-routes.ts +40 -0
- package/src/runtime/routes/btw-routes.ts +0 -17
- package/src/runtime/routes/conversation-query-routes.ts +63 -1
- package/src/runtime/routes/conversation-routes.ts +4 -44
- package/src/runtime/routes/diagnostics-routes.ts +1 -477
- package/src/runtime/routes/identity-routes.ts +18 -29
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +4 -33
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +1 -1
- package/src/runtime/routes/integrations/vercel.ts +89 -0
- package/src/runtime/routes/log-export-routes.ts +5 -0
- package/src/runtime/routes/memory-item-routes.ts +24 -6
- package/src/runtime/routes/migration-rollback-routes.ts +209 -0
- package/src/runtime/routes/migration-routes.ts +17 -1
- package/src/runtime/routes/notification-routes.ts +58 -0
- package/src/runtime/routes/schedule-routes.ts +65 -0
- package/src/runtime/routes/settings-routes.ts +41 -1
- package/src/runtime/routes/tts-routes.ts +86 -0
- package/src/runtime/routes/upgrade-broadcast-routes.ts +26 -2
- package/src/runtime/routes/workspace-commit-routes.ts +62 -0
- package/src/runtime/routes/workspace-routes.test.ts +22 -1
- package/src/runtime/routes/workspace-routes.ts +1 -1
- package/src/runtime/routes/workspace-utils.ts +86 -2
- package/src/security/ces-credential-client.ts +59 -22
- package/src/security/ces-rpc-credential-backend.ts +85 -0
- package/src/security/credential-backend.ts +12 -88
- package/src/security/keychain-broker-client.ts +10 -2
- package/src/security/secure-keys.ts +94 -113
- package/src/skills/catalog-install.ts +13 -7
- package/src/telemetry/usage-telemetry-reporter.ts +4 -2
- package/src/tools/calls/call-start.ts +1 -0
- package/src/tools/executor.ts +0 -4
- package/src/tools/network/script-proxy/session-manager.ts +19 -4
- package/src/tools/network/web-fetch.ts +3 -1
- package/src/tools/skills/execute.ts +1 -1
- package/src/tools/types.ts +0 -8
- package/src/util/errors.ts +0 -12
- package/src/util/platform.ts +3 -50
- package/src/workspace/git-service.ts +5 -2
- package/src/workspace/migrations/001-avatar-rename.ts +15 -0
- package/src/workspace/migrations/003-seed-device-id.ts +17 -1
- package/src/workspace/migrations/004-extract-collect-usage-data.ts +33 -0
- package/src/workspace/migrations/005-add-send-diagnostics.ts +3 -0
- package/src/workspace/migrations/006-services-config.ts +49 -0
- package/src/workspace/migrations/007-web-search-provider-rename.ts +27 -0
- package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +3 -0
- package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +4 -0
- package/src/workspace/migrations/010-app-dir-rename.ts +78 -0
- package/src/workspace/migrations/011-backfill-installation-id.ts +11 -0
- package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +44 -0
- package/src/workspace/migrations/013-repair-conversation-disk-view.ts +5 -0
- package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +153 -0
- package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +156 -0
- package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +150 -0
- package/src/workspace/migrations/017-seed-persona-dirs.ts +95 -0
- package/src/workspace/migrations/migrate-to-workspace-volume.ts +23 -1
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/workspace/migrations/runner.ts +106 -2
- package/src/workspace/migrations/types.ts +4 -0
- package/src/__tests__/claude-code-skill-regression.test.ts +0 -206
- package/src/__tests__/claude-code-tool-profiles.test.ts +0 -99
- package/src/__tests__/diagnostics-export.test.ts +0 -288
- package/src/__tests__/local-gateway-health.test.ts +0 -209
- package/src/__tests__/secret-ingress-handler.test.ts +0 -120
- package/src/__tests__/swarm-conversation-integration.test.ts +0 -358
- package/src/__tests__/swarm-dag-pathological.test.ts +0 -547
- package/src/__tests__/swarm-orchestrator.test.ts +0 -463
- package/src/__tests__/swarm-plan-validator.test.ts +0 -384
- package/src/__tests__/swarm-recursion.test.ts +0 -197
- package/src/__tests__/swarm-router-planner.test.ts +0 -234
- package/src/__tests__/swarm-tool.test.ts +0 -185
- package/src/__tests__/swarm-worker-backend.test.ts +0 -144
- package/src/__tests__/swarm-worker-runner.test.ts +0 -288
- package/src/commands/__tests__/cc-command-registry.test.ts +0 -396
- package/src/commands/cc-command-registry.ts +0 -248
- package/src/config/bundled-skills/claude-code/SKILL.md +0 -53
- package/src/config/bundled-skills/claude-code/TOOLS.json +0 -47
- package/src/config/bundled-skills/claude-code/tools/claude-code.ts +0 -12
- package/src/config/bundled-skills/orchestration/SKILL.md +0 -33
- package/src/config/bundled-skills/orchestration/TOOLS.json +0 -35
- package/src/config/bundled-skills/orchestration/tools/swarm-delegate.ts +0 -12
- package/src/config/schemas/swarm.ts +0 -82
- package/src/logfire.ts +0 -135
- package/src/runtime/local-gateway-health.ts +0 -275
- package/src/security/secret-ingress.ts +0 -68
- package/src/swarm/backend-claude-code.ts +0 -225
- package/src/swarm/checkpoint.ts +0 -137
- package/src/swarm/graph-utils.ts +0 -53
- package/src/swarm/index.ts +0 -55
- package/src/swarm/limits.ts +0 -66
- package/src/swarm/orchestrator.ts +0 -424
- package/src/swarm/plan-validator.ts +0 -117
- package/src/swarm/router-planner.ts +0 -162
- package/src/swarm/router-prompts.ts +0 -39
- package/src/swarm/synthesizer.ts +0 -81
- package/src/swarm/types.ts +0 -72
- package/src/swarm/worker-backend.ts +0 -131
- package/src/swarm/worker-prompts.ts +0 -80
- package/src/swarm/worker-runner.ts +0 -170
- package/src/tools/claude-code/claude-code.ts +0 -610
- package/src/tools/swarm/delegate.ts +0 -205
|
@@ -24,9 +24,10 @@ import { getRootDir } from "../util/platform.js";
|
|
|
24
24
|
const log = getLogger("keychain-broker-client");
|
|
25
25
|
|
|
26
26
|
const REQUEST_TIMEOUT_MS = 5_000;
|
|
27
|
+
const CONNECT_TIMEOUT_MS = 3_000;
|
|
27
28
|
|
|
28
|
-
/** Cooldown periods (ms) after consecutive connection failures: 30s, 60s,
|
|
29
|
-
const RECONNECT_COOLDOWN_MS = [30_000, 60_000,
|
|
29
|
+
/** Cooldown periods (ms) after consecutive connection failures: 5s, 15s, 30s, 60s, 5min, then cap at 5min. */
|
|
30
|
+
const RECONNECT_COOLDOWN_MS = [5_000, 15_000, 30_000, 60_000, 300_000];
|
|
30
31
|
|
|
31
32
|
// ---------------------------------------------------------------------------
|
|
32
33
|
// Types
|
|
@@ -191,7 +192,13 @@ export function createBrokerClient(): KeychainBrokerClient {
|
|
|
191
192
|
|
|
192
193
|
const sock = createConnection({ path: socketPath });
|
|
193
194
|
|
|
195
|
+
const connectTimer = setTimeout(() => {
|
|
196
|
+
sock.destroy();
|
|
197
|
+
reject(new Error("Connect timeout"));
|
|
198
|
+
}, CONNECT_TIMEOUT_MS);
|
|
199
|
+
|
|
194
200
|
sock.on("connect", () => {
|
|
201
|
+
clearTimeout(connectTimer);
|
|
195
202
|
socket = sock;
|
|
196
203
|
consecutiveFailures = 0;
|
|
197
204
|
unavailableSince = null;
|
|
@@ -199,6 +206,7 @@ export function createBrokerClient(): KeychainBrokerClient {
|
|
|
199
206
|
});
|
|
200
207
|
|
|
201
208
|
sock.on("error", (err) => {
|
|
209
|
+
clearTimeout(connectTimer);
|
|
202
210
|
log.warn({ err }, "Keychain broker socket error");
|
|
203
211
|
cleanupSocket();
|
|
204
212
|
reject(err);
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Unified secure key storage — single-
|
|
2
|
+
* Unified secure key storage — single-backend routing through CredentialBackend
|
|
3
3
|
* adapters.
|
|
4
4
|
*
|
|
5
|
-
* Backend selection (`
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Backend selection (`resolveBackendAsync`) is the single async decision point:
|
|
6
|
+
* 1. CES RPC (primary) — injected via `setCesClient()`: delegates credential
|
|
7
|
+
* operations to the CES process over stdio RPC. This is the default path
|
|
8
|
+
* for all local modes (desktop app, dev, CLI).
|
|
9
|
+
* 2. CES HTTP — containerized mode (IS_CONTAINERIZED + CES_CREDENTIAL_URL):
|
|
10
|
+
* delegates to the CES sidecar over HTTP. Used in Docker/managed mode.
|
|
11
|
+
* 3. Encrypted file store (fallback) — used when CES is unavailable.
|
|
9
12
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* Deletes clean up both stores regardless of mode.
|
|
13
|
+
* All operations (reads, writes, lists, deletes) go to exactly one backend.
|
|
14
|
+
* There are no cross-backend fallbacks or merges.
|
|
13
15
|
*/
|
|
14
16
|
|
|
15
17
|
import type {
|
|
@@ -19,13 +21,12 @@ import type {
|
|
|
19
21
|
|
|
20
22
|
import providerEnvVarsRegistry from "../../../meta/provider-env-vars.json" with { type: "json" };
|
|
21
23
|
import { getIsContainerized } from "../config/env-registry.js";
|
|
24
|
+
import type { CesClient } from "../credential-execution/client.js";
|
|
22
25
|
import { getLogger } from "../util/logger.js";
|
|
23
26
|
import { createCesCredentialBackend } from "./ces-credential-client.js";
|
|
27
|
+
import { CesRpcCredentialBackend } from "./ces-rpc-credential-backend.js";
|
|
24
28
|
import type { CredentialBackend, DeleteResult } from "./credential-backend.js";
|
|
25
|
-
import {
|
|
26
|
-
createEncryptedStoreBackend,
|
|
27
|
-
createKeychainBackend,
|
|
28
|
-
} from "./credential-backend.js";
|
|
29
|
+
import { createEncryptedStoreBackend } from "./credential-backend.js";
|
|
29
30
|
|
|
30
31
|
export type { DeleteResult } from "./credential-backend.js";
|
|
31
32
|
|
|
@@ -36,15 +37,24 @@ export type { DeleteResult } from "./credential-backend.js";
|
|
|
36
37
|
*/
|
|
37
38
|
export type { SecureKeyBackend, SecureKeyDeleteResult };
|
|
38
39
|
|
|
40
|
+
export interface SecureKeyResult {
|
|
41
|
+
value: string | undefined;
|
|
42
|
+
unreachable: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
39
45
|
const log = getLogger("secure-keys");
|
|
40
46
|
|
|
41
|
-
let
|
|
47
|
+
let _cesClient: CesClient | undefined;
|
|
42
48
|
let _encryptedStore: CredentialBackend | undefined;
|
|
43
49
|
let _resolvedBackend: CredentialBackend | undefined;
|
|
50
|
+
let _resolvePromise: Promise<CredentialBackend> | undefined;
|
|
44
51
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
52
|
+
/** Inject a CES RPC client for credential routing. Resets the resolved backend. */
|
|
53
|
+
export function setCesClient(client: CesClient | undefined): void {
|
|
54
|
+
_cesClient = client;
|
|
55
|
+
// Reset resolved backend so next call picks up CES
|
|
56
|
+
_resolvedBackend = undefined;
|
|
57
|
+
_resolvePromise = undefined;
|
|
48
58
|
}
|
|
49
59
|
|
|
50
60
|
function getEncryptedStoreBackend(): CredentialBackend {
|
|
@@ -53,101 +63,96 @@ function getEncryptedStoreBackend(): CredentialBackend {
|
|
|
53
63
|
}
|
|
54
64
|
|
|
55
65
|
/**
|
|
56
|
-
* Resolve the primary credential backend for this process.
|
|
66
|
+
* Resolve the primary credential backend for this process (async).
|
|
57
67
|
*
|
|
58
68
|
* Priority:
|
|
59
|
-
* 1.
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* 3. Dev mode (VELLUM_DEV=1) → encrypted file store always.
|
|
69
|
+
* 1. CES RPC client → primary path for all local modes.
|
|
70
|
+
* 2. Containerized + CES_CREDENTIAL_URL → CES HTTP client (Docker/managed).
|
|
71
|
+
* 3. Encrypted file store → fallback when CES is unavailable.
|
|
63
72
|
*
|
|
64
73
|
* Once resolved, the backend does not change during the process lifetime.
|
|
65
74
|
* Call `_resetBackend()` in tests to clear the cached resolution.
|
|
66
75
|
*/
|
|
67
|
-
function
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
async function resolveBackendAsync(): Promise<CredentialBackend> {
|
|
77
|
+
if (_resolvedBackend) return _resolvedBackend;
|
|
78
|
+
if (!_resolvePromise) {
|
|
79
|
+
_resolvePromise = doResolveBackend();
|
|
80
|
+
}
|
|
81
|
+
return _resolvePromise;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function doResolveBackend(): Promise<CredentialBackend> {
|
|
85
|
+
// 1. CES RPC — primary credential backend for all local modes
|
|
86
|
+
if (_cesClient) {
|
|
87
|
+
const cesRpc = new CesRpcCredentialBackend(_cesClient);
|
|
88
|
+
if (cesRpc.isAvailable()) {
|
|
89
|
+
_resolvedBackend = cesRpc;
|
|
90
|
+
return cesRpc;
|
|
79
91
|
}
|
|
92
|
+
log.warn("CES RPC client is set but not ready — falling back to local credential store");
|
|
93
|
+
}
|
|
80
94
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
} else {
|
|
88
|
-
_resolvedBackend = getEncryptedStoreBackend();
|
|
89
|
-
}
|
|
95
|
+
// 2. CES HTTP — containerized / Docker / managed mode
|
|
96
|
+
if (getIsContainerized() && process.env.CES_CREDENTIAL_URL) {
|
|
97
|
+
const ces = createCesCredentialBackend();
|
|
98
|
+
if (ces.isAvailable()) {
|
|
99
|
+
_resolvedBackend = ces;
|
|
100
|
+
return ces;
|
|
90
101
|
}
|
|
102
|
+
log.warn(
|
|
103
|
+
"CES_CREDENTIAL_URL is set but CES backend is not available — " +
|
|
104
|
+
"falling back to local credential store",
|
|
105
|
+
);
|
|
91
106
|
}
|
|
107
|
+
|
|
108
|
+
// 3. Encrypted file store — fallback when CES is unavailable
|
|
109
|
+
_resolvedBackend = getEncryptedStoreBackend();
|
|
92
110
|
return _resolvedBackend;
|
|
93
111
|
}
|
|
94
112
|
|
|
95
113
|
/**
|
|
96
|
-
* List all account names
|
|
97
|
-
*
|
|
98
|
-
* In CES mode, only the CES backend is queried — there are no local stores.
|
|
114
|
+
* List all account names from the resolved backend (async).
|
|
99
115
|
*
|
|
100
|
-
*
|
|
101
|
-
* and the encrypted store (for legacy keys that haven't been migrated). The
|
|
102
|
-
* result is deduplicated. When the primary backend is already the encrypted
|
|
103
|
-
* store, only that store is queried.
|
|
116
|
+
* Queries exactly one backend — no cross-store merge.
|
|
104
117
|
*/
|
|
105
118
|
export async function listSecureKeysAsync(): Promise<string[]> {
|
|
106
|
-
const backend =
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
// CES mode — the sidecar is the single source of truth, no local merge.
|
|
110
|
-
if (backend.name === "ces-http") return primaryKeys;
|
|
111
|
-
|
|
112
|
-
// If primary backend is NOT the encrypted store, also check
|
|
113
|
-
// the encrypted store for legacy keys that haven't been migrated.
|
|
114
|
-
if (backend !== getEncryptedStoreBackend()) {
|
|
115
|
-
const encKeys = await getEncryptedStoreBackend().list();
|
|
116
|
-
const merged = new Set([...primaryKeys, ...encKeys]);
|
|
117
|
-
return Array.from(merged);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return primaryKeys;
|
|
119
|
+
const backend = await resolveBackendAsync();
|
|
120
|
+
return backend.list();
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
// ---------------------------------------------------------------------------
|
|
124
|
-
// Async CRUD — single-
|
|
124
|
+
// Async CRUD — single-backend routing
|
|
125
125
|
// ---------------------------------------------------------------------------
|
|
126
126
|
|
|
127
127
|
/**
|
|
128
|
-
* Retrieve a secret from secure storage
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
128
|
+
* Retrieve a secret from secure storage with richer result metadata.
|
|
129
|
+
*
|
|
130
|
+
* Returns both the value (if found) and whether the backend was
|
|
131
|
+
* unreachable. Callers that need to distinguish "not found" from
|
|
132
|
+
* "backend down" should use this instead of `getSecureKeyAsync`.
|
|
133
|
+
*
|
|
134
|
+
* Reads from exactly one backend — no cross-store fallback.
|
|
133
135
|
*/
|
|
134
|
-
export async function
|
|
136
|
+
export async function getSecureKeyResultAsync(
|
|
135
137
|
account: string,
|
|
136
|
-
): Promise<
|
|
137
|
-
const backend =
|
|
138
|
+
): Promise<SecureKeyResult> {
|
|
139
|
+
const backend = await resolveBackendAsync();
|
|
138
140
|
const result = await backend.get(account);
|
|
139
|
-
if (result != null)
|
|
140
|
-
|
|
141
|
-
// CES mode — no local fallback.
|
|
142
|
-
if (backend.name === "ces-http") return undefined;
|
|
143
|
-
|
|
144
|
-
// Legacy fallback: if primary backend is NOT the encrypted store,
|
|
145
|
-
// check the encrypted store for keys that haven't been migrated.
|
|
146
|
-
if (backend !== getEncryptedStoreBackend()) {
|
|
147
|
-
return await getEncryptedStoreBackend().get(account);
|
|
141
|
+
if (result.value != null) {
|
|
142
|
+
return { value: result.value, unreachable: false };
|
|
148
143
|
}
|
|
144
|
+
return { value: undefined, unreachable: result.unreachable };
|
|
145
|
+
}
|
|
149
146
|
|
|
150
|
-
|
|
147
|
+
/**
|
|
148
|
+
* Retrieve a secret from secure storage. Convenience wrapper over
|
|
149
|
+
* `getSecureKeyResultAsync` that returns only the value.
|
|
150
|
+
*/
|
|
151
|
+
export async function getSecureKeyAsync(
|
|
152
|
+
account: string,
|
|
153
|
+
): Promise<string | undefined> {
|
|
154
|
+
const result = await getSecureKeyResultAsync(account);
|
|
155
|
+
return result.value;
|
|
151
156
|
}
|
|
152
157
|
|
|
153
158
|
/**
|
|
@@ -158,7 +163,7 @@ export async function setSecureKeyAsync(
|
|
|
158
163
|
account: string,
|
|
159
164
|
value: string,
|
|
160
165
|
): Promise<boolean> {
|
|
161
|
-
const backend =
|
|
166
|
+
const backend = await resolveBackendAsync();
|
|
162
167
|
const ok = await backend.set(account, value);
|
|
163
168
|
if (!ok) {
|
|
164
169
|
log.warn(
|
|
@@ -172,38 +177,13 @@ export async function setSecureKeyAsync(
|
|
|
172
177
|
/**
|
|
173
178
|
* Delete a secret from secure storage.
|
|
174
179
|
*
|
|
175
|
-
*
|
|
176
|
-
* CES backend — there are no local stores to clean up.
|
|
177
|
-
*
|
|
178
|
-
* In local mode, always attempts deletion on both the keychain backend (if
|
|
179
|
-
* available) and the encrypted store backend, regardless of routing mode.
|
|
180
|
-
* This cleans up legacy data from both stores.
|
|
180
|
+
* Deletes from exactly one backend — no cross-store cleanup.
|
|
181
181
|
*/
|
|
182
182
|
export async function deleteSecureKeyAsync(
|
|
183
183
|
account: string,
|
|
184
184
|
): Promise<DeleteResult> {
|
|
185
|
-
const backend =
|
|
186
|
-
|
|
187
|
-
// In CES mode, the sidecar is the only store — no local cleanup needed.
|
|
188
|
-
if (backend.name === "ces-http") {
|
|
189
|
-
return backend.delete(account);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const keychain = getKeychainBackend();
|
|
193
|
-
const enc = getEncryptedStoreBackend();
|
|
194
|
-
|
|
195
|
-
let keychainResult: DeleteResult = "not-found";
|
|
196
|
-
if (keychain.isAvailable()) {
|
|
197
|
-
keychainResult = await keychain.delete(account);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const encResult = await enc.delete(account);
|
|
201
|
-
|
|
202
|
-
// Return "error" if either errored
|
|
203
|
-
if (keychainResult === "error" || encResult === "error") return "error";
|
|
204
|
-
// Return "deleted" if either deleted
|
|
205
|
-
if (keychainResult === "deleted" || encResult === "deleted") return "deleted";
|
|
206
|
-
return "not-found";
|
|
185
|
+
const backend = await resolveBackendAsync();
|
|
186
|
+
return backend.delete(account);
|
|
207
187
|
}
|
|
208
188
|
|
|
209
189
|
// ---------------------------------------------------------------------------
|
|
@@ -263,7 +243,8 @@ export async function getMaskedProviderKey(
|
|
|
263
243
|
|
|
264
244
|
/** @internal Test-only: reset the cached backends so they're re-created. */
|
|
265
245
|
export function _resetBackend(): void {
|
|
266
|
-
|
|
246
|
+
_cesClient = undefined;
|
|
267
247
|
_encryptedStore = undefined;
|
|
268
248
|
_resolvedBackend = undefined;
|
|
249
|
+
_resolvePromise = undefined;
|
|
269
250
|
}
|
|
@@ -88,11 +88,7 @@ function getConfigPlatformUrl(): string | undefined {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
function getPlatformUrl(): string {
|
|
91
|
-
return (
|
|
92
|
-
process.env.VELLUM_PLATFORM_URL ??
|
|
93
|
-
getConfigPlatformUrl() ??
|
|
94
|
-
"https://platform.vellum.ai"
|
|
95
|
-
);
|
|
91
|
+
return process.env.VELLUM_PLATFORM_URL ?? getConfigPlatformUrl() ?? "";
|
|
96
92
|
}
|
|
97
93
|
|
|
98
94
|
function buildHeaders(): Record<string, string> {
|
|
@@ -107,7 +103,11 @@ function buildHeaders(): Record<string, string> {
|
|
|
107
103
|
// ─── Catalog operations ──────────────────────────────────────────────────────
|
|
108
104
|
|
|
109
105
|
export async function fetchCatalog(): Promise<CatalogSkill[]> {
|
|
110
|
-
const
|
|
106
|
+
const platformUrl = getPlatformUrl();
|
|
107
|
+
if (!platformUrl) {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
const url = `${platformUrl}/v1/skills/`;
|
|
111
111
|
const response = await fetch(url, {
|
|
112
112
|
headers: buildHeaders(),
|
|
113
113
|
signal: AbortSignal.timeout(10000),
|
|
@@ -214,7 +214,13 @@ export async function fetchAndExtractSkill(
|
|
|
214
214
|
skillId: string,
|
|
215
215
|
destDir: string,
|
|
216
216
|
): Promise<void> {
|
|
217
|
-
const
|
|
217
|
+
const platformUrl = getPlatformUrl();
|
|
218
|
+
if (!platformUrl) {
|
|
219
|
+
throw new Error(
|
|
220
|
+
`Cannot fetch skill "${skillId}": VELLUM_PLATFORM_URL is not configured.`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
const url = `${platformUrl}/v1/skills/${encodeURIComponent(skillId)}/`;
|
|
218
224
|
const response = await fetch(url, {
|
|
219
225
|
headers: buildHeaders(),
|
|
220
226
|
signal: AbortSignal.timeout(15000),
|
|
@@ -140,7 +140,7 @@ export class UsageTelemetryReporter {
|
|
|
140
140
|
|
|
141
141
|
// Resolve auth context — skip flush when neither auth mode is viable
|
|
142
142
|
const client = await VellumPlatformClient.create();
|
|
143
|
-
if (!client && !getTelemetryAppToken()) {
|
|
143
|
+
if (!client && (!getTelemetryAppToken() || !getTelemetryPlatformUrl())) {
|
|
144
144
|
return;
|
|
145
145
|
}
|
|
146
146
|
|
|
@@ -203,7 +203,9 @@ export class UsageTelemetryReporter {
|
|
|
203
203
|
if (client) {
|
|
204
204
|
resp = await client.fetch(TELEMETRY_PATH, fetchInit);
|
|
205
205
|
} else {
|
|
206
|
-
const
|
|
206
|
+
const platformUrl = getTelemetryPlatformUrl();
|
|
207
|
+
if (!platformUrl) return;
|
|
208
|
+
const url = `${platformUrl}${TELEMETRY_PATH}`;
|
|
207
209
|
resp = await fetch(url, {
|
|
208
210
|
...fetchInit,
|
|
209
211
|
headers: {
|
package/src/tools/executor.ts
CHANGED
|
@@ -183,10 +183,6 @@ export class ToolExecutor {
|
|
|
183
183
|
);
|
|
184
184
|
// Buffer so the shell's own timeout fires first and handles cleanup
|
|
185
185
|
toolTimeoutMs = (shellTimeoutSec + 5) * 1000;
|
|
186
|
-
} else if (name === "claude_code") {
|
|
187
|
-
// Claude Code spawns a subprocess that manages its own turn limits
|
|
188
|
-
// (maxTurns). Give it a generous timeout so it isn't killed mid-task.
|
|
189
|
-
toolTimeoutMs = 10 * 60 * 1000; // 10 minutes
|
|
190
186
|
} else {
|
|
191
187
|
const rawTimeoutSec = getConfig().timeouts.toolExecutionTimeoutSec;
|
|
192
188
|
toolTimeoutMs = safeTimeoutMs(rawTimeoutSec);
|
|
@@ -61,10 +61,25 @@ const store = new SessionStore();
|
|
|
61
61
|
/**
|
|
62
62
|
* Host patterns that are allowed by default through the proxy policy engine,
|
|
63
63
|
* regardless of session configuration. Supports exact matches (e.g.
|
|
64
|
-
* `"localhost"`) and wildcard subdomain patterns (e.g. `"*.
|
|
65
|
-
* matches `
|
|
64
|
+
* `"localhost"`) and wildcard subdomain patterns (e.g. `"*.example.com"`
|
|
65
|
+
* matches `api.example.com`, `dev.example.com`, etc.).
|
|
66
|
+
*
|
|
67
|
+
* Additional patterns can be added via the `PROXY_ALLOWED_HOSTS` env var
|
|
68
|
+
* (comma-separated, e.g. `"*.example.com,api.foo.bar"`).
|
|
66
69
|
*/
|
|
67
|
-
const ALLOWED_HOST_PATTERNS: readonly string[] =
|
|
70
|
+
const ALLOWED_HOST_PATTERNS: readonly string[] = (() => {
|
|
71
|
+
const extra = process.env.PROXY_ALLOWED_HOSTS?.trim();
|
|
72
|
+
const defaults = ["localhost"];
|
|
73
|
+
if (extra) {
|
|
74
|
+
defaults.push(
|
|
75
|
+
...extra
|
|
76
|
+
.split(",")
|
|
77
|
+
.map((h) => h.trim())
|
|
78
|
+
.filter(Boolean),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
return defaults;
|
|
82
|
+
})();
|
|
68
83
|
|
|
69
84
|
/**
|
|
70
85
|
* Returns `true` when `hostname` matches any entry in
|
|
@@ -73,7 +88,7 @@ const ALLOWED_HOST_PATTERNS: readonly string[] = ["*.vellum.ai", "localhost"];
|
|
|
73
88
|
function isAllowedHost(hostname: string): boolean {
|
|
74
89
|
for (const pattern of ALLOWED_HOST_PATTERNS) {
|
|
75
90
|
if (pattern.startsWith("*.")) {
|
|
76
|
-
const suffix = pattern.slice(1); // e.g. ".
|
|
91
|
+
const suffix = pattern.slice(1); // e.g. ".example.com"
|
|
77
92
|
if (hostname.endsWith(suffix) || hostname === pattern.slice(2)) {
|
|
78
93
|
return true;
|
|
79
94
|
}
|
|
@@ -573,7 +573,9 @@ export async function executeWebFetch(
|
|
|
573
573
|
Accept:
|
|
574
574
|
"text/markdown, text/html;q=0.9, application/xhtml+xml;q=0.9, text/plain;q=0.8, application/json;q=0.7, */*;q=0.6",
|
|
575
575
|
"Accept-Encoding": "identity",
|
|
576
|
-
"User-Agent":
|
|
576
|
+
"User-Agent":
|
|
577
|
+
process.env.HTTP_USER_AGENT ||
|
|
578
|
+
"VellumAssistant/1.0 (+https://vellum.ai)",
|
|
577
579
|
};
|
|
578
580
|
|
|
579
581
|
let currentUrl = new URL(requestedUrl);
|
|
@@ -30,7 +30,7 @@ export class SkillExecuteTool implements Tool {
|
|
|
30
30
|
activity: {
|
|
31
31
|
type: "string",
|
|
32
32
|
description:
|
|
33
|
-
"Brief non-technical explanation of what you are doing and why, shown to the user as a
|
|
33
|
+
"Brief non-technical explanation of what you are doing and why, shown to the user as a progress update.",
|
|
34
34
|
},
|
|
35
35
|
},
|
|
36
36
|
required: ["tool", "input", "activity"],
|
package/src/tools/types.ts
CHANGED
|
@@ -118,14 +118,6 @@ export interface ToolContext {
|
|
|
118
118
|
proxyToolResolver?: ProxyToolResolver;
|
|
119
119
|
/** When set, only tools in this set may execute. Tools outside the set are blocked with an error. */
|
|
120
120
|
allowedToolNames?: Set<string>;
|
|
121
|
-
/** Request user confirmation for a sub-tool operation (used by claude_code tool). */
|
|
122
|
-
requestConfirmation?: (req: {
|
|
123
|
-
toolName: string;
|
|
124
|
-
input: Record<string, unknown>;
|
|
125
|
-
riskLevel: string;
|
|
126
|
-
executionTarget?: ExecutionTarget;
|
|
127
|
-
principal?: string;
|
|
128
|
-
}) => Promise<{ decision: "allow" | "deny" }>;
|
|
129
121
|
/** Prompt the user for a secret value via native SecureField UI. */
|
|
130
122
|
requestSecret?: (params: {
|
|
131
123
|
service: string;
|
package/src/util/errors.ts
CHANGED
|
@@ -20,9 +20,6 @@ export enum ErrorCode {
|
|
|
20
20
|
// WASM integrity check failures
|
|
21
21
|
INTEGRITY_ERROR = "INTEGRITY_ERROR",
|
|
22
22
|
|
|
23
|
-
// Secret detected in inbound content
|
|
24
|
-
INGRESS_BLOCKED = "INGRESS_BLOCKED",
|
|
25
|
-
|
|
26
23
|
// Internal/unexpected errors
|
|
27
24
|
INTERNAL_ERROR = "INTERNAL_ERROR",
|
|
28
25
|
}
|
|
@@ -178,12 +175,3 @@ export class IntegrityError extends AssistantError {
|
|
|
178
175
|
}
|
|
179
176
|
}
|
|
180
177
|
|
|
181
|
-
export class IngressBlockedError extends AssistantError {
|
|
182
|
-
constructor(
|
|
183
|
-
message: string,
|
|
184
|
-
public readonly detectedTypes: string[],
|
|
185
|
-
) {
|
|
186
|
-
super(message, ErrorCode.INGRESS_BLOCKED);
|
|
187
|
-
this.name = "IngressBlockedError";
|
|
188
|
-
}
|
|
189
|
-
}
|
package/src/util/platform.ts
CHANGED
|
@@ -3,7 +3,6 @@ import {
|
|
|
3
3
|
existsSync,
|
|
4
4
|
mkdirSync,
|
|
5
5
|
readFileSync,
|
|
6
|
-
writeFileSync,
|
|
7
6
|
} from "node:fs";
|
|
8
7
|
import { homedir } from "node:os";
|
|
9
8
|
import { join } from "node:path";
|
|
@@ -45,25 +44,6 @@ export function getClipboardCommand(): string | null {
|
|
|
45
44
|
return null;
|
|
46
45
|
}
|
|
47
46
|
|
|
48
|
-
/**
|
|
49
|
-
* Read and parse the lockfile (~/.vellum.lock.json).
|
|
50
|
-
* Respects BASE_DATA_DIR for non-standard home directories.
|
|
51
|
-
* Returns null if the file doesn't exist or is malformed.
|
|
52
|
-
*/
|
|
53
|
-
export function readLockfile(): Record<string, unknown> | null {
|
|
54
|
-
const base = getBaseDataDir() || homedir();
|
|
55
|
-
const lockPath = join(base, ".vellum.lock.json");
|
|
56
|
-
if (!existsSync(lockPath)) return null;
|
|
57
|
-
try {
|
|
58
|
-
const raw = JSON.parse(readFileSync(lockPath, "utf-8"));
|
|
59
|
-
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
60
|
-
return raw as Record<string, unknown>;
|
|
61
|
-
}
|
|
62
|
-
} catch {
|
|
63
|
-
// malformed JSON
|
|
64
|
-
}
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
47
|
|
|
68
48
|
/**
|
|
69
49
|
* Resolve the instance data directory from the lockfile.
|
|
@@ -155,45 +135,18 @@ export function resolveInstanceDataDir(): string | undefined {
|
|
|
155
135
|
* (see migration 007-assistant-id-to-self). However, the desktop UI
|
|
156
136
|
* sends the real assistant ID (e.g., "vellum-true-eel") while the
|
|
157
137
|
* inbound call path resolves phone numbers to config keys (typically
|
|
158
|
-
* "self"). This function maps
|
|
138
|
+
* "self"). This function maps the current assistant's ID to "self"
|
|
159
139
|
* so both sides use a consistent DB key.
|
|
160
|
-
*
|
|
161
|
-
* Multi-instance safety: each daemon process runs with a scoped
|
|
162
|
-
* BASE_DATA_DIR, so readLockfile() only sees the lockfile for this
|
|
163
|
-
* instance. The mapping to "self" is correct because each daemon is
|
|
164
|
-
* single-tenant — it only manages its own instance's data.
|
|
165
140
|
*/
|
|
166
141
|
export function normalizeAssistantId(assistantId: string): string {
|
|
167
142
|
if (assistantId === "self") return "self";
|
|
168
143
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const assistants = lockData?.assistants as
|
|
172
|
-
| Array<Record<string, unknown>>
|
|
173
|
-
| undefined;
|
|
174
|
-
if (assistants) {
|
|
175
|
-
for (const entry of assistants) {
|
|
176
|
-
if (entry.assistantId === assistantId) return "self";
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
} catch {
|
|
180
|
-
// lockfile unreadable — return as-is
|
|
181
|
-
}
|
|
144
|
+
const ownName = process.env.VELLUM_ASSISTANT_NAME;
|
|
145
|
+
if (ownName && assistantId === ownName) return "self";
|
|
182
146
|
|
|
183
147
|
return assistantId;
|
|
184
148
|
}
|
|
185
149
|
|
|
186
|
-
/**
|
|
187
|
-
* Write data to the primary lockfile (~/.vellum.lock.json).
|
|
188
|
-
* Respects BASE_DATA_DIR for non-standard home directories.
|
|
189
|
-
*/
|
|
190
|
-
export function writeLockfile(data: Record<string, unknown>): void {
|
|
191
|
-
const base = getBaseDataDir() || homedir();
|
|
192
|
-
writeFileSync(
|
|
193
|
-
join(base, ".vellum.lock.json"),
|
|
194
|
-
JSON.stringify(data, null, 2) + "\n",
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
150
|
|
|
198
151
|
/**
|
|
199
152
|
* Returns the root ~/.vellum directory. User-facing files (config, prompt
|
|
@@ -753,8 +753,11 @@ export class WorkspaceGitService {
|
|
|
753
753
|
* Must be called with the mutex lock held.
|
|
754
754
|
*/
|
|
755
755
|
private async ensureCommitIdentityLocked(): Promise<void> {
|
|
756
|
-
|
|
757
|
-
|
|
756
|
+
const gitName = process.env.ASSISTANT_GIT_USER_NAME || "Vellum Assistant";
|
|
757
|
+
const gitEmail =
|
|
758
|
+
process.env.ASSISTANT_GIT_USER_EMAIL || "assistant@vellum.ai";
|
|
759
|
+
await this.execGit(["config", "user.name", gitName]);
|
|
760
|
+
await this.execGit(["config", "user.email", gitEmail]);
|
|
758
761
|
}
|
|
759
762
|
|
|
760
763
|
/**
|
|
@@ -22,4 +22,19 @@ export const avatarRenameMigration: WorkspaceMigration = {
|
|
|
22
22
|
renameSync(oldTraits, newTraits);
|
|
23
23
|
}
|
|
24
24
|
},
|
|
25
|
+
down(workspaceDir: string): void {
|
|
26
|
+
const avatarDir = join(workspaceDir, "data", "avatar");
|
|
27
|
+
|
|
28
|
+
const newImage = join(avatarDir, "avatar-image.png");
|
|
29
|
+
const oldImage = join(avatarDir, "custom-avatar.png");
|
|
30
|
+
if (existsSync(newImage) && !existsSync(oldImage)) {
|
|
31
|
+
renameSync(newImage, oldImage);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const newTraits = join(avatarDir, "character-traits.json");
|
|
35
|
+
const oldTraits = join(avatarDir, "avatar-components.json");
|
|
36
|
+
if (existsSync(newTraits) && !existsSync(oldTraits)) {
|
|
37
|
+
renameSync(newTraits, oldTraits);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
25
40
|
};
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
unlinkSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from "node:fs";
|
|
2
8
|
import { join } from "node:path";
|
|
3
9
|
|
|
4
10
|
import { getDeviceIdBaseDir } from "../../util/device-id.js";
|
|
@@ -97,4 +103,14 @@ export const seedDeviceIdMigration: WorkspaceMigration = {
|
|
|
97
103
|
// Best-effort — getDeviceId() will generate a new one if this fails.
|
|
98
104
|
}
|
|
99
105
|
},
|
|
106
|
+
down(_workspaceDir: string): void {
|
|
107
|
+
// The forward migration seeds deviceId in ~/.vellum/device.json from the
|
|
108
|
+
// lockfile. Reverse by removing device.json entirely — getDeviceId() will
|
|
109
|
+
// generate a fresh one on next startup if needed.
|
|
110
|
+
const base = getDeviceIdBaseDir();
|
|
111
|
+
const devicePath = join(base, ".vellum", "device.json");
|
|
112
|
+
if (existsSync(devicePath)) {
|
|
113
|
+
unlinkSync(devicePath);
|
|
114
|
+
}
|
|
115
|
+
},
|
|
100
116
|
};
|