@vellumai/assistant 0.5.4 → 0.5.6
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 +17 -27
- package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-contracts/src/trust-rules.ts +42 -0
- package/package.json +1 -1
- package/src/__tests__/actor-token-service.test.ts +113 -0
- package/src/__tests__/config-schema.test.ts +2 -2
- package/src/__tests__/context-window-manager.test.ts +78 -0
- package/src/__tests__/conversation-title-service.test.ts +30 -1
- package/src/__tests__/credential-security-invariants.test.ts +2 -0
- package/src/__tests__/docker-signing-key-bootstrap.test.ts +207 -0
- package/src/__tests__/memory-regressions.test.ts +8 -30
- package/src/__tests__/openai-whisper.test.ts +93 -0
- package/src/__tests__/require-fresh-approval.test.ts +4 -0
- package/src/__tests__/slack-messaging-token-resolution.test.ts +319 -0
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +4 -0
- package/src/__tests__/tool-executor.test.ts +4 -0
- package/src/__tests__/volume-security-guard.test.ts +155 -0
- package/src/cli/commands/conversations.ts +0 -18
- package/src/config/bundled-skills/messaging/tools/shared.ts +1 -0
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +16 -37
- package/src/config/env-registry.ts +9 -0
- package/src/config/env.ts +8 -2
- package/src/config/feature-flag-registry.json +8 -8
- package/src/config/schema.ts +0 -12
- package/src/config/schemas/memory.ts +0 -4
- package/src/config/schemas/platform.ts +1 -1
- package/src/config/schemas/security.ts +4 -0
- package/src/context/window-manager.ts +53 -2
- package/src/credential-execution/managed-catalog.ts +5 -15
- package/src/daemon/conversation-agent-loop.ts +0 -60
- package/src/daemon/conversation-memory.ts +0 -117
- package/src/daemon/conversation-runtime-assembly.ts +0 -2
- package/src/daemon/daemon-control.ts +7 -0
- package/src/daemon/handlers/conversations.ts +0 -11
- package/src/daemon/lifecycle.ts +10 -47
- package/src/daemon/providers-setup.ts +2 -1
- package/src/followups/followup-store.ts +5 -2
- package/src/hooks/manager.ts +7 -0
- package/src/instrument.ts +33 -1
- package/src/memory/conversation-crud.ts +0 -236
- package/src/memory/conversation-title-service.ts +26 -10
- package/src/memory/db-init.ts +5 -13
- package/src/memory/embedding-local.ts +11 -5
- package/src/memory/indexer.ts +15 -106
- package/src/memory/job-handlers/conversation-starters.ts +24 -36
- package/src/memory/job-handlers/embedding.ts +0 -79
- package/src/memory/job-utils.ts +1 -1
- package/src/memory/jobs-store.ts +0 -8
- package/src/memory/jobs-worker.ts +0 -20
- package/src/memory/migrations/189-drop-simplified-memory.ts +42 -0
- package/src/memory/migrations/index.ts +1 -3
- package/src/memory/qdrant-client.ts +4 -6
- package/src/memory/schema/conversations.ts +0 -3
- package/src/memory/schema/index.ts +0 -2
- package/src/messaging/draft-store.ts +2 -2
- package/src/messaging/provider.ts +9 -0
- package/src/messaging/providers/slack/adapter.ts +29 -2
- package/src/oauth/connection-resolver.test.ts +22 -18
- package/src/oauth/connection-resolver.ts +92 -7
- package/src/oauth/platform-connection.test.ts +78 -69
- package/src/oauth/platform-connection.ts +12 -19
- package/src/permissions/defaults.ts +3 -3
- package/src/permissions/trust-client.ts +332 -0
- package/src/permissions/trust-store-interface.ts +105 -0
- package/src/permissions/trust-store.ts +531 -39
- package/src/platform/client.test.ts +148 -0
- package/src/platform/client.ts +71 -0
- package/src/providers/speech-to-text/openai-whisper.test.ts +190 -0
- package/src/providers/speech-to-text/openai-whisper.ts +68 -0
- package/src/providers/speech-to-text/resolve.ts +9 -0
- package/src/providers/speech-to-text/types.ts +17 -0
- package/src/runtime/auth/route-policy.ts +14 -0
- package/src/runtime/auth/token-service.ts +133 -0
- package/src/runtime/http-server.ts +4 -2
- package/src/runtime/routes/conversation-management-routes.ts +0 -36
- package/src/runtime/routes/conversation-query-routes.ts +44 -2
- package/src/runtime/routes/conversation-routes.ts +2 -1
- package/src/runtime/routes/inbound-message-handler.ts +27 -3
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -1
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +287 -0
- package/src/runtime/routes/inbound-stages/transcribe-audio.ts +122 -0
- package/src/runtime/routes/log-export-routes.ts +1 -0
- package/src/runtime/routes/memory-item-routes.test.ts +221 -3
- package/src/runtime/routes/memory-item-routes.ts +124 -2
- package/src/runtime/routes/secret-routes.ts +4 -1
- package/src/runtime/routes/upgrade-broadcast-routes.ts +151 -0
- package/src/schedule/schedule-store.ts +0 -21
- package/src/security/ces-credential-client.ts +173 -0
- package/src/security/secure-keys.ts +65 -22
- package/src/signals/bash.ts +3 -0
- package/src/signals/cancel.ts +3 -0
- package/src/signals/confirm.ts +3 -0
- package/src/signals/conversation-undo.ts +3 -0
- package/src/signals/event-stream.ts +7 -0
- package/src/signals/shotgun.ts +3 -0
- package/src/signals/trust-rule.ts +3 -0
- package/src/skills/inline-command-render.ts +5 -1
- package/src/skills/inline-command-runner.ts +30 -2
- package/src/telemetry/usage-telemetry-reporter.test.ts +23 -36
- package/src/telemetry/usage-telemetry-reporter.ts +21 -19
- package/src/tools/memory/handlers.ts +1 -129
- package/src/tools/permission-checker.ts +18 -0
- package/src/tools/skills/load.ts +9 -2
- package/src/util/device-id.ts +70 -7
- package/src/util/logger.ts +35 -9
- package/src/util/platform.ts +29 -5
- package/src/util/xml.ts +8 -0
- package/src/workspace/heartbeat-service.ts +5 -24
- package/src/workspace/migrations/migrate-to-workspace-volume.ts +113 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/__tests__/archive-recall.test.ts +0 -560
- package/src/__tests__/conversation-memory-dirty-tail.test.ts +0 -150
- package/src/__tests__/conversation-switch-memory-reduction.test.ts +0 -474
- package/src/__tests__/db-memory-archive-migration.test.ts +0 -372
- package/src/__tests__/db-memory-brief-state-migration.test.ts +0 -213
- package/src/__tests__/db-memory-reducer-checkpoints.test.ts +0 -273
- package/src/__tests__/memory-brief-open-loops.test.ts +0 -530
- package/src/__tests__/memory-brief-time.test.ts +0 -285
- package/src/__tests__/memory-brief-wrapper.test.ts +0 -311
- package/src/__tests__/memory-chunk-archive.test.ts +0 -400
- package/src/__tests__/memory-chunk-dual-write.test.ts +0 -453
- package/src/__tests__/memory-episode-archive.test.ts +0 -370
- package/src/__tests__/memory-episode-dual-write.test.ts +0 -626
- package/src/__tests__/memory-observation-archive.test.ts +0 -375
- package/src/__tests__/memory-observation-dual-write.test.ts +0 -318
- package/src/__tests__/memory-reducer-job.test.ts +0 -538
- package/src/__tests__/memory-reducer-scheduling.test.ts +0 -473
- package/src/__tests__/memory-reducer-store.test.ts +0 -728
- package/src/__tests__/memory-reducer-types.test.ts +0 -707
- package/src/__tests__/memory-reducer.test.ts +0 -704
- package/src/__tests__/memory-simplified-config.test.ts +0 -281
- package/src/__tests__/simplified-memory-e2e.test.ts +0 -666
- package/src/__tests__/simplified-memory-runtime.test.ts +0 -616
- package/src/config/schemas/memory-simplified.ts +0 -101
- package/src/memory/archive-recall.ts +0 -516
- package/src/memory/archive-store.ts +0 -400
- package/src/memory/brief-formatting.ts +0 -33
- package/src/memory/brief-open-loops.ts +0 -266
- package/src/memory/brief-time.ts +0 -162
- package/src/memory/brief.ts +0 -75
- package/src/memory/job-handlers/backfill-simplified-memory.ts +0 -462
- package/src/memory/job-handlers/reduce-conversation-memory.ts +0 -229
- package/src/memory/migrations/185-memory-brief-state.ts +0 -52
- package/src/memory/migrations/186-memory-archive.ts +0 -109
- package/src/memory/migrations/187-memory-reducer-checkpoints.ts +0 -19
- package/src/memory/reducer-scheduler.ts +0 -242
- package/src/memory/reducer-store.ts +0 -271
- package/src/memory/reducer-types.ts +0 -106
- package/src/memory/reducer.ts +0 -467
- package/src/memory/schema/memory-archive.ts +0 -121
- package/src/memory/schema/memory-brief.ts +0 -55
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
import { queryUnreportedLifecycleEvents } from "../memory/lifecycle-events-store.js";
|
|
23
23
|
import { queryUnreportedUsageEvents } from "../memory/llm-usage-store.js";
|
|
24
24
|
import { queryUnreportedTurnEvents } from "../memory/turn-events-store.js";
|
|
25
|
-
import {
|
|
25
|
+
import { VellumPlatformClient } from "../platform/client.js";
|
|
26
26
|
import { DAEMON_INTERNAL_ASSISTANT_ID } from "../runtime/assistant-scope.js";
|
|
27
27
|
import { getExternalAssistantId } from "../runtime/auth/external-assistant-id.js";
|
|
28
28
|
import { getDeviceId } from "../util/device-id.js";
|
|
@@ -139,22 +139,11 @@ export class UsageTelemetryReporter {
|
|
|
139
139
|
return;
|
|
140
140
|
|
|
141
141
|
// Resolve auth context — skip flush when neither auth mode is viable
|
|
142
|
-
const
|
|
143
|
-
if (!
|
|
142
|
+
const client = await VellumPlatformClient.create();
|
|
143
|
+
if (!client && !getTelemetryAppToken()) {
|
|
144
144
|
return;
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
let url: string;
|
|
148
|
-
let authHeaders: Record<string, string>;
|
|
149
|
-
|
|
150
|
-
if (proxyCtx.enabled) {
|
|
151
|
-
url = `${proxyCtx.platformBaseUrl}${TELEMETRY_PATH}`;
|
|
152
|
-
authHeaders = { Authorization: `Api-Key ${proxyCtx.assistantApiKey}` };
|
|
153
|
-
} else {
|
|
154
|
-
url = `${getTelemetryPlatformUrl()}${TELEMETRY_PATH}`;
|
|
155
|
-
authHeaders = { "X-Telemetry-Token": getTelemetryAppToken() };
|
|
156
|
-
}
|
|
157
|
-
|
|
158
147
|
// Build payload
|
|
159
148
|
const typedEvents: TelemetryEvent[] = [
|
|
160
149
|
...events.map(
|
|
@@ -195,26 +184,39 @@ export class UsageTelemetryReporter {
|
|
|
195
184
|
const payload = {
|
|
196
185
|
device_id: getDeviceId(),
|
|
197
186
|
assistant_id: assistantId,
|
|
198
|
-
|
|
187
|
+
assistant_version: APP_VERSION,
|
|
199
188
|
...(organizationId ? { organization_id: organizationId } : {}),
|
|
200
189
|
...(userId ? { user_id: userId } : {}),
|
|
201
190
|
events: typedEvents,
|
|
202
191
|
};
|
|
203
192
|
|
|
204
193
|
// Send
|
|
205
|
-
const
|
|
194
|
+
const fetchInit: RequestInit = {
|
|
206
195
|
method: "POST",
|
|
207
196
|
headers: {
|
|
208
197
|
"Content-Type": "application/json",
|
|
209
|
-
...authHeaders,
|
|
210
198
|
},
|
|
211
199
|
body: JSON.stringify(payload),
|
|
212
|
-
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
let resp: Response;
|
|
203
|
+
if (client) {
|
|
204
|
+
resp = await client.fetch(TELEMETRY_PATH, fetchInit);
|
|
205
|
+
} else {
|
|
206
|
+
const url = `${getTelemetryPlatformUrl()}${TELEMETRY_PATH}`;
|
|
207
|
+
resp = await fetch(url, {
|
|
208
|
+
...fetchInit,
|
|
209
|
+
headers: {
|
|
210
|
+
"Content-Type": "application/json",
|
|
211
|
+
"X-Telemetry-Token": getTelemetryAppToken(),
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
}
|
|
213
215
|
|
|
214
216
|
if (!resp.ok) {
|
|
215
217
|
await resp.text(); // consume body to release connection
|
|
216
218
|
log.warn(
|
|
217
|
-
{ status: resp.status
|
|
219
|
+
{ status: resp.status },
|
|
218
220
|
"Usage telemetry POST failed — will retry next cycle",
|
|
219
221
|
);
|
|
220
222
|
return;
|
|
@@ -2,8 +2,6 @@ import { and, eq, ne } from "drizzle-orm";
|
|
|
2
2
|
import { v4 as uuid } from "uuid";
|
|
3
3
|
|
|
4
4
|
import type { AssistantConfig } from "../../config/types.js";
|
|
5
|
-
import { buildArchiveRecall } from "../../memory/archive-recall.js";
|
|
6
|
-
import { insertObservation } from "../../memory/archive-store.js";
|
|
7
5
|
import { getDb } from "../../memory/db.js";
|
|
8
6
|
import { computeMemoryFingerprint } from "../../memory/fingerprint.js";
|
|
9
7
|
import { enqueueMemoryJob } from "../../memory/jobs-store.js";
|
|
@@ -20,7 +18,7 @@ const log = getLogger("memory-tools");
|
|
|
20
18
|
|
|
21
19
|
export async function handleMemorySave(
|
|
22
20
|
args: Record<string, unknown>,
|
|
23
|
-
|
|
21
|
+
_config: AssistantConfig,
|
|
24
22
|
conversationId: string,
|
|
25
23
|
messageId: string | undefined,
|
|
26
24
|
scopeId: string = "default",
|
|
@@ -65,19 +63,6 @@ export async function handleMemorySave(
|
|
|
65
63
|
? truncate(args.subject.trim(), 80, "")
|
|
66
64
|
: inferSubjectFromStatement(statement.trim());
|
|
67
65
|
|
|
68
|
-
// When simplified memory is enabled, save directly to the simplified
|
|
69
|
-
// observation/chunk tables instead of the legacy memory_items table.
|
|
70
|
-
if (config.memory.simplified.enabled) {
|
|
71
|
-
return handleSimplifiedMemorySave(
|
|
72
|
-
kind,
|
|
73
|
-
subject,
|
|
74
|
-
statement.trim(),
|
|
75
|
-
conversationId,
|
|
76
|
-
messageId,
|
|
77
|
-
scopeId,
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
66
|
try {
|
|
82
67
|
const db = getDb();
|
|
83
68
|
const id = uuid();
|
|
@@ -290,12 +275,6 @@ export async function handleMemoryRecall(
|
|
|
290
275
|
? args.scope.trim()
|
|
291
276
|
: "default";
|
|
292
277
|
|
|
293
|
-
// When simplified memory is enabled, use the archive recall path
|
|
294
|
-
// instead of the legacy hybrid retriever.
|
|
295
|
-
if (config.memory.simplified.enabled) {
|
|
296
|
-
return handleSimplifiedMemoryRecall(query.trim(), scopeId ?? "default");
|
|
297
|
-
}
|
|
298
|
-
|
|
299
278
|
// Scope policy: "conversation" means strict (only that scope),
|
|
300
279
|
// anything else allows fallback to the default scope.
|
|
301
280
|
const scopePolicyOverride: ScopePolicyOverride | undefined = scopeId
|
|
@@ -432,113 +411,6 @@ export async function handleMemoryDelete(
|
|
|
432
411
|
}
|
|
433
412
|
}
|
|
434
413
|
|
|
435
|
-
// ── Simplified memory helpers ────────────────────────────────────────
|
|
436
|
-
|
|
437
|
-
/**
|
|
438
|
-
* Save a memory item as an observation + chunk in the simplified system.
|
|
439
|
-
* This is used when simplified memory is enabled instead of writing to
|
|
440
|
-
* the legacy memory_items table.
|
|
441
|
-
*/
|
|
442
|
-
function handleSimplifiedMemorySave(
|
|
443
|
-
kind: string,
|
|
444
|
-
subject: string,
|
|
445
|
-
statement: string,
|
|
446
|
-
conversationId: string,
|
|
447
|
-
messageId: string | undefined,
|
|
448
|
-
scopeId: string,
|
|
449
|
-
): ToolExecutionResult {
|
|
450
|
-
try {
|
|
451
|
-
const trimmedStatement = truncate(statement, 500, "");
|
|
452
|
-
const content = `[${kind}] ${subject}: ${trimmedStatement}`;
|
|
453
|
-
|
|
454
|
-
const result = insertObservation({
|
|
455
|
-
conversationId,
|
|
456
|
-
messageId: messageId ?? null,
|
|
457
|
-
role: "user",
|
|
458
|
-
content,
|
|
459
|
-
scopeId,
|
|
460
|
-
modality: "text",
|
|
461
|
-
source: "tool:memory_save",
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
log.debug(
|
|
465
|
-
{
|
|
466
|
-
observationId: result.observationId,
|
|
467
|
-
chunkId: result.chunkId,
|
|
468
|
-
kind,
|
|
469
|
-
subject,
|
|
470
|
-
conversationId,
|
|
471
|
-
messageId,
|
|
472
|
-
},
|
|
473
|
-
"Memory saved via simplified system",
|
|
474
|
-
);
|
|
475
|
-
|
|
476
|
-
return {
|
|
477
|
-
content: `Saved to memory (ID: ${result.observationId}).\nKind: ${kind}\nSubject: ${subject}\nStatement: ${trimmedStatement}`,
|
|
478
|
-
isError: false,
|
|
479
|
-
};
|
|
480
|
-
} catch (err) {
|
|
481
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
482
|
-
log.error({ err }, "simplified memory_save failed");
|
|
483
|
-
return { content: `Error: Failed to save memory: ${msg}`, isError: true };
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
/**
|
|
488
|
-
* Recall memories using the simplified archive recall path instead of
|
|
489
|
-
* the legacy hybrid retriever.
|
|
490
|
-
*/
|
|
491
|
-
function handleSimplifiedMemoryRecall(
|
|
492
|
-
query: string,
|
|
493
|
-
scopeId: string,
|
|
494
|
-
): ToolExecutionResult {
|
|
495
|
-
try {
|
|
496
|
-
const recallResult = buildArchiveRecall(scopeId, query);
|
|
497
|
-
|
|
498
|
-
if (recallResult.bullets.length === 0) {
|
|
499
|
-
return {
|
|
500
|
-
content: JSON.stringify({
|
|
501
|
-
text: "No matching memories found.",
|
|
502
|
-
resultCount: 0,
|
|
503
|
-
degraded: false,
|
|
504
|
-
items: [],
|
|
505
|
-
sources: { semantic: 0, recency: 0 },
|
|
506
|
-
}),
|
|
507
|
-
isError: false,
|
|
508
|
-
};
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
const items = recallResult.bullets.map((b) => ({
|
|
512
|
-
id: b.sourceId,
|
|
513
|
-
type: b.source,
|
|
514
|
-
kind: b.source,
|
|
515
|
-
}));
|
|
516
|
-
|
|
517
|
-
const result = {
|
|
518
|
-
text: recallResult.text,
|
|
519
|
-
resultCount: recallResult.bullets.length,
|
|
520
|
-
degraded: false,
|
|
521
|
-
items,
|
|
522
|
-
sources: {
|
|
523
|
-
semantic: recallResult.prefetchHitCount,
|
|
524
|
-
recency: 0,
|
|
525
|
-
},
|
|
526
|
-
};
|
|
527
|
-
|
|
528
|
-
return {
|
|
529
|
-
content: JSON.stringify(result),
|
|
530
|
-
isError: false,
|
|
531
|
-
};
|
|
532
|
-
} catch (err) {
|
|
533
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
534
|
-
log.error({ err, query }, "simplified memory_recall failed");
|
|
535
|
-
return {
|
|
536
|
-
content: `Error: Memory recall failed: ${msg}`,
|
|
537
|
-
isError: true,
|
|
538
|
-
};
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
|
|
542
414
|
// ── Helpers ──────────────────────────────────────────────────────────
|
|
543
415
|
|
|
544
416
|
function inferSubjectFromStatement(statement: string): string {
|
|
@@ -137,6 +137,24 @@ export class PermissionChecker {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
if (result.decision === "prompt") {
|
|
140
|
+
// dangerouslySkipPermissions: when enabled, auto-approve all prompts
|
|
141
|
+
// without user interaction. Deny rules are still respected (they
|
|
142
|
+
// return before reaching this block).
|
|
143
|
+
//
|
|
144
|
+
// Note: unlike guardian auto-approve and temporary overrides below,
|
|
145
|
+
// this intentionally does NOT check `context.requireFreshApproval`.
|
|
146
|
+
// The setting is designed to skip ALL interactive prompts
|
|
147
|
+
// unconditionally — it is an explicit operator opt-out from the
|
|
148
|
+
// permission system, so requireFreshApproval does not apply.
|
|
149
|
+
const cfg = getConfig();
|
|
150
|
+
if (cfg.permissions.dangerouslySkipPermissions) {
|
|
151
|
+
log.info(
|
|
152
|
+
{ toolName: name, riskLevel },
|
|
153
|
+
"dangerouslySkipPermissions active — auto-approving without prompt",
|
|
154
|
+
);
|
|
155
|
+
return { allowed: true, decision: "dangerously_skip_permissions", riskLevel };
|
|
156
|
+
}
|
|
157
|
+
|
|
140
158
|
// Guardian-trust sessions (e.g. scheduled jobs, reminders) should be
|
|
141
159
|
// able to use bundled tools without interactive approval. The guardian
|
|
142
160
|
// is the owner - prompting makes no sense when there is no client.
|
package/src/tools/skills/load.ts
CHANGED
|
@@ -440,9 +440,16 @@ export class SkillLoadTool implements Tool {
|
|
|
440
440
|
"Rendered inline command expansions for included skill",
|
|
441
441
|
);
|
|
442
442
|
} catch (err) {
|
|
443
|
-
log.
|
|
443
|
+
log.error(
|
|
444
444
|
{ err, skillId: childId, parentSkillId: skill.id },
|
|
445
|
-
"Failed to render inline commands for included skill
|
|
445
|
+
"Failed to render inline commands for included skill; falling back to sanitized body",
|
|
446
|
+
);
|
|
447
|
+
// Strip raw !`...` inline command tokens so they don't leak into
|
|
448
|
+
// the prompt. Replace with a safe stub to maintain fail-closed
|
|
449
|
+
// contract for raw tokens while still isolating child failures.
|
|
450
|
+
childBody = childBody.replace(
|
|
451
|
+
/!`[^`]*`/g,
|
|
452
|
+
"[inline command unavailable]",
|
|
446
453
|
);
|
|
447
454
|
}
|
|
448
455
|
}
|
package/src/util/device-id.ts
CHANGED
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
* extensible for future per-device metadata.
|
|
7
7
|
*
|
|
8
8
|
* Path resolution:
|
|
9
|
-
* - Containerized (IS_CONTAINERIZED=true): uses
|
|
10
|
-
* persistent
|
|
9
|
+
* - Containerized (IS_CONTAINERIZED=true): uses /home/assistant (the assistant
|
|
10
|
+
* user's persistent home dir) so device.json lives on the assistant's own
|
|
11
|
+
* filesystem rather than the shared data volume. Falls back to BASE_DATA_DIR
|
|
12
|
+
* for migration from the old location.
|
|
11
13
|
* - Local (single or multi-instance): uses homedir() so all instances on the
|
|
12
14
|
* same machine share a single device ID, even when BASE_DATA_DIR is set to
|
|
13
15
|
* an instance-scoped directory.
|
|
@@ -31,18 +33,34 @@ let cached: string | undefined;
|
|
|
31
33
|
/**
|
|
32
34
|
* Resolve the base directory for device.json.
|
|
33
35
|
*
|
|
34
|
-
* In containerized environments,
|
|
35
|
-
*
|
|
36
|
+
* In containerized environments, device.json is stored under /home/assistant
|
|
37
|
+
* (the assistant user's persistent home dir) rather than on the shared data
|
|
38
|
+
* volume. Device ID is assistant-specific state that doesn't need to be shared.
|
|
36
39
|
* In local environments (including multi-instance), homedir() is stable and
|
|
37
40
|
* shared across instances, giving a true per-machine device ID.
|
|
38
41
|
*/
|
|
39
42
|
export function getDeviceIdBaseDir(): string {
|
|
40
43
|
if (getIsContainerized()) {
|
|
41
|
-
return
|
|
44
|
+
return "/home/assistant";
|
|
42
45
|
}
|
|
43
46
|
return homedir();
|
|
44
47
|
}
|
|
45
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Resolve the legacy base directory for device.json migration.
|
|
51
|
+
*
|
|
52
|
+
* Returns the old containerized path (BASE_DATA_DIR) so we can fall back to
|
|
53
|
+
* reading device.json from the shared volume if it hasn't been migrated yet.
|
|
54
|
+
* Returns undefined when not containerized or when no legacy path exists.
|
|
55
|
+
*/
|
|
56
|
+
function getLegacyDeviceIdBaseDir(): string | undefined {
|
|
57
|
+
if (!getIsContainerized()) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
const baseDataDir = getBaseDataDir();
|
|
61
|
+
return baseDataDir || undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
46
64
|
/**
|
|
47
65
|
* Get the stable device ID for this machine.
|
|
48
66
|
*
|
|
@@ -78,10 +96,55 @@ export function getDeviceId(): string {
|
|
|
78
96
|
}
|
|
79
97
|
}
|
|
80
98
|
} catch (err) {
|
|
81
|
-
log.warn({ err }, "Failed to read device.json —
|
|
99
|
+
log.warn({ err }, "Failed to read device.json — checking legacy path");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Migration fallback: check the legacy location (shared volume) if the new
|
|
103
|
+
// location doesn't have a valid device.json yet.
|
|
104
|
+
const legacyBase = getLegacyDeviceIdBaseDir();
|
|
105
|
+
if (legacyBase) {
|
|
106
|
+
const legacyPath = join(legacyBase, ".vellum", "device.json");
|
|
107
|
+
try {
|
|
108
|
+
if (existsSync(legacyPath)) {
|
|
109
|
+
const raw = JSON.parse(readFileSync(legacyPath, "utf-8"));
|
|
110
|
+
if (
|
|
111
|
+
raw &&
|
|
112
|
+
typeof raw === "object" &&
|
|
113
|
+
typeof raw.deviceId === "string" &&
|
|
114
|
+
raw.deviceId.length > 0
|
|
115
|
+
) {
|
|
116
|
+
cached = raw.deviceId as string;
|
|
117
|
+
log.info(
|
|
118
|
+
{ deviceId: cached },
|
|
119
|
+
"Resolved device ID from legacy device.json — will persist to new location",
|
|
120
|
+
);
|
|
121
|
+
// Persist to the new location so future reads don't need the fallback
|
|
122
|
+
try {
|
|
123
|
+
mkdirSync(vellumDir, { recursive: true });
|
|
124
|
+
writeFileSync(
|
|
125
|
+
filePath,
|
|
126
|
+
JSON.stringify({ deviceId: cached }, null, 2) + "\n",
|
|
127
|
+
{ mode: 0o644 },
|
|
128
|
+
);
|
|
129
|
+
log.info("Migrated device.json to new location");
|
|
130
|
+
} catch (writeErr) {
|
|
131
|
+
log.warn(
|
|
132
|
+
{ err: writeErr },
|
|
133
|
+
"Failed to migrate device.json to new location",
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
return cached;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} catch (err) {
|
|
140
|
+
log.warn(
|
|
141
|
+
{ err },
|
|
142
|
+
"Failed to read legacy device.json — generating new device ID",
|
|
143
|
+
);
|
|
144
|
+
}
|
|
82
145
|
}
|
|
83
146
|
|
|
84
|
-
// Either the file doesn't exist, or deviceId was missing/empty.
|
|
147
|
+
// Either the file doesn't exist at either location, or deviceId was missing/empty.
|
|
85
148
|
// Generate a new UUID and persist it.
|
|
86
149
|
try {
|
|
87
150
|
mkdirSync(vellumDir, { recursive: true });
|
package/src/util/logger.ts
CHANGED
|
@@ -76,20 +76,44 @@ let rootLogger: pino.Logger | null = null;
|
|
|
76
76
|
let activeLogDate: string | null = null;
|
|
77
77
|
let activeLogFileConfig: LogFileConfig | null = null;
|
|
78
78
|
|
|
79
|
+
function resolveLogDir(config: LogFileConfig): string | undefined {
|
|
80
|
+
if (!config.dir) return undefined;
|
|
81
|
+
|
|
82
|
+
if (!existsSync(config.dir)) {
|
|
83
|
+
try {
|
|
84
|
+
mkdirSync(config.dir, { recursive: true });
|
|
85
|
+
} catch (err) {
|
|
86
|
+
if (getIsContainerized()) {
|
|
87
|
+
// Config has a host-specific path that can't be created inside the
|
|
88
|
+
// container (e.g. /Users/…). Fall back to the default log directory.
|
|
89
|
+
const fallback = join(getLogPath(), "..");
|
|
90
|
+
console.warn(
|
|
91
|
+
`[logger] Configured logFile.dir "${config.dir}" cannot be created ` +
|
|
92
|
+
`in container (${(err as Error).message}). Falling back to "${fallback}".`,
|
|
93
|
+
);
|
|
94
|
+
if (!existsSync(fallback)) {
|
|
95
|
+
mkdirSync(fallback, { recursive: true });
|
|
96
|
+
}
|
|
97
|
+
return fallback;
|
|
98
|
+
}
|
|
99
|
+
throw err;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return config.dir;
|
|
104
|
+
}
|
|
105
|
+
|
|
79
106
|
function buildRotatingLogger(config: LogFileConfig): pino.Logger {
|
|
80
|
-
|
|
107
|
+
const dir = resolveLogDir(config);
|
|
108
|
+
if (!dir) {
|
|
81
109
|
return pino(
|
|
82
110
|
{ name: "assistant", serializers: logSerializers },
|
|
83
111
|
pinoPretty(prettyOpts({ destination: 1 })),
|
|
84
112
|
);
|
|
85
113
|
}
|
|
86
114
|
|
|
87
|
-
if (!existsSync(config.dir)) {
|
|
88
|
-
mkdirSync(config.dir, { recursive: true });
|
|
89
|
-
}
|
|
90
|
-
|
|
91
115
|
const today = formatDate(new Date());
|
|
92
|
-
const filePath = logFilePathForDate(
|
|
116
|
+
const filePath = logFilePathForDate(dir, new Date());
|
|
93
117
|
const fileDest = pino.destination({
|
|
94
118
|
dest: filePath,
|
|
95
119
|
sync: false,
|
|
@@ -107,7 +131,7 @@ function buildRotatingLogger(config: LogFileConfig): pino.Logger {
|
|
|
107
131
|
);
|
|
108
132
|
|
|
109
133
|
activeLogDate = today;
|
|
110
|
-
activeLogFileConfig = config;
|
|
134
|
+
activeLogFileConfig = { ...config, dir };
|
|
111
135
|
|
|
112
136
|
// When stdout is not a TTY (e.g. desktop app redirects to a hatch log file),
|
|
113
137
|
// write to the rotating file only — the hatch log already captured early
|
|
@@ -144,8 +168,10 @@ function ensureCurrentDate(): void {
|
|
|
144
168
|
export function initLogger(config: LogFileConfig): void {
|
|
145
169
|
rootLogger = buildRotatingLogger(config);
|
|
146
170
|
|
|
147
|
-
|
|
148
|
-
|
|
171
|
+
// Use the resolved dir (may differ from config.dir when containerized)
|
|
172
|
+
const resolvedDir = activeLogFileConfig?.dir;
|
|
173
|
+
if (resolvedDir && config.retentionDays > 0) {
|
|
174
|
+
const removed = pruneOldLogFiles(resolvedDir, config.retentionDays);
|
|
149
175
|
if (removed > 0) {
|
|
150
176
|
rootLogger.info(
|
|
151
177
|
{ removed, retentionDays: config.retentionDays },
|
package/src/util/platform.ts
CHANGED
|
@@ -8,7 +8,11 @@ import {
|
|
|
8
8
|
import { homedir } from "node:os";
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
getBaseDataDir,
|
|
13
|
+
getIsContainerized,
|
|
14
|
+
getWorkspaceDirOverride,
|
|
15
|
+
} from "../config/env-registry.js";
|
|
12
16
|
|
|
13
17
|
export function isMacOS(): boolean {
|
|
14
18
|
return process.platform === "darwin";
|
|
@@ -237,6 +241,15 @@ export function getInterfacesDir(): string {
|
|
|
237
241
|
return join(getDataDir(), "interfaces");
|
|
238
242
|
}
|
|
239
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Returns the sounds directory (~/.vellum/workspace/data/sounds).
|
|
246
|
+
* Custom sound files and sound configuration live here. Sound files
|
|
247
|
+
* can be large, so this directory is excluded from diagnostic exports.
|
|
248
|
+
*/
|
|
249
|
+
export function getSoundsDir(): string {
|
|
250
|
+
return join(getWorkspaceDir(), "data", "sounds");
|
|
251
|
+
}
|
|
252
|
+
|
|
240
253
|
/**
|
|
241
254
|
* Returns the TCP port the daemon should listen on for iOS clients.
|
|
242
255
|
* Hardcoded default: 8765.
|
|
@@ -356,13 +369,19 @@ export function getSignalsDir(): string {
|
|
|
356
369
|
// Currently not used by call-sites; wired in later PRs.
|
|
357
370
|
|
|
358
371
|
/**
|
|
359
|
-
* Returns
|
|
372
|
+
* Returns the workspace root for user-facing state.
|
|
373
|
+
*
|
|
374
|
+
* When the WORKSPACE_DIR env var is set, returns that value (used in
|
|
375
|
+
* containerized deployments where the workspace is a separate volume).
|
|
376
|
+
* Otherwise falls back to ~/.vellum/workspace.
|
|
360
377
|
*
|
|
361
378
|
* WARNING: The entire workspace directory is included in diagnostic log exports
|
|
362
379
|
* ("Send logs to Vellum"). Do not store secrets, API keys, or sensitive
|
|
363
380
|
* credentials here — use the credential store or ~/.vellum/protected/ instead.
|
|
364
381
|
*/
|
|
365
382
|
export function getWorkspaceDir(): string {
|
|
383
|
+
const override = getWorkspaceDirOverride();
|
|
384
|
+
if (override) return override;
|
|
366
385
|
return join(getRootDir(), "workspace");
|
|
367
386
|
}
|
|
368
387
|
|
|
@@ -413,13 +432,17 @@ export function ensureDataDir(): void {
|
|
|
413
432
|
const root = getRootDir();
|
|
414
433
|
const workspace = getWorkspaceDir();
|
|
415
434
|
const wsData = join(workspace, "data");
|
|
435
|
+
const containerized = getIsContainerized();
|
|
416
436
|
const dirs = [
|
|
417
|
-
// Root-level dirs (runtime
|
|
437
|
+
// Root-level dirs (runtime)
|
|
418
438
|
root,
|
|
419
|
-
|
|
439
|
+
// signals dir is needed everywhere (MCP reload, user-message signals)
|
|
440
|
+
join(root, "signals"),
|
|
441
|
+
// protected, hooks are local-only — skip in containerized mode
|
|
442
|
+
// (credentials via CES HTTP API, trust via gateway API)
|
|
443
|
+
...(containerized ? [] : [join(root, "protected"), join(root, "hooks")]),
|
|
420
444
|
// Workspace dirs
|
|
421
445
|
workspace,
|
|
422
|
-
join(root, "hooks"),
|
|
423
446
|
join(workspace, "skills"),
|
|
424
447
|
join(workspace, "embedding-models"),
|
|
425
448
|
join(workspace, "conversations"),
|
|
@@ -432,6 +455,7 @@ export function ensureDataDir(): void {
|
|
|
432
455
|
join(wsData, "memory", "knowledge"),
|
|
433
456
|
join(wsData, "apps"),
|
|
434
457
|
join(wsData, "interfaces"),
|
|
458
|
+
join(wsData, "sounds"),
|
|
435
459
|
];
|
|
436
460
|
for (const dir of dirs) {
|
|
437
461
|
if (!existsSync(dir)) {
|
package/src/util/xml.ts
CHANGED
|
@@ -6,3 +6,11 @@ export function escapeXmlAttr(s: string): string {
|
|
|
6
6
|
.replace(/</g, "<")
|
|
7
7
|
.replace(/>/g, ">");
|
|
8
8
|
}
|
|
9
|
+
|
|
10
|
+
/** Escape a string for safe inclusion as XML/HTML text content. */
|
|
11
|
+
export function escapeXmlContent(s: string): string {
|
|
12
|
+
return s
|
|
13
|
+
.replace(/&/g, "&")
|
|
14
|
+
.replace(/</g, "<")
|
|
15
|
+
.replace(/>/g, ">");
|
|
16
|
+
}
|
|
@@ -210,13 +210,11 @@ export class WorkspaceHeartbeatService {
|
|
|
210
210
|
|
|
211
211
|
try {
|
|
212
212
|
const now = this.now();
|
|
213
|
-
let shutdownFiles: string[] = [];
|
|
214
213
|
const { committed } = await service.commitIfDirty(
|
|
215
214
|
(st) => {
|
|
216
215
|
const uniqueFiles = [
|
|
217
216
|
...new Set([...st.staged, ...st.modified, ...st.untracked]),
|
|
218
217
|
];
|
|
219
|
-
shutdownFiles = uniqueFiles;
|
|
220
218
|
log.info(
|
|
221
219
|
{ workspaceDir, totalChanges: uniqueFiles.length },
|
|
222
220
|
"Committing pending changes on shutdown",
|
|
@@ -237,28 +235,11 @@ export class WorkspaceHeartbeatService {
|
|
|
237
235
|
if (committed) {
|
|
238
236
|
firstSeenDirty.delete(workspaceDir);
|
|
239
237
|
result.committed++;
|
|
240
|
-
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
workspaceDir,
|
|
246
|
-
trigger: "shutdown",
|
|
247
|
-
changedFiles: shutdownFiles,
|
|
248
|
-
timestampMs: this.now(),
|
|
249
|
-
};
|
|
250
|
-
getEnrichmentService().enqueue({
|
|
251
|
-
workspaceDir,
|
|
252
|
-
commitHash,
|
|
253
|
-
context: shutdownCtx,
|
|
254
|
-
gitService: service,
|
|
255
|
-
});
|
|
256
|
-
} catch (enrichErr) {
|
|
257
|
-
log.debug(
|
|
258
|
-
{ enrichErr },
|
|
259
|
-
"Failed to enqueue shutdown enrichment (non-fatal)",
|
|
260
|
-
);
|
|
261
|
-
}
|
|
238
|
+
// Skip enrichment for shutdown commits — the enrichment queue is
|
|
239
|
+
// about to be shut down anyway, and the fire-and-forget writeNote()
|
|
240
|
+
// can race with subsequent commitAllPending() calls (the async
|
|
241
|
+
// git-notes operation acquires the mutex and may leave behind an
|
|
242
|
+
// index.lock on some git versions, causing the next commit to fail).
|
|
262
243
|
} else {
|
|
263
244
|
result.skipped++;
|
|
264
245
|
}
|