@north-light/crouter 0.3.215 → 0.3.217
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/builtin-memory/05-kinds/advisor/advice-contract.md +1 -1
- package/dist/builtin-memory/05-kinds/plan/reviewers/lens-contract.md +1 -1
- package/dist/clients/attach/viewer.js +471 -471
- package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +2 -2
- package/dist/clients/inbox/tui/input.js +12 -8
- package/dist/clients/inbox/tui/slots.d.ts +3 -1
- package/dist/clients/inbox/tui/slots.js +9 -3
- package/dist/commands/memory/shared.d.ts +1 -1
- package/dist/commands/memory/shared.js +1 -1
- package/dist/commands/profile/kind.d.ts +0 -1
- package/dist/commands/profile/kind.js +2 -14
- package/dist/commands/profile/meta.js +3 -1
- package/dist/commands/profile/new.js +3 -3
- package/dist/commands/profile/show.js +2 -2
- package/dist/core/__tests__/human-cancel-guard.test.js +30 -12
- package/dist/core/config.d.ts +5 -0
- package/dist/core/config.js +15 -0
- package/dist/core/human/__tests__/page.test.js +12 -0
- package/dist/core/human/component-docs.js +5 -1
- package/dist/core/human/page-schema.d.ts +14 -0
- package/dist/core/human/page-schema.js +24 -7
- package/dist/core/human/page.d.ts +2 -0
- package/dist/core/human/page.js +17 -6
- package/dist/core/human/tickets.js +9 -2
- package/dist/core/profiles/manifest.d.ts +13 -1
- package/dist/core/profiles/manifest.js +62 -13
- package/dist/core/runtime/spawn-env.js +2 -2
- package/dist/daemon/api/handlers/profiles.js +5 -0
- package/dist/daemon/api/map.js +2 -1
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
|
@@ -5,7 +5,7 @@ import { z } from 'zod';
|
|
|
5
5
|
import { buildPageSummary } from './summary.js';
|
|
6
6
|
import { atomicWriteJson, atomicWriteText, clearProgress, claimPath, pageManifestPath, pageModulePath, pagePath, progressPath, publishJsonExclusive, requireTicket, responsePath, reviewPath } from './convention.js';
|
|
7
7
|
import { validateReviewDescriptor, validateReviewProjection } from './review-schema.js';
|
|
8
|
-
import { compilePage, deriveHtmlPageManifest, derivePageManifestFromDocument, parsePage, PageAuthoringError } from './page.js';
|
|
8
|
+
import { compilePage, deriveHtmlPageManifest, derivePageManifestFromDocument, parsePage, parsePageDelivery, PageAuthoringError } from './page.js';
|
|
9
9
|
import { isResponseBearingSlot, validatePageResponses, validatePartialPageResponses } from './page-schema.js';
|
|
10
10
|
import { signalInboxActivity } from './root.js';
|
|
11
11
|
import { readTicketClaim, releaseClaimLocked, withTicketLock } from './claim.js';
|
|
@@ -145,6 +145,13 @@ function requireSettleablePage(dir) {
|
|
|
145
145
|
throw new Error('page_has_no_response_lifecycle');
|
|
146
146
|
return manifest;
|
|
147
147
|
}
|
|
148
|
+
function requireCancelablePage(dir) {
|
|
149
|
+
// Cancellation never interprets answers. Reading only this stable envelope
|
|
150
|
+
// keeps an older resident daemon able to retract a page authored by a newer CLI.
|
|
151
|
+
const delivery = parsePageDelivery(dir);
|
|
152
|
+
if (!delivery.inbox && !delivery.reply)
|
|
153
|
+
throw new Error('page_has_no_response_lifecycle');
|
|
154
|
+
}
|
|
148
155
|
function isEmptyResponseMap(responses) {
|
|
149
156
|
return typeof responses === 'object' && responses !== null && !Array.isArray(responses) && Object.keys(responses).length === 0;
|
|
150
157
|
}
|
|
@@ -272,7 +279,7 @@ export function cancelTicket(dir, opts = {}) {
|
|
|
272
279
|
const ticket = requireTicket(dir);
|
|
273
280
|
const canceled = withTicketLock(ticket, () => {
|
|
274
281
|
if (existsSync(pageManifestPath(ticket)))
|
|
275
|
-
|
|
282
|
+
requireCancelablePage(ticket);
|
|
276
283
|
const result = { schema: 'humanloop.cancel/v1', kind: 'canceled', canceledAt: new Date().toISOString(), ...(opts.reason === undefined ? {} : { reason: opts.reason }), ...(opts.actor === undefined ? {} : { actor: opts.actor }) };
|
|
277
284
|
const won = exclusiveResult(ticket, result);
|
|
278
285
|
return { status: won ? 'canceled' : 'already_resolved', result: won ? result : readTicketResult(ticket) ?? result };
|
|
@@ -39,7 +39,19 @@ export declare function loadProfileManifest(profileIdOrName: string): ProfileEnt
|
|
|
39
39
|
* below run inside this. */
|
|
40
40
|
export declare function withProfileManifestLock<T>(profileId: string, fn: () => T): T;
|
|
41
41
|
export declare function ensureRootProfile(): ProfileEntry;
|
|
42
|
-
|
|
42
|
+
/** The env name a metadata entry surfaces as (spawn-env source F). Also the
|
|
43
|
+
* uniqueness domain for keys: the fold is lossy (`a-b`, `a_b`, `A_B` →
|
|
44
|
+
* `CRTR_PROFILE_META_A_B`), so two stored keys may not share a fold. */
|
|
45
|
+
export declare function metadataEnvName(key: string): string;
|
|
46
|
+
/** Reject anything a valid metadata map may not carry. Takes `unknown`
|
|
47
|
+
* because the API route hands over a caller-supplied body slice — the
|
|
48
|
+
* TypeScript type on the DTO proves nothing at runtime. */
|
|
49
|
+
export declare function assertProfileMetadata(entries: unknown): asserts entries is Record<string, string>;
|
|
50
|
+
/** Drop entries no valid mutation could have written — a manifest is
|
|
51
|
+
* hand-editable, so a projection or merge base must never trust it raw.
|
|
52
|
+
* Never throws; on an env-name collision the first entry wins (matching
|
|
53
|
+
* injection, where a deterministic winner beats insertion-order luck). */
|
|
54
|
+
export declare function sanitizeProfileMetadata(stored: unknown): Record<string, string>;
|
|
43
55
|
export declare function createProfile(name: string, projects?: string[], opts?: {
|
|
44
56
|
defaultKind?: string;
|
|
45
57
|
metadata?: Record<string, string>;
|
|
@@ -295,7 +295,23 @@ export function ensureRootProfile() {
|
|
|
295
295
|
* (`CRTR_PROFILE_META_<KEY>`, see `core/runtime/spawn-env.ts`) by
|
|
296
296
|
* construction. */
|
|
297
297
|
const METADATA_KEY_SHAPE = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
298
|
+
/** The env name a metadata entry surfaces as (spawn-env source F). Also the
|
|
299
|
+
* uniqueness domain for keys: the fold is lossy (`a-b`, `a_b`, `A_B` →
|
|
300
|
+
* `CRTR_PROFILE_META_A_B`), so two stored keys may not share a fold. */
|
|
301
|
+
export function metadataEnvName(key) {
|
|
302
|
+
return `CRTR_PROFILE_META_${key.toUpperCase().replace(/[^A-Z0-9]+/g, '_')}`;
|
|
303
|
+
}
|
|
304
|
+
/** Reject anything a valid metadata map may not carry. Takes `unknown`
|
|
305
|
+
* because the API route hands over a caller-supplied body slice — the
|
|
306
|
+
* TypeScript type on the DTO proves nothing at runtime. */
|
|
298
307
|
export function assertProfileMetadata(entries) {
|
|
308
|
+
if (typeof entries !== 'object' || entries === null || Array.isArray(entries)) {
|
|
309
|
+
throw usage('metadata must be a map of string keys to string values', {
|
|
310
|
+
field: 'metadata',
|
|
311
|
+
next: 'Pass an object, e.g. {"role": "support"}.',
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
const claimed = new Map();
|
|
299
315
|
for (const [key, value] of Object.entries(entries)) {
|
|
300
316
|
if (!METADATA_KEY_SHAPE.test(key)) {
|
|
301
317
|
throw usage(`invalid metadata key: ${JSON.stringify(key)}`, {
|
|
@@ -304,14 +320,54 @@ export function assertProfileMetadata(entries) {
|
|
|
304
320
|
next: 'Keys start with a letter or digit, followed by letters, digits, `_`, or `-`.',
|
|
305
321
|
});
|
|
306
322
|
}
|
|
323
|
+
if (typeof value !== 'string') {
|
|
324
|
+
throw usage(`metadata value for ${key} must be a string`, {
|
|
325
|
+
received: typeof value,
|
|
326
|
+
field: 'metadata',
|
|
327
|
+
next: 'Every metadata value is an env-var value — pass strings only.',
|
|
328
|
+
});
|
|
329
|
+
}
|
|
307
330
|
if (value.includes('\0')) {
|
|
308
331
|
throw usage(`metadata value for ${key} contains a NUL byte, which no OS accepts in an environment variable`, {
|
|
309
332
|
field: 'metadata',
|
|
310
333
|
next: 'Remove NUL bytes from the value and retry.',
|
|
311
334
|
});
|
|
312
335
|
}
|
|
336
|
+
const envName = metadataEnvName(key);
|
|
337
|
+
const prior = claimed.get(envName);
|
|
338
|
+
if (prior !== undefined) {
|
|
339
|
+
throw usage(`metadata keys ${JSON.stringify(prior)} and ${JSON.stringify(key)} both surface as ${envName}`, {
|
|
340
|
+
field: 'metadata',
|
|
341
|
+
next: 'Rename or drop one — keys must stay distinct after uppercasing and `_`-folding.',
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
claimed.set(envName, key);
|
|
313
345
|
}
|
|
314
346
|
}
|
|
347
|
+
/** Drop entries no valid mutation could have written — a manifest is
|
|
348
|
+
* hand-editable, so a projection or merge base must never trust it raw.
|
|
349
|
+
* Never throws; on an env-name collision the first entry wins (matching
|
|
350
|
+
* injection, where a deterministic winner beats insertion-order luck). */
|
|
351
|
+
export function sanitizeProfileMetadata(stored) {
|
|
352
|
+
if (typeof stored !== 'object' || stored === null || Array.isArray(stored))
|
|
353
|
+
return {};
|
|
354
|
+
const out = {};
|
|
355
|
+
const claimed = new Set();
|
|
356
|
+
for (const [key, value] of Object.entries(stored)) {
|
|
357
|
+
if (typeof value !== 'string')
|
|
358
|
+
continue;
|
|
359
|
+
if (!METADATA_KEY_SHAPE.test(key))
|
|
360
|
+
continue;
|
|
361
|
+
if (value.includes('\0'))
|
|
362
|
+
continue;
|
|
363
|
+
const envName = metadataEnvName(key);
|
|
364
|
+
if (claimed.has(envName))
|
|
365
|
+
continue;
|
|
366
|
+
claimed.add(envName);
|
|
367
|
+
out[key] = value;
|
|
368
|
+
}
|
|
369
|
+
return out;
|
|
370
|
+
}
|
|
315
371
|
export function createProfile(name, projects = [], opts = {}) {
|
|
316
372
|
const trimmed = name.trim();
|
|
317
373
|
if (trimmed === '')
|
|
@@ -420,9 +476,13 @@ export function setProfileDefaultKind(profileId, defaultKind) {
|
|
|
420
476
|
export function updateProfileMetadata(profileId, set, unset = []) {
|
|
421
477
|
assertProfileMetadata(set);
|
|
422
478
|
return mutateManifest(profileId, (m) => {
|
|
423
|
-
const next = { ...(m.metadata
|
|
479
|
+
const next = { ...sanitizeProfileMetadata(m.metadata), ...set };
|
|
424
480
|
for (const key of unset)
|
|
425
481
|
delete next[key];
|
|
482
|
+
// Re-assert the merged result: a set key may collide with a STORED key in
|
|
483
|
+
// the env-name domain (`a-b` over stored `a_b`), which per-argument
|
|
484
|
+
// validation cannot see.
|
|
485
|
+
assertProfileMetadata(next);
|
|
426
486
|
const { metadata: _dropped, ...rest } = m;
|
|
427
487
|
return { ...rest, ...(Object.keys(next).length > 0 ? { metadata: next } : {}) };
|
|
428
488
|
});
|
|
@@ -436,18 +496,7 @@ export function readProfileMetadata(profileId) {
|
|
|
436
496
|
if (profileId === null || profileId === '')
|
|
437
497
|
return {};
|
|
438
498
|
try {
|
|
439
|
-
|
|
440
|
-
const out = {};
|
|
441
|
-
for (const [key, value] of Object.entries(stored)) {
|
|
442
|
-
if (typeof value !== 'string')
|
|
443
|
-
continue;
|
|
444
|
-
if (!METADATA_KEY_SHAPE.test(key))
|
|
445
|
-
continue;
|
|
446
|
-
if (value.includes('\0'))
|
|
447
|
-
continue;
|
|
448
|
-
out[key] = value;
|
|
449
|
-
}
|
|
450
|
-
return out;
|
|
499
|
+
return sanitizeProfileMetadata(loadExactProfileManifest(profileId).manifest.metadata);
|
|
451
500
|
}
|
|
452
501
|
catch {
|
|
453
502
|
return {};
|
|
@@ -33,7 +33,7 @@ import { statSync } from 'node:fs';
|
|
|
33
33
|
import { delimiter, resolve } from 'node:path';
|
|
34
34
|
import { readMergedLaunchConfig } from '../config.js';
|
|
35
35
|
import { readProfileEnvVars } from '../profiles/env-store.js';
|
|
36
|
-
import { readProfileMetadata } from '../profiles/manifest.js';
|
|
36
|
+
import { metadataEnvName, readProfileMetadata } from '../profiles/manifest.js';
|
|
37
37
|
import { binShimDirFor } from './bin-contributions.js';
|
|
38
38
|
import { FRONT_DOOR_ENV } from './front-door-env.js';
|
|
39
39
|
/** A1 — literal env-var names copied verbatim (no value inspection): a login
|
|
@@ -220,7 +220,7 @@ export function buildOperationalEnvBase(opts) {
|
|
|
220
220
|
export function profileMetadataEnv(profileId) {
|
|
221
221
|
const out = {};
|
|
222
222
|
for (const [key, value] of Object.entries(readProfileMetadata(profileId))) {
|
|
223
|
-
out[
|
|
223
|
+
out[metadataEnvName(key)] = value;
|
|
224
224
|
}
|
|
225
225
|
return out;
|
|
226
226
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// GET /v1/profiles list all profiles
|
|
7
7
|
// GET /v1/profiles/:name resolve one profile by name or id
|
|
8
8
|
// DELETE /v1/profiles/:name required-force cascade or detach
|
|
9
|
+
import { assertInstalledKind } from '../../../core/config.js';
|
|
9
10
|
import { createProfile, ensureRootProfile, listProfiles, loadProfileManifest, } from '../../../core/profiles/manifest.js';
|
|
10
11
|
import { requestProfileDeletion } from '../../profile-delete.js';
|
|
11
12
|
import { toProfileDTO } from '../map.js';
|
|
@@ -25,6 +26,10 @@ function handleEnsure(ctx) {
|
|
|
25
26
|
if (existing !== undefined) {
|
|
26
27
|
return { status: 200, body: toProfileDTO(existing) };
|
|
27
28
|
}
|
|
29
|
+
// Same gate as `profile new`/`profile kind`: a persisted unknown kind would
|
|
30
|
+
// poison every future omitted-kind create under this profile.
|
|
31
|
+
if (req.default_kind !== undefined)
|
|
32
|
+
assertInstalledKind(req.default_kind);
|
|
28
33
|
const created = createProfile(name, req.projects ?? [], {
|
|
29
34
|
defaultKind: req.default_kind,
|
|
30
35
|
metadata: req.metadata,
|
package/dist/daemon/api/map.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// reasonable projections; this module is what makes them concrete, so any DTO
|
|
11
11
|
// field that projection proves wrong is corrected here at the source.
|
|
12
12
|
import { ReviewOperationError as ReviewOperationErrorClass } from '../../core/review/types.js';
|
|
13
|
+
import { sanitizeProfileMetadata } from '../../core/profiles/manifest.js';
|
|
13
14
|
import { contextDir, inboxPath, nodeDir, nodeMetaPath, reportsDir, reviewResultPath, transcriptPath, viewSocketPath, } from '../../core/canvas/paths.js';
|
|
14
15
|
import { FinalizationError } from '../../core/feed/feed.js';
|
|
15
16
|
import { NodeIdConflictError } from '../../core/runtime/nodes.js';
|
|
@@ -266,7 +267,7 @@ export function toProfileDTO(entry) {
|
|
|
266
267
|
home: entry.manifest.home,
|
|
267
268
|
paused_at: entry.manifest.paused_at,
|
|
268
269
|
default_kind: entry.manifest.default_kind ?? 'general',
|
|
269
|
-
metadata:
|
|
270
|
+
metadata: sanitizeProfileMetadata(entry.manifest.metadata),
|
|
270
271
|
default_dir: null,
|
|
271
272
|
};
|
|
272
273
|
}
|
package/package.json
CHANGED
package/runtime.lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/crouter",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.217",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@north-light/crouter",
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.217",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"dependencies": {
|