@entrorove/skill 0.1.55 → 0.1.57

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.
@@ -6,9 +6,9 @@ import path from 'node:path';
6
6
  export async function runnerDisciplineBefore(input, authority) {
7
7
  if (!authority.plan.policy.discipline) return null;
8
8
  const original = await disciplineRepositoryObservation(input, authority.root);
9
- return { input, original: original.comparison };
9
+ return { input, original: original.comparison, readOnly: authority.plan.policy.discipline.access === 'read-only' };
10
10
  }
11
- export async function runnerDisciplineAfter(before, authority) {
11
+ export async function runnerDisciplineAfter(before, authority, processResult) {
12
12
  if (!before) return null;
13
13
  const after = (await disciplineRepositoryObservation(before.input, authority.root)).comparison;
14
14
  const changed = disciplineRepositoryDifferences(before.original, after);
@@ -23,5 +23,10 @@ export async function runnerDisciplineAfter(before, authority) {
23
23
  catch (error) {
24
24
  if (error.code !== 'ENTROROVE_STATE_EXISTS' || canonical(await readMutationJson14(file)) !== canonical(record)) throw error;
25
25
  }
26
+ if (before.readOnly && changed.length) {
27
+ throw Object.assign(new Error('Explicit read-only execution changed repository bytes or permissions; no successful receipt is issued'),
28
+ { code: 'ENTROROVE_READ_ONLY_EXECUTION_DRIFT', execution: { schemaVersion: 'entrorove.validator-local-runner/1.0',
29
+ status: 'failed', process: processResult, evidence: [], effects: 'unknown', comparison: report } });
30
+ }
26
31
  return { ...report, reportDigest };
27
32
  }
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.55",
4
+ "version": "0.1.57",
5
5
  "endpoint": "https://entrorove.com",
6
6
  "requestSchemaVersion": "entrorove.skill.request/2.0",
7
7
  "supportedRequestSchemaVersions": [
@@ -111,7 +111,8 @@ function passBinding(pass, challenge, attestation, options) {
111
111
  mutationPass14(pass, options);
112
112
  if (pass.challengeId !== challenge.challengeId || pass.challengeDigest !== sha256(canonical(challenge))
113
113
  || pass.attestationDigest !== sha256(canonical(attestation)) || pass.snapshotDigest !== attestation.manifest.snapshotDigest
114
- || [...fields, 'previousPassId'].some(field => canonical(pass[field]) !== canonical(challenge[field]))
114
+ || [...fields, 'previousPassId', 'verificationMode', 'runnerKeyId', 'memberScopeKeyId', 'memberScopeBindingDigest', 'scopeBindingDigest']
115
+ .some(field => canonical(pass[field]) !== canonical(challenge[field]))
115
116
  || canonical(pass.writeDelegation) !== canonical(challenge.writeDelegation)) fail('Mutation pass differs from its original signed challenge or actual M snapshot');
116
117
  }
117
118
  async function snapshot(input, challenge, state, location) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@entrorove/skill",
3
- "version": "0.1.55",
3
+ "version": "0.1.57",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "entrorove": "cli.mjs"
@@ -71,6 +71,7 @@
71
71
  "workflow-local-client.mjs",
72
72
  "workflow-local-request.mjs",
73
73
  "workflow-local-contracts.mjs",
74
+ "workflow-local-acceptance.mjs",
74
75
  "local-budget-renewal.mjs",
75
76
  "local-probe.mjs",
76
77
  "local-flow-lifecycle.mjs",
@@ -219,6 +220,7 @@
219
220
  "ide-session.mjs",
220
221
  "break-glass.mjs",
221
222
  "ide-results.mjs",
223
+ "ide-local-acceptance.mjs",
222
224
  "ide-queue.mjs",
223
225
  "local-discipline-actions.mjs",
224
226
  "local-discipline-analysis.mjs",
@@ -275,5 +277,10 @@
275
277
  "tree-sitter-wasms": "0.1.13",
276
278
  "@vue/compiler-sfc": "3.5.43",
277
279
  "svelte": "5.57.1"
280
+ },
281
+ "repository": {
282
+ "type": "git",
283
+ "url": "git+https://github.com/88208555/entrorove.git",
284
+ "directory": "packages/entrorove-skill"
278
285
  }
279
286
  }
@@ -1,56 +1,58 @@
1
1
  {
2
2
  "schemaVersion": "entrorove.release-state/1.0",
3
- "version": "0.1.55",
4
- "digest": "9caa3f7a2e6f43718003b5a0608b77e8b3a635ed5a75f4303da57f4381693f61",
3
+ "version": "0.1.57",
4
+ "digest": "9234829890ae7b6f69f54a0467751ab2434e4c77dacb5c095ba3a37fe26423e7",
5
5
  "files": {
6
- "connected-cli.mjs": "260c0fc99586c24bd4d0351a14e0744849a612ebe6f1bd7951e4fce200daf920",
6
+ "connected-cli.mjs": "d5fc2542ae0aa36bf45b0f3d493fe1e7479a4a25c50c8f3730f8b0c4007a7f8f",
7
7
  "local-approved-process.mjs": "442856df788e63f38757439ba0657d1c689aaf797631a955fae3dd639e844e6d",
8
8
  "local-approved-supervisor.mjs": "87142a4c13d27c8265cc1fd7fc9088988da9bbb02134164e1a7c5417da926ac7",
9
9
  "local-mutation-consumption.mjs": "cd8650b9be894639ceb35af88d23c8002b4acff27afad2e64174eb21a9424cfb",
10
- "workflow-observation-client.mjs": "5e67bc32a8625de9523498f694655c378ae34af59c4aa2f0e07efcd72347dbc0",
11
- "workflow-observation-connection.mjs": "9235e9247b11e1d51628c1784eb1cb1e8e4b79d25ff791883722592dcf8583c0",
12
- "workflow-observation-remote.mjs": "1e5a8b9c2b9b095242ad28a1d5ebce5944efdae74383fb20d6676b93b240c5a7",
10
+ "workflow-local-acceptance.mjs": "743ae19c1edff473af2596c7356ddff3892ff9acfb3bcaa660b662eacba463a8",
11
+ "workflow-observation-client.mjs": "4ad34e5a467954ad1943a0f003e07dd6c4fb919d197ae9175a8b0c9b6e2aff32",
12
+ "workflow-observation-connection.mjs": "00e887c1430b13fc49c3f51ae814edb7a05c6d61ed7091262ae91a0706fad084",
13
+ "workflow-observation-remote.mjs": "26f7d741d2e00e4cb05240d8999378b6db3cb1c52eaa586ac20f9448fe296e4b",
13
14
  "workflow-observation-runner.mjs": "3eb8280ec7abffcb30398e3f4e5f7d0ec3df4385fa720f188ed83757b87cd977",
14
15
  "workflow-observation-state.mjs": "a525046df9b3de75b10d2bf35c2b6d7719f12a647e4aee1f1e85dbdbfb167321",
15
- "workflow-observation-wire.mjs": "846d0b48b7799bfca227e30faea025a5a919896538f963ce886cbdb4c75c0253",
16
+ "workflow-observation-wire.mjs": "90047ca4fe028e4931406ff26d4fe871bcafb28a59c456795aae4f5c4b739e43",
16
17
  "local-preserved-mutation-protection.mjs": "95041d4f75253a153c7fd85573ba0457513ffe47a6a6bd00c4f59379b81ce580",
17
18
  "local-preserved-mutation-signer.mjs": "2b0771cbdd18ba00267aa441b7892cd04feab9f8543ae5e2703430b1e580747f",
18
- "local-preserved-mutation-state.mjs": "1ff7939d668e39ec641a7cf46a4ce3014c63f3898207cc874f21170376479671",
19
- "local-preserved-mutation-wire.mjs": "d302bd4b115338c0d1bf69718ce2596dbceb595431ff3f57b998ea67d94cb34c",
19
+ "local-preserved-mutation-state.mjs": "337a172b2d0a03b0d5a89b8483afdd9a043a4999fe38f87679633c98c450126b",
20
+ "local-preserved-mutation-wire.mjs": "2f2f623e768d49af59dbb9bb8245627cb7e72184e5a5d3239d91f395152cf9bf",
20
21
  "local-delegated-modify-analysis.mjs": "f87148ddb023b79e241ca97db582991da8d0e1a51b26b1d9bc2799a76efd7297",
21
- "mutation-client.mjs": "8d27b267dc660510aa1406c44d4005e44b972a1260f90657de4fa0e75b7b6c3e",
22
+ "mutation-client.mjs": "4d5fadb0277ce8cc1eaf96f613ef496201910e64d21355627ec68d88c01b5eb9",
22
23
  "cloud-mutation-authority.mjs": "c7226e882a4f3be4d2f55728f515d5a815c0aa1f21aa5e4689314e6e6a8c66a2",
23
24
  "ide-code-mode.mjs": "f8ba4de33632ab4c6df52dab52f392414b4632c7e94b218b8065677970c075c7",
24
- "ide-command.mjs": "134fa07e152f3966d4763f10eea33e69559a2271fefb453a1384396fd5f01333",
25
- "ide-hook.mjs": "68ca1f5805a49b78182f2508bdd5fac390080e9b2c2ba3645a9aaea69f9d4d0f",
26
- "ide-install.mjs": "10247efadcfeef5da001022d12080bd0f789e67fdf7862bcc65db73dda3bbe57",
25
+ "ide-command.mjs": "3db45dd1d12db670e594b6ced35f2d7778cb01091695b1e240179b1ea34f25ee",
26
+ "ide-hook.mjs": "ae097c61db71a95096f3c3b30b4c59dcab41aedbf88445390b8ad98f81e4cec0",
27
+ "ide-install.mjs": "6322f76d8c0f5c864d8c61f4ee90ae029ccf4bbf1e31fbca7590b78b6c7eaa78",
27
28
  "ide-session.mjs": "6df04bca3231954e8b83f6b1b535befff16b6c62c51ca65ae3aa2559180ad7c2",
28
29
  "break-glass.mjs": "6ec9f97c5d68bb33121b2374c06b9d4c8eb3419b52d00849fec7124bc63e4582",
29
- "ide-results.mjs": "7ed5caf8e77cf42ae6d62cc1ed42b1639bca6069d809b90a399bec5192fb903e",
30
+ "ide-results.mjs": "baa0618845ced901334644479bf4e0d298bad3f955bc18846752de6c283a5573",
31
+ "ide-local-acceptance.mjs": "238eec6d7b6093fb8ad022da0ed3383a48ba2ffcf693e3ce8a241cf9624088d8",
30
32
  "ide-queue.mjs": "806e716c775fd0079417027b135977c8516f47dddb0bc5ca19e32a355dab0e9e",
31
33
  "local-discipline-actions.mjs": "418c220b8c8ef59cb8092b6eb680bb75a93b9fad1a09412b1efa98a78afff11b",
32
34
  "local-discipline-analysis.mjs": "e887abdd1c6d6ba59ef7323e9035528f0491e5458ca8159985ec26e1ca67a4af",
33
35
  "local-discipline-batch.mjs": "2cc90dbae55efdee0c445da399c0a9acafa92e915344c25208ebca554fc4c9e9",
34
36
  "local-typescript-function-reference.mjs": "af682b479fe0d69a5ecd62232406bb8078408ec8710733cfed9ae6b059762ba5",
35
37
  "local-scope-import-expressions.mjs": "20f1e065582c68d2334e0dfaa130a559ff19886a57f9038dc569f65993bb06c8",
36
- "local-discipline-contracts.mjs": "6cf8a3d132fdb9074eb1441600afe7aa199df33e88a71a660d9db534c14f19e4",
38
+ "local-discipline-contracts.mjs": "ab0c11dcc4a6ffd3cb79b53862c19308eaf0d159deee7f9958266852d977363e",
37
39
  "local-discipline-contracts.d.mts": "defc684c78dc88afa1c54340ab4a01ae36db7aa11257cbe1f5cd323f608c78c4",
38
40
  "local-discipline-gate.mjs": "e3851157bf1d8bc86eb44212a88acd20acfc2d1152e7d87af6dc2a37afffcd6f",
39
41
  "local-discipline-native.mjs": "8b99b5ce951cab3c1426428ad807650c61c0ffb2c346d747b73b99057b382049",
40
42
  "local-discipline-operations.mjs": "9fcb7b8889b7718d42e1d37936485d8198d5017d405e0cdbbabdbbd12fcc9d48",
41
43
  "local-discipline-repository.mjs": "b6392e02ecae333dfdc8df41f203fd69bed035d88b15b16d112ab1897b3c297c",
42
44
  "local-discipline-snapshot.mjs": "8fd35b2cfbece8f64579f144f589a66dec9b6022d50fe931c4c1f886cb83134d",
43
- "local-runner-discipline.mjs": "86f451de2f1e12a24f5596a29828891c1eba13cd4714ed0c18a77d66cecef934",
45
+ "local-runner-discipline.mjs": "8d4ada838f81d54963adbb7e433f1ca80ae1934ea126f90c59a49d29742979c4",
44
46
  "local-integrity.d.mts": "21357f306234bd841c0d3d90bf2d09f7663f63746f897b9f38190428ae8198b3",
45
47
  "local-equivalence-runner.mjs": "df2f406c693c4da7bb30542ec7b4d46a9336f56e9fdd263196c0f3a73d2f6e91",
46
48
  "validator-runtime-build.mjs": "da20bd19eb088d79d62e33969b4c075dc2199761b395e40060227f8f86b9519a",
47
49
  "runtime/validator-runtime-v7.0.44.mjs": "a3bf9aa24146e48ad5702dd498de9bd1ff270fc4263026abf0bf2a4ec20f5321",
48
50
  "runtime/validator-runtime-v7.0.44.d.mts": "7797edbfe2f7de24da370204dce4dfd22822a1bd80df93514fff900b98afe36f",
49
- "snapshot-retention.mjs": "4f8919fe8ec49fb4c23e080a8ad2fa3a9282bca3fffe7cf55c64e89ef316697e",
51
+ "snapshot-retention.mjs": "ca46aeb36b1a5a24fe7e967031a71b9a3d33f40cb715d344402cbe0dc9c425cb",
50
52
  "snapshot-cleanup-journal.mjs": "76a9684cc95b4eb076e1298d78a9dff37948b00d9607b8f546c599072d3ed331",
51
- "IDE-EXECUTION.md": "abe40303ab31637f5c69b0e5611a49fd579566a0bb95bd9fba1ebd39e7108fbd",
53
+ "IDE-EXECUTION.md": "cb98887cc9a905a5a69c14f8e95cdffc7801c8a2bf8c60f6b7975867e2f722e7",
52
54
  "REUSE-DISCIPLINE.md": "5b214cf313e7f715f52d54e28093a2d038f0f196ca64b15d1c49587b9d8eecf8",
53
- "reuse-discipline-release.mjs": "2935898d1aa807fb982a78d7ab5cb1abe05e3c8fc945dc0909da8098872aa1ac",
55
+ "reuse-discipline-release.mjs": "7f55ae10ec3396ed3bc5430a0c64f94c33c2dfbc57b32084caab92fd5fd1586f",
54
56
  "local-final-gate16.mjs": "083aec470670e6cdb7ad2ab27b7a51b53a9ec3a8b4121cb9cbfa17d9940892c2",
55
57
  "local-final-gate16-checks.mjs": "1dd30e9666d458d8b12ca6e4975f6f0ac8450c3ea9cbecc7401ae037f66b589a",
56
58
  "local-final-gate16-worker.mjs": "344d64b6c1c5552b96960e0d8514a876a3dbfa9762ab9eb749038e8351727121",
@@ -96,13 +98,13 @@
96
98
  "install-cleanup.mjs": "8a8e1a7dfe37427f86cd4d6e317890d0829894a9949d4216674a716b36edbc54",
97
99
  "review-client.mjs": "3491eef68ed533174b31db3c53febb6519b8f088f71a9964644714a9a4c03c77",
98
100
  "version.mjs": "1f392147d72c40af4b03b307ea607075fb815af5174fa07e254b95fe12dbf3e7",
99
- "SKILL.md": "684438bcdfefef590e51e4ce02be0a244a26d142a24599258bf3ae24c86bbfa1",
100
- "CAPABILITY-MAP.md": "69b2c49b111d651969dea7430f4df7950d8fdf2f0f151d8386fa04968b872ac0",
101
+ "SKILL.md": "96f83041a2f9933876e6a94f487e472e95d029251375449336ae4fc535801ef9",
102
+ "CAPABILITY-MAP.md": "0e85f26ebf2a5be5e6e5d2af66f910db03c69cb268edad1b2541dd3834991f90",
101
103
  "AGREEMENT.md": "f93a68e92c457200dd024610e65f74e478b8f60860f951da818b70561f1b9bf8",
102
104
  "DELIVERY-BOUNDARIES.md": "9b9db50ed1bcce9e77ff6527b6bb7eaf12f3b913162ea9cb7fe90db06b393a4f",
103
- "package.json": "e910dc5cf0db61b1fcb9feffa21a8f8d3c2a244b7d858e8af72202f5b91d403b",
105
+ "package.json": "3fbb42236d00d8c5a92c1cc4c75de4cea0581b8ee22721fe57aa760513744787",
104
106
  "manifest.json": "ab06e307795bc9da5add926752b6c4dbdaf18625090261a035aa356a679c504f",
105
- "release.mjs": "8b0dc36c47aec72cf9952c9ac1ce5fe27da5ae6829f50cd2339fa101b2eda93c",
107
+ "release.mjs": "a0693132999f5be6030761c3992071d7416a75471f6b73bdd720d52f55ea3c3f",
106
108
  "local-cli.mjs": "41e2a737c8587cb5ce469a7661f6d14b5ea4cd073f8286822fc4dc96b67f56a1",
107
109
  "local-state-lock.mjs": "1dbf64264642a0851faf043e07ee9f66d2ad0508e9f799b0626246e42fae893e",
108
110
  "local-execution.mjs": "185f34e917440a360b3c4bafd3af245243807d964ae3820b3cffaaa2e6348cc6",
@@ -123,7 +125,7 @@
123
125
  "local-chain-plan.mjs": "e45a9170f569c80f7e5532bbc109dbae3c24b696815316e503cb3779135be648",
124
126
  "local-chain-release-labels.mjs": "7ce5350d1062d5236f57f10e7e81713c1101463d5e7f252604ef0c2a30840bfe",
125
127
  "local-chain-results.mjs": "0455f72cad574cfef87c2c6c0928d6432bba7e5dbaff521a2b62a9575299ca10",
126
- "local-approved-plan.mjs": "26731b534f4c276c6235cb73bf6950943061947967a93f477f4bcd64acde72a6",
128
+ "local-approved-plan.mjs": "c2c88ab4085ff35a1811fe43e9a28699cfc9387eda54bfeafae3e7dd9e8cdb19",
127
129
  "local-budget-renewal.mjs": "6b436358066fe33b2f6d871fc90d8e7c551edd6255efd73c0d255d0fd35d5ef8",
128
130
  "local-probe.mjs": "9b948db1de1e34c7605c4c454cd4aced66d2b57d2cf4471adbb80bcc76dee635",
129
131
  "local-budget-grants.mjs": "c5f6542f5d2c3bfe07f3ca5b5bc849bf05d886b06014f720322d530510775d3d",
@@ -183,7 +185,7 @@
183
185
  "cloud-watch-client.mjs": "5b93935c19a104d72afed07d4a56746c9617707e075884b93d22050c95cee0c1",
184
186
  "cloud-event-stream.mjs": "e0eec9fc3e0613094523929634f25030df5ca5c5b5fe2c1e1a4586ab10574808",
185
187
  "ide-presentation.mjs": "3bac21bfc824fcdad63aafb0d7c3326cd6456923f4224106dd5c38682060f315",
186
- "automatic-feedback-client.mjs": "75e92f50d6fbfa4f482af36e16c9ce5dd323a02e99eb9b434dcfa3faa846a532",
188
+ "automatic-feedback-client.mjs": "532312d29e66fa211ea3fe879d9add1dd48841ee5ce82800b4b06252a160a255",
187
189
  "task-entry-client.mjs": "869f234402c4149eadf1ec138705fc140e2fa16b2a3ea5dc2b2f51dfb189d7de",
188
190
  "task-identity.mjs": "e0e6cf1f48bfa5ee3c5cb86a3c21adfce6d66b4220f4a164ca099263c7a09fb4",
189
191
  "task-identity.d.mts": "55472e1ffc81b9d5e5c5bd56c0bf1d18ecde03ee5170a58a15ce9caf222d8761",
@@ -201,8 +203,8 @@
201
203
  "local-task-handoff-read-authority.mjs": "7d2991cfac4562039232566d25441aa98106ce27bc9b7bcf6df4ff28d738e167",
202
204
  "local-task-handoff-write-archive.mjs": "3668f39d8ab5664ed717994cf1b4bcc7ea23e146a0e06291f851d30337faeb77",
203
205
  "local-task-handoff-write-receipts.mjs": "e1321127bd6fcad7840e185639f81ae68a8722a0ee4264a7b504a02b27887647",
204
- "workflow-local-runner.mjs": "9fa899a11da2b6b7a436bf9b561641856adcbc5f8b914f6ba89a64dae224900d",
205
- "workflow-local-contracts.mjs": "bc52a461727b34c72daa058e1aa77d58206eafad14a93a347537f9969ae51184",
206
+ "workflow-local-runner.mjs": "baeb0178aa9260f2e02c5c8bc3f1a4934827a5552d0db18fe8ad6acfee579459",
207
+ "workflow-local-contracts.mjs": "ed7807c6230c3f9a2bd7c7a0ca255aec5e8e922fe71a75b95904d87709a615b6",
206
208
  "workflow-local-client.mjs": "45c954f694d96bf0ea87ee2dc6902a6763dac2cb1df713b6a5e749632054e9af",
207
209
  "workflow-local-request.mjs": "4e24c543ce386292a87dc9cd41f90d23375f12079cacffc515c8cd01fd571780",
208
210
  "task-message-entry.mjs": "31d10056cb905237cd5649e8eb067009ff1612ec5dc1061e0373209fbc0b646a",
@@ -237,7 +239,7 @@
237
239
  "local-mutation-files.mjs": "087b00e8424a135e783eb432271012a71e55eeb4a5e30119490d1bc0b2160320",
238
240
  "local-mutation-snapshot.mjs": "f9077539680fb8d3ff33a962657756a8fe6afa8409b6191334509534216ebfe8",
239
241
  "local-mutation-audit.mjs": "5a30ee3446f632e032ffcfe46bcd97d6dfc0f099a76ea5185c3f9a9118c4f6cb",
240
- "local-mutation-wire.mjs": "7af0c585e8dd6936c0ff17f491c64a198744629f063279c31589d887ec83ae9c",
242
+ "local-mutation-wire.mjs": "52f3e4bd90e246abd47163199fed508d9a243770580bb8617bbf58f1da28e01f",
241
243
  "local-mutation-binding.mjs": "4440d792453a07a375b635fa5a91db607e58162824baced5c79a9a513f4ec918",
242
244
  "local-mutation-pass-store.mjs": "1edc5fcc537dfa04b8ae8b07af554d61ea5911a5da281423ad57214ccf753ad4",
243
245
  "local-mutation-authority.mjs": "ebac0cc8126969bafb0aaaeacf71e05124997d5c3214f8ec32b7a4fa14569bcf",
package/release.mjs CHANGED
@@ -38,6 +38,7 @@ const memberContinuationSourceFiles = Object.freeze([
38
38
  'local-approved-process.mjs',
39
39
  'local-approved-supervisor.mjs',
40
40
  'local-mutation-consumption.mjs',
41
+ 'workflow-local-acceptance.mjs',
41
42
  'workflow-observation-client.mjs',
42
43
  'workflow-observation-connection.mjs',
43
44
  'workflow-observation-remote.mjs',
package/releases.json CHANGED
@@ -1,4 +1,16 @@
1
1
  [
2
+ {
3
+ "version": "0.1.57",
4
+ "date": "2026-10-17",
5
+ "zh": "云端关闭时以原本地 IDE 执行与实际测试完成本地验收,补齐签名步骤续跑、最终验收、钩子与队列恢复;保留原范围、审批和 S/M 证据,明确未做独立验证。修复原规划字段诊断与沙箱模式恢复。",
6
+ "en": "Completed explicit local IDE acceptance with real execution and tests when cloud sandbox is disabled, including signed step progress, final acceptance, hooks and queue recovery while preserving original scope, approvals and S/M evidence without claiming independent verification. Added precise planning diagnostics and original sandbox-mode recovery."
7
+ },
8
+ {
9
+ "version": "0.1.56",
10
+ "date": "2026-10-17",
11
+ "zh": "补齐原工作流显式只读步骤及无写入结算的本地观察,复核真实受检仓库字节与权限;修复流程列表空输入兼容,返回精确规划前置条件与范围错误,并配置 GitHub OIDC 发布。",
12
+ "en": "Added explicit read-only workflow steps and local observations without fabricated mutation settlements, checking actual repository bytes and modes; fixed empty-input flow listing, precise planning prerequisite and scope errors, and GitHub OIDC publishing metadata."
13
+ },
2
14
  {
3
15
  "version": "0.1.55",
4
16
  "date": "2026-10-17",
@@ -1,4 +1,4 @@
1
- const ide = ['ide-code-mode.mjs', 'ide-command.mjs', 'ide-hook.mjs', 'ide-install.mjs', 'ide-session.mjs', 'break-glass.mjs', 'ide-results.mjs', 'ide-queue.mjs'];
1
+ const ide = ['ide-code-mode.mjs', 'ide-command.mjs', 'ide-hook.mjs', 'ide-install.mjs', 'ide-session.mjs', 'break-glass.mjs', 'ide-results.mjs', 'ide-local-acceptance.mjs', 'ide-queue.mjs'];
2
2
  const discipline = ['local-discipline-actions.mjs', 'local-discipline-analysis.mjs', 'local-discipline-batch.mjs',
3
3
  'local-typescript-function-reference.mjs', 'local-scope-import-expressions.mjs',
4
4
  'local-discipline-contracts.mjs', 'local-discipline-contracts.d.mts', 'local-discipline-gate.mjs',
@@ -5,6 +5,7 @@ import { canonical, sha256, verifySnapshotManifest } from './local-integrity.mjs
5
5
  import { withStateLock } from './local-state-lock.mjs';
6
6
  import { mutationStateRoot14, atomicMutationJson14, privateMutationDirectory14 } from './local-mutation-files.mjs';
7
7
  import { resumeSnapshotRemoval, removeSnapshotWithJournal } from './snapshot-cleanup-journal.mjs';
8
+ import { verifiedLocalWorkflowCompletion } from './ide-local-acceptance.mjs';
8
9
 
9
10
  const id = value => typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u.test(value);
10
11
  async function realDirectory(directory) {
@@ -24,9 +25,14 @@ async function safeTree(directory) {
24
25
  }
25
26
  return bytes;
26
27
  }
27
- function completedProof(call, result) {
28
+ async function completedProof(call, result, home) {
28
29
  if (call.operation !== 'continue' || call.input?.operation !== 'workflow-verdict' || !id(call.input.flowId)) return null;
29
30
  const output = result?.output, receipt = result?.feedback;
31
+ if (await verifiedLocalWorkflowCompletion(call, result, home)) return { flowId: output.flowId, requestId: output.requestId,
32
+ receiptId: receipt.receiptId, evaluationDigest: receipt.evaluation.digest,
33
+ verificationMode: 'member-local', independentValidation: 'not-performed' };
34
+ if (output?.localAcceptance || output?.result?.localAcceptance || output?.status === 'local-accepted'
35
+ || output?.result?.status === 'local-accepted') return null;
30
36
  const reports = output?.result?.reports ?? output?.reports;
31
37
  if (output?.status !== 'succeeded' || output.flowId !== call.input.flowId || !Array.isArray(reports) || !reports.length
32
38
  || reports.some(report => !['pass','pass-with-risk'].includes(report.verdict)) || receipt?.requestId !== output.requestId
@@ -74,7 +80,7 @@ async function cleanupRepository(root, repository, proof) {
74
80
  return { removed, retained: [...retained].sort(), releasedBytes };
75
81
  }
76
82
  export async function cleanupCompletedSnapshots(call, result, { home = os.homedir() } = {}) {
77
- const proof = completedProof(call, result);
83
+ const proof = await completedProof(call, result, home);
78
84
  if (!proof) return { status: 'preserved', reason: 'verified-original-completion-required' };
79
85
  const root = await mutationStateRoot14({ home }), snapshotRoot = path.join(root, 'snapshots');
80
86
  if (!await realDirectory(snapshotRoot)) return { status: 'completed', removed: 0, releasedBytes: 0 };
@@ -0,0 +1,35 @@
1
+ import { canonical, sha256 } from './local-integrity.mjs';
2
+ import { observationExact, observationFail, observationAuthoritySignature } from './workflow-observation-wire.mjs';
3
+
4
+ const identity = ['flowId', 'ownerId', 'taskId', 'taskDigest', 'chainDigest', 'planId', 'planDigest',
5
+ 'stepId', 'executionPlanSha256', 'leaseId', 'generation', 'observationId'];
6
+ const digestFields = ['taskDigest', 'chainDigest', 'planDigest', 'executionPlanSha256', 'leaseDigest', 'admissionDigest', 'resultDigest'];
7
+ const checks = ['sourceManifestUnchanged', 'protectedFilesUnchanged', 'repositoryUnchanged'];
8
+ export function verifyWorkflowLocalAcceptance(value, admission, result, reportRequestId) {
9
+ observationExact(value, ['schemaVersion', 'status', 'verificationMode', 'independentValidation', 'runnerKeyId',
10
+ ...identity, 'leaseDigest', 'admissionDigest', 'resultDigest', 'requestId', 'acceptedAt', 'authorityReceipt']);
11
+ observationAuthoritySignature(value);
12
+ if (value.schemaVersion !== 'entrorove.member-local-step-acceptance/1.0' || value.status !== 'local-accepted'
13
+ || value.verificationMode !== 'member-local' || value.runnerKeyId !== null || value.independentValidation !== 'not-performed'
14
+ || identity.some(field => value[field] !== admission[field])
15
+ || digestFields.some(field => typeof value[field] !== 'string' || !/^[a-f0-9]{64}$/.test(value[field]))
16
+ || value.admissionDigest !== sha256(canonical(admission)) || value.resultDigest !== sha256(canonical(result))
17
+ || !/^[a-f0-9]{64}$/.test(reportRequestId ?? '') || value.requestId !== reportRequestId
18
+ || typeof value.acceptedAt !== 'string' || !Number.isFinite(Date.parse(value.acceptedAt))
19
+ || new Date(value.acceptedAt).toISOString() !== value.acceptedAt) {
20
+ observationFail('Local acceptance lost its original signed admission, process result, lease or verification role');
21
+ }
22
+ observationExact(result.checks, checks);
23
+ const process = result.process;
24
+ if (result.schemaVersion !== 'entrorove.local-observation-result/1.0' || result.status !== 'incomplete'
25
+ || result.runner !== 'local' || result.receipt !== null || result.independentValidation !== 'pending'
26
+ || result.admissionDigest !== value.admissionDigest || result.observationId !== value.observationId
27
+ || result.sourceManifestDigest !== sha256(canonical(admission.sourceManifest))
28
+ || result.executionIsolation !== 'host-permissions' || result.externalEffects !== 'unverified'
29
+ || process?.started !== true || process.status !== 'succeeded' || process.exitCode !== 0 || process.signal !== null
30
+ || process.timedOut !== false || process.outputLimitExceeded !== false || checks.some(field => result.checks[field] !== true)
31
+ || !/^[a-f0-9]{64}$/.test(result.beforeRepositoryDigest ?? '') || result.beforeRepositoryDigest !== result.afterRepositoryDigest) {
32
+ observationFail('Local acceptance cannot replace a failed, changed or independently unverified process outcome');
33
+ }
34
+ return value;
35
+ }
@@ -40,11 +40,19 @@ export const WORKFLOW_EXECUTION_PLAN_SCHEMA = exact({ schemaVersion: { const: 'v
40
40
  });
41
41
  export const WORKFLOW_ACCEPTED_PLAN_SCHEMA = exact({ schemaVersion: { const: 'entrorove.workflow-local-plan/1.0' },
42
42
  planId: id, flowId: id, ownerId: id, taskDigest: sha256, chainDigest: sha256, chainRevision: integer(1),
43
- acceptedAt: timestamp, expiresAt: timestamp, runnerKeyId: sha256,
43
+ acceptedAt: timestamp, expiresAt: timestamp, runnerKeyId: { anyOf: [sha256, { type: 'null' }] },
44
44
  steps: boundedArray(exact({ stepId: id, sequence: integer(1, 128),
45
45
  dependsOn: { ...boundedArray(id, 0, 128), uniqueItems: true }, executionPlan: WORKFLOW_EXECUTION_PLAN_SCHEMA,
46
46
  executionPlanSha256: sha256 }), 1, 128), authorityReceipt,
47
47
  });
48
+ WORKFLOW_ACCEPTED_PLAN_SCHEMA.properties.verificationMode = enumeration(['independent', 'member-local']);
49
+ WORKFLOW_ACCEPTED_PLAN_SCHEMA.properties.memberScopeKeyId = sha256;
50
+ WORKFLOW_ACCEPTED_PLAN_SCHEMA.properties.memberScopeBindingDigest = sha256;
51
+ WORKFLOW_ACCEPTED_PLAN_SCHEMA.allOf = [{
52
+ if: { properties: { verificationMode: { const: 'member-local' } }, required: ['verificationMode'] },
53
+ then: { properties: { runnerKeyId: { type: 'null' } }, required: ['memberScopeKeyId', 'memberScopeBindingDigest'] },
54
+ else: { properties: { runnerKeyId: sha256, memberScopeKeyId: false, memberScopeBindingDigest: false } },
55
+ }];
48
56
  WORKFLOW_ACCEPTED_PLAN_SCHEMA.properties.blueprintBinding = exact({
49
57
  schemaVersion: { const: 'entrorove.blueprint-plan-binding/1.0' }, blueprintId: id, revision: integer(0),
50
58
  blueprintSha256: { type: 'string', pattern: '^sha256:[0-9a-f]{64}$' }, machineTasksDigest: sha256,
@@ -54,7 +62,16 @@ export const WORKFLOW_LOCAL_LEASE_SCHEMA = exact({ schemaVersion: { const: 'entr
54
62
  chainRevision: integer(1), stepId: id, sequence: integer(1, 128), generation: integer(1), issuedAt: timestamp, expiresAt: timestamp,
55
63
  verifiedDependencies: { ...boundedArray(exact({ stepId: id, evidenceDigest: sha256 }), 0, 128), uniqueItems: true }, authorityReceipt,
56
64
  });
65
+ WORKFLOW_LOCAL_LEASE_SCHEMA.properties.localDependencies = { ...boundedArray(exact({ stepId: id, acceptanceDigest: sha256 }), 0, 128), uniqueItems: true };
66
+ WORKFLOW_LOCAL_LEASE_SCHEMA.allOf = [{ if: { required: ['localDependencies'] },
67
+ then: { properties: { verifiedDependencies: { type: 'array', maxItems: 0 } } } }];
57
68
  export const WORKFLOW_LOCAL_OPERATION_SCHEMA = object({ repositoryRoot: absolutePath, home: absolutePath,
58
69
  acceptedPlan: WORKFLOW_ACCEPTED_PLAN_SCHEMA, lease: WORKFLOW_LOCAL_LEASE_SCHEMA, stepId: id,
59
70
  approvalPath: absolutePath, signerConfigPath: absolutePath,
60
71
  }, ['repositoryRoot', 'acceptedPlan', 'lease', 'stepId', 'approvalPath', 'signerConfigPath']);
72
+ WORKFLOW_LOCAL_OPERATION_SCHEMA.allOf = [{
73
+ if: { properties: { acceptedPlan: { type: 'object', properties: { verificationMode: { const: 'member-local' } }, required: ['verificationMode'] } } },
74
+ then: { properties: { lease: { type: 'object', required: ['localDependencies'],
75
+ properties: { verifiedDependencies: { type: 'array', maxItems: 0 } } } } },
76
+ else: { properties: { lease: { type: 'object', properties: { localDependencies: false } } } },
77
+ }];
@@ -64,6 +64,7 @@ function lifetime(start, end, max, now, permitExpired = false) {
64
64
  }
65
65
  function validatePlan(plan) {
66
66
  exact(plan, ['schemaVersion', 'planId', 'flowId', 'ownerId', 'taskDigest', 'chainDigest', 'chainRevision', 'acceptedAt', 'expiresAt', 'runnerKeyId', 'steps',
67
+ ...['verificationMode', 'memberScopeKeyId', 'memberScopeBindingDigest'].filter(field => Object.hasOwn(plan, field)),
67
68
  ...(plan.blueprintBinding ? ['blueprintBinding'] : [])], 'ENTROROVE_WORKFLOW_PLAN_FIELDS', 'accepted plan');
68
69
  if (plan.blueprintBinding) {
69
70
  const binding = exact(plan.blueprintBinding, ['schemaVersion', 'blueprintId', 'revision', 'blueprintSha256', 'machineTasksDigest'],
@@ -74,8 +75,12 @@ function validatePlan(plan) {
74
75
  fail('ENTROROVE_WORKFLOW_BLUEPRINT_BINDING', 'Blueprint plan binding is invalid');
75
76
  }
76
77
  }
78
+ const local = plan.verificationMode === 'member-local';
79
+ const validRole = local ? plan.runnerKeyId === null && SHA.test(plan.memberScopeKeyId ?? '') && SHA.test(plan.memberScopeBindingDigest ?? '')
80
+ : (plan.verificationMode === undefined || plan.verificationMode === 'independent') && SHA.test(plan.runnerKeyId ?? '')
81
+ && !Object.hasOwn(plan, 'memberScopeKeyId') && !Object.hasOwn(plan, 'memberScopeBindingDigest');
77
82
  if (plan.schemaVersion !== WORKFLOW_LOCAL_PLAN_SCHEMA || !SHA.test(plan.taskDigest) || !SHA.test(plan.chainDigest)
78
- || !SHA.test(plan.runnerKeyId) || !positive(plan.chainRevision) || !Array.isArray(plan.steps) || !plan.steps.length || plan.steps.length > 128) fail('ENTROROVE_WORKFLOW_PLAN_FIELDS', 'accepted plan contract is invalid');
83
+ || !validRole || !positive(plan.chainRevision) || !Array.isArray(plan.steps) || !plan.steps.length || plan.steps.length > 128) fail('ENTROROVE_WORKFLOW_PLAN_FIELDS', 'accepted plan contract is invalid');
79
84
  for (const field of ['planId', 'flowId', 'ownerId']) safeId(plan[field], field);
80
85
  const seen = new Set();
81
86
  for (const [index, step] of plan.steps.entries()) {
@@ -280,13 +285,16 @@ async function prepare(input, create = true) {
280
285
  if (input.executionAttemptNonce !== undefined) safeId(input.executionAttemptNonce, 'executionAttemptNonce');
281
286
  if (process.platform === 'win32' || typeof process.getuid !== 'function') fail('ENTROROVE_POSIX_REQUIRED', 'workflow local runner requires POSIX');
282
287
  const configured = publicKey(process.env.ENTROROVE_WORKFLOW_PLAN_PUBLIC_KEY?.trim(), 'ENTROROVE_WORKFLOW_AUTHORITY_KEY');
283
- const plan = validatePlan(verifyAuthority(input.acceptedPlan, configured)); const lease = verifyAuthority(input.lease, configured);
288
+ const plan = validatePlan(verifyAuthority(input.acceptedPlan, configured));
289
+ if (plan.verificationMode === 'member-local') fail('ENTROROVE_INDEPENDENT_VERIFICATION_REQUIRED',
290
+ 'A member-local plan cannot run through the independent Validator signer entry; use its original workflow-observe-step action');
291
+ const lease = verifyAuthority(input.lease, configured);
284
292
  const step = validateLease(lease, plan, input.stepId);
285
293
  lifetime(plan.acceptedAt, plan.expiresAt, MAX_PLAN_MS, Date.now(), true); lifetime(lease.issuedAt, lease.expiresAt, MAX_LEASE_MS, Date.now(), true);
286
294
  const root = await realDirectory(input.repositoryRoot, 'ENTROROVE_ROOT_INVALID');
287
295
  const home = await realDirectory(input.home ?? os.homedir(), 'ENTROROVE_HOME_INVALID');
288
296
  const directory = await privateHierarchy(home, ['.entrorove', 'workflow-local', sha(root).slice(0, 32), plan.flowId, step.stepId], create);
289
- return { root, plan, lease, step, file: path.join(directory, 'state.json') };
297
+ return { root, home, plan, lease, step, file: path.join(directory, 'state.json') };
290
298
  }
291
299
 
292
300
  export async function observeWorkflowLocalStep(rawInput) {
@@ -323,7 +331,7 @@ export async function runWorkflowLocalStep(rawInput) {
323
331
  });
324
332
  const signer = await loadSigner(authority.root, input.signerConfigPath, authority.plan.runnerKeyId);
325
333
  if (typeof approvedRunner.runApprovedPlanObject !== 'function') fail('ENTROROVE_WORKFLOW_RUNNER_UNAVAILABLE', 'approved object plan runner is unavailable');
326
- execution = await approvedRunner.runApprovedPlanObject({ repositoryRoot: authority.root, executionPlan: authority.step.executionPlan,
334
+ execution = await approvedRunner.runApprovedPlanObject({ repositoryRoot: authority.root, home: authority.home, flowId: authority.plan.flowId, executionPlan: authority.step.executionPlan,
327
335
  approvalPath: input.approvalPath, signerConfigPath: input.signerConfigPath, executionDeadlineAt: authority.lease.expiresAt });
328
336
  return await finalize(authority, signedResult(authority, execution, signer));
329
337
  } catch (error) {
@@ -9,6 +9,7 @@ const messages = {
9
9
  ENTROROVE_LOCAL_OBSERVATION_PREREQUISITE: 'Local observation requires cloud sandbox disabled and its original live execution lease',
10
10
  ENTROROVE_SCOPE_RECEIPT_EXPIRED: 'Refresh the original scope evidence and reuse the original observation input',
11
11
  ENTROROVE_SCOPE_AFTER_WRITE_REQUIRED: 'Observe the settled modifications in fresh scope evidence before local testing',
12
+ ENTROROVE_MEMBER_LOCAL_MODE_PREREQUISITE: 'Disable cloud sandbox and reuse only the original cached process report; do not restart its process or source writes',
12
13
  };
13
14
  function blocked(error, input) {
14
15
  const cause = error && typeof error === 'object' ? error : {};
@@ -16,15 +17,19 @@ function blocked(error, input) {
16
17
  ? cause.code : 'ENTROROVE_LOCAL_OBSERVATION';
17
18
  const output = cause.output, requestId = safeRequest(cause.requestId) ? cause.requestId : undefined;
18
19
  const recoverable = output?.status === 'blocked' && output.recoverable === true
19
- && (output.observationPrerequisite?.effect === 'none' || ['ENTROROVE_SCOPE_RECEIPT_EXPIRED',
20
+ && (output.observationPrerequisite?.effect === 'none' || code === 'ENTROROVE_MEMBER_LOCAL_MODE_PREREQUISITE'
21
+ && output.effects === 'none' && output.localModePrerequisite?.originalOperation === 'member-observation-report'
22
+ && output.localModePrerequisite.serverEffect === 'none' && output.localModePrerequisite.localProcessMayRestart === false
23
+ && output.localModePrerequisite.cachedLocalProcessResultRequired === true || ['ENTROROVE_SCOPE_RECEIPT_EXPIRED',
20
24
  'ENTROROVE_SCOPE_RUNNER_REQUIRED', 'ENTROROVE_SCOPE_SNAPSHOT_REQUIRED', 'ENTROROVE_SCOPE_AFTER_WRITE_REQUIRED'].includes(code));
21
- const reason = ['sandbox-enabled', 'lease-expired', 'receipt-expired', 'after-write-observation-required'].includes(output?.reason)
25
+ const reason = ['sandbox-enabled', 'cloud-sandbox-enabled', 'lease-expired', 'receipt-expired', 'after-write-observation-required'].includes(output?.reason)
22
26
  ? output.reason : undefined;
23
27
  return { schemaVersion: 'entrorove.local-observation-connection/1.0', status: 'blocked',
24
28
  ...(safeId(input?.flowId) ? { flowId: input.flowId } : {}), independentValidation: 'pending', code,
25
29
  message: messages[code] ?? 'Local observation could not continue; its original command and result remain preserved',
26
30
  ...(recoverable ? { prerequisite: { status: 'blocked', code, recoverable: true, ...(reason ? { reason } : {}),
27
31
  nextAction: { operation: 'workflow-observe-step', reuseOriginalInput: true,
32
+ ...(code === 'ENTROROVE_MEMBER_LOCAL_MODE_PREREQUISITE' ? { localProcessMayRestart: false, cachedLocalProcessResultRequired: true } : {}),
28
33
  ...(safeRequest(output.requestId) ? { resumeRequestId: output.requestId } : {}) } } } : {}),
29
34
  ...(cause.outcomeUnknown === true && requestId ? { outcomeUnknown: true, requestId, repeatPost: false,
30
35
  nextAction: { operation: 'workflow-observe-step', reuseOriginalInput: true, method: 'GET' } } : {}),
@@ -5,9 +5,12 @@ import { observationAdmission, observationFail } from './workflow-observation-wi
5
5
  import { observationLocation, readObservationState } from './workflow-observation-state.mjs';
6
6
  import { runWorkflowObservation } from './workflow-observation-runner.mjs';
7
7
  import { observationRemote, authorizeObservation } from './workflow-observation-remote.mjs';
8
+ import { verifyWorkflowLocalAcceptance } from './workflow-local-acceptance.mjs';
9
+ import { buildInvocation } from './remote-client.mjs';
8
10
 
9
11
  async function connect(input, state, location, dependencies) {
10
- const original = Object.fromEntries(['taskId', 'flowId', 'stepId', 'leaseId', 'generation', 'settlementId'].map(key => [key, input[key]]));
12
+ const fields = ['taskId', 'flowId', 'stepId', 'leaseId', 'generation', ...(Object.hasOwn(input, 'settlementId') ? ['settlementId'] : [])];
13
+ const original = Object.fromEntries(fields.map(key => [key, input[key]]));
11
14
  const admitted = state.admitted ?? await observationRemote('admitted', 'member-observation-admit', original, state, location, dependencies);
12
15
  const admission = observationAdmission(admitted.admission, input, { allowExpired: Boolean(state.result) });
13
16
  await authorizeObservation(admission, dependencies, !state.result);
@@ -21,12 +24,25 @@ async function connect(input, state, location, dependencies) {
21
24
  const reported = state.reported ?? await observationRemote('reported', 'member-observation-report', {
22
25
  observationId: admission.observationId, admissionDigest: sha256(canonical(admission)), result,
23
26
  }, state, location, dependencies);
24
- if (reported.observationId !== admission.observationId || reported.observationStatus !== 'pending-independent'
27
+ if (reported.observationId !== admission.observationId || !['pending-independent', 'local-accepted'].includes(reported.observationStatus)
25
28
  || reported.resultDigest !== sha256(canonical(result)) || canonical(reported.result) !== canonical(result)) {
26
29
  observationFail('Stored local observation differs from its actual original result');
27
30
  }
28
- return { ...result, taskId: input.taskId, flowId: input.flowId, stepId: input.stepId, cached, stored: true,
31
+ const summary = { ...result, taskId: input.taskId, flowId: input.flowId, stepId: input.stepId, cached, stored: true,
29
32
  resultDigest: sha256(canonical(result)), grantsExecutionAuthority: false, grantsValidatorAuthority: false };
33
+ if (reported.observationStatus !== 'local-accepted') {
34
+ if (Object.hasOwn(reported, 'localAcceptance')) observationFail('A pending observation cannot claim local acceptance');
35
+ return summary;
36
+ }
37
+ const requestId = buildInvocation('member-observation-report', {
38
+ observationId: admission.observationId, admissionDigest: sha256(canonical(admission)), result,
39
+ }).requestId;
40
+ const localAcceptance = verifyWorkflowLocalAcceptance(reported.localAcceptance, admission, result, requestId);
41
+ if (reported.status !== 'local-accepted' || reported.independentValidation !== 'not-performed') {
42
+ observationFail('Local acceptance output cannot change its distinct independent verification status');
43
+ }
44
+ return { ...summary, status: 'local-accepted', localObservationStatus: 'local-accepted', independentValidation: 'not-performed',
45
+ localAcceptance, result: structuredClone(result) };
30
46
  }
31
47
  export async function connectWorkflowObservation(repositoryRoot, original, dependencies) {
32
48
  const location = await observationLocation(repositoryRoot, original, dependencies.home, trustedOrigin(dependencies.apiOrigin()));
@@ -1,4 +1,4 @@
1
- import { canonical } from './local-integrity.mjs';
1
+ import { canonical, sha256 } from './local-integrity.mjs';
2
2
  import { buildInvocation, invokeRemoteResult } from './remote-client.mjs';
3
3
  import { requestTransport, parseTransportResponse } from './remote-transport.mjs';
4
4
  import { ideClientHeaders } from './ide-presentation.mjs';
@@ -25,7 +25,7 @@ async function recover(pending, dependencies) {
25
25
  response = await requestTransport(origin, `/api/skill/status/${pending.requestId}`, { headers: {
26
26
  authorization: `Bearer ${token}`, ...ideClientHeaders() } });
27
27
  result = parseTransportResponse(response, 'original local observation status');
28
- if (!response.ok || result?.output?.requestId !== pending.requestId
28
+ if (result?.output?.requestId !== pending.requestId || !response.ok && response.status >= 500
29
29
  || ['pending', 'running'].includes(result.output.status)) throw unknown(pending.requestId);
30
30
  result.feedback ??= await queryAutomaticFeedback(pending.requestId, dependencies);
31
31
  verifyAutomaticFeedback(result.feedback, pending.requestId, pending.requestId);
@@ -38,6 +38,19 @@ function prerequisite(output) {
38
38
  || ['ENTROROVE_SCOPE_RECEIPT_EXPIRED', 'ENTROROVE_SCOPE_RUNNER_REQUIRED', 'ENTROROVE_SCOPE_SNAPSHOT_REQUIRED',
39
39
  'ENTROROVE_SCOPE_AFTER_WRITE_REQUIRED'].includes(output.code));
40
40
  }
41
+ function cachedReportPrerequisite(result, pending, state, location) {
42
+ const output = result?.output, proof = output?.localModePrerequisite, next = output?.nextAction;
43
+ return pending.operation === 'member-observation-report' && state.result
44
+ && canonical(pending.input.result) === canonical(state.result) && output?.requestId === pending.requestId
45
+ && output.flowId === location.identity.flowId && output.status === 'blocked' && output.recoverable === true
46
+ && output.code === 'ENTROROVE_MEMBER_LOCAL_MODE_PREREQUISITE' && output.effects === 'none'
47
+ && output.reason === 'cloud-sandbox-enabled' && proof?.serverEffect === 'none'
48
+ && proof.originalOperation === pending.operation && proof.newSourceWritesByThisRequest === false
49
+ && proof.localProcessMayRestart === false && proof.cachedLocalProcessResultRequired === true
50
+ && next?.kind === 'disable-cloud-sandbox-then-resume-original-request' && next.originalOperation === pending.operation
51
+ && next.reuseOriginalInput === true && next.resumeRequestId === pending.requestId && next.preserveOriginalFlow === true
52
+ && result.feedback?.acknowledgement?.stored === true && result.feedback.resultDigest === sha256(canonical(output));
53
+ }
41
54
  export async function observationRemote(stage, operation, input, state, location, dependencies) {
42
55
  const requestId = buildInvocation(operation, input).requestId;
43
56
  let response, result;
@@ -58,8 +71,10 @@ export async function observationRemote(stage, operation, input, state, location
58
71
  throw unknown(requestId);
59
72
  }
60
73
  }
61
- if (!response.ok || !['reported', 'succeeded'].includes(result?.output?.status)) {
62
- if (operation === 'member-observation-admit' && prerequisite(result?.output)) {
74
+ const statuses = ['reported', 'succeeded', ...(operation === 'member-observation-report' ? ['local-accepted'] : [])];
75
+ if (!response.ok || !statuses.includes(result?.output?.status)) {
76
+ if (operation === 'member-observation-admit' && prerequisite(result?.output)
77
+ || cachedReportPrerequisite(result, state.pending, state, location)) {
63
78
  delete state.pending; await saveObservationState(location, state);
64
79
  throw Object.assign(new Error(result.output.message ?? 'Original local observation prerequisite is unresolved'),
65
80
  { code: result.output.code, output: result.output });
@@ -3,14 +3,16 @@ import { canonical, sha256 } from './local-integrity.mjs';
3
3
  import { mutationId14, mutationPaths14 } from './local-mutation-files.mjs';
4
4
 
5
5
  export const observationFail = message => { throw Object.assign(new Error(message), { code: 'ENTROROVE_LOCAL_OBSERVATION' }); };
6
- export const observationIdentityFields = ['taskId', 'flowId', 'stepId', 'leaseId', 'generation', 'settlementId'];
6
+ export const observationReadOnlyIdentityFields = ['taskId', 'flowId', 'stepId', 'leaseId', 'generation'];
7
+ export const observationIdentityFields = [...observationReadOnlyIdentityFields, 'settlementId'];
7
8
  export function observationExact(value, fields) {
8
9
  if (!value || typeof value !== 'object' || Array.isArray(value)
9
10
  || canonical(Object.keys(value).sort()) !== canonical([...fields].sort())) observationFail('Local observation fields are invalid');
10
11
  }
11
12
  export function observationInput(input) {
12
- observationExact(input, observationIdentityFields);
13
- for (const field of observationIdentityFields.filter(field => field !== 'generation')) mutationId14(input[field], field);
13
+ const fields = Object.hasOwn(input ?? {}, 'settlementId') ? observationIdentityFields : observationReadOnlyIdentityFields;
14
+ observationExact(input, fields);
15
+ for (const field of fields.filter(field => field !== 'generation')) mutationId14(input[field], field);
14
16
  if (!Number.isSafeInteger(input.generation) || input.generation < 1) observationFail('Local observation generation is invalid');
15
17
  return input;
16
18
  }
@@ -36,7 +38,7 @@ function files(values, modes) {
36
38
  && (!Number.isSafeInteger(file.mode) || file.mode < 0 || file.mode > 511)) observationFail('Local observation file baseline is invalid');
37
39
  }
38
40
  }
39
- function signature(value) {
41
+ export function observationAuthoritySignature(value) {
40
42
  try {
41
43
  const encoded = process.env.ENTROROVE_WORKFLOW_PLAN_PUBLIC_KEY, der = Buffer.from(encoded ?? '', 'base64');
42
44
  const key = createPublicKey({ key: der, type: 'spki', format: 'der' }), receipt = value.authorityReceipt;
@@ -50,18 +52,23 @@ function signature(value) {
50
52
  } catch { observationFail('Local observation requires its original pinned public cloud admission signature'); }
51
53
  }
52
54
  export function observationAdmission(value, input, { allowExpired = false } = {}) {
53
- observationExact(value, [...observationIdentityFields, 'schemaVersion', 'purpose', 'observationId', 'ownerId', 'taskDigest',
54
- 'chainDigest', 'epoch', 'planId', 'planDigest', 'executionPlanSha256', 'repositoryDigest', 'settlementDigest', 'command',
55
+ const readOnly = !Object.hasOwn(input, 'settlementId');
56
+ const fields = readOnly ? observationReadOnlyIdentityFields : observationIdentityFields;
57
+ observationExact(value, [...fields, 'schemaVersion', 'purpose', 'observationId', 'ownerId', 'taskDigest',
58
+ 'chainDigest', 'epoch', 'planId', 'planDigest', 'executionPlanSha256', 'repositoryDigest',
59
+ ...(readOnly ? ['sourceKind'] : ['settlementDigest']), 'command',
55
60
  'sourceManifest', 'protectedFiles', 'issuedAt', 'expiresAt', 'authorityReceipt']);
56
- signature(value);
61
+ observationAuthoritySignature(value);
57
62
  const start = Date.parse(value.issuedAt), end = Date.parse(value.expiresAt);
58
63
  if (value.schemaVersion !== 'entrorove.local-observation-admission/1.0' || value.purpose !== 'observe-approved-command'
59
- || value.epoch !== 0 || observationIdentityFields.some(field => value[field] !== input[field])
64
+ || value.epoch !== 0 || fields.some(field => value[field] !== input[field])
65
+ || readOnly && value.sourceKind !== 'original-read-only-plan'
60
66
  || value.repositoryDigest !== sha256(input.repositoryRoot).slice(0, 32) || !Number.isFinite(start) || !Number.isFinite(end)
61
67
  || start > Date.now() || end <= start || end - start > 600_000 || !allowExpired && end <= Date.now()
62
68
  || !/^[a-f0-9-]{36}$/.test(value.ownerId)
63
- || ['taskDigest', 'chainDigest', 'planDigest', 'executionPlanSha256', 'settlementDigest'].some(field => !/^[a-f0-9]{64}$/.test(value[field]))) {
64
- observationFail('Local observation admission is expired or lost its original task, repository or settlement');
69
+ || ['taskDigest', 'chainDigest', 'planDigest', 'executionPlanSha256', ...(readOnly ? [] : ['settlementDigest'])]
70
+ .some(field => !/^[a-f0-9]{64}$/.test(value[field]))) {
71
+ observationFail('Local observation admission is expired or lost its original task, repository or source binding');
65
72
  }
66
73
  mutationId14(value.observationId, 'observationId'); mutationId14(value.planId, 'planId');
67
74
  command(value.command); files(value.sourceManifest, true); files(value.protectedFiles, false);