@huynle/hub 0.7.1 → 0.7.2

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 (185) hide show
  1. package/CHANGELOG.md +70 -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/runtime-openshell/package.json +1 -1
  142. package/packages/runtime-sdk/package.json +1 -1
  143. package/packages/sdk/package.json +1 -1
  144. package/packages/sdk/src/client.ts +55 -4
  145. package/packages/sdk/src/errors.ts +1 -0
  146. package/packages/sdk/src/sse.ts +4 -1
  147. package/packages/sdk/src/types.ts +59 -0
  148. package/packages/spoke-sdk/package.json +1 -1
  149. package/packages/spoke-sdk/src/enroll.ts +26 -4
  150. package/src/central/agent/app-tokens.ts +27 -17
  151. package/src/central/agent/archive-inactive.ts +66 -0
  152. package/src/central/agent/directory-routes.ts +155 -12
  153. package/src/central/agent/directory.ts +1 -1
  154. package/src/central/agent/errors.ts +2 -0
  155. package/src/central/agent/event-cursor.ts +49 -0
  156. package/src/central/agent/event-writers.ts +181 -0
  157. package/src/central/agent/events-routes.ts +148 -0
  158. package/src/central/agent/events.ts +277 -0
  159. package/src/central/agent/names.ts +1 -1
  160. package/src/central/agent/read-routes.ts +32 -36
  161. package/src/central/agent/routes.ts +14 -11
  162. package/src/central/agent/watch.ts +65 -174
  163. package/src/central/agent/webhook-routes.ts +10 -2
  164. package/src/central/agent/webhooks.ts +49 -5
  165. package/src/central/auth.ts +127 -1
  166. package/src/central/backend/index.ts +3 -0
  167. package/src/central/backend/types.ts +2 -0
  168. package/src/central/http.ts +115 -32
  169. package/src/central/mcp-http.ts +116 -0
  170. package/src/central/store.ts +53 -0
  171. package/src/config.ts +70 -7
  172. package/src/index.ts +90 -91
  173. package/src/spoke/launchers.ts +109 -3
  174. package/web/dist/apple-touch-icon.png +0 -0
  175. package/web/dist/assets/index-DWnMhXk9.js +20 -0
  176. package/web/dist/assets/index-DmUXEnw9.css +1 -0
  177. package/web/dist/favicon.ico +0 -0
  178. package/web/dist/icon-192.png +0 -0
  179. package/web/dist/icon-512.png +0 -0
  180. package/web/dist/icon-maskable-512.png +0 -0
  181. package/web/dist/icon.svg +5 -5
  182. package/web/dist/index.html +10 -4
  183. package/web/dist/manifest.webmanifest +2 -2
  184. package/web/dist/assets/index-D9zMbxSg.css +0 -1
  185. package/web/dist/assets/index-db1z4duA.js +0 -20
