@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,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@huynle/hub-cli",
|
|
3
|
+
"version": "0.7.3",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "The hub CLI (`hub` / `hubctl`) for agents and scripts (internal; bundled into @huynle/hub, not published on its own).",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
"./package.json": "./package.json"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// The act verbs in the CLI (design §5, §8): the answer of `hub answer` (key=value pairs, or --json '{…}' / --json -),
|
|
2
|
+
// the short text of what the hub answers to approve, deny, answer, prompt and stop, and the lines a needs_user
|
|
3
|
+
// failure adds (its evaluation and the Decision it opened). The shapes are packages/sdk's types (acting-routes.ts).
|
|
4
|
+
import type { Approved, Denied, Envelope, Evaluation, FormAnswer, FormAnswered, PromptDelivery, Prompted, Stopped } from "../../sdk/src/index.ts"
|
|
5
|
+
import { jsonObjectArg, UsageError, type Parsed } from "./args"
|
|
6
|
+
import { oneLine } from "./format"
|
|
7
|
+
|
|
8
|
+
const grantText = (grant: string | undefined) => (grant ? `grant ${grant}` : undefined)
|
|
9
|
+
const inParens = (parts: readonly (string | undefined)[]) => {
|
|
10
|
+
const shown = parts.filter(Boolean)
|
|
11
|
+
return shown.length ? ` (${shown.join(", ")})` : ""
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const approvedText = (b: Approved): string => `approved ${b.requestID} in ${b.sessionID}${inParens([b.decision, grantText(b.grant)])}`
|
|
15
|
+
|
|
16
|
+
export function deniedText(b: Denied): string {
|
|
17
|
+
const reason = b.reasonSent === true ? "; the reason reached the harness" : b.reasonSent === false ? "; the harness did not get the reason (older spoke)" : ""
|
|
18
|
+
return `denied ${b.requestID} in ${b.sessionID}${inParens([grantText(b.grant)])}${reason}`
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const answeredText = (b: FormAnswered, cancelled: boolean): string => `${cancelled ? "cancelled" : "answered"} ${b.formID} in ${b.sessionID}${inParens([grantText(b.grant)])}`
|
|
22
|
+
|
|
23
|
+
export function promptedText(b: Prompted): string {
|
|
24
|
+
if ("messageID" in b) return `prompted ${b.sessionID}: ${b.messageID} delivered (${b.delivered})${b.grant ? `, grant ${b.grant}` : ""}`
|
|
25
|
+
return `prompted ${b.sessionID}: ${b.mode === "steer" ? "steered into the running turn" : "started a turn"}`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const stoppedText = (b: Stopped): string => `stopped ${b.sessionID}${inParens([grantText(b.grant)])}`
|
|
29
|
+
|
|
30
|
+
/** The deliveries of a prompt. */
|
|
31
|
+
export const DELIVERIES: readonly PromptDelivery[] = ["queue", "steer"]
|
|
32
|
+
|
|
33
|
+
/** The --delivery of `hub prompt`. */
|
|
34
|
+
export function deliveryArg(value: string | undefined): PromptDelivery | undefined {
|
|
35
|
+
if (value !== undefined && !(DELIVERIES as readonly string[]).includes(value)) throw new UsageError(`--delivery is one of ${DELIVERIES.join(", ")}`)
|
|
36
|
+
return value as PromptDelivery | undefined
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The answer of `hub answer`: key=value pairs (a repeated key is a list), or a JSON object from --json; "cancel" for --cancel. */
|
|
40
|
+
export async function formAnswerArg(pairs: readonly string[], flags: Parsed["flags"], stdin: (() => Promise<string>) | undefined): Promise<FormAnswer | "cancel"> {
|
|
41
|
+
const json = typeof flags.json === "string" ? flags.json : undefined
|
|
42
|
+
const given = [pairs.length > 0, json !== undefined, flags.cancel === true].filter(Boolean).length
|
|
43
|
+
if (given > 1) throw new UsageError("give either key=value pairs, --json '{…}' or --cancel")
|
|
44
|
+
if (flags.cancel === true) return "cancel"
|
|
45
|
+
if (json !== undefined) return (await jsonObjectArg(json, stdin, "answer")) as FormAnswer
|
|
46
|
+
if (!pairs.length) throw new UsageError("needs key=value pairs (or --json '{…}', or --cancel)")
|
|
47
|
+
const answer: Record<string, string | string[]> = {}
|
|
48
|
+
for (const pair of pairs) {
|
|
49
|
+
const eq = pair.indexOf("=")
|
|
50
|
+
if (eq <= 0) throw new UsageError(`answers are key=value (got "${pair}")`)
|
|
51
|
+
const key = pair.slice(0, eq)
|
|
52
|
+
const value = pair.slice(eq + 1)
|
|
53
|
+
const before = answer[key]
|
|
54
|
+
answer[key] = before === undefined ? value : [...(Array.isArray(before) ? before : [before]), value]
|
|
55
|
+
}
|
|
56
|
+
return answer
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** What a needs_user failure adds: the evaluation, the grants checked and the Decision opened for the user. */
|
|
60
|
+
export function needsUserLines(envelope: Envelope): string[] {
|
|
61
|
+
const lines: string[] = []
|
|
62
|
+
const evaluation = envelope.evaluation as Evaluation | undefined
|
|
63
|
+
if (evaluation && typeof evaluation === "object") {
|
|
64
|
+
lines.push(`evaluation: ${evaluation.verdict}${evaluation.tier ? ` (${evaluation.tier})` : ""} — ${oneLine(String(evaluation.reason ?? ""), 300)}`)
|
|
65
|
+
const grants = Array.isArray(evaluation.grants) ? evaluation.grants : []
|
|
66
|
+
lines.push(`grants checked: ${grants.length ? grants.join(", ") : "none"}`)
|
|
67
|
+
}
|
|
68
|
+
if (typeof envelope.decisionID === "string") lines.push(`decision ${envelope.decisionID} is open for the user`)
|
|
69
|
+
return lines
|
|
70
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Command-line parsing: `--flag`, `--flag value`, `--flag=value` and short aliases (-n, -h, -V). Unknown flags
|
|
2
|
+
// and wrong positional counts are usage errors (exit 2).
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* optional: a value only when given with `=` or when the next token starts with a digit (`--wait`, `--wait 30`).
|
|
6
|
+
* json: a value only when given with `=`, or when the next token looks like JSON (`{`, `[`) or is `-` (stdin):
|
|
7
|
+
* `hub answer … --json '{…}'` takes the answer while a bare `--json` keeps its usual meaning (raw output).
|
|
8
|
+
*/
|
|
9
|
+
export type FlagKind = "bool" | "string" | "multi" | "optional" | "json"
|
|
10
|
+
|
|
11
|
+
export interface CommandSpec {
|
|
12
|
+
readonly flags?: Readonly<Record<string, FlagKind>>
|
|
13
|
+
/** Positional arguments: required names, then optional ones (suffixed `?`), then any number (suffixed `...`). */
|
|
14
|
+
readonly args?: readonly string[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface Parsed {
|
|
18
|
+
readonly flags: Record<string, string | boolean | string[]>
|
|
19
|
+
readonly args: string[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class UsageError extends Error {}
|
|
23
|
+
|
|
24
|
+
/** Flags every command takes. */
|
|
25
|
+
const COMMON: Record<string, FlagKind> = { json: "bool", help: "bool" }
|
|
26
|
+
const SHORT: Record<string, string> = { h: "help", n: "n", V: "version" }
|
|
27
|
+
|
|
28
|
+
export function parse(argv: readonly string[], spec: CommandSpec): Parsed {
|
|
29
|
+
const kinds: Record<string, FlagKind> = { ...COMMON, ...spec.flags }
|
|
30
|
+
const flags: Parsed["flags"] = {}
|
|
31
|
+
const args: string[] = []
|
|
32
|
+
for (let i = 0; i < argv.length; i++) {
|
|
33
|
+
const token = argv[i]!
|
|
34
|
+
if (token === "--") {
|
|
35
|
+
args.push(...argv.slice(i + 1))
|
|
36
|
+
break
|
|
37
|
+
}
|
|
38
|
+
const long = /^--([a-z][a-z-]*)(?:=(.*))?$/s.exec(token)
|
|
39
|
+
const short = /^-([a-zA-Z])$/.exec(token)
|
|
40
|
+
if (!long && !short) {
|
|
41
|
+
if (token.startsWith("-") && token.length > 1) throw new UsageError(`unknown option ${token}`)
|
|
42
|
+
args.push(token)
|
|
43
|
+
continue
|
|
44
|
+
}
|
|
45
|
+
const name = long ? long[1]! : SHORT[short![1]!]
|
|
46
|
+
const kind = name === undefined ? undefined : kinds[name]
|
|
47
|
+
if (name === undefined || kind === undefined) throw new UsageError(`unknown option ${token}`)
|
|
48
|
+
if (kind === "bool") {
|
|
49
|
+
if (long?.[2] !== undefined) throw new UsageError(`${token}: --${name} takes no value`)
|
|
50
|
+
flags[name] = true
|
|
51
|
+
continue
|
|
52
|
+
}
|
|
53
|
+
let value = long?.[2]
|
|
54
|
+
const next = argv[i + 1] ?? ""
|
|
55
|
+
if (value === undefined && ((kind === "optional" && !/^\d/.test(next)) || (kind === "json" && !/^[{[]/.test(next) && next !== "-"))) {
|
|
56
|
+
flags[name] = true
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
if (value === undefined) {
|
|
60
|
+
value = argv[++i]
|
|
61
|
+
if (value === undefined) throw new UsageError(`--${name} needs a value`)
|
|
62
|
+
}
|
|
63
|
+
if (kind === "multi") flags[name] = [...((flags[name] as string[] | undefined) ?? []), value]
|
|
64
|
+
else flags[name] = value
|
|
65
|
+
}
|
|
66
|
+
if (flags.help) return { flags, args }
|
|
67
|
+
const names = spec.args ?? []
|
|
68
|
+
const required = names.filter((n) => !n.endsWith("?") && !n.endsWith("...")).length
|
|
69
|
+
if (args.length < required) throw new UsageError(`missing ${names.slice(args.length, required).join(" ")}`)
|
|
70
|
+
if (args.length > names.length && !names.at(-1)?.endsWith("...")) throw new UsageError(`unexpected argument ${args[names.length]}`)
|
|
71
|
+
return { flags, args }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const str = (flags: Parsed["flags"], name: string): string | undefined => (typeof flags[name] === "string" ? (flags[name] as string) : undefined)
|
|
75
|
+
|
|
76
|
+
/** A --json value: a JSON object, or all of stdin for "-"; anything else is a usage error. */
|
|
77
|
+
export async function jsonObjectArg(value: string, stdin: (() => Promise<string>) | undefined, what: string): Promise<Record<string, unknown>> {
|
|
78
|
+
let text = value
|
|
79
|
+
if (value === "-") {
|
|
80
|
+
if (!stdin) throw new UsageError(`cannot read the ${what} from stdin here`)
|
|
81
|
+
text = await stdin()
|
|
82
|
+
}
|
|
83
|
+
let parsed: unknown
|
|
84
|
+
try {
|
|
85
|
+
parsed = JSON.parse(text)
|
|
86
|
+
} catch {
|
|
87
|
+
parsed = undefined
|
|
88
|
+
}
|
|
89
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new UsageError(`--json takes the ${what} as a JSON object`)
|
|
90
|
+
return parsed as Record<string, unknown>
|
|
91
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
// Streaming readers for the release archives harness adapters unpack (OpenCode's tar.gz and zip, Codex's tar.gz),
|
|
2
|
+
// so an archive and what it unpacks to are never in memory whole: gzip and raw deflate through hub's own streaming
|
|
3
|
+
// decoder (inflate.ts), tar parsed block by block from the inflated stream, zip members located through the central
|
|
4
|
+
// directory and inflated by offset. Each reader only reports what the archive says; the adapters decide what is acceptable
|
|
5
|
+
// (names, types, layout) before a member's body is written anywhere. Limits are enforced while streaming: the
|
|
6
|
+
// inflated total (a gzip bomb stops at `maxBytes`), and a zip member never inflates past its recorded size.
|
|
7
|
+
// No dependency.
|
|
8
|
+
import type { ByteReader } from "../transaction/stream.ts"
|
|
9
|
+
import { crc32, gunzipChunks, InflateError, inflateRawChunks } from "./inflate.ts"
|
|
10
|
+
|
|
11
|
+
/** Why an archive was refused (the adapters phrase it). */
|
|
12
|
+
export class ArchiveError extends Error {
|
|
13
|
+
constructor(message: string) {
|
|
14
|
+
super(message)
|
|
15
|
+
this.name = "ArchiveError"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const BLOCK = 512
|
|
20
|
+
|
|
21
|
+
const field = (block: Uint8Array, at: number, len: number) => {
|
|
22
|
+
const raw = block.subarray(at, at + len)
|
|
23
|
+
const end = raw.indexOf(0)
|
|
24
|
+
return new TextDecoder().decode(end < 0 ? raw : raw.subarray(0, end))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** An octal tar number, or GNU base-256 (high bit set) for large sizes; undefined when unreadable. */
|
|
28
|
+
function tarNumber(block: Uint8Array, at: number, len: number): number | undefined {
|
|
29
|
+
if (block[at]! & 0x80) {
|
|
30
|
+
let n = 0
|
|
31
|
+
for (let i = at + 1; i < at + len; i++) n = n * 256 + block[i]!
|
|
32
|
+
return Number.isSafeInteger(n) ? n : undefined
|
|
33
|
+
}
|
|
34
|
+
const text = field(block, at, len).trim()
|
|
35
|
+
if (text === "") return 0
|
|
36
|
+
return /^[0-7]+$/.test(text) ? parseInt(text, 8) : undefined
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function checksumOK(block: Uint8Array): boolean {
|
|
40
|
+
let sum = 0
|
|
41
|
+
for (let i = 0; i < BLOCK; i++) sum += i >= 148 && i < 156 ? 0x20 : block[i]!
|
|
42
|
+
return tarNumber(block, 148, 8) === sum
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Pulls exact byte counts out of a chunked stream, counting everything it pulls against a limit. */
|
|
46
|
+
class Pull {
|
|
47
|
+
private chunk: Uint8Array = new Uint8Array()
|
|
48
|
+
private at = 0
|
|
49
|
+
private total = 0
|
|
50
|
+
constructor(
|
|
51
|
+
private readonly source: Iterator<Uint8Array>,
|
|
52
|
+
private readonly maxBytes: number,
|
|
53
|
+
private readonly overLimit: string,
|
|
54
|
+
private readonly damaged: string,
|
|
55
|
+
) {}
|
|
56
|
+
|
|
57
|
+
private async more(): Promise<boolean> {
|
|
58
|
+
let next: IteratorResult<Uint8Array>
|
|
59
|
+
try {
|
|
60
|
+
next = this.source.next()
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (error instanceof InflateError) throw new ArchiveError(`${this.damaged} (${error.message})`)
|
|
63
|
+
throw error
|
|
64
|
+
}
|
|
65
|
+
if (next.done) return false
|
|
66
|
+
this.total += next.value.byteLength
|
|
67
|
+
if (this.total > this.maxBytes) throw new ArchiveError(this.overLimit)
|
|
68
|
+
this.chunk = next.value
|
|
69
|
+
this.at = 0
|
|
70
|
+
return true
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Exactly `n` bytes (a header), or undefined at a clean end of the stream before any of them. */
|
|
74
|
+
async exact(n: number): Promise<Uint8Array | undefined> {
|
|
75
|
+
const out = new Uint8Array(n)
|
|
76
|
+
let got = 0
|
|
77
|
+
while (got < n) {
|
|
78
|
+
if (this.at >= this.chunk.byteLength && !(await this.more())) {
|
|
79
|
+
if (got === 0) return undefined
|
|
80
|
+
throw new ArchiveError("it is truncated")
|
|
81
|
+
}
|
|
82
|
+
const take = Math.min(n - got, this.chunk.byteLength - this.at)
|
|
83
|
+
out.set(this.chunk.subarray(this.at, this.at + take), got)
|
|
84
|
+
this.at += take
|
|
85
|
+
got += take
|
|
86
|
+
}
|
|
87
|
+
return out
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The next `n` bytes, as the chunks they arrive in. */
|
|
91
|
+
async *take(n: number): AsyncGenerator<Uint8Array> {
|
|
92
|
+
let left = n
|
|
93
|
+
while (left > 0) {
|
|
94
|
+
if (this.at >= this.chunk.byteLength && !(await this.more())) throw new ArchiveError("it is truncated")
|
|
95
|
+
const take = Math.min(left, this.chunk.byteLength - this.at)
|
|
96
|
+
const part = this.chunk.subarray(this.at, this.at + take)
|
|
97
|
+
this.at += take
|
|
98
|
+
left -= take
|
|
99
|
+
yield part
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async skip(n: number): Promise<void> {
|
|
104
|
+
for await (const _ of this.take(n));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface TarHeader {
|
|
109
|
+
/** The member name as the header has it (ustar prefix joined), before pax or GNU long names. */
|
|
110
|
+
readonly name: string
|
|
111
|
+
/** The type flag; a NUL flag reads as "0". */
|
|
112
|
+
readonly type: string
|
|
113
|
+
readonly size: number
|
|
114
|
+
/** The permission bits field, when readable. */
|
|
115
|
+
readonly mode: number | undefined
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface TarMember {
|
|
119
|
+
readonly header: TarHeader
|
|
120
|
+
/** The member's data, in chunks (once; what is not read is skipped). */
|
|
121
|
+
body(): AsyncGenerator<Uint8Array>
|
|
122
|
+
/** A small member's data whole (pax and GNU long-name records); ArchiveError beyond `limit`. */
|
|
123
|
+
bytes(limit: number): Promise<Uint8Array>
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Reads a gzipped tar from `reader`, calling `visit` for each member in order until the end marker. `visit` may
|
|
128
|
+
* refuse (throw ArchiveError) before reading the body. ArchiveError for a damaged, truncated or oversized archive:
|
|
129
|
+
* `maxBytes` bounds the inflated stream, and the limit is enforced as it inflates.
|
|
130
|
+
*/
|
|
131
|
+
export async function readTarGz(reader: ByteReader, options: { readonly maxBytes: number }, visit: (member: TarMember) => Promise<void>): Promise<void> {
|
|
132
|
+
const iterator = gunzipChunks(reader)
|
|
133
|
+
const pull = new Pull(iterator, options.maxBytes, `it inflates beyond ${options.maxBytes} bytes`, "it is not a gzip stream (or it is damaged)")
|
|
134
|
+
try {
|
|
135
|
+
for (;;) {
|
|
136
|
+
const block = await pull.exact(BLOCK)
|
|
137
|
+
if (!block) throw new ArchiveError("a tar without its end marker")
|
|
138
|
+
if (block.every((b) => b === 0)) return
|
|
139
|
+
if (!checksumOK(block)) throw new ArchiveError("a header checksum does not match")
|
|
140
|
+
const size = tarNumber(block, 124, 12)
|
|
141
|
+
if (size === undefined) throw new ArchiveError("a header has an unreadable size")
|
|
142
|
+
const magic = field(block, 257, 6)
|
|
143
|
+
const prefix = magic.startsWith("ustar") && magic !== "ustar " ? field(block, 345, 155) : ""
|
|
144
|
+
const name = prefix ? `${prefix}/${field(block, 0, 100)}` : field(block, 0, 100)
|
|
145
|
+
const flag = block[156]!
|
|
146
|
+
const header: TarHeader = { name, type: flag === 0 ? "0" : String.fromCharCode(flag), size, mode: tarNumber(block, 100, 8) }
|
|
147
|
+
let consumed = false
|
|
148
|
+
const body = async function* () {
|
|
149
|
+
if (consumed) throw new Error("a tar member's body is read once")
|
|
150
|
+
consumed = true
|
|
151
|
+
yield* pull.take(size)
|
|
152
|
+
}
|
|
153
|
+
await visit({
|
|
154
|
+
header,
|
|
155
|
+
body,
|
|
156
|
+
async bytes(limit) {
|
|
157
|
+
if (size > limit) throw new ArchiveError(`a ${header.type} record is larger than ${limit} bytes`)
|
|
158
|
+
const out = new Uint8Array(size)
|
|
159
|
+
let at = 0
|
|
160
|
+
for await (const part of body()) {
|
|
161
|
+
out.set(part, at)
|
|
162
|
+
at += part.byteLength
|
|
163
|
+
}
|
|
164
|
+
return out
|
|
165
|
+
},
|
|
166
|
+
})
|
|
167
|
+
if (!consumed) await pull.skip(size)
|
|
168
|
+
await pull.skip((BLOCK - (size % BLOCK)) % BLOCK)
|
|
169
|
+
}
|
|
170
|
+
} finally {
|
|
171
|
+
iterator.return(undefined)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ---------- zip ----------
|
|
176
|
+
|
|
177
|
+
export interface ZipMember {
|
|
178
|
+
readonly name: string
|
|
179
|
+
readonly kind: "file" | "directory" | "other"
|
|
180
|
+
readonly encrypted: boolean
|
|
181
|
+
readonly method: number
|
|
182
|
+
readonly compressedSize: number
|
|
183
|
+
readonly size: number
|
|
184
|
+
/** The CRC-32 the directory records for the member's data. */
|
|
185
|
+
readonly crc32: number
|
|
186
|
+
/** The local header's offset. */
|
|
187
|
+
readonly local: number
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** The members a zip's central directory lists (zip64 is refused); ArchiveError when it is not a readable zip. */
|
|
191
|
+
export function zipMembers(reader: ByteReader, options: { readonly maxBytes: number; readonly maxEntries?: number }): ZipMember[] {
|
|
192
|
+
const tailStart = Math.max(0, reader.size - 22 - 0xffff)
|
|
193
|
+
const tail = reader.read(tailStart, reader.size - tailStart)
|
|
194
|
+
const tv = new DataView(tail.buffer, tail.byteOffset, tail.byteLength)
|
|
195
|
+
let eocd = -1
|
|
196
|
+
for (let i = tail.byteLength - 22; i >= 0; i--)
|
|
197
|
+
if (tv.getUint32(i, true) === 0x06054b50) {
|
|
198
|
+
eocd = i
|
|
199
|
+
break
|
|
200
|
+
}
|
|
201
|
+
if (eocd < 0) throw new ArchiveError("not a zip archive")
|
|
202
|
+
const count = tv.getUint16(eocd + 10, true)
|
|
203
|
+
const cdSize = tv.getUint32(eocd + 12, true)
|
|
204
|
+
const cdOffset = tv.getUint32(eocd + 16, true)
|
|
205
|
+
if (count === 0xffff || cdOffset === 0xffffffff || cdOffset + cdSize > tailStart + eocd) throw new ArchiveError("an unsupported or damaged zip (zip64 or bad directory)")
|
|
206
|
+
if (count > (options.maxEntries ?? 10_000) || cdSize > 16 * 1024 * 1024) throw new ArchiveError("a zip directory larger than any release has")
|
|
207
|
+
const cd = reader.read(cdOffset, cdSize)
|
|
208
|
+
const view = new DataView(cd.buffer, cd.byteOffset, cd.byteLength)
|
|
209
|
+
const decoder = new TextDecoder("utf-8", { fatal: true })
|
|
210
|
+
const members: ZipMember[] = []
|
|
211
|
+
let at = 0
|
|
212
|
+
let total = 0
|
|
213
|
+
for (let n = 0; n < count; n++) {
|
|
214
|
+
if (at + 46 > cd.byteLength || view.getUint32(at, true) !== 0x02014b50) throw new ArchiveError("a damaged zip directory")
|
|
215
|
+
const flags = view.getUint16(at + 8, true)
|
|
216
|
+
const method = view.getUint16(at + 10, true)
|
|
217
|
+
const crc = view.getUint32(at + 16, true)
|
|
218
|
+
const compressedSize = view.getUint32(at + 20, true)
|
|
219
|
+
const size = view.getUint32(at + 24, true)
|
|
220
|
+
const nameLen = view.getUint16(at + 28, true)
|
|
221
|
+
const extraLen = view.getUint16(at + 30, true)
|
|
222
|
+
const commentLen = view.getUint16(at + 32, true)
|
|
223
|
+
const madeBy = view.getUint8(at + 5)
|
|
224
|
+
const external = view.getUint32(at + 38, true)
|
|
225
|
+
const local = view.getUint32(at + 42, true)
|
|
226
|
+
if (at + 46 + nameLen > cd.byteLength) throw new ArchiveError("a damaged zip directory")
|
|
227
|
+
let name: string
|
|
228
|
+
try {
|
|
229
|
+
name = decoder.decode(cd.subarray(at + 46, at + 46 + nameLen))
|
|
230
|
+
} catch {
|
|
231
|
+
throw new ArchiveError("a member name that is not UTF-8")
|
|
232
|
+
}
|
|
233
|
+
at += 46 + nameLen + extraLen + commentLen
|
|
234
|
+
const type = madeBy === 3 ? (external >>> 16) & 0o170000 : name.endsWith("/") ? 0o040000 : 0o100000
|
|
235
|
+
const kind = type === 0o040000 ? "directory" : type === 0o100000 ? "file" : "other"
|
|
236
|
+
if (kind === "file") {
|
|
237
|
+
total += size
|
|
238
|
+
if (total > options.maxBytes) throw new ArchiveError(`it unpacks to more than ${options.maxBytes} bytes`)
|
|
239
|
+
}
|
|
240
|
+
members.push({ name, kind, encrypted: (flags & 0x1) !== 0, method, compressedSize, size, crc32: crc, local })
|
|
241
|
+
}
|
|
242
|
+
return members
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* One zip member's data, inflated as it streams, in chunks; ArchiveError if it is damaged, uses an unsupported
|
|
247
|
+
* method, does not inflate to exactly its recorded size (it is cut off at that size, never past it), or does not
|
|
248
|
+
* have its recorded CRC-32 (checked once the last chunk is out: the caller discards what it wrote).
|
|
249
|
+
*/
|
|
250
|
+
export async function* zipMemberData(reader: ByteReader, member: ZipMember): AsyncGenerator<Uint8Array> {
|
|
251
|
+
if (member.encrypted) throw new ArchiveError(`${member.name} is encrypted`)
|
|
252
|
+
if (member.method !== 0 && member.method !== 8) throw new ArchiveError(`${member.name} uses an unsupported compression method (${member.method})`)
|
|
253
|
+
const header = reader.read(member.local, 30)
|
|
254
|
+
const hv = new DataView(header.buffer, header.byteOffset, header.byteLength)
|
|
255
|
+
if (header.byteLength < 30 || hv.getUint32(0, true) !== 0x04034b50) throw new ArchiveError("a damaged zip member")
|
|
256
|
+
const start = member.local + 30 + hv.getUint16(26, true) + hv.getUint16(28, true)
|
|
257
|
+
if (start + member.compressedSize > reader.size) throw new ArchiveError("a truncated zip")
|
|
258
|
+
let produced = 0
|
|
259
|
+
let crc = 0
|
|
260
|
+
const wrongSize = () => new ArchiveError(`${member.name} does not have its recorded size`)
|
|
261
|
+
try {
|
|
262
|
+
const chunks = member.method === 0 ? reader.stream(start, start + member.compressedSize) : inflateRawChunks(reader, start, start + member.compressedSize).chunks
|
|
263
|
+
for await (const chunk of chunks) {
|
|
264
|
+
produced += chunk.byteLength
|
|
265
|
+
if (produced > member.size) throw wrongSize()
|
|
266
|
+
crc = crc32(crc, chunk)
|
|
267
|
+
yield chunk
|
|
268
|
+
}
|
|
269
|
+
} catch (error) {
|
|
270
|
+
if (error instanceof InflateError) throw new ArchiveError(`${member.name} does not decompress (${error.message})`)
|
|
271
|
+
throw error
|
|
272
|
+
}
|
|
273
|
+
if (produced !== member.size) throw wrongSize()
|
|
274
|
+
if (crc !== member.crc32) throw new ArchiveError(`${member.name} does not have its recorded CRC-32`)
|
|
275
|
+
}
|
|
276
|
+
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Claude Code bootstrap adapter (HBS-08)
|
|
2
|
+
|
|
3
|
+
This adapter plans, installs and verifies Claude Code for `hubctl setup`. It implements contracts §3.6.1, §3.10 and
|
|
4
|
+
§5 of `docs/bootstrap-contracts.md`. It is a pure planner. It reaches outside itself only through injected ports,
|
|
5
|
+
and it never gets a shell.
|
|
6
|
+
|
|
7
|
+
| File | Role |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `adapter.ts` | `createClaudeAdapter` (a `HarnessAdapter`) and `planClaude`. Plans, in order: pinned `install-harness`, `select-harness-version`, `merge-config` of hub's own `config:hub/claude-code.json`, `verify-harness`. Also `discover()` and `binary()`, both through the read-only `DiscoveryProbe`. |
|
|
10
|
+
| `source.ts` | `createClaudeHarnessSource`, the `HarnessSource` for `installHarnessExecutor`. It streams the pinned file into the step's staging area, records the optional evidence (against the staged digest; codesign on a scratch copy written in chunks) and installs the staged binary itself as `<version>/claude`. |
|
|
11
|
+
| `openpgp.ts` | An in-house check of a detached OpenPGP v4 RSA signature. It verifies upstream's signed `manifest.json` against the pinned release key and needs no `gpg`. |
|
|
12
|
+
| `verify.ts` | The real readiness probes, `claudeReadiness` (the capability is offered only when the harness is ready) and `claudeVerifyExecutor`. |
|
|
13
|
+
| `ports.ts` / `node.ts` | The injected ports (`Downloader`, `ProbeRunner`, `Scratch`) and their Node defaults (`httpsDownloader`, `nodeRunner`, `tempScratch`). |
|
|
14
|
+
| `index.ts` | Re-exports, and `claudeExecutors`: `install-harness` and `verify-harness` for claude-code. |
|
|
15
|
+
|
|
16
|
+
## Authenticity
|
|
17
|
+
|
|
18
|
+
The mandatory check is the sha256 that hub's signed manifest pins. `installHarnessExecutor` checks it first,
|
|
19
|
+
through `checkHarnessEvidence`. The optional evidence is then recorded as `verified`, `unavailable` or `failed`.
|
|
20
|
+
Any `failed` result rejects the artifact.
|
|
21
|
+
|
|
22
|
+
- **`gpg-manifest`.** The adapter fetches `…/claude-code-releases/<v>/manifest.json`, its `.sig`, and
|
|
23
|
+
`https://downloads.claude.ai/keys/claude-code.asc`. The key's fingerprint must be
|
|
24
|
+
`31DDDE24DDFAB679F42D7BD2BAA929FF1A7ECACE` (`CLAUDE_SIGNING_FINGERPRINT`). The signature is a v4 RSA signature
|
|
25
|
+
over a binary document, using SHA-2.
|
|
26
|
+
- `verified`: the signature is good, and the signed manifest lists exactly the pinned version, checksum and size
|
|
27
|
+
for this platform.
|
|
28
|
+
- `failed`: the signature does not verify, the signature is malformed, or the signed manifest lists other bytes
|
|
29
|
+
or another version.
|
|
30
|
+
- `unavailable`: there is no signature (releases before 2.1.89), the bucket cannot be reached, the key is
|
|
31
|
+
another one or a rotated one, the format is unsupported, or the pin URL is outside upstream's bucket.
|
|
32
|
+
- **`macos-notarization`.** This check runs on macOS only. It runs `/usr/bin/codesign --verify --strict` (fixed
|
|
33
|
+
argv) on a private copy of the downloaded bytes.
|
|
34
|
+
- It first checks `-R=anchor apple generic and certificate leaf[subject.OU] = "Q6L2SF6YDW"`, which means signed
|
|
35
|
+
by Anthropic PBC. If this fails, the result is `failed`.
|
|
36
|
+
- It then checks `-R=notarized and …`. If this passes, the result is `verified`. If it fails, the result is
|
|
37
|
+
`unavailable`, because the ticket may not be confirmable offline.
|
|
38
|
+
- The result is also `unavailable` when codesign is missing, when the host is not macOS, or when the target is
|
|
39
|
+
Linux.
|
|
40
|
+
|
|
41
|
+
## User config and credentials
|
|
42
|
+
|
|
43
|
+
- No plan ever touches `~/.claude` or `~/.claude.json`. The existing integration loads the hub plugin with
|
|
44
|
+
`--plugin-dir` (`packages/claude-code/src/wrapper.ts`).
|
|
45
|
+
- The only config hub edits is its own `config:hub/claude-code.json`, and only the key `/binary`. The edit is
|
|
46
|
+
lossless and pinned by a precondition. A rollback removes the file, or restores it if it existed before.
|
|
47
|
+
- Logins are user-mediated (`claude auth login`). `provider-auth` runs `claude auth status`, reads only its exit
|
|
48
|
+
code and `authMethod`, and drops the rest of the output. The child gets the user's environment minus `HUB_*` and
|
|
49
|
+
`BUN_BE_BUN`. `version-exec` gets only `PATH`, `HOME` and the update switches.
|
|
50
|
+
|
|
51
|
+
## Pin template for `release/harness-pins.json` (fill in from a fresh re-download; never copy digests from here)
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{ "pin": { "harness": "claude-code", "version": "<exact, e.g. the stable channel's>", "target": "darwin-arm64",
|
|
55
|
+
"url": "https://downloads.claude.ai/claude-code-releases/<version>/darwin-arm64/claude",
|
|
56
|
+
"file": "claude", "size": <bytes>, "sha256": "sha256:<hex of the re-downloaded file>",
|
|
57
|
+
"evidence": ["gpg-manifest", "macos-notarization"], "minimumVersion": "2.1.89" },
|
|
58
|
+
"review": { "by": "<handle>", "on": "<date>", "checked": ["upstream-release", "digest-redownloaded", "evidence-listed"] } }
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- Pins for `linux-x64` and `linux-arm64` list `"evidence": ["gpg-manifest"]`. Linux binaries are not codesigned.
|
|
62
|
+
- `minimumVersion` 2.1.89 is the first release with a signed manifest.
|
|
63
|
+
- `file` must be `claude`, upstream's raw binary. The `.zst` variant is refused by `unpack`.
|