@entrorove/skill 0.1.70 → 0.1.73
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 +15 -0
- package/IDE-EXECUTION.md +4 -0
- package/SKILL.md +3 -2
- package/local-budget-reads.mjs +7 -2
- package/local-budget.mjs +3 -2
- package/local-probe.mjs +16 -7
- package/local-scope-build-data.mjs +73 -0
- package/local-scope-build-facts.mjs +33 -0
- package/local-scope-build-io.mjs +57 -0
- package/local-scope-build-read.mjs +22 -0
- package/local-scope-build-readset.mjs +49 -0
- package/local-scope-files.mjs +20 -7
- package/local-scope-repository.mjs +47 -37
- package/local-scope-signature.mjs +6 -4
- package/local-scope-state.mjs +7 -5
- package/local-scope-uni-build.mjs +69 -119
- package/local-scope-uni-conditional.mjs +8 -6
- package/local-scope-uni-directives.mjs +5 -5
- package/local-scope-uni-expressions.mjs +10 -5
- package/local-scope-workspaces.mjs +3 -3
- package/manifest.json +1 -1
- package/package.json +12 -2
- package/release-source-groups.mjs +46 -0
- package/release-state.json +40 -30
- package/release.mjs +6 -40
- package/releases.json +18 -0
- package/remote-client.mjs +1 -1
- package/remote-transport.mjs +14 -0
- package/scope-amendment-client.mjs +31 -19
- package/scope-amendment-reference.mjs +22 -4
- package/scope-amendment-state.mjs +1 -1
- package/scope-context-client.mjs +17 -8
- package/scope-context-journal.mjs +5 -4
- package/scope-context-observer.mjs +3 -2
- package/scope-context-remote.mjs +5 -5
- package/scope-context-state.mjs +4 -1
- package/scope-context-wire.mjs +9 -1
- package/scope-evidence-local.mjs +52 -0
- package/scope-evidence-remote.mjs +78 -0
- package/scope-evidence-wire.mjs +120 -0
- package/scope-runner-client.mjs +65 -35
- package/scope-runner-journal.mjs +32 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.73 — 2026-10-20
|
|
4
|
+
|
|
5
|
+
- 修复通用范围连接在长时间扫描后的签名期限绑定:原 S 证明到期时间不晚于原云端挑战,保留原随机数、完整范围、独立验签和失败历史;已过期挑战仍明确拒绝。
|
|
6
|
+
- Bind ordinary scope signatures to their original cloud deadline after long scans. The original S proof expires no later than its challenge, preserving the nonce, complete scope, independent signatures and failure history; expired challenges still reject explicitly.
|
|
7
|
+
|
|
8
|
+
## 0.1.72 — 2026-10-20
|
|
9
|
+
|
|
10
|
+
- 将完整范围证据与签名控制分离:四类原 S 证明经有界独立通道上传,紧凑权威绑定不可变摘要与字节数;未知上传仅恢复原 GET,保留原路径、上下文、审批、独立 M 写入和全部旧失败记录。
|
|
11
|
+
- Separate full scope evidence from signed controls: upload four kinds of original S proof through a bounded data channel and bind immutable hashes and byte counts in compact authorities. Recover uncertain uploads with the original GET only, preserving complete paths, contexts, approvals, separate M writes and prior failures.
|
|
12
|
+
|
|
13
|
+
## 0.1.71 — 2026-10-20
|
|
14
|
+
|
|
15
|
+
- 通用分离源码依赖事实与未知构建行为:动态配置、宏及加载器保留真实哈希与只读来源,不执行项目代码,不把构建未知标为完整。原审批与独立 S/M 签名仅允许已知源码的非结构修改;隐私文件、路径、资源预算、未知目标及配置漂移继续明确拒绝,实际构建与测试仍需原流程验收。
|
|
16
|
+
- Separate raw source dependency facts from opaque build behavior across projects. Preserve actual hashes and read-only origins for dynamic configuration, macros and loaders without executing project code or claiming compiled completeness. Original approvals and separate S/M signatures permit only known-source nonstructural modifications; private files, unsafe paths, resource budgets, unknown targets and configuration drift remain explicit refusals. Actual builds and tests still require original-flow acceptance.
|
|
17
|
+
|
|
3
18
|
## 0.1.70 — 2026-10-20
|
|
4
19
|
|
|
5
20
|
- 云端沙箱固定中英文输出语言,界面、通知和 Blueprint 文档正文按对应语种显示,原始证据单列来源并保留旧流程精确版本。修复 uni-app 默认条件编译分支解析与长扫描后的原任务签名挑战衔接,核对真实读集并保留原审批、完整路径、快照及失败历史。
|
package/IDE-EXECUTION.md
CHANGED
|
@@ -92,6 +92,10 @@ When intake returns `workflow-gate`, connect the current repository before invok
|
|
|
92
92
|
|
|
93
93
|
Scope inspection resolves aliases from actual `tsconfig.json` or `jsconfig.json`, including relative and npm `extends` and ordered inheritance arrays. Npm configuration lookup uses the pinned TypeScript JSON-config resolver over controlled metadata reads, including scoped packages, subpaths, package `tsconfig`, `exports` and `typesVersions`. Read-only candidate probes treat a child beneath an ordinary file as absent so normal TypeScript fallbacks continue; actual sources, control files and mutation targets retain strict validation. Rejected paths report the requested path, blocking ancestor, resolved target and reason, including broken or cyclic dependency links. Configurations and package metadata enter the signed hashes; dependencies are read only, never executed. Repository-local pnpm links use their actual configuration location. Read-only external dependency packages support parents and mappings inside their reference boundary; references requiring an unrepresentable external canonical ancestor fail explicitly rather than selecting a different same-name repository package. For plugin source compiled by a host project whose config is outside the source's ancestors, include `importContexts: [{"sourcePrefix":"<plugin source directory>","configPath":"<actual host tsconfig.json>"}]` in `scope-connect` input. Inspect the real build configuration first; never invent an alias or mark an unresolved import as external. These context paths remain inside the repository, their hashes enter the signed facts, and the local task binding freezes them.
|
|
94
94
|
|
|
95
|
+
Full scope observations travel through a separate authenticated evidence channel. The SDK keeps the complete original canonical UTF8 S proof in a private SHA256-derived sidecar, reserves an upload with the discovered `scope-evidence-prepare`, and sends its exact bytes once through that reservation's `PUT`. The fixed data limit is 16,000,000 bytes, with bounded structure, time and concurrency; ordinary request, signed-control and journal limits remain unchanged. Ordinary observations, context corrections, scope revisions and post-write refreshes have distinct evidence kinds and exact original identity bindings. An upload or reference grants no execution authority. Apply still verifies the original signatures, freshness, full scope, owner approval, nonce and revision CAS, including separate M consumption for changed writes.
|
|
96
|
+
|
|
97
|
+
An interrupted upload is recovered only with its original reservation's `GET`; a missing or pending result does not authorize another upload, nonce or changed input. Reuse the exact original connector command and input so its private journal performs that recovery. Compact v2 authorities sign the immutable evidence reference, digest, byte count and original task identities. Their revision digest hashes the signed compact record, never a hydrated projection. Readers verify the full sidecar before using the retained context history or amendment reference. Old v1 records and known failures retain their exact bytes; a known terminal rejection is archived before a fresh same-proposal attempt. Do not hand-edit a managed SDK, sidecar, binding or journal to bypass these checks.
|
|
98
|
+
|
|
95
99
|
### Correcting build contexts before the first scope gate
|
|
96
100
|
|
|
97
101
|
An unresolved alias can occur after the original local signer has been enrolled but before any scope facts or gate have been accepted. Ordinary `scope-connect` still rejects a changed `importContexts` list. Use the explicit `scope-context <original absolute repositoryRoot>` recovery on that same flow; do not edit `identity/binding.json`, replace its key, drop target paths or create a replacement flow.
|
package/SKILL.md
CHANGED
|
@@ -5,7 +5,7 @@ description: Execute a task selected for Entrorove through explicit SDK operatio
|
|
|
5
5
|
|
|
6
6
|
# entrorove · 熵巡
|
|
7
7
|
|
|
8
|
-
Version: 0.1.
|
|
8
|
+
Version: 0.1.73.
|
|
9
9
|
|
|
10
10
|
Entrorove applies only to the task for which it is selected; reading these instructions, discovering capabilities or installing the package never selects or governs another task. Hooks are optional. 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
|
|
|
@@ -22,7 +22,7 @@ Entrorove applies only to the task for which it is selected; reading these instr
|
|
|
22
22
|
4. After real cloud acceptance, immediately display the returned entering status in the task’s selected language, including its **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.
|
|
23
23
|
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.
|
|
24
24
|
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. If the original scope challenge preparation is absent from authenticated status, `scope-amend` may use the discovered `scope-challenge-retire` lifecycle operation. The server validates the same approved proposal and receiver, atomically fences only an unadmitted challenge, and records a signed retirement without executing that challenge. Recover and acknowledge the original retirement result with GET before a fresh challenge attempt; retain both requests and all failures. An admitted request, issued nonce, scope application or mutation has no missing-challenge retirement path. An uncertain retirement itself permits only its original GET.
|
|
25
|
-
For a member scope, follow a returned `localAction` before submitting execution evidence. `mutation-connect` obtains only the original task's limited editing pass
|
|
25
|
+
For a member scope, follow a returned `localAction` before submitting execution evidence. `mutation-connect` obtains only the original task's limited editing pass. Its non-structural modify role requires known target source imports, preserves source imports and public contracts, and keeps unresolved origins, other observed dependencies and project configuration read only. Build scripts, macros and generated sources can remain opaque; source observations and this editing role never prove a complete compiled dependency graph or successful tests. After actual writes, refresh the original scope and run `mutation-settle`. Use the returned `previousPassId` for further edits. In a signed member-local plan, `workflow-observe-step` runs the original frozen command and tests without a validator private key. A server-signed `localAcceptance` advances local dependencies; a failed test does not. A frozen independent plan retains its independent validation requirement. Preserve the raw observation and its absent independent receipt. See [IDE-EXECUTION.md](IDE-EXECUTION.md#local-ide-acceptance).
|
|
26
26
|
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.
|
|
27
27
|
Never overwrite a newer package with older bytes, relabel an old archive as new, or roll back because the current state is uncertain. Package identity/version checks and immutable historical byte observations run in the guarded entries. A refusal requires preserved inputs and diagnosis; a `force`, `rollback`, timestamp or filename is not evidence. Intentional rollback uses the original independently signed merge rollback authorization, transaction journal, exact current/restore hashes and fresh original mutation pass. Preserve old failures, receipts and unknown effects; a successful rollback never makes the failed operation successful.
|
|
28
28
|
**Before preparing or writing any UI implementation, read [DESIGN-GATES.md](DESIGN-GATES.md).** Record the whole-site flow first, fetch current mainstream layout references and the specified framework's official rules, create actual mockups or prototypes using the host image/design tool, and show them to the user. Respect their choice of visual format; a prototype is a valid approval baseline. Obtain the original persisted design confirmation before UI code preparation. Carry that exact design approval through the original scope, plan and mutation gates. Neither a caller flag nor a text-only description substitutes for this sequence. Finish real functional tests, actual browser interaction/layout/style checks and original user visual acceptance before delivery. Keep Entrorove unchanged in every language.
|
|
@@ -46,6 +46,7 @@ When a new user requirement arrives while the original task is unfinished, store
|
|
|
46
46
|
- Invoke service operations with JSON stdin to `npx @entrorove/skill invoke` or a host-approved rewritten equivalent.
|
|
47
47
|
- Explicit domains use `native <domain> <operation>`. Domains: aimlock, blueprint, calctool, swarm, validator, mergeguard, archguard, confirm-protocol. Aimlock `brain-plan`, `brain-check`, `brain-validate`, `brain-status` use the original stored plan.
|
|
48
48
|
- At `workflow-gate`, connect the actual repository and exact source paths with `scope-connect`, then use its original `nextInput`. Scope inspection does not grant execution or writes. Dynamic references remain unknown; never set graph completeness manually.
|
|
49
|
+
- Complete S evidence uses a separate bounded upload and private hash-bound data file. Keep every original path, context and unknown origin. An interrupted upload permits only its original GET recovery through the unchanged connector input; a missing result never permits another PUT or challenge. Compact signed authorities bind the complete evidence reference while preserving the original control limits and v1 history.
|
|
49
50
|
- If a real missing build context blocks the first scope scan, use the same-flow `scope-context` review and original-owner answer described in [IDE-EXECUTION.md](IDE-EXECUTION.md#correcting-build-contexts-before-the-first-scope-gate). Apply its exact approved proposal with the original S signer and fresh actual snapshot, then retry the returned complete `scope-connect` input. Keep the original binding, all paths and failed attempts; ordinary reconnects cannot silently change contexts.
|
|
50
51
|
- If actual files were omitted before plan freeze, follow the discovered same-flow `workflow-scope-review`, original owner approval and `scope-amend` recovery in [IDE-EXECUTION.md](IDE-EXECUTION.md#correcting-omitted-scope-before-plan-freeze). Preserve each file's explicit write or read-only role and bind the resulting `scopeRevisionDigest` in the corrected plan; never widen ordinary `scope-connect` input to bypass its original binding.
|
|
51
52
|
- `local capabilities` describes scope, snapshot, budget, ownership, mutation, review and evidence contracts. Do not call a guessed operation or construct a local signature in place of cloud authority.
|
package/local-budget-reads.mjs
CHANGED
|
@@ -102,7 +102,7 @@ export async function readBudgetedProjectFile(input, target, options = {}) {
|
|
|
102
102
|
const root = await fs.realpath(input.repositoryRoot);
|
|
103
103
|
const sourcePath = options.sourcePath ?? path.relative(root, target).split(path.sep).join('/');
|
|
104
104
|
const before = await fs.lstat(target, { bigint: true });
|
|
105
|
-
const result = await budgetReadBuffer({ ...binding, path: sourcePath }, { target });
|
|
105
|
+
const result = await budgetReadBuffer({ ...binding, path: sourcePath }, { target, verifyReadTarget: options.verifyReadTarget });
|
|
106
106
|
const after = await fs.lstat(target, { bigint: true });
|
|
107
107
|
if (!sameSourceStat(before, after)) fail('ENTROROVE_SCOPE_CHANGED', 'budgeted project source changed during its bound observation');
|
|
108
108
|
projectBuffers.set(result.content, Object.freeze({ home: await fs.realpath(binding.home ?? os.homedir()),
|
|
@@ -110,7 +110,12 @@ export async function readBudgetedProjectFile(input, target, options = {}) {
|
|
|
110
110
|
return options.encoding ? result.content.toString(options.encoding) : result.content;
|
|
111
111
|
}
|
|
112
112
|
const handle = await fs.open(target, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
113
|
-
try {
|
|
113
|
+
try {
|
|
114
|
+
const descriptor = await handle.stat();
|
|
115
|
+
if (!descriptor.isFile()) fail('ENTROROVE_FILE_INVALID', 'source must be a regular file');
|
|
116
|
+
if (options.verifyReadTarget) await options.verifyReadTarget({ target: await fs.realpath(target), descriptor });
|
|
117
|
+
return await handle.readFile(options.encoding ? { encoding: options.encoding } : undefined);
|
|
118
|
+
}
|
|
114
119
|
finally { await handle.close(); }
|
|
115
120
|
}
|
|
116
121
|
|
package/local-budget.mjs
CHANGED
|
@@ -91,7 +91,7 @@ function assertReadAllowed(state, relative, tokens, now = Date.now()) {
|
|
|
91
91
|
if (tokens > state.maxTokenEstimate - state.tokenEstimate) fail('ENTROROVE_BUDGET_EXHAUSTED', 'read token budget exhausted');
|
|
92
92
|
if (now - Date.parse(state.startedAt) >= state.maxDurationMs) fail('ENTROROVE_BUDGET_EXHAUSTED', 'read duration budget exhausted');
|
|
93
93
|
}
|
|
94
|
-
async function readSource(location, state, relative, target, received) {
|
|
94
|
+
async function readSource(location, state, relative, target, received, verifyReadTarget) {
|
|
95
95
|
const binding = { home: location.home, repositoryRoot: location.root, flowId: location.flowId, path: relative };
|
|
96
96
|
await assertBudgetHandoffReadAllowed(binding, state);
|
|
97
97
|
const actualTarget = await budgetSourceTarget(location, relative, target);
|
|
@@ -101,6 +101,7 @@ async function readSource(location, state, relative, target, received) {
|
|
|
101
101
|
try {
|
|
102
102
|
const stat = await handle.stat(); if (!stat.isFile()) fail('ENTROROVE_FILE_INVALID', 'budget source must be a regular file');
|
|
103
103
|
await assertDescriptorTarget(target, actualTarget, stat);
|
|
104
|
+
if (verifyReadTarget) await verifyReadTarget({ target: actualTarget, descriptor: stat });
|
|
104
105
|
assertReadAllowed(state, relative, Math.ceil(stat.size / 4));
|
|
105
106
|
const cap = state.enforced ? Math.max(0, (state.maxTokenEstimate - state.tokenEstimate) * 4) : Number.MAX_SAFE_INTEGER;
|
|
106
107
|
const chunks = []; let size = 0;
|
|
@@ -131,7 +132,7 @@ export async function budgetReadBuffer(input, options = {}) {
|
|
|
131
132
|
if (state.grant?.purpose === 'handoff-read') requireLiveBudget(budgetHandoffReadValidationState(state));
|
|
132
133
|
else state = consumeBudgetRenewal({ state });
|
|
133
134
|
const target = options.target ?? path.join(location.root, relative);
|
|
134
|
-
const content = await readSource(location, state, relative, target, received); const tokens = Math.ceil(content.length / 4);
|
|
135
|
+
const content = await readSource(location, state, relative, target, received, options.verifyReadTarget); const tokens = Math.ceil(content.length / 4);
|
|
135
136
|
const updated = { ...state, files: state.files.includes(relative) ? state.files : [...state.files, relative], readCalls: state.readCalls + 1, tokenEstimate: state.tokenEstimate + tokens };
|
|
136
137
|
const committed = await commitBudget(location, loaded, updated, 'budget-read', { path: relative, tokens, bytes: content.length,
|
|
137
138
|
readId, renewedIntervals: (state.autoRenew?.renewalsUsed ?? 0) - (loaded.state.autoRenew?.renewalsUsed ?? 0), reason: state.autoRenew?.lastRenewalReason ?? null });
|
package/local-probe.mjs
CHANGED
|
@@ -50,7 +50,7 @@ async function handoffReadBudget(input) {
|
|
|
50
50
|
const loaded = await loadBudget(await budgetLocation(binding));
|
|
51
51
|
return loaded.state.enforced && loaded.state.grant?.purpose === 'handoff-read';
|
|
52
52
|
}
|
|
53
|
-
async function probeHandoffCandidate(input, root, candidate) {
|
|
53
|
+
async function probeHandoffCandidate(input, root, candidate, signingOptions) {
|
|
54
54
|
scopeBinding(input);
|
|
55
55
|
const native = await import('./local-task-handoff-read-authority.mjs');
|
|
56
56
|
const readHome = await native.assertTaskHandoffReadHome({ ...input, taskId: candidate.receiverTaskId, ownerId: candidate.ownerId });
|
|
@@ -67,13 +67,22 @@ async function probeHandoffCandidate(input, root, candidate) {
|
|
|
67
67
|
preservedSnapshotDigest: null, preservedSnapshotId: null, currentSnapshotFresh: true,
|
|
68
68
|
receiverOrigin: { flowId: candidate.flowId, taskId: candidate.receiverTaskId,
|
|
69
69
|
scopeDigest: candidate.receiverOriginalScopeDigest, checkpointDigest: candidate.receiverCheckpointDigest,
|
|
70
|
-
acceptanceReceiptId: candidate.receiverAcceptanceReceiptId, sourceCopiesVerified: false } });
|
|
70
|
+
acceptanceReceiptId: candidate.receiverAcceptanceReceiptId, sourceCopiesVerified: false } }, signingOptions);
|
|
71
71
|
}
|
|
72
|
-
|
|
72
|
+
function probeSigningOptions(options) {
|
|
73
|
+
if (!options || typeof options !== 'object' || Array.isArray(options)
|
|
74
|
+
|| Object.keys(options).some(key => key !== 'maximumExpiresAt')
|
|
75
|
+
|| options.maximumExpiresAt !== undefined && (!Number.isSafeInteger(options.maximumExpiresAt) || options.maximumExpiresAt < 1)) {
|
|
76
|
+
scopeFail('ENTROROVE_SCOPE_ATTESTATION_BINDING', 'Internal probe signing options require only a finite exact deadline');
|
|
77
|
+
}
|
|
78
|
+
return { maximumExpiresAt: options.maximumExpiresAt };
|
|
79
|
+
}
|
|
80
|
+
export async function probe(input, options = {}) {
|
|
81
|
+
const signingOptions = probeSigningOptions(options);
|
|
73
82
|
rejectScopePrivileges(input);
|
|
74
83
|
const root = await scopeRoot(input.repositoryRoot);
|
|
75
84
|
const candidate = await verifiedScopeCandidate(input, root);
|
|
76
|
-
if (candidate && await handoffReadBudget(input)) return probeHandoffCandidate(input, root, candidate);
|
|
85
|
+
if (candidate && await handoffReadBudget(input)) return probeHandoffCandidate(input, root, candidate, signingOptions);
|
|
77
86
|
const hasBinding = ['flowId', 'stepId', 'snapshotId'].some(field => input[field] !== undefined);
|
|
78
87
|
const retained = hasBinding ? await retainedScope(input, root, true) : null;
|
|
79
88
|
const fresh = !!candidate || (!!retained && retained.stepId !== input.stepId);
|
|
@@ -97,15 +106,15 @@ export async function probe(input) {
|
|
|
97
106
|
classification: { ...result.classification, mode: candidate ? result.classification.mode : retained.mode }, requiredMode: result.classification.mode,
|
|
98
107
|
originalBinding: { flowId: retained.flowId, stepId: retained.stepId, snapshotId: retained.originalSnapshot.snapshotId },
|
|
99
108
|
snapshotDigest: result.facts.snapshotEvidence.snapshotDigest, preservedSnapshotDigest: retained.originalSnapshot.digest,
|
|
100
|
-
preservedSnapshotId: retained.originalSnapshot.snapshotId, currentSnapshotFresh: true });
|
|
109
|
+
preservedSnapshotId: retained.originalSnapshot.snapshotId, currentSnapshotFresh: true }, signingOptions);
|
|
101
110
|
}
|
|
102
111
|
const saved = await initializeScope(input, result.repositoryRoot, result.facts, result.classification);
|
|
103
112
|
return attestScopeFacts(input, { ...result, revision: saved.state.revision, statePath: saved.statePath,
|
|
104
113
|
classification: { ...result.classification, mode: saved.state.mode }, requiredMode: result.classification.mode,
|
|
105
114
|
snapshotDigest: result.facts.snapshotEvidence.snapshotDigest, preservedSnapshotDigest: saved.state.originalSnapshot.digest,
|
|
106
|
-
preservedSnapshotId: saved.state.originalSnapshot.snapshotId, currentSnapshotFresh: true });
|
|
115
|
+
preservedSnapshotId: saved.state.originalSnapshot.snapshotId, currentSnapshotFresh: true }, signingOptions);
|
|
107
116
|
}
|
|
108
|
-
return attestScopeFacts(input, result);
|
|
117
|
+
return attestScopeFacts(input, result, signingOptions);
|
|
109
118
|
}
|
|
110
119
|
function observationReassessment(input, result) {
|
|
111
120
|
const previousMode = input.currentMode === undefined ? result.classification.mode : input.currentMode;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import ts from 'typescript';
|
|
3
|
+
import { protectedBuildRelativePath } from './local-scope-build-read.mjs';
|
|
4
|
+
import { uniConditionalError } from './local-scope-uni-expressions.mjs';
|
|
5
|
+
|
|
6
|
+
export const buildConfigurationNames = ['vite.config.js', 'vite.config.mjs', 'vite.config.cjs', 'vite.config.ts',
|
|
7
|
+
'vite.config.mts', 'vite.config.cts', 'vue.config.js', 'vue.config.cjs', 'webpack.config.js', 'webpack.config.cjs',
|
|
8
|
+
'webpack.config.mjs', 'webpack.config.ts', 'rollup.config.js', 'rollup.config.mjs', 'rollup.config.ts',
|
|
9
|
+
'babel.config.js', 'babel.config.cjs', 'babel.config.json', '.babelrc', '.babelrc.json', '.babelrc.js',
|
|
10
|
+
'next.config.js', 'next.config.mjs', 'next.config.ts', 'nuxt.config.js', 'nuxt.config.ts', 'svelte.config.js'];
|
|
11
|
+
export function buildDataPath(base, value) {
|
|
12
|
+
if (typeof value !== 'string' || value.includes('\\') || value.includes('\0') || path.posix.isAbsolute(value)) {
|
|
13
|
+
throw Object.assign(new Error('Build data references require literal repository-relative paths'), { code: 'ENTROROVE_PATH_UNSAFE' });
|
|
14
|
+
}
|
|
15
|
+
const relative = path.posix.normalize(path.posix.join(base, value)).replace(/\/$/, '');
|
|
16
|
+
if (relative === '..' || relative.startsWith('../')) throw Object.assign(new Error('Build data references cannot escape the original repository'), { code: 'ENTROROVE_PATH_UNSAFE' });
|
|
17
|
+
if (protectedBuildRelativePath(relative)) throw uniConditionalError(relative, 0, 'Build data cannot read environment or private control values');
|
|
18
|
+
return relative;
|
|
19
|
+
}
|
|
20
|
+
export function buildScriptReferences(manifest, filename = 'package.json') {
|
|
21
|
+
const references = new Set(); let opaque = false;
|
|
22
|
+
for (const [name, command] of Object.entries(manifest.scripts ?? {})) {
|
|
23
|
+
if (typeof command !== 'string') throw uniConditionalError(filename, 0, 'Build package scripts require string commands');
|
|
24
|
+
const relevant = /^(?:dev|build)(?::|$)/.test(name) || /(?:^|\s)(?:--(?:experimental-loader|loader|require|import)(?:=|\s)|-r\s)/.test(command);
|
|
25
|
+
if (!relevant) continue;
|
|
26
|
+
opaque = true;
|
|
27
|
+
// Literal references are data only, with no shell, cwd or execution claim.
|
|
28
|
+
if (/[$`\n\r|<>;]/.test(command) || /(?:^|&&)\s*cd\s/.test(command)) continue;
|
|
29
|
+
for (const segment of command.split('&&')) {
|
|
30
|
+
const words = segment.match(/"[^"]*"|'[^']*'|[^\s]+/g)?.map(word => /^['"]/.test(word) ? word.slice(1, -1) : word) ?? [];
|
|
31
|
+
let cursor = words[0] === 'cross-env' ? 1 : 0;
|
|
32
|
+
while (/^[A-Za-z_][A-Za-z0-9_]*=/.test(words[cursor] ?? '')) cursor++;
|
|
33
|
+
if (words[cursor] === 'node' && words[cursor + 1] && !words[cursor + 1].startsWith('-')) references.add(words[cursor + 1]);
|
|
34
|
+
for (let index = cursor + 1; index < words.length; index++) {
|
|
35
|
+
if (['--config', '-c', '--experimental-loader', '--loader', '--require', '--import', '-r'].includes(words[index]) && words[index + 1]) references.add(words[++index]);
|
|
36
|
+
else if (/^--(?:config|experimental-loader|loader|require|import)=/.test(words[index])) references.add(words[index].slice(words[index].indexOf('=') + 1));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return { opaque, references: [...references].sort() };
|
|
41
|
+
}
|
|
42
|
+
export function buildModuleData(source, filename) {
|
|
43
|
+
let ast;
|
|
44
|
+
try { ast = ts.createSourceFile(filename, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS); }
|
|
45
|
+
catch (error) { if (!(error instanceof RangeError)) throw error; throw Object.assign(uniConditionalError(filename, 0, 'Build data syntax exceeds its parser bound'), { code: 'ENTROROVE_SCOPE_LIMIT' }); }
|
|
46
|
+
const references = new Set(); let nodes = 0;
|
|
47
|
+
const inspect = node => {
|
|
48
|
+
if (++nodes > 8192) throw Object.assign(uniConditionalError(filename, 0, 'Build data syntax exceeds its 8192-node bound'), { code: 'ENTROROVE_SCOPE_LIMIT' });
|
|
49
|
+
if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteralLike(node.moduleSpecifier)) references.add(node.moduleSpecifier.text);
|
|
50
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'require'
|
|
51
|
+
&& node.arguments.length === 1 && ts.isStringLiteralLike(node.arguments[0])) references.add(node.arguments[0].text);
|
|
52
|
+
if (ts.isPropertyAssignment(node) && !ts.isComputedPropertyName(node.name) && ['loader', 'use'].includes(node.name.text)
|
|
53
|
+
&& ts.isStringLiteralLike(node.initializer)) references.add(node.initializer.text);
|
|
54
|
+
// Function bodies remain opaque build code. Inspect module-level literal
|
|
55
|
+
// declarations without simulating callbacks or proving a runtime closure.
|
|
56
|
+
if (ts.isFunctionLike(node)) return;
|
|
57
|
+
ts.forEachChild(node, inspect);
|
|
58
|
+
};
|
|
59
|
+
inspect(ast);
|
|
60
|
+
const literalRoots = [];
|
|
61
|
+
for (const statement of ast.statements) {
|
|
62
|
+
let expression = ts.isExportAssignment(statement) ? statement.expression : ts.isExpressionStatement(statement)
|
|
63
|
+
&& ts.isBinaryExpression(statement.expression) && statement.expression.left.getText(ast) === 'module.exports'
|
|
64
|
+
&& statement.expression.operatorToken.kind === ts.SyntaxKind.EqualsToken ? statement.expression.right : undefined;
|
|
65
|
+
while (expression && (ts.isParenthesizedExpression(expression) || ts.isAsExpression(expression) || ts.isSatisfiesExpression(expression))) expression = expression.expression;
|
|
66
|
+
if (!expression || !ts.isObjectLiteralExpression(expression)) continue;
|
|
67
|
+
const literal = Object.create(null);
|
|
68
|
+
for (const property of expression.properties) if (ts.isPropertyAssignment(property) && !ts.isComputedPropertyName(property.name)
|
|
69
|
+
&& ['root', 'envDir', 'context'].includes(property.name.text) && ts.isStringLiteralLike(property.initializer)) literal[property.name.text] = property.initializer.text;
|
|
70
|
+
literalRoots.push(literal);
|
|
71
|
+
}
|
|
72
|
+
return { references: [...references].filter(value => value.startsWith('.')).sort(), literalRoots };
|
|
73
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { packageBoundary, scopeFail, scopePath, scopeBuildSourceRead } from './local-scope-files.mjs';
|
|
2
|
+
|
|
3
|
+
// Opaque build programs are readonly origins. They never replace source identities
|
|
4
|
+
// or establish a complete compiled graph, and are never executed by inspection.
|
|
5
|
+
export async function observeBuildFacts(root, proof, input, graph) {
|
|
6
|
+
if (!proof || !Array.isArray(proof.observations) || !Array.isArray(proof.unresolvedBuildSources)) {
|
|
7
|
+
scopeFail('ENTROROVE_IMPORT_INVALID', 'Build inspection requires explicit observed files and uncertainty origins');
|
|
8
|
+
}
|
|
9
|
+
for (const file of proof.observations) {
|
|
10
|
+
scopePath(file.path);
|
|
11
|
+
if (!/^[a-f0-9]{64}$/.test(file.sha256 ?? '')) scopeFail('ENTROROVE_IMPORT_INVALID', 'Build observation has no actual file hash');
|
|
12
|
+
const previous = graph.metadata.get(file.path);
|
|
13
|
+
if (previous && previous.sha256 !== file.sha256) scopeFail('ENTROROVE_SCOPE_CHANGED', 'Build metadata changed during source inspection');
|
|
14
|
+
graph.metadata.set(file.path, { path: file.path, sha256: file.sha256 });
|
|
15
|
+
}
|
|
16
|
+
for (const origin of proof.unresolvedBuildSources) {
|
|
17
|
+
const file = graph.metadata.get(origin.path);
|
|
18
|
+
if (!file || file.sha256 !== origin.sha256 || typeof origin.reason !== 'string' || !/^[a-z][a-z-]{0,95}$/.test(origin.reason)) {
|
|
19
|
+
scopeFail('ENTROROVE_IMPORT_INVALID', 'Opaque build origin must match an actual readonly file observation');
|
|
20
|
+
}
|
|
21
|
+
if (!graph.visited.has(file.path)) {
|
|
22
|
+
if (graph.visited.size >= 1000) scopeFail('ENTROROVE_SCOPE_LIMIT', 'import graph exceeds the 1000-file inspection bound');
|
|
23
|
+
const boundary = await packageBoundary(root, file.path, input, scopeBuildSourceRead);
|
|
24
|
+
graph.visited.set(file.path, { ...file, module: boundary.path, boundary });
|
|
25
|
+
}
|
|
26
|
+
if (graph.visited.get(file.path).sha256 !== file.sha256) scopeFail('ENTROROVE_SCOPE_CHANGED', 'Opaque build origin differs from its actual source node');
|
|
27
|
+
if (graph.dynamicSources.some(item => item.path === file.path)) continue;
|
|
28
|
+
graph.dynamicSources.push(file);
|
|
29
|
+
graph.unresolvedImports.push({ path: file.path, line: 1, column: 1, reason: origin.reason,
|
|
30
|
+
message: 'Build behavior remains unproven; this origin is read-only and source projection grants no compiled dependency guarantee' });
|
|
31
|
+
}
|
|
32
|
+
return proof.unresolvedBuildSources.length > 0;
|
|
33
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { scopeFail, scopeProbeTarget, scopeTarget } from './local-scope-files.mjs';
|
|
3
|
+
import { isProtectedBuildPath, protectedBuildRelativePath as protectedPath } from './local-scope-build-read.mjs';
|
|
4
|
+
|
|
5
|
+
const dependency = relative => relative.split('/').includes('node_modules');
|
|
6
|
+
|
|
7
|
+
// Inspect names and physical destinations before a build helper can read bytes.
|
|
8
|
+
// A dependency symlink is never permission to read an environment/control file.
|
|
9
|
+
export function createScopeBuildIO(root, readMetadata, hashMetadata) {
|
|
10
|
+
const protectedDestination = actual => isProtectedBuildPath(root, actual);
|
|
11
|
+
const probe = async relative => {
|
|
12
|
+
if (relative === '.') return { exists: true, file: false, directory: true };
|
|
13
|
+
const target = await scopeProbeTarget(root, relative, dependency(relative));
|
|
14
|
+
return { exists: target.exists, file: Boolean(target.stat?.isFile()), directory: Boolean(target.stat?.isDirectory()) };
|
|
15
|
+
};
|
|
16
|
+
const readTarget = async (relative, operation) => {
|
|
17
|
+
if (protectedPath(relative)) scopeFail('ENTROROVE_IMPORT_UNSUPPORTED', 'Build inspection cannot read environment or control files');
|
|
18
|
+
const target = await scopeProbeTarget(root, relative, dependency(relative));
|
|
19
|
+
if (target.exists && protectedDestination(await fs.realpath(target.target))) {
|
|
20
|
+
scopeFail('ENTROROVE_IMPORT_UNSUPPORTED', 'Build dependency resolves to an environment or control file; values were not read');
|
|
21
|
+
}
|
|
22
|
+
return operation(relative);
|
|
23
|
+
};
|
|
24
|
+
const read = relative => readTarget(relative, readMetadata);
|
|
25
|
+
const inspectDirectory = async (relative, maximumEntries) => {
|
|
26
|
+
if (protectedPath(relative)) scopeFail('ENTROROVE_IMPORT_UNSUPPORTED', 'Build inspection cannot enter a private control directory');
|
|
27
|
+
const target = relative === '.' ? root : (await scopeTarget(root, relative)).target;
|
|
28
|
+
const before = await fs.lstat(target), realPath = await fs.realpath(target);
|
|
29
|
+
if (!before.isDirectory() || before.isSymbolicLink() || protectedDestination(realPath)) {
|
|
30
|
+
scopeFail('ENTROROVE_IMPORT_UNSUPPORTED', 'Build directory must be a real non-private directory');
|
|
31
|
+
}
|
|
32
|
+
const identity = { dev: before.dev, ino: before.ino, mode: before.mode & 0o777, realPath };
|
|
33
|
+
const verify = async () => {
|
|
34
|
+
const current = await fs.lstat(target), actual = await fs.realpath(target);
|
|
35
|
+
if (!current.isDirectory() || current.isSymbolicLink() || current.dev !== identity.dev || current.ino !== identity.ino
|
|
36
|
+
|| (current.mode & 0o777) !== identity.mode || actual !== realPath || protectedDestination(actual)) {
|
|
37
|
+
scopeFail('ENTROROVE_SCOPE_CHANGED', 'Build directory identity changed during name observation');
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
await verify();
|
|
41
|
+
const directory = await fs.opendir(target), environmentFiles = [], children = []; let entries = 0;
|
|
42
|
+
let iterating = false;
|
|
43
|
+
try {
|
|
44
|
+
await verify(); iterating = true;
|
|
45
|
+
for await (const entry of directory) {
|
|
46
|
+
if (++entries > maximumEntries) scopeFail('ENTROROVE_SCOPE_LIMIT', 'Conditional build directory exceeds its bounded entry inspection');
|
|
47
|
+
if (entry.name === '.env' || entry.name.startsWith('.env.')) environmentFiles.push(entry.name);
|
|
48
|
+
children.push({ name: entry.name, kind: entry.isSymbolicLink() ? 'symlink' : entry.isDirectory() ? 'directory'
|
|
49
|
+
: entry.isFile() ? 'file' : 'unsupported' });
|
|
50
|
+
}
|
|
51
|
+
} finally { if (!iterating) await directory.close(); }
|
|
52
|
+
await verify();
|
|
53
|
+
children.sort((left, right) => left.name.localeCompare(right.name));
|
|
54
|
+
return { environmentFiles: environmentFiles.sort(), entries, children, identity };
|
|
55
|
+
};
|
|
56
|
+
return { read, probe, inspectDirectory, ...(hashMetadata ? { hashMetadata: relative => readTarget(relative, hashMetadata) } : {}) };
|
|
57
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const protectedBuildRelativePath = value => value.split(/[\\/]/).some(part => part === '.env' || part.startsWith('.env.')
|
|
5
|
+
|| ['.git', '.entrorove', '.aimlock', '.archguard', '.ssh', '.aws', '.gnupg', '.kube', '.azure',
|
|
6
|
+
'.codex', '.claude', '.agents', '.npmrc', '.netrc', '.pypirc', '.gitconfig', 'id_rsa', 'id_ed25519'].includes(part)
|
|
7
|
+
|| /\.(?:pem|key)$/i.test(part));
|
|
8
|
+
export function isProtectedBuildPath(root, actual) {
|
|
9
|
+
const relative = path.relative(root, actual);
|
|
10
|
+
return protectedBuildRelativePath(relative === '..' || relative.startsWith(`..${path.sep}`) ? actual : relative);
|
|
11
|
+
}
|
|
12
|
+
export async function assertBuildReadTarget(root, relative, target, descriptor) {
|
|
13
|
+
if (protectedBuildRelativePath(relative) || isProtectedBuildPath(root, target)) {
|
|
14
|
+
throw Object.assign(new Error('Build dependency resolves to an environment or control file; values were not read'),
|
|
15
|
+
{ code: 'ENTROROVE_IMPORT_UNSUPPORTED' });
|
|
16
|
+
}
|
|
17
|
+
const actual = await fs.stat(target);
|
|
18
|
+
if (!actual.isFile() || actual.dev !== descriptor.dev || actual.ino !== descriptor.ino) {
|
|
19
|
+
throw Object.assign(new Error('Build dependency destination changed before reading any source bytes'),
|
|
20
|
+
{ code: 'ENTROROVE_SCOPE_CHANGED' });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { uniConditionalError } from './local-scope-uni-expressions.mjs';
|
|
2
|
+
import { buildDataPath } from './local-scope-build-data.mjs';
|
|
3
|
+
import { sha256 } from './local-integrity.mjs';
|
|
4
|
+
|
|
5
|
+
const absent = error => ['ENOENT', 'ENOTDIR'].includes(error.code);
|
|
6
|
+
const changed = path => Object.assign(uniConditionalError(path, 0, 'Build data or candidate absence changed during inspection'), { code: 'ENTROROVE_SCOPE_CHANGED' });
|
|
7
|
+
const shape = value => value ? { entries: value.entries, environmentFiles: [...value.environmentFiles].sort(),
|
|
8
|
+
...(value.identity ? { identity: { ...value.identity } } : {}),
|
|
9
|
+
...(value.children ? { children: value.children.map(({ name, kind }) => ({ name, kind })).sort((a, b) => a.name.localeCompare(b.name)) } : {}) } : null;
|
|
10
|
+
export function createBuildDataReadset(read, inspectDirectory) {
|
|
11
|
+
const files = new Map(), directories = new Map(); let bytes = 0, entries = 0;
|
|
12
|
+
const observe = async relative => {
|
|
13
|
+
buildDataPath('.', relative); if (files.has(relative)) return files.get(relative);
|
|
14
|
+
if (files.size >= 2048) throw uniConditionalError(relative, 0, 'Build data exceeds its 2048-path inspection bound');
|
|
15
|
+
let item; try { item = await read(relative); } catch (error) { if (!absent(error)) throw error; }
|
|
16
|
+
if (item) {
|
|
17
|
+
const content = Buffer.from(item.contentBase64, 'base64'), size = content.length;
|
|
18
|
+
if (size > 1_048_576 || bytes + size > 8_388_608) throw Object.assign(uniConditionalError(relative, 0, 'Build data exceeds its 1-MiB/file or 8-MiB bound'), { code: 'ENTROROVE_SCOPE_LIMIT' });
|
|
19
|
+
if (item.path !== relative || !/^[a-f0-9]{64}$/.test(item.sha256) || item.sha256 !== sha256(content)) throw uniConditionalError(relative, 0, 'Build data observation has no actual path and hash');
|
|
20
|
+
bytes += size;
|
|
21
|
+
}
|
|
22
|
+
files.set(relative, item ?? null); return item ?? null;
|
|
23
|
+
};
|
|
24
|
+
const directory = async relative => {
|
|
25
|
+
if (relative !== '.') buildDataPath('.', relative);
|
|
26
|
+
if (directories.has(relative)) return directories.get(relative);
|
|
27
|
+
if (directories.size >= 128 || typeof inspectDirectory !== 'function') throw uniConditionalError(relative, 0, 'Build directory absence requires bounded actual inspection');
|
|
28
|
+
let value; try { value = await inspectDirectory(relative, Math.min(8192, 32768 - entries)); } catch (error) { if (!absent(error)) throw error; }
|
|
29
|
+
if (value) {
|
|
30
|
+
if (!Number.isInteger(value.entries) || value.entries < 0 || !Array.isArray(value.environmentFiles)
|
|
31
|
+
|| value.entries > 8192 || entries + value.entries > 32768) throw Object.assign(uniConditionalError(relative, 0, 'Build directory exceeds its bounded name inspection'), { code: 'ENTROROVE_SCOPE_LIMIT' });
|
|
32
|
+
entries += value.entries;
|
|
33
|
+
}
|
|
34
|
+
const result = shape(value); directories.set(relative, result); return result;
|
|
35
|
+
};
|
|
36
|
+
const verify = async () => {
|
|
37
|
+
for (const [relative, expected] of files) {
|
|
38
|
+
let actual; try { actual = await read(relative); } catch (error) { if (!absent(error)) throw error; }
|
|
39
|
+
if ((actual?.sha256 ?? null) !== (expected?.sha256 ?? null) || actual?.realPath !== expected?.realPath
|
|
40
|
+
|| actual?.mode !== expected?.mode || JSON.stringify(actual?.referenceRoot) !== JSON.stringify(expected?.referenceRoot)) throw changed(relative);
|
|
41
|
+
}
|
|
42
|
+
let checked = 0;
|
|
43
|
+
for (const [relative, expected] of directories) {
|
|
44
|
+
let actual; try { actual = await inspectDirectory(relative, Math.min(8192, 32768 - checked)); } catch (error) { if (!absent(error)) throw error; }
|
|
45
|
+
checked += actual?.entries ?? 0; if (JSON.stringify(shape(actual)) !== JSON.stringify(expected)) throw changed(relative);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
return { observe, directory, verify, get observations() { return [...files.values()].filter(Boolean).map(({ path, sha256 }) => ({ path, sha256 })).sort((a, b) => a.path.localeCompare(b.path)); } };
|
|
49
|
+
}
|
package/local-scope-files.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { readBudgetedProjectFile } from './local-budget-reads.mjs';
|
|
|
5
5
|
import { verifiedSfnt } from './local-scope-font.mjs';
|
|
6
6
|
import { verifiedZip } from './local-scope-zip.mjs';
|
|
7
7
|
import { observeScopeAbsence, observeScopeRead, observeScopeTarget } from './local-scope-read-set.mjs';
|
|
8
|
+
import { assertBuildReadTarget, protectedBuildRelativePath } from './local-scope-build-read.mjs';
|
|
8
9
|
|
|
9
10
|
export function scopeFail(code, message) { throw Object.assign(new Error(message), { code }); }
|
|
10
11
|
export function observedScopeRisk(files) {
|
|
@@ -100,7 +101,7 @@ export async function scopeProbeTarget(root, relative, allowDependency = false)
|
|
|
100
101
|
return { target: path.join(root, relative), exists: false, stat: null };
|
|
101
102
|
}
|
|
102
103
|
}
|
|
103
|
-
async function safeScopeRead(root, relative, input, allowDependency = true) {
|
|
104
|
+
async function safeScopeRead(root, relative, input, allowDependency = true, verifyReadTarget) {
|
|
104
105
|
const dependency = allowDependency && dependencyReference(relative), target = await scopeTarget(root, relative, false, dependency);
|
|
105
106
|
if (!target.stat.isFile()) await invalidScopeTarget(root, relative, target.target, 'not-regular-file');
|
|
106
107
|
if (target.stat.size > 16_777_216) scopeFail('ENTROROVE_SCOPE_LIMIT', 'scope source exceeds the 16 MiB inspection bound');
|
|
@@ -108,7 +109,15 @@ async function safeScopeRead(root, relative, input, allowDependency = true) {
|
|
|
108
109
|
try {
|
|
109
110
|
const before = await handle.stat();
|
|
110
111
|
const beforeReal = dependency ? await fs.realpath(target.target) : null;
|
|
111
|
-
|
|
112
|
+
if (verifyReadTarget) {
|
|
113
|
+
if (before.ino !== target.stat.ino || before.dev !== target.stat.dev) scopeFail('ENTROROVE_SCOPE_CHANGED', 'Build source changed before its descriptor was opened');
|
|
114
|
+
await verifyReadTarget({ target: beforeReal ?? target.target, descriptor: before });
|
|
115
|
+
}
|
|
116
|
+
const boundedVerify = verifyReadTarget ? async actual => {
|
|
117
|
+
if (actual.descriptor.ino !== before.ino || actual.descriptor.dev !== before.dev) scopeFail('ENTROROVE_SCOPE_CHANGED', 'Build source changed before its accounted descriptor was opened');
|
|
118
|
+
await verifyReadTarget(actual);
|
|
119
|
+
} : undefined;
|
|
120
|
+
const content = input ? await readBudgetedProjectFile(input, target.target, { sourcePath: relative, verifyReadTarget: boundedVerify }) : await handle.readFile();
|
|
112
121
|
const after = await handle.stat();
|
|
113
122
|
const latest = dependency ? await fs.stat(target.target) : await fs.lstat(target.target);
|
|
114
123
|
const afterReal = dependency ? await fs.realpath(target.target) : null;
|
|
@@ -118,13 +127,17 @@ async function safeScopeRead(root, relative, input, allowDependency = true) {
|
|
|
118
127
|
await scopeTarget(root, relative, false, dependency);
|
|
119
128
|
const digest = sha256(content);
|
|
120
129
|
observeScopeRead(root, relative, { sha256: digest, mode: after.mode & 0o777, realPath: afterReal ?? target.target }, async () => {
|
|
121
|
-
const actual = await safeScopeRead(root, relative, input, allowDependency);
|
|
130
|
+
const actual = await safeScopeRead(root, relative, input, allowDependency, verifyReadTarget);
|
|
122
131
|
return { sha256: actual.sha256, mode: actual.mode, realPath: await fs.realpath(target.target) };
|
|
123
132
|
});
|
|
124
133
|
return { content, mode: after.mode & 0o777, sha256: digest, bytes: content.length };
|
|
125
134
|
} finally { await handle.close(); }
|
|
126
135
|
}
|
|
127
136
|
export const scopeRead = (root, relative, input = { repositoryRoot: root }) => safeScopeRead(root, relative, input, true);
|
|
137
|
+
export function scopeBuildSourceRead(root, relative, input = { repositoryRoot: root }) {
|
|
138
|
+
if (protectedBuildRelativePath(relative)) scopeFail('ENTROROVE_IMPORT_UNSUPPORTED', 'Build inspection cannot read environment or credential values');
|
|
139
|
+
return safeScopeRead(root, relative, input, true, ({ target, descriptor }) => assertBuildReadTarget(root, relative, target, descriptor));
|
|
140
|
+
}
|
|
128
141
|
// Internal control roles only: signed manifest metadata and already validated private signer files.
|
|
129
142
|
export const scopeControlRead = (root, relative) => safeScopeRead(root, relative, null, false);
|
|
130
143
|
const excluded = new Set(['.git', 'node_modules', '.entrorove', '.aimlock', '.archguard']);
|
|
@@ -152,12 +165,12 @@ export function lineCount(content) {
|
|
|
152
165
|
if (!content.length || verifiedSfnt(content) || verifiedZip(content)) return 0;
|
|
153
166
|
return content.toString('utf8').split(/\r\n|\n|\r/).length - (/[\r\n]$/.test(content.toString('utf8')) ? 1 : 0);
|
|
154
167
|
}
|
|
155
|
-
export async function packageBoundary(root, relative, input = { repositoryRoot: root }) {
|
|
168
|
+
export async function packageBoundary(root, relative, input = { repositoryRoot: root }, readSource = scopeRead) {
|
|
156
169
|
let directory = path.posix.dirname(relative);
|
|
157
170
|
while (true) {
|
|
158
171
|
const manifestPath = directory === '.' ? 'package.json' : `${directory}/package.json`;
|
|
159
172
|
try {
|
|
160
|
-
const read = await
|
|
173
|
+
const read = await readSource(root, manifestPath, input); let manifest;
|
|
161
174
|
try { manifest = JSON.parse(read.content.toString('utf8')); } catch { scopeFail('ENTROROVE_PACKAGE_INVALID', 'package manifest is not valid JSON'); }
|
|
162
175
|
return { path: directory, manifestPath, manifestDigest: read.sha256, name: typeof manifest.name === 'string' ? manifest.name : null };
|
|
163
176
|
} catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
@@ -166,11 +179,11 @@ export async function packageBoundary(root, relative, input = { repositoryRoot:
|
|
|
166
179
|
}
|
|
167
180
|
return { path: relative.includes('/') ? relative.split('/')[0] : '.', manifestPath: null, manifestDigest: null, name: null };
|
|
168
181
|
}
|
|
169
|
-
export async function stableScope(root, files, input = { repositoryRoot: root }) {
|
|
182
|
+
export async function stableScope(root, files, input = { repositoryRoot: root }, readSource = scopeRead) {
|
|
170
183
|
for (const file of files) {
|
|
171
184
|
if (file.exists === false) {
|
|
172
185
|
if ((await scopeTarget(root, file.path, true)).exists) scopeFail('ENTROROVE_SCOPE_CHANGED', 'a deleted target reappeared during inspection');
|
|
173
|
-
} else if ((await
|
|
186
|
+
} else if ((await readSource(root, file.path, input)).sha256 !== file.sha256) scopeFail('ENTROROVE_SCOPE_CHANGED', 'scope files changed before the observation completed');
|
|
174
187
|
}
|
|
175
188
|
}
|
|
176
189
|
export const scopeDigest = value => sha256(canonical(value));
|