deadbyte-mcp 0.15.41 → 0.15.42

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.
Files changed (85) hide show
  1. package/DEADBYTE-MCP.cmd +8 -4
  2. package/MANIFEST.SHA256 +84 -41
  3. package/PROVENANCE.json +53 -15
  4. package/README.txt +27 -18
  5. package/RELEASE.json +43 -15
  6. package/SBOM.cdx.json +2 -2
  7. package/TEST-MATRIX.json +106 -14
  8. package/bridge/package.json +1 -1
  9. package/controller/deadbyte-controller.ps1 +132 -12
  10. package/controller/deadbyte-process-policy.json +4 -4
  11. package/docs/R65-AUTHORITY-CONTINUITY-DESIGN.json +27 -0
  12. package/docs/R65-REFERENCE-GAP-MATRIX.json +22 -0
  13. package/package.json +5 -4
  14. package/scripts/deploy-r48.mjs +123 -7
  15. package/scripts/gate-windows-r65-prepublish.ps1 +65 -0
  16. package/scripts/npm-publication-r65.mjs +139 -0
  17. package/scripts/npm-release-r48.mjs +56 -37
  18. package/scripts/r65-canonical-full.mjs +45 -0
  19. package/scripts/r65-focused.mjs +37 -0
  20. package/scripts/verify-r65-parent-source.mjs +75 -0
  21. package/src/activation-continuity-engine-r65.mjs +225 -0
  22. package/src/activation-lock-r65.mjs +155 -0
  23. package/src/activation-preflight-r65.mjs +197 -0
  24. package/src/authority-continuity-r65.mjs +541 -0
  25. package/src/authority-epoch-cli-r65.mjs +10 -0
  26. package/src/autonomous-policy.mjs +7 -3
  27. package/src/coding-plane.mjs +12 -3
  28. package/src/desktop-policy-r40.mjs +6 -3
  29. package/src/doctor-transactions-r58.mjs +1 -1
  30. package/src/host-plane.mjs +9 -3
  31. package/src/process-policy.mjs +7 -4
  32. package/src/remote-authority.mjs +10 -1
  33. package/src/runtime-update.mjs +15 -5
  34. package/src/version.mjs +1 -1
  35. package/test/audit-context.test.mjs +15 -4
  36. package/test/autonomous-local-supervisor.test.mjs +7 -6
  37. package/test/autonomous-supervisor-integration.test.mjs +8 -7
  38. package/test/capability-resolver.test.mjs +11 -5
  39. package/test/codex-autonomous-r44.test.mjs +7 -6
  40. package/test/coding-observation.test.mjs +4 -3
  41. package/test/commander-real-parity-r47.test.mjs +5 -4
  42. package/test/commander-torture-r47.test.mjs +5 -4
  43. package/test/desktop-policy-r40.test.mjs +6 -4
  44. package/test/desktop-runtime-r40.test.mjs +8 -3
  45. package/test/host-inline-r45.test.mjs +4 -3
  46. package/test/machine-fs-runtime.test.mjs +4 -3
  47. package/test/process-longrun-r36.test.mjs +3 -2
  48. package/test/process-policy.test.mjs +10 -6
  49. package/test/process-runtime.test.mjs +9 -6
  50. package/test/r52-host-revocation.test.mjs +1 -0
  51. package/test/r59-tool-surface-compat.test.mjs +1 -1
  52. package/test/r64-release-contract.test.mjs +2 -1
  53. package/test/r65-activation-lock-recovery.test.mjs +72 -0
  54. package/test/r65-activation-lock.test.mjs +59 -0
  55. package/test/r65-activation-orchestration.test.mjs +179 -0
  56. package/test/r65-activation-preflight.test.mjs +35 -0
  57. package/test/r65-authority-continuity-decision.test.mjs +58 -0
  58. package/test/r65-authority-durable-policy-binding.test.mjs +34 -0
  59. package/test/r65-authority-epoch-cli.test.mjs +125 -0
  60. package/test/r65-authority-epoch-races.test.mjs +87 -0
  61. package/test/r65-authority-epoch-recovery.test.mjs +82 -0
  62. package/test/r65-authority-epoch-store.test.mjs +94 -0
  63. package/test/r65-authority-fixture.mjs +41 -0
  64. package/test/r65-authority-grant-capture.test.mjs +37 -0
  65. package/test/r65-authority-observation-epoch.test.mjs +25 -0
  66. package/test/r65-authority-policy-envelope.test.mjs +46 -0
  67. package/test/r65-authority-restored-safe.test.mjs +75 -0
  68. package/test/r65-authority-runtime-epoch.test.mjs +115 -0
  69. package/test/r65-authority-runtime-policy-binding.test.mjs +29 -0
  70. package/test/r65-authority-test-trust.mjs +20 -0
  71. package/test/r65-cli-invocation.test.mjs +38 -0
  72. package/test/r65-controller-authority-epoch-live.test.mjs +100 -0
  73. package/test/r65-controller-authority-epoch.test.mjs +37 -0
  74. package/test/r65-deploy-preflight.test.mjs +55 -0
  75. package/test/r65-epoch-native-activation-integration.test.mjs +141 -0
  76. package/test/r65-epoch-native-continuity-engine.test.mjs +112 -0
  77. package/test/r65-epoch-native-cross-plane.test.mjs +95 -0
  78. package/test/r65-legacy-authority-migration.test.mjs +63 -0
  79. package/test/r65-legacy-auto-restore-rejection.test.mjs +17 -0
  80. package/test/r65-npm-publication.test.mjs +114 -0
  81. package/test/r65-parent-source-verifier.test.mjs +63 -0
  82. package/test/r65-release-contract.test.mjs +74 -0
  83. package/test/r65-transaction-precondition.test.mjs +36 -0
  84. package/test/remote-session-cli.test.mjs +6 -6
  85. package/test/standing-grants.test.mjs +20 -4
