@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.
- package/CHANGELOG.md +87 -0
- package/Dockerfile +3 -0
- package/README.md +148 -11
- package/package.json +3 -1
- package/packages/claude-code/package.json +1 -1
- package/packages/claude-code/src/main.ts +4 -4
- package/packages/claude-code/src/wrapper.ts +3 -2
- package/packages/cli/dist/hub-cli.js +19334 -7849
- package/packages/cli/package.json +11 -0
- package/packages/cli/src/acting.ts +70 -0
- package/packages/cli/src/args.ts +91 -0
- package/packages/cli/src/bootstrap/adapters/archive-stream.ts +276 -0
- package/packages/cli/src/bootstrap/adapters/claude/README.md +63 -0
- package/packages/cli/src/bootstrap/adapters/claude/adapter.ts +250 -0
- package/packages/cli/src/bootstrap/adapters/claude/index.ts +42 -0
- package/packages/cli/src/bootstrap/adapters/claude/node.ts +99 -0
- package/packages/cli/src/bootstrap/adapters/claude/openpgp.ts +221 -0
- package/packages/cli/src/bootstrap/adapters/claude/ports.ts +44 -0
- package/packages/cli/src/bootstrap/adapters/claude/source.ts +160 -0
- package/packages/cli/src/bootstrap/adapters/claude/verify.ts +165 -0
- package/packages/cli/src/bootstrap/adapters/codex/archive.ts +103 -0
- package/packages/cli/src/bootstrap/adapters/codex/index.ts +60 -0
- package/packages/cli/src/bootstrap/adapters/codex/plan.ts +129 -0
- package/packages/cli/src/bootstrap/adapters/codex/sigstore.ts +298 -0
- package/packages/cli/src/bootstrap/adapters/codex/source.ts +102 -0
- package/packages/cli/src/bootstrap/adapters/codex/verify.ts +298 -0
- package/packages/cli/src/bootstrap/adapters/inflate.ts +433 -0
- package/packages/cli/src/bootstrap/adapters/node-services/control.ts +392 -0
- package/packages/cli/src/bootstrap/adapters/node-services/index.ts +23 -0
- package/packages/cli/src/bootstrap/adapters/node-services/launch.ts +223 -0
- package/packages/cli/src/bootstrap/adapters/node-services/plan.ts +222 -0
- package/packages/cli/src/bootstrap/adapters/node-services/render.ts +183 -0
- package/packages/cli/src/bootstrap/adapters/node-services/validate.ts +197 -0
- package/packages/cli/src/bootstrap/adapters/opencode/README.md +204 -0
- package/packages/cli/src/bootstrap/adapters/opencode/apply.ts +205 -0
- package/packages/cli/src/bootstrap/adapters/opencode/archive.ts +91 -0
- package/packages/cli/src/bootstrap/adapters/opencode/config.ts +212 -0
- package/packages/cli/src/bootstrap/adapters/opencode/index.ts +81 -0
- package/packages/cli/src/bootstrap/adapters/opencode/plan.ts +266 -0
- package/packages/cli/src/bootstrap/adapters/opencode/verify.ts +208 -0
- package/packages/cli/src/bootstrap/adapters/readiness.ts +19 -0
- package/packages/cli/src/bootstrap/ansible/index.ts +2 -0
- package/packages/cli/src/bootstrap/ansible/render.ts +279 -0
- package/packages/cli/src/bootstrap/contracts/actions.ts +470 -0
- package/packages/cli/src/bootstrap/contracts/artifact.ts +323 -0
- package/packages/cli/src/bootstrap/contracts/canonical.ts +54 -0
- package/packages/cli/src/bootstrap/contracts/credentials.ts +108 -0
- package/packages/cli/src/bootstrap/contracts/discovery.ts +215 -0
- package/packages/cli/src/bootstrap/contracts/envelope.ts +143 -0
- package/packages/cli/src/bootstrap/contracts/exec.ts +74 -0
- package/packages/cli/src/bootstrap/contracts/harness.ts +202 -0
- package/packages/cli/src/bootstrap/contracts/index.ts +28 -0
- package/packages/cli/src/bootstrap/contracts/journal.ts +198 -0
- package/packages/cli/src/bootstrap/contracts/paths.ts +91 -0
- package/packages/cli/src/bootstrap/contracts/plan.ts +155 -0
- package/packages/cli/src/bootstrap/contracts/platform.ts +62 -0
- package/packages/cli/src/bootstrap/contracts/preview.ts +190 -0
- package/packages/cli/src/bootstrap/contracts/profile.ts +105 -0
- package/packages/cli/src/bootstrap/contracts/secrets.ts +52 -0
- package/packages/cli/src/bootstrap/contracts/validate.ts +212 -0
- package/packages/cli/src/bootstrap/discovery/discover.ts +146 -0
- package/packages/cli/src/bootstrap/discovery/executables.ts +134 -0
- package/packages/cli/src/bootstrap/discovery/host.ts +96 -0
- package/packages/cli/src/bootstrap/discovery/index.ts +6 -0
- package/packages/cli/src/bootstrap/discovery/node-probe.ts +97 -0
- package/packages/cli/src/bootstrap/discovery/probe.ts +41 -0
- package/packages/cli/src/bootstrap/discovery/util.ts +13 -0
- package/packages/cli/src/bootstrap/enrollment/enroll.ts +226 -0
- package/packages/cli/src/bootstrap/enrollment/index.ts +7 -0
- package/packages/cli/src/bootstrap/enrollment/policy.ts +54 -0
- package/packages/cli/src/bootstrap/enrollment/result.ts +63 -0
- package/packages/cli/src/bootstrap/enrollment/status.ts +119 -0
- package/packages/cli/src/bootstrap/enrollment/store.ts +173 -0
- package/packages/cli/src/bootstrap/preview/index.ts +5 -0
- package/packages/cli/src/bootstrap/preview/links.ts +31 -0
- package/packages/cli/src/bootstrap/preview/plan.ts +242 -0
- package/packages/cli/src/bootstrap/preview/preview.ts +104 -0
- package/packages/cli/src/bootstrap/preview/render.ts +30 -0
- package/packages/cli/src/bootstrap/profiles/audit.ts +87 -0
- package/packages/cli/src/bootstrap/profiles/catalog.ts +49 -0
- package/packages/cli/src/bootstrap/profiles/desired.ts +96 -0
- package/packages/cli/src/bootstrap/profiles/drift.ts +286 -0
- package/packages/cli/src/bootstrap/profiles/history.ts +263 -0
- package/packages/cli/src/bootstrap/profiles/index.ts +11 -0
- package/packages/cli/src/bootstrap/profiles/upgrade.ts +276 -0
- package/packages/cli/src/bootstrap/profiles/versions.ts +50 -0
- package/packages/cli/src/bootstrap/release/README.md +115 -0
- package/packages/cli/src/bootstrap/release/assets.ts +39 -0
- package/packages/cli/src/bootstrap/release/cli.ts +332 -0
- package/packages/cli/src/bootstrap/release/harness-pins.json +94 -0
- package/packages/cli/src/bootstrap/release/manifest.ts +118 -0
- package/packages/cli/src/bootstrap/release/pins.ts +192 -0
- package/packages/cli/src/bootstrap/release/text-assets.d.ts +9 -0
- package/packages/cli/src/bootstrap/release/trust-root.json +13 -0
- package/packages/cli/src/bootstrap/release/trust-root.ts +39 -0
- package/packages/cli/src/bootstrap/release/verify.ts +88 -0
- package/packages/cli/src/bootstrap/transaction/builtin.ts +312 -0
- package/packages/cli/src/bootstrap/transaction/engine.ts +994 -0
- package/packages/cli/src/bootstrap/transaction/executor.ts +112 -0
- package/packages/cli/src/bootstrap/transaction/fs.ts +194 -0
- package/packages/cli/src/bootstrap/transaction/index.ts +12 -0
- package/packages/cli/src/bootstrap/transaction/json-merge.ts +403 -0
- package/packages/cli/src/bootstrap/transaction/lock.ts +132 -0
- package/packages/cli/src/bootstrap/transaction/resolve.ts +48 -0
- package/packages/cli/src/bootstrap/transaction/result.ts +60 -0
- package/packages/cli/src/bootstrap/transaction/state.ts +88 -0
- package/packages/cli/src/bootstrap/transaction/store.ts +189 -0
- package/packages/cli/src/bootstrap/transaction/stream.ts +254 -0
- package/packages/cli/src/cli.ts +814 -0
- package/packages/cli/src/credentials.ts +75 -0
- package/packages/cli/src/decisions.ts +106 -0
- package/packages/cli/src/format.ts +185 -0
- package/packages/cli/src/grants.ts +105 -0
- package/packages/cli/src/http.ts +86 -0
- package/packages/cli/src/launches.ts +157 -0
- package/packages/cli/src/login.ts +160 -0
- package/packages/cli/src/main.ts +63 -0
- package/packages/cli/src/mcp.ts +879 -0
- package/packages/cli/src/messaging.ts +108 -0
- package/packages/cli/src/node.ts +279 -0
- package/packages/cli/src/openshell.ts +166 -0
- package/packages/cli/src/setup/commands.ts +809 -0
- package/packages/cli/src/setup/contributors.ts +171 -0
- package/packages/cli/src/setup/dispatch.ts +74 -0
- package/packages/cli/src/setup/launch-driver.ts +109 -0
- package/packages/cli/src/setup/runtime.ts +456 -0
- package/packages/cli/src/setup/spoke-probe.ts +120 -0
- package/packages/cli/src/setup/state.ts +244 -0
- package/packages/cli/src/setup/user-bin.ts +39 -0
- package/packages/cli/src/spoke-services.ts +109 -0
- package/packages/cli/src/watch.ts +28 -0
- package/packages/codex/package.json +1 -1
- package/packages/codex/src/main.ts +3 -2
- package/packages/codex/src/spoke.ts +4 -1
- package/packages/codex/src/wrapper.ts +7 -3
- package/packages/node/package.json +1 -1
- package/packages/node/src/harnesses.ts +39 -4
- package/packages/node/src/run.ts +1 -1
- package/packages/preset/package.json +1 -1
- package/packages/protocol/package.json +1 -1
- package/packages/protocol/src/harness-events.ts +2 -2
- package/packages/protocol/src/index.ts +70 -7
- package/packages/runtime-openshell/package.json +1 -1
- package/packages/runtime-sdk/package.json +1 -1
- package/packages/sdk/package.json +1 -1
- package/packages/sdk/src/client.ts +55 -4
- package/packages/sdk/src/errors.ts +1 -0
- package/packages/sdk/src/sse.ts +4 -1
- package/packages/sdk/src/types.ts +59 -0
- package/packages/spoke-sdk/package.json +1 -1
- package/packages/spoke-sdk/src/enroll.ts +26 -4
- package/src/central/agent/app-tokens.ts +27 -17
- package/src/central/agent/archive-inactive.ts +66 -0
- package/src/central/agent/directory-routes.ts +155 -12
- package/src/central/agent/directory.ts +1 -1
- package/src/central/agent/errors.ts +2 -0
- package/src/central/agent/event-cursor.ts +49 -0
- package/src/central/agent/event-writers.ts +181 -0
- package/src/central/agent/events-routes.ts +148 -0
- package/src/central/agent/events.ts +277 -0
- package/src/central/agent/names.ts +1 -1
- package/src/central/agent/read-routes.ts +32 -36
- package/src/central/agent/routes.ts +14 -11
- package/src/central/agent/watch.ts +65 -174
- package/src/central/agent/webhook-routes.ts +10 -2
- package/src/central/agent/webhooks.ts +49 -5
- package/src/central/auth.ts +127 -1
- package/src/central/backend/index.ts +3 -0
- package/src/central/backend/types.ts +2 -0
- package/src/central/http.ts +116 -32
- package/src/central/mcp-http.ts +116 -0
- package/src/central/push.ts +14 -1
- package/src/central/registry.ts +34 -5
- package/src/central/store.ts +60 -0
- package/src/config.ts +70 -7
- package/src/index.ts +90 -91
- package/src/spoke/launchers.ts +109 -3
- package/src/spoke/spoke.ts +8 -4
- package/src/spoke/timeline.ts +59 -3
- package/src/spoke/tracker.ts +59 -6
- package/src/status.ts +97 -6
- package/web/dist/apple-touch-icon.png +0 -0
- package/web/dist/assets/index-Dm8OmkaO.css +1 -0
- package/web/dist/assets/index-FUJbj_Du.js +20 -0
- package/web/dist/favicon.ico +0 -0
- package/web/dist/icon-192.png +0 -0
- package/web/dist/icon-512.png +0 -0
- package/web/dist/icon-maskable-512.png +0 -0
- package/web/dist/icon.svg +5 -5
- package/web/dist/index.html +10 -4
- package/web/dist/manifest.webmanifest +2 -2
- package/web/dist/sw.js +4 -1
- package/web/dist/assets/index-D9zMbxSg.css +0 -1
- 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
|
+
})
|