@entrorove/skill 0.1.52 → 0.1.54

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.
Files changed (50) hide show
  1. package/CAPABILITY-MAP.md +14 -2
  2. package/CHANGELOG.md +12 -0
  3. package/DELIVERY-BOUNDARIES.md +4 -2
  4. package/IDE-EXECUTION.md +25 -3
  5. package/SKILL.md +8 -4
  6. package/cli.mjs +4 -6
  7. package/cloud-mutation-authority.mjs +96 -0
  8. package/connected-cli.mjs +20 -0
  9. package/ide-code-mode.mjs +30 -8
  10. package/ide-command.mjs +27 -1
  11. package/ide-hook.mjs +70 -17
  12. package/ide-results.mjs +8 -6
  13. package/ide-session.mjs +44 -1
  14. package/local-approved-plan.mjs +3 -21
  15. package/local-approved-process.mjs +54 -0
  16. package/local-approved-supervisor.mjs +99 -0
  17. package/local-cli.mjs +13 -2
  18. package/local-contracts.mjs +2 -0
  19. package/local-delegated-modify-analysis.mjs +232 -0
  20. package/local-discipline-actions.mjs +2 -0
  21. package/local-discipline-gate.mjs +3 -1
  22. package/local-error.mjs +3 -0
  23. package/local-execution.mjs +1 -1
  24. package/local-mutation-authority.mjs +23 -5
  25. package/local-mutation-batch.mjs +1 -0
  26. package/local-mutation-binding.mjs +14 -5
  27. package/local-mutation-consumption.mjs +93 -0
  28. package/local-mutation-pass-store.mjs +6 -4
  29. package/local-mutation-wire.mjs +21 -4
  30. package/local-preserved-mutation-protection.mjs +51 -0
  31. package/local-preserved-mutation-signer.mjs +81 -0
  32. package/local-preserved-mutation-state.mjs +96 -0
  33. package/local-preserved-mutation-wire.mjs +49 -0
  34. package/local-scope-create-require.mjs +142 -0
  35. package/local-scope-import-expressions.mjs +2 -2
  36. package/local-scope-repository.mjs +42 -15
  37. package/local-scope-require-resolution.mjs +78 -0
  38. package/manifest.json +1 -1
  39. package/mutation-client.mjs +290 -0
  40. package/package.json +21 -2
  41. package/release-state.json +48 -29
  42. package/release.mjs +29 -0
  43. package/releases.json +12 -0
  44. package/remote-client.mjs +2 -1
  45. package/workflow-observation-client.mjs +44 -0
  46. package/workflow-observation-connection.mjs +35 -0
  47. package/workflow-observation-remote.mjs +84 -0
  48. package/workflow-observation-runner.mjs +87 -0
  49. package/workflow-observation-state.mjs +36 -0
  50. package/workflow-observation-wire.mjs +69 -0
@@ -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',
@@ -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, () => withTaskHandoffWriteFence({ ...input,
110
- epoch: pass.epoch, passId: pass.passId, leaseId: pass.leaseId }, state => performWrite(input, state, value)));
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
  }
@@ -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
  }
@@ -7,8 +7,12 @@ import { mutationFail14, mutationHome14, mutationRoot14, mutationStateRoot14 } f
7
7
  import { snapshotAuthority14, snapshotVerify14, verifySnapshotSources14 } from './local-mutation-snapshot.mjs';
8
8
  import { mutationChallenge14, mutationPass14 } from './local-mutation-wire.mjs';
9
9
  import { withMutationAudit14 } from './local-mutation-audit.mjs';
10
+ import { preservedMutationOriginalBinding, withPreservedMutationFence } from './local-preserved-mutation-state.mjs';
11
+ import { loadPreservedMutationSigner } from './local-preserved-mutation-signer.mjs';
12
+ import { capturePreservedMutationProtection } from './local-preserved-mutation-protection.mjs';
10
13
 
