@crouton-kit/crouter 0.3.60 → 0.3.62
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-views/canvas/core.mjs +2 -1
- package/dist/clients/attach/__tests__/context-message.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/context-message.test.js +46 -0
- package/dist/clients/attach/__tests__/transport-relay.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/transport-relay.test.js +176 -0
- package/dist/clients/attach/attach-cmd.js +747 -743
- package/dist/clients/attach/canvas-panels.d.ts +4 -4
- package/dist/clients/attach/canvas-panels.js +108 -37
- package/dist/clients/attach/chat-view.js +11 -0
- package/dist/clients/attach/context-message.d.ts +3 -1
- package/dist/clients/attach/context-message.js +30 -21
- package/dist/clients/attach/graph-overlay.d.ts +8 -1
- package/dist/clients/attach/graph-overlay.js +201 -22
- package/dist/clients/attach/transport-relay.d.ts +51 -0
- package/dist/clients/attach/transport-relay.js +196 -0
- package/dist/clients/attach/transport-socket.d.ts +29 -0
- package/dist/clients/attach/transport-socket.js +164 -0
- package/dist/clients/attach/transport.d.ts +32 -0
- package/dist/clients/attach/transport.js +26 -0
- package/dist/clients/attach/view-socket.d.ts +40 -44
- package/dist/clients/attach/view-socket.js +94 -197
- package/dist/clients/web/server.js +1 -0
- package/dist/commands/memory/delete.js +3 -3
- package/dist/commands/memory/origin.js +1 -1
- package/dist/commands/memory/read.js +3 -3
- package/dist/commands/memory/shared.d.ts +8 -6
- package/dist/commands/memory/shared.js +23 -9
- package/dist/commands/memory/write.js +3 -3
- package/dist/commands/node-worktree.js +9 -9
- package/dist/commands/node.js +71 -23
- package/dist/commands/sys/doctor.js +1 -0
- package/dist/core/__tests__/broker-fork-seam.test.js +17 -2
- package/dist/core/__tests__/broker-snapshot-history.test.js +12 -2
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +79 -0
- package/dist/core/__tests__/context-intro.test.js +11 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +9 -4
- package/dist/core/__tests__/fixtures/fake-engine.js +12 -4
- package/dist/core/__tests__/kickoff.test.js +21 -0
- package/dist/core/__tests__/spawn-root.test.js +60 -0
- package/dist/core/__tests__/worktree.test.js +24 -1
- package/dist/core/canvas/browse/app.d.ts +2 -0
- package/dist/core/canvas/browse/app.js +83 -48
- package/dist/core/canvas/db.js +3 -2
- package/dist/core/canvas/index.d.ts +1 -0
- package/dist/core/canvas/index.js +1 -0
- package/dist/core/canvas/nav-model.d.ts +5 -3
- package/dist/core/canvas/nav-model.js +7 -6
- package/dist/core/canvas/paths.d.ts +8 -0
- package/dist/core/canvas/paths.js +10 -0
- package/dist/core/canvas/render.d.ts +6 -0
- package/dist/core/canvas/render.js +117 -20
- package/dist/core/canvas/source.d.ts +26 -0
- package/dist/core/canvas/source.js +29 -0
- package/dist/core/canvas/status-glyph.js +1 -0
- package/dist/core/canvas/types.d.ts +4 -3
- package/dist/core/config.d.ts +18 -2
- package/dist/core/config.js +88 -10
- package/dist/core/fault-classifier.d.ts +2 -2
- package/dist/core/memory-resolver.d.ts +9 -6
- package/dist/core/memory-resolver.js +28 -4
- package/dist/core/profiles/__tests__/root-profile.test.d.ts +1 -0
- package/dist/core/profiles/__tests__/root-profile.test.js +69 -0
- package/dist/core/profiles/manifest.d.ts +2 -0
- package/dist/core/profiles/manifest.js +40 -7
- package/dist/core/profiles/select.d.ts +4 -4
- package/dist/core/profiles/select.js +13 -13
- package/dist/core/runtime/__tests__/spawn-env.test.d.ts +1 -0
- package/dist/core/runtime/__tests__/spawn-env.test.js +268 -0
- package/dist/core/runtime/bearings.d.ts +5 -4
- package/dist/core/runtime/bearings.js +12 -6
- package/dist/core/runtime/broker.js +59 -7
- package/dist/core/runtime/fault-recovery.js +4 -0
- package/dist/core/runtime/front-door.js +1 -1
- package/dist/core/runtime/host.js +43 -3
- package/dist/core/runtime/kickoff.js +6 -0
- package/dist/core/runtime/launch.d.ts +6 -1
- package/dist/core/runtime/launch.js +7 -1
- package/dist/core/runtime/nodes.d.ts +7 -6
- package/dist/core/runtime/nodes.js +3 -3
- package/dist/core/runtime/situational-context.d.ts +12 -0
- package/dist/core/runtime/situational-context.js +48 -0
- package/dist/core/runtime/spawn-env.d.ts +27 -0
- package/dist/core/runtime/spawn-env.js +169 -0
- package/dist/core/runtime/spawn.d.ts +16 -7
- package/dist/core/runtime/spawn.js +8 -2
- package/dist/core/substrate/session-cache.js +7 -1
- package/dist/core/substrate/subject.d.ts +1 -1
- package/dist/core/view/remote-canvas-target.d.ts +3 -0
- package/dist/core/view/remote-canvas-target.js +25 -0
- package/dist/core/worktree.d.ts +4 -4
- package/dist/core/worktree.js +41 -19
- package/dist/daemon/crtrd.js +32 -0
- package/dist/daemon/predicate-eval.js +12 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -0
- package/dist/pi-extensions/canvas-bash-valve.d.ts +2 -0
- package/dist/pi-extensions/canvas-bash-valve.js +238 -0
- package/dist/pi-extensions/canvas-inbox-watcher.d.ts +12 -0
- package/dist/pi-extensions/canvas-inbox-watcher.js +41 -5
- package/dist/types.d.ts +32 -0
- package/dist/types.js +7 -0
- package/dist/web-client/apple-touch-icon.png +0 -0
- package/dist/web-client/assets/index-BnmSLNLa.css +2 -0
- package/dist/web-client/assets/index-BvzxXXGU.js +77 -0
- package/dist/web-client/assets/workbox-window.prod.es5-Bd17z0YL.js +2 -0
- package/dist/web-client/icon-192.png +0 -0
- package/dist/web-client/icon-512.png +0 -0
- package/dist/web-client/icon-maskable-512.png +0 -0
- package/dist/web-client/index.html +6 -3
- package/dist/web-client/manifest.webmanifest +1 -0
- package/dist/web-client/sw.js +1 -0
- package/docs/public-api.md +5 -3
- package/package.json +4 -3
- package/dist/web-client/assets/fraunces-latin-ext-wght-normal-Ca2vKHc0.woff2 +0 -0
- package/dist/web-client/assets/fraunces-latin-wght-normal-ukD16Tqj.woff2 +0 -0
- package/dist/web-client/assets/fraunces-vietnamese-wght-normal-CnvboYUG.woff2 +0 -0
- package/dist/web-client/assets/index-72YQdji_.js +0 -77
- package/dist/web-client/assets/index-bDhImjJy.css +0 -2
- package/dist/web-client/assets/instrument-sans-latin-ext-wght-normal-B5bTHO_g.woff2 +0 -0
- package/dist/web-client/assets/instrument-sans-latin-wght-normal-BbzFLZTg.woff2 +0 -0
|
@@ -18,7 +18,7 @@ import { connect } from 'node:net';
|
|
|
18
18
|
import { getNode } from '../canvas/index.js';
|
|
19
19
|
import { nodeDir, jobDir, viewSocketPath } from '../canvas/paths.js';
|
|
20
20
|
import { encodeFrame } from './broker-protocol.js';
|
|
21
|
-
import {
|
|
21
|
+
import { buildBrokerEnv } from './spawn-env.js';
|
|
22
22
|
import { hostExecPath } from './branded-host.js';
|
|
23
23
|
import { isPidAlive, killProcessTreePids, isAnyPidAlive, captureTeardownSnapshot } from '../canvas/pid.js';
|
|
24
24
|
import { logger } from '../log.js';
|
|
@@ -36,6 +36,17 @@ function resolveBrokerEntry() {
|
|
|
36
36
|
return join(here, 'broker-cli.js');
|
|
37
37
|
}
|
|
38
38
|
const DEFAULT_BROKER_ENGINE = '@earendil-works/pi-coding-agent';
|
|
39
|
+
/** Resolve the broker engine spec for THIS launch, mirroring broker-sdk.ts's
|
|
40
|
+
* own resolution order (`inv.env` first, then the launching process's ambient
|
|
41
|
+
* `CRTR_BROKER_ENGINE` — the T11 test seam — then the real SDK default).
|
|
42
|
+
* Called once by `preflightBrokerLaunch` (to validate resolvability before we
|
|
43
|
+
* detach a process) and again by `launch` (to inject the SAME resolved value
|
|
44
|
+
* into the child's env — see the child-env comment at the spawn() call for
|
|
45
|
+
* why: the default-deny allowlist in spawn-env.ts strips `CRTR_*` from
|
|
46
|
+
* ambient env, so the child can no longer re-derive this itself). */
|
|
47
|
+
function resolveEngineSpec(inv) {
|
|
48
|
+
return inv.env['CRTR_BROKER_ENGINE'] ?? process.env['CRTR_BROKER_ENGINE'] ?? DEFAULT_BROKER_ENGINE;
|
|
49
|
+
}
|
|
39
50
|
function isPlainLaunchSessionPath(token) {
|
|
40
51
|
return token.includes('/') || token.endsWith('.jsonl');
|
|
41
52
|
}
|
|
@@ -67,7 +78,7 @@ export function preflightBrokerLaunch(nodeId, inv, cwd) {
|
|
|
67
78
|
if (!Array.isArray(recipe.argv)) {
|
|
68
79
|
throw new Error(`[broker] refusing to launch ${nodeId}: broker-launch.json is missing the argv array`);
|
|
69
80
|
}
|
|
70
|
-
const engineSpec = inv
|
|
81
|
+
const engineSpec = resolveEngineSpec(inv);
|
|
71
82
|
try {
|
|
72
83
|
const resolvedEngine = import.meta.resolve(engineSpec);
|
|
73
84
|
if (resolvedEngine.startsWith('file:') && !existsSync(fileURLToPath(resolvedEngine))) {
|
|
@@ -179,11 +190,40 @@ export const headlessBrokerHost = {
|
|
|
179
190
|
// Launch from the crouter-branded host binary (a copy of node) so the
|
|
180
191
|
// broker shows "crouter" in macOS Full Disk Access, not "node". On
|
|
181
192
|
// non-darwin / dev / tests this is just process.execPath (see branded-host).
|
|
193
|
+
// buildBrokerEnv's default-deny allowlist has no `CRTR_` prefix, so a bare
|
|
194
|
+
// `CRTR_BROKER_ENGINE` set only in the LAUNCHING process's ambient env (the
|
|
195
|
+
// T11 test seam — see broker-sdk.ts) would never reach the child, which
|
|
196
|
+
// would silently fall back to booting the real SDK. Inject the SAME value
|
|
197
|
+
// preflightBrokerLaunch already resolved+validated above: it is
|
|
198
|
+
// parent-resolved, crtr-trusted data (the same trust category as
|
|
199
|
+
// `inv.env`), not a blanket `CRTR_*` passthrough, so this does not weaken
|
|
200
|
+
// the allowlist's hardening. In production `engineSpec` is always the real
|
|
201
|
+
// SDK default, so this is a no-op there.
|
|
202
|
+
const engineSpec = resolveEngineSpec(inv);
|
|
203
|
+
const childEnv = { ...buildBrokerEnv(inv), CRTR_BROKER_ENGINE: engineSpec };
|
|
204
|
+
// NODE_OPTIONS is deliberately absent from the operational allowlist (it is
|
|
205
|
+
// a code-execution vector — `--require`/`--import` — and DAEMON_ENV_STRIP_KEYS
|
|
206
|
+
// strips it from the daemon's own env for the identical reason: a poisoned
|
|
207
|
+
// NODE_OPTIONS in an inheriting shell must never reach a process that then
|
|
208
|
+
// handles model credentials). It is genuinely needed ONLY for the T11 test
|
|
209
|
+
// seam above: the fake engine is a `.ts` fixture, and the broker CLI entry
|
|
210
|
+
// itself is only a `.js` file under the compiled `dist/`, so the harness
|
|
211
|
+
// relies on `NODE_OPTIONS=--import tsx/esm` (set on its own ambient env, same
|
|
212
|
+
// seam as CRTR_BROKER_ENGINE) to make the spawned child resolve both under
|
|
213
|
+
// tsx. Gating this carry on `engineSpec !== DEFAULT_BROKER_ENGINE` — i.e. only
|
|
214
|
+
// when the trusted test seam above already fired — keeps production (where
|
|
215
|
+
// the engine is never overridden) byte-identical to today: NODE_OPTIONS is
|
|
216
|
+
// never forwarded there, so an ambient poisoned value stays contained.
|
|
217
|
+
if (engineSpec !== DEFAULT_BROKER_ENGINE) {
|
|
218
|
+
const nodeOptions = inv.env['NODE_OPTIONS'] ?? process.env['NODE_OPTIONS'];
|
|
219
|
+
if (nodeOptions !== undefined)
|
|
220
|
+
childEnv['NODE_OPTIONS'] = nodeOptions;
|
|
221
|
+
}
|
|
182
222
|
const child = spawn(hostExecPath(), [resolveBrokerEntry(), nodeId], {
|
|
183
223
|
cwd: opts.cwd,
|
|
184
224
|
detached: true,
|
|
185
225
|
stdio: ['ignore', logFd, logFd],
|
|
186
|
-
env:
|
|
226
|
+
env: childEnv,
|
|
187
227
|
});
|
|
188
228
|
// The child holds its own dup of the fd; release the parent's copy so the
|
|
189
229
|
// launching process (CLI or daemon) never leaks it.
|
|
@@ -249,6 +249,12 @@ export function buildReviveKickoff(meta, bearings, wakeReason) {
|
|
|
249
249
|
}
|
|
250
250
|
const files = listContextDir(nodeId);
|
|
251
251
|
parts.push(`<context-dir path="${contextDir(nodeId)}">\n${files.length > 0 ? files.join('\n') : '(empty)'}\n</context-dir>`);
|
|
252
|
+
// Hidden applet/situation-origin ambient context (see situational-context.ts)
|
|
253
|
+
// is deliberately NOT injected here — this kickoff is the VISIBLE revive prompt.
|
|
254
|
+
// canvas-context-intro's session_start handler re-fires on every fresh/cycling
|
|
255
|
+
// revive (a new pi process boot) and re-injects buildContextIntro(), which
|
|
256
|
+
// already carries the sidecar as a hidden sibling of <crtr-bearings>; that seam
|
|
257
|
+
// covers this path, so duplicating it into the visible prompt would leak it.
|
|
252
258
|
parts.push(feedBlock(nodeId, bearings.unreadDigest));
|
|
253
259
|
parts.push(bearings.yieldMsg !== null
|
|
254
260
|
? `<yield-message>\n${bearings.yieldMsg.trim()}\n</yield-message>`
|
|
@@ -9,6 +9,7 @@ export declare const CANVAS_PASSIVE_CONTEXT_PATH: string;
|
|
|
9
9
|
export declare const CANVAS_CONTEXT_INTRO_PATH: string;
|
|
10
10
|
export declare const CANVAS_DOC_SUBSTRATE_PATH: string;
|
|
11
11
|
export declare const CANVAS_STRUCTURED_OUTPUT_PATH: string;
|
|
12
|
+
export declare const CANVAS_BASH_VALVE_PATH: string;
|
|
12
13
|
/** The canvas extensions every node loads, in order: stophook (routing +
|
|
13
14
|
* telemetry + session-id capture), inbox-watcher (wake), nav (in-editor
|
|
14
15
|
* graph chrome), recap (the per-node inactivity recap card — Haiku over the
|
|
@@ -18,7 +19,11 @@ export declare const CANVAS_STRUCTURED_OUTPUT_PATH: string;
|
|
|
18
19
|
* context-intro (inject the <crtr-bearings> block + the <memory kind="knowledge">
|
|
19
20
|
* catalog as its own session message, once per brand-new chat),
|
|
20
21
|
* doc-substrate (the unified document substrate's two hooks: the
|
|
21
|
-
* <memory kind="preference"> block at boot, on-read context injection)
|
|
22
|
+
* <memory kind="preference"> block at boot, on-read context injection),
|
|
23
|
+
* bash-valve (replaces the builtin bash tool with a file-backed exec
|
|
24
|
+
* backend: commands under 5min are byte-identical to today; past 5min
|
|
25
|
+
* with no explicit timeout, the tool call returns with output-so-far and
|
|
26
|
+
* the command keeps running detached, alerting via inbox on exit).
|
|
22
27
|
* All self-gate on CRTR_NODE_ID. goal-capture precedes passive-context so it
|
|
23
28
|
* reads the raw user text. */
|
|
24
29
|
export declare const CANVAS_EXTENSIONS: string[];
|
|
@@ -36,6 +36,7 @@ export const CANVAS_PASSIVE_CONTEXT_PATH = resolveExtension('canvas-passive-cont
|
|
|
36
36
|
export const CANVAS_CONTEXT_INTRO_PATH = resolveExtension('canvas-context-intro');
|
|
37
37
|
export const CANVAS_DOC_SUBSTRATE_PATH = resolveExtension('canvas-doc-substrate');
|
|
38
38
|
export const CANVAS_STRUCTURED_OUTPUT_PATH = resolveExtension('canvas-structured-output');
|
|
39
|
+
export const CANVAS_BASH_VALVE_PATH = resolveExtension('canvas-bash-valve');
|
|
39
40
|
/** The canvas extensions every node loads, in order: stophook (routing +
|
|
40
41
|
* telemetry + session-id capture), inbox-watcher (wake), nav (in-editor
|
|
41
42
|
* graph chrome), recap (the per-node inactivity recap card — Haiku over the
|
|
@@ -45,7 +46,11 @@ export const CANVAS_STRUCTURED_OUTPUT_PATH = resolveExtension('canvas-structured
|
|
|
45
46
|
* context-intro (inject the <crtr-bearings> block + the <memory kind="knowledge">
|
|
46
47
|
* catalog as its own session message, once per brand-new chat),
|
|
47
48
|
* doc-substrate (the unified document substrate's two hooks: the
|
|
48
|
-
* <memory kind="preference"> block at boot, on-read context injection)
|
|
49
|
+
* <memory kind="preference"> block at boot, on-read context injection),
|
|
50
|
+
* bash-valve (replaces the builtin bash tool with a file-backed exec
|
|
51
|
+
* backend: commands under 5min are byte-identical to today; past 5min
|
|
52
|
+
* with no explicit timeout, the tool call returns with output-so-far and
|
|
53
|
+
* the command keeps running detached, alerting via inbox on exit).
|
|
49
54
|
* All self-gate on CRTR_NODE_ID. goal-capture precedes passive-context so it
|
|
50
55
|
* reads the raw user text. */
|
|
51
56
|
export const CANVAS_EXTENSIONS = [
|
|
@@ -58,6 +63,7 @@ export const CANVAS_EXTENSIONS = [
|
|
|
58
63
|
CANVAS_CONTEXT_INTRO_PATH,
|
|
59
64
|
CANVAS_DOC_SUBSTRATE_PATH,
|
|
60
65
|
CANVAS_STRUCTURED_OUTPUT_PATH,
|
|
66
|
+
CANVAS_BASH_VALVE_PATH,
|
|
61
67
|
];
|
|
62
68
|
// ---------------------------------------------------------------------------
|
|
63
69
|
// Two-axis model resolution: (provider × strength) → concrete `model:thinking`.
|
|
@@ -47,12 +47,13 @@ export interface SpawnNodeOpts {
|
|
|
47
47
|
* persona default. Persisted to `meta.model_override` so polymorphs preserve
|
|
48
48
|
* it. Omit to use the persona default. */
|
|
49
49
|
modelOverride?: string | null;
|
|
50
|
-
/** The profile-directory id (`<slug>-<id>`) this node runs under; null
|
|
51
|
-
* root/no-profile
|
|
52
|
-
* into the pi process env as `CRTR_PROFILE_ID` by
|
|
53
|
-
* → resolver seam every downstream memory/gate
|
|
54
|
-
* resolve/validate the operand (via
|
|
55
|
-
* here; this layer just persists the
|
|
50
|
+
/** The profile-directory id (`<slug>-<id>`) this node runs under; null only
|
|
51
|
+
* for historical root/no-profile nodes. Written to `meta.profile_id` and the
|
|
52
|
+
* row, and mirrored into the pi process env as `CRTR_PROFILE_ID` by
|
|
53
|
+
* `nodeEnv` — the runtime → resolver seam every downstream memory/gate
|
|
54
|
+
* consumer reads. Callers resolve/validate the operand (via
|
|
55
|
+
* `loadProfileManifest`) before it reaches here; this layer just persists the
|
|
56
|
+
* already-resolved id. */
|
|
56
57
|
profile_id?: string | null;
|
|
57
58
|
/** Managed worktree already created for this node, if any. */
|
|
58
59
|
managedWorktree?: ManagedWorktree;
|
|
@@ -65,9 +65,9 @@ export function nodeEnv(meta) {
|
|
|
65
65
|
CRTR_CONTEXT_DIR: contextDir(meta.node_id),
|
|
66
66
|
CRTR_CYCLES: (meta.cycles ?? 0).toString(),
|
|
67
67
|
// The runtime→resolver seam: memory-resolver.ts/scope.ts/config.ts read
|
|
68
|
-
// this process-wide env var
|
|
69
|
-
//
|
|
70
|
-
//
|
|
68
|
+
// this process-wide env var to pull in the node's profile memory store and
|
|
69
|
+
// multi-project pointer set. '' (never omitted) only for historical
|
|
70
|
+
// null-profile rows, matching every other CRTR_* scalar here.
|
|
71
71
|
CRTR_PROFILE_ID: meta.profile_id ?? '',
|
|
72
72
|
};
|
|
73
73
|
if (meta.parent)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** The `customType` stamped on the hidden custom message the inbox watcher
|
|
2
|
+
* sends for an existing node's situational-context update. */
|
|
3
|
+
export declare const SITUATIONAL_CONTEXT_CUSTOM_TYPE = "crtr-situational-context";
|
|
4
|
+
/** Upsert the node's current situational context (replace, not append) — the
|
|
5
|
+
* one sanctioned writer for the sidecar. No-op for blank text, so a caller
|
|
6
|
+
* can pass through an optional/empty value with no guard at the call site. */
|
|
7
|
+
export declare function appendSituationalContext(nodeId: string, text: string): void;
|
|
8
|
+
/** Read the current situational context, or null if none was ever set. */
|
|
9
|
+
export declare function readSituationalContext(nodeId: string): string | null;
|
|
10
|
+
/** The `<situational-context>` block for `nodeId`, or '' when none is set —
|
|
11
|
+
* callers append/filter this like any other optional sibling block. */
|
|
12
|
+
export declare function situationalContextBlock(nodeId: string): string;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// situational-context.ts — the hidden ambient "current situation" sidecar.
|
|
2
|
+
//
|
|
3
|
+
// A node-owned runtime file (nodes/<id>/situational-context.md, NOT under
|
|
4
|
+
// context/ — see `situationalContextPath`) carrying the LATEST applet/
|
|
5
|
+
// situation-origin ambient text: current state (e.g. which applet/page a
|
|
6
|
+
// concierge is attached to right now), not task history, and never the node's
|
|
7
|
+
// goal or a visible chat message. Upserted (replace), never appended: only the
|
|
8
|
+
// newest block is ever carried, so stale applet pages never accumulate across
|
|
9
|
+
// concierge reuse.
|
|
10
|
+
//
|
|
11
|
+
// Read at three injection seams, always rendered as a `<situational-context>`
|
|
12
|
+
// block SIBLING to whatever bearings/kickoff frame is being built (never
|
|
13
|
+
// nested inside it, and never routed through coalesce()'s visible digest
|
|
14
|
+
// renderer):
|
|
15
|
+
// - buildContextBearings() — fresh node kickoff (canvas-context-intro).
|
|
16
|
+
// - buildReviveKickoff() — fresh/cycling revive.
|
|
17
|
+
// - canvas-inbox-watcher's flush() — a hidden pi.sendMessage custom message
|
|
18
|
+
// for an existing live/dormant node, delivered ahead of (and never as
|
|
19
|
+
// part of) the visible inbox digest.
|
|
20
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
21
|
+
import { situationalContextPath, nodeDir } from '../canvas/paths.js';
|
|
22
|
+
/** The `customType` stamped on the hidden custom message the inbox watcher
|
|
23
|
+
* sends for an existing node's situational-context update. */
|
|
24
|
+
export const SITUATIONAL_CONTEXT_CUSTOM_TYPE = 'crtr-situational-context';
|
|
25
|
+
/** Upsert the node's current situational context (replace, not append) — the
|
|
26
|
+
* one sanctioned writer for the sidecar. No-op for blank text, so a caller
|
|
27
|
+
* can pass through an optional/empty value with no guard at the call site. */
|
|
28
|
+
export function appendSituationalContext(nodeId, text) {
|
|
29
|
+
const body = text.trim();
|
|
30
|
+
if (body === '')
|
|
31
|
+
return;
|
|
32
|
+
mkdirSync(nodeDir(nodeId), { recursive: true });
|
|
33
|
+
writeFileSync(situationalContextPath(nodeId), body + '\n', 'utf8');
|
|
34
|
+
}
|
|
35
|
+
/** Read the current situational context, or null if none was ever set. */
|
|
36
|
+
export function readSituationalContext(nodeId) {
|
|
37
|
+
const p = situationalContextPath(nodeId);
|
|
38
|
+
if (!existsSync(p))
|
|
39
|
+
return null;
|
|
40
|
+
const body = readFileSync(p, 'utf8').trim();
|
|
41
|
+
return body !== '' ? body : null;
|
|
42
|
+
}
|
|
43
|
+
/** The `<situational-context>` block for `nodeId`, or '' when none is set —
|
|
44
|
+
* callers append/filter this like any other optional sibling block. */
|
|
45
|
+
export function situationalContextBlock(nodeId) {
|
|
46
|
+
const text = readSituationalContext(nodeId);
|
|
47
|
+
return text === null ? '' : `<situational-context>\n${text}\n</situational-context>`;
|
|
48
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { PiInvocation } from './launch.js';
|
|
2
|
+
/** Sources A–C: the operational base shared by both the broker boundary (this
|
|
3
|
+
* module's `buildBrokerEnv`) and the predicate-eval `when_script` path
|
|
4
|
+
* (`daemon/predicate-eval.ts`) — the single shared definition of "non-secret
|
|
5
|
+
* host state the runtime exposes to any spawned shell." Does not include
|
|
6
|
+
* `inv.env`/`FRONT_DOOR_ENV` (source D), which is broker-specific. */
|
|
7
|
+
export declare function buildOperationalEnvBase(opts: {
|
|
8
|
+
/** The TARGET node's cwd — never the calling/daemon process's own ambient
|
|
9
|
+
* `process.cwd()`. Threaded into `readMergedLaunchConfig` so source C
|
|
10
|
+
* (`spawnEnv.allow`) resolves from the TARGET's own project-scope config,
|
|
11
|
+
* not whichever cwd happened to launch the process that builds this env. */
|
|
12
|
+
targetCwd: string;
|
|
13
|
+
/** The TARGET node's profile id — never the calling process's own ambient
|
|
14
|
+
* `CRTR_PROFILE_ID`. `null`/`''` for no profile. */
|
|
15
|
+
targetProfileId: string | null;
|
|
16
|
+
host?: NodeJS.ProcessEnv;
|
|
17
|
+
}): NodeJS.ProcessEnv;
|
|
18
|
+
/** The one authoritative broker child env: sources A–C (the operational
|
|
19
|
+
* base, resolved from the TARGET node's own cwd/profile — `inv.env`'s
|
|
20
|
+
* `CRTR_NODE_CWD`/`CRTR_PROFILE_ID`, never this process's ambient
|
|
21
|
+
* `process.cwd()`/`CRTR_PROFILE_ID`, which may be a launcher's or the
|
|
22
|
+
* daemon's own and belong to an unrelated scope) plus D — the trusted,
|
|
23
|
+
* crtr-constructed `inv.env` overlay and the fork-bomb recursion guard
|
|
24
|
+
* `FRONT_DOOR_ENV=1`. Every broker launch (front-door root, managed child,
|
|
25
|
+
* `--root`, every revive/daemon relaunch) funnels through
|
|
26
|
+
* `headlessBrokerHost.launch()`, which calls this. */
|
|
27
|
+
export declare function buildBrokerEnv(inv: PiInvocation): NodeJS.ProcessEnv;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// The spawn-env boundary (design: C-1 crtr substrate spawn-env boundary).
|
|
2
|
+
//
|
|
3
|
+
// One authoritative builder for every broker child's process env. The prior
|
|
4
|
+
// shape was `{ ...process.env, ...inv.env }` — the whole host env, including
|
|
5
|
+
// every ambient credential (`MISTRAL_API_KEY`, `GITHUB_TOKEN`, `AWS_*`, …),
|
|
6
|
+
// inherited verbatim into the broker and, through it, into pi's bash tool
|
|
7
|
+
// (`printenv`/`ps eww` both expose it). This module replaces that spread with
|
|
8
|
+
// a default-deny allowlist: nothing in `process.env` crosses unless it is a
|
|
9
|
+
// literal non-secret operational name (A1), a value-guarded proxy var (A2), a
|
|
10
|
+
// configured model-provider key (B), or an explicit `spawnEnv.allow` entry
|
|
11
|
+
// (C). `inv.env` (D) is crtr-constructed and trusted, so it overlays
|
|
12
|
+
// unconditionally.
|
|
13
|
+
//
|
|
14
|
+
// `GIT_*` is deliberately absent from the allowlist: the whole namespace is an
|
|
15
|
+
// auth/config-injection surface (`GIT_CONFIG_KEY_*`/`GIT_CONFIG_VALUE_*`,
|
|
16
|
+
// `GIT_ASKPASS`, `GIT_SSH_COMMAND`), not a non-secret operational family, and
|
|
17
|
+
// ordinary git needs none of it (identity/credential-helper config via
|
|
18
|
+
// `~/.gitconfig`, reached through the allowlisted `HOME`; SSH remotes via the
|
|
19
|
+
// allowlisted `SSH_AUTH_SOCK`). A deliberate `GIT_*` need goes through
|
|
20
|
+
// `spawnEnv.allow`.
|
|
21
|
+
import { readMergedLaunchConfig } from '../config.js';
|
|
22
|
+
import { FRONT_DOOR_ENV } from './front-door.js';
|
|
23
|
+
/** A1 — literal env-var names copied verbatim (no value inspection): a login
|
|
24
|
+
* shell plus dev toolchain needs exactly these, and none of them can carry a
|
|
25
|
+
* credential. */
|
|
26
|
+
const OPERATIONAL_LITERAL_NAMES = new Set([
|
|
27
|
+
'HOME',
|
|
28
|
+
'PATH',
|
|
29
|
+
'SHELL',
|
|
30
|
+
'USER',
|
|
31
|
+
'LOGNAME',
|
|
32
|
+
'PWD',
|
|
33
|
+
'TMPDIR',
|
|
34
|
+
'TERM',
|
|
35
|
+
'TERMINFO',
|
|
36
|
+
'COLORTERM',
|
|
37
|
+
'LANG',
|
|
38
|
+
'LANGUAGE',
|
|
39
|
+
'TZ',
|
|
40
|
+
'NO_PROXY',
|
|
41
|
+
'no_proxy',
|
|
42
|
+
'SSH_AUTH_SOCK',
|
|
43
|
+
// Bash safety-valve deadline override (ms) — an operator-wide knob set in
|
|
44
|
+
// the daemon's own ambient env, not a per-node secret.
|
|
45
|
+
'CRTR_BASH_VALVE_MS',
|
|
46
|
+
]);
|
|
47
|
+
/** A1 — prefixes admitted in full: each is a structurally non-secret family
|
|
48
|
+
* (locale, XDG base-dir paths, pi's own config/rendering knobs — pi's auth is
|
|
49
|
+
* file-backed in `auth.json`, never an env value). `GIT_` is NOT here — see
|
|
50
|
+
* module comment. */
|
|
51
|
+
const OPERATIONAL_PREFIXES = ['LC_', 'XDG_', 'PI_'];
|
|
52
|
+
/** A2 — proxy var names. Admitted only when the value's URL authority carries
|
|
53
|
+
* no userinfo component (no `user@` / `user:pass@`) — the one value-inspected
|
|
54
|
+
* entry in the whole builder, because a proxy var is the single operational
|
|
55
|
+
* name whose *value* (not name) can carry a secret. */
|
|
56
|
+
const PROXY_NAMES = ['HTTP_PROXY', 'HTTPS_PROXY', 'http_proxy', 'https_proxy'];
|
|
57
|
+
/** B — the exact env-var name of each crtr-configured model provider's API
|
|
58
|
+
* key. Deliberately an explicit provider→env-key mapping, NOT inferred from
|
|
59
|
+
* `MODEL_PROVIDER_KEYS` in launch.ts (that constant is a provider-ID list —
|
|
60
|
+
* `['anthropic', 'openai']` — not env-var names; conflating the two would
|
|
61
|
+
* silently admit the wrong names the moment either list changes). */
|
|
62
|
+
const PROVIDER_ENV_KEYS = {
|
|
63
|
+
anthropic: 'ANTHROPIC_API_KEY',
|
|
64
|
+
openai: 'OPENAI_API_KEY',
|
|
65
|
+
};
|
|
66
|
+
/** True when `value` is an absolute `http(s)` proxy URL with a host and NO
|
|
67
|
+
* embedded userinfo (`user@` / `user:pass@` / `:pass@`) — the only shape
|
|
68
|
+
* allowed to cross by default. Rejects three leak shapes a naive
|
|
69
|
+
* `new URL(value).username === ''` check misses:
|
|
70
|
+
* - password-only authority (`http://:pass@host`) — `username` is empty but
|
|
71
|
+
* `password` is not, so both must be checked.
|
|
72
|
+
* - scheme-less userinfo (`user:pass@host:3128`, no `http(s)://` prefix) —
|
|
73
|
+
* `URL` parses the text before the first `:` as a CUSTOM scheme (here
|
|
74
|
+
* `user:`), leaving `username`/`password` both empty even though the
|
|
75
|
+
* value plainly carries credentials. Requiring `protocol` to be exactly
|
|
76
|
+
* `http:`/`https:` rejects this shape before userinfo parsing could ever
|
|
77
|
+
* be fooled by it, rather than normalizing/re-parsing a scheme-less form.
|
|
78
|
+
* - anything else unparseable, protocol-mismatched, or hostless — safe-fail
|
|
79
|
+
* to "never admit" rather than silently passing a value through. */
|
|
80
|
+
function isCredentialFreeProxyValue(value) {
|
|
81
|
+
let parsed;
|
|
82
|
+
try {
|
|
83
|
+
parsed = new URL(value);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
|
|
89
|
+
return false;
|
|
90
|
+
if (parsed.hostname === '')
|
|
91
|
+
return false;
|
|
92
|
+
if (parsed.username !== '' || parsed.password !== '')
|
|
93
|
+
return false;
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
/** Anchored FULL-STRING glob match for one `spawnEnv.allow` entry: `*` matches
|
|
97
|
+
* any (possibly empty) run of characters; every other character — including
|
|
98
|
+
* regex metacharacters — matches literally. A pattern with no `*` is a plain
|
|
99
|
+
* exact-name match (the common case, and the only shape prior to this fix).
|
|
100
|
+
* A bare/all-`*` "match everything" pattern is rejected earlier, at config
|
|
101
|
+
* read (`core/config.ts`'s `mergeSpawnEnv`), so it can never reach here. */
|
|
102
|
+
function matchesAllowPattern(name, pattern) {
|
|
103
|
+
if (!pattern.includes('*'))
|
|
104
|
+
return name === pattern;
|
|
105
|
+
const escaped = pattern
|
|
106
|
+
.split('*')
|
|
107
|
+
.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
|
108
|
+
.join('.*');
|
|
109
|
+
return new RegExp(`^${escaped}$`).test(name);
|
|
110
|
+
}
|
|
111
|
+
/** Sources A–C: the operational base shared by both the broker boundary (this
|
|
112
|
+
* module's `buildBrokerEnv`) and the predicate-eval `when_script` path
|
|
113
|
+
* (`daemon/predicate-eval.ts`) — the single shared definition of "non-secret
|
|
114
|
+
* host state the runtime exposes to any spawned shell." Does not include
|
|
115
|
+
* `inv.env`/`FRONT_DOOR_ENV` (source D), which is broker-specific. */
|
|
116
|
+
export function buildOperationalEnvBase(opts) {
|
|
117
|
+
const host = opts.host ?? process.env;
|
|
118
|
+
const out = {};
|
|
119
|
+
// A1 — literal names + non-secret prefixes, copied verbatim.
|
|
120
|
+
for (const [key, value] of Object.entries(host)) {
|
|
121
|
+
if (value === undefined)
|
|
122
|
+
continue;
|
|
123
|
+
if (OPERATIONAL_LITERAL_NAMES.has(key) || OPERATIONAL_PREFIXES.some((prefix) => key.startsWith(prefix))) {
|
|
124
|
+
out[key] = value;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// A2 — proxy vars, admitted only past the userinfo guard.
|
|
128
|
+
for (const key of PROXY_NAMES) {
|
|
129
|
+
const value = host[key];
|
|
130
|
+
if (value !== undefined && isCredentialFreeProxyValue(value))
|
|
131
|
+
out[key] = value;
|
|
132
|
+
}
|
|
133
|
+
// B — configured model-provider keys, by exact env-var name.
|
|
134
|
+
for (const envKey of Object.values(PROVIDER_ENV_KEYS)) {
|
|
135
|
+
const value = host[envKey];
|
|
136
|
+
if (value !== undefined)
|
|
137
|
+
out[envKey] = value;
|
|
138
|
+
}
|
|
139
|
+
// C — explicit config-declared extra allowlist (literal names and/or
|
|
140
|
+
// globs), resolved from the TARGET's own cwd/profile scope (default empty).
|
|
141
|
+
const allow = readMergedLaunchConfig(opts.targetCwd, opts.targetProfileId).spawnEnv.allow;
|
|
142
|
+
if (allow.length > 0) {
|
|
143
|
+
for (const [key, value] of Object.entries(host)) {
|
|
144
|
+
if (value === undefined)
|
|
145
|
+
continue;
|
|
146
|
+
if (allow.some((pattern) => matchesAllowPattern(key, pattern)))
|
|
147
|
+
out[key] = value;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
/** The one authoritative broker child env: sources A–C (the operational
|
|
153
|
+
* base, resolved from the TARGET node's own cwd/profile — `inv.env`'s
|
|
154
|
+
* `CRTR_NODE_CWD`/`CRTR_PROFILE_ID`, never this process's ambient
|
|
155
|
+
* `process.cwd()`/`CRTR_PROFILE_ID`, which may be a launcher's or the
|
|
156
|
+
* daemon's own and belong to an unrelated scope) plus D — the trusted,
|
|
157
|
+
* crtr-constructed `inv.env` overlay and the fork-bomb recursion guard
|
|
158
|
+
* `FRONT_DOOR_ENV=1`. Every broker launch (front-door root, managed child,
|
|
159
|
+
* `--root`, every revive/daemon relaunch) funnels through
|
|
160
|
+
* `headlessBrokerHost.launch()`, which calls this. */
|
|
161
|
+
export function buildBrokerEnv(inv) {
|
|
162
|
+
const targetCwd = inv.env['CRTR_NODE_CWD'] ?? process.cwd();
|
|
163
|
+
const targetProfileId = inv.env['CRTR_PROFILE_ID'] || null;
|
|
164
|
+
return {
|
|
165
|
+
...buildOperationalEnvBase({ targetCwd, targetProfileId }),
|
|
166
|
+
...inv.env,
|
|
167
|
+
[FRONT_DOOR_ENV]: '1',
|
|
168
|
+
};
|
|
169
|
+
}
|
|
@@ -7,9 +7,10 @@ export interface BootRootOpts {
|
|
|
7
7
|
/** Optional starter prompt (bare `crtr` requires none). */
|
|
8
8
|
prompt?: string;
|
|
9
9
|
/** Explicit `--profile <id-or-name>` on the front-door invocation, validated
|
|
10
|
-
* through `loadProfileManifest`. Omit to run the MRU/create-or-root
|
|
11
|
-
* selector for `cwd` (see `selectProfileForCwd`) — a root has no
|
|
12
|
-
* inherit from, so this is the front door's only source of profile
|
|
10
|
+
* through `loadProfileManifest`. Omit to run the MRU/create-or-root-profile
|
|
11
|
+
* startup selector for `cwd` (see `selectProfileForCwd`) — a root has no
|
|
12
|
+
* spawner to inherit from, so this is the front door's only source of profile
|
|
13
|
+
* identity. */
|
|
13
14
|
profile?: string | null;
|
|
14
15
|
/** `crtr --pick-profile`: force the startup profile chooser to open even when
|
|
15
16
|
* a per-cwd default is pinned, so the user can re-pin or unpin it. Without
|
|
@@ -56,15 +57,23 @@ export interface SpawnChildOpts {
|
|
|
56
57
|
/** Select the profile this node runs under — an exact profile id or a unique
|
|
57
58
|
* manifest name, validated through `loadProfileManifest`. Omit to INHERIT the
|
|
58
59
|
* spawner's current `profile_id` (managed child or --root alike — --root only
|
|
59
|
-
* means top-level, it does not reset to
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* the
|
|
60
|
+
* means top-level, it does not reset to a different profile; a shell root
|
|
61
|
+
* falls back to the stable root profile in the startup selector). Passed
|
|
62
|
+
* through unchanged into a deferred trigger recipe so a triggered birth
|
|
63
|
+
* re-resolves the SAME choice at fire time (an explicit value re-validates;
|
|
64
|
+
* inheritance re-reads the spawner's THEN-current profile). */
|
|
63
65
|
profile?: string | null;
|
|
64
66
|
/** Create a managed worktree for this node and pin its cwd to it. */
|
|
65
67
|
worktree?: boolean;
|
|
66
68
|
/** Preallocated node id, used when a managed worktree must be named before birth. */
|
|
67
69
|
nodeId?: string;
|
|
70
|
+
/** Hidden ambient "current situation" text — NOT the visible prompt/body.
|
|
71
|
+
* Persisted via `appendSituationalContext` BEFORE launch so canvas-context-
|
|
72
|
+
* intro's session_start bearings carry it as a `<situational-context>`
|
|
73
|
+
* sibling from the node's very first turn. Passed through unchanged into a
|
|
74
|
+
* deferred trigger recipe (the same field survives a scheduled node_birth).
|
|
75
|
+
* Omit for no ambient context. */
|
|
76
|
+
situationalContext?: string;
|
|
68
77
|
}
|
|
69
78
|
/** Resolve a `--fork-from` value to an ABSOLUTE `.jsonl` source path for the
|
|
70
79
|
* broker fork (`SessionManager.forkFrom`, which loads a file — never a bare id).
|
|
@@ -21,6 +21,7 @@ import { gitSync } from '../git.js';
|
|
|
21
21
|
import { createManagedWorktree } from '../worktree.js';
|
|
22
22
|
import { usage } from '../errors.js';
|
|
23
23
|
import { writeGoal } from './kickoff.js';
|
|
24
|
+
import { appendSituationalContext } from './situational-context.js';
|
|
24
25
|
import { hasRoadmap, seedRoadmap } from './roadmap.js';
|
|
25
26
|
import { buildIdentityAssertion, buildWakeBearings } from './bearings.js';
|
|
26
27
|
import { installMenuBinding, installNavBindings, installViewNavBindings } from './tmux-chrome.js';
|
|
@@ -68,7 +69,8 @@ export async function bootRoot(opts) {
|
|
|
68
69
|
const kind = opts.kind ?? 'general';
|
|
69
70
|
// The front door's only source of profile identity — a root has no spawner
|
|
70
71
|
// to inherit from. Runs BEFORE spawnNode: explicit --profile > MRU profile
|
|
71
|
-
// covering cwd > a synchronous create-or-root prompt (
|
|
72
|
+
// covering cwd > a synchronous create-or-root-profile prompt (the stable root
|
|
73
|
+
// profile, headless).
|
|
72
74
|
const profileId = await selectProfileForCwd(opts.cwd, opts.profile, opts.pickProfile ?? false);
|
|
73
75
|
// A born-resident root starts in base mode; it earns the orchestrator persona
|
|
74
76
|
// the first time it delegates (or on promotion). Resident lifecycle either way.
|
|
@@ -258,7 +260,7 @@ export function resolveForkSource(value) {
|
|
|
258
260
|
* there is NO spawner at all — `crtr node new --root` run directly from a
|
|
259
261
|
* shell, not from inside a node — there is no profile to inherit, so this runs
|
|
260
262
|
* the same startup selector the front door uses (MRU covering `cwd`, else the
|
|
261
|
-
*
|
|
263
|
+
* root-profile prompt/default). */
|
|
262
264
|
async function resolveProfileId(explicit, spawner, cwd) {
|
|
263
265
|
if (explicit !== undefined && explicit !== null && explicit !== '') {
|
|
264
266
|
return loadProfileManifest(explicit).profileId;
|
|
@@ -347,6 +349,10 @@ export async function spawnChild(opts) {
|
|
|
347
349
|
});
|
|
348
350
|
// Persist the task as the child's goal for a fresh revive to re-read.
|
|
349
351
|
writeGoal(meta.node_id, opts.prompt);
|
|
352
|
+
// Hidden ambient context, written BEFORE the engine launches so canvas-
|
|
353
|
+
// context-intro's session_start bearings carry it from the first turn.
|
|
354
|
+
if (opts.situationalContext !== undefined)
|
|
355
|
+
appendSituationalContext(meta.node_id, opts.situationalContext);
|
|
350
356
|
// A fork inherits the SOURCE node's entire first-person conversation, so the
|
|
351
357
|
// identity re-assertion must ride the STRONGEST channel too, not only the
|
|
352
358
|
// session-start bearings (a trailing custom_message the model weighs only by
|
|
@@ -42,7 +42,13 @@ export function clearSessionCache() {
|
|
|
42
42
|
* `listFn` is injected by the caller to avoid circular imports at module init. */
|
|
43
43
|
export function cachedAllMemoryDocs(listFn) {
|
|
44
44
|
if (_cache.allMemoryDocs === null) {
|
|
45
|
-
|
|
45
|
+
// The boot pipeline (render + on-read) EXCLUDES node-scope docs: node-local
|
|
46
|
+
// memory surfaces at boot via render.ts's nodeLocalDocs (any kind, floored
|
|
47
|
+
// into the knowledge block), NOT through the resolver corpus, so a node doc
|
|
48
|
+
// must not also enter here (it would double-surface at boot and could fire
|
|
49
|
+
// on-read). The `node` scope reaches CLI read/list/find/delete through the
|
|
50
|
+
// uncached listAllMemoryDocs() path instead.
|
|
51
|
+
_cache.allMemoryDocs = listFn().filter((d) => d.scope !== 'node');
|
|
46
52
|
}
|
|
47
53
|
return _cache.allMemoryDocs;
|
|
48
54
|
}
|
|
@@ -24,7 +24,7 @@ export interface NodeConfigSubject {
|
|
|
24
24
|
};
|
|
25
25
|
/** DERIVED: `meta.profile_id` resolved to its manifest's mutable `name` (NOT
|
|
26
26
|
* the stable directory id — gate authors write `gate: {profile: <name>}`).
|
|
27
|
-
* null for
|
|
27
|
+
* null for historical null-profile rows, and null (never throwing) when the
|
|
28
28
|
* profile has been deleted or its manifest is unreadable — this is a HOT,
|
|
29
29
|
* every-doc-render path and must never brick boot/on-read gating. */
|
|
30
30
|
profile: string | null;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// remote-canvas-target.ts — resolves a named `remoteCanvas.targets.<name>`
|
|
2
|
+
// config entry (see `RemoteCanvasTarget`, `src/types.ts`) into a `RelayTarget`
|
|
3
|
+
// ready for `RelayTransport` (`src/clients/attach/transport-relay.ts`).
|
|
4
|
+
//
|
|
5
|
+
// The raw bearer token NEVER lives in config or argv — only the name of an
|
|
6
|
+
// environment variable holding it (`relayTokenEnv`), read here at connect
|
|
7
|
+
// time. This is the Phase 1 interim contract; Phase 3 owns the real
|
|
8
|
+
// secret-store-backed `crtr canvas config`/`crtr canvas use` CLI.
|
|
9
|
+
import { usage } from '../errors.js';
|
|
10
|
+
import { readConfig } from '../config.js';
|
|
11
|
+
export function resolveRemoteCanvasTarget(name, scope = 'user') {
|
|
12
|
+
const cfg = readConfig(scope);
|
|
13
|
+
const target = cfg.remoteCanvas.targets[name];
|
|
14
|
+
if (target === undefined) {
|
|
15
|
+
const known = Object.keys(cfg.remoteCanvas.targets);
|
|
16
|
+
throw usage(`no remote canvas target named '${name}'` +
|
|
17
|
+
(known.length > 0 ? ` — configured: ${known.join(', ')}` : ' — none configured yet') +
|
|
18
|
+
`. Add remoteCanvas.targets.${name} to config.json (run \`crtr sys config path\`): {"previewEndpoint": "https://<hash>.preview.bl.run", "relayTokenEnv": "<ENV_VAR_NAME>"}.`);
|
|
19
|
+
}
|
|
20
|
+
const relayToken = process.env[target.relayTokenEnv];
|
|
21
|
+
if (relayToken === undefined || relayToken === '') {
|
|
22
|
+
throw usage(`remote canvas target '${name}' needs its relay token in $${target.relayTokenEnv} — that environment variable is unset. Export it before running this command; the token is never read from config or argv.`);
|
|
23
|
+
}
|
|
24
|
+
return { previewEndpoint: target.previewEndpoint, relayToken };
|
|
25
|
+
}
|
package/dist/core/worktree.d.ts
CHANGED
|
@@ -20,14 +20,14 @@ export interface CloseManagedWorktreeResult {
|
|
|
20
20
|
node_id: string;
|
|
21
21
|
branch: string;
|
|
22
22
|
worktree_path: string;
|
|
23
|
-
|
|
24
|
-
/** False when the
|
|
25
|
-
* automatically afterward — the close still counts as done (state is
|
|
23
|
+
landed_sha: string;
|
|
24
|
+
/** False when the landing happened but the worktree checkout could not be
|
|
25
|
+
* removed automatically afterward — the close still counts as done (state is
|
|
26
26
|
* closed, push-final is unblocked); `worktree_remove_error` carries the
|
|
27
27
|
* manual cleanup note. */
|
|
28
28
|
worktree_removed: boolean;
|
|
29
29
|
worktree_remove_error?: string;
|
|
30
|
-
/** False when the
|
|
30
|
+
/** False when the landing happened but the local branch could not be deleted
|
|
31
31
|
* afterward — same non-fatal treatment as worktree_removed. */
|
|
32
32
|
branch_deleted: boolean;
|
|
33
33
|
branch_delete_error?: string;
|