@north-light/api 0.3.561 → 0.3.563
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/dist/api/client.d.ts +27 -4
- package/dist/api/client.js +53 -0
- package/dist/api/contract.d.ts +1 -1
- package/dist/api/contract.js +1 -1
- package/dist/api/dto/bash-jobs.d.ts +5 -1
- package/dist/api/dto/broker-ops.d.ts +25 -1
- package/dist/api/dto/charges.d.ts +9 -2
- package/dist/api/dto/delivery.d.ts +26 -2
- package/dist/api/dto/execution-start.d.ts +57 -0
- package/dist/api/dto/execution-start.js +2 -0
- package/dist/api/dto/modelauth.d.ts +5 -0
- package/dist/api/dto/node-records.d.ts +16 -0
- package/dist/api/dto/objects.d.ts +1 -1
- package/dist/api/dto/reports.d.ts +0 -3
- package/dist/api/dto/run-events.d.ts +7 -1
- package/dist/api/dto/settings.d.ts +55 -0
- package/dist/api/dto/settings.js +4 -0
- package/dist/api/index.d.ts +2 -0
- package/dist/api/index.js +2 -0
- package/dist/api/routes.d.ts +11 -0
- package/dist/api/routes.js +12 -0
- package/dist/core/canvas/paths.d.ts +0 -1
- package/dist/core/canvas/paths.js +0 -3
- package/dist/core/canvas/person-state.d.ts +26 -0
- package/dist/core/canvas/person-state.js +41 -0
- package/dist/core/command-plugins/bundle.d.ts +1 -1
- package/dist/core/command-plugins/bundle.js +5 -5
- package/dist/core/config.d.ts +16 -15
- package/dist/core/config.js +99 -76
- package/dist/core/conversation-store/client.d.ts +2 -0
- package/dist/core/conversation-store/client.js +111 -0
- package/dist/core/conversation-store/index.d.ts +96 -0
- package/dist/core/conversation-store/index.js +62 -0
- package/dist/core/conversation-store/patch.d.ts +3 -0
- package/dist/core/conversation-store/patch.js +192 -0
- package/dist/core/conversation-store/repair.d.ts +45 -0
- package/dist/core/conversation-store/repair.js +84 -0
- package/dist/core/env-name.d.ts +1 -0
- package/dist/core/env-name.js +16 -0
- package/dist/core/events/emit.js +7 -5
- package/dist/core/git.d.ts +28 -0
- package/dist/core/git.js +103 -0
- package/dist/core/installed-plugins.js +12 -7
- package/dist/core/managed-providers.d.ts +72 -0
- package/dist/core/managed-providers.js +61 -0
- package/dist/core/plugin-env.js +1 -2
- package/dist/core/profiles/manifest.d.ts +14 -18
- package/dist/core/profiles/manifest.js +79 -98
- package/dist/core/runtime/broker/attachments-fetch.d.ts +5 -0
- package/dist/core/runtime/broker/attachments-fetch.js +20 -0
- package/dist/core/runtime/broker/daemon-ops.d.ts +10 -1
- package/dist/core/runtime/broker/daemon-ops.js +41 -3
- package/dist/core/runtime/broker/node-token.d.ts +3 -0
- package/dist/core/runtime/broker/node-token.js +16 -0
- package/dist/core/runtime/broker/signal-stream.js +2 -2
- package/dist/core/scoped-state/materialized-plugins.d.ts +18 -0
- package/dist/core/scoped-state/materialized-plugins.js +103 -0
- package/dist/core/scoped-state/person-store.d.ts +72 -0
- package/dist/core/scoped-state/person-store.js +112 -0
- package/dist/core/user-settings.js +1 -1
- package/dist/daemon/broker-signals/bus.d.ts +48 -11
- package/dist/daemon/broker-signals/bus.js +116 -46
- package/dist/shared/inbox-entry-body.js +2 -2
- package/dist/types.d.ts +12 -0
- package/package.json +1 -1
- package/dist/core/canvas/legacy-person.d.ts +0 -4
- package/dist/core/canvas/legacy-person.js +0 -4
- package/dist/core/locked-file.d.ts +0 -27
- package/dist/core/locked-file.js +0 -121
- package/dist/core/profiles/env-store.d.ts +0 -22
- package/dist/core/profiles/env-store.js +0 -166
- package/dist/core/scoped-state/db.d.ts +0 -17
- package/dist/core/scoped-state/db.js +0 -261
- package/dist/core/scoped-state/migrate.d.ts +0 -8
- package/dist/core/scoped-state/migrate.js +0 -187
- package/dist/core/scoped-state/profiles.d.ts +0 -27
- package/dist/core/scoped-state/profiles.js +0 -94
- package/dist/core/scoped-state/providers.d.ts +0 -24
- package/dist/core/scoped-state/providers.js +0 -19
- package/dist/core/scoped-state/schema.d.ts +0 -10
- package/dist/core/scoped-state/schema.js +0 -47
- package/dist/core/scoped-state/settings.d.ts +0 -28
- package/dist/core/scoped-state/settings.js +0 -104
- package/dist/core/sqlite-statements.d.ts +0 -4
- package/dist/core/sqlite-statements.js +0 -17
- package/dist/core/subscription-state.d.ts +0 -129
- package/dist/core/subscription-state.js +0 -299
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
// Profile definitions live in scoped state; memory and environment have separate roots.
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { existsSync, realpathSync, rmSync, statSync } from 'node:fs';
|
|
4
|
-
import {
|
|
4
|
+
import { resolve as resolvePath } from 'node:path';
|
|
5
5
|
import { SCHEMA_VERSION } from '../../types.js';
|
|
6
6
|
import { profileSpace } from '../layout.js';
|
|
7
7
|
import { PROFILE_PROJECT_MEMORY_VALUES } from '../../api/dto/profiles.js';
|
|
8
|
-
import { userScopeRoot } from '../scope.js';
|
|
9
8
|
import { ensureDir, nowIso } from '../fs-utils.js';
|
|
10
|
-
import {
|
|
9
|
+
import { cachedPausedProfileIds, cachedProfilePaused, getCachedProfile as getStoredProfile, listCachedProfiles, writeSettings } from '../scoped-state/person-store.js';
|
|
11
10
|
import { usage, notFound, ambiguous, general } from '../errors.js';
|
|
12
|
-
import { withExclusiveDirectoryLock } from '../exclusive-lock.js';
|
|
13
11
|
import { fuzzyMatchProfile } from './fuzzy-match.js';
|
|
14
12
|
// A generated id and its stored app determine the memory and environment paths.
|
|
15
13
|
/** `<slug>-<shortid>`: slug is lower-case, non-alphanumeric collapsed to `-`,
|
|
@@ -33,7 +31,7 @@ function assertSafeProfileId(profileId) {
|
|
|
33
31
|
}
|
|
34
32
|
export function profileDir(profileId) {
|
|
35
33
|
assertSafeProfileId(profileId);
|
|
36
|
-
const stored = getStoredProfile(
|
|
34
|
+
const stored = getStoredProfile(profileId);
|
|
37
35
|
if (stored === null)
|
|
38
36
|
throw notFound(`profile not found: ${profileId}`, { received: profileId });
|
|
39
37
|
return profileSpace(profileApp(stored.manifest), profileId).dir;
|
|
@@ -61,7 +59,7 @@ export function assertDeliveryLimits(value) {
|
|
|
61
59
|
throw usage(`invalid delivery limit for ${handle}: ${JSON.stringify(level)}`, { received: String(level), field: 'delivery_limits', next: `Set ${handle} to one of: ${levels}.` });
|
|
62
60
|
}
|
|
63
61
|
}
|
|
64
|
-
function enumerateProfiles() { return { entries:
|
|
62
|
+
function enumerateProfiles() { return { entries: listCachedProfiles(), unreadableIds: [] }; }
|
|
65
63
|
export function listProfiles() { return enumerateProfiles().entries; }
|
|
66
64
|
export function lookupProfile(profileIdOrName) { const entries = listProfiles(); const exact = findExactProfile(entries, profileIdOrName, entryCandidate); return exact ? { kind: 'resolved', entry: exact } : { kind: 'absent' }; }
|
|
67
65
|
// Three resolvers, in descending tolerance. `resolveProfileOperand` is what a
|
|
@@ -75,7 +73,7 @@ export function lookupProfile(profileIdOrName) { const entries = listProfiles();
|
|
|
75
73
|
* Used at persistence gates that already hold a resolved durable identity. */
|
|
76
74
|
export function loadExactProfileManifest(profileId) {
|
|
77
75
|
assertSafeProfileId(profileId);
|
|
78
|
-
const stored = getStoredProfile(
|
|
76
|
+
const stored = getStoredProfile(profileId);
|
|
79
77
|
if (stored !== null)
|
|
80
78
|
return stored;
|
|
81
79
|
throw notFound(`profile not found: ${profileId}`, { received: profileId, next: 'Run `crtr profile list` to see available profiles.' });
|
|
@@ -159,19 +157,6 @@ export function matchProfileOperand(all, operand, key) {
|
|
|
159
157
|
}
|
|
160
158
|
throw profileNotFound(all, operand, key);
|
|
161
159
|
}
|
|
162
|
-
function locksDir() { return join(userScopeRoot(), 'profile-locks'); }
|
|
163
|
-
/** Hold the per-profile manifest lock for the duration of `fn`. ALL manifest
|
|
164
|
-
* mutations (create/rename/add-project/remove-project/delete/last-used)
|
|
165
|
-
* below run inside this. */
|
|
166
|
-
export function withProfileManifestLock(profileId, fn) {
|
|
167
|
-
assertSafeProfileId(profileId);
|
|
168
|
-
ensureDir(locksDir());
|
|
169
|
-
return withExclusiveDirectoryLock(join(locksDir(), `${profileId}.lock`), fn, {
|
|
170
|
-
timeoutError: () => general(`timed out waiting for the profile lock: ${profileId}`, {
|
|
171
|
-
next: 'Retry — another `crtr profile` command may be running concurrently on this profile.',
|
|
172
|
-
}),
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
160
|
// Project dirs — resolved to absolute real paths, must exist + be a directory
|
|
176
161
|
// on add/create, deduped by real path, stored in manifest order.
|
|
177
162
|
function resolveExistingProjectDir(dir) {
|
|
@@ -212,32 +197,30 @@ function generateProfileId(name) {
|
|
|
212
197
|
for (let attempt = 0; attempt < 20; attempt++) {
|
|
213
198
|
const shortId = randomUUID().replace(/-/g, '').slice(0, 8);
|
|
214
199
|
const candidate = `${slug}-${shortId}`;
|
|
215
|
-
if (getStoredProfile(
|
|
200
|
+
if (getStoredProfile(candidate) === null)
|
|
216
201
|
return candidate;
|
|
217
202
|
}
|
|
218
203
|
throw general(`failed to generate a unique profile id for "${name}" after 20 attempts`);
|
|
219
204
|
}
|
|
220
205
|
// Mutations — all under withProfileManifestLock, all atomic writes.
|
|
221
|
-
export function ensureRootProfile() {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
return { profileId: ROOT_PROFILE_ID, manifest: next };
|
|
240
|
-
});
|
|
206
|
+
export async function ensureRootProfile() {
|
|
207
|
+
const existing = getStoredProfile(ROOT_PROFILE_ID);
|
|
208
|
+
if (existing !== null) {
|
|
209
|
+
scaffoldProfile(ROOT_PROFILE_ID, existing.manifest);
|
|
210
|
+
return existing;
|
|
211
|
+
}
|
|
212
|
+
const next = {
|
|
213
|
+
schema_version: SCHEMA_VERSION,
|
|
214
|
+
name: ROOT_PROFILE_NAME,
|
|
215
|
+
projects: [],
|
|
216
|
+
grantee: 'app:terminal',
|
|
217
|
+
paused_at: null,
|
|
218
|
+
created_at: nowIso(),
|
|
219
|
+
last_used_at: nowIso(),
|
|
220
|
+
};
|
|
221
|
+
scaffoldProfile(ROOT_PROFILE_ID, next);
|
|
222
|
+
await writeSettings((snapshot) => snapshot.profiles.some((p) => p.profileId === ROOT_PROFILE_ID) ? [] : [{ op: 'profile.insert', profile: { profileId: ROOT_PROFILE_ID, manifest: next } }]);
|
|
223
|
+
return getStoredProfile(ROOT_PROFILE_ID);
|
|
241
224
|
}
|
|
242
225
|
/** Key shape every stored metadata entry must satisfy — kept env-mappable
|
|
243
226
|
* (`CRTR_PROFILE_META_<KEY>`, see `core/runtime/spawn-env.ts`) by
|
|
@@ -316,7 +299,7 @@ export function sanitizeProfileMetadata(stored) {
|
|
|
316
299
|
}
|
|
317
300
|
return out;
|
|
318
301
|
}
|
|
319
|
-
export function createProfile(name, projects = [], opts = {}) {
|
|
302
|
+
export async function createProfile(name, projects = [], opts = {}) {
|
|
320
303
|
const trimmed = name.trim();
|
|
321
304
|
if (trimmed === '')
|
|
322
305
|
throw usage('profile name must not be empty');
|
|
@@ -325,24 +308,22 @@ export function createProfile(name, projects = [], opts = {}) {
|
|
|
325
308
|
assertProfileProjects(projects);
|
|
326
309
|
const resolvedProjects = dedupeProjects(projects.map((p) => ({ path: resolveExistingProjectDir(p.path) })));
|
|
327
310
|
const profileId = generateProfileId(trimmed);
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
345
|
-
export function ensureAppProfile(grantee) {
|
|
311
|
+
const manifest = {
|
|
312
|
+
schema_version: SCHEMA_VERSION,
|
|
313
|
+
name: trimmed,
|
|
314
|
+
projects: resolvedProjects,
|
|
315
|
+
paused_at: null,
|
|
316
|
+
...(opts.grantee === undefined ? {} : { grantee: opts.grantee }),
|
|
317
|
+
...(opts.defaultKind !== undefined ? { default_kind: opts.defaultKind } : {}),
|
|
318
|
+
...(opts.metadata !== undefined && Object.keys(opts.metadata).length > 0 ? { metadata: { ...opts.metadata } } : {}),
|
|
319
|
+
created_at: nowIso(),
|
|
320
|
+
last_used_at: null,
|
|
321
|
+
};
|
|
322
|
+
scaffoldProfile(profileId, manifest);
|
|
323
|
+
await writeSettings(() => [{ op: 'profile.insert', profile: { profileId, manifest } }]);
|
|
324
|
+
return { profileId, manifest };
|
|
325
|
+
}
|
|
326
|
+
export async function ensureAppProfile(grantee) {
|
|
346
327
|
if (!grantee.startsWith('app:') || grantee.length <= 4)
|
|
347
328
|
throw usage('an app grantee is required');
|
|
348
329
|
const name = `app-${grantee.slice(4)}`;
|
|
@@ -351,24 +332,27 @@ export function ensureAppProfile(grantee) {
|
|
|
351
332
|
throw new Error(`duplicate default profile for ${grantee}`);
|
|
352
333
|
if (existing[0])
|
|
353
334
|
return existing[0];
|
|
354
|
-
return createProfile(name, [], { grantee });
|
|
335
|
+
return await createProfile(name, [], { grantee });
|
|
355
336
|
}
|
|
356
337
|
function requireExistingProfile(profileId) {
|
|
357
338
|
assertSafeProfileId(profileId);
|
|
358
|
-
if (getStoredProfile(
|
|
339
|
+
if (getStoredProfile(profileId) === null)
|
|
359
340
|
throw notFound(`profile not found: ${profileId}`, { received: profileId, next: 'Run `crtr profile list` to see available profiles.' });
|
|
360
341
|
}
|
|
361
|
-
function mutateManifest(profileId, mutate) {
|
|
342
|
+
async function mutateManifest(profileId, mutate) {
|
|
362
343
|
requireExistingProfile(profileId);
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
344
|
+
let result;
|
|
345
|
+
await writeSettings((snapshot) => {
|
|
346
|
+
const current = snapshot.profiles.find((p) => p.profileId === profileId);
|
|
347
|
+
if (current === undefined)
|
|
366
348
|
throw notFound(`profile not found: ${profileId}`, { received: profileId });
|
|
367
|
-
|
|
349
|
+
result = { profileId, manifest: mutate(structuredClone(current.manifest)) };
|
|
350
|
+
return [{ op: 'profile.update', profile: result }];
|
|
368
351
|
});
|
|
352
|
+
return result;
|
|
369
353
|
}
|
|
370
|
-
export function updateProfileLastUsed(profileId) {
|
|
371
|
-
return mutateManifest(profileId, (m) => ({ ...m, last_used_at: nowIso() }));
|
|
354
|
+
export async function updateProfileLastUsed(profileId) {
|
|
355
|
+
return await mutateManifest(profileId, (m) => ({ ...m, last_used_at: nowIso() }));
|
|
372
356
|
}
|
|
373
357
|
/** Resolve an existing profile for a launch gate without making stale durable
|
|
374
358
|
* profile ids a new failure mode on a hot path. */
|
|
@@ -390,17 +374,13 @@ function loadProfileManifestOrNull(profileId) {
|
|
|
390
374
|
export function isProfilePaused(profileId) {
|
|
391
375
|
if (profileId === null || profileId === undefined || profileId === '')
|
|
392
376
|
return false;
|
|
393
|
-
|
|
394
|
-
assertSafeProfileId(profileId);
|
|
395
|
-
return profilePaused(userScopeRoot(), profileId);
|
|
396
|
-
}
|
|
397
|
-
catch {
|
|
377
|
+
if (!ID_SHAPE.test(profileId))
|
|
398
378
|
return false;
|
|
399
|
-
|
|
379
|
+
return cachedProfilePaused(profileId);
|
|
400
380
|
}
|
|
401
381
|
/** One set-based pause read for canvas-wide projections. */
|
|
402
382
|
export function pausedCanvasProfiles() {
|
|
403
|
-
return
|
|
383
|
+
return cachedPausedProfileIds();
|
|
404
384
|
}
|
|
405
385
|
/** Refuse a launch under a paused profile, naming the resume command. Shared by
|
|
406
386
|
* every gate that already holds a loaded manifest. */
|
|
@@ -419,25 +399,25 @@ export function assertProfileActive(profileId) {
|
|
|
419
399
|
assertProfileEntryActive(entry);
|
|
420
400
|
return entry;
|
|
421
401
|
}
|
|
422
|
-
export function pauseProfile(profileId) {
|
|
423
|
-
return mutateManifest(profileId, (m) => ({ ...m, paused_at: m.paused_at ?? nowIso() }));
|
|
402
|
+
export async function pauseProfile(profileId) {
|
|
403
|
+
return await mutateManifest(profileId, (m) => ({ ...m, paused_at: m.paused_at ?? nowIso() }));
|
|
424
404
|
}
|
|
425
|
-
export function resumeProfile(profileId) {
|
|
405
|
+
export async function resumeProfile(profileId) {
|
|
426
406
|
let pausedAt = null;
|
|
427
|
-
const entry = mutateManifest(profileId, (m) => {
|
|
407
|
+
const entry = await mutateManifest(profileId, (m) => {
|
|
428
408
|
pausedAt = m.paused_at;
|
|
429
409
|
return { ...m, paused_at: null };
|
|
430
410
|
});
|
|
431
411
|
return { ...entry, pausedAt };
|
|
432
412
|
}
|
|
433
|
-
export function setProfileDefaultKind(profileId, defaultKind) {
|
|
434
|
-
return mutateManifest(profileId, (m) => ({ ...m, default_kind: defaultKind }));
|
|
413
|
+
export async function setProfileDefaultKind(profileId, defaultKind) {
|
|
414
|
+
return await mutateManifest(profileId, (m) => ({ ...m, default_kind: defaultKind }));
|
|
435
415
|
}
|
|
436
416
|
/** Merge `set` entries over the stored map and drop `unset` keys; the
|
|
437
417
|
* `metadata` field is omitted entirely when the result is empty. */
|
|
438
|
-
export function updateProfileMetadata(profileId, set, unset = []) {
|
|
418
|
+
export async function updateProfileMetadata(profileId, set, unset = []) {
|
|
439
419
|
assertProfileMetadata(set);
|
|
440
|
-
return mutateManifest(profileId, (m) => {
|
|
420
|
+
return await mutateManifest(profileId, (m) => {
|
|
441
421
|
const next = { ...sanitizeProfileMetadata(m.metadata), ...set };
|
|
442
422
|
for (const key of unset)
|
|
443
423
|
delete next[key];
|
|
@@ -467,32 +447,32 @@ export function readProfileMetadata(profileId) {
|
|
|
467
447
|
/** Change the manifest `name` only. Callers that own the canvas (the daemon's
|
|
468
448
|
* `PATCH /v1/profiles/:name`) must also rename the profile's canvas handle —
|
|
469
449
|
* use `renameProfileAndHandle` there; the CLI and panels go through that route. */
|
|
470
|
-
export function renameProfile(profileId, name) {
|
|
450
|
+
export async function renameProfile(profileId, name) {
|
|
471
451
|
const trimmed = name.trim();
|
|
472
452
|
if (trimmed === '')
|
|
473
453
|
throw usage('profile name must not be empty');
|
|
474
|
-
const result = mutateManifest(profileId, (m) => ({ ...m, name: trimmed }));
|
|
454
|
+
const result = await mutateManifest(profileId, (m) => ({ ...m, name: trimmed }));
|
|
475
455
|
return result;
|
|
476
456
|
}
|
|
477
457
|
/** A new path appends; a path already listed keeps its position. */
|
|
478
|
-
export function addProfileProject(profileId, dir) {
|
|
458
|
+
export async function addProfileProject(profileId, dir) {
|
|
479
459
|
assertProfileProjects([{ path: dir }]);
|
|
480
460
|
const resolved = resolveExistingProjectDir(dir);
|
|
481
|
-
const result = mutateManifest(profileId, (m) => ({ ...m, projects: dedupeProjects([...m.projects, { path: resolved }]) }));
|
|
461
|
+
const result = await mutateManifest(profileId, (m) => ({ ...m, projects: dedupeProjects([...m.projects, { path: resolved }]) }));
|
|
482
462
|
return result;
|
|
483
463
|
}
|
|
484
464
|
/** Set one owner's automatic delivery limit. `content` is the uncapped level,
|
|
485
465
|
* so it removes the owner's entry instead of storing it. */
|
|
486
|
-
export function setProfileDeliveryLimit(profileId, owner, level) {
|
|
466
|
+
export async function setProfileDeliveryLimit(profileId, owner, level) {
|
|
487
467
|
assertDeliveryLimits({ [owner]: level });
|
|
488
|
-
return mutateManifest(profileId, (m) => {
|
|
468
|
+
return await mutateManifest(profileId, (m) => {
|
|
489
469
|
const { [owner]: _previous, ...rest } = m.delivery_limits ?? {};
|
|
490
470
|
const limits = level === 'content' ? rest : { ...rest, [owner]: level };
|
|
491
471
|
const { delivery_limits: _old, ...base } = m;
|
|
492
472
|
return Object.keys(limits).length === 0 ? base : { ...base, delivery_limits: limits };
|
|
493
473
|
});
|
|
494
474
|
}
|
|
495
|
-
export function removeProfileProject(profileId, dir) {
|
|
475
|
+
export async function removeProfileProject(profileId, dir) {
|
|
496
476
|
// Lenient resolution: a project dir may have been deleted since it was
|
|
497
477
|
// added, so removal must not require it to still exist on disk. Compare
|
|
498
478
|
// against both its real path (if still resolvable) and its plain resolved
|
|
@@ -505,7 +485,7 @@ export function removeProfileProject(profileId, dir) {
|
|
|
505
485
|
catch {
|
|
506
486
|
/* dir no longer exists — fall back to comparing by its resolved path */
|
|
507
487
|
}
|
|
508
|
-
const result = mutateManifest(profileId, (m) => {
|
|
488
|
+
const result = await mutateManifest(profileId, (m) => {
|
|
509
489
|
const filtered = m.projects.filter((p) => p.path !== real && p.path !== abs);
|
|
510
490
|
if (filtered.length === m.projects.length) {
|
|
511
491
|
throw notFound(`project not found on profile: ${dir}`, {
|
|
@@ -517,13 +497,14 @@ export function removeProfileProject(profileId, dir) {
|
|
|
517
497
|
});
|
|
518
498
|
return result;
|
|
519
499
|
}
|
|
520
|
-
export function deleteProfile(profileId) {
|
|
500
|
+
export async function deleteProfile(profileId) {
|
|
521
501
|
requireExistingProfile(profileId);
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
if (!
|
|
502
|
+
const space = profileSpace(profileApp(loadExactProfileManifest(profileId).manifest), profileId);
|
|
503
|
+
await writeSettings((snapshot) => {
|
|
504
|
+
if (!snapshot.profiles.some((p) => p.profileId === profileId))
|
|
525
505
|
throw notFound(`profile not found: ${profileId}`, { received: profileId });
|
|
526
|
-
|
|
527
|
-
rmSync(space.env, { force: true });
|
|
506
|
+
return [{ op: 'profile.delete', profileId }];
|
|
528
507
|
});
|
|
508
|
+
rmSync(space.dir, { recursive: true, force: true });
|
|
509
|
+
rmSync(space.env, { force: true });
|
|
529
510
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Download every attachment of this node's run that is not already on the broker's disk (D98). The control plane puts
|
|
2
|
+
* each fetched upload in the blob store and records it on the run; the files are written where the run's prompt names
|
|
3
|
+
* them. Run before the broker hands the agent a prompt or mail. A failed download throws: the agent must not be told
|
|
4
|
+
* of a path that holds nothing. */
|
|
5
|
+
export declare function fetchRunAttachments(nodeId: string): Promise<void>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { daemonClient } from './daemon-ops.js';
|
|
4
|
+
/** Download every attachment of this node's run that is not already on the broker's disk (D98). The control plane puts
|
|
5
|
+
* each fetched upload in the blob store and records it on the run; the files are written where the run's prompt names
|
|
6
|
+
* them. Run before the broker hands the agent a prompt or mail. A failed download throws: the agent must not be told
|
|
7
|
+
* of a path that holds nothing. */
|
|
8
|
+
export async function fetchRunAttachments(nodeId) {
|
|
9
|
+
const client = daemonClient();
|
|
10
|
+
const { attachments } = await client.listNodeAttachments(nodeId);
|
|
11
|
+
for (const file of attachments) {
|
|
12
|
+
if (existsSync(file.path))
|
|
13
|
+
continue;
|
|
14
|
+
const bytes = await client.readNodeAttachment(nodeId, file.sha256);
|
|
15
|
+
mkdirSync(dirname(file.path), { recursive: true });
|
|
16
|
+
const partial = `${file.path}.partial`;
|
|
17
|
+
writeFileSync(partial, bytes);
|
|
18
|
+
renameSync(partial, file.path);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -1,11 +1,16 @@
|
|
|
1
|
+
import { CrtrClient } from '../../../api/client.js';
|
|
1
2
|
import { type BrokerSignalCachedNode } from '../../../daemon/broker-signals/bus.js';
|
|
2
3
|
import type { BrokerExtensionStateDTO, BrokerGeneratedNameRequest, BrokerGeneratedNameResultDTO, BrokerModelCommitRequest, BrokerModelCommitResultDTO, BrokerParkActivityResultDTO, BrokerParkCompleteRequest, BrokerPersonaAckRequest, BrokerPersonaAckResultDTO, BrokerSessionBoundRequest, BrokerSessionBoundResultDTO, BrokerSettleDirective, BrokerSettleRequest, BrokerTelemetryRequest } from '../../../api/dto/broker-ops.js';
|
|
3
4
|
import type { AcknowledgeMailResultDTO, ClaimMailResultDTO, MailReceiptDTO } from '../../../api/dto/mail.js';
|
|
4
5
|
import type { BrokerFaultInputDTO, BrokerFaultRequest, BrokerFaultResultDTO, BrokerProviderRetryRequest, BrokerProviderRetryResultDTO, BrokerTurnRequest, BrokerTurnResultDTO, RecoveryStateDTO } from '../../../api/dto/recovery.js';
|
|
5
6
|
import type { RelaunchRootResultDTO } from '../../../api/dto/lifecycle.js';
|
|
6
7
|
import type { PushReportRequest } from '../../../api/dto/reports.js';
|
|
7
|
-
import type { BackgroundedJobDTO, BackgroundedJobRequest, DeliveryRequest, DeliveryResponse } from '../../../api/dto/delivery.js';
|
|
8
|
+
import type { BackgroundedJobDTO, BackgroundedJobRequest, JobReportRequest, DeliveryRequest, DeliveryResponse } from '../../../api/dto/delivery.js';
|
|
8
9
|
import type { NodeLogAppendResultDTO, NodeMessageBodyDTO, NodePushedFinalResultDTO, NodeTelemetryRecordRequest } from '../../../api/dto/node-records.js';
|
|
10
|
+
/** Where this broker reaches `/v1`: `CRTR_API_URL` when the launcher set one (an http(s) URL in a
|
|
11
|
+
* sandbox), else the local API socket. */
|
|
12
|
+
export declare function brokerApiUrl(): string | undefined;
|
|
13
|
+
export declare function daemonClient(): CrtrClient;
|
|
9
14
|
export declare function appendBrokerLog(nodeId: string, record: string): Promise<NodeLogAppendResultDTO>;
|
|
10
15
|
export declare function putBrokerTelemetry(nodeId: string, telemetry: Omit<NodeTelemetryRecordRequest, 'expected_execution_id'>): Promise<void>;
|
|
11
16
|
export declare function putBrokerRecap(nodeId: string, briefing: string): Promise<void>;
|
|
@@ -22,15 +27,19 @@ export declare function startBrokerBashJob(nodeId: string, request: {
|
|
|
22
27
|
}>;
|
|
23
28
|
/** Record a command the valve just backgrounded as this node's job object. */
|
|
24
29
|
export declare function recordBackgroundedJob(nodeId: string, jobId: string, request: BackgroundedJobRequest): Promise<BackgroundedJobDTO>;
|
|
30
|
+
/** Report on a job this broker runs: process identity, activity, output, exit. */
|
|
31
|
+
export declare function reportBrokerJob(nodeId: string, jobId: string, request: Omit<JobReportRequest, 'expected_execution_id'>): Promise<void>;
|
|
25
32
|
export declare function stopBrokerBashJob(nodeId: string, jobId: string): Promise<void>;
|
|
26
33
|
export declare function brokerExecutionId(): string;
|
|
27
34
|
export declare function bindBrokerSession(nodeId: string, request: Omit<BrokerSessionBoundRequest, 'executionId'>): Promise<BrokerSessionBoundResultDTO>;
|
|
28
35
|
type BrokerInput<T> = T extends unknown ? Omit<T, 'expected_execution_id'> : never;
|
|
29
36
|
export declare function settleBroker(nodeId: string, request: BrokerInput<BrokerSettleRequest>): Promise<BrokerSettleDirective>;
|
|
37
|
+
export declare function postBrokerIdle(nodeId: string): Promise<void>;
|
|
30
38
|
export declare function completeBrokerPark(nodeId: string, request: BrokerInput<BrokerParkCompleteRequest>): Promise<BrokerSettleDirective>;
|
|
31
39
|
export declare function recordBrokerParkActivity(nodeId: string): Promise<BrokerParkActivityResultDTO>;
|
|
32
40
|
export declare function recordBrokerTelemetry(nodeId: string, request: Omit<BrokerTelemetryRequest, 'expected_execution_id'>): Promise<void>;
|
|
33
41
|
export declare function claimBrokerMail(nodeId: string, channel: 'wake' | 'passive' | 'deferred_run', reportBodies: Map<string, Promise<string>>): Promise<ClaimMailResultDTO>;
|
|
42
|
+
/** Mail is settled only after the entries the turn wrote are durable: a failed flush rejects and nothing is acknowledged. */
|
|
34
43
|
export declare function acknowledgeBrokerMail(nodeId: string, deliveries: MailReceiptDTO[]): Promise<AcknowledgeMailResultDTO>;
|
|
35
44
|
export declare function recordBrokerTurn(nodeId: string, request: Omit<BrokerTurnRequest, 'expected_execution_id'>): Promise<BrokerTurnResultDTO>;
|
|
36
45
|
export declare function mutateBrokerProviderRetry(nodeId: string, request: Omit<BrokerProviderRetryRequest, 'expected_execution_id'>): Promise<BrokerProviderRetryResultDTO>;
|
|
@@ -1,14 +1,33 @@
|
|
|
1
1
|
// Broker-to-daemon operation client. A broker is launched by crtrd, so a cold
|
|
2
2
|
// daemon socket must fail loud — it must never use the CLI factory that can
|
|
3
3
|
// autostart another daemon.
|
|
4
|
+
import { CrtrClient } from '../../../api/client.js';
|
|
5
|
+
import { currentNodeToken } from './node-token.js';
|
|
4
6
|
import { localClient } from '../../../api/node-transport.js';
|
|
5
7
|
import { ApiError } from '../../../api/errors.js';
|
|
6
8
|
import { envExecutionId } from '../../../shared/env.js';
|
|
7
9
|
import { brokerSignalClient } from './signal-stream.js';
|
|
8
10
|
import { cachedBrokerNodeFields } from '../../../daemon/broker-signals/bus.js';
|
|
11
|
+
import { fetchRunAttachments } from './attachments-fetch.js';
|
|
9
12
|
import { reportRefNodeId } from '../../../shared/inbox-entry-body.js';
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
/** Where this broker reaches `/v1`: `CRTR_API_URL` when the launcher set one (an http(s) URL in a
|
|
14
|
+
* sandbox), else the local API socket. */
|
|
15
|
+
export function brokerApiUrl() {
|
|
16
|
+
const url = process.env['CRTR_API_URL'];
|
|
17
|
+
if (url === undefined || url === '')
|
|
18
|
+
return undefined;
|
|
19
|
+
if (!/^https?:\/\//.test(url))
|
|
20
|
+
throw new Error(`CRTR_API_URL must be an http(s) URL: ${url}`);
|
|
21
|
+
return url;
|
|
22
|
+
}
|
|
23
|
+
export function daemonClient() {
|
|
24
|
+
const url = brokerApiUrl();
|
|
25
|
+
if (url === undefined)
|
|
26
|
+
return localClient({ autostart: false });
|
|
27
|
+
const token = currentNodeToken();
|
|
28
|
+
if (token === undefined)
|
|
29
|
+
throw new Error('broker has no node token for CRTR_API_URL');
|
|
30
|
+
return new CrtrClient({ baseUrl: url, headers: { authorization: `Bearer ${token}` } });
|
|
12
31
|
}
|
|
13
32
|
export function appendBrokerLog(nodeId, record) {
|
|
14
33
|
return daemonClient().appendNodeLog(nodeId, { record, expected_execution_id: brokerExecutionId() });
|
|
@@ -38,6 +57,10 @@ export function startBrokerBashJob(nodeId, request) {
|
|
|
38
57
|
export function recordBackgroundedJob(nodeId, jobId, request) {
|
|
39
58
|
return daemonClient().recordBackgroundedJob(nodeId, jobId, request);
|
|
40
59
|
}
|
|
60
|
+
/** Report on a job this broker runs: process identity, activity, output, exit. */
|
|
61
|
+
export function reportBrokerJob(nodeId, jobId, request) {
|
|
62
|
+
return daemonClient().reportBashJob(nodeId, jobId, { ...request, expected_execution_id: brokerExecutionId() });
|
|
63
|
+
}
|
|
41
64
|
export function stopBrokerBashJob(nodeId, jobId) {
|
|
42
65
|
return daemonClient().request('DELETE', `/v1/nodes/${encodeURIComponent(nodeId)}/jobs/${encodeURIComponent(jobId)}?foreground=1`, {
|
|
43
66
|
expected_execution_id: brokerExecutionId(),
|
|
@@ -55,6 +78,9 @@ export function bindBrokerSession(nodeId, request) {
|
|
|
55
78
|
export function settleBroker(nodeId, request) {
|
|
56
79
|
return daemonClient().settleBroker(nodeId, { ...request, expected_execution_id: brokerExecutionId() });
|
|
57
80
|
}
|
|
81
|
+
export function postBrokerIdle(nodeId) {
|
|
82
|
+
return daemonClient().postBrokerIdle(nodeId, { expected_execution_id: brokerExecutionId() });
|
|
83
|
+
}
|
|
58
84
|
export function completeBrokerPark(nodeId, request) {
|
|
59
85
|
return daemonClient().completeBrokerPark(nodeId, { ...request, expected_execution_id: brokerExecutionId() });
|
|
60
86
|
}
|
|
@@ -64,12 +90,21 @@ export function recordBrokerParkActivity(nodeId) {
|
|
|
64
90
|
export function recordBrokerTelemetry(nodeId, request) {
|
|
65
91
|
return daemonClient().recordBrokerTelemetry(nodeId, { ...request, expected_execution_id: brokerExecutionId() });
|
|
66
92
|
}
|
|
93
|
+
/** Entries whose run attachments this broker has already downloaded. */
|
|
94
|
+
const attachmentsChecked = new Set();
|
|
67
95
|
export async function claimBrokerMail(nodeId, channel, reportBodies) {
|
|
68
96
|
const client = daemonClient();
|
|
69
97
|
const executionId = brokerExecutionId();
|
|
70
98
|
const result = await client.claimNodeMail(nodeId, { execution_id: executionId, channel });
|
|
71
99
|
if (result.action === 'hold')
|
|
72
100
|
return result;
|
|
101
|
+
// Mail the agent has not seen may name a file of this run's attachments (D98): have it on disk before the mail is handed over.
|
|
102
|
+
const fresh = result.deliveries.filter((delivery) => !attachmentsChecked.has(delivery.entry.entry_id));
|
|
103
|
+
if (fresh.length > 0) {
|
|
104
|
+
await fetchRunAttachments(nodeId);
|
|
105
|
+
for (const delivery of fresh)
|
|
106
|
+
attachmentsChecked.add(delivery.entry.entry_id);
|
|
107
|
+
}
|
|
73
108
|
const deliveries = await Promise.all(result.deliveries.map(async (delivery) => {
|
|
74
109
|
const ref = delivery.entry.ref;
|
|
75
110
|
if (ref === undefined || reportRefNodeId(ref) === undefined)
|
|
@@ -103,7 +138,10 @@ export async function claimBrokerMail(nodeId, channel, reportBodies) {
|
|
|
103
138
|
}
|
|
104
139
|
return { ...result, deliveries };
|
|
105
140
|
}
|
|
106
|
-
|
|
141
|
+
/** Mail is settled only after the entries the turn wrote are durable: a failed flush rejects and nothing is acknowledged. */
|
|
142
|
+
export async function acknowledgeBrokerMail(nodeId, deliveries) {
|
|
143
|
+
// Loaded on use: the store's index pulls in the pi engine, which a thin CLI leaf must not load.
|
|
144
|
+
await (await import('../../conversation-store/index.js')).flushConversation();
|
|
107
145
|
return daemonClient().acknowledgeNodeMail(nodeId, { execution_id: brokerExecutionId(), deliveries });
|
|
108
146
|
}
|
|
109
147
|
export function recordBrokerTurn(nodeId, request) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createReadStream } from 'node:fs';
|
|
2
|
+
let token = null;
|
|
3
|
+
/** fd 3 is a one-shot pipe from the daemon, not a file or an environment value. */
|
|
4
|
+
export async function receiveNodeToken() {
|
|
5
|
+
if (token !== null)
|
|
6
|
+
throw new Error('node token was already received');
|
|
7
|
+
const chunks = [];
|
|
8
|
+
for await (const chunk of createReadStream('', { fd: 3, autoClose: true }))
|
|
9
|
+
chunks.push(Buffer.from(chunk));
|
|
10
|
+
const received = Buffer.concat(chunks).toString('utf8');
|
|
11
|
+
if (!/^[A-Za-z0-9_-]{43}$/.test(received))
|
|
12
|
+
throw new Error('broker received an invalid node token');
|
|
13
|
+
token = received;
|
|
14
|
+
return received;
|
|
15
|
+
}
|
|
16
|
+
export function currentNodeToken() { return token ?? undefined; }
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Process-wide broker signal connection. Pi's Jiti loader evaluates extensions in
|
|
2
2
|
// another module graph, so a module-local singleton would open duplicate streams.
|
|
3
|
-
import {
|
|
3
|
+
import { daemonClient } from './daemon-ops.js';
|
|
4
4
|
import { ApiError } from '../../../api/errors.js';
|
|
5
5
|
const KEY = Symbol.for('@crouton-kit/crtr:broker-signal-client');
|
|
6
6
|
export class BrokerSignalClient {
|
|
@@ -84,7 +84,7 @@ export class BrokerSignalClient {
|
|
|
84
84
|
this.controller = controller;
|
|
85
85
|
void (async () => {
|
|
86
86
|
try {
|
|
87
|
-
for await (const line of
|
|
87
|
+
for await (const line of daemonClient().getBrokerSignals(this.nodeId, this.executionId, { signal: controller.signal, maxRetries: 0 })) {
|
|
88
88
|
if (!this.running || this.controller !== controller)
|
|
89
89
|
return;
|
|
90
90
|
this.armHeartbeat(controller);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** One enabled plugin row of the person's user scope, with its marketplace's url and ref. */
|
|
2
|
+
export interface PluginInstall {
|
|
3
|
+
name: string;
|
|
4
|
+
version: string | null;
|
|
5
|
+
sourcePath: string | null;
|
|
6
|
+
marketplace: {
|
|
7
|
+
name: string;
|
|
8
|
+
url: string | null;
|
|
9
|
+
ref: string | null;
|
|
10
|
+
} | null;
|
|
11
|
+
}
|
|
12
|
+
/** The directory holding this person's materialized plugins as `<dir>/<plugin name>`; undefined when none were materialized. */
|
|
13
|
+
export declare function materializedPluginsDir(person: string): string | undefined;
|
|
14
|
+
/** Install `installs` for `person` at `revision` into this process's plugin cache, once per (person, revision). When a plugin cannot
|
|
15
|
+
* be installed — a source on another machine, a marketplace that does not list it, a failed clone — the failure is held for that person
|
|
16
|
+
* and thrown by the next read of their plugins (`materializedPluginsDir`), so a request that never reads plugins, such as the one that
|
|
17
|
+
* fixes the settings, still works; the next load retries. */
|
|
18
|
+
export declare function materializePlugins(person: string, revision: number, installs: readonly PluginInstall[]): Promise<void>;
|