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