@vellumai/assistant 0.5.7 → 0.5.9
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/Dockerfile +2 -1
- package/docker-entrypoint.sh +9 -0
- package/docs/architecture/memory.md +13 -11
- package/eslint.config.mjs +0 -31
- package/node_modules/@vellumai/ces-contracts/src/error.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/grants.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/handles.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/rpc.ts +1 -1
- package/package.json +1 -1
- package/src/__tests__/approval-cascade.test.ts +0 -1
- package/src/__tests__/browser-fill-credential.test.ts +1 -1
- package/src/__tests__/call-controller.test.ts +0 -1
- package/src/__tests__/ces-rpc-credential-backend.test.ts +3 -3
- package/src/__tests__/ces-startup-timeout.test.ts +40 -0
- package/src/__tests__/config-schema-cmd.test.ts +0 -1
- package/src/__tests__/config-schema.test.ts +2 -0
- package/src/__tests__/conversation-abort-tool-results.test.ts +0 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -2
- package/src/__tests__/conversation-agent-loop.test.ts +2 -4
- package/src/__tests__/conversation-confirmation-signals.test.ts +0 -1
- package/src/__tests__/conversation-error.test.ts +15 -1
- package/src/__tests__/conversation-messaging-secret-redirect.test.ts +1 -1
- package/src/__tests__/conversation-pre-run-repair.test.ts +0 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -1
- package/src/__tests__/conversation-queue.test.ts +0 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +227 -0
- package/src/__tests__/conversation-slash-queue.test.ts +0 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +0 -1
- package/src/__tests__/conversation-workspace-injection.test.ts +0 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -1
- package/src/__tests__/credential-execution-client.test.ts +5 -2
- package/src/__tests__/credential-execution-feature-gates.test.ts +31 -16
- package/src/__tests__/credential-execution-managed-contract.test.ts +2 -2
- package/src/__tests__/credential-security-e2e.test.ts +1 -1
- package/src/__tests__/credential-security-invariants.test.ts +2 -5
- package/src/__tests__/credentials-cli.test.ts +4 -3
- package/src/__tests__/daemon-credential-client.test.ts +123 -0
- package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -0
- package/src/__tests__/gateway-client-managed-outbound.test.ts +79 -1
- package/src/__tests__/journal-context.test.ts +335 -0
- package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -3
- package/src/__tests__/memory-lifecycle-e2e.test.ts +70 -25
- package/src/__tests__/memory-recall-quality.test.ts +48 -17
- package/src/__tests__/memory-regressions.test.ts +408 -363
- package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -3
- package/src/__tests__/non-member-access-request.test.ts +2 -2
- package/src/__tests__/notification-decision-strategy.test.ts +71 -0
- package/src/__tests__/oauth-cli.test.ts +5 -1
- package/src/__tests__/provider-commit-message-generator.test.ts +0 -37
- package/src/__tests__/provider-error-scenarios.test.ts +0 -267
- package/src/__tests__/provider-streaming.benchmark.test.ts +2 -81
- package/src/__tests__/relay-server.test.ts +1 -2
- package/src/__tests__/script-proxy-injection-runtime.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +1 -1
- package/src/__tests__/secure-keys.test.ts +18 -15
- package/src/__tests__/skill-memory.test.ts +17 -3
- package/src/__tests__/stale-approval-dedup.test.ts +171 -0
- package/src/__tests__/stt-hints.test.ts +437 -0
- package/src/__tests__/task-memory-cleanup.test.ts +14 -0
- package/src/__tests__/twilio-routes-twiml.test.ts +139 -1
- package/src/__tests__/voice-quality.test.ts +58 -0
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
- package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +5 -3
- package/src/acp/agent-process.ts +9 -1
- package/src/agent/loop.ts +1 -1
- package/src/approvals/guardian-request-resolvers.ts +164 -38
- package/src/calls/__tests__/tts-text-sanitizer.test.ts +254 -0
- package/src/calls/call-controller.ts +9 -5
- package/src/calls/fish-audio-client.ts +26 -14
- package/src/calls/stt-hints.ts +189 -0
- package/src/calls/tts-text-sanitizer.ts +61 -0
- package/src/calls/twilio-routes.ts +32 -4
- package/src/calls/voice-quality.ts +15 -3
- package/src/calls/voice-session-bridge.ts +1 -0
- package/src/cli/commands/avatar.ts +2 -2
- package/src/cli/commands/credentials.ts +110 -94
- package/src/cli/commands/doctor.ts +2 -2
- package/src/cli/commands/keys.ts +7 -7
- package/src/cli/commands/memory.ts +1 -1
- package/src/cli/commands/oauth/connections.ts +11 -29
- package/src/cli/commands/oauth/platform.ts +389 -43
- package/src/cli/lib/daemon-credential-client.ts +284 -0
- package/src/cli.ts +1 -1
- package/src/config/bundled-skills/AGENTS.md +34 -0
- package/src/config/bundled-skills/acp/SKILL.md +10 -0
- package/src/config/bundled-skills/app-builder/SKILL.md +0 -4
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +1 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +1 -0
- package/src/config/bundled-skills/settings/SKILL.md +15 -2
- package/src/config/bundled-skills/settings/TOOLS.json +46 -1
- package/src/config/bundled-skills/settings/tools/avatar-remove.ts +59 -0
- package/src/config/bundled-skills/settings/tools/avatar-update.ts +80 -0
- package/src/config/bundled-skills/slack/SKILL.md +1 -1
- package/src/config/bundled-tool-registry.ts +4 -0
- package/src/config/defaults.ts +0 -2
- package/src/config/env-registry.ts +4 -4
- package/src/config/env.ts +14 -1
- package/src/config/feature-flag-registry.json +1 -1
- package/src/config/loader.ts +8 -11
- package/src/config/schema.ts +5 -16
- package/src/config/schemas/calls.ts +17 -0
- package/src/config/schemas/inference.ts +2 -2
- package/src/config/schemas/journal.ts +16 -0
- package/src/config/schemas/memory-processing.ts +2 -2
- package/src/config/types.ts +1 -0
- package/src/contacts/contact-store.ts +2 -2
- package/src/credential-execution/executable-discovery.ts +1 -1
- package/src/credential-execution/startup-timeout.ts +36 -0
- package/src/daemon/approval-generators.ts +3 -9
- package/src/daemon/conversation-agent-loop.ts +6 -0
- package/src/daemon/conversation-error.ts +13 -1
- package/src/daemon/conversation-memory.ts +1 -2
- package/src/daemon/conversation-process.ts +18 -1
- package/src/daemon/conversation-runtime-assembly.ts +61 -1
- package/src/daemon/conversation-surfaces.ts +30 -1
- package/src/daemon/conversation.ts +20 -9
- package/src/daemon/guardian-action-generators.ts +3 -9
- package/src/daemon/lifecycle.ts +18 -11
- package/src/daemon/message-types/conversations.ts +1 -0
- package/src/daemon/server.ts +2 -3
- package/src/memory/app-store.ts +31 -0
- package/src/memory/db-init.ts +4 -0
- package/src/memory/indexer.ts +19 -10
- package/src/memory/items-extractor.ts +315 -322
- package/src/memory/job-handlers/summarization.ts +26 -16
- package/src/memory/jobs-store.ts +33 -1
- package/src/memory/journal-memory.ts +214 -0
- package/src/memory/migrations/193-add-source-type-columns.ts +81 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/retriever.test.ts +37 -25
- package/src/memory/retriever.ts +24 -49
- package/src/memory/schema/memory-core.ts +2 -0
- package/src/memory/search/formatting.ts +7 -44
- package/src/memory/search/staleness.ts +4 -0
- package/src/memory/search/tier-classifier.ts +10 -2
- package/src/memory/search/types.ts +2 -5
- package/src/memory/task-memory-cleanup.ts +4 -3
- package/src/notifications/adapters/slack.ts +168 -6
- package/src/notifications/broadcaster.ts +1 -0
- package/src/notifications/copy-composer.ts +59 -2
- package/src/notifications/signal.ts +2 -0
- package/src/notifications/types.ts +2 -0
- package/src/prompts/journal-context.ts +133 -0
- package/src/prompts/persona-resolver.ts +80 -24
- package/src/prompts/system-prompt.ts +30 -0
- package/src/prompts/templates/NOW.md +26 -0
- package/src/prompts/templates/SOUL.md +20 -0
- package/src/prompts/update-bulletin-format.ts +0 -2
- package/src/providers/provider-send-message.ts +3 -32
- package/src/providers/registry.ts +2 -139
- package/src/providers/types.ts +1 -1
- package/src/runtime/access-request-helper.ts +4 -0
- package/src/runtime/auth/__tests__/guard-tests.test.ts +9 -50
- package/src/runtime/auth/route-policy.ts +2 -0
- package/src/runtime/gateway-client.ts +47 -4
- package/src/runtime/guardian-decision-types.ts +45 -4
- package/src/runtime/http-server.ts +5 -2
- package/src/runtime/routes/access-request-decision.ts +2 -2
- package/src/runtime/routes/app-management-routes.ts +2 -1
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +219 -30
- package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +37 -14
- package/src/runtime/routes/channel-readiness-routes.ts +9 -4
- package/src/runtime/routes/debug-routes.ts +12 -9
- package/src/runtime/routes/guardian-approval-interception.ts +168 -11
- package/src/runtime/routes/guardian-approval-prompt.ts +6 -1
- package/src/runtime/routes/guardian-approval-reply-helpers.ts +103 -21
- package/src/runtime/routes/identity-routes.ts +1 -1
- package/src/runtime/routes/inbound-message-handler.ts +31 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +64 -5
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +52 -40
- package/src/runtime/routes/integrations/twilio.ts +52 -10
- package/src/runtime/routes/memory-item-routes.test.ts +3 -3
- package/src/runtime/routes/memory-item-routes.ts +25 -11
- package/src/runtime/routes/secret-routes.ts +141 -10
- package/src/runtime/routes/tts-routes.ts +11 -1
- package/src/security/ces-credential-client.ts +18 -9
- package/src/security/ces-rpc-credential-backend.ts +4 -3
- package/src/security/credential-backend.ts +10 -4
- package/src/security/secure-keys.ts +21 -4
- package/src/skills/catalog-install.ts +4 -36
- package/src/skills/inline-command-expansions.ts +7 -7
- package/src/skills/skill-memory.ts +1 -0
- package/src/subagent/manager.ts +2 -5
- package/src/tools/acp/spawn.ts +78 -1
- package/src/tools/credentials/vault.ts +5 -3
- package/src/tools/memory/definitions.ts +3 -2
- package/src/tools/memory/handlers.ts +10 -7
- package/src/tools/sensitive-output-placeholders.ts +2 -2
- package/src/tools/terminal/safe-env.ts +1 -0
- package/src/util/browser.ts +15 -0
- package/src/util/platform.ts +1 -1
- package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +4 -4
- package/src/workspace/migrations/017-seed-persona-dirs.ts +2 -1
- package/src/workspace/migrations/018-rekey-compound-credential-keys.ts +184 -0
- package/src/workspace/migrations/019-scope-journal-to-guardian.ts +103 -0
- package/src/workspace/migrations/migrate-to-workspace-volume.ts +4 -4
- package/src/workspace/migrations/registry.ts +4 -0
- package/src/workspace/provider-commit-message-generator.ts +12 -21
- package/src/__tests__/provider-fail-open-selection.test.ts +0 -271
- package/src/__tests__/provider-failover-actual-provider.test.ts +0 -66
- package/src/memory/search/lexical.ts +0 -48
- package/src/providers/failover.ts +0 -186
|
@@ -400,8 +400,9 @@ class CredentialStoreTool implements Tool {
|
|
|
400
400
|
const credIdSuffix = metadata
|
|
401
401
|
? ` (credential_id: ${metadata.credentialId})`
|
|
402
402
|
: "";
|
|
403
|
+
const retrieveHint = ` Retrieve with: \`assistant credentials reveal --service ${service} --field ${field}\``;
|
|
403
404
|
return {
|
|
404
|
-
content: `Stored credential for ${service}/${field}.${credIdSuffix}${
|
|
405
|
+
content: `Stored credential for ${service}/${field}.${credIdSuffix}${retrieveHint}${
|
|
405
406
|
slackChannelResult
|
|
406
407
|
? formatSlackChannelStatus(slackChannelResult)
|
|
407
408
|
: ""
|
|
@@ -428,7 +429,7 @@ class CredentialStoreTool implements Tool {
|
|
|
428
429
|
// (e.g. keychain) and the encrypted store (legacy keys).
|
|
429
430
|
let secureKeySet: Set<string> | undefined;
|
|
430
431
|
try {
|
|
431
|
-
secureKeySet = new Set(await listSecureKeysAsync());
|
|
432
|
+
secureKeySet = new Set((await listSecureKeysAsync()).accounts);
|
|
432
433
|
} catch (err) {
|
|
433
434
|
log.error(
|
|
434
435
|
{ err },
|
|
@@ -810,8 +811,9 @@ class CredentialStoreTool implements Tool {
|
|
|
810
811
|
const promptCredIdSuffix = promptMeta
|
|
811
812
|
? ` (credential_id: ${promptMeta.credentialId})`
|
|
812
813
|
: "";
|
|
814
|
+
const promptRetrieveHint = ` Retrieve with: \`assistant credentials reveal --service ${service} --field ${field}\``;
|
|
813
815
|
return {
|
|
814
|
-
content: `Credential stored for ${service}/${field}.${promptCredIdSuffix}${
|
|
816
|
+
content: `Credential stored for ${service}/${field}.${promptCredIdSuffix}${promptRetrieveHint}${
|
|
815
817
|
slackChannelResult
|
|
816
818
|
? formatSlackChannelStatus(slackChannelResult)
|
|
817
819
|
: ""
|
|
@@ -46,9 +46,10 @@ const memoryManageProperties = {
|
|
|
46
46
|
"decision",
|
|
47
47
|
"constraint",
|
|
48
48
|
"event",
|
|
49
|
+
"journal",
|
|
49
50
|
],
|
|
50
51
|
description:
|
|
51
|
-
'Category of the memory item (required for save). Use "constraint" for mistakes, gotchas, discoveries, and working solutions - write as advice to your future self.',
|
|
52
|
+
'Category of the memory item (required for save). Use "constraint" for mistakes, gotchas, discoveries, and working solutions - write as advice to your future self. Use "journal" for journal-style memories — experiential snapshots, upcoming events, things to carry forward.',
|
|
52
53
|
},
|
|
53
54
|
subject: {
|
|
54
55
|
type: "string" as const,
|
|
@@ -59,7 +60,7 @@ const memoryManageProperties = {
|
|
|
59
60
|
export const memoryManageDefinition: ToolDefinition = {
|
|
60
61
|
name: "memory_manage",
|
|
61
62
|
description:
|
|
62
|
-
"Save, update, or delete memory items.
|
|
63
|
+
"Save, update, or delete memory items. If you want to remember something, save it now. Use 'save' for new information worth remembering (facts, preferences, mistakes, discoveries, gotchas), 'update' to correct existing items, 'delete' to remove outdated items. When a user says 'remember this', save immediately. Be proactive: if you learn something important that may be useful in the future, always call this tool — don't just say or hope you'll remember it. This is not a substitute for updating workspace files when relevant - do both.",
|
|
63
64
|
input_schema: {
|
|
64
65
|
type: "object",
|
|
65
66
|
properties: memoryManageProperties,
|
|
@@ -9,7 +9,6 @@ import { buildMemoryRecall } from "../../memory/retriever.js";
|
|
|
9
9
|
import { memoryItems } from "../../memory/schema.js";
|
|
10
10
|
import type { ScopePolicyOverride } from "../../memory/search/types.js";
|
|
11
11
|
import { getLogger } from "../../util/logger.js";
|
|
12
|
-
import { truncate } from "../../util/truncate.js";
|
|
13
12
|
import type { ToolExecutionResult } from "../types.js";
|
|
14
13
|
|
|
15
14
|
const log = getLogger("memory-tools");
|
|
@@ -39,6 +38,7 @@ export async function handleMemorySave(
|
|
|
39
38
|
"decision",
|
|
40
39
|
"constraint",
|
|
41
40
|
"event",
|
|
41
|
+
"journal",
|
|
42
42
|
]);
|
|
43
43
|
if (typeof rawKind !== "string") {
|
|
44
44
|
return {
|
|
@@ -60,14 +60,14 @@ export async function handleMemorySave(
|
|
|
60
60
|
|
|
61
61
|
const subject =
|
|
62
62
|
typeof args.subject === "string" && args.subject.trim().length > 0
|
|
63
|
-
?
|
|
63
|
+
? args.subject.trim()
|
|
64
64
|
: inferSubjectFromStatement(statement.trim());
|
|
65
65
|
|
|
66
66
|
try {
|
|
67
67
|
const db = getDb();
|
|
68
68
|
const id = uuid();
|
|
69
69
|
const now = Date.now();
|
|
70
|
-
const trimmedStatement =
|
|
70
|
+
const trimmedStatement = statement.trim();
|
|
71
71
|
|
|
72
72
|
const fingerprint = computeMemoryFingerprint(
|
|
73
73
|
scopeId,
|
|
@@ -93,6 +93,7 @@ export async function handleMemorySave(
|
|
|
93
93
|
status: "active",
|
|
94
94
|
importance: 0.8,
|
|
95
95
|
lastSeenAt: now,
|
|
96
|
+
sourceType: "tool",
|
|
96
97
|
verificationState: "user_confirmed",
|
|
97
98
|
})
|
|
98
99
|
.where(eq(memoryItems.id, existing.id))
|
|
@@ -115,6 +116,7 @@ export async function handleMemorySave(
|
|
|
115
116
|
confidence: 0.95, // explicit saves have high confidence
|
|
116
117
|
importance: 0.8, // explicit saves are high importance
|
|
117
118
|
fingerprint,
|
|
119
|
+
sourceType: "tool",
|
|
118
120
|
verificationState: "user_confirmed",
|
|
119
121
|
scopeId,
|
|
120
122
|
firstSeenAt: now,
|
|
@@ -187,7 +189,7 @@ export async function handleMemoryUpdate(
|
|
|
187
189
|
}
|
|
188
190
|
|
|
189
191
|
const now = Date.now();
|
|
190
|
-
const trimmedStatement =
|
|
192
|
+
const trimmedStatement = statement.trim();
|
|
191
193
|
|
|
192
194
|
const fingerprint = computeMemoryFingerprint(
|
|
193
195
|
scopeId,
|
|
@@ -221,6 +223,7 @@ export async function handleMemoryUpdate(
|
|
|
221
223
|
fingerprint,
|
|
222
224
|
lastSeenAt: now,
|
|
223
225
|
importance: 0.8,
|
|
226
|
+
sourceType: "tool",
|
|
224
227
|
verificationState: "user_confirmed",
|
|
225
228
|
})
|
|
226
229
|
.where(eq(memoryItems.id, existing.id))
|
|
@@ -308,7 +311,7 @@ export async function handleMemoryRecall(
|
|
|
308
311
|
items: [],
|
|
309
312
|
sources: {
|
|
310
313
|
semantic: recall.semanticHits,
|
|
311
|
-
recency:
|
|
314
|
+
recency: 0,
|
|
312
315
|
},
|
|
313
316
|
};
|
|
314
317
|
return {
|
|
@@ -328,7 +331,7 @@ export async function handleMemoryRecall(
|
|
|
328
331
|
})),
|
|
329
332
|
sources: {
|
|
330
333
|
semantic: recall.semanticHits,
|
|
331
|
-
recency:
|
|
334
|
+
recency: 0,
|
|
332
335
|
},
|
|
333
336
|
};
|
|
334
337
|
|
|
@@ -416,7 +419,7 @@ export async function handleMemoryDelete(
|
|
|
416
419
|
function inferSubjectFromStatement(statement: string): string {
|
|
417
420
|
// Take first few words as a subject label
|
|
418
421
|
const words = statement.split(/\s+/).slice(0, 6).join(" ");
|
|
419
|
-
return
|
|
422
|
+
return words;
|
|
420
423
|
}
|
|
421
424
|
|
|
422
425
|
/**
|
|
@@ -85,8 +85,8 @@ export function extractAndSanitize(content: string): SanitizeResult {
|
|
|
85
85
|
// Step 1: parse directives
|
|
86
86
|
// Reset lastIndex for safety since the regex is global
|
|
87
87
|
DIRECTIVE_RE.lastIndex = 0;
|
|
88
|
-
let match: RegExpExecArray |
|
|
89
|
-
while ((match = DIRECTIVE_RE.exec(content)
|
|
88
|
+
let match: RegExpExecArray | null;
|
|
89
|
+
while ((match = DIRECTIVE_RE.exec(content)) !== null) {
|
|
90
90
|
const kind = match[1];
|
|
91
91
|
const value = match[2];
|
|
92
92
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { isLinux, isMacOS } from "./platform.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Open a URL in the user's default browser, falling back to printing the URL
|
|
5
|
+
* to stderr on unsupported platforms.
|
|
6
|
+
*/
|
|
7
|
+
export function openInBrowser(url: string): void {
|
|
8
|
+
if (isMacOS()) {
|
|
9
|
+
Bun.spawn(["open", url], { stdout: "ignore", stderr: "ignore" });
|
|
10
|
+
} else if (isLinux()) {
|
|
11
|
+
Bun.spawn(["xdg-open", url], { stdout: "ignore", stderr: "ignore" });
|
|
12
|
+
} else {
|
|
13
|
+
process.stderr.write(`Open this URL to authorize:\n\n${url}\n`);
|
|
14
|
+
}
|
|
15
|
+
}
|
package/src/util/platform.ts
CHANGED
|
@@ -324,7 +324,7 @@ export function getSignalsDir(): string {
|
|
|
324
324
|
/**
|
|
325
325
|
* Returns the workspace root for user-facing state.
|
|
326
326
|
*
|
|
327
|
-
* When the
|
|
327
|
+
* When the VELLUM_WORKSPACE_DIR env var is set, returns that value (used in
|
|
328
328
|
* containerized deployments where the workspace is a separate volume).
|
|
329
329
|
* Otherwise falls back to ~/.vellum/workspace.
|
|
330
330
|
*
|
|
@@ -105,10 +105,10 @@ export const migrateCredentialsFromKeychainMigration: WorkspaceMigration = {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
if (!brokerAvailable) {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
108
|
+
throw new Error(
|
|
109
|
+
"Keychain broker not available after waiting — credential migration " +
|
|
110
|
+
"will be retried on next startup",
|
|
111
|
+
);
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
const { setKey } = await import("../../security/encrypted-store.js");
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
} from "node:fs";
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
|
|
11
|
-
import { eq } from "drizzle-orm";
|
|
11
|
+
import { desc, eq } from "drizzle-orm";
|
|
12
12
|
|
|
13
13
|
import { generateUserFileSlug } from "../../contacts/contact-store.js";
|
|
14
14
|
import { getDb } from "../../memory/db.js";
|
|
@@ -68,6 +68,7 @@ export const seedPersonaDirsMigration: WorkspaceMigration = {
|
|
|
68
68
|
.select()
|
|
69
69
|
.from(contacts)
|
|
70
70
|
.where(eq(contacts.role, "guardian"))
|
|
71
|
+
.orderBy(desc(contacts.createdAt))
|
|
71
72
|
.limit(1)
|
|
72
73
|
.get();
|
|
73
74
|
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { credentialKey } from "../../security/credential-key.js";
|
|
2
|
+
import { getLogger } from "../../util/logger.js";
|
|
3
|
+
import type { WorkspaceMigration } from "./types.js";
|
|
4
|
+
|
|
5
|
+
const log = getLogger("workspace-migrations");
|
|
6
|
+
const CREDENTIAL_PREFIX = "credential/";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Re-key compound credential storage keys from the old indexOf-based split
|
|
10
|
+
* to the new lastIndexOf-based split.
|
|
11
|
+
*
|
|
12
|
+
* The old code split "integration:google:access_token" at the first colon:
|
|
13
|
+
* service = "integration", field = "google:access_token"
|
|
14
|
+
* → key = "credential/integration/google:access_token"
|
|
15
|
+
*
|
|
16
|
+
* The new code splits at the last colon:
|
|
17
|
+
* service = "integration:google", field = "access_token"
|
|
18
|
+
* → key = "credential/integration:google/access_token"
|
|
19
|
+
*
|
|
20
|
+
* Detection heuristic: if the field portion of a stored key contains a colon,
|
|
21
|
+
* it was stored with the old indexOf logic and needs re-keying. Simple
|
|
22
|
+
* service:field names (single colon) produce the same key with both methods
|
|
23
|
+
* and don't need migration.
|
|
24
|
+
*/
|
|
25
|
+
export const rekeyCompoundCredentialKeysMigration: WorkspaceMigration = {
|
|
26
|
+
id: "018-rekey-compound-credential-keys",
|
|
27
|
+
description:
|
|
28
|
+
"Re-key compound credential keys from indexOf to lastIndexOf split format",
|
|
29
|
+
|
|
30
|
+
async run(_workspaceDir: string): Promise<void> {
|
|
31
|
+
const {
|
|
32
|
+
listSecureKeysAsync,
|
|
33
|
+
getSecureKeyAsync,
|
|
34
|
+
setSecureKeyAsync,
|
|
35
|
+
deleteSecureKeyAsync,
|
|
36
|
+
} = await import("../../security/secure-keys.js");
|
|
37
|
+
|
|
38
|
+
const { accounts, unreachable } = await listSecureKeysAsync();
|
|
39
|
+
if (unreachable) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
"Credential store unreachable — migration will be retried on next startup",
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let migratedCount = 0;
|
|
46
|
+
let failedCount = 0;
|
|
47
|
+
|
|
48
|
+
for (const account of accounts) {
|
|
49
|
+
if (!account.startsWith(CREDENTIAL_PREFIX)) continue;
|
|
50
|
+
|
|
51
|
+
const rest = account.slice(CREDENTIAL_PREFIX.length);
|
|
52
|
+
const slashIdx = rest.indexOf("/");
|
|
53
|
+
if (slashIdx < 1 || slashIdx >= rest.length - 1) continue;
|
|
54
|
+
|
|
55
|
+
const oldService = rest.slice(0, slashIdx);
|
|
56
|
+
const oldField = rest.slice(slashIdx + 1);
|
|
57
|
+
|
|
58
|
+
// Only migrate keys where the field contains a colon — these were
|
|
59
|
+
// stored using the old indexOf(":") split and need re-keying.
|
|
60
|
+
if (!oldField.includes(":")) continue;
|
|
61
|
+
|
|
62
|
+
// Reconstruct the original "service:field" name and re-split with lastIndexOf
|
|
63
|
+
const originalName = `${oldService}:${oldField}`;
|
|
64
|
+
const lastColonIdx = originalName.lastIndexOf(":");
|
|
65
|
+
const newService = originalName.slice(0, lastColonIdx);
|
|
66
|
+
const newField = originalName.slice(lastColonIdx + 1);
|
|
67
|
+
const newKey = credentialKey(newService, newField);
|
|
68
|
+
|
|
69
|
+
// Skip if the key format didn't actually change
|
|
70
|
+
if (account === newKey) continue;
|
|
71
|
+
|
|
72
|
+
// Skip if the new key already exists (idempotent — may have been
|
|
73
|
+
// partially migrated or the user already stored under the new format)
|
|
74
|
+
const existingNewValue = await getSecureKeyAsync(newKey);
|
|
75
|
+
if (existingNewValue !== undefined) {
|
|
76
|
+
// New key exists — just clean up the old orphaned key
|
|
77
|
+
await deleteSecureKeyAsync(account);
|
|
78
|
+
log.info(
|
|
79
|
+
{ oldKey: account, newKey },
|
|
80
|
+
"Deleted orphaned old-format credential key (new key already exists)",
|
|
81
|
+
);
|
|
82
|
+
migratedCount++;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const value = await getSecureKeyAsync(account);
|
|
87
|
+
if (value === undefined) continue;
|
|
88
|
+
|
|
89
|
+
// Write new key first, then delete old key (crash-safe order)
|
|
90
|
+
const stored = await setSecureKeyAsync(newKey, value);
|
|
91
|
+
if (!stored) {
|
|
92
|
+
log.warn(
|
|
93
|
+
{ oldKey: account, newKey },
|
|
94
|
+
"Failed to write re-keyed credential — skipping",
|
|
95
|
+
);
|
|
96
|
+
failedCount++;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
await deleteSecureKeyAsync(account);
|
|
101
|
+
migratedCount++;
|
|
102
|
+
log.info({ oldKey: account, newKey }, "Re-keyed compound credential");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (migratedCount > 0 || failedCount > 0) {
|
|
106
|
+
log.info(
|
|
107
|
+
{ migratedCount, failedCount },
|
|
108
|
+
"Compound credential key migration complete",
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
async down(_workspaceDir: string): Promise<void> {
|
|
114
|
+
// Reverse: re-key from lastIndexOf format back to indexOf format.
|
|
115
|
+
// Keys where the service contains ":" were migrated from old format.
|
|
116
|
+
const {
|
|
117
|
+
listSecureKeysAsync,
|
|
118
|
+
getSecureKeyAsync,
|
|
119
|
+
setSecureKeyAsync,
|
|
120
|
+
deleteSecureKeyAsync,
|
|
121
|
+
} = await import("../../security/secure-keys.js");
|
|
122
|
+
|
|
123
|
+
const { accounts, unreachable } = await listSecureKeysAsync();
|
|
124
|
+
if (unreachable) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
"Credential store unreachable — rollback will be retried on next startup",
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let rolledBackCount = 0;
|
|
131
|
+
let failedCount = 0;
|
|
132
|
+
|
|
133
|
+
for (const account of accounts) {
|
|
134
|
+
if (!account.startsWith(CREDENTIAL_PREFIX)) continue;
|
|
135
|
+
|
|
136
|
+
const rest = account.slice(CREDENTIAL_PREFIX.length);
|
|
137
|
+
const slashIdx = rest.indexOf("/");
|
|
138
|
+
if (slashIdx < 1 || slashIdx >= rest.length - 1) continue;
|
|
139
|
+
|
|
140
|
+
const service = rest.slice(0, slashIdx);
|
|
141
|
+
const field = rest.slice(slashIdx + 1);
|
|
142
|
+
|
|
143
|
+
// Only rollback keys where the service contains ":" — these are in
|
|
144
|
+
// the new lastIndexOf format and need reverting to indexOf format.
|
|
145
|
+
if (!service.includes(":")) continue;
|
|
146
|
+
|
|
147
|
+
// Reconstruct the original name and re-split with indexOf (old format)
|
|
148
|
+
const originalName = `${service}:${field}`;
|
|
149
|
+
const firstColonIdx = originalName.indexOf(":");
|
|
150
|
+
const oldService = originalName.slice(0, firstColonIdx);
|
|
151
|
+
const oldField = originalName.slice(firstColonIdx + 1);
|
|
152
|
+
const oldKey = credentialKey(oldService, oldField);
|
|
153
|
+
|
|
154
|
+
if (account === oldKey) continue;
|
|
155
|
+
|
|
156
|
+
const value = await getSecureKeyAsync(account);
|
|
157
|
+
if (value === undefined) continue;
|
|
158
|
+
|
|
159
|
+
const stored = await setSecureKeyAsync(oldKey, value);
|
|
160
|
+
if (!stored) {
|
|
161
|
+
log.warn(
|
|
162
|
+
{ newKey: account, oldKey },
|
|
163
|
+
"Failed to rollback re-keyed credential — skipping",
|
|
164
|
+
);
|
|
165
|
+
failedCount++;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
await deleteSecureKeyAsync(account);
|
|
170
|
+
rolledBackCount++;
|
|
171
|
+
log.info(
|
|
172
|
+
{ newKey: account, oldKey },
|
|
173
|
+
"Rolled back compound credential key",
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (rolledBackCount > 0 || failedCount > 0) {
|
|
178
|
+
log.info(
|
|
179
|
+
{ rolledBackCount, failedCount },
|
|
180
|
+
"Compound credential key rollback complete",
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readdirSync,
|
|
5
|
+
renameSync,
|
|
6
|
+
rmdirSync,
|
|
7
|
+
statSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
|
|
11
|
+
import { desc, eq } from "drizzle-orm";
|
|
12
|
+
|
|
13
|
+
import { getDb } from "../../memory/db.js";
|
|
14
|
+
import { contacts } from "../../memory/schema/contacts.js";
|
|
15
|
+
import type { WorkspaceMigration } from "./types.js";
|
|
16
|
+
|
|
17
|
+
export const scopeJournalToGuardianMigration: WorkspaceMigration = {
|
|
18
|
+
id: "019-scope-journal-to-guardian",
|
|
19
|
+
description:
|
|
20
|
+
"Move root journal entries into per-user subdirectory for guardian",
|
|
21
|
+
|
|
22
|
+
run(workspaceDir: string): void {
|
|
23
|
+
const journalDir = join(workspaceDir, "journal");
|
|
24
|
+
if (!existsSync(journalDir)) return;
|
|
25
|
+
|
|
26
|
+
// Find .md files in the root journal directory (not in subdirs)
|
|
27
|
+
let entries: string[];
|
|
28
|
+
try {
|
|
29
|
+
entries = readdirSync(journalDir);
|
|
30
|
+
} catch {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const mdFiles = entries.filter((f) => {
|
|
34
|
+
if (!f.endsWith(".md") || f.toLowerCase() === "readme.md") return false;
|
|
35
|
+
try {
|
|
36
|
+
return statSync(join(journalDir, f)).isFile();
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
if (mdFiles.length === 0) return;
|
|
42
|
+
|
|
43
|
+
// Resolve guardian user slug (same pattern as 017-seed-persona-dirs)
|
|
44
|
+
let slug = "guardian";
|
|
45
|
+
try {
|
|
46
|
+
const db = getDb();
|
|
47
|
+
const guardian = db
|
|
48
|
+
.select()
|
|
49
|
+
.from(contacts)
|
|
50
|
+
.where(eq(contacts.role, "guardian"))
|
|
51
|
+
.orderBy(desc(contacts.createdAt))
|
|
52
|
+
.limit(1)
|
|
53
|
+
.get();
|
|
54
|
+
if (guardian?.userFile) {
|
|
55
|
+
slug = guardian.userFile.replace(/\.md$/, "");
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
// DB not ready — use fallback "guardian"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Create per-user directory and move files (renameSync preserves birthtimes)
|
|
62
|
+
const destDir = join(journalDir, slug);
|
|
63
|
+
mkdirSync(destDir, { recursive: true });
|
|
64
|
+
for (const f of mdFiles) {
|
|
65
|
+
const src = join(journalDir, f);
|
|
66
|
+
const dest = join(destDir, f);
|
|
67
|
+
if (!existsSync(dest)) {
|
|
68
|
+
renameSync(src, dest);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
down(workspaceDir: string): void {
|
|
74
|
+
const journalDir = join(workspaceDir, "journal");
|
|
75
|
+
if (!existsSync(journalDir)) return;
|
|
76
|
+
let entries: string[];
|
|
77
|
+
try {
|
|
78
|
+
entries = readdirSync(journalDir);
|
|
79
|
+
} catch {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
const subdir = join(journalDir, entry);
|
|
84
|
+
try {
|
|
85
|
+
if (!statSync(subdir).isDirectory()) continue;
|
|
86
|
+
} catch {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
for (const f of readdirSync(subdir)) {
|
|
90
|
+
if (!f.endsWith(".md")) continue;
|
|
91
|
+
const dest = join(journalDir, f);
|
|
92
|
+
if (!existsSync(dest)) {
|
|
93
|
+
renameSync(join(subdir, f), dest);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
rmdirSync(subdir);
|
|
98
|
+
} catch {
|
|
99
|
+
// not empty — leave it
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
};
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
* Workspace migration: Migrate workspace data from /data to /workspace volume.
|
|
3
3
|
*
|
|
4
4
|
* In the old Docker volume layout, workspace data lived at
|
|
5
|
-
* `$BASE_DATA_DIR/.vellum/workspace`. In the new layout,
|
|
5
|
+
* `$BASE_DATA_DIR/.vellum/workspace`. In the new layout, VELLUM_WORKSPACE_DIR points
|
|
6
6
|
* to a dedicated volume (e.g. `/workspace`). On first boot with the new layout,
|
|
7
7
|
* this migration copies existing workspace data from the old location to the
|
|
8
8
|
* new volume so nothing is lost.
|
|
9
9
|
*
|
|
10
10
|
* Idempotent:
|
|
11
|
-
* - Skips if
|
|
11
|
+
* - Skips if VELLUM_WORKSPACE_DIR is not set (non-Docker or old layout).
|
|
12
12
|
* - Skips if the workspace volume already has data (config.json exists).
|
|
13
13
|
* - Skips if the sentinel file exists (already migrated).
|
|
14
14
|
* - Skips if the old workspace directory doesn't exist or is empty.
|
|
@@ -34,7 +34,7 @@ const SENTINEL_FILENAME = ".workspace-volume-migrated";
|
|
|
34
34
|
export const migrateToWorkspaceVolumeMigration: WorkspaceMigration = {
|
|
35
35
|
id: "014-migrate-to-workspace-volume",
|
|
36
36
|
description:
|
|
37
|
-
"Copy workspace data from old /data/.vellum/workspace to new
|
|
37
|
+
"Copy workspace data from old /data/.vellum/workspace to new VELLUM_WORKSPACE_DIR volume on first boot",
|
|
38
38
|
|
|
39
39
|
down(workspaceDir: string): void {
|
|
40
40
|
// This migration copies data between volumes. Actually reversing the copy
|
|
@@ -55,7 +55,7 @@ export const migrateToWorkspaceVolumeMigration: WorkspaceMigration = {
|
|
|
55
55
|
run(workspaceDir: string): void {
|
|
56
56
|
const workspaceDirOverride = getWorkspaceDirOverride();
|
|
57
57
|
|
|
58
|
-
// Only relevant when
|
|
58
|
+
// Only relevant when VELLUM_WORKSPACE_DIR is explicitly set (Docker with separate volume)
|
|
59
59
|
if (!workspaceDirOverride) return;
|
|
60
60
|
|
|
61
61
|
const sentinelPath = join(workspaceDir, SENTINEL_FILENAME);
|
|
@@ -14,6 +14,8 @@ import { migrateCredentialsToKeychainMigration } from "./015-migrate-credentials
|
|
|
14
14
|
import { extractFeatureFlagsToProtectedMigration } from "./016-extract-feature-flags-to-protected.js";
|
|
15
15
|
import { migrateCredentialsFromKeychainMigration } from "./016-migrate-credentials-from-keychain.js";
|
|
16
16
|
import { seedPersonaDirsMigration } from "./017-seed-persona-dirs.js";
|
|
17
|
+
import { rekeyCompoundCredentialKeysMigration } from "./018-rekey-compound-credential-keys.js";
|
|
18
|
+
import { scopeJournalToGuardianMigration } from "./019-scope-journal-to-guardian.js";
|
|
17
19
|
import { migrateToWorkspaceVolumeMigration } from "./migrate-to-workspace-volume.js";
|
|
18
20
|
import type { WorkspaceMigration } from "./types.js";
|
|
19
21
|
|
|
@@ -39,4 +41,6 @@ export const WORKSPACE_MIGRATIONS: WorkspaceMigration[] = [
|
|
|
39
41
|
migrateCredentialsFromKeychainMigration,
|
|
40
42
|
seedPersonaDirsMigration,
|
|
41
43
|
extractFeatureFlagsToProtectedMigration,
|
|
44
|
+
rekeyCompoundCredentialKeysMigration,
|
|
45
|
+
scopeJournalToGuardianMigration,
|
|
42
46
|
];
|
|
@@ -52,15 +52,7 @@ const KEYLESS_PROVIDERS = new Set(["ollama"]);
|
|
|
52
52
|
const deterministicProvider = new DefaultCommitMessageProvider();
|
|
53
53
|
|
|
54
54
|
function getProviderCandidates(config: ReturnType<typeof getConfig>): string[] {
|
|
55
|
-
|
|
56
|
-
const seen = new Set<string>();
|
|
57
|
-
const out: string[] = [];
|
|
58
|
-
for (const name of [config.services.inference.provider, ...order]) {
|
|
59
|
-
if (seen.has(name)) continue;
|
|
60
|
-
seen.add(name);
|
|
61
|
-
out.push(name);
|
|
62
|
-
}
|
|
63
|
-
return out;
|
|
55
|
+
return [config.services.inference.provider];
|
|
64
56
|
}
|
|
65
57
|
|
|
66
58
|
function buildDeterministicResult(
|
|
@@ -121,7 +113,7 @@ export class ProviderCommitMessageGenerator {
|
|
|
121
113
|
|
|
122
114
|
// ── Fallback check order (canonical) ──────────────────────────────
|
|
123
115
|
// 1. disabled
|
|
124
|
-
// 2. resolve configured provider
|
|
116
|
+
// 2. resolve configured provider:
|
|
125
117
|
// - missing_provider_api_key OR provider_not_initialized
|
|
126
118
|
// 3. selected-provider API key preflight (except keyless providers)
|
|
127
119
|
// 4. breaker_open
|
|
@@ -138,7 +130,7 @@ export class ProviderCommitMessageGenerator {
|
|
|
138
130
|
return buildDeterministicResult(context, "disabled");
|
|
139
131
|
}
|
|
140
132
|
|
|
141
|
-
// Step 2: Resolve configured provider
|
|
133
|
+
// Step 2: Resolve configured provider.
|
|
142
134
|
// If nothing is resolvable, differentiate likely missing-key cases from
|
|
143
135
|
// true registry/init failures.
|
|
144
136
|
const resolved = await resolveConfiguredProvider();
|
|
@@ -168,18 +160,17 @@ export class ProviderCommitMessageGenerator {
|
|
|
168
160
|
}
|
|
169
161
|
|
|
170
162
|
const provider = resolved.provider;
|
|
171
|
-
const
|
|
163
|
+
const providerName = resolved.configuredProviderName;
|
|
172
164
|
|
|
173
|
-
// Step 2b: API key preflight for the
|
|
174
|
-
if (!KEYLESS_PROVIDERS.has(
|
|
175
|
-
const providerApiKey = await getSecureKeyAsync(
|
|
165
|
+
// Step 2b: API key preflight for the configured provider (skip keyless).
|
|
166
|
+
if (!KEYLESS_PROVIDERS.has(providerName)) {
|
|
167
|
+
const providerApiKey = await getSecureKeyAsync(providerName);
|
|
176
168
|
if (!providerApiKey) {
|
|
177
169
|
log.debug(
|
|
178
170
|
{
|
|
179
|
-
|
|
180
|
-
configuredProvider: config.services.inference.provider,
|
|
171
|
+
provider: providerName,
|
|
181
172
|
},
|
|
182
|
-
"
|
|
173
|
+
"Provider API key missing; falling back to deterministic",
|
|
183
174
|
);
|
|
184
175
|
return buildDeterministicResult(context, "missing_provider_api_key");
|
|
185
176
|
}
|
|
@@ -211,13 +202,13 @@ export class ProviderCommitMessageGenerator {
|
|
|
211
202
|
|
|
212
203
|
// Step 5: Fast model preflight — resolve before any provider call
|
|
213
204
|
const fastModel =
|
|
214
|
-
llmConfig.providerFastModelOverrides[
|
|
215
|
-
PROVIDER_DEFAULT_FAST_MODELS[
|
|
205
|
+
llmConfig.providerFastModelOverrides[providerName] ??
|
|
206
|
+
PROVIDER_DEFAULT_FAST_MODELS[providerName];
|
|
216
207
|
|
|
217
208
|
if (!fastModel) {
|
|
218
209
|
log.debug(
|
|
219
210
|
{
|
|
220
|
-
provider:
|
|
211
|
+
provider: providerName,
|
|
221
212
|
configuredProvider: config.services.inference.provider,
|
|
222
213
|
},
|
|
223
214
|
"No fast model resolvable for provider; falling back to deterministic",
|