@entrorove/skill 0.1.53 → 0.1.55
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/CHANGELOG.md +10 -0
- package/IDE-EXECUTION.md +4 -0
- package/SKILL.md +3 -3
- package/ide-code-mode.mjs +51 -9
- package/ide-command.mjs +53 -0
- package/ide-hook.mjs +73 -22
- package/ide-session.mjs +1 -1
- 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/package.json +3 -1
- package/release-state.json +15 -13
- package/release.mjs +4 -0
- package/releases.json +12 -0
- package/scope-runner-client.mjs +8 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.55 — 2026-10-17
|
|
4
|
+
|
|
5
|
+
- 修复任务间消息被技能误拦截,将已授权通信交回主机审批;规范只读和 SDK 命令执行参数;新增原流程计划阶段的显式范围重评与恢复提示,保留原门禁、合同、请求身份及失败历史。
|
|
6
|
+
- Restored host-approved task messaging, constrained read and SDK execution inputs, and added explicit planning-scope reassessment with phase-correct recovery while retaining original gates, contracts, request identities, and failure history.
|
|
7
|
+
|
|
8
|
+
## 0.1.54 — 2026-10-17
|
|
9
|
+
|
|
10
|
+
- 修复受管 Node 只读 SHA-256 命令在 Codex 工具门禁中的静态识别与安全改写;按已证明的 createRequire 原目录解析实际安装依赖并记录只读元数据,保留未知上下文和越界拒绝。
|
|
11
|
+
- Fixed static recognition and safe rewriting of managed Node SHA-256 reads in Codex tool guards; resolved installed dependencies from proven createRequire anchors with read-only metadata while preserving unknown-context and path-boundary refusals.
|
|
12
|
+
|
|
3
13
|
## 0.1.53 — 2026-10-17
|
|
4
14
|
|
|
5
15
|
- 补齐原任务受限编辑、真实写后结算与连续修改,保留未知依赖和独立验证边界;沙箱关闭后可运行原批准命令并保存本地观察。修复前置条件、断线恢复和停止循环,独立监督器约束崩溃后的子进程。本地成功不代表独立验证或交付。
|
package/IDE-EXECUTION.md
CHANGED
|
@@ -6,6 +6,10 @@ If the IDE guard itself is the blocked step, run `entrorove unlock` directly fro
|
|
|
6
6
|
|
|
7
7
|
Capability discovery and offline unlock remain administrative calls during `entry`, before a flow exists. The guard recognizes its own rendered commands with quoted identity assignments, their order changed, or a literal `env` prefix containing only supported host/session identities; it rewrites approved calls to the installed runtime and current session. This does not authorize native scripts or unbound project writes.
|
|
8
8
|
|
|
9
|
+
Approved Codex `exec_command` reads and SDK calls run with `/bin/sh` and `login:false`; static code mode uses the same execution input check. Its noninteractive workdir and output controls pass through. Other hosts keep their adapter's command schema, including timeout where supported, with custom executors and permission expansion rejected.
|
|
10
|
+
|
|
11
|
+
For a Codex task-to-task report explicitly authorized by the human user, the guard passes the exact `mcp__codex_app__send_message_to_thread` tool through unchanged, including a static `tools.mcp__codex_app__send_message_to_thread({...})` call inside `functions.exec`. Codex still decides whether the user authorized the message. Its text and response grant no Entrorove flow, file, shell or task-creation authority.
|
|
12
|
+
|
|
9
13
|
To stop using Entrorove on this computer, run `entrorove uninstall` directly. It works offline, removes only the recorded Entrorove host hooks and pristine global skill, and leaves other host settings, credentials and task records in place. A hand-edited file is reported as preserved rather than deleted. This command does not inspect or change another project.
|
|
10
14
|
|
|
11
15
|
Every invocation rereads that same credential file, including clients already running. After an authenticated account page supplies the current key and `configure` synchronizes it once, all tasks under the same OS user automatically use the new value. A revoked or expired key cannot obtain new permissions for itself: return to the authenticated account page and synchronize once through JSON stdin. Status recovery only queries the original request; it never rotates credentials or repeats the operation.
|
package/SKILL.md
CHANGED
|
@@ -5,7 +5,7 @@ description: Execute an engineering task through Entrorove, preserving its origi
|
|
|
5
5
|
|
|
6
6
|
# entrorove · 熵巡
|
|
7
7
|
|
|
8
|
-
Version: 0.1.
|
|
8
|
+
Version: 0.1.55.
|
|
9
9
|
|
|
10
10
|
Entrorove applies to the whole task once selected. Reading this document or calling an entry once does not execute the task. On every later turn, including after compaction, continue the original task and its required next operation before dependent work. Keep the original goal, scope, account, flow, cloud task and failure records. Never describe planning, a reported step or model output as verified delivery.
|
|
11
11
|
|
|
@@ -19,8 +19,8 @@ Entrorove applies to the whole task once selected. Reading this document or call
|
|
|
19
19
|
With cloud sandbox disabled, the IDE performs its own planning and continues the original API flow. Do not require a cloud model call or create a sandbox task to perform local work.
|
|
20
20
|
When the model produces a task contract, preserve its `workType`, `risk`, `difficulty`, `crossModule`, `parallel`, `unclear`, `architectureContract`, optional `baselineRef`, and optional `interaction` as returned. Valid work types are `implementation`, `documentation`, `calculation`, `merge`, `review`, `decision`, and `diagnosis`. A `merge` requires a concrete `baselineRef` identifying the branch, tag, commit, or review baseline; keep that baseline unchanged through independent verification. A `decision` requires `interaction`, including a risk description for medium or high risk. Do not fill missing required fields by guessing.
|
|
21
21
|
4. After real cloud acceptance, immediately display **云端任务 <taskNumber> · 已进入**. Preserve `taskNumber` verbatim: it is the homepage sandbox tab number. Keep its UUID `taskId` separately. Run the returned `cloud-watch`, show each real public event and its cursor, and continue the original accepted flow from status. A command log alone is insufficient. No accepted cloud task means no cloud ID or model-execution claim.
|
|
22
|
-
5. For an accepted `flowId`, execute the returned `nextStep` with its required payload. Discover the schema before constructing input. An empty payload is valid only when that operation's schema allows it. Reconnect and refresh only the original flow. A changed snapshot is a binding failure to investigate; do not invent a fresh successful baseline, rebuild a flow to evade it, or erase the old failure. If the terminal flow truly owns a stale task slot, use the explicit `flow-abandon` lifecycle operation described below before routing the original task again.
|
|
23
|
-
6. All governed mutations use the original authority: `local guarded-write`, `local guarded-delete`, `local guarded-move`, or the original frozen approved runner plan. Native patches, scripts, terminal writes, database mutations and unknown MCP operations must not substitute for the controlled entry. **A failed gate or unknown outcome must never be replaced with another executor or reported as local success.** Query the original request read-only, repair the cause, and obtain the original flow's valid authority before proceeding.
|
|
22
|
+
5. For an accepted `flowId`, execute the returned `nextStep` with its required payload. Discover the schema before constructing input. An empty payload is valid only when that operation's schema allows it. Reconnect and refresh only the original flow. A changed snapshot is a binding failure to investigate; do not invent a fresh successful baseline, rebuild a flow to evade it, or erase the old failure. If a fresh scope proof returns `ENTROROVE_SCOPE_PLANNING_REASSESSMENT_REQUIRED` before plan freeze, follow its explicit `workflow-scope-reassess` recovery and retry the original complete request; a local reassessment or snapshot refresh alone does not renew the accepted gate. If the terminal flow truly owns a stale task slot, use the explicit `flow-abandon` lifecycle operation described below before routing the original task again.
|
|
23
|
+
6. All governed mutations use the original authority: `local guarded-write`, `local guarded-delete`, `local guarded-move`, or the original frozen approved runner plan. Native patches, scripts, terminal writes, database mutations and unknown MCP operations must not substitute for the controlled entry. User-authorized task messages use the host's message tool and remain subject to host approval; sending a report does not change the flow or grant execution authority. **A failed gate or unknown outcome must never be replaced with another executor or reported as local success.** Query the original request read-only, repair the cause, and obtain the original flow's valid authority before proceeding.
|
|
24
24
|
For a restricted member scope, follow a returned `localAction` before submitting execution evidence. `mutation-connect` obtains only the original task's limited editing pass; it keeps unresolved loader files and project configuration read only. After actual writes, refresh the original scope and run `mutation-settle`. Use the returned `previousPassId` for further edits. With sandbox disabled, `workflow-observe-step` runs the original approved command and preserves its real local result without a validator private key; independent verification remains pending. See [IDE-EXECUTION.md](IDE-EXECUTION.md#restricted-member-editing).
|
|
25
25
|
7. Read [REUSE-DISCIPLINE.md](REUSE-DISCIPLINE.md) when modifying an existing system. The comparison baseline is the existing implementation in that same repository. It never means another project, another account, or copying external systems.
|
|
26
26
|
8. Read [DELIVERY-BOUNDARIES.md](DELIVERY-BOUNDARIES.md) before claiming a delivered stage or showing consumption. A delivered stage requires a clean boundary and uses `delivered-stage-N`. Blocked or unknown work can wait with recovery data intact; it is not a delivered stage. Display consumption only with a measured baseline.
|
package/ide-code-mode.mjs
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
|
-
import { literalCommand, sdkCommand, readCommand } from './ide-command.mjs';
|
|
2
|
+
import { literalCommand, sdkCommand, readCommand, runtimeHashRead, prepareIdeShellInput } from './ide-command.mjs';
|
|
3
3
|
|
|
4
|
-
export
|
|
5
|
-
|
|
4
|
+
export const hostTaskMessageTool = 'mcp__codex_app__send_message_to_thread';
|
|
5
|
+
export function hostTaskMessageInput(value) {
|
|
6
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
7
|
+
&& typeof value.threadId === 'string' && value.threadId.trim().length > 0
|
|
8
|
+
&& typeof value.prompt === 'string' && value.prompt.trim().length > 0
|
|
9
|
+
&& Object.entries(value).every(([key, item]) =>
|
|
10
|
+
['threadId', 'prompt', 'hostId', 'model', 'thinking'].includes(key) && typeof item === 'string');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function prepareIdeCodeMode(code, runtime) {
|
|
14
|
+
if (typeof code !== 'string' || code.length > 1_000_000) return { allowed: false };
|
|
6
15
|
const ts = createRequire(runtime.cli)('typescript');
|
|
7
16
|
const source = ts.createSourceFile('host-call.js', code, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
|
|
8
|
-
if (source.parseDiagnostics.length) return false;
|
|
9
|
-
let allowed = true;
|
|
17
|
+
if (source.parseDiagnostics.length) return { allowed: false };
|
|
18
|
+
let allowed = true, unsafeShellInput = false; const replacements = [];
|
|
10
19
|
const literal = node => {
|
|
11
20
|
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return node.text;
|
|
12
21
|
if (ts.isNumericLiteral(node)) return Number(node.text);
|
|
@@ -21,13 +30,38 @@ export function inspectIdeCodeMode(code, runtime) {
|
|
|
21
30
|
throw new Error('Dynamic tool arguments');
|
|
22
31
|
};
|
|
23
32
|
const visit = node => {
|
|
24
|
-
if (ts.isImportDeclaration(node) || ts.isNewExpression(node) || ts.isElementAccessExpression(node)
|
|
33
|
+
if (ts.isImportDeclaration(node) || ts.isNewExpression(node) || ts.isElementAccessExpression(node)
|
|
34
|
+
|| ts.isFunctionExpression(node) || ts.isTaggedTemplateExpression(node)) allowed = false;
|
|
25
35
|
if (ts.isCallExpression(node)) {
|
|
26
36
|
const name = node.expression.getText(source);
|
|
27
37
|
if (name === 'tools.exec_command') {
|
|
28
38
|
try {
|
|
29
|
-
const
|
|
30
|
-
|
|
39
|
+
const argument = node.arguments[0], args = literal(argument), command = literalCommand(args.cmd ?? args.command);
|
|
40
|
+
const hash = runtimeHashRead(command, runtime);
|
|
41
|
+
if (hash) {
|
|
42
|
+
const safe = node.arguments.length === 1 && typeof args.cmd === 'string' && !Object.hasOwn(args, 'command')
|
|
43
|
+
&& Object.keys(args).every(key => ['cmd', 'workdir', 'max_output_tokens', 'yield_time_ms', 'login', 'shell'].includes(key))
|
|
44
|
+
&& (args.shell === undefined || args.shell === '/bin/sh')
|
|
45
|
+
&& (args.login === undefined || args.login === false)
|
|
46
|
+
&& (args.workdir === undefined || typeof args.workdir === 'string')
|
|
47
|
+
&& (args.max_output_tokens === undefined || Number.isSafeInteger(args.max_output_tokens) && args.max_output_tokens > 0)
|
|
48
|
+
&& (args.yield_time_ms === undefined || Number.isSafeInteger(args.yield_time_ms) && args.yield_time_ms >= 0);
|
|
49
|
+
if (!safe) allowed = false;
|
|
50
|
+
else {
|
|
51
|
+
const fixed = { ...args, cmd: hash, shell: '/bin/sh', login: false };
|
|
52
|
+
replacements.push({ start: argument.getStart(source), end: argument.getEnd(), text: JSON.stringify(fixed) });
|
|
53
|
+
}
|
|
54
|
+
} else if (!(readCommand(command) || sdkCommand(command, runtime))) allowed = false;
|
|
55
|
+
else {
|
|
56
|
+
const fixed = node.arguments.length === 1 && !node.questionDotToken
|
|
57
|
+
&& prepareIdeShellInput('exec_command', args, args.cmd);
|
|
58
|
+
if (!fixed) { allowed = false; unsafeShellInput = true; }
|
|
59
|
+
else replacements.push({ start: argument.getStart(source), end: argument.getEnd(), text: JSON.stringify(fixed) });
|
|
60
|
+
}
|
|
61
|
+
} catch { allowed = false; }
|
|
62
|
+
} else if (name === `tools.${hostTaskMessageTool}`) {
|
|
63
|
+
try {
|
|
64
|
+
if (node.questionDotToken || node.arguments.length !== 1 || !hostTaskMessageInput(literal(node.arguments[0]))) allowed = false;
|
|
31
65
|
} catch { allowed = false; }
|
|
32
66
|
} else if (name === 'tools.update_goal' || name === 'tools.get_goal') {
|
|
33
67
|
try {
|
|
@@ -40,5 +74,13 @@ export function inspectIdeCodeMode(code, runtime) {
|
|
|
40
74
|
}
|
|
41
75
|
ts.forEachChild(node, visit);
|
|
42
76
|
};
|
|
43
|
-
visit(source);
|
|
77
|
+
visit(source);
|
|
78
|
+
if (!allowed) return { allowed: false, ...(unsafeShellInput ? { reason: 'shell-input' } : {}) };
|
|
79
|
+
let prepared = code;
|
|
80
|
+
for (const item of replacements.sort((a, b) => b.start - a.start)) {
|
|
81
|
+
prepared = `${prepared.slice(0, item.start)}${item.text}${prepared.slice(item.end)}`;
|
|
82
|
+
}
|
|
83
|
+
return { allowed: true, code: prepared };
|
|
44
84
|
}
|
|
85
|
+
|
|
86
|
+
export function inspectIdeCodeMode(code, runtime) { return prepareIdeCodeMode(code, runtime).allowed; }
|
package/ide-command.mjs
CHANGED
|
@@ -113,6 +113,59 @@ export function readCommand(parsed) {
|
|
|
113
113
|
return false;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
const safeShellKeys = {
|
|
117
|
+
exec_command: new Set(['cmd', 'command', 'workdir', 'max_output_tokens', 'yield_time_ms', 'tty', 'shell', 'login']),
|
|
118
|
+
Bash: new Set(['command', 'cmd', 'workdir', 'timeout', 'description', 'max_output_chars', 'max_output_tokens', 'yield_time_ms', 'tty']),
|
|
119
|
+
shell_command: new Set(['command', 'cmd', 'workdir', 'timeout', 'description', 'max_output_chars', 'max_output_tokens', 'yield_time_ms', 'tty']),
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export function prepareIdeShellInput(toolName, input, normalizedCommand, mirrorCommand = false) {
|
|
123
|
+
const keys = safeShellKeys[toolName];
|
|
124
|
+
if (!keys || !input || typeof input !== 'object' || Array.isArray(input)
|
|
125
|
+
|| typeof normalizedCommand !== 'string' || !Reflect.ownKeys(input).every(key => keys.has(key))) return null;
|
|
126
|
+
const primary = toolName === 'exec_command' ? 'cmd' : 'command';
|
|
127
|
+
if (typeof input[primary] !== 'string' || input.cmd !== undefined && input.command !== undefined
|
|
128
|
+
&& input.cmd !== input.command || input.tty !== undefined && input.tty !== false
|
|
129
|
+
|| input.workdir !== undefined && typeof input.workdir !== 'string'
|
|
130
|
+
|| input.description !== undefined && typeof input.description !== 'string'
|
|
131
|
+
|| input.timeout !== undefined && (!Number.isSafeInteger(input.timeout) || input.timeout <= 0)
|
|
132
|
+
|| ['max_output_chars', 'max_output_tokens'].some(key => input[key] !== undefined
|
|
133
|
+
&& (!Number.isSafeInteger(input[key]) || input[key] <= 0))
|
|
134
|
+
|| input.yield_time_ms !== undefined && (!Number.isSafeInteger(input.yield_time_ms) || input.yield_time_ms < 0)
|
|
135
|
+
|| input.shell !== undefined && input.shell !== '/bin/sh'
|
|
136
|
+
|| input.login !== undefined && input.login !== false) return null;
|
|
137
|
+
const { cmd: _cmd, command: _command, ...options } = input;
|
|
138
|
+
return { ...options, [primary]: normalizedCommand,
|
|
139
|
+
...(toolName === 'exec_command' && mirrorCommand ? { command: normalizedCommand } : {}),
|
|
140
|
+
...(toolName === 'exec_command' ? { shell: '/bin/sh', login: false } : {}) };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const runtimeHashTool = process.platform === 'darwin'
|
|
144
|
+
? { name: 'shasum', executable: '/usr/bin/shasum', flags: ['-a', '256'] }
|
|
145
|
+
: process.platform === 'linux'
|
|
146
|
+
? { name: 'sha256sum', executable: '/usr/bin/sha256sum', flags: [] } : null;
|
|
147
|
+
|
|
148
|
+
export function runtimeHashCommand(runtime) {
|
|
149
|
+
const node = runtime?.node;
|
|
150
|
+
if (!runtimeHashTool || typeof node !== 'string' || node.length > 4096 || !path.isAbsolute(node)
|
|
151
|
+
|| path.basename(node) !== 'node' || path.normalize(node) !== node
|
|
152
|
+
|| !/^[A-Za-z0-9/._+@ -]+$/u.test(node)) return null;
|
|
153
|
+
return `/usr/bin/env -i PATH=/usr/bin:/bin ${runtimeHashTool.executable} ${[...runtimeHashTool.flags, '--'].join(' ')} '${node}'`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function runtimeHashRead(parsed, runtime) {
|
|
157
|
+
const canonical = runtimeHashCommand(runtime);
|
|
158
|
+
if (!canonical || !parsed || parsed.input) return null;
|
|
159
|
+
const words = parsed.words, { name, executable, flags } = runtimeHashTool;
|
|
160
|
+
const normal = [name, ...flags, runtime.node];
|
|
161
|
+
const absolute = [executable, ...flags, runtime.node];
|
|
162
|
+
const withSeparator = [executable, ...flags, '--', runtime.node];
|
|
163
|
+
const protectedCommand = ['/usr/bin/env', '-i', 'PATH=/usr/bin:/bin', ...withSeparator];
|
|
164
|
+
if ([normal, absolute, withSeparator, protectedCommand].some(expected =>
|
|
165
|
+
words.length === expected.length && words.every((word, index) => word === expected[index]))) return canonical;
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
116
169
|
export function renderSdkCommand(call, runtime, sessionId, host = process.env.ENTROROVE_IDE ?? 'Codex') {
|
|
117
170
|
const quote = value => `'${String(value).replace(/'/gu, "'\\''")}'`;
|
|
118
171
|
const header = `ENTROROVE_IDE=${quote(host)} ENTROROVE_SESSION=${quote(sessionId)} CODEX_THREAD_ID=${quote(sessionId)} ${[runtime.node, runtime.cli, ...call.args].map(quote).join(' ')}`;
|
package/ide-hook.mjs
CHANGED
|
@@ -4,8 +4,9 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
import { ideSession, ideRuntime, bindIdeAccount, ideContext, ideDigest, readBreakGlass,
|
|
5
5
|
ideBlockIdentity, repeatedIdeBlock, observeIdeToolOutcome } from './ide-session.mjs';
|
|
6
6
|
import { isStageTerminal, stageOf } from './local-stage-boundary.mjs';
|
|
7
|
-
import { literalCommand, sdkCommand, readCommand, renderSdkCommand, installerCommand
|
|
8
|
-
|
|
7
|
+
import { literalCommand, sdkCommand, readCommand, renderSdkCommand, installerCommand,
|
|
8
|
+
runtimeHashCommand, runtimeHashRead, prepareIdeShellInput } from './ide-command.mjs';
|
|
9
|
+
import { hostTaskMessageInput, hostTaskMessageTool, prepareIdeCodeMode } from './ide-code-mode.mjs';
|
|
9
10
|
import { pathToFileURL } from 'node:url';
|
|
10
11
|
import path from 'node:path';
|
|
11
12
|
import { parseIdeResults, consumeIdeResults } from './ide-results.mjs';
|
|
@@ -35,17 +36,44 @@ function operationOf(call) {
|
|
|
35
36
|
}
|
|
36
37
|
function isManagementCall(call) { return managementOperations.has(operationOf(call)); }
|
|
37
38
|
function resetBlockStreak(state) { state.blockStreak = 0; delete state.lastBlockKey; delete state.lastBlockIdentity; }
|
|
38
|
-
function blockedReason(state, event, reason) {
|
|
39
|
+
function blockedReason(state, event, reason, detail = {}) {
|
|
39
40
|
const key = ideDigest({ identity: ideBlockIdentity(state), tool: event.tool_name, phase: state.phase, reason });
|
|
40
41
|
const repeated = state.lastBlockKey === key;
|
|
41
42
|
state.blockStreak = repeated ? (state.blockStreak ?? 0) + 1 : 1;
|
|
42
43
|
state.lastBlockKey = key;
|
|
43
44
|
state.lastBlockIdentity = ideBlockIdentity(state);
|
|
44
|
-
const mechanism = `机制:${state.stateFile ?? '会话状态文件'} 当前 phase=${state.phase}${state.pendingCall ? `,pendingCall=${state.pendingCall.toolUseId}` : ''};该阶段只放行 Entrorove
|
|
45
|
+
const mechanism = detail.mechanism ?? `机制:${state.stateFile ?? '会话状态文件'} 当前 phase=${state.phase}${state.pendingCall ? `,pendingCall=${state.pendingCall.toolUseId}` : ''};该阶段只放行 Entrorove 管理调用及主机明确授权的任务间报告,业务与写操作需先建立原 flow 并取得 nextStep。`;
|
|
45
46
|
const repeat = repeated && state.blockStreak >= 2 ? `同一阻断已出现 ${state.blockStreak} 次,禁止再次检查:说明机制、给出绕过方案并执行绕过。` : '';
|
|
46
|
-
|
|
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
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function denyUnsafeShellInput(state, event) {
|
|
72
|
+
return denyBlocked(state, event, 'ENTROROVE_IDE_EXEC_INPUT_DENIED:只读或 SDK 命令含不安全的执行参数。', {
|
|
73
|
+
mechanism: '机制:仅接受静态命令及只读的 workdir、输出上限和超时参数;自定义 shell、登录模式、环境变量、交互终端或权限扩张均不属于受管调用。',
|
|
74
|
+
recovery: '恢复:移除执行器和权限扩张参数。exec_command 将固定为 /bin/sh 且 login=false;本次在 hook 本地预检中拒绝,未发起协议请求。',
|
|
75
|
+
});
|
|
47
76
|
}
|
|
48
|
-
const denyBlocked = (state, event, reason) => deny(blockedReason(state, event, reason));
|
|
49
77
|
function lifecycleResult(value, depth = 0) {
|
|
50
78
|
if (!value || typeof value !== 'object' || depth > 4) return null;
|
|
51
79
|
if (value.operation === 'flow-cancel' || value.operation === 'flow-abandon') return value;
|
|
@@ -108,12 +136,11 @@ async function beforeTool(event, state, home) {
|
|
|
108
136
|
const isShell = ['Bash', 'exec_command', 'shell_command'].includes(event.tool_name);
|
|
109
137
|
const installer = isShell && installerCommand(parsed);
|
|
110
138
|
if (installer) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return output('PreToolUse', { permissionDecision: 'allow', updatedInput:
|
|
116
|
-
...(Object.hasOwn(event.tool_input ?? {}, 'cmd') ? { cmd: installer } : {}) } });
|
|
139
|
+
const safeInput = prepareIdeShellInput(event.tool_name, event.tool_input, installer, true);
|
|
140
|
+
if (!safeInput) return denyUnsafeShellInput(state, event);
|
|
141
|
+
if (state.active) { state.readCalls ??= {};
|
|
142
|
+
state.readCalls[event.tool_use_id] = { toolUseId: event.tool_use_id, command: 'install' }; }
|
|
143
|
+
return output('PreToolUse', { permissionDecision: 'allow', updatedInput: safeInput });
|
|
117
144
|
}
|
|
118
145
|
const namedSdk = parsed?.words.some(word => word === 'entrorove' || /^@entrorove\/skill(?:@[^ ]+)?$/u.test(word)
|
|
119
146
|
|| /(?:@entrorove\/skill|entrorove-skill)\/cli\.mjs$/u.test(word));
|
|
@@ -121,20 +148,20 @@ async function beforeTool(event, state, home) {
|
|
|
121
148
|
const runtime = isShell && parsed ? await ideRuntime(home) : null;
|
|
122
149
|
const call = runtime && sdkCommand(parsed, runtime);
|
|
123
150
|
if (call) {
|
|
151
|
+
if (!prepareIdeShellInput(event.tool_name, event.tool_input, command, true)) return denyUnsafeShellInput(state, event);
|
|
124
152
|
if (call.args[0] === 'uninstall') {
|
|
125
153
|
state.active = false; state.phase = 'user-exited';
|
|
126
|
-
return output('PreToolUse', { permissionDecision: 'allow', updatedInput:
|
|
127
|
-
|
|
128
|
-
...(Object.hasOwn(event.tool_input ?? {}, 'cmd') ? { cmd: renderSdkCommand(call, runtime, state.sessionId) } : {}) } });
|
|
154
|
+
return output('PreToolUse', { permissionDecision: 'allow', updatedInput: prepareIdeShellInput(event.tool_name,
|
|
155
|
+
event.tool_input, renderSdkCommand(call, runtime, state.sessionId), true) });
|
|
129
156
|
}
|
|
130
157
|
state.active = true;
|
|
131
158
|
if (call.args[0] !== 'configure' && call.args[0] !== 'unlock') await bindIdeAccount(state, home);
|
|
132
|
-
if (state.phase === 'setup' && !unlocked && !isManagementCall(call)) return denyBlocked(state, event, 'Entrorove:自举阶段只放行 Entrorove
|
|
159
|
+
if (state.phase === 'setup' && !unlocked && !isManagementCall(call)) return denyBlocked(state, event, 'Entrorove:自举阶段只放行 Entrorove 管理调用及主机明确授权的任务间报告;业务与写操作需先建立原 flow。');
|
|
133
160
|
if (call.args[0] === 'unlock') {
|
|
134
161
|
resetBlockStreak(state);
|
|
135
162
|
const rewritten = renderSdkCommand(call, runtime, state.sessionId);
|
|
136
|
-
return output('PreToolUse', { permissionDecision: 'allow', updatedInput:
|
|
137
|
-
|
|
163
|
+
return output('PreToolUse', { permissionDecision: 'allow', updatedInput: prepareIdeShellInput(event.tool_name,
|
|
164
|
+
event.tool_input, rewritten, true), additionalContext: `${ideContext(state)}离线管理解锁已放行。` });
|
|
138
165
|
}
|
|
139
166
|
const newStart = call.args[0] === 'cloud-task' || call.args[0] === 'invoke' && ['start','cloud-task'].includes(call.input?.operation);
|
|
140
167
|
if (newStart && (state.flowId || state.taskId) && state.phase !== 'ended') return denyBlocked(state, event, 'Entrorove:必须继续原任务,禁止用第二次 start 或 cloud-task 绕过原门禁。');
|
|
@@ -165,16 +192,40 @@ async function beforeTool(event, state, home) {
|
|
|
165
192
|
} else state.pendingCall = pending;
|
|
166
193
|
const installation = call.args[0] === 'install' && parsed.words.some(word => ['npx','npx.cmd'].includes(word));
|
|
167
194
|
const rewritten = renderSdkCommand(call, runtime, state.sessionId);
|
|
168
|
-
return output('PreToolUse', { permissionDecision: 'allow',
|
|
169
|
-
|
|
195
|
+
return output('PreToolUse', { permissionDecision: 'allow', updatedInput: prepareIdeShellInput(event.tool_name,
|
|
196
|
+
event.tool_input, installation ? command : rewritten, true), additionalContext: ideContext(state) });
|
|
170
197
|
}
|
|
171
198
|
if (!state.active) return {};
|
|
199
|
+
// The host owns user authorization for peer messages; this hook does not rewrite or grant it.
|
|
200
|
+
if (event.tool_name === hostTaskMessageTool && hostTaskMessageInput(event.tool_input)) return output('PreToolUse', { additionalContext: ideContext(state) });
|
|
172
201
|
if (['exec','functions.exec'].includes(event.tool_name)) {
|
|
173
202
|
const current = await ideRuntime(home), code = typeof event.tool_input === 'string' ? event.tool_input : event.tool_input?.code;
|
|
174
|
-
|
|
203
|
+
const prepared = prepareIdeCodeMode(code, current);
|
|
204
|
+
if (prepared.allowed) return output('PreToolUse', { additionalContext: ideContext(state),
|
|
205
|
+
...(prepared.code === code ? {} : { updatedInput: typeof event.tool_input === 'string' ? prepared.code
|
|
206
|
+
: { ...event.tool_input, code: prepared.code } }) });
|
|
207
|
+
if (prepared.reason === 'shell-input') return denyUnsafeShellInput(state, event);
|
|
208
|
+
if (typeof code === 'string' && /\b(?:shasum|sha256sum)\b/u.test(code)) return denyRuntimeHash(state, event, current);
|
|
209
|
+
return denyBlocked(state, event, 'Entrorove:代码模式含未批准的原生执行或动态工具输入;请使用原任务的受控入口。');
|
|
175
210
|
}
|
|
176
211
|
if (event.tool_name === 'update_goal' && (state.phase === 'setup' || unlocked)) return output('PreToolUse', { additionalContext: ideContext(state) });
|
|
177
|
-
if (isShell
|
|
212
|
+
if (isShell) {
|
|
213
|
+
const fixedHash = runtimeHashRead(parsed, runtime);
|
|
214
|
+
if (fixedHash) {
|
|
215
|
+
if (!safeHashToolInput(event.tool_input)) return denyRuntimeHash(state, event, runtime);
|
|
216
|
+
resetBlockStreak(state);
|
|
217
|
+
return output('PreToolUse', { permissionDecision: 'allow', updatedInput: { ...event.tool_input, command: fixedHash,
|
|
218
|
+
...(Object.hasOwn(event.tool_input ?? {}, 'cmd') ? { cmd: fixedHash } : {}),
|
|
219
|
+
...(event.tool_name === 'exec_command' ? { shell: '/bin/sh', login: false } : {}) },
|
|
220
|
+
additionalContext: ideContext(state) });
|
|
221
|
+
}
|
|
222
|
+
if (runtimeHashAttempt(command, parsed)) return denyRuntimeHash(state, event, runtime ?? await ideRuntime(home));
|
|
223
|
+
}
|
|
224
|
+
if (isShell && readCommand(parsed)) {
|
|
225
|
+
const safeInput = prepareIdeShellInput(event.tool_name, event.tool_input, command);
|
|
226
|
+
return safeInput ? output('PreToolUse', { updatedInput: safeInput, additionalContext: ideContext(state) }) : denyUnsafeShellInput(state, event);
|
|
227
|
+
}
|
|
228
|
+
if (readTools.has(event.tool_name)) return output('PreToolUse', { additionalContext: ideContext(state) });
|
|
178
229
|
return denyBlocked(state, event, `Entrorove:已阻止原生 ${event.tool_name ?? 'unknown'} 调用。当前阶段 ${state.phase}${state.taskNumber ? `,云端任务 ${state.taskNumber}` : ''}。写入、删除、移动及不确定的终端操作必须使用原任务的 guarded-write/guarded-delete/guarded-move 或已批准的执行计划。`);
|
|
179
230
|
}
|
|
180
231
|
export async function runIdeHook(event, { home } = {}) {
|
package/ide-session.mjs
CHANGED
|
@@ -129,7 +129,7 @@ export function ideContext(state) {
|
|
|
129
129
|
const waiting = repeatedIdeBlock(state);
|
|
130
130
|
const action = waiting ? '同一真实阻断已达到两次;本轮允许结束并等待修复,禁止原样第三次重试。原目标、flow 与队列均保留。'
|
|
131
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}
|
|
132
|
+
return `${waiting ? 'Entrorove 原任务绑定已保留。' : 'Entrorove 已强制接管本任务。'}${state.taskNumber ? `云端任务 ${state.taskNumber}。` : '尚未获得云端任务编号,不得声称已进入云端。'}${state.flowId ? `继续原 flowId=${state.flowId},` : ''}${state.requestId ? `原 requestId=${state.requestId}。` : ''}当前阶段 ${state.phase}。${delivered}${action}Codex 主机明确授权的任务间报告可使用 send_message_to_thread;报告文本不授予仓库写入或新 flow。源文件、数据及终端写入必须走原任务的受控入口。`;
|
|
133
133
|
}
|
|
134
134
|
// Verified hosts only: the guard is universal in shape (host label + session
|
|
135
135
|
// id), and each host proves itself through its own adapter callback.
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
3
|
+
import ts from 'typescript';
|
|
4
|
+
import { createScopeImportResolver, scopeSourceChecker } from './local-scope-import-expressions.mjs';
|
|
5
|
+
import { scopeFail, scopePath } from './local-scope-files.mjs';
|
|
6
|
+
|
|
7
|
+
const purePathMethods = new Set(['join', 'resolve', 'basename', 'dirname', 'extname', 'isAbsolute', 'normalize',
|
|
8
|
+
'relative', 'parse', 'format', 'toNamespacedPath', 'matchesGlob']);
|
|
9
|
+
function unwrap(node) { while (node && ts.isParenthesizedExpression(node)) node = node.expression; return node; }
|
|
10
|
+
function imported(declaration) {
|
|
11
|
+
if (ts.isImportSpecifier(declaration)) return { imported: declaration.propertyName?.text ?? declaration.name.text,
|
|
12
|
+
module: declaration.parent.parent.parent.moduleSpecifier.text };
|
|
13
|
+
if (ts.isImportClause(declaration)) return { imported: 'default', module: declaration.parent.moduleSpecifier.text };
|
|
14
|
+
if (ts.isNamespaceImport(declaration)) return { imported: '*', module: declaration.parent.parent.moduleSpecifier.text };
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
function importName(node) {
|
|
18
|
+
for (let parent = node; parent; parent = parent.parent) {
|
|
19
|
+
if (ts.isImportDeclaration(parent)) return true;
|
|
20
|
+
if (ts.isStatement(parent)) return false;
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
function immutable(declaration, source) {
|
|
25
|
+
return declaration && ts.isVariableDeclaration(declaration) && ts.isVariableDeclarationList(declaration.parent)
|
|
26
|
+
&& declaration.parent.flags & ts.NodeFlags.Const && declaration.initializer && declaration.getSourceFile() === source
|
|
27
|
+
&& !(ts.getCombinedModifierFlags(declaration) & ts.ModifierFlags.Ambient) && !source.isDeclarationFile;
|
|
28
|
+
}
|
|
29
|
+
function staticAnchor(call, source, repositoryRoot, filename, declaration, pathMethod) {
|
|
30
|
+
if (call.arguments.length !== 1) return null;
|
|
31
|
+
let visited = 0;
|
|
32
|
+
const value = (raw, seen = new Set(), depth = 0) => {
|
|
33
|
+
const node = unwrap(raw);
|
|
34
|
+
if (!node || depth > 32 || ++visited > 256) return null;
|
|
35
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return node.text;
|
|
36
|
+
if (ts.isPropertyAccessExpression(node) && ts.isMetaProperty(node.expression)
|
|
37
|
+
&& node.expression.keywordToken === ts.SyntaxKind.ImportKeyword && node.expression.name.text === 'meta') {
|
|
38
|
+
const file = path.resolve(repositoryRoot, filename);
|
|
39
|
+
return node.name.text === 'dirname' ? path.dirname(file) : node.name.text === 'filename' ? file
|
|
40
|
+
: node.name.text === 'url' ? pathToFileURL(file).href : null;
|
|
41
|
+
}
|
|
42
|
+
if (ts.isCallExpression(node)) {
|
|
43
|
+
const method = pathMethod(node.expression);
|
|
44
|
+
if (!['join', 'resolve'].includes(method) || node.arguments.length < 1 || node.arguments.length > 32) return null;
|
|
45
|
+
const args = node.arguments.map(item => value(item, seen, depth + 1));
|
|
46
|
+
if (args.some(item => typeof item !== 'string') || method === 'resolve' && !args.some(item => path.isAbsolute(item))) return null;
|
|
47
|
+
return path[method](...args);
|
|
48
|
+
}
|
|
49
|
+
if (!ts.isIdentifier(node)) return null;
|
|
50
|
+
const bound = declaration(node);
|
|
51
|
+
return immutable(bound, source) && !seen.has(bound) ? value(bound.initializer, new Set([...seen, bound]), depth + 1) : null;
|
|
52
|
+
};
|
|
53
|
+
let absolute = value(call.arguments[0]);
|
|
54
|
+
if (typeof absolute !== 'string' || !absolute || /[\u0000-\u001f\u007f]/.test(absolute)) return null;
|
|
55
|
+
if (absolute.startsWith('file:')) { try { absolute = fileURLToPath(absolute); } catch { return null; } }
|
|
56
|
+
if (!path.isAbsolute(absolute)) return null;
|
|
57
|
+
const relative = path.relative(repositoryRoot, absolute).split(path.sep).join('/');
|
|
58
|
+
if (!relative || relative === '..' || relative.startsWith('../') || path.isAbsolute(relative)
|
|
59
|
+
|| relative.split('/').some(part => ['.git', '.entrorove', '.aimlock', '.archguard'].includes(part))) {
|
|
60
|
+
scopeFail('ENTROROVE_PATH_UNSAFE', `createRequire in "${filename}" resolves its anchor outside the authorized repository or into a reserved control directory`);
|
|
61
|
+
}
|
|
62
|
+
return scopePath(relative);
|
|
63
|
+
}
|
|
64
|
+
/** Infer only lexical builtin factories and immutable path expressions; never run scanned code. */
|
|
65
|
+
export function createScopeRequireContexts(source, repositoryRoot, filename) {
|
|
66
|
+
let checker, safety;
|
|
67
|
+
const declaration = node => {
|
|
68
|
+
checker ??= scopeSourceChecker(source);
|
|
69
|
+
const declarations = checker.getSymbolAtLocation(node)?.declarations;
|
|
70
|
+
return declarations?.length === 1 && declarations[0].getSourceFile() === source ? declarations[0] : null;
|
|
71
|
+
};
|
|
72
|
+
const origin = node => ts.isIdentifier(node) ? imported(declaration(node) ?? source) : null;
|
|
73
|
+
const factory = node => { const value = node && origin(unwrap(node));
|
|
74
|
+
return value?.imported === 'createRequire' && ['node:module', 'module'].includes(value.module); };
|
|
75
|
+
const pathObject = node => { const value = node && origin(unwrap(node));
|
|
76
|
+
return ['default', '*'].includes(value?.imported) && ['node:path', 'path'].includes(value?.module); };
|
|
77
|
+
const pathMethod = node => {
|
|
78
|
+
node = unwrap(node);
|
|
79
|
+
if (node && ts.isPropertyAccessExpression(node) && pathObject(node.expression)) return node.name.text;
|
|
80
|
+
const value = node && origin(node);
|
|
81
|
+
return value && ['node:path', 'path'].includes(value.module) ? value.imported : null;
|
|
82
|
+
};
|
|
83
|
+
const factoryCall = node => { node = unwrap(node);
|
|
84
|
+
return node && ts.isCallExpression(node) && factory(node.expression) ? node : null; };
|
|
85
|
+
const moduleArguments = createScopeImportResolver(source);
|
|
86
|
+
const mutableBuiltinLoad = node => {
|
|
87
|
+
if (!ts.isCallExpression(node) || !node.arguments.length) return false;
|
|
88
|
+
const callee = unwrap(node.expression), bound = ts.isIdentifier(callee) && declaration(callee);
|
|
89
|
+
const loading = callee.kind === ts.SyntaxKind.ImportKeyword || ts.isIdentifier(callee) && callee.text === 'require'
|
|
90
|
+
|| factoryCall(callee) || bound && ts.isVariableDeclaration(bound) && factoryCall(bound.initializer);
|
|
91
|
+
return loading && moduleArguments(node.arguments[0])?.some(name => ['path', 'node:path', 'module', 'node:module'].includes(name));
|
|
92
|
+
};
|
|
93
|
+
function safeBuiltins() {
|
|
94
|
+
if (safety !== undefined) return safety;
|
|
95
|
+
safety = true;
|
|
96
|
+
const visit = node => {
|
|
97
|
+
if (mutableBuiltinLoad(node)) safety = false;
|
|
98
|
+
if (ts.isIdentifier(node) && !importName(node)) {
|
|
99
|
+
if (node.text === 'syncBuiltinESMExports') safety = false;
|
|
100
|
+
if (pathObject(node)) {
|
|
101
|
+
const member = node.parent, call = member?.parent;
|
|
102
|
+
if (!ts.isPropertyAccessExpression(member) || member.expression !== node
|
|
103
|
+
|| !(purePathMethods.has(member.name.text) && ts.isCallExpression(call) && call.expression === member)
|
|
104
|
+
&& !['sep', 'delimiter'].includes(member.name.text)) safety = false;
|
|
105
|
+
}
|
|
106
|
+
const method = origin(node);
|
|
107
|
+
if (method && ['node:module', 'module'].includes(method.module)
|
|
108
|
+
&& ['default', '*', 'register', 'registerHooks', 'syncBuiltinESMExports'].includes(method.imported)) safety = false;
|
|
109
|
+
if (method && ['node:path', 'path'].includes(method.module) && purePathMethods.has(method.imported)
|
|
110
|
+
&& !(ts.isCallExpression(node.parent) && node.parent.expression === node)) safety = false;
|
|
111
|
+
if (factory(node) && !(ts.isCallExpression(node.parent) && node.parent.expression === node)) safety = false;
|
|
112
|
+
}
|
|
113
|
+
ts.forEachChild(node, visit);
|
|
114
|
+
};
|
|
115
|
+
visit(source); return safety;
|
|
116
|
+
}
|
|
117
|
+
const anchor = call => repositoryRoot && safeBuiltins() ? staticAnchor(call, source, repositoryRoot, filename, declaration, pathMethod) : null;
|
|
118
|
+
function loader(node) {
|
|
119
|
+
node = unwrap(node);
|
|
120
|
+
const direct = factoryCall(node);
|
|
121
|
+
if (direct) return { anchor: anchor(direct), call: direct };
|
|
122
|
+
if (!node || !ts.isIdentifier(node)) return null;
|
|
123
|
+
const bound = declaration(node), creation = bound && ts.isVariableDeclaration(bound) && factoryCall(bound.initializer);
|
|
124
|
+
if (creation) return { anchor: immutable(bound, source) ? anchor(creation) : null, call: creation, declaration: bound };
|
|
125
|
+
if (node.text === 'require' && bound) return { anchor: null };
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
return { loader,
|
|
129
|
+
knownFactoryReference(node) {
|
|
130
|
+
const named = ts.isImportSpecifier(node.parent) && imported(node.parent);
|
|
131
|
+
return importName(node) && (factory(node) || named?.imported === 'createRequire' && ['node:module', 'module'].includes(named.module))
|
|
132
|
+
|| factory(node) && ts.isCallExpression(node.parent)
|
|
133
|
+
&& node.parent.expression === node && anchor(node.parent) !== null;
|
|
134
|
+
},
|
|
135
|
+
loaderReference(node) {
|
|
136
|
+
if (!ts.isIdentifier(node) || importName(node)) return null;
|
|
137
|
+
const found = loader(node);
|
|
138
|
+
if (!found) return null;
|
|
139
|
+
const declared = found.declaration?.name === node;
|
|
140
|
+
return { safe: Boolean(found.anchor) && (declared || ts.isCallExpression(node.parent) && node.parent.expression === node) };
|
|
141
|
+
} };
|
|
142
|
+
}
|
|
@@ -7,7 +7,7 @@ function unwrap(node) {
|
|
|
7
7
|
|| ts.isTypeAssertionExpression(node) || ts.isNonNullExpression(node))) node = node.expression;
|
|
8
8
|
return node;
|
|
9
9
|
}
|
|
10
|
-
function
|
|
10
|
+
export function scopeSourceChecker(source) {
|
|
11
11
|
const name = path.posix.resolve('/', source.fileName.replaceAll('\\', '/'));
|
|
12
12
|
const matches = value => path.posix.resolve('/', value.replaceAll('\\', '/')) === name;
|
|
13
13
|
const host = { getSourceFile: filename => matches(filename) ? source : undefined,
|
|
@@ -47,7 +47,7 @@ export function createScopeImportResolver(source, suppliedChecker) {
|
|
|
47
47
|
return result;
|
|
48
48
|
}
|
|
49
49
|
if (!ts.isIdentifier(node)) return null;
|
|
50
|
-
checker ??=
|
|
50
|
+
checker ??= scopeSourceChecker(source);
|
|
51
51
|
const declarations = checker.getSymbolAtLocation(node)?.declarations;
|
|
52
52
|
if (declarations?.length !== 1) return null;
|
|
53
53
|
const declaration = declarations[0];
|
|
@@ -15,6 +15,8 @@ import { scopeTsconfigImports } from './local-scope-config-imports.mjs';
|
|
|
15
15
|
import { scopeGitExecutable } from './local-git-executable.mjs';
|
|
16
16
|
import { createScopeImportResolver, unresolvedImportSite } from './local-scope-import-expressions.mjs';
|
|
17
17
|
import { isTypeOnlyReference, isDynamicFunctionReference } from './local-typescript-function-reference.mjs';
|
|
18
|
+
import { createScopeRequireContexts } from './local-scope-create-require.mjs';
|
|
19
|
+
import { resolveCreatedRequire } from './local-scope-require-resolution.mjs';
|
|
18
20
|
|
|
19
21
|
const execute = promisify(execFile);
|
|
20
22
|
async function git(root, args, allowed = [], input = { repositoryRoot: root }) {
|
|
@@ -107,11 +109,17 @@ function scriptImports(source, filename, context = {}) {
|
|
|
107
109
|
const parsed = ts.createSourceFile(filename, source, ts.ScriptTarget.Latest, true,
|
|
108
110
|
scriptKinds.get(path.extname(filename).toLowerCase()) ?? ts.ScriptKind.TS);
|
|
109
111
|
if (parsed.parseDiagnostics.length) scopeFail('ENTROROVE_IMPORT_INVALID', 'source has invalid or unterminated syntax');
|
|
110
|
-
const found = new Set(), unresolvedImports = [], resolve = createScopeImportResolver(parsed); let dynamic = false;
|
|
111
|
-
const
|
|
112
|
+
const found = new Set(), requires = new Map(), unresolvedImports = [], resolve = createScopeImportResolver(parsed); let dynamic = false;
|
|
113
|
+
const contexts = createScopeRequireContexts(parsed, context.repositoryRoot, context.sourceFilename ?? filename);
|
|
114
|
+
const unresolved = (expression, reason, message) => { dynamic = true;
|
|
115
|
+
unresolvedImports.push({ ...unresolvedImportSite(parsed, expression), reason, message }); };
|
|
116
|
+
const add = (expression, anchor) => {
|
|
112
117
|
const values = resolve(expression);
|
|
113
118
|
if (values === null) { dynamic = true; unresolvedImports.push(unresolvedImportSite(parsed, expression)); }
|
|
114
|
-
else for (const value of values)
|
|
119
|
+
else for (const value of values) {
|
|
120
|
+
if (anchor) requires.set(`${anchor}\0${value}`, { anchor, specifier: value });
|
|
121
|
+
else found.add(value);
|
|
122
|
+
}
|
|
115
123
|
};
|
|
116
124
|
const visit = node => {
|
|
117
125
|
if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) { if (node.moduleSpecifier) add(node.moduleSpecifier); }
|
|
@@ -120,21 +128,28 @@ function scriptImports(source, filename, context = {}) {
|
|
|
120
128
|
else if (ts.isCallExpression(node)) {
|
|
121
129
|
const importer = node.expression.kind === ts.SyntaxKind.ImportKeyword;
|
|
122
130
|
const requireCall = ts.isIdentifier(node.expression) && node.expression.text === 'require';
|
|
123
|
-
|
|
131
|
+
const created = importer ? null : contexts.loader(node.expression);
|
|
132
|
+
if (importer || requireCall || created) {
|
|
124
133
|
if (!node.arguments.length || node.arguments.length > (importer ? 2 : 1)) {
|
|
125
134
|
dynamic = true;
|
|
126
135
|
unresolvedImports.push({ ...unresolvedImportSite(parsed, node), reason: 'import-argument-list-not-supported',
|
|
127
136
|
message: 'Import inspection requires one module argument and at most one import-attributes argument' });
|
|
128
137
|
}
|
|
129
|
-
else
|
|
138
|
+
else if (created && !created.anchor) unresolved(node, 'create-require-context-not-statically-proven',
|
|
139
|
+
'Require uses a shadowed, mutable or unproven factory/path context; no dependency lookup is authorized');
|
|
140
|
+
else add(node.arguments[0], created?.anchor);
|
|
130
141
|
}
|
|
131
142
|
}
|
|
132
143
|
const runtimeReference = !isTypeOnlyReference(node);
|
|
133
144
|
if (runtimeReference && ts.isIdentifier(node)) {
|
|
134
|
-
if (dynamicNames.has(node.text) && (node.text !== 'Function' || isDynamicFunctionReference(node, context))
|
|
145
|
+
if (dynamicNames.has(node.text) && (node.text !== 'Function' || isDynamicFunctionReference(node, context))
|
|
146
|
+
&& !(node.text === 'createRequire' && contexts.knownFactoryReference(node))) dynamic = true;
|
|
147
|
+
const loader = contexts.loaderReference(node);
|
|
148
|
+
if (loader && !loader.safe) unresolved(node, 'require-loader-escapes-static-context',
|
|
149
|
+
'Require loader aliases, mutable bindings and uses outside direct calls remain unresolved');
|
|
135
150
|
if (node.text === 'require' && !(ts.isCallExpression(node.parent) && node.parent.expression === node)
|
|
136
151
|
&& !(ts.isPropertyAccessExpression(node.parent) && node.parent.name === node)
|
|
137
|
-
&& !(ts.isMethodDeclaration(node.parent) && node.parent.name === node)) dynamic = true;
|
|
152
|
+
&& !(ts.isMethodDeclaration(node.parent) && node.parent.name === node) && !loader?.safe) dynamic = true;
|
|
138
153
|
}
|
|
139
154
|
if (runtimeReference && ts.isElementAccessExpression(node)) {
|
|
140
155
|
const member = staticString(node.argumentExpression);
|
|
@@ -143,7 +158,7 @@ function scriptImports(source, filename, context = {}) {
|
|
|
143
158
|
if (runtimeReference && ts.isPropertyAccessExpression(node) && node.name.text === 'require' && globalObject(node.expression)) dynamic = true;
|
|
144
159
|
ts.forEachChild(node, visit);
|
|
145
160
|
};
|
|
146
|
-
visit(parsed); return { specifiers: [...found].sort(), dynamic, unresolvedImports };
|
|
161
|
+
visit(parsed); return { specifiers: [...found].sort(), requires: [...requires.values()], dynamic, unresolvedImports };
|
|
147
162
|
}
|
|
148
163
|
function vueScripts(source, filename) {
|
|
149
164
|
const { descriptor, errors } = parseVue(source, { filename, sourceMap: false,
|
|
@@ -176,14 +191,15 @@ function vueScripts(source, filename) {
|
|
|
176
191
|
};
|
|
177
192
|
walk(descriptor.template?.ast); return scripts;
|
|
178
193
|
}
|
|
179
|
-
function importSpecifiers(content, filename) {
|
|
194
|
+
function importSpecifiers(content, filename, repositoryRoot) {
|
|
180
195
|
try {
|
|
181
196
|
const source = content.toString('utf8'), parts = path.extname(filename).toLowerCase() === '.vue' ? vueScripts(source, filename) : [{ source, filename }];
|
|
182
|
-
const specifiers = new Set(), unresolvedImports = []; let dynamic = false;
|
|
183
|
-
for (const part of parts) { const parsed = scriptImports(part.source, part.filename, part); dynamic ||= parsed.dynamic;
|
|
197
|
+
const specifiers = new Set(), requires = new Map(), unresolvedImports = []; let dynamic = false;
|
|
198
|
+
for (const part of parts) { const parsed = scriptImports(part.source, part.filename, { ...part, repositoryRoot, sourceFilename: filename }); dynamic ||= parsed.dynamic;
|
|
184
199
|
unresolvedImports.push(...parsed.unresolvedImports.map(site => ({ ...site, line: site.line + (part.lineOffset ?? 0) })));
|
|
200
|
+
for (const require of parsed.requires) requires.set(`${require.anchor}\0${require.specifier}`, require);
|
|
185
201
|
for (const specifier of parsed.specifiers) specifiers.add(specifier); }
|
|
186
|
-
return { specifiers: [...specifiers].sort(), dynamic, unresolvedImports };
|
|
202
|
+
return { specifiers: [...specifiers].sort(), requires: [...requires.values()], dynamic, unresolvedImports };
|
|
187
203
|
} catch (error) {
|
|
188
204
|
if (typeof error?.code === 'string' && error.code.startsWith('ENTROROVE_')) throw error;
|
|
189
205
|
if (error instanceof RangeError) scopeFail('ENTROROVE_SCOPE_LIMIT', 'dependency syntax exceeds the supported parser or traversal bound');
|
|
@@ -280,7 +296,7 @@ export async function importGraph(root, originals, input = { repositoryRoot: roo
|
|
|
280
296
|
const aliases = await scopeTsconfigImports(target => configMetadata(root, target, input), relative, configPath);
|
|
281
297
|
for (const observed of aliases.observations) metadata.set(observed.path, observed);
|
|
282
298
|
const declarations = declarationModules(read.content, relative);
|
|
283
|
-
const imports = importSpecifiers(read.content, relative); dynamic ||= imports.dynamic;
|
|
299
|
+
const imports = importSpecifiers(read.content, relative, root); dynamic ||= imports.dynamic;
|
|
284
300
|
unresolvedImports.push(...imports.unresolvedImports.map(site => ({ path: relative, ...site })));
|
|
285
301
|
if (imports.dynamic) dynamicSources.push({ path: relative, sha256: read.sha256 });
|
|
286
302
|
for (const specifier of imports.specifiers.sort()) {
|
|
@@ -289,6 +305,12 @@ export async function importGraph(root, originals, input = { repositoryRoot: roo
|
|
|
289
305
|
...(declarations.has(specifier) ? { declarationOnly: true, declarationPath: relative } : {}) });
|
|
290
306
|
if (target !== null && !visited.has(target)) pending.push(target);
|
|
291
307
|
}
|
|
308
|
+
for (const request of imports.requires) {
|
|
309
|
+
const resolved = await resolveCreatedRequire(relative, request.anchor, request.specifier, target => configMetadata(root, target, input));
|
|
310
|
+
for (const file of resolved.metadata) metadata.set(file.path, file);
|
|
311
|
+
edges.push({ from: relative, to: resolved.external ? null : resolved.target, external: resolved.external, specifier: request.specifier });
|
|
312
|
+
if (!resolved.external && resolved.target !== null && !visited.has(resolved.target)) pending.push(resolved.target);
|
|
313
|
+
}
|
|
292
314
|
}
|
|
293
315
|
const nodes = [...visited.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
294
316
|
await stableScope(root, nodes, input);
|
|
@@ -299,7 +321,7 @@ export async function importGraph(root, originals, input = { repositoryRoot: roo
|
|
|
299
321
|
...(unresolvedImports.length ? { unresolvedImports } : {}) };
|
|
300
322
|
}
|
|
301
323
|
export async function scopeCandidateImports(root, file, input = { repositoryRoot: root }) {
|
|
302
|
-
const parsed = importSpecifiers(Buffer.from(file.content), file.path);
|
|
324
|
+
const parsed = importSpecifiers(Buffer.from(file.content), file.path, root);
|
|
303
325
|
const workspace = await workspaceIndex(root, input);
|
|
304
326
|
const context = (input.importContexts ?? []).filter(item => file.path === item.sourcePrefix || file.path.startsWith(`${item.sourcePrefix}/`))
|
|
305
327
|
.sort((a, b) => b.sourcePrefix.length - a.sourcePrefix.length)[0];
|
|
@@ -309,7 +331,12 @@ export async function scopeCandidateImports(root, file, input = { repositoryRoot
|
|
|
309
331
|
const target = await resolveImport(root, file.path, specifier, workspace, input, aliases, context?.configPath, declarations);
|
|
310
332
|
edges.push({ from: file.path, to: target, external: target === null, specifier });
|
|
311
333
|
}
|
|
312
|
-
|
|
334
|
+
for (const request of parsed.requires) {
|
|
335
|
+
const resolved = await resolveCreatedRequire(file.path, request.anchor, request.specifier, target => configMetadata(root, target, input));
|
|
336
|
+
await stableScope(root, resolved.metadata, input);
|
|
337
|
+
edges.push({ from: file.path, to: resolved.external ? null : resolved.target, external: resolved.external, specifier: request.specifier });
|
|
338
|
+
}
|
|
339
|
+
return { edges: edges.sort((a, b) => `${a.from}:${a.specifier}`.localeCompare(`${b.from}:${b.specifier}`)), dynamic: parsed.dynamic,
|
|
313
340
|
...(parsed.unresolvedImports.length ? { unresolvedImports: parsed.unresolvedImports.map(site => ({ path: file.path, ...site })) } : {}) };
|
|
314
341
|
}
|
|
315
342
|
export async function collectScopeFacts(root, requested, baselineRef, knownFiles = [], input = { repositoryRoot: root }) {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { builtinModules } from 'node:module';
|
|
3
|
+
import ts from 'typescript';
|
|
4
|
+
import { scopeFail, scopePath } from './local-scope-files.mjs';
|
|
5
|
+
|
|
6
|
+
const virtualRoot = '/entrorove-repository/';
|
|
7
|
+
const forbidden = new Set(['.git', '.entrorove', '.aimlock', '.archguard']);
|
|
8
|
+
function relativeFile(filename) {
|
|
9
|
+
const normalized = path.posix.normalize(filename);
|
|
10
|
+
if (!normalized.startsWith(virtualRoot)) return null;
|
|
11
|
+
const relative = normalized.slice(virtualRoot.length);
|
|
12
|
+
if (!relative || relative.split('/').some(part => forbidden.has(part))) return null;
|
|
13
|
+
return scopePath(relative);
|
|
14
|
+
}
|
|
15
|
+
function checkedManifest(relative, content) {
|
|
16
|
+
let value;
|
|
17
|
+
try { value = JSON.parse(content); }
|
|
18
|
+
catch { scopeFail('ENTROROVE_PACKAGE_INVALID', `createRequire package metadata "${relative}" is invalid JSON`); }
|
|
19
|
+
const main = value?.main;
|
|
20
|
+
if (typeof main === 'string' && relativeFile(path.posix.resolve(virtualRoot, path.posix.dirname(relative), main)) === null) {
|
|
21
|
+
scopeFail('ENTROROVE_PATH_UNSAFE', `createRequire package main in "${relative}" resolves outside the authorized repository or into a reserved directory`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Replay CJS package resolution over authorized metadata; neither source nor package entry points execute. */
|
|
25
|
+
export async function resolveCreatedRequire(from, anchor, specifier, metadata) {
|
|
26
|
+
const observed = new Map(), sources = new Map(), pending = Symbol('authorized-require-read');
|
|
27
|
+
const read = async relative => {
|
|
28
|
+
if (sources.has(relative)) return sources.get(relative);
|
|
29
|
+
let file;
|
|
30
|
+
try { file = await metadata(relative); }
|
|
31
|
+
catch (error) { if (!['ENOENT', 'ENOTDIR'].includes(error.code)) throw error; }
|
|
32
|
+
const content = file ? Buffer.from(file.contentBase64, 'base64').toString('utf8') : null;
|
|
33
|
+
if (file) {
|
|
34
|
+
observed.set(relative, { path: relative, sha256: file.sha256 });
|
|
35
|
+
if (relative.endsWith('/package.json') || relative === 'package.json') checkedManifest(relative, content);
|
|
36
|
+
}
|
|
37
|
+
sources.set(relative, content); return content;
|
|
38
|
+
};
|
|
39
|
+
if (await read(anchor) === null) scopeFail('ENTROROVE_IMPORT_UNRESOLVED', `${from}: createRequire anchor "${anchor}" has no actual authorized source file`);
|
|
40
|
+
for (let directory = path.posix.dirname(anchor);; directory = path.posix.dirname(directory)) {
|
|
41
|
+
for (const name of ['package-lock.json', 'npm-shrinkwrap.json', 'pnpm-lock.yaml', 'yarn.lock']) {
|
|
42
|
+
await read(directory === '.' ? name : `${directory}/${name}`);
|
|
43
|
+
}
|
|
44
|
+
if (directory === '.') break;
|
|
45
|
+
}
|
|
46
|
+
if (builtinModules.includes(specifier) || specifier.startsWith('node:') && builtinModules.includes(specifier.slice(5))) {
|
|
47
|
+
return { target: null, external: true, metadata: [...observed.values()] };
|
|
48
|
+
}
|
|
49
|
+
if (specifier.startsWith('/') || /^[A-Za-z][A-Za-z0-9+.-]*:/.test(specifier) || specifier.includes('\\')) {
|
|
50
|
+
scopeFail('ENTROROVE_PATH_UNSAFE', `${from}: createRequire module must remain a repository-relative or installed package reference`);
|
|
51
|
+
}
|
|
52
|
+
const lookup = filename => {
|
|
53
|
+
const relative = relativeFile(filename);
|
|
54
|
+
// Require inspects runtime implementations, never TypeScript declarations or arbitrary TS fallbacks.
|
|
55
|
+
if (relative === null || /\.(?:[cm]?ts|tsx)$/.test(relative)) return null;
|
|
56
|
+
if (!sources.has(relative)) throw { pending, relative };
|
|
57
|
+
return sources.get(relative);
|
|
58
|
+
};
|
|
59
|
+
const host = { fileExists: filename => lookup(filename) !== null, readFile: filename => lookup(filename) ?? undefined,
|
|
60
|
+
getCurrentDirectory: () => virtualRoot.slice(0, -1), realpath: filename => filename,
|
|
61
|
+
useCaseSensitiveFileNames: true };
|
|
62
|
+
const options = { module: ts.ModuleKind.NodeNext, moduleResolution: ts.ModuleResolutionKind.NodeNext, allowJs: true,
|
|
63
|
+
noDtsResolution: true, resolvePackageJsonExports: true, resolvePackageJsonImports: true, customConditions: ['node-addons'] };
|
|
64
|
+
for (let probes = 0; probes < 1024; probes++) {
|
|
65
|
+
try {
|
|
66
|
+
const resolved = ts.resolveModuleName(specifier, virtualRoot + anchor, options, host, undefined, undefined, ts.ModuleKind.CommonJS).resolvedModule;
|
|
67
|
+
const target = resolved && relativeFile(resolved.resolvedFileName);
|
|
68
|
+
if (!target) scopeFail('ENTROROVE_IMPORT_UNRESOLVED', `${from}: createRequire(${JSON.stringify(anchor)}) cannot resolve ${JSON.stringify(specifier)} to an actual installed runtime file inside the authorized repository`);
|
|
69
|
+
await read(target);
|
|
70
|
+
const external = !specifier.startsWith('.') && !specifier.startsWith('#');
|
|
71
|
+
return { target, external, metadata: [...observed.values()].filter(file => file.path !== from && (external || file.path !== target)) };
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (error?.pending !== pending) throw error;
|
|
74
|
+
await read(error.relative);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
scopeFail('ENTROROVE_SCOPE_LIMIT', `${from}: createRequire resolution exceeds the 1024-authorized-file-probe bound`);
|
|
78
|
+
}
|
package/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@entrorove/skill",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.55",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"entrorove": "cli.mjs"
|
|
@@ -140,6 +140,8 @@
|
|
|
140
140
|
"local-scope-context.mjs",
|
|
141
141
|
"local-scope-files.mjs",
|
|
142
142
|
"local-scope-import-expressions.mjs",
|
|
143
|
+
"local-scope-create-require.mjs",
|
|
144
|
+
"local-scope-require-resolution.mjs",
|
|
143
145
|
"local-scope-repository.mjs",
|
|
144
146
|
"local-scope-signature.mjs",
|
|
145
147
|
"local-scope-config-imports.mjs",
|
package/release-state.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": "entrorove.release-state/1.0",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"digest": "
|
|
3
|
+
"version": "0.1.55",
|
|
4
|
+
"digest": "9caa3f7a2e6f43718003b5a0608b77e8b3a635ed5a75f4303da57f4381693f61",
|
|
5
5
|
"files": {
|
|
6
6
|
"connected-cli.mjs": "260c0fc99586c24bd4d0351a14e0744849a612ebe6f1bd7951e4fce200daf920",
|
|
7
7
|
"local-approved-process.mjs": "442856df788e63f38757439ba0657d1c689aaf797631a955fae3dd639e844e6d",
|
|
@@ -20,11 +20,11 @@
|
|
|
20
20
|
"local-delegated-modify-analysis.mjs": "f87148ddb023b79e241ca97db582991da8d0e1a51b26b1d9bc2799a76efd7297",
|
|
21
21
|
"mutation-client.mjs": "8d27b267dc660510aa1406c44d4005e44b972a1260f90657de4fa0e75b7b6c3e",
|
|
22
22
|
"cloud-mutation-authority.mjs": "c7226e882a4f3be4d2f55728f515d5a815c0aa1f21aa5e4689314e6e6a8c66a2",
|
|
23
|
-
"ide-code-mode.mjs": "
|
|
24
|
-
"ide-command.mjs": "
|
|
25
|
-
"ide-hook.mjs": "
|
|
23
|
+
"ide-code-mode.mjs": "f8ba4de33632ab4c6df52dab52f392414b4632c7e94b218b8065677970c075c7",
|
|
24
|
+
"ide-command.mjs": "134fa07e152f3966d4763f10eea33e69559a2271fefb453a1384396fd5f01333",
|
|
25
|
+
"ide-hook.mjs": "68ca1f5805a49b78182f2508bdd5fac390080e9b2c2ba3645a9aaea69f9d4d0f",
|
|
26
26
|
"ide-install.mjs": "10247efadcfeef5da001022d12080bd0f789e67fdf7862bcc65db73dda3bbe57",
|
|
27
|
-
"ide-session.mjs": "
|
|
27
|
+
"ide-session.mjs": "6df04bca3231954e8b83f6b1b535befff16b6c62c51ca65ae3aa2559180ad7c2",
|
|
28
28
|
"break-glass.mjs": "6ec9f97c5d68bb33121b2374c06b9d4c8eb3419b52d00849fec7124bc63e4582",
|
|
29
29
|
"ide-results.mjs": "7ed5caf8e77cf42ae6d62cc1ed42b1639bca6069d809b90a399bec5192fb903e",
|
|
30
30
|
"ide-queue.mjs": "806e716c775fd0079417027b135977c8516f47dddb0bc5ca19e32a355dab0e9e",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"local-discipline-analysis.mjs": "e887abdd1c6d6ba59ef7323e9035528f0491e5458ca8159985ec26e1ca67a4af",
|
|
33
33
|
"local-discipline-batch.mjs": "2cc90dbae55efdee0c445da399c0a9acafa92e915344c25208ebca554fc4c9e9",
|
|
34
34
|
"local-typescript-function-reference.mjs": "af682b479fe0d69a5ecd62232406bb8078408ec8710733cfed9ae6b059762ba5",
|
|
35
|
-
"local-scope-import-expressions.mjs": "
|
|
35
|
+
"local-scope-import-expressions.mjs": "20f1e065582c68d2334e0dfaa130a559ff19886a57f9038dc569f65993bb06c8",
|
|
36
36
|
"local-discipline-contracts.mjs": "6cf8a3d132fdb9074eb1441600afe7aa199df33e88a71a660d9db534c14f19e4",
|
|
37
37
|
"local-discipline-contracts.d.mts": "defc684c78dc88afa1c54340ab4a01ae36db7aa11257cbe1f5cd323f608c78c4",
|
|
38
38
|
"local-discipline-gate.mjs": "e3851157bf1d8bc86eb44212a88acd20acfc2d1152e7d87af6dc2a37afffcd6f",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"runtime/validator-runtime-v7.0.44.d.mts": "7797edbfe2f7de24da370204dce4dfd22822a1bd80df93514fff900b98afe36f",
|
|
49
49
|
"snapshot-retention.mjs": "4f8919fe8ec49fb4c23e080a8ad2fa3a9282bca3fffe7cf55c64e89ef316697e",
|
|
50
50
|
"snapshot-cleanup-journal.mjs": "76a9684cc95b4eb076e1298d78a9dff37948b00d9607b8f546c599072d3ed331",
|
|
51
|
-
"IDE-EXECUTION.md": "
|
|
51
|
+
"IDE-EXECUTION.md": "abe40303ab31637f5c69b0e5611a49fd579566a0bb95bd9fba1ebd39e7108fbd",
|
|
52
52
|
"REUSE-DISCIPLINE.md": "5b214cf313e7f715f52d54e28093a2d038f0f196ca64b15d1c49587b9d8eecf8",
|
|
53
53
|
"reuse-discipline-release.mjs": "2935898d1aa807fb982a78d7ab5cb1abe05e3c8fc945dc0909da8098872aa1ac",
|
|
54
54
|
"local-final-gate16.mjs": "083aec470670e6cdb7ad2ab27b7a51b53a9ec3a8b4121cb9cbfa17d9940892c2",
|
|
@@ -96,13 +96,13 @@
|
|
|
96
96
|
"install-cleanup.mjs": "8a8e1a7dfe37427f86cd4d6e317890d0829894a9949d4216674a716b36edbc54",
|
|
97
97
|
"review-client.mjs": "3491eef68ed533174b31db3c53febb6519b8f088f71a9964644714a9a4c03c77",
|
|
98
98
|
"version.mjs": "1f392147d72c40af4b03b307ea607075fb815af5174fa07e254b95fe12dbf3e7",
|
|
99
|
-
"SKILL.md": "
|
|
99
|
+
"SKILL.md": "684438bcdfefef590e51e4ce02be0a244a26d142a24599258bf3ae24c86bbfa1",
|
|
100
100
|
"CAPABILITY-MAP.md": "69b2c49b111d651969dea7430f4df7950d8fdf2f0f151d8386fa04968b872ac0",
|
|
101
101
|
"AGREEMENT.md": "f93a68e92c457200dd024610e65f74e478b8f60860f951da818b70561f1b9bf8",
|
|
102
102
|
"DELIVERY-BOUNDARIES.md": "9b9db50ed1bcce9e77ff6527b6bb7eaf12f3b913162ea9cb7fe90db06b393a4f",
|
|
103
|
-
"package.json": "
|
|
103
|
+
"package.json": "e910dc5cf0db61b1fcb9feffa21a8f8d3c2a244b7d858e8af72202f5b91d403b",
|
|
104
104
|
"manifest.json": "ab06e307795bc9da5add926752b6c4dbdaf18625090261a035aa356a679c504f",
|
|
105
|
-
"release.mjs": "
|
|
105
|
+
"release.mjs": "8b0dc36c47aec72cf9952c9ac1ce5fe27da5ae6829f50cd2339fa101b2eda93c",
|
|
106
106
|
"local-cli.mjs": "41e2a737c8587cb5ce469a7661f6d14b5ea4cd073f8286822fc4dc96b67f56a1",
|
|
107
107
|
"local-state-lock.mjs": "1dbf64264642a0851faf043e07ee9f66d2ad0508e9f799b0626246e42fae893e",
|
|
108
108
|
"local-execution.mjs": "185f34e917440a360b3c4bafd3af245243807d964ae3820b3cffaaa2e6348cc6",
|
|
@@ -141,7 +141,9 @@
|
|
|
141
141
|
"local-scope-candidate.mjs": "8f0bd9606e70d4a0ebacdd3cd600ec3afed85e2bb3e1760aa1144cf55d4e028a",
|
|
142
142
|
"local-scope-context.mjs": "79753abfc6c76089d17f003a1be537dfaea10d6731c542f7fc53f2a9996ed164",
|
|
143
143
|
"local-scope-files.mjs": "4c14a5e226dd5518aeb37f199b1aa896943a66d41e5a1757a3b3fd8a79446d76",
|
|
144
|
-
"local-scope-repository.mjs": "
|
|
144
|
+
"local-scope-repository.mjs": "941eb895c8210da1ca90e8b3d1d6936c24408d10139fe6c3eba2761329248392",
|
|
145
|
+
"local-scope-create-require.mjs": "6b6a209bfcad8fe1398e99c5c8cc3af189c120b9c38e244a73dd1a99c3d91295",
|
|
146
|
+
"local-scope-require-resolution.mjs": "b24596462c8776cbefa6f0a74749d665aa928d1643c68d44798eb93e2dd5ad42",
|
|
145
147
|
"local-scope-git.mjs": "53bbbb6c4795db711b5bc164591a6ad79a561427da046d6bbe997dae6eb82829",
|
|
146
148
|
"local-scope-git-reads.mjs": "5d8a2636bab955cb325cb693db7423e69512355887ae5b5af3b8cb615f5ddad0",
|
|
147
149
|
"local-git-executable.mjs": "9391d716529025b1de3ee006df658a9d765b6ef29ad24ef9b7436829318e651e",
|
|
@@ -174,7 +176,7 @@
|
|
|
174
176
|
"local-task-routing.mjs": "6e2e506b2fb0e2e8d8c5147d7df32849b57a4e94a23634f01a355d45735858e4",
|
|
175
177
|
"remote-client.mjs": "0d1ec6506fe53a1e33c31c5ba19d14e30e00e85e11eaf7ff8277ea798b9f36c9",
|
|
176
178
|
"cloud-task-client.mjs": "11acb1a033c94cdfbdfb177a3402ad97f6a5226f577138c8cf6395dc3ebb1a39",
|
|
177
|
-
"scope-runner-client.mjs": "
|
|
179
|
+
"scope-runner-client.mjs": "2afcbb8bc8a26d03f26787cb2ffd9994c4377757d16d54aa0c83752a29e833ea",
|
|
178
180
|
"native-client.mjs": "6f5575909bae9daba643287f876ffa0f9ff1c53a367b32a40145d740456d6605",
|
|
179
181
|
"native-notification.mjs": "8859a26c6f13d222711f763acf49ed8546b3306cf71f990643ac31a5ccaf1ba1",
|
|
180
182
|
"local-peer-status.mjs": "a971bcc69d60bb32b875ca5bb4297d948631d5d2821b812c279613ea059a8b4f",
|
package/release.mjs
CHANGED
|
@@ -84,6 +84,8 @@ const sourceFiles = [
|
|
|
84
84
|
'local-scope-context.mjs',
|
|
85
85
|
'local-scope-files.mjs',
|
|
86
86
|
'local-scope-repository.mjs',
|
|
87
|
+
'local-scope-create-require.mjs',
|
|
88
|
+
'local-scope-require-resolution.mjs',
|
|
87
89
|
'local-scope-git.mjs',
|
|
88
90
|
'local-scope-git-reads.mjs',
|
|
89
91
|
'local-git-executable.mjs',
|
|
@@ -365,6 +367,8 @@ function releaseLabels() {
|
|
|
365
367
|
'local-scope-context.mjs': { zh: '精确符号范围绑定', en: 'exact symbol scope binding' },
|
|
366
368
|
'local-scope-files.mjs': { zh: '范围文件与安全读取', en: 'scope files and guarded reads' },
|
|
367
369
|
'local-scope-repository.mjs': { zh: '真实仓库范围事实', en: 'actual repository scope facts' },
|
|
370
|
+
'local-scope-create-require.mjs': { zh: '静态 createRequire 原目录绑定', en: 'static createRequire source-directory binding' },
|
|
371
|
+
'local-scope-require-resolution.mjs': { zh: '实际 CommonJS 依赖只读解析', en: 'read-only installed CommonJS dependency resolution' },
|
|
368
372
|
'local-scope-config-imports.mjs': { zh: 'tsconfig 范围路径与依赖只读引用', en: 'tsconfig scope paths and read-only dependency references' },
|
|
369
373
|
'local-scope-config-extends.mjs': { zh: 'tsconfig npm 配置继承解析', en: 'tsconfig npm configuration inheritance resolution' },
|
|
370
374
|
'local-scope-git.mjs': { zh: '实际Git范围与变更事实', en: 'actual Git scope and change facts' },
|
package/releases.json
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.1.55",
|
|
4
|
+
"date": "2026-10-17",
|
|
5
|
+
"zh": "修复任务间消息被技能误拦截,将已授权通信交回主机审批;规范只读和 SDK 命令执行参数;新增原流程计划阶段的显式范围重评与恢复提示,保留原门禁、合同、请求身份及失败历史。",
|
|
6
|
+
"en": "Restored host-approved task messaging, constrained read and SDK execution inputs, and added explicit planning-scope reassessment with phase-correct recovery while retaining original gates, contracts, request identities, and failure history."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.1.54",
|
|
10
|
+
"date": "2026-10-17",
|
|
11
|
+
"zh": "修复受管 Node 只读 SHA-256 命令在 Codex 工具门禁中的静态识别与安全改写;按已证明的 createRequire 原目录解析实际安装依赖并记录只读元数据,保留未知上下文和越界拒绝。",
|
|
12
|
+
"en": "Fixed static recognition and safe rewriting of managed Node SHA-256 reads in Codex tool guards; resolved installed dependencies from proven createRequire anchors with read-only metadata while preserving unknown-context and path-boundary refusals."
|
|
13
|
+
},
|
|
2
14
|
{
|
|
3
15
|
"version": "0.1.53",
|
|
4
16
|
"date": "2026-10-17",
|
package/scope-runner-client.mjs
CHANGED
|
@@ -82,10 +82,16 @@ async function connect(identity, directory, dependencies) {
|
|
|
82
82
|
const observed = await probe({ ...input, ownerId: challenge.ownerId, task: challenge.task, taskDigest: challenge.taskDigest,
|
|
83
83
|
chainDigest: challenge.chainDigest, repositoryId: challenge.repositoryId, challenge: challenge.challenge, signerConfigPath });
|
|
84
84
|
const accepted = await remote('scope-runner-attest', { flowId: identity.flowId, attestation: observed.attestation }, dependencies);
|
|
85
|
+
const next = accepted.nextStep ?? { operation: 'workflow-gate', payload: { targetFiles: [...identity.targetPaths] } };
|
|
86
|
+
if (next.localOperation === 'scope-connect') return { schemaVersion: 'entrorove.scope-connection/1.0', status: 'succeeded',
|
|
87
|
+
flowId: identity.flowId, runnerId: binding.runnerId, scopeEvidence: accepted.scopeEvidence, grantsExecutionAuthority: false,
|
|
88
|
+
nextOperation: 'scope-connect', nextInput: { flowId: identity.flowId, paths: [...identity.targetPaths], importContexts: identity.importContexts }, nextStep: next };
|
|
89
|
+
const resumeOriginalRequest = ['workflow-contract', 'workflow-plan'].includes(next.operation);
|
|
90
|
+
const nextInput = next.operation === 'task-resume' ? { flowId: identity.flowId }
|
|
91
|
+
: { flowId: identity.flowId, operation: next.operation, payload: next.payload ?? {} };
|
|
85
92
|
return { schemaVersion: 'entrorove.scope-connection/1.0', status: 'succeeded', flowId: identity.flowId,
|
|
86
93
|
runnerId: binding.runnerId, scopeEvidence: accepted.scopeEvidence, grantsExecutionAuthority: false,
|
|
87
|
-
nextOperation:
|
|
88
|
-
payload: { targetFiles: [...identity.targetPaths] } } };
|
|
94
|
+
nextOperation: next.operation, ...(resumeOriginalRequest ? { resumeOriginalRequest: true } : { nextInput }), nextStep: next };
|
|
89
95
|
}
|
|
90
96
|
export async function connectScopeRunner(repositoryRoot, input, dependencies) {
|
|
91
97
|
if (!input || Object.keys(input).some(field => !['flowId', 'paths', 'importContexts'].includes(field))
|