@entrorove/skill 0.1.52 → 0.1.53
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 +14 -2
- package/CHANGELOG.md +7 -0
- package/DELIVERY-BOUNDARIES.md +4 -2
- package/IDE-EXECUTION.md +25 -3
- package/SKILL.md +8 -4
- package/cli.mjs +4 -6
- package/cloud-mutation-authority.mjs +96 -0
- package/connected-cli.mjs +20 -0
- package/ide-command.mjs +1 -1
- package/ide-hook.mjs +24 -10
- package/ide-results.mjs +8 -6
- package/ide-session.mjs +44 -1
- package/local-approved-plan.mjs +3 -21
- package/local-approved-process.mjs +54 -0
- package/local-approved-supervisor.mjs +99 -0
- package/local-cli.mjs +13 -2
- package/local-contracts.mjs +2 -0
- package/local-delegated-modify-analysis.mjs +232 -0
- package/local-discipline-actions.mjs +2 -0
- package/local-discipline-gate.mjs +3 -1
- package/local-error.mjs +3 -0
- package/local-execution.mjs +1 -1
- package/local-mutation-authority.mjs +23 -5
- package/local-mutation-batch.mjs +1 -0
- package/local-mutation-binding.mjs +14 -5
- package/local-mutation-consumption.mjs +93 -0
- package/local-mutation-pass-store.mjs +6 -4
- package/local-mutation-wire.mjs +21 -4
- package/local-preserved-mutation-protection.mjs +51 -0
- package/local-preserved-mutation-signer.mjs +81 -0
- package/local-preserved-mutation-state.mjs +96 -0
- package/local-preserved-mutation-wire.mjs +49 -0
- package/manifest.json +1 -1
- package/mutation-client.mjs +290 -0
- package/package.json +19 -2
- package/release-state.json +43 -26
- package/release.mjs +25 -0
- package/releases.json +6 -0
- package/remote-client.mjs +2 -1
- package/workflow-observation-client.mjs +44 -0
- package/workflow-observation-connection.mjs +35 -0
- package/workflow-observation-remote.mjs +84 -0
- package/workflow-observation-runner.mjs +87 -0
- package/workflow-observation-state.mjs +36 -0
- package/workflow-observation-wire.mjs +69 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { fork } from 'node:child_process';
|
|
2
|
+
import { performance } from 'node:perf_hooks';
|
|
3
|
+
|
|
4
|
+
const SUPERVISOR = new URL('./local-approved-supervisor.mjs', import.meta.url);
|
|
5
|
+
const MESSAGE_SCHEMA = 'entrorove.approved-process-supervision/1.0';
|
|
6
|
+
const RUNNER_SCHEMA = 'entrorove.validator-local-runner/1.0';
|
|
7
|
+
|
|
8
|
+
function launchInput(authority) {
|
|
9
|
+
const now = Date.now(), policy = authority.policy;
|
|
10
|
+
const leaseDeadline = authority.executionDeadlineAt === undefined ? Infinity : Date.parse(authority.executionDeadlineAt);
|
|
11
|
+
const startDeadlineAt = authority.startDeadlineAt === undefined ? null : Date.parse(authority.startDeadlineAt);
|
|
12
|
+
if ((!Number.isFinite(leaseDeadline) && leaseDeadline !== Infinity) || leaseDeadline <= now
|
|
13
|
+
|| (startDeadlineAt !== null && (!Number.isFinite(startDeadlineAt) || startDeadlineAt <= now))) {
|
|
14
|
+
throw Object.assign(new Error('workflow execution lease expired before process launch'), { code: 'ENTROROVE_WORKFLOW_EXPIRED' });
|
|
15
|
+
}
|
|
16
|
+
return { schemaVersion: MESSAGE_SCHEMA, root: authority.root, executable: policy.executable, args: policy.args,
|
|
17
|
+
environment: policy.environment, timeoutMs: policy.timeoutMs, maxOutputBytes: policy.maxOutputBytes,
|
|
18
|
+
objectPlan: Boolean(authority.objectPlan), deadlineAt: Math.min(now + policy.timeoutMs, leaseDeadline), startDeadlineAt,
|
|
19
|
+
leaseDeadline: leaseDeadline <= now + policy.timeoutMs };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function supervisionError(code, message, effects = 'unknown') {
|
|
23
|
+
return Object.assign(new Error(message), { code,
|
|
24
|
+
execution: { schemaVersion: RUNNER_SCHEMA, status: 'failed', evidence: [], effects } });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function runApprovedProcess(authority) {
|
|
28
|
+
const started = performance.now(), input = launchInput(authority);
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
let finished = false;
|
|
31
|
+
const supervisor = fork(SUPERVISOR, [], { execPath: process.execPath, execArgv: [], env: {}, detached: true,
|
|
32
|
+
stdio: ['ignore', 'ignore', 'ignore', 'ipc'], serialization: 'json' });
|
|
33
|
+
const finish = (error, result) => {
|
|
34
|
+
if (finished) return; finished = true;
|
|
35
|
+
if (supervisor.connected) supervisor.disconnect();
|
|
36
|
+
if (error) reject(error); else resolve({ ...result, durationMs: Math.max(0, Math.round(performance.now() - started)) });
|
|
37
|
+
};
|
|
38
|
+
supervisor.on('message', message => {
|
|
39
|
+
if (message?.schemaVersion !== MESSAGE_SCHEMA || message.type !== 'result') return;
|
|
40
|
+
if (message.error) {
|
|
41
|
+
const error = Object.assign(new Error(message.error.message), { code: message.error.code });
|
|
42
|
+
if (message.process) error.execution = { schemaVersion: RUNNER_SCHEMA, status: 'failed', process: message.process,
|
|
43
|
+
evidence: [], effects: message.process.started === true ? 'unknown' : 'none' };
|
|
44
|
+
finish(error); return;
|
|
45
|
+
}
|
|
46
|
+
finish(null, message.process);
|
|
47
|
+
});
|
|
48
|
+
supervisor.once('error', () => finish(supervisionError('ENTROROVE_PROCESS_SUPERVISOR', 'Approved process supervision could not start', 'none')));
|
|
49
|
+
supervisor.once('close', () => { if (!finished) finish(supervisionError('ENTROROVE_PROCESS_SUPERVISOR', 'Approved process supervision ended without a result')); });
|
|
50
|
+
supervisor.send(input, error => {
|
|
51
|
+
if (error) finish(supervisionError('ENTROROVE_PROCESS_SUPERVISOR', 'Approved process supervision could not receive its frozen command', 'none'));
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { performance } from 'node:perf_hooks';
|
|
5
|
+
|
|
6
|
+
const MESSAGE_SCHEMA = 'entrorove.approved-process-supervision/1.0';
|
|
7
|
+
const RUNNER_SCHEMA = 'entrorove.validator-local-runner/1.0';
|
|
8
|
+
const FIELDS = ['schemaVersion', 'root', 'executable', 'args', 'environment', 'timeoutMs', 'maxOutputBytes', 'objectPlan', 'deadlineAt', 'startDeadlineAt', 'leaseDeadline'];
|
|
9
|
+
const ENV = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
10
|
+
|
|
11
|
+
function validInput(value) {
|
|
12
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
13
|
+
&& Object.keys(value).sort().join('|') === [...FIELDS].sort().join('|') && value.schemaVersion === MESSAGE_SCHEMA
|
|
14
|
+
&& typeof value.root === 'string' && path.isAbsolute(value.root) && !value.root.includes('\0')
|
|
15
|
+
&& typeof value.executable === 'string' && path.isAbsolute(value.executable) && !value.executable.includes('\0')
|
|
16
|
+
&& Array.isArray(value.args) && value.args.length <= 128
|
|
17
|
+
&& value.args.every(item => typeof item === 'string' && !item.includes('\0') && item.length <= 4096)
|
|
18
|
+
&& value.environment && typeof value.environment === 'object' && !Array.isArray(value.environment)
|
|
19
|
+
&& Object.keys(value.environment).length <= 64
|
|
20
|
+
&& Object.entries(value.environment).every(([key, item]) => ENV.test(key) && typeof item === 'string' && !item.includes('\0') && item.length <= 16_384)
|
|
21
|
+
&& Number.isSafeInteger(value.timeoutMs) && value.timeoutMs > 0 && value.timeoutMs <= 300_000
|
|
22
|
+
&& Number.isSafeInteger(value.maxOutputBytes) && value.maxOutputBytes > 0 && value.maxOutputBytes <= 1_048_576
|
|
23
|
+
&& typeof value.objectPlan === 'boolean' && typeof value.leaseDeadline === 'boolean'
|
|
24
|
+
&& (value.startDeadlineAt === null || Number.isSafeInteger(value.startDeadlineAt))
|
|
25
|
+
&& Number.isSafeInteger(value.deadlineAt) && value.deadlineAt <= Date.now() + value.timeoutMs;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resultMessage(processResult, error) {
|
|
29
|
+
return { schemaVersion: MESSAGE_SCHEMA, type: 'result', process: processResult,
|
|
30
|
+
...(error ? { error: { code: error.code ?? 'ENTROROVE_PROCESS_SUPERVISOR', message: String(error.message).slice(0, 1024) } } : {}) };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function deliver(value) {
|
|
34
|
+
if (!process.connected) return;
|
|
35
|
+
process.send(value, () => { if (process.connected) process.disconnect(); });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function assertLaunchable(input) {
|
|
39
|
+
if (!process.connected || input.deadlineAt <= Date.now() || input.startDeadlineAt !== null && input.startDeadlineAt <= Date.now()) {
|
|
40
|
+
throw Object.assign(new Error('workflow execution lease expired before process launch'), { code: 'ENTROROVE_WORKFLOW_EXPIRED' });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function supervise(input) {
|
|
45
|
+
const started = performance.now(), stdout = [], stderr = [], stdoutHash = createHash('sha256'), stderrHash = createHash('sha256');
|
|
46
|
+
let bytes = 0, dropped = 0, timedOut = false, terminating = false, spawned = false, closed = false;
|
|
47
|
+
let processStartedAt = null, spawnError = null, killTimer;
|
|
48
|
+
assertLaunchable(input);
|
|
49
|
+
const child = spawn(input.executable, input.args, { cwd: input.root, env: { ...input.environment },
|
|
50
|
+
shell: false, detached: true, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
51
|
+
const signalGroup = signal => { if (!closed && child.pid) { try { process.kill(-child.pid, signal); } catch {} } };
|
|
52
|
+
const terminate = () => {
|
|
53
|
+
if (terminating || closed) return; terminating = true; signalGroup('SIGTERM');
|
|
54
|
+
killTimer = setTimeout(() => signalGroup('SIGKILL'), 250);
|
|
55
|
+
};
|
|
56
|
+
const consume = (target, hash, chunk) => {
|
|
57
|
+
const data = Buffer.from(chunk); hash.update(data);
|
|
58
|
+
const keep = Math.max(0, Math.min(data.length, input.maxOutputBytes - bytes));
|
|
59
|
+
if (keep) target.push(data.subarray(0, keep)); bytes += keep; dropped += data.length - keep;
|
|
60
|
+
if (dropped) terminate();
|
|
61
|
+
};
|
|
62
|
+
const timer = setTimeout(() => { timedOut = true; terminate(); }, Math.max(1, input.deadlineAt - Date.now()));
|
|
63
|
+
process.once('disconnect', terminate); process.once('SIGTERM', terminate); process.once('SIGINT', terminate);
|
|
64
|
+
child.stdout.on('data', chunk => consume(stdout, stdoutHash, chunk)); child.stderr.on('data', chunk => consume(stderr, stderrHash, chunk));
|
|
65
|
+
child.once('spawn', () => { spawned = true; processStartedAt = new Date().toISOString(); });
|
|
66
|
+
child.once('error', error => { spawnError = error; terminate(); });
|
|
67
|
+
child.once('exit', () => signalGroup('SIGKILL'));
|
|
68
|
+
child.once('close', (exitCode, signal) => {
|
|
69
|
+
closed = true; clearTimeout(timer); clearTimeout(killTimer);
|
|
70
|
+
process.removeListener('disconnect', terminate); process.removeListener('SIGTERM', terminate); process.removeListener('SIGINT', terminate);
|
|
71
|
+
const processResult = { schemaVersion: RUNNER_SCHEMA,
|
|
72
|
+
status: exitCode === 0 && signal === null && !timedOut && !dropped && !spawnError ? 'succeeded' : 'failed',
|
|
73
|
+
exitCode, signal, timedOut, outputLimitExceeded: dropped > 0, durationMs: Math.max(0, Math.round(performance.now() - started)),
|
|
74
|
+
stdout: Buffer.concat(stdout).toString('utf8'), stderr: Buffer.concat(stderr).toString('utf8'),
|
|
75
|
+
stdoutSha256: stdoutHash.digest('hex'), stderrSha256: stderrHash.digest('hex'), capturedBytes: bytes, droppedBytes: dropped,
|
|
76
|
+
...(input.objectPlan ? { started: spawned, startedAt: processStartedAt, finishedAt: new Date().toISOString(),
|
|
77
|
+
executionDeadlineExceeded: timedOut && input.leaseDeadline } : {}) };
|
|
78
|
+
deliver(resultMessage(processResult, spawnError));
|
|
79
|
+
});
|
|
80
|
+
if (!process.connected) terminate();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function main() {
|
|
84
|
+
if (process.argv.length !== 2 || typeof process.send !== 'function' || !process.connected) { process.exitCode = 2; return; }
|
|
85
|
+
const startup = setTimeout(() => { if (process.connected) process.disconnect(); }, 5_000);
|
|
86
|
+
const disconnect = () => clearTimeout(startup); process.once('disconnect', disconnect);
|
|
87
|
+
process.once('message', input => {
|
|
88
|
+
clearTimeout(startup); process.removeListener('disconnect', disconnect);
|
|
89
|
+
if (!process.connected) return;
|
|
90
|
+
if (!validInput(input)) {
|
|
91
|
+
deliver(resultMessage(null, { code: 'ENTROROVE_PROCESS_SUPERVISOR', message: 'Approved process supervision requires one bounded frozen command' })); return;
|
|
92
|
+
}
|
|
93
|
+
try { supervise(input); }
|
|
94
|
+
catch (error) { deliver(resultMessage(null, error.code === 'ENTROROVE_WORKFLOW_EXPIRED' ? error
|
|
95
|
+
: { code: 'ENTROROVE_PROCESS_SUPERVISOR', message: 'Approved process supervision could not launch its frozen command' })); }
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
main();
|
package/local-cli.mjs
CHANGED
|
@@ -28,6 +28,10 @@ import { DISCIPLINE_LOCAL_OPERATIONS } from './local-discipline-operations.mjs';
|
|
|
28
28
|
import { assertIdeExecution, settleBreakGlassSession } from './ide-session.mjs';
|
|
29
29
|
import { runManagedWorkflowLocalStep } from './workflow-local-client.mjs';
|
|
30
30
|
import { localErrorEnvelope } from './local-error.mjs';
|
|
31
|
+
import { preparePreservedMutationSigner } from './local-preserved-mutation-signer.mjs';
|
|
32
|
+
import { hydrateCloudMutationAuthority } from './cloud-mutation-authority.mjs';
|
|
33
|
+
import { attestMutationConsumption14 } from './local-mutation-consumption.mjs';
|
|
34
|
+
import { apiUrl } from './remote-client.mjs';
|
|
31
35
|
|
|
32
36
|
async function stdinJson() {
|
|
33
37
|
let raw = '';
|
|
@@ -40,6 +44,8 @@ async function stdinJson() {
|
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
const operations = {
|
|
47
|
+
'mutation-consumption-attest': attestMutationConsumption14,
|
|
48
|
+
'mutation-delegate-prepare': preparePreservedMutationSigner,
|
|
43
49
|
...DISCIPLINE_LOCAL_OPERATIONS,
|
|
44
50
|
'snapshot-plan': snapshotPlan, 'snapshot-verify': snapshotVerify, 'mutation-snapshot-attest': attestMutationSnapshot14, 'mutate-gate': mutateGate,
|
|
45
51
|
'gate-issue': issueMutationPass, 'gate-verify': verifyMutationPass, 'guarded-write': guardedWrite,
|
|
@@ -79,7 +85,11 @@ function capabilitiesFor(group) {
|
|
|
79
85
|
}
|
|
80
86
|
|
|
81
87
|
async function checkedLocalInput(operation, input) {
|
|
82
|
-
try {
|
|
88
|
+
try {
|
|
89
|
+
const validated = validateLocalInput(operation, input);
|
|
90
|
+
if (['mutation-snapshot-attest', 'mutation-consumption-attest', 'workflow-run-step', 'mutate-gate', 'gate-issue', 'gate-verify', 'guarded-write', 'guarded-delete', 'guarded-move', 'discipline-review', 'discipline-check'].includes(operation)) await hydrateCloudMutationAuthority(input.home, new URL(apiUrl).origin);
|
|
91
|
+
return validated;
|
|
92
|
+
}
|
|
83
93
|
catch (cause) {
|
|
84
94
|
if (['guarded-write', 'guarded-delete', 'guarded-move', 'gate-issue', 'mutate-gate', 'mutation-snapshot-attest'].includes(operation)) await recordMutationDenied14(input, cause, operation);
|
|
85
95
|
throw cause;
|
|
@@ -94,9 +104,10 @@ export async function runLocal(args = [], dependencies) {
|
|
|
94
104
|
process.stdout.write(`${JSON.stringify(capabilitiesFor(args[1]))}\n`); return;
|
|
95
105
|
}
|
|
96
106
|
const handler = operations[operation];
|
|
97
|
-
if (!handler) throw new Error(`
|
|
107
|
+
if (!handler && process.stdin.isTTY) throw new Error(`Unsupported local operation: ${operation}`);
|
|
98
108
|
const input = await stdinJson();
|
|
99
109
|
await assertIdeExecution(['local', ...args], input);
|
|
110
|
+
if (!handler) throw new Error(`Usage: entrorove local <${['capabilities', ...Object.keys(operations)].join('|')}> [repositoryRoot]`);
|
|
100
111
|
if (args[1] && input.repositoryRoot === undefined) input.repositoryRoot = args[1];
|
|
101
112
|
const validated = await checkedLocalInput(operation, input);
|
|
102
113
|
const supplied = operation === 'workflow-run-step' || Object.hasOwn(LOCAL_CHAIN_OPERATIONS, operation) ? dependencies : undefined;
|
package/local-contracts.mjs
CHANGED
|
@@ -29,6 +29,8 @@ add('flow-cancel|flow-abandon', flowLifecycle, ['repositoryRoot', 'flowId']);
|
|
|
29
29
|
add('snapshot-plan', { ...step, paths, snapshotId: identifier, contractDigest: nullable(sha256), handoffCandidate: HANDOFF_SNAPSHOT_CANDIDATE_SCHEMA }, ['repositoryRoot', 'flowId', 'stepId', 'paths']);
|
|
30
30
|
add('snapshot-verify', { ...step, snapshotId: identifier }, ['repositoryRoot', 'flowId', 'stepId', 'snapshotId']);
|
|
31
31
|
const gate = { ...step, taskId: identifier, mutationPass: record };
|
|
32
|
+
add('mutation-delegate-prepare', { ...step, taskId: identifier }, ['repositoryRoot', 'home', 'flowId']);
|
|
33
|
+
add('mutation-consumption-attest', { ...step, taskId: identifier, gatePassPath: text }, ['repositoryRoot', 'flowId', 'stepId', 'gatePassPath']);
|
|
32
34
|
add('mutate-gate|gate-issue', gate, ['repositoryRoot', 'flowId', 'stepId', 'mutationPass']);
|
|
33
35
|
add('mutation-snapshot-attest', { ...step, taskId: identifier, snapshotId: identifier, challenge: record, signerConfigPath: text },
|
|
34
36
|
['repositoryRoot', 'flowId', 'stepId', 'snapshotId', 'challenge', 'signerConfigPath']);
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import { parse as parseVue, parseCache } from '@vue/compiler-sfc';
|
|
3
|
+
import { canonical } from './local-integrity.mjs';
|
|
4
|
+
import { createScopeImportResolver } from './local-scope-import-expressions.mjs';
|
|
5
|
+
import { isDynamicFunctionReference, isTypeOnlyReference } from './local-typescript-function-reference.mjs';
|
|
6
|
+
|
|
7
|
+
const fail = () => { throw Object.assign(new Error('Preserved modification changed an existing definition, public contract, dependency, or unsupported source boundary'),
|
|
8
|
+
{ code: 'ENTROROVE_MUTATION_DELEGATION_CHANGE' }); };
|
|
9
|
+
const printer = ts.createPrinter({ removeComments: true });
|
|
10
|
+
const print = node => node ? printer.printNode(ts.EmitHint.Unspecified, node, node.getSourceFile()) : null;
|
|
11
|
+
function unwrap(node) {
|
|
12
|
+
while (node && (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)
|
|
13
|
+
|| ts.isSatisfiesExpression(node) || ts.isNonNullExpression(node))) node = node.expression;
|
|
14
|
+
return node;
|
|
15
|
+
}
|
|
16
|
+
function names(node) {
|
|
17
|
+
if (!node) return [];
|
|
18
|
+
if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node) || ts.isPrivateIdentifier(node)) return [node.text];
|
|
19
|
+
if (ts.isObjectBindingPattern(node) || ts.isArrayBindingPattern(node)) return node.elements.flatMap(item => ts.isBindingElement(item) ? names(item.name) : []);
|
|
20
|
+
return [print(node)];
|
|
21
|
+
}
|
|
22
|
+
function owner(node) {
|
|
23
|
+
if (node.name) return names(node.name).join(',');
|
|
24
|
+
if (node.parent?.name && (ts.isVariableDeclaration(node.parent) || ts.isPropertyAssignment(node.parent))) return names(node.parent.name).join(',');
|
|
25
|
+
return '<anonymous>';
|
|
26
|
+
}
|
|
27
|
+
function scope(node) {
|
|
28
|
+
const parts = [];
|
|
29
|
+
for (let parent = node.parent; parent && !ts.isSourceFile(parent); parent = parent.parent) {
|
|
30
|
+
if (ts.isFunctionLike(parent) || ts.isClassLike(parent) || ts.isModuleDeclaration(parent)) parts.unshift(`${parent.kind}:${owner(parent)}`);
|
|
31
|
+
}
|
|
32
|
+
return parts.join('/');
|
|
33
|
+
}
|
|
34
|
+
const modifiers = node => node.modifiers?.map(value => value.kind) ?? [];
|
|
35
|
+
function signature(node) {
|
|
36
|
+
return { kind: node.kind, name: node.name ? print(node.name) : null, modifiers: modifiers(node), optional: Boolean(node.questionToken),
|
|
37
|
+
generator: Boolean(node.asteriskToken), types: node.typeParameters?.map(print) ?? [], type: print(node.type),
|
|
38
|
+
parameters: node.parameters?.map(print) ?? [] };
|
|
39
|
+
}
|
|
40
|
+
function exported(node, exportNames) {
|
|
41
|
+
if (names(node.name).some(name => exportNames.has(name))) return true;
|
|
42
|
+
for (let current = node; current && !ts.isSourceFile(current); current = current.parent) {
|
|
43
|
+
if (current !== node && ts.isFunctionLike(current)) return false;
|
|
44
|
+
if (current.modifiers?.some(value => [ts.SyntaxKind.ExportKeyword, ts.SyntaxKind.DefaultKeyword].includes(value.kind))
|
|
45
|
+
|| ts.isExportAssignment(current)) return true;
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
function exportedNames(source) {
|
|
50
|
+
const result = new Set();
|
|
51
|
+
for (const node of source.statements) {
|
|
52
|
+
if (ts.isExportDeclaration(node) && !node.moduleSpecifier && node.exportClause && ts.isNamedExports(node.exportClause)) {
|
|
53
|
+
for (const item of node.exportClause.elements) result.add((item.propertyName ?? item.name).text);
|
|
54
|
+
}
|
|
55
|
+
if (ts.isExportAssignment(node) && ts.isIdentifier(unwrap(node.expression))) result.add(unwrap(node.expression).text);
|
|
56
|
+
}
|
|
57
|
+
const common = node => {
|
|
58
|
+
if (commonJs(node)) {
|
|
59
|
+
const value = unwrap(node.right);
|
|
60
|
+
if (ts.isIdentifier(value)) result.add(value.text);
|
|
61
|
+
if (ts.isObjectLiteralExpression(value)) for (const property of value.properties) {
|
|
62
|
+
if (ts.isShorthandPropertyAssignment(property)) result.add(property.name.text);
|
|
63
|
+
if (ts.isPropertyAssignment(property) && ts.isIdentifier(unwrap(property.initializer))) result.add(unwrap(property.initializer).text);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
ts.forEachChild(node, common);
|
|
67
|
+
};
|
|
68
|
+
common(source);
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
function isDefinition(node) {
|
|
72
|
+
return ts.isVariableDeclaration(node) || ts.isParameter(node) || ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node)
|
|
73
|
+
|| ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isEnumDeclaration(node) || ts.isEnumMember(node)
|
|
74
|
+
|| ts.isPropertySignature(node) || ts.isMethodSignature(node) || ts.isPropertyDeclaration(node) || ts.isMethodDeclaration(node)
|
|
75
|
+
|| ts.isPropertyAssignment(node) || ts.isShorthandPropertyAssignment(node) || ts.isGetAccessorDeclaration(node)
|
|
76
|
+
|| ts.isSetAccessorDeclaration(node) || ts.isModuleDeclaration(node) || ts.isImportSpecifier(node) || ts.isImportClause(node)
|
|
77
|
+
|| ts.isNamespaceImport(node) || ts.isImportEqualsDeclaration(node) || ts.isTypeParameterDeclaration(node);
|
|
78
|
+
}
|
|
79
|
+
const increment = (map, key) => map.set(key, (map.get(key) ?? 0) + 1);
|
|
80
|
+
function variableContract(node) {
|
|
81
|
+
const value = unwrap(node.initializer);
|
|
82
|
+
return { ...signature(node), binding: node.parent.flags & (ts.NodeFlags.Const | ts.NodeFlags.Let),
|
|
83
|
+
callable: value && ts.isFunctionLike(value) ? signature(value) : null,
|
|
84
|
+
alias: value && (ts.isIdentifier(value) || ts.isPropertyAccessExpression(value) || ts.isElementAccessExpression(value)) ? print(value) : null };
|
|
85
|
+
}
|
|
86
|
+
function isDependency(node) {
|
|
87
|
+
return ts.isImportDeclaration(node) || ts.isExportDeclaration(node) || ts.isImportEqualsDeclaration(node) || ts.isImportTypeNode(node)
|
|
88
|
+
|| ts.isCallExpression(node) && (node.expression.kind === ts.SyntaxKind.ImportKeyword
|
|
89
|
+
|| ts.isIdentifier(node.expression) && node.expression.text === 'require');
|
|
90
|
+
}
|
|
91
|
+
function dependency(node, source, resolve) {
|
|
92
|
+
const call = ts.isCallExpression(node);
|
|
93
|
+
return { syntax: print(node), alternatives: call ? resolve(node.arguments[0]) : null,
|
|
94
|
+
comments: source.text.slice(node.getStart(source), node.end).match(/\/\*[\s\S]*?\*\//g) ?? [] };
|
|
95
|
+
}
|
|
96
|
+
function commonJs(node) {
|
|
97
|
+
return ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken
|
|
98
|
+
&& /^(?:module\.exports|exports)(?:\.[A-Za-z_$][\w$]*|\[[\s\S]+\])?$/.test(print(node.left) ?? '');
|
|
99
|
+
}
|
|
100
|
+
function sourceChecker(source) {
|
|
101
|
+
const filename = `/${source.fileName.replace(/^\/+/, '')}`;
|
|
102
|
+
const matches = value => `/${value.replace(/^\/+/, '')}` === filename;
|
|
103
|
+
return ts.createProgram([filename], { noLib: true, noResolve: true, allowJs: true }, {
|
|
104
|
+
getSourceFile: value => matches(value) ? source : undefined, getDefaultLibFileName: () => '/none.d.ts', writeFile: () => {},
|
|
105
|
+
getCurrentDirectory: () => '/', getDirectories: () => [], fileExists: matches,
|
|
106
|
+
readFile: value => matches(value) ? source.text : undefined, getCanonicalFileName: value => value,
|
|
107
|
+
useCaseSensitiveFileNames: () => true, getNewLine: () => '\n',
|
|
108
|
+
}).getTypeChecker();
|
|
109
|
+
}
|
|
110
|
+
function exportedObject(value) {
|
|
111
|
+
return value.properties.map(property => {
|
|
112
|
+
const initializer = ts.isPropertyAssignment(property) ? unwrap(property.initializer) : property;
|
|
113
|
+
return { name: print(property.name), kind: property.kind,
|
|
114
|
+
alias: ts.isIdentifier(initializer) || ts.isPropertyAccessExpression(initializer) ? print(initializer) : null,
|
|
115
|
+
callable: ts.isFunctionLike(initializer) ? signature(initializer) : null };
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
function unknownReference(node, context) {
|
|
119
|
+
if (isTypeOnlyReference(node)) return false;
|
|
120
|
+
if (ts.isIdentifier(node) && (['eval', 'createRequire'].includes(node.text) || isDynamicFunctionReference(node, context))) return true;
|
|
121
|
+
if (ts.isIdentifier(node) && node.text === 'require' && !(ts.isCallExpression(node.parent) && node.parent.expression === node)
|
|
122
|
+
&& !(ts.isPropertyAccessExpression(node.parent) && node.parent.name === node)) return true;
|
|
123
|
+
if (ts.isElementAccessExpression(node) && (!ts.isStringLiteralLike(node.argumentExpression)
|
|
124
|
+
|| ['Function', 'eval', 'require', 'createRequire'].includes(node.argumentExpression.text))) return true;
|
|
125
|
+
return ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression)
|
|
126
|
+
&& ['Object', 'Reflect'].includes(node.expression.expression.text) && ['get', 'set', 'defineProperty'].includes(node.expression.name.text);
|
|
127
|
+
}
|
|
128
|
+
function scriptProjection(part) {
|
|
129
|
+
const kind = /\.[jt]sx$/i.test(part.path) ? ts.ScriptKind.TSX : /\.[cm]?js$/i.test(part.path) ? ts.ScriptKind.JS : ts.ScriptKind.TS;
|
|
130
|
+
const source = ts.createSourceFile(part.path, part.content, ts.ScriptTarget.Latest, true, kind);
|
|
131
|
+
if (source.parseDiagnostics.length) fail();
|
|
132
|
+
const result = { dependencies: [], definitions: new Map(), signatures: new Map(), public: [], unknown: new Map() };
|
|
133
|
+
const exports = exportedNames(source), resolve = createScopeImportResolver(source);
|
|
134
|
+
let visited = 0, checker;
|
|
135
|
+
const inferred = node => {
|
|
136
|
+
checker ??= sourceChecker(source);
|
|
137
|
+
return checker.typeToString(checker.getTypeAtLocation(node), node, ts.TypeFormatFlags.NoTruncation);
|
|
138
|
+
};
|
|
139
|
+
const visit = node => {
|
|
140
|
+
if (++visited > 200_000) fail();
|
|
141
|
+
const key = `${scope(node)}/${node.kind}:${names(node.name).join(',')}`;
|
|
142
|
+
if (isDependency(node)) result.dependencies.push(dependency(node, source, resolve));
|
|
143
|
+
if (isDefinition(node)) for (const name of names(node.name)) increment(result.definitions, `${scope(node)}/${node.kind}:${name}`);
|
|
144
|
+
if (ts.isClassExpression(node)) increment(result.definitions, `${scope(node)}/${node.kind}:${owner(node)}`);
|
|
145
|
+
if (ts.isFunctionLike(node)) increment(result.signatures, canonical({ scope: scope(node), signature: signature(node) }));
|
|
146
|
+
if (ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isEnumDeclaration(node)) increment(result.signatures, canonical({ key, contract: print(node) }));
|
|
147
|
+
if (exported(node, exports) && isDefinition(node)) {
|
|
148
|
+
const contract = ts.isVariableDeclaration(node) ? variableContract(node) : signature(node);
|
|
149
|
+
result.public.push({ key, contract });
|
|
150
|
+
if (ts.isFunctionLike(node) || ts.isVariableDeclaration(node) && (contract.callable || contract.alias)) {
|
|
151
|
+
result.public.push({ key, inferred: inferred(node) });
|
|
152
|
+
}
|
|
153
|
+
if (ts.isPropertyAssignment(node) && ['props', 'emits'].includes(names(node.name)[0])) result.public.push({ key, metadata: print(node.initializer) });
|
|
154
|
+
if (ts.isClassDeclaration(node)) result.public.push({ key, heritage: node.heritageClauses?.map(print) ?? [] });
|
|
155
|
+
}
|
|
156
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)
|
|
157
|
+
&& ['defineProps', 'defineEmits', 'defineSlots', 'defineModel', 'withDefaults'].includes(node.expression.text)) {
|
|
158
|
+
result.public.push({ macro: print(node) });
|
|
159
|
+
}
|
|
160
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'defineExpose') {
|
|
161
|
+
result.public.push({ expose: node.arguments.map(value => ts.isObjectLiteralExpression(value) ? exportedObject(value) : print(value)) });
|
|
162
|
+
}
|
|
163
|
+
if (ts.isExportAssignment(node) && ![ts.SyntaxKind.ObjectLiteralExpression, ts.SyntaxKind.CallExpression,
|
|
164
|
+
ts.SyntaxKind.FunctionExpression, ts.SyntaxKind.ClassExpression, ts.SyntaxKind.ArrowFunction].includes(unwrap(node.expression).kind)) {
|
|
165
|
+
result.public.push({ defaultAlias: print(node.expression), equals: node.isExportEquals });
|
|
166
|
+
}
|
|
167
|
+
if (commonJs(node)) result.public.push({ commonJs: print(node.left), contract: ts.isFunctionLike(unwrap(node.right))
|
|
168
|
+
? { signature: signature(unwrap(node.right)), inferred: inferred(unwrap(node.right)) }
|
|
169
|
+
: ts.isObjectLiteralExpression(unwrap(node.right)) ? exportedObject(unwrap(node.right)) : print(node.right) });
|
|
170
|
+
if (unknownReference(node, part.context)) increment(result.unknown, print(node));
|
|
171
|
+
ts.forEachChild(node, visit);
|
|
172
|
+
};
|
|
173
|
+
visit(source);
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
function templateParts(root, path) {
|
|
177
|
+
const parts = [], seen = new WeakSet();
|
|
178
|
+
const walk = node => {
|
|
179
|
+
if (!node || typeof node !== 'object' || seen.has(node)) return;
|
|
180
|
+
seen.add(node);
|
|
181
|
+
if (node.type === 7 && ['on', 'for'].includes(node.name) && node.exp?.content) {
|
|
182
|
+
parts.push({ path: `${path}.ts`, content: node.name === 'for' ? `for (${node.exp.content}) {}` : node.exp.content });
|
|
183
|
+
seen.add(node.exp);
|
|
184
|
+
}
|
|
185
|
+
if (node.type === 4 && !node.isStatic && node.content) parts.push({ path: `${path}.ts`, content: `void (${node.content});` });
|
|
186
|
+
for (const [key, child] of Object.entries(node)) if (!['loc', 'ast', 'parent', 'codegenNode'].includes(key)) {
|
|
187
|
+
if (Array.isArray(child)) child.forEach(walk); else walk(child);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
walk(root);
|
|
191
|
+
return parts;
|
|
192
|
+
}
|
|
193
|
+
function parts(path, content) {
|
|
194
|
+
if (!/\.vue$/i.test(path)) {
|
|
195
|
+
if (!/\.[cm]?[jt]sx?$/i.test(path)) fail();
|
|
196
|
+
return { metadata: [], scripts: [{ path, content }] };
|
|
197
|
+
}
|
|
198
|
+
const parsed = parseVue(content, { filename: path, sourceMap: false, templateParseOptions: { expressionPlugins: ['typescript'] } });
|
|
199
|
+
parseCache.clear();
|
|
200
|
+
if (parsed.errors.length) fail();
|
|
201
|
+
const value = parsed.descriptor, scripts = [];
|
|
202
|
+
const metadata = [value.template?.attrs ?? null, value.styles.map(block => ({ attrs: block.attrs, content: block.content })),
|
|
203
|
+
value.customBlocks.map(block => ({ type: block.type, attrs: block.attrs, content: block.content }))];
|
|
204
|
+
if (value.template?.lang) fail();
|
|
205
|
+
for (const [index, block] of [value.script, value.scriptSetup].entries()) {
|
|
206
|
+
metadata.push(block?.attrs ?? null);
|
|
207
|
+
if (!block) continue;
|
|
208
|
+
const extension = { javascript: 'js', typescript: 'ts' }[block.lang] ?? block.lang ?? 'js';
|
|
209
|
+
if (!['js', 'ts', 'jsx', 'tsx'].includes(extension)) fail();
|
|
210
|
+
scripts.push({ path: `${path}.${index}.${extension}`, content: block.src ? `import ${JSON.stringify(block.src)};` : block.content,
|
|
211
|
+
context: { vueComponent: true, vueSetup: block === value.scriptSetup } });
|
|
212
|
+
}
|
|
213
|
+
scripts.push(...templateParts(value.template?.ast, path));
|
|
214
|
+
return { metadata, scripts };
|
|
215
|
+
}
|
|
216
|
+
function retained(before, after) {
|
|
217
|
+
for (const [key, count] of before) if ((after.get(key) ?? 0) < count) fail();
|
|
218
|
+
}
|
|
219
|
+
export function assertDelegatedSourceModification(input) {
|
|
220
|
+
const { path, before, after } = input ?? {};
|
|
221
|
+
if (typeof path !== 'string' || typeof before !== 'string' || typeof after !== 'string'
|
|
222
|
+
|| Buffer.byteLength(before) > 1_048_576 || Buffer.byteLength(after) > 1_048_576) fail();
|
|
223
|
+
try {
|
|
224
|
+
const original = parts(path, before), candidate = parts(path, after);
|
|
225
|
+
if (canonical(original.metadata) !== canonical(candidate.metadata) || original.scripts.length !== candidate.scripts.length) fail();
|
|
226
|
+
for (let index = 0; index < original.scripts.length; index++) {
|
|
227
|
+
const a = scriptProjection(original.scripts[index]), b = scriptProjection(candidate.scripts[index]);
|
|
228
|
+
if (canonical(a.dependencies) !== canonical(b.dependencies) || canonical(a.public) !== canonical(b.public)) fail();
|
|
229
|
+
retained(a.definitions, b.definitions); retained(a.signatures, b.signatures); retained(b.unknown, a.unknown);
|
|
230
|
+
}
|
|
231
|
+
} catch { fail(); }
|
|
232
|
+
}
|
|
@@ -11,6 +11,7 @@ import { auditMutation14, withMutationAudit14 } from './local-mutation-audit.mjs
|
|
|
11
11
|
import { mutationFail14, removeMutationProjectFile14, moveMutationProjectFile14 } from './local-mutation-files.mjs';
|
|
12
12
|
|
|
13
13
|
function actionIntent(input, value, kind, review) {
|
|
14
|
+
if (value.pass.writeDelegation) mutationFail14('ENTROROVE_MUTATION_DELEGATION_CHANGE', 'Preserved mutation forbids structural file actions');
|
|
14
15
|
if (!value.pass.disciplinePolicy) mutationFail14('ENTROROVE_DISCIPLINE_NEEDS_HUMAN', `${kind} needs a separately approved original change declaration`);
|
|
15
16
|
const policy = disciplineApprovedPolicy(value.pass.disciplinePolicy), intent = policy.writes.find(write => write.targetPath === value.target);
|
|
16
17
|
const { usageReviewDigest, ...supplied } = input.declaration ?? {};
|
|
@@ -63,6 +64,7 @@ async function perform(input, kind, original, state) {
|
|
|
63
64
|
async function guardedAction(input, kind) {
|
|
64
65
|
return withMutationAudit14(input, `guarded-${kind}`, async () => {
|
|
65
66
|
const value = await checkedPass(input), pass = value.pass;
|
|
67
|
+
if (pass.writeDelegation) mutationFail14('ENTROROVE_MUTATION_DELEGATION_CHANGE', 'Preserved mutation forbids structural file actions');
|
|
66
68
|
return withStateLock(value.locations.consumptionPath, () => withTaskHandoffWriteFence({ ...input,
|
|
67
69
|
epoch: pass.epoch, passId: pass.passId, leaseId: pass.leaseId }, state => perform(input, kind, value, state)));
|
|
68
70
|
});
|
|
@@ -5,6 +5,7 @@ import { disciplineDuplicateMappings, disciplineFormattingLines } from './local-
|
|
|
5
5
|
import { disciplineRepositoryObservation, assertDisciplineRepository } from './local-discipline-repository.mjs';
|
|
6
6
|
import { readDisciplineSnapshot } from './local-discipline-snapshot.mjs';
|
|
7
7
|
import { readSnapshotCopy14 } from './local-mutation-snapshot.mjs';
|
|
8
|
+
import { assertDelegatedSourceModification } from './local-delegated-modify-analysis.mjs';
|
|
8
9
|
|
|
9
10
|
const needs = message => mutationFail14('ENTROROVE_DISCIPLINE_NEEDS_HUMAN', message);
|
|
10
11
|
function declaredWrite(input, pass, baseline) {
|
|
@@ -73,11 +74,12 @@ export async function disciplineWritePreflight(input, value, content) {
|
|
|
73
74
|
const review = await readDisciplineSnapshot(snapshot), baseline = snapshot.manifest.files.find(file => file.path === target);
|
|
74
75
|
await assertDisciplineRepository(input, binding.root, review, ledger);
|
|
75
76
|
const intent = declaredWrite(input, pass, baseline);
|
|
77
|
+
const original = baseline.exists ? (await readSnapshotCopy14(input, snapshot.snapshotRoot, snapshot.manifest, target)).content.toString('utf8') : '';
|
|
78
|
+
if (pass.writeDelegation) assertDelegatedSourceModification({ path: target, before: original, after: content });
|
|
76
79
|
const candidate = await disciplineRepositoryObservation(input, binding.root, { analyze: true, overrides: new Map([[target, content]]) });
|
|
77
80
|
const removed = checkRemoved(review, candidate.analysis, target, intent, snapshot.manifest.disciplineReviewDigest, input.declaration);
|
|
78
81
|
checkContracts(review, candidate.analysis, target, intent);
|
|
79
82
|
const reuse = checkReuse(review, candidate.analysis, target, intent, pass.parallelPaths);
|
|
80
|
-
const original = baseline.exists ? (await readSnapshotCopy14(input, snapshot.snapshotRoot, snapshot.manifest, target)).content.toString('utf8') : '';
|
|
81
83
|
const formatting = baseline.exists ? disciplineFormattingLines(original, content, target) : [];
|
|
82
84
|
if (formatting.length) mutationFail14('ENTROROVE_DISCIPLINE_FORMAT_SCOPE', `Formatting touches lines without functional changes: ${target}:${formatting.join(',')}`);
|
|
83
85
|
return { reviewDigest: snapshot.manifest.disciplineReviewDigest, declaredKind: baseline.exists ? 'modify' : 'create', removed, reuse,
|
package/local-error.mjs
CHANGED
|
@@ -13,6 +13,8 @@ export const LOCAL_ERROR_CODE_CATALOG = Object.freeze({
|
|
|
13
13
|
ENTROROVE_DISCIPLINE_USAGE_UNKNOWN: Object.freeze({ disposition: 'needs-human', stability: 'stable' }),
|
|
14
14
|
ENTROROVE_DISCIPLINE_REVIEW_REQUIRED: Object.freeze({ disposition: 'needs-human', stability: 'stable' }),
|
|
15
15
|
ENTROROVE_DISCIPLINE_DATA_SNAPSHOT_REQUIRED: Object.freeze({ disposition: 'needs-human', stability: 'stable' }),
|
|
16
|
+
ENTROROVE_MUTATION_DELEGATION_CHANGE: Object.freeze({ disposition: 'terminal', stability: 'stable' }),
|
|
17
|
+
ENTROROVE_MUTATION_DELEGATION_INVALID: Object.freeze({ disposition: 'terminal', stability: 'stable' }),
|
|
16
18
|
ENTROROVE_IDE_GUARD_REQUIRED: Object.freeze({ disposition: 'needs-human', stability: 'stable' }),
|
|
17
19
|
ENTROROVE_IDE_GUARD_UNSUPPORTED: Object.freeze({ disposition: 'terminal', stability: 'stable' }),
|
|
18
20
|
});
|
|
@@ -25,6 +27,7 @@ const BLOCKED_CODES = new Set([
|
|
|
25
27
|
'ENTROROVE_GIT_UNAVAILABLE',
|
|
26
28
|
'ENTROROVE_MUTATE_NOT_ACCEPTED', 'ENTROROVE_MUTATION_AUTHORITY_REQUIRED',
|
|
27
29
|
'ENTROROVE_MUTATION_PASS_INVALID', 'ENTROROVE_MUTATION_PASS_EXPIRED',
|
|
30
|
+
'ENTROROVE_MUTATION_DELEGATION_CHANGE', 'ENTROROVE_MUTATION_DELEGATION_INVALID',
|
|
28
31
|
'ENTROROVE_BUDGET_LEASE_EXPIRED', 'ENTROROVE_BUDGET_AUTHORITY_REQUIRED',
|
|
29
32
|
'ENTROROVE_SWARM_AUTHORITY_INVALID', 'ENTROROVE_SWARM_CLOUD_AUTHORITY_REQUIRED',
|
|
30
33
|
'ENTROROVE_HANDOFF_FENCE', 'ENTROROVE_HANDOFF_WRITE_REVOKED',
|
package/local-execution.mjs
CHANGED
|
@@ -277,7 +277,7 @@ export async function finalGate(input) {
|
|
|
277
277
|
}
|
|
278
278
|
|
|
279
279
|
const LOCAL_OPERATION_GROUPS = Object.freeze(['core', 'budget', 'scope', 'merge', 'chain', 'handoff', 'archguard', 'calctool', 'swarm', 'validator', 'formula', 'evidence']);
|
|
280
|
-
const PASS_REQUIRED_OPERATIONS = new Set(['mutation-snapshot-attest', 'mutate-gate', 'gate-issue', 'gate-verify', 'guarded-write',
|
|
280
|
+
const PASS_REQUIRED_OPERATIONS = new Set(['mutation-snapshot-attest', 'mutation-consumption-attest', 'mutate-gate', 'gate-issue', 'gate-verify', 'guarded-write',
|
|
281
281
|
'discipline-review', 'discipline-check', 'guarded-delete', 'guarded-move',
|
|
282
282
|
'merge-verified', 'rollback', 'archguard-contract-create', 'archguard-contract-update', 'archguard-checkpoint', 'archguard-scope-request',
|
|
283
283
|
'budget-handoff', 'budget-transition', 'spawn-bind', 'peer-complete', 'complete']);
|
|
@@ -14,6 +14,8 @@ import { withSwarmMutationLease } from './local-swarm-mutation.mjs';
|
|
|
14
14
|
import { disciplineWritePreflight } from './local-discipline-gate.mjs';
|
|
15
15
|
import { readDisciplineSnapshot } from './local-discipline-snapshot.mjs';
|
|
16
16
|
import { assertDisciplineRepository } from './local-discipline-repository.mjs';
|
|
17
|
+
import { withPreservedMutationFence } from './local-preserved-mutation-state.mjs';
|
|
18
|
+
import { assertPreservedMutationProtection } from './local-preserved-mutation-protection.mjs';
|
|
17
19
|
export { beginMutationBatch14, finishMutationBatch14, failMutationBatch14 } from './local-mutation-batch.mjs';
|
|
18
20
|
|
|
19
21
|
const SCHEMA = 'entrorove.local-execution/1.0';
|
|
@@ -25,7 +27,8 @@ export async function mutateGate14(input) {
|
|
|
25
27
|
return withMutationAudit14(input, 'mutate-gate', async () => {
|
|
26
28
|
const pass = suppliedPass(input), binding = await mutationOriginalBinding14(input, pass);
|
|
27
29
|
const snapshot = await mutationSnapshotBinding14(input, pass, { allSources: true });
|
|
28
|
-
assertTaskHandoffSnapshotBinding(binding.state, pass.snapshotReceipt);
|
|
30
|
+
if (!pass.writeDelegation) assertTaskHandoffSnapshotBinding(binding.state, pass.snapshotReceipt);
|
|
31
|
+
else await assertPreservedMutationProtection(input, binding.root, pass);
|
|
29
32
|
return { schemaVersion: SCHEMA, status: 'verified', allowed: true, snapshotId: snapshot.manifest.snapshotId,
|
|
30
33
|
receiptDigest: pass.snapshotReceiptDigest, paths: [...pass.paths], coordinationRequired: false };
|
|
31
34
|
});
|
|
@@ -33,6 +36,18 @@ export async function mutateGate14(input) {
|
|
|
33
36
|
export async function issueMutationPass14(input) {
|
|
34
37
|
return withMutationAudit14(input, 'mutation-pass-issue', async () => {
|
|
35
38
|
const pass = suppliedPass(input);
|
|
39
|
+
if (pass.writeDelegation) {
|
|
40
|
+
const binding = await mutationOriginalBinding14(input, pass);
|
|
41
|
+
await mutationSnapshotBinding14(input, pass, { allSources: true });
|
|
42
|
+
await assertPreservedMutationProtection(input, binding.root, pass);
|
|
43
|
+
mutationLifetime14(pass);
|
|
44
|
+
const locations = await installMutationPass14(input, binding, pass);
|
|
45
|
+
await withPreservedMutationFence(input, pass, async () => {
|
|
46
|
+
await assertPreservedMutationProtection(input, binding.root, pass);
|
|
47
|
+
await auditMutation14(input, 'mutation-pass-issued', { passId: pass.passId, leaseId: pass.leaseId, generation: pass.generation });
|
|
48
|
+
}, { issue: true });
|
|
49
|
+
return { schemaVersion: SCHEMA, gatePassPath: locations.gatePassPath, pass };
|
|
50
|
+
}
|
|
36
51
|
return withTaskHandoffIssueFence(input, async state => {
|
|
37
52
|
const binding = await mutationOriginalBinding14(input, pass, state);
|
|
38
53
|
await mutationSnapshotBinding14(input, pass, { allSources: true });
|
|
@@ -50,7 +65,8 @@ export async function checkedPass(input, state, { allowTargetDrift = false } = {
|
|
|
50
65
|
if (!pass.paths.includes(target)) mutationFail14('ENTROROVE_MUTATION_WRITE_SCOPE', 'Guarded writing is outside the cloud pass path set');
|
|
51
66
|
const binding = await mutationOriginalBinding14(input, pass, state);
|
|
52
67
|
const snapshot = await mutationSnapshotBinding14(input, pass, { targetPath: target, allowTargetDrift });
|
|
53
|
-
assertTaskHandoffSnapshotBinding(binding.state, pass.snapshotReceipt);
|
|
68
|
+
if (!pass.writeDelegation) assertTaskHandoffSnapshotBinding(binding.state, pass.snapshotReceipt);
|
|
69
|
+
else await assertPreservedMutationProtection(input, binding.root, pass);
|
|
54
70
|
const locations = await mutationPassLocations14(input, binding, pass);
|
|
55
71
|
const ledger = await readMutationConsumption14(pass, locations);
|
|
56
72
|
const consumedWrite = ledger.writes[target];
|
|
@@ -92,9 +108,10 @@ async function performWrite(input, state, value) {
|
|
|
92
108
|
disciplineReviewDigest: discipline.reviewDigest, declaredKind: discipline.declaredKind });
|
|
93
109
|
mutationLifetime14(pass);
|
|
94
110
|
const written = await writeMutationProjectFile14(input, binding.root, target, content, baseline);
|
|
95
|
-
const completed = { ...ledger, writes: { ...ledger.writes, [target]: { ...entry, status: 'written' } } };
|
|
111
|
+
const completed = { ...ledger, writes: { ...ledger.writes, [target]: { ...entry, status: 'written', writtenAt: new Date().toISOString() } } };
|
|
96
112
|
await saveMutationConsumption14(locations, completed);
|
|
97
113
|
await assertDisciplineRepository(input, binding.root, await readDisciplineSnapshot(snapshot), completed);
|
|
114
|
+
if (pass.writeDelegation) await assertPreservedMutationProtection(input, binding.root, pass);
|
|
98
115
|
await auditMutation14(input, 'guarded-write', { passId: pass.passId, sha256: written.sha256 });
|
|
99
116
|
const stateRoot = await mutationStateRoot14(input);
|
|
100
117
|
await privateMutationDirectory14(path.join(stateRoot, 'ledgers', pass.repositoryDigest), { privateFrom: stateRoot });
|
|
@@ -106,7 +123,8 @@ async function performWrite(input, state, value) {
|
|
|
106
123
|
export async function guardedWrite14(input) {
|
|
107
124
|
return withMutationAudit14(input, 'guarded-write', async () => {
|
|
108
125
|
const value = await checkedPass(input), pass = value.pass;
|
|
109
|
-
return withStateLock(value.locations.consumptionPath, () =>
|
|
110
|
-
|
|
126
|
+
return withStateLock(value.locations.consumptionPath, () => pass.writeDelegation
|
|
127
|
+
? withPreservedMutationFence(input, pass, state => performWrite(input, state, value))
|
|
128
|
+
: withTaskHandoffWriteFence({ ...input, epoch: pass.epoch, passId: pass.passId, leaseId: pass.leaseId }, state => performWrite(input, state, value)));
|
|
111
129
|
});
|
|
112
130
|
}
|
package/local-mutation-batch.mjs
CHANGED
|
@@ -41,6 +41,7 @@ function tokenState(token, permitted) {
|
|
|
41
41
|
}
|
|
42
42
|
async function authority(input, supplied) {
|
|
43
43
|
const pass = await readStoredMutationPass14(input);
|
|
44
|
+
if (pass.writeDelegation) mutationFail14('ENTROROVE_MUTATION_DELEGATION_CHANGE', 'Preserved mutation requires individual existing-file modification');
|
|
44
45
|
for (const value of [supplied, input.mutationPass]) if (value !== undefined && canonical(mutationPass14(value)) !== canonical(pass)) {
|
|
45
46
|
mutationFail14('ENTROROVE_MUTATION_PASS_CHANGED', 'Batch cannot replace its stored original signed pass');
|
|
46
47
|
}
|