chati-dev 4.5.28 → 4.5.29
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/bin/chati.js +151 -123
- package/framework/agents/build/dev.md +4 -4
- package/framework/agents/deploy/devops.md +3 -3
- package/framework/agents/discover/brief.md +18 -6
- package/framework/agents/discover/brownfield-wu.md +3 -3
- package/framework/agents/discover/greenfield-wu.md +4 -4
- package/framework/agents/plan/architect.md +2 -2
- package/framework/agents/plan/detail.md +4 -4
- package/framework/agents/plan/phases.md +3 -3
- package/framework/agents/plan/tasks.md +47 -19
- package/framework/agents/plan/ux-brand-architect.md +1 -1
- package/framework/agents/plan/ux.md +26 -2
- package/framework/agents/quality/qa-implementation.md +8 -8
- package/framework/agents/quality/qa-planning.md +58 -4
- package/framework/agents/quality/qa-visual.md +4 -4
- package/framework/config.yaml +2 -2
- package/framework/constitution.md +12 -1
- package/framework/context/governance.md +2 -1
- package/framework/context/root.md +3 -1
- package/framework/domains/constitution.yaml +1 -1
- package/framework/domains/global.yaml +1 -1
- package/framework/domains/workflows/standard-flow.yaml +7 -8
- package/framework/hooks/advance-trigger.js +3 -2
- package/framework/hooks/brief-validator.js +1 -1
- package/framework/hooks/post-dev.js +1 -1
- package/framework/hooks/team-quality-gate.js +9 -2
- package/framework/i18n/en.yaml +1 -0
- package/framework/i18n/es.yaml +1 -0
- package/framework/i18n/fr.yaml +1 -0
- package/framework/i18n/pt.yaml +1 -0
- package/framework/manifest.json +83 -83
- package/framework/manifest.sig +1 -1
- package/framework/orchestrator/chati-router.js +14 -6
- package/framework/orchestrator/chati.md +61 -23
- package/framework/rules/human-writing-style.md +7 -0
- package/framework/schemas/session.schema.json +25 -2
- package/framework/schemas/task.schema.json +26 -0
- package/framework/tasks/orchestrator-handoff.md +10 -10
- package/framework/tasks/orchestrator-resume.md +3 -3
- package/framework/workflows/brownfield-fullstack.yaml +6 -3
- package/framework/workflows/brownfield-service.yaml +5 -3
- package/framework/workflows/brownfield-ui.yaml +5 -3
- package/framework/workflows/greenfield-fullstack.yaml +6 -3
- package/framework/workflows/standard-flow.yaml +8 -3
- package/node_modules/@chati/core/native/browser-proof-wire.h +29 -0
- package/node_modules/@chati/core/native/darwin-browser-listener.c +36 -0
- package/node_modules/@chati/core/native/darwin-browser-listener.h +32 -0
- package/node_modules/@chati/core/native/darwin-browser-proof.h +86 -0
- package/node_modules/@chati/core/native/darwin-code-identity.h +35 -0
- package/node_modules/@chati/core/native/darwin-launch-messages.h +37 -0
- package/node_modules/@chati/core/native/darwin-launcher.c +312 -0
- package/node_modules/@chati/core/native/descriptor-fs.c +407 -0
- package/node_modules/@chati/core/native/execution-admission.h +59 -0
- package/node_modules/@chati/core/native/execution-gate.c +41 -0
- package/node_modules/@chati/core/native/linux-process.c +41 -0
- package/node_modules/@chati/core/native/prebuilds/darwin-arm64/darwin-launcher +0 -0
- package/node_modules/@chati/core/native/prebuilds/darwin-arm64/descriptor-fs.node +0 -0
- package/node_modules/@chati/core/native/prebuilds/darwin-arm64/execution-gate +0 -0
- package/node_modules/@chati/core/native/prebuilds/linux-x64/descriptor-fs.node +0 -0
- package/node_modules/@chati/core/native/prebuilds/linux-x64/execution-gate +0 -0
- package/node_modules/@chati/core/native/process-info-pipe.c +91 -0
- package/node_modules/@chati/core/package.json +2 -1
- package/node_modules/@chati/core/scripts/build-native.js +36 -0
- package/node_modules/@chati/core/src/darwin-browser-proof.js +87 -0
- package/node_modules/@chati/core/src/darwin-host-boot.js +58 -0
- package/node_modules/@chati/core/src/darwin-process.js +79 -0
- package/node_modules/@chati/core/src/darwin-supervisor-channels.js +82 -0
- package/node_modules/@chati/core/src/darwin-terminal-receipt.js +111 -0
- package/node_modules/@chati/core/src/descriptor-fs.js +222 -0
- package/node_modules/@chati/core/src/execution-gate.js +8 -0
- package/node_modules/@chati/core/src/host-boot-marker.js +45 -0
- package/node_modules/@chati/core/src/index.js +18 -0
- package/node_modules/@chati/core/src/linux-host-boot.js +218 -0
- package/node_modules/@chati/core/src/linux-process.js +133 -0
- package/node_modules/@chati/planning/src/browser-verification-plan.js +71 -0
- package/node_modules/@chati/planning/src/index.js +33 -0
- package/node_modules/@chati/planning/src/planning-material.js +64 -0
- package/node_modules/@chati/planning/src/verification-plan.js +57 -0
- package/node_modules/@chati/rail/package.json +1 -0
- package/node_modules/@chati/rail/src/execution-restoration.js +92 -0
- package/node_modules/@chati/rail/src/index.js +812 -167
- package/node_modules/@chati/rail/src/provider-processing.js +118 -0
- package/node_modules/@chati/rail/src/terminal-containment.js +23 -0
- package/node_modules/@chati/release-lane/src/index.js +40 -43
- package/node_modules/@chati/review-council/src/index.js +223 -0
- package/node_modules/@chati/tracking-clickup/src/index.js +67 -58
- package/package-artifact-manifest.json +1 -1
- package/package-artifact-manifest.sig +1 -1
- package/package.json +3 -2
- package/scripts/verify-real-harness-e2e.js +20 -8
- package/src/installer/core.js +147 -107
- package/src/installer/package-artifact.js +7 -0
- package/src/installer/preflight.js +13 -0
- package/src/installer/provider-overlay.js +1 -2
- package/src/installer/templates.js +58 -0
- package/src/installer-v2/catalog-client.js +31 -1
- package/src/installer-v2/clickup-connection-storage.js +92 -0
- package/src/installer-v2/clickup-mcp-client.js +155 -0
- package/src/installer-v2/clickup-oauth-loopback.js +96 -0
- package/src/installer-v2/clickup-oauth.js +233 -0
- package/src/installer-v2/index.js +141 -15
- package/src/installer-v2/installation-authority.js +98 -4
- package/src/installer-v2/provider-executable.js +117 -12
- package/src/installer-v2/wizard-installation.js +11 -0
- package/src/orchestrator/agent-selector.js +39 -1
- package/src/orchestrator/browser-artifact-store.js +131 -0
- package/src/orchestrator/cli.js +762 -205
- package/src/orchestrator/clickup-mcp-observer.js +226 -0
- package/src/orchestrator/clickup-runtime.js +45 -24
- package/src/orchestrator/council-gate-runtime.js +903 -0
- package/src/orchestrator/evidence-authority-enrollment.js +75 -0
- package/src/orchestrator/evidence-authority-request.js +46 -0
- package/src/orchestrator/handoff-engine.js +4 -11
- package/src/orchestrator/pipeline-manager.js +42 -4
- package/src/orchestrator/planning-contract-revision.js +711 -0
- package/src/orchestrator/planning-reconciliation.js +302 -0
- package/src/orchestrator/planning-runtime.js +476 -29
- package/src/orchestrator/rail-adjudication-evidence.js +15 -24
- package/src/orchestrator/rail-human-effort-authority.js +46 -0
- package/src/orchestrator/rail-human-effort.js +149 -0
- package/src/orchestrator/rail-runtime.js +291 -47
- package/src/orchestrator/rail-verification.js +278 -0
- package/src/orchestrator/review-runtime.js +37 -2
- package/src/orchestrator/runtime-installation-v2.js +143 -24
- package/src/orchestrator/session-manager.js +1457 -29
- package/src/tasks/handoff.js +57 -33
- package/src/terminal/adapters/claude-adapter.js +3 -0
- package/src/terminal/adapters/codex-adapter.js +13 -0
- package/src/terminal/adapters/grok-adapter.js +1 -0
- package/src/terminal/agent-attempt-budget.js +35 -0
- package/src/terminal/browser-endpoint-proxy.js +174 -0
- package/src/terminal/browser-observer-driver.js +94 -0
- package/src/terminal/browser-observer-supervisor.js +73 -0
- package/src/terminal/browser-observer.js +95 -0
- package/src/terminal/browser-process-facts.js +75 -0
- package/src/terminal/browser-runtime.js +131 -0
- package/src/terminal/darwin-authority-policy.js +213 -0
- package/src/terminal/darwin-browser-application.js +116 -0
- package/src/terminal/darwin-browser-observer.js +57 -0
- package/src/terminal/darwin-execution.js +185 -0
- package/src/terminal/executable-snapshot.js +202 -0
- package/src/terminal/handoff-parser.js +48 -2
- package/src/terminal/isolation.js +14 -13
- package/src/terminal/prompt-builder.js +176 -15
- package/src/terminal/provider-preflight.js +47 -27
- package/src/terminal/rail-execution-worktree.js +22 -6
- package/src/terminal/rail-process-observer.js +10 -0
- package/src/terminal/rail-prompts.js +21 -3
- package/src/terminal/rail-readonly-workspace.js +54 -5
- package/src/terminal/rail-worktree-checkpoint.js +315 -0
- package/src/terminal/rail-worktree-restoration.js +485 -0
- package/src/terminal/run-agent.js +164 -42
- package/src/terminal/run-council-gate.js +338 -0
- package/src/terminal/run-planning-reconciliation.js +49 -0
- package/src/terminal/run-rail-adjudication.js +29 -7
- package/src/terminal/run-rail-recovery.js +97 -0
- package/src/terminal/run-rail-review.js +36 -15
- package/src/terminal/run-rail-rework.js +26 -15
- package/src/terminal/run-rail-task.js +22 -9
- package/src/terminal/runtime-native-helper.js +85 -0
- package/src/terminal/spawner.js +899 -142
- package/src/terminal/verification-process-facts.js +49 -0
- package/src/upgrade/entrypoints.js +68 -0
- package/src/upgrade/generation.js +194 -0
- package/src/upgrade/maintenance.js +79 -0
- package/src/upgrade/paths.js +81 -0
- package/src/upgrade/upgrade.js +267 -0
- package/src/utils/execution-role.js +15 -0
- package/src/utils/handoff-paths.js +59 -0
- package/src/wizard/i18n.js +1 -0
- package/src/wizard/index.js +17 -5
- package/src/wizard/questions.js +24 -12
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Maintainer build only. Installation never downloads or compiles native code.
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { chmodSync, existsSync, mkdirSync, realpathSync } from 'node:fs';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
if (!['linux', 'darwin'].includes(process.platform)) throw new Error('native POSIX build requires Linux or macOS');
|
|
8
|
+
const root = fileURLToPath(new URL('..', import.meta.url));
|
|
9
|
+
const include = join(dirname(dirname(realpathSync(process.execPath))), 'include/node');
|
|
10
|
+
if (!existsSync(join(include, 'node_api.h'))) throw new Error(`Node-API development headers are missing: ${include}`);
|
|
11
|
+
const output = join(root, 'native/prebuilds', `${process.platform}-${process.arch}`);
|
|
12
|
+
mkdirSync(output, { recursive: true });
|
|
13
|
+
execFileSync('/usr/bin/cc', [
|
|
14
|
+
'-std=c11', '-Wall', '-Wextra', '-Werror', '-O2', '-fPIC', '-DNAPI_VERSION=8',
|
|
15
|
+
'-I', include, ...(process.platform === 'darwin' ? ['-bundle', '-undefined', 'dynamic_lookup'] : ['-shared']),
|
|
16
|
+
join(root, 'native/descriptor-fs.c'), '-o', join(output, 'descriptor-fs.node'),
|
|
17
|
+
], { stdio: 'inherit' });
|
|
18
|
+
chmodSync(join(output, 'descriptor-fs.node'), 0o755);
|
|
19
|
+
process.stdout.write(`Built descriptor-relative storage: ${process.platform}/${process.arch}\n`);
|
|
20
|
+
execFileSync('/usr/bin/cc', [
|
|
21
|
+
'-std=c11', '-Wall', '-Wextra', '-Werror', '-O2',
|
|
22
|
+
join(root, 'native/execution-gate.c'), '-o', join(output, 'execution-gate'),
|
|
23
|
+
], { stdio: 'inherit' });
|
|
24
|
+
// Compiler output follows the maintainer's umask. The shipped trusted helper
|
|
25
|
+
// must never inherit group/other write permissions from that environment.
|
|
26
|
+
chmodSync(join(output, 'execution-gate'), 0o755);
|
|
27
|
+
process.stdout.write(`Built native execution admission gate: ${process.platform}/${process.arch}\n`);
|
|
28
|
+
if (process.platform === 'darwin') {
|
|
29
|
+
execFileSync('/usr/bin/cc', [
|
|
30
|
+
'-std=c11', '-Wall', '-Wextra', '-Werror', '-O2',
|
|
31
|
+
'-framework', 'CoreFoundation', '-framework', 'Security',
|
|
32
|
+
join(root, 'native/darwin-launcher.c'), '-o', join(output, 'darwin-launcher'),
|
|
33
|
+
], { stdio: 'inherit' });
|
|
34
|
+
chmodSync(join(output, 'darwin-launcher'), 0o755);
|
|
35
|
+
process.stdout.write(`Built Darwin suspended-image launcher: ${process.arch}\n`);
|
|
36
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/** Private fixed-frame transport for the admitted Darwin helper. This module
|
|
2
|
+
* does not attest a helper or select a PID. The caller owns sealed launch and
|
|
3
|
+
* exclusive fd9/fd10 inheritance, as well as final native completion. */
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { closeSync, writeSync } from 'node:fs';
|
|
6
|
+
import { Socket } from 'node:net';
|
|
7
|
+
import { createProcessInfoPipeSync } from './descriptor-fs.js';
|
|
8
|
+
|
|
9
|
+
const REQUEST_SIZE = 104; const RESPONSE_SIZE = 116;
|
|
10
|
+
const digest = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
11
|
+
function error(code) { return Object.assign(new Error(code), { code }); }
|
|
12
|
+
|
|
13
|
+
export function createDarwinBrowserProofChannels(nonce) {
|
|
14
|
+
if (typeof nonce !== 'string' || !/^[a-f0-9]{64}$/.test(nonce)) throw error('BROWSER_PROOF_NONCE_INVALID');
|
|
15
|
+
const owned = new Set();
|
|
16
|
+
function pipe() { const pair = createProcessInfoPipeSync(); for (const fd of pair) owned.add(fd); return pair; }
|
|
17
|
+
function close(fd) { if (owned.delete(fd)) closeSync(fd); }
|
|
18
|
+
let request; let reply; let reader; let pending;
|
|
19
|
+
let phase = 'prepared'; let sequence = 0n; let queued = Promise.resolve();
|
|
20
|
+
let received = Buffer.alloc(0); let chain = digest(Buffer.from(nonce)); let positives = 0;
|
|
21
|
+
function dispose(failure = error('BROWSER_PROOF_CHANNEL_CLOSED')) {
|
|
22
|
+
if (phase === 'closed') return false;
|
|
23
|
+
phase = 'closed';
|
|
24
|
+
if (pending) { clearTimeout(pending.timer); pending.reject(failure); pending = undefined; }
|
|
25
|
+
reader?.destroy();
|
|
26
|
+
for (const fd of [...owned]) close(fd);
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
request = pipe(); reply = pipe();
|
|
31
|
+
reader = new Socket({ fd: reply[0], readable: true, writable: false });
|
|
32
|
+
owned.delete(reply[0]); // Socket now owns this descriptor exclusively.
|
|
33
|
+
reader.on('error', () => dispose(error('BROWSER_PROOF_CHANNEL_ERROR')));
|
|
34
|
+
reader.on('end', () => dispose(error('BROWSER_PROOF_CHANNEL_EOF')));
|
|
35
|
+
reader.on('close', () => dispose(error('BROWSER_PROOF_CHANNEL_CLOSED')));
|
|
36
|
+
reader.on('data', bytes => {
|
|
37
|
+
if (!pending || received.length + bytes.length > RESPONSE_SIZE) {
|
|
38
|
+
dispose(error('BROWSER_PROOF_FRAME_INVALID')); return;
|
|
39
|
+
}
|
|
40
|
+
received = Buffer.concat([received, bytes]);
|
|
41
|
+
if (received.length !== RESPONSE_SIZE) return;
|
|
42
|
+
if (!received.subarray(0, REQUEST_SIZE).equals(pending.frame) || received[104] > 1
|
|
43
|
+
|| received.subarray(105, 108).some(byte => byte !== 0)
|
|
44
|
+
|| (received[104] === 1) !== (received.readBigUInt64BE(108) > 0n)) {
|
|
45
|
+
dispose(error('BROWSER_PROOF_FRAME_INVALID')); return;
|
|
46
|
+
}
|
|
47
|
+
const current = pending; pending = undefined; clearTimeout(current.timer);
|
|
48
|
+
const proved = received[104] === 1;
|
|
49
|
+
if (proved) positives++;
|
|
50
|
+
chain = digest(Buffer.concat([Buffer.from(chain, 'hex'), received]));
|
|
51
|
+
received = Buffer.alloc(0);
|
|
52
|
+
current.resolve(proved);
|
|
53
|
+
});
|
|
54
|
+
} catch (failure) { dispose(failure); throw failure; }
|
|
55
|
+
|
|
56
|
+
function query(tuple) {
|
|
57
|
+
if (phase !== 'active') throw error('BROWSER_PROOF_CHANNEL_CLOSED');
|
|
58
|
+
if (!tuple || Object.keys(tuple).length !== 4 || tuple.local_address !== '127.0.0.1' || tuple.remote_address !== '127.0.0.1'
|
|
59
|
+
|| !Number.isInteger(tuple.local_port) || tuple.local_port < 1 || tuple.local_port > 65535
|
|
60
|
+
|| !Number.isInteger(tuple.remote_port) || tuple.remote_port < 1024 || tuple.remote_port > 65535) throw error('BROWSER_PROOF_TUPLE_INVALID');
|
|
61
|
+
if (++sequence >= 2n ** 128n) { dispose(); throw error('BROWSER_PROOF_SEQUENCE_EXHAUSTED'); }
|
|
62
|
+
const frame = Buffer.alloc(REQUEST_SIZE);
|
|
63
|
+
frame.write('CBP1'); frame.write(nonce, 4); frame.write(sequence.toString(16).padStart(32, '0'), 68);
|
|
64
|
+
frame.writeUInt16BE(tuple.local_port, 100); frame.writeUInt16BE(tuple.remote_port, 102);
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
pending = { frame, resolve, reject, timer: setTimeout(() => dispose(error('BROWSER_PROOF_TIMEOUT')), 1000) };
|
|
67
|
+
try {
|
|
68
|
+
if (writeSync(request[1], frame) !== frame.length) dispose(error('BROWSER_PROOF_WRITE_FAILED'));
|
|
69
|
+
} catch { dispose(error('BROWSER_PROOF_WRITE_FAILED')); }
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return Object.freeze({
|
|
73
|
+
inheritedDescriptors: Object.freeze([request[0], reply[1]]),
|
|
74
|
+
afterSpawn() {
|
|
75
|
+
if (phase !== 'prepared') throw error('BROWSER_PROOF_CHANNEL_STATE_INVALID');
|
|
76
|
+
close(request[0]); close(reply[1]); phase = 'active';
|
|
77
|
+
},
|
|
78
|
+
verifyPeer(tuple) {
|
|
79
|
+
const frozen = structuredClone(tuple);
|
|
80
|
+
const result = queued.then(() => query(frozen));
|
|
81
|
+
queued = result.catch(() => {});
|
|
82
|
+
return result;
|
|
83
|
+
},
|
|
84
|
+
summary: () => Object.freeze({ nonce, positive_proofs: positives, proof_chain_sha256: chain, closed: phase === 'closed' }),
|
|
85
|
+
dispose,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
const UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
|
|
6
|
+
const NIL_UUID = '00000000-0000-0000-0000-000000000000';
|
|
7
|
+
let binding;
|
|
8
|
+
function fail(code, message, cause) { throw Object.assign(new Error(message), { code, ...(cause ? { cause } : {}) }); }
|
|
9
|
+
|
|
10
|
+
export function assertDarwinHostBootIdentity(identity) {
|
|
11
|
+
if (!identity || typeof identity !== 'object' || Array.isArray(identity)
|
|
12
|
+
|| Object.keys(identity).length !== 4 || identity.schema_version !== 1 || identity.platform !== 'darwin'
|
|
13
|
+
|| typeof identity.host_id_sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(identity.host_id_sha256)
|
|
14
|
+
|| typeof identity.boot_session_uuid !== 'string' || !UUID.test(identity.boot_session_uuid)
|
|
15
|
+
|| identity.boot_session_uuid === NIL_UUID) {
|
|
16
|
+
fail('INVALID_DARWIN_HOST_BOOT_IDENTITY', 'a complete native host and boot-session identity is required');
|
|
17
|
+
}
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Native host observation, never environment variables, hostname, wall time or
|
|
22
|
+
* provider output. The hardware UUID is hashed here and never enters receipts. */
|
|
23
|
+
export function captureDarwinHostBootIdentity() {
|
|
24
|
+
if (process.platform !== 'darwin') fail('DARWIN_HOST_BOOT_UNAVAILABLE', 'Darwin host identity requires the native Darwin observer');
|
|
25
|
+
try { binding ??= require(`../native/prebuilds/darwin-${process.arch}/descriptor-fs.node`); }
|
|
26
|
+
catch (error) { fail('DARWIN_HOST_BOOT_UNAVAILABLE', 'matching packaged native host observer is required', error); }
|
|
27
|
+
if (typeof binding.darwinHostBoot !== 'function') fail('DARWIN_HOST_BOOT_UNAVAILABLE', 'packaged native observer lacks the host/boot contract');
|
|
28
|
+
const observed = binding.darwinHostBoot();
|
|
29
|
+
if (!observed || !UUID.test(observed.host_uuid ?? '') || observed.host_uuid === NIL_UUID
|
|
30
|
+
|| !UUID.test(observed.boot_session_uuid ?? '') || observed.boot_session_uuid === NIL_UUID) {
|
|
31
|
+
fail('DARWIN_HOST_BOOT_UNAVAILABLE', 'kernel host or boot UUID is unavailable');
|
|
32
|
+
}
|
|
33
|
+
const identity = Object.freeze({ schema_version: 1, platform: 'darwin',
|
|
34
|
+
host_id_sha256: createHash('sha256').update(`chati-darwin-host-v1\0${observed.host_uuid}`).digest('hex'),
|
|
35
|
+
boot_session_uuid: observed.boot_session_uuid });
|
|
36
|
+
assertDarwinHostBootIdentity(identity);
|
|
37
|
+
return identity;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Pure comparison of already authenticated/observed facts, not authentication. */
|
|
41
|
+
export function compareDarwinHostBootIdentity(previous, current) {
|
|
42
|
+
assertDarwinHostBootIdentity(previous);
|
|
43
|
+
assertDarwinHostBootIdentity(current);
|
|
44
|
+
if (previous.host_id_sha256 !== current.host_id_sha256) {
|
|
45
|
+
fail('DARWIN_HOST_IDENTITY_MISMATCH', 'recovery cannot prove termination on a different machine');
|
|
46
|
+
}
|
|
47
|
+
return previous.boot_session_uuid === current.boot_session_uuid ? 'same-boot' : 'previous-boot';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** A new kernel boot kills all earlier processes, including detached groups.
|
|
51
|
+
* Same-boot absence/group completion never supplies that stronger proof. */
|
|
52
|
+
export function observeDarwinPreviousBoot(previous) {
|
|
53
|
+
const current = captureDarwinHostBootIdentity();
|
|
54
|
+
if (compareDarwinHostBootIdentity(previous, current) !== 'previous-boot') {
|
|
55
|
+
fail('RECOVERY_DARWIN_LIFETIME_UNPROVEN', 'detached writers remain unproven in the current boot; recovery requires a new boot of the same machine');
|
|
56
|
+
}
|
|
57
|
+
return Object.freeze({ status: 'exited', reason: 'previous-boot', signal_sent: false, host_boot_identity: current });
|
|
58
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/** Strict readers for private native Darwin supervisor messages. Parsing does
|
|
2
|
+
* not authenticate a sender: callers must own the attested helper and private
|
|
3
|
+
* descriptors. No PID lookup, process signaling, receipt signing or recovery. */
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
|
|
6
|
+
const READY_KEYS = ['schema_version', 'phase', 'nonce', 'helper_pid', 'provider_pid', 'verified_cdhash', 'lifetime_kind'];
|
|
7
|
+
const COMPLETION_KEYS = [...READY_KEYS, 'elapsed_ms', 'exit_code', 'signal', 'supervisor_signal',
|
|
8
|
+
'reaper_result', 'process_group_empty', 'detached_descendants'];
|
|
9
|
+
function fail(message) {
|
|
10
|
+
throw Object.assign(new Error(message), { code: 'INVALID_DARWIN_PROCESS_OBSERVATION' });
|
|
11
|
+
}
|
|
12
|
+
function object(value) { return value && typeof value === 'object' && !Array.isArray(value); }
|
|
13
|
+
function integer(value, minimum, maximum) { return Number.isSafeInteger(value) && value >= minimum && value <= maximum; }
|
|
14
|
+
function hash(value, length) { return typeof value === 'string' && value.length === length && /^[a-f0-9]+$/.test(value); }
|
|
15
|
+
function exactKeys(value, keys) {
|
|
16
|
+
return object(value) && Object.keys(value).length === keys.length && keys.every(key => Object.hasOwn(value, key));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function readMessage(input, phase, keys) {
|
|
20
|
+
if (typeof input !== 'string' || Buffer.byteLength(input) > 4096) fail('native message must be bounded UTF-8 text');
|
|
21
|
+
let value;
|
|
22
|
+
try { value = JSON.parse(input); } catch { fail('native message is not JSON'); }
|
|
23
|
+
// The native writer emits one compact JSON object with one final newline.
|
|
24
|
+
// Exact roundtrip rejects duplicate keys and ambiguous/trailing material.
|
|
25
|
+
if (!exactKeys(value, keys) || `${JSON.stringify(value)}\n` !== input
|
|
26
|
+
|| value.schema_version !== 1 || value.phase !== phase) fail('native message schema or framing is invalid');
|
|
27
|
+
if (!hash(value.nonce, 64) || !hash(value.verified_cdhash, 40)
|
|
28
|
+
|| !integer(value.helper_pid, 2, 0x7fffffff) || !integer(value.provider_pid, 2, 0x7fffffff)
|
|
29
|
+
|| value.helper_pid === value.provider_pid
|
|
30
|
+
|| value.lifetime_kind !== 'darwin_process_group_v1') fail('native process identity or lifetime contract is invalid');
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function darwinSupervisorNonce(context) {
|
|
35
|
+
const command = context?.authority_kind === 'verification_check';
|
|
36
|
+
const keys = ['authority_kind', 'invocation_id', 'launch_nonce', command ? 'executable_binding_digest' : 'provider_id'];
|
|
37
|
+
if (command && (!exactKeys(context, keys) || !hash(context.executable_binding_digest, 64))) {
|
|
38
|
+
fail('verification commands require their exact executable identity, not provider/model routing');
|
|
39
|
+
}
|
|
40
|
+
if (!object(context) || keys.some(key => typeof context[key] !== 'string'
|
|
41
|
+
|| !context[key].trim() || context[key].includes('\0') || Buffer.byteLength(context[key]) > 4096)) {
|
|
42
|
+
fail('a complete public invocation context is required for the native nonce');
|
|
43
|
+
}
|
|
44
|
+
return createHash('sha256').update(JSON.stringify(['chati-darwin-supervisor-v1', ...keys.map(key => context[key])])).digest('hex');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function parseDarwinReadyMessage(input, expected) {
|
|
48
|
+
if (!object(expected) || !hash(expected.nonce, 64) || !hash(expected.verified_cdhash, 40)
|
|
49
|
+
|| !integer(expected.helper_pid, 2, 0x7fffffff)) fail('trusted native launch expectations are required');
|
|
50
|
+
const value = readMessage(input, 'ready', READY_KEYS);
|
|
51
|
+
for (const key of ['nonce', 'helper_pid', 'verified_cdhash']) {
|
|
52
|
+
if (value[key] !== expected[key]) fail(`native READY differs from its launch binding: ${key}`);
|
|
53
|
+
}
|
|
54
|
+
return Object.freeze(value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function parseDarwinCompletionMessage(input, ready, helperExitStatus) {
|
|
58
|
+
if (!exactKeys(ready, READY_KEYS)) fail('the complete bound READY observation is required');
|
|
59
|
+
// Revalidate stored input too; a caller cannot smuggle a different lifetime
|
|
60
|
+
// contract by constructing a ready-shaped object instead of reading the pipe.
|
|
61
|
+
parseDarwinReadyMessage(`${JSON.stringify(ready)}\n`, ready);
|
|
62
|
+
const value = readMessage(input, 'completed', COMPLETION_KEYS);
|
|
63
|
+
for (const key of ['nonce', 'helper_pid', 'provider_pid', 'verified_cdhash', 'lifetime_kind']) {
|
|
64
|
+
if (value[key] !== ready[key]) fail(`native completion differs from READY: ${key}`);
|
|
65
|
+
}
|
|
66
|
+
if (!integer(value.elapsed_ms, 0, Number.MAX_SAFE_INTEGER)
|
|
67
|
+
|| !integer(value.signal, 0, 127) || !integer(value.supervisor_signal, 0, 127)
|
|
68
|
+
|| !(value.signal === 0 ? integer(value.exit_code, 0, 255) : value.exit_code === -1)
|
|
69
|
+
|| !['acknowledged', 'eperm_then_group_empty'].includes(value.reaper_result)
|
|
70
|
+
|| value.process_group_empty !== true || value.detached_descendants !== 'unknown') {
|
|
71
|
+
fail('native completion must report its actual bounded child result and cleanup scope');
|
|
72
|
+
}
|
|
73
|
+
const expectedStatus = value.supervisor_signal ? 128 + value.supervisor_signal
|
|
74
|
+
: value.signal ? 128 + value.signal : value.exit_code;
|
|
75
|
+
if (!integer(helperExitStatus, 0, 255) || helperExitStatus !== expectedStatus) {
|
|
76
|
+
fail('native completion does not match the observed helper exit status');
|
|
77
|
+
}
|
|
78
|
+
return Object.freeze(value);
|
|
79
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/** Own the private descriptors for one native Darwin supervisor invocation.
|
|
2
|
+
* This transport never chooses a helper/policy or signs authority. Its caller
|
|
3
|
+
* must spawn the attested helper and persist dispatch before authorize(). */
|
|
4
|
+
import { closeSync, writeSync } from 'node:fs';
|
|
5
|
+
import { createProcessInfoPipeSync, readProcessInfoPipeSync } from './descriptor-fs.js';
|
|
6
|
+
import { darwinSupervisorNonce, parseDarwinReadyMessage, parseDarwinCompletionMessage } from './darwin-process.js';
|
|
7
|
+
|
|
8
|
+
export function createDarwinSupervisorChannels(context) {
|
|
9
|
+
const nonce = darwinSupervisorNonce(context);
|
|
10
|
+
const owned = new Set();
|
|
11
|
+
let phase = 'prepared';
|
|
12
|
+
let ready;
|
|
13
|
+
function close(fd) {
|
|
14
|
+
if (!owned.delete(fd)) return;
|
|
15
|
+
closeSync(fd);
|
|
16
|
+
}
|
|
17
|
+
function dispose() {
|
|
18
|
+
if (phase === 'disposed') return false;
|
|
19
|
+
phase = 'disposed';
|
|
20
|
+
const errors = [];
|
|
21
|
+
for (const fd of owned) {
|
|
22
|
+
try { close(fd); } catch (error) { errors.push(error); }
|
|
23
|
+
}
|
|
24
|
+
if (errors.length) throw new AggregateError(errors, 'private supervisor descriptor cleanup failed');
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
function failAndDispose(error) {
|
|
28
|
+
try { dispose(); } catch (cleanupError) { error.cleanupError = cleanupError; }
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
function requirePhase(expected) {
|
|
32
|
+
if (phase !== expected) throw Object.assign(new Error(`supervisor channel phase must be ${expected}, not ${phase}`), {
|
|
33
|
+
code: 'INVALID_DARWIN_SUPERVISOR_STATE',
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
function pipe() {
|
|
37
|
+
const pair = createProcessInfoPipeSync();
|
|
38
|
+
for (const fd of pair) owned.add(fd);
|
|
39
|
+
return pair;
|
|
40
|
+
}
|
|
41
|
+
let go; let readyPipe; let completion; let owner;
|
|
42
|
+
try { go = pipe(); readyPipe = pipe(); completion = pipe(); owner = pipe(); }
|
|
43
|
+
catch (error) { failAndDispose(error); }
|
|
44
|
+
const inheritedDescriptors = Object.freeze([go[0], readyPipe[1], completion[1], owner[0]]);
|
|
45
|
+
return Object.freeze({
|
|
46
|
+
nonce,
|
|
47
|
+
// Exact fd5..fd8 mapping. The caller supplies stdio, optional prompt and cwd.
|
|
48
|
+
inheritedDescriptors,
|
|
49
|
+
readReady(helperPid, cdhash) {
|
|
50
|
+
requirePhase('prepared');
|
|
51
|
+
phase = 'starting';
|
|
52
|
+
try {
|
|
53
|
+
// Close parent copies of child ends immediately after the real spawn.
|
|
54
|
+
// Otherwise our own READY/completion writer would prevent EOF forever.
|
|
55
|
+
for (const fd of inheritedDescriptors) close(fd);
|
|
56
|
+
ready = parseDarwinReadyMessage(readProcessInfoPipeSync(readyPipe[0], 10000), {
|
|
57
|
+
nonce, helper_pid: helperPid, verified_cdhash: cdhash,
|
|
58
|
+
});
|
|
59
|
+
close(readyPipe[0]);
|
|
60
|
+
phase = 'ready';
|
|
61
|
+
return ready;
|
|
62
|
+
} catch (error) { return failAndDispose(error); }
|
|
63
|
+
},
|
|
64
|
+
authorize() {
|
|
65
|
+
requirePhase('ready');
|
|
66
|
+
try {
|
|
67
|
+
// The caller's synchronous durable-dispatch callback precedes this.
|
|
68
|
+
// Owner lifetime stays open when this one-shot GO writer closes.
|
|
69
|
+
writeSync(go[1], 'G'); close(go[1]); phase = 'authorized';
|
|
70
|
+
} catch (error) { failAndDispose(error); }
|
|
71
|
+
},
|
|
72
|
+
readCompletion(helperExitStatus) {
|
|
73
|
+
requirePhase('authorized');
|
|
74
|
+
try {
|
|
75
|
+
const value = parseDarwinCompletionMessage(readProcessInfoPipeSync(completion[0], 5000), ready, helperExitStatus);
|
|
76
|
+
close(completion[0]); dispose();
|
|
77
|
+
return value;
|
|
78
|
+
} catch (error) { return failAndDispose(error); }
|
|
79
|
+
},
|
|
80
|
+
dispose,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/** Typed Darwin terminal facts, not authentication or proof of all descendants'
|
|
2
|
+
* death. The authority must authenticate receipts before a consumer uses them. */
|
|
3
|
+
import { posix } from 'node:path';
|
|
4
|
+
import { canonicalize, sha256 } from './index.js';
|
|
5
|
+
import { darwinSupervisorNonce, parseDarwinReadyMessage, parseDarwinCompletionMessage } from './darwin-process.js';
|
|
6
|
+
import { assertDarwinHostBootIdentity } from './darwin-host-boot.js';
|
|
7
|
+
|
|
8
|
+
const BINDING_KEYS = ['schema_version', 'helper_sha256', 'policy_digest', 'ready', 'write_scope', 'detached_descendants', 'host_boot_identity'];
|
|
9
|
+
const IDENTITIES = ['authority_kind', 'invocation_id', 'launch_nonce', 'terminal_id', 'provider_id', 'harness_id',
|
|
10
|
+
'model_id', 'reasoning_configuration', 'provider_executable_path', 'provider_executable_sha256'];
|
|
11
|
+
export const PROVIDER_CONTROL_PLANE_DIRECTORIES = Object.freeze(['.chati', '.chati.dev', '.claude', '.codex', '.grok', '.agents']);
|
|
12
|
+
const ADMISSION_FIELDS = ['execution_admission_protocol', 'execution_admission_helper_sha256', 'execution_admission_receipt_digest'];
|
|
13
|
+
function fail(message) { throw Object.assign(new Error(message), { code: 'INVALID_DARWIN_TERMINAL_RECEIPT' }); }
|
|
14
|
+
function object(value) { return value && typeof value === 'object' && !Array.isArray(value); }
|
|
15
|
+
function exact(value, keys) { return object(value) && Object.keys(value).length === keys.length && keys.every(key => Object.hasOwn(value, key)); }
|
|
16
|
+
function text(value) {
|
|
17
|
+
return typeof value === 'string' && value.trim() && Buffer.byteLength(value) <= 4096
|
|
18
|
+
&& !Array.from(value).some(character => character.codePointAt(0) < 32 || character.codePointAt(0) === 127);
|
|
19
|
+
}
|
|
20
|
+
function digest(value) { return typeof value === 'string' && /^[a-f0-9]{64}$/.test(value); }
|
|
21
|
+
function path(value) { return text(value) && posix.isAbsolute(value) && posix.resolve(value) === value && value !== '/'; }
|
|
22
|
+
function within(root, target) { const rel = posix.relative(root, target); return !rel || (rel !== '..' && !rel.startsWith('../') && !posix.isAbsolute(rel)); }
|
|
23
|
+
function nativeRead(callback) {
|
|
24
|
+
try { return callback(); } catch (error) { fail(`invalid private native observation: ${error.message}`); }
|
|
25
|
+
}
|
|
26
|
+
function assertBinding(binding) {
|
|
27
|
+
if (!exact(binding, BINDING_KEYS) || binding.schema_version !== 1
|
|
28
|
+
|| !digest(binding.helper_sha256) || !digest(binding.policy_digest)
|
|
29
|
+
|| binding.detached_descendants !== 'unknown') fail('Darwin launch requires the complete closed native binding');
|
|
30
|
+
nativeRead(() => parseDarwinReadyMessage(`${JSON.stringify(binding.ready)}\n`, binding.ready));
|
|
31
|
+
nativeRead(() => assertDarwinHostBootIdentity(binding.host_boot_identity));
|
|
32
|
+
const scope = binding.write_scope;
|
|
33
|
+
if (!exact(scope, ['full_project', 'entries']) || typeof scope.full_project !== 'boolean'
|
|
34
|
+
|| !Array.isArray(scope.entries) || scope.entries.length > 1024
|
|
35
|
+
|| (scope.full_project && scope.entries.length)) fail('Darwin write scope must be explicit and unmixed');
|
|
36
|
+
const targets = new Set();
|
|
37
|
+
for (const entry of scope.entries) {
|
|
38
|
+
if (!exact(entry, ['target', 'kind']) || !path(entry.target) || !['file', 'directory'].includes(entry.kind)
|
|
39
|
+
|| targets.has(entry.target)) fail('Darwin partial write scopes require distinct canonical targets');
|
|
40
|
+
targets.add(entry.target);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Shared by the actual spawner and contract tests. It cannot sign a receipt or
|
|
45
|
+
* turn caller-supplied values into an authenticated native observation. */
|
|
46
|
+
export function createDarwinTerminalLaunchBinding({ helperSha256, policyDigest, ready, writeScopes, hostBootIdentity }) {
|
|
47
|
+
if (!writeScopes || !Array.isArray(writeScopes.entries)) fail('prepared native write scopes are required');
|
|
48
|
+
const binding = {
|
|
49
|
+
schema_version: 1, helper_sha256: helperSha256, policy_digest: policyDigest, ready, host_boot_identity: hostBootIdentity,
|
|
50
|
+
write_scope: { full_project: writeScopes.fullProject,
|
|
51
|
+
entries: writeScopes.entries.map(({ target, kind }) => ({ target, kind })) },
|
|
52
|
+
detached_descendants: 'unknown',
|
|
53
|
+
};
|
|
54
|
+
assertBinding(binding);
|
|
55
|
+
return Object.freeze(canonicalize(binding));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function assertDarwinTerminalLaunch(receipt) {
|
|
59
|
+
if (!object(receipt) || receipt.schema_version !== 1 || receipt.purpose !== 'terminal_launch'
|
|
60
|
+
|| receipt.process_containment_kind !== 'darwin_scoped_process_group_v1'
|
|
61
|
+
|| !['rail_execution', 'rail_review', 'rail_adjudication', 'planning_execution', 'council_gate'].includes(receipt.authority_kind)
|
|
62
|
+
|| IDENTITIES.some(key => !text(receipt[key]))
|
|
63
|
+
|| !Number.isInteger(receipt.process_pid) || receipt.process_pid < 2 || receipt.process_pid > 0x7fffffff
|
|
64
|
+
|| receipt.process_group_id !== receipt.process_pid || !text(receipt.process_hostname)
|
|
65
|
+
|| receipt.process_containment_id !== `darwin:${receipt.terminal_id}`
|
|
66
|
+
|| !path(receipt.working_dir) || !path(receipt.provider_executable_path)
|
|
67
|
+
|| ['provider_executable_sha256', 'process_containment_digest', 'project_identity_digest', 'command_digest'].some(key => !digest(receipt[key]))
|
|
68
|
+
|| Object.hasOwn(receipt, 'process_namespace_identity')) fail('Darwin launch identity or containment variant is incomplete');
|
|
69
|
+
const binding = receipt.darwin_launch_binding;
|
|
70
|
+
assertBinding(binding);
|
|
71
|
+
nativeRead(() => parseDarwinReadyMessage(`${JSON.stringify(binding.ready)}\n`, {
|
|
72
|
+
nonce: darwinSupervisorNonce(receipt), helper_pid: receipt.process_pid, verified_cdhash: binding.ready.verified_cdhash,
|
|
73
|
+
}));
|
|
74
|
+
for (const entry of binding.write_scope.entries) {
|
|
75
|
+
if (entry.target === receipt.working_dir || !within(receipt.working_dir, entry.target)
|
|
76
|
+
|| PROVIDER_CONTROL_PLANE_DIRECTORIES.some(name => {
|
|
77
|
+
const control = posix.join(receipt.working_dir, name);
|
|
78
|
+
return within(control, entry.target) || within(entry.target, control);
|
|
79
|
+
})) fail('Darwin write scope escapes the product or overlaps the control plane');
|
|
80
|
+
}
|
|
81
|
+
if (receipt.authority_kind === 'rail_execution') {
|
|
82
|
+
if (receipt.execution_admission_protocol !== 'native-positive-go-v1'
|
|
83
|
+
|| receipt.execution_admission_helper_sha256 !== binding.helper_sha256
|
|
84
|
+
|| !digest(receipt.execution_admission_receipt_digest)) fail('Darwin execution must bind its exact positive-GO admission helper');
|
|
85
|
+
} else {
|
|
86
|
+
if (ADMISSION_FIELDS.some(key => Object.hasOwn(receipt, key))) fail('Only RAIL execution may carry execution admission');
|
|
87
|
+
if (binding.write_scope.full_project
|
|
88
|
+
|| (receipt.authority_kind !== 'planning_execution' && binding.write_scope.entries.length)) {
|
|
89
|
+
fail('Darwin Planning requires partial product scope; review, adjudication and council require readonly scope');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function assertDarwinTerminalCompletion(receipt, launch) {
|
|
96
|
+
assertDarwinTerminalLaunch(launch);
|
|
97
|
+
if (!object(receipt) || receipt.schema_version !== 1 || receipt.purpose !== 'terminal_completion'
|
|
98
|
+
|| IDENTITIES.some(key => receipt[key] !== launch[key])
|
|
99
|
+
|| receipt.launch_receipt_digest !== sha256(launch)
|
|
100
|
+
|| receipt.darwin_launch_binding_digest !== sha256(launch.darwin_launch_binding)
|
|
101
|
+
|| Object.hasOwn(receipt, 'process_namespace_identity_digest') || Object.hasOwn(receipt, 'process_namespace_identity')
|
|
102
|
+
|| !digest(receipt.stdout_digest) || !digest(receipt.stderr_digest)
|
|
103
|
+
|| receipt.process_time_kind !== 'provider-invocation-wall-time'
|
|
104
|
+
|| !Number.isFinite(receipt.process_elapsed_ms) || receipt.process_elapsed_ms < 0
|
|
105
|
+
|| typeof receipt.completed_at !== 'string'
|
|
106
|
+
|| !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(receipt.completed_at)
|
|
107
|
+
|| !Number.isFinite(Date.parse(receipt.completed_at))) fail('Darwin completion must bind its canonical launch and observed output/time');
|
|
108
|
+
nativeRead(() => parseDarwinCompletionMessage(`${JSON.stringify(receipt.darwin_completion_observation)}\n`,
|
|
109
|
+
launch.darwin_launch_binding.ready, receipt.exit_status));
|
|
110
|
+
return true;
|
|
111
|
+
}
|