@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
package/ide-hook.mjs
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { realpathSync } from 'node:fs';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import { ideSession, ideRuntime, bindIdeAccount, ideContext, ideDigest, readBreakGlass
|
|
4
|
+
import { ideSession, ideRuntime, bindIdeAccount, ideContext, ideDigest, readBreakGlass,
|
|
5
|
+
ideBlockIdentity, repeatedIdeBlock, observeIdeToolOutcome } from './ide-session.mjs';
|
|
5
6
|
import { isStageTerminal, stageOf } from './local-stage-boundary.mjs';
|
|
6
|
-
import { literalCommand, sdkCommand, readCommand, renderSdkCommand, installerCommand
|
|
7
|
-
|
|
7
|
+
import { literalCommand, sdkCommand, readCommand, renderSdkCommand, installerCommand,
|
|
8
|
+
runtimeHashCommand, runtimeHashRead } from './ide-command.mjs';
|
|
9
|
+
import { prepareIdeCodeMode } from './ide-code-mode.mjs';
|
|
8
10
|
import { pathToFileURL } from 'node:url';
|
|
9
11
|
import path from 'node:path';
|
|
10
12
|
import { parseIdeResults, consumeIdeResults } from './ide-results.mjs';
|
|
@@ -25,7 +27,7 @@ function userPrompt(event) {
|
|
|
25
27
|
}
|
|
26
28
|
return prompt;
|
|
27
29
|
}
|
|
28
|
-
const managementOperations = new Set(['capabilities', 'help', 'start', 'continue', 'task-describe', 'flow-cancel', 'flow-abandon', 'scope-connect', 'status', 'feedback', 'cloud-status', 'cloud-watch', 'queue', 'unlock', 'usage', 'billing', 'recurring']);
|
|
30
|
+
const managementOperations = new Set(['capabilities', 'help', 'start', 'continue', 'task-describe', 'flow-cancel', 'flow-abandon', 'scope-connect', 'mutation-connect', 'mutation-settle', 'mutation-consumption-attest', 'mutation-delegate-prepare', 'status', 'feedback', 'cloud-status', 'cloud-watch', 'queue', 'unlock', 'usage', 'billing', 'recurring']);
|
|
29
31
|
function operationOf(call) {
|
|
30
32
|
if (!call) return null;
|
|
31
33
|
if (call.args[0] === 'invoke') return call.input?.operation ?? null;
|
|
@@ -33,17 +35,39 @@ function operationOf(call) {
|
|
|
33
35
|
return call.args[0] ?? null;
|
|
34
36
|
}
|
|
35
37
|
function isManagementCall(call) { return managementOperations.has(operationOf(call)); }
|
|
36
|
-
function resetBlockStreak(state) { state.blockStreak = 0; delete state.lastBlockKey; }
|
|
37
|
-
function blockedReason(state, event, reason) {
|
|
38
|
-
const key = ideDigest({ tool: event.tool_name, phase: state.phase, reason });
|
|
38
|
+
function resetBlockStreak(state) { state.blockStreak = 0; delete state.lastBlockKey; delete state.lastBlockIdentity; }
|
|
39
|
+
function blockedReason(state, event, reason, detail = {}) {
|
|
40
|
+
const key = ideDigest({ identity: ideBlockIdentity(state), tool: event.tool_name, phase: state.phase, reason });
|
|
39
41
|
const repeated = state.lastBlockKey === key;
|
|
40
42
|
state.blockStreak = repeated ? (state.blockStreak ?? 0) + 1 : 1;
|
|
41
43
|
state.lastBlockKey = key;
|
|
42
|
-
|
|
44
|
+
state.lastBlockIdentity = ideBlockIdentity(state);
|
|
45
|
+
const mechanism = detail.mechanism ?? `机制:${state.stateFile ?? '会话状态文件'} 当前 phase=${state.phase}${state.pendingCall ? `,pendingCall=${state.pendingCall.toolUseId}` : ''};该阶段只放行 Entrorove 管理调用,业务与写操作需先建立原 flow 并取得 nextStep。`;
|
|
43
46
|
const repeat = repeated && state.blockStreak >= 2 ? `同一阻断已出现 ${state.blockStreak} 次,禁止再次检查:说明机制、给出绕过方案并执行绕过。` : '';
|
|
44
|
-
|
|
47
|
+
const recovery = detail.recovery ?? `绕过方案(离线、无网络、无协议、不经 hook,终端直接运行):\n${unlockCommand}\n执行后重试原管理调用;项目写入仍需原任务授权。`;
|
|
48
|
+
return `${reason} ${mechanism} ${repeat}\n${recovery}`;
|
|
49
|
+
}
|
|
50
|
+
const denyBlocked = (state, event, reason, detail) => deny(blockedReason(state, event, reason, detail));
|
|
51
|
+
function runtimeHashAttempt(command, parsed) {
|
|
52
|
+
const name = parsed?.words[0];
|
|
53
|
+
return ['shasum', '/usr/bin/shasum', 'sha256sum', '/usr/bin/sha256sum'].includes(name)
|
|
54
|
+
|| name === '/usr/bin/env' && parsed.words[1] === '-i' && parsed.words[2] === 'PATH=/usr/bin:/bin'
|
|
55
|
+
&& ['/usr/bin/shasum', '/usr/bin/sha256sum'].includes(parsed.words[3])
|
|
56
|
+
|| typeof command === 'string' && /^\s*(?:(?:\/usr\/bin\/)?(?:shasum|sha256sum)|\/usr\/bin\/env\s+-i\s+PATH=\/usr\/bin:\/bin\s+\/usr\/bin\/(?:shasum|sha256sum))(?:\s|$)/u.test(command);
|
|
57
|
+
}
|
|
58
|
+
function safeHashToolInput(input) {
|
|
59
|
+
return input && typeof input === 'object' && Object.keys(input).every(key =>
|
|
60
|
+
['cmd', 'command', 'workdir', 'max_output_tokens', 'yield_time_ms', 'shell', 'login'].includes(key))
|
|
61
|
+
&& (input.shell === undefined || input.shell === '/bin/sh')
|
|
62
|
+
&& (input.login === undefined || input.login === false);
|
|
63
|
+
}
|
|
64
|
+
function denyRuntimeHash(state, event, runtime) {
|
|
65
|
+
const safe = runtimeHashCommand(runtime);
|
|
66
|
+
return denyBlocked(state, event, 'ENTROROVE_IDE_RUNTIME_HASH_DENIED:仅允许读取本次安装所绑定的 Node 可执行文件 SHA-256。', {
|
|
67
|
+
mechanism: `机制:哈希路径必须精确等于受管 runtime.node;拒绝其他文件、额外参数、shell 展开和自定义执行器。当前 phase=${state.phase}。`,
|
|
68
|
+
recovery: `恢复:用固定只读命令重新取得该 Node 文件的哈希:\n${safe ?? '当前平台没有受支持的固定哈希命令;请使用受管安装所绑定的 Node 路径。'}${safe ? `\n代码模式请传静态 tools.exec_command({cmd:${JSON.stringify(safe)},shell:'/bin/sh',login:false})。` : ''}\n本次在 hook 本地预检中拒绝,未发起协议请求,因此没有 requestId。`,
|
|
69
|
+
});
|
|
45
70
|
}
|
|
46
|
-
const denyBlocked = (state, event, reason) => deny(blockedReason(state, event, reason));
|
|
47
71
|
function lifecycleResult(value, depth = 0) {
|
|
48
72
|
if (!value || typeof value !== 'object' || depth > 4) return null;
|
|
49
73
|
if (value.operation === 'flow-cancel' || value.operation === 'flow-abandon') return value;
|
|
@@ -140,11 +164,11 @@ async function beforeTool(event, state, home) {
|
|
|
140
164
|
const observer = discovery || ['status','feedback','cloud-status','configure','agreement','install','queue','flows','usage','billing','recurring'].includes(call.args[0]);
|
|
141
165
|
if (unclassifiedIdeRequirements(state) && !observer && !(unlocked && isManagementCall(call))) return denyBlocked(state, event, 'Entrorove:新需求已保存,先用 queue status 读取并按轻重缓急 route 分类;禁止丢弃新需求或覆盖原任务。');
|
|
142
166
|
if (newStart && !checkQueuedStart(state,call)) return denyBlocked(state, event, 'Entrorove:新任务目标必须与实际进入队列的用户原文一致。');
|
|
143
|
-
if (call.args[0]
|
|
167
|
+
if (['local', 'mutation-connect', 'mutation-settle', 'workflow-observe-step'].includes(call.args[0]) && !discovery && !state.flowId) return denyBlocked(state, event, 'Entrorove:本地执行前必须先受理原任务并绑定账号、flowId 与仓库;不能直接使用其它流程的通行证。');
|
|
144
168
|
if (state.pendingCall && !observer && !(unlocked && isManagementCall(call))) return denyBlocked(state, event, 'Entrorove:原调用尚未收到结束回执;先只读恢复其状态,禁止并发重发。');
|
|
145
169
|
const input = call.args[0] === 'invoke' ? call.input?.input : call.input;
|
|
146
170
|
if (input?.flowId && state.flowId && input.flowId !== state.flowId || input?.taskId && state.taskId && ['cloud-watch','cloud-status'].includes(call.args[0]) && input.taskId !== state.taskId) return denyBlocked(state, event, 'Entrorove:调用与本 IDE 任务的原编号不一致。');
|
|
147
|
-
if (call.args[0]
|
|
171
|
+
if (['scope-connect', 'mutation-connect', 'mutation-settle', 'workflow-observe-step'].includes(call.args[0]) && call.args[1] !== state.repositoryRoot
|
|
148
172
|
|| call.args[0] === 'local' && (input?.repositoryRoot && input.repositoryRoot !== state.repositoryRoot || call.args[2] && call.args[2] !== state.repositoryRoot)) return denyBlocked(state, event, 'Entrorove:调用不能替换本任务的实际仓库。');
|
|
149
173
|
let requestId;
|
|
150
174
|
if (newStart) state.taskGoal = call.args[0] === 'cloud-task' ? call.input?.goal : call.input?.input?.task?.goal;
|
|
@@ -152,7 +176,10 @@ async function beforeTool(event, state, home) {
|
|
|
152
176
|
const { buildInvocation } = await import(pathToFileURL(path.join(path.dirname(runtime.cli), 'remote-client.mjs')).href);
|
|
153
177
|
requestId = buildInvocation(call.input.operation, call.input.input).requestId;
|
|
154
178
|
}
|
|
155
|
-
const pending = { toolUseId: event.tool_use_id, inputDigest: ideDigest(call), command: call.args[0],
|
|
179
|
+
const pending = { toolUseId: event.tool_use_id, inputDigest: ideDigest(call), command: call.args[0],
|
|
180
|
+
operation: operationOf(call), observer, managementOnly: observer || isManagementCall(call)
|
|
181
|
+
&& !['start', 'continue', 'cloud-task'].includes(operationOf(call)) || operationOf(call) === 'task-checkpoint',
|
|
182
|
+
...(requestId ? { requestId } : {}) };
|
|
156
183
|
if (observer) {
|
|
157
184
|
state.readCalls ??= {};
|
|
158
185
|
if (Object.keys(state.readCalls).length >= 32) return denyBlocked(state, event, 'Entrorove:只读恢复尚未返回,请等待原调用。');
|
|
@@ -160,16 +187,32 @@ async function beforeTool(event, state, home) {
|
|
|
160
187
|
} else state.pendingCall = pending;
|
|
161
188
|
const installation = call.args[0] === 'install' && parsed.words.some(word => ['npx','npx.cmd'].includes(word));
|
|
162
189
|
const rewritten = renderSdkCommand(call, runtime, state.sessionId);
|
|
163
|
-
resetBlockStreak(state);
|
|
164
190
|
return output('PreToolUse', { permissionDecision: 'allow', ...(installation ? {} : { updatedInput: { ...event.tool_input, command: rewritten,
|
|
165
191
|
...(Object.hasOwn(event.tool_input ?? {}, 'cmd') ? { cmd: rewritten } : {}) } }), additionalContext: ideContext(state) });
|
|
166
192
|
}
|
|
167
193
|
if (!state.active) return {};
|
|
168
194
|
if (['exec','functions.exec'].includes(event.tool_name)) {
|
|
169
195
|
const current = await ideRuntime(home), code = typeof event.tool_input === 'string' ? event.tool_input : event.tool_input?.code;
|
|
170
|
-
|
|
196
|
+
const prepared = prepareIdeCodeMode(code, current);
|
|
197
|
+
if (prepared.allowed) return output('PreToolUse', { additionalContext: ideContext(state),
|
|
198
|
+
...(prepared.code === code ? {} : { updatedInput: typeof event.tool_input === 'string' ? prepared.code
|
|
199
|
+
: { ...event.tool_input, code: prepared.code } }) });
|
|
200
|
+
if (typeof code === 'string' && /\b(?:shasum|sha256sum)\b/u.test(code)) return denyRuntimeHash(state, event, current);
|
|
201
|
+
return denyBlocked(state, event, 'Entrorove:代码模式含未批准的原生执行或动态工具输入;请使用原任务的受控入口。');
|
|
171
202
|
}
|
|
172
203
|
if (event.tool_name === 'update_goal' && (state.phase === 'setup' || unlocked)) return output('PreToolUse', { additionalContext: ideContext(state) });
|
|
204
|
+
if (isShell) {
|
|
205
|
+
const fixedHash = runtimeHashRead(parsed, runtime);
|
|
206
|
+
if (fixedHash) {
|
|
207
|
+
if (!safeHashToolInput(event.tool_input)) return denyRuntimeHash(state, event, runtime);
|
|
208
|
+
resetBlockStreak(state);
|
|
209
|
+
return output('PreToolUse', { permissionDecision: 'allow', updatedInput: { ...event.tool_input, command: fixedHash,
|
|
210
|
+
...(Object.hasOwn(event.tool_input ?? {}, 'cmd') ? { cmd: fixedHash } : {}),
|
|
211
|
+
...(event.tool_name === 'exec_command' ? { shell: '/bin/sh', login: false } : {}) },
|
|
212
|
+
additionalContext: ideContext(state) });
|
|
213
|
+
}
|
|
214
|
+
if (runtimeHashAttempt(command, parsed)) return denyRuntimeHash(state, event, runtime ?? await ideRuntime(home));
|
|
215
|
+
}
|
|
173
216
|
if (isShell && readCommand(parsed) || readTools.has(event.tool_name)) return output('PreToolUse', { additionalContext: ideContext(state) });
|
|
174
217
|
return denyBlocked(state, event, `Entrorove:已阻止原生 ${event.tool_name ?? 'unknown'} 调用。当前阶段 ${state.phase}${state.taskNumber ? `,云端任务 ${state.taskNumber}` : ''}。写入、删除、移动及不确定的终端操作必须使用原任务的 guarded-write/guarded-delete/guarded-move 或已批准的执行计划。`);
|
|
175
218
|
}
|
|
@@ -178,6 +221,10 @@ export async function runIdeHook(event, { home } = {}) {
|
|
|
178
221
|
const name = event.hook_event_name;
|
|
179
222
|
const selected = name === 'UserPromptSubmit' && selectPrompt.test(userPrompt(event));
|
|
180
223
|
const newSelection = selected && !state.active;
|
|
224
|
+
if (name === 'UserPromptSubmit') {
|
|
225
|
+
state.turnSequence = (Number.isSafeInteger(state.turnSequence) ? state.turnSequence : 0) + 1;
|
|
226
|
+
delete state.stopNoticeTurn;
|
|
227
|
+
}
|
|
181
228
|
if (name === 'PreToolUse') state.enforcement = 'enforced';
|
|
182
229
|
if (name === 'UserPromptSubmit' && exitPrompt.test(userPrompt(event))) {
|
|
183
230
|
if (state.phase !== 'user-exited') state.exitPhase = state.phase;
|
|
@@ -224,16 +271,22 @@ export async function runIdeHook(event, { home } = {}) {
|
|
|
224
271
|
const recoveredOriginal = pendingRead && state.pendingCall?.requestId
|
|
225
272
|
&& values.some(result => (result.output ?? result).requestId === state.pendingCall.requestId);
|
|
226
273
|
const messages = collectResult(state, bound, pending.command === 'local');
|
|
274
|
+
observeIdeToolOutcome(state, bound, pending);
|
|
227
275
|
if (pendingRead) delete state.readCalls[event.tool_use_id];
|
|
228
276
|
if (!pendingRead || recoveredOriginal) delete state.pendingCall;
|
|
229
|
-
const resumed = await advanceIdeQueue(state,home);
|
|
277
|
+
const resumed = state.phase === 'blocked' || repeatedIdeBlock(state) ? '' : await advanceIdeQueue(state,home);
|
|
230
278
|
if (resumed) messages.push(resumed);
|
|
231
279
|
return { ...output(name, { additionalContext: ideContext(state) }), ...(messages.length ? { systemMessage: messages.join('\n') } : {}) };
|
|
232
280
|
}
|
|
233
281
|
if (name==='Stop' && state.active && !(await readBreakGlass(home, state.sessionId))) {
|
|
282
|
+
const turn = state.turnSequence ?? 0;
|
|
283
|
+
if (repeatedIdeBlock(state) || event.stop_hook_active === true || state.stopNoticeTurn === turn) return {};
|
|
234
284
|
const resumed = await advanceIdeQueue(state,home);
|
|
235
285
|
if (resumed || unclassifiedIdeRequirements(state) || unfinishedIdeQueue(state) && !['blocked','needs-input','delivered'].includes(state.phase)
|
|
236
|
-
|| !['ended','blocked','needs-input','setup','delivered'].includes(state.phase))
|
|
286
|
+
|| !['ended','blocked','needs-input','setup','delivered'].includes(state.phase)) {
|
|
287
|
+
state.stopNoticeTurn = turn;
|
|
288
|
+
return {decision:'block',reason:`${ideContext(state)}${ideQueueContext(state)}${resumed}原任务或队列尚未取得结束回执;本轮仅提醒一次继续可运行工作。真实阻断应明确报告并等待修复,目标与队列保持不变。`};
|
|
289
|
+
}
|
|
237
290
|
}
|
|
238
291
|
return state.active && ['SessionStart','UserPromptSubmit','PostCompact','PreCompact'].includes(name) ? output(name, { additionalContext: `${ideContext(state)}${ideQueueContext(state)}` }) : {};
|
|
239
292
|
}, home);
|
package/ide-results.mjs
CHANGED
|
@@ -46,15 +46,17 @@ export async function consumeIdeResults(event, state, pending, home) {
|
|
|
46
46
|
return proof.values;
|
|
47
47
|
}
|
|
48
48
|
export function captureIdeResults() {
|
|
49
|
-
const original = process.stdout.write, chunks = [];
|
|
49
|
+
const original = process.stdout.write, originalError = process.stderr.write, chunks = [], errors = [];
|
|
50
50
|
let size = 0;
|
|
51
|
-
|
|
51
|
+
const capture = (writer, target) => function(chunk, ...rest) {
|
|
52
52
|
size += Buffer.byteLength(chunk);
|
|
53
|
-
if (size <= 8_000_000)
|
|
54
|
-
return
|
|
53
|
+
if (size <= 8_000_000) target.push(Buffer.isBuffer(chunk) ? chunk.toString('utf8') : chunk);
|
|
54
|
+
return writer.call(this, chunk, ...rest);
|
|
55
55
|
};
|
|
56
|
+
process.stdout.write = capture(original, chunks);
|
|
57
|
+
process.stderr.write = capture(originalError, errors);
|
|
56
58
|
return async () => {
|
|
57
|
-
process.stdout.write = original;
|
|
58
|
-
if (size <= 8_000_000) await persistIdeResults(ideResultContext(), parseIdeResults(chunks.join('')));
|
|
59
|
+
process.stdout.write = original; process.stderr.write = originalError;
|
|
60
|
+
if (size <= 8_000_000) await persistIdeResults(ideResultContext(), [...parseIdeResults(chunks.join('')), ...parseIdeResults(errors.join(''))]);
|
|
59
61
|
};
|
|
60
62
|
}
|
package/ide-session.mjs
CHANGED
|
@@ -83,10 +83,53 @@ export async function ideRuntime(home = os.homedir()) {
|
|
|
83
83
|
if (ideDigest(cli.toString('utf8')) !== runtime.cliDigest) throw ideError('ENTROROVE_IDE_RUNTIME', 'Installed CLI changed; reinstall the managed runtime');
|
|
84
84
|
return runtime;
|
|
85
85
|
}
|
|
86
|
+
export const ideBlockIdentity = state => ideDigest({ repositoryRoot: state.repositoryRoot, flowId: state.flowId, taskId: state.taskId });
|
|
87
|
+
export function repeatedIdeBlock(state) {
|
|
88
|
+
const identity = ideBlockIdentity(state);
|
|
89
|
+
return state.executionBlock?.identity === identity && state.executionBlock.count >= 2
|
|
90
|
+
|| state.lastBlockIdentity === identity && state.blockStreak >= 2;
|
|
91
|
+
}
|
|
92
|
+
function outcomeNodes(values) {
|
|
93
|
+
const pending = values.map(value => ({ value, depth: 0 })), nodes = [];
|
|
94
|
+
while (pending.length && nodes.length < 64) {
|
|
95
|
+
const { value, depth } = pending.shift();
|
|
96
|
+
if (!value || typeof value !== 'object' || depth > 4) continue;
|
|
97
|
+
nodes.push(value);
|
|
98
|
+
for (const key of ['output', 'result']) if (value[key]) pending.push({ value: value[key], depth: depth + 1 });
|
|
99
|
+
}
|
|
100
|
+
return nodes;
|
|
101
|
+
}
|
|
102
|
+
export function observeIdeToolOutcome(state, values, pending) {
|
|
103
|
+
if (state.phase === 'ended' || !state.flowId && !state.taskId) { delete state.executionBlock; return; }
|
|
104
|
+
const nodes = outcomeNodes(values), identity = ideBlockIdentity(state);
|
|
105
|
+
const failure = nodes.findLast(value => ['blocked', 'failed', 'incomplete'].includes(value.status)
|
|
106
|
+
|| ['blocked', 'failed'].includes(value.taskStatus));
|
|
107
|
+
const previous = state.executionBlock?.identity === identity ? state.executionBlock : null;
|
|
108
|
+
const management = pending.observer || pending.managementOnly;
|
|
109
|
+
if (failure) {
|
|
110
|
+
const key = ideDigest({ identity, inputDigest: pending.inputDigest });
|
|
111
|
+
// Management observations cannot replace a failed execution's identity or
|
|
112
|
+
// clear its streak. One consumed call counts once, even with two copies.
|
|
113
|
+
if (!management || !previous || previous.key === key) {
|
|
114
|
+
state.executionBlock = { identity, key, operation: pending.operation ?? pending.command,
|
|
115
|
+
count: previous?.key === key ? previous.count + Number(previous.toolUseId !== pending.toolUseId) : 1,
|
|
116
|
+
toolUseId: pending.toolUseId, observedAt: new Date().toISOString() };
|
|
117
|
+
}
|
|
118
|
+
state.phase = 'blocked';
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (management && previous) { state.phase = 'blocked'; return; }
|
|
122
|
+
if (!management && nodes.some(value => ['verified', 'succeeded', 'started', 'reported'].includes(value.status) || value.nextStep)) {
|
|
123
|
+
delete state.executionBlock; state.blockStreak = 0; delete state.lastBlockKey; delete state.lastBlockIdentity;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
86
126
|
export function ideContext(state) {
|
|
87
127
|
const delivered = state.phase === 'delivered' && Number.isSafeInteger(state.deliveredStage)
|
|
88
128
|
? `阶段 ${state.deliveredStage} 已按边界独立交付(终态,不等于失败或阻塞);续跑从下一个边界继续,不重新执行已交付阶段。` : '';
|
|
89
|
-
|
|
129
|
+
const waiting = repeatedIdeBlock(state);
|
|
130
|
+
const action = waiting ? '同一真实阻断已达到两次;本轮允许结束并等待修复,禁止原样第三次重试。原目标、flow 与队列均保留。'
|
|
131
|
+
: '必须读 SKILL.md 并执行返回的 nextStep;禁止在失败后替换执行器。';
|
|
132
|
+
return `${waiting ? 'Entrorove 原任务绑定已保留。' : 'Entrorove 已强制接管本任务。'}${state.taskNumber ? `云端任务 ${state.taskNumber}。` : '尚未获得云端任务编号,不得声称已进入云端。'}${state.flowId ? `继续原 flowId=${state.flowId},` : ''}${state.requestId ? `原 requestId=${state.requestId}。` : ''}当前阶段 ${state.phase}。${delivered}${action}源文件、数据及终端写入必须走原任务的受控入口。`;
|
|
90
133
|
}
|
|
91
134
|
// Verified hosts only: the guard is universal in shape (host label + session
|
|
92
135
|
// id), and each host proves itself through its own adapter callback.
|
package/local-approved-plan.mjs
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { LOCAL_OPERATION_SCHEMAS } from './local-contracts.mjs';
|
|
2
2
|
import { createHash, createPrivateKey, createPublicKey, randomUUID, sign } from 'node:crypto';
|
|
3
|
-
import {
|
|
3
|
+
import { runApprovedProcess } from './local-approved-process.mjs';
|
|
4
4
|
import { promises as fs, constants } from 'node:fs';
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
-
import { performance } from 'node:perf_hooks';
|
|
8
7
|
import { assertBudgetRunnerAllowed } from './local-budget-reads.mjs';
|
|
9
8
|
import { validateRunnerParallelPaths, captureRunnerEquivalence } from './local-equivalence-runner.mjs';
|
|
10
9
|
import { disciplineApprovedPolicy, disciplineDataSnapshotsBound } from './local-discipline-contracts.mjs';
|
|
@@ -87,25 +86,8 @@ async function authority(input) {
|
|
|
87
86
|
return { root, plan, planFile, approval, signing };
|
|
88
87
|
}
|
|
89
88
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const started = performance.now(); const { policy } = authority.plan; const remaining = authority.executionDeadlineAt ? Date.parse(authority.executionDeadlineAt) - Date.now() : policy.timeoutMs;
|
|
93
|
-
if (remaining <= 0) { reject(Object.assign(new Error('workflow execution lease expired before process launch'), { code: 'ENTROROVE_WORKFLOW_EXPIRED' })); return; }
|
|
94
|
-
const child = spawn(policy.executable, policy.args, { cwd: authority.root, env: { ...policy.environment }, shell: false, detached: true, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
95
|
-
const stdout = []; const stderr = []; let bytes = 0; let dropped = 0; let timedOut = false; let terminating = false; let spawned = false; let processStartedAt = null; let spawnError = null; let timer; let killTimer; const stdoutHash = createHash('sha256'); const stderrHash = createHash('sha256');
|
|
96
|
-
const terminate = () => { if (terminating) return; terminating = true; try { process.kill(-child.pid, 'SIGTERM'); } catch {} killTimer = setTimeout(() => { try { process.kill(-child.pid, 'SIGKILL'); } catch {} }, 250); killTimer.unref(); };
|
|
97
|
-
const consume = (target, hash, chunk) => { const data = Buffer.from(chunk); hash.update(data); const keep = Math.max(0, Math.min(data.length, policy.maxOutputBytes - bytes)); if (keep) target.push(data.subarray(0, keep)); bytes += keep; dropped += data.length - keep; if (dropped) terminate(); };
|
|
98
|
-
child.stdout.on('data', chunk => consume(stdout, stdoutHash, chunk)); child.stderr.on('data', chunk => consume(stderr, stderrHash, chunk));
|
|
99
|
-
child.once('spawn', () => { spawned = true; processStartedAt = new Date().toISOString(); }); child.once('error', error => { spawnError = error; terminate(); }); timer = setTimeout(() => { timedOut = true; terminate(); }, Math.min(policy.timeoutMs, remaining));
|
|
100
|
-
child.once('close', (exitCode, signal) => {
|
|
101
|
-
clearTimeout(timer); clearTimeout(killTimer);
|
|
102
|
-
const processResult = { schemaVersion: RUNNER_SCHEMA, status: exitCode === 0 && signal === null && !timedOut && !dropped && !spawnError ? 'succeeded' : 'failed', exitCode, signal, timedOut, outputLimitExceeded: dropped > 0, durationMs: Math.max(0, Math.round(performance.now() - started)), stdout: Buffer.concat(stdout).toString('utf8'), stderr: Buffer.concat(stderr).toString('utf8'), stdoutSha256: stdoutHash.digest('hex'), stderrSha256: stderrHash.digest('hex'), capturedBytes: bytes, droppedBytes: dropped,
|
|
103
|
-
...(authority.objectPlan ? { started: spawned, startedAt: processStartedAt, finishedAt: new Date().toISOString(), executionDeadlineExceeded: timedOut && remaining <= policy.timeoutMs } : {}) };
|
|
104
|
-
if (spawnError) { spawnError.execution = { schemaVersion: RUNNER_SCHEMA, status: 'failed', process: processResult, evidence: [], effects: spawned ? 'unknown' : 'none' }; reject(spawnError); return; }
|
|
105
|
-
resolve(processResult);
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
}
|
|
89
|
+
const runCommand = authority => runApprovedProcess({ ...authority, policy: authority.plan.policy });
|
|
90
|
+
|
|
109
91
|
async function subject(authority, result) {
|
|
110
92
|
const equivalence = await captureRunnerEquivalence(authority, result, projectFile);
|
|
111
93
|
return { schemaVersion: 'validator.validation-subject/1.0', artifactSha256: authority.plan.artifactSha256, memberId: authority.plan.memberId, chainId: authority.plan.chainId, executedAt: new Date().toISOString(), files: authority.plan.files, validationRunId: authority.plan.validationRunId, planId: authority.plan.planId, tests: authority.plan.tests, policy: { command: [authority.plan.policy.executable, ...authority.plan.policy.args].map(value => JSON.stringify(value)).join(' '), requiredExitCode: 0, executionPlanSha256: authority.planFile.sha256, ...(authority.plan.policy.parallelPaths ? { parallelPaths: authority.plan.policy.parallelPaths } : {}), ...(authority.plan.policy.discipline ? { disciplineReviewRequired: true } : {}), ...(authority.plan.evidencePolicy ? { evidenceKind: authority.plan.evidencePolicy.kind, evidenceRunner: authority.plan.evidencePolicy.runner, workflowBinding: { flowId: authority.plan.evidencePolicy.flowId, stepId: authority.plan.evidencePolicy.stepId }, ...(authority.plan.evidencePolicy.localVerification ? { localVerification: authority.plan.evidencePolicy.localVerification } : {}) } : {}) }, ...(equivalence === undefined ? {} : { equivalence }), ...(authority.disciplineComparison ? { disciplineComparison: authority.disciplineComparison } : {}), goldenBaseline: authority.plan.goldenBaseline, contracts: authority.plan.contracts, execution: { exitCode: result.exitCode, signal: result.signal, timedOut: result.timedOut, outputLimitExceeded: result.outputLimitExceeded } }; }
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { fork } from 'node:child_process';
|
|
2
|
+
import { performance } from 'node:perf_hooks';
|
|
3
|
+
|
|
4
|
+
const SUPERVISOR = new URL('./local-approved-supervisor.mjs', import.meta.url);
|
|
5
|
+
const MESSAGE_SCHEMA = 'entrorove.approved-process-supervision/1.0';
|
|
6
|
+
const RUNNER_SCHEMA = 'entrorove.validator-local-runner/1.0';
|
|
7
|
+
|
|
8
|
+
function launchInput(authority) {
|
|
9
|
+
const now = Date.now(), policy = authority.policy;
|
|
10
|
+
const leaseDeadline = authority.executionDeadlineAt === undefined ? Infinity : Date.parse(authority.executionDeadlineAt);
|
|
11
|
+
const startDeadlineAt = authority.startDeadlineAt === undefined ? null : Date.parse(authority.startDeadlineAt);
|
|
12
|
+
if ((!Number.isFinite(leaseDeadline) && leaseDeadline !== Infinity) || leaseDeadline <= now
|
|
13
|
+
|| (startDeadlineAt !== null && (!Number.isFinite(startDeadlineAt) || startDeadlineAt <= now))) {
|
|
14
|
+
throw Object.assign(new Error('workflow execution lease expired before process launch'), { code: 'ENTROROVE_WORKFLOW_EXPIRED' });
|
|
15
|
+
}
|
|
16
|
+
return { schemaVersion: MESSAGE_SCHEMA, root: authority.root, executable: policy.executable, args: policy.args,
|
|
17
|
+
environment: policy.environment, timeoutMs: policy.timeoutMs, maxOutputBytes: policy.maxOutputBytes,
|
|
18
|
+
objectPlan: Boolean(authority.objectPlan), deadlineAt: Math.min(now + policy.timeoutMs, leaseDeadline), startDeadlineAt,
|
|
19
|
+
leaseDeadline: leaseDeadline <= now + policy.timeoutMs };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function supervisionError(code, message, effects = 'unknown') {
|
|
23
|
+
return Object.assign(new Error(message), { code,
|
|
24
|
+
execution: { schemaVersion: RUNNER_SCHEMA, status: 'failed', evidence: [], effects } });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function runApprovedProcess(authority) {
|
|
28
|
+
const started = performance.now(), input = launchInput(authority);
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
let finished = false;
|
|
31
|
+
const supervisor = fork(SUPERVISOR, [], { execPath: process.execPath, execArgv: [], env: {}, detached: true,
|
|
32
|
+
stdio: ['ignore', 'ignore', 'ignore', 'ipc'], serialization: 'json' });
|
|
33
|
+
const finish = (error, result) => {
|
|
34
|
+
if (finished) return; finished = true;
|
|
35
|
+
if (supervisor.connected) supervisor.disconnect();
|
|
36
|
+
if (error) reject(error); else resolve({ ...result, durationMs: Math.max(0, Math.round(performance.now() - started)) });
|
|
37
|
+
};
|
|
38
|
+
supervisor.on('message', message => {
|
|
39
|
+
if (message?.schemaVersion !== MESSAGE_SCHEMA || message.type !== 'result') return;
|
|
40
|
+
if (message.error) {
|
|
41
|
+
const error = Object.assign(new Error(message.error.message), { code: message.error.code });
|
|
42
|
+
if (message.process) error.execution = { schemaVersion: RUNNER_SCHEMA, status: 'failed', process: message.process,
|
|
43
|
+
evidence: [], effects: message.process.started === true ? 'unknown' : 'none' };
|
|
44
|
+
finish(error); return;
|
|
45
|
+
}
|
|
46
|
+
finish(null, message.process);
|
|
47
|
+
});
|
|
48
|
+
supervisor.once('error', () => finish(supervisionError('ENTROROVE_PROCESS_SUPERVISOR', 'Approved process supervision could not start', 'none')));
|
|
49
|
+
supervisor.once('close', () => { if (!finished) finish(supervisionError('ENTROROVE_PROCESS_SUPERVISOR', 'Approved process supervision ended without a result')); });
|
|
50
|
+
supervisor.send(input, error => {
|
|
51
|
+
if (error) finish(supervisionError('ENTROROVE_PROCESS_SUPERVISOR', 'Approved process supervision could not receive its frozen command', 'none'));
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { performance } from 'node:perf_hooks';
|
|
5
|
+
|
|
6
|
+
const MESSAGE_SCHEMA = 'entrorove.approved-process-supervision/1.0';
|
|
7
|
+
const RUNNER_SCHEMA = 'entrorove.validator-local-runner/1.0';
|
|
8
|
+
const FIELDS = ['schemaVersion', 'root', 'executable', 'args', 'environment', 'timeoutMs', 'maxOutputBytes', 'objectPlan', 'deadlineAt', 'startDeadlineAt', 'leaseDeadline'];
|
|
9
|
+
const ENV = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
10
|
+
|
|
11
|
+
function validInput(value) {
|
|
12
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
13
|
+
&& Object.keys(value).sort().join('|') === [...FIELDS].sort().join('|') && value.schemaVersion === MESSAGE_SCHEMA
|
|
14
|
+
&& typeof value.root === 'string' && path.isAbsolute(value.root) && !value.root.includes('\0')
|
|
15
|
+
&& typeof value.executable === 'string' && path.isAbsolute(value.executable) && !value.executable.includes('\0')
|
|
16
|
+
&& Array.isArray(value.args) && value.args.length <= 128
|
|
17
|
+
&& value.args.every(item => typeof item === 'string' && !item.includes('\0') && item.length <= 4096)
|
|
18
|
+
&& value.environment && typeof value.environment === 'object' && !Array.isArray(value.environment)
|
|
19
|
+
&& Object.keys(value.environment).length <= 64
|
|
20
|
+
&& Object.entries(value.environment).every(([key, item]) => ENV.test(key) && typeof item === 'string' && !item.includes('\0') && item.length <= 16_384)
|
|
21
|
+
&& Number.isSafeInteger(value.timeoutMs) && value.timeoutMs > 0 && value.timeoutMs <= 300_000
|
|
22
|
+
&& Number.isSafeInteger(value.maxOutputBytes) && value.maxOutputBytes > 0 && value.maxOutputBytes <= 1_048_576
|
|
23
|
+
&& typeof value.objectPlan === 'boolean' && typeof value.leaseDeadline === 'boolean'
|
|
24
|
+
&& (value.startDeadlineAt === null || Number.isSafeInteger(value.startDeadlineAt))
|
|
25
|
+
&& Number.isSafeInteger(value.deadlineAt) && value.deadlineAt <= Date.now() + value.timeoutMs;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resultMessage(processResult, error) {
|
|
29
|
+
return { schemaVersion: MESSAGE_SCHEMA, type: 'result', process: processResult,
|
|
30
|
+
...(error ? { error: { code: error.code ?? 'ENTROROVE_PROCESS_SUPERVISOR', message: String(error.message).slice(0, 1024) } } : {}) };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function deliver(value) {
|
|
34
|
+
if (!process.connected) return;
|
|
35
|
+
process.send(value, () => { if (process.connected) process.disconnect(); });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function assertLaunchable(input) {
|
|
39
|
+
if (!process.connected || input.deadlineAt <= Date.now() || input.startDeadlineAt !== null && input.startDeadlineAt <= Date.now()) {
|
|
40
|
+
throw Object.assign(new Error('workflow execution lease expired before process launch'), { code: 'ENTROROVE_WORKFLOW_EXPIRED' });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function supervise(input) {
|
|
45
|
+
const started = performance.now(), stdout = [], stderr = [], stdoutHash = createHash('sha256'), stderrHash = createHash('sha256');
|
|
46
|
+
let bytes = 0, dropped = 0, timedOut = false, terminating = false, spawned = false, closed = false;
|
|
47
|
+
let processStartedAt = null, spawnError = null, killTimer;
|
|
48
|
+
assertLaunchable(input);
|
|
49
|
+
const child = spawn(input.executable, input.args, { cwd: input.root, env: { ...input.environment },
|
|
50
|
+
shell: false, detached: true, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
51
|
+
const signalGroup = signal => { if (!closed && child.pid) { try { process.kill(-child.pid, signal); } catch {} } };
|
|
52
|
+
const terminate = () => {
|
|
53
|
+
if (terminating || closed) return; terminating = true; signalGroup('SIGTERM');
|
|
54
|
+
killTimer = setTimeout(() => signalGroup('SIGKILL'), 250);
|
|
55
|
+
};
|
|
56
|
+
const consume = (target, hash, chunk) => {
|
|
57
|
+
const data = Buffer.from(chunk); hash.update(data);
|
|
58
|
+
const keep = Math.max(0, Math.min(data.length, input.maxOutputBytes - bytes));
|
|
59
|
+
if (keep) target.push(data.subarray(0, keep)); bytes += keep; dropped += data.length - keep;
|
|
60
|
+
if (dropped) terminate();
|
|
61
|
+
};
|
|
62
|
+
const timer = setTimeout(() => { timedOut = true; terminate(); }, Math.max(1, input.deadlineAt - Date.now()));
|
|
63
|
+
process.once('disconnect', terminate); process.once('SIGTERM', terminate); process.once('SIGINT', terminate);
|
|
64
|
+
child.stdout.on('data', chunk => consume(stdout, stdoutHash, chunk)); child.stderr.on('data', chunk => consume(stderr, stderrHash, chunk));
|
|
65
|
+
child.once('spawn', () => { spawned = true; processStartedAt = new Date().toISOString(); });
|
|
66
|
+
child.once('error', error => { spawnError = error; terminate(); });
|
|
67
|
+
child.once('exit', () => signalGroup('SIGKILL'));
|
|
68
|
+
child.once('close', (exitCode, signal) => {
|
|
69
|
+
closed = true; clearTimeout(timer); clearTimeout(killTimer);
|
|
70
|
+
process.removeListener('disconnect', terminate); process.removeListener('SIGTERM', terminate); process.removeListener('SIGINT', terminate);
|
|
71
|
+
const processResult = { schemaVersion: RUNNER_SCHEMA,
|
|
72
|
+
status: exitCode === 0 && signal === null && !timedOut && !dropped && !spawnError ? 'succeeded' : 'failed',
|
|
73
|
+
exitCode, signal, timedOut, outputLimitExceeded: dropped > 0, durationMs: Math.max(0, Math.round(performance.now() - started)),
|
|
74
|
+
stdout: Buffer.concat(stdout).toString('utf8'), stderr: Buffer.concat(stderr).toString('utf8'),
|
|
75
|
+
stdoutSha256: stdoutHash.digest('hex'), stderrSha256: stderrHash.digest('hex'), capturedBytes: bytes, droppedBytes: dropped,
|
|
76
|
+
...(input.objectPlan ? { started: spawned, startedAt: processStartedAt, finishedAt: new Date().toISOString(),
|
|
77
|
+
executionDeadlineExceeded: timedOut && input.leaseDeadline } : {}) };
|
|
78
|
+
deliver(resultMessage(processResult, spawnError));
|
|
79
|
+
});
|
|
80
|
+
if (!process.connected) terminate();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function main() {
|
|
84
|
+
if (process.argv.length !== 2 || typeof process.send !== 'function' || !process.connected) { process.exitCode = 2; return; }
|
|
85
|
+
const startup = setTimeout(() => { if (process.connected) process.disconnect(); }, 5_000);
|
|
86
|
+
const disconnect = () => clearTimeout(startup); process.once('disconnect', disconnect);
|
|
87
|
+
process.once('message', input => {
|
|
88
|
+
clearTimeout(startup); process.removeListener('disconnect', disconnect);
|
|
89
|
+
if (!process.connected) return;
|
|
90
|
+
if (!validInput(input)) {
|
|
91
|
+
deliver(resultMessage(null, { code: 'ENTROROVE_PROCESS_SUPERVISOR', message: 'Approved process supervision requires one bounded frozen command' })); return;
|
|
92
|
+
}
|
|
93
|
+
try { supervise(input); }
|
|
94
|
+
catch (error) { deliver(resultMessage(null, error.code === 'ENTROROVE_WORKFLOW_EXPIRED' ? error
|
|
95
|
+
: { code: 'ENTROROVE_PROCESS_SUPERVISOR', message: 'Approved process supervision could not launch its frozen command' })); }
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
main();
|
package/local-cli.mjs
CHANGED
|
@@ -28,6 +28,10 @@ import { DISCIPLINE_LOCAL_OPERATIONS } from './local-discipline-operations.mjs';
|
|
|
28
28
|
import { assertIdeExecution, settleBreakGlassSession } from './ide-session.mjs';
|
|
29
29
|
import { runManagedWorkflowLocalStep } from './workflow-local-client.mjs';
|
|
30
30
|
import { localErrorEnvelope } from './local-error.mjs';
|
|
31
|
+
import { preparePreservedMutationSigner } from './local-preserved-mutation-signer.mjs';
|
|
32
|
+
import { hydrateCloudMutationAuthority } from './cloud-mutation-authority.mjs';
|
|
33
|
+
import { attestMutationConsumption14 } from './local-mutation-consumption.mjs';
|
|
34
|
+
import { apiUrl } from './remote-client.mjs';
|
|
31
35
|
|
|
32
36
|
async function stdinJson() {
|
|
33
37
|
let raw = '';
|
|
@@ -40,6 +44,8 @@ async function stdinJson() {
|
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
const operations = {
|
|
47
|
+
'mutation-consumption-attest': attestMutationConsumption14,
|
|
48
|
+
'mutation-delegate-prepare': preparePreservedMutationSigner,
|
|
43
49
|
...DISCIPLINE_LOCAL_OPERATIONS,
|
|
44
50
|
'snapshot-plan': snapshotPlan, 'snapshot-verify': snapshotVerify, 'mutation-snapshot-attest': attestMutationSnapshot14, 'mutate-gate': mutateGate,
|
|
45
51
|
'gate-issue': issueMutationPass, 'gate-verify': verifyMutationPass, 'guarded-write': guardedWrite,
|
|
@@ -79,7 +85,11 @@ function capabilitiesFor(group) {
|
|
|
79
85
|
}
|
|
80
86
|
|
|
81
87
|
async function checkedLocalInput(operation, input) {
|
|
82
|
-
try {
|
|
88
|
+
try {
|
|
89
|
+
const validated = validateLocalInput(operation, input);
|
|
90
|
+
if (['mutation-snapshot-attest', 'mutation-consumption-attest', 'workflow-run-step', 'mutate-gate', 'gate-issue', 'gate-verify', 'guarded-write', 'guarded-delete', 'guarded-move', 'discipline-review', 'discipline-check'].includes(operation)) await hydrateCloudMutationAuthority(input.home, new URL(apiUrl).origin);
|
|
91
|
+
return validated;
|
|
92
|
+
}
|
|
83
93
|
catch (cause) {
|
|
84
94
|
if (['guarded-write', 'guarded-delete', 'guarded-move', 'gate-issue', 'mutate-gate', 'mutation-snapshot-attest'].includes(operation)) await recordMutationDenied14(input, cause, operation);
|
|
85
95
|
throw cause;
|
|
@@ -94,9 +104,10 @@ export async function runLocal(args = [], dependencies) {
|
|
|
94
104
|
process.stdout.write(`${JSON.stringify(capabilitiesFor(args[1]))}\n`); return;
|
|
95
105
|
}
|
|
96
106
|
const handler = operations[operation];
|
|
97
|
-
if (!handler) throw new Error(`
|
|
107
|
+
if (!handler && process.stdin.isTTY) throw new Error(`Unsupported local operation: ${operation}`);
|
|
98
108
|
const input = await stdinJson();
|
|
99
109
|
await assertIdeExecution(['local', ...args], input);
|
|
110
|
+
if (!handler) throw new Error(`Usage: entrorove local <${['capabilities', ...Object.keys(operations)].join('|')}> [repositoryRoot]`);
|
|
100
111
|
if (args[1] && input.repositoryRoot === undefined) input.repositoryRoot = args[1];
|
|
101
112
|
const validated = await checkedLocalInput(operation, input);
|
|
102
113
|
const supplied = operation === 'workflow-run-step' || Object.hasOwn(LOCAL_CHAIN_OPERATIONS, operation) ? dependencies : undefined;
|
package/local-contracts.mjs
CHANGED
|
@@ -29,6 +29,8 @@ add('flow-cancel|flow-abandon', flowLifecycle, ['repositoryRoot', 'flowId']);
|
|
|
29
29
|
add('snapshot-plan', { ...step, paths, snapshotId: identifier, contractDigest: nullable(sha256), handoffCandidate: HANDOFF_SNAPSHOT_CANDIDATE_SCHEMA }, ['repositoryRoot', 'flowId', 'stepId', 'paths']);
|
|
30
30
|
add('snapshot-verify', { ...step, snapshotId: identifier }, ['repositoryRoot', 'flowId', 'stepId', 'snapshotId']);
|
|
31
31
|
const gate = { ...step, taskId: identifier, mutationPass: record };
|
|
32
|
+
add('mutation-delegate-prepare', { ...step, taskId: identifier }, ['repositoryRoot', 'home', 'flowId']);
|
|
33
|
+
add('mutation-consumption-attest', { ...step, taskId: identifier, gatePassPath: text }, ['repositoryRoot', 'flowId', 'stepId', 'gatePassPath']);
|
|
32
34
|
add('mutate-gate|gate-issue', gate, ['repositoryRoot', 'flowId', 'stepId', 'mutationPass']);
|
|
33
35
|
add('mutation-snapshot-attest', { ...step, taskId: identifier, snapshotId: identifier, challenge: record, signerConfigPath: text },
|
|
34
36
|
['repositoryRoot', 'flowId', 'stepId', 'snapshotId', 'challenge', 'signerConfigPath']);
|