@josephyulei/summon-foundation 0.1.2 → 0.1.4

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/README.md CHANGED
@@ -1,18 +1,22 @@
1
1
  # summon foundation
2
2
 
3
+ 0.1.4 提供获取期间只读进度页。获取成功后同一页面关联运行程序返回的目录/确认页,仍由 Codex 内置浏览器打开;配合 Foundation 0.2.10 展示本次流程、当前目标和剩余动作。临时服务只随本次任务运行,不是守护进程,不执行未核验归档,也不替本人确认。原操作回执仍可独立只读查询,页断开不改写已核实终态。
4
+
3
5
  Foundation 的薄获取入口,不包含完整运行包,没有 npm 安装生命周期脚本。
4
6
  只有明确执行才查询/获取;安装和 Skill 注册继续各自需要用户确认。
5
7
 
6
8
  可用性以 npm registry 的实际版本和公共 GitHub Release 为准;本目录或说明存在不是发布回执。只使用本项目已核验的包名,缺包时不换同名来源。
7
9
 
8
10
  ```text
9
- npx --yes --package @josephyulei/summon-foundation@0.1.2 summon foundation
11
+ npx --yes --package @josephyulei/summon-foundation@0.1.4 summon foundation
10
12
  ```
11
13
 
12
- 0.1.2 入口配合 Foundation 0.2.7 或后续支持目录选择的正式发行使用。默认命令实际查询、固定版本、披露缓存、下载并校验,然后保持运行等待安装页目录选择。选择目录后还要亲自确认精确安装计划;不会注册 Skill 或接入项目。实际获取仍须通过 registry 与 Release 校验,本文不替代发布证明。
14
+ 0.1.4 入口配合 Foundation 0.2.10 正式发行使用。默认命令实际查询、固定版本、披露缓存、下载并校验,然后保持运行等待安装页目录选择。选择目录后还要亲自确认精确安装计划;不会自动注册 Skill 或接入项目。实际获取仍须通过 registry 与 Release 校验,本文不替代发布证明。
13
15
 
14
16
  Codex 需要打开返回的内置浏览器网址,并分段等待同次操作结果。npm 不能自行操控或唤醒 Codex,对话主动告知不是程序退出码保证;新对话接续仍待真人验收。系统浏览器只是明确告知后的备用,不自动打开。
15
17
 
18
+ 0.1.3 的程序收尾:联网前建立同次操作记录,输出阶段、大文件实际字节/总量、尝试次数与最近进展;不能保存早期记录则不开始联网。大文件每次最多 10 分钟,连接 15 秒、低速 60 秒有界停止,仅暂时网络错误可重试一次。失败保留 curl 退出码和脱敏类别,校验失败与网络失败分开。`--status <返回的结果文件>` 只读恢复,不获取、安装或重试;中间记录不证明进程仍在运行。新运行版安装后程序核验稳定入口,启动唯一工作台至就绪便返回结果,不等常驻工作台退出;Codex 打开返回的实际网址并解释结果。旧 0.1.2 不含上述修复。工具返回 running 不是任务完成,运行中的 Codex 应继续观察原句柄;程序无法强制宿主不提前结束,新对话无追问反馈仍待验证。
19
+
16
20
  显式 `--inspect` 才只查询,不下载运行文件。已有选择时可用 `--prepare --version <固定版本> --destination <绝对目录>`;`--acquire --version <固定版本>` 只准备更新输入,不执行更新。更新使用已安装稳定入口。旧运行版不支持页面选择时拒绝自动采用默认目录。没有 npm 时先从 GitHub 安装说明检查环境,不自动安装全局依赖。
17
21
 
18
22
  需要 macOS arm64、Node.js 22.9+ 与 npm。无 TTY 不会卡在终端提问;目录选择在页面进行,关闭页面不算确认或取消。
package/bin/summon.mjs CHANGED
@@ -2,18 +2,23 @@
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import {spawn,spawnSync} from 'node:child_process';
5
- import {inspectRelease,acquireRelease,plainPath} from '../lib/acquire.mjs';
5
+ import {inspectRelease,plainPath} from '../lib/acquire.mjs';
6
6
 
7
+ import {acquireInWorker} from '../lib/acquisition-task.mjs';
8
+ import {startProgressPage} from '../lib/progress-page.mjs';
7
9
  import {parseSummonArgs} from '../lib/cli-options.mjs';
10
+ import {createOperation,saveOperation,readOperation,runtimeObservation} from '../lib/operation-result.mjs';
8
11
 
9
12
  const help=`summon foundation [--version x.y.z] [--destination /absolute/folder]
10
13
  summon foundation --inspect [--version x.y.z]
11
14
  summon foundation --prepare --version x.y.z --destination /absolute/folder
12
15
  summon foundation --acquire --version x.y.z
16
+ summon foundation --status /absolute/acquisition/operation-result.json
13
17
  默认命令下载并验证正式发行,进入安装准备;未给目录时在安装页选择。不会静默安装或注册 Skill。
14
18
  --inspect 才只读查询发行,不下载运行归档。
15
19
  --prepare 下载并核验固定发行,打开 Foundation 本人确认流程;不会代替确认。
16
20
  --acquire 仅下载并核验更新材料,返回候选与回执;不启动安装或执行更新。
21
+ --status 只读本次结果记录;中间记录不证明进程仍在运行,不重试或重放确认。
17
22
  仅 macOS arm64。npm 入口需要 Node/npm;已安装 Foundation 使用随包运行时。
18
23
  普通终端不能控制 Codex 对话或内置浏览器。无需 npm 的引导见公共仓库安装说明。`;
19
24
  function fail(message){throw Error(message);}
@@ -34,36 +39,72 @@ function stageDirectory(){
34
39
  const stat=fs.lstatSync(cache);if(stat.uid!==process.getuid()||(stat.mode&0o777)!==0o700)fail('获取缓存必须由当前用户独占 0700;不会自动 chmod 已有目录');
35
40
  return fs.mkdtempSync(path.join(cache,'acquisition.'));
36
41
  }
