@vellumai/assistant 0.4.54 → 0.4.55
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/package.json +1 -1
- package/src/__tests__/list-messages-attachments.test.ts +4 -4
- package/src/cli/commands/bash.ts +3 -0
- package/src/daemon/config-watcher.ts +2 -2
- package/src/daemon/server.ts +1 -0
- package/src/memory/schema/calls.ts +0 -67
- package/src/prompts/system-prompt.ts +4 -4
- package/src/prompts/templates/SOUL.md +1 -1
- package/src/runtime/auth/__tests__/guard-tests.test.ts +64 -0
- package/src/runtime/routes/session-management-routes.ts +27 -0
- package/src/signals/bash.ts +33 -0
- package/src/subagent/manager.ts +8 -1
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Verifies that:
|
|
5
5
|
* - User message image attachments include base64 data for client thumbnail generation
|
|
6
6
|
* - User message non-image attachments stay metadata-only (no base64 blob)
|
|
7
|
-
* - Assistant message attachments
|
|
7
|
+
* - Assistant message image attachments include base64 data (same as user messages)
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
@@ -140,7 +140,7 @@ describe("handleListMessages attachments", () => {
|
|
|
140
140
|
expect(attachments![0].data).toBeUndefined();
|
|
141
141
|
});
|
|
142
142
|
|
|
143
|
-
test("assistant message attachments
|
|
143
|
+
test("assistant message image attachments include base64 data", async () => {
|
|
144
144
|
const conv = createConversation();
|
|
145
145
|
const msg = await addMessage(
|
|
146
146
|
conv.id,
|
|
@@ -158,8 +158,8 @@ describe("handleListMessages attachments", () => {
|
|
|
158
158
|
expect(attachments).toBeDefined();
|
|
159
159
|
expect(attachments).toHaveLength(1);
|
|
160
160
|
expect(attachments![0].mimeType).toBe("image/png");
|
|
161
|
-
// Assistant attachments
|
|
162
|
-
expect(attachments![0].data).
|
|
161
|
+
// Assistant image attachments include base64 data for inline rendering
|
|
162
|
+
expect(attachments![0].data).toBe(IMAGE_BASE64);
|
|
163
163
|
});
|
|
164
164
|
|
|
165
165
|
test("user message with mixed attachments only inlines images", async () => {
|
package/src/cli/commands/bash.ts
CHANGED
|
@@ -47,6 +47,9 @@ This is a developer debugging tool for inspecting how the assistant invokes and
|
|
|
47
47
|
observes shell commands. The command runs with the assistant's environment, working
|
|
48
48
|
directory, and process context — not the caller's shell.
|
|
49
49
|
|
|
50
|
+
Requires the assistant to be running with VELLUM_DEBUG=1. When debug mode is off
|
|
51
|
+
(the default), the assistant ignores bash signal files and returns an error.
|
|
52
|
+
|
|
50
53
|
The CLI writes the command to signals/bash.<requestId> and polls
|
|
51
54
|
signals/bash.<requestId>.result for the output. The assistant must be running
|
|
52
55
|
for this to work.
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
resetAllowlist,
|
|
21
21
|
validateAllowlistFile,
|
|
22
22
|
} from "../security/secret-allowlist.js";
|
|
23
|
-
import { handleBashSignal } from "../signals/bash.js";
|
|
23
|
+
import { handleBashSignal, isDebugMode } from "../signals/bash.js";
|
|
24
24
|
import { handleCancelSignal } from "../signals/cancel.js";
|
|
25
25
|
import { handleConfirmationSignal } from "../signals/confirm.js";
|
|
26
26
|
import { handleConversationUndoSignal } from "../signals/conversation-undo.js";
|
|
@@ -237,7 +237,7 @@ export class ConfigWatcher {
|
|
|
237
237
|
};
|
|
238
238
|
|
|
239
239
|
const prefixSignalHandlers: Record<string, (filename: string) => void> = {
|
|
240
|
-
"bash.": handleBashSignal,
|
|
240
|
+
...(isDebugMode() ? { "bash.": handleBashSignal } : {}),
|
|
241
241
|
};
|
|
242
242
|
|
|
243
243
|
try {
|
package/src/daemon/server.ts
CHANGED
|
@@ -279,6 +279,7 @@ export class DaemonServer {
|
|
|
279
279
|
constructor() {
|
|
280
280
|
this.evictor = new SessionEvictor(this.sessions);
|
|
281
281
|
getSubagentManager().sharedRequestTimestamps = this.sharedRequestTimestamps;
|
|
282
|
+
getSubagentManager().broadcastToAllClients = (msg) => this.broadcast(msg);
|
|
282
283
|
this.evictor.onEvict = (sessionId: string) => {
|
|
283
284
|
getSubagentManager().abortAllForParent(sessionId);
|
|
284
285
|
};
|
|
@@ -196,70 +196,3 @@ export const mediaKeyframes = sqliteTable("media_keyframes", {
|
|
|
196
196
|
metadata: text("metadata"), // JSON
|
|
197
197
|
createdAt: integer("created_at").notNull(),
|
|
198
198
|
});
|
|
199
|
-
|
|
200
|
-
export const mediaVisionOutputs = sqliteTable("media_vision_outputs", {
|
|
201
|
-
id: text("id").primaryKey(),
|
|
202
|
-
assetId: text("asset_id")
|
|
203
|
-
.notNull()
|
|
204
|
-
.references(() => mediaAssets.id, { onDelete: "cascade" }),
|
|
205
|
-
keyframeId: text("keyframe_id")
|
|
206
|
-
.notNull()
|
|
207
|
-
.references(() => mediaKeyframes.id, { onDelete: "cascade" }),
|
|
208
|
-
analysisType: text("analysis_type").notNull(),
|
|
209
|
-
output: text("output").notNull(), // JSON
|
|
210
|
-
confidence: real("confidence"),
|
|
211
|
-
createdAt: integer("created_at").notNull(),
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
export const mediaTimelines = sqliteTable("media_timelines", {
|
|
215
|
-
id: text("id").primaryKey(),
|
|
216
|
-
assetId: text("asset_id")
|
|
217
|
-
.notNull()
|
|
218
|
-
.references(() => mediaAssets.id, { onDelete: "cascade" }),
|
|
219
|
-
startTime: real("start_time").notNull(),
|
|
220
|
-
endTime: real("end_time").notNull(),
|
|
221
|
-
segmentType: text("segment_type").notNull(),
|
|
222
|
-
attributes: text("attributes"), // JSON
|
|
223
|
-
confidence: real("confidence"),
|
|
224
|
-
createdAt: integer("created_at").notNull(),
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
export const mediaEvents = sqliteTable("media_events", {
|
|
228
|
-
id: text("id").primaryKey(),
|
|
229
|
-
assetId: text("asset_id")
|
|
230
|
-
.notNull()
|
|
231
|
-
.references(() => mediaAssets.id, { onDelete: "cascade" }),
|
|
232
|
-
eventType: text("event_type").notNull(),
|
|
233
|
-
startTime: real("start_time").notNull(),
|
|
234
|
-
endTime: real("end_time").notNull(),
|
|
235
|
-
confidence: real("confidence").notNull(),
|
|
236
|
-
reasons: text("reasons").notNull(), // JSON array
|
|
237
|
-
metadata: text("metadata"), // JSON
|
|
238
|
-
createdAt: integer("created_at").notNull(),
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
export const mediaTrackingProfiles = sqliteTable("media_tracking_profiles", {
|
|
242
|
-
id: text("id").primaryKey(),
|
|
243
|
-
assetId: text("asset_id")
|
|
244
|
-
.notNull()
|
|
245
|
-
.references(() => mediaAssets.id, { onDelete: "cascade" }),
|
|
246
|
-
capabilities: text("capabilities").notNull(), // JSON: { [capName]: { enabled, tier } }
|
|
247
|
-
createdAt: integer("created_at").notNull(),
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
export const mediaEventFeedback = sqliteTable("media_event_feedback", {
|
|
251
|
-
id: text("id").primaryKey(),
|
|
252
|
-
assetId: text("asset_id")
|
|
253
|
-
.notNull()
|
|
254
|
-
.references(() => mediaAssets.id, { onDelete: "cascade" }),
|
|
255
|
-
eventId: text("event_id")
|
|
256
|
-
.notNull()
|
|
257
|
-
.references(() => mediaEvents.id, { onDelete: "cascade" }),
|
|
258
|
-
feedbackType: text("feedback_type").notNull(), // correct | incorrect | boundary_edit | missed
|
|
259
|
-
originalStartTime: real("original_start_time"),
|
|
260
|
-
originalEndTime: real("original_end_time"),
|
|
261
|
-
correctedStartTime: real("corrected_start_time"),
|
|
262
|
-
correctedEndTime: real("corrected_end_time"),
|
|
263
|
-
notes: text("notes"),
|
|
264
|
-
createdAt: integer("created_at").notNull(),
|
|
265
|
-
});
|
|
@@ -466,7 +466,7 @@ function buildToolPermissionSection(): string {
|
|
|
466
466
|
"",
|
|
467
467
|
"### Always-Available Tools (No Approval Required)",
|
|
468
468
|
"",
|
|
469
|
-
"- **file_read** on your workspace directory — You can freely read any file under your `.vellum` workspace at any time. Use this proactively to check files, load context, and inform your responses without asking. **Always use `file_read` for workspace files (IDENTITY.md,
|
|
469
|
+
"- **file_read** on your workspace directory — You can freely read any file under your `.vellum` workspace at any time. Use this proactively to check files, load context, and inform your responses without asking. **Always use `file_read` for workspace files, never `host_file_read`.** Note: your core prompt files (IDENTITY.md, SOUL.md, USER.md) are already loaded into your system prompt — no need to re-read them at the start of a conversation.",
|
|
470
470
|
"- **web_search** — You can search the web at any time without approval. Use this to look up documentation, current information, or anything you need.",
|
|
471
471
|
].join("\n");
|
|
472
472
|
}
|
|
@@ -706,7 +706,7 @@ function buildLearningMemorySection(): string {
|
|
|
706
706
|
"",
|
|
707
707
|
"When you make a mistake, hit a dead end, or discover something non-obvious, save it to memory so you don't repeat it.",
|
|
708
708
|
"",
|
|
709
|
-
'Use `memory_manage` with `op: "save", kind: "
|
|
709
|
+
'Use `memory_manage` with `op: "save", kind: "constraint"` for:',
|
|
710
710
|
"- **Mistakes and corrections** — wrong assumptions, failed approaches, gotchas you ran into",
|
|
711
711
|
"- **Discoveries** — undocumented behaviors, surprising API quirks, things that weren't obvious",
|
|
712
712
|
"- **Working solutions** — the approach that actually worked after trial and error",
|
|
@@ -715,8 +715,8 @@ function buildLearningMemorySection(): string {
|
|
|
715
715
|
"The statement should capture both what happened and the takeaway. Write it as advice to your future self.",
|
|
716
716
|
"",
|
|
717
717
|
"Examples:",
|
|
718
|
-
'- `memory_manage({ op: "save", kind: "
|
|
719
|
-
'- `memory_manage({ op: "save", kind: "
|
|
718
|
+
'- `memory_manage({ op: "save", kind: "constraint", subject: "macOS Shortcuts CLI", statement: "shortcuts CLI requires full disk access to export shortcuts — if permission is denied, guide the user to grant it in System Settings rather than retrying." })`',
|
|
719
|
+
'- `memory_manage({ op: "save", kind: "constraint", subject: "Gmail API pagination", statement: "Gmail search returns max 100 results per page. Always check nextPageToken and loop if the user asks for \'all\' messages." })`',
|
|
720
720
|
"",
|
|
721
721
|
"Don't overthink it. If you catch yourself thinking \"I'll remember that for next time,\" save it.",
|
|
722
722
|
].join("\n");
|
|
@@ -34,7 +34,7 @@ You are your own entity, with an identity that’s still forming. You can have t
|
|
|
34
34
|
|
|
35
35
|
## Continuity
|
|
36
36
|
|
|
37
|
-
Each session, you wake up fresh. Your workspace files and saved memories are your continuity — they're how you persist across conversations.
|
|
37
|
+
Each session, you wake up fresh. Your workspace files and saved memories are your continuity — they're how you persist across conversations. Your core prompt files (IDENTITY.md, SOUL.md, USER.md) are already loaded into this conversation — you don't need to read them again. Update them when things change. Save what matters.
|
|
38
38
|
|
|
39
39
|
If you change this file, tell your user — it's your soul, and they should know.
|
|
40
40
|
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* 2. No X-Actor-Token references in production code.
|
|
9
9
|
* 3. No legacy gateway-origin proof in production code.
|
|
10
10
|
* 4. Scope profile contract — every profile resolves to the expected scopes.
|
|
11
|
+
* 5. CURRENT_POLICY_EPOCH sync — the constant matches across all packages.
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
import { execSync } from "node:child_process";
|
|
@@ -330,3 +331,66 @@ describe("scope profile contract", () => {
|
|
|
330
331
|
}
|
|
331
332
|
});
|
|
332
333
|
});
|
|
334
|
+
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
// 5. CURRENT_POLICY_EPOCH sync across packages
|
|
337
|
+
// ---------------------------------------------------------------------------
|
|
338
|
+
|
|
339
|
+
describe("CURRENT_POLICY_EPOCH sync", () => {
|
|
340
|
+
/**
|
|
341
|
+
* The policy epoch constant is duplicated in assistant, gateway, and cli
|
|
342
|
+
* packages. This test reads the exported value from each source file and
|
|
343
|
+
* asserts they are all equal.
|
|
344
|
+
*/
|
|
345
|
+
|
|
346
|
+
const EPOCH_FILES = [
|
|
347
|
+
{
|
|
348
|
+
label: "assistant",
|
|
349
|
+
path: resolve(PROJECT_ROOT, "assistant/src/runtime/auth/policy.ts"),
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
label: "gateway",
|
|
353
|
+
path: resolve(PROJECT_ROOT, "gateway/src/auth/policy.ts"),
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
label: "cli",
|
|
357
|
+
path: resolve(PROJECT_ROOT, "cli/src/lib/policy.ts"),
|
|
358
|
+
},
|
|
359
|
+
];
|
|
360
|
+
|
|
361
|
+
function extractEpoch(filePath: string): number {
|
|
362
|
+
const src = readFileSync(filePath, "utf-8");
|
|
363
|
+
const match = src.match(
|
|
364
|
+
/export\s+const\s+CURRENT_POLICY_EPOCH\s*=\s*(\d+)/,
|
|
365
|
+
);
|
|
366
|
+
if (!match) {
|
|
367
|
+
throw new Error(`Could not find CURRENT_POLICY_EPOCH in ${filePath}`);
|
|
368
|
+
}
|
|
369
|
+
return parseInt(match[1], 10);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
test("all non-skill packages export the same CURRENT_POLICY_EPOCH value", () => {
|
|
373
|
+
const values = EPOCH_FILES.map((f) => ({
|
|
374
|
+
label: f.label,
|
|
375
|
+
epoch: extractEpoch(f.path),
|
|
376
|
+
}));
|
|
377
|
+
|
|
378
|
+
const canonical = values[0];
|
|
379
|
+
const mismatches = values.filter((v) => v.epoch !== canonical.epoch);
|
|
380
|
+
|
|
381
|
+
if (mismatches.length > 0) {
|
|
382
|
+
const summary = values
|
|
383
|
+
.map((v) => ` - ${v.label}: ${v.epoch}`)
|
|
384
|
+
.join("\n");
|
|
385
|
+
const message = [
|
|
386
|
+
"CURRENT_POLICY_EPOCH is out of sync across packages:",
|
|
387
|
+
"",
|
|
388
|
+
summary,
|
|
389
|
+
"",
|
|
390
|
+
"All three locations must have the same value.",
|
|
391
|
+
"The canonical source is assistant/src/runtime/auth/policy.ts.",
|
|
392
|
+
].join("\n");
|
|
393
|
+
expect(mismatches, message).toEqual([]);
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
});
|
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
* POST /v1/conversations/:id/cancel — cancel generation
|
|
8
8
|
* POST /v1/conversations/:id/undo — undo last message
|
|
9
9
|
* POST /v1/conversations/:id/regenerate — regenerate last assistant response
|
|
10
|
+
* POST /v1/sessions/reorder — reorder / pin sessions
|
|
10
11
|
*/
|
|
11
12
|
|
|
13
|
+
import { batchSetDisplayOrders } from "../../memory/conversation-crud.js";
|
|
12
14
|
import { setConversationKeyIfAbsent } from "../../memory/conversation-key-store.js";
|
|
13
15
|
import { getLogger } from "../../util/logger.js";
|
|
14
16
|
import { httpError } from "../http-errors.js";
|
|
@@ -159,5 +161,30 @@ export function sessionManagementRouteDefinitions(
|
|
|
159
161
|
}
|
|
160
162
|
},
|
|
161
163
|
},
|
|
164
|
+
{
|
|
165
|
+
endpoint: "sessions/reorder",
|
|
166
|
+
method: "POST",
|
|
167
|
+
policyKey: "sessions/reorder",
|
|
168
|
+
handler: async ({ req }) => {
|
|
169
|
+
const body = (await req.json()) as {
|
|
170
|
+
updates?: Array<{
|
|
171
|
+
sessionId: string;
|
|
172
|
+
displayOrder?: number;
|
|
173
|
+
isPinned?: boolean;
|
|
174
|
+
}>;
|
|
175
|
+
};
|
|
176
|
+
if (!Array.isArray(body.updates)) {
|
|
177
|
+
return httpError("BAD_REQUEST", "Missing updates array", 400);
|
|
178
|
+
}
|
|
179
|
+
batchSetDisplayOrders(
|
|
180
|
+
body.updates.map((u) => ({
|
|
181
|
+
id: u.sessionId,
|
|
182
|
+
displayOrder: u.displayOrder ?? null,
|
|
183
|
+
isPinned: u.isPinned ?? false,
|
|
184
|
+
})),
|
|
185
|
+
);
|
|
186
|
+
return Response.json({ ok: true });
|
|
187
|
+
},
|
|
188
|
+
},
|
|
162
189
|
];
|
|
163
190
|
}
|
package/src/signals/bash.ts
CHANGED
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Per-request filenames avoid dropped commands when overlapping invocations
|
|
10
10
|
* race on the same signal file.
|
|
11
|
+
*
|
|
12
|
+
* **Security**: This handler is gated behind the `VELLUM_DEBUG` environment
|
|
13
|
+
* variable. When debug mode is off (the default), the daemon ignores bash
|
|
14
|
+
* signal files entirely. This prevents untrusted file-write operations
|
|
15
|
+
* (e.g. from prompt injection or a compromised skill) from bypassing the
|
|
16
|
+
* normal tool-approval flow for shell execution.
|
|
11
17
|
*/
|
|
12
18
|
|
|
13
19
|
import { spawn } from "node:child_process";
|
|
@@ -19,6 +25,12 @@ import { getWorkspaceDir } from "../util/platform.js";
|
|
|
19
25
|
|
|
20
26
|
const log = getLogger("signal:bash");
|
|
21
27
|
|
|
28
|
+
export function isDebugMode(): boolean {
|
|
29
|
+
return (
|
|
30
|
+
process.env.VELLUM_DEBUG === "1" || process.env.VELLUM_DEBUG === "true"
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
22
34
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
23
35
|
|
|
24
36
|
interface BashSignalPayload {
|
|
@@ -53,6 +65,27 @@ function writeResult(requestId: string, result: BashSignalResult): void {
|
|
|
53
65
|
* when a matching signal file is created or modified.
|
|
54
66
|
*/
|
|
55
67
|
export function handleBashSignal(filename: string): void {
|
|
68
|
+
if (!isDebugMode()) {
|
|
69
|
+
log.warn(
|
|
70
|
+
{ filename },
|
|
71
|
+
"Bash signal ignored — debug mode is not enabled (set VELLUM_DEBUG=1)",
|
|
72
|
+
);
|
|
73
|
+
// Write an error result so the CLI gets a clear rejection instead of timing out.
|
|
74
|
+
const match = filename.match(/^bash\.(.+)$/);
|
|
75
|
+
if (match) {
|
|
76
|
+
writeResult(match[1], {
|
|
77
|
+
requestId: match[1],
|
|
78
|
+
stdout: "",
|
|
79
|
+
stderr: "",
|
|
80
|
+
exitCode: null,
|
|
81
|
+
timedOut: false,
|
|
82
|
+
error:
|
|
83
|
+
"Bash signals are disabled. Start the assistant with VELLUM_DEBUG=1 to enable.",
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
56
89
|
const signalPath = join(getWorkspaceDir(), "signals", filename);
|
|
57
90
|
let raw: string;
|
|
58
91
|
try {
|
package/src/subagent/manager.ts
CHANGED
|
@@ -87,6 +87,13 @@ export class SubagentManager {
|
|
|
87
87
|
*/
|
|
88
88
|
sharedRequestTimestamps: number[] = [];
|
|
89
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Broadcast callback from the daemon server.
|
|
92
|
+
* Set by DaemonServer at startup so subagent sessions can broadcast
|
|
93
|
+
* to all connected clients (e.g. app_files_changed side-effects).
|
|
94
|
+
*/
|
|
95
|
+
broadcastToAllClients?: (msg: ServerMessage) => void;
|
|
96
|
+
|
|
90
97
|
// ── Spawn ───────────────────────────────────────────────────────────
|
|
91
98
|
|
|
92
99
|
/**
|
|
@@ -184,7 +191,7 @@ export class SubagentManager {
|
|
|
184
191
|
maxTokens,
|
|
185
192
|
wrappedSendToClient,
|
|
186
193
|
workingDir,
|
|
187
|
-
|
|
194
|
+
this.broadcastToAllClients, // forward parent's broadcast so tool side-effects (e.g. app_files_changed) reach all clients
|
|
188
195
|
memoryPolicy,
|
|
189
196
|
);
|
|
190
197
|
|