@hanamesh/dsh-app-host 0.6.21 → 0.6.24

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 (39) hide show
  1. package/README.md +19 -15
  2. package/dist/client-ui.js +8 -16
  3. package/dist/dsh-compat.js +1 -1
  4. package/dist/dsh.d.ts +1 -2
  5. package/dist/dsh.js +5 -9
  6. package/dist/git-command-worker.js +12 -0
  7. package/dist/git-process.js +8 -3
  8. package/dist/git-worker.js +37 -7
  9. package/dist/install-target/suite.js +3 -3
  10. package/dist/installation-status.d.ts +0 -8
  11. package/dist/library/diagnostics.js +0 -1
  12. package/dist/library/github-target.js +1 -1
  13. package/dist/library/handoff.js +3 -11
  14. package/dist/library/install-target-http.js +39 -0
  15. package/dist/library/install.js +13 -60
  16. package/dist/library/installation-cli.js +41 -16
  17. package/dist/library/installation-command-worker.js +3 -2
  18. package/dist/library/installation-discovery.js +95 -0
  19. package/dist/library/installation-status.js +14 -13
  20. package/dist/library/installation-worker.js +72 -14
  21. package/dist/library/locate.js +4 -13
  22. package/dist/library/routes.js +2 -10
  23. package/dist/library/service.js +5 -5
  24. package/dist/library/suite.js +9 -10
  25. package/dist/node-runtime/service.js +4 -4
  26. package/dist/router/sources.js +5 -2
  27. package/dist/runtime.js +15 -1
  28. package/dist/store.js +12 -5
  29. package/dist/windows-job.js +30 -0
  30. package/dist/windows-job.ps1 +75 -0
  31. package/docs/APP_PACKAGE.md +1 -1
  32. package/docs/CONTRACT.md +7 -7
  33. package/docs/DSH_COMPATIBILITY.md +1 -1
  34. package/docs/INSTALLATION_STATUS.md +29 -18
  35. package/docs/INSTALL_TARGET.md +9 -5
  36. package/docs/LIBRARY.md +3 -14
  37. package/docs/LICENSES.json +207 -69
  38. package/docs/SECURITY.md +16 -1
  39. package/package.json +41 -27
@@ -1,4 +1,5 @@
1
1
  import { installationStatusCors, serveInstallationStatus } from './installation-status-http.js';
2
+ import {serveInstallTargetProbe} from './install-target-http.js';
2
3
  import { randomUUID } from 'node:crypto';
3
4
  import { AppHostError, requireCondition } from '../errors.js';
4
5
  import { controlOrigins } from '../control-origin.js';
