@huynle/hub 0.7.1 → 0.7.3

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 (194) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/Dockerfile +3 -0
  3. package/README.md +148 -11
  4. package/package.json +3 -1
  5. package/packages/claude-code/package.json +1 -1
  6. package/packages/claude-code/src/main.ts +4 -4
  7. package/packages/claude-code/src/wrapper.ts +3 -2
  8. package/packages/cli/dist/hub-cli.js +19334 -7849
  9. package/packages/cli/package.json +11 -0
  10. package/packages/cli/src/acting.ts +70 -0
  11. package/packages/cli/src/args.ts +91 -0
  12. package/packages/cli/src/bootstrap/adapters/archive-stream.ts +276 -0
  13. package/packages/cli/src/bootstrap/adapters/claude/README.md +63 -0
  14. package/packages/cli/src/bootstrap/adapters/claude/adapter.ts +250 -0
  15. package/packages/cli/src/bootstrap/adapters/claude/index.ts +42 -0
  16. package/packages/cli/src/bootstrap/adapters/claude/node.ts +99 -0
  17. package/packages/cli/src/bootstrap/adapters/claude/openpgp.ts +221 -0
  18. package/packages/cli/src/bootstrap/adapters/claude/ports.ts +44 -0
  19. package/packages/cli/src/bootstrap/adapters/claude/source.ts +160 -0
  20. package/packages/cli/src/bootstrap/adapters/claude/verify.ts +165 -0
  21. package/packages/cli/src/bootstrap/adapters/codex/archive.ts +103 -0
  22. package/packages/cli/src/bootstrap/adapters/codex/index.ts +60 -0
  23. package/packages/cli/src/bootstrap/adapters/codex/plan.ts +129 -0
  24. package/packages/cli/src/bootstrap/adapters/codex/sigstore.ts +298 -0
  25. package/packages/cli/src/bootstrap/adapters/codex/source.ts +102 -0
  26. package/packages/cli/src/bootstrap/adapters/codex/verify.ts +298 -0
  27. package/packages/cli/src/bootstrap/adapters/inflate.ts +433 -0
  28. package/packages/cli/src/bootstrap/adapters/node-services/control.ts +392 -0
  29. package/packages/cli/src/bootstrap/adapters/node-services/index.ts +23 -0
  30. package/packages/cli/src/bootstrap/adapters/node-services/launch.ts +223 -0
  31. package/packages/cli/src/bootstrap/adapters/node-services/plan.ts +222 -0
  32. package/packages/cli/src/bootstrap/adapters/node-services/render.ts +183 -0
  33. package/packages/cli/src/bootstrap/adapters/node-services/validate.ts +197 -0
  34. package/packages/cli/src/bootstrap/adapters/opencode/README.md +204 -0
  35. package/packages/cli/src/bootstrap/adapters/opencode/apply.ts +205 -0
  36. package/packages/cli/src/bootstrap/adapters/opencode/archive.ts +91 -0
  37. package/packages/cli/src/bootstrap/adapters/opencode/config.ts +212 -0
  38. package/packages/cli/src/bootstrap/adapters/opencode/index.ts +81 -0
  39. package/packages/cli/src/bootstrap/adapters/opencode/plan.ts +266 -0
  40. package/packages/cli/src/bootstrap/adapters/opencode/verify.ts +208 -0
  41. package/packages/cli/src/bootstrap/adapters/readiness.ts +19 -0
  42. package/packages/cli/src/bootstrap/ansible/index.ts +2 -0
  43. package/packages/cli/src/bootstrap/ansible/render.ts +279 -0
  44. package/packages/cli/src/bootstrap/contracts/actions.ts +470 -0
  45. package/packages/cli/src/bootstrap/contracts/artifact.ts +323 -0
  46. package/packages/cli/src/bootstrap/contracts/canonical.ts +54 -0
  47. package/packages/cli/src/bootstrap/contracts/credentials.ts +108 -0
  48. package/packages/cli/src/bootstrap/contracts/discovery.ts +215 -0
  49. package/packages/cli/src/bootstrap/contracts/envelope.ts +143 -0
  50. package/packages/cli/src/bootstrap/contracts/exec.ts +74 -0
  51. package/packages/cli/src/bootstrap/contracts/harness.ts +202 -0
  52. package/packages/cli/src/bootstrap/contracts/index.ts +28 -0
  53. package/packages/cli/src/bootstrap/contracts/journal.ts +198 -0
  54. package/packages/cli/src/bootstrap/contracts/paths.ts +91 -0
  55. package/packages/cli/src/bootstrap/contracts/plan.ts +155 -0
  56. package/packages/cli/src/bootstrap/contracts/platform.ts +62 -0
  57. package/packages/cli/src/bootstrap/contracts/preview.ts +190 -0
  58. package/packages/cli/src/bootstrap/contracts/profile.ts +105 -0
  59. package/packages/cli/src/bootstrap/contracts/secrets.ts +52 -0
  60. package/packages/cli/src/bootstrap/contracts/validate.ts +212 -0
  61. package/packages/cli/src/bootstrap/discovery/discover.ts +146 -0
  62. package/packages/cli/src/bootstrap/discovery/executables.ts +134 -0
  63. package/packages/cli/src/bootstrap/discovery/host.ts +96 -0
  64. package/packages/cli/src/bootstrap/discovery/index.ts +6 -0
  65. package/packages/cli/src/bootstrap/discovery/node-probe.ts +97 -0
  66. package/packages/cli/src/bootstrap/discovery/probe.ts +41 -0
  67. package/packages/cli/src/bootstrap/discovery/util.ts +13 -0
  68. package/packages/cli/src/bootstrap/enrollment/enroll.ts +226 -0
  69. package/packages/cli/src/bootstrap/enrollment/index.ts +7 -0
  70. package/packages/cli/src/bootstrap/enrollment/policy.ts +54 -0
  71. package/packages/cli/src/bootstrap/enrollment/result.ts +63 -0
  72. package/packages/cli/src/bootstrap/enrollment/status.ts +119 -0
  73. package/packages/cli/src/bootstrap/enrollment/store.ts +173 -0
  74. package/packages/cli/src/bootstrap/preview/index.ts +5 -0
  75. package/packages/cli/src/bootstrap/preview/links.ts +31 -0
  76. package/packages/cli/src/bootstrap/preview/plan.ts +242 -0
  77. package/packages/cli/src/bootstrap/preview/preview.ts +104 -0
  78. package/packages/cli/src/bootstrap/preview/render.ts +30 -0
  79. package/packages/cli/src/bootstrap/profiles/audit.ts +87 -0
  80. package/packages/cli/src/bootstrap/profiles/catalog.ts +49 -0
  81. package/packages/cli/src/bootstrap/profiles/desired.ts +96 -0
  82. package/packages/cli/src/bootstrap/profiles/drift.ts +286 -0
  83. package/packages/cli/src/bootstrap/profiles/history.ts +263 -0
  84. package/packages/cli/src/bootstrap/profiles/index.ts +11 -0
  85. package/packages/cli/src/bootstrap/profiles/upgrade.ts +276 -0
  86. package/packages/cli/src/bootstrap/profiles/versions.ts +50 -0
  87. package/packages/cli/src/bootstrap/release/README.md +115 -0
  88. package/packages/cli/src/bootstrap/release/assets.ts +39 -0
  89. package/packages/cli/src/bootstrap/release/cli.ts +332 -0
  90. package/packages/cli/src/bootstrap/release/harness-pins.json +94 -0
  91. package/packages/cli/src/bootstrap/release/manifest.ts +118 -0
  92. package/packages/cli/src/bootstrap/release/pins.ts +192 -0
  93. package/packages/cli/src/bootstrap/release/text-assets.d.ts +9 -0
  94. package/packages/cli/src/bootstrap/release/trust-root.json +13 -0
  95. package/packages/cli/src/bootstrap/release/trust-root.ts +39 -0
  96. package/packages/cli/src/bootstrap/release/verify.ts +88 -0
  97. package/packages/cli/src/bootstrap/transaction/builtin.ts +312 -0
  98. package/packages/cli/src/bootstrap/transaction/engine.ts +994 -0
  99. package/packages/cli/src/bootstrap/transaction/executor.ts +112 -0
  100. package/packages/cli/src/bootstrap/transaction/fs.ts +194 -0
  101. package/packages/cli/src/bootstrap/transaction/index.ts +12 -0
  102. package/packages/cli/src/bootstrap/transaction/json-merge.ts +403 -0
  103. package/packages/cli/src/bootstrap/transaction/lock.ts +132 -0
  104. package/packages/cli/src/bootstrap/transaction/resolve.ts +48 -0
  105. package/packages/cli/src/bootstrap/transaction/result.ts +60 -0
  106. package/packages/cli/src/bootstrap/transaction/state.ts +88 -0
  107. package/packages/cli/src/bootstrap/transaction/store.ts +189 -0
  108. package/packages/cli/src/bootstrap/transaction/stream.ts +254 -0
  109. package/packages/cli/src/cli.ts +814 -0
  110. package/packages/cli/src/credentials.ts +75 -0
  111. package/packages/cli/src/decisions.ts +106 -0
  112. package/packages/cli/src/format.ts +185 -0
  113. package/packages/cli/src/grants.ts +105 -0
  114. package/packages/cli/src/http.ts +86 -0
  115. package/packages/cli/src/launches.ts +157 -0
  116. package/packages/cli/src/login.ts +160 -0
  117. package/packages/cli/src/main.ts +63 -0
  118. package/packages/cli/src/mcp.ts +879 -0
  119. package/packages/cli/src/messaging.ts +108 -0
  120. package/packages/cli/src/node.ts +279 -0
  121. package/packages/cli/src/openshell.ts +166 -0
  122. package/packages/cli/src/setup/commands.ts +809 -0
  123. package/packages/cli/src/setup/contributors.ts +171 -0
  124. package/packages/cli/src/setup/dispatch.ts +74 -0
  125. package/packages/cli/src/setup/launch-driver.ts +109 -0
  126. package/packages/cli/src/setup/runtime.ts +456 -0
  127. package/packages/cli/src/setup/spoke-probe.ts +120 -0
  128. package/packages/cli/src/setup/state.ts +244 -0
  129. package/packages/cli/src/setup/user-bin.ts +39 -0
  130. package/packages/cli/src/spoke-services.ts +109 -0
  131. package/packages/cli/src/watch.ts +28 -0
  132. package/packages/codex/package.json +1 -1
  133. package/packages/codex/src/main.ts +3 -2
  134. package/packages/codex/src/spoke.ts +4 -1
  135. package/packages/codex/src/wrapper.ts +7 -3
  136. package/packages/node/package.json +1 -1
  137. package/packages/node/src/harnesses.ts +39 -4
  138. package/packages/node/src/run.ts +1 -1
  139. package/packages/preset/package.json +1 -1
  140. package/packages/protocol/package.json +1 -1
  141. package/packages/protocol/src/harness-events.ts +2 -2
  142. package/packages/protocol/src/index.ts +70 -7
  143. package/packages/runtime-openshell/package.json +1 -1
  144. package/packages/runtime-sdk/package.json +1 -1
  145. package/packages/sdk/package.json +1 -1
  146. package/packages/sdk/src/client.ts +55 -4
  147. package/packages/sdk/src/errors.ts +1 -0
  148. package/packages/sdk/src/sse.ts +4 -1
  149. package/packages/sdk/src/types.ts +59 -0
  150. package/packages/spoke-sdk/package.json +1 -1
  151. package/packages/spoke-sdk/src/enroll.ts +26 -4
  152. package/src/central/agent/app-tokens.ts +27 -17
  153. package/src/central/agent/archive-inactive.ts +66 -0
  154. package/src/central/agent/directory-routes.ts +155 -12
  155. package/src/central/agent/directory.ts +1 -1
  156. package/src/central/agent/errors.ts +2 -0
  157. package/src/central/agent/event-cursor.ts +49 -0
  158. package/src/central/agent/event-writers.ts +181 -0
  159. package/src/central/agent/events-routes.ts +148 -0
  160. package/src/central/agent/events.ts +277 -0
  161. package/src/central/agent/names.ts +1 -1
  162. package/src/central/agent/read-routes.ts +32 -36
  163. package/src/central/agent/routes.ts +14 -11
  164. package/src/central/agent/watch.ts +65 -174
  165. package/src/central/agent/webhook-routes.ts +10 -2
  166. package/src/central/agent/webhooks.ts +49 -5
  167. package/src/central/auth.ts +127 -1
  168. package/src/central/backend/index.ts +3 -0
  169. package/src/central/backend/types.ts +2 -0
  170. package/src/central/http.ts +116 -32
  171. package/src/central/mcp-http.ts +116 -0
  172. package/src/central/push.ts +14 -1
  173. package/src/central/registry.ts +34 -5
  174. package/src/central/store.ts +60 -0
  175. package/src/config.ts +70 -7
  176. package/src/index.ts +90 -91
  177. package/src/spoke/launchers.ts +109 -3
  178. package/src/spoke/spoke.ts +8 -4
  179. package/src/spoke/timeline.ts +59 -3
  180. package/src/spoke/tracker.ts +59 -6
  181. package/src/status.ts +97 -6
  182. package/web/dist/apple-touch-icon.png +0 -0
  183. package/web/dist/assets/index-Dm8OmkaO.css +1 -0
  184. package/web/dist/assets/index-FUJbj_Du.js +20 -0
  185. package/web/dist/favicon.ico +0 -0
  186. package/web/dist/icon-192.png +0 -0
  187. package/web/dist/icon-512.png +0 -0
  188. package/web/dist/icon-maskable-512.png +0 -0
  189. package/web/dist/icon.svg +5 -5
  190. package/web/dist/index.html +10 -4
  191. package/web/dist/manifest.webmanifest +2 -2
  192. package/web/dist/sw.js +4 -1
  193. package/web/dist/assets/index-D9zMbxSg.css +0 -1
  194. package/web/dist/assets/index-db1z4duA.js +0 -20
