@hanamesh/dsh-app-host 0.6.25 → 0.6.26

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,3 +1,5 @@
1
+ 0.6.26 候选:Codex 官方 GitHub 市场 URL 的 `.git` 后缀按精确仓库身份归一;市场映射与每项安装来源独立校验,不同仓库、凭据、查询、fragment 仍拒绝。CD2 扩大只读覆盖:ChatGPT 桌面 App 内置 Codex CLI、SemVer 预发布版本(仍须不低于最低版本);官方用户资料目录不存在即判未安装;HanaMesh 客户端从磁盘读取 DSH 桌面版 profile(`~/.dsh/profiles/desktop`)给出 DSH 渠道已安装/未安装。检测不执行安装或配置写入,见[安装状态](docs/INSTALLATION_STATUS.md)。
2
+
1
3
  0.6.25 候选:本机 HTTPS 目录允许回环端口及 *.localhost,修复彩排种子初始化阻断检测监听;公网 HTTPS 非标准端口仍拒绝。Claude 官方 GitHub 市场清单按 repo 身份核对,保留不同来源拒绝。安装物回归命令见[安装状态](docs/INSTALLATION_STATUS.md),不以页面打开代替检测通过。
2
4
 
3
5
  PX3 跨平台候选:Windows 可以运行插件/公开服务、attach 应用、受控只读 CLI 与普通包安装;`owned` 应用仍在创建进程、数据目录及请求凭据之前明确拒绝 `PLATFORM_UNSUPPORTED`。Windows 单测保留真实 attach 的 HTTP/租约/网关断言,并验证 owned 的持久失败、无进程/文件写入及不消费凭据;这些断言不构成 Windows owned 运行支持。独立 Node 可用不扩大 owned 支持;受管 Node 准备仍遵守原平台限制。Windows 本地快照采用文件 flush 加同目录 rename,不承诺断电后目录项持久;Coding OAuth Gateway 文件源因缺私有 ACL 证明继续拒绝,HTTP 提供方不变。各平台实际结果以本分支 GitHub Actions 矩阵收据为准。
