@entrorove/skill 0.1.25 → 0.1.28
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/CAPABILITY-MAP.md +68 -16
- package/CHANGELOG.md +15 -0
- package/SKILL.md +5 -1
- package/local-archguard-checkpoint.mjs +4 -1
- package/local-archguard-snapshot.mjs +6 -1
- package/local-autocoord17-authority.mjs +36 -0
- package/local-autocoord17-controller.mjs +164 -0
- package/local-autocoord17-peers.mjs +111 -0
- package/local-autocoord17-store.mjs +110 -0
- package/local-autocoord17-tasks.mjs +68 -0
- package/local-autocoord17-waits.mjs +118 -0
- package/local-calctool-composition17.mjs +50 -0
- package/local-validator-isolation17.mjs +256 -0
- package/manifest.json +1 -1
- package/package.json +11 -2
- package/release-state.json +18 -9
- package/release.mjs +18 -1
- package/releases.json +18 -0
- package/workflow-local-request.mjs +3 -1
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { canonical } from './local-integrity.mjs';
|
|
2
|
+
import { canonicalSwarmPaths, swarmPathsOverlap } from './local-swarm-state.mjs';
|
|
3
|
+
import { coordDuration17, coordExact17, coordFail17, coordId17, coordMessage17 } from './local-autocoord17-store.mjs';
|
|
4
|
+
|
|
5
|
+
export function taskCard17(value, native) {
|
|
6
|
+
coordExact17(value, ['taskScope', 'plannedActions', 'deployTarget', 'eta', 'baselineHash', 'archConstraints']);
|
|
7
|
+
const taskScope = canonicalSwarmPaths(value.taskScope);
|
|
8
|
+
if (taskScope.some(target => !native.paths.includes(target)) || value.baselineHash !== native.grant.frozenManifestDigest
|
|
9
|
+
|| !Array.isArray(value.plannedActions) || !value.plannedActions.length || value.plannedActions.some(item => typeof item !== 'string' || !item || item.length > 500)
|
|
10
|
+
|| value.deployTarget !== null && (typeof value.deployTarget !== 'string' || !value.deployTarget || value.deployTarget.length > 500)
|
|
11
|
+
|| !Number.isFinite(Date.parse(value.eta)) || !Array.isArray(value.archConstraints)
|
|
12
|
+
|| value.archConstraints.some(item => typeof item !== 'string' || !/^(?:forbid-scope:[^\s]+|requirement:[^=\s]+=[^\s]+)$/.test(item))) {
|
|
13
|
+
coordFail17('ENTROROVE_COORD_TASKCARD', 'taskcard must preserve the original signed scope, baseline and explicit constraints');
|
|
14
|
+
}
|
|
15
|
+
return { ...structuredClone(value), taskScope };
|
|
16
|
+
}
|
|
17
|
+
export function taskConflicts17(state, task) {
|
|
18
|
+
return state.tasks.filter(other => other.taskId !== task.taskId).flatMap(other => {
|
|
19
|
+
const reasons = [];
|
|
20
|
+
if (other.card.taskScope.some(scope => task.card.taskScope.some(target => swarmPathsOverlap(scope, target)))) reasons.push('scope-overlap');
|
|
21
|
+
if (task.card.deployTarget && task.card.deployTarget === other.card.deployTarget) reasons.push('deploy-target');
|
|
22
|
+
for (const [left, right] of [[task, other], [other, task]]) for (const constraint of left.card.archConstraints) {
|
|
23
|
+
if (constraint.startsWith('forbid-scope:') && right.card.taskScope.some(target => swarmPathsOverlap(constraint.slice(13), target))) reasons.push('architecture-scope');
|
|
24
|
+
if (constraint.startsWith('requirement:')) {
|
|
25
|
+
const prefix = constraint.slice(0, constraint.indexOf('=') + 1);
|
|
26
|
+
if (right.card.archConstraints.some(value => value.startsWith(prefix) && value !== constraint)) reasons.push('decision-conflict');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return reasons.length ? [{ taskId: other.taskId, reasons: [...new Set(reasons)] }] : [];
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
export function registerTask17(state, input, native, cloud) {
|
|
33
|
+
coordId17(input.taskId); coordId17(input.agentId); coordId17(input.chainId);
|
|
34
|
+
if (input.taskId !== native.grant.taskId || input.chainId !== native.step.executionPlan.chainId
|
|
35
|
+
|| input.material.repositoryRoot !== state.repositoryRoot) coordFail17('ENTROROVE_COORD_TASK_BINDING', 'taskcard changed its original signed task, chain or repository');
|
|
36
|
+
const card = taskCard17(input.card, native), task = { taskId: input.taskId, agentId: input.agentId, chainId: input.chainId,
|
|
37
|
+
material: structuredClone(input.material), card, ...cloud, registeredAt: new Date().toISOString() };
|
|
38
|
+
const existing = state.tasks.find(value => value.taskId === task.taskId);
|
|
39
|
+
if (existing) {
|
|
40
|
+
if (canonical({ ...existing, registeredAt: null }) !== canonical({ ...task, registeredAt: null })) coordFail17('ENTROROVE_COORD_TASK_CHANGED', 'same taskcard cannot change its original identity or contents');
|
|
41
|
+
return existing;
|
|
42
|
+
}
|
|
43
|
+
state.tasks.push(task);
|
|
44
|
+
for (const conflict of taskConflicts17(state, task)) for (const target of [task.taskId, conflict.taskId]) {
|
|
45
|
+
coordMessage17(state, target, 'task-conflict', { otherTaskId: target === task.taskId ? conflict.taskId : task.taskId,
|
|
46
|
+
reasons: conflict.reasons }, [task.taskId, conflict.taskId].sort().join(':'));
|
|
47
|
+
}
|
|
48
|
+
return task;
|
|
49
|
+
}
|
|
50
|
+
export function enqueueResource17(state, task, input) {
|
|
51
|
+
coordId17(input.queueId); coordId17(input.resource); coordDuration17(input.queueTimeoutMs);
|
|
52
|
+
if (!['file', 'build', 'deploy'].includes(input.kind) || input.baselineHash !== task.card.baselineHash) {
|
|
53
|
+
coordFail17('ENTROROVE_COORD_BASELINE', 'resource queue requires the original current taskcard baseline');
|
|
54
|
+
}
|
|
55
|
+
const paths = input.kind === 'file' ? canonicalSwarmPaths(input.paths) : [];
|
|
56
|
+
if (paths.some(target => !task.card.taskScope.includes(target))) coordFail17('ENTROROVE_COORD_SCOPE', 'queue paths exceed its original taskcard');
|
|
57
|
+
const request = { queueId: input.queueId, taskId: task.taskId, agentId: task.agentId, chainId: task.chainId,
|
|
58
|
+
kind: input.kind, resource: input.resource, paths, baselineHash: input.baselineHash, queueTimeoutMs: input.queueTimeoutMs };
|
|
59
|
+
const existing = state.lockQueues.find(value => value.queueId === request.queueId);
|
|
60
|
+
if (existing) {
|
|
61
|
+
if (canonical(existing.request) !== canonical(request)) coordFail17('ENTROROVE_COORD_QUEUE_CHANGED', 'queue identity cannot change its original request');
|
|
62
|
+
return existing;
|
|
63
|
+
}
|
|
64
|
+
const now = Date.now(), entry = { request, queueId: request.queueId, taskId: task.taskId, status: 'waiting',
|
|
65
|
+
registeredAt: new Date(now).toISOString(), deadlineAt: new Date(now + input.queueTimeoutMs).toISOString(),
|
|
66
|
+
grant: null, consumerAvailable: false, unsupported: input.kind === 'file' ? 'restrictive-native-subset-adapter' : `${input.kind}-consumer` };
|
|
67
|
+
state.lockQueues.push(entry); return entry;
|
|
68
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { canonical, sha256 } from './local-integrity.mjs';
|
|
2
|
+
import { canonicalSwarmPaths } from './local-swarm-state.mjs';
|
|
3
|
+
import { coordDuration17, coordExact17, coordFail17, coordId17, coordEvent17, coordMessage17 } from './local-autocoord17-store.mjs';
|
|
4
|
+
|
|
5
|
+
const ACTIVE = ['waiting', 'need-human'];
|
|
6
|
+
const EVENT_TYPES = ['signal', 'peer-ready', 'task-reported', 'task-failed', 'lease-expired', 'lock-released'];
|
|
7
|
+
export function requestDecision17(state, task, reason, key, paths = []) {
|
|
8
|
+
const decisionId = sha256(`${reason}\0${task.taskId}\0${key}`);
|
|
9
|
+
const existing = state.decisions.find(value => value.decisionId === decisionId);
|
|
10
|
+
if (existing) return existing;
|
|
11
|
+
const decision = { schemaVersion: 'entrorove.coordination-decision/1.0', decisionId, taskId: task.taskId,
|
|
12
|
+
agentId: task.agentId, chainId: task.chainId, status: 'need-human', reason, paths,
|
|
13
|
+
risk: 'high', request: { type: 'confirm', question: `Resolve coordination ${reason} for the original task ${task.taskId}.`,
|
|
14
|
+
riskDescription: 'Unresolved coordination may conflict with another original task; no engineering authority is granted.',
|
|
15
|
+
options: [{ id: 'resume', label: 'Resume this coordination wait' }, { id: 'cancel', label: 'Cancel this coordination wait' }],
|
|
16
|
+
callback: { operation: 'coordination-resolve-human', payload: { decisionId } } }, createdAt: new Date().toISOString(), audit: null };
|
|
17
|
+
state.decisions.push(decision); coordMessage17(state, task.taskId, 'need-human', { decisionId, reason }, key);
|
|
18
|
+
return decision;
|
|
19
|
+
}
|
|
20
|
+
function match(wait, event) {
|
|
21
|
+
return wait.request.waitFor.taskId === event.taskId && wait.request.event.type === event.type
|
|
22
|
+
&& wait.request.event.resource === event.resource;
|
|
23
|
+
}
|
|
24
|
+
function cycle(state, start, current, seen = new Set()) {
|
|
25
|
+
if (current === start) return true;
|
|
26
|
+
if (seen.has(current)) return false;
|
|
27
|
+
seen.add(current);
|
|
28
|
+
return state.waits.filter(value => ACTIVE.includes(value.status) && value.taskId === current)
|
|
29
|
+
.some(value => cycle(state, start, value.request.waitFor.taskId, seen));
|
|
30
|
+
}
|
|
31
|
+
export function registerWait17(state, task, input) {
|
|
32
|
+
coordId17(input.waitId); coordDuration17(input.expectedWithinMs);
|
|
33
|
+
coordExact17(input.waitFor, ['taskId']); coordId17(input.waitFor.taskId);
|
|
34
|
+
coordExact17(input.event, ['type', 'resource']); coordId17(input.event.resource);
|
|
35
|
+
if (!EVENT_TYPES.includes(input.event.type) || !state.tasks.some(value => value.taskId === input.waitFor.taskId)
|
|
36
|
+
|| !['resume', 'notify'].includes(input.onArrival) || !['cancel', 'notify', 'escalate-need-human'].includes(input.onTimeout)) {
|
|
37
|
+
coordFail17('ENTROROVE_COORD_WAIT', 'wait must name a registered original task, structured event and explicit policies');
|
|
38
|
+
}
|
|
39
|
+
const refetchPaths = input.refetchPaths.length ? canonicalSwarmPaths(input.refetchPaths) : [];
|
|
40
|
+
if (refetchPaths.some(target => !task.card.taskScope.includes(target))) coordFail17('ENTROROVE_COORD_SCOPE', 'wait refetch paths exceed the original taskcard');
|
|
41
|
+
const request = { waitId: input.waitId, waiter: { taskId: task.taskId, agentId: task.agentId, chainId: task.chainId },
|
|
42
|
+
waitFor: input.waitFor, event: input.event, expectedWithinMs: input.expectedWithinMs,
|
|
43
|
+
onArrival: input.onArrival, onTimeout: input.onTimeout, refetchPaths };
|
|
44
|
+
const previous = state.waits.find(value => value.waitId === input.waitId);
|
|
45
|
+
if (previous) {
|
|
46
|
+
if (canonical(previous.request) !== canonical(request)) coordFail17('ENTROROVE_COORD_WAIT_CHANGED', 'same wait ID cannot change its original dependency');
|
|
47
|
+
return previous;
|
|
48
|
+
}
|
|
49
|
+
const now = Date.now(), wait = { waitId: input.waitId, taskId: task.taskId, request, status: 'waiting',
|
|
50
|
+
registeredAt: new Date(now).toISOString(), deadlineAt: new Date(now + input.expectedWithinMs).toISOString(),
|
|
51
|
+
wakeup: null, decisionId: null };
|
|
52
|
+
state.waits.push(wait);
|
|
53
|
+
const existing = state.events.find(value => match(wait, value));
|
|
54
|
+
if (existing) arrive(state, task, wait, existing);
|
|
55
|
+
else if (cycle(state, task.taskId, input.waitFor.taskId)) {
|
|
56
|
+
const involved = state.waits.filter(value => ACTIVE.includes(value.status)
|
|
57
|
+
&& (cycle(state, value.taskId, value.request.waitFor.taskId)));
|
|
58
|
+
for (const value of involved) {
|
|
59
|
+
const owner = state.tasks.find(item => item.taskId === value.taskId);
|
|
60
|
+
value.status = 'need-human'; value.decisionId = requestDecision17(state, owner, 'dependency-cycle', value.waitId, value.request.refetchPaths).decisionId;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return wait;
|
|
64
|
+
}
|
|
65
|
+
function arrive(state, task, wait, event) {
|
|
66
|
+
if (wait.status === 'need-human') return;
|
|
67
|
+
wait.status = ['task-failed', 'lease-expired'].includes(event.type) ? 'terminated' : 'arrived';
|
|
68
|
+
wait.wakeup = { event: structuredClone(event), refetchPaths: [...wait.request.refetchPaths],
|
|
69
|
+
freshAimlockSnapshotRequired: wait.request.refetchPaths.length > 0, receivedAt: new Date().toISOString() };
|
|
70
|
+
task.consecutiveTimeouts = 0;
|
|
71
|
+
coordMessage17(state, task.taskId, 'dependency-wakeup', { waitId: wait.waitId, ...wait.wakeup }, wait.waitId);
|
|
72
|
+
}
|
|
73
|
+
export function scanWaits17(state) {
|
|
74
|
+
const now = Date.now();
|
|
75
|
+
for (const wait of state.waits.filter(value => value.status === 'waiting')) {
|
|
76
|
+
const task = state.tasks.find(value => value.taskId === wait.taskId), event = state.events.find(value => match(wait, value));
|
|
77
|
+
const terminal = state.events.find(value => value.taskId === wait.request.waitFor.taskId
|
|
78
|
+
&& ['task-failed', 'lease-expired'].includes(value.type));
|
|
79
|
+
if (event || terminal) { arrive(state, task, wait, event ?? terminal); continue; }
|
|
80
|
+
if (Date.parse(wait.deadlineAt) > now) continue;
|
|
81
|
+
task.consecutiveTimeouts = (task.consecutiveTimeouts ?? 0) + 1;
|
|
82
|
+
const human = wait.request.onTimeout === 'escalate-need-human' || task.consecutiveTimeouts >= 2;
|
|
83
|
+
wait.status = human ? 'need-human' : 'timed-out';
|
|
84
|
+
coordMessage17(state, task.taskId, 'dependency-timeout', { waitId: wait.waitId, consecutive: task.consecutiveTimeouts }, wait.waitId);
|
|
85
|
+
if (human) wait.decisionId = requestDecision17(state, task, 'dependency-timeout', wait.waitId, wait.request.refetchPaths).decisionId;
|
|
86
|
+
}
|
|
87
|
+
return state.waits;
|
|
88
|
+
}
|
|
89
|
+
export function cancelWait17(state, task, waitId) {
|
|
90
|
+
const wait = state.waits.find(value => value.waitId === waitId && value.taskId === task.taskId);
|
|
91
|
+
if (!wait) coordFail17('ENTROROVE_COORD_WAIT_BINDING', 'cancel must name this original task wait');
|
|
92
|
+
if (wait.status === 'need-human') coordFail17('ENTROROVE_COORD_HUMAN_REQUIRED', 'cancellation cannot bypass an unresolved real human decision');
|
|
93
|
+
if (wait.status === 'waiting') {
|
|
94
|
+
wait.status = 'cancelled'; coordEvent17(state, 'wait-cancelled', task.taskId, waitId, { waitId }, waitId);
|
|
95
|
+
}
|
|
96
|
+
return wait;
|
|
97
|
+
}
|
|
98
|
+
export function scanQueues17(state) {
|
|
99
|
+
const now = Date.now(), decisions = [];
|
|
100
|
+
for (const queue of state.lockQueues.filter(value => value.status === 'waiting')) {
|
|
101
|
+
if (!Number.isFinite(Date.parse(queue.deadlineAt))) coordFail17('ENTROROVE_COORD_QUEUE_DEADLINE', 'queue cannot borrow an ETA or lock TTL');
|
|
102
|
+
if (Date.parse(queue.deadlineAt) > now) continue;
|
|
103
|
+
queue.status = 'timed-out';
|
|
104
|
+
const task = state.tasks.find(value => value.taskId === queue.taskId);
|
|
105
|
+
coordMessage17(state, task.taskId, 'lock-denied', { queueId: queue.queueId, resource: queue.request.resource }, queue.queueId);
|
|
106
|
+
decisions.push(requestDecision17(state, task, 'lock-queue-timeout', queue.queueId, queue.request.paths));
|
|
107
|
+
}
|
|
108
|
+
return decisions;
|
|
109
|
+
}
|
|
110
|
+
export function coordinationBlocked17(state, task, paths = []) {
|
|
111
|
+
const waits = state.waits.filter(value => value.taskId === task.taskId && ACTIVE.includes(value.status));
|
|
112
|
+
const decisions = state.decisions.filter(value => value.taskId === task.taskId && value.status === 'need-human');
|
|
113
|
+
const peers = state.intents.filter(value => value.taskId === task.taskId && ['waiting', 'partial'].includes(value.status)
|
|
114
|
+
&& (!paths.length || paths.some(target => value.blockedPaths.includes(target))));
|
|
115
|
+
return { blocked: waits.length > 0 || decisions.length > 0 || peers.length > 0,
|
|
116
|
+
waitIds: waits.map(value => value.waitId), decisionIds: decisions.map(value => value.decisionId),
|
|
117
|
+
intentIds: peers.map(value => value.intentId), writeAuthorized: false };
|
|
118
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { runCalctoolLocal } from './local-calctool.mjs';
|
|
6
|
+
import { verifyFormulaProgram } from './local-formula-engine.mjs';
|
|
7
|
+
import { runWorkflowLocalStep } from './workflow-local-runner.mjs';
|
|
8
|
+
|
|
9
|
+
const sha = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
10
|
+
function fail(message) { throw Object.assign(new Error(message), { code: 'ENTROROVE_CALCTOOL_COMPOSITION' }); }
|
|
11
|
+
export async function compileCalctoolArtifacts17(program) {
|
|
12
|
+
verifyFormulaProgram(program);
|
|
13
|
+
const root = await fs.realpath(await fs.mkdtemp(path.join(os.tmpdir(), 'entrorove-calctool17-control-')));
|
|
14
|
+
try {
|
|
15
|
+
const repositoryRoot = path.join(root, 'repository'); const home = path.join(root, 'home');
|
|
16
|
+
await fs.mkdir(repositoryRoot, { mode: 0o700 }); await fs.mkdir(home, { mode: 0o700 });
|
|
17
|
+
const output = await runCalctoolLocal('generate', { repositoryRoot, home, program });
|
|
18
|
+
const files = await Promise.all(output.files.map(async entry => {
|
|
19
|
+
const bytes = await fs.readFile(path.join(output.directory, entry.path));
|
|
20
|
+
if (sha(bytes) !== entry.sha256) fail('Native generated Calctool bytes changed');
|
|
21
|
+
return { path: entry.path, sha256: entry.sha256 };
|
|
22
|
+
}));
|
|
23
|
+
if (files.map(file => file.path).sort().join('|') !== 'program.json|runtime.mjs|tool.mjs') fail('Native Calctool triplet is incomplete');
|
|
24
|
+
return files.sort((a, b) => a.path.localeCompare(b.path));
|
|
25
|
+
} finally { await fs.rm(root, { recursive: true, force: true }); }
|
|
26
|
+
}
|
|
27
|
+
export function assertCalctoolStep17(composition, step) {
|
|
28
|
+
verifyFormulaProgram(composition.program, composition);
|
|
29
|
+
if (composition.schemaVersion !== 'entrorove.calctool-composition/1.0' || !Array.isArray(composition.files)
|
|
30
|
+
|| composition.files.length !== 3) fail('Original Calctool composition is required');
|
|
31
|
+
for (const file of composition.files) if (!step.executionPlan.files.some(entry => entry.path === file.path && entry.sha256 === file.sha256)) {
|
|
32
|
+
fail('Calctool generated bytes must be in the original frozen execution manifest');
|
|
33
|
+
}
|
|
34
|
+
const tool = composition.files.find(file => file.path.endsWith('/tool.mjs'));
|
|
35
|
+
if (!tool || JSON.stringify(step.executionPlan.policy.args) !== JSON.stringify([tool.path, '--verify'])
|
|
36
|
+
|| !step.executionPlan.tests.some(test => test.path === tool.path)) fail('Calctool step must execute its original Decimal sample verifier');
|
|
37
|
+
}
|
|
38
|
+
export async function runCalctoolCompositionStep17(input) {
|
|
39
|
+
const { composition, ...managed } = input;
|
|
40
|
+
const native = await compileCalctoolArtifacts17(composition.program);
|
|
41
|
+
const expected = native.map(file => ({ ...file, path: `${composition.directory}/${file.path}` }));
|
|
42
|
+
if (JSON.stringify(expected) !== JSON.stringify(composition.files)) fail('Composition differs from actual native Calctool generation');
|
|
43
|
+
const step = managed.acceptedPlan?.steps.find(item => item.stepId === managed.stepId);
|
|
44
|
+
if (!step || managed.acceptedPlan.flowId !== composition.flowId || managed.acceptedPlan.ownerId !== composition.ownerId
|
|
45
|
+
|| managed.acceptedPlan.taskDigest !== composition.taskDigest || managed.acceptedPlan.chainDigest !== composition.chainDigest) {
|
|
46
|
+
fail('Calctool composition must preserve its original accepted task and chain');
|
|
47
|
+
}
|
|
48
|
+
assertCalctoolStep17(composition, step);
|
|
49
|
+
return runWorkflowLocalStep(managed);
|
|
50
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { createHash, createPrivateKey, createPublicKey, randomUUID, sign, verify } from 'node:crypto';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { runApprovedPlanObject, RUNNER_LIMITS } from './local-approved-plan.mjs';
|
|
6
|
+
import { assertBudgetRunnerAllowed } from './local-budget-reads.mjs';
|
|
7
|
+
|
|
8
|
+
export const ISOLATION_SCHEMA17 = 'entrorove.validator-isolation-receipt/1.0';
|
|
9
|
+
const CONTROL_SCHEMA = 'entrorove.validator-isolation-control/1.0';
|
|
10
|
+
const SANDBOX = '/usr/bin/sandbox-exec';
|
|
11
|
+
const MARKER = 'ENTROROVE_ISOLATION17:';
|
|
12
|
+
export const isolationCanonical17 = value => Array.isArray(value) ? `[${value.map(isolationCanonical17).join(',')}]`
|
|
13
|
+
: value && typeof value === 'object' ? `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${isolationCanonical17(value[key])}`).join(',')}}` : JSON.stringify(value);
|
|
14
|
+
export const isolationDigest17 = value => createHash('sha256').update(value).digest('hex');
|
|
15
|
+
function fail(code, message) { throw Object.assign(new Error(message), { code }); }
|
|
16
|
+
function exact(value, fields, label) {
|
|
17
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).sort().join('|') !== [...fields].sort().join('|')) {
|
|
18
|
+
fail('ENTROROVE_ISOLATION_INVALID', `${label} requires its exact original fields`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function platform() {
|
|
22
|
+
if (process.platform !== 'darwin') fail('ENTROROVE_ISOLATION_PLATFORM_UNSUPPORTED', 'Validator isolation17 requires the macOS Seatbelt backend');
|
|
23
|
+
}
|
|
24
|
+
function inside(root, target) { const r = path.relative(root, target); return r === '' || !path.isAbsolute(r) && r !== '..' && !r.startsWith('../'); }
|
|
25
|
+
async function directory(value, privateDirectory = false) {
|
|
26
|
+
if (typeof value !== 'string' || !path.isAbsolute(value)) fail('ENTROROVE_ISOLATION_PATH', 'isolation directory must be absolute');
|
|
27
|
+
const canonical = await fs.realpath(value), stat = await fs.lstat(canonical);
|
|
28
|
+
if (canonical !== value || !stat.isDirectory() || stat.isSymbolicLink() || privateDirectory && (stat.uid !== process.getuid() || (stat.mode & 0o777) !== 0o700)) {
|
|
29
|
+
fail('ENTROROVE_ISOLATION_PATH', 'isolation directory must be canonical, owned and private where required');
|
|
30
|
+
}
|
|
31
|
+
return canonical;
|
|
32
|
+
}
|
|
33
|
+
async function regular(file, privateFile = false) {
|
|
34
|
+
const stat = await fs.lstat(file);
|
|
35
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > RUNNER_LIMITS.maxExecutableBytes || await fs.realpath(file) !== file
|
|
36
|
+
|| privateFile && (stat.uid !== process.getuid() || (stat.mode & 0o777) !== 0o600 || stat.nlink !== 1 || stat.size > RUNNER_LIMITS.maxControlBytes)) {
|
|
37
|
+
fail('ENTROROVE_ISOLATION_FILE', 'isolation input must be its canonical bounded regular file');
|
|
38
|
+
}
|
|
39
|
+
const bytes = await fs.readFile(file); return { bytes, sha256: isolationDigest17(bytes) };
|
|
40
|
+
}
|
|
41
|
+
async function privateControl(root, file) {
|
|
42
|
+
if (typeof file !== 'string' || !path.isAbsolute(file) || inside(root, file)) fail('ENTROROVE_ISOLATION_CONTROL', 'isolation controls must be outside the repository');
|
|
43
|
+
await directory(path.dirname(file), true); return regular(file, true);
|
|
44
|
+
}
|
|
45
|
+
const BOOTSTRAP = `import {readFileSync,writeFileSync} from 'node:fs';
|
|
46
|
+
import net from 'node:net';import {spawn} from 'node:child_process';
|
|
47
|
+
const [nonce,readTarget,writeTarget,port,executable,...args]=process.argv.slice(2);
|
|
48
|
+
const denied=work=>{try{work();return {denied:false,code:'ALLOWED'};}catch(e){return {denied:['EPERM','EACCES'].includes(e.code),code:e.code??'UNKNOWN'};}};
|
|
49
|
+
const probes={externalRead:denied(()=>readFileSync(readTarget)),externalWrite:denied(()=>writeFileSync(writeTarget,'DENIED17'))};
|
|
50
|
+
probes.network=await new Promise(resolve=>{const socket=net.connect({host:'127.0.0.1',port:Number(port)});let done=false;
|
|
51
|
+
const finish=result=>{if(done)return;done=true;clearTimeout(timer);socket.destroy();resolve(result)};
|
|
52
|
+
const timer=setTimeout(()=>finish({denied:false,code:'TIMEOUT'}),2000);
|
|
53
|
+
socket.once('connect',()=>finish({denied:false,code:'ALLOWED'}));socket.once('error',e=>finish({denied:['EPERM','EACCES'].includes(e.code),code:e.code??'UNKNOWN'}));});
|
|
54
|
+
process.stdout.write('${MARKER}'+JSON.stringify({nonce,probes})+'\\n');
|
|
55
|
+
if(Object.values(probes).some(p=>!p.denied)){process.exitCode=78;}else{
|
|
56
|
+
const child=spawn(executable,args,{cwd:process.cwd(),env:process.env,shell:false,stdio:['ignore','inherit','inherit']});
|
|
57
|
+
child.once('error',()=>{process.exitCode=79});child.once('close',(code,signal)=>{if(signal)process.kill(process.pid,signal);else process.exitCode=code??79;});}
|
|
58
|
+
`;
|
|
59
|
+
export const ISOLATION_BOOTSTRAP_SHA25617 = isolationDigest17(BOOTSTRAP);
|
|
60
|
+
function ancestors(files) {
|
|
61
|
+
const result = new Set();
|
|
62
|
+
for (const file of files) { let at = path.dirname(file); while (true) { result.add(at); if (at === '/') break; at = path.dirname(at); } }
|
|
63
|
+
return [...result].sort();
|
|
64
|
+
}
|
|
65
|
+
function profile(root, plan, node, bootstrapPath) {
|
|
66
|
+
const files = [...plan.files.map(file => path.resolve(root, file.path)), node, plan.policy.executable, bootstrapPath];
|
|
67
|
+
if (plan.files.some(file => !inside(root, path.resolve(root, file.path)) || !file.path || path.isAbsolute(file.path))) {
|
|
68
|
+
fail('ENTROROVE_ISOLATION_MANIFEST', 'isolation reads require the exact original repository manifest');
|
|
69
|
+
}
|
|
70
|
+
const literals = values => [...new Set(values)].sort().map(value => `(literal ${JSON.stringify(value)})`).join(' ');
|
|
71
|
+
return ['(version 1)', '(deny default)', '(allow process-fork)', `(allow process-exec ${literals([node, plan.policy.executable])})`,
|
|
72
|
+
'(allow signal (target self))', '(allow sysctl-read)', '(allow file-read-metadata)',
|
|
73
|
+
'(allow file-read-data (subpath "/System/Library") (subpath "/usr/lib") (subpath "/private/var/db/dyld")'
|
|
74
|
+
+ ` (literal "/dev/null") (literal "/dev/random") (literal "/dev/urandom") ${literals(files)})`,
|
|
75
|
+
`(allow file-read* ${literals(ancestors([...files, path.join(root, 'directory-entry')]))})`].join('\n');
|
|
76
|
+
}
|
|
77
|
+
function wrapped(control) {
|
|
78
|
+
const plan = structuredClone(control.originalPlan);
|
|
79
|
+
plan.policy = { ...plan.policy, executable: SANDBOX, executableSha256: control.sandboxSha256,
|
|
80
|
+
args: ['-f', control.profilePath, control.nodeExecutable, control.bootstrapPath,
|
|
81
|
+
control.nonce, control.readTarget, control.writeTarget, String(control.networkProbePort), control.originalPlan.policy.executable, ...control.originalPlan.policy.args] };
|
|
82
|
+
return plan;
|
|
83
|
+
}
|
|
84
|
+
const command = plan => [plan.policy.executable, ...plan.policy.args].map(value => JSON.stringify(value)).join(' ');
|
|
85
|
+
function guardEnvironment(environment) {
|
|
86
|
+
if (!environment || typeof environment !== 'object' || Object.keys(environment).some(key => /^(DYLD_|LD_|NODE_OPTIONS$|NODE_PATH$)/.test(key))) {
|
|
87
|
+
fail('ENTROROVE_ISOLATION_ENVIRONMENT', 'isolation cannot inherit loader injection or alternate Node startup policies');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export async function prepareValidatorIsolation17(input) {
|
|
91
|
+
platform(); await assertBudgetRunnerAllowed(input);
|
|
92
|
+
const fields = ['repositoryRoot', 'executionPlan', 'controlRoot', ...['networkProbePort', 'home', 'flowId'].filter(key => input[key] !== undefined)];
|
|
93
|
+
exact(input, fields, 'isolation preparation');
|
|
94
|
+
const root = await directory(input.repositoryRoot), controlRoot = await directory(input.controlRoot, true);
|
|
95
|
+
if (inside(root, controlRoot)) fail('ENTROROVE_ISOLATION_CONTROL', 'isolation control root must be external');
|
|
96
|
+
const originalPlan = structuredClone(input.executionPlan); guardEnvironment(originalPlan?.policy?.environment);
|
|
97
|
+
if (!Array.isArray(originalPlan?.files) || !originalPlan.files.length || typeof originalPlan.policy.executable !== 'string') {
|
|
98
|
+
fail('ENTROROVE_ISOLATION_PLAN', 'original frozen execution plan is required');
|
|
99
|
+
}
|
|
100
|
+
const nodeExecutable = await fs.realpath(process.execPath), node = await regular(nodeExecutable), sandbox = await regular(SANDBOX);
|
|
101
|
+
const original = await regular(originalPlan.policy.executable);
|
|
102
|
+
if (original.sha256 !== originalPlan.policy.executableSha256 || originalPlan.policy.executable === SANDBOX) {
|
|
103
|
+
fail('ENTROROVE_ISOLATION_EXECUTABLE', 'original approved executable must match its actual independent digest');
|
|
104
|
+
}
|
|
105
|
+
const port = input.networkProbePort ?? 9;
|
|
106
|
+
if (!Number.isSafeInteger(port) || port < 1 || port > 65535) fail('ENTROROVE_ISOLATION_PROBE', 'network denial probe requires a bounded loopback port');
|
|
107
|
+
const owned = await fs.mkdtemp(path.join(controlRoot, 'isolation17-')), nonce = randomUUID();
|
|
108
|
+
const bootstrapPath = path.join(owned, 'bootstrap.mjs'), profilePath = path.join(owned, 'profile.sb');
|
|
109
|
+
const control = { schemaVersion: CONTROL_SCHEMA, repositoryRoot: root, originalPlan,
|
|
110
|
+
originalPlanSha256: isolationDigest17(isolationCanonical17(originalPlan) + '\n'), nonce,
|
|
111
|
+
nodeExecutable, nodeSha256: node.sha256, sandboxSha256: sandbox.sha256, profilePath, bootstrapPath,
|
|
112
|
+
producerSha256: (await regular(await fs.realpath(fileURLToPath(import.meta.url)))).sha256,
|
|
113
|
+
profile: profile(root, originalPlan, nodeExecutable, bootstrapPath),
|
|
114
|
+
readTarget: path.join(owned, 'outside-read.canary'), writeTarget: path.join(owned, 'outside-write.canary'), networkProbePort: port };
|
|
115
|
+
await fs.writeFile(bootstrapPath, BOOTSTRAP, { flag: 'wx', mode: 0o600 });
|
|
116
|
+
await fs.writeFile(profilePath, control.profile, { flag: 'wx', mode: 0o600 });
|
|
117
|
+
await fs.writeFile(control.readTarget, `CANARY17:${nonce}`, { flag: 'wx', mode: 0o600 });
|
|
118
|
+
const isolationControlPath = path.join(owned, 'control.json');
|
|
119
|
+
await fs.writeFile(isolationControlPath, isolationCanonical17(control) + '\n', { flag: 'wx', mode: 0o600 });
|
|
120
|
+
const executionPlan = wrapped(control);
|
|
121
|
+
if (command(executionPlan).length > 2000) fail('ENTROROVE_ISOLATION_COMMAND_LIMIT', 'wrapped command must fit the original exact native target limit');
|
|
122
|
+
return { executionPlan, command: command(executionPlan), isolationControlPath, originalPlanSha256: control.originalPlanSha256,
|
|
123
|
+
runnerIdentity: identity(control), approvalRequired: true };
|
|
124
|
+
}
|
|
125
|
+
export async function bindValidatorIsolation17(input) {
|
|
126
|
+
platform(); await assertBudgetRunnerAllowed(input);
|
|
127
|
+
exact(input, ['repositoryRoot', 'isolationControlPath', 'executionPlan', 'evidencePolicy',
|
|
128
|
+
...['home', 'flowId'].filter(key => input[key] !== undefined)], 'isolation evidence binding');
|
|
129
|
+
const loaded = await loadControl(input), policy = input.evidencePolicy;
|
|
130
|
+
exact(policy, ['kind', 'runner', 'flowId', 'stepId', 'localVerification'], 'isolation evidence policy');
|
|
131
|
+
exact(policy.localVerification, ['operation', 'requestedTarget', 'requirementsSha256'], 'native local verification');
|
|
132
|
+
if (!['test', 'security'].includes(policy.kind) || policy.runner !== 'sandbox'
|
|
133
|
+
|| !['sandbox-run', 'intrusive-test'].includes(policy.localVerification.operation)
|
|
134
|
+
|| policy.kind !== (policy.localVerification.operation === 'sandbox-run' ? 'test' : 'security')
|
|
135
|
+
|| policy.localVerification.requestedTarget !== command(input.executionPlan)
|
|
136
|
+
|| !/^[0-9a-f]{64}$/.test(policy.localVerification.requirementsSha256)
|
|
137
|
+
|| [policy.flowId, policy.stepId].some(value => typeof value !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value))) {
|
|
138
|
+
fail('ENTROROVE_ISOLATION_BINDING', 'binding must declare its exact actual wrapped native request');
|
|
139
|
+
}
|
|
140
|
+
const control = loaded.value; control.originalPlan.evidencePolicy = structuredClone(policy);
|
|
141
|
+
control.originalPlanSha256 = isolationDigest17(isolationCanonical17(control.originalPlan) + '\n');
|
|
142
|
+
const temporary = input.isolationControlPath + '.' + randomUUID() + '.tmp';
|
|
143
|
+
try { await fs.writeFile(temporary, isolationCanonical17(control) + '\n', { flag: 'wx', mode: 0o600 }); await fs.rename(temporary, input.isolationControlPath); }
|
|
144
|
+
finally { await fs.rm(temporary, { force: true }); }
|
|
145
|
+
const executionPlan = wrapped(control);
|
|
146
|
+
return { executionPlan, command: command(executionPlan), isolationControlPath: input.isolationControlPath,
|
|
147
|
+
originalPlanSha256: control.originalPlanSha256, runnerIdentity: identity(control), approvalRequired: true };
|
|
148
|
+
}
|
|
149
|
+
function identity(control) {
|
|
150
|
+
return { backend: 'macos-seatbelt', platform: 'darwin', sandboxExecutable: SANDBOX,
|
|
151
|
+
sandboxExecutableSha256: control.sandboxSha256, nodeExecutable: control.nodeExecutable, nodeExecutableSha256: control.nodeSha256,
|
|
152
|
+
profilePath: control.profilePath, profileSha256: isolationDigest17(control.profile), bootstrapPath: control.bootstrapPath,
|
|
153
|
+
bootstrapSha256: ISOLATION_BOOTSTRAP_SHA25617, producerSha256: control.producerSha256, originalPlanSha256: control.originalPlanSha256 };
|
|
154
|
+
}
|
|
155
|
+
async function loadControl(input) {
|
|
156
|
+
const root = await directory(input.repositoryRoot), file = await privateControl(root, input.isolationControlPath);
|
|
157
|
+
let value; try { value = JSON.parse(file.bytes.toString('utf8')); } catch { fail('ENTROROVE_ISOLATION_CONTROL', 'isolation control JSON is invalid'); }
|
|
158
|
+
exact(value, ['schemaVersion', 'repositoryRoot', 'originalPlan', 'originalPlanSha256', 'nonce', 'nodeExecutable', 'nodeSha256',
|
|
159
|
+
'sandboxSha256', 'profilePath', 'bootstrapPath', 'producerSha256', 'profile', 'readTarget', 'writeTarget', 'networkProbePort'], 'isolation control');
|
|
160
|
+
if (value.schemaVersion !== CONTROL_SCHEMA || value.repositoryRoot !== root || value.nodeExecutable !== await fs.realpath(process.execPath)
|
|
161
|
+
|| value.originalPlanSha256 !== isolationDigest17(isolationCanonical17(value.originalPlan) + '\n')
|
|
162
|
+
|| value.profile !== profile(root, value.originalPlan, value.nodeExecutable, value.bootstrapPath)
|
|
163
|
+
|| isolationCanonical17(input.executionPlan) !== isolationCanonical17(wrapped(value))
|
|
164
|
+
|| value.readTarget !== path.join(path.dirname(input.isolationControlPath), 'outside-read.canary')
|
|
165
|
+
|| value.writeTarget !== path.join(path.dirname(input.isolationControlPath), 'outside-write.canary')
|
|
166
|
+
|| value.bootstrapPath !== path.join(path.dirname(input.isolationControlPath), 'bootstrap.mjs')
|
|
167
|
+
|| value.profilePath !== path.join(path.dirname(input.isolationControlPath), 'profile.sb')) {
|
|
168
|
+
fail('ENTROROVE_ISOLATION_CHANGED', 'isolation command, profile, source plan or private original control changed');
|
|
169
|
+
}
|
|
170
|
+
guardEnvironment(value.originalPlan.policy.environment);
|
|
171
|
+
if ((await privateControl(root, value.bootstrapPath)).sha256 !== ISOLATION_BOOTSTRAP_SHA25617
|
|
172
|
+
|| (await privateControl(root, value.profilePath)).sha256 !== isolationDigest17(value.profile)
|
|
173
|
+
|| (await regular(await fs.realpath(fileURLToPath(import.meta.url)))).sha256 !== value.producerSha256) {
|
|
174
|
+
fail('ENTROROVE_ISOLATION_CHANGED', 'actual private bootstrap, profile or first-party producer source changed');
|
|
175
|
+
}
|
|
176
|
+
for (const [binary, expected] of [[SANDBOX, value.sandboxSha256], [value.nodeExecutable, value.nodeSha256],
|
|
177
|
+
[value.originalPlan.policy.executable, value.originalPlan.policy.executableSha256]]) {
|
|
178
|
+
if ((await regular(binary)).sha256 !== expected) fail('ENTROROVE_ISOLATION_EXECUTABLE', 'actual isolation executable identity changed');
|
|
179
|
+
}
|
|
180
|
+
if ((await privateControl(root, value.readTarget)).bytes.toString('utf8') !== `CANARY17:${value.nonce}`) {
|
|
181
|
+
fail('ENTROROVE_ISOLATION_PROBE', 'original private external read canary changed');
|
|
182
|
+
}
|
|
183
|
+
try { await fs.lstat(value.writeTarget); fail('ENTROROVE_ISOLATION_PROBE', 'external write canary must not exist'); }
|
|
184
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
185
|
+
return { value, sha256: file.sha256 };
|
|
186
|
+
}
|
|
187
|
+
async function signing(input) {
|
|
188
|
+
const root = input.repositoryRoot, file = await privateControl(root, input.signerConfigPath);
|
|
189
|
+
let config; try { config = JSON.parse(file.bytes.toString('utf8')); } catch { fail('ENTROROVE_ISOLATION_SIGNER', 'external signer config JSON is invalid'); }
|
|
190
|
+
exact(config, ['schemaVersion', 'privateKeyPath', 'keyId', 'receiptTtlMs'], 'external signer');
|
|
191
|
+
const pinned = process.env.ENTROROVE_VALIDATOR_RECEIPT_PUBLIC_KEY?.trim();
|
|
192
|
+
if (!pinned || config.schemaVersion !== 'validator.runner-signer/1.0') fail('ENTROROVE_ISOLATION_SIGNER', 'trusted external Validator receipt public key is required');
|
|
193
|
+
let publicKey;
|
|
194
|
+
try { publicKey = createPublicKey(pinned.includes('BEGIN PUBLIC KEY') ? pinned : { key: Buffer.from(pinned, 'base64'), format: 'der', type: 'spki' }); }
|
|
195
|
+
catch { fail('ENTROROVE_ISOLATION_SIGNER', 'trusted Validator public key configuration is invalid'); }
|
|
196
|
+
if (publicKey.asymmetricKeyType !== 'ed25519' || config.keyId !== isolationDigest17(publicKey.export({ format: 'der', type: 'spki' }))) {
|
|
197
|
+
fail('ENTROROVE_ISOLATION_SIGNER', 'caller signer cannot replace the externally pinned Validator key');
|
|
198
|
+
}
|
|
199
|
+
const privateFile = await privateControl(root, config.privateKeyPath); let privateKey;
|
|
200
|
+
try { privateKey = createPrivateKey(privateFile.bytes); } finally { privateFile.bytes.fill(0); }
|
|
201
|
+
if (privateKey.asymmetricKeyType !== 'ed25519' || isolationDigest17(createPublicKey(privateKey).export({ format: 'der', type: 'spki' })) !== config.keyId) {
|
|
202
|
+
fail('ENTROROVE_ISOLATION_SIGNER', 'external signer private key differs from the configured trusted identity');
|
|
203
|
+
}
|
|
204
|
+
return { privateKey, publicKey, keyId: config.keyId, configSha256: file.sha256 };
|
|
205
|
+
}
|
|
206
|
+
function probes(result, control) {
|
|
207
|
+
const lines = result.process.stdout.split('\n').filter(line => line.startsWith(MARKER));
|
|
208
|
+
if (lines.length !== 1 || !result.process.stdout.startsWith(MARKER)) fail('ENTROROVE_ISOLATION_UNPROVEN', 'fixed isolation bootstrap did not produce one original denial result');
|
|
209
|
+
let value; try { value = JSON.parse(lines[0].slice(MARKER.length)); } catch { fail('ENTROROVE_ISOLATION_UNPROVEN', 'original isolation denial result is malformed'); }
|
|
210
|
+
exact(value, ['nonce', 'probes'], 'bootstrap result'); exact(value.probes, ['externalRead', 'externalWrite', 'network'], 'kernel probes');
|
|
211
|
+
if (value.nonce !== control.nonce) fail('ENTROROVE_ISOLATION_UNPROVEN', 'kernel probes lost their private original nonce');
|
|
212
|
+
for (const probe of Object.values(value.probes)) {
|
|
213
|
+
exact(probe, ['denied', 'code'], 'kernel denial');
|
|
214
|
+
if (probe.denied !== true || !['EPERM', 'EACCES'].includes(probe.code)) fail('ENTROROVE_ISOLATION_UNPROVEN', 'actual kernel did not deny every required external access');
|
|
215
|
+
}
|
|
216
|
+
return value.probes;
|
|
217
|
+
}
|
|
218
|
+
export async function runValidatorIsolation17(input) {
|
|
219
|
+
platform(); await assertBudgetRunnerAllowed(input);
|
|
220
|
+
exact(input, ['repositoryRoot', 'executionPlan', 'approvalPath', 'signerConfigPath', 'isolationControlPath',
|
|
221
|
+
...['home', 'flowId', 'executionDeadlineAt'].filter(key => input[key] !== undefined)], 'isolation execution');
|
|
222
|
+
const before = await loadControl(input), signer = await signing(input);
|
|
223
|
+
const { isolationControlPath: _control, ...runnerInput } = input;
|
|
224
|
+
const result = await runApprovedPlanObject(runnerInput), actualProbes = probes(result, before.value);
|
|
225
|
+
const after = await loadControl(input), afterSigner = await signing(input);
|
|
226
|
+
if (after.sha256 !== before.sha256 || afterSigner.configSha256 !== signer.configSha256) fail('ENTROROVE_ISOLATION_CHANGED', 'original isolation authority changed during execution');
|
|
227
|
+
const evidence = result.evidence[0], receipt = evidence?.receipt;
|
|
228
|
+
if (!receipt || receipt.keyId !== signer.keyId || receipt.subjectDigest !== evidence.subjectDigest || receipt.subjectDigest !== isolationDigest17(isolationCanonical17(result.subject))) {
|
|
229
|
+
fail('ENTROROVE_ISOLATION_UNPROVEN', 'actual approved execution has no original trusted bound receipt');
|
|
230
|
+
}
|
|
231
|
+
const { signature, ...original } = receipt;
|
|
232
|
+
if (!verify(null, Buffer.from(isolationCanonical17(original)), signer.publicKey, Buffer.from(signature, 'base64url'))) {
|
|
233
|
+
fail('ENTROROVE_ISOLATION_UNPROVEN', 'original approved execution signature is invalid');
|
|
234
|
+
}
|
|
235
|
+
const issuedAt = new Date().toISOString();
|
|
236
|
+
if (Date.parse(receipt.expiresAt) <= Date.parse(issuedAt)) fail('ENTROROVE_ISOLATION_EXPIRED', 'original approved receipt expired before isolation attestation');
|
|
237
|
+
const body = { schemaVersion: ISOLATION_SCHEMA17, keyId: signer.keyId, subjectDigest: evidence.subjectDigest,
|
|
238
|
+
evidenceReceiptDigest: isolationDigest17(isolationCanonical17(receipt)), evidenceReceiptNonce: receipt.nonce,
|
|
239
|
+
processDigest: isolationDigest17(isolationCanonical17(result.process)), planSha256: result.planSha256,
|
|
240
|
+
approvalSha256: result.approvalSha256, runnerIdentity: identity(before.value), probes: actualProbes, issuedAt, expiresAt: receipt.expiresAt };
|
|
241
|
+
return { ...result, isolationReceipt: { ...body, signature: sign(null, Buffer.from(isolationCanonical17(body)), signer.privateKey).toString('base64url') } };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const shaSchema = { type: 'string', pattern: '^[0-9a-f]{64}$' };
|
|
245
|
+
const stringSchema = { type: 'string', minLength: 1 };
|
|
246
|
+
const objectSchema = properties => ({ type: 'object', additionalProperties: false, required: Object.keys(properties), properties });
|
|
247
|
+
const denialSchema = objectSchema({ denied: { const: true }, code: { enum: ['EPERM', 'EACCES'] } });
|
|
248
|
+
export const VALIDATOR_ISOLATION_RECEIPT_SCHEMA17 = objectSchema({ schemaVersion: { const: ISOLATION_SCHEMA17 }, keyId: shaSchema,
|
|
249
|
+
subjectDigest: shaSchema, evidenceReceiptDigest: shaSchema, evidenceReceiptNonce: stringSchema, processDigest: shaSchema,
|
|
250
|
+
planSha256: shaSchema, approvalSha256: shaSchema, runnerIdentity: objectSchema({ backend: { const: 'macos-seatbelt' },
|
|
251
|
+
platform: { const: 'darwin' }, sandboxExecutable: { const: SANDBOX }, sandboxExecutableSha256: shaSchema,
|
|
252
|
+
nodeExecutable: stringSchema, nodeExecutableSha256: shaSchema, profilePath: stringSchema, profileSha256: shaSchema,
|
|
253
|
+
bootstrapPath: stringSchema, bootstrapSha256: shaSchema, producerSha256: shaSchema, originalPlanSha256: shaSchema }),
|
|
254
|
+
probes: objectSchema({ externalRead: denialSchema, externalWrite: denialSchema, network: denialSchema }),
|
|
255
|
+
issuedAt: { ...stringSchema, format: 'date-time' }, expiresAt: { ...stringSchema, format: 'date-time' },
|
|
256
|
+
signature: { type: 'string', pattern: '^[A-Za-z0-9_-]{86}$' } });
|
package/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@entrorove/skill",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.28",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"entrorove": "cli.mjs"
|
|
@@ -24,6 +24,14 @@
|
|
|
24
24
|
"local-swarm-state.mjs",
|
|
25
25
|
"local-swarm-storage.mjs",
|
|
26
26
|
"local-swarm.mjs",
|
|
27
|
+
"local-autocoord17-authority.mjs",
|
|
28
|
+
"local-autocoord17-controller.mjs",
|
|
29
|
+
"local-autocoord17-peers.mjs",
|
|
30
|
+
"local-autocoord17-store.mjs",
|
|
31
|
+
"local-autocoord17-tasks.mjs",
|
|
32
|
+
"local-autocoord17-waits.mjs",
|
|
33
|
+
"local-validator-isolation17.mjs",
|
|
34
|
+
"local-calctool-composition17.mjs",
|
|
27
35
|
"local-mutation-files.mjs",
|
|
28
36
|
"local-mutation-snapshot.mjs",
|
|
29
37
|
"local-mutation-audit.mjs",
|
|
@@ -35,6 +43,7 @@
|
|
|
35
43
|
"local-mutation-batch-receipt.mjs",
|
|
36
44
|
"local-integrity.mjs",
|
|
37
45
|
"local-evidence-verification.mjs",
|
|
46
|
+
"local-error.mjs",
|
|
38
47
|
"local-formula.mjs",
|
|
39
48
|
"local-ledger.mjs",
|
|
40
49
|
"local-chain-bindings.mjs",
|
|
@@ -176,7 +185,7 @@
|
|
|
176
185
|
"local-final-gate16-release.mjs"
|
|
177
186
|
],
|
|
178
187
|
"scripts": {
|
|
179
|
-
"test": "node --test test/*.test.mjs",
|
|
188
|
+
"test": "node --test --test-concurrency=1 test/*.test.mjs",
|
|
180
189
|
"prepublishOnly": "node release.mjs",
|
|
181
190
|
"prepare:archguard": "node archguard-runtime-build.mjs",
|
|
182
191
|
"check:archguard": "node archguard-runtime-build.mjs --check",
|
package/release-state.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": "entrorove.release-state/1.0",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"digest": "
|
|
3
|
+
"version": "0.1.28",
|
|
4
|
+
"digest": "45cbb490c5d7fc99eb1ee6d4d852f9695e614ea3fd0c9923e30a6d24e777401e",
|
|
5
5
|
"files": {
|
|
6
6
|
"local-final-gate16.mjs": "b30caab08441449ad556a6e9a3cefbf91190f0b3f7370f46d2a21156375aa8e3",
|
|
7
7
|
"local-final-gate16-checks.mjs": "1dd30e9666d458d8b12ca6e4975f6f0ac8450c3ea9cbecc7401ae037f66b589a",
|
|
@@ -36,15 +36,23 @@
|
|
|
36
36
|
"local-swarm-state.mjs": "ff84c5f35ef3e4a0ad9887d145e404f5786ae0dc92496116699d031b98e90c6a",
|
|
37
37
|
"local-swarm-storage.mjs": "4b506481913fe22769f47a14a024a2cc4c0f1e986bb52ef606d14c7844226086",
|
|
38
38
|
"local-swarm.mjs": "94b5adde0239cc21bada288886482934d409f4383d31076025437ed621ef43b6",
|
|
39
|
+
"local-autocoord17-authority.mjs": "86b79798f6019523b1260b819622070a5b0d39670f607da930fb6923259dff1d",
|
|
40
|
+
"local-autocoord17-controller.mjs": "1307c4decf8dc98a447e4072d27f69caec64149f8d48545c84ef7cc50a561777",
|
|
41
|
+
"local-autocoord17-peers.mjs": "103d6a6634bbd3b4b850c5db7b571e3f05736b94e59a8fd7da63741281c903a4",
|
|
42
|
+
"local-autocoord17-store.mjs": "b48847a4a40c3aeb16bc1431b6a16221fb5ffdc0a6ba5c263acf196279d80ed1",
|
|
43
|
+
"local-autocoord17-tasks.mjs": "44b9f2d326ab3060c36657685b48b50a98711644e5baf47d4e8cb72c4c45f2a7",
|
|
44
|
+
"local-autocoord17-waits.mjs": "2b0a713987822548a94e1ed3a9a891532db45383797dfc5d5bc8d1e35593cf25",
|
|
45
|
+
"local-validator-isolation17.mjs": "48e94604e28b8fddd567dfb35ff60b73fd094a5803bd49f076388b7f310f92aa",
|
|
46
|
+
"local-calctool-composition17.mjs": "ac89fcb41ab0d0d9b47c1b5696945281b0894eb064820775e36a011c76314e6a",
|
|
39
47
|
"cli.mjs": "c85289b1cbe0ea469a57e67edb1fc475dc9c626c2beae00f5e0954c00757481d",
|
|
40
48
|
"review-client.mjs": "3491eef68ed533174b31db3c53febb6519b8f088f71a9964644714a9a4c03c77",
|
|
41
49
|
"version.mjs": "1f392147d72c40af4b03b307ea607075fb815af5174fa07e254b95fe12dbf3e7",
|
|
42
|
-
"SKILL.md": "
|
|
43
|
-
"CAPABILITY-MAP.md": "
|
|
50
|
+
"SKILL.md": "87c5e85b0c767c85660ebb9958bccaeb69f3f8c2f122f210f5b38e30df891014",
|
|
51
|
+
"CAPABILITY-MAP.md": "128ecc8d90b2c17846ce6f0c650eada9e7fa7240f5d3c03b31e3ee01135557b8",
|
|
44
52
|
"AGREEMENT.md": "f93a68e92c457200dd024610e65f74e478b8f60860f951da818b70561f1b9bf8",
|
|
45
|
-
"package.json": "
|
|
53
|
+
"package.json": "cb54098a13e05f22ef09bcb6a35a47995415bb14732791b61c0334ec7c7d1403",
|
|
46
54
|
"manifest.json": "ab06e307795bc9da5add926752b6c4dbdaf18625090261a035aa356a679c504f",
|
|
47
|
-
"release.mjs": "
|
|
55
|
+
"release.mjs": "607f63013ed2cdf9448d451c7e5a4e130715be5bae70e8905fb15d874ea7b93a",
|
|
48
56
|
"local-cli.mjs": "ecc85bc5f36a5663fef004e59525374dc7883d3889f5a0cfe219ae5e84b2d3ea",
|
|
49
57
|
"local-state-lock.mjs": "1dbf64264642a0851faf043e07ee9f66d2ad0508e9f799b0626246e42fae893e",
|
|
50
58
|
"local-execution.mjs": "97dfad5e889e679e9cc34434005342526502795abba21078fa40673788073153",
|
|
@@ -52,6 +60,7 @@
|
|
|
52
60
|
"local-evidence-verification.mjs": "9b7dea31777505272f63616fd62af7603338f703bb1285fc565d6451715c7ce0",
|
|
53
61
|
"local-formula.mjs": "706d5ec0cb2caf95a547531a81ca746fe246d4c53d902e03595a3b360e8c85af",
|
|
54
62
|
"local-ledger.mjs": "2009fb68aadcd7292b72f1d0d99f378d03644be1c5b4b4150d9f6a0b3f7c15ce",
|
|
63
|
+
"local-error.mjs": "66484e2e27c9870a17dc0f1cf64d108ea384ad9fb41b7adc3aacb243ae65fe2e",
|
|
55
64
|
"blueprint-criterion-wire.mjs": "fb6b7dcce0b97a699e3e8968372767a3fddd83fedc750998573f789177243072",
|
|
56
65
|
"blueprint-criterion-runner.mjs": "761c0e670b4b20c6bed53e5ca5d0178759505c3977ea2ed1b7223383c534bc4c",
|
|
57
66
|
"local-chain-bindings.mjs": "b8eff999ccd17b53c614a3c9f5376da78fcaa8632bb3e07bc1adcceda2e75f6c",
|
|
@@ -126,13 +135,13 @@
|
|
|
126
135
|
"workflow-local-runner.mjs": "9fa899a11da2b6b7a436bf9b561641856adcbc5f8b914f6ba89a64dae224900d",
|
|
127
136
|
"workflow-local-contracts.mjs": "604afa9e97c6dfc3d07b01509ef2476887a2fc219c9e827bcac5d563ab4719e7",
|
|
128
137
|
"workflow-local-client.mjs": "45c954f694d96bf0ea87ee2dc6902a6763dac2cb1df713b6a5e749632054e9af",
|
|
129
|
-
"workflow-local-request.mjs": "
|
|
138
|
+
"workflow-local-request.mjs": "4e24c543ce386292a87dc9cd41f90d23375f12079cacffc515c8cd01fd571780",
|
|
130
139
|
"task-message-entry.mjs": "31d10056cb905237cd5649e8eb067009ff1612ec5dc1061e0373209fbc0b646a",
|
|
131
140
|
"task-message-cli.mjs": "3ca65e96c28129b3bd5cac962d106e0859a1f17df1f6a66ee543c6efc22f918d",
|
|
132
141
|
"local-contracts.mjs": "43715cd73460ccea734501a3dd5667184da0f6a5b0189e038319a439ba52fc76",
|
|
133
142
|
"local-contract-types.mjs": "5d244237fc69052b2fa83098022761bae929e8b75a0683883b620711bc132050",
|
|
134
143
|
"local-archguard-ast.mjs": "cefd835312a623e83bf48e4d8e1f76ac6a8adc56c35843705c16967d16bec4e6",
|
|
135
|
-
"local-archguard-checkpoint.mjs": "
|
|
144
|
+
"local-archguard-checkpoint.mjs": "37896902520cc995720c96288e4a5ed3585952b55c9c35be281f91fa754c71ec",
|
|
136
145
|
"local-archguard-components.mjs": "a8fb3d0fd787f9160905013fd333cc0b9e8cf624cce797461f36f4ab31d170ad",
|
|
137
146
|
"local-archguard-contract.mjs": "cfc397a1ce171ab84f8bf1db152e6d8d280b7ca041da56a7a4cd62b02c2e2e89",
|
|
138
147
|
"local-archguard-contracts.mjs": "29e9b86f14dc086aa0b791c874d6efc50d849f806b3f2b8725481114f01ba8ba",
|
|
@@ -142,7 +151,7 @@
|
|
|
142
151
|
"local-archguard-operations.mjs": "1a35268b857b86eab54dd4efa78fc54bf08b39dfab4fa980c7dd63b8959206b0",
|
|
143
152
|
"local-archguard-scope.mjs": "afbd23a79c175f198892ddb2026ecac219a1650ee6115763736d9b1ee5a07ee0",
|
|
144
153
|
"local-archguard-signing.mjs": "7de0fe4b8f6f5a92d043ad8976054faaa76b7b37556ed582e5e012c6f0facc87",
|
|
145
|
-
"local-archguard-snapshot.mjs": "
|
|
154
|
+
"local-archguard-snapshot.mjs": "81282fa7d0e3bbf879fc5a3c6a9e4f44fb15dee36029bb0ce2a5e4f1710cf4c9",
|
|
146
155
|
"local-archguard-state.mjs": "1d3c74c4ec222646e82120be4aa6a00c52d2867fb8c20323420d6ab0bf2cd46c",
|
|
147
156
|
"local-archguard.mjs": "4c8ec5d569d728b15161eb88e49371c77def7d1fe511aa16694581a2c1d59433",
|
|
148
157
|
"archguard-runtime-build.mjs": "45bf31764763973bd8de32b3e16d194162407e7cb74802df3ab363f0fd30e866",
|