@entrorove/skill 0.1.22 → 0.1.24
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/CHANGELOG.md +10 -0
- package/SKILL.md +12 -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 +18 -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 +22 -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-cli.mjs +49 -16
- package/local-contract-types.mjs +46 -0
- package/local-contracts.mjs +124 -0
- package/local-credential.mjs +101 -0
- package/local-decimal.mjs +85 -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 +97 -0
- package/local-formula-engine.mjs +111 -0
- package/local-formula-tests.mjs +61 -0
- package/local-formula.mjs +25 -125
- package/local-integrity.mjs +22 -41
- package/local-invocations.mjs +107 -0
- package/local-ledger.mjs +2 -7
- 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 +96 -0
- package/local-mutation-batch-receipt.mjs +84 -0
- package/local-mutation-batch.mjs +143 -0
- package/local-mutation-binding.mjs +86 -0
- package/local-mutation-files.mjs +245 -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 +38 -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 +153 -3
- package/release-state.json +153 -17
- package/release.mjs +238 -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 +1887 -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) { process.stderr.write(JSON.stringify({status:'blocked',code:error.code??'FORMULA_INPUT_INVALID',message:error instanceof SyntaxError?'Formula input or artifact must be valid JSON':error.message})+'\\n'); process.exitCode=2; }
|
|
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)])));
|
package/local-cli.mjs
CHANGED
|
@@ -1,58 +1,91 @@
|
|
|
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 { runManagedWorkflowLocalStep } from './workflow-local-client.mjs';
|
|
11
25
|
|
|
12
26
|
async function stdinJson() {
|
|
13
27
|
let raw = '';
|
|
14
28
|
for await (const chunk of process.stdin) raw += chunk;
|
|
15
29
|
if (!raw.trim()) return {};
|
|
16
|
-
|
|
30
|
+
let value;
|
|
31
|
+
try { value = JSON.parse(raw); } catch { throw new Error('Local input must be valid JSON'); }
|
|
17
32
|
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('local input must be a JSON object');
|
|
18
33
|
return value;
|
|
19
34
|
}
|
|
20
35
|
|
|
21
36
|
const operations = {
|
|
22
|
-
'snapshot-plan': snapshotPlan, 'snapshot-verify': snapshotVerify, 'mutate-gate': mutateGate,
|
|
37
|
+
'snapshot-plan': snapshotPlan, 'snapshot-verify': snapshotVerify, 'mutation-snapshot-attest': attestMutationSnapshot14, 'mutate-gate': mutateGate,
|
|
23
38
|
'gate-issue': issueMutationPass, 'gate-verify': verifyMutationPass, 'guarded-write': guardedWrite,
|
|
24
39
|
'budget-init': budgetInit, 'budget-context': budgetContext, 'budget-read': budgetRead, 'budget-status': budgetStatus,
|
|
25
40
|
'budget-extend': budgetExtend, 'peer-coordinate': peerCoordinate, coordinate: peerCoordinate,
|
|
26
41
|
'spawn-bind': spawnBind, 'peer-complete': completePeer, complete: completePeer,
|
|
27
42
|
'lease-verify': verifyCoordinationLease,
|
|
28
43
|
'evidence-sign': signEvidence, 'evidence-verify': verifyEvidence, 'merge-verified': mergeVerified,
|
|
29
|
-
rollback, '
|
|
30
|
-
'
|
|
31
|
-
'formula-
|
|
32
|
-
'formula-
|
|
44
|
+
rollback, 'merge-freeze': prepareMergeArtifacts, 'merge-preflight': produceMergeArchguardPreflight,
|
|
45
|
+
'merge-journal-head': produceMergeJournalHead, 'final-gate': finalGate,
|
|
46
|
+
'formula-evaluate': input => ({ schemaVersion: 'entrorove.local-execution/1.0', status: 'reported', value: evaluateFormula(input.formula, input.values ?? {}).toString() }),
|
|
47
|
+
'formula-graph': input => ({ schemaVersion: 'entrorove.local-execution/1.0', status: 'reported', values: evaluateFormulaGraph(input.formulas, input.inputs ?? {}) }),
|
|
48
|
+
'formula-final-gate': input => formulaFinalGate(input.engine, input), 'calculation-final-gate': formulaFinalGateFile,
|
|
33
49
|
'run-approved-plan': runApprovedPlan,
|
|
34
|
-
|
|
50
|
+
'workflow-run-step': runManagedWorkflowLocalStep,
|
|
51
|
+
...LOCAL_LEDGER_OPERATIONS, ...LOCAL_BUDGET_TRANSITION_OPERATIONS,
|
|
35
52
|
...LOCAL_RENEWAL_OPERATIONS, ...LOCAL_PROBE_OPERATIONS,
|
|
36
53
|
...LOCAL_TASK_ROUTING_OPERATIONS,
|
|
54
|
+
...ARCHGUARD_LOCAL_OPERATIONS,
|
|
55
|
+
...LOCAL_BUDGET_HANDOFF_OPERATIONS,
|
|
56
|
+
...CALCTOOL_LOCAL_OPERATIONS,
|
|
37
57
|
};
|
|
38
58
|
|
|
39
59
|
export const LOCAL_OPERATIONS = Object.freeze(operations);
|
|
40
60
|
|
|
61
|
+
async function checkedLocalInput(operation, input) {
|
|
62
|
+
try { return validateLocalInput(operation, input); }
|
|
63
|
+
catch (cause) {
|
|
64
|
+
if (['guarded-write', 'gate-issue', 'mutate-gate', 'mutation-snapshot-attest'].includes(operation)) await recordMutationDenied14(input, cause, operation);
|
|
65
|
+
throw cause;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
41
69
|
export async function runLocal(args = []) {
|
|
42
70
|
const operation = args[0];
|
|
43
|
-
if (operation === 'capabilities' || operation === 'help') {
|
|
71
|
+
if (operation === 'capabilities' || operation === 'help') {
|
|
72
|
+
validateLocalInput(operation, process.stdin.isTTY ? {} : await stdinJson());
|
|
73
|
+
process.stdout.write(`${JSON.stringify(LOCAL_CAPABILITIES)}\n`); return;
|
|
74
|
+
}
|
|
44
75
|
const handler = operations[operation];
|
|
45
76
|
if (!handler) throw new Error(`Usage: entrorove local <${['capabilities', ...Object.keys(operations)].join('|')}> [repositoryRoot]`);
|
|
46
77
|
const input = await stdinJson();
|
|
47
78
|
if (args[1] && input.repositoryRoot === undefined) input.repositoryRoot = args[1];
|
|
48
|
-
const
|
|
79
|
+
const validated = await checkedLocalInput(operation, input);
|
|
80
|
+
const output = await withLocalInvocation(operation, validated, () => withBudgetReadContext(validated, () => handler(validated)));
|
|
49
81
|
process.stdout.write(`${JSON.stringify(output)}\n`);
|
|
50
82
|
if (output?.status === 'blocked' || output?.status === 'failed' || output?.status === 'incomplete') process.exitCode = 2;
|
|
51
83
|
}
|
|
52
84
|
|
|
53
|
-
export async function runLocalInput(operation, input) {
|
|
54
|
-
if (operation === 'capabilities' || operation === 'help') return LOCAL_CAPABILITIES;
|
|
85
|
+
export async function runLocalInput(operation, input, dependencies) {
|
|
86
|
+
if (operation === 'capabilities' || operation === 'help') { validateLocalInput(operation, input); return LOCAL_CAPABILITIES; }
|
|
55
87
|
const handler = operations[operation];
|
|
56
88
|
if (!handler) throw new Error(`Unsupported local operation: ${operation}`);
|
|
57
|
-
|
|
89
|
+
const validated = await checkedLocalInput(operation, input);
|
|
90
|
+
return withLocalInvocation(operation, validated, () => withBudgetReadContext(validated, () => handler(validated, operation === 'workflow-run-step' ? dependencies : undefined)));
|
|
58
91
|
}
|
|
@@ -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'], 0), 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'] };
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { FINAL_GATE16_INPUT_SCHEMA, FINAL_GATE16_READ_INPUT_SCHEMA } from './local-final-gate16-contract.mjs';
|
|
2
|
+
import { SWARM_LOCAL_OPERATION_SCHEMAS } from './local-swarm-contracts.mjs';
|
|
3
|
+
import Ajv from 'ajv';
|
|
4
|
+
import { CALCTOOL_LOCAL_OPERATION_SCHEMAS } from './local-calctool-contracts.mjs';
|
|
5
|
+
import { HANDOFF_BUDGET_INIT_PROPERTIES, HANDOFF_BUDGET_INIT_CONDITION, handoffBudgetOperationSchema } from './local-budget-handoff-contracts.mjs';
|
|
6
|
+
import { ARCHGUARD_LOCAL_OPERATION_SCHEMAS } from './local-archguard-contracts.mjs';
|
|
7
|
+
import { MERGE_LOCAL_OPERATION_SCHEMAS } from './local-merge-contracts.mjs';
|
|
8
|
+
import { WORKFLOW_LOCAL_OPERATION_SCHEMA } from './workflow-local-contracts.mjs';
|
|
9
|
+
import { HANDOFF_WRITE_PASS_SCHEMA, HANDOFF_PROVISIONAL_LEASE_SCHEMA, HANDOFF_SNAPSHOT_CANDIDATE_SCHEMA } from './local-task-handoff-contracts.mjs';
|
|
10
|
+
import { array, binding, change, confirmation, enumeration, evidence, evidenceArray, executionContext, executionPlan,
|
|
11
|
+
formula, formulaDefinitions, formulaEntry, decimalValues, identifier, integer, ledgerIdentifier, modes, nullable, object, paths, record, relativePath, sha256, text } from './local-contract-types.mjs';
|
|
12
|
+
|
|
13
|
+
const schemas = {};
|
|
14
|
+
const base = { repositoryRoot: text, home: text };
|
|
15
|
+
const flow = { ...base, flowId: identifier };
|
|
16
|
+
const step = { ...flow, stepId: identifier };
|
|
17
|
+
function add(names, properties, required = [], alternatives) {
|
|
18
|
+
const schema = object(properties, required);
|
|
19
|
+
if (JSON.stringify(properties).includes('#/$defs/formula')) schema.$defs = formulaDefinitions;
|
|
20
|
+
if (alternatives) schema.anyOf = alternatives.map(field => ({ required: [field] }));
|
|
21
|
+
for (const name of names.split('|')) schemas[name] = schema;
|
|
22
|
+
}
|
|
23
|
+
add('capabilities|help', {});
|
|
24
|
+
add('snapshot-plan', { ...step, paths, snapshotId: identifier, contractDigest: nullable(sha256), handoffCandidate: HANDOFF_SNAPSHOT_CANDIDATE_SCHEMA }, ['repositoryRoot', 'flowId', 'stepId', 'paths']);
|
|
25
|
+
add('snapshot-verify', { ...step, snapshotId: identifier }, ['repositoryRoot', 'flowId', 'stepId', 'snapshotId']);
|
|
26
|
+
const gate = { ...step, taskId: identifier, mutationPass: record };
|
|
27
|
+
add('mutate-gate|gate-issue', gate, ['repositoryRoot', 'flowId', 'stepId', 'mutationPass']);
|
|
28
|
+
add('mutation-snapshot-attest', { ...step, taskId: identifier, snapshotId: identifier, challenge: record, signerConfigPath: text },
|
|
29
|
+
['repositoryRoot', 'flowId', 'stepId', 'snapshotId', 'challenge', 'signerConfigPath']);
|
|
30
|
+
const write = { ...step, taskId: identifier, generation: integer(1), executionAttemptNonce: identifier, gatePassPath: text, targetPath: relativePath, workerId: identifier, leaseId: identifier };
|
|
31
|
+
add('gate-verify', write, ['repositoryRoot', 'flowId', 'stepId', 'gatePassPath', 'targetPath']);
|
|
32
|
+
add('guarded-write', { ...write, content: {}, expectedSha256: nullable(sha256) }, ['repositoryRoot', 'flowId', 'stepId', 'gatePassPath', 'targetPath', 'content']);
|
|
33
|
+
schemas['guarded-write'] = { oneOf: [schemas['guarded-write'], object({ ...step, targetPath: relativePath, content: {},
|
|
34
|
+
expectedSha256: nullable(sha256), handoffId: ledgerIdentifier, epoch: integer(1, 2147483647),
|
|
35
|
+
handoffPass: HANDOFF_WRITE_PASS_SCHEMA, provisionalLease: HANDOFF_PROVISIONAL_LEASE_SCHEMA },
|
|
36
|
+
['repositoryRoot', 'flowId', 'stepId', 'targetPath', 'content', 'handoffId', 'epoch', 'handoffPass', 'provisionalLease'])] };
|
|
37
|
+
add('budget-init', { ...flow, mode: modes, executionContext, grant: record, ...HANDOFF_BUDGET_INIT_PROPERTIES }, ['repositoryRoot', 'flowId', 'mode', 'executionContext']);
|
|
38
|
+
schemas['budget-init'].allOf = [{ if: { type: 'object', properties: { executionContext: { type: 'object', properties: { executionIsolation: { const: 'sandbox' } }, required: ['executionIsolation'] } }, required: ['executionContext'] }, then: { type: 'object', required: ['grant'] } }];
|
|
39
|
+
schemas['budget-init'].allOf.push(HANDOFF_BUDGET_INIT_CONDITION);
|
|
40
|
+
schemas['budget-handoff'] = handoffBudgetOperationSchema(flow, modes, executionContext);
|
|
41
|
+
add('budget-context', { ...flow, executionContext }, ['repositoryRoot', 'flowId', 'executionContext']);
|
|
42
|
+
add('budget-read', { ...flow, path: relativePath }, ['repositoryRoot', 'flowId', 'path']);
|
|
43
|
+
add('budget-status', flow, ['repositoryRoot', 'flowId']);
|
|
44
|
+
add('budget-extend', { ...flow, confirmation, grant: record, additions: object({ files: integer(0), tokenEstimate: integer(0), durationMs: integer(0) }, ['files', 'tokenEstimate', 'durationMs']) }, ['repositoryRoot', 'flowId', 'additions'], ['grant', 'confirmation']);
|
|
45
|
+
add('budget-transition', { ...flow, grant: record }, ['repositoryRoot', 'flowId', 'grant']);
|
|
46
|
+
schemas['budget-extend'].properties.additions.anyOf = ['files', 'tokenEstimate', 'durationMs'].map(field => ({ type: 'object', properties: { [field]: integer(1) }, required: [field] }));
|
|
47
|
+
Object.assign(schemas, SWARM_LOCAL_OPERATION_SCHEMAS);
|
|
48
|
+
const signableEvidence = { ...evidence, required: evidence.required.filter(field => !['schemaVersion', 'runner'].includes(field)) };
|
|
49
|
+
add('evidence-sign', { ...base, evidence: signableEvidence }, ['repositoryRoot', 'evidence']);
|
|
50
|
+
add('evidence-verify', { ...base, evidence, receiptPublicKey: text }, ['evidence']);
|
|
51
|
+
add('merge-verified', { ...step, sourceRef: text, targetRef: text, snapshotReceipt: record, evidence: evidenceArray, preflight: record, receiptPublicKey: text, changes: array(change) }, ['repositoryRoot', 'flowId', 'stepId', 'sourceRef', 'targetRef', 'snapshotReceipt', 'evidence', 'preflight']);
|
|
52
|
+
add('rollback', { ...step, snapshotId: identifier }, ['repositoryRoot', 'flowId', 'snapshotId']);
|
|
53
|
+
const check = { anyOf: [{ type: 'boolean' }, { type: 'object', properties: { status: text }, required: ['status'] }] };
|
|
54
|
+
add('final-gate', { ...base, ...binding, audit: check, test: check, operations: check, evidence: array(evidence), preflight: record }, ['audit', 'test', 'operations', 'evidence']);
|
|
55
|
+
schemas['final-gate'] = { oneOf: [schemas['final-gate'], FINAL_GATE16_INPUT_SCHEMA, FINAL_GATE16_READ_INPUT_SCHEMA] };
|
|
56
|
+
add('formula-evaluate', { formula, values: decimalValues }, ['formula']);
|
|
57
|
+
add('formula-graph', { formulas: array(formulaEntry), inputs: decimalValues }, ['formulas']);
|
|
58
|
+
add('formula-final-gate', { engine: record, flowId: identifier, stepId: identifier }, ['engine']);
|
|
59
|
+
add('calculation-final-gate', { ...step, enginePath: relativePath }, ['repositoryRoot', 'enginePath']);
|
|
60
|
+
add('run-approved-plan', { repositoryRoot: text, planPath: relativePath, approvalPath: text, signerConfigPath: nullable(text) }, ['repositoryRoot', 'planPath', 'approvalPath', 'signerConfigPath']);
|
|
61
|
+
schemas['run-approved-plan'] = { oneOf: [schemas['run-approved-plan'], object({ ...flow, planPath: relativePath, approvalPath: text, signerConfigPath: nullable(text) },
|
|
62
|
+
['repositoryRoot', 'home', 'flowId', 'planPath', 'approvalPath', 'signerConfigPath'])] };
|
|
63
|
+
schemas['workflow-run-step'] = WORKFLOW_LOCAL_OPERATION_SCHEMA;
|
|
64
|
+
add('branch-create|branch-create-virtual', { ...base, name: ledgerIdentifier, branchName: ledgerIdentifier, fromRef: text, description: { type: 'string' }, snapshotId: nullable(ledgerIdentifier) }, ['repositoryRoot'], ['name', 'branchName']);
|
|
65
|
+
add('branch-list', base, ['repositoryRoot']);
|
|
66
|
+
add('branch-switch', { ...base, branchName: ledgerIdentifier, name: ledgerIdentifier }, ['repositoryRoot'], ['branchName', 'name']);
|
|
67
|
+
add('diff-report', { ...base, baseRef: text, targetRef: nullable(text), limit: integer(1, 500) }, ['repositoryRoot']);
|
|
68
|
+
add('preflight', { ...base, ...binding, archguard: record, preflight: record, evidence: array(evidence) }, ['repositoryRoot']);
|
|
69
|
+
add('ledger-query|chain-ledger-query', { ...base, limit: integer(1, 500) }, ['repositoryRoot']);
|
|
70
|
+
add('chain-init|chain-ledger-init', { ...base, plan: executionPlan, executionPlan }, ['repositoryRoot'], ['plan', 'executionPlan']);
|
|
71
|
+
const chain = { ...base, chainId: ledgerIdentifier };
|
|
72
|
+
add('chain-resume', { ...chain, stepId: ledgerIdentifier, status: enumeration(['running', 'waiting', 'reported', 'verified', 'blocked', 'failed']), output: {}, error: {} }, ['repositoryRoot', 'chainId']);
|
|
73
|
+
add('chain-status|chain-ledger-status', chain, ['repositoryRoot', 'chainId']);
|
|
74
|
+
add('chain-answer', { ...chain, stepId: ledgerIdentifier, answer: { anyOf: [text, object({ id: text }, ['id'])] }, accepted: { type: 'boolean' }, actorId: nullable(text) }, ['repositoryRoot', 'chainId', 'answer']);
|
|
75
|
+
const renewal = { ...flow, requestId: identifier, scope: object({ goal: text, allowedPaths: paths, forbiddenPaths: paths }, ['goal', 'allowedPaths']), policy: object({ intervalMs: integer(1), maxRenewals: integer(1, 100) }, ['intervalMs', 'maxRenewals']) };
|
|
76
|
+
add('budget-auto-renew-request', renewal, ['repositoryRoot', 'flowId', 'requestId', 'scope', 'policy']);
|
|
77
|
+
add('budget-auto-renew', { ...renewal, confirmation, grant: record }, ['repositoryRoot', 'flowId', 'requestId', 'scope', 'policy'], ['grant', 'confirmation']);
|
|
78
|
+
add('budget-auto-renew-stop', { ...flow, reason: enumeration(['revoked', 'completed']) }, ['repositoryRoot', 'flowId', 'reason']);
|
|
79
|
+
const probePaths = { anyOf: [paths, { const: ['.'] }] };
|
|
80
|
+
const scopeRequirement = object({ id: ledgerIdentifier, description: { ...text, pattern: '\\S', maxLength: 12000 } }, ['id', 'description']);
|
|
81
|
+
const scopeInteraction = object({ type: enumeration(['confirm', 'choice', 'multi', 'input']), question: text,
|
|
82
|
+
options: array(object({ id: ledgerIdentifier, label: text, hint: { type: 'string' } }, ['id', 'label'])), riskDescription: { type: 'string' },
|
|
83
|
+
callback: object({ operation: ledgerIdentifier, payload: record }, ['operation', 'payload']) }, ['type', 'question', 'options', 'callback']);
|
|
84
|
+
const scopeTask = object({ goal: { ...text, pattern: '\\S', maxLength: 12000 }, workType: enumeration(['implementation', 'documentation', 'calculation', 'merge', 'review', 'decision', 'diagnosis']),
|
|
85
|
+
difficulty: enumeration(['low', 'medium', 'high']), risk: enumeration(['low', 'medium', 'high']), crossModule: { type: 'boolean' }, parallel: { type: 'boolean' }, needParallel: { type: 'boolean' },
|
|
86
|
+
unclear: { type: 'boolean' }, architectureContract: { type: 'boolean' }, explicitAimlockRequested: { type: 'boolean' }, explicitEntroroveRequested: { type: 'boolean' }, userSpecifiedSkills: array(text),
|
|
87
|
+
requirements: { ...array(scopeRequirement, 1), maxItems: 500 }, phase: enumeration(['plan', 'coordinate', 'architecture', 'verify', 'merge', 'decision']), baselineRef: text, interaction: scopeInteraction }, ['goal', 'workType', 'difficulty', 'risk']);
|
|
88
|
+
const probe = { ...step, paths: probePaths, targetPaths: probePaths, task: scopeTask, baselineRef: text, snapshotId: identifier,
|
|
89
|
+
targetSymbols: array(text, 1), contextMapPath: relativePath, signerConfigPath: nullable(text), taskDigest: sha256, ownerId: ledgerIdentifier, chainDigest: sha256, repositoryId: identifier, challenge: ledgerIdentifier };
|
|
90
|
+
add('probe', { ...probe, handoffCandidate: HANDOFF_SNAPSHOT_CANDIDATE_SCHEMA }, ['repositoryRoot']);
|
|
91
|
+
schemas.probe.allOf = [{ if: { type: 'object', anyOf: ['flowId', 'stepId', 'snapshotId'].map(field => ({ required: [field] })) },
|
|
92
|
+
then: { type: 'object', required: ['flowId', 'stepId', 'snapshotId'], anyOf: ['paths', 'targetPaths', 'targetSymbols'].map(field => ({ required: [field] })) } },
|
|
93
|
+
{ if: { type: 'object', properties: { signerConfigPath: text }, required: ['signerConfigPath'] },
|
|
94
|
+
then: { type: 'object', required: ['flowId', 'stepId', 'snapshotId', 'taskDigest', 'ownerId', 'chainDigest', 'repositoryId', 'challenge'] } },
|
|
95
|
+
{ if: { type: 'object', required: ['handoffCandidate'] }, then: { type: 'object', required: ['flowId', 'stepId', 'snapshotId', 'paths', 'taskDigest', 'ownerId', 'chainDigest', 'repositoryId', 'challenge', 'signerConfigPath'],
|
|
96
|
+
properties: { signerConfigPath: text } } },
|
|
97
|
+
{ if: { type: 'object', required: ['targetSymbols'] }, then: { type: 'object', required: ['contextMapPath'] } }];
|
|
98
|
+
add('reassess', { ...probe, snapshotId: nullable(identifier), currentMode: modes, expectedRevision: integer(1) }, ['repositoryRoot']);
|
|
99
|
+
schemas.reassess.allOf = [{ if: { type: 'object', anyOf: [
|
|
100
|
+
...['flowId', 'stepId', 'expectedRevision', 'taskDigest', 'ownerId', 'chainDigest', 'repositoryId', 'challenge'].map(field => ({ required: [field] })),
|
|
101
|
+
...['snapshotId', 'signerConfigPath'].map(field => ({ properties: { [field]: text }, required: [field] }))] },
|
|
102
|
+
then: { type: 'object', required: ['flowId', 'stepId', 'snapshotId', 'expectedRevision'], properties: { snapshotId: identifier },
|
|
103
|
+
anyOf: ['paths', 'targetPaths', 'targetSymbols'].map(field => ({ required: [field] })) } },
|
|
104
|
+
{ if: { type: 'object', required: ['targetSymbols'] }, then: { type: 'object', required: ['contextMapPath'] } }];
|
|
105
|
+
const task = { ...base, taskId: ledgerIdentifier };
|
|
106
|
+
add('task-describe', { ...task, goal: { ...text, pattern: '\\S' }, ownerId: nullable(text), checkpoint: {}, createdAt: text }, ['repositoryRoot', 'goal']);
|
|
107
|
+
add('message-route', { ...task, messageId: ledgerIdentifier, body: {}, destination: nullable(text) }, ['repositoryRoot', 'taskId']);
|
|
108
|
+
add('message-accept', { ...task, messageId: ledgerIdentifier, ownerId: nullable(text) }, ['repositoryRoot', 'taskId', 'messageId']);
|
|
109
|
+
add('task-resume', task, ['repositoryRoot', 'taskId']);
|
|
110
|
+
|
|
111
|
+
Object.assign(schemas, ARCHGUARD_LOCAL_OPERATION_SCHEMAS, CALCTOOL_LOCAL_OPERATION_SCHEMAS, MERGE_LOCAL_OPERATION_SCHEMAS);
|
|
112
|
+
export const LOCAL_OPERATION_SCHEMAS = Object.freeze(schemas);
|
|
113
|
+
const ajv = new Ajv({ strict: true, strictRequired: false, allErrors: true });
|
|
114
|
+
const validators = new Map(Object.entries(schemas).map(([operation, schema]) => [operation, ajv.compile(schema)]));
|
|
115
|
+
export function validateLocalInput(operation, input) {
|
|
116
|
+
const validate = validators.get(operation);
|
|
117
|
+
if (!validate) throw Object.assign(new Error(`Unsupported local operation: ${operation}`), { code: 'ENTROROVE_OPERATION_UNKNOWN' });
|
|
118
|
+
if (!validate(input)) {
|
|
119
|
+
const details = validate.errors.map(error => `${error.instancePath || '/'} ${error.message}`).join('; ');
|
|
120
|
+
throw Object.assign(new Error(`Invalid ${operation} input: ${details}`), { code: 'ENTROROVE_INPUT_INVALID' });
|
|
121
|
+
}
|
|
122
|
+
return input;
|
|
123
|
+
}
|
|
124
|
+
export const operationSchemasFor = operations => Object.freeze(Object.fromEntries(Object.keys(operations).map(name => [name, schemas[name]])));
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { constants, promises as fs } from 'node:fs';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { withStateLock } from './local-state-lock.mjs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
const SCHEMA = 'entrorove.credential/1.0';
|
|
8
|
+
const TOKEN = /^ent_[A-Za-z0-9_-]{43}$/;
|
|
9
|
+
const error = (code, message) => Object.assign(new Error(message), { code });
|
|
10
|
+
|
|
11
|
+
export function credentialPath(home = os.homedir()) {
|
|
12
|
+
if (typeof home !== 'string' || !path.isAbsolute(home)) throw error('ENTROROVE_CREDENTIAL_HOME', 'Credential home must be absolute');
|
|
13
|
+
if (process.platform === 'win32') {
|
|
14
|
+
const appData = process.env.LOCALAPPDATA;
|
|
15
|
+
if (!appData || !path.isAbsolute(appData)) throw error('ENTROROVE_CREDENTIAL_HOME', 'LOCALAPPDATA must be absolute');
|
|
16
|
+
return path.join(appData, 'Entrorove', 'credential.json');
|
|
17
|
+
}
|
|
18
|
+
const parts = process.platform === 'darwin' ? ['Library', 'Application Support', 'Entrorove']
|
|
19
|
+
: process.platform === 'win32' ? ['AppData', 'Local', 'Entrorove'] : ['.local', 'share', 'entrorove'];
|
|
20
|
+
return path.join(home, ...parts, 'credential.json');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function owned(stat) {
|
|
24
|
+
return process.platform === 'win32' || stat.uid === process.getuid();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function credentialDirectory(home = os.homedir(), create = false) {
|
|
28
|
+
const homeStat = await fs.lstat(home).catch(() => { throw error('ENTROROVE_CREDENTIAL_HOME', 'Credential home is unavailable'); });
|
|
29
|
+
if (!homeStat.isDirectory() || homeStat.isSymbolicLink() || !owned(homeStat)) {
|
|
30
|
+
throw error('ENTROROVE_CREDENTIAL_DIRECTORY', 'Credential home must be an owned real directory');
|
|
31
|
+
}
|
|
32
|
+
const root = process.platform === 'win32' ? path.parse(credentialPath(home)).root : await fs.realpath(home);
|
|
33
|
+
let current = root;
|
|
34
|
+
const relative = path.relative(process.platform === 'win32' ? root : home, path.dirname(credentialPath(home)));
|
|
35
|
+
for (const part of relative.split(path.sep)) {
|
|
36
|
+
current = path.join(current, part);
|
|
37
|
+
if (create) await fs.mkdir(current, { mode: 0o700 }).catch(cause => { if (cause.code !== 'EEXIST') throw cause; });
|
|
38
|
+
const stat = await fs.lstat(current).catch(() => { throw error('ENTROROVE_CREDENTIAL_MISSING', 'Stored credential is unavailable; configure the account key privately'); });
|
|
39
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || !owned(stat)) {
|
|
40
|
+
throw error('ENTROROVE_CREDENTIAL_DIRECTORY', 'Credential directory must be an owned real directory');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (create && process.platform !== 'win32') await fs.chmod(current, 0o700);
|
|
44
|
+
const stat = await fs.lstat(current);
|
|
45
|
+
if (process.platform !== 'win32' && (stat.mode & 0o077)) throw error('ENTROROVE_CREDENTIAL_PERMISSIONS', 'Credential directory permissions must be 0700');
|
|
46
|
+
return current;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function privateFile(stat) {
|
|
50
|
+
if (!stat.isFile() || stat.nlink !== 1 || !owned(stat)) throw error('ENTROROVE_CREDENTIAL_FILE', 'Credential path must be an owned private regular file');
|
|
51
|
+
if (process.platform !== 'win32' && (stat.mode & 0o777) !== 0o600) throw error('ENTROROVE_CREDENTIAL_PERMISSIONS', 'Credential permissions must be 0600');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function readCredential(home = os.homedir()) {
|
|
55
|
+
const directory = await credentialDirectory(home);
|
|
56
|
+
let handle;
|
|
57
|
+
try {
|
|
58
|
+
handle = await fs.open(path.join(directory, 'credential.json'), constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
59
|
+
const stat = await handle.stat(); privateFile(stat);
|
|
60
|
+
if (stat.size > 4096) throw error('ENTROROVE_CREDENTIAL_INVALID', 'Invalid credential file');
|
|
61
|
+
let value;
|
|
62
|
+
try { value = JSON.parse(await handle.readFile('utf8')); }
|
|
63
|
+
catch { throw error('ENTROROVE_CREDENTIAL_INVALID', 'Invalid credential file'); }
|
|
64
|
+
if (!value || value.schemaVersion !== SCHEMA || !TOKEN.test(value.token)
|
|
65
|
+
|| Object.keys(value).sort().join(',') !== 'schemaVersion,token') throw error('ENTROROVE_CREDENTIAL_INVALID', 'Invalid credential file');
|
|
66
|
+
return value.token;
|
|
67
|
+
} catch (cause) {
|
|
68
|
+
if (cause.code?.startsWith('ENTROROVE_CREDENTIAL_')) throw cause;
|
|
69
|
+
throw error('ENTROROVE_CREDENTIAL_MISSING', 'Stored credential is unavailable; configure the account key privately');
|
|
70
|
+
} finally { await handle?.close(); }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function writeCredential(token, home = os.homedir()) {
|
|
74
|
+
if (typeof token !== 'string' || !TOKEN.test(token)) throw error('ENTROROVE_CREDENTIAL_INVALID', 'Invalid entrorove key');
|
|
75
|
+
const directory = await credentialDirectory(home, true);
|
|
76
|
+
const file = path.join(directory, 'credential.json');
|
|
77
|
+
const prior = await fs.lstat(file).catch(cause => { if (cause.code !== 'ENOENT') throw cause; return null; });
|
|
78
|
+
if (prior) privateFile(prior);
|
|
79
|
+
return withStateLock(file, async () => {
|
|
80
|
+
const locked = await fs.lstat(file).catch(cause => { if (cause.code !== 'ENOENT') throw cause; return null; });
|
|
81
|
+
if (locked) privateFile(locked);
|
|
82
|
+
if (Boolean(locked) !== Boolean(prior) || locked && (locked.ino !== prior.ino || locked.dev !== prior.dev)) {
|
|
83
|
+
throw error('ENTROROVE_CREDENTIAL_CHANGED', 'Credential changed before the private rotation lock was acquired');
|
|
84
|
+
}
|
|
85
|
+
const stage = path.join(directory, `.credential-${randomUUID()}.tmp`);
|
|
86
|
+
let handle;
|
|
87
|
+
try {
|
|
88
|
+
handle = await fs.open(stage, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 0o600);
|
|
89
|
+
await handle.writeFile(`${JSON.stringify({ schemaVersion: SCHEMA, token })}\n`);
|
|
90
|
+
await handle.sync(); await handle.close(); handle = undefined;
|
|
91
|
+
const current = await fs.lstat(file).catch(cause => { if (cause.code !== 'ENOENT') throw cause; return null; });
|
|
92
|
+
if (current) privateFile(current);
|
|
93
|
+
if (Boolean(current) !== Boolean(prior) || current && (current.ino !== prior.ino || current.dev !== prior.dev)) {
|
|
94
|
+
throw error('ENTROROVE_CREDENTIAL_CHANGED', 'Credential changed during configuration; read the current credential before replacing it');
|
|
95
|
+
}
|
|
96
|
+
await fs.rename(stage, file);
|
|
97
|
+
const parent = await fs.open(directory, constants.O_RDONLY);
|
|
98
|
+
try { await parent.sync(); } finally { await parent.close(); }
|
|
99
|
+
} finally { await handle?.close(); await fs.rm(stage, { force: true }); }
|
|
100
|
+
});
|
|
101
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export const DECIMAL_SCALE = 28;
|
|
2
|
+
export const DECIMAL_POLICY = Object.freeze({ precision: 28, rounding: 'half-away-from-zero', representation: 'decimal-string' });
|
|
3
|
+
const LITERAL = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/;
|
|
4
|
+
const MAX_DIGITS = 4096;
|
|
5
|
+
|
|
6
|
+
export function formulaError(code, message, details = {}) {
|
|
7
|
+
return Object.assign(new Error(message), { code, ...details });
|
|
8
|
+
}
|
|
9
|
+
function scale(value, maximum = DECIMAL_SCALE) {
|
|
10
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > maximum) throw formulaError('DECIMAL_SCALE_INVALID', `decimal places must be 0..${maximum}`);
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
function ten(power) { return 10n ** BigInt(scale(power, MAX_DIGITS * 2)); }
|
|
14
|
+
function normalize(coefficient, places) {
|
|
15
|
+
while (places > 0 && coefficient % 10n === 0n) { coefficient /= 10n; places -= 1; }
|
|
16
|
+
return { coefficient, scale: places };
|
|
17
|
+
}
|
|
18
|
+
function rounded(coefficient, fromScale, toScale) {
|
|
19
|
+
if (fromScale <= toScale) return coefficient * ten(toScale - fromScale);
|
|
20
|
+
const divisor = ten(fromScale - toScale);
|
|
21
|
+
let quotient = coefficient / divisor;
|
|
22
|
+
const remainder = coefficient % divisor;
|
|
23
|
+
if ((remainder < 0n ? -remainder : remainder) * 2n >= divisor) quotient += coefficient < 0n ? -1n : 1n;
|
|
24
|
+
return quotient;
|
|
25
|
+
}
|
|
26
|
+
function requireCoefficient(value) {
|
|
27
|
+
if (typeof value !== 'bigint') throw formulaError('INVALID_DECIMAL', 'decimal coefficient must be a bigint');
|
|
28
|
+
if (value.toString().replace('-', '').length > MAX_DIGITS) throw formulaError('DECIMAL_LIMIT', 'decimal exceeds the supported digit limit');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class Decimal {
|
|
32
|
+
constructor(value) {
|
|
33
|
+
if (value instanceof Decimal) { this.coefficient = value.coefficient; this.scale = value.scale; }
|
|
34
|
+
else {
|
|
35
|
+
if (!['string', 'number', 'bigint'].includes(typeof value) || (typeof value === 'number' && (!Number.isFinite(value) || (Number.isInteger(value) && !Number.isSafeInteger(value))))) {
|
|
36
|
+
throw formulaError('INVALID_DECIMAL', 'decimal must be a finite, lossless number or decimal string');
|
|
37
|
+
}
|
|
38
|
+
const source = String(value).trim();
|
|
39
|
+
if (source.length > MAX_DIGITS || !LITERAL.test(source)) throw formulaError('INVALID_DECIMAL', 'invalid decimal literal');
|
|
40
|
+
const negative = source[0] === '-';
|
|
41
|
+
const [integer = '0', fraction = ''] = source.replace(/^[+-]/, '').split('.');
|
|
42
|
+
const coefficient = BigInt(`${integer || '0'}${fraction}`) * (negative ? -1n : 1n);
|
|
43
|
+
Object.assign(this, normalize(rounded(coefficient, fraction.length, Math.min(fraction.length, DECIMAL_SCALE)), Math.min(fraction.length, DECIMAL_SCALE)));
|
|
44
|
+
}
|
|
45
|
+
this.value = Decimal.format(this.coefficient, this.scale);
|
|
46
|
+
this.s = this.value;
|
|
47
|
+
Object.freeze(this);
|
|
48
|
+
}
|
|
49
|
+
static from(value) { return value instanceof Decimal ? value : new Decimal(value); }
|
|
50
|
+
static fromParts(coefficient, places) {
|
|
51
|
+
requireCoefficient(coefficient); scale(places, MAX_DIGITS);
|
|
52
|
+
const boundedScale = Math.min(places, DECIMAL_SCALE);
|
|
53
|
+
const parts = normalize(rounded(coefficient, places, boundedScale), boundedScale);
|
|
54
|
+
return new Decimal(Decimal.format(parts.coefficient, parts.scale));
|
|
55
|
+
}
|
|
56
|
+
static format(coefficient, places) {
|
|
57
|
+
requireCoefficient(coefficient); scale(places, MAX_DIGITS);
|
|
58
|
+
const digits = (coefficient < 0n ? -coefficient : coefficient).toString().padStart(places + 1, '0');
|
|
59
|
+
const value = places === 0 ? digits : `${digits.slice(0, -places)}.${digits.slice(-places)}`;
|
|
60
|
+
return coefficient < 0n ? `-${value}` : value;
|
|
61
|
+
}
|
|
62
|
+
plus(other) {
|
|
63
|
+
const right = Decimal.from(other); const places = Math.max(this.scale, right.scale);
|
|
64
|
+
return Decimal.fromParts(this.coefficient * ten(places - this.scale) + right.coefficient * ten(places - right.scale), places);
|
|
65
|
+
}
|
|
66
|
+
minus(other) { return this.plus(Decimal.from(other).negate()); }
|
|
67
|
+
negate() { return Decimal.fromParts(-this.coefficient, this.scale); }
|
|
68
|
+
times(other) { const right = Decimal.from(other); return Decimal.fromParts(this.coefficient * right.coefficient, this.scale + right.scale); }
|
|
69
|
+
div(other) {
|
|
70
|
+
const right = Decimal.from(other);
|
|
71
|
+
if (right.isZero()) throw formulaError('DIV_ZERO', 'division by zero', { nodeId: 'div' });
|
|
72
|
+
const numerator = this.coefficient * ten(right.scale + DECIMAL_SCALE);
|
|
73
|
+
const denominator = right.coefficient * ten(this.scale);
|
|
74
|
+
let quotient = numerator / denominator;
|
|
75
|
+
const remainder = numerator % denominator;
|
|
76
|
+
if ((remainder < 0n ? -remainder : remainder) * 2n >= (denominator < 0n ? -denominator : denominator)) quotient += (numerator < 0n) !== (denominator < 0n) ? -1n : 1n;
|
|
77
|
+
return Decimal.fromParts(quotient, DECIMAL_SCALE);
|
|
78
|
+
}
|
|
79
|
+
rounded(dp) { scale(dp); return this.scale <= dp ? this : Decimal.fromParts(rounded(this.coefficient, this.scale, dp), dp); }
|
|
80
|
+
toDecimalPlaces(dp) { return this.rounded(dp); }
|
|
81
|
+
isZero() { return this.coefficient === 0n; }
|
|
82
|
+
isNegative() { return this.coefficient < 0n; }
|
|
83
|
+
toString() { return this.value; }
|
|
84
|
+
}
|
|
85
|
+
export const Decimal28 = Decimal;
|