42
+ let operation=null,stage=null,progressPage=null;
43
+ function updateOperation(patch){if(!operation)return;Object.assign(operation,patch,{updatedAt:new Date().toISOString()});const file=saveOperation(operation,stage);if(file)operation.resultFile=file;progressPage?.publish();console.log(JSON.stringify({status:'FOUNDATION_INSTALL_OPERATION',...operation,resultFile:file,recordSaved:!!file}));}
44
+ async function openInstalledWorkbench(root,env){
45
+ let child;
46
+ try{
47
+ plainPath(root);const launcher=path.join(root,'bin/foundation-kit');plainPath(launcher);
48
+ const stdout=path.join(stage,'workbench.stdout.log'),stderr=path.join(stage,'workbench.stderr.log');
49
+ const out=fs.openSync(stdout,'wx',0o600);let err;
50
+ try{err=fs.openSync(stderr,'wx',0o600);child=spawn(launcher,['workbench','open','--root',root],{detached:true,stdio:['ignore',out,err],env});}finally{fs.closeSync(out);if(err!==undefined)fs.closeSync(err);}
51
+ await new Promise((resolve,reject)=>{child.once('spawn',resolve);child.once('error',reject);});child.unref();
52
+ for(let n=0;n<100;n++){
53
+ if(child.exitCode!==null)throw Error('工作台进程提前退出');
54
+ let ready;try{ready=JSON.parse(fs.readFileSync(stdout,'utf8'));}catch{}
55
+ if(ready?.url){const url=new URL(ready.url);if(url.protocol!=='http:'||url.hostname!=='127.0.0.1'||url.username||url.password)throw Error('工作台网址无效');const response=await fetch(url,{signal:AbortSignal.timeout(5000),redirect:'error'});if(!response.ok)throw Error('工作台未就绪');return{state:'ready',url:url.href,pid:child.pid,requiredHostAction:'open-returned-loopback-url-in-codex-browser'};}
56
+ await new Promise(r=>setTimeout(r,100));
57
+ }
58
+ throw Error('工作台启动超时');
59
+ }catch{if(child&&child.exitCode===null)child.kill('SIGTERM');return{state:'not-ready',reason:'安装结果保留;工作台未能就绪,可从已安装入口重新打开'};}
60
+ }
37
61
  try{
38
62
  const args=parseSummonArgs(process.argv.slice(2));
39
63
  if(args.help){console.log(help);process.exit(0);}
64
+ if(args.status){console.log(JSON.stringify(readOperation(args.status),null,2));process.exit(0);}
40
65
  const [major,minor]=process.versions.node.split('.').map(Number);
41
66
  if(major<22||(major===22&&minor<9))fail('获取入口需要 Node.js 22.9 或以上;尚未安装,不自动安装依赖');
42
67
  if(process.platform!=='darwin'||process.arch!=='arm64')fail('当前仅支持 macOS arm64;尚未安装');
43
68
  if(args.prepare||args.acquire)sourceGuard();
69
+ if(args.prepare||args.acquire){operation=createOperation();stage=stageDirectory();updateOperation({});if(!saveOperation(operation,stage))fail('无法在已披露的获取目录保存操作记录;尚未联网或安装');}
44
70
  if(args.prepare)console.log('正在准备 Foundation 安装:将查询并固定本次正式版本,说明缓存位置后下载核验。未选目录会在安装页选择;之后仍需本人确认精确计划。请保持当前 Codex 任务等待并打开返回的内置浏览器网址;不会自动改用系统浏览器。');
45
- const context=inspectRelease(args.version);
46
- const guide=`https://github.com/${context.repository.full_name}/blob/${context.documentationCommit}/docs/install-with-codex.md`;
47
71
  if(!args.prepare&&!args.acquire){
72
+ const context=inspectRelease(args.version);
73
+ const guide=`https://github.com/${context.repository.full_name}/blob/${context.documentationCommit}/docs/install-with-codex.md`;
48
74
  console.log(JSON.stringify({schemaVersion:'1.0.0',status:'RELEASE_DISCOVERED_NOT_ACQUIRED',product:'Foundation',version:context.version,sourceCommit:context.sourceCommit,documentationCommit:context.documentationCommit,repository:context.repository.full_name,guide,installationPerformed:false,skillRegistered:false,
49
75
  conversationNextStep:'本次 --inspect 仅查询;未下载、未安装,不自动接续写入。需要安装时由用户发起默认安装入口,并核对当前任务权限。',
50
76
  terminalBoundary:'普通终端无法创建或唤醒 Codex 对话;请在有相应权限的 Codex 任务发起安装。',
51
77
  acquisition:'尚未下载或验证运行归档。默认入口或 --prepare 执行匿名 GitHub 证明与完整性核验。'},null,2));
52
78
  }else{
53
- if(args.destination)plainPath(args.destination);const stage=stageDirectory();console.log(`本次独占获取目录:${stage}`);
54
- const receipt=await acquireRelease(context,stage);console.log(JSON.stringify({status:'GITHUB_ACQUISITION_VERIFIED',...receipt,destinationIntent:args.destination}));
55
- if(args.acquire){console.log(JSON.stringify({status:'UPDATE_INPUT_READY_NOT_APPLIED',candidate:path.dirname(receipt.launcher),receipt:path.join(stage,'acquisition.json'),next:'使用当前健康安装的稳定入口生成独立更新计划;本人确认后才更新。获取回执不是删除授权。'}));process.exit(0);}
79
+ if(args.destination)plainPath(args.destination);console.log(`本次独占获取目录:${stage}`);
80
+ progressPage=await startProgressPage(()=>operation);
81
+ updateOperation({progressUrl:progressPage.url});
82
+ console.log(JSON.stringify({status:'FOUNDATION_ACQUISITION_PROGRESS_PAGE',url:progressPage.url,operationId:operation.operationId,next:'在 Codex 内置浏览器打开同次只读进度页,保持任务等待;不会自动打开系统浏览器。'}));
83
+ const {context,receipt}=await acquireInWorker({version:args.version,stage,operationId:operation.operationId,onContext:context=>updateOperation({version:context.version,sourceCommit:context.sourceCommit}),onProgress:download=>updateOperation({download}),onPhase:phase=>updateOperation({phase})});console.log(JSON.stringify({status:'GITHUB_ACQUISITION_VERIFIED',...receipt,destinationIntent:args.destination}));
84
+ updateOperation({phase:'acquired',version:context.version,sourceCommit:context.sourceCommit});
85
+ if(args.acquire){updateOperation({state:'update-input-ready',terminal:true});console.log(JSON.stringify({status:'UPDATE_INPUT_READY_NOT_APPLIED',candidate:path.dirname(receipt.launcher),receipt:path.join(stage,'acquisition.json'),next:'使用当前健康安装的稳定入口生成独立更新计划;本人确认后才更新。获取回执不是删除授权。'}));await progressPage.close();progressPage=null;process.exit(0);}
56
86
  const env={...process.env};for(const key of ['NODE_OPTIONS','NODE_PATH','NODE_V8_COVERAGE','NODE_REDIRECT_WARNINGS','NODE_COMPILE_CACHE','NODE_COMPILE_CACHE_PORTABLE','NODE_PRESERVE_SYMLINKS'])delete env[key];
87
+ const supported=spawnSync(receipt.launcher,['--help'],{encoding:'utf8',env,timeout:15000});
57
88
  if(!args.destination){
58
- const supported=spawnSync(receipt.launcher,['onboarding','--help'],{encoding:'utf8',env,timeout:15000});
59
89
  if(supported.status!==0||!supported.stdout.includes('--choose-destination'))fail('本次发行尚不支持页面选择目录;未启动安装。请由当前对话取得目录后使用明确的 --destination,或等待新版发行,不静默采用默认位置');
60
90
  }
61
- const child=spawn(receipt.launcher,['install',...(args.destination?['--destination',args.destination]:['--choose-destination']),'--browser','codex'],{stdio:'inherit',env});
91
+ updateOperation({phase:'starting-confirmation-service',installationWrites:'unknown'});
92
+ const child=spawn(receipt.launcher,['install',...(!args.destination&&supported.stdout?.includes('--journey-id')?['--journey-id',operation.operationId]:[]),...(args.destination?['--destination',args.destination]:['--choose-destination']),'--browser','codex'],{stdio:['inherit','pipe','inherit'],env});
93
+ child.stdout.setEncoding('utf8');
94
+ let pending='',json='';
95
+ child.stdout.on('data',bytes=>{process.stdout.write(bytes);pending+=bytes.toString();if(pending.length>1_000_000){pending='';json='';return;}let end;while((end=pending.indexOf('\n'))>=0){const line=pending.slice(0,end);pending=pending.slice(end+1);if(!json&&!line.trim().startsWith('{'))continue;json+=line+'\n';if(json.length>1_000_000){json='';continue;}let e;try{e=JSON.parse(json)}catch{continue}json='';
96
+ const observed=runtimeObservation(e);if(observed)updateOperation(observed);
97
+ }});
62
98
  const interrupt=signal=>{child.kill(signal);};
63
99
  const int=()=>interrupt('SIGINT'),term=()=>interrupt('SIGTERM');
64
100
  process.once('SIGINT',int);process.once('SIGTERM',term);
65
101
  const ended=await new Promise((resolve,reject)=>{child.once('error',reject);child.once('close',(code,signal)=>resolve({code,signal}));}).finally(()=>{process.off('SIGINT',int);process.off('SIGTERM',term);});
66
102
  if(ended.signal||ended.code===null)fail(`确认服务未正常返回;结果待核实,保留 ${stage},不要自动重试安装`);
103
+ if(!operation.terminal)updateOperation({state:'verification-required',terminal:true,childExitCode:ended.code,next:'服务退出不等于成功或取消;只读核验原 session 与 installed 状态,不自动重试'});
104
+ if(operation.state==='completed'&&operation.installationRoot){
105
+ const workbench=await openInstalledWorkbench(operation.installationRoot,env);
106
+ updateOperation({phase:'finished',workbench,next:workbench.state==='ready'?'在 Codex 内置浏览器打开已就绪工作台;Skill 与项目仍未接入,按用户偏好另行询问':'安装已完成,但工作台未就绪;保留安装结果并只读检查'});
107
+ }
67
108
  process.exitCode=ended.code;
68
109
  }
69
- }catch(e){console.error(`错误:${e.message}`);process.exitCode=1;}
110
+ }catch(e){updateOperation({state:operation&&operation.installationWrites!=='none'?'verification-required':'failed',terminal:true,errorCode:['ACQUISITION_TRANSPORT_FAILED','ACQUISITION_VALIDATION_FAILED'].includes(e.code)?e.code:'INSTALL_ENTRY_FAILED',diagnostic:e.diagnostic||null,next:'先核验旧工具进程已结束及同次结果;保留材料,不自动重试或重放安装确认'});const message=String(e.message).replace(/https?:\/\/\S+/g,'[已脱敏网址]').replace(/(?:github_pat_|ghp_|npm_)[A-Za-z0-9_]+/g,'[已脱敏凭据]');console.error(`错误:${message}`);process.exitCode=1;}finally{if(progressPage)await progressPage.close();}
package/lib/acquire.mjs CHANGED
@@ -6,26 +6,54 @@ import {Updater} from 'tuf-js';
6
6
  import snappy from 'snappyjs';