@@ -0,0 +1,298 @@
1
+ // Codex verification (HarnessAdapter.verify) and its explicit capability advertisement. Readiness comes only from
2
+ // real probes run through injected, typed capabilities (no shell, no generic exec):
3
+ //
4
+ // version-exec ProbeRunner runs the fixed word list [<binary>, "--version"] against the resolved binary (hub's
5
+ // current version, or what a shim resolves to, never just the shim) and must print the expected
6
+ // version.
7
+ // api-health AppServerLauncher starts `<binary> app-server --listen stdio://` (the same app-server the Codex
8
+ // spoke uses) and the spoke's own openAppServer handshake (initialize, initialized) must succeed.
9
+ // provider-auth over that connection, account/read (refreshToken: true) must report an account, and a live
10
+ // request must succeed: for a ChatGPT account account/rateLimits/read, which reaches OpenAI's
11
+ // backend; other account types need an injected providerProbe (a bounded turn, say) and are not
12
+ // ready without one. Authentication stays with the user: hub asks the app-server and never reads
13
+ // ~/.codex; a missing login says to run `codex login` on the machine.
14
+ // spoke-connected SpokeStatus (the hub's view of this machine's Codex spoke) must report it connected.
15
+ //
16
+ // Probes run with a minimal environment (PATH, HOME, CODEX_HOME, locale): never the hub token or a provider key. A
17
+ // probe's detail is one short line with emails and credential-shaped words removed. codexCapabilities advertises
18
+ // Codex as ready only when isHarnessReady holds and its app-server answered, each probe fresh.
19
+ import { execFile } from "node:child_process"
20
+ import { openAppServer } from "../../../../../codex/src/app-server.ts"
21
+ import type { Channel } from "../../../../../codex/src/jsonrpc.ts"
22
+ import { CODEX_APP_SERVER_ARGS } from "../../../../../codex/src/spoke.ts"
23
+ import { spawnStdioChannel } from "../../../../../codex/src/stdio.ts"
24
+ import {
25
+ compareVersions,
26
+ executionContextOf,
27
+ harnessReadiness,
28
+ isHarnessReady,
29
+ isSecretValue,
30
+ PROBE_MAX_AGE_MS,
31
+ rootPathIn,
32
+ SEMVER,
33
+ type AdapterContext,
34
+ type DiscoverySnapshot,
35
+ type HarnessVerification,
36
+ type Probe,
37
+ type ProbeKind,
38
+ } from "../../contracts/index.ts"
39
+ import type { ActionExecutor } from "../../transaction/executor.ts"
40
+ import { txFail, txOk } from "../../transaction/result.ts"
41
+ import { passesAsPending, type ReadinessGate } from "../readiness.ts"
42
+
43
+ export interface ProbeResult {
44
+ /** null: it could not be started (or was killed). */
45
+ readonly exitCode: number | null
46
+ readonly stdout: string
47
+ readonly error?: string
48
+ }
49
+
50
+ /** Runs one fixed word list (never a shell string), bounded in time and output. */
51
+ export interface ProbeRunner {
52
+ run(argv: readonly string[], options: { readonly env: Record<string, string>; readonly timeoutMs: number; readonly signal?: AbortSignal }): Promise<ProbeResult>
53
+ }
54
+
55
+ /** Starts `<binary> app-server --listen stdio://` and gives its stdio as a channel. */
56
+ export interface AppServerLauncher {
57
+ start(binary: string, env: Record<string, string>): { readonly channel: Channel; stop(): void }
58
+ }
59
+
60
+ /** The hub's view of this machine's Codex spoke (HBS-13: the hub API, authenticated as this machine). */
61
+ export interface SpokeStatus {
62
+ codex(): Promise<{ readonly connected: boolean; readonly detail?: string }>
63
+ }
64
+
65
+ export interface CodexProbes {
66
+ readonly runner: ProbeRunner
67
+ readonly appServer: AppServerLauncher
68
+ readonly spoke: SpokeStatus
69
+ /** The binary to probe (codexBinaryPath), or undefined when there is none. */
70
+ readonly binary: () => string | undefined
71
+ /** hubctl's version, as the app-server client's. */
72
+ readonly clientVersion: string
73
+ /** The version the plan made current; another version fails version-exec. */
74
+ readonly expectedVersion?: string
75
+ /** Per probe; default 20 s. */
76
+ readonly timeoutMs?: number
77
+ /** A live provider check for account types without one through the app-server (apiKey, amazonBedrock, gcp). */
78
+ readonly providerProbe?: (account: { readonly type: string }) => Promise<{ readonly ok: boolean; readonly detail?: string }>
79
+ }
80
+
81
+ const DEFAULT_TIMEOUT_MS = 20_000
82
+ const PASSED_ENV = ["PATH", "HOME", "CODEX_HOME", "LANG", "LC_ALL", "TMPDIR"] as const
83
+ const VERSION = /\b(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)\b/
84
+
85
+ /** The environment a probe gets: only what Codex needs to find itself and its home. */
86
+ export function probeEnv(env: AdapterContext["env"]): Record<string, string> {
87
+ const out: Record<string, string> = {}
88
+ for (const key of PASSED_ENV) if (env[key] !== undefined) out[key] = env[key]!
89
+ return out
90
+ }
91
+
92
+ /** One short line, without emails or anything credential-shaped. */
93
+ function line(text: string): string {
94
+ const words = text
95
+ .replace(/[\r\n\t]+/g, " ")
96
+ .replace(/[^\s@]+@[^\s@]+\.[^\s@]+/g, "[email]")
97
+ .split(/\s+/)
98
+ .map((w) => (isSecretValue(w) || /^(?:sk|mt|hat|hlt1|hst1)[-_.]/i.test(w) ? "[redacted]" : w))
99
+ .join(" ")
100
+ .trim()
101
+ return words.length > 160 ? `${words.slice(0, 157)}...` : words || "(no detail)"
102
+ }
103
+
104
+ const message = (cause: unknown) => (cause instanceof Error ? cause.message : String(cause))
105
+
106
+ function withTimeout<T>(promise: Promise<T>, ms: number, what: string): Promise<T> {
107
+ let timer: ReturnType<typeof setTimeout> | undefined
108
+ const timeout = new Promise<T>((_, reject) => {
109
+ timer = setTimeout(() => reject(new Error(`${what} did not answer within ${ms} ms`)), ms)
110
+ })
111
+ return Promise.race([promise, timeout]).finally(() => clearTimeout(timer))
112
+ }
113
+
114
+ export async function verifyCodex(ctx: AdapterContext, p: CodexProbes): Promise<HarnessVerification> {
115
+ const probes: Probe[] = []
116
+ const record = (kind: ProbeKind, ok: boolean, detail: string) => probes.push({ kind, ok, observedAt: ctx.now(), detail: line(detail) })
117
+ const timeoutMs = p.timeoutMs ?? DEFAULT_TIMEOUT_MS
118
+ const env = probeEnv(ctx.env)
119
+ const binary = p.binary()
120
+
121
+ if (!binary) {
122
+ for (const kind of ["version-exec", "api-health", "provider-auth", "spoke-connected"] as const) record(kind, false, "no Codex binary to run (not installed)")
123
+ return { harness: "codex", probes }
124
+ }
125
+ // 1. The binary runs and is the expected version.
126
+ let runs = false
127
+ try {
128
+ const r = await p.runner.run([binary, "--version"], { env, timeoutMs, ...(ctx.signal ? { signal: ctx.signal } : {}) })
129
+ const version = VERSION.exec(r.stdout)?.[1]
130
+ runs = r.exitCode === 0 && version !== undefined
131
+ if (!runs) record("version-exec", false, `${binary} --version ${r.exitCode === null ? `did not run (${r.error ?? "no exit code"})` : `exited ${r.exitCode}`}`)
132
+ else if (p.expectedVersion && SEMVER.test(version!) && compareVersions(version!, p.expectedVersion) !== 0) record("version-exec", false, `codex runs ${version}, but ${p.expectedVersion} is current`)
133
+ else record("version-exec", true, `codex-cli ${version}`)
134
+ } catch (cause) {
135
+ record("version-exec", false, `${binary} --version did not run (${message(cause)})`)
136
+ }
137
+
138
+ // 2 and 3. Its app-server answers; its provider account is ready.
139
+ if (!runs) {
140
+ record("api-health", false, "skipped: the Codex binary does not run")
141
+ record("provider-auth", false, "skipped: the Codex binary does not run")
142
+ } else await appServerProbes(binary, env, timeoutMs, p, record)
143
+
144
+ // 4. The hub sees this machine's Codex spoke.
145
+ try {
146
+ const s = await withTimeout(p.spoke.codex(), timeoutMs, "the hub")
147
+ record("spoke-connected", s.connected, s.detail ?? (s.connected ? "the hub sees the Codex spoke" : "the hub does not see this machine's Codex spoke (is hub codex-spoke running?)"))
148
+ } catch (cause) {
149
+ record("spoke-connected", false, `the hub could not be asked (${message(cause)})`)
150
+ }
151
+ return { harness: "codex", probes }
152
+ }
153
+
154
+ async function appServerProbes(binary: string, env: Record<string, string>, timeoutMs: number, p: CodexProbes, record: (kind: ProbeKind, ok: boolean, detail: string) => void) {
155
+ let handle: ReturnType<AppServerLauncher["start"]>
156
+ try {
157
+ handle = p.appServer.start(binary, env)
158
+ } catch (cause) {
159
+ record("api-health", false, `the app-server did not start (${message(cause)})`)
160
+ record("provider-auth", false, "skipped: no app-server")
161
+ return
162
+ }
163
+ try {
164
+ let conn: Awaited<ReturnType<typeof openAppServer>>
165
+ try {
166
+ conn = await withTimeout(openAppServer(handle.channel, { name: "hub-verify", clientVersion: p.clientVersion, timeoutMs }), timeoutMs + 1000, "the app-server")
167
+ record("api-health", true, "the app-server answered initialize")
168
+ } catch (cause) {
169
+ record("api-health", false, `the app-server did not initialize (${message(cause)})`)
170
+ record("provider-auth", false, "skipped: no app-server")
171
+ return
172
+ }
173
+ try {
174
+ const read = (await conn.peer.request("account/read", { refreshToken: true }, { timeoutMs })) as { account?: { type?: unknown } | null; requiresOpenaiAuth?: unknown } | null
175
+ const type = typeof read?.account?.type === "string" ? read.account.type : undefined
176
+ if (!type) {
177
+ record("provider-auth", false, read?.requiresOpenaiAuth === false ? "no OpenAI account is needed, but no live provider check is available; readiness needs one" : "Codex is not logged in: run `codex login` on this machine")
178
+ return
179
+ }
180
+ if (type === "chatgpt") {
181
+ await conn.peer.request("account/rateLimits/read", undefined, { timeoutMs })
182
+ record("provider-auth", true, "chatgpt account ready (live rate-limit request answered)")
183
+ } else if (p.providerProbe) {
184
+ const r = await withTimeout(p.providerProbe({ type }), timeoutMs, "the provider check")
185
+ record("provider-auth", r.ok, `${type} account ${r.ok ? "ready" : "not ready"}${r.detail ? ` (${r.detail})` : ""}`)
186
+ } else record("provider-auth", false, `${type} account found, but no live provider check is configured for it`)
187
+ } catch (cause) {
188
+ record("provider-auth", false, `the provider check failed (${message(cause)})`)
189
+ }
190
+ } finally {
191
+ handle.stop()
192
+ }
193
+ }
194
+
195
+ /** What hub advertises for Codex: each capability only from a fresh passing probe. */
196
+ export interface CodexCapabilities {
197
+ readonly harness: "codex"
198
+ readonly ready: boolean
199
+ readonly version?: string
200
+ readonly appServer: boolean
201
+ readonly provider?: string
202
+ readonly spoke: boolean
203
+ /** The oldest probe it rests on. */
204
+ readonly observedAt?: number
205
+ }
206
+
207
+ export function codexCapabilities(v: HarnessVerification, now: number): CodexCapabilities {
208
+ const fresh = (kind: ProbeKind) => {
209
+ const ps = v.probes.filter((x) => x.kind === kind)
210
+ return ps.length > 0 && ps.every((x) => x.ok && x.observedAt <= now + 60_000 && now - x.observedAt <= PROBE_MAX_AGE_MS) ? ps[ps.length - 1] : undefined
211
+ }
212
+ const version = fresh("version-exec")?.detail && VERSION.exec(fresh("version-exec")!.detail!)?.[1]
213
+ const provider = fresh("provider-auth")?.detail?.split(" ")[0]
214
+ const appServer = fresh("api-health") !== undefined
215
+ const observed = v.probes.map((x) => x.observedAt)
216
+ return {
217
+ harness: "codex",
218
+ ready: v.harness === "codex" && isHarnessReady(v, now) && appServer,
219
+ ...(version ? { version } : {}),
220
+ appServer,
221
+ ...(provider ? { provider } : {}),
222
+ spoke: fresh("spoke-connected") !== undefined,
223
+ ...(observed.length > 0 ? { observedAt: Math.min(...observed) } : {}),
224
+ }
225
+ }
226
+
227
+ /**
228
+ * The binary verification runs: hub's current version (data:hub/harnesses/codex/current/bin/codex) when hub
229
+ * installs or installed Codex, else what the discovered shim resolves to (contract §3.10), else its path.
230
+ */
231
+ export function codexBinaryPath(snapshot: DiscoverySnapshot, options: { readonly preferHub: boolean }): string | undefined {
232
+ const found = snapshot.harnesses.find((h) => h.id === "codex")
233
+ const data = snapshot.roots.find((r) => r.id === "data")
234
+ if (data && (options.preferHub || found?.installMethod === "hub")) return `${rootPathIn(data, executionContextOf(snapshot))}/hub/harnesses/codex/current/bin/codex`
235
+ if (!found?.installed || !found.path) return undefined
236
+ const shim = snapshot.topology.shims.find((s) => s.path === found.path)
237
+ return shim?.resolvesTo ?? found.path
238
+ }
239
+
240
+ /**
241
+ * The verify-harness executor for Codex: passes when Codex is advertised ready, or (without requireReady) when it
242
+ * only waits on the user (contract v1.2); refuses (harness_not_ready) otherwise. The app-server (api-health) is
243
+ * always required: it is an integrity probe.
244
+ */
245
+ export function codexVerifyExecutor(
246
+ options: {
247
+ readonly verify: (ctx: AdapterContext) => Promise<HarnessVerification>
248
+ readonly context: () => AdapterContext
249
+ /** Receives each verification (the capability advertisement, the apply report). */
250
+ readonly onVerified?: (verification: HarnessVerification) => void
251
+ } & ReadinessGate,
252
+ ): ActionExecutor<"verify-harness"> {
253
+ const notCodex = (harness: string) => txFail("internal", `the Codex verifier cannot verify ${harness}`)
254
+ return {
255
+ kind: "verify-harness",
256
+ plan: () => ({ class: "external" }),
257
+ check: async (action) => (action.harness === "codex" ? txOk(undefined) : notCodex(action.harness)),
258
+ change: async (action) =>
259
+ action.harness !== "codex"
260
+ ? notCodex(action.harness)
261
+ : txOk({
262
+ type: "effect",
263
+ perform: async () => {
264
+ const ctx = options.context()
265
+ const v = await options.verify(ctx)
266
+ options.onVerified?.(v)
267
+ const readiness = harnessReadiness(v, ctx.now(), [...action.probes, ...(action.probes.includes("api-health") ? [] : ["api-health" as const])])
268
+ options.onReadiness?.(structuredClone(readiness))
269
+ if (codexCapabilities(v, ctx.now()).ready) return txOk(undefined)
270
+ if (v.harness === "codex" && passesAsPending(readiness, options)) return txOk(undefined)
271
+ const failing = v.probes.filter((x) => !x.ok).map((x) => `${x.kind}: ${x.detail ?? "failed"}`)
272
+ return txFail("harness_not_ready", `Codex is not ready (${failing.join("; ") || "a probe is missing or stale"})`)
273
+ },
274
+ }),
275
+ }
276
+ }
277
+
278
+ /** A ProbeRunner over execFile: the word list as is (no shell), bounded output and time. */
279
+ export const nodeProbeRunner: ProbeRunner = {
280
+ run: (argv, options) =>
281
+ new Promise((resolve) => {
282
+ const [file, ...args] = argv
283
+ if (!file) return resolve({ exitCode: null, stdout: "", error: "empty argv" })
284
+ execFile(file, args, { env: options.env, timeout: options.timeoutMs, maxBuffer: 64 * 1024, shell: false, windowsHide: true, ...(options.signal ? { signal: options.signal } : {}) }, (error, stdout) => {
285
+ if (!error) return resolve({ exitCode: 0, stdout: String(stdout) })
286
+ const code = (error as NodeJS.ErrnoException & { code?: unknown }).code
287
+ resolve(typeof code === "number" ? { exitCode: code, stdout: String(stdout ?? "") } : { exitCode: null, stdout: String(stdout ?? ""), error: message(error) })
288
+ })
289
+ }),
290
+ }
291
+
292
+ /** Starts the app-server with the packages/codex stdio channel (`argsBefore`: tests run a fake server script). */
293
+ export const stdioAppServerLauncher = (options: { readonly argsBefore?: readonly string[] } = {}): AppServerLauncher => ({
294
+ start(binary, env) {
295
+ const child = spawnStdioChannel({ command: binary, args: [...(options.argsBefore ?? []), ...CODEX_APP_SERVER_ARGS], env })
296
+ return { channel: child.channel, stop: () => child.channel.close() }
297
+ },
298
+ })