@entrorove/skill 0.1.71 → 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 CHANGED
@@ -1,5 +1,15 @@
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
+
3
13
  ## 0.1.71 — 2026-10-20
4
14
 
5
15
  - 通用分离源码依赖事实与未知构建行为:动态配置、宏及加载器保留真实哈希与只读来源,不执行项目代码,不把构建未知标为完整。原审批与独立 S/M 签名仅允许已知源码的非结构修改;隐私文件、路径、资源预算、未知目标及配置漂移继续明确拒绝,实际构建与测试仍需原流程验收。
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.71.
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
 
@@ -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-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
- export async function probe(input) {
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;
package/manifest.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": "entrorove.skill-manifest/1.0",
3
3
  "name": "entrorove",
4
- "version": "0.1.71",
4
+ "version": "0.1.73",
5
5
  "endpoint": "https://entrorove.com",
6
6
  "requestSchemaVersion": "entrorove.skill.request/2.0",
7
7
  "supportedRequestSchemaVersions": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@entrorove/skill",
3
- "version": "0.1.71",
3
+ "version": "0.1.73",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "entrorove": "cli.mjs"
@@ -325,7 +325,11 @@
325
325
  "local-scope-build-io.mjs",
326
326
  "local-scope-build-facts.mjs",
327
327
  "local-scope-build-data.mjs",
328
- "local-scope-build-readset.mjs"
328
+ "local-scope-build-readset.mjs",
329
+ "scope-evidence-wire.mjs",
330
+ "scope-evidence-local.mjs",
331
+ "scope-evidence-remote.mjs",
332
+ "scope-runner-journal.mjs"
329
333
  ],