package/dist/dsh.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import {pathToFileURL} from 'node:url';
2
- import { createInstallationStatusReader } from './library/installation-status.js';
2
+ import { createInstallationStatusReader,officialDshDesktopProfile } from './library/installation-status.js';
3
3
  /**
4
4
  * DSH plugin entry for @hanamesh/dsh-app-host, using the public APIs audited in
5
5
  * official DSH 0.2.0-rc.2 and 0.2.1-alpha.2:
@@ -258,7 +258,7 @@ export async function apply(ctx, config) {
258
258
  };
259
259
  const identity=await detectClient({desktop});
260
260
  if(typeof ctx.logger?.info==='function')ctx.logger.info('hanamesh-app-host library: client %s (%s)',identity.client,identity.evidence);
261
- library=createLibraryService({domain:{global:libraryDomain.global,close:closeLibraryDomain},host,config:libraryConfig,dataRoot,ledgerReader:provisionApi?.ledger??(async()=>({schema:1,items:{}})),installer,client:identity.client,unavailable,suiteSourceService:()=>suiteSourceService,installationStatus:nodeStatusBinding(()=>nodeRuntime.executable,workerNode=>createInstallationStatusReader({client:identity.client,core:()=>coreService,workerNode})),logger:ctx.logger});
261
+ library=createLibraryService({domain:{global:libraryDomain.global,close:closeLibraryDomain},host,config:libraryConfig,dataRoot,ledgerReader:provisionApi?.ledger??(async()=>({schema:1,items:{}})),installer,client:identity.client,unavailable,suiteSourceService:()=>suiteSourceService,installationStatus:nodeStatusBinding(()=>nodeRuntime.executable,workerNode=>createInstallationStatusReader({client:identity.client,core:()=>coreService,workerNode,otherProfileDir:identity.client==='hanamesh'?()=>officialDshDesktopProfile(locations.profileDir):null})),logger:ctx.logger});
262
262
  own('library', () => library.close());
263
263
  await library.init();
264
264
  // rc.27: app bundles register plain `app.json` (no package name); the installed scan knows which npm package ships
@@ -3,7 +3,7 @@ export const INSTALLATION_STATUS_CAPABILITY:Readonly<{contract:'hanamesh.install
3
3
  export interface InstallationStatusRequest {contractVersion:'1';consent:'read-installations'}
4
4
  export interface InstallationObservation {
5
5
  channel:'hanamesh'|'dsh'|'claude'|'codex';
6
- scope:{id:'current-runtime'|'other-runtime'|'agent-cli'|'desktop-app';kind:'responding-runtime'|'unobserved-runtime'|'official-user-profile'|'desktop-app';label:string};
6
+ scope:{id:'current-runtime'|'other-runtime'|'agent-cli'|'desktop-app';kind:'responding-runtime'|'other-client'|'official-user-profile'|'desktop-app';label:string};
7
7
  status:'installed'|'not-installed'|'incomplete'|'unknown';version:string|null;versionKind:'suite-bundle'|'agent-distribution';
8
8
  components:readonly {name:string;version:string|null;active?:boolean;status?:string}[];
9
9
  identity:{pluginName:string;marketplace:string}|null;
@@ -22,7 +22,8 @@ export function installationEnvironment(source,platform=process.platform){
22
22
  const fail=code=>{throw new AppHostError(code,'The automatically discovered official installation reader could not complete.',{},503)};
23
23
  export const MINIMUM_READER_VERSIONS=Object.freeze({claude:'2.1.301',codex:'0.162.1'});
24
24
  export function readerVersionFailure(channel,output){
25
- const pattern=channel==='claude'?/^(\d+\.\d+\.\d+) \(Claude Code\)$/:/^codex-cli (\d+\.\d+\.\d+)$/;
25
+ // SemVer prerelease is allowed: desktop apps bundle prerelease CLIs (ChatGPT.app: codex-cli 0.167.0-alpha.3.2).
26
+ const pattern=channel==='claude'?/^(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?) \(Claude Code\)$/:/^codex-cli (\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/;
26
27
  const version=pattern.exec(output.trim())?.[1];
27
28
  if(!version||semver.valid(version)!==version)return 'AGENT_READER_VERSION_INVALID';
28
29
  return semver.gte(version,MINIMUM_READER_VERSIONS[channel])?null:'AGENT_READER_VERSION_UNSUPPORTED';
@@ -36,15 +36,26 @@ export async function readLoginEnvironment({home=homedir(),env=process.env,signa
36
36
  if(!value||typeof value!=='object'||Array.isArray(value)||typeof value.PATH!=='string'||!Object.entries(value).every(([key,path])=>INSTALLATION_ENV_KEYS.includes(key)&&typeof path==='string'&&!/[\x00-\x1f]/.test(path)))throw failure('AGENT_SHELL_ENV_UNAVAILABLE');
37
37
  return installationEnvironment(value);
38
38
  }
39
- export async function readLoginPath(options){return(await readLoginEnvironment(options)).PATH}
40
39
 
41
40
  /** Select once: inherited PATH, login PATH, then documented layouts; never try another CLI after execution. */
