@vellumai/assistant 0.6.1 → 0.6.2
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/docker-entrypoint.sh +12 -2
- package/node_modules/@vellumai/ces-contracts/src/handles.ts +7 -9
- package/openapi.yaml +1 -1
- package/package.json +1 -1
- package/src/__tests__/assistant-event-hub.test.ts +30 -0
- package/src/__tests__/checker.test.ts +104 -170
- package/src/__tests__/cli-command-risk-guard.test.ts +1 -1
- package/src/__tests__/context-overflow-approval.test.ts +5 -5
- package/src/__tests__/conversation-analysis-routes.test.ts +169 -0
- package/src/__tests__/conversation-directories-parse.test.ts +105 -0
- package/src/__tests__/credential-execution-approval-bridge.test.ts +0 -2
- package/src/__tests__/init-feature-flag-overrides.test.ts +167 -0
- package/src/__tests__/inline-command-runner.test.ts +7 -5
- package/src/__tests__/log-export-workspace.test.ts +190 -0
- package/src/__tests__/managed-credential-catalog-cli.test.ts +12 -14
- package/src/__tests__/navigate-settings-tab.test.ts +14 -1
- package/src/__tests__/notification-broadcaster.test.ts +65 -0
- package/src/__tests__/onboarding-template-contract.test.ts +5 -4
- package/src/__tests__/pkb-autoinject.test.ts +96 -0
- package/src/__tests__/require-fresh-approval.test.ts +0 -2
- package/src/__tests__/sandbox-diagnostics.test.ts +1 -32
- package/src/__tests__/terminal-sandbox.test.ts +1 -1
- package/src/__tests__/terminal-tools.test.ts +2 -5
- package/src/__tests__/test-preload.ts +14 -0
- package/src/__tests__/tool-domain-event-publisher.test.ts +0 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -8
- package/src/__tests__/tool-executor.test.ts +0 -1
- package/src/__tests__/transport-hints-queue.test.ts +77 -0
- package/src/__tests__/trust-store.test.ts +4 -4
- package/src/__tests__/workspace-migration-030-seed-pkb-autoinject.test.ts +168 -0
- package/src/__tests__/workspace-policy.test.ts +2 -7
- package/src/agent/loop.ts +0 -29
- package/src/channels/types.ts +5 -0
- package/src/cli/__tests__/run-assistant-command.ts +34 -7
- package/src/cli/__tests__/unknown-command.test.ts +33 -0
- package/src/cli/commands/default-action.ts +68 -1
- package/src/cli/commands/oauth/__tests__/connect.test.ts +27 -0
- package/src/cli/commands/oauth/connect.ts +11 -0
- package/src/cli/commands/platform/__tests__/connect.test.ts +1 -1
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +1 -1
- package/src/cli/program.ts +9 -2
- package/src/config/assistant-feature-flags.ts +59 -55
- package/src/config/bundled-skills/app-builder/SKILL.md +87 -4
- package/src/config/bundled-skills/gmail/SKILL.md +11 -6
- package/src/config/bundled-skills/gmail/TOOLS.json +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -1
- package/src/config/bundled-skills/settings/TOOLS.json +1 -1
- package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +8 -3
- package/src/config/feature-flag-registry.json +2 -2
- package/src/config/schemas/services.ts +8 -0
- package/src/credential-execution/approval-bridge.ts +0 -1
- package/src/credential-execution/managed-catalog.ts +3 -7
- package/src/daemon/config-watcher.ts +6 -2
- package/src/daemon/context-overflow-approval.ts +0 -1
- package/src/daemon/conversation-agent-loop.ts +33 -12
- package/src/daemon/conversation-attachments.ts +0 -1
- package/src/daemon/conversation-messaging.ts +3 -0
- package/src/daemon/conversation-process.ts +18 -2
- package/src/daemon/conversation-queue-manager.ts +8 -0
- package/src/daemon/conversation-runtime-assembly.ts +64 -7
- package/src/daemon/conversation-surfaces.ts +65 -0
- package/src/daemon/conversation-tool-setup.ts +0 -3
- package/src/daemon/conversation.ts +3 -5
- package/src/daemon/handlers/conversations.ts +2 -1
- package/src/daemon/handlers/shared.ts +7 -0
- package/src/daemon/lifecycle.ts +21 -1
- package/src/daemon/message-types/conversations.ts +4 -0
- package/src/daemon/message-types/messages.ts +0 -1
- package/src/daemon/message-types/notifications.ts +12 -0
- package/src/daemon/message-types/settings.ts +12 -0
- package/src/daemon/server.ts +21 -24
- package/src/daemon/transport-hints.ts +33 -0
- package/src/index.ts +1 -1
- package/src/memory/conversation-crud.ts +15 -10
- package/src/memory/conversation-directories.ts +39 -0
- package/src/memory/conversation-group-migration.ts +65 -5
- package/src/memory/embedding-local.ts +1 -1
- package/src/memory/graph/capability-seed.ts +3 -5
- package/src/memory/group-crud.ts +25 -9
- package/src/messaging/provider.ts +1 -1
- package/src/notifications/broadcaster.ts +6 -0
- package/src/notifications/conversation-pairing.ts +12 -4
- package/src/notifications/emit-signal.ts +14 -0
- package/src/notifications/signal.ts +11 -0
- package/src/oauth/platform-connection.test.ts +2 -2
- package/src/oauth/seed-providers.ts +1 -0
- package/src/permissions/checker.ts +3 -3
- package/src/permissions/defaults.ts +7 -8
- package/src/permissions/prompter.ts +0 -2
- package/src/platform/client.ts +1 -1
- package/src/prompts/templates/BOOTSTRAP.md +14 -5
- package/src/prompts/templates/SOUL.md +11 -11
- package/src/runtime/assistant-event-hub.ts +22 -0
- package/src/runtime/auth/token-service.ts +8 -0
- package/src/runtime/routes/conversation-analysis-routes.ts +18 -6
- package/src/runtime/routes/conversation-routes.ts +9 -3
- package/src/runtime/routes/group-routes.ts +22 -8
- package/src/runtime/routes/log-export/AGENTS.md +104 -0
- package/src/runtime/routes/log-export/__tests__/workspace-allowlist-error-contract.test.ts +103 -0
- package/src/runtime/routes/log-export/__tests__/workspace-allowlist.test.ts +716 -0
- package/src/runtime/routes/log-export/workspace-allowlist.ts +458 -0
- package/src/runtime/routes/log-export-routes.ts +18 -3
- package/src/skills/inline-command-runner.ts +12 -14
- package/src/tools/permission-checker.ts +0 -18
- package/src/tools/secret-detection-handler.ts +0 -1
- package/src/tools/skills/sandbox-runner.ts +3 -6
- package/src/tools/terminal/sandbox-diagnostics.ts +4 -4
- package/src/tools/terminal/sandbox.ts +4 -1
- package/src/tools/terminal/shell.ts +3 -5
- package/src/tools/types.ts +0 -3
- package/src/watcher/provider-types.ts +1 -1
- package/src/workspace/migrations/029-seed-pkb.ts +1 -0
- package/src/workspace/migrations/030-seed-pkb-autoinject.ts +73 -0
- package/src/workspace/migrations/registry.ts +2 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { parseInterfaceId } from "../channels/types.js";
|
|
2
|
+
import type { ConversationTransportMetadata } from "./message-types/conversations.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Build enriched transport hints from conversation transport metadata.
|
|
6
|
+
*
|
|
7
|
+
* Interface ID first, then host environment (macOS only), then any
|
|
8
|
+
* client-provided hints. Shared between the conversation creation path
|
|
9
|
+
* (server.ts) and the queue drain path (conversation-process.ts).
|
|
10
|
+
*/
|
|
11
|
+
export function buildTransportHints(
|
|
12
|
+
transport: ConversationTransportMetadata,
|
|
13
|
+
): string[] {
|
|
14
|
+
const hints: string[] = [];
|
|
15
|
+
|
|
16
|
+
const interfaceLabel = parseInterfaceId(transport.interfaceId) ?? "vellum";
|
|
17
|
+
hints.push(`User is messaging from interface: ${interfaceLabel}`);
|
|
18
|
+
|
|
19
|
+
if (transport.interfaceId === "macos") {
|
|
20
|
+
if (transport.hostHomeDir) {
|
|
21
|
+
hints.push(`Host home directory: ${transport.hostHomeDir}`);
|
|
22
|
+
}
|
|
23
|
+
if (transport.hostUsername) {
|
|
24
|
+
hints.push(`Host username: ${transport.hostUsername}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (transport.hints) {
|
|
29
|
+
hints.push(...transport.hints);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return hints;
|
|
33
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -314,12 +314,15 @@ export function createConversation(
|
|
|
314
314
|
|
|
315
315
|
// group_id is NOT in the Drizzle schema (raw-query-only pattern).
|
|
316
316
|
// Set via raw SQL after the INSERT succeeds.
|
|
317
|
-
|
|
317
|
+
// Always set group_id — default to "system:all" when none provided.
|
|
318
|
+
{
|
|
319
|
+
const effectiveGroupId = groupId ?? "system:all";
|
|
318
320
|
for (let attempt = 0; ; attempt++) {
|
|
319
321
|
try {
|
|
320
322
|
rawRun(
|
|
321
|
-
"UPDATE conversations SET group_id = ? WHERE id = ?",
|
|
322
|
-
|
|
323
|
+
"UPDATE conversations SET group_id = ?, is_pinned = ? WHERE id = ?",
|
|
324
|
+
effectiveGroupId,
|
|
325
|
+
effectiveGroupId === "system:pinned" ? 1 : 0,
|
|
323
326
|
id,
|
|
324
327
|
);
|
|
325
328
|
break;
|
|
@@ -469,7 +472,7 @@ export function forkConversation(params: {
|
|
|
469
472
|
const fc = createConversation({
|
|
470
473
|
title: forkTitle,
|
|
471
474
|
conversationType: "standard",
|
|
472
|
-
groupId: parentGroupId ??
|
|
475
|
+
groupId: parentGroupId ?? "system:all",
|
|
473
476
|
});
|
|
474
477
|
|
|
475
478
|
db.update(conversations)
|
|
@@ -1547,17 +1550,19 @@ export function batchSetDisplayOrders(
|
|
|
1547
1550
|
if (update.groupId !== undefined) {
|
|
1548
1551
|
// New client: groupId is authoritative.
|
|
1549
1552
|
// Derive is_pinned from groupId.
|
|
1550
|
-
// Sanitize: if groupId references a deleted/unknown group,
|
|
1551
|
-
// to
|
|
1553
|
+
// Sanitize: if groupId is null or references a deleted/unknown group,
|
|
1554
|
+
// fall back to "system:all" to avoid FK violation that would roll back
|
|
1555
|
+
// the entire batch.
|
|
1552
1556
|
let safeGroupId = update.groupId;
|
|
1553
|
-
if (
|
|
1554
|
-
safeGroupId
|
|
1557
|
+
if (safeGroupId === null) {
|
|
1558
|
+
safeGroupId = "system:all";
|
|
1559
|
+
} else if (
|
|
1555
1560
|
!rawGet<{ id: string }>(
|
|
1556
1561
|
"SELECT id FROM conversation_groups WHERE id = ?",
|
|
1557
1562
|
safeGroupId,
|
|
1558
1563
|
)
|
|
1559
1564
|
) {
|
|
1560
|
-
safeGroupId =
|
|
1565
|
+
safeGroupId = "system:all";
|
|
1561
1566
|
}
|
|
1562
1567
|
rawRun(
|
|
1563
1568
|
"UPDATE conversations SET display_order = ?, is_pinned = ?, group_id = ? WHERE id = ?",
|
|
@@ -1587,7 +1592,7 @@ export function batchSetDisplayOrders(
|
|
|
1587
1592
|
WHEN source IN ('schedule', 'reminder') THEN 'system:scheduled'
|
|
1588
1593
|
WHEN source IN ('heartbeat', 'task') THEN 'system:background'
|
|
1589
1594
|
WHEN conversation_type = 'background' AND COALESCE(source, '') != 'notification' THEN 'system:background'
|
|
1590
|
-
ELSE
|
|
1595
|
+
ELSE 'system:all'
|
|
1591
1596
|
END
|
|
1592
1597
|
ELSE group_id END
|
|
1593
1598
|
WHERE id = ?`,
|
|
@@ -26,6 +26,45 @@ export function getConversationDirName(
|
|
|
26
26
|
return `${getConversationDirTimestamp(createdAtMs)}_${id}`;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Parse a canonical conversation directory name (`<ISO-with-dashes>_<id>`)
|
|
31
|
+
* back into its components. Returns null for names that don't match the
|
|
32
|
+
* canonical format — callers should treat null as "skip this entry"
|
|
33
|
+
* rather than as an error.
|
|
34
|
+
*
|
|
35
|
+
* Inverse of {@link getConversationDirName}. Does NOT parse the legacy
|
|
36
|
+
* `<id>_<ISO-with-dashes>` format.
|
|
37
|
+
*/
|
|
38
|
+
export function parseConversationDirName(
|
|
39
|
+
name: string,
|
|
40
|
+
): { conversationId: string; createdAtMs: number } | null {
|
|
41
|
+
// Canonical format: YYYY-MM-DDTHH-MM-SS.sssZ_<uuid>
|
|
42
|
+
// The timestamp portion has 4 hyphens (date + time-component separators)
|
|
43
|
+
// and ends in `Z`. Anchor the regex to enforce that.
|
|
44
|
+
const match = name.match(
|
|
45
|
+
/^(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2}\.\d{1,9}Z)_(.+)$/,
|
|
46
|
+
);
|
|
47
|
+
if (!match) return null;
|
|
48
|
+
const [, datePart, hh, mm, ssAndMs, conversationId] = match;
|
|
49
|
+
const iso = `${datePart}T${hh}:${mm}:${ssAndMs}`;
|
|
50
|
+
const ms = Date.parse(iso);
|
|
51
|
+
if (Number.isNaN(ms)) return null;
|
|
52
|
+
if (!conversationId) return null;
|
|
53
|
+
// Reject conversation IDs that are path-traversal-shaped or contain
|
|
54
|
+
// path separators. These are never valid conversation IDs and would
|
|
55
|
+
// be a defense-in-depth concern if parsed.conversationId is later used
|
|
56
|
+
// to construct filesystem paths.
|
|
57
|
+
if (
|
|
58
|
+
conversationId === "." ||
|
|
59
|
+
conversationId === ".." ||
|
|
60
|
+
conversationId.includes("/") ||
|
|
61
|
+
conversationId.includes("\\")
|
|
62
|
+
) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
return { conversationId, createdAtMs: ms };
|
|
66
|
+
}
|
|
67
|
+
|
|
29
68
|
/**
|
|
30
69
|
* Return the absolute path to a conversation's timestamp-first disk-view
|
|
31
70
|
* directory.
|
|
@@ -57,15 +57,45 @@ export function ensureGroupMigration(): void {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
// 3. Seed system groups (
|
|
60
|
+
// 3. Seed system groups (four: pinned, scheduled, background, all)
|
|
61
|
+
const now = Math.floor(Date.now() / 1000);
|
|
61
62
|
rawExec(`
|
|
62
|
-
INSERT OR IGNORE INTO conversation_groups (id, name, sort_position, is_system_group)
|
|
63
|
+
INSERT OR IGNORE INTO conversation_groups (id, name, sort_position, is_system_group, created_at, updated_at)
|
|
63
64
|
VALUES
|
|
64
|
-
('system:pinned', 'Pinned', 0, TRUE),
|
|
65
|
-
('system:scheduled', 'Scheduled', 1, TRUE),
|
|
66
|
-
('system:background', 'Background', 2, TRUE)
|
|
65
|
+
('system:pinned', 'Pinned', 0, TRUE, ${now}, ${now}),
|
|
66
|
+
('system:scheduled', 'Scheduled', 1, TRUE, ${now}, ${now}),
|
|
67
|
+
('system:background', 'Background', 2, TRUE, ${now}, ${now}),
|
|
68
|
+
('system:all', 'Recents', 3, TRUE, ${now}, ${now})
|
|
67
69
|
`);
|
|
68
70
|
|
|
71
|
+
// One-time migration: move system:all to sortPosition 3 (from 999999).
|
|
72
|
+
// Bump custom groups at position 3+ up by 1 to make room. Wrapped in a
|
|
73
|
+
// transaction so a crash between the shift and the sentinel can't cause
|
|
74
|
+
// repeated drift on restart.
|
|
75
|
+
const sortShiftDone = rawGet<{ id: string }>(
|
|
76
|
+
"SELECT id FROM conversation_groups WHERE id = '_sort_shift_complete'",
|
|
77
|
+
);
|
|
78
|
+
if (!sortShiftDone) {
|
|
79
|
+
try {
|
|
80
|
+
rawExec("BEGIN");
|
|
81
|
+
rawRun(
|
|
82
|
+
"UPDATE conversation_groups SET sort_position = sort_position + 1 WHERE is_system_group = 0 AND sort_position >= 3",
|
|
83
|
+
);
|
|
84
|
+
rawRun(
|
|
85
|
+
"UPDATE conversation_groups SET sort_position = 3 WHERE id = 'system:all' AND sort_position != 3",
|
|
86
|
+
);
|
|
87
|
+
rawRun(
|
|
88
|
+
`INSERT OR IGNORE INTO conversation_groups (id, name, sort_position, is_system_group, created_at, updated_at)
|
|
89
|
+
VALUES ('_sort_shift_complete', '_sort_shift_complete', -1, TRUE, ${now}, ${now})`,
|
|
90
|
+
);
|
|
91
|
+
rawExec("COMMIT");
|
|
92
|
+
} catch (err) {
|
|
93
|
+
rawExec("ROLLBACK");
|
|
94
|
+
log.error({ err }, "Sort-position shift transaction failed, rolled back");
|
|
95
|
+
throw err;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
69
99
|
// 4. One-time backfill (guard: persistent marker prevents re-running on restart)
|
|
70
100
|
//
|
|
71
101
|
// The backfill sets group_id on existing conversations based on their attributes.
|
|
@@ -153,5 +183,35 @@ export function ensureGroupMigration(): void {
|
|
|
153
183
|
}
|
|
154
184
|
}
|
|
155
185
|
|
|
186
|
+
// 5. One-time backfill: assign all ungrouped conversations to system:all
|
|
187
|
+
//
|
|
188
|
+
// Separate from the initial backfill above because system:all is added later.
|
|
189
|
+
// Uses its own sentinel so it runs exactly once, even on existing installations
|
|
190
|
+
// where the original backfill already completed.
|
|
191
|
+
const allBackfillDone = rawGet<{ id: string }>(
|
|
192
|
+
"SELECT id FROM conversation_groups WHERE id = '_backfill_all_complete'",
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
if (!allBackfillDone) {
|
|
196
|
+
try {
|
|
197
|
+
rawExec("BEGIN");
|
|
198
|
+
|
|
199
|
+
rawExec(`
|
|
200
|
+
UPDATE conversations SET group_id = 'system:all' WHERE group_id IS NULL
|
|
201
|
+
`);
|
|
202
|
+
|
|
203
|
+
rawExec(`
|
|
204
|
+
INSERT OR IGNORE INTO conversation_groups (id, name, sort_position, is_system_group)
|
|
205
|
+
VALUES ('_backfill_all_complete', '_backfill_all_complete', -1, TRUE)
|
|
206
|
+
`);
|
|
207
|
+
|
|
208
|
+
rawExec("COMMIT");
|
|
209
|
+
} catch (err) {
|
|
210
|
+
rawExec("ROLLBACK");
|
|
211
|
+
log.error({ err }, "system:all backfill transaction failed, rolled back");
|
|
212
|
+
throw err;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
156
216
|
migrated = true;
|
|
157
217
|
}
|
|
@@ -205,7 +205,7 @@ export class LocalEmbeddingBackend implements EmbeddingBackend {
|
|
|
205
205
|
);
|
|
206
206
|
|
|
207
207
|
const proc = Bun.spawn({
|
|
208
|
-
cmd: [bunPath, workerPath, this.model, modelCacheDir],
|
|
208
|
+
cmd: [bunPath, "--smol", workerPath, this.model, modelCacheDir],
|
|
209
209
|
stdin: "pipe",
|
|
210
210
|
stdout: "pipe",
|
|
211
211
|
stderr: "pipe",
|
|
@@ -127,9 +127,7 @@ export function seedSkillGraphNodes(): void {
|
|
|
127
127
|
// skip catalog-based pruning to avoid incorrectly marking valid
|
|
128
128
|
// uninstalled catalog nodes as gone. But still prune locally disabled
|
|
129
129
|
// skills so stale capability nodes don't linger after cold start.
|
|
130
|
-
log.info(
|
|
131
|
-
"Catalog cache is cold — pruning only locally disabled skills",
|
|
132
|
-
);
|
|
130
|
+
log.info("Catalog cache is cold — pruning only locally disabled skills");
|
|
133
131
|
const disabled = resolved.filter((r) => r.state !== "enabled");
|
|
134
132
|
for (const { summary } of disabled) {
|
|
135
133
|
deleteSkillCapabilityNode(summary.id);
|
|
@@ -158,9 +156,9 @@ export function seedSkillGraphNodes(): void {
|
|
|
158
156
|
* Seed graph nodes for all CLI commands.
|
|
159
157
|
* Prunes stale nodes whose commands are no longer registered.
|
|
160
158
|
*/
|
|
161
|
-
export function seedCliGraphNodes(): void {
|
|
159
|
+
export async function seedCliGraphNodes(): Promise<void> {
|
|
162
160
|
try {
|
|
163
|
-
const program = buildCliProgram();
|
|
161
|
+
const program = await buildCliProgram();
|
|
164
162
|
|
|
165
163
|
const seenKeys = new Set<string>();
|
|
166
164
|
for (const cmd of program.commands) {
|
package/src/memory/group-crud.ts
CHANGED
|
@@ -33,7 +33,7 @@ export function listGroups(): ConversationGroupRow[] {
|
|
|
33
33
|
created_at: number;
|
|
34
34
|
updated_at: number;
|
|
35
35
|
}>(
|
|
36
|
-
"SELECT id, name, sort_position, is_system_group, created_at, updated_at FROM conversation_groups WHERE id
|
|
36
|
+
"SELECT id, name, sort_position, is_system_group, created_at, updated_at FROM conversation_groups WHERE id NOT IN ('_backfill_complete', '_backfill_all_complete', '_sort_shift_complete') ORDER BY sort_position ASC",
|
|
37
37
|
);
|
|
38
38
|
return rows.map((r) => ({
|
|
39
39
|
id: r.id,
|
|
@@ -79,8 +79,8 @@ export function getGroup(groupId: string): ConversationGroupRow | null {
|
|
|
79
79
|
|
|
80
80
|
/**
|
|
81
81
|
* Create a custom group. Server assigns sort_position as max(custom) + 1.
|
|
82
|
-
* System groups occupy positions 0 (pinned
|
|
83
|
-
* First custom group gets position
|
|
82
|
+
* System groups occupy positions 0–3 (pinned, scheduled, background, all).
|
|
83
|
+
* First custom group gets position 4. Fallback ?? 3 ensures 3 + 1 = 4 when
|
|
84
84
|
* no custom groups exist.
|
|
85
85
|
*/
|
|
86
86
|
export function createGroup(name: string): ConversationGroupRow {
|
|
@@ -88,7 +88,7 @@ export function createGroup(name: string): ConversationGroupRow {
|
|
|
88
88
|
const maxPos =
|
|
89
89
|
rawGet<{ max: number | null }>(
|
|
90
90
|
"SELECT MAX(sort_position) as max FROM conversation_groups WHERE is_system_group = 0",
|
|
91
|
-
)?.max ??
|
|
91
|
+
)?.max ?? 3;
|
|
92
92
|
const sortPosition = maxPos + 1;
|
|
93
93
|
const id = uuid();
|
|
94
94
|
const now = Math.floor(Date.now() / 1000);
|
|
@@ -153,13 +153,16 @@ export function updateGroup(
|
|
|
153
153
|
// Delete
|
|
154
154
|
// ---------------------------------------------------------------------------
|
|
155
155
|
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
// non-existent group.
|
|
156
|
+
// Reassign conversations to system:all before deleting the group so they
|
|
157
|
+
// don't end up with NULL group_id (which would violate the system:all
|
|
158
|
+
// invariant). The FK ON DELETE SET NULL would otherwise leave NULLs that
|
|
159
|
+
// the one-time backfill won't re-fix.
|
|
161
160
|
export function deleteGroup(groupId: string): boolean {
|
|
162
161
|
ensureGroupMigration();
|
|
162
|
+
rawRun(
|
|
163
|
+
"UPDATE conversations SET group_id = 'system:all' WHERE group_id = ?",
|
|
164
|
+
groupId,
|
|
165
|
+
);
|
|
163
166
|
rawRun("DELETE FROM conversation_groups WHERE id = ?", groupId);
|
|
164
167
|
return true;
|
|
165
168
|
}
|
|
@@ -176,6 +179,19 @@ export function reorderGroups(
|
|
|
176
179
|
rawExec("BEGIN");
|
|
177
180
|
try {
|
|
178
181
|
for (const update of updates) {
|
|
182
|
+
// Look up the group first — skip unknown/stale IDs and system groups
|
|
183
|
+
const group = rawGet<{ id: string; is_system_group: number }>(
|
|
184
|
+
"SELECT id, is_system_group FROM conversation_groups WHERE id = ?",
|
|
185
|
+
update.groupId,
|
|
186
|
+
);
|
|
187
|
+
if (!group) continue;
|
|
188
|
+
if (group.is_system_group === 1) continue;
|
|
189
|
+
|
|
190
|
+
if (update.sortPosition < 4) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
`Custom group sort_position must be >= 4 (got ${update.sortPosition} for ${update.groupId})`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
179
195
|
rawRun(
|
|
180
196
|
"UPDATE conversation_groups SET sort_position = ?, updated_at = ? WHERE id = ?",
|
|
181
197
|
update.sortPosition,
|
|
@@ -25,7 +25,7 @@ export interface MessagingProvider {
|
|
|
25
25
|
id: string;
|
|
26
26
|
/** Human-readable name (e.g. 'Slack', 'Gmail'). */
|
|
27
27
|
displayName: string;
|
|
28
|
-
/** Credential service name for token-manager (e.g. '
|
|
28
|
+
/** Credential service name for token-manager (e.g. 'slack'). */
|
|
29
29
|
credentialService: string;
|
|
30
30
|
|
|
31
31
|
// ── Universal operations (every platform must implement) ──────────
|
|
@@ -41,6 +41,10 @@ export interface ConversationCreatedInfo {
|
|
|
41
41
|
sourceEventName: string;
|
|
42
42
|
/** Present when the conversation is for a guardian-sensitive notification. */
|
|
43
43
|
targetGuardianPrincipalId?: string;
|
|
44
|
+
/** Conversation group identifier from the signal producer (e.g. "system:scheduled"). */
|
|
45
|
+
groupId?: string;
|
|
46
|
+
/** Semantic source from the signal producer (e.g. "schedule", "reminder"). */
|
|
47
|
+
source?: string;
|
|
44
48
|
}
|
|
45
49
|
export type OnConversationCreatedFn = (info: ConversationCreatedInfo) => void;
|
|
46
50
|
export interface BroadcastDecisionOptions {
|
|
@@ -238,6 +242,8 @@ export class NotificationBroadcaster {
|
|
|
238
242
|
title: conversationTitle,
|
|
239
243
|
sourceEventName: signal.sourceEventName,
|
|
240
244
|
targetGuardianPrincipalId,
|
|
245
|
+
groupId: signal.conversationMetadata?.groupId,
|
|
246
|
+
source: signal.conversationMetadata?.source,
|
|
241
247
|
};
|
|
242
248
|
|
|
243
249
|
// The per-dispatch onConversationCreated callback fires whenever a vellum
|
|
@@ -130,7 +130,9 @@ export async function pairDeliveryWithConversation(
|
|
|
130
130
|
const targetId = conversationAction.conversationId;
|
|
131
131
|
const existing = getConversation(targetId);
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
const effectiveSource =
|
|
134
|
+
signal.conversationMetadata?.source ?? "notification";
|
|
135
|
+
if (existing && existing.source === effectiveSource) {
|
|
134
136
|
// Append the seed message to the existing conversation
|
|
135
137
|
const message = await addMessage(
|
|
136
138
|
existing.id,
|
|
@@ -186,7 +188,9 @@ export async function pairDeliveryWithConversation(
|
|
|
186
188
|
const conversation = createConversation({
|
|
187
189
|
title,
|
|
188
190
|
conversationType,
|
|
189
|
-
source: "notification",
|
|
191
|
+
source: signal.conversationMetadata?.source ?? "notification",
|
|
192
|
+
groupId: signal.conversationMetadata?.groupId,
|
|
193
|
+
scheduleJobId: signal.conversationMetadata?.scheduleJobId,
|
|
190
194
|
});
|
|
191
195
|
|
|
192
196
|
const message = await addMessage(
|
|
@@ -241,7 +245,9 @@ export async function pairDeliveryWithConversation(
|
|
|
241
245
|
existingBinding.conversationId,
|
|
242
246
|
);
|
|
243
247
|
|
|
244
|
-
|
|
248
|
+
const effectiveSource =
|
|
249
|
+
signal.conversationMetadata?.source ?? "notification";
|
|
250
|
+
if (boundConversation && boundConversation.source === effectiveSource) {
|
|
245
251
|
const message = await addMessage(
|
|
246
252
|
boundConversation.id,
|
|
247
253
|
"assistant",
|
|
@@ -299,7 +305,9 @@ export async function pairDeliveryWithConversation(
|
|
|
299
305
|
const conversation = createConversation({
|
|
300
306
|
title,
|
|
301
307
|
conversationType,
|
|
302
|
-
source: "notification",
|
|
308
|
+
source: signal.conversationMetadata?.source ?? "notification",
|
|
309
|
+
groupId: signal.conversationMetadata?.groupId,
|
|
310
|
+
scheduleJobId: signal.conversationMetadata?.scheduleJobId,
|
|
303
311
|
});
|
|
304
312
|
|
|
305
313
|
// Skip memory indexing — notification audit messages are not conversational
|
|
@@ -79,6 +79,8 @@ function getBroadcaster(): NotificationBroadcaster {
|
|
|
79
79
|
title: info.title,
|
|
80
80
|
sourceEventName: info.sourceEventName,
|
|
81
81
|
targetGuardianPrincipalId: info.targetGuardianPrincipalId,
|
|
82
|
+
groupId: info.groupId,
|
|
83
|
+
source: info.source,
|
|
82
84
|
});
|
|
83
85
|
log.info(
|
|
84
86
|
{
|
|
@@ -176,6 +178,17 @@ export interface EmitSignalParams<TEventName extends string = string> {
|
|
|
176
178
|
* Useful for direct user-invoked actions that must fail closed.
|
|
177
179
|
*/
|
|
178
180
|
throwOnError?: boolean;
|
|
181
|
+
/**
|
|
182
|
+
* Optional metadata propagated to the conversation created by the notification
|
|
183
|
+
* pipeline. Allows signal producers (e.g. the scheduler) to set groupId,
|
|
184
|
+
* scheduleJobId, or override the default "notification" source on the
|
|
185
|
+
* resulting conversation so it appears in the correct folder on clients.
|
|
186
|
+
*/
|
|
187
|
+
conversationMetadata?: {
|
|
188
|
+
groupId?: string;
|
|
189
|
+
scheduleJobId?: string;
|
|
190
|
+
source?: string;
|
|
191
|
+
};
|
|
179
192
|
}
|
|
180
193
|
|
|
181
194
|
export interface EmitSignalResult {
|
|
@@ -210,6 +223,7 @@ export async function emitNotificationSignal<TEventName extends string>(
|
|
|
210
223
|
routingIntent: params.routingIntent,
|
|
211
224
|
routingHints: params.routingHints,
|
|
212
225
|
conversationAffinityHint: params.conversationAffinityHint,
|
|
226
|
+
conversationMetadata: params.conversationMetadata,
|
|
213
227
|
};
|
|
214
228
|
|
|
215
229
|
try {
|
|
@@ -200,4 +200,15 @@ export interface NotificationSignal<TEventName extends string = string> {
|
|
|
200
200
|
* affinity within a call session.
|
|
201
201
|
*/
|
|
202
202
|
conversationAffinityHint?: Partial<Record<string, string>>;
|
|
203
|
+
/**
|
|
204
|
+
* Optional metadata propagated to the conversation created by the notification
|
|
205
|
+
* pipeline. Allows signal producers (e.g. the scheduler) to set groupId,
|
|
206
|
+
* scheduleJobId, or override the default "notification" source on the
|
|
207
|
+
* resulting conversation so it appears in the correct folder on clients.
|
|
208
|
+
*/
|
|
209
|
+
conversationMetadata?: {
|
|
210
|
+
groupId?: string;
|
|
211
|
+
scheduleJobId?: string;
|
|
212
|
+
source?: string;
|
|
213
|
+
};
|
|
203
214
|
}
|
|
@@ -27,7 +27,7 @@ function makeMockClient(
|
|
|
27
27
|
fetch: mock(async (path: string, init?: RequestInit) => {
|
|
28
28
|
const url = `https://platform.example.com${path}`;
|
|
29
29
|
const headers = new Headers(init?.headers);
|
|
30
|
-
headers.set("Authorization", "
|
|
30
|
+
headers.set("Authorization", "Bearer test-api-key");
|
|
31
31
|
return mockFetchFn(url, { ...init, headers });
|
|
32
32
|
}),
|
|
33
33
|
} as unknown as VellumPlatformClient;
|
|
@@ -53,7 +53,7 @@ describe("PlatformOAuthConnection", () => {
|
|
|
53
53
|
);
|
|
54
54
|
expect(init?.method).toBe("POST");
|
|
55
55
|
const headers = new Headers(init?.headers);
|
|
56
|
-
expect(headers.get("Authorization")).toBe("
|
|
56
|
+
expect(headers.get("Authorization")).toBe("Bearer test-api-key");
|
|
57
57
|
expect(headers.get("Content-Type")).toBe("application/json");
|
|
58
58
|
|
|
59
59
|
const parsed = JSON.parse(init?.body as string);
|
|
@@ -3,6 +3,7 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
|
|
5
5
|
import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
|
|
6
|
+
import { getIsContainerized } from "../config/env-registry.js";
|
|
6
7
|
import { getConfig } from "../config/loader.js";
|
|
7
8
|
import { loadSkillCatalog, resolveSkillSelector } from "../config/skills.js";
|
|
8
9
|
import { indexCatalogById } from "../skills/include-graph.js";
|
|
@@ -1097,9 +1098,8 @@ export async function check(
|
|
|
1097
1098
|
!matchedRule &&
|
|
1098
1099
|
risk === RiskLevel.Low
|
|
1099
1100
|
) {
|
|
1100
|
-
//
|
|
1101
|
-
|
|
1102
|
-
if (toolName === "bash" && !sandboxEnabled) {
|
|
1101
|
+
// Outside a container, bash runs on the host — don't auto-allow
|
|
1102
|
+
if (toolName === "bash" && !getIsContainerized()) {
|
|
1103
1103
|
// Fall through to risk-based policy below
|
|
1104
1104
|
} else if (isWorkspaceScopedInvocation(toolName, input, workingDir)) {
|
|
1105
1105
|
return {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
|
|
3
|
+
import { getIsContainerized } from "../config/env-registry.js";
|
|
3
4
|
import { getConfig } from "../config/loader.js";
|
|
4
5
|
import { getBundledSkillsDir } from "../config/skills.js";
|
|
5
6
|
import { getWorkspaceDir } from "../util/platform.js";
|
|
@@ -42,7 +43,6 @@ export function getDefaultRuleTemplates(): DefaultRuleTemplate[] {
|
|
|
42
43
|
// Some test suites mock getConfig() with partial objects; treat missing
|
|
43
44
|
// branches as defaults so rule generation remains deterministic.
|
|
44
45
|
const config = getConfig() as {
|
|
45
|
-
sandbox?: { enabled?: boolean };
|
|
46
46
|
skills?: { load?: { extraDirs?: unknown } };
|
|
47
47
|
};
|
|
48
48
|
|
|
@@ -67,12 +67,11 @@ export function getDefaultRuleTemplates(): DefaultRuleTemplate[] {
|
|
|
67
67
|
priority: 50,
|
|
68
68
|
};
|
|
69
69
|
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
// commands
|
|
74
|
-
const
|
|
75
|
-
const sandboxShellRule: DefaultRuleTemplate | null = sandboxEnabled
|
|
70
|
+
// When running inside a container (IS_CONTAINERIZED=true), bash commands
|
|
71
|
+
// execute in an isolated environment — auto-allow all of them (including
|
|
72
|
+
// high-risk) so the user is never prompted. Outside a container, bash
|
|
73
|
+
// commands run on the host and go through normal permission checks.
|
|
74
|
+
const bashShellRule: DefaultRuleTemplate | null = getIsContainerized()
|
|
76
75
|
? {
|
|
77
76
|
id: "default:allow-bash-global",
|
|
78
77
|
tool: "bash",
|
|
@@ -300,7 +299,7 @@ export function getDefaultRuleTemplates(): DefaultRuleTemplate[] {
|
|
|
300
299
|
return [
|
|
301
300
|
...hostFileRules,
|
|
302
301
|
hostShellRule,
|
|
303
|
-
...(
|
|
302
|
+
...(bashShellRule ? [bashShellRule] : []),
|
|
304
303
|
...computerUseRules,
|
|
305
304
|
...managedSkillRules,
|
|
306
305
|
...workspacePromptRules,
|
|
@@ -58,7 +58,6 @@ export class PermissionPrompter {
|
|
|
58
58
|
newContent: string;
|
|
59
59
|
isNewFile: boolean;
|
|
60
60
|
},
|
|
61
|
-
sandboxed?: boolean,
|
|
62
61
|
conversationId?: string,
|
|
63
62
|
executionTarget?: ExecutionTarget,
|
|
64
63
|
persistentDecisionsAllowed?: boolean,
|
|
@@ -119,7 +118,6 @@ export class PermissionPrompter {
|
|
|
119
118
|
scope: o.scope,
|
|
120
119
|
})),
|
|
121
120
|
diff,
|
|
122
|
-
sandboxed,
|
|
123
121
|
conversationId,
|
|
124
122
|
executionTarget,
|
|
125
123
|
persistentDecisionsAllowed: persistentDecisionsAllowed ?? true,
|
package/src/platform/client.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Centralized platform API client.
|
|
3
3
|
*
|
|
4
4
|
* Owns managed proxy context resolution, prerequisite validation, and
|
|
5
|
-
* authenticated fetch for all platform API calls
|
|
5
|
+
* authenticated fetch for all platform API calls.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { getPlatformAssistantId } from "../config/env.js";
|
|
@@ -86,14 +86,21 @@ Read BOOTSTRAP-REFERENCE.md for the exact `ui_show` card payload.
|
|
|
86
86
|
|
|
87
87
|
**Pacing rule:** Don't ask more than 2 questions in a row without doing something. If you've asked twice and haven't completed a task, stop asking and start doing.
|
|
88
88
|
|
|
89
|
-
### Step 5:
|
|
89
|
+
### Step 5: Keep the Momentum
|
|
90
90
|
|
|
91
|
-
After the task is done,
|
|
91
|
+
After the task is done, don't pivot to setup. Build on what just happened.
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
**First choice: chain off the task.** Suggest one natural follow-up that extends the work they just did. Examples:
|
|
94
|
+
- Built a deck → "Want to send this to someone or refine a specific slide?"
|
|
95
|
+
- Connected email → "Want me to summarize what needs your attention today?"
|
|
96
|
+
- Researched a topic → "Want me to go deeper on one of those points, or turn this into something shareable?"
|
|
97
|
+
- Built an app → Proactively suggest a specific improvement to what they built (a missing feature, a UI polish, better error handling). Show you have taste.
|
|
95
98
|
|
|
96
|
-
|
|
99
|
+
The follow-up should feel like a coworker saying "while we're at it..." — not a product tour.
|
|
100
|
+
|
|
101
|
+
**Fallback: plant a hook for next time.** If the task was a dead-end (photo edit, one-off question), reach back to Step 3. Pick up something from their "what's on your mind" answer and offer to work on it: "You mentioned [X] earlier — I can dig into that and have something ready next time you open this."
|
|
102
|
+
|
|
103
|
+
If they engage, do it. If they decline or wrap up, move on. One offer, no pressure.
|
|
97
104
|
|
|
98
105
|
### Step 6: Before You Go
|
|
99
106
|
|
|
@@ -119,6 +126,8 @@ Mark declined fields so you don't re-ask later (e.g., `Work role: declined_by_us
|
|
|
119
126
|
|
|
120
127
|
**Call `file_edit` immediately whenever you learn something, in the same turn.** Don't batch saves. The moment the user gives you a name, save it. The moment you infer their style, save it.
|
|
121
128
|
|
|
129
|
+
**After tool calls, do not repeat yourself.** Your text before tool calls is already visible to the user. When tool results return and you continue, pick up where you left off — don't re-confirm, re-greet, or re-ask the same question. If you already asked something and are waiting for the user's answer, just stop.
|
|
130
|
+
|
|
122
131
|
**The contents of IDENTITY.md, SOUL.md, and USER.md are already in your system prompt.** Use the exact text you see there for `old_string` in `file_edit`. Do not guess or invent content.
|
|
123
132
|
|
|
124
133
|
Update `IDENTITY.md` (name, nature, personality, style) and `USER.md` (their name, pronouns, goals, locale, work role, hobbies, daily tools). Save behavioral guidelines to `SOUL.md`.
|