@north-light/crouter 0.3.175 → 0.3.177
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 +4 -4
- package/dist/api/client.js +2 -2
- package/dist/api/dto/lifecycle.d.ts +1 -1
- package/dist/api/dto/nodes.d.ts +7 -1
- package/dist/api/dto/profiles.d.ts +3 -0
- package/dist/api/dto/reports.d.ts +1 -1
- package/dist/clients/attach/__tests__/group-activity.test.js +2 -2
- package/dist/clients/attach/config.js +5 -5
- package/dist/clients/attach/input/clipboard-image.js +2 -2
- package/dist/clients/attach/input/ref-autocomplete.js +1 -1
- package/dist/clients/attach/input/titled-editor.d.ts +12 -13
- package/dist/clients/attach/input/titled-editor.js +14 -15
- package/dist/clients/attach/overlays/dialogs.d.ts +1 -1
- package/dist/clients/attach/overlays/dialogs.js +2 -2
- package/dist/clients/attach/overlays/mcp.js +5 -12
- package/dist/clients/attach/render/chat-view.d.ts +1 -1
- package/dist/clients/attach/render/chat-view.js +2 -2
- package/dist/clients/attach/render/crtr-output.d.ts +3 -3
- package/dist/clients/attach/render/crtr-output.js +25 -13
- package/dist/clients/attach/render/group-recap.js +7 -0
- package/dist/clients/attach/session/context.js +3 -3
- package/dist/clients/attach/session/editor-inventory.js +6 -6
- package/dist/clients/attach/session/input-wiring.js +1 -1
- package/dist/clients/attach/session/teardown.js +7 -9
- package/dist/clients/attach/slash/dispatch.d.ts +1 -2
- package/dist/clients/attach/slash/dispatch.js +4 -8
- package/dist/clients/attach/viewer.js +536 -566
- package/dist/clients/inbox/controller.js +3 -2
- package/dist/clients/inbox/deck-adapter.js +3 -2
- package/dist/clients/surfaces/host.d.ts +1 -1
- package/dist/commands/api-client.d.ts +12 -13
- package/dist/commands/api-client.js +15 -16
- package/dist/commands/chord.js +1 -2
- package/dist/commands/human/queue.js +6 -5
- package/dist/commands/memory/find.js +3 -3
- package/dist/commands/node/create.js +7 -3
- package/dist/commands/node/lifecycle.js +1 -1
- package/dist/commands/node-lifecycle-revive.d.ts +1 -1
- package/dist/commands/node-lifecycle-revive.js +3 -3
- package/dist/commands/profile/list.js +3 -1
- package/dist/commands/profile/new.js +4 -1
- package/dist/commands/profile/project.js +23 -9
- package/dist/commands/profile/show.js +3 -1
- package/dist/commands/profile.js +2 -2
- package/dist/commands/push.js +1 -1
- package/dist/commands/surface/node/placement.d.ts +1 -1
- package/dist/commands/surface/node/placement.js +1 -1
- package/dist/commands/surface-edit.js +1 -4
- package/dist/commands/sys/logs.js +4 -2
- package/dist/commands/sys/panels/panel.d.ts +3 -3
- package/dist/commands/sys/panels/panel.js +3 -3
- package/dist/commands/sys/setup-core.js +1 -7
- package/dist/commands/sys/sync-deps.js +1 -3
- package/dist/commands/sys/sync-project-guidance.js +2 -11
- package/dist/commands/sys/sysprompt.js +1 -3
- package/dist/core/__tests__/branded-host-concurrency.test.js +1 -1
- package/dist/core/__tests__/fixtures/fake-engine.js +1 -1
- package/dist/core/__tests__/helpers/harness.d.ts +1 -1
- package/dist/core/__tests__/helpers/harness.js +1 -1
- package/dist/core/__tests__/on-read-crouter-home-fence.test.js +1 -1
- package/dist/core/__tests__/on-read-dedup-resume.test.js +1 -1
- package/dist/core/canvas/boot.js +1 -1
- package/dist/core/canvas/browse/app.js +2 -2
- package/dist/core/canvas/canvas.d.ts +3 -3
- package/dist/core/canvas/canvas.js +3 -3
- package/dist/core/canvas/daemon-owner.js +1 -2
- package/dist/core/canvas/db.js +7 -8
- package/dist/core/canvas/extensions.d.ts +1 -1
- package/dist/core/canvas/extensions.js +7 -1
- package/dist/core/canvas/index.js +2 -2
- package/dist/core/canvas/pid.d.ts +12 -14
- package/dist/core/canvas/pid.js +16 -20
- package/dist/core/canvas/remote-transport.js +2 -2
- package/dist/core/canvas/status-glyph.d.ts +1 -1
- package/dist/core/canvas/status-glyph.js +1 -1
- package/dist/core/canvas/types.d.ts +1 -1
- package/dist/core/command-manifests/manifest.js +4 -6
- package/dist/core/command-manifests/schema.js +10 -12
- package/dist/core/command-plugins/transport/exec-invoke.js +5 -7
- package/dist/core/command-plugins/transport/http-invoke.js +14 -8
- package/dist/core/command.js +3 -1
- package/dist/core/config.d.ts +1 -1
- package/dist/core/config.js +6 -29
- package/dist/core/events/read.js +1 -3
- package/dist/core/exclusive-lock.d.ts +14 -0
- package/dist/core/exclusive-lock.js +124 -0
- package/dist/core/fault-classifier.js +1 -3
- package/dist/core/fs-utils.d.ts +7 -0
- package/dist/core/fs-utils.js +29 -2
- package/dist/core/host-exports/export.d.ts +4 -5
- package/dist/core/host-exports/export.js +4 -5
- package/dist/core/human/claim.js +4 -2
- package/dist/core/human/convention.d.ts +0 -11
- package/dist/core/human/convention.js +1 -131
- package/dist/core/human/scan.js +5 -4
- package/dist/core/io.d.ts +23 -0
- package/dist/core/io.js +60 -0
- package/dist/core/preview-registry.d.ts +10 -5
- package/dist/core/preview-registry.js +136 -2537
- package/dist/core/profiles/manifest.d.ts +13 -2
- package/dist/core/profiles/manifest.js +58 -49
- package/dist/core/review/store.d.ts +4 -1
- package/dist/core/review/store.js +10 -0
- package/dist/core/review/types.d.ts +1 -1
- package/dist/core/review/types.js +1 -1
- package/dist/core/runtime/branded-host.js +6 -84
- package/dist/core/runtime/broker/frame-dispatch.js +4 -4
- package/dist/core/runtime/broker-cli.js +1 -1
- package/dist/core/runtime/broker-extension-render.js +1 -1
- package/dist/core/runtime/broker-protocol.d.ts +9 -10
- package/dist/core/runtime/broker-protocol.js +9 -10
- package/dist/core/runtime/broker-sdk.d.ts +1 -1
- package/dist/core/runtime/broker.js +6 -6
- package/dist/core/runtime/canvas-extensions.d.ts +1 -0
- package/dist/core/runtime/canvas-extensions.js +2 -0
- package/dist/core/runtime/host.js +14 -14
- package/dist/core/runtime/launch.d.ts +1 -1
- package/dist/core/runtime/launch.js +1 -1
- package/dist/core/runtime/lifecycle.js +16 -21
- package/dist/core/runtime/node-read.js +2 -3
- package/dist/core/runtime/package-health.js +1 -8
- package/dist/core/runtime/persona.js +2 -2
- package/dist/core/runtime/pi-vendored.d.ts +4 -2
- package/dist/core/runtime/pi-vendored.js +5 -16
- package/dist/core/runtime/promote.js +1 -1
- package/dist/core/runtime/reopen.js +2 -2
- package/dist/core/runtime/revive-all.d.ts +3 -3
- package/dist/core/runtime/revive-all.js +6 -6
- package/dist/core/runtime/revive.js +1 -1
- package/dist/core/runtime/spawn.d.ts +1 -2
- package/dist/core/runtime/spawn.js +2 -3
- package/dist/core/runtime/structured-output.js +4 -6
- package/dist/core/runtime/tmux-bindings.js +1 -1
- package/dist/core/runtime/tmux-driver.d.ts +2 -2
- package/dist/core/runtime/tmux-driver.js +3 -5
- package/dist/core/runtime/tool-group-summary.js +5 -4
- package/dist/core/runtime/warm-pool.d.ts +9 -8
- package/dist/core/runtime/warm-pool.js +58 -62
- package/dist/core/scope.js +2 -9
- package/dist/core/self-update.js +3 -3
- package/dist/core/spawn.d.ts +0 -1
- package/dist/core/spawn.js +1 -4
- package/dist/core/substrate/index.d.ts +2 -1
- package/dist/core/substrate/index.js +2 -1
- package/dist/core/substrate/on-read-node.d.ts +7 -0
- package/dist/core/substrate/on-read-node.js +37 -0
- package/dist/core/substrate/on-read.d.ts +7 -5
- package/dist/core/substrate/on-read.js +16 -32
- package/dist/core/termrender/termrender.js +16 -96
- package/dist/core/wake.js +2 -2
- package/dist/core/worktree.d.ts +3 -3
- package/dist/core/worktree.js +16 -107
- package/dist/daemon/api/bridge.js +1 -1
- package/dist/daemon/api/handlers/broker-ops.js +1 -2
- package/dist/daemon/api/handlers/messages.js +2 -2
- package/dist/daemon/api/handlers/nodes.js +33 -10
- package/dist/daemon/api/handlers/reports.js +1 -1
- package/dist/daemon/api/handlers/review-comments.js +5 -44
- package/dist/daemon/api/handlers/reviews.js +5 -53
- package/dist/daemon/api/handlers/validate.d.ts +23 -0
- package/dist/daemon/api/handlers/validate.js +58 -0
- package/dist/daemon/api/map.js +1 -0
- package/dist/daemon/cron-run.js +7 -7
- package/dist/daemon/human/finish.js +3 -2
- package/dist/daemon/manage.d.ts +6 -6
- package/dist/daemon/manage.js +15 -16
- package/dist/daemon/review/finish.js +1 -10
- package/dist/pi-extensions/canvas-context-intro.js +1 -14
- package/dist/pi-extensions/canvas-doc-substrate.js +2 -2
- package/dist/pi-extensions/canvas-preview-result.d.ts +22 -0
- package/dist/pi-extensions/canvas-preview-result.js +60 -0
- package/dist/pi-extensions/canvas-review-boundary.js +1 -14
- package/dist/pi-extensions/truncate.d.ts +5 -0
- package/dist/pi-extensions/truncate.js +14 -0
- package/dist/shared/predicates.d.ts +2 -0
- package/dist/shared/predicates.js +4 -0
- package/dist/shared/shell-quote.d.ts +3 -0
- package/dist/shared/shell-quote.js +5 -0
- package/dist/shared/tool-groups.d.ts +4 -2
- package/dist/shared/tool-groups.js +3 -3
- package/dist/types.d.ts +8 -5
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/dist/core/substrate/on-read-pure.d.ts +0 -7
- package/dist/core/substrate/on-read-pure.js +0 -272
|
@@ -14,6 +14,7 @@ import { detectNerdFont, nerdFontInstallCommand, NERD_FONT_SELECT_HINT } from '.
|
|
|
14
14
|
import { ensureRenderer, isRendererReady } from '../../core/termrender/termrender.js';
|
|
15
15
|
import { installFromMarketplace } from '../pkg/plugin-manage.js';
|
|
16
16
|
import { findPluginByName } from '../../core/resolver.js';
|
|
17
|
+
import { expandTilde } from '../../core/fs-utils.js';
|
|
17
18
|
function isKeybindingsObject(value) {
|
|
18
19
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
19
20
|
}
|
|
@@ -214,13 +215,6 @@ export function buildSetupManifest(resolveBuiltinPackageDir = builtinPiPackageDi
|
|
|
214
215
|
};
|
|
215
216
|
});
|
|
216
217
|
}
|
|
217
|
-
function expandTilde(pathValue, homeDir) {
|
|
218
|
-
if (pathValue === '~')
|
|
219
|
-
return homeDir;
|
|
220
|
-
if (pathValue.startsWith('~/'))
|
|
221
|
-
return join(homeDir, pathValue.slice(2));
|
|
222
|
-
return pathValue;
|
|
223
|
-
}
|
|
224
218
|
function npmPackageName(source) {
|
|
225
219
|
if (!source.startsWith('npm:'))
|
|
226
220
|
return null;
|
|
@@ -32,13 +32,11 @@ import { parseFrontmatterGeneric } from '../../core/frontmatter.js';
|
|
|
32
32
|
import { pathExists, readJsonIfExists, readText, walkFiles, writeText } from '../../core/fs-utils.js';
|
|
33
33
|
import { serializeMemoryDoc } from '../memory/shared.js';
|
|
34
34
|
import { routeFromDescription, scalarString } from './sync-shared.js';
|
|
35
|
+
import { isRecord } from '../../shared/predicates.js';
|
|
35
36
|
/** Marker value written to the generated top INDEX's `generated-by` field —
|
|
36
37
|
* the wipe guard: `deps` only ever deletes a deps/ dir whose INDEX carries
|
|
37
38
|
* this marker. */
|
|
38
39
|
export const DEPS_GENERATED_MARKER = 'crtr sys sync deps';
|
|
39
|
-
function isRecord(value) {
|
|
40
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
41
|
-
}
|
|
42
40
|
function invalidDepsConfig(message) {
|
|
43
41
|
throw usage(`--deps: invalid package.json crtr.sync.deps.exclude: ${message}`);
|
|
44
42
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// `.crouter/memory/`. Each project guide becomes the root INDEX front door;
|
|
4
4
|
// rules retain their explicit path routes, or use the workspace-open `.` route
|
|
5
5
|
// when they are pathless.
|
|
6
|
-
import { existsSync, readdirSync,
|
|
6
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
7
7
|
import { basename, dirname, join, relative, resolve, sep } from 'node:path';
|
|
8
8
|
import { defineLeaf } from '../../core/command.js';
|
|
9
9
|
import { parseFrontmatterGeneric } from '../../core/frontmatter.js';
|
|
@@ -13,6 +13,7 @@ import { loadProfileManifest } from '../../core/profiles/manifest.js';
|
|
|
13
13
|
import { CRTR_DIR_NAME } from '../../types.js';
|
|
14
14
|
import { memoryFilePath, serializeMemoryDoc } from '../memory/shared.js';
|
|
15
15
|
import { normalizedName, routeFromDescription, scalarString } from './sync-shared.js';
|
|
16
|
+
import { realpathOrSelf } from '../../core/fs-utils.js';
|
|
16
17
|
// Directories a project-doc walk never descends into — build/dependency trees
|
|
17
18
|
// plus crouter's own state dir.
|
|
18
19
|
const PROJECT_DOC_JUNK_DIRS = new Set([
|
|
@@ -136,16 +137,6 @@ function gitWorktreeRoot(dir) {
|
|
|
136
137
|
const top = res.stdout.trim();
|
|
137
138
|
return top === '' ? undefined : resolve(top);
|
|
138
139
|
}
|
|
139
|
-
/** `realpathSync`, tolerant of a path that doesn't exist or can't be resolved
|
|
140
|
-
* (falls back to the path as given). */
|
|
141
|
-
function realpathOrSelf(p) {
|
|
142
|
-
try {
|
|
143
|
-
return realpathSync(p);
|
|
144
|
-
}
|
|
145
|
-
catch {
|
|
146
|
-
return p;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
140
|
/** First Markdown H1 heading in the doc body, heading marker stripped. */
|
|
150
141
|
function h1Title(body) {
|
|
151
142
|
for (const raw of body.split('\n')) {
|
|
@@ -7,9 +7,7 @@ import { jobDir } from '../../core/canvas/paths.js';
|
|
|
7
7
|
import { openNodeWindow } from '../../core/runtime/placement-tmux.js';
|
|
8
8
|
import { ApiError } from '../../api/index.js';
|
|
9
9
|
import { cliClient } from '../api-client.js';
|
|
10
|
-
|
|
11
|
-
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
12
|
-
}
|
|
10
|
+
import { shellQuote } from '../../shared/shell-quote.js';
|
|
13
11
|
export const sysSyspromptLeaf = defineLeaf({
|
|
14
12
|
name: 'sysprompt',
|
|
15
13
|
description: 'print a node\'s assembled system prompt',
|
|
@@ -35,7 +35,7 @@ test('concurrent processes reconcile one coherent branded host tuple', async ()
|
|
|
35
35
|
assert.ok(deadOwner !== undefined);
|
|
36
36
|
const abandonedLock = join(home, '.crouter', 'host.lock');
|
|
37
37
|
mkdirSync(abandonedLock);
|
|
38
|
-
writeFileSync(join(abandonedLock,
|
|
38
|
+
writeFileSync(join(abandonedLock, `owner.${deadOwner}.abandoned`), '');
|
|
39
39
|
try {
|
|
40
40
|
const moduleUrl = pathToFileURL(join(process.cwd(), 'src/core/runtime/branded-host.ts')).href;
|
|
41
41
|
const results = await Promise.all(Array.from({ length: 4 }, () => reconcileInChild(home, moduleUrl)));
|
|
@@ -139,7 +139,7 @@ export class SessionManager {
|
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
// ---------------------------------------------------------------------------
|
|
142
|
-
// AgentSessionEvent stream synthesis
|
|
142
|
+
// AgentSessionEvent stream synthesis. The fake emits a
|
|
143
143
|
// REALISTIC streaming assistant turn through session.subscribe — the SAME channel
|
|
144
144
|
// the real pi engine feeds the broker's fan-out — so the frames a `crtr surface attach`
|
|
145
145
|
// client receives are byte-for-byte real AgentSessionEvents. The EMITTED event
|
|
@@ -37,7 +37,7 @@ export interface HarnessOpts {
|
|
|
37
37
|
* harness does NOT gate on tmux, does NOT eagerly create the isolated tmux
|
|
38
38
|
* session, and skips the per-pane `set-environment` seeding — nothing in a
|
|
39
39
|
* headless run opens a window. CRTR_NODE_SESSION/CRTR_ROOT_SESSION are still
|
|
40
|
-
* scrubbed/restored for env hygiene, but
|
|
40
|
+
* scrubbed/restored for env hygiene, but do not route placement: a managed
|
|
41
41
|
* child opens NO viewer (it boots a detached broker, supervised by pid), and a
|
|
42
42
|
* --root opens one viewer in the caller's current session. Headless tests must
|
|
43
43
|
* use `spawnHeadlessChild`, never `spawnChild`. See createHeadlessHarness(). */
|
|
@@ -312,7 +312,7 @@ export async function createHarness(opts = {}) {
|
|
|
312
312
|
}
|
|
313
313
|
// Pre-create the isolated session on the DEFAULT server so teardown always
|
|
314
314
|
// has a kill-sessions target.
|
|
315
|
-
// ISOLATION ASSUMPTION (see header
|
|
315
|
+
// ISOLATION ASSUMPTION (see header): isolation is by SESSION NAME on
|
|
316
316
|
// the DEFAULT tmux server only. The runtime CLI shells `tmux` with no `-L`, so
|
|
317
317
|
// a custom-socket server (`tmux -L foo`) would be invisible to it; this harness
|
|
318
318
|
// therefore assumes the default socket and only ever kill-sessions, never the
|
|
@@ -13,7 +13,7 @@ import { join } from 'node:path';
|
|
|
13
13
|
import { closeDb } from '../canvas/db.js';
|
|
14
14
|
import { resetScopeCache } from '../scope.js';
|
|
15
15
|
import { spawnNode } from '../runtime/nodes.js';
|
|
16
|
-
import { renderOnReadDocs } from '../substrate/on-read.js';
|
|
16
|
+
import { renderOnReadDocs } from '../substrate/on-read-node.js';
|
|
17
17
|
let canvasHome;
|
|
18
18
|
let fakeHome;
|
|
19
19
|
let prevHomeEnv;
|
|
@@ -13,7 +13,7 @@ import { join } from 'node:path';
|
|
|
13
13
|
import { closeDb } from '../canvas/db.js';
|
|
14
14
|
import { resetScopeCache } from '../scope.js';
|
|
15
15
|
import { spawnNode } from '../runtime/nodes.js';
|
|
16
|
-
import { renderOnReadDocs } from '../substrate/on-read.js';
|
|
16
|
+
import { renderOnReadDocs } from '../substrate/on-read-node.js';
|
|
17
17
|
import { loadInjectedDocs, saveInjectedDocs, clearInjectedDocs, } from '../substrate/injected-store.js';
|
|
18
18
|
const FIXTURE_BODY = 'Fixture body that must surface exactly once.';
|
|
19
19
|
let home;
|
package/dist/core/canvas/boot.js
CHANGED
|
@@ -148,7 +148,7 @@ export function reconcileBootLiveness(current = currentBootIdentity()) {
|
|
|
148
148
|
const rows = db
|
|
149
149
|
.prepare('SELECT node_id FROM nodes WHERE pi_pid IS NOT NULL')
|
|
150
150
|
.all();
|
|
151
|
-
// Clear the launch-time identity baseline
|
|
151
|
+
// Clear the launch-time identity baseline
|
|
152
152
|
// alongside `pi_pid` — a boot resets the OS process table, so any recorded
|
|
153
153
|
// identity is as meaningless as the pid it was captured for; leaving it
|
|
154
154
|
// behind would let a coincidentally-reused pid on the NEW boot compare
|
|
@@ -358,8 +358,8 @@ export async function runBrowse(opts) {
|
|
|
358
358
|
// Coalesced search repaint. The expensive work per keystroke is recompute()
|
|
359
359
|
// (a fuzzyMatch flatten over the whole corpus) + a full renderFrame. Doing it
|
|
360
360
|
// synchronously on every key blocks the event loop, so fast typing batches into
|
|
361
|
-
// the stdin reads
|
|
362
|
-
// Instead, each key only mutates state.query (cheap) and marks the view dirty;
|
|
361
|
+
// the stdin reads, where a naive per-key handler drops batched chunks (see
|
|
362
|
+
// onKeySearch). Instead, each key only mutates state.query (cheap) and marks the view dirty;
|
|
363
363
|
// a single setImmediate drains AFTER every queued stdin 'data' callback (poll
|
|
364
364
|
// phase) has appended its chars, so N rapid keys collapse to ONE recompute+paint
|
|
365
365
|
// and no keystroke is ignored. The sort may still take a moment — that's fine.
|
|
@@ -33,7 +33,7 @@ export declare function setPresence(nodeId: string, presence: {
|
|
|
33
33
|
pane?: string | null;
|
|
34
34
|
}): void;
|
|
35
35
|
/** Record the live pi pid (daemon liveness signal) PLUS its launch-time
|
|
36
|
-
* identity fingerprint, in one atomic write
|
|
36
|
+
* identity fingerprint, in one atomic write:
|
|
37
37
|
* `capturePidIdentities([pid])` is a signal-0-adjacent `ps -p <pid>` probe
|
|
38
38
|
* taken RIGHT NOW, while `pid` is the process we just spawned/bound and
|
|
39
39
|
* therefore definitely still exists — this is the LAUNCH-time baseline
|
|
@@ -242,7 +242,7 @@ export type TerminalGuardResult<T> = {
|
|
|
242
242
|
} | {
|
|
243
243
|
kind: 'missing';
|
|
244
244
|
};
|
|
245
|
-
/**
|
|
245
|
+
/** Close the TOCTOU between checking a target has a natural cycle ahead
|
|
246
246
|
* of it and the write that depends on that (for example, a deferred inbox
|
|
247
247
|
* append). Fresh-reads `nodeId`'s
|
|
248
248
|
* (status, final_report) and either runs `body` (a write) or short-circuits
|
|
@@ -254,7 +254,7 @@ export type TerminalGuardResult<T> = {
|
|
|
254
254
|
* write left open. Pass the shared `hasNoNaturalCycle` predicate rather than
|
|
255
255
|
* a bespoke one so the two never drift.
|
|
256
256
|
*
|
|
257
|
-
*
|
|
257
|
+
* A dangling `nodeId` — the row deleted after the
|
|
258
258
|
* caller's own selection but before this guard's fresh read — MUST NEVER run
|
|
259
259
|
* `body`. Reporting `'missing'` distinctly from `'terminal'` (rather than
|
|
260
260
|
* folding it into either the terminal branch or a fabricated "safe" empty
|
|
@@ -243,7 +243,7 @@ export function setPresence(nodeId, presence) {
|
|
|
243
243
|
.run(presence.tmux_session ?? null, presence.window ?? null, presence.pane ?? null, nodeId);
|
|
244
244
|
}
|
|
245
245
|
/** Record the live pi pid (daemon liveness signal) PLUS its launch-time
|
|
246
|
-
* identity fingerprint, in one atomic write
|
|
246
|
+
* identity fingerprint, in one atomic write:
|
|
247
247
|
* `capturePidIdentities([pid])` is a signal-0-adjacent `ps -p <pid>` probe
|
|
248
248
|
* taken RIGHT NOW, while `pid` is the process we just spawned/bound and
|
|
249
249
|
* therefore definitely still exists — this is the LAUNCH-time baseline
|
|
@@ -633,7 +633,7 @@ export function settleDeadMessageWait(nodeId, controller, deliver) {
|
|
|
633
633
|
return result;
|
|
634
634
|
});
|
|
635
635
|
}
|
|
636
|
-
/**
|
|
636
|
+
/** Close the TOCTOU between checking a target has a natural cycle ahead
|
|
637
637
|
* of it and the write that depends on that (for example, a deferred inbox
|
|
638
638
|
* append). Fresh-reads `nodeId`'s
|
|
639
639
|
* (status, final_report) and either runs `body` (a write) or short-circuits
|
|
@@ -645,7 +645,7 @@ export function settleDeadMessageWait(nodeId, controller, deliver) {
|
|
|
645
645
|
* write left open. Pass the shared `hasNoNaturalCycle` predicate rather than
|
|
646
646
|
* a bespoke one so the two never drift.
|
|
647
647
|
*
|
|
648
|
-
*
|
|
648
|
+
* A dangling `nodeId` — the row deleted after the
|
|
649
649
|
* caller's own selection but before this guard's fresh read — MUST NEVER run
|
|
650
650
|
* `body`. Reporting `'missing'` distinctly from `'terminal'` (rather than
|
|
651
651
|
* folding it into either the terminal branch or a fabricated "safe" empty
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
// daemon-owner.ts — the canvas-layer authoritative daemon ownership primitive
|
|
2
|
-
// (Phase 1 runtime-boundary, task D0).
|
|
1
|
+
// daemon-owner.ts — the canvas-layer authoritative daemon ownership primitive.
|
|
3
2
|
//
|
|
4
3
|
// A pidfile check followed by a later write is not singleton ownership: two
|
|
5
4
|
// `crtrd` invocations racing a cold start can both read "no pidfile", both
|
package/dist/core/canvas/db.js
CHANGED
|
@@ -58,10 +58,9 @@ function addNodeColumnIfMissing(db, columns, name, ddl) {
|
|
|
58
58
|
db.exec(ddl);
|
|
59
59
|
columns.add(name);
|
|
60
60
|
}
|
|
61
|
-
/** v2 — additive runtime columns
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* NULL, so nothing observes a behavior change until a later phase reads them. */
|
|
61
|
+
/** v2 — the additive runtime columns: `intent, pi_pid, window, tmux_session`.
|
|
62
|
+
* `status` already lives in the baseline row, so it is NOT re-added here. All
|
|
63
|
+
* four default to NULL; v3 backfills them. */
|
|
65
64
|
function addRuntimeColumns(db) {
|
|
66
65
|
const columns = nodeColumns(db);
|
|
67
66
|
addNodeColumnIfMissing(db, columns, 'intent', `ALTER TABLE nodes ADD COLUMN intent TEXT;`);
|
|
@@ -69,14 +68,14 @@ function addRuntimeColumns(db) {
|
|
|
69
68
|
addNodeColumnIfMissing(db, columns, 'window', `ALTER TABLE nodes ADD COLUMN window TEXT;`);
|
|
70
69
|
addNodeColumnIfMissing(db, columns, 'tmux_session', `ALTER TABLE nodes ADD COLUMN tmux_session TEXT;`);
|
|
71
70
|
}
|
|
72
|
-
/** v3 — DATA backfill
|
|
71
|
+
/** v3 — DATA backfill. The runtime fields
|
|
73
72
|
* (`intent, pi_pid, window, tmux_session`) become authoritative in the row;
|
|
74
73
|
* copy each existing node's values out of its meta.json into the row columns
|
|
75
74
|
* once, so the version boundary loses no live state. `status` already mirrors
|
|
76
75
|
* the row, so it is not re-copied.
|
|
77
76
|
*
|
|
78
|
-
* LAYERING NOTE
|
|
79
|
-
*
|
|
77
|
+
* LAYERING NOTE — a sanctioned exception: a *data* migration must read
|
|
78
|
+
* meta.json, which db.ts normally would not. Reading it
|
|
80
79
|
* directly here — via paths.ts, a one-time, clearly-labeled boot-time data
|
|
81
80
|
* migration — is the deliberate choice over splitting the `user_version`
|
|
82
81
|
* counter across two modules. Idempotent and gated: it runs exactly once at the
|
|
@@ -367,7 +366,7 @@ function addProfileIdColumn(db) {
|
|
|
367
366
|
* identity fingerprint (`pid.ts`'s `composeIdentity`) recorded alongside
|
|
368
367
|
* `pi_pid` (v2) by `recordPid`, so `headlessBrokerHost.teardown()` can refuse
|
|
369
368
|
* to signal a `pi_pid` the OS has since recycled for an unrelated process
|
|
370
|
-
* (
|
|
369
|
+
* (see `NodeRuntime.pi_pid_identity`).
|
|
371
370
|
* Defaults NULL ⇒ no baseline ⇒ the guard fails open, exactly like a node
|
|
372
371
|
* booted before this column existed. No data backfill: an existing `pi_pid`
|
|
373
372
|
* with no recorded identity simply has no launch-time baseline to compare
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const CANVAS_EXTENSION_NAMES: readonly ["canvas-inbox-watcher", "canvas-review-boundary", "canvas-stophook", "canvas-recap", "canvas-goal-capture", "canvas-passive-context", "canvas-context-intro", "canvas-tool-guide", "canvas-doc-substrate", "canvas-structured-output", "canvas-bash-valve"];
|
|
1
|
+
export declare const CANVAS_EXTENSION_NAMES: readonly ["canvas-inbox-watcher", "canvas-review-boundary", "canvas-stophook", "canvas-recap", "canvas-goal-capture", "canvas-passive-context", "canvas-context-intro", "canvas-tool-guide", "canvas-doc-substrate", "canvas-structured-output", "canvas-bash-valve", "canvas-preview-result"];
|
|
2
2
|
/** Structural shipped-hook identity: a canvas-hook stem directly under a
|
|
3
3
|
* `pi-extensions` directory. A user's own extension elsewhere may share the
|
|
4
4
|
* stem and is not shipped. */
|
|
@@ -7,9 +7,15 @@ export const CANVAS_EXTENSION_NAMES = [
|
|
|
7
7
|
'canvas-inbox-watcher', 'canvas-review-boundary', 'canvas-stophook', 'canvas-recap',
|
|
8
8
|
'canvas-goal-capture', 'canvas-passive-context', 'canvas-context-intro',
|
|
9
9
|
'canvas-tool-guide', 'canvas-doc-substrate', 'canvas-structured-output',
|
|
10
|
-
'canvas-bash-valve',
|
|
10
|
+
'canvas-bash-valve', 'canvas-preview-result',
|
|
11
11
|
];
|
|
12
12
|
const HISTORICAL_CANVAS_EXTENSION_PREFIXES = [
|
|
13
|
+
[
|
|
14
|
+
'canvas-inbox-watcher', 'canvas-review-boundary', 'canvas-stophook', 'canvas-recap',
|
|
15
|
+
'canvas-goal-capture', 'canvas-passive-context', 'canvas-context-intro',
|
|
16
|
+
'canvas-tool-guide', 'canvas-doc-substrate', 'canvas-structured-output',
|
|
17
|
+
'canvas-bash-valve',
|
|
18
|
+
],
|
|
13
19
|
[
|
|
14
20
|
'canvas-inbox-watcher', 'canvas-stophook', 'canvas-recap',
|
|
15
21
|
'canvas-goal-capture', 'canvas-passive-context', 'canvas-review-boundary',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// The canvas: one global graph of nodes + edges.
|
|
2
|
-
//
|
|
1
|
+
// The canvas: one global graph of nodes + edges.
|
|
2
|
+
// Topology in sqlite (WAL), node flesh on disk.
|
|
3
3
|
export * from './types.js';
|
|
4
4
|
export * from './labels.js';
|
|
5
5
|
export * from './paths.js';
|
|
@@ -8,8 +8,7 @@ export declare function isPidAlive(pid: number | null | undefined): boolean;
|
|
|
8
8
|
* `composeIdentity` fingerprints are compared (this module's own
|
|
9
9
|
* `recordedPidLiveness`, `captureTeardownSnapshot`'s reuse check, and
|
|
10
10
|
* `killProcessTreePids`'s reuse check) — centralized so no call site can
|
|
11
|
-
* drift from this rule
|
|
12
|
-
* fix).
|
|
11
|
+
* drift from this rule.
|
|
13
12
|
*
|
|
14
13
|
* Two hazards this closes, both in the false-DEAD direction (the dangerous
|
|
15
14
|
* one — a false DEAD lets `reviveNode`'s double-launch guard relaunch a
|
|
@@ -94,7 +93,7 @@ export declare function recordedPidLiveness(pid: number | null | undefined, expe
|
|
|
94
93
|
* swallowing ESRCH (already gone). Used by cron cancellation, timeout, and
|
|
95
94
|
* stale-lease recovery. Lives beside isPidAlive: canvas/ is
|
|
96
95
|
* the lowest shared layer every process-liveness/-teardown primitive sits at.
|
|
97
|
-
* Guards `pid` before signaling
|
|
96
|
+
* Guards `pid` before signaling: never hit pid 0
|
|
98
97
|
* (which means "my own group") or a negative/non-integer value. */
|
|
99
98
|
export declare function killProcessGroup(pid: number, signal?: NodeJS.Signals): void;
|
|
100
99
|
/** The kernel `boot_id` (`/proc/sys/kernel/random/boot_id`) is stable for the
|
|
@@ -155,8 +154,8 @@ export interface PsLivenessSnapshot {
|
|
|
155
154
|
}
|
|
156
155
|
export declare function captureLivenessSnapshot(): PsLivenessSnapshot | null;
|
|
157
156
|
/** Every transitive descendant of `rootPid`, discovered by walking `ps`'s
|
|
158
|
-
* pid/ppid table (BFS) — NOT process-GROUP membership.
|
|
159
|
-
*
|
|
157
|
+
* pid/ppid table (BFS) — NOT process-GROUP membership. The pi SDK's bash tool
|
|
158
|
+
* spawns its shell child with
|
|
160
159
|
* `detached: true` (verified in `@earendil-works/pi-agent-core`'s
|
|
161
160
|
* `harness/env/nodejs.js`), which calls `setsid()` and puts that child in a
|
|
162
161
|
* brand-new process GROUP of its own — so `kill(-rootPid)` never reaches it.
|
|
@@ -173,7 +172,7 @@ export declare function descendantPids(rootPid: number): number[];
|
|
|
173
172
|
* batched `ps -p <list>` call. `lstart` is a standard format keyword on BOTH
|
|
174
173
|
* BSD ps (macOS) and procps-ng (Linux), so this needs no `/proc` parsing or
|
|
175
174
|
* other Linux-only machinery. Used to guard against PID REUSE during the
|
|
176
|
-
* multi-second teardown escalation ladder
|
|
175
|
+
* multi-second teardown escalation ladder: a
|
|
177
176
|
* captured pid can exit and the OS can recycle it for an unrelated process
|
|
178
177
|
* before the SIGKILL rung fires, and a bare `kill(pid, 0)` / `kill(pid, sig)`
|
|
179
178
|
* cannot tell the difference.
|
|
@@ -182,9 +181,9 @@ export declare function descendantPids(rootPid: number): number[];
|
|
|
182
181
|
* could not be spawned, or was killed by the timeout): a probe failure means
|
|
183
182
|
* we have NO information, which is a fundamentally different outcome from a
|
|
184
183
|
* probe that SUCCEEDED and simply found no row for a given pid (that pid is
|
|
185
|
-
* genuinely gone). Collapsing these two into the same empty map
|
|
186
|
-
*
|
|
187
|
-
*
|
|
184
|
+
* genuinely gone). Collapsing these two into the same empty map is unsafe:
|
|
185
|
+
* `killProcessTreePids` cannot tell "pid confirmed gone" from "we don't know"
|
|
186
|
+
* without this
|
|
188
187
|
* distinction, and misreading a failed probe as positive proof of
|
|
189
188
|
* absence/reuse either signals a reused stranger or — worse — silently skips
|
|
190
189
|
* reaping a real live orphan. A successful call may still return a map with
|
|
@@ -210,7 +209,7 @@ export declare function capturePidIdentities(pids: readonly number[]): Map<numbe
|
|
|
210
209
|
* signal. */
|
|
211
210
|
export declare function capturePidCommand(pid: number): string | null;
|
|
212
211
|
/** The full pre-signal snapshot `teardown()` needs, taken ONCE, from ONE `ps`
|
|
213
|
-
* table
|
|
212
|
+
* table: the process TREE rooted at `rootPid`
|
|
214
213
|
* (itself + every transitive descendant) alongside each member's identity
|
|
215
214
|
* fingerprint — PLUS, load-bearing, a launch-time-identity REUSE check on
|
|
216
215
|
* `rootPid` itself before the tree is even walked.
|
|
@@ -259,21 +258,20 @@ export declare function captureTeardownSnapshot(rootPid: number, expectedIdentit
|
|
|
259
258
|
* caller before sending any signal — see the doc on `isProcessTreeAlive` for
|
|
260
259
|
* why re-deriving via `ps` after signaling starts is unsafe). Signals EACH
|
|
261
260
|
* pid's OWN process group (a detached SDK-spawned descendant is its own
|
|
262
|
-
* group leader — the pi bash tool's shell child
|
|
261
|
+
* group leader — the pi bash tool's shell child)
|
|
263
262
|
* PLUS a plain `kill(pid)` on each (belt-and-suspenders for a pid that is
|
|
264
263
|
* NOT its own group leader). Every signal is best-effort — `killProcessGroup`
|
|
265
264
|
* swallows ESRCH and the plain `kill` here does too — so firing this against
|
|
266
265
|
* an already-fully-dead snapshot is a harmless no-op.
|
|
267
266
|
*
|
|
268
|
-
* `identities`, when supplied
|
|
267
|
+
* `identities`, when supplied, is a
|
|
269
268
|
* `capturePidIdentities` snapshot taken alongside the pid list at the SAME
|
|
270
269
|
* moment — `null` if that INITIAL capture's `ps` probe itself failed (see
|
|
271
270
|
* `capturePidIdentities`'s doc). Before signaling EITHER the group or the
|
|
272
271
|
* plain kill for a pid, this re-derives CURRENT identities in one fresh
|
|
273
272
|
* batched call and compares — but ONLY skips on POSITIVE proof of reuse; any
|
|
274
273
|
* failure or absence of evidence falls through to the normal signal path
|
|
275
|
-
* (fail-open, matching this module's
|
|
276
|
-
* crouter#98 signal-safety finding):
|
|
274
|
+
* (fail-open, matching this module's unguarded behavior):
|
|
277
275
|
*
|
|
278
276
|
* - baseline identity present AND the current probe SUCCEEDED AND the
|
|
279
277
|
* current identity for this pid is present AND DIFFERENT — the OS has
|
package/dist/core/canvas/pid.js
CHANGED
|
@@ -4,9 +4,8 @@
|
|
|
4
4
|
// LOWEST shared layer — so canvas/, runtime/, AND daemon/ can all import it
|
|
5
5
|
// "down" without a reverse-layer violation (runtime/ sits above canvas/, daemon/
|
|
6
6
|
// above runtime/). It is a pure process-existence utility, NOT data-model access,
|
|
7
|
-
// so it is exempt from the "only canvas.ts touches the db" rule.
|
|
8
|
-
//
|
|
9
|
-
// placement.ts, and crtrd.ts into one (Phase 3 review reuse MINOR-1).
|
|
7
|
+
// so it is exempt from the "only canvas.ts touches the db" rule. canvas.ts,
|
|
8
|
+
// revive.ts, placement.ts, and crtrd.ts all probe liveness through it.
|
|
10
9
|
import { spawnSync } from 'node:child_process';
|
|
11
10
|
import { readFileSync } from 'node:fs';
|
|
12
11
|
import { readKernelBootId } from './boot-id.js';
|
|
@@ -62,8 +61,7 @@ const BOOT_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12
|
|
|
62
61
|
* purely-numeric suffix counts as ticks (the shape `procStartTicks` and
|
|
63
62
|
* `composeIdentity` guarantee); a malformed or empty suffix (`A#`, `A#bad`,
|
|
64
63
|
* `A#1#2`) is treated as ABSENT ticks, so the base compare then applies — a
|
|
65
|
-
* garbage suffix must never be trusted as a fine discriminator
|
|
66
|
-
* Minor). */
|
|
64
|
+
* garbage suffix must never be trusted as a fine discriminator. */
|
|
67
65
|
function splitIdentity(identity) {
|
|
68
66
|
const i = identity.indexOf('#');
|
|
69
67
|
if (i === -1)
|
|
@@ -75,8 +73,7 @@ function splitIdentity(identity) {
|
|
|
75
73
|
* `composeIdentity` fingerprints are compared (this module's own
|
|
76
74
|
* `recordedPidLiveness`, `captureTeardownSnapshot`'s reuse check, and
|
|
77
75
|
* `killProcessTreePids`'s reuse check) — centralized so no call site can
|
|
78
|
-
* drift from this rule
|
|
79
|
-
* fix).
|
|
76
|
+
* drift from this rule.
|
|
80
77
|
*
|
|
81
78
|
* Two hazards this closes, both in the false-DEAD direction (the dangerous
|
|
82
79
|
* one — a false DEAD lets `reviveNode`'s double-launch guard relaunch a
|
|
@@ -208,7 +205,7 @@ function logPidLivenessDecision(pid, isPidAliveResult, expectedIdentity, probeOu
|
|
|
208
205
|
* swallowing ESRCH (already gone). Used by cron cancellation, timeout, and
|
|
209
206
|
* stale-lease recovery. Lives beside isPidAlive: canvas/ is
|
|
210
207
|
* the lowest shared layer every process-liveness/-teardown primitive sits at.
|
|
211
|
-
* Guards `pid` before signaling
|
|
208
|
+
* Guards `pid` before signaling: never hit pid 0
|
|
212
209
|
* (which means "my own group") or a negative/non-integer value. */
|
|
213
210
|
export function killProcessGroup(pid, signal = 'SIGTERM') {
|
|
214
211
|
if (!Number.isInteger(pid) || pid <= 0)
|
|
@@ -220,8 +217,8 @@ export function killProcessGroup(pid, signal = 'SIGTERM') {
|
|
|
220
217
|
/* already gone */
|
|
221
218
|
}
|
|
222
219
|
}
|
|
223
|
-
/** Linux-only higher-resolution discriminator layered onto `lstart
|
|
224
|
-
*
|
|
220
|
+
/** Linux-only higher-resolution discriminator layered onto `lstart`:
|
|
221
|
+
* `ps lstart` is only second-granular, so a PID reused
|
|
225
222
|
* within the same wall-clock second compares equal and slips past the reuse
|
|
226
223
|
* guard. `/proc/<pid>/stat`'s `starttime` field is the kernel's own jiffies-
|
|
227
224
|
* since-boot counter for that process — effectively unique per pid even for
|
|
@@ -395,8 +392,8 @@ function descendantPidsFrom(rootPid, childrenOf) {
|
|
|
395
392
|
return out;
|
|
396
393
|
}
|
|
397
394
|
/** Every transitive descendant of `rootPid`, discovered by walking `ps`'s
|
|
398
|
-
* pid/ppid table (BFS) — NOT process-GROUP membership.
|
|
399
|
-
*
|
|
395
|
+
* pid/ppid table (BFS) — NOT process-GROUP membership. The pi SDK's bash tool
|
|
396
|
+
* spawns its shell child with
|
|
400
397
|
* `detached: true` (verified in `@earendil-works/pi-agent-core`'s
|
|
401
398
|
* `harness/env/nodejs.js`), which calls `setsid()` and puts that child in a
|
|
402
399
|
* brand-new process GROUP of its own — so `kill(-rootPid)` never reaches it.
|
|
@@ -420,7 +417,7 @@ export function descendantPids(rootPid) {
|
|
|
420
417
|
* batched `ps -p <list>` call. `lstart` is a standard format keyword on BOTH
|
|
421
418
|
* BSD ps (macOS) and procps-ng (Linux), so this needs no `/proc` parsing or
|
|
422
419
|
* other Linux-only machinery. Used to guard against PID REUSE during the
|
|
423
|
-
* multi-second teardown escalation ladder
|
|
420
|
+
* multi-second teardown escalation ladder: a
|
|
424
421
|
* captured pid can exit and the OS can recycle it for an unrelated process
|
|
425
422
|
* before the SIGKILL rung fires, and a bare `kill(pid, 0)` / `kill(pid, sig)`
|
|
426
423
|
* cannot tell the difference.
|
|
@@ -429,9 +426,9 @@ export function descendantPids(rootPid) {
|
|
|
429
426
|
* could not be spawned, or was killed by the timeout): a probe failure means
|
|
430
427
|
* we have NO information, which is a fundamentally different outcome from a
|
|
431
428
|
* probe that SUCCEEDED and simply found no row for a given pid (that pid is
|
|
432
|
-
* genuinely gone). Collapsing these two into the same empty map
|
|
433
|
-
*
|
|
434
|
-
*
|
|
429
|
+
* genuinely gone). Collapsing these two into the same empty map is unsafe:
|
|
430
|
+
* `killProcessTreePids` cannot tell "pid confirmed gone" from "we don't know"
|
|
431
|
+
* without this
|
|
435
432
|
* distinction, and misreading a failed probe as positive proof of
|
|
436
433
|
* absence/reuse either signals a reused stranger or — worse — silently skips
|
|
437
434
|
* reaping a real live orphan. A successful call may still return a map with
|
|
@@ -529,21 +526,20 @@ export function captureTeardownSnapshot(rootPid, expectedIdentity) {
|
|
|
529
526
|
* caller before sending any signal — see the doc on `isProcessTreeAlive` for
|
|
530
527
|
* why re-deriving via `ps` after signaling starts is unsafe). Signals EACH
|
|
531
528
|
* pid's OWN process group (a detached SDK-spawned descendant is its own
|
|
532
|
-
* group leader — the pi bash tool's shell child
|
|
529
|
+
* group leader — the pi bash tool's shell child)
|
|
533
530
|
* PLUS a plain `kill(pid)` on each (belt-and-suspenders for a pid that is
|
|
534
531
|
* NOT its own group leader). Every signal is best-effort — `killProcessGroup`
|
|
535
532
|
* swallows ESRCH and the plain `kill` here does too — so firing this against
|
|
536
533
|
* an already-fully-dead snapshot is a harmless no-op.
|
|
537
534
|
*
|
|
538
|
-
* `identities`, when supplied
|
|
535
|
+
* `identities`, when supplied, is a
|
|
539
536
|
* `capturePidIdentities` snapshot taken alongside the pid list at the SAME
|
|
540
537
|
* moment — `null` if that INITIAL capture's `ps` probe itself failed (see
|
|
541
538
|
* `capturePidIdentities`'s doc). Before signaling EITHER the group or the
|
|
542
539
|
* plain kill for a pid, this re-derives CURRENT identities in one fresh
|
|
543
540
|
* batched call and compares — but ONLY skips on POSITIVE proof of reuse; any
|
|
544
541
|
* failure or absence of evidence falls through to the normal signal path
|
|
545
|
-
* (fail-open, matching this module's
|
|
546
|
-
* crouter#98 signal-safety finding):
|
|
542
|
+
* (fail-open, matching this module's unguarded behavior):
|
|
547
543
|
*
|
|
548
544
|
* - baseline identity present AND the current probe SUCCEEDED AND the
|
|
549
545
|
* current identity for this pid is present AND DIFFERENT — the OS has
|
|
@@ -21,8 +21,8 @@ export function createRemoteExec(previewEndpoint, relayToken, timeoutMs = DEFAUL
|
|
|
21
21
|
// A malformed stored token (control chars / non-Latin1 bytes) would throw
|
|
22
22
|
// from the Headers constructor with the invalid value IN the thrown
|
|
23
23
|
// message — validate up front so that never happens, and fail with a
|
|
24
|
-
// GENERIC message instead
|
|
25
|
-
//
|
|
24
|
+
// GENERIC message instead: no token bytes may ever reach returned stderr.
|
|
25
|
+
// HTTP header values are ISO-8859-1; reject
|
|
26
26
|
// anything outside `/^[\x20-\x7e]+$/` (printable ASCII, no CR/LF/control).
|
|
27
27
|
const tokenValid = /^[\x20-\x7e]+$/.test(relayToken);
|
|
28
28
|
return async function exec(bin, args) {
|
|
@@ -28,7 +28,7 @@ export declare function resolveNodeVisual(status: NodeStatus, opts: {
|
|
|
28
28
|
* dashboard flags the node for the human. Exported so a recovery verb (e.g.
|
|
29
29
|
* `node lifecycle revive --now`, src/commands/node-lifecycle-revive.ts) can gate on the SAME
|
|
30
30
|
* predicate the dashboard advertises instead of drifting out of sync with it
|
|
31
|
-
*
|
|
31
|
+
* and rejecting exactly the nodes shown as "needs you". */
|
|
32
32
|
export declare function faultNeedsYou(fault: Pick<Fault, 'retry'>): boolean;
|
|
33
33
|
/** Human-friendly active-fault disposition text. `auto` faults show the current
|
|
34
34
|
* retry attempt plus the next scheduled retry; client-owned redials say
|
|
@@ -69,7 +69,7 @@ function retryCountdown(iso, now) {
|
|
|
69
69
|
* dashboard flags the node for the human. Exported so a recovery verb (e.g.
|
|
70
70
|
* `node lifecycle revive --now`, src/commands/node-lifecycle-revive.ts) can gate on the SAME
|
|
71
71
|
* predicate the dashboard advertises instead of drifting out of sync with it
|
|
72
|
-
*
|
|
72
|
+
* and rejecting exactly the nodes shown as "needs you". */
|
|
73
73
|
export function faultNeedsYou(fault) {
|
|
74
74
|
return fault.retry.disposition !== 'auto';
|
|
75
75
|
}
|
|
@@ -191,7 +191,7 @@ export interface NodeRuntime {
|
|
|
191
191
|
/** Stable process-identity fingerprint (`pid.ts`'s `composeIdentity`) captured
|
|
192
192
|
* at the SAME moment `pi_pid` is recorded (`recordPid`) — the LAUNCH-time
|
|
193
193
|
* baseline `headlessBrokerHost.teardown()` compares `pi_pid`'s CURRENT
|
|
194
|
-
* identity against before signaling anything
|
|
194
|
+
* identity against before signaling anything:
|
|
195
195
|
* a mismatch proves the OS has already recycled `pi_pid` for an unrelated
|
|
196
196
|
* process since this node launched it, and teardown must refuse to signal
|
|
197
197
|
* that stranger. `null` for a node booted before this field existed, or if
|
|
@@ -8,14 +8,12 @@
|
|
|
8
8
|
// This is pure validation: no fetch, no registration lookup, no file I/O beyond
|
|
9
9
|
// the raw manifest bytes passed in.
|
|
10
10
|
import { validateCommandNode } from './schema.js';
|
|
11
|
+
import { isRecord } from '../../shared/predicates.js';
|
|
11
12
|
// ---------------------------------------------------------------------------
|
|
12
13
|
// Validation helpers
|
|
13
14
|
// ---------------------------------------------------------------------------
|
|
14
15
|
const KEBAB = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
15
16
|
const REST_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']);
|
|
16
|
-
function isObject(v) {
|
|
17
|
-
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
18
|
-
}
|
|
19
17
|
function typeName(v) {
|
|
20
18
|
if (v === null)
|
|
21
19
|
return 'null';
|
|
@@ -38,7 +36,7 @@ export function validateCommandManifest(raw, options) {
|
|
|
38
36
|
issues.push({ code, message, received, expected, next, ...(path ? { path } : {}) });
|
|
39
37
|
};
|
|
40
38
|
// Top-level structure check
|
|
41
|
-
if (!
|
|
39
|
+
if (!isRecord(raw)) {
|
|
42
40
|
issue('command_manifest_invalid', 'manifest must be an object', typeName(raw), options.transport === 'http' ? '{ schemaVersion, baseUrl?, timeouts?, mounts }' : '{ schemaVersion, mounts }', 'Provide a valid JSON manifest object.');
|
|
43
41
|
return { issues };
|
|
44
42
|
}
|
|
@@ -126,7 +124,7 @@ export function validateCommandManifest(raw, options) {
|
|
|
126
124
|
// Timeouts validation
|
|
127
125
|
// ---------------------------------------------------------------------------
|
|
128
126
|
function validateTimeouts(raw, issue) {
|
|
129
|
-
if (!
|
|
127
|
+
if (!isRecord(raw)) {
|
|
130
128
|
issue('command_manifest_invalid', 'timeouts must be an object', typeName(raw), '{ connectMs?, requestMs?, streamIdleMs? }', 'Fix timeouts.', 'timeouts');
|
|
131
129
|
return null;
|
|
132
130
|
}
|
|
@@ -150,7 +148,7 @@ function validateTimeouts(raw, issue) {
|
|
|
150
148
|
}
|
|
151
149
|
function validateMount(raw, index, transport, issue) {
|
|
152
150
|
const path = `mounts[${index}]`;
|
|
153
|
-
if (!
|
|
151
|
+
if (!isRecord(raw)) {
|
|
154
152
|
issue('command_manifest_invalid', 'mount must be an object', typeName(raw), '{ parent, node }', 'Fix the mount.', path);
|
|
155
153
|
return null;
|
|
156
154
|
}
|