330
334
  "scripts": {
331
335
  "test": "node test/run.mjs",
@@ -37,3 +37,10 @@ export const memberContinuationSourceFiles = Object.freeze([
37
37
  'workflow-observation-state.mjs',
38
38
  'workflow-observation-wire.mjs',
39
39
  ])
40
+ export const scopeEvidenceSourceFiles = Object.freeze([
41
+ 'scope-evidence-wire.mjs', 'scope-evidence-local.mjs', 'scope-evidence-remote.mjs', 'scope-runner-journal.mjs',
42
+ ])
43
+ export const scopeEvidenceReleaseNote = {
44
+ zh: '将完整范围证据与签名控制分离:四类原 S 证明经有界独立通道上传,紧凑权威绑定不可变摘要与字节数;未知上传仅恢复原 GET,保留原路径、上下文、审批、独立 M 写入和全部旧失败记录。',
45
+ en: '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.',
46
+ }
@@ -1,21 +1,25 @@
1
1
  {
2
2
  "schemaVersion": "entrorove.release-state/1.0",
3
- "version": "0.1.71",
4
- "digest": "365984167bef91bf1b780eb3c479b0d0fddd6ba79b2df59bc6ae9c8dc4f48e15",
3
+ "version": "0.1.73",
4
+ "digest": "06693e8422b76b4037a70c0be657104b918234d74fa4c5dd1e29a394d5c83e0e",
5
5
  "files": {
6
- "release-source-groups.mjs": "6dc0915b79aa495759e7fd1e8dfebde36ab2b6e50f22a2d716eca14b5f9957f9",
6
+ "release-source-groups.mjs": "876b8c8bcf459d3b469bf4f2021d4206de5b199add545597a5c8f6dea7385b38",
7
7
  "local-scope-build-read.mjs": "361d4b461cfa2c77b4ec22bc59a0a1c78fed2cfb0ea2e05a8da5c2551ecd88ac",
8
8
  "local-scope-build-io.mjs": "8af69722738fa094780f9a2dfcddaf12993fc3b936a75c56c4b0d4f90cb20fd1",
9
9
  "local-scope-build-facts.mjs": "8b0bd19347d06d1cb66d8cd2d6784103af9d00ea691d7f4acea523d994b874fb",
10
10
  "local-scope-build-data.mjs": "b6e272ca4385629326b4517a53408c561272f5f1a01e5b701f17eaa4c5e64b2e",
11
11
  "local-scope-build-readset.mjs": "fd1cb1470c54b3902a4612056281746e947cf96d1f5c03a09ea0becbcd79cc45",
12
- "scope-context-client.mjs": "dbfec7db0e118f3025a0e45475f3328a171e4cd1dd86e6c8d326d779ce61b459",
12
+ "scope-evidence-wire.mjs": "d283bbfd002b034e1deb539633b4970b430a97065388288ab3398e2bc0285633",
13
+ "scope-evidence-local.mjs": "25b22ba2cc5a17978d2422e290ee015299f05d639b4b4b9551001afefab55c0d",
14
+ "scope-evidence-remote.mjs": "27077bb8238f3d11e6e18453e2ea8a6e096a104bbee5ee43a82b8c099d253c1e",
15
+ "scope-runner-journal.mjs": "cddb2f20a2812e8bcc3268d8ebdd486c22d0c7f5740e958d0b35cd2cc8819f92",
16
+ "scope-context-client.mjs": "115d82022f71396bab137e9af06421a6d0aa9671838f0df64e6aa83d9d7fb039",
13
17
  "scope-context-observer.mjs": "25a84339512c1e80711aca53baed20da556fa2ae5d84e83fa95d58dcdca7cf7d",
14
18
  "scope-context-observation.mjs": "d92f2fca3c923caa6f0c5c9cc60b4f12bdae212071da64da6692e3e99b251969",
15
- "scope-context-remote.mjs": "b52cdee03e845edc15eae6bb3ccfaea9c4482c03f2fae7001439e4abcb2acd33",
16
- "scope-context-state.mjs": "7bc434620bfc505ee290f1977e64ef22e50f72665d75e7dfd6805deab4e81826",
17
- "scope-context-wire.mjs": "a15d0b966615b8293e53e4229edc8fb444fb144cc336dbd6354ac995f33e8cc8",
18
- "scope-context-journal.mjs": "753821f546e36185b2e891e8f38758a2d15a591f700f0650d478892c8d958a45",
19
+ "scope-context-remote.mjs": "cbcef3fb0b9e1cf11b68f877c227ac9b0c617432221dc6e661bce9bc4667b87a",
20
+ "scope-context-state.mjs": "dc47da57e411ef11677560a767f2e6d50f8a2fae17706b44715d8cd115ffcf8e",
21
+ "scope-context-wire.mjs": "1aa8c86223e54927a209de59d3514393b4c905ee2fc520d49819aec0f1e97162",
22
+ "scope-context-journal.mjs": "0d9aa1315204d3600f62075fe9a4b0b958b21a04c3a50a743eb5dce1eb16e18a",
19
23
  "scope-context-release.mjs": "3b6f5d3bf7abe25e1a1edd0521053f4c1b1e3487b7f37d5f7c6080d32bc0c044",
20
24
  "cloud-locale.mjs": "0627ac75b98b439563b61ab217aacb55a099c1d0c8ed3b118776d2007c52dd70",
21
25
  "cloud-locale.d.mts": "aff4bced9bbac140f73fd0de24681287349389be5750d89d96abe0f8a2167664",
@@ -82,7 +86,7 @@
82
86
  "runtime/validator-runtime-v7.0.44.d.mts": "7797edbfe2f7de24da370204dce4dfd22822a1bd80df93514fff900b98afe36f",
83
87
  "snapshot-retention.mjs": "ca46aeb36b1a5a24fe7e967031a71b9a3d33f40cb715d344402cbe0dc9c425cb",
84
88
  "snapshot-cleanup-journal.mjs": "76a9684cc95b4eb076e1298d78a9dff37948b00d9607b8f546c599072d3ed331",
85
- "IDE-EXECUTION.md": "26d4f32de1694c7f29ceefc5a5e5f4bba717efc5f91c37a91c399faa1574a191",
89
+ "IDE-EXECUTION.md": "3da7cbfafc1a4e7d4059c6fe8e69d14dacd2c72e300fe8e056581faea7f23aeb",
86
90
  "REUSE-DISCIPLINE.md": "a2452911c2b50c4762b4a1374f1958059eb03efd59f63e1b50e1650c52b0dc98",
87
91
  "install-identity.mjs": "41b8c8d8b3e265d2057a07bf7dc98066c34d6b05c65bf7159ca9609c01ee4646",
88
92
  "package-archive.mjs": "e06db8d709db8710bead254ee950b78928088be01c58dcea383beaa5eb574e59",
@@ -158,13 +162,13 @@
158
162
  "install-cleanup.mjs": "8a8e1a7dfe37427f86cd4d6e317890d0829894a9949d4216674a716b36edbc54",
159
163
  "review-client.mjs": "3491eef68ed533174b31db3c53febb6519b8f088f71a9964644714a9a4c03c77",
160
164
  "version.mjs": "85284d4cd5e3314b449e6ae124243f96397bcc274817b5983a79cfdd903d46fd",
161
- "SKILL.md": "910bc4b97159582d6faa6e34785d94b8b7cd484b9a39ef4106edcffc767be9d2",
165
+ "SKILL.md": "1c1509924526711c92df7fd0600e4165353cc602663d83b225e083676501bba3",
162
166
  "CAPABILITY-MAP.md": "517809bb20750e5de89b2dab5705c902a27fa68cf4f3f392a5d8cf2f19818888",
163
167
  "AGREEMENT.md": "f93a68e92c457200dd024610e65f74e478b8f60860f951da818b70561f1b9bf8",
164
168
  "DELIVERY-BOUNDARIES.md": "cb23ce71b2197085c8441ba7c5b6c05fb21f2a954bf66b4d60cfef8b15895b8b",
165
- "package.json": "6aeead2f5dd48f4715cf45c0695f8462c362cdc3c2929439565f201b2576bd2f",
169
+ "package.json": "9a3bb032185a679b7b0146cc6bd1b46ed44e59d16cbab236c5e90fd4f167ad7c",
166
170
  "manifest.json": "ab06e307795bc9da5add926752b6c4dbdaf18625090261a035aa356a679c504f",
167
- "release.mjs": "a1c8f8a6945687658e43f75892bfcca58dee6c4947dc9ea002f31bee0ab67538",
171
+ "release.mjs": "c3a5af873db43872e5a6bdc16eb3d859e8d69f6b21bd1c3b754f4a2b6c305360",
168
172
  "local-cli.mjs": "058ac9a4653bc12e0d031b72034ebfe6a72b91b9b23353b9fee04881c7d2e098",
169
173
  "local-state-lock.mjs": "1dbf64264642a0851faf043e07ee9f66d2ad0508e9f799b0626246e42fae893e",
170
174
  "local-execution.mjs": "721ed7d29bbe4ce6d088909cd84ffc440978c3c9eb0946c49a4281ee685fe3aa",
@@ -187,7 +191,7 @@
187
191
  "local-chain-results.mjs": "0455f72cad574cfef87c2c6c0928d6432bba7e5dbaff521a2b62a9575299ca10",
188
192
  "local-approved-plan.mjs": "c2c88ab4085ff35a1811fe43e9a28699cfc9387eda54bfeafae3e7dd9e8cdb19",
189
193
  "local-budget-renewal.mjs": "6b436358066fe33b2f6d871fc90d8e7c551edd6255efd73c0d255d0fd35d5ef8",
190
- "local-probe.mjs": "9b948db1de1e34c7605c4c454cd4aced66d2b57d2cf4471adbb80bcc76dee635",
194
+ "local-probe.mjs": "398f5dd4973503ccad280e0224e4f567c9763d21555a0816afdd83c75987d33a",
191
195
  "local-budget-grants.mjs": "c5f6542f5d2c3bfe07f3ca5b5bc849bf05d886b06014f720322d530510775d3d",
192
196
  "local-budget-handoff.mjs": "85a8f006dad9c0153843c7b7748ee7fcd756fdba159c97c3905d7fe0c4f604af",
193
197
  "local-budget-handoff-contracts.mjs": "bb53ebf135ec7ecfbecee858cfb354f03fee9cfb3940fa5d967d45008bf9aac5",
@@ -241,16 +245,16 @@
241
245
  "local-scope-state.mjs": "7a6d275d6267a4d0bb55582e1bcc7654c502ecba9221b84b2149a98f0ee7518f",
242
246
  "local-scope-workspaces.mjs": "8a75ce7b6639b5682370b5738c5c2a90408e3130729608e008f03053cd50351f",
243
247
  "local-task-routing.mjs": "6e2e506b2fb0e2e8d8c5147d7df32849b57a4e94a23634f01a355d45735858e4",
244
- "remote-client.mjs": "d30fd61fab3171a9e41546706a57ae372786d20ea9e9c01cad39d9863c156b40",
248
+ "remote-client.mjs": "6cf62706f8ac5f69368d951cd697f912040b2e58ac480dfb151bddb18b09b20d",
245
249
  "cloud-task-client.mjs": "f9085c5e6af6f179282670d2e872b85bfabac2657d3d232b1246dc23f5a6ccdd",
246
- "scope-runner-client.mjs": "b85999bca05efd804325f97b6e5704623ac3fcf751c6f9760717a23f40661ad1",
247
- "scope-amendment-client.mjs": "64a38deddc2dbfae543fead2cff8c9c78771b576391cc903aa58db925a138ad7",
248
- "scope-amendment-state.mjs": "922c4fd5a71101651d40fdb11f94f09c904cc244ff2574ae6b6ee004448aad06",
250
+ "scope-runner-client.mjs": "cc4fe202d5f777a0ec4c46f9b41a18aaf024c0d2e769a0b26bc6505f2f69f460",
251
+ "scope-amendment-client.mjs": "022ec88ecf12a98dd6e9e7915158fc9111cdadfda12cf714804948c8027a57d3",
252
+ "scope-amendment-state.mjs": "9f88cc7029f62eed3b1a0cc1c03b6a98c5713b467a2112278692de90c973b2be",
249
253
  "scope-amendment-wire.mjs": "3c4823e23bbdf330616bb675f07f9fcad7423266bd50b92e1134d226060e9b83",
250
254
  "scope-amendment-remote.mjs": "15a0b3126248f3101a52e3d7742f9a816dd3d8e58f21bac33e99e810e86cc4fd",
251
255
  "scope-amendment-observer.mjs": "de796f2aca2f3bc7f33ae3323b07d98096c0f16b1ffde70e3055610d70d18020",
252
256
  "scope-amendment-mutation.mjs": "fa24191df290efad29d41eca3d1a081981fefde02b7dc4a11c75c6118a69106e",
253
- "scope-amendment-reference.mjs": "0537e53a1bea4b695f4804c5051261778c88f05b1ae4aff86c709dcfe83aa148",
257
+ "scope-amendment-reference.mjs": "dc1768131beb7db224dd2ff8522dbf6361f73e01f466c9334cdfa24118720078",
254
258
  "native-client.mjs": "6f5575909bae9daba643287f876ffa0f9ff1c53a367b32a40145d740456d6605",
255
259
  "native-notification.mjs": "8859a26c6f13d222711f763acf49ed8546b3306cf71f990643ac31a5ccaf1ba1",
256
260
  "local-peer-status.mjs": "a971bcc69d60bb32b875ca5bb4297d948631d5d2821b812c279613ea059a8b4f",
@@ -261,7 +265,7 @@
261
265
  "task-entry-client.mjs": "940fb068afe28a2a34fe96e285e149afdd9ba79649c0c65cde37ec4ed71ae39d",
262
266
  "task-identity.mjs": "e0e6cf1f48bfa5ee3c5cb86a3c21adfce6d66b4220f4a164ca099263c7a09fb4",
263
267
  "task-identity.d.mts": "55472e1ffc81b9d5e5c5bd56c0bf1d18ecde03ee5170a58a15ce9caf222d8761",
264
- "remote-transport.mjs": "cf0e31a0319518b6c7e1ce2ac99766a98fe9c936d26bbbec05952aaf0c33efb0",
268
+ "remote-transport.mjs": "e9ecb3b9cd4d3bcc7abe1f7097069a07a15319ca0d5641fee752d094eb039194",
265
269
  "local-credential.mjs": "2a302a7f67b5ea8dc73453fc96ab33db349b59ca11fa9456e48ca1d9669d877c",
266
270
  "local-invocations.mjs": "f6ac4c41c1a5ec34de358c4cca210ca8e403b3a913150a43bb55ec9be55d9998",
267
271
  "local-task-handoff-fence.mjs": "5532a359ee29b885f7ce0cf7178f76dba0fc9245bdd92ef460982275baf0d557",
package/release.mjs CHANGED
@@ -9,10 +9,10 @@ import { fileURLToPath } from 'node:url'
9
9
  import { chainReleaseLabels } from './local-chain-release-labels.mjs'
10
10
 
11
11
  const directory = path.dirname(fileURLToPath(import.meta.url))
12
- import { swarmSourceFiles, coordination17SourceFiles, preservedMutationSourceFiles, memberContinuationSourceFiles } from './release-source-groups.mjs'
12
+ import { swarmSourceFiles, coordination17SourceFiles, preservedMutationSourceFiles, memberContinuationSourceFiles, scopeEvidenceSourceFiles, scopeEvidenceReleaseNote } from './release-source-groups.mjs'
13
13
  const sourceFiles = [
14
14
  'release-source-groups.mjs', 'local-scope-build-read.mjs', 'local-scope-build-io.mjs', 'local-scope-build-facts.mjs', 'local-scope-build-data.mjs', 'local-scope-build-readset.mjs',
15
- ...SCOPE_CONTEXT_SOURCE_FILES, 'cloud-locale.mjs', 'cloud-locale.d.mts', 'cloud-task-images.mjs', 'model-images.mjs', 'model-images.d.mts', 'cli-input.mjs',
15
+ ...scopeEvidenceSourceFiles, ...SCOPE_CONTEXT_SOURCE_FILES, 'cloud-locale.mjs', 'cloud-locale.d.mts', 'cloud-task-images.mjs', 'model-images.mjs', 'model-images.d.mts', 'cli-input.mjs',
16
16
  'scope-amendment-retirement.mjs', 'scope-amendment-consumption.mjs', 'mutation-connection-state.mjs', 'mutation-challenge-refresh.mjs',
17
17
  'local-scope-font.mjs', 'local-scope-zip.mjs', 'member-producer-repair-authority.mjs',
18
18
  'local-mutation-completion-evidence.mjs', 'local-mutation-environment-observation.mjs', 'local-mutation-finder-observation.mjs', 'local-finder-store.mjs', 'local-finder-plist.mjs',
@@ -273,6 +273,7 @@ function releaseNote(changed) {
273
273
  if (suppliedZh && suppliedEn) return {
274
274
  zh: suppliedZh.replaceAll(/\s+/g, ' '), en: suppliedEn.replaceAll(/\s+/g, ' '),
275
275
  }
276
+ if (changed.some(file => scopeEvidenceSourceFiles.includes(file))) return scopeEvidenceReleaseNote
276
277
  if (changed.some(file => SCOPE_CONTEXT_SOURCE_FILES.includes(file))) return scopeContextReleaseNote
277
278
  if (changed.some(file => file.startsWith('scope-amendment-'))) return {
278
279
  zh: '新增同一任务的精确范围补正:原账号审核文件哈希与写入/只读角色,原S签名真实事实并重评原合同;计划、写入和结算绑定不可变范围修订,改后刷新保留原M消费证据与失败历史。',
package/releases.json CHANGED
@@ -1,4 +1,16 @@
1
1
  [
2
+ {
3
+ "version": "0.1.73",
4
+ "date": "2026-10-20",
5
+ "zh": "修复通用范围连接在长时间扫描后的签名期限绑定:原 S 证明到期时间不晚于原云端挑战,保留原随机数、完整范围、独立验签和失败历史;已过期挑战仍明确拒绝。",
6
+ "en": "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
+ {
9
+ "version": "0.1.72",
10
+ "date": "2026-10-20",
11
+ "zh": "将完整范围证据与签名控制分离:四类原 S 证明经有界独立通道上传,紧凑权威绑定不可变摘要与字节数;未知上传仅恢复原 GET,保留原路径、上下文、审批、独立 M 写入和全部旧失败记录。",
12
+ "en": "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."
13
+ },
2
14
  {
3
15
  "version": "0.1.71",
4
16
  "date": "2026-10-20",
package/remote-client.mjs CHANGED
@@ -39,7 +39,7 @@ export function buildInvocation(operation, input) {
39
39
  'message-status', 'message-delivery-start', 'message-delivery-report', 'message-resolve', 'message-complete',
40
40
  'handoff-prepare', 'handoff-release', 'handoff-claim', 'handoff-status', 'handoff-resume', 'handoff-execution-authority', 'mutation-snapshot-challenge', 'mutation-pass-issue', 'mutation-pass-settle',
41
41
  'member-observation-admit', 'member-observation-report', 'member-observation-status',
42
- 'scope-runner-prepare', 'scope-runner-register', 'scope-runner-challenge', 'scope-runner-attest',
42
+ 'scope-runner-prepare', 'scope-runner-register', 'scope-runner-challenge', 'scope-runner-attest', 'scope-evidence-prepare',
43
43
  'scope-runner-context-review', 'scope-runner-context-status', 'scope-runner-context-challenge', 'scope-runner-context-apply',
44
44
  'start', 'continue'].includes(operation)) throw new Error('Invalid entrorove operation');
45
45
  if (!input || typeof input !== 'object' || Array.isArray(input)) throw new Error('Input must be a JSON object');
@@ -108,6 +108,20 @@ export async function requestTransport(origin, pathname, options = {}) {
108
108
  }
109
109
  }
110
110
 
111
+ // Evidence is a separate bounded data channel. A persisted upload reservation
112
+ // is attempted once; every uncertain outcome must use its original GET.
113
+ export async function putScopeEvidence(origin, uploadId, body, headers = {}) {
114
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/.test(uploadId ?? '') || !Buffer.isBuffer(body)
115
+ || body.length < 1 || body.length > 16_000_000) throw new Error('Invalid Entrorove scope evidence upload');
116
+ const url = new URL(`/api/skill/evidence/${uploadId}`, trustedOrigin(origin));
117
+ const response = await attempt(url, { method: 'PUT', body, timeoutMs: 30_000,
118
+ headers: { ...headers, 'content-type': 'application/json; charset=utf-8', 'content-length': body.length, 'accept-encoding': 'gzip' } });
119
+ if (response.status >= 300 && response.status < 400) {
120
+ throw Object.assign(new Error('Entrorove API redirects are refused'), { code: 'ENTROROVE_REDIRECT', established: true });
121
+ }
122
+ return { ...response, attempts: 1 };
123
+ }
124
+
111
125
  export function parseTransportResponse(response, label = 'Entrorove') {
112
126
  try { return JSON.parse(response.body); }
113
127
  catch { throw Object.assign(new Error(`Invalid ${label} response`), { code: 'ENTROROVE_RESPONSE_INVALID' }); }
@@ -9,6 +9,7 @@ import { scopeAmendmentLocation, readScopeAmendmentState, saveScopeAmendmentStat
9
9
  import { amendmentRemote, scopeAmendmentStatus } from './scope-amendment-remote.mjs';
10
10
  import { observeScopeAmendment } from './scope-amendment-observer.mjs';
11
11
  import { retainScopeAmendmentReference } from './scope-amendment-reference.mjs';
12
+ import { saveLocalScopeEvidence, ensureScopeEvidence, validateAppliedEvidence } from './scope-evidence-remote.mjs';
12
13
 
13
14
  const schemaVersion = 'entrorove.scope-amendment-connection/1.0';
14
15
  function validateProposal(status, input) {
@@ -17,8 +18,9 @@ function validateProposal(status, input) {
17
18
  || !['pending', 'approved', 'declined'].includes(proposal.approval)) amendmentFail('Current account has no matching original scope proposal');
18
19
  return proposal;
19
20
  }
20
- function validateApplied(output, state, location) {
21
- const effective = output.effectiveAuthority, record = scopeRevisionAuthority(effective?.record, 'entrorove.scope-revision-authority/1.0');
21
+ async function validateApplied(output, state, location) {
22
+ const effective = output.effectiveAuthority, version = effective?.record?.schemaVersion === 'entrorove.scope-revision-authority/2.0' ? '2.0' : '1.0';
23
+ const record = scopeRevisionAuthority(effective?.record, `entrorove.scope-revision-authority/${version}`);
22
24
  const challenge = scopeAmendmentChallenge(state.challenged.challenge, location, { allowExpired: true }), refresh = challenge.purpose === 'scope-revision-refresh';
23
25
  if (output.status !== 'succeeded' || output.grantsExecutionAuthority !== false || output.claimsCompletion !== false
24
26
  || output.scopeRevisionDigest !== sha256(canonical(record)) || effective.revisionDigest !== output.scopeRevisionDigest
@@ -30,21 +32,26 @@ function validateApplied(output, state, location) {
30
32
  || canonical(record.files.map(file => ({ ...file, sha256: null }))) !== canonical(challenge.files.map(file => ({ ...file, sha256: null })))) {
31
33
  amendmentFail('Applied scope amendment lost its signed original authority or exact roles');
32
34
  }
33
- const fact = refresh ? scopeRevisionAuthority(effective.fact, 'entrorove.scope-revision-facts/1.0') : record;
35
+ const factVersion = effective?.fact?.schemaVersion === 'entrorove.scope-revision-facts/2.0' ? '2.0' : '1.0';
36
+ const fact = refresh ? scopeRevisionAuthority(effective.fact, `entrorove.scope-revision-facts/${factVersion}`) : record;
37
+ const compact = fact.schemaVersion.endsWith('/2.0');
38
+ const attestation = compact ? await validateAppliedEvidence(fact, state, location, challenge,
39
+ refresh ? 'scope-refresh' : 'scope-revision') : state.attestation;
34
40
  if (refresh && (fact.scopeRevisionDigest !== output.scopeRevisionDigest || fact.factRevision !== challenge.expectedFactRevision
35
41
  || ['flowId', 'ownerId', 'taskDigest', 'chainDigest'].some(field => fact[field] !== challenge[field]))
36
- || canonical(fact.candidateAttestation) !== canonical(state.attestation)
37
- || fact.scope?.attestationDigest !== sha256(canonical(state.attestation.scopeAttestation))
42
+ || !compact && canonical(fact.candidateAttestation) !== canonical(attestation)
43
+ || fact.scope?.attestationDigest !== sha256(canonical(attestation.scopeAttestation))
44
+ || compact && effective.candidateAttestationDigest !== sha256(canonical(attestation))
38
45
  || effective.factRevision !== (refresh ? fact.factRevision : 0) || output.factRevision !== effective.factRevision) {
39
46
  amendmentFail('Applied scope proof differs from this exact actual signed candidate');
40
47
  }
41
48
  return { schemaVersion, status: 'succeeded', flowId: challenge.flowId, proposalId: challenge.proposalId,
42
49
  proposalDigest: challenge.proposalDigest, scopeRevisionDigest: output.scopeRevisionDigest, revision: output.revision,
43
50
  factRevision: effective.factRevision, grantsExecutionAuthority: false, claimsCompletion: false,
44
- ...(refresh && state.attestation.memberMutation ? { localAction: {
51
+ ...(refresh && attestation.memberMutation ? { localAction: {
45
52
  command: 'npx @entrorove/skill mutation-settle <absolute repositoryRoot>', input: {
46
- flowId: challenge.flowId, taskId: state.attestation.memberMutation.consumptionReceipt.taskId,
47
- stepId: state.attestation.memberMutation.consumptionReceipt.stepId, gatePassPath: location.identity.gatePassPath } } } : {}),
53
+ flowId: challenge.flowId, taskId: attestation.memberMutation.consumptionReceipt.taskId,
54
+ stepId: attestation.memberMutation.consumptionReceipt.stepId, gatePassPath: location.identity.gatePassPath } } } : {}),
48
55
  nextStep: output.nextStep, ...(state.feedback ? { feedback: state.feedback } : {}) };
49
56
  }
50
57
  async function archiveAttempt(location, state) {
@@ -56,23 +63,28 @@ async function archiveAttempt(location, state) {
56
63
  await saveScopeAmendmentState(location, state);
57
64
  }
58
65
  async function finish(location, state, dependencies) {
59
- if (!state.attestation) {
60
- state.attestation = await observeScopeAmendment(location, state);
61
- state.attestationDigest = sha256(canonical(state.attestation)); await saveScopeAmendmentState(location, state);
66
+ if (!state.evidenceDescriptor && !state.applied) {
67
+ const attestation = state.attestation ?? await observeScopeAmendment(location, state);
68
+ const challenge = scopeAmendmentChallenge(state.challenged.challenge, location, { allowExpired: true });
69
+ await saveLocalScopeEvidence(attestation, challenge.purpose === 'scope-revision-refresh' ? 'scope-refresh' : 'scope-revision',
70
+ challenge, state, location, saveScopeAmendmentState);
71
+ }
72
+ if (!state.applied) {
73
+ const attestationRef = await ensureScopeEvidence(state, location, dependencies, state.challenged.challenge, saveScopeAmendmentState);
74
+ await amendmentRemote('applied', 'workflow-scope-apply', { proposalId: location.identity.proposalId,
75
+ proposalDigest: location.identity.proposalDigest, attestationRef }, state, location, dependencies);
62
76
  }
63
- const applied = state.applied ?? await amendmentRemote('applied', 'workflow-scope-apply', {
64
- proposalId: location.identity.proposalId, proposalDigest: location.identity.proposalDigest,
65
- attestation: state.attestation }, state, location, dependencies);
66
- const result = validateApplied(applied, state, location);
77
+ const applied = state.applied, result = await validateApplied(applied, state, location);
67
78
  await retainScopeAmendmentReference(location, applied); return result;
68
79
  }
69
80
  async function amend(location, dependencies) {
70
81
  const state = await readScopeAmendmentState(location);
82
+ if (state.evidencePending || state.pending?.operation === 'scope-evidence-prepare') return finish(location, state, dependencies);
71
83
  if (state.pending) {
72
84
  const pending = state.pending;
73
85
  await amendmentRemote(pending.stage, pending.input.operation, pending.input.payload, state, location, dependencies);
74
86
  if (pending.stage === 'applied') {
75
- const result = validateApplied(state.applied, state, location);
87
+ const result = await validateApplied(state.applied, state, location);
76
88
  await retainScopeAmendmentReference(location, state.applied); return result;
77
89
  }
78
90
  return finish(location, state, dependencies);
@@ -95,7 +107,7 @@ async function amend(location, dependencies) {
95
107
  const challenge = scopeAmendmentChallenge(challenged.challenge, location);
96
108
  if (status.effective ? challenge.purpose !== 'scope-revision-refresh'
97
109
  || challenge.authorityRevisionDigest !== status.effective.revisionDigest || challenge.expectedFactRevision !== status.effective.factRevision + 1
98
- || challenge.currentScopeArtifactDigest !== sha256(canonical(status.effective.scope))
110
+ || challenge.currentScopeArtifactDigest !== (status.effective.scopeArtifactDigest ?? sha256(canonical(status.effective.scope)))
99
111
  : challenge.purpose !== 'scope-revision' || challenge.expectedRevision !== proposal.expectedRevision
100
112
  || challenge.previousDigest !== proposal.previousDigest) amendmentFail('Scope amendment challenge has a different original authority head');
101
113
  return finish(location, state, dependencies);
@@ -115,9 +127,9 @@ export async function runScopeAmend(repositoryRoot, input, dependencies) {
115
127
  message: ['ENTROROVE_SCOPE_AMENDMENT_BINDING', 'ENTROROVE_SCOPE_AMENDMENT_STATE', 'ENTROROVE_SCOPE_AMENDMENT_UNKNOWN',
116
128
  'ENTROROVE_SCOPE_AMENDMENT_NOT_SENT', 'ENTROROVE_SCOPE_AMENDMENT_MUTATION_PENDING'].includes(error?.code) ? error.message
117
129
  : 'Scope amendment stopped; preserve the original proposal, signer, journal and actual evidence.',
118
- ...(requestId ? { requestId } : {}), ...(error?.output ? { output: error.output } : {}),
130
+ ...(requestId ? { requestId } : {}), ...(error?.uploadId ? { uploadId: error.uploadId } : {}), ...(error?.output ? { output: error.output } : {}),
119
131
  ...(error?.feedback ? { feedback: error.feedback } : {}),
120
- ...(error?.outcomeUnknown ? { outcomeUnknown: true, nextAction: { operation: 'scope-amend', requestId,
132
+ ...(error?.outcomeUnknown ? { outcomeUnknown: true, nextAction: { operation: 'scope-amend', ...(requestId ? { requestId } : { uploadId: error.uploadId }),
121
133
  method: 'GET', reuseOriginalInput: true, repeatPost: false } } : {}) };
122
134
  }
123
135
  }
@@ -1,7 +1,10 @@
1
1
  import path from 'node:path';
2
2
  import { canonical, sha256 } from './local-integrity.mjs';
3
- import { mutationHome14, privateMutationDirectory14, readMutationJson14, atomicMutationJson14 } from './local-mutation-files.mjs';
3
+ import { mutationHome14, mutationFile14, privateMutationDirectory14, readMutationJson14, atomicMutationJson14 } from './local-mutation-files.mjs';
4
4
  import { scopeRevisionAuthority, amendmentFail } from './scope-amendment-wire.mjs';
5
+ import { readScopeEvidence } from './scope-evidence-local.mjs';
6
+ import { evidenceReference } from './scope-evidence-wire.mjs';
7
+ import { trustedOrigin } from './remote-transport.mjs';
5
8
 
6
9
  async function referenceLocation(root, home, flowId, revisionDigest) {
7
10
  const stateRoot = path.join(await mutationHome14(home), '.entrorove');
@@ -11,21 +14,36 @@ async function referenceLocation(root, home, flowId, revisionDigest) {
11
14
  export async function retainScopeAmendmentReference(location, output) {
12
15
  const effective = output.effectiveAuthority, target = await referenceLocation(location.root, location.home,
13
16
  location.identity.flowId, output.scopeRevisionDigest);
14
- const value = { schemaVersion: 'entrorove.scope-amendment-reference/1.0', input: {
17
+ const compact = effective.record.schemaVersion === 'entrorove.scope-revision-authority/2.0';
18
+ if (compact) await readScopeEvidence(location, evidenceReference(effective.record.candidateAttestationRef, undefined, location));
19
+ const value = { schemaVersion: compact ? 'entrorove.scope-amendment-reference/2.0' : 'entrorove.scope-amendment-reference/1.0',
20
+ ...(compact ? { apiOrigin: location.apiOrigin } : {}), input: {
15
21
  flowId: location.identity.flowId, proposalId: location.identity.proposalId, proposalDigest: location.identity.proposalDigest }, record: effective.record };
16
22
  await privateMutationDirectory14(target.directory, { privateFrom: target.stateRoot });
17
23
  try { if (canonical(await readMutationJson14(target.file)) !== canonical(value)) amendmentFail('Scope amendment original authority reference changed'); }
18
24
  catch (error) { if (error.code !== 'ENOENT') throw error; await atomicMutationJson14(target.file, value, { privateFrom: target.stateRoot }); }
19
25
  }
26
+ async function readReferenceEvidence(reference, input) {
27
+ const origin = trustedOrigin(reference.apiOrigin), stateRoot = path.join(await mutationHome14(input.home), '.entrorove');
28
+ const base = path.join(stateRoot, 'scope-runners', sha256(`${origin}:${reference.record.flowId}:${input.repositoryRoot}`));
29
+ const file = path.join(base, 'identity', 'binding.json'), item = await mutationFile14(file);
30
+ if (item.stat.size > 1_000_000 || item.stat.nlink !== 1) amendmentFail('Original amendment signer binding is invalid');
31
+ const binding = await readMutationJson14(file);
32
+ if (binding.repositoryRoot !== input.repositoryRoot || binding.apiOrigin !== origin || binding.flowId !== reference.record.flowId) amendmentFail('Original amendment signer binding changed');
33
+ const location = { base, binding };
34
+ await readScopeEvidence(location, evidenceReference(reference.record.candidateAttestationRef, undefined, location));
35
+ }
20
36
  export async function scopeAmendmentRefreshAction(input, pass) {
21
37
  if (!pass.scopeRevisionDigest) return undefined;
22
38
  try {
23
39
  const target = await referenceLocation(input.repositoryRoot, input.home, pass.flowId, pass.scopeRevisionDigest);
24
40
  const reference = await readMutationJson14(target.file);
25
- const record = scopeRevisionAuthority(reference.record, 'entrorove.scope-revision-authority/1.0');
26
- if (reference.schemaVersion !== 'entrorove.scope-amendment-reference/1.0' || sha256(canonical(record)) !== pass.scopeRevisionDigest
41
+ const compact = reference.schemaVersion === 'entrorove.scope-amendment-reference/2.0';
42
+ const record = scopeRevisionAuthority(reference.record, `entrorove.scope-revision-authority/${compact ? '2.0' : '1.0'}`);
43
+ if (!['entrorove.scope-amendment-reference/1.0', 'entrorove.scope-amendment-reference/2.0'].includes(reference.schemaVersion) || sha256(canonical(record)) !== pass.scopeRevisionDigest
27
44
  || record.flowId !== pass.flowId || record.taskDigest !== pass.taskDigest || record.scope.keyId !== pass.memberScopeKeyId
28
45
  || ['flowId', 'proposalId', 'proposalDigest'].some(field => reference.input[field] !== record[field])) amendmentFail('Stored refresh reference has another original authority');
46
+ if (compact) await readReferenceEvidence(reference, input);
29
47
  return { command: 'npx @entrorove/skill scope-amend <absolute repositoryRoot>',
30
48
  input: { ...reference.input, gatePassPath: input.gatePassPath }, resumeOriginalFlow: true, requiredBefore: 'mutation-settle' };
31
49
  } catch {
@@ -80,7 +80,7 @@ export async function scopeAmendmentLocation(repositoryRoot, input, dependencies
80
80
  const directory = path.join(base, 'amendments', sha256(canonical(input)));
81
81
  await privateMutationDirectory14(directory, { privateFrom: stateRoot });
82
82
  const contextAuthority = await boundScopeContexts(base, binding, dependencies);
83
- return { root, home, stateRoot, directory, file: path.join(directory, 'journal.json'), identity,
83
+ return { root, home, apiOrigin, base, stateRoot, directory, file: path.join(directory, 'journal.json'), identity,
84
84
  binding, key, signerConfigPath, original, contextAuthority };
85
85
  }
86
86
  export async function readScopeAmendmentState(location) {
@@ -9,6 +9,7 @@ import { scopeContextLocation, effectiveScopeContexts, retainContextAuthority,
9
9
  import { contextRemote } from './scope-context-remote.mjs';
10
10
  import { inspectContextEvidence, observeScopeContext, prepareScopeContext } from './scope-context-observer.mjs';
11
11
  import { contextJournal, renewContextChallenge } from './scope-context-journal.mjs';
12
+ import { saveLocalScopeEvidence, ensureScopeEvidence, validateAppliedEvidence } from './scope-evidence-remote.mjs';
12
13
 
13
14
  const schemaVersion = 'entrorove.scope-context-connection/1.0';
14
15
  function proposalFor(status, input, location) {
@@ -56,8 +57,10 @@ async function review(location, state, dependencies) {
56
57
  async function applied(location, state) {
57
58
  const effective = state.applied.contextAuthority;
58
59
  const challenge = contextChallenge((state.signingChallenge ?? state.challenged).challenge, location, state.proposal, { allowExpired: true });
60
+ const compact = effective?.record?.schemaVersion === 'entrorove.scope-context-authority/2.0';
61
+ if (compact) await validateAppliedEvidence(effective.record, state, location, challenge, 'scope-context');
59
62
  if (state.applied.grantsExecutionAuthority !== false || !effective?.record
60
- || canonical(effective.record.attestation) !== canonical(state.attestation)
63
+ || !compact && canonical(effective.record.attestation) !== canonical(state.attestation)
61
64
  || ['proposalId', 'proposalDigest', 'ownerId', 'taskDigest', 'chainDigest'].some(field => effective.record[field] !== challenge[field])
62
65
  || canonical(effective.record.importContexts) !== canonical(challenge.importContexts)
63
66
  || canonical(effective.record.evidence) !== canonical(challenge.evidence)) contextFail('Applied context authority differs from this exact actual candidate');
@@ -76,14 +79,20 @@ async function apply(location, state, dependencies) {
76
79
  state.proposal = proposal; await saveContextJournal(location, state);
77
80
  if (!state.challenged) await contextRemote('challenged', 'scope-runner-context-challenge', { flowId: location.binding.flowId,
78
81
  proposalId: proposal.proposalId, proposalDigest: proposal.proposalDigest, attemptId: state.attemptId }, state, location, dependencies);
79
- if (!state.attestation) {
82
+ if (!state.evidenceDescriptor) {
83
+ let attestation = state.attestation;
84
+ if (!attestation) {
80
85
  const prepared = await prepareScopeContext(location, state, proposal);
81
- state.attestation = await observeScopeContext(location, state, proposal, prepared,
86
+ attestation = await observeScopeContext(location, state, proposal, prepared,
82
87
  () => renewContextChallenge(location, state, proposal, dependencies));
83
- await saveContextJournal(location, state);
88
+ }
89
+ await saveLocalScopeEvidence(attestation, 'scope-context', (state.signingChallenge ?? state.challenged).challenge,
90
+ state, location, saveContextJournal);
84
91
  }
92
+ const attestationRef = await ensureScopeEvidence(state, location, dependencies,
93
+ (state.signingChallenge ?? state.challenged).challenge, saveContextJournal);
85
94
  await contextRemote('applied', 'scope-runner-context-apply', { flowId: location.binding.flowId,
86
- proposalId: proposal.proposalId, proposalDigest: proposal.proposalDigest, attestation: state.attestation }, state, location, dependencies);
95
+ proposalId: proposal.proposalId, proposalDigest: proposal.proposalDigest, attestationRef }, state, location, dependencies);
87
96
  return applied(location, state);
88
97
  }
89
98
  async function run(location, input, dependencies) {
@@ -96,7 +105,7 @@ async function run(location, input, dependencies) {
96
105
  state.completed = true; await saveContextJournal(location, state);
97
106
  return { schemaVersion, ...status, status: 'succeeded', flowId: input.flowId, grantsExecutionAuthority: false, claimsCompletion: false };
98
107
  } catch (error) {
99
- if (!state.pending) { state.rejection ??= { code: error.code, message: error.message }; await saveContextJournal(location, state); }
108
+ if (!state.pending && !state.evidencePending) { state.rejection ??= { code: error.code, message: error.message }; await saveContextJournal(location, state); }
100
109
  throw error;
101
110
  }
102
111
  }
@@ -111,8 +120,8 @@ export async function runScopeContext(repositoryRoot, input, dependencies) {
111
120
  return { schemaVersion, status: 'blocked', flowId: input?.flowId,
112
121
  code: error?.code?.startsWith('ENTROROVE_') ? error.code : 'ENTROROVE_SCOPE_CONTEXT_PREREQUISITE',
113
122
  message: error?.code?.startsWith('ENTROROVE_') ? error.message : 'Context recovery stopped; preserve original signer, binding, journal and evidence.',
114
- ...(error?.requestId ? { requestId: error.requestId } : {}), ...(error?.output ? { output: error.output } : {}),
123
+ ...(error?.requestId ? { requestId: error.requestId } : {}), ...(error?.uploadId ? { uploadId: error.uploadId } : {}), ...(error?.output ? { output: error.output } : {}),
115
124
  ...(error?.outcomeUnknown ? { outcomeUnknown: true, nextAction: { operation: 'scope-context', method: 'GET',
116
- requestId: error.requestId, reuseOriginalInput: true, repeatPost: false } } : {}) };
125
+ ...(error.requestId ? { requestId: error.requestId } : { uploadId: error.uploadId }), reuseOriginalInput: true, repeatPost: false } } : {}) };
117
126
  }
118
127
  }
@@ -13,16 +13,17 @@ export async function archiveContextAttempt(location, state) {
13
13
  catch (error) { if (error.code !== 'ENOENT') throw error; await atomicMutationJson14(file, state, { privateFrom: location.stateRoot }); }
14
14
  }
15
15
  function reusableSnapshot(state, input) {
16
- return input.action === 'apply' && state.snapshot && state.challenged && !state.pending && !state.completed && !state.applied
16
+ return input.action === 'apply' && state.snapshot && state.challenged && !state.pending && !state.evidencePending && !state.completed && !state.applied
17
17
  && state.rejection && !state.rejection.requestId && !state.rejection.output;
18
18
  }
19
19
  export async function contextJournal(location, input) {
20
20
  let state = await readContextJournal(location, input);
21
21
  if (!state.previous && reusableSnapshot(state, input)) {
22
22
  await archiveContextAttempt(location, state);
23
- for (const field of ['rejection', 'checked', 'attestation', 'signingChallenge', 'signingAttemptId']) delete state[field];
23
+ for (const field of ['rejection', 'checked', 'attestation', 'signingChallenge', 'signingAttemptId',
24
+ 'evidenceDescriptor', 'evidenceSignature', 'evidencePrepared', 'attestationRef', 'evidenceResult']) delete state[field];
24
25
  await saveContextJournal(location, state);
25
- } else if (state.previous || !state.pending && !state.applied && (state.rejection
26
+ } else if (state.previous || !state.pending && !state.evidencePending && !state.applied && (state.rejection
26
27
  || input.action === 'status' && state.completed || input.action === 'apply' && state.completed)) {
27
28
  await archiveContextAttempt(location, state.previous ?? state); state = {};
28
29
  }
@@ -33,7 +34,7 @@ export async function contextJournal(location, input) {
33
34
  return state;
34
35
  }
35
36
  export async function renewContextChallenge(location, state, proposal, dependencies) {
36
- if (state.pending) contextFail('Recover the original pending request before any challenge renewal');
37
+ if (state.pending || state.evidencePending) contextFail('Recover the original pending request before any challenge renewal');
37
38
  await archiveContextAttempt(location, state);
38
39
  delete state.signingChallenge;
39
40
  state.signingAttemptId = randomUUID(); await saveContextJournal(location, state);
@@ -30,18 +30,18 @@ async function notSent(pending, home) {
30
30
  return !rows.length || rows.at(-1).event === 'not-sent';
31
31
  } catch (error) { return error.code === 'ENOENT'; }
32
32
  }
33
- export async function contextRemote(stage, operation, input, state, location, dependencies) {
33
+ export async function contextRemote(stage, operation, input, state, location, dependencies, save = saveContextJournal) {
34
34
  const requestId = buildInvocation(operation, input).requestId; let response, result;
35
35
  if (state.pending) {
36
36
  if (state.pending.stage !== stage || state.pending.requestId !== requestId || state.pending.operation !== operation
37
37
  || canonical(state.pending.input) !== canonical(input)) contextFail('Recover the original context POST before another operation');
38
38
  ({ response, result } = await recover(state.pending, dependencies));
39
39
  } else {
40
- state.pending = { stage, operation, input, requestId }; await saveContextJournal(location, state);
40
+ state.pending = { stage, operation, input, requestId }; await save(location, state);
41
41
  try { ({ response, result } = await invokeRemoteResult({ operation, input }, dependencies)); }
42
42
  catch (error) {
43
43
  if (await notSent(state.pending, dependencies.home)) {
44
- delete state.pending; await saveContextJournal(location, state);
44
+ delete state.pending; await save(location, state);
45
45
  throw Object.assign(new Error('Context request was not transmitted; repair its prerequisite and reuse the original input'),
46
46
  { code: 'ENTROROVE_SCOPE_CONTEXT_NOT_SENT', knownBeforeTransmission: true, requestId });
47
47
  }
@@ -51,10 +51,10 @@ export async function contextRemote(stage, operation, input, state, location, de
51
51
  if (!result?.output || result.output.requestId !== requestId) throw unknown(requestId);
52
52
  if (result.feedback) state.feedback = result.feedback;
53
53
  if (!response.ok || !['succeeded', 'needs-input'].includes(result.output.status)) {
54
- state.rejection = { requestId, output: result.output }; delete state.pending; await saveContextJournal(location, state);
54
+ state.rejection = { requestId, output: result.output }; delete state.pending; await save(location, state);
55
55
  throw Object.assign(new Error('Original context request was rejected; preserve its exact evidence and history'),
56
56
  { code: result.output.code ?? 'ENTROROVE_SCOPE_CONTEXT_REJECTED', requestId, output: result.output });
57
57
  }
58
- state[stage] = result.output.result ?? result.output; delete state.pending; await saveContextJournal(location, state);
58
+ state[stage] = result.output.result ?? result.output; delete state.pending; await save(location, state);
59
59
  return state[stage];
60
60
  }
@@ -8,6 +8,7 @@ import { mutationHome14, mutationFile14, readMutationJson14, atomicMutationJson1
8
8
  import { trustedOrigin } from './remote-transport.mjs';
9
9
  import { contextExact, contextFail, contextList, validateContextAuthority } from './scope-context-wire.mjs';
10
10
  import { hydrateCloudMutationAuthority, bootstrapCloudMutationAuthority } from './cloud-mutation-authority.mjs';
11
+ import { readScopeEvidence } from './scope-evidence-local.mjs';
11
12
 
12
13
  export async function contextPrivate(file, final = 'file', maximumBytes = 8_000_000) {
13
14
  const item = await mutationFile14(file, { final });
@@ -53,6 +54,7 @@ export async function effectiveScopeContexts(location) {
53
54
  for (const name of names) {
54
55
  const file = path.join(directory, name); await contextPrivate(file, 'file', 1_000_000);
55
56
  const saved = await readMutationJson14(file), checked = validateContextAuthority(saved.record, location, current);
57
+ if (checked.record.attestationRef) await readScopeEvidence(location, checked.record.attestationRef);
56
58
  if (saved.revisionDigest !== checked.revisionDigest || name !== `${String(checked.record.revision).padStart(6, '0')}-${checked.revisionDigest}.json`) contextFail('Context history checksum or order changed');
57
59
  current = checked;
58
60
  }
@@ -68,6 +70,7 @@ export async function retainContextAuthority(location, effective) {
68
70
  const current = await effectiveScopeContexts(location);
69
71
  if (current?.revisionDigest === effective?.revisionDigest && canonical(current.record) === canonical(effective.record)) return current;
70
72
  const checked = validateContextAuthority(effective?.record, location, current);
73
+ if (checked.record.attestationRef) await readScopeEvidence(location, checked.record.attestationRef);
71
74
  if (effective.revisionDigest !== checked.revisionDigest) contextFail('Applied context authority checksum changed');
72
75
  const directory = path.join(location.base, 'context-authority'), file = path.join(directory, `${String(checked.record.revision).padStart(6, '0')}-${checked.revisionDigest}.json`);
73
76
  await atomicMutationJson14(file, checked, { privateFrom: location.stateRoot }); return checked;
@@ -77,7 +80,7 @@ export async function readContextJournal(location, input) {
77
80
  await contextPrivate(location.file);
78
81
  const value = await readMutationJson14(location.file);
79
82
  if (value.schemaVersion !== 'entrorove.scope-context-journal/1.0' || value.localBindingDigest !== location.localBindingDigest) contextFail('Context journal cannot replace its original identity');
80
- if (value.pending && canonical(value.input) !== canonical(input)) contextFail('Recover the original pending context request before another operation');
83
+ if ((value.pending || value.evidencePending) && canonical(value.input) !== canonical(input)) contextFail('Recover the original pending context request before another operation');
81
84
  return canonical(value.input) === canonical(input) ? value : { previous: value };
82
85
  } catch (error) { if (error.code === 'ENOENT') return {}; throw error; }
83
86
  }
@@ -1,6 +1,7 @@
1
1
  import { canonical, sha256 } from './local-integrity.mjs';
2
2
  import { scopeFail, scopePath } from './local-scope-files.mjs';
3
3
  import { scopeRevisionAuthority } from './scope-amendment-wire.mjs';
4
+ import { evidenceReference } from './scope-evidence-wire.mjs';
4
5
 
5
6
  const id = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/;
6
7
  const hash = /^[a-f0-9]{64}$/;
@@ -51,7 +52,8 @@ export function contextInput(input) {
51
52
  export const originalContextDigest = location => sha256(canonical({ localBindingDigest: location.localBindingDigest,
52
53
  importContexts: location.binding.importContexts }));
53
54
  export function validateContextAuthority(value, location, previous) {
54
- const record = scopeRevisionAuthority(value, 'entrorove.scope-context-authority/1.0');
55
+ const version = value?.schemaVersion === 'entrorove.scope-context-authority/2.0' ? '2.0' : '1.0';
56
+ const record = scopeRevisionAuthority(value, `entrorove.scope-context-authority/${version}`);
55
57
  const expectedRevision = (previous?.record.revision ?? 0) + 1;
56
58
  if (['flowId', 'runnerId', 'repositoryId', 'keyId'].some(field => record[field] !== location.binding[field])
57
59
  || record.localBindingDigest !== location.localBindingDigest || record.revision !== expectedRevision
@@ -64,6 +66,12 @@ export function validateContextAuthority(value, location, previous) {
64
66
  contextEvidence(record.evidence);
65
67
  if (previous && ['ownerId', 'taskDigest', 'chainDigest'].some(field => record[field] !== previous.record[field])
66
68
  || record.importContexts.some(context => !record.evidence.some(file => file.path === context.configPath))) contextFail('Context authority lost its original task or actual configuration evidence');
69
+ if (version === '2.0') {
70
+ const reference = evidenceReference(record.attestationRef, undefined, location);
71
+ if (reference.kind !== 'scope-context' || Object.hasOwn(record, 'attestation')
72
+ || ['flowId', 'ownerId', 'taskDigest', 'chainDigest', 'proposalId', 'proposalDigest', 'challengeId'].some(field => record[field] !== reference[field])
73
+ || !hash.test(record.challengeDigest ?? '')) contextFail('Compact context authority lost its exact evidence binding');
74
+ }
67
75
  return { record, revisionDigest: sha256(canonical(record)) };
68
76
  }
69
77
  export function contextChallenge(value, location, proposal, { allowExpired = false } = {}) {
@@ -0,0 +1,52 @@
1
+ import { constants, promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { mutationFile14, writeMutationFile14 } from './local-mutation-files.mjs';
4
+ import { maximumScopeEvidenceBytes, evidenceFail, verifyEvidenceData } from './scope-evidence-wire.mjs';
5
+
6
+ function evidencePath(location, descriptor) {
7
+ if (!/^[a-f0-9]{64}$/.test(descriptor?.sha256 ?? '')) evidenceFail('Scope evidence requires its exact SHA256 derived private data file');
8
+ return path.join(location.base, 'evidence-data', `${descriptor.sha256}.json`);
9
+ }
10
+ async function privateEvidence(file) {
11
+ const item = await mutationFile14(file);
12
+ if (process.getuid && item.stat.uid !== process.getuid() || process.platform !== 'win32' && (item.stat.mode & 0o777) !== 0o600
13
+ || item.stat.nlink !== 1 || item.stat.size < 1 || item.stat.size > maximumScopeEvidenceBytes) evidenceFail('Scope evidence must remain a bounded private single-link data file');
14
+ const directory = await mutationFile14(path.dirname(file), { final: 'directory' });
15
+ if (process.getuid && directory.stat.uid !== process.getuid()
16
+ || process.platform !== 'win32' && (directory.stat.mode & 0o777) !== 0o700) evidenceFail('Scope evidence directory must remain private');
17
+ return item;
18
+ }
19
+ export async function readScopeEvidence(location, descriptor) {
20
+ const file = evidencePath(location, descriptor), before = await privateEvidence(file);
21
+ const handle = await fs.open(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
22
+ try {
23
+ const opened = await handle.stat();
24
+ const changed = stat => ['dev', 'ino', 'size', 'mtimeMs', 'ctimeMs', 'mode', 'nlink', 'uid'].some(field => stat[field] !== before.stat[field]);
25
+ if (changed(opened) || opened.size !== descriptor.canonicalBytes) evidenceFail('Scope evidence changed before its bounded private read');
26
+ const buffer = Buffer.alloc(opened.size + 1); let length = 0;
27
+ while (length < buffer.length) {
28
+ const chunk = await handle.read(buffer, length, buffer.length - length, length);
29
+ if (!chunk.bytesRead) break; length += chunk.bytesRead;
30
+ }
31
+ const bytes = buffer.subarray(0, length), after = await handle.stat(), latest = await privateEvidence(file);
32
+ if ([opened, after, latest.stat].some(stat => ['dev', 'ino', 'size', 'mtimeMs', 'ctimeMs', 'mode', 'nlink', 'uid']
33
+ .some(field => stat[field] !== before.stat[field])) || latest.ancestors.length !== before.ancestors.length
34
+ || latest.ancestors.some((item, index) => item.path !== before.ancestors[index].path
35
+ || item.stat.dev !== before.ancestors[index].stat.dev || item.stat.ino !== before.ancestors[index].stat.ino)) evidenceFail('Scope evidence changed during its stable private read');
36
+ let value;
37
+ try { value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes)); }
38
+ catch { evidenceFail('Scope evidence data has invalid UTF8 JSON'); }
39
+ if (!verifyEvidenceData(value, descriptor, location).equals(bytes)) evidenceFail('Scope evidence data lost its exact canonical bytes');
40
+ return value;
41
+ } finally { await handle.close(); }
42
+ }
43
+ export async function retainScopeEvidence(location, descriptor, value) {
44
+ const bytes = verifyEvidenceData(value, descriptor, location), file = evidencePath(location, descriptor);
45
+ try { await readScopeEvidence(location, descriptor); }
46
+ catch (error) {
47
+ if (error.code !== 'ENOENT') throw error;
48
+ await writeMutationFile14(file, bytes, { privateFrom: location.stateRoot });
49
+ await readScopeEvidence(location, descriptor);
50
+ }
51
+ return descriptor;
52
+ }
@@ -0,0 +1,78 @@
1
+ import { canonical, sha256 } from './local-integrity.mjs';
2
+ import { contextRemote } from './scope-context-remote.mjs';
3
+ import { readScopeEvidence, retainScopeEvidence } from './scope-evidence-local.mjs';
4
+ import { describeScopeEvidence, evidenceBytes, evidenceFail, evidenceUpload, evidenceReference } from './scope-evidence-wire.mjs';
5
+ import { requestTransport, putScopeEvidence, parseTransportResponse } from './remote-transport.mjs';
6
+ import { ideClientHeaders } from './ide-presentation.mjs';
7
+
8
+ const unknown = uploadId => Object.assign(new Error('Scope evidence upload outcome is unknown; recover the original reservation with GET only'),
9
+ { code: 'ENTROROVE_SCOPE_EVIDENCE_UNKNOWN', outcomeUnknown: true, uploadId });
10
+ export async function saveLocalScopeEvidence(value, kind, challenge, state, location, save) {
11
+ const described = describeScopeEvidence(kind, value, challenge, location);
12
+ await retainScopeEvidence(location, described.descriptor, value);
13
+ state.evidenceDescriptor = described.descriptor; state.evidenceSignature = described.signature;
14
+ state.attestationDigest = described.descriptor.sha256;
15
+ delete state.attestation;
16
+ await save(location, state);
17
+ }
18
+ function resultReference(value, upload, descriptor, location) {
19
+ if (value?.schemaVersion !== 'entrorove.scope-evidence-upload-status/1.0' || value.uploadId !== upload.uploadId
20
+ || value.grantsExecutionAuthority !== false || value.expiresAt !== upload.expiresAt
21
+ || !['reserved', 'receiving', 'stored', 'rejected'].includes(value.status)) throw unknown(upload.uploadId);
22
+ if (value.status !== 'stored') return null;
23
+ const reference = evidenceReference(value.evidenceReference, descriptor, location);
24
+ if (['ownerId', 'taskDigest', 'chainDigest'].some(field => reference[field] !== upload[field])) evidenceFail('Stored scope evidence replaced its original account or task');
25
+ return reference;
26
+ }
27
+ async function acceptResult(response, upload, state, location, save) {
28
+ let value;
29
+ try { value = parseTransportResponse(response, 'scope evidence upload'); }
30
+ catch { throw unknown(upload.uploadId); }
31
+ const reference = resultReference(value, upload, state.evidenceDescriptor, location);
32
+ if (reference) {
33
+ state.attestationRef = reference; state.evidenceResult = value; delete state.evidencePending;
34
+ await save(location, state); return reference;
35
+ }
36
+ if (value.status === 'rejected' && /^ENTROROVE_EVIDENCE_[A-Z_]+$/.test(value.code ?? '')) {
37
+ state.evidenceResult = value; state.rejection = { uploadId: upload.uploadId, output: value };
38
+ delete state.evidencePending; await save(location, state);
39
+ throw Object.assign(new Error('The original evidence reservation was rejected; preserve it before another signed attempt'),
40
+ { code: value.code, uploadId: upload.uploadId, output: value });
41
+ }
42
+ throw unknown(upload.uploadId);
43
+ }
44
+ export async function ensureScopeEvidence(state, location, dependencies, challenge, save) {
45
+ const descriptor = state.evidenceDescriptor;
46
+ if (!descriptor || !state.evidenceSignature) evidenceFail('Original signed evidence descriptor is unavailable');
47
+ const value = await readScopeEvidence(location, descriptor);
48
+ if (state.attestationRef) return evidenceReference(state.attestationRef, descriptor, location);
49
+ if (!state.evidencePrepared) await contextRemote('evidencePrepared', 'scope-evidence-prepare', {
50
+ flowId: descriptor.flowId, descriptor, signature: state.evidenceSignature }, state, location, dependencies, save);
51
+ const upload = evidenceUpload(state.evidencePrepared.upload, descriptor, location, challenge);
52
+ const token = await dependencies.readCredential(dependencies.home), headers = { authorization: `Bearer ${token}`, ...ideClientHeaders() };
53
+ const origin = dependencies.apiOrigin();
54
+ if (state.evidencePending) {
55
+ if (state.evidencePending.uploadId !== upload.uploadId || state.evidencePending.sha256 !== descriptor.sha256
56
+ || state.evidencePending.canonicalBytes !== descriptor.canonicalBytes) evidenceFail('Recover the exact original evidence reservation first');
57
+ let response;
58
+ try { response = await requestTransport(origin, `/api/skill/evidence/${upload.uploadId}`, { headers }); }
59
+ catch { throw unknown(upload.uploadId); }
60
+ return acceptResult(response, upload, state, location, save);
61
+ }
62
+ state.evidencePending = { uploadId: upload.uploadId, sha256: descriptor.sha256, canonicalBytes: descriptor.canonicalBytes };
63
+ await save(location, state);
64
+ let response;
65
+ try {
66
+ if (Date.parse(upload.expiresAt) <= Date.now()) response = await requestTransport(origin, `/api/skill/evidence/${upload.uploadId}`, { headers });
67
+ else response = await putScopeEvidence(origin, upload.uploadId, evidenceBytes(value), headers);
68
+ } catch { throw unknown(upload.uploadId); }
69
+ return acceptResult(response, upload, state, location, save);
70
+ }
71
+ export async function validateAppliedEvidence(record, state, location, challenge, kind) {
72
+ const reference = evidenceReference(kind === 'scope-context' ? record.attestationRef : record.candidateAttestationRef,
73
+ state.evidenceDescriptor, location);
74
+ if (reference.kind !== kind || canonical(reference) !== canonical(state.attestationRef)
75
+ || record.challengeId !== challenge.challengeId || record.challengeDigest !== sha256(canonical(challenge))
76
+ || ['ownerId', 'taskDigest', 'chainDigest'].some(field => reference[field] !== challenge[field])) evidenceFail('Applied authority differs from its original uploaded S evidence');
77
+ return readScopeEvidence(location, reference);
78
+ }
@@ -0,0 +1,120 @@
1
+ import { createPublicKey, sign, verify } from 'node:crypto';
2
+ import { canonical, sha256 } from './local-integrity.mjs';
3
+ import { scopeRevisionAuthority } from './scope-amendment-wire.mjs';
4
+
5
+ export const maximumScopeEvidenceBytes = 16_000_000;
6
+ const hash = /^[a-f0-9]{64}$/, id = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/;
7
+ const common = ['kind', 'flowId', 'challengeId', 'runnerId', 'repositoryId', 'keyId', 'sha256', 'canonicalBytes'];
8
+ export const evidenceFail = message => { throw Object.assign(new Error(message), { code: 'ENTROROVE_SCOPE_EVIDENCE_BINDING' }); };
9
+ function exact(value, fields) {
10
+ if (!value || typeof value !== 'object' || Array.isArray(value)
11
+ || Object.keys(value).sort().join('\0') !== [...fields].sort().join('\0')) evidenceFail('Scope evidence fields are invalid');
12
+ }
13
+ function validString(value) {
14
+ if (value.includes('\0') || Buffer.byteLength(value) > maximumScopeEvidenceBytes) evidenceFail('Scope evidence string exceeds its valid bounded UTF8 representation');
15
+ for (let index = 0; index < value.length; index++) {
16
+ const code = value.charCodeAt(index);
17
+ if (code >= 0xd800 && code <= 0xdbff) {
18
+ const next = value.charCodeAt(++index);
19
+ if (!(next >= 0xdc00 && next <= 0xdfff)) evidenceFail('Scope evidence contains an invalid Unicode surrogate');
20
+ } else if (code >= 0xdc00 && code <= 0xdfff) evidenceFail('Scope evidence contains an invalid Unicode surrogate');
21
+ }
22
+ }
23
+ export function evidenceBytes(value) {
24
+ const stack = [[value, 0, false]], ancestors = new Set(); let nodes = 0;
25
+ while (stack.length) {
26
+ const [item, depth, leaving] = stack.pop();
27
+ if (leaving) { ancestors.delete(item); continue; }
28
+ if (++nodes > 250_000 || depth > 64) evidenceFail('Scope evidence exceeds its bounded structure');
29
+ if (typeof item === 'string') { validString(item); continue; }
30
+ if (item === null || typeof item === 'boolean') continue;
31
+ if (typeof item === 'number' && Number.isFinite(item)) continue;
32
+ if (!item || typeof item !== 'object' || ancestors.has(item)
33
+ || !Array.isArray(item) && ![Object.prototype, null].includes(Object.getPrototypeOf(item))) evidenceFail('Scope evidence requires finite acyclic JSON');
34
+ ancestors.add(item); stack.push([item, depth, true]);
35
+ const fields = Object.getOwnPropertyDescriptors(item), keys = Reflect.ownKeys(fields);
36
+ if (Array.isArray(item) ? item.length > 100_000 : keys.length > 1024) evidenceFail('Scope evidence collection exceeds its bounded structure');
37
+ for (const key of keys) {
38
+ if (Array.isArray(item) && key === 'length') continue;
39
+ const field = fields[key];
40
+ if (typeof key !== 'string' || !Object.hasOwn(field, 'value') || !field.enumerable) evidenceFail('Scope evidence refuses accessors and non-JSON properties');
41
+ validString(key);
42
+ stack.push([field.value, depth + 1, false]);
43
+ }
44
+ }
45
+ const bytes = Buffer.from(canonical(value));
46
+ if (!bytes.length || bytes.length > maximumScopeEvidenceBytes) evidenceFail('Scope evidence exceeds its separate sixteen million byte data limit');
47
+ return bytes;
48
+ }
49
+ export function evidenceFields(value) {
50
+ if (!['scope-context', 'scope-revision', 'scope-refresh', 'scope-observation'].includes(value?.kind)) evidenceFail('Scope evidence kind is invalid');
51
+ return [...common, ...(value.kind === 'scope-observation' ? ['stepId'] : ['proposalId', 'proposalDigest'])];
52
+ }
53
+ export function validateEvidenceIdentity(value, expected, location) {
54
+ for (const field of evidenceFields(value)) {
55
+ if (field === 'canonicalBytes') {
56
+ if (!Number.isSafeInteger(value[field]) || value[field] < 1 || value[field] > maximumScopeEvidenceBytes) evidenceFail('Scope evidence byte length is invalid');
57
+ } else if (!(['sha256', 'keyId', 'proposalDigest'].includes(field) ? hash : id).test(value[field] ?? '')) evidenceFail('Scope evidence identity is invalid');
58
+ if (expected && value[field] !== expected[field]) evidenceFail('Scope evidence reference replaced its exact original data identity');
59
+ }
60
+ if (location && ['flowId', 'runnerId', 'repositoryId', 'keyId'].some(field => value[field] !== location.binding[field])) {
61
+ evidenceFail('Scope evidence replaced its original local signer binding');
62
+ }
63
+ return value;
64
+ }
65
+ function signature(value, key) {
66
+ const { signature: encoded, ...core } = value ?? {};
67
+ if (typeof encoded !== 'string' || !/^[A-Za-z0-9_-]{86}$/.test(encoded)
68
+ || Buffer.from(encoded, 'base64url').toString('base64url') !== encoded
69
+ || !verify(null, Buffer.from(canonical(core)), key, Buffer.from(encoded, 'base64url'))) evidenceFail('Original scope evidence S signature changed');
70
+ }
71
+ export function verifyEvidenceData(value, descriptor, location) {
72
+ validateEvidenceIdentity(descriptor, undefined, location);
73
+ const bytes = evidenceBytes(value);
74
+ if (sha256(bytes) !== descriptor.sha256 || bytes.length !== descriptor.canonicalBytes) evidenceFail('Scope evidence hash or canonical UTF8 length changed');
75
+ const key = createPublicKey({ key: Buffer.from(location.binding.publicKey, 'base64'), type: 'spki', format: 'der' });
76
+ if (sha256(key.export({ type: 'spki', format: 'der' })) !== descriptor.keyId) evidenceFail('Scope evidence original public key changed');
77
+ signature(value, key);
78
+ const observation = descriptor.kind === 'scope-observation', inner = observation ? value : value.scopeAttestation;
79
+ if (!observation) signature(inner, key);
80
+ if (inner?.schemaVersion !== 'entrorove.scope-facts/1.0'
81
+ || ['flowId', 'repositoryId', 'keyId'].some(field => inner[field] !== descriptor[field])
82
+ || observation && (inner.challenge !== descriptor.challengeId || inner.stepId !== descriptor.stepId)
83
+ || !observation && value.proposalDigest !== descriptor.proposalDigest
84
+ || descriptor.kind === 'scope-context' && (value.schemaVersion !== 'entrorove.scope-context-attestation/1.0' || value.challengeId !== descriptor.challengeId)
85
+ || ['scope-revision', 'scope-refresh'].includes(descriptor.kind) && (value.schemaVersion !== 'entrorove.scope-revision-attestation/1.0'
86
+ || value.proposalId !== descriptor.proposalId || value.purpose !== (descriptor.kind === 'scope-refresh' ? 'scope-revision-refresh' : 'scope-revision'))
87
+ || ['ownerId', 'taskDigest', 'chainDigest'].some(field => descriptor[field] !== undefined && inner[field] !== descriptor[field])) {
88
+ evidenceFail('Scope evidence has another original kind, task or challenge');
89
+ }
90
+ return bytes;
91
+ }
92
+ export function describeScopeEvidence(kind, value, challenge, location) {
93
+ const bytes = evidenceBytes(value), observation = kind === 'scope-observation';
94
+ const descriptor = { schemaVersion: 'entrorove.scope-evidence-descriptor/1.0', kind, flowId: challenge.flowId,
95
+ ...(observation ? { stepId: challenge.stepId } : { proposalId: challenge.proposalId, proposalDigest: challenge.proposalDigest }),
96
+ challengeId: observation ? challenge.challenge : challenge.challengeId, runnerId: location.binding.runnerId,
97
+ repositoryId: location.binding.repositoryId, keyId: location.binding.keyId, sha256: sha256(bytes), canonicalBytes: bytes.length };
98
+ verifyEvidenceData(value, descriptor, location);
99
+ const inner = observation ? value : value.scopeAttestation;
100
+ if (['flowId', 'ownerId', 'taskDigest', 'chainDigest', 'repositoryId', 'stepId', 'challenge'].some(field => inner[field] !== challenge[field])
101
+ || Date.parse(inner.expiresAt) > Date.parse(challenge.expiresAt)) evidenceFail('Scope evidence differs from its exact original challenge');
102
+ return { descriptor, signature: sign(null, Buffer.from(canonical(descriptor)), location.key).toString('base64url') };
103
+ }
104
+ export function evidenceReference(value, expected, location) {
105
+ exact(value, ['schemaVersion', 'evidenceId', ...evidenceFields(value), 'ownerId', 'taskDigest', 'chainDigest']);
106
+ if (value.schemaVersion !== 'entrorove.scope-evidence-reference/1.0' || !id.test(value.evidenceId ?? '')
107
+ || !id.test(value.ownerId ?? '') || !hash.test(value.taskDigest ?? '') || !hash.test(value.chainDigest ?? '')) evidenceFail('Scope evidence reference is invalid');
108
+ return validateEvidenceIdentity(value, expected, location);
109
+ }
110
+ export function evidenceUpload(value, descriptor, location, challenge) {
111
+ const upload = scopeRevisionAuthority(value, 'entrorove.scope-evidence-upload/1.0');
112
+ exact(upload, ['schemaVersion', ...evidenceFields(upload), 'uploadId', 'ownerId', 'taskDigest', 'chainDigest', 'issuedAt', 'expiresAt', 'grantsExecutionAuthority', 'authorityReceipt']);
113
+ validateEvidenceIdentity(upload, descriptor, location);
114
+ const issued = Date.parse(upload.issuedAt), expires = Date.parse(upload.expiresAt);
115
+ if (!id.test(upload.uploadId ?? '') || upload.grantsExecutionAuthority !== false
116
+ || ['ownerId', 'taskDigest', 'chainDigest'].some(field => upload[field] !== challenge[field])
117
+ || !Number.isFinite(issued) || !Number.isFinite(expires) || expires <= issued || expires - issued > 90_000
118
+ || expires > Date.parse(challenge.expiresAt)) evidenceFail('Scope evidence upload replaced its original challenge or authority');
119
+ return upload;
120
+ }
@@ -6,7 +6,9 @@ import { scopeFail, scopePath, scopeRoot, scopeTarget } from './local-scope-file
6
6
  import { withStateLock } from './local-state-lock.mjs';
7
7
  import { snapshotPlan, snapshotVerify } from './local-execution.mjs';
8
8
  import { probe } from './local-probe.mjs';
9
- import { invokeRemoteResult } from './remote-client.mjs';
9
+ import { contextRemote } from './scope-context-remote.mjs';
10
+ import { scopeRunnerJournal, saveScopeRunnerJournal } from './scope-runner-journal.mjs';
11
+ import { saveLocalScopeEvidence, ensureScopeEvidence } from './scope-evidence-remote.mjs';
10
12
  import { boundScopeContexts } from './scope-context-state.mjs';
11
13
 
12
14
  async function privateDirectory(directory) {
@@ -54,38 +56,7 @@ async function signer(directory, identity, dependencies) {
54
56
  }
55
57
  return { binding, key, signerConfigPath: path.join(controls, 'signer.json'), contextAuthority };
56
58
  }
57
- async function remote(operation, input, dependencies) {
58
- const { response, result } = await invokeRemoteResult({ operation, input }, dependencies);
59
- if (!response.ok || result.output.status !== 'succeeded') {
60
- throw Object.assign(new Error(result.output.message ?? `Scope connection blocked at ${operation}`),
61
- { code: result.output.code ?? 'ENTROROVE_SCOPE_BLOCKED', output: result.output });
62
- }
63
- return result.output;
64
- }
65
- async function connect(identity, directory, dependencies) {
66
- const { binding, key, signerConfigPath, contextAuthority } = await signer(directory, identity, dependencies);
67
- const prepared = await remote('scope-runner-prepare', { flowId: identity.flowId, attemptId: randomUUID(), runnerId: binding.runnerId,
68
- repositoryId: binding.repositoryId, publicKey: binding.publicKey, targetPaths: identity.targetPaths }, dependencies);
69
- const enrollment = prepared.enrollment;
70
- if (enrollment.flowId !== identity.flowId || enrollment.runnerId !== binding.runnerId || enrollment.keyId !== binding.keyId
71
- || canonical(enrollment.targetPaths) !== canonical(identity.targetPaths)) scopeFail('ENTROROVE_SCOPE_BINDING', 'Cloud enrollment differs from the original local task signer');
72
- await remote('scope-runner-register', { flowId: identity.flowId, enrollmentId: enrollment.enrollmentId,
73
- signature: sign(null, Buffer.from(canonical(enrollment)), key).toString('base64url') }, dependencies);
74
- const issued = await remote('scope-runner-challenge', { flowId: identity.flowId, receiverId: binding.runnerId, attemptId: randomUUID() }, dependencies);
75
- const challenge = issued.challenge;
76
- if (challenge.flowId !== identity.flowId || challenge.ownerId !== enrollment.ownerId || challenge.runnerId !== binding.runnerId
77
- || challenge.repositoryId !== binding.repositoryId || challenge.taskDigest !== enrollment.taskDigest
78
- || challenge.chainDigest !== enrollment.chainDigest || canonical(challenge.targetPaths) !== canonical(identity.targetPaths)) {
79
- scopeFail('ENTROROVE_SCOPE_BINDING', 'Cloud scope challenge differs from the original account and accepted task');
80
- }
81
- const input = { repositoryRoot: identity.repositoryRoot, home: dependencies.home, flowId: identity.flowId,
82
- stepId: challenge.stepId, paths: identity.targetPaths, snapshotId: `snapshot-${randomUUID()}`, importContexts: identity.importContexts,
83
- ...(contextAuthority ? { contextRevisionDigest: contextAuthority.revisionDigest, contextEvidence: contextAuthority.record.evidence } : {}) };
84
- const planned = await snapshotPlan(input), verified = await snapshotVerify(input);
85
- if (verified.verified !== true || verified.receipt.snapshotDigest !== planned.snapshotDigest) scopeFail('ENTROROVE_SCOPE_SNAPSHOT', 'Actual copied snapshot did not verify');
86
- const observed = await probe({ ...input, ownerId: challenge.ownerId, task: challenge.task, taskDigest: challenge.taskDigest,
87
- chainDigest: challenge.chainDigest, repositoryId: challenge.repositoryId, challenge: challenge.challenge, signerConfigPath });
88
- const accepted = await remote('scope-runner-attest', { flowId: identity.flowId, attestation: observed.attestation }, dependencies);
59
+ async function scopeConnectionResult(identity, binding, accepted) {
89
60
  const next = accepted.nextStep ?? { operation: 'workflow-gate', payload: { targetFiles: [...identity.targetPaths] } };
90
61
  if (next.localOperation === 'scope-connect') return { schemaVersion: 'entrorove.scope-connection/1.0', status: 'succeeded',
91
62
  flowId: identity.flowId, runnerId: binding.runnerId, scopeEvidence: accepted.scopeEvidence, grantsExecutionAuthority: false,
@@ -97,6 +68,63 @@ async function connect(identity, directory, dependencies) {
97
68
  runnerId: binding.runnerId, scopeEvidence: accepted.scopeEvidence, grantsExecutionAuthority: false,
98
69
  nextOperation: next.operation, ...(resumeOriginalRequest ? { resumeOriginalRequest: true } : { nextInput }), nextStep: next };
99
70
  }
71
+ async function observeConnection(identity, location, state, challenge, contextAuthority, dependencies) {
72
+ if (state.evidenceDescriptor) return;
73
+ const input = { repositoryRoot: identity.repositoryRoot, home: dependencies.home, flowId: identity.flowId,
74
+ stepId: challenge.stepId, paths: identity.targetPaths, snapshotId: state.snapshotId, importContexts: identity.importContexts,
75
+ ...(contextAuthority ? { contextRevisionDigest: contextAuthority.revisionDigest, contextEvidence: contextAuthority.record.evidence } : {}) };
76
+ if (!state.snapshotStarted) {
77
+ state.snapshotStarted = true; await saveScopeRunnerJournal(location, state);
78
+ try { await snapshotPlan(input); }
79
+ catch (error) { delete state.snapshotStarted; await saveScopeRunnerJournal(location, state); throw error; }
80
+ }
81
+ const verified = await snapshotVerify(input);
82
+ if (verified.verified !== true) scopeFail('ENTROROVE_SCOPE_SNAPSHOT', 'Actual copied snapshot did not verify');
83
+ const observed = await probe({ ...input, ownerId: challenge.ownerId, task: challenge.task, taskDigest: challenge.taskDigest,
84
+ chainDigest: challenge.chainDigest, repositoryId: challenge.repositoryId, challenge: challenge.challenge, signerConfigPath: location.signerConfigPath },
85
+ { maximumExpiresAt: Date.parse(challenge.expiresAt) });
86
+ if (observed.attestation.snapshotDigest !== verified.receipt.snapshotDigest) scopeFail('ENTROROVE_SCOPE_SNAPSHOT', 'Actual S proof replaced its original verified snapshot');
87
+ await saveLocalScopeEvidence(observed.attestation, 'scope-observation', challenge, state, location, saveScopeRunnerJournal);
88
+ }
89
+ async function connect(identity, directory, dependencies) {
90
+ const local = await signer(directory, identity, dependencies), { binding, key, contextAuthority } = local;
91
+ const location = { ...local, root: identity.repositoryRoot, home: dependencies.home, base: directory,
92
+ stateRoot: path.join(dependencies.home, '.entrorove'), directory: path.join(directory, 'scope-connection'),
93
+ file: path.join(directory, 'scope-connection', 'journal.json') };
94
+ const state = await scopeRunnerJournal(location, identity);
95
+ const remote = (stage, operation, input) => contextRemote(stage, operation, input, state, location, dependencies, saveScopeRunnerJournal);
96
+ try {
97
+ if (state.pending) await remote(state.pending.stage, state.pending.operation, state.pending.input);
98
+ const prepared = state.prepared ?? await remote('prepared', 'scope-runner-prepare', { flowId: identity.flowId,
99
+ attemptId: state.attemptId, runnerId: binding.runnerId, repositoryId: binding.repositoryId, publicKey: binding.publicKey, targetPaths: identity.targetPaths });
100
+ const enrollment = prepared.enrollment;
101
+ if (enrollment.flowId !== identity.flowId || enrollment.runnerId !== binding.runnerId || enrollment.keyId !== binding.keyId
102
+ || canonical(enrollment.targetPaths) !== canonical(identity.targetPaths)) scopeFail('ENTROROVE_SCOPE_BINDING', 'Cloud enrollment differs from the original local task signer');
103
+ if (!state.registered) await remote('registered', 'scope-runner-register', { flowId: identity.flowId, enrollmentId: enrollment.enrollmentId,
104
+ signature: sign(null, Buffer.from(canonical(enrollment)), key).toString('base64url') });
105
+ const issued = state.challenged ?? await remote('challenged', 'scope-runner-challenge', { flowId: identity.flowId,
106
+ receiverId: binding.runnerId, attemptId: state.attemptId });
107
+ const challenge = issued.challenge;
108
+ if (challenge.flowId !== identity.flowId || challenge.ownerId !== enrollment.ownerId || challenge.runnerId !== binding.runnerId
109
+ || challenge.repositoryId !== binding.repositoryId || challenge.taskDigest !== enrollment.taskDigest
110
+ || challenge.chainDigest !== enrollment.chainDigest || canonical(challenge.targetPaths) !== canonical(identity.targetPaths)) {
111
+ scopeFail('ENTROROVE_SCOPE_BINDING', 'Cloud scope challenge differs from the original account and accepted task');
112
+ }
113
+ if (!state.snapshotId) { state.snapshotId = `snapshot-${randomUUID()}`; await saveScopeRunnerJournal(location, state); }
114
+ if (!state.accepted) {
115
+ await observeConnection(identity, location, state, challenge, contextAuthority, dependencies);
116
+ const attestationRef = await ensureScopeEvidence(state, location, dependencies, challenge, saveScopeRunnerJournal);
117
+ await remote('accepted', 'scope-runner-attest', { flowId: identity.flowId, attestationRef });
118
+ }
119
+ state.completed = true; await saveScopeRunnerJournal(location, state);
120
+ return scopeConnectionResult(identity, binding, state.accepted);
121
+ } catch (error) {
122
+ if (!state.pending && !state.evidencePending) {
123
+ state.rejection ??= { code: error.code, message: error.message }; await saveScopeRunnerJournal(location, state);
124
+ }
125
+ throw error;
126
+ }
127
+ }
100
128
  export async function connectScopeRunner(repositoryRoot, input, dependencies) {
101
129
  if (!input || Object.keys(input).some(field => !['flowId', 'paths', 'importContexts'].includes(field))
102
130
  || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(input.flowId ?? '') || !Array.isArray(input.paths) || !input.paths.length) {
@@ -129,8 +157,10 @@ export async function runScopeConnect(repositoryRoot, input, dependencies) {
129
157
  if (!await dependencies.agreementAccepted(dependencies.home)) return dependencies.agreementRequired(dependencies.home);
130
158
  try { return await connectScopeRunner(repositoryRoot, input, dependencies); }
131
159
  catch (error) {
132
- if (error.outcomeUnknown || !error.code?.startsWith('ENTROROVE_')) throw error;
160
+ if (!error.code?.startsWith('ENTROROVE_')) throw error;
133
161
  return { schemaVersion: 'entrorove.scope-connection/1.0', status: 'blocked', flowId: input?.flowId,
134
- code: error.code, message: error.message, ...(error.output ? { output: error.output } : {}) };
162
+ code: error.code, message: error.message, ...(error.output ? { output: error.output } : {}),
163
+ ...(error.outcomeUnknown ? { outcomeUnknown: true, nextAction: { method: 'GET', operation: 'scope-connect',
164
+ ...(error.requestId ? { requestId: error.requestId } : { uploadId: error.uploadId }), reuseOriginalInput: true, repeatPost: false } } : {}) };
135
165
  }
136
166
  }
@@ -0,0 +1,32 @@
1
+ import path from 'node:path';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { canonical, sha256 } from './local-integrity.mjs';
4
+ import { readMutationJson14, atomicMutationJson14, mutationFile14 } from './local-mutation-files.mjs';
5
+ import { evidenceFail } from './scope-evidence-wire.mjs';
6
+
7
+ export async function saveScopeRunnerJournal(location, state) {
8
+ if (Buffer.byteLength(canonical(state)) + 1 > 8_000_000) evidenceFail('Scope connection journal exceeds its unchanged bounded control limit');
9
+ await atomicMutationJson14(location.file, state, { privateFrom: location.stateRoot, replace: true });
10
+ }
11
+ export async function scopeRunnerJournal(location, identity) {
12
+ let state;
13
+ try {
14
+ const item = await mutationFile14(location.file);
15
+ if (item.stat.size > 8_000_000 || item.stat.nlink !== 1) evidenceFail('Scope connection journal is invalid');
16
+ state = await readMutationJson14(location.file);
17
+ if (state.schemaVersion !== 'entrorove.scope-connection-journal/1.0' || canonical(state.identity) !== canonical(identity)) {
18
+ evidenceFail('Scope connection journal replaced its original task, repository or complete input');
19
+ }
20
+ } catch (error) { if (error.code !== 'ENOENT') throw error; }
21
+ if (state && !state.pending && !state.evidencePending && (state.completed || state.rejection)) {
22
+ const archive = path.join(location.directory, `${state.attemptId}-${sha256(canonical(state))}.json`);
23
+ try { if (canonical(await readMutationJson14(archive)) !== canonical(state)) evidenceFail('Scope connection history changed'); }
24
+ catch (error) { if (error.code !== 'ENOENT') throw error; await atomicMutationJson14(archive, state, { privateFrom: location.stateRoot }); }
25
+ state = undefined;
26
+ }
27
+ if (!state) {
28
+ state = { schemaVersion: 'entrorove.scope-connection-journal/1.0', identity, attemptId: randomUUID() };
29
+ await saveScopeRunnerJournal(location, state);
30
+ }
31
+ return state;
32
+ }