@@ -1,7 +1,9 @@
1
1
  import { spawnSync } from 'node:child_process';
2
2
  import { createHash } from 'node:crypto';
3
- import { verifyPublishedTarballIdentity } from './npm-artifact-identity-r61.mjs';
4
- import { mkdtemp, readFile, rm, unlink } from 'node:fs/promises';
3
+ import { reconcileNpmPublication } from './npm-publication-r65.mjs';
4
+ import { mkdtemp, readFile, rm, unlink, mkdir, rename } from 'node:fs/promises';
5
+ import { existsSync } from 'node:fs';
6
+ import { resolveStateRoot } from '../src/state-root.mjs';
5
7
  import path from 'node:path';
6
8
  import os from 'node:os';
7
9
  import { fileURLToPath } from 'node:url';
@@ -11,7 +13,7 @@ const node = process.execPath;
11
13
  const npmCli = process.env.npm_execpath || path.join(path.dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js');
12
14
 
13
15
  function run(command, args, options = {}) {
14
- const { cwd = root, timeout = 180000 } = options;
16
+ const { cwd = root, timeout = 180000, env=process.env } = options;
15
17
  const r = spawnSync(command, args, {
16
18
  cwd,
17
19
  encoding: 'utf8',
@@ -19,6 +21,7 @@ function run(command, args, options = {}) {
19
21
  stdio: ['ignore', 'pipe', 'pipe'],
20
22
  shell: false,
21
23
  windowsHide: true,
24
+ env,
22
25
  });
23
26
  if (r.error) throw r.error;
24
27
  if (r.status !== 0) {
@@ -95,30 +98,34 @@ async function main() {
95
98
  const existingText = (existing.stdout || '').trim();
96
99
  const isAlreadyPublished = existing.status === 0 && existingText && existingText !== 'null';
97
100
 
98
- if (isAlreadyPublished) {
99
- if (!isVerifyOnly) {
100
- throw new Error(`deadbyte-mcp@${version} is already published on registry: ${existingText}`);
101
- } else {
102
- console.log(`Notice: deadbyte-mcp@${version} is already published on registry (${existingText}), proceeding with pre-publish verification only.`);
103
- }
104
- }
101
+ if (isAlreadyPublished) console.log(`Notice: deadbyte-mcp@${version} exists on registry (${existingText}); exact-byte reconciliation is required.`);
105
102
 
106
103
  console.log('[2/7] Pre-pack gates: verifying working tree manifest and doctor...');
107
104
  run(node, [path.join(root, 'scripts', 'release-manifest.mjs'), 'verify']);
108
- run(node, [path.join(root, 'src', 'deadbyte-cli.mjs'), 'doctor']);
105
+ const doctorState=await mkdtemp(path.join(os.tmpdir(),'deadbyte-r65-release-doctor-'));
106
+ try{run(node,[path.join(root,'src','deadbyte-cli.mjs'),'doctor'],{env:{...process.env,DEADBYTE_STATE_ROOT:doctorState}});}
107
+ finally{await rm(doctorState,{recursive:true,force:true});}
109
108
 
110
109
  console.log('[3/7] Packing artifact with npm pack...');
111
- const packed = parseJson(runNpm(['pack', '--json'], { timeout: 180000 }));
112
- const packRecord = Array.isArray(packed) ? packed[0] : packed?.[pkg.name];
110
+ const publicationDir=isVerifyOnly?null:path.join(resolveStateRoot(),'evidence','npm-publication',version);
111
+ const preservedTarball=publicationDir?path.join(publicationDir,`deadbyte-mcp-${version}.tgz`):null;
112
+ const resuming=Boolean(preservedTarball&&existsSync(path.join(publicationDir,'claim.json')));
113
+ if(publicationDir)await mkdir(publicationDir,{recursive:true});
114
+ if(resuming&&!existsSync(preservedTarball))throw new Error('immutable npm publication claim exists but exact local tarball is missing; manual artifact reconciliation required');
115
+ if(!resuming&&preservedTarball&&existsSync(preservedTarball))throw new Error('unclaimed preserved npm tarball exists; inspect artifact evidence before another pack');
116
+ const packed=resuming?null:parseJson(runNpm(['pack', '--json'], { timeout: 180000 }));
117
+ const packRecord=resuming?{filename:path.basename(preservedTarball)}:(Array.isArray(packed) ? packed[0] : packed?.[pkg.name]);
113
118
  const filename = packRecord?.filename;
114
119
  if (!filename) throw new Error('npm pack did not return a tarball filename');
115
- const tarball = path.join(root, filename);
120
+ const tarball = resuming?preservedTarball:path.join(root, filename);
116
121
  let releaseSucceeded = false;
117
122
  let publishAttempted = false;
123
+ let releasePending = false;
118
124
 
119
125
  try {
120
126
  console.log('[4/7] Verifying packed artifact file surface against MANIFEST.SHA256 and packaging contract...');
121
127
  const manifestText = await readFile(path.join(root, 'MANIFEST.SHA256'), 'utf8');
128
+ const manifestSha256=createHash('sha256').update(manifestText).digest('hex');
122
129
  const manifestEntries = new Set();
123
130
  for (const line of manifestText.split(/\r?\n/)) {
124
131
  if (!line) continue;
@@ -169,7 +176,7 @@ async function main() {
169
176
  }
170
177
 
171
178
  run(node, [path.join(extractedPackage, 'src', 'deadbyte-cli.mjs'), 'doctor'], {
172
- cwd: extractedPackage,
179
+ cwd: extractedPackage,env:{...process.env,DEADBYTE_STATE_ROOT:path.join(sandbox,'state')},
173
180
  });
174
181
 
175
182
  console.log('Artifact isolation gate PASS (clean doctor and version check on unpacked tarball in sandbox)');
@@ -189,30 +196,37 @@ async function main() {
189
196
  return;
190
197
  }
191
198
 
192
- const localBytes = await readFile(tarball);
199
+ if(!resuming)await rename(tarball,preservedTarball);
200
+ const publishTarball=preservedTarball;
201
+ const localBytes = await readFile(publishTarball);
202
+ const localIntegrity=`sha512-${createHash('sha512').update(localBytes).digest('base64')}`;
203
+ if(packRecord.integrity&&packRecord.integrity!==localIntegrity)throw new Error('npm packed artifact integrity changed before publication');
193
204
 
194
205
  console.log('[6/7] Publishing artifact to npm registry...');
195
206
  publishAttempted = true;
196
- let publishOut;
197
- try {
198
- publishOut = runNpmInteractive(['publish', tarball, '--access', 'public', '--auth-type=web'], { timeout: 300000 });
199
- } catch (publishError) {
200
- const detail = String(publishError?.message || publishError);
201
- if (/EOTP|one-time password|two-factor|2FA|security.?key|authentication/i.test(detail)) {
202
- console.error('Publish requires interactive npm authentication (Security Key/WebAuthn).');
203
- console.error('Tarball preserved for retry: ' + tarball);
204
- }
205
- throw publishError;
207
+ const reconciliation=await reconcileNpmPublication({recordPath:publicationDir,version,manifestSha256,localBytes,
208
+ publish:async()=>runNpmInteractive(['publish',publishTarball,'--access','public','--auth-type=web'],{timeout:300000}),
209
+ lookup:async()=>{
210
+ try{
211
+ const viewed=parseJson(runNpm(['view',`deadbyte-mcp@${version}`,'version','dist.integrity','dist.shasum','dist.tarball','--json'],{timeout:120000}));
212
+ return Array.isArray(viewed)?viewed[0]:viewed;
213
+ }catch(error){if(/E404|404 Not Found/.test(String(error?.message)))return null;throw error;}
214
+ },
215
+ tags:async()=>parseJson(runNpm(['view','deadbyte-mcp','dist-tags','--json'],{timeout:120000})),
216
+ fetchTarball:async published=>{
217
+ const response=await fetch(published?.dist?.tarball,{signal:AbortSignal.timeout(120000)});
218
+ if(!response.ok)throw Object.assign(new Error(`registry tarball fetch failed status=${response.status}`),{status:response.status});
219
+ return Buffer.from(await response.arrayBuffer());
220
+ }});
221
+ if(reconciliation.state!=='REGISTRY_IDENTITY_VERIFIED'){
222
+ console.log('RELEASE_PENDING '+JSON.stringify({state:reconciliation.state,version,tarball:publishTarball,record_path:publicationDir,reason:reconciliation.reason??null}));
223
+ releasePending=true;
224
+ process.exitCode=2;return;
206
225
  }
207
- const viewed = parseJson(runNpm(['view', `deadbyte-mcp@${version}`, 'version', 'dist.integrity', 'dist.shasum', 'dist.tarball', '--json'], { timeout: 120000 }));
208
- const published = Array.isArray(viewed) ? viewed[0] : viewed;
209
- const dist = published?.dist ?? published;
210
- if(typeof dist?.tarball !== 'string')throw new Error('published registry tarball URL missing');
211
- const registryResponse = await fetch(dist.tarball,{signal:AbortSignal.timeout(120000)});
212
- if(!registryResponse.ok) throw new Error(`registry tarball fetch failed status=${registryResponse.status}`);
213
- const registryBytes = Buffer.from(await registryResponse.arrayBuffer());
214
- const tags = parseJson(runNpm(['view', 'deadbyte-mcp', 'dist-tags', '--json'], { timeout: 120000 }));
215
- const identity=verifyPublishedTarballIdentity({version,localBytes,registryBytes,published,tags});
226
+ const identity=reconciliation.identity;
227
+ const published={version,dist:{integrity:identity.npm_integrity,shasum:identity.shasum}};
228
+ const tags={latest:version};
229
+ const publishOut='registry identity verified by durable reconciliation';
216
230
 
217
231
  console.log('[7/7] Post-publish external registry smoke test (executed outside repo in temp)...');
218
232
  const tempDir = os.tmpdir();
@@ -227,6 +241,7 @@ async function main() {
227
241
  const externalDoctor = runNpm(['exec', '--yes', `--package=deadbyte-mcp@${version}`, '--', 'deadbyte-mcp', 'doctor'], {
228
242
  cwd: tempDir,
229
243
  timeout: 300000,
244
+ env:{...process.env,DEADBYTE_STATE_ROOT:path.join(publicationDir,'external-doctor-state')},
230
245
  });
231
246
 
232
247
  if(!publishOnly){
@@ -253,10 +268,14 @@ async function main() {
253
268
  }, null, 2));
254
269
 
255
270
  } finally {
256
- if ((releaseSucceeded && !publishOnly) || !publishAttempted) {
271
+ if (isVerifyOnly || (!publishAttempted&&!publicationDir)) {
257
272
  await unlink(tarball).catch(() => {});
273
+ } else if(releasePending){
274
+ console.log('Publication processing or reconciliation pending; exact tarball preserved: '+preservedTarball);
275
+ } else if(releaseSucceeded){
276
+ console.log('Verified release tarball retained for audit: '+preservedTarball);
258
277
  } else {
259
- console.error('Release artifact preserved after failure: ' + tarball);
278
+ console.error('Release artifact preserved for reconciliation: ' + preservedTarball);
260
279
  }
261
280
  }
262
281
  }
@@ -0,0 +1,45 @@
1
+ import {createHash} from 'node:crypto';
2
+ import {mkdirSync,readdirSync,writeFileSync} from 'node:fs';
3
+ import {spawnSync} from 'node:child_process';
4
+ import path from 'node:path';
5
+
6
+ const runId=process.argv[2]??'manual';
7
+ if(!/^[A-Za-z0-9._-]{1,96}$/.test(runId))throw new Error('invalid run id');
8
+ const testDir=path.join(process.cwd(),'test');
9
+ const tests=readdirSync(testDir,{withFileTypes:true})
10
+ .filter(entry=>entry.isFile()&&entry.name.endsWith('.test.mjs'))
11
+ .map(entry=>path.join('test',entry.name))
12
+ .sort((a,b)=>a.localeCompare(b,'en'));
13
+ const startedAt=new Date().toISOString();
14
+ const run=spawnSync(process.execPath,['--test','--test-concurrency=4',...tests],{
15
+ cwd:process.cwd(),
16
+ encoding:'utf8',
17
+ maxBuffer:128*1024*1024,
18
+ windowsHide:true,
19
+ shell:false
20
+ });
21
+ const stdout=run.stdout||'',stderr=run.stderr||'';
22
+ const evidenceDir=path.resolve(process.cwd(),'..','..','testing','0.15.42-r65','full-runs',runId);
23
+ mkdirSync(evidenceDir,{recursive:true});
24
+ writeFileSync(path.join(evidenceDir,'stdout.log'),stdout,'utf8');
25
+ writeFileSync(path.join(evidenceDir,'stderr.log'),stderr,'utf8');
26
+ const grab=(name)=>{const m=stdout.match(new RegExp('(?:ℹ|#)\\s*'+name+'\\s+(\\d+)','i'));return m?Number(m[1]):null;};
27
+ const result={
28
+ schema:'deadbyte.r65-full-run.v1',
29
+ run_id:runId,
30
+ started_at_utc:startedAt,
31
+ finished_at_utc:new Date().toISOString(),
32
+ command:[process.execPath,'--test','--test-concurrency=4',...tests],
33
+ test_files:tests.length,
34
+ exit_code:run.status??null,
35
+ signal:run.signal??null,
36
+ error:run.error?String(run.error.stack||run.error):null,
37
+ tests:grab('tests'),pass:grab('pass'),fail:grab('fail'),skipped:grab('skipped'),
38
+ stdout_sha256:createHash('sha256').update(stdout).digest('hex'),
39
+ stderr_sha256:createHash('sha256').update(stderr).digest('hex')
40
+ };
41
+ writeFileSync(path.join(evidenceDir,'result.json'),JSON.stringify(result,null,2)+'\n','utf8');
42
+ process.stdout.write(stdout);
43
+ process.stderr.write(stderr);
44
+ if(run.error)process.stderr.write(String(run.error.stack||run.error)+'\n');
45
+ process.exit(run.status??1);
@@ -0,0 +1,37 @@
1
+ import {spawnSync} from 'node:child_process';
2
+
3
+ const tests=[
4
+ 'test/r65-activation-lock.test.mjs',
5
+ 'test/r65-activation-lock-recovery.test.mjs',
6
+ 'test/r65-activation-orchestration.test.mjs',
7
+ 'test/r65-activation-preflight.test.mjs',
8
+ 'test/r65-authority-continuity-decision.test.mjs',
9
+ 'test/r65-authority-durable-policy-binding.test.mjs',
10
+ 'test/r65-authority-epoch-cli.test.mjs',
11
+ 'test/r65-authority-epoch-races.test.mjs',
12
+ 'test/r65-authority-epoch-recovery.test.mjs',
13
+ 'test/r65-authority-epoch-store.test.mjs',
14
+ 'test/r65-authority-grant-capture.test.mjs',
15
+ 'test/r65-authority-observation-epoch.test.mjs',
16
+ 'test/r65-authority-policy-envelope.test.mjs',
17
+ 'test/r65-authority-runtime-epoch.test.mjs',
18
+ 'test/r65-authority-runtime-policy-binding.test.mjs',
19
+ 'test/r65-authority-restored-safe.test.mjs',
20
+ 'test/r65-controller-authority-epoch.test.mjs',
21
+ 'test/r65-controller-authority-epoch-live.test.mjs',
22
+ 'test/r65-deploy-preflight.test.mjs',
23
+ 'test/r65-epoch-native-activation-integration.test.mjs',
24
+ 'test/r65-epoch-native-continuity-engine.test.mjs',
25
+ 'test/r65-epoch-native-cross-plane.test.mjs',
26
+ 'test/r65-legacy-authority-migration.test.mjs',
27
+ 'test/r65-legacy-auto-restore-rejection.test.mjs',
28
+ 'test/r65-parent-source-verifier.test.mjs',
29
+ 'test/r65-release-contract.test.mjs',
30
+ 'test/r65-transaction-precondition.test.mjs'
31
+ ];
32
+ const run=spawnSync(process.execPath,['--test','--test-concurrency=2',...tests],{
33
+ cwd:process.cwd(),encoding:'utf8',maxBuffer:64*1024*1024,windowsHide:true
34
+ });
35
+ process.stdout.write(run.stdout||'');
36
+ process.stderr.write(run.stderr||'');
37
+ process.exit(run.status??1);
@@ -0,0 +1,75 @@
1
+ import {createHash} from 'node:crypto';
2
+ import {lstat,readFile,realpath} from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import {fileURLToPath} from 'node:url';
5
+
6
+ const HEX64=/^[0-9a-f]{64}$/;
7
+ const sha=bytes=>createHash('sha256').update(bytes).digest('hex');
8
+
9
+ function parseArgs(argv){
10
+ const out={root:null,expectedManifestSha256:null};
11
+ for(let i=0;i<argv.length;i++){
12
+ const arg=argv[i];
13
+ if(arg==='--root')out.root=argv[++i]??null;
14
+ else if(arg==='--expected-manifest-sha256')out.expectedManifestSha256=argv[++i]??null;
15
+ else throw new Error('unknown argument '+arg);
16
+ }
17
+ if(typeof out.root!=='string'||out.root.length<1)throw new Error('--root required');
18
+ if(!HEX64.test(String(out.expectedManifestSha256)))throw new Error('--expected-manifest-sha256 must be lowercase hex');
19
+ return out;
20
+ }
21
+
22
+ function parseManifest(text){
23
+ const entries=[];
24
+ const seen=new Set();
25
+ for(const raw of text.split(/\r?\n/)){
26
+ if(raw==='')continue;
27
+ const match=/^([0-9a-f]{64}) (.+)$/.exec(raw);
28
+ if(!match)throw new Error('manifest line invalid');
29
+ const rel=match[2];
30
+ if(rel.includes('\\')||path.posix.isAbsolute(rel)||rel==='.'||rel.split('/').includes('..')||path.posix.normalize(rel)!==rel)
31
+ throw new Error('manifest path invalid: '+rel);
32
+ if(seen.has(rel))throw new Error('manifest duplicate path: '+rel);
33
+ seen.add(rel);
34
+ entries.push({sha256:match[1],rel});
35
+ }
36
+ if(entries.length<1)throw new Error('manifest has no entries');
37
+ return entries;
38
+ }
39
+
40
+ export async function verifyDeclaredParentSource({root,expectedManifestSha256}){
41
+ const absolute=path.resolve(root);
42
+ const rootReal=await realpath(absolute);
43
+ const manifestPath=path.join(absolute,'MANIFEST.SHA256');
44
+ const manifestBytes=await readFile(manifestPath);
45
+ const actualManifestSha=sha(manifestBytes);
46
+ if(actualManifestSha!==expectedManifestSha256)
47
+ throw new Error(`manifest SHA-256 mismatch expected=${expectedManifestSha256} actual=${actualManifestSha}`);
48
+ const entries=parseManifest(manifestBytes.toString('utf8'));
49
+ for(const entry of entries){
50
+ const target=path.resolve(absolute,...entry.rel.split('/'));
51
+ const info=await lstat(target).catch(error=>{
52
+ if(error?.code==='ENOENT')throw new Error('listed file missing: '+entry.rel);
53
+ throw error;
54
+ });
55
+ if(!info.isFile()||info.isSymbolicLink())throw new Error('listed path is not a regular file: '+entry.rel);
56
+ const targetReal=await realpath(target);
57
+ const prefix=rootReal.endsWith(path.sep)?rootReal:rootReal+path.sep;
58
+ if(targetReal!==rootReal&&!targetReal.startsWith(prefix))throw new Error('listed path escapes root: '+entry.rel);
59
+ const actual=sha(await readFile(target));
60
+ if(actual!==entry.sha256)throw new Error(`hash mismatch ${entry.rel} expected=${entry.sha256} actual=${actual}`);
61
+ }
62
+ return Object.freeze({files:entries.length,manifest_sha256:actualManifestSha,root:rootReal});
63
+ }
64
+
65
+ const invoked=process.argv[1]&&path.resolve(process.argv[1])===path.resolve(fileURLToPath(import.meta.url));
66
+ if(invoked){
67
+ try{
68
+ const opts=parseArgs(process.argv.slice(2));
69
+ const result=await verifyDeclaredParentSource(opts);
70
+ console.log(`R65_PARENT_SOURCE_VERIFY_OK files=${result.files} manifest_sha256=${result.manifest_sha256}`);
71
+ }catch(error){
72
+ console.error('R65_PARENT_SOURCE_VERIFY_FAIL '+String(error?.stack??error));
73
+ process.exitCode=1;
74
+ }
75
+ }
@@ -0,0 +1,225 @@
1
+ import {createHash,randomBytes} from 'node:crypto';
2
+ import {open,readFile,rm} from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import {canonicalJson} from './canonical-json.mjs';
5
+ import {
6
+ captureStandingGrant,compareSecurityEnvelopes,createAuthorityEpochStore,deriveSecurityEnvelope
7
+ } from './authority-continuity-r65.mjs';
8
+
9
+ const PLANES=Object.freeze(['process','coding','autonomous','desktop','host']);
10
+ const REVOKE_ORDER=Object.freeze(['host','desktop','autonomous','coding','process']);
11
+ const RESTORE_ORDER=Object.freeze(['process','coding','autonomous','desktop','host']);
12
+ const HEX64=/^[0-9a-f]{64}$/;
13
+ const GRANT_PATH_FIELD=Object.freeze({
14
+ process:'grant_file',coding:'lease_file',autonomous:'lease_file',desktop:'grant_file',host:'arm_file'
15
+ });
16
+ const sha=bytes=>createHash('sha256').update(bytes).digest('hex');
17
+ const plain=value=>value&&typeof value==='object'&&!Array.isArray(value);
18
+
19
+ function parseJson(bytes,label){
20
+ try{return JSON.parse(bytes.toString('utf8'));}
21
+ catch{throw new Error(`${label} JSON invalid`);}
22
+ }
23
+ function inside(root,target){
24
+ const rel=path.relative(path.resolve(root),path.resolve(target));
25
+ return rel===''||(!rel.startsWith('..'+path.sep)&&rel!=='..'&&!path.isAbsolute(rel));
26
+ }
27
+ function grantPath(policy,plane,stateRoot){
28
+ const field=GRANT_PATH_FIELD[plane],raw=policy?.[field];
29
+ if(typeof raw!=='string'||!path.isAbsolute(raw))throw new Error(`${plane} grant path missing`);
30
+ const resolved=path.resolve(raw),runtimeRoot=path.join(path.resolve(stateRoot),'runtime');
31
+ if(!inside(runtimeRoot,resolved))throw new Error(`${plane} grant path escapes runtime state`);
32
+ return resolved;
33
+ }
34
+ async function readOptional(file){
35
+ try{return await readFile(file);}catch(error){if(error?.code==='ENOENT')return null;throw error;}
36
+ }
37
+ async function writeExclusive(file,bytes){
38
+ const handle=await open(file,'wx',0o600);
39
+ try{await handle.writeFile(bytes);await handle.sync();}
40
+ finally{await handle.close();}
41
+ }
42
+ async function removeIfExact(file,expectedSha256){
43
+ const current=await readOptional(file);
44
+ if(current===null)return {removed:false,reason:'absent'};
45
+ if(sha(current)!==expectedSha256)return {removed:false,reason:'replacement'};
46
+ await rm(file,{force:true});
47
+ return {removed:true,reason:'matched'};
48
+ }
49
+ function validateCapture(capture){
50
+ if(!plain(capture)||typeof capture.update_id!=='string'||!Array.isArray(capture.planes)||!plain(capture.records))throw new Error('authority continuity capture invalid');
51
+ for(const plane of capture.planes)if(!PLANES.includes(plane)||!capture.records[plane])throw new Error('authority continuity capture plane invalid');
52
+ }
53
+ function scopePermitted(plane,scope,policy){
54
+ if(plane==='process')return Array.isArray(scope.root_ids)&&Array.isArray(scope.profile_ids)&&
55
+ scope.root_ids.every(id=>Object.hasOwn(policy.roots??{},id))&&
56
+ scope.profile_ids.every(id=>Object.hasOwn(policy.profiles??{},id)&&
57
+ Array.isArray(policy.profiles[id]?.root_ids)&&policy.profiles[id].root_ids.every(rootId=>scope.root_ids.includes(rootId)));
58
+ if(plane==='coding')return Array.isArray(scope.root_ids)&&scope.root_ids.every(id=>Object.hasOwn(policy.roots??{},id));
59
+ if(plane==='autonomous')return Array.isArray(scope.root_ids)&&scope.root_ids.every(id=>(policy.root_ids??[]).includes(id));
60
+ if(plane==='desktop')return Array.isArray(scope.action_kinds)&&scope.action_kinds.every(id=>(policy.allowed_actions??[]).includes(id));
61
+ return plane==='host';
62
+ }
63
+ function buildSuccessorGrant({plane,record,successorPolicyBytes,successorPolicy}){
64
+ const policySha256=sha(successorPolicyBytes),instanceNonce=randomBytes(32).toString('hex'),grantedAt=new Date().toISOString();
65
+ let grant;
66
+ if(plane==='process')grant={schema:record.grant_schema,policy_sha256:policySha256,instance_nonce:instanceNonce,
67
+ profile_ids:[...record.granted_scope.profile_ids],root_ids:[...record.granted_scope.root_ids],granted_at_utc:grantedAt};
68
+ else if(plane==='coding')grant={schema:record.grant_schema,policy_sha256:policySha256,instance_nonce:instanceNonce,
69
+ root_ids:[...record.granted_scope.root_ids],granted_at_utc:grantedAt};
70
+ else if(plane==='autonomous')grant={schema:record.grant_schema,policy_sha256:policySha256,
71
+ coding_policy_sha256:successorPolicy.coding_policy_sha256,instance_nonce:instanceNonce,
72
+ root_ids:[...record.granted_scope.root_ids],granted_at_utc:grantedAt};
73
+ else if(plane==='desktop')grant={schema:record.grant_schema,policy_sha256:policySha256,instance_nonce:instanceNonce,
74
+ action_kinds:[...record.granted_scope.action_kinds],granted_at_utc:grantedAt};
75
+ else if(plane==='host')grant={schema:record.grant_schema,policy_sha256:policySha256,instance_nonce:instanceNonce,granted_at_utc:grantedAt};
76
+ else throw new Error('authority continuity plane invalid');
77
+ return {grant,bytes:Buffer.from(canonicalJson(grant),'utf8'),instanceNonce};
78
+ }
79
+ function classifyConflict(head,record,updateId){
80
+ if(!head)return 'REAUTH_REQUIRED';
81
+ if(head.action==='user_revoke')return 'REVOKED_DURING_UPDATE';
82
+ if((head.action==='grant'||head.action==='transaction_restore')&&
83
+ (head.grant_sha256!==record.grant_sha256||head.grant_instance_nonce!==record.grant_instance_nonce))return 'REPLACED';
84
+ if(head.action==='transaction_revoke'&&head.update_id===updateId&&
85
+ head.grant_sha256===record.grant_sha256&&head.grant_instance_nonce===record.grant_instance_nonce)return null;
86
+ return 'REAUTH_REQUIRED';
87
+ }
88
+
89
+ export function createEpochNativeContinuityEngine({stateRoot,predecessorPolicyPaths,successorPolicyPaths,updateId}={}){
90
+ const state=path.resolve(String(stateRoot??''));
91
+ if(!stateRoot||!plain(predecessorPolicyPaths)||typeof successorPolicyPaths!=='function'||
92
+ typeof updateId!=='string'||!/^[A-Za-z0-9._-]{1,96}$/.test(updateId))
93
+ throw new Error('authority continuity engine configuration invalid');
94
+
95
+ return Object.freeze({
96
+ async capture({observation,subject_sha256,pointer,expectedParent,candidate,expectedCandidate}){
97
+ if(!HEX64.test(String(subject_sha256))||!plain(observation?.authority))throw new Error('authority continuity capture subject invalid');
98
+ if(expectedParent?.release_id!==pointer?.release_id||expectedParent?.generation_manifest_sha256!==pointer?.generation_manifest_sha256||
99
+ expectedCandidate?.version!==candidate?.version||expectedCandidate?.manifest_sha256!==candidate?.manifest_sha256)
100
+ throw new Error('authority continuity lineage mismatch');
101
+ const records={},planes=[];
102
+ for(const plane of PLANES){
103
+ const authority=observation.authority[plane];
104
+ if(!authority?.armed)continue;
105
+ if(!Number.isSafeInteger(authority.epoch_seq)||authority.epoch_seq<1||!HEX64.test(String(authority.epoch_sha256)))
106
+ throw new Error(`${plane} standing authority is legacy or epoch identity is incomplete`);
107
+ const policyPath=predecessorPolicyPaths[plane];
108
+ if(typeof policyPath!=='string'||!path.isAbsolute(policyPath))throw new Error(`${plane} predecessor policy path unavailable`);
109
+ const policyBytes=await readFile(policyPath),policy=parseJson(policyBytes,`${plane} predecessor policy`);
110
+ const file=grantPath(policy,plane,state),grantBytes=await readFile(file),grant=parseJson(grantBytes,`${plane} predecessor grant`);
111
+ const store=createAuthorityEpochStore({runtimeDir:path.dirname(file),policyBytes});
112
+ const verified=await store.verifyGrant({plane,grantBytes,grantInstanceNonce:grant.instance_nonce,policyBytes});
113
+ const head=await store.readHead({plane});
114
+ if(!verified.trusted||!head||head.action!=='grant'&&head.action!=='transaction_restore'||
115
+ head.seq!==authority.epoch_seq||head.event_sha256!==authority.epoch_sha256)
116
+ throw new Error(`${plane} observed standing authority does not match durable epoch`);
117
+ const record=captureStandingGrant({plane,policyBytes,grantBytes,predecessor:expectedParent,candidate:expectedCandidate,updateId});
118
+ records[plane]=Object.freeze({record,policy_path:policyPath,grant_file:file,prior_policy_bytes:policyBytes,
119
+ prior_grant_bytes:grantBytes,captured_head:Object.freeze({...head})});
120
+ planes.push(plane);
121
+ }
122
+ if(planes.length===0)throw new Error('epoch-native continuity capture has no armed planes');
123
+ return Object.freeze({schema:'deadbyte.activation-authority-capture.r65.v1',update_id:updateId,subject_sha256,
124
+ planes:Object.freeze(planes),records:Object.freeze(records)});
125
+ },
126
+
127
+ async revoke(capture){
128
+ validateCapture(capture);
129
+ const outcomes={};
130
+ for(const plane of REVOKE_ORDER){
131
+ const item=capture.records[plane];
132
+ if(!item)continue;
133
+ const {record,prior_policy_bytes:policyBytes,prior_grant_bytes:grantBytes,grant_file:file,captured_head:capturedHead}=item;
134
+ const store=createAuthorityEpochStore({runtimeDir:path.dirname(file),policyBytes});
135
+ const current=await store.readHead({plane});
136
+ if(!current||current.event_sha256!==capturedHead.event_sha256||current.seq!==capturedHead.seq)
137
+ throw new Error(`${plane} authority changed before transaction revoke`);
138
+ const revoked=await store.append({plane,action:'transaction_revoke',grantBytes,
139
+ grantInstanceNonce:record.grant_instance_nonce,updateId:capture.update_id,expectedHeadSha256:capturedHead.event_sha256});
140
+ let observed=await store.readHead({plane});
141
+ if(!observed||observed.event_sha256!==revoked.event_sha256)
142
+ throw new Error(`${plane} authority changed during transaction revoke`);
143
+ const sentinel=await readOptional(file);
144
+ if(sentinel===null)throw new Error(`${plane} grant disappeared during transaction revoke`);
145
+ if(sha(sentinel)!==record.grant_sha256)throw new Error(`${plane} replacement grant appeared during transaction revoke`);
146
+ observed=await store.readHead({plane});
147
+ if(!observed||observed.event_sha256!==revoked.event_sha256){
148
+ const conflict=classifyConflict(observed,record,capture.update_id);
149
+ throw new Error(`${plane} authority changed after transaction revoke: ${conflict??'unknown'}`);
150
+ }
151
+ const verification=await store.verifyGrant({plane,grantBytes,grantInstanceNonce:record.grant_instance_nonce,policyBytes});
152
+ if(verification.trusted)throw new Error(`${plane} remained trusted after transaction revoke`);
153
+ outcomes[plane]='TRANSACTION_REVOKED';
154
+ }
155
+ return Object.freeze(outcomes);
156
+ },
157
+
158
+ async restore({capture,successor_observation,activation,candidate,expectedCandidate,subject_sha256}){
159
+ validateCapture(capture);
160
+ if(capture.subject_sha256!==subject_sha256||expectedCandidate?.version!==candidate?.version||
161
+ expectedCandidate?.manifest_sha256!==candidate?.manifest_sha256||activation?.verified!==true)
162
+ throw new Error('authority continuity restore subject invalid');
163
+ const paths=await successorPolicyPaths();
164
+ if(!plain(paths))throw new Error('successor authority policy paths unavailable');
165
+ const outcomes=Object.fromEntries(PLANES.map(plane=>[plane,capture.records[plane]?'REAUTH_REQUIRED':'NOT_APPLICABLE']));
166
+ const reauth=[],restoredNonces={};
167
+ let codingRestored=!capture.records.coding;
168
+ for(const plane of RESTORE_ORDER){
169
+ const item=capture.records[plane];
170
+ if(!item)continue;
171
+ const {record,prior_policy_bytes:priorPolicyBytes,prior_grant_bytes:priorGrantBytes,grant_file:file}=item;
172
+ if(plane==='autonomous'&&!codingRestored){outcomes[plane]='REAUTH_REQUIRED';reauth.push(plane);continue;}
173
+ const policyPath=paths[plane];
174
+ if(typeof policyPath!=='string'||!path.isAbsolute(policyPath)){outcomes[plane]='REAUTH_REQUIRED';reauth.push(plane);continue;}
175
+ let successorPolicyBytes,successorPolicy,priorPolicy;
176
+ try{
177
+ successorPolicyBytes=await readFile(policyPath);
178
+ successorPolicy=parseJson(successorPolicyBytes,`${plane} successor policy`);
179
+ priorPolicy=parseJson(priorPolicyBytes,`${plane} predecessor policy`);
180
+ }catch{outcomes[plane]='REAUTH_REQUIRED';reauth.push(plane);continue;}
181
+ const priorEnvelope=deriveSecurityEnvelope(plane,priorPolicy),successorEnvelope=deriveSecurityEnvelope(plane,successorPolicy);
182
+ const relation=compareSecurityEnvelopes(plane,priorEnvelope,successorEnvelope).relation;
183
+ if(relation==='WIDER'){outcomes[plane]='POLICY_WIDENED';reauth.push(plane);continue;}
184
+ if(relation==='UNVERIFIED'||!scopePermitted(plane,record.granted_scope,successorPolicy)){
185
+ outcomes[plane]='REAUTH_REQUIRED';reauth.push(plane);continue;
186
+ }
187
+ const successorGrantPath=grantPath(successorPolicy,plane,state);
188
+ if(successorGrantPath!==file){outcomes[plane]='REAUTH_REQUIRED';reauth.push(plane);continue;}
189
+ const store=createAuthorityEpochStore({runtimeDir:path.dirname(file),policyBytes:successorPolicyBytes});
190
+ const head=await store.readHead({plane}),conflict=classifyConflict(head,record,capture.update_id);
191
+ if(conflict){
192
+ if(conflict==='REVOKED_DURING_UPDATE')await removeIfExact(file,record.grant_sha256).catch(()=>{});
193
+ outcomes[plane]=conflict;reauth.push(plane);continue;
194
+ }
195
+ const existing=await readOptional(file);
196
+ if(existing===null){outcomes[plane]='REVOKED_DURING_UPDATE';reauth.push(plane);continue;}
197
+ if(sha(existing)!==record.grant_sha256){outcomes[plane]='REPLACED';reauth.push(plane);continue;}
198
+ await rm(file,{force:true});
199
+ const prepared=buildSuccessorGrant({plane,record,successorPolicyBytes,successorPolicy});
200
+ try{await writeExclusive(file,prepared.bytes);}
201
+ catch(error){if(error?.code==='EEXIST'){outcomes[plane]='REPLACED';reauth.push(plane);continue;}throw error;}
202
+ try{
203
+ const restoreBinding={prior_grant_sha256:record.grant_sha256,prior_grant_instance_nonce:record.grant_instance_nonce,
204
+ predecessor_policy_envelope_sha256:record.prior_policy_envelope_sha256,successor_policy_envelope_sha256:successorEnvelope.sha256,
205
+ candidate_manifest_sha256:expectedCandidate.manifest_sha256,continuity_subject_sha256:capture.subject_sha256};
206
+ const restored=await store.append({plane,action:'transaction_restore',grantBytes:prepared.bytes,
207
+ grantInstanceNonce:prepared.instanceNonce,updateId:capture.update_id,expectedHeadSha256:head.event_sha256,restoreBinding});
208
+ const verified=await store.verifyGrant({plane,grantBytes:prepared.bytes,grantInstanceNonce:prepared.instanceNonce,policyBytes:successorPolicyBytes});
209
+ if(!verified.trusted||verified.event_sha256!==restored.event_sha256)throw new Error(`${plane} restored grant did not verify`);
210
+ outcomes[plane]=relation==='NARROWER'?'RESTORED_SAFE':'RESTORED_EXACT';restoredNonces[plane]=prepared.instanceNonce;
211
+ if(plane==='coding')codingRestored=true;
212
+ }catch(error){
213
+ const current=await store.readHead({plane}).catch(()=>null);
214
+ const currentConflict=classifyConflict(current,record,capture.update_id);
215
+ const cleanup=await removeIfExact(file,sha(prepared.bytes)).catch(()=>({reason:'cleanup-failed'}));
216
+ if(currentConflict){outcomes[plane]=currentConflict;reauth.push(plane);continue;}
217
+ if(cleanup.reason==='replacement'){outcomes[plane]='REPLACED';reauth.push(plane);continue;}
218
+ throw error;
219
+ }
220
+ }
221
+ return Object.freeze({authority_outcomes:Object.freeze(outcomes),restored_nonces:Object.freeze(restoredNonces),
222
+ reauth_planes:Object.freeze([...new Set(reauth)])});
223
+ }
224
+ });
225
+ }