@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,103 @@
|
|
|
1
|
+
import Ajv from 'ajv';
|
|
2
|
+
import { CALCTOOL_LOCAL_NAMES, CALCTOOL_LOCAL_OPERATION_SCHEMAS } from './local-calctool-contracts.mjs';
|
|
3
|
+
import { withBudgetReadContext } from './local-budget-reads.mjs';
|
|
4
|
+
import { formulaError } from './local-decimal.mjs';
|
|
5
|
+
import { buildFormulaEngine, compileFormulaEngine, verifyFormulaProgram, evaluateFormulaProgram, formulaImpact, formulaDigest } from './local-formula-engine.mjs';
|
|
6
|
+
import { formulaFinalGate, readFormulaFile } from './local-formula.mjs';
|
|
7
|
+
import { storeFormula, loadStoredFormula, formulaInventory, purgeStoredFormula } from './local-calctool-storage.mjs';
|
|
8
|
+
import { loadCalctoolRuntime, calctoolRuntimeBytes } from './local-calctool-runtime.mjs';
|
|
9
|
+
|
|
10
|
+
const schemaVersion = 'entrorove.local-execution/1.0';
|
|
11
|
+
const ajv = new Ajv({ strict: true, strictRequired: false, allErrors: true });
|
|
12
|
+
const validators = new Map(Object.entries(CALCTOOL_LOCAL_OPERATION_SCHEMAS).map(([name, schema]) => [name, ajv.compile(schema)]));
|
|
13
|
+
export function validateCalctoolLocalInput(operation, input) {
|
|
14
|
+
if (typeof operation !== 'string') throw formulaError('ENTROROVE_OPERATION_UNKNOWN', 'Calctool operation must be a string');
|
|
15
|
+
const name = operation.startsWith('calctool-') ? operation : `calctool-${operation}`;
|
|
16
|
+
const validator = validators.get(name);
|
|
17
|
+
if (!validator) throw formulaError('ENTROROVE_OPERATION_UNKNOWN', 'unsupported Calctool operation');
|
|
18
|
+
if (!validator(input)) throw formulaError('ENTROROVE_INPUT_INVALID', `Invalid ${name} input: ${validator.errors.map(item => `${item.instancePath || '/'} ${item.message}`).join('; ')}`);
|
|
19
|
+
return input;
|
|
20
|
+
}
|
|
21
|
+
async function programFrom(input) {
|
|
22
|
+
if (input.program) return verifyFormulaProgram(input.program, input);
|
|
23
|
+
if (input.engineId) return (await loadStoredFormula(input)).program;
|
|
24
|
+
const engine = input.engine ?? (input.enginePath ? (await readFormulaFile(input)).engine : buildFormulaEngine(input.requirements));
|
|
25
|
+
return verifyFormulaProgram(compileFormulaEngine(engine), input);
|
|
26
|
+
}
|
|
27
|
+
function validatedPlan(plan, completed = []) {
|
|
28
|
+
if (!plan || !Array.isArray(plan.tasks)) throw formulaError('RUN_PLAN_INVALID', 'runPlan tasks are required');
|
|
29
|
+
const definitions = new Map();
|
|
30
|
+
for (const task of plan.tasks) {
|
|
31
|
+
if (!task || typeof task.workItemId !== 'string' || definitions.has(task.workItemId) || !Array.isArray(task.dependsOn)) throw formulaError('RUN_PLAN_INVALID', 'runPlan requires unique work items and explicit dependencies');
|
|
32
|
+
definitions.set(task.workItemId, task);
|
|
33
|
+
}
|
|
34
|
+
const visiting = new Set(); const done = new Set();
|
|
35
|
+
const visit = key => {
|
|
36
|
+
if (done.has(key)) return;
|
|
37
|
+
if (!definitions.has(key)) throw formulaError('RUN_PLAN_INVALID', 'runPlan contains an unknown dependency');
|
|
38
|
+
if (visiting.has(key)) throw formulaError('RUN_PLAN_INVALID', 'runPlan dependency cycle');
|
|
39
|
+
visiting.add(key); for (const dependency of definitions.get(key).dependsOn) visit(dependency); visiting.delete(key); done.add(key);
|
|
40
|
+
};
|
|
41
|
+
for (const key of definitions.keys()) visit(key);
|
|
42
|
+
for (const key of completed) if (!definitions.has(key)) throw formulaError('RUN_PLAN_INVALID', 'completed work item is not part of the runPlan');
|
|
43
|
+
}
|
|
44
|
+
const TOOL_SOURCE = `import { readFile } from 'node:fs/promises';
|
|
45
|
+
import { evaluateCompiledProgram, verifyCompiledProgram } from './runtime.mjs';
|
|
46
|
+
let input = ''; for await (const chunk of process.stdin) input += chunk;
|
|
47
|
+
try { const program = JSON.parse(await readFile(new URL('./program.json', import.meta.url), 'utf8')); if(process.argv.includes('--verify')) { const result=verifyCompiledProgram(program); process.stdout.write(JSON.stringify(result)+'\\n'); process.exitCode=result.computationsPassed?0:2; } else { const values = evaluateCompiledProgram(program, JSON.parse(input)); process.stdout.write(JSON.stringify({status:'reported',programDigest:program.programDigest,values})+'\\n'); } }
|
|
48
|
+
catch (error) { const code=error.code??'FORMULA_INPUT_INVALID'; const status=/(?:AUTHORITY_REQUIRED|LEASE_EXPIRED|NOT_ACCEPTED|REVOKED)$/.test(code)?'blocked':'failed'; process.stderr.write(JSON.stringify({schemaVersion:'entrorove.local-error/1.0',status,code,message:error instanceof SyntaxError?'Formula input or artifact must be valid JSON':error.message})+'\\n'); process.exitCode=status==='blocked'?2:1; }
|
|
49
|
+
`;
|
|
50
|
+
async function store(input, generate) {
|
|
51
|
+
const program = await programFrom(input);
|
|
52
|
+
const files = generate ? [{ path: 'runtime.mjs', bytes: await calctoolRuntimeBytes() }, { path: 'tool.mjs', bytes: Buffer.from(TOOL_SOURCE) }] : undefined;
|
|
53
|
+
const stored = await storeFormula(input, program, files);
|
|
54
|
+
return { schemaVersion, status: 'reported', engineId: program.engineId, engineVersion: program.engineVersion, runtimeVersion: program.runtimeVersion,
|
|
55
|
+
engineDigest: program.engineDigest, programDigest: program.programDigest, directory: stored.directory, files: stored.manifest.files,
|
|
56
|
+
...(generate ? { executable: `${stored.directory}/tool.mjs`, validatorCommand: { executable: process.execPath, args: [`${stored.directory}/tool.mjs`, '--verify'] } } : {}), reused: stored.reused };
|
|
57
|
+
}
|
|
58
|
+
async function execute(operation, input) {
|
|
59
|
+
if (['compile', 'generate'].includes(operation)) return store(input, operation !== 'compile');
|
|
60
|
+
if (operation === 'compile-tool') {
|
|
61
|
+
const generated = await store(input, true); const stored = await loadStoredFormula({ ...input, engineId: generated.engineId, engineVersion: generated.engineVersion });
|
|
62
|
+
const runtime = await loadCalctoolRuntime(); const project = await runtime.runLocalCore('compile-tool', { engine: stored.program.engine });
|
|
63
|
+
return { ...generated, pages: project.pages, commands: project.commands, environment: project.environment,
|
|
64
|
+
projectFiles: project.files.map(file => file.kind === 'engine' ? { ...file, digest: stored.program.engineDigest } : file), projectStatus: 'planned' };
|
|
65
|
+
}
|
|
66
|
+
if (operation === 'inventory') return { schemaVersion, status: 'reported', engines: await formulaInventory(input) };
|
|
67
|
+
if (operation === 'purge') return { schemaVersion, status: 'reported', ...await purgeStoredFormula(input) };
|
|
68
|
+
if (operation === 'status') { const stored = await loadStoredFormula(input); return { schemaVersion, status: 'reported', ...stored.manifest }; }
|
|
69
|
+
if (operation === 'final-gate' && input.gateId) {
|
|
70
|
+
const { runFinalGate16 } = await import('./local-final-gate16.mjs');
|
|
71
|
+
return runFinalGate16(input);
|
|
72
|
+
}
|
|
73
|
+
if (['run', 'verify', 'final-gate', 'estimate', 'impact', 'blueprint-orchestrate'].includes(operation)) {
|
|
74
|
+
const program = await programFrom(input);
|
|
75
|
+
if (operation === 'run') {
|
|
76
|
+
const values = evaluateFormulaProgram(program, input.inputs, input);
|
|
77
|
+
return { schemaVersion, status: 'reported', engineId: program.engineId, engineVersion: program.engineVersion, programDigest: program.programDigest,
|
|
78
|
+
values, artifactSha256: formulaDigest({ programDigest: program.programDigest, inputs: input.inputs, values }) };
|
|
79
|
+
}
|
|
80
|
+
if (['verify', 'final-gate'].includes(operation)) return formulaFinalGate(program.engine, input);
|
|
81
|
+
if (operation === 'impact') return { schemaVersion, status: 'reported', programDigest: program.programDigest, affected: formulaImpact(program, input.changedKeys) };
|
|
82
|
+
if (operation === 'estimate') return { schemaVersion, status: 'reported', programDigest: program.programDigest, formulas: program.formulas.length,
|
|
83
|
+
fields: program.engine.fields.length, dependencies: Object.values(program.dependencies).reduce((total, refs) => total + refs.length, 0), order: program.order };
|
|
84
|
+
const core = await loadCalctoolRuntime(); const output = await core.runLocalCore(operation, { engine: program.engine, goal: input.goal });
|
|
85
|
+
return { ...output, status: 'reported', source: 'calctool-plan', executed: false };
|
|
86
|
+
}
|
|
87
|
+
const core = await loadCalctoolRuntime();
|
|
88
|
+
if (operation === 'probe-env') return { schemaVersion, status: 'reported', probe: core.probeLocalEnvironment() };
|
|
89
|
+
if (operation === 'adapt-config') return { schemaVersion, status: 'reported', adapted: core.adaptLocalEnvironment(input.probe ?? core.probeLocalEnvironment()) };
|
|
90
|
+
if (input.runPlan) validatedPlan(input.runPlan, input.completedWorkItemIds);
|
|
91
|
+
const output = await core.runLocalCore(operation, input);
|
|
92
|
+
if (output.status === 'blocked' || output.status === 'failed') return output;
|
|
93
|
+
if (output.status === 'incomplete') return { ...output, source: 'calctool-plan', executed: false };
|
|
94
|
+
if (output.runPlan) validatedPlan(output.runPlan);
|
|
95
|
+
return { ...output, status: 'reported', source: 'calctool-plan', executed: false, ...(operation === 'brain-handshake' ? { brainUsed: false } : {}) };
|
|
96
|
+
}
|
|
97
|
+
export async function runCalctoolLocal(operation, input = {}) {
|
|
98
|
+
if (typeof operation !== 'string') throw formulaError('ENTROROVE_OPERATION_UNKNOWN', 'Calctool operation must be a string');
|
|
99
|
+
const name = operation.replace(/^calctool-/, '');
|
|
100
|
+
validateCalctoolLocalInput(name, input);
|
|
101
|
+
return withBudgetReadContext(input, () => execute(name, input));
|
|
102
|
+
}
|
|
103
|
+
export const CALCTOOL_LOCAL_OPERATIONS = Object.freeze(Object.fromEntries(CALCTOOL_LOCAL_NAMES.map(name => [`calctool-${name}`, input => runCalctoolLocal(name, input)])));
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { constants, promises as fs } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { validateLocalInput } from './local-contracts.mjs';
|
|
6
|
+
import { observeWorkflowLocalStep } from './workflow-local-runner.mjs';
|
|
7
|
+
import { validateLocalChainPlan } from './local-chain-plan.mjs';
|
|
8
|
+
|
|
9
|
+
export const CHAIN_PACKAGE_ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const ROOT = await fs.realpath(CHAIN_PACKAGE_ROOT);
|
|
11
|
+
const SKILLS = ['aimlock', 'archguard', 'blueprint', 'calctool', 'confirm-protocol', 'mergeguard', 'swarm', 'validator'];
|
|
12
|
+
export function chainFail(code, message) { throw Object.assign(new Error(message), { code }); }
|
|
13
|
+
export function chainCanonical(value) {
|
|
14
|
+
if (Array.isArray(value)) return `[${value.map(chainCanonical).join(',')}]`;
|
|
15
|
+
if (value && typeof value === 'object') return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${chainCanonical(value[key])}`).join(',')}}`;
|
|
16
|
+
return JSON.stringify(value);
|
|
17
|
+
}
|
|
18
|
+
export const chainSha = value => createHash('sha256').update(value).digest('hex');
|
|
19
|
+
export function chainExact(value, required, optional = []) {
|
|
20
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || required.some(key => !Object.hasOwn(value, key))
|
|
21
|
+
|| Object.keys(value).some(key => !required.includes(key) && !optional.includes(key))) {
|
|
22
|
+
chainFail('ENTROROVE_CHAIN_FIELDS', 'chain input has missing or unknown fields');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export async function chainControlFile(file, maximum = 8 * 1024 * 1024) {
|
|
26
|
+
const descriptor = await fs.open(file, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
27
|
+
try {
|
|
28
|
+
const before = await descriptor.stat();
|
|
29
|
+
if (!before.isFile() || before.nlink !== 1 || before.size > maximum
|
|
30
|
+
|| (typeof process.getuid === 'function' && before.uid !== process.getuid())) {
|
|
31
|
+
chainFail('ENTROROVE_CHAIN_CONTROL_INVALID', 'chain control must be a bounded owned regular file');
|
|
32
|
+
}
|
|
33
|
+
const bytes = await descriptor.readFile(), after = await descriptor.stat(), current = await fs.lstat(file);
|
|
34
|
+
if (bytes.length !== before.size || before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size
|
|
35
|
+
|| before.mtimeMs !== after.mtimeMs || current.isSymbolicLink() || current.dev !== before.dev || current.ino !== before.ino) {
|
|
36
|
+
chainFail('ENTROROVE_CHAIN_CONTROL_CHANGED', 'chain control changed during its actual read');
|
|
37
|
+
}
|
|
38
|
+
return { bytes, stat: after };
|
|
39
|
+
} finally { await descriptor.close(); }
|
|
40
|
+
}
|
|
41
|
+
async function sourceNames() {
|
|
42
|
+
const rootNames = (await fs.readdir(ROOT)).filter(name => name.endsWith('.mjs'));
|
|
43
|
+
const runtimeNames = (await fs.readdir(path.join(ROOT, 'runtime'))).filter(name => name.endsWith('.mjs')).map(name => `runtime/${name}`);
|
|
44
|
+
return [...rootNames, ...runtimeNames, 'package.json', 'manifest.json'].sort();
|
|
45
|
+
}
|
|
46
|
+
export async function captureChainPackage(plan) {
|
|
47
|
+
for (const skill of plan.skills) {
|
|
48
|
+
if (!SKILLS.includes(skill.skillId) || await fs.realpath(skill.packageRoot) !== ROOT) {
|
|
49
|
+
chainFail('ENTROROVE_CHAIN_SKILL_PACKAGE', 'native chain skills must use the current fixed Entrorove package');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const files = [];
|
|
53
|
+
for (const name of await sourceNames()) files.push({ path: name, sha256: chainSha((await chainControlFile(path.join(ROOT, name))).bytes) });
|
|
54
|
+
const metadata = JSON.parse((await chainControlFile(path.join(ROOT, 'package.json'))).bytes.toString('utf8'));
|
|
55
|
+
if (metadata.name !== '@entrorove/skill') chainFail('ENTROROVE_CHAIN_SKILL_PACKAGE', 'installed native package identity is invalid');
|
|
56
|
+
return { packageRoot: ROOT, name: metadata.name, version: metadata.version, files,
|
|
57
|
+
authorityKeyDigest: chainSha(process.env.ENTROROVE_WORKFLOW_PLAN_PUBLIC_KEY ?? '') };
|
|
58
|
+
}
|
|
59
|
+
export async function checkChainPackage(snapshot) {
|
|
60
|
+
const current = await captureChainPackage(snapshot.plan);
|
|
61
|
+
if (chainCanonical(current) !== chainCanonical(snapshot.package)) chainFail('ENTROROVE_CHAIN_PACKAGE_CHANGED', 'installed source, metadata or authority changed after chain initialization');
|
|
62
|
+
}
|
|
63
|
+
export async function validateChainBindings(planValue, values, identity) {
|
|
64
|
+
const plan = validateLocalChainPlan(planValue);
|
|
65
|
+
if (!Array.isArray(values)) chainFail('ENTROROVE_CHAIN_BINDING', 'command bindings must be an explicit array');
|
|
66
|
+
const commands = plan.steps.filter(step => step.kind === 'command'), bindings = [];
|
|
67
|
+
if (values.length !== commands.length) chainFail('ENTROROVE_CHAIN_BINDING', 'every command requires exactly one original signed workflow mapping');
|
|
68
|
+
for (const step of commands) {
|
|
69
|
+
const selected = values.filter(value => value.stepId === step.stepId);
|
|
70
|
+
if (selected.length !== 1) chainFail('ENTROROVE_CHAIN_BINDING', 'command mapping must have a unique original step ID');
|
|
71
|
+
chainExact(selected[0], ['stepId', 'workflowInput']);
|
|
72
|
+
const input = validateLocalInput('workflow-run-step', structuredClone(selected[0].workflowInput));
|
|
73
|
+
const native = input.acceptedPlan.steps.find(value => value.stepId === input.stepId);
|
|
74
|
+
if (input.repositoryRoot !== identity.repositoryRoot || input.home !== identity.home || input.stepId !== step.stepId
|
|
75
|
+
|| input.acceptedPlan.flowId !== identity.flowId || input.acceptedPlan.ownerId !== identity.ownerId
|
|
76
|
+
|| input.lease.stepId !== step.stepId || !native || chainCanonical(native.dependsOn) !== chainCanonical(step.dependsOn)) {
|
|
77
|
+
chainFail('ENTROROVE_CHAIN_BINDING', 'command mapping changed its original home, repository, flow, owner or step dependencies');
|
|
78
|
+
}
|
|
79
|
+
const policy = native.executionPlan.policy;
|
|
80
|
+
if (chainCanonical([step.input.executable, step.input.args, step.input.environment, step.input.timeoutMs, step.input.evidenceKind])
|
|
81
|
+
!== chainCanonical([policy.executable, policy.args, policy.environment, policy.timeoutMs, native.executionPlan.evidencePolicy?.kind ?? 'test'])) {
|
|
82
|
+
chainFail('ENTROROVE_CHAIN_BINDING', 'command policy differs from its actual frozen workflow plan');
|
|
83
|
+
}
|
|
84
|
+
if (path.resolve(identity.repositoryRoot, step.input.workingDirectory) !== identity.repositoryRoot) {
|
|
85
|
+
chainFail('ENTROROVE_CHAIN_COMMAND_CWD_UNSUPPORTED', 'the fixed native workflow runner currently executes only at its original repository root');
|
|
86
|
+
}
|
|
87
|
+
// This real observer verifies the configured Ed25519 plan and lease without
|
|
88
|
+
// starting a process or inventing an execution authority.
|
|
89
|
+
await observeWorkflowLocalStep(input);
|
|
90
|
+
bindings.push({ stepId: step.stepId, workflowInput: input });
|
|
91
|
+
}
|
|
92
|
+
return bindings;
|
|
93
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import Ajv from 'ajv';
|
|
2
|
+
import { chainCanonical as canonical, chainFail as fail } from './local-chain-bindings.mjs';
|
|
3
|
+
import { inspectLocalChainResult } from './local-chain-plan.mjs';
|
|
4
|
+
|
|
5
|
+
const STAGES = Object.freeze({ 'workflow-gate': 'aimlock', 'workflow-contract': 'aimlock',
|
|
6
|
+
'workflow-plan': 'blueprint', 'workflow-dispatch': 'swarm', 'workflow-verdict': 'validator' });
|
|
7
|
+
function nativeSchema(runtime, operation) {
|
|
8
|
+
const schemas = runtime.operationSchemas;
|
|
9
|
+
const selected = Array.isArray(schemas) ? schemas.find(value => value.operation === operation) : schemas?.[operation];
|
|
10
|
+
return selected?.inputSchema ?? selected?.input ?? selected;
|
|
11
|
+
}
|
|
12
|
+
export async function captureChainCloud(plan, client) {
|
|
13
|
+
if (!plan.skills.length) return null;
|
|
14
|
+
const output = await client.invoke('capabilities', {}), capabilities = output.capabilities;
|
|
15
|
+
if (output.status !== 'succeeded' || !capabilities?.operationSchemas?.continue
|
|
16
|
+
|| !Array.isArray(capabilities.runtimes)) fail('ENTROROVE_CHAIN_DISCOVERY', 'original cloud discovery is incomplete');
|
|
17
|
+
const runtimes = plan.skills.map(skill => {
|
|
18
|
+
const matches = capabilities.runtimes.filter(runtime => runtime.slug === skill.skillId);
|
|
19
|
+
if (matches.length !== 1 || typeof matches[0].version !== 'string' || typeof matches[0].endpoint !== 'string') {
|
|
20
|
+
fail('ENTROROVE_CHAIN_DISCOVERY', 'declared native skill has no unique original runtime metadata');
|
|
21
|
+
}
|
|
22
|
+
return structuredClone(matches[0]);
|
|
23
|
+
});
|
|
24
|
+
return { runtimes, operationSchemas: structuredClone(capabilities.operationSchemas),
|
|
25
|
+
workflowOperations: structuredClone(capabilities.workflowOperations ?? null) };
|
|
26
|
+
}
|
|
27
|
+
export async function checkChainCloud(state, client) {
|
|
28
|
+
if (canonical(await captureChainCloud(state.plan, client)) !== canonical(state.cloud)) {
|
|
29
|
+
fail('ENTROROVE_CHAIN_DISCOVERY_CHANGED', 'original cloud runtime or operation metadata changed after initialization');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function checkChainSkillDispatch(original, state, resumed, input) {
|
|
33
|
+
const next = resumed.nextStep;
|
|
34
|
+
if (resumed.canContinue !== true || next?.operation !== 'continue'
|
|
35
|
+
|| next.input?.flowId !== state.identity.flowId || next.input.operation !== original.operation) {
|
|
36
|
+
fail('ENTROROVE_CHAIN_STAGE_BINDING', 'declared skill must be the exact next operation of its original workflow');
|
|
37
|
+
}
|
|
38
|
+
const slug = STAGES[original.operation] ?? resumed.route?.slug;
|
|
39
|
+
if (slug !== original.skillId) fail('ENTROROVE_CHAIN_SKILL_BINDING', 'declared skill differs from the original native workflow stage');
|
|
40
|
+
const runtime = state.cloud.runtimes.find(value => value.slug === original.skillId);
|
|
41
|
+
const schema = state.cloud.workflowOperations?.operationSchemas?.[original.operation] ?? nativeSchema(runtime, original.operation);
|
|
42
|
+
if (schema) {
|
|
43
|
+
const validate = new Ajv({ strict: true, strictRequired: false, allErrors: true }).compile(schema);
|
|
44
|
+
if (!validate(input)) fail('ENTROROVE_CHAIN_SKILL_INPUT', 'bound input violates its fixed original operation schema');
|
|
45
|
+
}
|
|
46
|
+
// Native operations without public nested schemas remain server-validated on
|
|
47
|
+
// the exact original active route; no arbitrary endpoint is opened here.
|
|
48
|
+
}
|
|
49
|
+
export function inspectChainSkillResponse(original, state, response) {
|
|
50
|
+
if (response.flowId !== state.identity.flowId || (response.taskId !== undefined && response.taskId !== state.identity.taskId)) {
|
|
51
|
+
fail('ENTROROVE_CHAIN_RESULT_BINDING', 'skill result lost its original flow or task identity');
|
|
52
|
+
}
|
|
53
|
+
if (!response.result || typeof response.result !== 'object' || Array.isArray(response.result)) {
|
|
54
|
+
fail('ENTROROVE_CHAIN_RESULT_BINDING', 'skill result must retain its actual native structured output');
|
|
55
|
+
}
|
|
56
|
+
const native = response.result;
|
|
57
|
+
if (native.source === 'needs-clarification' || native.intake?.complete === false) {
|
|
58
|
+
return { state: 'waiting', code: 'ENTROROVE_CHAIN_CLARIFICATION_REQUIRED', eligible: false };
|
|
59
|
+
}
|
|
60
|
+
return inspectLocalChainResult(original, response);
|
|
61
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { WORKFLOW_LOCAL_OPERATION_SCHEMA } from './workflow-local-contracts.mjs';
|
|
2
|
+
|
|
3
|
+
const id = { type: 'string', pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$' };
|
|
4
|
+
const text = { type: 'string', minLength: 1 };
|
|
5
|
+
const object = (properties, required = Object.keys(properties)) => ({ type: 'object', additionalProperties: false, properties, required });
|
|
6
|
+
const array = items => ({ type: 'array', items });
|
|
7
|
+
const pointer = { type: 'string', pattern: '^(?:/(?:[^~]|~[01])*)*$' };
|
|
8
|
+
const step = object({ stepId: id, kind: { enum: ['skill', 'coordinator', 'command'] },
|
|
9
|
+
skillId: { anyOf: [id, { type: 'null' }] }, operation: id, input: { type: 'object' }, dependsOn: array(id),
|
|
10
|
+
bindings: array(object({ stepId: id, source: pointer, target: { ...pointer, minLength: 1 } })),
|
|
11
|
+
continueWhen: object({ answer: { anyOf: [text, { ...array(text), minItems: 1, uniqueItems: true }] } }),
|
|
12
|
+
}, ['stepId', 'kind', 'skillId', 'operation', 'input', 'dependsOn', 'bindings']);
|
|
13
|
+
export const LOCAL_CHAIN_PLAN_INPUT_SCHEMA = object({ schemaVersion: { const: 'entrorove.execution-plan/1.0' }, chainId: id,
|
|
14
|
+
skills: array(object({ skillId: id, packageRoot: text })), steps: { ...array(step), minItems: 1 } });
|
|
15
|
+
const identity = { repositoryRoot: text, home: text, chainId: id };
|
|
16
|
+
export const LOCAL_CHAIN_OPERATION_SCHEMAS = Object.freeze({
|
|
17
|
+
'chain-init': object({ repositoryRoot: text, home: text, flowId: id, ownerId: id, taskId: id,
|
|
18
|
+
plan: LOCAL_CHAIN_PLAN_INPUT_SCHEMA, commandBindings: array(object({ stepId: id, workflowInput: WORKFLOW_LOCAL_OPERATION_SCHEMA })) }),
|
|
19
|
+
'chain-resume': object(identity), 'chain-status': object(identity),
|
|
20
|
+
'chain-answer': object({ ...identity, actorId: id }),
|
|
21
|
+
});
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { constants, promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { withChainLock } from './local-chain-lock.mjs';
|
|
5
|
+
import { captureChainCloud, checkChainCloud, checkChainSkillDispatch, inspectChainSkillResponse } from './local-chain-broker.mjs';
|
|
6
|
+
import { originalChainInteraction, readChainTerminalAnswer, inspectChainHumanResponse } from './local-chain-human.mjs';
|
|
7
|
+
import { validateArchivedChainResults, revalidateArchivedChainSkill, revalidateArchivedChainCommand } from './local-chain-results.mjs';
|
|
8
|
+
import { buildInvocation, createTaskMessageClient } from './remote-client.mjs';
|
|
9
|
+
import { runManagedWorkflowLocalStep } from './workflow-local-client.mjs';
|
|
10
|
+
import { observeWorkflowLocalStep } from './workflow-local-runner.mjs';
|
|
11
|
+
import { bindLocalChainStepInput, localChainPlanDigest, validateLocalChainPlan } from './local-chain-plan.mjs';
|
|
12
|
+
import { captureChainPackage, checkChainPackage, validateChainBindings, chainCanonical as canonical,
|
|
13
|
+
chainControlFile, chainExact as exact, chainFail as fail, chainSha as sha } from './local-chain-bindings.mjs';
|
|
14
|
+
|
|
15
|
+
export const LOCAL_CHAIN_CONTROLLER_SCHEMA = 'entrorove.local-chain-controller/1.0';
|
|
16
|
+
const ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
17
|
+
const SHA = /^[0-9a-f]{64}$/;
|
|
18
|
+
const STATES = ['pending', 'running', 'waiting', 'reported', 'verified', 'uncertain', 'blocked', 'failed'];
|
|
19
|
+
const MAX_BYTES = 8 * 1024 * 1024;
|
|
20
|
+
function safeId(value) { if (typeof value !== 'string' || !ID.test(value)) fail('ENTROROVE_CHAIN_ID', 'chain identity must be explicit and safe'); }
|
|
21
|
+
async function directory(value) {
|
|
22
|
+
if (typeof value !== 'string' || !path.isAbsolute(value)) fail('ENTROROVE_CHAIN_DIRECTORY', 'chain home and repository must be explicit absolute directories');
|
|
23
|
+
const stat = await fs.lstat(value);
|
|
24
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) fail('ENTROROVE_CHAIN_DIRECTORY', 'chain directories must be real directories');
|
|
25
|
+
return fs.realpath(value);
|
|
26
|
+
}
|
|
27
|
+
async function location(input, create = false) {
|
|
28
|
+
const repositoryRoot = await directory(input.repositoryRoot), home = await directory(input.home);
|
|
29
|
+
safeId(input.chainId ?? input.plan?.chainId);
|
|
30
|
+
let target = home;
|
|
31
|
+
for (const name of ['.entrorove', 'chain-controller', sha(repositoryRoot).slice(0, 32), input.chainId ?? input.plan.chainId]) {
|
|
32
|
+
target = path.join(target, name);
|
|
33
|
+
if (create) await fs.mkdir(target, { mode: 0o700 }).catch(error => { if (error.code !== 'EEXIST') throw error; });
|
|
34
|
+
const stat = await fs.lstat(target);
|
|
35
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || (stat.mode & 0o777) !== 0o700
|
|
36
|
+
|| (typeof process.getuid === 'function' && stat.uid !== process.getuid())) fail('ENTROROVE_CHAIN_STORAGE', 'chain state directories must be owned, private and real');
|
|
37
|
+
}
|
|
38
|
+
return { repositoryRoot, home, file: path.join(target, 'state.json') };
|
|
39
|
+
}
|
|
40
|
+
async function atomic(file, value) {
|
|
41
|
+
const bytes = Buffer.from(canonical(value) + '\n');
|
|
42
|
+
if (bytes.length > MAX_BYTES) fail('ENTROROVE_CHAIN_JOURNAL_LIMIT', 'chain journal exceeds its control byte bound');
|
|
43
|
+
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
44
|
+
const descriptor = await fs.open(temporary, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, 0o600);
|
|
45
|
+
try { await descriptor.writeFile(bytes); await descriptor.sync(); }
|
|
46
|
+
finally { await descriptor.close(); }
|
|
47
|
+
try {
|
|
48
|
+
await fs.rename(temporary, file);
|
|
49
|
+
const directory = await fs.open(path.dirname(file), constants.O_RDONLY);
|
|
50
|
+
try { await directory.sync(); } finally { await directory.close(); }
|
|
51
|
+
}
|
|
52
|
+
finally { await fs.rm(temporary, { force: true }); }
|
|
53
|
+
}
|
|
54
|
+
function reduce(records) {
|
|
55
|
+
let state = null, head = null;
|
|
56
|
+
for (const [index, record] of records.entries()) {
|
|
57
|
+
exact(record, ['sequence', 'event', 'at', 'stepId', 'data', 'previousDigest', 'digest']);
|
|
58
|
+
const { digest, ...core } = record;
|
|
59
|
+
if (record.sequence !== index + 1 || record.previousDigest !== head || !SHA.test(digest) || sha(canonical(core)) !== digest) {
|
|
60
|
+
fail('ENTROROVE_CHAIN_JOURNAL_INVALID', 'chain journal hash lineage is invalid');
|
|
61
|
+
}
|
|
62
|
+
if (index === 0) {
|
|
63
|
+
if (record.event !== 'init' || record.stepId !== null) fail('ENTROROVE_CHAIN_JOURNAL_INVALID', 'chain journal must start with its original initialization');
|
|
64
|
+
state = structuredClone(record.data);
|
|
65
|
+
if (!state || typeof state !== 'object' || !Array.isArray(state.steps) || !state.identity) {
|
|
66
|
+
fail('ENTROROVE_CHAIN_JOURNAL_INVALID', 'chain initialization controls are invalid');
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
if (record.event !== 'step') fail('ENTROROVE_CHAIN_JOURNAL_INVALID', 'chain journal contains an unknown event');
|
|
70
|
+
const step = state.steps.find(value => value.stepId === record.stepId);
|
|
71
|
+
if (!step || !record.data || !STATES.includes(record.data.status)) fail('ENTROROVE_CHAIN_JOURNAL_INVALID', 'chain journal changed its original step or status');
|
|
72
|
+
if (record.data.phase === 'answer' && step.output?.interaction && !step.originalInteraction) {
|
|
73
|
+
step.originalInteraction = structuredClone(step.output);
|
|
74
|
+
}
|
|
75
|
+
Object.assign(step, structuredClone(record.data));
|
|
76
|
+
}
|
|
77
|
+
head = digest;
|
|
78
|
+
}
|
|
79
|
+
if (!state || state.steps.some(step => !step || typeof step !== 'object' || !STATES.includes(step.status))
|
|
80
|
+
|| localChainPlanDigest(state.plan) !== state.planDigest || state.steps.length !== state.plan.steps.length
|
|
81
|
+
|| state.steps.some((step, index) => step.stepId !== state.plan.steps[index].stepId)) fail('ENTROROVE_CHAIN_JOURNAL_INVALID', 'chain journal lost its immutable original plan');
|
|
82
|
+
return state;
|
|
83
|
+
}
|
|
84
|
+
async function load(ctx) {
|
|
85
|
+
const { bytes, stat } = await chainControlFile(ctx.file);
|
|
86
|
+
if ((stat.mode & 0o777) !== 0o600) fail('ENTROROVE_CHAIN_STORAGE', 'chain journal must remain private');
|
|
87
|
+
let journal; try { journal = JSON.parse(bytes.toString('utf8')); } catch { fail('ENTROROVE_CHAIN_JOURNAL_INVALID', 'chain journal JSON is invalid'); }
|
|
88
|
+
exact(journal, ['schemaVersion', 'records']);
|
|
89
|
+
if (journal.schemaVersion !== LOCAL_CHAIN_CONTROLLER_SCHEMA || !Array.isArray(journal.records) || !journal.records.length) fail('ENTROROVE_CHAIN_JOURNAL_INVALID', 'chain journal schema is invalid');
|
|
90
|
+
const state = reduce(journal.records);
|
|
91
|
+
if (state.identity.home !== ctx.home || state.identity.repositoryRoot !== ctx.repositoryRoot) fail('ENTROROVE_CHAIN_BINDING', 'chain journal belongs to another original home or repository');
|
|
92
|
+
await checkChainPackage(state);
|
|
93
|
+
await validateArchivedChainResults(state);
|
|
94
|
+
return { ctx, journal, state };
|
|
95
|
+
}
|
|
96
|
+
async function append(loaded, stepId, data, event = 'step') {
|
|
97
|
+
const records = loaded.journal.records;
|
|
98
|
+
const core = { sequence: records.length + 1, event, at: new Date().toISOString(), stepId, data,
|
|
99
|
+
previousDigest: records.at(-1)?.digest ?? null };
|
|
100
|
+
const journal = { schemaVersion: LOCAL_CHAIN_CONTROLLER_SCHEMA, records: [...records, { ...core, digest: sha(canonical(core)) }] };
|
|
101
|
+
await atomic(loaded.ctx.file, journal); loaded.journal = journal; loaded.state = reduce(journal.records);
|
|
102
|
+
}
|
|
103
|
+
function view(loaded) {
|
|
104
|
+
const steps = loaded.state.steps;
|
|
105
|
+
const status = steps.find(step => ['failed', 'blocked', 'uncertain', 'waiting', 'running', 'reported', 'pending'].includes(step.status))?.status ?? 'verified';
|
|
106
|
+
return { schemaVersion: LOCAL_CHAIN_CONTROLLER_SCHEMA, chainId: loaded.state.plan.chainId, status,
|
|
107
|
+
flowId: loaded.state.identity.flowId, steps: structuredClone(steps), sequence: loaded.journal.records.length,
|
|
108
|
+
head: loaded.journal.records.at(-1).digest, authority: 'original-workflow-step-results', taskCompletionVerified: false };
|
|
109
|
+
}
|
|
110
|
+
async function dependenciesFor(home, supplied) { return supplied ?? (await import('./cli.mjs')).remoteDependencies(home); }
|
|
111
|
+
function checkCloudTask(output, identity) {
|
|
112
|
+
if (!output?.task || output.task.taskId !== identity.taskId || output.task.ownerId !== identity.ownerId
|
|
113
|
+
|| output.task.flowId !== identity.flowId || output.flowId !== identity.flowId || output.continuity?.handoffPaused
|
|
114
|
+
|| output.continuity?.activeTakeover || !['accepted', 'active'].includes(output.task.status)) {
|
|
115
|
+
fail('ENTROROVE_CHAIN_CLOUD_BINDING', 'original cloud task cannot authorize this local chain');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function originalResponse(output, requestId) {
|
|
119
|
+
if (!output || output.requestId !== requestId || output.schemaVersion !== 'entrorove.skill.response/1.0') {
|
|
120
|
+
fail('ENTROROVE_CHAIN_RESPONSE_BINDING', 'cloud result must retain its original canonical request ID');
|
|
121
|
+
}
|
|
122
|
+
return output;
|
|
123
|
+
}
|
|
124
|
+
async function cloudRequest(loaded, step, operation, input, client, phase) {
|
|
125
|
+
const expected = buildInvocation(operation, input), existing = step.request;
|
|
126
|
+
if (existing && (existing.requestId !== expected.requestId || existing.operation !== operation
|
|
127
|
+
|| canonical(existing.input) !== canonical(input))) fail('ENTROROVE_CHAIN_REQUEST_BINDING', 'pending cloud request cannot be replaced');
|
|
128
|
+
const request = existing ?? { requestId: expected.requestId, operation, input };
|
|
129
|
+
if (!existing) await append(loaded, step.stepId, { ...step, request, phase, status: step.status });
|
|
130
|
+
try {
|
|
131
|
+
// A saved request can have been sent before an abrupt host exit. Recovery
|
|
132
|
+
// observes that exact durable result; it never issues another POST.
|
|
133
|
+
return originalResponse(await (existing ? client.status(request.requestId) : client.invoke(operation, input)), request.requestId);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
await append(loaded, step.stepId, { ...loaded.state.steps.find(value => value.stepId === step.stepId),
|
|
136
|
+
status: 'uncertain', error: { code: error.code ?? 'ENTROROVE_CHAIN_CLOUD_UNKNOWN', requestId: request.requestId } });
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function initLocalChain(input, suppliedDependencies) {
|
|
142
|
+
exact(input, ['repositoryRoot', 'home', 'flowId', 'ownerId', 'taskId', 'plan', 'commandBindings']);
|
|
143
|
+
for (const field of ['flowId', 'ownerId', 'taskId']) safeId(input[field]);
|
|
144
|
+
const plan = validateLocalChainPlan(input.plan), ctx = await location({ ...input, plan }, true);
|
|
145
|
+
return withChainLock(ctx.file, async () => {
|
|
146
|
+
try { await fs.lstat(ctx.file); fail('ENTROROVE_CHAIN_EXISTS', 'the original chain already exists; resume it without reinitializing'); }
|
|
147
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
148
|
+
const identity = { repositoryRoot: ctx.repositoryRoot, home: ctx.home, flowId: input.flowId, ownerId: input.ownerId, taskId: input.taskId };
|
|
149
|
+
const bindings = await validateChainBindings(plan, input.commandBindings, identity), packagePin = await captureChainPackage(plan);
|
|
150
|
+
const configured = await dependenciesFor(ctx.home, suppliedDependencies), client = createTaskMessageClient(configured);
|
|
151
|
+
const resumed = await client.invoke('task-resume', { flowId: identity.flowId }); checkCloudTask(resumed, identity);
|
|
152
|
+
const cloud = await captureChainCloud(plan, client);
|
|
153
|
+
const state = { identity, plan, planDigest: localChainPlanDigest(plan), commandBindings: bindings, package: packagePin, cloud,
|
|
154
|
+
apiOrigin: new URL(configured.apiOrigin()).origin,
|
|
155
|
+
steps: plan.steps.map(step => ({ stepId: step.stepId, status: 'pending', phase: 'execute', request: null, localResult: null, output: null, error: null })) };
|
|
156
|
+
const loaded = { ctx, journal: { schemaVersion: LOCAL_CHAIN_CONTROLLER_SCHEMA, records: [] }, state: null };
|
|
157
|
+
await append(loaded, null, state, 'init'); return view(loaded);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function reconcileSkill(loaded, original, input, client) {
|
|
162
|
+
let step = loaded.state.steps.find(value => value.stepId === original.stepId);
|
|
163
|
+
if (step.status === 'waiting') return false;
|
|
164
|
+
if (!step.request) {
|
|
165
|
+
const resumed = await client.invoke('task-resume', { flowId: loaded.state.identity.flowId });
|
|
166
|
+
checkCloudTask(resumed, loaded.state.identity); checkChainSkillDispatch(original, loaded.state, resumed, input);
|
|
167
|
+
await append(loaded, step.stepId, { ...step, status: 'running' });
|
|
168
|
+
step = loaded.state.steps.find(value => value.stepId === original.stepId);
|
|
169
|
+
}
|
|
170
|
+
const invocation = { flowId: loaded.state.identity.flowId, operation: original.operation, payload: input };
|
|
171
|
+
const response = await cloudRequest(loaded, step, 'continue', invocation, client, 'skill');
|
|
172
|
+
if (!response) return false;
|
|
173
|
+
const observed = inspectChainSkillResponse(original, loaded.state, response);
|
|
174
|
+
await append(loaded, step.stepId, { ...step, status: observed.eligible ? 'verified' : observed.state,
|
|
175
|
+
phase: observed.eligible ? 'done' : 'skill', output: response.result, request: loaded.state.steps.find(value => value.stepId === step.stepId).request,
|
|
176
|
+
response, error: observed.eligible ? null : { code: observed.code } });
|
|
177
|
+
return observed.eligible;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function reconcileAnswer(loaded, original, client) {
|
|
181
|
+
const step = loaded.state.steps.find(value => value.stepId === original.stepId);
|
|
182
|
+
const response = await cloudRequest(loaded, step, 'continue', step.request.input, client, 'answer');
|
|
183
|
+
if (!response) return false;
|
|
184
|
+
const observed = inspectChainHumanResponse(original, loaded.state, step, response);
|
|
185
|
+
await append(loaded, step.stepId, { ...step, status: observed.eligible ? 'verified' : observed.state,
|
|
186
|
+
phase: observed.eligible ? 'done' : 'answer', output: { ...response.result, answer: response.result.auditEntry.answer },
|
|
187
|
+
response, error: observed.eligible ? null : { code: observed.code } });
|
|
188
|
+
return observed.eligible;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export async function statusLocalChain(input, suppliedDependencies) {
|
|
192
|
+
exact(input, ['repositoryRoot', 'home', 'chainId']); const ctx = await location(input);
|
|
193
|
+
const loaded = await load(ctx), verified = loaded.state.plan.steps.filter(original =>
|
|
194
|
+
loaded.state.steps.find(step => step.stepId === original.stepId)?.status === 'verified');
|
|
195
|
+
if (verified.length) {
|
|
196
|
+
const configured = await dependenciesFor(ctx.home, suppliedDependencies);
|
|
197
|
+
if (new URL(configured.apiOrigin()).origin !== loaded.state.apiOrigin) fail('ENTROROVE_CHAIN_CLOUD_BINDING', 'status cannot switch its original cloud origin');
|
|
198
|
+
const client = createTaskMessageClient(configured);
|
|
199
|
+
for (const original of verified) await (original.kind === 'command' ? revalidateArchivedChainCommand : revalidateArchivedChainSkill)(
|
|
200
|
+
loaded.state, original, loaded.state.steps.find(step => step.stepId === original.stepId), client);
|
|
201
|
+
}
|
|
202
|
+
return view(loaded);
|
|
203
|
+
}
|
|
204
|
+
async function localCommand(loaded, step, binding, configured) {
|
|
205
|
+
let result;
|
|
206
|
+
try {
|
|
207
|
+
if (step.status !== 'pending') {
|
|
208
|
+
result = await observeWorkflowLocalStep(binding.workflowInput);
|
|
209
|
+
if (!result) { await append(loaded, step.stepId, { ...step, status: 'uncertain', error: { code: 'ENTROROVE_CHAIN_PROCESS_UNKNOWN' } }); return false; }
|
|
210
|
+
} else {
|
|
211
|
+
await append(loaded, step.stepId, { ...step, status: 'running' });
|
|
212
|
+
result = await runManagedWorkflowLocalStep(binding.workflowInput, configured);
|
|
213
|
+
}
|
|
214
|
+
} catch (error) {
|
|
215
|
+
const uncertain = error.outcomeUnknown || ['ENTROROVE_WORKFLOW_RECOVERY_REQUIRED', 'ENTROROVE_WORKFLOW_BUSY'].includes(error.code);
|
|
216
|
+
await append(loaded, step.stepId, { ...loaded.state.steps.find(value => value.stepId === step.stepId),
|
|
217
|
+
status: uncertain ? 'uncertain' : 'failed', error: { code: error.code ?? 'ENTROROVE_CHAIN_EXECUTION_REJECTED' } }); return false;
|
|
218
|
+
}
|
|
219
|
+
if (!result.receipt || !result.evidence?.length || !Number.isInteger(result.process?.exitCode)) {
|
|
220
|
+
await append(loaded, step.stepId, { ...step, status: 'uncertain', localResult: result, error: { code: 'ENTROROVE_CHAIN_PROCESS_UNKNOWN' } }); return false;
|
|
221
|
+
}
|
|
222
|
+
await append(loaded, step.stepId, { ...step, status: 'reported', phase: 'report', localResult: result, request: null, error: null }); return true;
|
|
223
|
+
}
|
|
224
|
+
function checkReported(output, loaded, step, binding, verified) {
|
|
225
|
+
const value = output.result, evidence = step.localResult.evidence[0], lease = binding.workflowInput.lease;
|
|
226
|
+
if (!['succeeded', 'reported'].includes(output.status) || !value || value.flowId !== loaded.state.identity.flowId
|
|
227
|
+
|| value.stepId !== step.stepId || value.generation !== lease.generation || value.lease?.leaseId !== lease.leaseId
|
|
228
|
+
|| value.evidenceDigest !== sha(canonical(evidence)) || value.status !== (verified ? 'verified' : step.localResult.status)) {
|
|
229
|
+
fail('ENTROROVE_CHAIN_RESULT_BINDING', 'cloud execution result differs from its original process, lease or evidence');
|
|
230
|
+
}
|
|
231
|
+
if (verified && (value.verification?.summary?.passed !== 1 || value.verification?.summary?.failed !== 0
|
|
232
|
+
|| value.verification?.findings?.length !== 0)) fail('ENTROROVE_CHAIN_VERIFICATION', 'original independent verification did not pass the process receipt');
|
|
233
|
+
return value;
|
|
234
|
+
}
|
|
235
|
+
async function reconcileCommand(loaded, stepId, configured, client) {
|
|
236
|
+
let step = loaded.state.steps.find(value => value.stepId === stepId);
|
|
237
|
+
const binding = loaded.state.commandBindings.find(value => value.stepId === stepId);
|
|
238
|
+
if (step.phase === 'execute') {
|
|
239
|
+
if (!await localCommand(loaded, step, binding, configured)) return false;
|
|
240
|
+
step = loaded.state.steps.find(value => value.stepId === stepId);
|
|
241
|
+
}
|
|
242
|
+
if (step.phase === 'report') {
|
|
243
|
+
const input = { flowId: loaded.state.identity.flowId, operation: 'execution-report', payload: { stepId,
|
|
244
|
+
leaseId: binding.workflowInput.lease.leaseId, generation: binding.workflowInput.lease.generation, evidence: step.localResult.evidence[0] } };
|
|
245
|
+
const output = await cloudRequest(loaded, step, 'continue', input, client, 'report'); if (!output) return false;
|
|
246
|
+
const reported = checkReported(output, loaded, step, binding, false);
|
|
247
|
+
if (reported.status === 'failed') { await append(loaded, stepId, { ...step, status: 'failed', output: reported, request: null }); return false; }
|
|
248
|
+
await append(loaded, stepId, { ...step, status: 'reported', phase: 'verify', output: reported, request: null, error: null });
|
|
249
|
+
step = loaded.state.steps.find(value => value.stepId === stepId);
|
|
250
|
+
}
|
|
251
|
+
if (step.phase === 'verify') {
|
|
252
|
+
const input = { flowId: loaded.state.identity.flowId, operation: 'execution-verify', payload: { stepId } };
|
|
253
|
+
const output = await cloudRequest(loaded, step, 'continue', input, client, 'verify'); if (!output) return false;
|
|
254
|
+
const verified = checkReported(output, loaded, step, binding, true);
|
|
255
|
+
await append(loaded, stepId, { ...step, status: 'verified', phase: 'done', output: verified, request: null, error: null });
|
|
256
|
+
}
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export async function resumeLocalChain(input, suppliedDependencies) {
|
|
261
|
+
exact(input, ['repositoryRoot', 'home', 'chainId']); const ctx = await location(input);
|
|
262
|
+
return withChainLock(ctx.file, async () => {
|
|
263
|
+
const loaded = await load(ctx), configured = await dependenciesFor(ctx.home, suppliedDependencies);
|
|
264
|
+
if (new URL(configured.apiOrigin()).origin !== loaded.state.apiOrigin) fail('ENTROROVE_CHAIN_CLOUD_BINDING', 'chain recovery cannot switch its original cloud origin');
|
|
265
|
+
const client = createTaskMessageClient(configured);
|
|
266
|
+
await checkChainCloud(loaded.state, client);
|
|
267
|
+
for (const original of loaded.state.plan.steps) {
|
|
268
|
+
const step = loaded.state.steps.find(value => value.stepId === original.stepId);
|
|
269
|
+
if (step.status === 'verified') {
|
|
270
|
+
await (original.kind === 'command' ? revalidateArchivedChainCommand : revalidateArchivedChainSkill)(loaded.state, original, step, client);
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (['failed', 'blocked'].includes(step.status)) return view(loaded);
|
|
274
|
+
const boundInput = bindLocalChainStepInput(loaded.state.plan, original.stepId, loaded.state.steps.filter(value => value.status === 'verified')
|
|
275
|
+
.map(value => ({ stepId: value.stepId, status: 'succeeded', output: value.output })));
|
|
276
|
+
if (original.kind === 'coordinator') {
|
|
277
|
+
await append(loaded, original.stepId, { ...step, status: 'blocked', error: { code: 'ENTROROVE_CHAIN_ADAPTER_PENDING', kind: original.kind } }); return view(loaded);
|
|
278
|
+
}
|
|
279
|
+
try {
|
|
280
|
+
const complete = original.kind === 'command' ? await reconcileCommand(loaded, original.stepId, configured, client)
|
|
281
|
+
: step.phase === 'answer' ? await reconcileAnswer(loaded, original, client) : await reconcileSkill(loaded, original, boundInput, client);
|
|
282
|
+
if (!complete) return view(loaded);
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
const current = loaded.state.steps.find(value => value.stepId === original.stepId);
|
|
286
|
+
await append(loaded, original.stepId, { ...current, status: 'blocked', error: { code: error.code ?? 'ENTROROVE_CHAIN_RECONCILE_REJECTED' } }); return view(loaded);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return view(loaded);
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export async function answerLocalChain(input, suppliedDependencies) {
|
|
294
|
+
exact(input, ['repositoryRoot', 'home', 'chainId', 'actorId']); safeId(input.actorId);
|
|
295
|
+
const ctx = await location(input);
|
|
296
|
+
return withChainLock(ctx.file, async () => {
|
|
297
|
+
const loaded = await load(ctx), configured = await dependenciesFor(ctx.home, suppliedDependencies);
|
|
298
|
+
if (new URL(configured.apiOrigin()).origin !== loaded.state.apiOrigin) fail('ENTROROVE_CHAIN_CLOUD_BINDING', 'answer cannot switch its original cloud origin');
|
|
299
|
+
const client = createTaskMessageClient(configured); await checkChainCloud(loaded.state, client);
|
|
300
|
+
const step = loaded.state.steps.find(value => value.status !== 'verified');
|
|
301
|
+
const original = loaded.state.plan.steps.find(value => value.stepId === step?.stepId);
|
|
302
|
+
if (original?.kind !== 'skill' || original.skillId !== 'confirm-protocol' || original.operation !== 'interaction-request') {
|
|
303
|
+
fail('ENTROROVE_CHAIN_DECISION_REQUIRED', 'only an original pending confirmation can accept a terminal answer');
|
|
304
|
+
}
|
|
305
|
+
if (step.phase === 'answer' && step.request) { await reconcileAnswer(loaded, original, client); return view(loaded); }
|
|
306
|
+
if (step.status !== 'waiting') fail('ENTROROVE_CHAIN_DECISION_REQUIRED', 'the original confirmation must be genuinely waiting');
|
|
307
|
+
const resumed = await client.invoke('task-resume', { flowId: loaded.state.identity.flowId }); checkCloudTask(resumed, loaded.state.identity);
|
|
308
|
+
const question = originalChainInteraction(step, loaded.state, resumed), payload = await readChainTerminalAnswer(question);
|
|
309
|
+
const invocation = { flowId: loaded.state.identity.flowId, operation: 'interaction-answer', payload };
|
|
310
|
+
await append(loaded, step.stepId, { ...step, phase: 'answer', status: 'running', request: null, humanActorLabel: input.actorId });
|
|
311
|
+
const current = loaded.state.steps.find(value => value.stepId === step.stepId);
|
|
312
|
+
const response = await cloudRequest(loaded, current, 'continue', invocation, client, 'answer');
|
|
313
|
+
if (response) {
|
|
314
|
+
const saved = loaded.state.steps.find(value => value.stepId === step.stepId), observed = inspectChainHumanResponse(original, loaded.state, saved, response);
|
|
315
|
+
await append(loaded, step.stepId, { ...saved, status: observed.eligible ? 'verified' : observed.state,
|
|
316
|
+
phase: observed.eligible ? 'done' : 'answer', output: { ...response.result, answer: response.result.auditEntry.answer }, response,
|
|
317
|
+
error: observed.eligible ? null : { code: observed.code } });
|
|
318
|
+
}
|
|
319
|
+
return view(loaded);
|
|
320
|
+
});
|
|
321
|
+
}
|