7
7
  import {verifyReleaseProof} from './release-proof.mjs';
8
8
  import policy from './public-policy.json' with {type:'json'};
9
+ import {readOperation} from './operation-result.mjs';
9
10
 
10
11
  const hash = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
11
- const fail = message => { throw new Error(`错误:${message}`); };
12
+ const fail = message => { throw Object.assign(new Error(`错误:${message}`),{code:'ACQUISITION_VALIDATION_FAILED'}); };
12
13
  const semver = /^\d+\.\d+\.\d+$/;
13
14
  const json = bytes => JSON.parse(bytes.toString('utf8'));
14
15
  const canonical = value => Array.isArray(value) ? `[${value.map(canonical).join(',')}]` : value && typeof value==='object' ? `{${Object.keys(value).sort().map(k=>`${JSON.stringify(k)}:${canonical(value[k])}`).join(',')}}` : JSON.stringify(value);
16
+ export function downloadFailure(result,hostname) {
17
+ const code=Number.isInteger(result.status)?result.status:null;
18
+ const category=({5:'proxy-dns',6:'dns',7:'connection',22:'http',28:'timeout-or-low-speed',35:'tls',60:'tls-certificate'})[code] || (result.signal?'terminated':'transport');
19
+ // Do not retain stderr: redirects, proxy credentials and signed query URLs may occur there.
20
+ const error=Error(`匿名获取失败(${hostname});类别 ${category},curl 退出码 ${code ?? '未知'};未执行安装`);
21
+ error.code='ACQUISITION_TRANSPORT_FAILED';error.diagnostic={category,curlExitCode:code,signal:['SIGINT','SIGTERM','SIGKILL'].includes(result.signal)?result.signal:null,host:hostname};return error;
22
+ }
15
23
  // Public transport never reads gh credentials or curl configuration. Proxy
