@vellumai/assistant 0.5.5 → 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 +3 -4
- 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__/docker-signing-key-bootstrap.test.ts +207 -0
- package/src/__tests__/memory-regressions.test.ts +8 -30
- package/src/__tests__/require-fresh-approval.test.ts +4 -0
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +4 -0
- package/src/__tests__/tool-executor.test.ts +4 -0
- package/src/cli/commands/conversations.ts +0 -18
- package/src/config/env.ts +8 -2
- package/src/config/feature-flag-registry.json +0 -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/daemon/config-watcher.ts +1 -4
- 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/handlers/conversations.ts +0 -11
- package/src/daemon/lifecycle.ts +3 -46
- package/src/followups/followup-store.ts +5 -2
- 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/indexer.ts +15 -106
- 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/permissions/defaults.ts +3 -3
- package/src/permissions/trust-client.ts +2 -13
- package/src/permissions/trust-store.ts +8 -3
- package/src/runtime/auth/route-policy.ts +14 -0
- package/src/runtime/auth/token-service.ts +133 -0
- package/src/runtime/http-server.ts +2 -0
- 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/memory-item-routes.test.ts +221 -3
- package/src/runtime/routes/memory-item-routes.ts +124 -2
- package/src/runtime/routes/upgrade-broadcast-routes.ts +151 -0
- package/src/schedule/schedule-store.ts +0 -21
- package/src/skills/inline-command-render.ts +5 -1
- package/src/skills/inline-command-runner.ts +30 -2
- 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/platform.ts +5 -5
- package/src/util/xml.ts +8 -0
- package/src/workspace/heartbeat-service.ts +5 -24
- 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
|
@@ -11,10 +11,7 @@ import type { MemoryJob } from "../jobs-store.js";
|
|
|
11
11
|
import { extractMediaBlocks } from "../message-content.js";
|
|
12
12
|
import {
|
|
13
13
|
mediaAssets,
|
|
14
|
-
memoryChunks,
|
|
15
|
-
memoryEpisodes,
|
|
16
14
|
memoryItems,
|
|
17
|
-
memoryObservations,
|
|
18
15
|
memorySegments,
|
|
19
16
|
memorySummaries,
|
|
20
17
|
messages,
|
|
@@ -93,26 +90,6 @@ export async function embedSummaryJob(
|
|
|
93
90
|
);
|
|
94
91
|
}
|
|
95
92
|
|
|
96
|
-
export async function embedChunkJob(
|
|
97
|
-
job: MemoryJob,
|
|
98
|
-
config: AssistantConfig,
|
|
99
|
-
): Promise<void> {
|
|
100
|
-
const chunkId = asString(job.payload.chunkId);
|
|
101
|
-
if (!chunkId) return;
|
|
102
|
-
const db = getDb();
|
|
103
|
-
const chunk = db
|
|
104
|
-
.select()
|
|
105
|
-
.from(memoryChunks)
|
|
106
|
-
.where(eq(memoryChunks.id, chunkId))
|
|
107
|
-
.get();
|
|
108
|
-
if (!chunk) return;
|
|
109
|
-
await embedAndUpsert(config, "chunk", chunk.id, chunk.content, {
|
|
110
|
-
observation_id: chunk.observationId,
|
|
111
|
-
created_at: chunk.createdAt,
|
|
112
|
-
memory_scope_id: chunk.scopeId,
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
|
|
116
93
|
export async function embedMediaJob(
|
|
117
94
|
job: MemoryJob,
|
|
118
95
|
config: AssistantConfig,
|
|
@@ -146,40 +123,6 @@ export async function embedMediaJob(
|
|
|
146
123
|
});
|
|
147
124
|
}
|
|
148
125
|
|
|
149
|
-
export async function embedObservationJob(
|
|
150
|
-
job: MemoryJob,
|
|
151
|
-
config: AssistantConfig,
|
|
152
|
-
): Promise<void> {
|
|
153
|
-
const observationId = asString(job.payload.observationId);
|
|
154
|
-
const chunkId = asString(job.payload.chunkId);
|
|
155
|
-
if (!observationId || !chunkId) return;
|
|
156
|
-
|
|
157
|
-
const db = getDb();
|
|
158
|
-
const observation = db
|
|
159
|
-
.select()
|
|
160
|
-
.from(memoryObservations)
|
|
161
|
-
.where(eq(memoryObservations.id, observationId))
|
|
162
|
-
.get();
|
|
163
|
-
if (!observation) return;
|
|
164
|
-
|
|
165
|
-
const chunk = db
|
|
166
|
-
.select()
|
|
167
|
-
.from(memoryChunks)
|
|
168
|
-
.where(eq(memoryChunks.id, chunkId))
|
|
169
|
-
.get();
|
|
170
|
-
if (!chunk) return;
|
|
171
|
-
|
|
172
|
-
await embedAndUpsert(config, "observation", chunk.id, chunk.content, {
|
|
173
|
-
observation_id: observationId,
|
|
174
|
-
conversation_id: observation.conversationId,
|
|
175
|
-
role: observation.role,
|
|
176
|
-
modality: observation.modality,
|
|
177
|
-
source: observation.source,
|
|
178
|
-
created_at: observation.createdAt,
|
|
179
|
-
memory_scope_id: observation.scopeId,
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
|
|
183
126
|
export async function embedAttachmentJob(
|
|
184
127
|
job: MemoryJob,
|
|
185
128
|
config: AssistantConfig,
|
|
@@ -216,25 +159,3 @@ export async function embedAttachmentJob(
|
|
|
216
159
|
memory_scope_id: memoryScopeId,
|
|
217
160
|
});
|
|
218
161
|
}
|
|
219
|
-
|
|
220
|
-
export async function embedEpisodeJob(
|
|
221
|
-
job: MemoryJob,
|
|
222
|
-
config: AssistantConfig,
|
|
223
|
-
): Promise<void> {
|
|
224
|
-
const episodeId = asString(job.payload.episodeId);
|
|
225
|
-
if (!episodeId) return;
|
|
226
|
-
const db = getDb();
|
|
227
|
-
const episode = db
|
|
228
|
-
.select()
|
|
229
|
-
.from(memoryEpisodes)
|
|
230
|
-
.where(eq(memoryEpisodes.id, episodeId))
|
|
231
|
-
.get();
|
|
232
|
-
if (!episode) return;
|
|
233
|
-
const text = `[episode] ${episode.title}: ${episode.summary}`;
|
|
234
|
-
await embedAndUpsert(config, "episode", episode.id, text, {
|
|
235
|
-
conversation_id: episode.conversationId,
|
|
236
|
-
created_at: episode.startAt,
|
|
237
|
-
last_seen_at: episode.endAt,
|
|
238
|
-
memory_scope_id: episode.scopeId,
|
|
239
|
-
});
|
|
240
|
-
}
|
package/src/memory/job-utils.ts
CHANGED
|
@@ -142,7 +142,7 @@ export function truncate(text: string, max: number): string {
|
|
|
142
142
|
|
|
143
143
|
export async function embedAndUpsert(
|
|
144
144
|
config: AssistantConfig,
|
|
145
|
-
targetType: "segment" | "item" | "summary" | "
|
|
145
|
+
targetType: "segment" | "item" | "summary" | "media",
|
|
146
146
|
targetId: string,
|
|
147
147
|
input: EmbeddingInput,
|
|
148
148
|
extraPayload?: Record<string, unknown>,
|
package/src/memory/jobs-store.ts
CHANGED
|
@@ -12,9 +12,6 @@ export type MemoryJobType =
|
|
|
12
12
|
| "embed_segment"
|
|
13
13
|
| "embed_item"
|
|
14
14
|
| "embed_summary"
|
|
15
|
-
| "embed_chunk"
|
|
16
|
-
| "embed_episode"
|
|
17
|
-
| "embed_observation"
|
|
18
15
|
| "extract_items"
|
|
19
16
|
| "extract_entities"
|
|
20
17
|
| "cleanup_stale_superseded_items"
|
|
@@ -30,8 +27,6 @@ export type MemoryJobType =
|
|
|
30
27
|
| "embed_media"
|
|
31
28
|
| "embed_attachment"
|
|
32
29
|
| "generate_conversation_starters"
|
|
33
|
-
| "reduce_conversation_memory"
|
|
34
|
-
| "backfill_simplified_memory"
|
|
35
30
|
| "generate_capability_cards" // legacy compat — silently dropped by worker (capability cards removed)
|
|
36
31
|
| "generate_thread_starters"; // legacy compat — silently dropped by worker (renamed to generate_conversation_starters)
|
|
37
32
|
|
|
@@ -39,9 +34,6 @@ const EMBED_JOB_TYPES: MemoryJobType[] = [
|
|
|
39
34
|
"embed_segment",
|
|
40
35
|
"embed_item",
|
|
41
36
|
"embed_summary",
|
|
42
|
-
"embed_chunk",
|
|
43
|
-
"embed_episode",
|
|
44
|
-
"embed_observation",
|
|
45
37
|
"embed_media",
|
|
46
38
|
"embed_attachment",
|
|
47
39
|
];
|
|
@@ -3,7 +3,6 @@ import type { AssistantConfig } from "../config/types.js";
|
|
|
3
3
|
import { getLogger } from "../util/logger.js";
|
|
4
4
|
import { rawRun } from "./db.js";
|
|
5
5
|
import { backfillJob } from "./job-handlers/backfill.js";
|
|
6
|
-
import { backfillSimplifiedMemoryJob } from "./job-handlers/backfill-simplified-memory.js";
|
|
7
6
|
import {
|
|
8
7
|
cleanupStaleSupersededItemsJob,
|
|
9
8
|
pruneOldConversationsJob,
|
|
@@ -12,11 +11,8 @@ import { generateConversationStartersJob } from "./job-handlers/conversation-sta
|
|
|
12
11
|
// ── Per-job-type handlers ──────────────────────────────────────────
|
|
13
12
|
import {
|
|
14
13
|
embedAttachmentJob,
|
|
15
|
-
embedChunkJob,
|
|
16
|
-
embedEpisodeJob,
|
|
17
14
|
embedItemJob,
|
|
18
15
|
embedMediaJob,
|
|
19
|
-
embedObservationJob,
|
|
20
16
|
embedSegmentJob,
|
|
21
17
|
embedSummaryJob,
|
|
22
18
|
} from "./job-handlers/embedding.js";
|
|
@@ -26,7 +22,6 @@ import {
|
|
|
26
22
|
rebuildIndexJob,
|
|
27
23
|
} from "./job-handlers/index-maintenance.js";
|
|
28
24
|
import { mediaProcessingJob } from "./job-handlers/media-processing.js";
|
|
29
|
-
import { reduceConversationMemoryJob } from "./job-handlers/reduce-conversation-memory.js";
|
|
30
25
|
import { buildConversationSummaryJob } from "./job-handlers/summarization.js";
|
|
31
26
|
import {
|
|
32
27
|
BackendUnavailableError,
|
|
@@ -272,15 +267,6 @@ async function processJob(
|
|
|
272
267
|
case "embed_summary":
|
|
273
268
|
await embedSummaryJob(job, config);
|
|
274
269
|
return;
|
|
275
|
-
case "embed_chunk":
|
|
276
|
-
await embedChunkJob(job, config);
|
|
277
|
-
return;
|
|
278
|
-
case "embed_episode":
|
|
279
|
-
await embedEpisodeJob(job, config);
|
|
280
|
-
return;
|
|
281
|
-
case "embed_observation":
|
|
282
|
-
await embedObservationJob(job, config);
|
|
283
|
-
return;
|
|
284
270
|
case "extract_items":
|
|
285
271
|
await extractItemsJob(job);
|
|
286
272
|
return;
|
|
@@ -321,12 +307,6 @@ async function processJob(
|
|
|
321
307
|
case "embed_attachment":
|
|
322
308
|
await embedAttachmentJob(job, config);
|
|
323
309
|
return;
|
|
324
|
-
case "reduce_conversation_memory":
|
|
325
|
-
await reduceConversationMemoryJob(job);
|
|
326
|
-
return;
|
|
327
|
-
case "backfill_simplified_memory":
|
|
328
|
-
await backfillSimplifiedMemoryJob(job);
|
|
329
|
-
return;
|
|
330
310
|
case "generate_conversation_starters":
|
|
331
311
|
await generateConversationStartersJob(job);
|
|
332
312
|
return;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { DrizzleDb } from "../db-connection.js";
|
|
2
|
+
import { getSqliteFrom } from "../db-connection.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Drop simplified-memory tables and reducer checkpoint columns added by
|
|
6
|
+
* the simplified-memory-v1 plan, reverting to the legacy item/tier/XML
|
|
7
|
+
* memory system.
|
|
8
|
+
*/
|
|
9
|
+
export function migrateDropSimplifiedMemory(database: DrizzleDb): void {
|
|
10
|
+
const raw = getSqliteFrom(database);
|
|
11
|
+
|
|
12
|
+
// Drop simplified-memory tables (idempotent — IF EXISTS).
|
|
13
|
+
raw.exec(`DROP TABLE IF EXISTS time_contexts`);
|
|
14
|
+
raw.exec(`DROP TABLE IF EXISTS open_loops`);
|
|
15
|
+
raw.exec(`DROP TABLE IF EXISTS memory_observations`);
|
|
16
|
+
raw.exec(`DROP TABLE IF EXISTS memory_chunks`);
|
|
17
|
+
raw.exec(`DROP TABLE IF EXISTS memory_episodes`);
|
|
18
|
+
|
|
19
|
+
// Remove reducer checkpoint columns from conversations.
|
|
20
|
+
// SQLite doesn't support DROP COLUMN before 3.35.0, but Bun's built-in
|
|
21
|
+
// SQLite is >= 3.38, so this is safe.
|
|
22
|
+
for (const col of [
|
|
23
|
+
"memory_reduced_through_message_id",
|
|
24
|
+
"memory_dirty_tail_since_message_id",
|
|
25
|
+
"memory_last_reduced_at",
|
|
26
|
+
]) {
|
|
27
|
+
try {
|
|
28
|
+
raw.exec(`ALTER TABLE conversations DROP COLUMN ${col}`);
|
|
29
|
+
} catch {
|
|
30
|
+
// Column doesn't exist — already cleaned up.
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Remove embedding rows for archive target types that no longer exist.
|
|
35
|
+
try {
|
|
36
|
+
raw.exec(
|
|
37
|
+
`DELETE FROM memory_embeddings WHERE target_type IN ('observation', 'chunk', 'episode')`,
|
|
38
|
+
);
|
|
39
|
+
} catch {
|
|
40
|
+
// Column doesn't exist — table was never migrated to include target_type.
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -126,10 +126,8 @@ export { migrateRenameThreadStartersCheckpoints } from "./181-rename-thread-star
|
|
|
126
126
|
export { migrateOAuthProvidersDisplayMetadata } from "./182-oauth-providers-display-metadata.js";
|
|
127
127
|
export { migrateConversationForkLineage } from "./183-add-conversation-fork-lineage.js";
|
|
128
128
|
export { migrateLlmRequestLogProvider } from "./184-llm-request-log-provider.js";
|
|
129
|
-
export { migrateMemoryBriefState } from "./185-memory-brief-state.js";
|
|
130
|
-
export { migrateMemoryArchiveTables } from "./186-memory-archive.js";
|
|
131
|
-
export { migrateMemoryReducerCheckpoints } from "./187-memory-reducer-checkpoints.js";
|
|
132
129
|
export { migrateScheduleQuietFlag } from "./188-schedule-quiet-flag.js";
|
|
130
|
+
export { migrateDropSimplifiedMemory } from "./189-drop-simplified-memory.js";
|
|
133
131
|
export {
|
|
134
132
|
MIGRATION_REGISTRY,
|
|
135
133
|
type MigrationRegistryEntry,
|
|
@@ -20,7 +20,7 @@ export interface QdrantClientConfig {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export interface QdrantPointPayload {
|
|
23
|
-
target_type: "segment" | "item" | "summary" | "
|
|
23
|
+
target_type: "segment" | "item" | "summary" | "media";
|
|
24
24
|
target_id: string;
|
|
25
25
|
text: string;
|
|
26
26
|
kind?: string;
|
|
@@ -230,7 +230,7 @@ export class VellumQdrantClient {
|
|
|
230
230
|
}
|
|
231
231
|
|
|
232
232
|
async upsert(
|
|
233
|
-
targetType: "segment" | "item" | "summary" | "
|
|
233
|
+
targetType: "segment" | "item" | "summary" | "media",
|
|
234
234
|
targetId: string,
|
|
235
235
|
vector: number[],
|
|
236
236
|
payload: Omit<QdrantPointPayload, "target_type" | "target_id">,
|
|
@@ -324,9 +324,7 @@ export class VellumQdrantClient {
|
|
|
324
324
|
async searchWithFilter(
|
|
325
325
|
vector: number[],
|
|
326
326
|
limit: number,
|
|
327
|
-
targetTypes: Array<
|
|
328
|
-
"segment" | "item" | "summary" | "media" | "chunk" | "episode"
|
|
329
|
-
>,
|
|
327
|
+
targetTypes: Array<"segment" | "item" | "summary" | "media">,
|
|
330
328
|
excludeMessageIds?: string[],
|
|
331
329
|
scopeIds?: string[],
|
|
332
330
|
): Promise<QdrantSearchResult[]> {
|
|
@@ -349,7 +347,7 @@ export class VellumQdrantClient {
|
|
|
349
347
|
},
|
|
350
348
|
{
|
|
351
349
|
key: "target_type",
|
|
352
|
-
match: { any: ["segment", "summary", "media"
|
|
350
|
+
match: { any: ["segment", "summary", "media"] },
|
|
353
351
|
},
|
|
354
352
|
],
|
|
355
353
|
});
|
|
@@ -30,9 +30,6 @@ export const conversations = sqliteTable(
|
|
|
30
30
|
forkParentMessageId: text("fork_parent_message_id"),
|
|
31
31
|
isAutoTitle: integer("is_auto_title").notNull().default(1),
|
|
32
32
|
scheduleJobId: text("schedule_job_id"),
|
|
33
|
-
memoryReducedThroughMessageId: text("memory_reduced_through_message_id"),
|
|
34
|
-
memoryDirtyTailSinceMessageId: text("memory_dirty_tail_since_message_id"),
|
|
35
|
-
memoryLastReducedAt: integer("memory_last_reduced_at"),
|
|
36
33
|
},
|
|
37
34
|
(table) => [
|
|
38
35
|
index("idx_conversations_updated_at").on(table.updatedAt),
|
|
@@ -3,8 +3,6 @@ export * from "./contacts.js";
|
|
|
3
3
|
export * from "./conversations.js";
|
|
4
4
|
export * from "./guardian.js";
|
|
5
5
|
export * from "./infrastructure.js";
|
|
6
|
-
export * from "./memory-archive.js";
|
|
7
|
-
export * from "./memory-brief.js";
|
|
8
6
|
export * from "./memory-core.js";
|
|
9
7
|
export * from "./notifications.js";
|
|
10
8
|
export * from "./oauth.js";
|
|
@@ -10,7 +10,7 @@ import { readdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
|
|
12
12
|
import { ensureDir, pathExists } from "../util/fs.js";
|
|
13
|
-
import {
|
|
13
|
+
import { getWorkspaceDir } from "../util/platform.js";
|
|
14
14
|
|
|
15
15
|
export interface Draft {
|
|
16
16
|
id: string;
|
|
@@ -25,7 +25,7 @@ export interface Draft {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
function getDraftsDir(platform: string): string {
|
|
28
|
-
const dir = join(
|
|
28
|
+
const dir = join(getWorkspaceDir(), "data", "drafts", platform);
|
|
29
29
|
ensureDir(dir);
|
|
30
30
|
return dir;
|
|
31
31
|
}
|
|
@@ -2,7 +2,7 @@ import { join } from "node:path";
|
|
|
2
2
|
|
|
3
3
|
import { getConfig } from "../config/loader.js";
|
|
4
4
|
import { getBundledSkillsDir } from "../config/skills.js";
|
|
5
|
-
import {
|
|
5
|
+
import { getWorkspaceDir } from "../util/platform.js";
|
|
6
6
|
|
|
7
7
|
export interface DefaultRuleTemplate {
|
|
8
8
|
id: string;
|
|
@@ -116,7 +116,7 @@ export function getDefaultRuleTemplates(): DefaultRuleTemplate[] {
|
|
|
116
116
|
// Workspace prompt files — the agent should always be able to read, edit,
|
|
117
117
|
// and write these without prompting. Also allow `rm BOOTSTRAP.md` so the
|
|
118
118
|
// agent can delete it at the end of the onboarding ritual.
|
|
119
|
-
const workspaceDir =
|
|
119
|
+
const workspaceDir = getWorkspaceDir().replaceAll("\\", "/");
|
|
120
120
|
const WORKSPACE_PROMPT_FILES = [
|
|
121
121
|
"IDENTITY.md",
|
|
122
122
|
"USER.md",
|
|
@@ -163,7 +163,7 @@ export function getDefaultRuleTemplates(): DefaultRuleTemplate[] {
|
|
|
163
163
|
// Skill source directories — writing or editing skill source files should
|
|
164
164
|
// require explicit user approval so a compromised agent loop cannot silently
|
|
165
165
|
// modify skill code to escalate privileges.
|
|
166
|
-
const managedSkillsDir = join(
|
|
166
|
+
const managedSkillsDir = join(getWorkspaceDir(), "skills").replaceAll(
|
|
167
167
|
"\\",
|
|
168
168
|
"/",
|
|
169
169
|
);
|
|
@@ -33,17 +33,6 @@ export interface AcceptStarterBundleResult {
|
|
|
33
33
|
// Helpers
|
|
34
34
|
// ---------------------------------------------------------------------------
|
|
35
35
|
|
|
36
|
-
/**
|
|
37
|
-
* Resolve the gateway base URL for trust rule requests.
|
|
38
|
-
*
|
|
39
|
-
* Prefers the `GATEWAY_INTERNAL_URL` env var (set in Docker environments
|
|
40
|
-
* where the gateway runs in a separate container), falling back to the
|
|
41
|
-
* existing `getGatewayInternalBaseUrl()` helper for local deployments.
|
|
42
|
-
*/
|
|
43
|
-
function getBaseUrl(): string {
|
|
44
|
-
return process.env.GATEWAY_INTERNAL_URL ?? getGatewayInternalBaseUrl();
|
|
45
|
-
}
|
|
46
|
-
|
|
47
36
|
function authHeaders(): Record<string, string> {
|
|
48
37
|
return {
|
|
49
38
|
Authorization: `Bearer ${mintDaemonDeliveryToken()}`,
|
|
@@ -60,7 +49,7 @@ async function request<T>(
|
|
|
60
49
|
path: string,
|
|
61
50
|
body?: unknown,
|
|
62
51
|
): Promise<T> {
|
|
63
|
-
const url = `${
|
|
52
|
+
const url = `${getGatewayInternalBaseUrl()}${path}`;
|
|
64
53
|
const options: RequestInit = {
|
|
65
54
|
method,
|
|
66
55
|
headers: authHeaders(),
|
|
@@ -102,7 +91,7 @@ async function request<T>(
|
|
|
102
91
|
* Write operations are user-initiated and infrequent, so blocking is acceptable.
|
|
103
92
|
*/
|
|
104
93
|
function requestSync<T>(method: string, path: string, body?: unknown): T {
|
|
105
|
-
const url = `${
|
|
94
|
+
const url = `${getGatewayInternalBaseUrl()}${path}`;
|
|
106
95
|
const headers = authHeaders();
|
|
107
96
|
const args: string[] = [
|
|
108
97
|
"curl",
|
|
@@ -185,9 +185,10 @@ function backfillDefaults(rules: TrustRule[]): boolean {
|
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
// Migrate existing default rules whose priority, pattern, decision,
|
|
189
|
-
// allowHighRisk has changed in the template (e.g. host_bash pattern
|
|
190
|
-
// from '*' to '**', host tool priorities changed from 1000 to 50
|
|
188
|
+
// Migrate existing default rules whose priority, pattern, scope, decision,
|
|
189
|
+
// or allowHighRisk has changed in the template (e.g. host_bash pattern
|
|
190
|
+
// changed from '*' to '**', host tool priorities changed from 1000 to 50,
|
|
191
|
+
// workspace scope changed from getRootDir()+workspace to getWorkspaceDir()).
|
|
191
192
|
for (const template of getDefaultRuleTemplates()) {
|
|
192
193
|
if (existingIds.has(template.id)) {
|
|
193
194
|
const rule = rules.find((r) => r.id === template.id);
|
|
@@ -195,6 +196,7 @@ function backfillDefaults(rules: TrustRule[]): boolean {
|
|
|
195
196
|
rule &&
|
|
196
197
|
(rule.priority !== template.priority ||
|
|
197
198
|
rule.pattern !== template.pattern ||
|
|
199
|
+
rule.scope !== template.scope ||
|
|
198
200
|
rule.decision !== template.decision ||
|
|
199
201
|
rule.allowHighRisk !== template.allowHighRisk)
|
|
200
202
|
) {
|
|
@@ -205,11 +207,14 @@ function backfillDefaults(rules: TrustRule[]): boolean {
|
|
|
205
207
|
newPriority: template.priority,
|
|
206
208
|
oldPattern: rule.pattern,
|
|
207
209
|
newPattern: template.pattern,
|
|
210
|
+
oldScope: rule.scope,
|
|
211
|
+
newScope: template.scope,
|
|
208
212
|
},
|
|
209
213
|
"Migrated default rule to updated template values",
|
|
210
214
|
);
|
|
211
215
|
rule.priority = template.priority;
|
|
212
216
|
rule.pattern = template.pattern;
|
|
217
|
+
rule.scope = template.scope;
|
|
213
218
|
rule.decision = template.decision;
|
|
214
219
|
if (template.allowHighRisk != null) {
|
|
215
220
|
rule.allowHighRisk = template.allowHighRisk;
|
|
@@ -176,6 +176,7 @@ const ACTOR_ENDPOINTS: Array<{ endpoint: string; scopes: Scope[] }> = [
|
|
|
176
176
|
|
|
177
177
|
// Settings / integrations / identity
|
|
178
178
|
{ endpoint: "identity", scopes: ["settings.read"] },
|
|
179
|
+
{ endpoint: "identity/intro", scopes: ["settings.read"] },
|
|
179
180
|
{ endpoint: "brain-graph", scopes: ["settings.read"] },
|
|
180
181
|
{ endpoint: "brain-graph-ui", scopes: ["settings.read"] },
|
|
181
182
|
{ endpoint: "contacts", scopes: ["settings.read"] },
|
|
@@ -347,6 +348,10 @@ const ACTOR_ENDPOINTS: Array<{ endpoint: string; scopes: Scope[] }> = [
|
|
|
347
348
|
{ endpoint: "config/embeddings:GET", scopes: ["settings.read"] },
|
|
348
349
|
{ endpoint: "config/embeddings:PUT", scopes: ["settings.write"] },
|
|
349
350
|
|
|
351
|
+
// Permissions config
|
|
352
|
+
{ endpoint: "config/permissions/skip:GET", scopes: ["settings.read"] },
|
|
353
|
+
{ endpoint: "config/permissions/skip:PUT", scopes: ["settings.write"] },
|
|
354
|
+
|
|
350
355
|
// Conversation management
|
|
351
356
|
{ endpoint: "conversations:DELETE", scopes: ["chat.write"] },
|
|
352
357
|
{ endpoint: "conversations/wipe", scopes: ["chat.write"] },
|
|
@@ -355,6 +360,9 @@ const ACTOR_ENDPOINTS: Array<{ endpoint: string; scopes: Scope[] }> = [
|
|
|
355
360
|
// Conversation search
|
|
356
361
|
{ endpoint: "conversations/search", scopes: ["chat.read"] },
|
|
357
362
|
|
|
363
|
+
// Conversation starters
|
|
364
|
+
{ endpoint: "conversation-starters", scopes: ["chat.read"] },
|
|
365
|
+
|
|
358
366
|
// Message content
|
|
359
367
|
{ endpoint: "messages/content", scopes: ["chat.read"] },
|
|
360
368
|
{ endpoint: "messages/llm-context", scopes: ["chat.read"] },
|
|
@@ -498,3 +506,9 @@ for (const endpoint of INTERNAL_ENDPOINTS) {
|
|
|
498
506
|
allowedPrincipalTypes: ["svc_gateway"],
|
|
499
507
|
});
|
|
500
508
|
}
|
|
509
|
+
|
|
510
|
+
// Admin control-plane endpoints: gateway-only
|
|
511
|
+
registerPolicy("admin/upgrade-broadcast", {
|
|
512
|
+
requiredScopes: ["internal.write"],
|
|
513
|
+
allowedPrincipalTypes: ["svc_gateway"],
|
|
514
|
+
});
|
|
@@ -28,6 +28,22 @@ import type { ScopeProfile, TokenAudience, TokenClaims } from "./types.js";
|
|
|
28
28
|
|
|
29
29
|
const log = getLogger("token-service");
|
|
30
30
|
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Bootstrap sentinel error
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Thrown when the gateway's signing-key bootstrap endpoint returns 403,
|
|
37
|
+
* indicating that bootstrap has already completed (daemon restart case).
|
|
38
|
+
* The caller should fall back to loading the key from disk.
|
|
39
|
+
*/
|
|
40
|
+
export class BootstrapAlreadyCompleted extends Error {
|
|
41
|
+
constructor() {
|
|
42
|
+
super("Gateway signing key bootstrap already completed");
|
|
43
|
+
this.name = "BootstrapAlreadyCompleted";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
31
47
|
// ---------------------------------------------------------------------------
|
|
32
48
|
// Signing key management
|
|
33
49
|
// ---------------------------------------------------------------------------
|
|
@@ -78,6 +94,123 @@ export function loadOrCreateSigningKey(): Buffer {
|
|
|
78
94
|
return newKey;
|
|
79
95
|
}
|
|
80
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Fetch the shared signing key from the gateway's bootstrap endpoint.
|
|
99
|
+
*
|
|
100
|
+
* Used in Docker mode where the gateway owns the signing key and the daemon
|
|
101
|
+
* must fetch it at startup. Retries up to 30 times with 1s intervals to
|
|
102
|
+
* tolerate gateway startup delays.
|
|
103
|
+
*
|
|
104
|
+
* @returns A 32-byte Buffer containing the signing key.
|
|
105
|
+
* @throws {BootstrapAlreadyCompleted} If the gateway returns 403 (bootstrap
|
|
106
|
+
* already completed — daemon restart case). Caller should fall back to
|
|
107
|
+
* loading the key from disk.
|
|
108
|
+
* @throws {Error} If the gateway is unreachable after all retry attempts.
|
|
109
|
+
*/
|
|
110
|
+
export async function fetchSigningKeyFromGateway(): Promise<Buffer> {
|
|
111
|
+
const gatewayUrl = process.env.GATEWAY_INTERNAL_URL;
|
|
112
|
+
if (!gatewayUrl) {
|
|
113
|
+
throw new Error("GATEWAY_INTERNAL_URL not set — cannot fetch signing key");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const maxAttempts = 30;
|
|
117
|
+
const intervalMs = 1000;
|
|
118
|
+
|
|
119
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
120
|
+
let resp: Response | undefined;
|
|
121
|
+
try {
|
|
122
|
+
resp = await fetch(`${gatewayUrl}/internal/signing-key-bootstrap`, {
|
|
123
|
+
signal: AbortSignal.timeout(5000),
|
|
124
|
+
});
|
|
125
|
+
} catch (err) {
|
|
126
|
+
log.warn(
|
|
127
|
+
{ err, attempt },
|
|
128
|
+
"Signing key bootstrap: connection failed, retrying",
|
|
129
|
+
);
|
|
130
|
+
await Bun.sleep(intervalMs);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (resp.ok) {
|
|
135
|
+
const body = (await resp.json()) as { key: string };
|
|
136
|
+
const keyBuf = Buffer.from(body.key, "hex");
|
|
137
|
+
if (keyBuf.length !== 32) {
|
|
138
|
+
throw new Error(`Invalid signing key length: ${keyBuf.length}`);
|
|
139
|
+
}
|
|
140
|
+
log.info("Signing key fetched from gateway bootstrap endpoint");
|
|
141
|
+
return keyBuf;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (resp.status === 403) {
|
|
145
|
+
// Bootstrap already completed — fall through to file-based load.
|
|
146
|
+
// This happens on daemon restart when the gateway lockfile persists.
|
|
147
|
+
log.info(
|
|
148
|
+
"Gateway signing key bootstrap already completed — loading from disk",
|
|
149
|
+
);
|
|
150
|
+
throw new BootstrapAlreadyCompleted();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
log.warn(
|
|
154
|
+
{ status: resp.status, attempt },
|
|
155
|
+
"Signing key bootstrap: gateway not ready, retrying",
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
await Bun.sleep(intervalMs);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
throw new Error("Signing key bootstrap: timed out waiting for gateway");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Persist a signing key to disk using an atomic-write pattern.
|
|
166
|
+
* Used after fetching the key from the gateway so daemon restarts can
|
|
167
|
+
* load it from disk when the gateway returns 403.
|
|
168
|
+
*/
|
|
169
|
+
function persistSigningKey(key: Buffer): void {
|
|
170
|
+
const keyPath = getSigningKeyPath();
|
|
171
|
+
const dir = dirname(keyPath);
|
|
172
|
+
if (!existsSync(dir)) {
|
|
173
|
+
mkdirSync(dir, { recursive: true });
|
|
174
|
+
}
|
|
175
|
+
const tmpPath = keyPath + ".tmp." + process.pid;
|
|
176
|
+
writeFileSync(tmpPath, key, { mode: 0o600 });
|
|
177
|
+
renameSync(tmpPath, keyPath);
|
|
178
|
+
chmodSync(keyPath, 0o600);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Resolve the signing key for the current environment.
|
|
183
|
+
*
|
|
184
|
+
* In Docker mode (IS_CONTAINERIZED=true + GATEWAY_INTERNAL_URL set), fetches
|
|
185
|
+
* the key from the gateway's bootstrap endpoint and persists it locally for
|
|
186
|
+
* restart resilience. On daemon restart (gateway returns 403), falls back to
|
|
187
|
+
* loading the key from disk.
|
|
188
|
+
*
|
|
189
|
+
* In local mode, delegates to the existing file-based loadOrCreateSigningKey().
|
|
190
|
+
*/
|
|
191
|
+
export async function resolveSigningKey(): Promise<Buffer> {
|
|
192
|
+
const isContainerized = process.env.IS_CONTAINERIZED === "true";
|
|
193
|
+
const gatewayUrl = process.env.GATEWAY_INTERNAL_URL;
|
|
194
|
+
|
|
195
|
+
if (isContainerized && gatewayUrl) {
|
|
196
|
+
try {
|
|
197
|
+
const key = await fetchSigningKeyFromGateway();
|
|
198
|
+
// Persist locally so daemon restarts (where gateway returns 403) load from disk.
|
|
199
|
+
persistSigningKey(key);
|
|
200
|
+
return key;
|
|
201
|
+
} catch (err) {
|
|
202
|
+
if (err instanceof BootstrapAlreadyCompleted) {
|
|
203
|
+
// Gateway already bootstrapped (daemon restart) — load from disk.
|
|
204
|
+
return loadOrCreateSigningKey();
|
|
205
|
+
}
|
|
206
|
+
throw err;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Local mode: use file-based load/create (unchanged behavior).
|
|
211
|
+
return loadOrCreateSigningKey();
|
|
212
|
+
}
|
|
213
|
+
|
|
81
214
|
function getSigningKey(): Buffer {
|
|
82
215
|
if (!_authSigningKey) {
|
|
83
216
|
if (process.env.NODE_ENV === "test") {
|
|
@@ -172,6 +172,7 @@ import { surfaceContentRouteDefinitions } from "./routes/surface-content-routes.
|
|
|
172
172
|
import { telemetryRouteDefinitions } from "./routes/telemetry-routes.js";
|
|
173
173
|
import { traceEventRouteDefinitions } from "./routes/trace-event-routes.js";
|
|
174
174
|
import { trustRulesRouteDefinitions } from "./routes/trust-rules-routes.js";
|
|
175
|
+
import { upgradeBroadcastRouteDefinitions } from "./routes/upgrade-broadcast-routes.js";
|
|
175
176
|
import { usageRouteDefinitions } from "./routes/usage-routes.js";
|
|
176
177
|
import { watchRouteDefinitions } from "./routes/watch-routes.js";
|
|
177
178
|
import { workItemRouteDefinitions } from "./routes/work-items-routes.js";
|
|
@@ -918,6 +919,7 @@ export class RuntimeHttpServer {
|
|
|
918
919
|
getCesClient: this.getCesClient,
|
|
919
920
|
}),
|
|
920
921
|
...identityRouteDefinitions(),
|
|
922
|
+
...upgradeBroadcastRouteDefinitions(),
|
|
921
923
|
...debugRouteDefinitions(),
|
|
922
924
|
...usageRouteDefinitions(),
|
|
923
925
|
...telemetryRouteDefinitions(),
|