@@ -33,16 +34,7 @@ export function createLibraryHttpHandler(library,{parentOrigin,transportOrigin,a
33
34
  await serveInstallationStatus(req,res,{read:library.installationStatus,maxBodyBytes:HANDOFF_BODY_MAX_BYTES,traceId,cors,send});return;
34
35
  }
35
36
  if(path==='/hanamesh/library/install-target'){
36
- // Match the website handoff policy; same-origin workspace and non-browser local reads remain available.
37
- const origin=req.headers.origin;
38
- if(origin!==undefined){
39
- requireCondition(origin===requestOrigin||origins.acceptsOrigin(origin)||library.handoffSettings().allowedOrigins.includes(origin),'ORIGIN_DENIED','Not available.',undefined,403);
40
- cors={'access-control-allow-origin':origin,vary:'Origin','access-control-allow-private-network':'true'};
41
- }
42
- if(req.method==='OPTIONS'){send(res,204,undefined,{...cors,'access-control-allow-methods':'GET'});return;}
43
- requireCondition(req.method==='GET','METHOD_NOT_ALLOWED','Discovery requires GET.',{},405);
44
- requireCondition(!url.search,'UNKNOWN_FIELDS','Discovery takes no parameters.');
45
- send(res,200,{...library.installTarget(),traceId},cors);return;
37
+ serveInstallTargetProbe(req,res,{allowedOrigins:[requestOrigin,origins.parent,...library.handoffSettings().allowedOrigins],read:()=>library.installTarget(),traceId,send});return;
46
38
  }
47
39
  const readOnly=req.method==='GET'&&READ_ROUTES.has(path);
48
40
  requireCondition(readOnly||(req.method==='POST'&&!['/hanamesh/library/installedPlugins','/hanamesh/library/target','/hanamesh/library/handoffs'].includes(path)),'METHOD_NOT_ALLOWED','Unsupported library method.',{},405);
@@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto';
2
2
  import { AppHostError, requireCondition, SerialQueue } from '../errors.js';
3
3
  import { loadCatalog, isApplication, itemKind, upgradeAvailable, validateManifestUrl } from './catalog.js';
4
4
  import { scanInstalled, scanInstalledPlugins, snapshotProfileDependencies, readProfileManifest } from './installed.js';
5
- import {packageInstallTarget,installTargetName,sameInstallTarget,catalogPackageTargets,pinnedGithubSpec,catalogInstallationAllowed} from './github-target.js';
5
+ import {packageInstallTarget,installTargetName,sameInstallTarget,catalogPackageTargets,pinnedGithubSpec} from './github-target.js';
6
6
  import {SUITE_BUNDLE,SUITE_TARGET,SUITE_PACKAGES,readSuiteStatus,CORE_SUITE_DECLARATION_MISSING,readSuiteSourceState} from './suite.js';
7
7
  import { PROTECTED_PACKAGES } from './install.js';
8
8
  import { handoffSettings } from './handoff.js';
@@ -80,11 +80,11 @@ export function createLibraryService({domain,host,config={},dataRoot,ledgerReade
80
80
  requireCondition(itemId===undefined||(text(itemId,160)&&!/[\u0000-\u001f\u007f]/.test(itemId)),'INVALID_INPUT','itemId is invalid.');
81
81
  requireCondition(packageName===undefined||(text(packageName,214)&&PACKAGE_NAME.test(packageName)),'INVALID_INPUT','packageName is invalid.');
82
82
  async function resolved(found,source){
83
- requireCondition(catalogInstallationAllowed(found),'NOT_INSTALLABLE','The catalog has not approved one-click installation.',{reason:found.oneClickInstall?.reason});
83
+ requireCondition(found.oneClickInstall?.allowed===true,'NOT_INSTALLABLE','The catalog has not approved one-click installation.',{reason:found.oneClickInstall?.reason??'fixed-install-target-required'});
84
84
  const targets=catalogPackageTargets(found),selected=targets.find(target=>expected?sameInstallTarget(target,expected):packageName===undefined||installTargetName(target)===packageName);
85
85
  requireCondition(!expected||selected,'CATALOG_CHANGED','The reviewed installation source or build permission changed. Review the current target again.',{},409);
86
- requireCondition(selected||(found.installMethods===undefined&&found.package?.registry==='npm'),'NOT_INSTALLABLE','This catalog entry has no fixed package installation target for the requested package.',{reason:'fixed-install-target-required'});
87
- const name=selected?installTargetName(selected):found.package.name;
86
+ requireCondition(selected,'NOT_INSTALLABLE','This catalog entry has no fixed package installation target for the requested package.',{reason:'fixed-install-target-required'});
87
+ const name=installTargetName(selected);
88
88
  requireCondition(!protectedPackage(name),'PACKAGE_DENIED','The HanaMesh suite is not managed through the market.',{},403);
89
89
  const rows=[...await installedPlugins(),...await installed()],row=rows.find(value=>value.packageName===name)??null;
90
90
  let upgrade=npmUpgradeAvailable(selected,row);
@@ -92,7 +92,7 @@ export function createLibraryService({domain,host,config={},dataRoot,ledgerReade
92
92
  const saved=await readProfileManifest(config.profileDir);
93
93
  upgrade=saved.dependencies[name]!==pinnedGithubSpec(selected.repo,selected.ref,selected.path)||row.active===false;
94
94
  }
95
- const item={...found,...(selected?{package:{registry:selected.source,name},installTarget:selected}:{}),installed:row,upgradeAvailable:upgrade};
95
+ const item={...found,package:{registry:selected.source,name},installTarget:selected,installed:row,upgradeAvailable:upgrade};
96
96
  return{contractVersion:INSTALL_TARGET_CONTRACT_VERSION,source,item:{...item,kind:itemKind(item)}};
97
97
  }
98
98
  const packageMatches=row=>row.package?.name===packageName||(row.installMethods??[]).some(method=>method.target&&(method.target.packageName??method.target.name)===packageName);
@@ -39,13 +39,11 @@ const exact=value=>typeof value==='string'&&semver.valid(value)===value;
39
39
  const digest=value=>typeof value==='string'&&/^[a-f0-9]{64}$/.test(value);
40
40
  const nullableText=value=>value===null||text(value);
41
41
  const invalid=()=>requireCondition(false,'CORE_SUITE_DECLARATION_INVALID','Core 套件声明无法用于精确安装。',{},503);
42
- function exactInstallCommand(value){
43
- if(value===null)return true;
44
- const prefix=`dsh plugin --profile desktop add --save-exact ${SUITE_BUNDLE}@`;
45
- return typeof value==='string'&&value.startsWith(prefix)&&exact(value.slice(prefix.length));
42
+ function validInstallation(value,version){
43
+ return object(value)&&value.name===SUITE_BUNDLE&&value.version===version&&text(value.command)&&Array.isArray(value.args)&&value.args.length>0&&value.args.every(text);
46
44
  }
47
45
  function validateDeclaration(value){
48
- if(!object(value)||value.schemaVersion!==1||!text(value.checkedAt)||!exactInstallCommand(value.installCommand)||!digest(value.declarationId)||!text(value.nodeVersion)||!nullableText(value.registry)||!(value.bundleVersion===null||exact(value.bundleVersion))||!['complete','incomplete','unknown'].includes(value.completeness))invalid();
46
+ if(!object(value)||value.schemaVersion!==1||!text(value.checkedAt)||!nullableText(value.installCommand)||!digest(value.declarationId)||!text(value.nodeVersion)||!nullableText(value.registry)||!(value.bundleVersion===null||exact(value.bundleVersion))||!['complete','incomplete','unknown'].includes(value.completeness))invalid();
49
47
  if(value.completeness==='complete'&&!exact(value.bundleVersion))invalid();
50
48
  const manifest=value.manifest;
51
49
  if(!object(manifest)||manifest.schemaVersion!==1||!text(manifest.id)||!object(manifest.bundle)||manifest.bundle.name!==SUITE_BUNDLE||!text(manifest.bundle.repository)||!object(manifest.core)||manifest.core.name!==SUITE_PACKAGES[0].name||!exact(manifest.core.version)||!text(manifest.core.repository)||!Array.isArray(manifest.components)||manifest.components.length!==2)invalid();
@@ -61,6 +59,7 @@ function validateDeclaration(value){
61
59
  if(!Object.values(value.peerFacts).every(nullableText)||!Object.values(value.metadataDigests).every(text))invalid();
62
60
  for(const upgrade of value.upgrades){
63
61
  if(!object(upgrade)||!object(upgrade.bundle)||!text(upgrade.bundle.id)||upgrade.bundle.moduleName!==SUITE_BUNDLE||!exact(upgrade.bundle.version)||!text(upgrade.bundle.repository)||!digest(upgrade.bundle.metadataDigest)||!text(upgrade.registry)||!Array.isArray(upgrade.targets)||upgrade.targets.length!==3)invalid();
62
+ if(!validInstallation(upgrade.installation,upgrade.bundle.version))invalid();
64
63
  const targets=new Set();
65
64
  for(const row of upgrade.targets){if(!object(row)||!text(row.id)||!SUITE_PACKAGES.some(p=>p.name===row.moduleName)||targets.has(row.moduleName)||!exact(row.version)||!text(row.repository)||!digest(row.metadataDigest))invalid();targets.add(row.moduleName);}
66
65
  }
@@ -76,7 +75,7 @@ export function readSuiteSourceState(source){
76
75
  return{protocolVersion:state.protocolVersion,status:state.status,selection:state.selection,registry:state.registry,archiveCount:state.archiveCount,lastError:state.lastError};
77
76
  }
78
77
  /** Consume only the optional protocol-1 Cordis method and the public C-CORE section D DTO. */
79
- export function createCoreSuiteAdapter({core=()=>undefined,registry,suiteSourceMode=()=>'configured',suiteSourceService=()=>undefined}={}){
78
+ export function createCoreSuiteAdapter({core=()=>undefined,registry,suiteSourceMode=()=>'configured',suiteSourceService=()=>undefined,profileName}={}){
80
79
  const current=()=>{const value=core();return value?.protocolVersion==='1'&&typeof value.getSuiteDeclaration==='function'?value:undefined;};
81
80
  // AppHost-local service identity only: Core protocol 1 supplies no revision/generation.
82
81
  const instances=new WeakMap();
@@ -101,17 +100,17 @@ export function createCoreSuiteAdapter({core=()=>undefined,registry,suiteSourceM
101
100
  }
102
101
  const get=async()=>{
103
102
  const binding=sourceBinding();
104
- const declaration=await read({registry:binding.registry});
103
+ const declaration=await read({...(binding.registry===undefined?{}:{registry:binding.registry}),...(profileName===undefined?{}:{profile:profileName})});
105
104
  requireCondition(JSON.stringify(sourceBinding())===JSON.stringify(binding),'SUITE_PLAN_CHANGED','套件安装包来源已变化,请重新查看并确认。',{},409);
106
105
  requireCondition(declaration.completeness==='complete','CORE_SUITE_CURRENT_INCOMPLETE','Core 套件完整性:'+declaration.completeness,{completeness:declaration.completeness,blocked:declaration.blocked},409);
107
106
  let expected;try{const url=new URL(binding.registry);url.pathname=url.pathname.replace(/\/$/,'')+'/';expected=url.href;}catch{}
108
- requireCondition(expected&&declaration.registry===expected,'CORE_SUITE_SOURCE_MISMATCH','Core 声明来源与执行 registry 不符。',{blocked:declaration.blocked},409);
107
+ requireCondition(text(declaration.registry)&&(binding.registry===undefined||declaration.registry===expected),'CORE_SUITE_SOURCE_MISMATCH','Core 声明来源与执行 registry 不符。',{blocked:declaration.blocked},409);
109
108
  requireCondition(declaration.upgrades.length>0,'CORE_SUITE_NO_COMPATIBLE_TUPLE','Core 未提供可执行升级组合。',{blocked:declaration.blocked},409);
110
109
  // The first displayed Core tuple is the presentation default; App-host never ranks versions.
111
110
  const selectedUpgrade=declaration.upgrades[0];
112
111
  requireCondition(selectedUpgrade.registry===declaration.registry,'CORE_SUITE_SOURCE_MISMATCH','Core 升级组合来源与执行 registry 不符。',{},409);
113
112
  const components=SUITE_PACKAGES.map(({name,label})=>{const from=declaration.installed.find(row=>row.moduleName===name).actualVersion;if(!exact(from))invalid();return{name,label,from,to:selectedUpgrade.targets.find(row=>row.moduleName===name).version};});
114
- return{available:true,bundle:{name:SUITE_BUNDLE,from:declaration.bundleVersion,to:selectedUpgrade.bundle.version},components,installCommand:declaration.installCommand,sourceBinding:binding,declarationId:declaration.declarationId,registry:declaration.registry,selectedUpgrade,physicalBaseline:declaration.installed,upgrades:declaration.upgrades,blocked:declaration.blocked};
113
+ return{available:true,bundle:{name:SUITE_BUNDLE,from:declaration.bundleVersion,to:selectedUpgrade.bundle.version},components,installation:selectedUpgrade.installation,installCommand:declaration.installCommand,sourceBinding:binding,declarationId:declaration.declarationId,registry:declaration.registry,selectedUpgrade,physicalBaseline:declaration.installed,upgrades:declaration.upgrades,blocked:declaration.blocked};
115
114
  };
116
115
  Object.defineProperty(get,'unavailable',{get:()=>current()?null:CORE_SUITE_DECLARATION_MISSING});
117
116
  get.readFacts=async()=>{
@@ -132,7 +131,7 @@ export async function prepareSuiteUpgrade({coreSuitePlan}={}){
132
131
  const plan=await coreSuitePlan();
133
132
  requireCondition(plan!==undefined&&plan!==null,'CORE_SUITE_DECLARATION_MISSING',CORE_SUITE_DECLARATION_MISSING,{},503);
134
133
  const invalid=()=>requireCondition(false,'CORE_SUITE_DECLARATION_INVALID','Core 套件声明无法用于精确安装。',{},503);
135
- if(typeof plan!=='object'||Array.isArray(plan)||typeof plan.available!=='boolean'||!object(plan.bundle)||plan.bundle.name!==SUITE_BUNDLE||!exact(plan.bundle.from)||!exact(plan.bundle.to)||!exactInstallCommand(plan.installCommand)||!Array.isArray(plan.components)||plan.components.length!==SUITE_PACKAGES.length)invalid();
134
+ if(typeof plan!=='object'||Array.isArray(plan)||typeof plan.available!=='boolean'||!object(plan.bundle)||plan.bundle.name!==SUITE_BUNDLE||!exact(plan.bundle.from)||!exact(plan.bundle.to)||!nullableText(plan.installCommand)||!validInstallation(plan.installation,plan.bundle.to)||!Array.isArray(plan.components)||plan.components.length!==SUITE_PACKAGES.length)invalid();
136
135
  const rows=new Map();
137
136
  for(const row of plan.components){
138
137
  if(!row||typeof row!=='object'||!SUITE_PACKAGES.some(p=>p.name===row.name)||rows.has(row.name)||typeof row.from!=='string'||semver.valid(row.from)!==row.from||typeof row.to!=='string'||semver.valid(row.to)!==row.to)invalid();
@@ -19,7 +19,7 @@ const checkScript="console.log(JSON.stringify({version:process.versions.node,pla
19
19
  export async function probeStandaloneNode(binary,{platform,version,signal}={}){
20
20
  requireCondition(typeof binary==='string'&&isAbsolute(binary),'NODE_BINARY_INVALID','独立 Node 路径必须是绝对路径。');
21
21
  await access(binary,constants.X_OK);
22
- const {stdout}=await run(binary,['-e',checkScript],{env:{PATH:''},maxBuffer:4096,signal});
22
+ const {stdout}=await run(binary,['-e',checkScript],{env:{PATH:''},timeout:2_000,maxBuffer:4096,signal});
23
23
  let actual;try{actual=JSON.parse(stdout);}catch{throw new AppHostError('NODE_NOT_STANDALONE','所选文件没有返回独立 Node 的检查结果;Electron 包装脚本不能代替独立 Node。');}
24
24
  requireCondition(actual.electron===null&&/^24\./.test(actual.version)&&(Number(actual.version.split('.')[1])>13||Number(actual.version.split('.')[1])===13&&Number(actual.version.split('.')[2])>=1)&&(!version||actual.version===version)&&`${actual.platform}-${actual.arch}`===platform,'NODE_NOT_STANDALONE','需要当前平台的独立 Node.js 24(24.13.1 或更新);所选文件的版本、平台或 Electron 检查未通过。');
25
25
  return actual;
@@ -47,8 +47,8 @@ export function createNodeRuntime({domain,dataRoot,config={},initialBinary,platf
47
47
  requireCondition(next.mode!=='external'||isAbsolute(next.externalBinary),'NODE_BINARY_INVALID','已有独立 Node 路径必须是绝对路径。');
48
48
  return {...next,sourceBase:source.href.replace(/\/$/,'')};
49
49
  }
50
- const externalCheck=()=>requireCondition(['darwin-arm64','darwin-x64','linux-arm64','linux-x64'].includes(platform),'NODE_PLATFORM_UNSUPPORTED','当前平台尚未完成独立 Node 的进程守护支持;Windows 不支持本安装流程。');
51
- const supportedCheck=()=>requireCondition(supported,'NODE_PLATFORM_UNSUPPORTED','一键准备当前仅支持 macOS 13.5 及以上的 arm64 / x64;Windows 安装与进程守护尚未接通。');
50
+ const externalCheck=()=>requireCondition(['darwin-arm64','darwin-x64','linux-arm64','linux-x64','win32-x64'].includes(platform),'NODE_PLATFORM_UNSUPPORTED','当前平台尚未完成已有独立 Node 的受控 CLI 支持。');
51
+ const supportedCheck=()=>requireCondition(supported,'NODE_PLATFORM_UNSUPPORTED','一键准备当前仅支持 macOS 13.5 及以上的 arm64 / x64;Windows 受管准备和 owned 应用守护尚未接通。');
52
52
  async function publish(next){const whole={...next,revision:state.revision+1};await domain.global.set(whole);state=whole;}
53
53
  function manifest(p){return {schema:1,sources:[{id:'confirmed-source',kind:'https',base:new URL(`v${p.version}/`,p.sourceBase+'/').href}],items:[{id:'node',version:p.version,kind:'tar.gz',installTo:'runtime',platforms:{[platform]:{asset:p.asset,sha256:p.sha256,size:p.bytes,strip:1}},verify:{exec:'bin/node',args:['-e',checkScript],expect:JSON.stringify({version:p.version,platform:'darwin',arch:platform.slice(7),electron:null})}}]};}
54
54
  async function verifyActive(active,signal,heldLock){
@@ -66,7 +66,7 @@ export function createNodeRuntime({domain,dataRoot,config={},initialBinary,platf
66
66
  else if(state.active){supportedCheck();executable=await verifyActive(state.active);}
67
67
  }catch(e){operation={id:null,state:'failed',phase:'restore',error:{message:'安装环境尚未就绪:'+causeText(e),code:e.code??'NODE_CHECK_FAILED'}};}
68
68
  },
69
- status(){return copy({settings,names:NODE_SETTING_NAMES,versions:[...new Set(catalog.archives.map(a=>a.version))],platform,supported,ready:!!executable,executable:executable??null,active:state?.active??null,operation,limitations:'一键准备支持 macOS 13.5+ arm64 / x64;Windows 尚不支持。下载仅在确认后开始;不重试、不切换来源。'});},
69
+ status(){return copy({settings,names:NODE_SETTING_NAMES,versions:[...new Set(catalog.archives.map(a=>a.version))],platform,supported,ready:!!executable,executable:executable??null,active:state?.active??null,operation,limitations:'一键准备支持 macOS 13.5+ arm64 / x64;Windows x64 可使用已验证的已有独立 Node 运行受控 CLI,受管准备和 owned 应用守护仍不支持。下载仅在确认后开始;不重试、不切换来源。'});},
70
70
  async save(input){exact(input,['settings']);idle();const next=validate(input.settings);saving=true;plan=null;
71
71
  try{await safeAncestors(next.storageRoot);let external;
72
72
  if(next.mode==='external'){externalCheck();await probe(next.externalBinary,{platform});external=next.externalBinary;}
@@ -36,9 +36,12 @@ export function createProviderSources({ credentials, apps, llm = () => undefined
36
36
  if (!dshHome) return null;
37
37
  const path = join(dshHome,'.coding-oauth-gateway.json');
38
38
  let fileStat, value;
39
- try { fileStat = await stat(path); value = JSON.parse(await readFile(path,'utf8')); } catch { return null; }
40
- if ((fileStat.mode & 0o077) !== 0) return { id:'coding-oauth-gateway', source:'coding-oauth-gateway', kind:'api-key',
39
+ try { fileStat = await stat(path); } catch { return null; }
40
+ // Node mode bits cannot prove a private Windows ACL. Refuse file-mode
41
+ // secrets before reading them; the authenticated HTTP provider remains available.
42
+ if (process.platform === 'win32' || (fileStat.mode & 0o077) !== 0) return { id:'coding-oauth-gateway', source:'coding-oauth-gateway', kind:'api-key',
41
43
  displayName:'Coding OAuth Gateway', state:'unreachable' };
44
+ try { value = JSON.parse(await readFile(path,'utf8')); } catch { return null; }
42
45
  const base = { id:'coding-oauth-gateway', source:'coding-oauth-gateway', kind:'api-key', displayName:'Coding OAuth Gateway',
43
46
  ...(Array.isArray(value.models) ? { models:value.models } : {}), ...(value.keyHint ? { keyHint:value.keyHint } : {}) };
44
47
  if (!value.enabled) return { ...base, state:'gateway-off' };
package/dist/runtime.js CHANGED
@@ -22,6 +22,16 @@ export async function resolveNodeBinary(nodeBinary) {
22
22
  }
23
23
  try { await access(selected, constants.X_OK); }
24
24
  catch { throw new AppHostError('NODE_RUNTIME_REQUIRED', 'nodeBinary must be an absolute executable path.'); }
25
+ if (process.platform === 'win32') {
26
+ // Windows X_OK only checks existence. Verify the selected native runtime
27
+ // with a fixed expression before it can become a guardian executable.
28
+ try {
29
+ const {stdout} = await exec(selected, ['-p', 'JSON.stringify({node:process.release.name,electron:process.versions.electron??null})'],
30
+ {timeout:2_000,maxBuffer:1024,windowsHide:true,env:guardianEnvironment(selected)});
31
+ const result=JSON.parse(stdout);
32
+ if(result.node!=='node'||result.electron!==null)throw new Error('Not a standalone Node executable.');
33
+ } catch { throw new AppHostError('NODE_RUNTIME_REQUIRED','nodeBinary must be a standalone executable Node.js runtime.'); }
34
+ }
25
35
  return selected;
26
36
  }
27
37
  export function guardianEnvironment(nodeBinary, source = process.env) {
@@ -30,7 +40,11 @@ export function guardianEnvironment(nodeBinary, source = process.env) {
30
40
  if (typeof source[key] === 'string' && source[key].length) env[key] = source[key];
31
41
  }
32
42
  if (!env.LANG) env.LANG = 'C.UTF-8';
33
- env.PATH = `${dirname(nodeBinary)}:/usr/bin:/bin`;
43
+ if(process.platform==='win32') {
44
+ const systemRoot=source.SystemRoot??source.SYSTEMROOT??process.env.SystemRoot??process.env.SYSTEMROOT;
45
+ if(systemRoot)env.SystemRoot=systemRoot;
46
+ env.PATH=dirname(nodeBinary);
47
+ } else env.PATH = `${dirname(nodeBinary)}:/usr/bin:/bin`;
34
48
  return env;
35
49
  }
36
50
  export async function freePort() {
package/dist/store.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { constants } from 'node:fs';
2
2
  import { mkdir, readFile, open, rename, unlink, lstat, realpath } from 'node:fs/promises';
3
- import { dirname, isAbsolute, join, resolve } from 'node:path';
3
+ import { dirname, isAbsolute, join, resolve, parse, sep } from 'node:path';
4
4
  import { randomUUID } from 'node:crypto';
5
5
  import { AppHostError, SerialQueue, copy, requireCondition } from './errors.js';
6
6
  import { processIdentity } from './runtime.js';
@@ -42,8 +42,9 @@ export function validateSnapshot(s) {
42
42
  // Only use under an explicitly selected local profile root. Reject symlink components.
43
43
  export async function secureDirectory(path) {
44
44
  requireCondition(isAbsolute(path), 'INVALID_ROOT', 'An absolute data directory is required.');
45
- const parts = resolve(path).split('/').filter(Boolean);
46
- let current = '/';
45
+ const absolute = resolve(path), root = parse(absolute).root;
46
+ const parts = absolute.slice(root.length).split(sep).filter(Boolean);
47
+ let current = root;
47
48
  for (const part of parts) {
48
49
  current = join(current, part);
49
50
  await mkdir(current, { mode: 0o700 }).catch(e => { if (e.code !== 'EEXIST') throw e; });
@@ -218,8 +219,14 @@ export class AtomicFileStore {
218
219
  await this.checkpoint('snapshot-temp-fsynced', copy(image));
219
220
  await fd.close(); fd = undefined;
220
221
  await rename(temp, this.path); renamed = true;
221
- const directory = await open(dirname(this.path), constants.O_RDONLY);
222
- try { await directory.sync(); } finally { await directory.close(); }
222
+ // Node cannot open a directory for fsync on Windows. The completed
223
+ // file sync and same-directory rename still publish one whole image;
224
+ // only POSIX additionally synchronizes the directory entry. Do not
225
+ // reinterpret an arbitrary POSIX fsync failure as success.
226
+ if (process.platform !== 'win32') {
227
+ const directory = await open(dirname(this.path), constants.O_RDONLY);
228
+ try { await directory.sync(); } finally { await directory.close(); }
229
+ }
223
230
  await this.checkpoint('snapshot-published', copy(image));
224
231
  } catch (e) {
225
232
  // Post-rename failure is an ambiguous commit, not a guaranteed rollback.
@@ -0,0 +1,30 @@
1
+ /** A Windows Job Object assigned before an IPC anchor is allowed to spawn commands. */
2
+ import {spawn} from 'node:child_process';
3
+ import {join} from 'node:path';
4
+ import {fileURLToPath} from 'node:url';
5
+ export function createWindowsJob({pid,env,cwd,signal}){
6
+ if(process.platform!=='win32'||!Number.isSafeInteger(pid)||pid<=1)throw new Error('A live Windows anchor PID is required.');
7
+ const command=join(env.SystemRoot??'C:\\Windows','System32','WindowsPowerShell','v1.0','powershell.exe');
8
+ const child=spawn(command,['-NoLogo','-NoProfile','-NonInteractive','-ExecutionPolicy','Bypass','-File',fileURLToPath(new URL('./windows-job.ps1',import.meta.url)),'-AnchorPid',String(pid)],{env,cwd,signal,windowsHide:true,stdio:['pipe','pipe','pipe']});
9
+ let pending='',fault,closed=false,readyResolve,readyReject,stopPromise;const requests=[];
10
+ const ready=new Promise((resolve,reject)=>{readyResolve=resolve;readyReject=reject});
11
+ const exit=new Promise(resolve=>child.once('close',()=>{closed=true;fail(new Error('Windows ownership helper closed.'));resolve()}));
12
+ function fail(error){fault??=error;readyReject(error);for(const request of requests.splice(0))request.reject(error);if(!child.stdin.destroyed)child.stdin.end()}
13
+ child.once('error',fail);child.stdin.on('error',fail);
14
+ child.stderr.on('data',()=>fail(new Error('Windows ownership helper failed.')));
15
+ child.stdout.on('data',chunk=>{
16
+ pending+=chunk.toString('utf8');if(pending.length>4096){fail(new Error('Invalid Windows job response.'));child.stdin.end();return}
17
+ const lines=pending.split(/\r?\n/);pending=lines.pop();
18
+ for(const line of lines){if(line==='READY'){readyResolve();continue}const request=requests.shift();
19
+ if(!request||!request.pattern.test(line)){fail(new Error('Invalid Windows job control response.'));child.stdin.end();return}request.resolve(line);
20
+ }
21
+ });
22
+ const send=(text,pattern)=>new Promise((resolve,reject)=>{
23
+ if(fault||closed){reject(fault??new Error('Windows ownership helper is closed.'));return}
24
+ requests.push({resolve,reject,pattern});child.stdin.write(text+'\n',error=>{if(error)fail(error)});
25
+ });
26
+ return ready.then(()=>({
27
+ active:async()=>Number((await send('ACTIVE',/^ACTIVE \d+$/)).slice(7)),
28
+ stop:()=>stopPromise??=(async()=>{try{await send('STOP',/^STOPPED$/)}finally{child.stdin.end();await exit}})(),
29
+ }),async error=>{child.stdin.end();await exit;throw error;});
30
+ }
@@ -0,0 +1,75 @@
1
+ param([Parameter(Mandatory=$true)][int]$AnchorPid)
2
+ $ErrorActionPreference = 'Stop'
3
+ $ProgressPreference = 'SilentlyContinue'
4
+ $PSModuleAutoLoadingPreference = 'None'
5
+ Import-Module ($PSHOME+'\Modules\Microsoft.PowerShell.Utility\Microsoft.PowerShell.Utility.psd1') -ErrorAction Stop
6
+ Add-Type -TypeDefinition @'
7
+ using System;
8
+ using System.Runtime.InteropServices;
9
+ using System.Threading;
10
+ public static class HanaMeshOwnedJob {
11
+ [StructLayout(LayoutKind.Sequential)] struct BasicLimits {
12
+ public long ProcessTime, JobTime;
13
+ public uint Flags;
14
+ public UIntPtr MinimumWorkingSet, MaximumWorkingSet;
15
+ public uint ActiveLimit;
16
+ public UIntPtr Affinity;
17
+ public uint PriorityClass, SchedulingClass;
18
+ }
19
+ [StructLayout(LayoutKind.Sequential)] struct IoCounters {
20
+ public ulong ReadOperations, WriteOperations, OtherOperations, ReadBytes, WriteBytes, OtherBytes;
21
+ }
22
+ [StructLayout(LayoutKind.Sequential)] struct ExtendedLimits {
23
+ public BasicLimits Basic;
24
+ public IoCounters Io;
25
+ public UIntPtr ProcessMemory, JobMemory, PeakProcessMemory, PeakJobMemory;
26
+ }
27
+ [StructLayout(LayoutKind.Sequential)] struct Accounting {
28
+ public long UserTime, KernelTime, PeriodUserTime, PeriodKernelTime;
29
+ public uint PageFaults, TotalProcesses, ActiveProcesses, TerminatedProcesses;
30
+ }
31
+ [DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)] static extern IntPtr CreateJobObject(IntPtr attributes, string name);
32
+ [DllImport("kernel32.dll", SetLastError=true)] static extern bool SetInformationJobObject(IntPtr job, int infoClass, ref ExtendedLimits info, uint length);
33
+ [DllImport("kernel32.dll", SetLastError=true)] static extern bool QueryInformationJobObject(IntPtr job, int infoClass, out Accounting info, uint length, IntPtr returnedLength);
34
+ [DllImport("kernel32.dll", SetLastError=true)] static extern IntPtr OpenProcess(uint access, bool inherit, int pid);
35
+ [DllImport("kernel32.dll", SetLastError=true)] static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process);
36
+ [DllImport("kernel32.dll", SetLastError=true)] static extern bool TerminateJobObject(IntPtr job, uint exitCode);
37
+ [DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr handle);
38
+ static Exception Failure(string operation) { return new InvalidOperationException(operation+":"+Marshal.GetLastWin32Error()); }
39
+ static uint Active(IntPtr job) {
40
+ Accounting info;
41
+ if(!QueryInformationJobObject(job, 1, out info, (uint)Marshal.SizeOf(typeof(Accounting)), IntPtr.Zero)) throw Failure("query-job");
42
+ return info.ActiveProcesses;
43
+ }
44
+ public static void Run(int pid) {
45
+ IntPtr job=CreateJobObject(IntPtr.Zero,null), process=IntPtr.Zero;
46
+ if(job==IntPtr.Zero) throw Failure("create-job");
47
+ try {
48
+ ExtendedLimits limits=new ExtendedLimits(); limits.Basic.Flags=0x2000; // JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
49
+ if(!SetInformationJobObject(job,9,ref limits,(uint)Marshal.SizeOf(typeof(ExtendedLimits)))) throw Failure("limit-job");
50
+ process=OpenProcess(0x0101,false,pid); // PROCESS_SET_QUOTA | PROCESS_TERMINATE
51
+ if(process==IntPtr.Zero) throw Failure("open-anchor");
52
+ if(!AssignProcessToJobObject(job,process)) throw Failure("assign-anchor");
53
+ CloseHandle(process); process=IntPtr.Zero;
54
+ Console.WriteLine("READY"); Console.Out.Flush();
55
+ string line;
56
+ while((line=Console.ReadLine())!=null) {
57
+ if(line=="ACTIVE") { Console.WriteLine("ACTIVE "+Active(job)); Console.Out.Flush(); }
58
+ else if(line=="STOP") {
59
+ if(!TerminateJobObject(job,1)) throw Failure("terminate-job");
60
+ DateTime deadline=DateTime.UtcNow.AddSeconds(1);
61
+ while(Active(job)!=0) {
62
+ if(DateTime.UtcNow>=deadline) throw new InvalidOperationException("job-not-empty");
63
+ Thread.Sleep(5);
64
+ }
65
+ Console.WriteLine("STOPPED"); Console.Out.Flush(); return;
66
+ } else throw new InvalidOperationException("invalid-job-control");
67
+ }
68
+ } finally {
69
+ if(process!=IntPtr.Zero) CloseHandle(process);
70
+ CloseHandle(job); // EOF or any helper failure kills the entire assigned tree.
71
+ }
72
+ }
73
+ }
74
+ '@
75
+ [HanaMeshOwnedJob]::Run($AnchorPid)
@@ -35,7 +35,7 @@ export async function apply(ctx, config = {}) {
35
35
  }
36
36
  ```
37
37
 
38
- 预发布版本只用于隔离验收且不得成为 `latest`;应用库生产路径只接受 registry `latest` 返回的稳定精确版本。发布者必须同时保留 npm 包完整性、runtime 三方来源与 sha256、以及与 app-host 精确 peer 版本对应的可复核构建记录。
38
+ 预发布版本只用于隔离验收且不得成为 `latest`;应用库安装路径只接受启用目录 installMethods 给出的审核精确版本,且条目 oneClickInstall.allowed:true;不在安装时读取可变标签。发布者必须同时保留 npm 包完整性、runtime 三方来源与 sha256、以及与 app-host 精确 peer 版本对应的可复核构建记录。
39
39
 
40
40
  ## Existing configuration and original upstream archives (D-230)
41
41
 
package/docs/CONTRACT.md CHANGED
@@ -1,4 +1,4 @@
1
- 0.6.7 adds the explicitly consented public installation-status capability on both existing library listeners. Full request/response, strict refusals, configured official-reader support and scope/lifecycle are defined in [INSTALLATION_STATUS.md](INSTALLATION_STATUS.md); it does not authorize installation or alter recording consent.
1
+ 0.6.7 adds the explicitly consented public installation-status capability on both existing library listeners. Full request/response, strict refusals, automatic official-reader discovery and minimum-version support and scope/lifecycle are defined in [INSTALLATION_STATUS.md](INSTALLATION_STATUS.md); it does not authorize installation or alter recording consent.
2
2
 
3
3
  # 公开契约 v1(候选,0.1.0-rc.41;rc.41 卸载 runtime 时清理生成的 Python 字节码缓存、未知残留仍 fail-closed;rc.40 市场只读查询待启动实例,ready 后一次 resume;前台即时 heartbeat 与失效帧显式清理;rc.39 Market seat 内打开应用时将原有应用帧固定铺满原生窗口并覆盖 Settings,同一回执/租约/关闭动作;rc.38 投射时仅提升 shell.overlay 层级以盖过原生 Settings;rc.35 市场开关按钮即时反馈、owned 应用已退出时提前结束 TERM 等待并确认精确进程组终止;rc.34 内置市场消费 heartbeat/视图清理;rc.33 owned 回收与启动握手分阶段限时;rc.32 客户端 `market` 席位与 dshmarket 互斥、壳内重启改走 postMessage 桥(见「市场席位」);rc.28 HanaMesh 市场:插件与应用同路径装卸、已装含插件、可升级/需重启、`installedPlugins` 与 `plugins/*` 路由、`LIB-PROVISION-INPUT` 400(见「市场」);rc.27 应用使用证据经 usage 插件 record 座位上报(见「应用使用证据」);rc.26 应用库默认目录源 + 纯 DSH 位置推断(见「应用库配置」);rc.15 目录条目校验补 updatedAt;rc.16 应用库默认 category=hanamesh-app、q/cursor 透传;rc.13/rc.14 与 rc.12 契约相同:rc.13 补许可证/repository/精确 peer,rc.14 去掉客户端对 hanameshCore 的读取)
4
4
 
@@ -151,7 +151,7 @@ DSH 绑定必须提供同一个 `single` domain 的一次完整镜像 publish
151
151
  |---|---|---|---|
152
152
  | `/hanamesh/library` | GET | `q`、`category`(缺省 `hanamesh-app`;`category=` 空 = 全部)、`cursor` | `{source, items[], categories[], installed[], plugins[], restartRequired, page}`;`items[]` 每条目在目录字段之外加 `kind`(`application` = `hanamesh-app` + npm 包;`plugin` = 其它 npm 包;`listing` = 无 npm 包,仅收录)、`application`(兼容)、`installed`(应用行或插件行,或 `null`)、`upgradeAvailable`(已选 npm `installTarget.version` 严格新于已装 `version`;不读取退休目录版本;已装行处于 `uninstalled-not-unloaded` 时恒 false);`categories` = 本页条目出现过的类别(目录契约没有类别清单端点) |
153
153
  | `/hanamesh/library/installedPlugins` | GET(POST → 405) | 无参数 | `{plugins[], apps[], restartRequired}`;`plugins[]`:`{packageName, version, kind:'plugin', bundle, active, state, errorCode?}`,`state` ∈ `installed` / `installed-not-loaded`(启动后新增)/ `uninstalled-not-unloaded`(启动后移除,进程仍加载)/ `invalid`(依赖里有、`node_modules` 里读不到);`apps[]` = 原已安装应用扫描行(`registered` / `installed-not-loaded` / `runtime-missing` / `invalid`) |
154
- | `/hanamesh/library/install` | POST | `{itemId, packageName?}` | 202 `{operationId,status:'started'}`;`packageName` 给出时用它精确查目录(`q=<包名>&category=`)再按 `itemId` 匹配;应用与插件同一路径走官方 CLI;有 installMethods 时必须选中固定 target,npm 用 target.version,GitHub 用完整 SHA/子目录/构建名单,不在安装时解析 latest。无匹配目标拒绝 NOT_INSTALLABLE 并返回具名原因。未提供 installMethods 的标准目录保持既有 npm 路径;应用另外供给 runtime;也是升级路径 |
154
+ | `/hanamesh/library/install` | POST | `{itemId, packageName?}` | 202 `{operationId,status:'started'}`;`packageName` 给出时用它精确查目录(`q=<包名>&category=`)再按 `itemId` 匹配;应用与插件同一路径走官方 CLI;必须有 installMethods、oneClickInstall.allowed:true 并选中固定 target,npm 用 target.version,GitHub 用完整 SHA/子目录/构建名单,不在安装时解析 latest。无匹配目标拒绝 NOT_INSTALLABLE 并返回具名原因。未提供 installMethods 拒绝安装;应用另外供给 runtime;也是升级路径 |
155
155
  | `/hanamesh/library/plugins/install` | POST | `{packageName}` | 202;包必须在启用目录里能搜到(否则 404 `CATALOG_ITEM_MISSING`);目录条目是应用则走应用路径 |
156
156
  | `/hanamesh/library/plugins/uninstall` | POST | `{packageName}` | 202;`dsh plugin remove <包>`;若该包是已装应用则转应用卸载(runtime 清理 + 运行中 `INSTANCE_IN_USE` 409) |
157
157
  | `/hanamesh/library/provision` | POST | `{appId, packageName, runtimeItem}` 三者必填 | 202;缺字段/非法 → 400 `INVALID_INPUT`(`LIB-PROVISION-INPUT`) |
@@ -222,7 +222,7 @@ Authenticated same-origin routes (normal workspace CSRF and connection cookie):
222
222
 
223
223
  Public install-target remains read-only and unchanged. Preparation never weakens authentication/CORS or guardian admission. Subsequent AppHost spawns, library CLI supervisors and installation readers capture the new standalone binding without restarting DSH. In-flight operations retain their delegate; settings/preparation reject while library/host operations are in use.
224
224
 
225
- Support implementation: macOS13.5+ arm64/x64, known Node24.13.1 or24.22.0 (default24.22.0). Windows is explicitly unsupported: POSIX-owned process supervision, cancellation and reader chain need separate implementation and real-host proof. No Windows ready claim. mac x64 native proof is tracked separately from its signed archive integrity. No client or upstream files change. No npm lifecycle download.
225
+ Support implementation: macOS13.5+ arm64/x64, known Node24.13.1 or24.22.0 (default24.22.0). Windows managed Node preparation remains unsupported (`supported:false` describes managed preparation). Windows x64 accepts an explicitly selected existing standalone Node for controlled CLI and installation-reader operations only, after the fixed probe verifies actual Node 24 version, OS/architecture and absence of Electron; a verified external selection may report `ready:true`. Owned application launch remains unsupported and fails before spawn or credential projection. Automatic installation readers have a separate Windows discovery/owned-tree implementation; their native proof remains required. mac x64 native proof is tracked separately from its signed archive integrity. No client or upstream files change. No npm lifecycle download.
226
226
 
227
227
  Storage space budget is archive bytes + twice measured regular-file payload (new tree + staging); never delete another version or unknown files to make space. lib-provision alone owns staging/ledger/manifests; interrupted or cancelled downloads may leave its resumable owned cache. Another attempt requires a new visible confirmation. Existing versions remain until an explicit owned cleanup is designed; no automatic deletion or implied rollback. Offline startup verifies an existing active tree/ledger and standalone executable; failures are visible and never trigger a download.
228
228
 
@@ -231,7 +231,7 @@ Storage space budget is archive bytes + twice measured regular-file payload (new
231
231
 
232
232
  ## B17 套件安装包来源(2026-10-21 候选)
233
233
 
234
- AppHost 自有设置页「套件安装包来源」,公开同源端点 `GET|POST /hanamesh/library/suite-source`。GET 无参数,返回 `{mode, configuredRegistry, observation, traceId}`;`mode` 为 `configured|core`。`configured` 是唯一新增默认:继续使用原 `library.registry`(未设仍为 `https://registry.npmjs.org`)。市场目录 `library.sources` 的未设播种、显式空数组、自定义及已保存值均独立保持;目录 manifest URL 不是 npm registry。
234
+ AppHost 自有设置页「套件安装包来源」,公开同源端点 `GET|POST /hanamesh/library/suite-source`。GET 无参数,返回 `{mode, configuredRegistry, observation, traceId}`;`mode` 为 `configured|core`。`configured` 是唯一新增默认:使用显式 `library.registry`,未设时请求Core声明实际来源。市场目录 `library.sources` 的未设播种、显式空数组、自定义及已保存值均独立保持;目录 manifest URL 不是 npm registry。
235
235
 
236
236
  POST 严格只接受 `{mode:'configured'}` 或 `{mode:'core'}`,不接受 registry、版本或其他字段/查询参数。沿用精确 Host、同源 Origin、workspace-v1、Connection admission、逐操作 authorization、JSON/65536 字节及 no-store 规则。选择存自己的 `hanamesh_library` 单镜像可选 `suiteSource:{mode}`;未存值不改旧记录。来源、目录和网站交接设置按接收顺序串行读改写整个镜像,成功持久化后才发布新内存状态;保存失败 `500/SUITE_SOURCE_SAVE_FAILED`,旧选择和其他字段保持;关闭等待已排队写入。
237
237
 
@@ -239,7 +239,7 @@ GET 的 `observation` 为 `{observable:false,code}` 或 `{observable:true,state}
239
239
 
240
240
  明确选择 core 后,预览、确认、紧接执行之前各自重新读取当前 source 和 Core `getSuiteDeclaration({registry})`,并在异步声明返回后复核来源。`ready` 仅表示来源可读;兼容性仍唯一由 Core declaration 决定。disabled/failed/disposed 或无有效 registry 为 `409/CORE_SUITE_SOURCE_NOT_READY`;不缓存 ready、不回退配置 registry、不挑最新、不重试。沿用 declarationId、精确 tuple/metadata digest/repository、物理已装 baseline 和本人 planId。URL、selection、选择模式或提供服务的对象身份变化均终止原确认;新预览仍须本人确认。服务对象身份是 AppHost 内存内 opaque token,**不是 Core 的 generation/revision**;protocol 1 没有该事实或 watch,不能声称观察到两次读取间完全消失又恢复原状的事件。
241
241
 
242
- 最后执行仍调用现有官方 CLI `add --save-exact --strict-peer-dependencies --registry=<声明原址>`,精确三组件;正常市场外部包路径不变。正在执行时源连接丢失由实际 CLI/输运终态报告,不靠重试/退出屏障维持源。Core B17 服务只来自候选 `c5e1a7373b9896c385f14f79a64bb277f43bde5c` 的公开能力;无 Core 运行时 import,无 Desktop hook/bootstrap、三锁或先于插件加载的服务。源码/真实 Cordis+JSON domain(AUTH_DOUBLE)与正常包验证不等同真实原生安装或 owner 验收。
242
+ 最后调用现有官方 CLI,完整 argv 来自Core的升级 installation,安装一个精确总包;正常市场外部包路径不变。正在执行时源连接丢失由实际 CLI/输运终态报告,不靠重试/退出屏障维持源。Core B17 服务只来自候选 `c5e1a7373b9896c385f14f79a64bb277f43bde5c` 的公开能力;无 Core 运行时 import,无 Desktop hook/bootstrap、三锁或先于插件加载的服务。源码/真实 Cordis+JSON domain(AUTH_DOUBLE)与正常包验证不等同真实原生安装或 owner 验收。
243
243
 
244
244
  公开类型位于 `@hanamesh/dsh-app-host/dsh` 的 `SuiteSourceMode/CoreSuiteSourceState/SuiteSourceSettings/SuiteSourceSettingsInput`;页面导出 `SuiteSourceSettingsSection`。回归:`suite-source.test.mjs`、`handoff-integration.test.mjs` B17-CORDIS,变异 M84–M89(M61 检查全部后续读取被错误缓存)。
245
245
 
@@ -256,6 +256,6 @@ B17 review repair(同0.6.8候选):`AppHost` 未注入 `runtimeLedgerReader
256
256
 
257
257
  ## 0.6.16 市场查询来源与诊断(原协议1)
258
258
 
259
- 普通市场安装/升级的 latest 查询消费 DSH 公开 pluginManager.registries() 与公开 registryPlan/attributeFailure,沿用用户配置源顺序和私有源隔离规则;成功后将同一源选择传入原受控CLI:显式源用--registry=<source>,pnpm自有源(null)不传--registry以保留scope/认证配置。整个配置/查询/读回共30000ms,超时abort并报REGISTRY_LOOKUP_TIMEOUT,未提供公开能力/解析源报REGISTRY_CONFIG_UNAVAILABLE;原生异常为REGISTRY_LOOKUP_FAILED并保留cause。没有新的用户配置、请求字段或协议版本。既有 details.diagnostic 带脱敏类型/消息/cause.code/实际阶段及已知CLI exit/stderr摘要,同时写宿主日志;其它错误、回滚与重启语义保留。Core套件升级路径的声明/源和缺省值单独保留,不采用普通市场的源选择。
259
+ A2 0.6.22:普通插件必须有 Indexer 已审核的 installMethods 和 oneClickInstall.allowed:true;仅执行其 npm 精确版本或完整 GitHub SHA。缺目标具名拒绝,不根据包名查可变版本标签。CLI 使用显式 library.registry(若有),否则保留宿主原生 registry 配置。删除普通插件按包名内部直装方法、元数据查询及其专用设置/规则加载。套件仅透传 Core installCommand。安装失败保留脱敏 CLI/读回诊断及补偿结果;精确版本发布后即可原生验收。
260
260
 
261
- 0.6.16最小接线:公开规则模块从宿主manager同锚点/版本守卫加载,不在AppHost声明manager依赖/peer;找不到模块或公开函数则REGISTRY_CONFIG_UNAVAILABLE,无npmjs回退或自创排序。发布依赖闭包与0.6.15一致。
261
+ A2 0.6.22:升级tuple必须带Core生成的installation(name/version/command/args),name/version与该tuple一致。AppHost传实际profile和可选配置来源;未配置来源由Core显式声明。CLI直接执行args;confirmation同时绑定该定义。installCommand是Core的可空修复展示文本,不解析命令前缀。旧Core缺定义不回退自拼。
@@ -33,7 +33,7 @@ Internal connection HTTPS/bind-address handling changed upstream; the local HTTP
33
33
 
34
34
  ## Installation behavior
35
35
 
36
- 0.6.15 retains the monitored public CLI. CLI failures carry bounded, redacted stdout and stderr through the asynchronous operation's diagnostic details; the market shows the original cause in an expandable panel. The main failure message includes the exact pnpm category. A minimum-release-age rejection lists affected package versions, explains the default 24-hour requirement while deferring to the actual cutoff, explains the known first-entry-only defect in pnpm 11.7.0 bundled with DSH 0.2.0-rc.2, and directs the person to DSH Settings to check the version and upgrade DSH, or wait until the rule is met before a manual retry. No profile configuration, dependency exclusion or supply-chain policy is changed.
36
+ 0.6.15 retains the monitored public CLI. CLI failures carry bounded, redacted stdout and stderr through the asynchronous operation's diagnostic details; the market shows the original cause in an expandable panel. The main failure message includes the exact pnpm category. A minimum-release-age rejection lists affected package versions and retains the actual cutoff in technical details. Verify the exact installation target and upgrade DSH through its Settings if necessary. No profile configuration, dependency exclusion or supply-chain policy is changed.
37
37
 
38
38
  The official 0.2.0-rc.2 bundled pnpm 11.7.0 uses only the first matching package-name rule in `minimumReleaseAgeExclude`: an earlier exact old-version entry shadows a later exact new-version entry. The existing lockfile verifier can therefore reject an unrelated installation even though the new version is visibly listed. The 11.7.0 package obtained from npm has the same entry and core bytes as the supplied Desktop copy. Official Desktop 0.2.1-rc.3's pnpm 11.28.5 unions all matching exact versions and passes the same diagnostic profile. This establishes two tested endpoints, not the first fixed version. Both Desktop CLI and in-process manager use this pnpm path and set `ELECTRON_RUN_AS_NODE=1`; switching to the manager does not correct the 11.7.0 rule evaluation.
39
39
 
@@ -48,35 +48,44 @@ interface InstallationObservation {
48
48
  }
49
49
  ```
50
50
 
51
- The five distinct observations are: responding runtime, the other DSH/HanaMesh runtime (always `unknown`, not observed by this responder), Claude Code configured user-level CLI scope, Codex CLI configured local-marketplace user scope, and Codex/ChatGPT desktop App scope (always named `unknown`). CLI labels explicitly exclude project/other isolated profiles and desktop App. A valid absence is absence **only in the named configured scope**; never show it as a machine-wide uninstall. Preserve every responder/scope; ports are not durable instance identities. Conflicts between responders are unknown, never resolved by choosing one installed result.
51
+ The five distinct observations are: responding runtime, the other DSH/HanaMesh runtime (always `unknown`, not observed by this responder), Claude Code current-user CLI scope, Codex CLI current-user local-marketplace scope, and Codex/ChatGPT desktop App scope (always named `unknown`). CLI labels explicitly exclude project/other isolated profiles and desktop App. A valid absence is absence **only in the named current-user scope**; never show it as a machine-wide uninstall. Preserve every responder/scope; ports are not durable instance identities. Conflicts between responders are unknown, never resolved by choosing one installed result.
52
52
 
53
53
  Current runtime status/version/components come only from the optional public Core protocol `'1'` `getSuiteDeclaration()` **without an argument**, through the existing Core adapter. No registry request, health substitution, physical file-count completeness, version-range/peer/source algorithm or local upgrade selection is added. Complete maps to installed; incomplete stays incomplete; unknown stays unknown with top-level version null but known component versions and sanitized named blocked codes retained. Missing method, malformed declaration and read failure have distinct reasons. Each detection re-reads; Core removal/invalidation cannot reuse an old installed answer. Agent distribution version is independent of the DSH suite bundle version. Missing active fields remain absent.
54
54
 
55
- ## Explicit agent reader settings
55
+ ## Automatic official CLI discovery
56
56
 
57
- `DshPluginConfig.library.installationReaders` may configure `claude` and/or `codex`. Each reader requires all these fields; **there are no inferred/default values**:
57
+ AppHost 0.6.23 discovers Claude Code and Codex for the current OS user after the consented request. There is no manual reader setting; the former `library.installationReaders` option and public reader configuration type are removed. Browser requests cannot supply paths, commands, environment or profile selection. The registered suite identity is `hanamesh@hanamesh`, with the exact HTTPS source `https://github.com/yzsnstotz/hanamesh-plugins`, as in the existing suite catalog contract.
58
58
 
59
- | Field | Required value |
59
+ Selection is deterministic: the first executable in the inherited absolute PATH directories wins; if absent on macOS/Linux, query the user's interactive login shell once (`-ilc`), extracting only the named infrastructure-path environment in a framed JSON value. This follows [VS Code's shell environment practice](https://code.visualstudio.com/docs/configure/command-line#_how-do-i-detect-when-a-shell-was-launched-by-vs-code). No full environment dump is collected. Like [VS Code's implementation](https://github.com/microsoft/vscode/blob/main/src/vs/platform/shell/node/shellEnv.ts), shell stderr is separate from success: exit/result and a valid framed payload decide success. Normal Bash non-TTY diagnostics are discarded, not treated as failure; both streams still count toward the existing byte budget. Plugin inventory commands retain their clean-stderr requirement. Shell startup noise and stderr never enter public output. Next search the fixed layouts below in listed order. Finally, scan the listed manager version directories in order, newest Node SemVer first within each directory. Relative/empty PATH segments are excluded. Do not run every candidate or switch to a second CLI when the selected command fails, is old or returns an invalid inventory. Executable names alone are not a signature guarantee; the fixed official version syntax and inventory schema are validated before returning facts.
60
+
61
+ | Layout | Searched locations and source |
60
62
  |---|---|
61
- | executable | Absolute path to the person's official CLI executable |
62
- | home | Absolute explicitly selected user's home directory |
63
- | configDir | Absolute existing official config directory for that selected profile (`CLAUDE_CONFIG_DIR` or `CODEX_HOME`) |
64
- | pluginName / marketplace | Exact registered canonical AgentPluginDTO identity; not a guessed hanamesh@hanamesh |
65
- | marketplaceUrl | Exact registered `installation.marketplaceUrl`, canonical credential-free HTTPS, no query/fragment |
66
- | timeoutMs / maxOutputBytes | Required positive safe-integer read budgets; timeout at most Node's 2147483647 limit |
63
+ | Official native / standalone | `~/.local/bin/claude[.exe]` ([Claude setup](https://code.claude.com/docs/en/setup#auto-updates)); native launcher symlinks resolve through normal OS execution. Codex `~/.local/bin/codex`, `CODEX_INSTALL_DIR`, Windows `%LOCALAPPDATA%/Programs/OpenAI/Codex/bin/codex.exe` ([official installer variables](https://learn.chatgpt.com/docs/config-file/environment-variables#installer-variables), [installation](https://learn.chatgpt.com/docs/cli)). Standalone release binaries in custom directories are found through inherited/login PATH or `CODEX_INSTALL_DIR`; unlinked archives are not guessed. |
64
+ | Legacy Claude local | `~/.claude/local/claude`, `~/.claude/local/node_modules/.bin/claude`. These are historical npm-local layouts, not the current native default; [upstream issue with the original launcher path](https://github.com/anthropics/claude-code/issues/2824). No `.claude.json` or installMethod file is read. |
65
+ | npm global | `npm_config_prefix` / `NPM_CONFIG_PREFIX` (`bin` on POSIX, prefix itself on Windows), `~/.npm-global/bin`, `~/.npm/bin`, `%APPDATA%/npm`, `%ProgramFiles%/nodejs`; system prefixes and active Node manager bins below. [npm folder rules](https://docs.npmjs.com/cli/v11/configuring-npm/folders/). No npmrc read or npm config command. |
66
+ | pnpm global | `PNPM_HOME` and its `bin`, `~/.local/share/pnpm[/bin]`, `~/Library/pnpm[/bin]`, `%LOCALAPPDATA%/pnpm`. [pnpm setup](https://pnpm.io/cli/setup). Both earlier direct-bin and current bin-subdirectory layouts are recognized. |
67
+ | Yarn Classic | `~/.yarn/bin`, `~/.config/yarn/global/node_modules/.bin`, custom prefix via PATH/login shell. [Yarn global](https://classic.yarnpkg.com/lang/en/docs/cli/global/). |
68
+ | Volta / asdf / mise | `VOLTA_HOME/bin` or `~/.volta/bin`, `%LOCALAPPDATA%/Volta/bin`; `ASDF_DATA_DIR/shims` or `~/.asdf/shims`; `MISE_DATA_DIR/shims` or `~/.local/share/mise/shims`. [Volta](https://docs.volta.sh/guide/understanding), [asdf](https://asdf-vm.com/manage/configuration.html), [mise](https://mise.jdx.dev/directories.html). |
69
+ | Homebrew / system | `/opt/homebrew/bin`, `/usr/local/bin`, `/home/linuxbrew/.linuxbrew/bin`, `/usr/bin`, `/bin`. [Homebrew prefixes](https://docs.brew.sh/Installation); [Claude Homebrew setup](https://code.claude.com/docs/en/setup), [Codex distribution](https://learn.chatgpt.com/docs/cli). Brew-linked native binaries use the same discovery path. |
70
+ | nvm / fnm / installed manager versions | `NVM_BIN`, `NVM_SYMLINK`; `NVM_DIR/versions/node/*/bin` or `~/.nvm/versions/node/*/bin`; Windows `NVM_HOME/*`; `FNM_MULTISHELL_PATH`; `FNM_DIR/node-versions/*/installation[/bin]`, `~/.local/share/fnm/node-versions/*/installation/bin`, `~/Library/Application Support/fnm/node-versions/*/installation/bin`, `%APPDATA%/fnm/node-versions/*/installation`; asdf `installs/nodejs/*/bin`, mise `installs/node/*/bin`. Only SemVer directory names are scanned. [nvm](https://github.com/nvm-sh/nvm), [fnm](https://github.com/Schniz/fnm/blob/master/docs/commands.md), [asdf](https://asdf-vm.com/manage/configuration.html), [mise](https://mise.jdx.dev/directories.html). |
71
+ | Windows native links / npm shims | `%LOCALAPPDATA%/Microsoft/WinGet/Links` ([Claude WinGet](https://code.claude.com/docs/en/setup)). `.exe` precedes `.cmd` per directory. A `.cmd` is never sent to a shell: resolve only the adjacent official npm `node_modules/@anthropic-ai/claude-code/cli.js` or `node_modules/@openai/codex/bin/codex.js`, run it with the verified standalone Node. Missing JS is a named failure, not a guessed shell command. |
72
+
73
+ HOME is the host user's HOME/USERPROFILE. Honor existing `CLAUDE_CONFIG_DIR` / `CODEX_HOME` environment paths, otherwise use `~/.claude` / `~/.codex`. These directories must already exist; discovery never creates them, reads their contents, or reads credentials, sessions or private configuration files. Only official read commands observe plugin inventory. The named profile scope is not a machine-wide inventory. Electron hosts retain the existing verified standalone Node requirement.
67
74
 
68
- No website body can set these fields, executable arguments, environment or filesystem paths. No PATH/profile discovery or direct reading of private authentication/chat/config inventories is implemented. An absent/invalid reader is an actionable unknown, not “not installed”. Missing directories are not created. Windows readers are explicitly unsupported in this implementation; runtime Core reads still work. Electron hosts need the existing verified standalone `nodeBinary` for the process supervisor; absent Node is named unknown, not an Electron workaround.
75
+ Named automatic read budgets are `INSTALLATION_READ_BUDGET.timeoutMs=2000` and `maxOutputBytes=20000`, retaining the existing installation-command fixtures' values without extending deadlines or adding retries. **待追认:** the former product configuration had required budgets but no defaults; adopting those existing fixture values for automatic product reads requires owner acknowledgement. They are code constants documented here, with no manual reader form.
69
76
 
70
- Exact verified protocols (other CLI versions return `AGENT_READER_VERSION_UNSUPPORTED`):
77
+ Minimum CLI version and fixed syntax (no new upper bound):
71
78
 
72
- | Channel | Verified `--version` result | Fixed subsequent read argv |
79
+ | Channel | Minimum version / accepted `--version` syntax | Fixed subsequent read argv |
73
80
  |---|---|---|
74
- | Claude Code | `2.1.301 (Claude Code)` | `plugin list --json`; `plugin marketplace list --json` |
75
- | Codex CLI | `codex-cli 0.162.1` | `plugin list --json --disable remote_plugin`; `plugin marketplace list --json --disable remote_plugin` |
81
+ | Claude Code | `>=2.1.301`, `MAJOR.MINOR.PATCH (Claude Code)` | `plugin list --json`; `plugin marketplace list --json` |
82
+ | Codex CLI | `>=0.162.1`, `codex-cli MAJOR.MINOR.PATCH` | `plugin list --json --disable remote_plugin`; `plugin marketplace list --json --disable remote_plugin` |
76
83
 
77
- The Codex flag was checked through this exact CLI's public `features list`; it excludes remote directory reads. A version match is necessary but not sufficient: both JSON shapes must validate. Claude uses the official installed array's `id`, user `scope`, enabled/version and load diagnostics. Other project-only target rows are unknown for this reader. Codex uses the official `installed` array and its qualified identity, installed/enabled/version. The exact target marketplace must match the registered HTTPS source in marketplace list; Codex's per-install `marketplaceSource` must also be present, supported and consistent. A local/different/unreadable source, duplicate/conflicting target or malformed/truncated/empty raw output is unknown. Only a complete valid clean inventory with no target in the configured scope is not-installed. A known install may have version null; consumers must show version unknown. Official load/payload issues produce incomplete, not successful readiness. No plugin MCP/hook definitions, unrelated inventory or CLI raw diagnostics are returned.
84
+ Malformed version output is `AGENT_READER_VERSION_INVALID`; below-minimum versions are `AGENT_READER_VERSION_UNSUPPORTED`; JSON parse/shape failure is `AGENT_INVENTORY_INVALID`. New versions such as 2.1.319 / 0.167.0 must pass exactly the existing inventory validation.
78
85
 
79
- Commands run with an empty stdin in a disposable neutral cwd, only named HOME/config/platform paths, no inherited model/API keys, proxy/auth/askpass/Node preload, and disabled Git credential helpers. No model/install/upgrade/marketplace refresh command runs. A detached IPC reaper owns the neutral cwd outside a separate live command-group anchor. The anchor remains alive until output closes; abort, budget exhaustion, overflow, provider disposal or parent IPC disconnect makes the surviving reaper terminate only that live anchor's group. The reaper waits for group close before removing the cwd, so active writers cannot race deletion, including after caller death. No released PID/PGID or arbitrary process name is used. The caller also performs exact-path cleanup when alive. There is no retry or stale-result cache. Automatic recovery after an internal reaper/anchor itself is externally SIGKILLed or suffers a native crash is outside this lifecycle guarantee; an external intervention can leave command processes or temporary files and is never reported as an installed/absent inventory.
86
+ The Codex flag was checked through this exact CLI's public `features list`; it excludes remote directory reads. Passing the version floor is necessary but not sufficient: both JSON shapes must validate. Claude uses the official installed array's `id`, user `scope`, enabled/version and load diagnostics. Other project-only target rows are unknown for this reader. Codex uses the official `installed` array and its qualified identity, installed/enabled/version. The exact target marketplace must match the registered HTTPS source in marketplace list; Codex's per-install `marketplaceSource` must also be present, supported and consistent. A local/different/unreadable source, duplicate/conflicting target or malformed/truncated/empty raw output is unknown. Only a complete valid clean inventory with no target in the observed user scope is not-installed. A known install may have version null; consumers must show version unknown. Official load/payload issues produce incomplete, not successful readiness. No plugin MCP/hook definitions, unrelated inventory or CLI raw diagnostics are returned.
87
+
88
+ Commands run with an empty stdin in a disposable neutral cwd, only named HOME/config/platform paths, only the selected inherited/recovered absolute PATH plus allowlisted manager and standard XDG directory variables, no inherited model/API keys, proxy/auth/askpass/Node preload, and disabled Git credential helpers. The verified Node and selected executable directories prefix the selected PATH; retain its manager binary directories, so an official asdf shim can resolve `asdf`. Preserve existing XDG_CONFIG_HOME/CACHE_HOME/DATA_HOME/STATE_HOME/RUNTIME_DIR and CONFIG_DIRS/DATA_DIRS; absent variables stay absent and retain manager OS defaults. The supervisor never overwrites XDG paths with empty temporary directories. Only TMPDIR/TMP/TEMP and cwd use the owned disposable directory. Manager paths additionally include ASDF_CONFIG_FILE, MISE_CONFIG_DIR/GLOBAL_CONFIG_FILE/CACHE_DIR/STATE_DIR alongside the earlier manager-home keys; credentials, proxy, Node preloads and askpass remain excluded. Recovered shell manager/XDG paths follow the same allowlist; there is no fallback to a different installation. No model/install/upgrade/marketplace refresh command runs. On POSIX, a detached IPC reaper owns the neutral cwd outside a separate live command-group anchor. The anchor remains alive until output closes; abort, budget exhaustion, overflow, provider disposal or parent IPC disconnect makes the surviving reaper terminate only that live anchor's group. The reaper waits for group close before removing the cwd, so active writers cannot race deletion, including after caller death. No released PID/PGID or arbitrary process name is used. The caller also performs exact-path cleanup when alive. On Windows the same live anchor/reaper ownership uses the OS taskkill tree operation for its precise owned anchor; no process-name match or command-shell execution. Windows/Linux runtime evidence is separate from macOS fixture execution. There is no retry or stale-result cache. Automatic recovery after an internal reaper/anchor itself is externally SIGKILLed or suffers a native crash is outside this lifecycle guarantee; an external intervention can leave command processes or temporary files and is never reported as an installed/absent inventory.
80
89
 
81
90
  ## Refusals, unknown reasons, and lifecycle
82
91
 
@@ -86,7 +95,7 @@ Per-scope unknown reasons always include a safe code/message/action:
86
95
 
87
96
  - `CORE_SUITE_DECLARATION_MISSING`, `CORE_SUITE_DECLARATION_INVALID`, `CORE_SUITE_READ_FAILED`, `CORE_SUITE_DECLARATION_UNKNOWN`; incomplete: `CORE_SUITE_CURRENT_INCOMPLETE`, plus Core blocked codes.
88
97
  - `OTHER_RUNTIME_NOT_OBSERVED`, `CODEX_DESKTOP_SCOPE_UNOBSERVABLE`.
89
- - `AGENT_READER_NOT_CONFIGURED`, `AGENT_READER_CONFIG_INVALID`, `AGENT_READER_VERSION_UNSUPPORTED`, `AGENT_READER_PLATFORM_UNSUPPORTED`, `AGENT_READER_NODE_REQUIRED`.
98
+ - `AGENT_READER_VERSION_INVALID`, `AGENT_READER_VERSION_UNSUPPORTED`, `AGENT_READER_NODE_REQUIRED`, `AGENT_SHELL_ENV_UNAVAILABLE`, `AGENT_READER_ENTRYPOINT_UNAVAILABLE`, `AGENT_READER_DISCOVERY_INVALID`.
90
99
  - `AGENT_PROFILE_UNAVAILABLE`, `AGENT_READER_UNAVAILABLE`, `AGENT_READER_PERMISSION_DENIED`, `AGENT_READER_TIMEOUT`, `AGENT_READER_OUTPUT_LIMIT`, `AGENT_READER_DIAGNOSTIC`, `AGENT_READER_CLEANUP_FAILED`, `AGENT_READER_FAILED`.
91
100
  - `AGENT_INVENTORY_INVALID`, `AGENT_INVENTORY_CONFLICT`, `AGENT_SCOPE_UNOBSERVABLE`, `AGENT_MARKETPLACE_SOURCE_UNAVAILABLE`, `AGENT_MARKETPLACE_SOURCE_MISMATCH`.
92
101
  - An installed record with unobserved version uses `AGENT_VERSION_UNAVAILABLE`; incomplete uses `AGENT_PLUGIN_LOAD_FAILED` / `AGENT_PLUGIN_PAYLOAD_MISSING`.
@@ -96,3 +105,5 @@ Start each explicit detection by clearing previous observations. Cancel fetch wh
96
105
  Official command references: [Claude Code plugin commands](https://code.claude.com/docs/en/plugins/cli-reference), [Codex CLI plugin reference](https://learn.chatgpt.com/docs/cli/reference). [OpenAI local plugin scope documentation](https://developers.openai.com/plugins/build/plugins) describes multiple client scopes; CLI installation is not used here as proof of the desktop App.
97
106
 
98
107
  For DSH/HanaMesh runtime observations, versionKind is suite-bundle and version is the installed @hanamesh/hanamesh version. Component rows retain their physical versions. Agent distribution observations retain their existing semantics.
108
+
109
+ R115 regression fixtures execute real non-TTY Bash, asdf 0.20.2 official shim and mise 2026.11.4 with default/custom/shell-defined global XDG selection. Tool provenance, hashes and the explicit test-only binary inputs are in source `tests/fixtures/installation-managers/README.md`; binaries do not enter the npm package.