11
14
  export async function mutationOriginalBinding14(input, authority, state) {
15
+ if (authority.writeDelegation) return preservedMutationOriginalBinding(input, authority);
12
16
  const root = await mutationRoot14(input.repositoryRoot), home = await mutationHome14(input.home);
13
17
  await mutationStateRoot14(input);
14
18
  const ctx = await context({ ...input, repositoryRoot: root });
@@ -67,23 +71,28 @@ export async function attestMutationSnapshot14(input) {
67
71
  if (input.snapshotId !== challenge.snapshotId) mutationFail14('ENTROROVE_MUTATION_SNAPSHOT_BINDING', 'Snapshot attestation requires the cloud-assigned original snapshot identifier');
68
72
  const verified = await snapshotVerify14(input);
69
73
  const snapshot = await snapshotAuthority14(input, binding.root, verified.receipt);
70
- assertTaskHandoffSnapshotBinding(binding.state, verified.receipt);
74
+ if (!challenge.writeDelegation) assertTaskHandoffSnapshotBinding(binding.state, verified.receipt);
71
75
  if (snapshot.manifest.contractDigest !== challenge.scopeContractDigest
72
76
  || canonical(snapshot.manifest.paths) !== canonical(challenge.paths)
73
77
  || snapshot.manifest.files.some(file => !file.exists && !challenge.newFilePaths.includes(file.path))) {
74
78
  mutationFail14('ENTROROVE_MUTATION_SNAPSHOT_BINDING', 'Snapshot attestation differs from its original cloud scope challenge');
75
79
  }
76
- const signer = await loadTaskHandoffSigner(binding.context, input.signerConfigPath, challenge.runnerKeyId);
80
+ const signer = challenge.writeDelegation
81
+ ? await loadPreservedMutationSigner(input, input.signerConfigPath, challenge.writeDelegation.mutationKeyId)
82
+ : await loadTaskHandoffSigner(binding.context, input.signerConfigPath, challenge.runnerKeyId);
77
83
  const now = Date.now(), expires = Math.min(now + signer.config.receiptTtlMs, Date.parse(challenge.expiresAt));
78
84
  if (expires <= now) mutationFail14('ENTROROVE_MUTATION_AUTHORITY_EXPIRED', 'Snapshot challenge expired before runner attestation');
79
85
  const payload = { schemaVersion: 'entrorove.mutation-snapshot-attestation/1.0', challengeId: challenge.challengeId,
80
86
  challengeDigest: sha256(canonical(challenge)), nonce: challenge.nonce, runnerId: challenge.runnerId,
81
- keyId: challenge.runnerKeyId, flowId: challenge.flowId, taskId: challenge.taskId, ownerId: challenge.ownerId,
87
+ keyId: signer.config.keyId, flowId: challenge.flowId, taskId: challenge.taskId, ownerId: challenge.ownerId,
82
88
  stepId: challenge.stepId, leaseId: challenge.leaseId, generation: challenge.generation,
83
89
  repositoryId: challenge.repositoryId, repositoryDigest: challenge.repositoryDigest, snapshotId: challenge.snapshotId,
84
90
  paths: [...challenge.paths], manifest: snapshot.manifest, receipt: verified.receipt,
85
91
  issuedAt: new Date(now).toISOString(), expiresAt: new Date(expires).toISOString() };
86
- return { schemaVersion: 'entrorove.local-execution/1.0', verified: true,
87
- attestation: { ...payload, signature: sign(null, Buffer.from(canonical(payload)), signer.privateKey).toString('base64url') } };
92
+ const output = () => ({ schemaVersion: 'entrorove.local-execution/1.0', verified: true,
93
+ attestation: { ...payload, signature: sign(null, Buffer.from(canonical(payload)), signer.privateKey).toString('base64url') } });
94
+ if (!challenge.writeDelegation) return output();
95
+ const protectedFiles = await capturePreservedMutationProtection(input, binding.root, challenge, snapshot);
96
+ return withPreservedMutationFence(input, challenge, output, { attest: true, protectedFiles });
88
97
  });
89
98
  }
