@entrorove/skill 0.1.37 → 0.1.39
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 +11 -0
- package/CHANGELOG.md +10 -0
- package/IDE-EXECUTION.md +97 -0
- package/REUSE-DISCIPLINE.md +18 -0
- package/SKILL.md +23 -92
- package/cli.mjs +15 -11
- package/cloud-task-client.mjs +3 -1
- package/ide-code-mode.mjs +38 -0
- package/ide-command.mjs +76 -0
- package/ide-hook.mjs +183 -0
- package/ide-install.mjs +60 -0
- package/ide-presentation.mjs +6 -0
- package/ide-queue.mjs +108 -0
- package/ide-results.mjs +59 -0
- package/ide-session.mjs +86 -0
- package/local-approved-plan.mjs +21 -6
- package/local-archguard-ast.mjs +6 -1
- package/local-archguard-language-ast.mjs +7 -0
- package/local-cli.mjs +6 -1
- package/local-contracts.mjs +4 -1
- package/local-discipline-actions.mjs +71 -0
- package/local-discipline-analysis.mjs +153 -0
- package/local-discipline-batch.mjs +32 -0
- package/local-discipline-contracts.d.mts +6 -0
- package/local-discipline-contracts.mjs +62 -0
- package/local-discipline-gate.mjs +85 -0
- package/local-discipline-native.mjs +33 -0
- package/local-discipline-operations.mjs +39 -0
- package/local-discipline-repository.mjs +87 -0
- package/local-discipline-snapshot.mjs +29 -0
- package/local-equivalence-runner.mjs +33 -0
- package/local-error.mjs +10 -0
- package/local-execution.mjs +1 -0
- package/local-integrity.d.mts +5 -0
- package/local-integrity.mjs +4 -0
- package/local-merge-controller.mjs +1 -0
- package/local-merge-rollback.mjs +2 -1
- package/local-mutation-authority.mjs +16 -7
- package/local-mutation-batch.mjs +7 -3
- package/local-mutation-files.mjs +22 -0
- package/local-mutation-snapshot.mjs +6 -3
- package/local-mutation-wire.mjs +6 -1
- package/local-runner-discipline.mjs +23 -0
- package/local-scope-repository.mjs +16 -0
- package/local-task-handoff-write-receipts.mjs +11 -0
- package/manifest.json +1 -1
- package/package.json +34 -4
- package/release-state.json +56 -28
- package/release.mjs +3 -1
- package/releases.json +12 -0
- package/remote-client.mjs +8 -0
- package/reuse-discipline-release.mjs +18 -0
- package/runtime/validator-runtime-v7.0.44.d.mts +3 -0
- package/runtime/validator-runtime-v7.0.44.mjs +1362 -0
- package/snapshot-cleanup-journal.mjs +32 -0
- package/snapshot-retention.mjs +95 -0
- package/task-entry-client.mjs +2 -2
- package/validator-runtime-build.mjs +16 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { canonical, sha256 } from './local-integrity.mjs';
|
|
4
|
+
import { mutationRead14, mutationPath14, mutationTarget14, mutationFail14 } from './local-mutation-files.mjs';
|
|
5
|
+
import { analyzeDisciplineSources } from './local-discipline-analysis.mjs';
|
|
6
|
+
import { importGraph, scopeCandidateImports } from './local-scope-repository.mjs';
|
|
7
|
+
import { nativeDisciplineDefinitions } from './local-discipline-native.mjs';
|
|
8
|
+
|
|
9
|
+
const script = /\.(?:[cm]?[jt]sx?|php|py|java|rs|vue|svelte|css)$/i;
|
|
10
|
+
const excluded = new Set(['.git', 'node_modules']);
|
|
11
|
+
async function inventory(root, prefix = '') {
|
|
12
|
+
const entries = await fs.readdir(path.join(root, prefix), { withFileTypes: true }), files = [];
|
|
13
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
14
|
+
if (excluded.has(entry.name)) continue;
|
|
15
|
+
const relative = mutationPath14(prefix ? `${prefix}/${entry.name}` : entry.name);
|
|
16
|
+
if (entry.isDirectory()) files.push(...await inventory(root, relative));
|
|
17
|
+
else if (entry.isFile() || entry.isSymbolicLink()) files.push(relative);
|
|
18
|
+
else mutationFail14('ENTROROVE_DISCIPLINE_SCAN_UNKNOWN', 'Repository comparison encountered an unsupported filesystem entry');
|
|
19
|
+
}
|
|
20
|
+
return files.sort();
|
|
21
|
+
}
|
|
22
|
+
export async function disciplineRepositoryObservation(input, root, { analyze = false, overrides = new Map() } = {}) {
|
|
23
|
+
const paths = await inventory(root), files = [], sources = [], unresolved = [];
|
|
24
|
+
for (const relative of paths) {
|
|
25
|
+
const stat = await fs.lstat(path.join(root, relative));
|
|
26
|
+
if (stat.isSymbolicLink()) {
|
|
27
|
+
files.push({ path: relative, sha256: sha256(await fs.readlink(path.join(root, relative))), mode: stat.mode & 0o777, kind: 'symlink' });
|
|
28
|
+
unresolved.push({ path: relative, reason: 'symlink-reference' }); continue;
|
|
29
|
+
}
|
|
30
|
+
const read = await mutationRead14(input, root, relative);
|
|
31
|
+
files.push({ path: relative, sha256: read.sha256, mode: read.mode, kind: 'file' });
|
|
32
|
+
if (analyze && script.test(relative)) sources.push({ path: relative, content: overrides.get(relative) ?? read.content.toString('utf8') });
|
|
33
|
+
}
|
|
34
|
+
if (canonical(paths) !== canonical(await inventory(root))) mutationFail14('ENTROROVE_DISCIPLINE_SCAN_CHANGED', 'Repository inventory changed while observing its original baseline');
|
|
35
|
+
const comparison = { schemaVersion: 'entrorove.repository-comparison/1.0', files,
|
|
36
|
+
scope: 'whole-repository-including-ignored-files', excludedDirectoryNames: [...excluded].sort() };
|
|
37
|
+
if (!analyze) return { comparison };
|
|
38
|
+
// The existing resolver supplies actual aliases, package exports and import edges.
|
|
39
|
+
// Unsupported/dynamic references stay unknown; they are never converted to unused.
|
|
40
|
+
const roots = sources.filter(file => /\.[cm]?[jt]sx?$/i.test(file.path)).map(file => ({ path: file.path, exists: true }));
|
|
41
|
+
for (const [relative, content] of overrides) if (!paths.includes(relative) && script.test(relative)) sources.push({ path: relative, content });
|
|
42
|
+
let graph;
|
|
43
|
+
try { graph = roots.length ? await importGraph(root, roots, input) : { edges: [], dynamicSources: [], complete: true }; }
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (!['ENTROROVE_IMPORT_UNRESOLVED', 'ENTROROVE_IMPORT_INVALID', 'ENTROROVE_IMPORT_UNSUPPORTED', 'ENTROROVE_SCOPE_LIMIT'].includes(error.code)) throw error;
|
|
46
|
+
graph = { edges: [], dynamicSources: [], complete: false }; unresolved.push({ reason: error.code });
|
|
47
|
+
}
|
|
48
|
+
for (const [relative, content] of overrides) if (/\.[cm]?[jt]sx?$/i.test(relative)) {
|
|
49
|
+
try {
|
|
50
|
+
const candidate = await scopeCandidateImports(root, { path: relative, content }, input);
|
|
51
|
+
graph.edges = [...graph.edges.filter(edge => edge.from !== relative), ...candidate.edges];
|
|
52
|
+
if (candidate.dynamic) unresolved.push({ path: relative, reason: 'candidate-dynamic-import' });
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (!['ENTROROVE_IMPORT_UNRESOLVED', 'ENTROROVE_IMPORT_INVALID', 'ENTROROVE_IMPORT_UNSUPPORTED'].includes(error.code)) throw error;
|
|
55
|
+
unresolved.push({ path: relative, reason: error.code });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const analysis = analyzeDisciplineSources(sources, { edges: graph.edges,
|
|
59
|
+
unresolved: [...unresolved, ...graph.dynamicSources.map(item => ({ path: item.path, reason: 'dynamic-import' }))] });
|
|
60
|
+
analysis.definitions.push(...await nativeDisciplineDefinitions(sources));
|
|
61
|
+
return { comparison, analysis, scanComplete: graph.complete && analysis.unresolved.length === 0 };
|
|
62
|
+
}
|
|
63
|
+
export function disciplineRepositoryDifferences(before, after, authorized = new Map()) {
|
|
64
|
+
const left = new Map(before.files.map(file => [file.path, file])), right = new Map(after.files.map(file => [file.path, file]));
|
|
65
|
+
const changed = [];
|
|
66
|
+
for (const relative of [...new Set([...left.keys(), ...right.keys()])].sort()) {
|
|
67
|
+
const expected = authorized.has(relative) ? authorized.get(relative) : left.get(relative) ?? null;
|
|
68
|
+
if (canonical(expected) !== canonical(right.get(relative) ?? null)) changed.push(relative);
|
|
69
|
+
}
|
|
70
|
+
return changed;
|
|
71
|
+
}
|
|
72
|
+
export async function assertDisciplineRepository(input, root, review, ledger) {
|
|
73
|
+
const expected = new Map();
|
|
74
|
+
for (const [relative, write] of Object.entries(ledger.writes ?? {})) {
|
|
75
|
+
if (write.status !== 'written') mutationFail14('ENTROROVE_MUTATION_PASS_CONSUMED', 'Repository comparison found an unresolved physical action');
|
|
76
|
+
if (write.kind === 'delete') expected.set(relative, null);
|
|
77
|
+
else {
|
|
78
|
+
const baseline = review.comparison.files.find(file => file.path === relative);
|
|
79
|
+
expected.set(relative, { path: relative, sha256: write.contentDigest, mode: write.mode ?? baseline?.mode ?? 0o644,
|
|
80
|
+
kind: baseline?.kind ?? 'file' });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const current = await disciplineRepositoryObservation(input, root);
|
|
84
|
+
const changed = disciplineRepositoryDifferences(review.comparison, current.comparison, expected);
|
|
85
|
+
if (changed.length) mutationFail14('ENTROROVE_DISCIPLINE_REPOSITORY_DRIFT', `Whole-repository comparison differs outside completed declared actions: ${changed.join(', ')}`);
|
|
86
|
+
return current.comparison;
|
|
87
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { canonical, sha256 } from './local-integrity.mjs';
|
|
3
|
+
import { atomicMutationJson14, mutationFail14, readMutationJson14 } from './local-mutation-files.mjs';
|
|
4
|
+
import { disciplineRepositoryObservation } from './local-discipline-repository.mjs';
|
|
5
|
+
|
|
6
|
+
export function snapshotDisciplineBinding(value) {
|
|
7
|
+
if (!Object.hasOwn(value, 'disciplineReviewDigest')) return {};
|
|
8
|
+
if (!/^[0-9a-f]{64}$/.test(value.disciplineReviewDigest ?? '')) mutationFail14('ENTROROVE_DISCIPLINE_REVIEW_INVALID', 'Snapshot discipline review digest is invalid');
|
|
9
|
+
return { disciplineReviewDigest: value.disciplineReviewDigest };
|
|
10
|
+
}
|
|
11
|
+
export async function captureDisciplineSnapshot(input, root, location, binding) {
|
|
12
|
+
const observed = await disciplineRepositoryObservation(input, root, { analyze: true });
|
|
13
|
+
const review = { schemaVersion: 'entrorove.discipline-review/1.0', ...binding,
|
|
14
|
+
repositoryDigest: sha256(root).slice(0, 32), createdAt: new Date().toISOString(), ...observed,
|
|
15
|
+
thresholds: { deletionAdditionRatio: null, semanticOverlap: null, directLines: null, usageScopeLimit: null } };
|
|
16
|
+
const digest = sha256(canonical(review));
|
|
17
|
+
await atomicMutationJson14(path.join(location.snapshotRoot, 'discipline-review.json'), review, { privateFrom: location.stateRoot });
|
|
18
|
+
return { disciplineReviewDigest: digest };
|
|
19
|
+
}
|
|
20
|
+
export async function readDisciplineSnapshot(snapshot) {
|
|
21
|
+
const manifest = snapshot.manifest;
|
|
22
|
+
if (!manifest.disciplineReviewDigest) mutationFail14('ENTROROVE_DISCIPLINE_REVIEW_REQUIRED', 'A new write requires its original deterministic repository and usage review; reconnect the original snapshot');
|
|
23
|
+
const review = await readMutationJson14(path.join(snapshot.snapshotRoot, 'discipline-review.json'));
|
|
24
|
+
if (review.schemaVersion !== 'entrorove.discipline-review/1.0' || sha256(canonical(review)) !== manifest.disciplineReviewDigest
|
|
25
|
+
|| ['snapshotId', 'flowId', 'stepId', 'repositoryDigest'].some(key => review[key] !== manifest[key])) {
|
|
26
|
+
mutationFail14('ENTROROVE_DISCIPLINE_REVIEW_INVALID', 'Repository or usage review differs from the original signed snapshot');
|
|
27
|
+
}
|
|
28
|
+
return review;
|
|
29
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { validatorBindEquivalenceResult, validatorParallelDeclarationsValid } from './runtime/validator-runtime-v7.0.44.mjs';
|
|
2
|
+
|
|
3
|
+
export function validateRunnerParallelPaths(plan) {
|
|
4
|
+
const pairs = plan.policy.parallelPaths;
|
|
5
|
+
if (pairs === undefined) return;
|
|
6
|
+
if (!validatorParallelDeclarationsValid(pairs) || pairs.some(pair =>
|
|
7
|
+
![pair.originalPath, pair.candidatePath].every(path => plan.files.some(file => file.path === path))
|
|
8
|
+
|| !plan.files.some(file => file.path === pair.baselinePath && file.sha256 === pair.baselineSha256)
|
|
9
|
+
|| plan.goldenBaseline.source.kind !== 'artifact' || plan.goldenBaseline.source.locator !== pair.baselinePath
|
|
10
|
+
|| plan.goldenBaseline.source.digestSha256 !== pair.baselineSha256)) {
|
|
11
|
+
throw Object.assign(new Error('Parallel paths must bind the original frozen manifest and real prior output baseline'), { code: 'ENTROROVE_EQ_PLAN_INVALID' });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export async function captureRunnerEquivalence(authority, result, readFile) {
|
|
15
|
+
const pairs = authority.plan.policy.parallelPaths;
|
|
16
|
+
if (!pairs) return undefined;
|
|
17
|
+
let output;
|
|
18
|
+
try { output = JSON.parse(result.stdout); } catch { return undefined; }
|
|
19
|
+
if (!Array.isArray(output?.equivalence)) return undefined;
|
|
20
|
+
if (output.equivalence.some(item => !item || Object.keys(item).sort().join('|') !== 'cases|comparisonId')
|
|
21
|
+
|| new Set(output.equivalence.map(item => item.comparisonId)).size !== output.equivalence.length
|
|
22
|
+
|| output.equivalence.some(item => !pairs.some(pair => pair.comparisonId === item.comparisonId))) {
|
|
23
|
+
throw Object.assign(new Error('Actual comparison output contains undeclared identities'), { code: 'ENTROROVE_EQ_OUTPUT_INVALID' });
|
|
24
|
+
}
|
|
25
|
+
const results = [];
|
|
26
|
+
for (const pair of pairs) {
|
|
27
|
+
const actual = output.equivalence.find(item => item.comparisonId === pair.comparisonId);
|
|
28
|
+
if (!actual) continue;
|
|
29
|
+
const baseline = await readFile(authority.root, pair.baselinePath);
|
|
30
|
+
results.push(validatorBindEquivalenceResult(pair, baseline.bytes, actual.cases, authority.plan.goldenBaseline.frozenAt));
|
|
31
|
+
}
|
|
32
|
+
return results;
|
|
33
|
+
}
|
package/local-error.mjs
CHANGED
|
@@ -9,9 +9,19 @@ export const LOCAL_ERROR_CODE_CATALOG = Object.freeze({
|
|
|
9
9
|
ENTROROVE_BUDGET_LEASE_EXPIRED: Object.freeze({ disposition: 'retriable', stability: 'stable' }),
|
|
10
10
|
ENTROROVE_GIT_READER_UNSUPPORTED: Object.freeze({ disposition: 'terminal', stability: 'stable' }),
|
|
11
11
|
ENTROROVE_GIT_UNAVAILABLE: Object.freeze({ disposition: 'needs-human', stability: 'stable' }),
|
|
12
|
+
ENTROROVE_DISCIPLINE_NEEDS_HUMAN: Object.freeze({ disposition: 'needs-human', stability: 'stable' }),
|
|
13
|
+
ENTROROVE_DISCIPLINE_USAGE_UNKNOWN: Object.freeze({ disposition: 'needs-human', stability: 'stable' }),
|
|
14
|
+
ENTROROVE_DISCIPLINE_REVIEW_REQUIRED: Object.freeze({ disposition: 'needs-human', stability: 'stable' }),
|
|
15
|
+
ENTROROVE_DISCIPLINE_DATA_SNAPSHOT_REQUIRED: Object.freeze({ disposition: 'needs-human', stability: 'stable' }),
|
|
16
|
+
ENTROROVE_IDE_GUARD_REQUIRED: Object.freeze({ disposition: 'needs-human', stability: 'stable' }),
|
|
17
|
+
ENTROROVE_IDE_GUARD_UNSUPPORTED: Object.freeze({ disposition: 'terminal', stability: 'stable' }),
|
|
12
18
|
});
|
|
13
19
|
|
|
14
20
|
const BLOCKED_CODES = new Set([
|
|
21
|
+
'ENTROROVE_DISCIPLINE_NEEDS_HUMAN', 'ENTROROVE_DISCIPLINE_USAGE_UNKNOWN', 'ENTROROVE_DISCIPLINE_REVIEW_REQUIRED',
|
|
22
|
+
'ENTROROVE_DISCIPLINE_DATA_SNAPSHOT_REQUIRED', 'ENTROROVE_DISCIPLINE_REPOSITORY_DRIFT', 'ENTROROVE_DISCIPLINE_FORMAT_SCOPE',
|
|
23
|
+
'ENTROROVE_DISCIPLINE_SPECULATIVE', 'ENTROROVE_DISCIPLINE_REVIEW_INVALID',
|
|
24
|
+
'ENTROROVE_IDE_GUARD_REQUIRED', 'ENTROROVE_IDE_GUARD_UNSUPPORTED', 'ENTROROVE_IDE_ACCOUNT',
|
|
15
25
|
'ENTROROVE_GIT_UNAVAILABLE',
|
|
16
26
|
'ENTROROVE_MUTATE_NOT_ACCEPTED', 'ENTROROVE_MUTATION_AUTHORITY_REQUIRED',
|
|
17
27
|
'ENTROROVE_MUTATION_PASS_INVALID', 'ENTROROVE_MUTATION_PASS_EXPIRED',
|
package/local-execution.mjs
CHANGED
|
@@ -277,6 +277,7 @@ export async function finalGate(input) {
|
|
|
277
277
|
|
|
278
278
|
const LOCAL_OPERATION_GROUPS = Object.freeze(['core', 'budget', 'scope', 'merge', 'chain', 'handoff', 'archguard', 'calctool', 'swarm', 'validator', 'formula', 'evidence']);
|
|
279
279
|
const PASS_REQUIRED_OPERATIONS = new Set(['mutation-snapshot-attest', 'mutate-gate', 'gate-issue', 'gate-verify', 'guarded-write',
|
|
280
|
+
'discipline-review', 'discipline-check', 'guarded-delete', 'guarded-move',
|
|
280
281
|
'merge-verified', 'rollback', 'archguard-contract-create', 'archguard-contract-update', 'archguard-checkpoint', 'archguard-scope-request',
|
|
281
282
|
'budget-handoff', 'budget-transition', 'spawn-bind', 'peer-complete', 'complete']);
|
|
282
283
|
function operationGroup(operation) {
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export function canonical(value: unknown): string;
|
|
2
|
+
export function sha256(value: string | Buffer): string;
|
|
3
|
+
export function verifySnapshotManifest(manifest: unknown, binding: {
|
|
4
|
+
snapshotId: string; flowId: string; stepId: string; repositoryDigest: string;
|
|
5
|
+
}): { verified: boolean; reason?: string };
|
package/local-integrity.mjs
CHANGED
|
@@ -48,6 +48,10 @@ export function verifySnapshotManifest(manifest, binding) {
|
|
|
48
48
|
}
|
|
49
49
|
const core = { snapshotId: manifest.snapshotId, flowId: manifest.flowId, stepId: manifest.stepId,
|
|
50
50
|
paths: manifest.paths, files: manifest.files, contractDigest: manifest.contractDigest };
|
|
51
|
+
if (Object.hasOwn(manifest, 'disciplineReviewDigest')) {
|
|
52
|
+
if (!SHA256.test(manifest.disciplineReviewDigest ?? '')) return { verified: false, reason: 'snapshot discipline digest is invalid' };
|
|
53
|
+
core.disciplineReviewDigest = manifest.disciplineReviewDigest;
|
|
54
|
+
}
|
|
51
55
|
try { Object.assign(core, snapshotHandoffBinding(manifest)); }
|
|
52
56
|
catch { return { verified: false, reason: 'snapshot handoff binding is invalid' }; }
|
|
53
57
|
return manifest.snapshotDigest === sha256(canonical(core))
|
|
@@ -85,6 +85,7 @@ async function commit(ctx, input, state, registration, images, assertCurrent) {
|
|
|
85
85
|
token = await bridge.beginMutationBatch14({ ...ctx.input, signerConfigPath: input.runnerSignerConfigPath }, {
|
|
86
86
|
mutationPass: images.mutation.pass, contract: input.contract, contractDigest, transactionId,
|
|
87
87
|
beforeFiles: journal.before.filter(file => input.preparation.paths.includes(file.path)),
|
|
88
|
+
contents: images.afterWide.filter(file => input.preparation.paths.includes(file.path) && file.exists).map(file => ({ path: file.path, contentBase64: file.bytes.toString('base64') })),
|
|
88
89
|
afterFiles: journal.after.filter(file => input.preparation.paths.includes(file.path)) }); },
|
|
89
90
|
progress: async progress => { journal = { ...journal, ...progress }; await saveMergeJournal(ctx, journal); },
|
|
90
91
|
finish: async () => {
|
package/local-merge-rollback.mjs
CHANGED
|
@@ -65,7 +65,8 @@ async function rollbackCommit(ctx, input, state, journal, images, assertCurrent)
|
|
|
65
65
|
latest = await saveMergeJournal(ctx, { ...journal, rollback: attempt });
|
|
66
66
|
token = await bridge.beginMutationBatch14({ ...ctx.input, signerConfigPath: input.runnerSignerConfigPath }, {
|
|
67
67
|
mutationPass: images.mutation.pass, contract: auth, contractDigest: binding.contractDigest, transactionId: auth.transactionId,
|
|
68
|
-
beforeFiles: selected(before), afterFiles: selected(after)
|
|
68
|
+
beforeFiles: selected(before), afterFiles: selected(after),
|
|
69
|
+
contents: after.filter(file => auth.paths.includes(file.path) && file.exists).map(file => ({ path: file.path, contentBase64: file.bytes.toString('base64') })) });
|
|
69
70
|
},
|
|
70
71
|
progress: async progress => { attempt = { ...attempt, ...progress }; latest = await saveMergeJournal(ctx, { ...journal, rollback: attempt }); },
|
|
71
72
|
finish: async () => {
|
|
@@ -4,13 +4,16 @@ import { appendLedgerRecord } from './local-ledger.mjs';
|
|
|
4
4
|
import { withStateLock } from './local-state-lock.mjs';
|
|
5
5
|
import { withTaskHandoffIssueFence, withTaskHandoffWriteFence, assertTaskHandoffSnapshotBinding } from './local-task-handoff-fence.mjs';
|
|
6
6
|
import { mutationFail14, mutationPath14, mutationStateRoot14,
|
|
7
|
-
mutationRead14, privateMutationDirectory14, writeMutationProjectFile14 } from './local-mutation-files.mjs';
|
|
7
|
+
mutationRead14, mutationTarget14, privateMutationDirectory14, writeMutationProjectFile14 } from './local-mutation-files.mjs';
|
|
8
8
|
import { mutationPass14, mutationLifetime14 } from './local-mutation-wire.mjs';
|
|
9
9
|
import { mutationOriginalBinding14, mutationSnapshotBinding14 } from './local-mutation-binding.mjs';
|
|
10
10
|
import { installMutationPass14, mutationPassLocations14, readMutationConsumption14,
|
|
11
11
|
readStoredMutationPass14, saveMutationConsumption14 } from './local-mutation-pass-store.mjs';
|
|
12
12
|
import { auditMutation14, withMutationAudit14 } from './local-mutation-audit.mjs';
|
|
13
13
|
import { withSwarmMutationLease } from './local-swarm-mutation.mjs';
|
|
14
|
+
import { disciplineWritePreflight } from './local-discipline-gate.mjs';
|
|
15
|
+
import { readDisciplineSnapshot } from './local-discipline-snapshot.mjs';
|
|
16
|
+
import { assertDisciplineRepository } from './local-discipline-repository.mjs';
|
|
14
17
|
export { beginMutationBatch14, finishMutationBatch14, failMutationBatch14 } from './local-mutation-batch.mjs';
|
|
15
18
|
|
|
16
19
|
const SCHEMA = 'entrorove.local-execution/1.0';
|
|
@@ -42,7 +45,7 @@ export async function issueMutationPass14(input) {
|
|
|
42
45
|
});
|
|
43
46
|
});
|
|
44
47
|
}
|
|
45
|
-
async function checkedPass(input, state, { allowTargetDrift = false } = {}) {
|
|
48
|
+
export async function checkedPass(input, state, { allowTargetDrift = false } = {}) {
|
|
46
49
|
const pass = await readStoredMutationPass14(input), target = mutationPath14(input.targetPath, 'targetPath');
|
|
47
50
|
if (!pass.paths.includes(target)) mutationFail14('ENTROROVE_MUTATION_WRITE_SCOPE', 'Guarded writing is outside the cloud pass path set');
|
|
48
51
|
const binding = await mutationOriginalBinding14(input, pass, state);
|
|
@@ -53,8 +56,10 @@ async function checkedPass(input, state, { allowTargetDrift = false } = {}) {
|
|
|
53
56
|
const consumedWrite = ledger.writes[target];
|
|
54
57
|
if (allowTargetDrift) {
|
|
55
58
|
if (consumedWrite?.status !== 'written') mutationFail14('ENTROROVE_MUTATION_PASS_CONSUMED', 'Checkpoint requires a completed guarded write');
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
if (consumedWrite.kind !== 'delete') {
|
|
60
|
+
const current = await mutationRead14(input, binding.root, target);
|
|
61
|
+
if (current.sha256 !== consumedWrite.contentDigest) mutationFail14('ENTROROVE_MUTATION_WRITE_CHANGED', 'Checkpoint target differs from its completed guarded write');
|
|
62
|
+
} else if ((await mutationTarget14(binding.root, target, true)).exists) mutationFail14('ENTROROVE_MUTATION_WRITE_CHANGED', 'Deleted checkpoint target was recreated');
|
|
58
63
|
} else if (consumedWrite) {
|
|
59
64
|
mutationFail14('ENTROROVE_MUTATION_PASS_CONSUMED', 'Mutation pass was already consumed or has an unresolved physical write');
|
|
60
65
|
}
|
|
@@ -80,12 +85,16 @@ async function performWrite(input, state, value) {
|
|
|
80
85
|
if (canonical(value.pass) !== canonical(pass)) mutationFail14('ENTROROVE_MUTATION_PASS_CHANGED', 'Mutation pass changed while waiting for its original write lock');
|
|
81
86
|
return withSwarmMutationLease(input, pass, async () => {
|
|
82
87
|
const baseline = snapshot.manifest.files.find(file => file.path === target), content = contentFor(input, baseline);
|
|
83
|
-
const
|
|
88
|
+
const discipline = await disciplineWritePreflight(input, current, content);
|
|
89
|
+
const entry = { status: 'pending', contentDigest: sha256(content), mode: baseline.mode ?? 0o644, at: new Date().toISOString() };
|
|
84
90
|
await saveMutationConsumption14(locations, { ...ledger, writes: { ...ledger.writes, [target]: entry } });
|
|
85
|
-
await auditMutation14(input, 'guarded-write-authorized', { passId: pass.passId, sha256: entry.contentDigest
|
|
91
|
+
await auditMutation14(input, 'guarded-write-authorized', { passId: pass.passId, sha256: entry.contentDigest,
|
|
92
|
+
disciplineReviewDigest: discipline.reviewDigest, declaredKind: discipline.declaredKind });
|
|
86
93
|
mutationLifetime14(pass);
|
|
87
94
|
const written = await writeMutationProjectFile14(input, binding.root, target, content, baseline);
|
|
88
|
-
|
|
95
|
+
const completed = { ...ledger, writes: { ...ledger.writes, [target]: { ...entry, status: 'written' } } };
|
|
96
|
+
await saveMutationConsumption14(locations, completed);
|
|
97
|
+
await assertDisciplineRepository(input, binding.root, await readDisciplineSnapshot(snapshot), completed);
|
|
89
98
|
await auditMutation14(input, 'guarded-write', { passId: pass.passId, sha256: written.sha256 });
|
|
90
99
|
const stateRoot = await mutationStateRoot14(input);
|
|
91
100
|
await privateMutationDirectory14(path.join(stateRoot, 'ledgers', pass.repositoryDigest), { privateFrom: stateRoot });
|
package/local-mutation-batch.mjs
CHANGED
|
@@ -9,6 +9,8 @@ import { readStoredMutationPass14, mutationPassLocations14, readMutationConsumpt
|
|
|
9
9
|
saveMutationConsumption14 } from './local-mutation-pass-store.mjs';
|
|
10
10
|
import { auditMutation14, withMutationAudit14 } from './local-mutation-audit.mjs';
|
|
11
11
|
import { mutationBatchReceiptBinding14, signMutationBatchReceipt14 } from './local-mutation-batch-receipt.mjs';
|
|
12
|
+
import { disciplineBatchBefore } from './local-discipline-batch.mjs';
|
|
13
|
+
import { assertDisciplineRepository } from './local-discipline-repository.mjs';
|
|
12
14
|
|
|
13
15
|
const tokens = new WeakMap(), MISSING = sha256('entrorove.missing-file/1.0');
|
|
14
16
|
function clone(value) {
|
|
@@ -26,7 +28,7 @@ function files(value, paths) {
|
|
|
26
28
|
}
|
|
27
29
|
function options(value) {
|
|
28
30
|
if (!value || typeof value !== 'object' || Array.isArray(value)
|
|
29
|
-
|| Object.keys(value).some(field => !['mutationPass', 'beforeFiles', 'afterFiles', 'contract', 'contractDigest', 'transactionId'].includes(field))) {
|
|
31
|
+
|| Object.keys(value).some(field => !['mutationPass', 'beforeFiles', 'afterFiles', 'contents', 'contract', 'contractDigest', 'transactionId'].includes(field))) {
|
|
30
32
|
mutationFail14('ENTROROVE_MUTATION_BATCH_INPUT', 'Batch begin requires its original pass and exact before and after images');
|
|
31
33
|
}
|
|
32
34
|
return value;
|
|
@@ -47,7 +49,7 @@ async function authority(input, supplied) {
|
|
|
47
49
|
return { pass, binding, snapshot, locations: await mutationPassLocations14(input, binding, pass) };
|
|
48
50
|
}
|
|
49
51
|
function batchEntry(state, file, status = 'pending') {
|
|
50
|
-
return { status, contentDigest: file.sha256, at: state.at, batchId: state.batchId, batchDigest: state.batchDigest,
|
|
52
|
+
return { status, contentDigest: file.sha256, kind: file.exists ? 'modify' : 'delete', mode: file.mode, at: state.at, batchId: state.batchId, batchDigest: state.batchDigest,
|
|
51
53
|
beforeDigest: state.beforeDigest, afterDigest: state.afterDigest };
|
|
52
54
|
}
|
|
53
55
|
async function originalLedger(state) {
|
|
@@ -85,8 +87,9 @@ export async function beginMutationBatch14(rawInput, rawOptions) {
|
|
|
85
87
|
}
|
|
86
88
|
const snapshot = await mutationSnapshotBinding14(input, fresh.pass, { allSources: true });
|
|
87
89
|
if (canonical(before) !== canonical(snapshot.manifest.files)) mutationFail14('ENTROROVE_MUTATION_BATCH_BEFORE', 'Batch before image differs from the real original signed snapshot');
|
|
90
|
+
const review = await disciplineBatchBefore(input, { ...fresh, snapshot }, value, before, after, ledger);
|
|
88
91
|
mutationLifetime14(fresh.pass);
|
|
89
|
-
const state = { ...fresh, ...receiptBinding, input, before, after, status: 'pending', batchId: randomUUID(), at: new Date().toISOString(),
|
|
92
|
+
const state = { ...fresh, ...receiptBinding, input, before, after, review, status: 'pending', batchId: randomUUID(), at: new Date().toISOString(),
|
|
90
93
|
passDigest: sha256(canonical(fresh.pass)), beforeDigest: sha256(canonical(before)), afterDigest: sha256(canonical(after)) };
|
|
91
94
|
state.batchDigest = sha256(canonical({ passDigest: state.passDigest, beforeDigest: state.beforeDigest,
|
|
92
95
|
afterDigest: state.afterDigest, batchId: state.batchId }));
|
|
@@ -114,6 +117,7 @@ export async function finishMutationBatch14(token) {
|
|
|
114
117
|
mutationLifetime14(fresh.pass);
|
|
115
118
|
const writes = { ...ledger.writes };
|
|
116
119
|
for (const file of state.after) writes[file.path] = batchEntry(state, file, 'written');
|
|
120
|
+
await assertDisciplineRepository(state.input, state.binding.root, state.review, { ...ledger, writes });
|
|
117
121
|
await saveMutationConsumption14(state.locations, { ...ledger, writes });
|
|
118
122
|
state.status = 'completed';
|
|
119
123
|
await auditMutation14(state.input, 'mutation-batch-finished', { passId: state.pass.passId, batchId: state.batchId,
|
package/local-mutation-files.mjs
CHANGED
|
@@ -268,3 +268,25 @@ export async function writeMutationProjectFile14(input, root, relative, content,
|
|
|
268
268
|
return { target: published.target, sha256: sha256(content), mode: published.stat.mode & 0o777 };
|
|
269
269
|
} finally { await handle.close(); await cleanupTemporary(temporary, saved); }
|
|
270
270
|
}
|
|
271
|
+
export async function removeMutationProjectFile14(input, root, expected) {
|
|
272
|
+
if (!expected?.exists) mutationFail14('ENTROROVE_DISCIPLINE_BASELINE', 'Deletion requires an existing original snapshot copy');
|
|
273
|
+
const before = await expectedProjectFile(input, root, expected.path, expected);
|
|
274
|
+
await assertAncestors(before, 'ENTROROVE_FILE_CHANGED');
|
|
275
|
+
const latest = await expectedProjectFile(input, root, expected.path, expected);
|
|
276
|
+
if (!sameNode(before.stat, latest.stat)) mutationFail14('ENTROROVE_FILE_CHANGED', 'Deletion target changed before removal');
|
|
277
|
+
await fs.unlink(before.target);
|
|
278
|
+
await assertAncestors(before, 'ENTROROVE_FILE_CHANGED');
|
|
279
|
+
if ((await mutationTarget14(root, expected.path, true)).exists) mutationFail14('ENTROROVE_FILE_CHANGED', 'Deleted file was replaced during removal');
|
|
280
|
+
}
|
|
281
|
+
export async function moveMutationProjectFile14(input, root, source, destination) {
|
|
282
|
+
if (!source?.exists || destination?.exists) mutationFail14('ENTROROVE_DISCIPLINE_BASELINE', 'Move requires an existing original source and an absent planned destination');
|
|
283
|
+
await expectedProjectFile(input, root, source.path, source);
|
|
284
|
+
await projectParents(root, destination.path);
|
|
285
|
+
const before = await expectedProjectFile(input, root, source.path, source), after = await expectedProjectFile(input, root, destination.path, destination);
|
|
286
|
+
await assertAncestors(before, 'ENTROROVE_FILE_CHANGED'); await assertAncestors(after, 'ENTROROVE_FILE_CHANGED');
|
|
287
|
+
await fs.link(before.target, after.target);
|
|
288
|
+
const linked = await mutationTarget14(root, destination.path);
|
|
289
|
+
if (!sameNode(before.stat, linked.stat)) mutationFail14('ENTROROVE_FILE_CHANGED', 'Move destination changed during publication');
|
|
290
|
+
await removeMutationProjectFile14(input, root, source);
|
|
291
|
+
await assertAncestors(after, 'ENTROROVE_FILE_CHANGED');
|
|
292
|
+
}
|
|
@@ -3,6 +3,7 @@ import { promises as fs } from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { canonical, sha256, snapshotHandoffBinding, verifySnapshotManifest } from './local-integrity.mjs';
|
|
5
5
|
import { budgetBindingForRead } from './local-budget-reads.mjs';
|
|
6
|
+
import { captureDisciplineSnapshot, snapshotDisciplineBinding } from './local-discipline-snapshot.mjs';
|
|
6
7
|
import { atomicMutationJson14, cleanupMutationDirectory14, mutationFail14, mutationId14, mutationPaths14,
|
|
7
8
|
mutationPath14, mutationRead14, mutationRoot14, mutationStateRoot14, mutationTarget14, privateMutationDirectory14,
|
|
8
9
|
readMutationJson14, writeMutationFile14 } from './local-mutation-files.mjs';
|
|
@@ -26,7 +27,7 @@ function handoffBinding(value) {
|
|
|
26
27
|
function receiptCore(receipt) {
|
|
27
28
|
return { schemaVersion: RECEIPT_SCHEMA, snapshotId: receipt.snapshotId, flowId: receipt.flowId, stepId: receipt.stepId,
|
|
28
29
|
repositoryDigest: receipt.repositoryDigest, snapshotDigest: receipt.snapshotDigest, contractDigest: receipt.contractDigest ?? null,
|
|
29
|
-
paths: receipt.paths, fileManifestDigest: receipt.fileManifestDigest, ...handoffBinding(receipt), ...layoutBinding(receipt) };
|
|
30
|
+
paths: receipt.paths, fileManifestDigest: receipt.fileManifestDigest, ...handoffBinding(receipt), ...layoutBinding(receipt), ...snapshotDisciplineBinding(receipt) };
|
|
30
31
|
}
|
|
31
32
|
export function loadSnapshotReceipt14(receipt) {
|
|
32
33
|
if (!receipt || typeof receipt !== 'object' || Array.isArray(receipt) || receipt.schemaVersion !== RECEIPT_SCHEMA
|
|
@@ -161,7 +162,8 @@ export async function snapshotPlan14(input, { handoffBinding: binding = {} } = {
|
|
|
161
162
|
await privateMutationDirectory14(path.join(loc.snapshotRoot, 'files'), { privateFrom: loc.stateRoot });
|
|
162
163
|
const files = [];
|
|
163
164
|
for (const relative of paths) files.push(await copySource(input, root, loc.snapshotRoot, relative, loc.stateRoot));
|
|
164
|
-
const
|
|
165
|
+
const discipline = await captureDisciplineSnapshot(input, root, loc, { snapshotId, flowId, stepId });
|
|
166
|
+
const core = { snapshotId, flowId, stepId, paths, files, contractDigest, ...handoff, ...discipline };
|
|
165
167
|
const manifest = { schemaVersion: SNAPSHOT_SCHEMA, ...core, repositoryDigest: repoKey(root), copyLayout: COPY_LAYOUT,
|
|
166
168
|
createdAt: new Date().toISOString(), snapshotDigest: sha256(canonical(core)) };
|
|
167
169
|
await atomicMutationJson14(loc.manifestPath, manifest, { privateFrom: loc.stateRoot });
|
|
@@ -193,7 +195,8 @@ export async function snapshotAuthority14(input, root, value) {
|
|
|
193
195
|
const manifest = checkManifest(await readMutationJson14(loc.manifestPath), root, receipt);
|
|
194
196
|
if (manifest.snapshotDigest !== receipt.snapshotDigest || canonical(manifest.paths) !== canonical(receipt.paths)
|
|
195
197
|
|| manifest.contractDigest !== receipt.contractDigest || canonical(handoffBinding(manifest)) !== canonical(handoffBinding(receipt))
|
|
196
|
-
|| canonical(layoutBinding(manifest)) !== canonical(layoutBinding(receipt))
|
|
198
|
+
|| canonical(layoutBinding(manifest)) !== canonical(layoutBinding(receipt))
|
|
199
|
+
|| canonical(snapshotDisciplineBinding(manifest)) !== canonical(snapshotDisciplineBinding(receipt))) mutationFail14('ENTROROVE_SNAPSHOT_BINDING', 'snapshot receipt does not match its manifest or layout');
|
|
197
200
|
const files = await copyFacts(input, loc.snapshotRoot, manifest);
|
|
198
201
|
if (receipt.fileManifestDigest !== sha256(canonical(files))) mutationFail14('ENTROROVE_SNAPSHOT_BINDING', 'snapshot receipt file manifest digest is invalid');
|
|
199
202
|
return { manifest, manifestPath: loc.manifestPath, snapshotRoot: loc.snapshotRoot, copyRoot: copyRoot(loc.snapshotRoot, manifest) };
|
package/local-mutation-wire.mjs
CHANGED
|
@@ -2,6 +2,8 @@ import { createPublicKey, verify } from 'node:crypto';
|
|
|
2
2
|
import { canonical, sha256 } from './local-integrity.mjs';
|
|
3
3
|
import { canonicalHandoffSignature } from './local-task-handoff-overlay-wire.mjs';
|
|
4
4
|
import { mutationFail14, mutationPaths14 } from './local-mutation-files.mjs';
|
|
5
|
+
import { disciplineApprovedPolicy } from './local-discipline-contracts.mjs';
|
|
6
|
+
import { validatorParallelDeclarationsValid } from './runtime/validator-runtime-v7.0.44.mjs';
|
|
5
7
|
|
|
6
8
|
export const MUTATION_IDENTITY_FIELDS14 = ['flowId', 'taskId', 'ownerId', 'taskDigest', 'chainDigest', 'chainRevision',
|
|
7
9
|
'acceptanceReceiptId', 'acceptanceDigest', 'routeDigest', 'receiver', 'receiverDigest', 'stepId', 'leaseId', 'leaseDigest',
|
|
@@ -80,7 +82,10 @@ export function mutationChallenge14(value, options) {
|
|
|
80
82
|
return authority(value, CHALLENGE_FIELDS, 'entrorove.mutation-snapshot-challenge/1.0', options);
|
|
81
83
|
}
|
|
82
84
|
export function mutationPass14(value, options) {
|
|
83
|
-
const
|
|
85
|
+
const fields = [...PASS_FIELDS, ...['disciplinePolicy', 'parallelPaths'].filter(key => Object.hasOwn(value ?? {}, key))];
|
|
86
|
+
const pass = authority(value, fields, 'entrorove.mutation-pass/1.0', options);
|
|
87
|
+
if (pass.disciplinePolicy) disciplineApprovedPolicy(pass.disciplinePolicy);
|
|
88
|
+
if (pass.parallelPaths && !validatorParallelDeclarationsValid(pass.parallelPaths)) mutationFail14('ENTROROVE_MUTATION_AUTHORITY_INVALID', 'Parallel paths must match the frozen equivalence contract');
|
|
84
89
|
if (!ID.test(pass.passId) || !ID.test(pass.challengeNonce) || pass.handoffEpoch !== pass.epoch || pass.coordinationRequired !== false
|
|
85
90
|
|| ['challengeDigest', 'snapshotDigest', 'snapshotReceiptDigest', 'snapshotManifestDigest', 'attestationDigest'].some(field => !SHA.test(pass[field]))) {
|
|
86
91
|
mutationFail14('ENTROROVE_MUTATION_AUTHORITY_INVALID', 'Mutation pass has invalid snapshot or original epoch bindings');
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { canonical, sha256 } from './local-integrity.mjs';
|
|
2
|
+
import { disciplineRepositoryObservation, disciplineRepositoryDifferences } from './local-discipline-repository.mjs';
|
|
3
|
+
import { atomicMutationJson14, privateMutationDirectory14, mutationStateRoot14 } from './local-mutation-files.mjs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
export async function runnerDisciplineBefore(input, authority) {
|
|
7
|
+
if (!authority.plan.policy.discipline) return null;
|
|
8
|
+
const original = await disciplineRepositoryObservation(input, authority.root);
|
|
9
|
+
return { input, original: original.comparison };
|
|
10
|
+
}
|
|
11
|
+
export async function runnerDisciplineAfter(before, authority) {
|
|
12
|
+
if (!before) return null;
|
|
13
|
+
const after = (await disciplineRepositoryObservation(before.input, authority.root)).comparison;
|
|
14
|
+
const changed = disciplineRepositoryDifferences(before.original, after);
|
|
15
|
+
const report = { schemaVersion: 'entrorove.discipline-runner-diff/1.0', planId: authority.plan.planId,
|
|
16
|
+
comparisonScope: after.scope, originalManifestDigest: sha256(canonical(before.original)), actualManifestDigest: sha256(canonical(after)),
|
|
17
|
+
changedCount: changed.length, changedPaths: changed.slice(0, 256), gates: { repositoryUnchanged: changed.length === 0 } };
|
|
18
|
+
const reportDigest = sha256(canonical(report)), state = await mutationStateRoot14(before.input);
|
|
19
|
+
const directory = await privateMutationDirectory14(path.join(state, 'discipline-reports'));
|
|
20
|
+
await atomicMutationJson14(path.join(directory.target, `${reportDigest}.json`), { ...report, reportDigest,
|
|
21
|
+
original: before.original, actual: after }, { replace: true });
|
|
22
|
+
return { ...report, reportDigest };
|
|
23
|
+
}
|
|
@@ -260,6 +260,22 @@ export async function importGraph(root, originals, input = { repositoryRoot: roo
|
|
|
260
260
|
return { nodes, edges: edges.sort((a, b) => `${a.from}:${a.specifier}`.localeCompare(`${b.from}:${b.specifier}`)),
|
|
261
261
|
dynamic, complete: !dynamic, dynamicSources: dynamicSources.sort((a, b) => a.path.localeCompare(b.path)), workspaceMetadata };
|
|
262
262
|
}
|
|
263
|
+
export async function scopeCandidateImports(root, file, input = { repositoryRoot: root }) {
|
|
264
|
+
const parsed = importSpecifiers(Buffer.from(file.content), file.path);
|
|
265
|
+
const workspace = await workspaceIndex(root, input);
|
|
266
|
+
const context = (input.importContexts ?? []).filter(item => file.path === item.sourcePrefix || file.path.startsWith(`${item.sourcePrefix}/`))
|
|
267
|
+
.sort((a, b) => b.sourcePrefix.length - a.sourcePrefix.length)[0];
|
|
268
|
+
const aliases = await scopeTsconfigImports(async relative => {
|
|
269
|
+
const read = await scopeRead(root, relative, input);
|
|
270
|
+
return { path: relative, sha256: read.sha256, contentBase64: read.content.toString('base64') };
|
|
271
|
+
}, file.path, context?.configPath);
|
|
272
|
+
const declarations = declarationModules(Buffer.from(file.content), file.path), edges = [];
|
|
273
|
+
for (const specifier of parsed.specifiers) {
|
|
274
|
+
const target = await resolveImport(root, file.path, specifier, workspace, input, aliases, context?.configPath, declarations);
|
|
275
|
+
edges.push({ from: file.path, to: target, external: target === null, specifier });
|
|
276
|
+
}
|
|
277
|
+
return { edges, dynamic: parsed.dynamic };
|
|
278
|
+
}
|
|
263
279
|
export async function collectScopeFacts(root, requested, baselineRef, knownFiles = [], input = { repositoryRoot: root }) {
|
|
264
280
|
const selected = await scopeFiles(root, requested, knownFiles); const files = [];
|
|
265
281
|
for (const relative of selected) {
|
|
@@ -6,6 +6,9 @@ import { receiverClaim } from './local-task-handoff-overlay-wire.mjs';
|
|
|
6
6
|
import { checkHandoffWriteConstraints } from './local-task-handoff-write-constraints.mjs';
|
|
7
7
|
import { verifyHandoffWriteArchive, verifyTaskHandoffWriteArchives } from './local-task-handoff-write-archive.mjs';
|
|
8
8
|
import { handoffWriteFail as fail, writeReceiptIdentity, verifyWriteReceipt, verifyWriteNativeResult, assertWriteReceiptFields } from './local-task-handoff-write-wire.mjs';
|
|
9
|
+
import { disciplineWritePreflight } from './local-discipline-gate.mjs';
|
|
10
|
+
import { readDisciplineSnapshot } from './local-discipline-snapshot.mjs';
|
|
11
|
+
import { assertDisciplineRepository } from './local-discipline-repository.mjs';
|
|
9
12
|
|
|
10
13
|
function sourceReadContext(ctx) { return Object.freeze({ home: ctx.home, repositoryRoot: ctx.root, flowId: ctx.flowId }); }
|
|
11
14
|
async function fileObservation(ctx, relative) {
|
|
@@ -50,6 +53,11 @@ export async function runHandoffTrustedWrite({ input, context: ctx, state, persi
|
|
|
50
53
|
const key = await loadTaskHandoffSigner(ctx, process.env.ENTROROVE_TASK_HANDOFF_WRITE_SIGNER_CONFIG, pass.runnerKeyId);
|
|
51
54
|
const content = typeof input.content === 'string' ? input.content : JSON.stringify(input.content);
|
|
52
55
|
if (typeof content !== 'string') fail('actual proposed content must be text or JSON');
|
|
56
|
+
const snapshot = { manifest, snapshotRoot: snapshotDirectory };
|
|
57
|
+
const writes = Object.fromEntries([...verified.files].filter(([name, file]) => file.exists
|
|
58
|
+
&& manifest.files.find(original => original.path === name)?.sha256 !== file.sha256).map(([name, file]) =>
|
|
59
|
+
[name, { status: 'written', contentDigest: file.sha256, mode: manifest.files.find(original => original.path === name)?.mode ?? 0o644 }]));
|
|
60
|
+
await disciplineWritePreflight(input, { pass, snapshot, binding: { root: ctx.root }, ledger: { writes }, target: input.targetPath }, content);
|
|
53
61
|
const last = archive.receipts.at(-1), nonce = randomUUID();
|
|
54
62
|
const pending = { blockId: `${pass.passId}:${nonce}`, path: input.targetPath, beforeSha256: actual.sha256,
|
|
55
63
|
beforeExists: actual.exists, proposedAfterSha256: sha256(content), observedAt: new Date().toISOString(), nonce,
|
|
@@ -92,6 +100,9 @@ export async function runHandoffTrustedWrite({ input, context: ctx, state, persi
|
|
|
92
100
|
const finalScope = new Map(verified.files); finalScope.set(input.targetPath, projected);
|
|
93
101
|
await verifyActualScope(ctx, finalScope);
|
|
94
102
|
const core = receiptCore(archive, ctx, pending, rejected ? 'blocked' : 'retained', after, result, key);
|
|
103
|
+
const review = await readDisciplineSnapshot(snapshot);
|
|
104
|
+
await assertDisciplineRepository(input, ctx.root, review, { writes: rejected ? writes : { ...writes,
|
|
105
|
+
[input.targetPath]: { status: 'written', contentDigest: after.sha256, mode: manifest.files.find(file => file.path === input.targetPath)?.mode ?? 0o644 } } });
|
|
95
106
|
const receipt = { ...core, signature: sign(null, Buffer.from(canonical(core)), key.privateKey).toString('base64url') };
|
|
96
107
|
await saveCompleted(state, ctx, persist, archive, receipt);
|
|
97
108
|
if (rejected) throw rejected;
|
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.39",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"entrorove": "cli.mjs"
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"local-mutation-batch.mjs",
|
|
44
44
|
"local-mutation-batch-receipt.mjs",
|
|
45
45
|
"local-integrity.mjs",
|
|
46
|
+
"local-integrity.d.mts",
|
|
46
47
|
"local-evidence-verification.mjs",
|
|
47
48
|
"local-error.mjs",
|
|
48
49
|
"local-formula.mjs",
|
|
@@ -196,16 +197,45 @@
|
|
|
196
197
|
"local-final-gate16-contract.mjs",
|
|
197
198
|
"local-final-gate16-release.mjs",
|
|
198
199
|
"native-client.mjs",
|
|
199
|
-
"native-notification.mjs"
|
|
200
|
+
"native-notification.mjs",
|
|
201
|
+
"ide-code-mode.mjs",
|
|
202
|
+
"ide-command.mjs",
|
|
203
|
+
"ide-hook.mjs",
|
|
204
|
+
"ide-install.mjs",
|
|
205
|
+
"ide-session.mjs",
|
|
206
|
+
"ide-results.mjs",
|
|
207
|
+
"ide-queue.mjs",
|
|
208
|
+
"local-discipline-actions.mjs",
|
|
209
|
+
"local-discipline-analysis.mjs",
|
|
210
|
+
"local-discipline-batch.mjs",
|
|
211
|
+
"local-discipline-contracts.mjs",
|
|
212
|
+
"local-discipline-gate.mjs",
|
|
213
|
+
"local-discipline-native.mjs",
|
|
214
|
+
"local-discipline-operations.mjs",
|
|
215
|
+
"local-discipline-repository.mjs",
|
|
216
|
+
"local-discipline-snapshot.mjs",
|
|
217
|
+
"local-equivalence-runner.mjs",
|
|
218
|
+
"local-runner-discipline.mjs",
|
|
219
|
+
"snapshot-retention.mjs",
|
|
220
|
+
"snapshot-cleanup-journal.mjs",
|
|
221
|
+
"validator-runtime-build.mjs",
|
|
222
|
+
"local-discipline-contracts.d.mts",
|
|
223
|
+
"runtime/validator-runtime-v7.0.44.mjs",
|
|
224
|
+
"runtime/validator-runtime-v7.0.44.d.mts",
|
|
225
|
+
"IDE-EXECUTION.md",
|
|
226
|
+
"REUSE-DISCIPLINE.md",
|
|
227
|
+
"reuse-discipline-release.mjs"
|
|
200
228
|
],
|
|
201
229
|
"scripts": {
|
|
202
230
|
"test": "node --test --test-concurrency=1 test/*.test.mjs",
|
|
203
231
|
"prepublishOnly": "node release.mjs",
|
|
204
232
|
"prepare:archguard": "node archguard-runtime-build.mjs",
|
|
205
233
|
"check:archguard": "node archguard-runtime-build.mjs --check",
|
|
206
|
-
"prepack": "node archguard-runtime-build.mjs && node calctool-runtime-build.mjs",
|
|
234
|
+
"prepack": "node archguard-runtime-build.mjs && node calctool-runtime-build.mjs && node validator-runtime-build.mjs",
|
|
207
235
|
"prepare:calctool": "node calctool-runtime-build.mjs",
|
|
208
|
-
"check:calctool": "node calctool-runtime-build.mjs --check"
|
|
236
|
+
"check:calctool": "node calctool-runtime-build.mjs --check",
|
|
237
|
+
"prepare:validator": "node validator-runtime-build.mjs",
|
|
238
|
+
"check:validator": "node validator-runtime-build.mjs --check"
|
|
209
239
|
},
|
|
210
240
|
"engines": {
|
|
211
241
|
"node": ">=20"
|