@@ -0,0 +1,298 @@
1
+ // Offline verification of the sigstore bundles openai/codex publishes next to its Linux release assets
2
+ // (`<asset>.sigstore`; legacy cosign bundles: `{base64Signature, cert, rekorBundle: {SignedEntryTimestamp,
3
+ // Payload}}`), with node:crypto only (no sigstore dependency, no network). The upstream evidence is optional extra
4
+ // evidence on top of hub's pinned digest (docs/bootstrap-contracts.md §3.6.1, §5, Q4), so the result is one of:
5
+ //
6
+ // verified every check below passed;
7
+ // unavailable this hubctl cannot judge the bundle: it carries no sigstore trusted root, the certificate comes from
8
+ // a CA it does not know, the entry is in a log it does not know, or the bundle is in a format it does
9
+ // not read (sigstore bundle v0.3, say). The pinned digest still vouches for the bytes;
10
+ // failed a check it can run failed: the chain, the time, the identity, the signature over the artifact, the
11
+ // Rekor SET, or the Rekor body's binding to this artifact, signature and certificate. The contract
12
+ // rejects the artifact (evidence_failed).
13
+ //
14
+ // Checks, in order: the leaf certificate chains to a trusted Fulcio CA (each signature verified) and is a code
15
+ // signing certificate; the Rekor entry's integrated time lies inside the leaf's validity and the CA's and log's
16
+ // validFor; the leaf's SAN is exactly the expected workflow identity and its Fulcio OIDC-issuer extension
17
+ // (1.3.6.1.4.1.57264.1.8, or the older .1.1) the expected issuer; the signature verifies over the artifact bytes;
18
+ // the SET verifies with the trusted log key over the canonical {body, integratedTime, logID, logIndex}; and the
19
+ // hashedrekord body names this artifact's sha256, this signature and this certificate.
20
+ //
21
+ // Not checked (limitations, documented in the report): embedded SCTs (certificate transparency) and Merkle
22
+ // inclusion proofs (legacy bundles carry only the SET).
23
+ import { createHash, createPublicKey, createVerify, verify, X509Certificate, type KeyObject } from "node:crypto"
24
+ import type { EvidenceResult } from "../../contracts/index.ts"
25
+ import { txFail, txOk, type TxResult } from "../../transaction/result.ts"
26
+ import { CHUNK_BYTES, type ByteReader } from "../../transaction/stream.ts"
27
+
28
+ export interface SigstoreAuthority {
29
+ /** Issuing CA first, root last. */
30
+ readonly chain: readonly X509Certificate[]
31
+ readonly validFor: Validity
32
+ }
33
+
34
+ export interface SigstoreLog {
35
+ /** Hex sha256 of the log's DER public key (Rekor's log ID). */
36
+ readonly logID: string
37
+ readonly key: KeyObject
38
+ readonly validFor: Validity
39
+ }
40
+
41
+ interface Validity {
42
+ readonly start: number
43
+ readonly end?: number
44
+ }
45
+
46
+ /** The trust material: Fulcio CAs and Rekor logs, from sigstore's trusted_root.json. */
47
+ export interface SigstoreTrust {
48
+ readonly authorities: readonly SigstoreAuthority[]
49
+ readonly logs: readonly SigstoreLog[]
50
+ }
51
+
52
+ /** Who must have signed: the certificate's SAN and its OIDC issuer. */
53
+ export interface SigstoreIdentity {
54
+ readonly san: string
55
+ readonly issuer: string
56
+ }
57
+
58
+ /** openai/codex signs its release assets from its rust-release workflow, at the release tag, through GitHub Actions OIDC. */
59
+ export const codexSigstoreIdentity = (version: string): SigstoreIdentity => ({
60
+ san: `https://github.com/openai/codex/.github/workflows/rust-release.yml@refs/tags/rust-v${version}`,
61
+ issuer: "https://token.actions.githubusercontent.com",
62
+ })
63
+
64
+ const CODE_SIGNING = "1.3.6.1.5.5.7.3.3"
65
+ const OIDC_ISSUER_V2 = "1.3.6.1.4.1.57264.1.8"
66
+ const OIDC_ISSUER_V1 = "1.3.6.1.4.1.57264.1.1"
67
+
68
+ const isObj = (v: unknown): v is Record<string, any> => typeof v === "object" && v !== null && !Array.isArray(v)
69
+ const b64 = (s: unknown): Buffer | undefined => (typeof s === "string" && /^[A-Za-z0-9+/]*={0,2}$/.test(s) ? Buffer.from(s, "base64") : undefined)
70
+
71
+ function validity(v: unknown): Validity | undefined {
72
+ if (!isObj(v)) return undefined
73
+ const start = Date.parse(v.start)
74
+ const end = v.end === undefined ? undefined : Date.parse(v.end)
75
+ if (!Number.isFinite(start) || (end !== undefined && !Number.isFinite(end))) return undefined
76
+ return end === undefined ? { start } : { start, end }
77
+ }
78
+ const within = (t: number, v: Validity) => t >= v.start && (v.end === undefined || t <= v.end)
79
+
80
+ /** Reads sigstore's trusted_root.json (mediaType application/vnd.dev.sigstore.trustedroot+json). */
81
+ export function trustFromTrustedRoot(json: unknown): TxResult<SigstoreTrust> {
82
+ if (!isObj(json) || !Array.isArray(json.certificateAuthorities) || !Array.isArray(json.tlogs)) return txFail("invalid", "not a sigstore trusted root")
83
+ const authorities: SigstoreAuthority[] = []
84
+ const logs: SigstoreLog[] = []
85
+ try {
86
+ for (const ca of json.certificateAuthorities) {
87
+ const raw = isObj(ca) && isObj(ca.certChain) && Array.isArray(ca.certChain.certificates) ? ca.certChain.certificates : []
88
+ const chain = raw.map((c: unknown) => b64(isObj(c) ? c.rawBytes : undefined)).filter((b: Buffer | undefined): b is Buffer => b !== undefined && b.byteLength > 0)
89
+ const v = validity(isObj(ca) ? ca.validFor : undefined)
90
+ if (chain.length > 0 && chain.length === raw.length && v) authorities.push({ chain: chain.map((b: Buffer) => new X509Certificate(b)), validFor: v })
91
+ }
92
+ for (const log of json.tlogs) {
93
+ const raw = isObj(log) && isObj(log.publicKey) ? b64(log.publicKey.rawBytes) : undefined
94
+ const id = isObj(log) && isObj(log.logId) ? b64(log.logId.keyId) : undefined
95
+ const v = validity(isObj(log) && isObj(log.publicKey) ? log.publicKey.validFor : undefined)
96
+ if (raw && id && v) logs.push({ logID: id.toString("hex"), key: createPublicKey({ key: raw, format: "der", type: "spki" }), validFor: v })
97
+ }
98
+ } catch (cause) {
99
+ return txFail("invalid", `the sigstore trusted root has an unreadable key or certificate: ${cause instanceof Error ? cause.message : String(cause)}`)
100
+ }
101
+ if (authorities.length === 0 || logs.length === 0) return txFail("invalid", "the sigstore trusted root has no usable CA or transparency log")
102
+ return txOk({ authorities, logs })
103
+ }
104
+
105
+ // ---------- DER, just enough to read certificate extensions ----------
106
+
107
+ interface Tlv {
108
+ readonly tag: number
109
+ readonly body: Uint8Array
110
+ readonly end: number
111
+ }
112
+
113
+ function tlv(buf: Uint8Array, at: number): Tlv {
114
+ const tag = buf[at]!
115
+ let len = buf[at + 1]!
116
+ let start = at + 2
117
+ if (len & 0x80) {
118
+ const n = len & 0x7f
119
+ if (n < 1 || n > 4) throw new Error("bad DER length")
120
+ len = 0
121
+ for (let i = 0; i < n; i++) len = len * 256 + buf[start + i]!
122
+ start += n
123
+ }
124
+ if (start + len > buf.byteLength) throw new Error("truncated DER")
125
+ return { tag, body: buf.subarray(start, start + len), end: start + len }
126
+ }
127
+
128
+ function children(buf: Uint8Array): Tlv[] {
129
+ const out: Tlv[] = []
130
+ for (let at = 0; at < buf.byteLength; ) {
131
+ const t = tlv(buf, at)
132
+ out.push(t)
133
+ at = t.end
134
+ }
135
+ return out
136
+ }
137
+
138
+ function oidText(body: Uint8Array): string {
139
+ const parts = [Math.floor(body[0]! / 40), body[0]! % 40]
140
+ let n = 0
141
+ for (const b of body.subarray(1)) {
142
+ n = n * 128 + (b & 0x7f)
143
+ if (!(b & 0x80)) {
144
+ parts.push(n)
145
+ n = 0
146
+ }
147
+ }
148
+ return parts.join(".")
149
+ }
150
+
151
+ /** The certificate's extensions, OID → value bytes (the content of the extnValue OCTET STRING). */
152
+ function extensions(cert: X509Certificate): Map<string, Uint8Array> {
153
+ const out = new Map<string, Uint8Array>()
154
+ const tbs = children(tlv(new Uint8Array(cert.raw), 0).body)[0]!
155
+ const wrapper = children(tbs.body).find((t) => t.tag === 0xa3)
156
+ if (!wrapper) return out
157
+ for (const ext of children(tlv(wrapper.body, 0).body)) {
158
+ const parts = children(ext.body)
159
+ const value = parts[parts.length - 1]
160
+ if (parts[0]?.tag === 0x06 && value?.tag === 0x04) out.set(oidText(parts[0].body), value.body)
161
+ }
162
+ return out
163
+ }
164
+
165
+ function oidcIssuer(cert: X509Certificate): string | undefined {
166
+ const ext = extensions(cert)
167
+ const v2 = ext.get(OIDC_ISSUER_V2)
168
+ if (v2) {
169
+ const s = tlv(v2, 0)
170
+ if (s.tag === 0x0c) return new TextDecoder().decode(s.body)
171
+ }
172
+ const v1 = ext.get(OIDC_ISSUER_V1)
173
+ return v1 ? new TextDecoder().decode(v1) : undefined
174
+ }
175
+
176
+ // ---------- the bundle ----------
177
+
178
+ interface LegacyBundle {
179
+ readonly signature: Buffer
180
+ readonly certPEM: string
181
+ readonly set: Buffer
182
+ readonly payload: { readonly body: string; readonly integratedTime: number; readonly logID: string; readonly logIndex: number }
183
+ }
184
+
185
+ function parseLegacy(bytes: Uint8Array): LegacyBundle | undefined {
186
+ let raw: unknown
187
+ try {
188
+ raw = JSON.parse(new TextDecoder().decode(bytes))
189
+ } catch {
190
+ return undefined
191
+ }
192
+ if (!isObj(raw) || !isObj(raw.rekorBundle) || !isObj(raw.rekorBundle.Payload)) return undefined
193
+ const p = raw.rekorBundle.Payload
194
+ const signature = b64(raw.base64Signature)
195
+ const cert = b64(raw.cert)
196
+ const set = b64(raw.rekorBundle.SignedEntryTimestamp)
197
+ if (!signature || !cert || !set || typeof p.body !== "string" || !Number.isSafeInteger(p.integratedTime) || typeof p.logID !== "string" || !Number.isSafeInteger(p.logIndex)) return undefined
198
+ return { signature, certPEM: cert.toString("utf8"), set, payload: { body: p.body, integratedTime: p.integratedTime, logID: p.logID, logIndex: p.logIndex } }
199
+ }
200
+
201
+ const result = (status: EvidenceResult["status"], detail: string): EvidenceResult => ({ kind: "sigstore", status, detail })
202
+ const failed = (why: string) => result("failed", `sigstore: ${why}`)
203
+ const unavailable = (why: string) => result("unavailable", `sigstore: ${why}`)
204
+
205
+ const issued = (child: X509Certificate, parent: X509Certificate) => Boolean(child.checkIssued(parent))
206
+
207
+ function verifySET(log: SigstoreLog, b: LegacyBundle): boolean {
208
+ // Rekor signs the canonical JSON of the entry (keys sorted, no whitespace).
209
+ const canonical = JSON.stringify({ body: b.payload.body, integratedTime: b.payload.integratedTime, logID: b.payload.logID, logIndex: b.payload.logIndex })
210
+ try {
211
+ return verify(log.key.asymmetricKeyType === "ed25519" ? null : "sha256", Buffer.from(canonical), log.key, b.set)
212
+ } catch {
213
+ return false
214
+ }
215
+ }
216
+
217
+ /** The artifact's bytes in chunks: a staged artifact (a ~150 MB Codex package) is never read whole. */
218
+ function* piecesOf(artifact: Uint8Array | ByteReader): Generator<Uint8Array> {
219
+ if (artifact instanceof Uint8Array) {
220
+ yield artifact
221
+ return
222
+ }
223
+ for (let at = 0; at < artifact.size; ) {
224
+ const piece = artifact.read(at, CHUNK_BYTES)
225
+ if (piece.byteLength === 0) return
226
+ at += piece.byteLength
227
+ yield piece
228
+ }
229
+ }
230
+
231
+ /** One sigstore evidence result for `artifact`, in memory or staged (never throws). */
232
+ export function verifySigstoreBundle(bundle: Uint8Array, artifact: Uint8Array | ByteReader, options: { readonly trust?: SigstoreTrust; readonly identity: SigstoreIdentity }): EvidenceResult {
233
+ const { trust, identity } = options
234
+ if (!trust) return unavailable("this hubctl carries no sigstore trusted root")
235
+ const b = parseLegacy(bundle)
236
+ if (!b) return unavailable("the bundle is not in a format this hubctl reads (legacy cosign bundle)")
237
+ try {
238
+ let leaf: X509Certificate
239
+ try {
240
+ leaf = new X509Certificate(b.certPEM)
241
+ } catch {
242
+ return failed("the bundle's certificate cannot be read")
243
+ }
244
+ // The chain: an authority whose issuing CA issued the leaf, every signature up to its self-signed root.
245
+ const authority = trust.authorities.find((a) => issued(leaf, a.chain[0]!))
246
+ if (!authority) return unavailable("the certificate comes from a CA this hubctl does not trust")
247
+ const chain = [leaf, ...authority.chain]
248
+ for (let i = 0; i < chain.length - 1; i++)
249
+ if (!issued(chain[i]!, chain[i + 1]!) || !chain[i]!.verify(chain[i + 1]!.publicKey)) return failed("the certificate chain does not verify")
250
+ const root = chain[chain.length - 1]!
251
+ if (!root.verify(root.publicKey)) return failed("the certificate chain does not verify")
252
+ if (leaf.keyUsage !== undefined && !leaf.keyUsage.includes(CODE_SIGNING)) return failed("the certificate is not for code signing")
253
+
254
+ const t = b.payload.integratedTime * 1000
255
+ if (t < Date.parse(leaf.validFrom) || t > Date.parse(leaf.validTo)) return failed("the entry was logged outside the certificate's validity")
256
+ if (!within(t, authority.validFor)) return failed("the entry was logged outside the CA's validity")
257
+
258
+ const sans = (leaf.subjectAltName ?? "").split(/,\s*/)
259
+ if (!sans.includes(`URI:${identity.san}`)) return failed(`the certificate is not for ${identity.san}`)
260
+ const issuer = oidcIssuer(leaf)
261
+ if (issuer !== identity.issuer) return failed(`the certificate's OIDC issuer is not ${identity.issuer}`)
262
+
263
+ let signed = false
264
+ try {
265
+ const verifier = createVerify("sha256")
266
+ for (const piece of piecesOf(artifact)) verifier.update(piece)
267
+ signed = verifier.verify(leaf.publicKey, b.signature)
268
+ } catch {}
269
+ if (!signed) return failed("the signature does not match the artifact")
270
+
271
+ const log = trust.logs.find((l) => l.logID === b.payload.logID.toLowerCase())
272
+ if (!log) return unavailable("the entry is in a transparency log this hubctl does not trust")
273
+ if (!within(t, log.validFor)) return failed("the entry was logged outside the log key's validity")
274
+ if (!verifySET(log, b)) return failed("the transparency log's signed entry timestamp does not verify")
275
+
276
+ let body: any
277
+ try {
278
+ body = JSON.parse(Buffer.from(b.payload.body, "base64").toString("utf8"))
279
+ } catch {
280
+ return failed("the transparency log entry cannot be read")
281
+ }
282
+ const hash = createHash("sha256")
283
+ for (const piece of piecesOf(artifact)) hash.update(piece)
284
+ const digest = hash.digest("hex")
285
+ const spec = isObj(body) && body.kind === "hashedrekord" && isObj(body.spec) ? body.spec : undefined
286
+ if (!spec || spec.data?.hash?.algorithm !== "sha256" || spec.data?.hash?.value !== digest) return failed("the transparency log entry is for another artifact")
287
+ if (spec.signature?.content !== b.signature.toString("base64")) return failed("the transparency log entry has another signature")
288
+ let logged: X509Certificate | undefined
289
+ try {
290
+ logged = new X509Certificate(Buffer.from(String(spec.signature?.publicKey?.content ?? ""), "base64").toString("utf8"))
291
+ } catch {}
292
+ if (!logged || !Buffer.from(logged.raw).equals(Buffer.from(leaf.raw))) return failed("the transparency log entry has another certificate")
293
+
294
+ return result("verified", `sigstore: signed by ${identity.san} (log index ${b.payload.logIndex})`)
295
+ } catch (cause) {
296
+ return failed(`the bundle is malformed (${cause instanceof Error ? cause.message : String(cause)})`)
297
+ }
298
+ }
@@ -0,0 +1,102 @@
1
+ // The Codex HarnessSource for the install-harness executor (transaction/builtin.ts). It never runs anything: it
2
+ // downloads the pinned archive over https through an injected Downloader, streaming it into the step's staging area,
3
+ // attempts the upstream evidence the pin lists over the staged file, and unpacks it member by member into staging
4
+ // files (HBS-12: the package inflates to ~0.5 GB; nothing is held whole).
5
+ //
6
+ // fetch the pinned file (at most the pinned size). Where the pin lists sigstore (openai/codex publishes
7
+ // `<asset>.sigstore` for its Linux assets), the bundle is fetched from beside the asset and verified
8
+ // offline (sigstore.ts), but only for bytes that already match the pinned digest: substituted bytes are
9
+ // returned untouched for the executor to reject (integrity_mismatch) and are never inspected further.
10
+ // A missing bundle, a bundle that cannot be fetched, or no trusted root is "unavailable", never a
11
+ // blocker: hub's pinned digest is the mandatory evidence (contract §5, Q4).
12
+ // unpack the archive into the version directory's tree (archive.ts: bin/codex and its helpers, streamed).
13
+ import type { EvidenceResult, HarnessID, InstallHarness, VerifiedManifest } from "../../contracts/index.ts"
14
+ import type { HarnessSource } from "../../transaction/builtin.ts"
15
+ import { collect, responseChunks, type ByteReader, type ByteSource } from "../../transaction/stream.ts"
16
+ import { unpackCodexFile } from "./archive.ts"
17
+ import { codexSigstoreIdentity, verifySigstoreBundle, type SigstoreTrust } from "./sigstore.ts"
18
+
19
+ /** `bytes`: the body, whole (small things, fakes) or as chunks as they arrive. */
20
+ export type DownloadResult = { readonly status: "ok"; readonly bytes: ByteSource } | { readonly status: "missing" } | { readonly status: "error"; readonly detail: string }
21
+
22
+ /** An https GET of one fixed URL, bounded in size. The only way the Codex source reaches the network. */
23
+ export interface Downloader {
24
+ get(url: string, options: { readonly maxBytes: number }): Promise<DownloadResult>
25
+ }
26
+
27
+ export interface CodexSourceOptions {
28
+ /** The verified hub release manifest whose pins the plan names. */
29
+ readonly manifest: VerifiedManifest
30
+ readonly download: Downloader
31
+ /** sigstore's trusted root (trustFromTrustedRoot); without it sigstore evidence is "unavailable". */
32
+ readonly trust?: SigstoreTrust
33
+ /** The Codex version hub installed before (from the profile history), for the downgrade policy. */
34
+ readonly installed?: (harness: HarnessID) => { readonly version: string; readonly sha256: string } | undefined
35
+ readonly floor?: (harness: HarnessID) => string | undefined
36
+ /** Explicit --allow-downgrade. */
37
+ readonly allowDowngrade?: boolean
38
+ }
39
+
40
+ /** Sigstore bundles are a few KB; anything larger is not one. */
41
+ const MAX_BUNDLE_BYTES = 256 * 1024
42
+
43
+ async function sigstoreEvidence(action: InstallHarness, artifact: ByteReader, options: CodexSourceOptions): Promise<EvidenceResult> {
44
+ if (!options.trust) return { kind: "sigstore", status: "unavailable", detail: "sigstore: this hubctl carries no sigstore trusted root" }
45
+ const bundle = await options.download.get(`${action.source.url}.sigstore`, { maxBytes: MAX_BUNDLE_BYTES })
46
+ if (bundle.status === "missing") return { kind: "sigstore", status: "unavailable", detail: "sigstore: upstream publishes no bundle for this asset" }
47
+ if (bundle.status === "error") return { kind: "sigstore", status: "unavailable", detail: `sigstore: the bundle could not be fetched (${bundle.detail})` }
48
+ let bytes: Uint8Array
49
+ try {
50
+ bytes = await collect(bundle.bytes, MAX_BUNDLE_BYTES)
51
+ } catch {
52
+ return { kind: "sigstore", status: "unavailable", detail: "sigstore: the bundle could not be fetched (larger than any bundle)" }
53
+ }
54
+ return verifySigstoreBundle(bytes, artifact, { trust: options.trust, identity: codexSigstoreIdentity(action.version) })
55
+ }
56
+
57
+ export function createCodexHarnessSource(options: CodexSourceOptions): HarnessSource {
58
+ const only = (harness: HarnessID) => {
59
+ if (harness !== "codex") throw new Error(`the Codex source only installs codex, not ${harness}`)
60
+ }
61
+ return {
62
+ manifest: options.manifest,
63
+ async fetch(action, staging) {
64
+ only(action.harness)
65
+ const archive = await options.download.get(action.source.url, { maxBytes: action.size })
66
+ if (archive.status !== "ok") throw new Error(`cannot download ${action.source.file}: ${archive.status === "missing" ? "not found" : archive.detail}`)
67
+ const file = await staging.write(action.source.file, archive.bytes, { maxBytes: action.size })
68
+ // Only bytes that are exactly the pinned artifact are looked at any further.
69
+ if (file.size !== action.size || file.sha256 !== action.sha256) return { file, evidence: [] }
70
+ const evidence: EvidenceResult[] = []
71
+ if (action.evidence.includes("sigstore")) evidence.push(await sigstoreEvidence(action, staging.open(file), options))
72
+ return { file, evidence }
73
+ },
74
+ async unpack(action, file, staging) {
75
+ only(action.harness)
76
+ return unpackCodexFile(staging.open(file), staging)
77
+ },
78
+ installed: (harness) => (harness === "codex" ? options.installed?.(harness) : undefined),
79
+ floor: (harness) => (harness === "codex" ? options.floor?.(harness) : undefined),
80
+ ...(options.allowDowngrade !== undefined ? { allowDowngrade: options.allowDowngrade } : {}),
81
+ }
82
+ }
83
+
84
+ /** A Downloader over fetch(): https only, no redirects to plain http, at most `maxBytes` read (as it streams). */
85
+ export const httpsDownloader = (fetchImpl: (url: string, init?: RequestInit) => Promise<Response> = fetch): Downloader => ({
86
+ async get(url, { maxBytes }) {
87
+ if (!url.startsWith("https://")) return { status: "error", detail: "only https downloads are allowed" }
88
+ let response: Response
89
+ try {
90
+ response = await fetchImpl(url, { redirect: "follow" })
91
+ } catch (cause) {
92
+ return { status: "error", detail: cause instanceof Error ? cause.message : String(cause) }
93
+ }
94
+ if (response.url && !response.url.startsWith("https://")) return { status: "error", detail: "redirected away from https" }
95
+ if (response.status === 404) return { status: "missing" }
96
+ if (!response.ok) return { status: "error", detail: `HTTP ${response.status}` }
97
+ const declared = Number(response.headers.get("content-length") ?? NaN)
98
+ if (Number.isFinite(declared) && declared > maxBytes) return { status: "error", detail: `larger than ${maxBytes} bytes` }
99
+ // The body streams; running past the limit fails the transfer (the staging write) as soon as it happens.
100
+ return { status: "ok", bytes: responseChunks(response, maxBytes, () => new Error(`${url} is larger than ${maxBytes} bytes`)) }
101
+ },
102
+ })