@north-light/crouter 0.3.215 → 0.3.216
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 +199 -199
- 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/config.d.ts +5 -0
- package/dist/core/config.js +15 -0
- 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
|
@@ -1,19 +1,7 @@
|
|
|
1
1
|
import { defineLeaf } from '../../core/command.js';
|
|
2
|
-
import {
|
|
3
|
-
import { readMergedLaunchConfig } from '../../core/config.js';
|
|
2
|
+
import { assertInstalledKind, readMergedLaunchConfig } from '../../core/config.js';
|
|
4
3
|
import { loadProfileManifest, setProfileDefaultKind } from '../../core/profiles/manifest.js';
|
|
5
4
|
import { stateBlock } from '../../core/help.js';
|
|
6
|
-
export function assertProfileDefaultKind(kind) {
|
|
7
|
-
const kinds = Object.keys(readMergedLaunchConfig().kinds).sort();
|
|
8
|
-
if (!kinds.includes(kind)) {
|
|
9
|
-
throw new InputError({
|
|
10
|
-
error: 'unknown_kind',
|
|
11
|
-
message: `unknown kind: ${kind}`,
|
|
12
|
-
field: 'kind',
|
|
13
|
-
next: `Valid kinds: ${kinds.join(', ')}.`,
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
5
|
function kindsStateBlock() {
|
|
18
6
|
return stateBlock('kinds', { count: Object.keys(readMergedLaunchConfig().kinds).length }, Object.keys(readMergedLaunchConfig().kinds).sort().join('\n'));
|
|
19
7
|
}
|
|
@@ -39,7 +27,7 @@ export const kindLeaf = defineLeaf({
|
|
|
39
27
|
},
|
|
40
28
|
run: async (input) => {
|
|
41
29
|
const kind = input['kind'];
|
|
42
|
-
|
|
30
|
+
assertInstalledKind(kind);
|
|
43
31
|
const { profileId } = loadProfileManifest(input['profile']);
|
|
44
32
|
const { manifest } = setProfileDefaultKind(profileId, kind);
|
|
45
33
|
return {
|
|
@@ -5,7 +5,9 @@ import { loadProfileManifest, updateProfileMetadata } from '../../core/profiles/
|
|
|
5
5
|
* with the manifest store (`assertProfileMetadata`); this only splits. */
|
|
6
6
|
export function resolveMetaPairs(raw, field) {
|
|
7
7
|
const pairs = Array.isArray(raw) ? raw.map(String) : typeof raw === 'string' && raw !== '' ? [raw] : [];
|
|
8
|
-
|
|
8
|
+
// Null prototype: on a plain object a `__proto__` key would silently
|
|
9
|
+
// vanish here instead of reaching the store's key-shape rejection.
|
|
10
|
+
const out = Object.create(null);
|
|
9
11
|
for (const pair of pairs) {
|
|
10
12
|
const eq = pair.indexOf('=');
|
|
11
13
|
if (eq <= 0) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { defineLeaf } from '../../core/command.js';
|
|
2
2
|
import { usage } from '../../core/errors.js';
|
|
3
|
+
import { assertInstalledKind } from '../../core/config.js';
|
|
3
4
|
import { createProfile, ensureRootProfile, profileRoot } from '../../core/profiles/manifest.js';
|
|
4
|
-
import { assertProfileDefaultKind } from './kind.js';
|
|
5
5
|
import { resolveMetaPairs } from './meta.js';
|
|
6
6
|
export const newLeaf = defineLeaf({
|
|
7
7
|
name: 'new',
|
|
@@ -23,7 +23,7 @@ export const newLeaf = defineLeaf({
|
|
|
23
23
|
name: 'root',
|
|
24
24
|
type: 'bool',
|
|
25
25
|
required: false,
|
|
26
|
-
constraint: 'Provision the canonical stable root profile (fixed id `root-00000000`, name `root`) — the fallback default identity for a home — instead of a fresh named one. Idempotent: re-running returns the existing root. Node-free (no `node new --root` needed). Mutually exclusive with `--name`/`--project`.',
|
|
26
|
+
constraint: 'Provision the canonical stable root profile (fixed id `root-00000000`, name `root`) — the fallback default identity for a home — instead of a fresh named one. Idempotent: re-running returns the existing root. Node-free (no `node new --root` needed). Mutually exclusive with `--name`/`--project`/`--default-kind`/`--meta`.',
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
29
|
kind: 'flag',
|
|
@@ -88,7 +88,7 @@ export const newLeaf = defineLeaf({
|
|
|
88
88
|
if (name === undefined)
|
|
89
89
|
throw usage('provide `--name <name>` to create a profile, or `--root` to provision the stable root profile.');
|
|
90
90
|
if (defaultKind !== undefined)
|
|
91
|
-
|
|
91
|
+
assertInstalledKind(defaultKind);
|
|
92
92
|
const { profileId, manifest } = createProfile(name, project !== undefined ? [project] : [], {
|
|
93
93
|
defaultKind,
|
|
94
94
|
...(Object.keys(metadata).length > 0 ? { metadata } : {}),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { defineLeaf } from '../../core/command.js';
|
|
4
|
-
import { loadProfileManifest, profileRoot } from '../../core/profiles/manifest.js';
|
|
4
|
+
import { loadProfileManifest, profileRoot, sanitizeProfileMetadata } from '../../core/profiles/manifest.js';
|
|
5
5
|
import { CONFIG_FILE } from '../../types.js';
|
|
6
6
|
export const showLeaf = defineLeaf({
|
|
7
7
|
name: 'show',
|
|
@@ -46,7 +46,7 @@ export const showLeaf = defineLeaf({
|
|
|
46
46
|
home: manifest.home,
|
|
47
47
|
paused_at: manifest.paused_at,
|
|
48
48
|
default_kind: manifest.default_kind ?? null,
|
|
49
|
-
metadata: manifest.metadata
|
|
49
|
+
metadata: sanitizeProfileMetadata(manifest.metadata),
|
|
50
50
|
created_at: manifest.created_at,
|
|
51
51
|
last_used_at: manifest.last_used_at,
|
|
52
52
|
path: profileRoot(profileId),
|
package/dist/core/config.d.ts
CHANGED
|
@@ -141,6 +141,11 @@ export declare function readMergedLaunchConfig(targetCwd?: string, targetProfile
|
|
|
141
141
|
* existence/launch-menu enumeration is a caller concern); this only
|
|
142
142
|
* resolves the config for a kind the caller already knows about. */
|
|
143
143
|
export declare function resolveKindConfig(kind: string): KindConfig | undefined;
|
|
144
|
+
/** Reject a kind no scope registers. The one validation gate for every
|
|
145
|
+
* surface that PERSISTS a kind (`profile new/kind`, the profile-ensure API
|
|
146
|
+
* route) — a stored unknown kind would poison future omitted-kind creates
|
|
147
|
+
* through `default_kind` resolution. */
|
|
148
|
+
export declare function assertInstalledKind(kind: string): void;
|
|
144
149
|
/** The sub-kinds available to spawn FROM a given top-level kind — every
|
|
145
150
|
* registered sub-kind (full path contains `/`) whose `availableTo` (default:
|
|
146
151
|
* its own top-level ancestor, e.g. `plan/reviewers/security` defaults to
|
package/dist/core/config.js
CHANGED
|
@@ -2,6 +2,7 @@ import { dirname, join } from 'node:path';
|
|
|
2
2
|
import { isDeepStrictEqual } from 'node:util';
|
|
3
3
|
import { CONFIG_FILE, STATE_FILE, CONDENSED_HISTORY_MODES, MOUSE_MODE_DEFAULTS, WHIP_MESSAGE_MODES, DEFAULT_WHIP_MESSAGES, defaultScopeConfig, defaultScopeState, defaultModelLaddersConfig, defaultKindsConfig, defaultRemoteCanvasConfig } from '../types.js';
|
|
4
4
|
import { BINDING_CATALOG, BINDING_IDS, isAttachPaneBinding } from './keybindings/catalog.js';
|
|
5
|
+
import { usage } from './errors.js';
|
|
5
6
|
import { emitEvent } from './events/emit.js';
|
|
6
7
|
import { atomicWriteJson, readJsonIfExists, writeJson, ensureDir } from './fs-utils.js';
|
|
7
8
|
import { scopeRoot, requireScopeRoot, findProjectScopeRoots } from './scope.js';
|
|
@@ -753,6 +754,20 @@ export function readMergedLaunchConfig(targetCwd = process.cwd(), targetProfileI
|
|
|
753
754
|
export function resolveKindConfig(kind) {
|
|
754
755
|
return readMergedLaunchConfig().kinds[kind];
|
|
755
756
|
}
|
|
757
|
+
/** Reject a kind no scope registers. The one validation gate for every
|
|
758
|
+
* surface that PERSISTS a kind (`profile new/kind`, the profile-ensure API
|
|
759
|
+
* route) — a stored unknown kind would poison future omitted-kind creates
|
|
760
|
+
* through `default_kind` resolution. */
|
|
761
|
+
export function assertInstalledKind(kind) {
|
|
762
|
+
const kinds = Object.keys(readMergedLaunchConfig().kinds).sort();
|
|
763
|
+
if (!kinds.includes(kind)) {
|
|
764
|
+
throw usage(`unknown kind: ${kind}`, {
|
|
765
|
+
received: kind,
|
|
766
|
+
field: 'kind',
|
|
767
|
+
next: `Valid kinds: ${kinds.join(', ')}.`,
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
}
|
|
756
771
|
/** The sub-kinds available to spawn FROM a given top-level kind — every
|
|
757
772
|
* registered sub-kind (full path contains `/`) whose `availableTo` (default:
|
|
758
773
|
* its own top-level ancestor, e.g. `plan/reviewers/security` defaults to
|
|
@@ -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.216",
|
|
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.216",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"dependencies": {
|