@@ -0,0 +1,93 @@
1
+ import path from 'node:path';
2
+ import { randomUUID, sign, verify } from 'node:crypto';
3
+ import { canonical, sha256 } from './local-integrity.mjs';
4
+ import { withStateLock } from './local-state-lock.mjs';
5
+ import { mutationFail14, mutationRead14, readMutationJson14, atomicMutationJson14, privateMutationDirectory14 } from './local-mutation-files.mjs';
6
+ import { readStoredMutationPass14, readMutationConsumption14, mutationPassLocations14 } from './local-mutation-pass-store.mjs';
7
+ import { assertPreservedMutationIssued } from './local-preserved-mutation-state.mjs';
8
+ import { loadPreservedMutationSigner } from './local-preserved-mutation-signer.mjs';
9
+ import { preservedMutationPublicKey } from './local-preserved-mutation-wire.mjs';
10
+ import { assertPreservedMutationProtection } from './local-preserved-mutation-protection.mjs';
11
+ import { snapshotAuthority14, readSnapshotCopy14 } from './local-mutation-snapshot.mjs';
12
+ import { assertDelegatedSourceModification } from './local-delegated-modify-analysis.mjs';
13
+ import { assertDisciplineRepository } from './local-discipline-repository.mjs';
14
+ import { readDisciplineSnapshot } from './local-discipline-snapshot.mjs';
15
+
16
+ const fail = () => mutationFail14('ENTROROVE_MUTATION_CONSUMPTION_INVALID', 'Mutation consumption requires its actual completed original writes, unchanged protection and separate M signer');
17
+ const iso = value => typeof value === 'string' && Number.isFinite(Date.parse(value)) && new Date(value).toISOString() === value;
18
+ export async function preservedMutationConsumptionState(input) {
19
+ const pass = await readStoredMutationPass14(input, { live: false });
20
+ if (!pass.writeDelegation) fail();
21
+ const binding = await assertPreservedMutationIssued(input, pass), locations = await mutationPassLocations14(input, binding, pass);
22
+ const ledger = await readMutationConsumption14(pass, locations);
23
+ return { pass, binding, locations, ledger, complete: pass.paths.every(target => ledger.writes[target]?.status === 'written') };
24
+ }
25
+ async function actualWrites(input, state) {
26
+ const { pass, binding, ledger } = state;
27
+ if (!state.complete || Object.keys(ledger.writes).length !== pass.paths.length) fail();
28
+ const snapshot = await snapshotAuthority14(input, binding.root, pass.snapshotReceipt);
29
+ if (sha256(canonical(snapshot.manifest)) !== pass.snapshotManifestDigest || snapshot.manifest.snapshotDigest !== pass.snapshotDigest) fail();
30
+ const writes = [];
31
+ for (const target of pass.paths) {
32
+ const completed = ledger.writes[target], before = snapshot.manifest.files.find(file => file.path === target);
33
+ if (!iso(completed.writtenAt)) mutationFail14('ENTROROVE_MUTATION_CONSUMPTION_WRITE_TIME', 'Original write has no verifiable completion timestamp; its old receipt cannot authorize another mutation');
34
+ if (!before?.exists || completed.kind && completed.kind !== 'modify' || !iso(completed.at)
35
+ || Date.parse(completed.writtenAt) < Date.parse(pass.issuedAt) || Date.parse(completed.writtenAt) >= Date.parse(pass.expiresAt)
36
+ || Date.parse(completed.writtenAt) < Date.parse(completed.at) || Date.parse(completed.writtenAt) > Date.now()) fail();
37
+ const current = await mutationRead14(input, binding.root, target), copy = await readSnapshotCopy14(input, snapshot.snapshotRoot, snapshot.manifest, target);
38
+ if (current.sha256 !== completed.contentDigest || current.mode !== completed.mode || current.mode !== before.mode) fail();
39
+ assertDelegatedSourceModification({ path: target, before: copy.content.toString('utf8'), after: current.content.toString('utf8') });
40
+ writes.push({ path: target, beforeSha256: before.sha256, beforeMode: before.mode,
41
+ afterSha256: current.sha256, afterMode: current.mode, writtenAt: completed.writtenAt });
42
+ }
43
+ await assertDisciplineRepository(input, binding.root, await readDisciplineSnapshot(snapshot), ledger);
44
+ await assertPreservedMutationProtection(input, binding.root, pass);
45
+ return writes;
46
+ }
47
+ function receiptCore(pass, writes) {
48
+ return { schemaVersion: 'entrorove.preserved-mutation-consumption/1.0', purpose: 'preserved-existing-modify-consumption',
49
+ keyId: pass.writeDelegation.mutationKeyId, ownerId: pass.ownerId, taskId: pass.taskId, flowId: pass.flowId, stepId: pass.stepId,
50
+ leaseId: pass.leaseId, generation: pass.generation, epoch: pass.epoch, passId: pass.passId, passDigest: sha256(canonical(pass)),
51
+ challengeId: pass.challengeId, challengeNonce: pass.challengeNonce, passNonce: pass.nonce,
52
+ snapshotId: pass.snapshotId, snapshotDigest: pass.snapshotDigest,
53
+ protectedFilesDigest: sha256(canonical(pass.writeDelegation.protectedFiles)), writes };
54
+ }
55
+ function cachedReceipt(value, pass, core) {
56
+ const { signature, issuedAt, expiresAt, nonce, ...actual } = value;
57
+ const unsigned = { ...actual, issuedAt, expiresAt, nonce }, bytes = typeof signature === 'string' ? Buffer.from(signature, 'base64url') : Buffer.alloc(0);
58
+ if (canonical(actual) !== canonical(core) || !iso(issuedAt) || !iso(expiresAt) || Date.parse(issuedAt) > Date.now()
59
+ || Date.parse(expiresAt) <= Date.parse(issuedAt) || Date.parse(expiresAt) - Date.parse(issuedAt) > 300_000
60
+ || !/^[0-9a-f-]{36}$/.test(nonce) || bytes.length !== 64 || bytes.toString('base64url') !== signature
61
+ || !verify(null, Buffer.from(canonical(unsigned)), preservedMutationPublicKey(pass.writeDelegation.mutationPublicKey).key, bytes)) fail();
62
+ return value;
63
+ }
64
+ async function consumptionReceipt(input, state, { renewExpired = false, allowExpiredReplay = false } = {}) {
65
+ const writes = await actualWrites(input, state), core = receiptCore(state.pass, writes);
66
+ const directory = path.join(state.binding.directory, 'consumption-receipts'), file = path.join(directory, `${state.pass.passId}.json`);
67
+ await privateMutationDirectory14(directory, { privateFrom: state.binding.stateRoot });
68
+ let previous;
69
+ try { previous = cachedReceipt(await readMutationJson14(file), state.pass, core); }
70
+ catch (error) { if (error.code !== 'ENOENT') throw error; }
71
+ if (previous && Date.parse(previous.expiresAt) > Date.now()) return previous;
72
+ if (previous && allowExpiredReplay && !renewExpired) return previous;
73
+ if (previous && !renewExpired) mutationFail14('ENTROROVE_MUTATION_CONSUMPTION_EXPIRED', 'Original M consumption receipt expired; recover any pending request before a proven zero-effect renewal');
74
+ const signer = await loadPreservedMutationSigner(input, state.binding.signerConfigPath, core.keyId), now = Date.now();
75
+ const unsigned = { ...core, issuedAt: new Date(now).toISOString(), expiresAt: new Date(now + 300_000).toISOString(), nonce: randomUUID() };
76
+ const result = { ...unsigned, signature: sign(null, Buffer.from(canonical(unsigned)), signer.privateKey).toString('base64url') };
77
+ if (previous) {
78
+ const archive = `${file}.${previous.nonce}.expired`;
79
+ try { if (canonical(await readMutationJson14(archive)) !== canonical(previous)) fail(); }
80
+ catch (error) { if (error.code !== 'ENOENT') throw error; await atomicMutationJson14(archive, previous, { privateFrom: state.binding.stateRoot }); }
81
+ }
82
+ await atomicMutationJson14(file, result, { privateFrom: state.binding.stateRoot, replace: Boolean(previous) });
83
+ return result;
84
+ }
85
+ export async function attestMutationConsumption14(input, options = {}) {
86
+ const original = await preservedMutationConsumptionState(input);
87
+ return withStateLock(original.locations.consumptionPath, async () => {
88
+ const state = await preservedMutationConsumptionState(input), receipt = await consumptionReceipt(input, state, options);
89
+ return { schemaVersion: 'entrorove.local-execution/1.0', status: 'reported', consumptionReceipt: receipt,
90
+ claimsVerifiedExecution: false, claimsCompletion: false,
91
+ nextAction: { operation: 'mutation-pass-settle', input: { passId: state.pass.passId, consumptionReceipt: receipt } } };
92
+ });
93
+ }
@@ -4,22 +4,24 @@ import { withStateLock } from './local-state-lock.mjs';
4
4
  import { atomicMutationJson14, mutationFail14, mutationRoot14, mutationStateRoot14, mutationTarget14,
5
5
  privateMutationDirectory14, readMutationJson14 } from './local-mutation-files.mjs';