42
- export async function discoverOfficialReader({channel,home=process.env.HOME??process.env.USERPROFILE??homedir(),env=process.env,platform=process.platform,
43
- systemBinDirs=platform==='win32'?[]:['/opt/homebrew/bin','/usr/local/bin','/home/linuxbrew/.linuxbrew/bin','/usr/bin','/bin'],loginPath,signal,workerNode}={}){
41
+ /** The official user profile directory each CLI reads (CLAUDE_CONFIG_DIR / CODEX_HOME, else ~/.claude / ~/.codex). */
42
+ export function agentProfileDir({channel,home=process.env.HOME??process.env.USERPROFILE??homedir(),env=process.env}={}){
44
43
  if(!['claude','codex'].includes(channel)||!absolute(home))throw failure('AGENT_PROFILE_UNAVAILABLE');
45
- signal?.throwIfAborted();
46
44
  const configDir=env[channel==='claude'?'CLAUDE_CONFIG_DIR':'CODEX_HOME']??join(home,channel==='claude'?'.claude':'.codex');
47
45
  if(!absolute(configDir))throw failure('AGENT_PROFILE_UNAVAILABLE');
46
+ return configDir;
47
+ }
48
+ /** Read-only existence check: an absent profile directory holds no user-scope plugin; unreadable is not absence. */
49
+ export async function agentProfileState({channel,home,env}={}){
50
+ const dir=agentProfileDir({channel,...(home===undefined?{}:{home}),...(env===undefined?{}:{env})});
51
+ try{if(!(await stat(dir)).isDirectory())throw failure('AGENT_PROFILE_UNAVAILABLE');return 'present'}
52
+ catch(error){if(['ENOENT','ENOTDIR'].includes(error.code))return 'absent';throw error.code==='AGENT_PROFILE_UNAVAILABLE'?error:failure('AGENT_PROFILE_UNAVAILABLE')}
53
+ }
54
+ export async function discoverOfficialReader({channel,home=process.env.HOME??process.env.USERPROFILE??homedir(),env=process.env,platform=process.platform,
55
+ systemBinDirs=platform==='win32'?[]:['/opt/homebrew/bin','/usr/local/bin','/home/linuxbrew/.linuxbrew/bin','/usr/bin','/bin'],
56
+ applicationDirs=platform==='darwin'?['/Applications',join(home,'Applications')]:[],loginPath,signal,workerNode}={}){
57
+ const configDir=agentProfileDir({channel,home,env});
58
+ signal?.throwIfAborted();
48
59
  const appData=env.APPDATA??join(home,'AppData','Roaming'),local=env.LOCALAPPDATA??join(home,'AppData','Local');
49
60
  const runtimeEnv=installationEnvironment(env,platform);
50
61
  const reader={home,configDir,...AGENT_INSTALLATION_TARGET,...INSTALLATION_READ_BUDGET,runtimeEnv};
@@ -81,7 +92,9 @@ export async function discoverOfficialReader({channel,home=process.env.HOME??pro
81
92
  ...[env.npm_config_prefix,env.NPM_CONFIG_PREFIX,join(home,'.npm-global'),join(home,'.npm')].filter(absolute).map(root=>platform==='win32'?root:join(root,'bin')),
82
93
  env.PNPM_HOME,...(absolute(env.PNPM_HOME)?[join(env.PNPM_HOME,'bin')]:[]),join(home,'.local','share','pnpm'),join(home,'.local','share','pnpm','bin'),join(home,'Library','pnpm'),join(home,'Library','pnpm','bin'),join(home,'.yarn','bin'),join(home,'.config','yarn','global','node_modules','.bin'),
83
94
  join(env.VOLTA_HOME??join(home,'.volta'),'bin'),join(asdf,'shims'),join(mise,'shims'),
84
- ...(platform==='win32'?[join(appData,'npm'),join(local,'pnpm'),join(local,'Yarn','bin'),join(local,'Programs','OpenAI','Codex','bin'),join(local,'Microsoft','WinGet','Links'),join(local,'Volta','bin'),...(absolute(env.ProgramFiles)?[join(env.ProgramFiles,'nodejs')]:[])]:[]),...systemBinDirs];
95
+ ...(platform==='win32'?[join(appData,'npm'),join(local,'pnpm'),join(local,'Yarn','bin'),join(local,'Programs','OpenAI','Codex','bin'),join(local,'Microsoft','WinGet','Links'),join(local,'Volta','bin'),...(absolute(env.ProgramFiles)?[join(env.ProgramFiles,'nodejs')]:[])]:[]),...systemBinDirs,
96
+ // Desktop app bundles last, so a standalone CLI the user installed wins: ChatGPT.app ships the Codex CLI.
97
+ ...(channel==='codex'?applicationDirs.map(root=>join(root,'ChatGPT.app','Contents','Resources','codex-cli','bin')):[])];
85
98
  selected=await find(fixed);if(selected)return selected;
86
99
  const managed=[];
87
100
  for(const [root,suffix]of [[join(nvm,'versions','node'),['bin']],[env.NVM_HOME,[]],[join(fnm,'node-versions'),['installation',...(platform==='win32'?[]:['bin'])]],
@@ -1,9 +1,13 @@
1
1
  /** Read fresh public installation facts. No compatibility calculation, persistence or polling. */
2
2
  import semver from 'semver';
3
3
  import {AppHostError} from '../errors.js';
4
- import {SUITE_BUNDLE,createCoreSuiteAdapter,SUITE_PACKAGES} from './suite.js';
4
+ import {readFile} from 'node:fs/promises';
5
+ import {join} from 'node:path';
6
+ import {realpathSync} from 'node:fs';
7
+ import {homedir} from 'node:os';
8
+ import {SUITE_BUNDLE,createCoreSuiteAdapter,SUITE_PACKAGES,readSuiteStatus} from './suite.js';
5
9
  import {readerVersionFailure,validReader,runOfficialInstallationCommand} from './installation-cli.js';
6
- import {discoverOfficialReader} from './installation-discovery.js';
10
+ import {discoverOfficialReader,agentProfileState,AGENT_INSTALLATION_TARGET} from './installation-discovery.js';
7
11
  const object=x=>x!==null&&typeof x==='object'&&!Array.isArray(x);
8
12
  const exact=x=>typeof x==='string'&&semver.valid(x)===x?x:null;
9
13
  const reason=(code,message,action)=>({code,message,action});
@@ -17,10 +21,12 @@ const unknown=(row,code,message,action)=>({...row,status:'unknown',version:null,
17
21
  const agentBase=channel=>base(channel,'agent-cli','official-user-profile',channel==='claude'?'Claude Code 用户级插件(不覆盖项目或其他隔离资料)':'Codex CLI 本地市场用户资料(不代表桌面 App、项目或其他隔离资料)');
18
22
  const inventoryError=()=>{throw new AppHostError('AGENT_INVENTORY_INVALID','Official inventory shape is unsupported.',{},503)};
19
23
  const qualified=x=>typeof x==='string'&&/^[a-z0-9][a-z0-9._-]*@[a-z0-9][a-z0-9._-]*$/.test(x)&&x.length<=257;
24
+ function githubGitUrl(url){return typeof url==='string'&&/^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\.git$/.test(url)?url.slice(0,-4):url;}
25
+ function codexMarketplaceUrl(source){return source?.sourceType==='git'?githubGitUrl(source.source):source?.sourceType==='url'?source.source:null;}
20
26
  function claudeMarketplaceUrl(value){
21
27
  if(value.source==='github'&&typeof value.repo==='string'&&/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(value.repo))return `https://github.com/${value.repo}`;
22
28
  const url=['url','git'].includes(value.source)?value.url:null;
23
- return value.source==='git'&&typeof url==='string'&&/^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\.git$/.test(url)?url.slice(0,-4):url;
29
+ return value.source==='git'?githubGitUrl(url):url;
24
30
  }
25
31
  function parseInventory(channel,plugins,marketplaces,target){
26
32
  let rows,markets;
@@ -34,7 +40,7 @@ function parseInventory(channel,plugins,marketplaces,target){
34
40
  if(!markets.every(r=>object(r)&&typeof r.name==='string'&&r.name.length>0&&r.name.length<=128))inventoryError();
35
41
  const sourceRows=markets.filter(r=>r.name===target.marketplace);
36
42
  if(sourceRows.length>1)throw new AppHostError('AGENT_INVENTORY_CONFLICT','Conflicting marketplace identities.',{},503);
37
- if(sourceRows.length){const m=sourceRows[0],url=channel==='claude'?claudeMarketplaceUrl(m):(['git','url'].includes(m.marketplaceSource?.sourceType)?m.marketplaceSource.source:null);
43
+ if(sourceRows.length){const m=sourceRows[0],url=channel==='claude'?claudeMarketplaceUrl(m):codexMarketplaceUrl(m.marketplaceSource);
38
44
  if(url!==target.marketplaceUrl)throw new AppHostError('AGENT_MARKETPLACE_SOURCE_MISMATCH','The configured marketplace source is not the registered target.',{},503);
39
45
  }
40
46
  const id=`${target.pluginName}@${target.marketplace}`;
@@ -47,7 +53,7 @@ function parseInventory(channel,plugins,marketplaces,target){
47
53
  const installedSource=scoped[0].marketplaceSource;
48
54
  if(channel==='codex'){
49
55
  if(!installedSource||!['git','url'].includes(installedSource.sourceType)||typeof installedSource.source!=='string')throw new AppHostError('AGENT_MARKETPLACE_SOURCE_UNAVAILABLE','Installed marketplace source is not observable.',{},503);
50
- if(installedSource.source!==target.marketplaceUrl)throw new AppHostError('AGENT_MARKETPLACE_SOURCE_MISMATCH','Installed marketplace source conflicts with the registered target.',{},503);
56
+ if(codexMarketplaceUrl(installedSource)!==target.marketplaceUrl)throw new AppHostError('AGENT_MARKETPLACE_SOURCE_MISMATCH','Installed marketplace source conflicts with the registered target.',{},503);
51
57
  }
52
58
  const row=scoped[0],version=exact(channel==='claude'&&row.readFromFolder!==undefined?row.folderVersion:row.version);
53
59
  const failed=channel==='claude'?Boolean(row.errors?.length||row.notes?.length):!row.installed;
@@ -55,8 +61,27 @@ function parseInventory(channel,plugins,marketplaces,target){
55
61
  reason:failed?reason(channel==='claude'?'AGENT_PLUGIN_LOAD_FAILED':'AGENT_PLUGIN_PAYLOAD_MISSING','官方清单报告插件负载或加载问题。','在该官方客户端的插件详情中检查并修复。'):version===null?reason('AGENT_VERSION_UNAVAILABLE','已确认该作用域的安装记录,但实际版本未知。','在该官方客户端插件详情中核对实际版本。'):null};
56
62
  }
57
63
  const failureCodes=new Set(['AGENT_SHELL_ENV_UNAVAILABLE','AGENT_READER_DISCOVERY_INVALID','AGENT_READER_ENTRYPOINT_UNAVAILABLE','AGENT_READER_UNAVAILABLE','AGENT_READER_PERMISSION_DENIED','AGENT_READER_NODE_REQUIRED','AGENT_PROFILE_UNAVAILABLE','AGENT_READER_TIMEOUT','AGENT_READER_OUTPUT_LIMIT','AGENT_READER_CLEANUP_FAILED','AGENT_READER_DIAGNOSTIC','AGENT_READER_FAILED','AGENT_INVENTORY_INVALID','AGENT_INVENTORY_CONFLICT','AGENT_MARKETPLACE_SOURCE_MISMATCH','AGENT_MARKETPLACE_SOURCE_UNAVAILABLE','AGENT_SCOPE_UNOBSERVABLE']);
58
- /** Automatic current-user discovery only; `core` is the optional public protocol-1 Cordis service accessor. */
59
- export function createInstallationStatusReader({client='dsh',core=()=>undefined,workerNode,runCommand=runOfficialInstallationCommand,discoverReader=discoverOfficialReader}={}){
64
+ /** Physical on-disk DSH profile inventory: no @hanamesh/hanamesh dependency (or no profile) is authoritative absence. */
65
+ async function profileSuite(row,profileDir){
66
+ let profile;try{profile=JSON.parse(await readFile(join(profileDir,'package.json'),'utf8'))}
67
+ catch(error){if(['ENOENT','ENOTDIR'].includes(error.code))return{...row,status:'not-installed'};return unknown(row,'DSH_PROFILE_UNREADABLE','DSH 桌面版 profile 清单无法读取。','打开 DSH 确认其插件页可用后重新检测。')}
68
+ if(!object(profile)||!object(profile.dependencies))return unknown(row,'DSH_PROFILE_UNREADABLE','DSH 桌面版 profile 清单无法读取。','打开 DSH 确认其插件页可用后重新检测。');
69
+ if(!Object.hasOwn(profile.dependencies,SUITE_BUNDLE))return{...row,status:'not-installed'};
70
+ const value=await readSuiteStatus(profileDir);
71
+ const components=value.components.map(c=>({name:c.name,version:c.version,active:c.active}));
72
+ if(value.status==='installed'&&value.version)return{...row,status:'installed',version:value.version,components};
73
+ if(value.status==='unknown')return unknown(row,'DSH_PROFILE_UNREADABLE','DSH 桌面版 profile 清单无法读取。','打开 DSH 确认其插件页可用后重新检测。');
74
+ return{...row,status:'incomplete',version:value.version,components,reason:reason('DSH_PROFILE_SUITE_INCOMPLETE','DSH 桌面版 profile 中的 HanaMesh 套件缺组件或未启用。','在 DSH 插件页启用 HanaMesh,或按 DSH 安装说明重新安装总包后重启 DSH。')};
75
+ }
76
+ /** Official DSH desktop profile; only distinct, resolvable physical profiles may answer for another client. */
77
+ export function officialDshDesktopProfile(ownProfileDir,home=homedir()){
78
+ const dir=join(home,'.dsh','profiles','desktop');
79
+ if(typeof ownProfileDir!=='string'||!ownProfileDir)return null;
80
+ try{return realpathSync(ownProfileDir)===realpathSync(dir)?null:dir}catch{return null}
81
+ }
82
+ /** Automatic current-user discovery only; `core` is the optional public protocol-1 Cordis service accessor.
83
+ * `otherProfileDir`: a fresh selector of the official DSH desktop profile (HanaMesh client only); trusted fixtures may supply a path. */
84
+ export function createInstallationStatusReader({client='dsh',core=()=>undefined,workerNode,runCommand=runOfficialInstallationCommand,discoverReader=discoverOfficialReader,profileState=agentProfileState,otherProfileDir=null}={}){
60
85
  if(!['dsh','hanamesh'].includes(client))throw new TypeError('client must be dsh or hanamesh');
61
86
  const active=new Set();let closed=false;
62
87
  const suite=createCoreSuiteAdapter({core});
@@ -72,6 +97,7 @@ export function createInstallationStatusReader({client='dsh',core=()=>undefined,
72
97
  async function agent(channel,signal){
73
98
  const initial=agentBase(channel);let row=initial;
74
99
  try{
100
+ if(await abortable(profileState({channel}),signal)==='absent')return{...initial,status:'not-installed',identity:{pluginName:AGENT_INSTALLATION_TARGET.pluginName,marketplace:AGENT_INSTALLATION_TARGET.marketplace}};
75
101
  const config=await abortable(discoverReader({channel,signal,workerNode}),signal);
76
102
  if(!validReader(config))throw new AppHostError('AGENT_READER_DISCOVERY_INVALID','Invalid automatic reader.',{},503);
77
103
  row={...initial,identity:{pluginName:config.pluginName,marketplace:config.marketplace}};
@@ -85,11 +111,16 @@ export function createInstallationStatusReader({client='dsh',core=()=>undefined,
85
111
  return{...row,...parseInventory(channel,plugins,marketplaces,config)};
86
112
  }catch(error){if(signal.aborted||error.code==='INSTALLATION_STATUS_CANCELLED')throw interrupted();const code=failureCodes.has(error.code)?error.code:'AGENT_READER_FAILED';const messages={AGENT_READER_UNAVAILABLE:'未在当前用户的 PATH 或常见安装位置找到官方 CLI。',AGENT_SHELL_ENV_UNAVAILABLE:'登录 shell 的 PATH 未能读取。',AGENT_PROFILE_UNAVAILABLE:'当前用户的官方资料目录不存在或不可访问。',AGENT_READER_TIMEOUT:'本次官方只读检测超时。',AGENT_INVENTORY_INVALID:'官方清单 JSON 不符合受支持的格式。',AGENT_READER_DISCOVERY_INVALID:'自动发现的官方 CLI 信息无效。',AGENT_READER_ENTRYPOINT_UNAVAILABLE:'已发现 Windows 命令入口,但官方 npm 程序文件缺失。'};return unknown(row,code,messages[code]??'本次官方清单读取未能提供完整且一致的安装事实。','在终端确认官方 CLI 可运行,并在其用户级插件页面检查清单、权限和市场来源后重新检测。')}
87
113
  }
114
+ async function other(signal){
115
+ const profileDir=client==='hanamesh'?(typeof otherProfileDir==='function'?otherProfileDir():otherProfileDir):null;
116
+ if(profileDir)return abortable(profileSuite(base('dsh','other-runtime','other-client','DSH 桌面版 profile(磁盘只读清单;运行中的 DSH 由其自身监听回答)'),profileDir),signal);
117
+ return unknown(base(client==='dsh'?'hanamesh':'dsh','other-runtime','other-client','另一客户端 runtime(须由其自身监听回答)'),'OTHER_RUNTIME_NOT_OBSERVED','本监听未读取另一客户端的 runtime。','启动该客户端,并向其已广告的状态入口显式检测。');
118
+ }
88
119
  const read=async({signal}={})=>{
89
120
  if(closed||signal?.aborted)throw interrupted();const controller=new AbortController();active.add(controller);const combined=signal?AbortSignal.any([signal,controller.signal]):controller.signal;
90
121
  try{const current=await runtime(combined);const agents=await Promise.all(['claude','codex'].map(channel=>agent(channel,combined)));if(combined.aborted)throw interrupted();
91
122
  return{schemaVersion:1,client,checkedAt:new Date().toISOString(),observations:[current,
92
- unknown(base(client==='dsh'?'hanamesh':'dsh','other-runtime','unobserved-runtime','另一客户端 runtime(须由其自身监听回答)'),'OTHER_RUNTIME_NOT_OBSERVED','本监听未读取另一客户端的 runtime。','启动该客户端,并向其已广告的状态入口显式检测。'),...agents,
123
+ await other(combined),...agents,
93
124
  unknown(base('codex','desktop-app','desktop-app','Codex / ChatGPT 桌面 App(独立作用域)'),'CODEX_DESKTOP_SCOPE_UNOBSERVABLE','Codex CLI 清单不能证明桌面 App 的安装。','在桌面 App 的插件目录核对;需官方公开桌面状态协议后才可接入。')]};
94
125
  }finally{active.delete(controller)}
95
126
  };
package/docs/CONTRACT.md CHANGED
@@ -1,3 +1,5 @@
1
+ 0.6.26:Codex 官方 GitHub `git` 来源的精确 `.git` 后缀在市场映射与每项安装来源分别归一;只读覆盖扩大到 ChatGPT.app 内置 Codex CLI、预发布 CLI 版本、不存在的官方资料目录(未安装)和 HanaMesh 客户端读取的 DSH 桌面版磁盘 profile;`other-runtime` 的 kind 改名为 `other-client`。只读命令与来源冲突拒绝不变,见 INSTALLATION_STATUS.md。
2
+
1
3
  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
4
 
3
5
  # 公开契约 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 的读取)
@@ -37,7 +37,7 @@ interface InstallationStatus {
37
37
  interface InstallationObservation {
38
38
  channel:'hanamesh'|'dsh'|'claude'|'codex';
39
39
  scope:{id:'current-runtime'|'other-runtime'|'agent-cli'|'desktop-app';
40
- kind:'responding-runtime'|'unobserved-runtime'|'official-user-profile'|'desktop-app';label:string};
40
+ kind:'responding-runtime'|'other-client'|'official-user-profile'|'desktop-app';label:string};
41
41
  status:'installed'|'not-installed'|'incomplete'|'unknown';
42
42
  version:string|null;
43
43
  versionKind:'suite-bundle'|'agent-distribution';
@@ -69,6 +69,7 @@ Selection is deterministic: the first executable in the inherited absolute PATH
69
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
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
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
+ | Desktop app bundle (macOS, Codex only, searched last) | `/Applications` and `~/Applications` `ChatGPT.app/Contents/Resources/codex-cli/bin/codex` (the ChatGPT desktop app ships the Codex CLI; it reads the same `CODEX_HOME` / `~/.codex`). A standalone CLI in any earlier layout wins. |
72
73
 
73
74
  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.
74
75
 
@@ -78,12 +79,12 @@ Minimum CLI version and fixed syntax (no new upper bound):
78
79
 
79
80
  | Channel | Minimum version / accepted `--version` syntax | Fixed subsequent read argv |
80
81
  |---|---|---|
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` |
82
+ | Claude Code | `>=2.1.301`, `MAJOR.MINOR.PATCH[-prerelease] (Claude Code)` | `plugin list --json`; `plugin marketplace list --json` |
83
+ | Codex CLI | `>=0.162.1`, `codex-cli MAJOR.MINOR.PATCH[-prerelease]` (desktop bundles report e.g. `0.167.0-alpha.3.2`; SemVer ordering applies, so `0.162.1-alpha.1` is below the minimum) | `plugin list --json --disable remote_plugin`; `plugin marketplace list --json --disable remote_plugin` |
83
84
 
84
85
  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.
85
86
 
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
+ 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. For official GitHub `sourceType:'git'` records, the exact credential/query/fragment-free `https://github.com/owner/repository.git` spelling identifies the same repository as the registered suffix-free URL. Normalize both the marketplace mapping and the per-install source independently; no other URL rewrite, repository substitution or source fallback is accepted. 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
 
88
89
  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.
89
90
 
@@ -94,7 +95,11 @@ HTTP refusals: `HOST_DENIED`, `ORIGIN_DENIED`, `FRAME_CONTROL_DENIED`, `CREDENTI
94
95
  Per-scope unknown reasons always include a safe code/message/action:
95
96
 
96
97
  - `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.
97
- - `OTHER_RUNTIME_NOT_OBSERVED`, `CODEX_DESKTOP_SCOPE_UNOBSERVABLE`.
98
+ - `OTHER_RUNTIME_NOT_OBSERVED`, `CODEX_DESKTOP_SCOPE_UNOBSERVABLE`, `DSH_PROFILE_UNREADABLE`; incomplete: `DSH_PROFILE_SUITE_INCOMPLETE`.
99
+
100
+ Authoritative absence (read-only, no CLI execution): an official user profile directory (`CLAUDE_CONFIG_DIR` / `CODEX_HOME`, else `~/.claude` / `~/.codex`) that does not exist holds no user-scope plugin, so that channel is `not-installed` with the registered identity, whether or not the CLI is installed. An existing but inaccessible directory stays `AGENT_PROFILE_UNAVAILABLE`; an existing profile without a readable CLI stays `AGENT_READER_UNAVAILABLE`.
101
+
102
+ The HanaMesh client's `other-runtime` row (`kind:'other-client'`) reads the official DSH desktop profile on disk at the upstream default home (`~/.dsh/profiles/desktop`, `@deepseek-ai/dsh-home-paths` `.dsh`): after distinct physical profile identities have been established, a missing profile manifest or no `@hanamesh/hanamesh` dependency is `not-installed`; the bundle and all three members resolved at the bundle's exact versions and selected in `dsh.profile.bundles` is `installed` with the bundle version (a physical inventory, not that runtime's Core completeness); otherwise `incomplete`; unreadable JSON is unknown. Before every explicit read, resolve both the current profile and the target with `realpath`; the reused reader retains a selector, never a cached identity decision. The same physical directory (including symlink aliases), a missing current profile identity, or any identity-resolution failure keeps the row `unknown / OTHER_RUNTIME_NOT_OBSERVED`; no target inventory is read. A missing target directory also fails this identity check conservatively. A DSH listener never reports the HanaMesh client: that row is always `OTHER_RUNTIME_NOT_OBSERVED`.
98
103
  - `AGENT_READER_VERSION_INVALID`, `AGENT_READER_VERSION_UNSUPPORTED`, `AGENT_READER_NODE_REQUIRED`, `AGENT_SHELL_ENV_UNAVAILABLE`, `AGENT_READER_ENTRYPOINT_UNAVAILABLE`, `AGENT_READER_DISCOVERY_INVALID`.
99
104
  - `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`.
100
105
  - `AGENT_INVENTORY_INVALID`, `AGENT_INVENTORY_CONFLICT`, `AGENT_SCOPE_UNOBSERVABLE`, `AGENT_MARKETPLACE_SOURCE_UNAVAILABLE`, `AGENT_MARKETPLACE_SOURCE_MISMATCH`.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "package": {
3
3
  "name": "@hanamesh/dsh-app-host",
4
- "version": "0.6.25",
4
+ "version": "0.6.26",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "source": "git+https://github.com/yzsnstotz/hanamesh-plugin-app-host.git",
7
7
  "purpose": "Application ownership, view leases, Router forwarding, and application library for official DSH"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanamesh/dsh-app-host",
3
- "version": "0.6.25",
3
+ "version": "0.6.26",
4
4
  "description": "HanaMesh application ownership, durable view leases, and application library for official DSH 0.2.x",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",