@entrorove/skill 0.1.23 → 0.1.25
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/AGREEMENT.md +6 -6
- package/CAPABILITY-MAP.md +6 -0
- package/CHANGELOG.md +10 -0
- package/SKILL.md +18 -4
- package/archguard-runtime-build.mjs +15 -0
- package/blueprint-criterion-runner.mjs +110 -0
- package/blueprint-criterion-wire.mjs +78 -0
- package/calctool-runtime-build.mjs +59 -0
- package/calctool-runtime-entry.mjs +92 -0
- package/cli.mjs +35 -83
- package/local-approved-plan.mjs +74 -15
- package/local-archguard-ast.mjs +371 -0
- package/local-archguard-checkpoint.mjs +123 -0
- package/local-archguard-components.mjs +177 -0
- package/local-archguard-contract.mjs +109 -0
- package/local-archguard-contracts.mjs +42 -0
- package/local-archguard-evaluate.mjs +120 -0
- package/local-archguard-language-ast.mjs +165 -0
- package/local-archguard-mutation-fence.mjs +28 -0
- package/local-archguard-operations.mjs +25 -0
- package/local-archguard-scope.mjs +64 -0
- package/local-archguard-signing.mjs +52 -0
- package/local-archguard-snapshot.mjs +126 -0
- package/local-archguard-state.mjs +200 -0
- package/local-archguard.mjs +74 -0
- package/local-budget-git-attributes.mjs +49 -0
- package/local-budget-git-loader.mjs +61 -0
- package/local-budget-git-native-attributes.mjs +81 -0
- package/local-budget-git-native-darwin.mjs +104 -0
- package/local-budget-git-native-linux.mjs +93 -0
- package/local-budget-git-native-reads.mjs +88 -0
- package/local-budget-git-native-request.mjs +53 -0
- package/local-budget-git-native-runtime.mjs +18 -0
- package/local-budget-git-native.c +313 -0
- package/local-budget-git-native.mjs +109 -0
- package/local-budget-git-outcome.mjs +33 -0
- package/local-budget-git-scratch-loader.mjs +158 -0
- package/local-budget-git-scratch.mjs +112 -0
- package/local-budget-git-system-attributes.mjs +44 -0
- package/local-budget-git.mjs +92 -0
- package/local-budget-grants.mjs +82 -0
- package/local-budget-handoff-completion.mjs +70 -0
- package/local-budget-handoff-contracts.mjs +53 -0
- package/local-budget-handoff.mjs +229 -0
- package/local-budget-lock.mjs +55 -0
- package/local-budget-reads.mjs +135 -0
- package/local-budget-renewal.mjs +86 -34
- package/local-budget-source-bindings.mjs +51 -0
- package/local-budget-store.mjs +131 -0
- package/local-budget-transition.mjs +46 -0
- package/local-budget.mjs +185 -0
- package/local-calctool-cli.mjs +24 -0
- package/local-calctool-contracts.mjs +42 -0
- package/local-calctool-runtime.mjs +10 -0
- package/local-calctool-storage.mjs +101 -0
- package/local-calctool.mjs +103 -0
- package/local-chain-bindings.mjs +93 -0
- package/local-chain-broker.mjs +61 -0
- package/local-chain-contracts.mjs +21 -0
- package/local-chain-controller.mjs +321 -0
- package/local-chain-human.mjs +48 -0
- package/local-chain-lock.mjs +54 -0
- package/local-chain-operations.mjs +7 -0
- package/local-chain-plan.mjs +232 -0
- package/local-chain-release-labels.mjs +12 -0
- package/local-chain-results.mjs +75 -0
- package/local-cli.mjs +65 -24
- package/local-contract-types.mjs +46 -0
- package/local-contracts.mjs +126 -0
- package/local-credential.mjs +101 -0
- package/local-decimal.mjs +85 -0
- package/local-error.mjs +23 -0
- package/local-evidence-verification.mjs +199 -0
- package/local-execution.mjs +115 -304
- package/local-final-gate16-bundle.mjs +21 -0
- package/local-final-gate16-checks.mjs +88 -0
- package/local-final-gate16-contract.mjs +15 -0
- package/local-final-gate16-plan.mjs +66 -0
- package/local-final-gate16-proof.mjs +75 -0
- package/local-final-gate16-release.mjs +17 -0
- package/local-final-gate16-state.mjs +70 -0
- package/local-final-gate16-worker.mjs +19 -0
- package/local-final-gate16.mjs +137 -0
- package/local-formula-ast.mjs +106 -0
- package/local-formula-engine.mjs +125 -0
- package/local-formula-tests.mjs +61 -0
- package/local-formula.mjs +25 -125
- package/local-integrity.mjs +22 -41
- package/local-invocations.mjs +142 -0
- package/local-ledger.mjs +15 -9
- package/local-merge-archguard-verification.d.mts +44 -0
- package/local-merge-archguard-verification.mjs +240 -0
- package/local-merge-archguard.mjs +138 -0
- package/local-merge-artifacts.mjs +86 -0
- package/local-merge-authority.mjs +89 -0
- package/local-merge-contracts.mjs +56 -0
- package/local-merge-controller.mjs +170 -0
- package/local-merge-head.mjs +56 -0
- package/local-merge-journal.mjs +77 -0
- package/local-merge-mutation.mjs +97 -0
- package/local-merge-rollback.mjs +126 -0
- package/local-merge-snapshot-rollback.mjs +26 -0
- package/local-merge-storage.mjs +103 -0
- package/local-merge-targets.mjs +47 -0
- package/local-merge-transaction.mjs +133 -0
- package/local-mutation-audit.mjs +34 -0
- package/local-mutation-authority.mjs +103 -0
- package/local-mutation-batch-receipt.mjs +84 -0
- package/local-mutation-batch.mjs +143 -0
- package/local-mutation-binding.mjs +89 -0
- package/local-mutation-files.mjs +270 -0
- package/local-mutation-pass-store.mjs +63 -0
- package/local-mutation-snapshot.mjs +200 -0
- package/local-mutation-wire.mjs +89 -0
- package/local-probe.mjs +133 -11
- package/local-scope-candidate.mjs +28 -0
- package/local-scope-context.mjs +28 -0
- package/local-scope-files.mjs +106 -0
- package/local-scope-git-attributes.mjs +200 -0
- package/local-scope-git-comparison.mjs +8 -0
- package/local-scope-git-history.mjs +72 -0
- package/local-scope-git-reads.mjs +66 -0
- package/local-scope-git.mjs +124 -0
- package/local-scope-repository.mjs +249 -0
- package/local-scope-signature.mjs +70 -0
- package/local-scope-snapshot.mjs +39 -0
- package/local-scope-state.mjs +204 -0
- package/local-scope-workspaces.mjs +83 -0
- package/local-swarm-authority.mjs +143 -0
- package/local-swarm-baseline.mjs +32 -0
- package/local-swarm-completion.mjs +102 -0
- package/local-swarm-contracts.mjs +47 -0
- package/local-swarm-mutation.mjs +142 -0
- package/local-swarm-priority.mjs +237 -0
- package/local-swarm-state.mjs +102 -0
- package/local-swarm-storage.mjs +135 -0
- package/local-swarm.mjs +179 -0
- package/local-task-handoff-completion-wire.mjs +71 -0
- package/local-task-handoff-contracts.mjs +66 -0
- package/local-task-handoff-fence.mjs +478 -0
- package/local-task-handoff-observation.mjs +41 -0
- package/local-task-handoff-overlay-wire.mjs +94 -0
- package/local-task-handoff-read-authority.mjs +236 -0
- package/local-task-handoff-storage.mjs +73 -0
- package/local-task-handoff-write-archive.mjs +80 -0
- package/local-task-handoff-write-constraints.mjs +66 -0
- package/local-task-handoff-write-receipts.mjs +99 -0
- package/local-task-handoff-write-wire.mjs +99 -0
- package/local-task-routing.mjs +202 -22
- package/manifest.json +1 -1
- package/package.json +164 -3
- package/release-state.json +164 -18
- package/release.mjs +242 -4
- package/releases.json +12 -0
- package/remote-client.mjs +127 -36
- package/remote-transport.mjs +80 -0
- package/review-client.mjs +38 -0
- package/runtime/archguard-runtime-v7.0.44.mjs +314 -0
- package/runtime/calctool-runtime-v7.0.43.mjs +1910 -0
- package/runtime/git-native-darwin-v1.mjs +5 -0
- package/runtime/git-native-linux-v1.mjs +5 -0
- package/task-message-cli.mjs +47 -0
- package/task-message-entry.mjs +159 -0
- package/workflow-local-client.mjs +35 -0
- package/workflow-local-contracts.mjs +58 -0
- package/workflow-local-request.mjs +53 -0
- package/workflow-local-runner.mjs +350 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline/promises';
|
|
2
|
+
import { chainCanonical as canonical, chainFail as fail, chainSha as sha } from './local-chain-bindings.mjs';
|
|
3
|
+
import { inspectLocalChainResult } from './local-chain-plan.mjs';
|
|
4
|
+
|
|
5
|
+
export function originalChainInteraction(step, state, resumed) {
|
|
6
|
+
const value = step.output, current = resumed.result;
|
|
7
|
+
if (!value?.interaction || !value.interactionId || value.interactionDigest !== sha(canonical(value.interaction))
|
|
8
|
+
|| current?.interactionId !== value.interactionId || current.interactionDigest !== value.interactionDigest
|
|
9
|
+
|| canonical(current.interaction) !== canonical(value.interaction)
|
|
10
|
+
|| resumed.nextStep?.operation !== 'continue' || resumed.nextStep.input?.flowId !== state.identity.flowId
|
|
11
|
+
|| resumed.nextStep.input.operation !== 'interaction-answer') {
|
|
12
|
+
fail('ENTROROVE_CHAIN_INTERACTION_BINDING', 'answer must address the exact original live interaction');
|
|
13
|
+
}
|
|
14
|
+
const interaction = value.interaction;
|
|
15
|
+
if (interaction.callback?.operation !== 'interaction-answer' || canonical(interaction.callback.payload) !== '{}') {
|
|
16
|
+
fail('ENTROROVE_CHAIN_CALLBACK_UNSUPPORTED', 'this fixed host accepts only the original workflow interaction-answer callback');
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
export async function readChainTerminalAnswer(value) {
|
|
21
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) fail('ENTROROVE_CHAIN_TTY_REQUIRED', 'a real interactive terminal is required; redirected answers are rejected');
|
|
22
|
+
const interaction = value.interaction, ids = interaction.options?.map(option => option.id);
|
|
23
|
+
if (!['confirm', 'choice', 'multi', 'input'].includes(interaction.type) || !Array.isArray(ids)
|
|
24
|
+
|| new Set(ids).size !== ids.length || (interaction.type !== 'input' && ids.length < 2)) {
|
|
25
|
+
fail('ENTROROVE_CHAIN_INTERACTION_BINDING', 'original interaction option contract is invalid');
|
|
26
|
+
}
|
|
27
|
+
process.stdout.write(`${interaction.question}\n${interaction.riskDescription ?? ''}\n`);
|
|
28
|
+
for (const option of interaction.options) process.stdout.write(`${option.id}: ${option.label}\n`);
|
|
29
|
+
const terminal = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
30
|
+
let answer; try { answer = (await terminal.question('Answer with the exact option ID: ')).trim(); } finally { terminal.close(); }
|
|
31
|
+
if (interaction.type === 'multi') answer = answer.split(',').map(value => value.trim());
|
|
32
|
+
if (interaction.type === 'input' ? !answer : Array.isArray(answer)
|
|
33
|
+
? !answer.length || new Set(answer).size !== answer.length || answer.some(value => !ids.includes(value)) : !ids.includes(answer)) {
|
|
34
|
+
fail('ENTROROVE_CHAIN_ANSWER_INVALID', 'terminal answer must match explicit original option IDs');
|
|
35
|
+
}
|
|
36
|
+
return { interactionId: value.interactionId, interactionDigest: value.interactionDigest, answer, remembered: false };
|
|
37
|
+
}
|
|
38
|
+
export function inspectChainHumanResponse(original, state, step, response) {
|
|
39
|
+
const native = response.result, audit = native?.auditEntry, question = step.output.interaction;
|
|
40
|
+
if (response.flowId !== state.identity.flowId || native?.interactionId !== step.output.interactionId
|
|
41
|
+
|| native.interactionDigest !== step.output.interactionDigest || audit?.schemaVersion !== 'confirm.audit-entry/1.0'
|
|
42
|
+
|| audit.actorId !== state.identity.ownerId || audit.requestId !== question.requestId || audit.question !== question.question
|
|
43
|
+
|| canonical(audit.answer) !== canonical(step.request.input.payload.answer) || audit.risk !== question.risk
|
|
44
|
+
|| !audit.auditId || !Number.isFinite(Date.parse(audit.answeredAt))) {
|
|
45
|
+
fail('ENTROROVE_CHAIN_ANSWER_BINDING', 'answer has no matching original server-persisted audit and callback result');
|
|
46
|
+
}
|
|
47
|
+
return inspectLocalChainResult(original, { ...response, result: { ...native, answer: audit.answer } });
|
|
48
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { chainControlFile, chainFail } from './local-chain-bindings.mjs';
|
|
4
|
+
|
|
5
|
+
function alive(pid) {
|
|
6
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) chainFail('ENTROROVE_CHAIN_LOCK_INVALID', 'chain lock PID is invalid');
|
|
7
|
+
try { process.kill(pid, 0); return true; } catch (error) { return error.code !== 'ESRCH'; }
|
|
8
|
+
}
|
|
9
|
+
async function owner(lock) {
|
|
10
|
+
const stat = await fs.lstat(lock);
|
|
11
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || (stat.mode & 0o777) !== 0o700
|
|
12
|
+
|| (typeof process.getuid === 'function' && stat.uid !== process.getuid())) {
|
|
13
|
+
chainFail('ENTROROVE_CHAIN_LOCK_INVALID', 'chain lock must be owned, private and real');
|
|
14
|
+
}
|
|
15
|
+
let value; try { value = JSON.parse((await chainControlFile(`${lock}/owner.json`, 1024)).bytes.toString()); }
|
|
16
|
+
catch { chainFail('ENTROROVE_CHAIN_LOCK_INVALID', 'chain lock owner is invalid'); }
|
|
17
|
+
if (!value || typeof value.token !== 'string' || !Number.isSafeInteger(value.pid)) {
|
|
18
|
+
chainFail('ENTROROVE_CHAIN_LOCK_INVALID', 'chain lock owner is invalid');
|
|
19
|
+
}
|
|
20
|
+
return { stat, value };
|
|
21
|
+
}
|
|
22
|
+
async function recover(lock) {
|
|
23
|
+
let original; try { original = await owner(lock); } catch (error) { if (error.code === 'ENOENT') return; throw error; }
|
|
24
|
+
if (alive(original.value.pid)) return;
|
|
25
|
+
const removed = `${lock}.dead-${randomUUID()}`;
|
|
26
|
+
try { await fs.rename(lock, removed); } catch (error) { if (error.code === 'ENOENT') return; throw error; }
|
|
27
|
+
const moved = await owner(removed);
|
|
28
|
+
if (moved.stat.ino !== original.stat.ino || moved.stat.dev !== original.stat.dev
|
|
29
|
+
|| moved.value.token !== original.value.token) chainFail('ENTROROVE_CHAIN_LOCK_CHANGED', 'chain lock changed during recovery');
|
|
30
|
+
await fs.rm(removed, { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
export async function withChainLock(file, action) {
|
|
33
|
+
const lock = `${file}.lock`, staging = `${lock}.private-${randomUUID()}`, token = randomUUID();
|
|
34
|
+
await fs.mkdir(staging, { mode: 0o700 });
|
|
35
|
+
const handle = await fs.open(`${staging}/owner.json`, 'wx', 0o600);
|
|
36
|
+
try { await handle.writeFile(JSON.stringify({ pid: process.pid, token })); await handle.sync(); } finally { await handle.close(); }
|
|
37
|
+
const deadline = Date.now() + 10_000;
|
|
38
|
+
try {
|
|
39
|
+
while (true) {
|
|
40
|
+
try { await fs.rename(staging, lock); break; }
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (!['ENOTEMPTY', 'EEXIST'].includes(error.code)) throw error;
|
|
43
|
+
await recover(lock);
|
|
44
|
+
if (Date.now() >= deadline) chainFail('ENTROROVE_CHAIN_BUSY', 'the original chain is running in another live host');
|
|
45
|
+
await new Promise(resolve => setTimeout(resolve, 25));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
try { return await action(); }
|
|
49
|
+
finally {
|
|
50
|
+
if ((await owner(lock)).value.token !== token) chainFail('ENTROROVE_CHAIN_LOCK_CHANGED', 'chain lock ownership changed');
|
|
51
|
+
await fs.rm(lock, { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
} finally { await fs.rm(staging, { recursive: true, force: true }); }
|
|
54
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { initLocalChain, resumeLocalChain, statusLocalChain, answerLocalChain, LOCAL_CHAIN_CONTROLLER_SCHEMA } from './local-chain-controller.mjs';
|
|
2
|
+
export const LOCAL_CHAIN_OPERATIONS = Object.freeze({ 'chain-init': initLocalChain, 'chain-resume': resumeLocalChain,
|
|
3
|
+
'chain-status': statusLocalChain, 'chain-answer': answerLocalChain });
|
|
4
|
+
export const LOCAL_CHAIN_CAPABILITIES = Object.freeze({ schemaVersion: LOCAL_CHAIN_CONTROLLER_SCHEMA,
|
|
5
|
+
operations: Object.keys(LOCAL_CHAIN_OPERATIONS), authority: 'original-workflow-step-results',
|
|
6
|
+
taskCompletionVerified: false, coordinatorStatus: 'adapter-pending', answerRequiresInteractiveTerminal: true,
|
|
7
|
+
legacyLedger: { operations: ['chain-ledger-init', 'chain-ledger-status', 'chain-ledger-query'], claimsExecution: false } });
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const LOCAL_CHAIN_PLAN_SCHEMA = 'entrorove.execution-plan/1.0';
|
|
5
|
+
const ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
6
|
+
const KINDS = ['skill', 'coordinator', 'command'];
|
|
7
|
+
const EVIDENCE_KINDS = ['test', 'build', 'lint', 'security', 'benchmark'];
|
|
8
|
+
const RESERVED_ENV = /(?:TOKEN|SECRET|PASSWORD|PRIVATE.?KEY|AUTHORIZATION|CREDENTIAL)/i;
|
|
9
|
+
const UNSAFE_TARGET = new Set(['__proto__', 'prototype', 'constructor']);
|
|
10
|
+
function fail(code, message) { throw Object.assign(new Error(message), { code }); }
|
|
11
|
+
function object(value, label) {
|
|
12
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)
|
|
13
|
+
|| ![null, Object.prototype].includes(Object.getPrototypeOf(value))) fail('ENTROROVE_CHAIN_JSON', `${label} must be a JSON object`);
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
function fields(value, required, optional, label) {
|
|
17
|
+
object(value, label);
|
|
18
|
+
if (required.some(key => !Object.hasOwn(value, key))
|
|
19
|
+
|| Object.keys(value).some(key => !required.includes(key) && !optional.includes(key))) {
|
|
20
|
+
fail('ENTROROVE_CHAIN_FIELDS', `${label} has missing or unknown fields`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function identifier(value, label) {
|
|
24
|
+
if (typeof value !== 'string' || !ID.test(value)) fail('ENTROROVE_CHAIN_ID', `${label} must be a safe identifier`);
|
|
25
|
+
}
|
|
26
|
+
function jsonCopy(value) {
|
|
27
|
+
const seen = new WeakSet(); let entries = 0;
|
|
28
|
+
function visit(item, depth = 0) {
|
|
29
|
+
if (depth > 128) fail('ENTROROVE_CHAIN_JSON', 'plan JSON exceeds its nesting bound');
|
|
30
|
+
if (++entries > 100000) fail('ENTROROVE_CHAIN_JSON', 'plan JSON exceeds its control bound');
|
|
31
|
+
if (item === null || typeof item === 'string' || typeof item === 'boolean') return;
|
|
32
|
+
if (typeof item === 'number' && Number.isFinite(item)) return;
|
|
33
|
+
if (typeof item !== 'object' || seen.has(item)) fail('ENTROROVE_CHAIN_JSON', 'values must be finite acyclic JSON');
|
|
34
|
+
if (!Array.isArray(item)) object(item, 'value');
|
|
35
|
+
seen.add(item);
|
|
36
|
+
for (const child of Object.values(item)) visit(child, depth + 1);
|
|
37
|
+
seen.delete(item);
|
|
38
|
+
}
|
|
39
|
+
visit(value);
|
|
40
|
+
const bytes = JSON.stringify(value);
|
|
41
|
+
if (Buffer.byteLength(bytes) > 1024 * 1024) fail('ENTROROVE_CHAIN_JSON', 'plan JSON exceeds its control byte bound');
|
|
42
|
+
return JSON.parse(bytes);
|
|
43
|
+
}
|
|
44
|
+
function freeze(value) {
|
|
45
|
+
if (value && typeof value === 'object') { for (const child of Object.values(value)) freeze(child); Object.freeze(value); }
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
function pointer(value, target = false) {
|
|
49
|
+
if (typeof value !== 'string' || (value !== '' && !value.startsWith('/')) || /~(?![01])/u.test(value)) {
|
|
50
|
+
fail('ENTROROVE_CHAIN_POINTER', 'binding must use a valid RFC 6901 JSON Pointer');
|
|
51
|
+
}
|
|
52
|
+
const parts = value === '' ? [] : value.slice(1).split('/').map(part => part.replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
53
|
+
if (target && (!parts.length || parts.some(part => UNSAFE_TARGET.has(part)))) {
|
|
54
|
+
fail('ENTROROVE_CHAIN_POINTER', 'binding target must name a declared safe input property');
|
|
55
|
+
}
|
|
56
|
+
return parts;
|
|
57
|
+
}
|
|
58
|
+
function position(value, part) {
|
|
59
|
+
if (Array.isArray(value)) {
|
|
60
|
+
if (!/^(0|[1-9][0-9]*)$/.test(part) || !Number.isSafeInteger(Number(part)) || Number(part) >= value.length) {
|
|
61
|
+
fail('ENTROROVE_CHAIN_POINTER', 'binding array index must identify an existing element');
|
|
62
|
+
}
|
|
63
|
+
} else object(value, 'pointer parent');
|
|
64
|
+
if (!Object.hasOwn(value, part)) fail('ENTROROVE_CHAIN_POINTER', 'binding pointer value is missing');
|
|
65
|
+
return part;
|
|
66
|
+
}
|
|
67
|
+
function readPointer(value, parts) {
|
|
68
|
+
let current = value;
|
|
69
|
+
for (const part of parts) current = current[position(current, part)];
|
|
70
|
+
return current;
|
|
71
|
+
}
|
|
72
|
+
function setPointer(value, parts, replacement) {
|
|
73
|
+
const parent = readPointer(value, parts.slice(0, -1));
|
|
74
|
+
const key = position(parent, parts.at(-1));
|
|
75
|
+
Object.defineProperty(parent, key, { value: jsonCopy(replacement), enumerable: true, configurable: true, writable: true });
|
|
76
|
+
}
|
|
77
|
+
function commandInput(input) {
|
|
78
|
+
fields(input, ['executable', 'args', 'workingDirectory', 'timeoutMs', 'evidenceKind', 'environment'], [], 'command input');
|
|
79
|
+
if (typeof input.executable !== 'string' || !input.executable || input.executable.includes('\0')
|
|
80
|
+
|| !Array.isArray(input.args) || input.args.some(arg => typeof arg !== 'string' || arg.includes('\0'))
|
|
81
|
+
|| typeof input.workingDirectory !== 'string' || !input.workingDirectory || input.workingDirectory.includes('\0')
|
|
82
|
+
|| !Number.isSafeInteger(input.timeoutMs) || input.timeoutMs < 1 || input.timeoutMs > 3600000
|
|
83
|
+
|| !EVIDENCE_KINDS.includes(input.evidenceKind)) fail('ENTROROVE_CHAIN_COMMAND', 'command policy is invalid');
|
|
84
|
+
object(input.environment, 'command environment');
|
|
85
|
+
for (const [key, value] of Object.entries(input.environment)) {
|
|
86
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || RESERVED_ENV.test(key) || typeof value !== 'string' || value.includes('\0')) {
|
|
87
|
+
fail('ENTROROVE_CHAIN_ENVIRONMENT', 'command environment must contain explicit non-secret strings');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (!path.isAbsolute(input.executable) && !input.environment.PATH) fail('ENTROROVE_CHAIN_COMMAND', 'relative executable requires an explicit PATH');
|
|
91
|
+
const directory = path.normalize(input.workingDirectory);
|
|
92
|
+
if (!path.isAbsolute(directory) && (directory === '..' || directory.startsWith(`..${path.sep}`))) {
|
|
93
|
+
fail('ENTROROVE_CHAIN_COMMAND', 'working directory cannot escape the original repository');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function validateBindings(step, previous) {
|
|
97
|
+
if (!Array.isArray(step.bindings)) fail('ENTROROVE_CHAIN_BINDING', 'step bindings must be an array');
|
|
98
|
+
if (step.kind === 'command' && step.bindings.length) fail('ENTROROVE_CHAIN_BINDING', 'command input cannot receive dynamic bindings');
|
|
99
|
+
const targets = [];
|
|
100
|
+
for (const binding of step.bindings) {
|
|
101
|
+
fields(binding, ['stepId', 'source', 'target'], [], 'binding');
|
|
102
|
+
if (!previous.has(binding.stepId)) fail('ENTROROVE_CHAIN_BINDING', 'binding source must name an earlier declared step');
|
|
103
|
+
pointer(binding.source);
|
|
104
|
+
const target = pointer(binding.target, true);
|
|
105
|
+
readPointer(step.input, target);
|
|
106
|
+
if (targets.some(parts => parts.every((part, index) => target[index] === part)
|
|
107
|
+
|| target.every((part, index) => parts[index] === part))) fail('ENTROROVE_CHAIN_BINDING', 'binding targets cannot overlap');
|
|
108
|
+
targets.push(target);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function validateStep(step, previous, skills) {
|
|
112
|
+
fields(step, ['stepId', 'kind', 'skillId', 'operation', 'input', 'dependsOn', 'bindings'], ['continueWhen'], 'step');
|
|
113
|
+
identifier(step.stepId, 'stepId'); identifier(step.operation, 'operation'); object(step.input, 'step input');
|
|
114
|
+
if (previous.has(step.stepId) || !KINDS.includes(step.kind) || !Array.isArray(step.dependsOn)
|
|
115
|
+
|| new Set(step.dependsOn).size !== step.dependsOn.length || step.dependsOn.some(id => !previous.has(id))) {
|
|
116
|
+
fail('ENTROROVE_CHAIN_TOPOLOGY', 'steps must have unique IDs and only earlier dependencies');
|
|
117
|
+
}
|
|
118
|
+
if (step.kind === 'skill' ? !skills.has(step.skillId) : step.skillId !== null) fail('ENTROROVE_CHAIN_SKILL', 'step skillId must match its declared kind and installed package declaration');
|
|
119
|
+
if (step.kind === 'command') {
|
|
120
|
+
if (step.operation !== 'exec') fail('ENTROROVE_CHAIN_COMMAND', 'command operation must be exec');
|
|
121
|
+
commandInput(step.input);
|
|
122
|
+
}
|
|
123
|
+
if (step.kind === 'coordinator' && step.operation === 'resolve-human') fail('ENTROROVE_CHAIN_DECISION', 'human resolution cannot be injected by a plan');
|
|
124
|
+
if (step.continueWhen !== undefined) {
|
|
125
|
+
if (step.kind !== 'skill' || step.skillId !== 'confirm-protocol' || step.operation !== 'interaction-request') {
|
|
126
|
+
fail('ENTROROVE_CHAIN_DECISION', 'continueWhen applies only to a declared confirmation request');
|
|
127
|
+
}
|
|
128
|
+
fields(step.continueWhen, ['answer'], [], 'continueWhen');
|
|
129
|
+
const answer = step.continueWhen.answer;
|
|
130
|
+
if (!(typeof answer === 'string' && answer.trim()) && !(Array.isArray(answer) && answer.length
|
|
131
|
+
&& answer.every(item => typeof item === 'string' && item.trim()) && new Set(answer).size === answer.length)) {
|
|
132
|
+
fail('ENTROROVE_CHAIN_DECISION', 'continueWhen must declare an explicit string or unique choice IDs');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
validateBindings(step, previous);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Metadata validation never verifies installed packages, approves commands or
|
|
139
|
+
// confers execution authority. The managed dispatcher retains those checks.
|
|
140
|
+
export function validateLocalChainPlan(value) {
|
|
141
|
+
const plan = jsonCopy(value);
|
|
142
|
+
fields(plan, ['schemaVersion', 'chainId', 'skills', 'steps'], [], 'plan');
|
|
143
|
+
identifier(plan.chainId, 'chainId');
|
|
144
|
+
if (plan.schemaVersion !== LOCAL_CHAIN_PLAN_SCHEMA || !Array.isArray(plan.skills)
|
|
145
|
+
|| !Array.isArray(plan.steps) || !plan.steps.length) fail('ENTROROVE_CHAIN_PLAN_INVALID', 'chain plan schema, skills and steps are required');
|
|
146
|
+
const skills = new Set();
|
|
147
|
+
for (const skill of plan.skills) {
|
|
148
|
+
fields(skill, ['skillId', 'packageRoot'], [], 'skill'); identifier(skill.skillId, 'skillId');
|
|
149
|
+
if (skills.has(skill.skillId) || typeof skill.packageRoot !== 'string' || !path.isAbsolute(skill.packageRoot)
|
|
150
|
+
|| skill.packageRoot.includes('\0')) fail('ENTROROVE_CHAIN_SKILL', 'skills require unique IDs and absolute package declarations');
|
|
151
|
+
skills.add(skill.skillId);
|
|
152
|
+
}
|
|
153
|
+
const previous = new Set();
|
|
154
|
+
for (const step of plan.steps) { validateStep(step, previous, skills); previous.add(step.stepId); }
|
|
155
|
+
return freeze(plan);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// completed must come from the dispatcher's original durable outcomes. This
|
|
159
|
+
// resolver does not authenticate caller-supplied results or advance a journal.
|
|
160
|
+
export function bindLocalChainStepInput(value, stepId, completed) {
|
|
161
|
+
const plan = validateLocalChainPlan(value), index = plan.steps.findIndex(step => step.stepId === stepId);
|
|
162
|
+
if (index < 0) fail('ENTROROVE_CHAIN_STEP_UNKNOWN', 'binding target is outside the immutable plan');
|
|
163
|
+
if (!Array.isArray(completed)) fail('ENTROROVE_CHAIN_RESULT', 'completed outcomes must be an array');
|
|
164
|
+
const earlier = new Set(plan.steps.slice(0, index).map(step => step.stepId)), results = new Map();
|
|
165
|
+
for (const result of completed) {
|
|
166
|
+
fields(result, ['stepId', 'status', 'output'], [], 'completed outcome');
|
|
167
|
+
if (!earlier.has(result.stepId) || results.has(result.stepId)) fail('ENTROROVE_CHAIN_RESULT', 'outcomes must belong to unique earlier steps');
|
|
168
|
+
results.set(result.stepId, jsonCopy(result));
|
|
169
|
+
}
|
|
170
|
+
const step = plan.steps[index];
|
|
171
|
+
for (const dependency of new Set([...step.dependsOn, ...step.bindings.map(binding => binding.stepId)])) {
|
|
172
|
+
if (results.get(dependency)?.status !== 'succeeded') fail('ENTROROVE_CHAIN_DEPENDENCY', 'binding requires the original successful dependency outcome');
|
|
173
|
+
}
|
|
174
|
+
const input = jsonCopy(step.input);
|
|
175
|
+
for (const binding of step.bindings) setPointer(input, pointer(binding.target, true), readPointer(results.get(binding.stepId).output, pointer(binding.source)));
|
|
176
|
+
return input;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function statuses(value, found = new Set()) {
|
|
180
|
+
if (value && typeof value === 'object') {
|
|
181
|
+
if (typeof value.status === 'string') found.add(value.status);
|
|
182
|
+
for (const child of Object.values(value)) statuses(child, found);
|
|
183
|
+
}
|
|
184
|
+
return found;
|
|
185
|
+
}
|
|
186
|
+
function equal(left, right) { return JSON.stringify(left) === JSON.stringify(right); }
|
|
187
|
+
function gate(state, code) { return Object.freeze({ state, code, eligible: state === 'eligible', executionVerified: false, authority: 'protocol-shape-only' }); }
|
|
188
|
+
|
|
189
|
+
// A protocol-success envelope is necessary, never proof of actual execution.
|
|
190
|
+
// The dispatcher must authenticate its broker/runner result before committing.
|
|
191
|
+
export function inspectLocalChainResult(step, observed) {
|
|
192
|
+
object(step, 'step'); const response = jsonCopy(observed);
|
|
193
|
+
if (!KINDS.includes(step.kind)) fail('ENTROROVE_CHAIN_STEP_INVALID', 'result gate requires a declared step kind');
|
|
194
|
+
object(response, 'response');
|
|
195
|
+
const output = response.output ?? response.result ?? response;
|
|
196
|
+
object(output, 'protocol output');
|
|
197
|
+
const states = new Set([response.status, output.status]);
|
|
198
|
+
if (states.has('uncertain') || output.process?.status === 'uncertain') return gate('uncertain', 'ENTROROVE_CHAIN_OUTCOME_UNCERTAIN');
|
|
199
|
+
if (states.has('failed')) return gate('failed', 'ENTROROVE_CHAIN_RESULT_FAILED');
|
|
200
|
+
if (states.has('blocked')) return gate('blocked', 'ENTROROVE_CHAIN_RESULT_BLOCKED');
|
|
201
|
+
if (['waiting', 'pending', 'pending-execution', 'incomplete'].some(state => states.has(state))
|
|
202
|
+
|| statuses(output).has('pending-execution')) return gate('waiting', 'ENTROROVE_CHAIN_RESULT_PENDING');
|
|
203
|
+
if (response.status !== 'succeeded') return gate('waiting', 'ENTROROVE_CHAIN_RESULT_UNVERIFIED');
|
|
204
|
+
if (step.kind === 'command') {
|
|
205
|
+
const process = output.process ?? output;
|
|
206
|
+
if (!Number.isInteger(process.exitCode)) return gate('uncertain', 'ENTROROVE_CHAIN_PROCESS_UNKNOWN');
|
|
207
|
+
if (process.exitCode !== 0 || process.signal || process.timedOut || process.outputLimitExceeded) return gate('failed', 'ENTROROVE_CHAIN_PROCESS_FAILED');
|
|
208
|
+
}
|
|
209
|
+
if (step.kind === 'skill' && step.skillId === 'validator') {
|
|
210
|
+
const verdict = output.report?.verdict ?? output.verdict;
|
|
211
|
+
if (step.operation === 'verdict' && !['pass', 'pass-with-risk'].includes(verdict)) return gate(verdict === 'blocked' ? 'blocked' : 'waiting', 'ENTROROVE_CHAIN_VALIDATOR_VERDICT');
|
|
212
|
+
if (step.operation === 'functional-verify') {
|
|
213
|
+
if (output.summary?.failed > 0 || output.evidence?.some(item => item.exitCode !== 0)) return gate('blocked', 'ENTROROVE_CHAIN_VALIDATOR_FAILED');
|
|
214
|
+
if (!Array.isArray(output.evidence) || !output.evidence.length || output.evidence.some(item => item.runner !== 'trusted-runner' || !item.receipt)) return gate('waiting', 'ENTROROVE_CHAIN_VALIDATOR_INCOMPLETE');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (step.kind === 'skill' && step.skillId === 'confirm-protocol' && step.operation === 'interaction-request') {
|
|
218
|
+
if (!step.continueWhen || !Object.hasOwn(output, 'answer')) return gate('waiting', 'ENTROROVE_CHAIN_DECISION_REQUIRED');
|
|
219
|
+
if (!equal(output.answer, step.continueWhen.answer)) return gate('blocked', 'ENTROROVE_CHAIN_DECISION_DENIED');
|
|
220
|
+
}
|
|
221
|
+
return gate('eligible', 'ENTROROVE_CHAIN_AUTHENTICATION_REQUIRED');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function localChainPlanDigest(plan) {
|
|
225
|
+
const value = validateLocalChainPlan(plan);
|
|
226
|
+
function canonical(item) {
|
|
227
|
+
if (Array.isArray(item)) return `[${item.map(canonical).join(',')}]`;
|
|
228
|
+
if (item && typeof item === 'object') return `{${Object.keys(item).sort().map(key => `${JSON.stringify(key)}:${canonical(item[key])}`).join(',')}}`;
|
|
229
|
+
return JSON.stringify(item);
|
|
230
|
+
}
|
|
231
|
+
return createHash('sha256').update(canonical(value)).digest('hex');
|
|
232
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const chainReleaseLabels = Object.freeze({
|
|
2
|
+
'local-chain-plan.mjs': { zh: '原链严格计划与结果门禁', en: 'strict original-chain plan and result gates' },
|
|
3
|
+
'local-chain-bindings.mjs': { zh: '原链安装元数据与执行映射', en: 'original-chain installed metadata and execution bindings' },
|
|
4
|
+
'local-chain-lock.mjs': { zh: '原链宿主锁与中断恢复', en: 'original-chain host locks and interruption recovery' },
|
|
5
|
+
'local-chain-broker.mjs': { zh: '原链同流程技能调用', en: 'original-chain skill calls on the same workflow' },
|
|
6
|
+
'local-chain-human.mjs': { zh: '原链终端确认与真实回调', en: 'original-chain terminal confirmation and original callbacks' },
|
|
7
|
+
'local-chain-controller.mjs': { zh: '原链持久受控执行器', en: 'durable original-chain controlled executor' },
|
|
8
|
+
'local-chain-contracts.mjs': { zh: '原链本地严格操作契约', en: 'strict original-chain local operation contracts' },
|
|
9
|
+
'local-chain-operations.mjs': { zh: '原链受控操作入口', en: 'original-chain controlled operation entry points' },
|
|
10
|
+
'local-chain-release-labels.mjs': { zh: '原链独立更新说明', en: 'independent original-chain release descriptions' },
|
|
11
|
+
'local-chain-results.mjs': { zh: '原链结果与退出状态', en: 'original-chain results and exit states' },
|
|
12
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { observeWorkflowLocalStep } from './workflow-local-runner.mjs';
|
|
2
|
+
import { buildInvocation } from './remote-client.mjs';
|
|
3
|
+
import { inspectChainSkillResponse } from './local-chain-broker.mjs';
|
|
4
|
+
import { inspectChainHumanResponse } from './local-chain-human.mjs';
|
|
5
|
+
import { bindLocalChainStepInput } from './local-chain-plan.mjs';
|
|
6
|
+
import { chainCanonical as canonical, chainFail as fail, chainSha as sha } from './local-chain-bindings.mjs';
|
|
7
|
+
|
|
8
|
+
function denied(message) { fail('ENTROROVE_CHAIN_RESULT_BINDING', message); }
|
|
9
|
+
async function commandResult(state, original, step) {
|
|
10
|
+
const binding = state.commandBindings.find(value => value.stepId === original.stepId);
|
|
11
|
+
if (!binding || step.phase !== 'done' || !step.localResult?.receipt || !step.output || step.error !== null) {
|
|
12
|
+
denied('verified command has no original process receipt and result');
|
|
13
|
+
}
|
|
14
|
+
let actual;
|
|
15
|
+
try { actual = await observeWorkflowLocalStep(binding.workflowInput); }
|
|
16
|
+
catch { denied('verified command has no independently valid original native cache'); }
|
|
17
|
+
if (!actual?.receipt || actual.status !== 'reported' || actual.receipt.result?.passed !== true
|
|
18
|
+
|| ['process', 'evidence', 'receipt', 'subject', 'statePath'].some(key => canonical(actual[key]) !== canonical(step.localResult[key]))) {
|
|
19
|
+
denied('verified command differs from its actual signed native process');
|
|
20
|
+
}
|
|
21
|
+
const output = step.output, lease = binding.workflowInput.lease;
|
|
22
|
+
if (output.status !== 'verified' || output.flowId !== state.identity.flowId || output.stepId !== step.stepId
|
|
23
|
+
|| output.generation !== lease.generation || canonical(output.lease) !== canonical(lease)
|
|
24
|
+
|| output.evidenceDigest !== sha(canonical(actual.evidence[0])) || canonical(output.evidence) !== canonical(actual.evidence[0])
|
|
25
|
+
|| output.verification?.summary?.passed !== 1 || output.verification.summary.failed !== 0
|
|
26
|
+
|| output.verification.findings?.length !== 0) denied('verified command has no matching independent verification of its exact original receipt');
|
|
27
|
+
}
|
|
28
|
+
function skillRequest(state, original, step) {
|
|
29
|
+
if (step.phase !== 'done' || !step.response || !step.request || step.error !== null) denied('verified skill has no original durable request result');
|
|
30
|
+
const preceding = state.steps.filter(value => value.status === 'verified' && value.stepId !== original.stepId)
|
|
31
|
+
.map(value => ({ stepId: value.stepId, status: 'succeeded', output: value.output }));
|
|
32
|
+
const bound = bindLocalChainStepInput(state.plan, original.stepId, preceding);
|
|
33
|
+
const answer = original.skillId === 'confirm-protocol' && original.operation === 'interaction-request' && step.response.result?.auditEntry;
|
|
34
|
+
const expected = answer ? buildInvocation('continue', step.request.input)
|
|
35
|
+
: buildInvocation('continue', { flowId: state.identity.flowId, operation: original.operation, payload: bound });
|
|
36
|
+
if (step.request.operation !== 'continue' || step.request.requestId !== expected.requestId || step.response.requestId !== expected.requestId
|
|
37
|
+
|| canonical(step.request.input) !== canonical(expected.body.input)) denied('verified skill lost its exact original canonical request');
|
|
38
|
+
if (answer) {
|
|
39
|
+
const saved = { ...step, output: step.originalInteraction };
|
|
40
|
+
if (!saved.output?.interaction || saved.output.interactionDigest !== sha(canonical(saved.output.interaction))
|
|
41
|
+
|| step.request.input.flowId !== state.identity.flowId || step.request.input.operation !== 'interaction-answer'
|
|
42
|
+
|| step.request.input.payload.interactionId !== saved.output.interactionId
|
|
43
|
+
|| step.request.input.payload.interactionDigest !== saved.output.interactionDigest
|
|
44
|
+
|| !inspectChainHumanResponse(original, state, saved, step.response).eligible) denied('verified answer lacks its original audited callback');
|
|
45
|
+
if (canonical(step.output) !== canonical({ ...step.response.result, answer: step.response.result.auditEntry.answer })) denied('verified answer changed its original audited output');
|
|
46
|
+
} else if (!inspectChainSkillResponse(original, state, step.response).eligible || canonical(step.output) !== canonical(step.response.result)) {
|
|
47
|
+
denied('verified skill changed its actual original protocol result');
|
|
48
|
+
}
|
|
49
|
+
return expected.requestId;
|
|
50
|
+
}
|
|
51
|
+
export async function validateArchivedChainResults(state) {
|
|
52
|
+
for (const original of state.plan.steps) {
|
|
53
|
+
const step = state.steps.find(value => value.stepId === original.stepId);
|
|
54
|
+
if (step.status !== 'verified') continue;
|
|
55
|
+
if (original.kind === 'command') await commandResult(state, original, step);
|
|
56
|
+
else if (original.kind === 'skill') skillRequest(state, original, step);
|
|
57
|
+
else denied('local ledger cannot manufacture a verified coordinator result');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export async function revalidateArchivedChainSkill(state, original, step, client) {
|
|
61
|
+
const requestId = skillRequest(state, original, step);
|
|
62
|
+
let observed;
|
|
63
|
+
try { observed = await client.status(requestId); } catch { denied('original verified skill result is unavailable; never replay its operation'); }
|
|
64
|
+
if (canonical(observed) !== canonical(step.response)) denied('archived skill differs from the actual durable original request result');
|
|
65
|
+
}
|
|
66
|
+
export async function revalidateArchivedChainCommand(state, original, step, client) {
|
|
67
|
+
await commandResult(state, original, step);
|
|
68
|
+
const request = buildInvocation('continue', { flowId: state.identity.flowId,
|
|
69
|
+
operation: 'execution-verify', payload: { stepId: original.stepId } });
|
|
70
|
+
let observed;
|
|
71
|
+
try { observed = await client.status(request.requestId); }
|
|
72
|
+
catch { denied('the original command has no actual durable independent verification; never manufacture or replay it'); }
|
|
73
|
+
if (observed.requestId !== request.requestId || !['succeeded', 'reported'].includes(observed.status)
|
|
74
|
+
|| canonical(observed.result) !== canonical(step.output)) denied('archived command differs from the actual independently verified original request');
|
|
75
|
+
}
|
package/local-cli.mjs
CHANGED
|
@@ -1,58 +1,99 @@
|
|
|
1
|
+
import { withLocalInvocation } from './local-invocations.mjs';
|
|
2
|
+
import { peerCoordinate, spawnBind, completePeer, verifyCoordinationLease } from './local-swarm.mjs';
|
|
3
|
+
import { attestMutationSnapshot14 } from './local-mutation-binding.mjs';
|
|
4
|
+
import { recordMutationDenied14 } from './local-mutation-audit.mjs';
|
|
1
5
|
import {
|
|
2
6
|
LOCAL_CAPABILITIES, budgetContext, budgetExtend, budgetInit, budgetRead, budgetStatus,
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
evaluateFormula, evaluateFormulaGraph, formulaFinalGate, formulaFinalGateFile, runApprovedPlan,
|
|
7
|
+
finalGate, guardedWrite, issueMutationPass, mergeVerified, mutateGate,
|
|
8
|
+
rollback, signEvidence, snapshotPlan, snapshotVerify,
|
|
9
|
+
verifyEvidence, verifyMutationPass,
|
|
10
|
+
evaluateFormula, evaluateFormulaGraph, formulaFinalGate, formulaFinalGateFile, runApprovedPlan, runWorkflowLocalStep,
|
|
7
11
|
LOCAL_LEDGER_OPERATIONS,
|
|
8
12
|
LOCAL_RENEWAL_OPERATIONS, LOCAL_PROBE_OPERATIONS,
|
|
9
13
|
LOCAL_TASK_ROUTING_OPERATIONS,
|
|
10
14
|
} from './local-execution.mjs';
|
|
15
|
+
import { validateLocalInput } from './local-contracts.mjs';
|
|
16
|
+
import { prepareMergeArtifacts } from './local-merge-artifacts.mjs';
|
|
17
|
+
import { produceMergeArchguardPreflight } from './local-merge-archguard.mjs';
|
|
18
|
+
import { produceMergeJournalHead } from './local-merge-head.mjs';
|
|
19
|
+
import { CALCTOOL_LOCAL_OPERATIONS } from './local-calctool.mjs';
|
|
20
|
+
import { withBudgetReadContext } from './local-budget-reads.mjs';
|
|
21
|
+
import { LOCAL_BUDGET_TRANSITION_OPERATIONS } from './local-budget-transition.mjs';
|
|
22
|
+
import { ARCHGUARD_LOCAL_OPERATIONS } from './local-archguard-operations.mjs';
|
|
23
|
+
import { LOCAL_BUDGET_HANDOFF_OPERATIONS } from './local-budget-handoff.mjs';
|
|
24
|
+
import { LOCAL_CHAIN_OPERATIONS } from './local-chain-operations.mjs';
|
|
25
|
+
import { runManagedWorkflowLocalStep } from './workflow-local-client.mjs';
|
|
26
|
+
import { localErrorEnvelope } from './local-error.mjs';
|
|
11
27
|
|
|
12
28
|
async function stdinJson() {
|
|
13
29
|
let raw = '';
|
|
14
30
|
for await (const chunk of process.stdin) raw += chunk;
|
|
15
31
|
if (!raw.trim()) return {};
|
|
16
|
-
|
|
32
|
+
let value;
|
|
33
|
+
try { value = JSON.parse(raw); } catch { throw new Error('Local input must be valid JSON'); }
|
|
17
34
|
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('local input must be a JSON object');
|
|
18
35
|
return value;
|
|
19
36
|
}
|
|
20
37
|
|
|
21
38
|
const operations = {
|
|
22
|
-
'snapshot-plan': snapshotPlan, 'snapshot-verify': snapshotVerify, 'mutate-gate': mutateGate,
|
|
39
|
+
'snapshot-plan': snapshotPlan, 'snapshot-verify': snapshotVerify, 'mutation-snapshot-attest': attestMutationSnapshot14, 'mutate-gate': mutateGate,
|
|
23
40
|
'gate-issue': issueMutationPass, 'gate-verify': verifyMutationPass, 'guarded-write': guardedWrite,
|
|
24
41
|
'budget-init': budgetInit, 'budget-context': budgetContext, 'budget-read': budgetRead, 'budget-status': budgetStatus,
|
|
25
42
|
'budget-extend': budgetExtend, 'peer-coordinate': peerCoordinate, coordinate: peerCoordinate,
|
|
26
43
|
'spawn-bind': spawnBind, 'peer-complete': completePeer, complete: completePeer,
|
|
27
44
|
'lease-verify': verifyCoordinationLease,
|
|
28
45
|
'evidence-sign': signEvidence, 'evidence-verify': verifyEvidence, 'merge-verified': mergeVerified,
|
|
29
|
-
rollback, '
|
|
30
|
-
'
|
|
31
|
-
'formula-
|
|
32
|
-
'formula-
|
|
46
|
+
rollback, 'merge-freeze': prepareMergeArtifacts, 'merge-preflight': produceMergeArchguardPreflight,
|
|
47
|
+
'merge-journal-head': produceMergeJournalHead, 'final-gate': finalGate,
|
|
48
|
+
'formula-evaluate': input => ({ schemaVersion: 'entrorove.local-execution/1.0', status: 'reported', value: evaluateFormula(input.formula, input.values ?? {}).toString() }),
|
|
49
|
+
'formula-graph': input => ({ schemaVersion: 'entrorove.local-execution/1.0', status: 'reported', values: evaluateFormulaGraph(input.formulas, input.inputs ?? {}) }),
|
|
50
|
+
'formula-final-gate': input => formulaFinalGate(input.engine, input), 'calculation-final-gate': formulaFinalGateFile,
|
|
33
51
|
'run-approved-plan': runApprovedPlan,
|
|
34
|
-
|
|
52
|
+
'workflow-run-step': runManagedWorkflowLocalStep,
|
|
53
|
+
...LOCAL_LEDGER_OPERATIONS, ...LOCAL_CHAIN_OPERATIONS, ...LOCAL_BUDGET_TRANSITION_OPERATIONS,
|
|
35
54
|
...LOCAL_RENEWAL_OPERATIONS, ...LOCAL_PROBE_OPERATIONS,
|
|
36
55
|
...LOCAL_TASK_ROUTING_OPERATIONS,
|
|
56
|
+
...ARCHGUARD_LOCAL_OPERATIONS,
|
|
57
|
+
...LOCAL_BUDGET_HANDOFF_OPERATIONS,
|
|
58
|
+
...CALCTOOL_LOCAL_OPERATIONS,
|
|
37
59
|
};
|
|
38
60
|
|
|
39
61
|
export const LOCAL_OPERATIONS = Object.freeze(operations);
|
|
40
62
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
63
|
+
async function checkedLocalInput(operation, input) {
|
|
64
|
+
try { return validateLocalInput(operation, input); }
|
|
65
|
+
catch (cause) {
|
|
66
|
+
if (['guarded-write', 'gate-issue', 'mutate-gate', 'mutation-snapshot-attest'].includes(operation)) await recordMutationDenied14(input, cause, operation);
|
|
67
|
+
throw cause;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function runLocal(args = [], dependencies) {
|
|
72
|
+
try {
|
|
73
|
+
const operation = args[0];
|
|
74
|
+
if (operation === 'capabilities' || operation === 'help') {
|
|
75
|
+
validateLocalInput(operation, process.stdin.isTTY ? {} : await stdinJson());
|
|
76
|
+
process.stdout.write(`${JSON.stringify(LOCAL_CAPABILITIES)}\n`); return;
|
|
77
|
+
}
|
|
78
|
+
const handler = operations[operation];
|
|
79
|
+
if (!handler) throw new Error(`Usage: entrorove local <${['capabilities', ...Object.keys(operations)].join('|')}> [repositoryRoot]`);
|
|
80
|
+
const input = await stdinJson();
|
|
81
|
+
if (args[1] && input.repositoryRoot === undefined) input.repositoryRoot = args[1];
|
|
82
|
+
const validated = await checkedLocalInput(operation, input);
|
|
83
|
+
const supplied = operation === 'workflow-run-step' || Object.hasOwn(LOCAL_CHAIN_OPERATIONS, operation) ? dependencies : undefined;
|
|
84
|
+
const output = await withLocalInvocation(operation, validated, () => withBudgetReadContext(validated, () => handler(validated, supplied)));
|
|
85
|
+
process.stdout.write(`${JSON.stringify(output)}\n`);
|
|
86
|
+
if (['pending', 'blocked', 'failed', 'incomplete'].includes(output?.status)) process.exitCode = 2;
|
|
87
|
+
} catch (cause) {
|
|
88
|
+
const error = localErrorEnvelope(cause); process.stderr.write(`${JSON.stringify(error)}\n`); process.exitCode = error.status === 'blocked' ? 2 : 1;
|
|
89
|
+
}
|
|
51
90
|
}
|
|
52
91
|
|
|
53
|
-
export async function runLocalInput(operation, input) {
|
|
54
|
-
if (operation === 'capabilities' || operation === 'help') return LOCAL_CAPABILITIES;
|
|
92
|
+
export async function runLocalInput(operation, input, dependencies) {
|
|
93
|
+
if (operation === 'capabilities' || operation === 'help') { validateLocalInput(operation, input); return LOCAL_CAPABILITIES; }
|
|
55
94
|
const handler = operations[operation];
|
|
56
95
|
if (!handler) throw new Error(`Unsupported local operation: ${operation}`);
|
|
57
|
-
|
|
96
|
+
const validated = await checkedLocalInput(operation, input);
|
|
97
|
+
const supplied = operation === 'workflow-run-step' || Object.hasOwn(LOCAL_CHAIN_OPERATIONS, operation) ? dependencies : undefined;
|
|
98
|
+
return withLocalInvocation(operation, validated, () => withBudgetReadContext(validated, () => handler(validated, supplied)));
|
|
58
99
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export const text = { type: 'string', minLength: 1 };
|
|
2
|
+
export const identifier = { ...text, pattern: '^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$' };
|
|
3
|
+
export const ledgerIdentifier = { ...text, pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' };
|
|
4
|
+
export const sha256 = { type: 'string', pattern: '^[0-9a-f]{64}$' };
|
|
5
|
+
export const nullable = schema => ({ anyOf: [schema, { type: 'null' }] });
|
|
6
|
+
export const array = (items, minItems = 0) => ({ type: 'array', items, minItems });
|
|
7
|
+
export const integer = (minimum, maximum = Number.MAX_SAFE_INTEGER) => ({ type: 'integer', minimum, maximum });
|
|
8
|
+
export const enumeration = values => ({ enum: values });
|
|
9
|
+
export const object = (properties, required = []) => ({ type: 'object', properties, required, additionalProperties: false });
|
|
10
|
+
export const record = { type: 'object' };
|
|
11
|
+
export const relativePath = { ...text, pattern: '^(?!/)(?![A-Za-z]:)(?!.*[\\u0000-\\u001f\\u007f])(?!.*\\\\)(?!.*(?:^|/)(?:\\.\\.?)(?:/|$))[^/]+(?:/[^/]+)*$', $comment: 'Paths must also be NFC; filesystem authority validates normalization and symlink boundaries.' };
|
|
12
|
+
export const paths = { ...array(relativePath, 1), uniqueItems: true };
|
|
13
|
+
export const modes = enumeration(['lock', 'probe', 'swarm']);
|
|
14
|
+
export const priority = enumeration(['urgent', 'normal', 'dependency', 'background']);
|
|
15
|
+
export const executionContext = { oneOf: [
|
|
16
|
+
object({ executionIsolation: { const: 'local' } }, ['executionIsolation']),
|
|
17
|
+
object({ executionIsolation: { const: 'direct' }, cloudSandboxEnabled: { type: 'boolean' } }, ['executionIsolation', 'cloudSandboxEnabled']),
|
|
18
|
+
object({ executionIsolation: { const: 'sandbox' }, cloudSandboxEnabled: { const: true } }, ['executionIsolation', 'cloudSandboxEnabled']),
|
|
19
|
+
] };
|
|
20
|
+
export const confirmation = object({ answer: { const: 'approve' }, requestId: identifier }, ['answer', 'requestId']);
|
|
21
|
+
export const binding = { flowId: identifier, stepId: identifier, snapshotDigest: sha256, contractDigest: sha256, receiptPublicKey: text };
|
|
22
|
+
export const evidence = object({
|
|
23
|
+
schemaVersion: { const: 'entrorove.test-evidence/2.0' }, evidenceId: { ...text, maxLength: 200, pattern: '\\S' },
|
|
24
|
+
kind: enumeration(['test', 'build', 'lint', 'security', 'benchmark']), runner: enumeration(['local', 'trusted-runner', 'sandbox']),
|
|
25
|
+
command: { ...text, maxLength: 2000, pattern: '\\S' }, exitCode: integer(-128, 255), durationMs: integer(0, 86400000),
|
|
26
|
+
summary: { ...text, maxLength: 4000, pattern: '\\S' }, artifactSha256: sha256,
|
|
27
|
+
subject: {}, subjectDigest: sha256, receipt: record, flowId: { ...text, maxLength: 200 }, stepId: { ...text, maxLength: 200 },
|
|
28
|
+
}, ['schemaVersion', 'evidenceId', 'kind', 'runner', 'command', 'exitCode', 'durationMs', 'summary', 'artifactSha256']);
|
|
29
|
+
export const evidenceArray = array(evidence, 1);
|
|
30
|
+
export const change = object({ gatePassPath: text, targetPath: relativePath, content: {}, expectedSha256: nullable(sha256), workerId: identifier, leaseId: identifier }, ['gatePassPath', 'targetPath', 'content']);
|
|
31
|
+
export const decimal = { anyOf: [{ type: 'string', pattern: '^\\s*[+-]?(?:[0-9]+(?:\\.[0-9]*)?|\\.[0-9]+)\\s*$' }, { type: 'number' }] };
|
|
32
|
+
export const formula = { $ref: '#/$defs/formula' };
|
|
33
|
+
function expression(operators, minItems, maxItems) {
|
|
34
|
+
return object({ op: enumeration(operators), args: { ...array(formula, minItems), ...(maxItems === undefined ? {} : { maxItems }) } }, ['op', 'args']);
|
|
35
|
+
}
|
|
36
|
+
export const formulaDefinitions = { formula: { oneOf: [
|
|
37
|
+
object({ ref: text }, ['ref']), object({ lit: decimal }, ['lit']),
|
|
38
|
+
expression(['add', 'mul'], 1), expression(['sub', 'div', 'safeDivide', 'percentOf'], 2, 2),
|
|
39
|
+
expression(['round'], 1, 1), expression(['if'], 3, 3),
|
|
40
|
+
] } };
|
|
41
|
+
export const decimalValues = { type: 'object', additionalProperties: decimal };
|
|
42
|
+
export const formulaEntry = { type: 'object', properties: { key: text, expression: formula }, required: ['key', 'expression'] };
|
|
43
|
+
export const executionPlan = { type: 'object', properties: {
|
|
44
|
+
schemaVersion: { const: 'entrorove.execution-plan/1.0' }, chainId: ledgerIdentifier,
|
|
45
|
+
steps: array({ type: 'object', properties: { stepId: ledgerIdentifier, kind: enumeration(['command', 'skill', 'coordinator', 'decision']) }, required: ['stepId', 'kind'] }, 1),
|
|
46
|
+
}, required: ['schemaVersion', 'chainId', 'steps'] };
|