6
6
  import { mutationPass14 } from './local-mutation-wire.mjs';
7
+ import { assertPreservedMutationIssued } from './local-preserved-mutation-state.mjs';
7
8
 
8
9
  const SCHEMA = 'entrorove.mutation-consumption/1.0';
9
- export async function readStoredMutationPass14(input) {
10
+ export async function readStoredMutationPass14(input, { live = true } = {}) {
10
11
  const root = await mutationRoot14(input.repositoryRoot), stateRoot = await mutationStateRoot14(input);
11
12
  if (typeof input.gatePassPath !== 'string' || !path.isAbsolute(input.gatePassPath)) {
12
13
  mutationFail14('ENTROROVE_MUTATION_AUTHORITY_REQUIRED', 'Guarded writing requires its stored original cloud pass');
13
14
  }
14
- const pass = mutationPass14(await readMutationJson14(input.gatePassPath));
15
+ const pass = mutationPass14(await readMutationJson14(input.gatePassPath), { live });
15
16
  const expected = path.join(stateRoot, 'gates', sha256(root).slice(0, 32), input.flowId, `${pass.passId}.json`);
16
17
  if (input.gatePassPath !== expected) mutationFail14('ENTROROVE_MUTATION_PASS_LOCATION', 'Mutation pass is outside its original repository and flow storage');
18
+ if (pass.writeDelegation) await assertPreservedMutationIssued(input, pass);
17
19
  return pass;
18
20
  }
19
21
  export async function mutationPassLocations14(input, binding, pass) {
20
22
  const stateRoot = await mutationStateRoot14(input);
21
23
  const gates = path.join(stateRoot, 'gates', pass.repositoryDigest, pass.flowId);
22
- const anchor = process.env.ENTROROVE_TASK_HANDOFF_STATE_DIRECTORY;
24
+ const anchor = pass.writeDelegation ? binding.directory : process.env.ENTROROVE_TASK_HANDOFF_STATE_DIRECTORY;
23
25
  const consumption = path.join(anchor, 'mutations', pass.repositoryDigest, pass.flowId);
24
26
  await privateMutationDirectory14(gates, { privateFrom: stateRoot });
25
27
  await privateMutationDirectory14(consumption, { privateFrom: anchor });
@@ -28,7 +30,7 @@ export async function mutationPassLocations14(input, binding, pass) {
28
30
  }
29
31
  function initial(pass, locations) {
30
32
  return { schemaVersion: SCHEMA, passId: pass.passId, passDigest: sha256(canonical(pass)), flowId: pass.flowId,
31
- stepId: pass.stepId, executionAttemptNonce: pass.executionAttemptNonce, executionHome: locations.executionHome,
33
+ stepId: pass.stepId, ...(pass.writeDelegation ? { delegationNonce: pass.challengeNonce } : { executionAttemptNonce: pass.executionAttemptNonce }), executionHome: locations.executionHome,
32
34
  paths: [...pass.paths], writes: {} };
33
35
  }
34
36
  export async function readMutationConsumption14(pass, locations) {
@@ -4,6 +4,7 @@ import { canonicalHandoffSignature } from './local-task-handoff-overlay-wire.mjs
4
4
  import { mutationFail14, mutationPaths14 } from './local-mutation-files.mjs';
5
5
  import { disciplineApprovedPolicy } from './local-discipline-contracts.mjs';
6
6
  import { validatorParallelDeclarationsValid } from './runtime/validator-runtime-v7.0.44.mjs';
7
+ import { preservedMutationDelegation } from './local-preserved-mutation-wire.mjs';
7
8
 
8
9
  export const MUTATION_IDENTITY_FIELDS14 = ['flowId', 'taskId', 'ownerId', 'taskDigest', 'chainDigest', 'chainRevision',
9
10
  'acceptanceReceiptId', 'acceptanceDigest', 'routeDigest', 'receiver', 'receiverDigest', 'stepId', 'leaseId', 'leaseDigest',
@@ -38,13 +39,18 @@ function pinnedAuthority() {
38
39
  } catch { mutationFail14('ENTROROVE_MUTATION_AUTHORITY_INVALID', 'Pinned cloud mutation authority is unavailable or invalid'); }
39
40
  }
40
41
  function identity(payload) {
42
+ if (Object.hasOwn(payload, 'previousPassId') && (!ID.test(payload.previousPassId) || payload.previousPassId === payload.passId)) {
43
+ mutationFail14('ENTROROVE_MUTATION_AUTHORITY_INVALID', 'Mutation continuation must retain a distinct original settled predecessor');
44
+ }
41
45
  const ids = ['flowId', 'taskId', 'acceptanceReceiptId', 'stepId', 'leaseId', 'planId', 'executionAuthorityId',
42
46
  'executionAttemptNonce', 'runnerId', 'repositoryId', 'snapshotId', 'nonce', 'challengeId'];
43
47
  const digests = ['taskDigest', 'chainDigest', 'acceptanceDigest', 'routeDigest', 'receiverDigest', 'leaseDigest',
44
48
  'planDigest', 'executionPlanSha256', 'executionAuthorityDigest', 'runnerKeyId', 'scopeBindingDigest',
45
49
  'scopeArtifactDigest', 'scopeContractDigest'];
46
- if (ids.some(field => typeof payload[field] !== 'string' || !ID.test(payload[field]))
47
- || digests.some(field => typeof payload[field] !== 'string' || !SHA.test(payload[field]))
50
+ const delegated = Object.hasOwn(payload, 'writeDelegation');
51
+ const execution = new Set(['executionAuthorityId', 'executionAuthorityDigest', 'executionAttemptNonce']);
52
+ if (ids.filter(field => !delegated || !execution.has(field)).some(field => typeof payload[field] !== 'string' || !ID.test(payload[field]))
53
+ || digests.filter(field => !delegated || !execution.has(field)).some(field => typeof payload[field] !== 'string' || !SHA.test(payload[field]))
48
54
  || !/^[0-9a-f]{32}$/.test(payload.repositoryDigest) || !/^[0-9a-f-]{36}$/.test(payload.ownerId)
49
55
  || !Number.isSafeInteger(payload.epoch) || payload.epoch < 0
50
56
  || ['generation', 'chainRevision'].some(field => !Number.isSafeInteger(payload[field]) || payload[field] < 1)) {
@@ -63,6 +69,7 @@ function identity(payload) {
63
69
  || ['projectId', 'hostId', 'threadId'].some(field => !ID.test(String(payload.receiver[field])))) {
64
70
  mutationFail14('ENTROROVE_MUTATION_AUTHORITY_INVALID', 'Mutation authority lost its original cloud receiver');
65
71
  }
72
+ if (delegated) preservedMutationDelegation(payload);
66
73
  }
67
74
  function authority(value, fields, schema, options) {
68
75
  exact(value, [...fields, 'authorityReceipt']);
@@ -79,12 +86,22 @@ function authority(value, fields, schema, options) {
79
86
  identity(payload); mutationLifetime14(payload, options); return value;
80
87
  }
81
88
  export function mutationChallenge14(value, options) {
82
- return authority(value, CHALLENGE_FIELDS, 'entrorove.mutation-snapshot-challenge/1.0', options);
89
+ return authority(value, authorityFields(value, CHALLENGE_FIELDS), 'entrorove.mutation-snapshot-challenge/1.0', options);
90
+ }
91
+ function authorityFields(value, fields) {
92
+ const original = Object.hasOwn(value ?? {}, 'writeDelegation') ? [...fields.filter(field =>
93
+ !['executionAuthorityId', 'executionAuthorityDigest', 'executionAttemptNonce'].includes(field)), 'writeDelegation'] : fields;
94
+ return [...original, ...['previousPassId'].filter(field => Object.hasOwn(value ?? {}, field))];
83
95
  }
84
96
  export function mutationPass14(value, options) {
85
- const fields = [...PASS_FIELDS, ...['disciplinePolicy', 'parallelPaths'].filter(key => Object.hasOwn(value ?? {}, key))];
97
+ const fields = [...authorityFields(value, PASS_FIELDS), ...['disciplinePolicy', 'parallelPaths'].filter(key => Object.hasOwn(value ?? {}, key))];
86
98
  const pass = authority(value, fields, 'entrorove.mutation-pass/1.0', options);
87
99
  if (pass.disciplinePolicy) disciplineApprovedPolicy(pass.disciplinePolicy);
100
+ if (pass.writeDelegation && (!pass.disciplinePolicy || pass.parallelPaths || pass.disciplinePolicy.dataOperations?.length
101
+ || pass.paths.some(target => { const write = pass.disciplinePolicy.writes.find(item => item.targetPath === target);
102
+ return !write || write.kind !== 'modify' || write.deletedSymbols?.length || write.exceptions?.length; }))) {
103
+ mutationFail14('ENTROROVE_MUTATION_DELEGATION_INVALID', 'Preserved mutation permits only the exact existing modification policy');
104
+ }
88
105
  if (pass.parallelPaths && !validatorParallelDeclarationsValid(pass.parallelPaths)) mutationFail14('ENTROROVE_MUTATION_AUTHORITY_INVALID', 'Parallel paths must match the frozen equivalence contract');
89
106
  if (!ID.test(pass.passId) || !ID.test(pass.challengeNonce) || pass.handoffEpoch !== pass.epoch || pass.coordinationRequired !== false
90
107
  || ['challengeDigest', 'snapshotDigest', 'snapshotReceiptDigest', 'snapshotManifestDigest', 'attestationDigest'].some(field => !SHA.test(pass[field]))) {