16
24
  // environment remains optional; neither Mono nor an author path is packaged.
17
- function download(url, limit=10_000_000, largeAsset=false) {
25
+ function download(url, limit=10_000_000) {
18
26
  const u = new URL(url);
19
27
  if (u.protocol !== 'https:' || u.username || u.password) fail('只接受无凭证 HTTPS 地址');
20
28
  const env = {PATH:'/usr/bin:/bin'};
21
29
  for (const key of ['HTTPS_PROXY','HTTP_PROXY','ALL_PROXY','NO_PROXY','https_proxy','http_proxy','all_proxy','no_proxy','TMPDIR']) if (process.env[key]) env[key]=process.env[key];
22
- // Large immutable assets may take minutes on a constrained proxy. Keep a
23
- // bounded transfer, fail stalled connections, and never execute partial data.
24
- const r=spawnSync('/usr/bin/curl',['-q',...(largeAsset?['--http1.1','--speed-limit','1024','--speed-time','60']:[]),'--fail','--silent','--show-error','--location','--max-redirs','4','--proto','=https','--proto-redir','=https','--connect-timeout','15','--max-time',largeAsset?'1200':'120',url],{env,maxBuffer:limit});
25
- if(r.status!==0 || r.error) fail(`匿名获取失败(${u.hostname});保留本次材料,不自动重试安装`);
30
+ const r=spawnSync('/usr/bin/curl',['-q','--fail','--silent','--show-error','--location','--max-redirs','4','--proto','=https','--proto-redir','=https','--connect-timeout','15','--max-time','120',url],{env,maxBuffer:limit});
31
+ if(r.status!==0 || r.error) throw downloadFailure(r,u.hostname);
26
32
  return r.stdout;
27
33
  }
28
34
  function api(endpoint) { return json(download(`https://api.github.com/repos/${policy.repository}/${endpoint}`)); }
35
+ async function downloadLarge(url,total,onProgress) {
36
+ const {spawn}=await import('node:child_process');
37
+ const env={PATH:'/usr/bin:/bin'};for(const k of ['HTTPS_PROXY','HTTP_PROXY','ALL_PROXY','NO_PROXY','https_proxy','http_proxy','all_proxy','no_proxy','TMPDIR'])if(process.env[k])env[k]=process.env[k];
38
+ // Retry acquisition only, never an installation. Each attempt has independent
39
+ // byte accounting and a hard bound; incomplete bytes are never executed.
40
+ for(let attempt=1;attempt<=2;attempt++){
41
+ let bytes=0,chunks=[],last=0,overflow=false;
42
+ onProgress({attempt,downloadedBytes:0,totalBytes:total,lastProgressAt:null});
43
+ const child=spawn('/usr/bin/curl',['-q','--http1.1','--speed-limit','1024','--speed-time','60','--fail','--silent','--show-error','--location','--max-redirs','4','--proto','=https','--proto-redir','=https','--connect-timeout','15','--max-time','600',url],{env,stdio:['ignore','pipe','pipe']});
44
+ const stop=signal=>child.kill(signal),int=()=>stop('SIGINT'),term=()=>stop('SIGTERM');process.once('SIGINT',int);process.once('SIGTERM',term);
45
+ let interrupted=null;const markInt=()=>interrupted='SIGINT',markTerm=()=>interrupted='SIGTERM';process.once('SIGINT',markInt);process.once('SIGTERM',markTerm);
46
+ child.stderr.resume();
47
+ child.stdout.on('data',b=>{bytes+=b.length;if(bytes>total){overflow=true;child.kill('SIGTERM');return;}chunks.push(b);const now=Date.now();if(now-last>=1000){last=now;onProgress({attempt,downloadedBytes:bytes,totalBytes:total,lastProgressAt:new Date(now).toISOString()});}});
48
+ const ended=await new Promise(resolve=>{child.once('error',error=>resolve({status:null,error}));child.once('close',(status,signal)=>resolve({status,signal}));});
49
+ process.off('SIGINT',int);process.off('SIGTERM',term);process.off('SIGINT',markInt);process.off('SIGTERM',markTerm);
50
+ onProgress({attempt,downloadedBytes:bytes,totalBytes:total,lastProgressAt:last?new Date(last).toISOString():null});
51
+ if(overflow)fail('下载超出已绑定资产长度;拒绝执行');
52
+ if(ended.status===0&&!interrupted)return Buffer.concat(chunks);
53
+ const error=downloadFailure({...ended,signal:interrupted||ended.signal},new URL(url).hostname);
54
+ if(attempt===2||interrupted||![5,6,7,28,52,56].includes(ended.status))throw error;
55
+ }
56
+ }
29
57
  function regular(file) { const s=fs.lstatSync(file); if(!s.isFile()||s.isSymbolicLink())fail('不是普通文件');return s; }
30
58
  export function plainPath(file) {
31
59
  if(!path.isAbsolute(file)||path.normalize(file)!==file)fail('路径必须是规范化绝对路径');
@@ -63,9 +91,9 @@ async function trustRoot(stage) {
63
91
  await updater.refresh();const info=await updater.getTargetInfo('trusted_root.json');if(!info)fail('GitHub 信任目标缺失');
64
92
  return json(fs.readFileSync(await updater.downloadTarget(info)));
65
93
  }
66
- async function verifiedAsset(context,asset,stage,trust) {
94
+ async function verifiedAsset(context,asset,stage,trust,onProgress) {
67
95
  if(!asset||!Number.isSafeInteger(asset.size)||asset.size<=0||asset.size>250_000_000||!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(asset.name)||!/^sha256:[a-f0-9]{64}$/.test(asset.digest)||asset.browser_download_url!==`https://github.com/${policy.repository}/releases/download/${context.release.tag_name}/${asset.name}`)fail('资产元数据无效');
68
- const bytes=download(asset.browser_download_url,asset.size+1,asset.size>10_000_000);
96
+ const bytes=asset.size>10_000_000?await downloadLarge(asset.browser_download_url,asset.size,onProgress):download(asset.browser_download_url,asset.size+1);
69
97
  if(bytes.length!==asset.size||`sha256:${hash(bytes)}`!==asset.digest)fail('下载长度或摘要不匹配');
70
98
  const proofs=api(`attestations/${asset.digest}?predicate_type=release&per_page=100`).attestations;
71
99
  if(!Array.isArray(proofs)||proofs.length>=100)fail('证明列表不完整或分页待核实');
@@ -107,20 +135,25 @@ function validateCandidate(root,release,sourceCommit) {
107
135
  // stage is always created by the CLI from the OS account in production.
108
136
  // Keeping this module callable allows contained engineering validation without
109
137
  // adding an install/cache override to the user-facing executable.
110
- export async function acquireRelease(context,stage) {
111
- plainPath(stage);const s=fs.lstatSync(stage);if(!s.isDirectory()||(s.mode&0o777)!==0o700||s.uid!==process.getuid()||fs.readdirSync(stage).length)fail('获取目录必须是当前用户的新建独占空目录');
138
+ export async function acquireRelease(context,stage,{onPhase=()=>{},onProgress=()=>{},operationId=null}={}) {
139
+ plainPath(stage);const s=fs.lstatSync(stage),entries=fs.readdirSync(stage);if(!s.isDirectory()||(s.mode&0o777)!==0o700||s.uid!==process.getuid())fail('获取目录必须是当前用户的新建独占空目录');
140
+ if(entries.length){if(entries.length!==1||entries[0]!=='operation-result.json'||!operationId)fail('获取目录包含未知内容');const r=readOperation(path.join(stage,entries[0]));if(r.operationId!==operationId||r.terminal||r.phase!=='discovering')fail('获取目录不是本次早期记录');}
112
141
  const beforeTmp=process.env.TMPDIR;process.env.TMPDIR=stage;
113
142
  try{
143
+ onPhase('verifying-trust');
114
144
  const trust=await trustRoot(stage);
115
145
  const name=`foundation-release-${context.version}-macos-arm64.json`;
116
146
  const catalogs=context.release.assets.filter(x=>x.name===name);if(catalogs.length!==1)fail('发行清单缺失或重复');
147
+ onPhase('fetching-and-verifying-catalog');
117
148
  const catalog=json((await verifiedAsset(context,catalogs[0],stage,trust)).bytes);
118
149
  const releases=catalog.releases?.filter(x=>x.version===context.version&&x.platform==='darwin'&&x.arch==='arm64');
119
150
  if(catalog.schemaVersion!=='1.0.0'||catalog.product!=='ai-product-foundation-kit'||catalog.keyId!==null||!Number.isSafeInteger(catalog.issuedAt)||catalog.issuedAt>Date.now()||!Number.isSafeInteger(catalog.expiresAt)||catalog.expiresAt<=Date.now()||catalog.expiresAt<=catalog.issuedAt||releases?.length!==1)fail('发行清单身份、有效期或架构不匹配');
120
151
  const release=releases[0];const assetName=`foundation-${release.sha256}.tar.gz`;
121
152
  if(release.repositoryId!==policy.repositoryId||release.sourceCommit!==context.sourceCommit||!/^[a-f0-9]{64}$/.test(release.candidateHash)||release.url!==`https://github.com/${policy.repository}/releases/download/${context.release.tag_name}/${assetName}`||!/^\d+\.\d+\.\d+$/.test(release.runtime?.version)||release.runtime.url!==`https://nodejs.org/dist/v${release.runtime.version}/node-v${release.runtime.version}-darwin-arm64.tar.gz`)fail('清单不能更换仓库、提交或官方运行时来源');
122
153
  const assets=context.release.assets.filter(x=>x.name===assetName&&x.size===release.bytes&&x.digest===`sha256:${release.sha256}`);if(assets.length!==1)fail('归档身份不匹配');
123
- const archive=await verifiedAsset(context,assets[0],stage,trust);
154
+ onPhase('fetching-and-verifying-runtime');
155
+ const archive=await verifiedAsset(context,assets[0],stage,trust,onProgress);
156
+ onPhase('extracting-and-validating');
124
157
  const listing=spawnSync('/usr/bin/tar',['-tzf',archive.file],{encoding:'utf8',maxBuffer:10_000_000});
125
158
  const types=spawnSync('/usr/bin/tar',['-tvzf',archive.file],{encoding:'utf8',maxBuffer:10_000_000});
126
159
  if(listing.status!==0||types.status!==0||listing.stdout.trim().split('\n').some(p=>p.startsWith('/')||p.includes('\\')||p.split('/').includes('..'))||types.stdout.trim().split('\n').some(p=>!['d','-'].includes(p[0])||/[sStT]/.test(p.slice(1,10))))fail('归档包含不安全路径、链接或特殊权限');
@@ -0,0 +1,24 @@
1
+ import {Worker} from 'node:worker_threads';
2
+
3
+ export function acquireInWorker({version,stage,operationId,onContext=()=>{},onPhase=()=>{},onProgress=()=>{}}) {
4
+ return new Promise((resolve,reject)=>{
5
+ const worker=new Worker(new URL('./acquisition-worker.mjs',import.meta.url),{workerData:{version,stage,operationId}});
6
+ let receipt,context,failure;
7
+ const interrupt=()=>{failure=Object.assign(Error('获取任务被中断;保留本次材料'),{code:'ACQUISITION_INTERRUPTED'});void worker.terminate();};
8
+ process.once('SIGINT',interrupt);process.once('SIGTERM',interrupt);
9
+ worker.on('message',event=>{
10
+ if(event.type==='context'){context=event.context;onContext(context);}
11
+ else if(event.type==='phase')onPhase(event.phase);
12
+ else if(event.type==='progress')onProgress(event.download);
13
+ else if(event.type==='complete')receipt=event.receipt;
14
+ else if(event.type==='failure')failure=Object.assign(Error(event.message),{code:event.code,diagnostic:event.diagnostic});
15
+ });
16
+ worker.on('error',error=>{failure=error;});
17
+ worker.once('exit',code=>{
18
+ process.off('SIGINT',interrupt);process.off('SIGTERM',interrupt);
19
+ if(failure)reject(failure);
20
+ else if(code!==0||!receipt||!context)reject(Error('获取进程未提供已验证结果;不启动运行文件'));
21
+ else resolve({context,receipt});
22
+ });
23
+ });
24
+ }
@@ -0,0 +1,17 @@
1
+ // Trusted npm code only. Network/proof routines contain synchronous calls;
2
+ // isolate those calls so the read-only progress server remains responsive.
3
+ import {parentPort, workerData} from 'node:worker_threads';
4
+ import {inspectRelease, acquireRelease} from './acquire.mjs';
5
+
6
+ try {
7
+ const context = inspectRelease(workerData.version);
8
+ parentPort.postMessage({type:'context',context});
9
+ const receipt = await acquireRelease(context, workerData.stage, {
10
+ operationId:workerData.operationId,
11
+ onPhase:phase=>parentPort.postMessage({type:'phase',phase}),
12
+ onProgress:download=>parentPort.postMessage({type:'progress',download}),
13
+ });
14
+ parentPort.postMessage({type:'complete',receipt});
15
+ } catch(error) {
16
+ parentPort.postMessage({type:'failure',code:error.code,diagnostic:error.diagnostic||null,message:String(error.message).replace(/https?:\/\/\S+/g,'[已脱敏网址]').replace(/(?:github_pat_|ghp_|npm_)[A-Za-z0-9_]+/g,'[已脱敏凭据]')});
17
+ }
@@ -8,10 +8,11 @@ export function parseSummonArgs(args){
8
8
  if(flag==='--prepare')result.prepare=true;
9
9
  else if(flag==='--acquire')result.acquire=true;
10
10
  else if(flag==='--inspect')result.inspect=true;
11
- else if(flag==='--version'||flag==='--destination'){if(!args.length||args[0].startsWith('--'))fail('参数缺值');result[flag.slice(2)]=args.shift();}
11
+ else if(flag==='--version'||flag==='--destination'||flag==='--status'){if(!args.length||args[0].startsWith('--'))fail('参数缺值');result[flag.slice(2)]=args.shift();}
12
12
  else fail('未知参数;请查看 --help');
13
13
  }
14
14
  if((result.prepare||result.acquire||result.destination)&&result.inspect||result.prepare&&result.acquire||result.destination&&result.acquire)fail('参数组合不支持');
15
+ if(seen.has('--status')){if(!result.status||seen.size!==1)fail('结果查询需要非空路径且不能与其他操作组合');return result;}
15
16
  if(!result.inspect&&!result.acquire)result.prepare=true;
16
17
  if(result.acquire&&!result.version)fail('获取更新材料前须固定版本');
17
18
  return result;
@@ -0,0 +1,12 @@
1
+ // Presentation only. Planned steps never grant execution authority.
2
+ export function journeyView({id,kind='install',observations={},skillChoice='undecided',includeAcquisition=false,includeSelection=false,includeHealth=true,includeCleanup=false,includeSkillRemoval=false}) {
3
+ const labels={acquire:'获取并核验运行文件',select:'选择安装目录',program:({install:'安装程序',update:'更新程序',uninstall:'移除程序'})[kind]||'执行当前维护',health:'验证稳定入口',cleanup:'处理本次暂存',material:'准备对话能力材料',register:'注册到 Codex',remove:'移除 Codex 注册'};
4
+ const statusLabels={completed:'已完成',pending:'待确认',planned:'尚待处理',running:'进行中',executing:'进行中',consumed:'进行中',failed:'失败',cancelled:'已取消',expired:'已过期',skipped:'已跳过',undecided:'可选,待选择',retained:'已处理,保留部分材料','verification-required':'待核实'};
5
+ const keys=[...(includeAcquisition?['acquire']:[]),...(includeSelection?['select']:[]),...(includeSkillRemoval?['remove']:[]),'program',...(includeHealth?['health']:[]),...(includeCleanup?['cleanup']:[])];
6
+ if(kind!=='uninstall'&&(observations.material||observations.register||skillChoice==='selected'))keys.push('material','register');else if(kind!=='uninstall')keys.push('optional-skill');
7
+ const steps=keys.map(key=>{const supplied=observations[key],state=key==='optional-skill'?(skillChoice==='skipped'?'skipped':'undecided'):supplied?.state||'planned';return{id:`${id}:${key}`,key,title:labels[key]||'Codex 对话能力',state,label:statusLabels[state]||'待核实',evidence:supplied?.evidence||null,required:key!=='optional-skill'};});
8
+ const unresolved=steps.filter(s=>s.required&&!['completed','skipped','retained'].includes(s.state)),name=({install:'安装',update:'更新',uninstall:'卸载'})[kind]||'维护';
9
+ const program=observations.program?.state==='completed',trouble=unresolved.find(s=>['failed','cancelled','expired','verification-required'].includes(s.state));
10
+ const title=!unresolved.length?`${name}完成`:program?`程序已完成,${(trouble||unresolved[0]).title}${trouble?'未完成':'仍待处理'}`:trouble?`${name}未完成`:`${name}流程`;
11
+ return {schemaVersion:'1.1.0',id,kind,title,ended:unresolved.length===0,steps,remaining:unresolved.map(s=>s.title),summary:unresolved.length?`尚余:${unresolved.map(s=>s.title+'('+s.label+')').join('、')}`:kind==='uninstall'?'本次卸载步骤已完成。请查看卸载回执中的实际删除范围、保留文件与清理说明。':skillChoice==='undecided'&&!observations.register?'已确定操作已完成;对话能力可选,尚未选择。项目接入独立。':'本次已选择步骤均已处理;新任务发现及项目接入另验。',executionAuthority:false};
12
+ }
@@ -0,0 +1,38 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import crypto from 'node:crypto';
4
+
5
+ export function createOperation() {
6
+ return {schemaVersion:'1.0.0',operationId:crypto.randomUUID(),pid:process.pid,startedAt:new Date().toISOString(),phase:'discovering',state:'running',terminal:false,installationWrites:'none',skillRegistered:false};
7
+ }
8
+ // Observations from the verified runtime, never confirmation or execution authority.
9
+ export function runtimeObservation(event) {
10
+ let confirmationUrl;
11
+ if(event.url){const url=new URL(event.url);if(url.protocol==='http:'&&url.hostname==='127.0.0.1'&&!url.username&&!url.password)confirmationUrl=url.href;}
12
+ if(event.status==='AWAITING_FOUNDATION_DIRECTORY_SELECTION')return {phase:'selecting-directory',selectionId:event.selectionId,journeyContext:event.journeyContext,installationWrites:'none',...(confirmationUrl?{confirmationUrl}:{})};
13
+ if(['FOUNDATION_SELECTION_BOUND','AWAITING_FOUNDATION_UI_CONFIRMATION'].includes(event.status))return {phase:'awaiting-confirmation',...(event.journeyContext?{journeyContext:event.journeyContext}:{}),sessionId:event.sessionId,planHash:event.planHash,installationWrites:'none',...(confirmationUrl?{confirmationUrl}:{})};
14
+ if(event.status==='FOUNDATION_OPERATION_STATE'&&['executing','consumed'].includes(event.state))return {phase:'executing',sessionId:event.sessionId,installationWrites:'possible'};
15
+ if(event.status==='BOOTSTRAP_OPERATION_ENDED'){
16
+ const state=['completed','failed','cancelled','expired'].includes(event.state)?event.state:'verification-required';
17
+ return {phase:'runtime-ended',state,terminal:state!=='verification-required',sessionId:event.sessionId,installationRoot:event.installationRoot,installationWrites:state==='completed'?'runtime-reported-installed':'unknown',runtimeHealth:event.result?.stableLauncherHealth||event.result?.lifecycleResult?.stableLauncherHealth||event.result?.executableHealth||'unknown',journeyContext:event.journeyContext,next:'只读核验稳定 installed launcher 或同次 session 结果,不重放确认'};
18
+ }
19
+ if(['cancelled-no-install','expired-no-install','shutdown-no-install'].includes(event.state))return {phase:'directory-selection-ended',state:event.state,terminal:true,installationWrites:'none'};
20
+ return null;
21
+ }
22
+ export function saveOperation(record,stage) {
23
+ if(!stage)return null;
24
+ try{
25
+ for(let p=stage;p!==path.dirname(p);p=path.dirname(p)){if(fs.lstatSync(p).isSymbolicLink())throw Error('symlink');}
26
+ const s=fs.lstatSync(stage);if(!s.isDirectory()||s.uid!==process.getuid()||(s.mode&511)!==448)throw Error('private stage required');
27
+ const file=path.join(stage,'operation-result.json');
28
+ if(fs.existsSync(file)){const old=fs.lstatSync(file);if(!old.isFile()||old.isSymbolicLink()||old.nlink!==1||old.uid!==process.getuid()||(old.mode&511)!==384)throw Error('unknown record');const previous=JSON.parse(fs.readFileSync(file));if(previous.operationId!==record.operationId)throw Error('different operation');}
29
+ const tmp=path.join(stage,`.operation-${crypto.randomUUID()}.json`);fs.writeFileSync(tmp,JSON.stringify(record,null,2)+'\n',{flag:'wx',mode:0o600});fs.renameSync(tmp,file);return file;
30
+ }catch{return null;}
31
+ }
32
+ export function readOperation(file) {
33
+ if(!path.isAbsolute(file)||path.normalize(file)!==file)throw Error('结果路径必须为规范化绝对路径');
34
+ for(let p=file;p!==path.dirname(p);p=path.dirname(p)){if(fs.lstatSync(p).isSymbolicLink())throw Error('拒绝链接结果路径');}
35
+ const s=fs.lstatSync(file);if(!s.isFile()||s.size>1_000_000||s.uid!==process.getuid()||(s.mode&511)!==384)throw Error('结果文件类型、大小或归属无效');
36
+ const r=JSON.parse(fs.readFileSync(file));if(r.schemaVersion!=='1.0.0'||typeof r.operationId!=='string'||typeof r.terminal!=='boolean')throw Error('结果记录无效');
37
+ return {...r,...(!r.terminal?{state:'verification-required',note:'这是最后一次记录,不证明原进程仍活着。先查原工具句柄和同次记录;不自动重试。'}:{}),readOnly:true,executionAuthority:false};
38
+ }
@@ -0,0 +1,38 @@
1
+ import http from 'node:http';
2
+ import crypto from 'node:crypto';
3
+ import {journeyView} from './journey-view.mjs';
4
+
5
+ export function acquisitionProgress(record) {
6
+ const phases={discovering:'查询正式发行','verifying-trust':'核验发行来源','fetching-and-verifying-catalog':'获取并核验发行清单','fetching-and-verifying-runtime':'下载并核验运行文件','extracting-and-validating':'展开并检查文件','acquired':'获取验证完成','starting-confirmation-service':'准备目录选择与确认页','selecting-directory':'等待你选择目录','awaiting-confirmation':'等待你确认安装',executing:'正在安装','runtime-ended':'读取安装结果',finished:'安装结果已返回'};
7
+ const terminal=record.terminal;
8
+ const acquired=['acquired','starting-confirmation-service','selecting-directory','directory-selection-ended','awaiting-confirmation','executing','runtime-ended','finished'].includes(record.phase);
9
+ const observations={acquire:{state:acquired?'completed':terminal?'failed':'running',evidence:record.resultFile}};
10
+ if(record.phase==='selecting-directory')observations.select={state:'pending'};
11
+ if(record.phase==='directory-selection-ended')observations.select={state:record.state==='cancelled-no-install'?'cancelled':record.state==='expired-no-install'?'expired':'verification-required',evidence:record.selectionId};
12
+ if(['awaiting-confirmation','executing','runtime-ended','finished'].includes(record.phase))observations.select={state:'completed',evidence:record.selectionId};
13
+ if(record.phase==='awaiting-confirmation')observations.program={state:'pending',evidence:record.sessionId};
14
+ if(record.phase==='executing')observations.program={state:'running',evidence:record.sessionId};
15
+ if(['runtime-ended','finished'].includes(record.phase))observations.program={state:record.state,evidence:record.sessionId};
16
+ if(record.runtimeHealth==='passed')observations.health={state:'completed',evidence:record.sessionId};
17
+ const journey=journeyView({id:record.operationId,includeAcquisition:true,includeSelection:true,observations,skillChoice:record.journeyContext?.skillChoice||'undecided'});
18
+ return {journey,operationId:record.operationId,title:terminal?(record.state==='completed'?'安装结果已返回':record.state==='update-input-ready'?'更新材料已就绪':record.state==='failed'?'获取或安装失败':'本次结果需查看'):'正在准备 Foundation',phase:phases[record.phase]||'阶段待核实',state:record.state,version:record.version||null,download:record.download||null,program:record.installationWrites==='runtime-reported-installed'?'程序安装已返回成功,健康以记录为准':record.installationWrites==='none'?'尚未写入安装目录':'安装结果待核实',skill:record.skillRegistered===true?'已有注册记录;新对话发现待验证':'未自动注册,可选独立步骤',project:'未自动接入项目',next:record.terminal&&record.state==='completed'?(record.journeyContext?.skillChoice==='skipped'?'程序已完成,本次已跳过对话能力;需要时再单独接入。':'程序结果已返回;对话能力和项目按当前选择分别处理。'):record.next||'保持当前任务观察;页面选择目录后仍需本人确认。',nextUrl:record.terminal?(record.workbench?.state==='ready'?record.workbench.url:null):record.confirmationUrl||null,nextLabel:record.terminal?'打开已安装工作台':'继续到目录选择 / 本人确认页',resultFile:record.resultFile||null,terminal,diagnostic:record.diagnostic||null};
19
+ }
20
+
21
+ // A bounded, read-only loopback view over the existing in-memory operation.
22
+ // No POST, launch endpoint, confirmation authority or downloaded executable.
23
+ export async function startProgressPage(readRecord) {
24
+ const token=crypto.randomBytes(24).toString('hex'),base=`/${token}`,clients=new Set();
25
+ const html=`<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Foundation 获取与安装进度</title><style>body{font:16px/1.6 system-ui;max-width:760px;margin:24px auto;padding:0 20px;color:#18181b;background:#fafafa;overflow-wrap:anywhere}section{background:white;border:1px solid #e4e4e7;border-radius:12px;padding:16px;margin:16px 0}h1{font-size:24px}h2{font-size:18px}a{display:inline-block;padding:12px;border:1px solid #71717a;border-radius:8px;color:inherit}pre{white-space:pre-wrap}a:focus-visible,summary:focus-visible{outline:3px solid #2563eb}[hidden]{display:none}</style><body><h1>正在准备 Foundation</h1><section role="status" aria-live="polite"><h2 id="phase">正在读取同次操作</h2><div id="journey-steps"></div><p id="journey-summary"></p><p id="download"></p><p id="program"></p><p id="skill"></p><p id="project"></p><p id="next"></p><a id="continue" hidden>继续到目录选择 / 本人确认页</a><p id="connection"></p></section><p>此页面只读,不批准安装;程序、对话能力与项目分别确认。未知长度不显示百分比。临时服务结束后,可从原对话读取保留的操作结果。</p><details><summary>操作与恢复详情</summary><pre id="raw"></pre></details><script>let last=null;function show(p){last=p;document.querySelector('h1').textContent=p.journey.title;document.getElementById('journey-summary').textContent=p.journey.summary;const steps=document.getElementById('journey-steps');steps.replaceChildren();for(const step of p.journey.steps){const line=document.createElement('p');line.textContent=step.title+':'+step.label;steps.append(line);}for(const key of ['phase','program','skill','project','next'])document.getElementById(key).textContent=p[key]||'';const d=p.download;document.getElementById('download').textContent=d&&Number.isFinite(d.downloadedBytes)?'本次实际接收 '+d.downloadedBytes+' 字节'+(Number.isFinite(d.totalBytes)?' / '+d.totalBytes+' 字节':'(总长度未知)'):'';const link=document.getElementById('continue');link.hidden=!p.nextUrl;link.textContent=p.nextLabel;if(p.nextUrl)link.href=p.nextUrl;document.getElementById('raw').textContent=JSON.stringify(p,null,2);}const events=new EventSource(location.pathname+'/events');events.onmessage=e=>show(JSON.parse(e.data));events.onerror=()=>{events.close();document.getElementById('connection').textContent=last&&last.terminal?'临时进度服务已结束,上方保留最后收到的结果。':'连接已中断,当前状态待核实;不自动重试安装。'};window.addEventListener('pagehide',()=>events.close(),{once:true});</script></body></html>`;
26
+ let origin;
27
+ const current=()=>JSON.stringify(acquisitionProgress(readRecord()));
28
+ const server=http.createServer((req,res)=>{
29
+ if(req.method!=='GET'||req.headers.host!==origin||req.headers.origin&&req.headers.origin!==`http://${origin}`){res.writeHead(403).end();return;}
30
+ const headers={'cache-control':'no-store','x-content-type-options':'nosniff','content-security-policy':"default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'"};
31
+ if(req.url===base){res.writeHead(200,{...headers,'content-type':'text/html; charset=utf-8'}).end(html);}
32
+ else if(req.url===base+'/events'){res.writeHead(200,{...headers,'content-type':'text/event-stream'});res.write(`data: ${current()}\n\n`);clients.add(res);req.on('close',()=>clients.delete(res));}
33
+ else{res.writeHead(404).end();}
34
+ });
35
+ await new Promise((resolve,reject)=>{server.once('error',reject);server.listen(0,'127.0.0.1',resolve);});
36
+ origin=`127.0.0.1:${server.address().port}`;
37
+ return {url:`http://${origin}${base}`,publish(){for(const res of clients)res.write(`data: ${current()}\n\n`);},close(){for(const res of clients)res.end();clients.clear();return new Promise(resolve=>server.close(resolve));}};
38
+ }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@josephyulei/summon-foundation",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@josephyulei/summon-foundation",
9
- "version": "0.1.2",
9
+ "version": "0.1.4",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@sigstore/bundle": "4.0.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@josephyulei/summon-foundation",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Prepare a verified Foundation release for an explicitly confirmed Codex installation",
5
5
  "type": "module",
6
6
  "bin": {"summon": "bin/summon.mjs"},