@huynle/hub 0.6.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.
- package/CHANGELOG.md +870 -0
- package/Dockerfile +33 -0
- package/README.md +1007 -66
- package/package.json +47 -2
- package/packages/claude-code/package.json +12 -0
- package/packages/claude-code/src/bridge.ts +79 -0
- package/packages/claude-code/src/daemon.ts +339 -0
- package/packages/claude-code/src/hook-input.ts +44 -0
- package/packages/claude-code/src/ids.ts +27 -0
- package/packages/claude-code/src/index.ts +18 -0
- package/packages/claude-code/src/launcher.ts +265 -0
- package/packages/claude-code/src/main.ts +127 -0
- package/packages/claude-code/src/mcp.ts +109 -0
- package/packages/claude-code/src/paths.ts +34 -0
- package/packages/claude-code/src/plugin.ts +99 -0
- package/packages/claude-code/src/references.ts +51 -0
- package/packages/claude-code/src/sessions.ts +600 -0
- package/packages/claude-code/src/tools.ts +66 -0
- package/packages/claude-code/src/wire.ts +71 -0
- package/packages/claude-code/src/wrapper.ts +156 -0
- package/packages/cli/dist/hub-cli.js +21647 -0
- 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 +12 -0
- package/packages/codex/src/app-server.ts +44 -0
- package/packages/codex/src/ids.ts +28 -0
- package/packages/codex/src/index.ts +11 -0
- package/packages/codex/src/jsonrpc.ts +168 -0
- package/packages/codex/src/main.ts +123 -0
- package/packages/codex/src/paths.ts +65 -0
- package/packages/codex/src/sessions.ts +578 -0
- package/packages/codex/src/spoke.ts +252 -0
- package/packages/codex/src/stdio.ts +101 -0
- package/packages/codex/src/websocket.ts +206 -0
- package/packages/codex/src/wrapper.ts +104 -0
- package/packages/node/package.json +12 -0
- package/packages/node/src/config.ts +184 -0
- package/packages/node/src/connection.ts +283 -0
- package/packages/node/src/daemon.ts +281 -0
- package/packages/node/src/git.ts +107 -0
- package/packages/node/src/harnesses.ts +95 -0
- package/packages/node/src/host.ts +319 -0
- package/packages/node/src/index.ts +17 -0
- package/packages/node/src/main.ts +11 -0
- package/packages/node/src/openshell.ts +73 -0
- package/packages/node/src/run.ts +106 -0
- package/packages/preset/package.json +12 -0
- package/packages/preset/presets/aider.json +8 -0
- package/packages/preset/presets/goose.json +8 -0
- package/packages/preset/src/index.ts +2 -0
- package/packages/preset/src/main.ts +195 -0
- package/packages/preset/src/presets.ts +87 -0
- package/packages/preset/src/spoke.ts +251 -0
- package/packages/preset/src/tmux.ts +160 -0
- package/packages/protocol/package.json +12 -0
- package/packages/protocol/src/harness-events.ts +207 -0
- package/packages/protocol/src/index.ts +1215 -0
- package/packages/protocol/src/launch.ts +172 -0
- package/packages/protocol/src/node.ts +409 -0
- package/packages/protocol/src/preset.ts +144 -0
- package/packages/protocol/src/webhook.ts +62 -0
- package/packages/protocol/src/wire.ts +54 -0
- package/packages/runtime-openshell/image/Dockerfile +36 -0
- package/packages/runtime-openshell/image/README.md +78 -0
- package/packages/runtime-openshell/image/build.ts +203 -0
- package/packages/runtime-openshell/image/entry.sh +163 -0
- package/packages/runtime-openshell/image/image.ts +119 -0
- package/packages/runtime-openshell/image/start.sh +24 -0
- package/packages/runtime-openshell/package.json +12 -0
- package/packages/runtime-openshell/src/cli.ts +584 -0
- package/packages/runtime-openshell/src/index.ts +6 -0
- package/packages/runtime-openshell/src/policy.ts +155 -0
- package/packages/runtime-openshell/src/profiles.ts +181 -0
- package/packages/runtime-openshell/src/runtime.ts +475 -0
- package/packages/runtime-sdk/package.json +12 -0
- package/packages/runtime-sdk/src/index.ts +87 -0
- package/packages/sdk/package.json +12 -0
- package/packages/sdk/src/address.ts +25 -0
- package/packages/sdk/src/client.ts +553 -0
- package/packages/sdk/src/envelope.ts +108 -0
- package/packages/sdk/src/errors.ts +107 -0
- package/packages/sdk/src/index.ts +10 -0
- package/packages/sdk/src/sse.ts +70 -0
- package/packages/sdk/src/types.ts +517 -0
- package/packages/sdk/src/webhook.ts +102 -0
- package/packages/spoke-sdk/package.json +12 -0
- package/packages/spoke-sdk/src/activity.ts +3 -0
- package/packages/spoke-sdk/src/connection.ts +295 -0
- package/packages/spoke-sdk/src/delivery.ts +119 -0
- package/packages/spoke-sdk/src/enroll.ts +101 -0
- package/packages/spoke-sdk/src/harness-hello.ts +79 -0
- package/packages/spoke-sdk/src/harness-spoke.ts +263 -0
- package/packages/spoke-sdk/src/index.ts +12 -0
- package/packages/spoke-sdk/src/prompt-refs.ts +154 -0
- package/packages/spoke-sdk/src/prompt-source.ts +8 -0
- package/packages/spoke-sdk/src/session-ids.ts +49 -0
- package/packages/spoke-sdk/src/session-tokens.ts +78 -0
- package/scripts/hub-claude-code.service +34 -0
- package/scripts/hub-codex-spoke.service +34 -0
- package/scripts/hub-node.service +35 -0
- package/scripts/io.github.huynle.hub-claude-code.plist +53 -0
- package/scripts/io.github.huynle.hub-codex-spoke.plist +53 -0
- package/scripts/io.github.huynle.hub-node.plist +52 -0
- package/src/central/actions.ts +233 -0
- package/src/central/agent/acting-routes.ts +133 -0
- package/src/central/agent/acting-switch.ts +93 -0
- package/src/central/agent/acting.ts +599 -0
- package/src/central/agent/actor.ts +193 -0
- package/src/central/agent/app-tokens.ts +152 -0
- package/src/central/agent/archive-inactive.ts +66 -0
- package/src/central/agent/audit.ts +185 -0
- package/src/central/agent/decisions.ts +530 -0
- package/src/central/agent/delivery.ts +678 -0
- package/src/central/agent/directory-routes.ts +328 -0
- package/src/central/agent/directory.ts +473 -0
- package/src/central/agent/envelope.ts +51 -0
- package/src/central/agent/errors.ts +74 -0
- package/src/central/agent/escalation.ts +219 -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/grant-requests.ts +221 -0
- package/src/central/agent/grant-routes.ts +70 -0
- package/src/central/agent/grants.ts +235 -0
- package/src/central/agent/launch-engine.ts +1025 -0
- package/src/central/agent/launch-routes.ts +252 -0
- package/src/central/agent/launch-spec.ts +289 -0
- package/src/central/agent/launch-store.ts +264 -0
- package/src/central/agent/launch-tokens.ts +206 -0
- package/src/central/agent/message-limits.ts +123 -0
- package/src/central/agent/message-routes.ts +385 -0
- package/src/central/agent/messages.ts +442 -0
- package/src/central/agent/names.ts +86 -0
- package/src/central/agent/placement.ts +419 -0
- package/src/central/agent/policy.ts +378 -0
- package/src/central/agent/read-routes.ts +385 -0
- package/src/central/agent/routes.ts +281 -0
- package/src/central/agent/session-tokens.ts +227 -0
- package/src/central/agent/versioned.ts +26 -0
- package/src/central/agent/watch.ts +112 -0
- package/src/central/agent/webhook-routes.ts +86 -0
- package/src/central/agent/webhooks.ts +493 -0
- package/src/central/agent-data.ts +177 -0
- package/src/central/auth.ts +350 -121
- package/src/central/backend/file-state.ts +240 -0
- package/src/central/backend/index.ts +116 -0
- package/src/central/backend/memory-bus.ts +93 -0
- package/src/central/backend/memory-catalog.ts +90 -0
- package/src/central/backend/memory-counters.ts +102 -0
- package/src/central/backend/memory-events.ts +45 -0
- package/src/central/backend/memory-kv.ts +38 -0
- package/src/central/backend/memory-state.ts +77 -0
- package/src/central/backend/postgres-migrate.ts +100 -0
- package/src/central/backend/postgres-state.ts +197 -0
- package/src/central/backend/types.ts +246 -0
- package/src/central/backend/valkey-bus.ts +425 -0
- package/src/central/backend/valkey-catalog.ts +190 -0
- package/src/central/backend/valkey-client.ts +36 -0
- package/src/central/backend/valkey-counters.ts +38 -0
- package/src/central/backend/valkey-events.ts +168 -0
- package/src/central/backend/valkey-kv.ts +54 -0
- package/src/central/backend/valkey-locks.ts +92 -0
- package/src/central/cluster/directory.ts +141 -0
- package/src/central/cluster/feed.ts +84 -0
- package/src/central/cluster/leader.ts +232 -0
- package/src/central/cluster/live.ts +151 -0
- package/src/central/cluster/recommend.ts +59 -0
- package/src/central/cluster/rpc.ts +150 -0
- package/src/central/cluster/view.ts +232 -0
- package/src/central/config.ts +197 -3
- package/src/central/done.ts +96 -0
- package/src/central/events-log.ts +100 -0
- package/src/central/http.ts +1149 -170
- package/src/central/import.ts +318 -0
- package/src/central/main.ts +211 -12
- package/src/central/mcp-http.ts +116 -0
- package/src/central/nodes.ts +314 -0
- package/src/central/oidc.ts +34 -14
- package/src/central/push-target.ts +118 -18
- package/src/central/push.ts +256 -82
- package/src/central/registry.ts +902 -92
- package/src/central/say-stream.ts +128 -0
- package/src/central/session-token-push.ts +233 -0
- package/src/central/speech/openai.ts +21 -14
- package/src/central/speech/openrouter.ts +27 -10
- package/src/central/speech/provider.ts +54 -0
- package/src/central/speech/service.ts +22 -7
- package/src/central/spoke-ws.ts +522 -44
- package/src/central/store-shared.ts +518 -0
- package/src/central/store.ts +625 -189
- package/src/central/token-keys.ts +165 -0
- package/src/config.ts +118 -7
- package/src/index.ts +195 -43
- package/src/protocol.ts +3 -650
- package/src/rpc.ts +27 -2
- package/src/spoke/client.ts +3 -272
- package/src/spoke/conversation.ts +16 -12
- package/src/spoke/deliver.ts +75 -0
- package/src/spoke/enroll.ts +2 -79
- package/src/spoke/env-inject.ts +160 -0
- package/src/spoke/hub-rpc.ts +115 -0
- package/src/spoke/hub-tool.ts +409 -0
- package/src/spoke/launch-source.ts +22 -0
- package/src/spoke/launchers.ts +174 -0
- package/src/spoke/locations.ts +3 -2
- package/src/spoke/node-roots.ts +61 -0
- package/src/spoke/prompt-refs.ts +116 -0
- package/src/spoke/session-tokens.ts +2 -0
- package/src/spoke/spoke.ts +465 -28
- package/src/spoke/taskboard.ts +243 -0
- package/src/spoke/timeline.ts +184 -6
- package/src/spoke/tracker.ts +79 -4
- package/src/status.ts +203 -10
- package/src/tui-commands.ts +13 -6
- package/src/tui-messaging.ts +93 -0
- package/src/tui.ts +15 -0
- package/web/dist/apple-touch-icon.png +0 -0
- package/web/dist/assets/highlight-y4rJE6Tt.js +5 -0
- package/web/dist/assets/index-DWnMhXk9.js +20 -0
- package/web/dist/assets/index-DmUXEnw9.css +1 -0
- package/web/dist/assets/ort-wasm-simd-threaded-CxTQ5xH-.wasm +0 -0
- package/web/dist/assets/silero-worker-C8-8vLVS.js +11 -0
- package/web/dist/assets/silero_vad-Dt9-bTDn.onnx +0 -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 +17 -4
- package/web/dist/manifest.webmanifest +2 -2
- package/web/dist/sw.js +79 -15
- package/web/dist/assets/index-DClBvfLL.css +0 -1
- package/web/dist/assets/index-Djo_yF0a.js +0 -12
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// The plan contributors the CLI adds beside the harness adapters and the node services (preview/plan.ts
|
|
2
|
+
// PlanContributor): pure, deterministic functions of (snapshot, profile) and what they were built with, so preview and
|
|
3
|
+
// apply plan the same digest.
|
|
4
|
+
//
|
|
5
|
+
// hubctlContributor the standalone hubctl this verified manifest pins for the host's exact target, into
|
|
6
|
+
// user-bin:hubctl (the stable authority services and MCP clients run), and the `hub` alias
|
|
7
|
+
// next to it (one command surface; the name grants nothing).
|
|
8
|
+
// enrollmentContributor inline enrollment: one enroll-machine for the hub the user named, only while this machine
|
|
9
|
+
// is not enrolled with it (the device code is approved by a person on the hub).
|
|
10
|
+
// refusing a contributor that can refuse: planSetup has no refusal channel for contributors, so a
|
|
11
|
+
// Result failure is thrown as a typed PlanRefusal the CLI catches and maps with exitFor.
|
|
12
|
+
import { ERROR_CODES, harnessCurrentRef, HARNESS_ORDER, selectArtifact, type Action, type ErrorCode, type HarnessID, type Issue, type PathRef, type PlanInput, type Result, type VerifiedManifest } from "../bootstrap/contracts/index.ts"
|
|
13
|
+
import type { PlanContributor } from "../bootstrap/preview/plan.ts"
|
|
14
|
+
import { userBinLinkDecision } from "./user-bin.ts"
|
|
15
|
+
|
|
16
|
+
export const HUBCTL_REF = { root: "user-bin", rel: "hubctl" } as const
|
|
17
|
+
export const HUB_ALIAS_REF = { root: "user-bin", rel: "hub" } as const
|
|
18
|
+
|
|
19
|
+
/** A plan that cannot be made, with the envelope error code it maps to. */
|
|
20
|
+
export class PlanRefusal extends Error {
|
|
21
|
+
override readonly name = "PlanRefusal"
|
|
22
|
+
constructor(
|
|
23
|
+
readonly code: ErrorCode,
|
|
24
|
+
message: string,
|
|
25
|
+
) {
|
|
26
|
+
super(message)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The envelope code for a planner issue: the issue code when it is one, else `invalid`. */
|
|
31
|
+
export const errorCodeOf = (code: string): ErrorCode => ((ERROR_CODES as readonly string[]).includes(code) ? (code as ErrorCode) : "invalid")
|
|
32
|
+
|
|
33
|
+
export const issuesText = (issues: readonly Issue[]): string => issues.map((i) => `${i.path ? `${i.path}: ` : ""}${i.message}`).join("; ")
|
|
34
|
+
|
|
35
|
+
/** A contributor from a planner that returns a Result: a refusal is thrown as a PlanRefusal. */
|
|
36
|
+
export function refusing(plan: (input: PlanInput) => Result<readonly Action[]>): PlanContributor {
|
|
37
|
+
return (input) => {
|
|
38
|
+
const r = plan(input)
|
|
39
|
+
if (!r.ok) throw new PlanRefusal(errorCodeOf(r.issues[0]!.code), issuesText(r.issues))
|
|
40
|
+
return r.value
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* What is installed now (read-only): a file's digest (undefined when there is no regular file) and whether a path is
|
|
46
|
+
* a symlink to a target. The CLI reads the files the plan would write; tests inject fakes.
|
|
47
|
+
*/
|
|
48
|
+
export interface InstalledInspector {
|
|
49
|
+
sha256(ref: PathRef): string | undefined
|
|
50
|
+
linksTo(ref: PathRef, target: PathRef): boolean
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The pinned hubctl and its `hub` alias, each only when not already in place: an installed hubctl with the pinned
|
|
55
|
+
* artifact's digest is that artifact, whichever path the hubctl that plans runs from (HBS-12 E3: a converged host
|
|
56
|
+
* plans no install and no link).
|
|
57
|
+
*/
|
|
58
|
+
export function hubctlContributor(options: { readonly manifest?: VerifiedManifest; readonly installed?: InstalledInspector }): PlanContributor {
|
|
59
|
+
return ({ snapshot }) => {
|
|
60
|
+
const manifest = options.manifest
|
|
61
|
+
if (!manifest) return []
|
|
62
|
+
const artifact = selectArtifact(manifest, snapshot.host.target)
|
|
63
|
+
if (!artifact.ok) return []
|
|
64
|
+
const installed = options.installed
|
|
65
|
+
const actions: Action[] = []
|
|
66
|
+
if (installed?.sha256(HUBCTL_REF) !== artifact.value.sha256)
|
|
67
|
+
actions.push({
|
|
68
|
+
id: "install-hubctl",
|
|
69
|
+
kind: "install-artifact",
|
|
70
|
+
escalation: "user",
|
|
71
|
+
summary: `Install hubctl ${manifest.release} for ${snapshot.host.target} as ~/.local/bin/hubctl (pinned by the signed release manifest)`,
|
|
72
|
+
artifact: "hubctl",
|
|
73
|
+
version: manifest.release,
|
|
74
|
+
target: snapshot.host.target,
|
|
75
|
+
sha256: artifact.value.sha256,
|
|
76
|
+
path: { ...HUBCTL_REF },
|
|
77
|
+
})
|
|
78
|
+
if (!installed?.linksTo(HUB_ALIAS_REF, HUBCTL_REF))
|
|
79
|
+
actions.push({ id: "link-hub", kind: "link-executable", escalation: "user", summary: "Link hub to hubctl (the same command surface)", path: { ...HUB_ALIAS_REF }, target: { ...HUBCTL_REF } })
|
|
80
|
+
return actions
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Read-only facts about paths a plan would link, and the user's PATH (the CLI reads them through the probe). */
|
|
85
|
+
export interface PathInspector {
|
|
86
|
+
/** What is at `ref` now, not following a final symlink. */
|
|
87
|
+
entry(ref: PathRef): "missing" | "file" | "directory" | "symlink" | "other"
|
|
88
|
+
/** The absolute path a symlink at `ref` points to (resolved against its directory); undefined for anything else. */
|
|
89
|
+
linkTarget(ref: PathRef): string | undefined
|
|
90
|
+
/** The absolute path of `ref` on this host. */
|
|
91
|
+
where(ref: PathRef): string | undefined
|
|
92
|
+
/** The first `name` on the user's PATH, as found (not followed). */
|
|
93
|
+
onPath(name: string): string | undefined
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Each harness's executable: its command name, and where it is inside an install root (the adapters' unpackers). */
|
|
97
|
+
export const HARNESS_EXECUTABLES: Readonly<Record<HarnessID, { readonly name: string; readonly rel: string; readonly label: string }>> = Object.freeze({
|
|
98
|
+
opencode: { name: "opencode", rel: "bin/opencode", label: "OpenCode" },
|
|
99
|
+
"claude-code": { name: "claude", rel: "claude", label: "Claude Code" },
|
|
100
|
+
codex: { name: "codex", rel: "bin/codex", label: "Codex" },
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Contract amendment A6 (HBS-12 E4): a hub-installed harness goes on PATH through a typed link in user-bin
|
|
105
|
+
* (`~/.local/bin/opencode` → `data:hub/harnesses/opencode/current/bin/opencode`), journaled and rolled back like any
|
|
106
|
+
* link. Only for a harness hub makes current (this plan selects it, or its `current` is already there), and only when
|
|
107
|
+
* nothing else of that name is on PATH and user-bin holds nothing of that name but a link of hub's own (one into
|
|
108
|
+
* hub's harness directory): a user's binary is never shadowed or replaced.
|
|
109
|
+
*/
|
|
110
|
+
export function harnessLinkContributor(inspector: PathInspector): PlanContributor {
|
|
111
|
+
return ({ profile, planned = [] }) => {
|
|
112
|
+
const actions: Action[] = []
|
|
113
|
+
for (const harness of HARNESS_ORDER) {
|
|
114
|
+
if (!profile.harnesses.includes(harness)) continue
|
|
115
|
+
const exe = HARNESS_EXECUTABLES[harness]
|
|
116
|
+
const current = harnessCurrentRef(harness)
|
|
117
|
+
const target: PathRef = { root: current.root, rel: `${current.rel}/${exe.rel}` }
|
|
118
|
+
const selected = planned.some((a) => a.kind === "select-harness-version" && a.harness === harness)
|
|
119
|
+
if (!selected && inspector.entry(target) !== "file") continue
|
|
120
|
+
const link: PathRef = { root: "user-bin", rel: exe.name }
|
|
121
|
+
const linkPath = inspector.where(link)
|
|
122
|
+
const targetPath = inspector.where(target)
|
|
123
|
+
const ownDir = inspector.where({ root: current.root, rel: `hub/harnesses/${harness}` })
|
|
124
|
+
if (!linkPath || !targetPath || !ownDir) continue
|
|
125
|
+
const pointsTo = inspector.linkTarget(link)
|
|
126
|
+
const found = inspector.onPath(exe.name)
|
|
127
|
+
const decision = userBinLinkDecision({
|
|
128
|
+
entry: inspector.entry(link),
|
|
129
|
+
...(pointsTo !== undefined ? { pointsTo } : {}),
|
|
130
|
+
target: targetPath,
|
|
131
|
+
linkPath,
|
|
132
|
+
hubOwned: (to) => to.startsWith(`${ownDir}/`),
|
|
133
|
+
...(found !== undefined ? { foundOnPath: found } : {}),
|
|
134
|
+
})
|
|
135
|
+
if (decision !== "create" && decision !== "replace") continue
|
|
136
|
+
actions.push({
|
|
137
|
+
id: `link-${harness}`,
|
|
138
|
+
kind: "link-executable",
|
|
139
|
+
escalation: "user",
|
|
140
|
+
summary: `Put the hub-installed ${exe.label} on PATH: link ~/.local/bin/${exe.name} to its current version`,
|
|
141
|
+
path: link,
|
|
142
|
+
target,
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
return actions
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface EnrollmentChoices {
|
|
150
|
+
/** The hub to enroll with (inline enrollment); without it the plan does not enroll. */
|
|
151
|
+
readonly server?: string
|
|
152
|
+
readonly machineName: string
|
|
153
|
+
/** The hub this machine is enrolled with now, if any (read from the credential store before planning). */
|
|
154
|
+
readonly enrolledWith: string | undefined
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function enrollmentContributor(choices: EnrollmentChoices): PlanContributor {
|
|
158
|
+
return () => {
|
|
159
|
+
if (!choices.server || choices.enrolledWith === choices.server) return []
|
|
160
|
+
return [
|
|
161
|
+
{
|
|
162
|
+
id: "enroll-machine",
|
|
163
|
+
kind: "enroll-machine",
|
|
164
|
+
escalation: "user",
|
|
165
|
+
summary: `Enroll this machine with ${choices.server} (device code; you approve it on the hub)`,
|
|
166
|
+
server: choices.server,
|
|
167
|
+
machineName: choices.machineName,
|
|
168
|
+
},
|
|
169
|
+
]
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Per-harness dispatch (HBS-13 integration item 1, adapter amendment A2). The engine holds one executor per action
|
|
2
|
+
// kind (ExecutorMap) and builtinExecutors one HarnessSource, but OpenCode, Claude Code and Codex each supply their own
|
|
3
|
+
// install-harness source and verify-harness (and OpenCode its register-mcp-server) executor. These route each step
|
|
4
|
+
// by `action.harness`. A harness nothing was registered for is refused with a typed failure at preflight, never
|
|
5
|
+
// thrown and never handed to another harness's executor.
|
|
6
|
+
import type { HarnessID, InstallHarness } from "../bootstrap/contracts/index.ts"
|
|
7
|
+
import type { HarnessSource } from "../bootstrap/transaction/builtin.ts"
|
|
8
|
+
import type { ActionExecutor, ActionOf, StepPlan } from "../bootstrap/transaction/executor.ts"
|
|
9
|
+
import { TxError, txFail, txOk } from "../bootstrap/transaction/result.ts"
|
|
10
|
+
|
|
11
|
+
/** The action kinds that name a harness. */
|
|
12
|
+
export type HarnessScopedKind = "install-harness" | "verify-harness" | "register-mcp-server"
|
|
13
|
+
|
|
14
|
+
const LABEL: Readonly<Record<HarnessScopedKind, string>> = {
|
|
15
|
+
"install-harness": "installs",
|
|
16
|
+
"verify-harness": "verifies",
|
|
17
|
+
"register-mcp-server": "registers MCP servers with",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** One executor of `kind` that hands each action to the executor registered for its harness. */
|
|
21
|
+
export function dispatchByHarness<K extends HarnessScopedKind>(kind: K, executors: Partial<Record<HarnessID, ActionExecutor<K>>>): ActionExecutor<K> {
|
|
22
|
+
const harnessOf = (action: ActionOf<K>) => (action as { readonly harness: HarnessID }).harness
|
|
23
|
+
const pick = (action: ActionOf<K>) => executors[harnessOf(action)]
|
|
24
|
+
const missing = (action: ActionOf<K>) => txFail("internal", `this hubctl ${LABEL[kind]} no ${harnessOf(action)} (no executor for ${kind} on ${harnessOf(action)})`)
|
|
25
|
+
// The journal class must not depend on what is registered: an unregistered harness keeps the kind's class.
|
|
26
|
+
const fallback = (action: ActionOf<K>): StepPlan =>
|
|
27
|
+
kind === "verify-harness" ? { class: "external" } : { class: kind === "install-harness" ? "harness" : "config", path: (action as { path: StepPlan["path"] }).path }
|
|
28
|
+
return {
|
|
29
|
+
kind,
|
|
30
|
+
plan: (action) => pick(action)?.plan(action) ?? fallback(action),
|
|
31
|
+
check: async (action, ctx) => {
|
|
32
|
+
const exec = pick(action)
|
|
33
|
+
if (!exec) return missing(action)
|
|
34
|
+
return exec.check ? exec.check(action, ctx) : txOk(undefined)
|
|
35
|
+
},
|
|
36
|
+
change: async (action, ctx) => {
|
|
37
|
+
const exec = pick(action)
|
|
38
|
+
return exec ? exec.change(action, ctx) : missing(action)
|
|
39
|
+
},
|
|
40
|
+
undo: async (action, ctx) => {
|
|
41
|
+
const exec = pick(action)
|
|
42
|
+
if (!exec) return missing(action)
|
|
43
|
+
return exec.undo ? exec.undo(action, ctx) : txOk(undefined)
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface HarnessSources {
|
|
49
|
+
/** The verified hub manifest every source pins against (one per release). */
|
|
50
|
+
readonly manifest: HarnessSource["manifest"]
|
|
51
|
+
readonly sources: Partial<Record<HarnessID, HarnessSource>>
|
|
52
|
+
/** Explicit --allow-downgrade (for every harness). */
|
|
53
|
+
readonly allowDowngrade?: boolean
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** One HarnessSource (builtinExecutors({harness})) over the adapters' sources, by `action.harness`. */
|
|
57
|
+
export function dispatchHarnessSource(options: HarnessSources): HarnessSource {
|
|
58
|
+
const pick = (action: InstallHarness) => options.sources[action.harness]
|
|
59
|
+
return {
|
|
60
|
+
manifest: options.manifest,
|
|
61
|
+
...(options.allowDowngrade ? { allowDowngrade: true } : {}),
|
|
62
|
+
fetch: async (action, staging) => {
|
|
63
|
+
const source = pick(action)
|
|
64
|
+
if (!source) throw new TxError("internal", `this hubctl has no source to install ${action.harness} from`)
|
|
65
|
+
return source.fetch(action, staging)
|
|
66
|
+
},
|
|
67
|
+
unpack: async (action, file, staging) => {
|
|
68
|
+
const source = pick(action)
|
|
69
|
+
return source ? source.unpack(action, file, staging) : txFail("internal", `this hubctl has no source to install ${action.harness} from`)
|
|
70
|
+
},
|
|
71
|
+
installed: (harness) => options.sources[harness]?.installed?.(harness),
|
|
72
|
+
floor: (harness) => options.sources[harness]?.floor?.(harness),
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// The LaunchDriver of `hubctl setup`'s optional verification launch (launch-session; node-services/launch.ts),
|
|
2
|
+
// over the existing hub launch API (the SDK's hub.launches, as `hub launch` uses it; no new central capability):
|
|
3
|
+
//
|
|
4
|
+
// start one launch of the harness in the workspace (checkout mode: the repository as it is), pinned to
|
|
5
|
+
// this machine (placement require), ending when idle, cut off at the bound, cleaned up by the hub
|
|
6
|
+
// awaitResponse until the launch runs a session (or ended without one: failed)
|
|
7
|
+
// awaitIdle until it ended idle (any other outcome fails)
|
|
8
|
+
// inspect what the node reported it left in the workspace (uncommitted changes, commits)
|
|
9
|
+
// cleanup nothing more: cleanup "remove" makes the hub's node remove the launch's own workspace changes
|
|
10
|
+
// cancel stops the launch (an ended one is already gone)
|
|
11
|
+
// slotFree whether the launch still counts against the node (any state but ended)
|
|
12
|
+
//
|
|
13
|
+
// The launch carries no credential; the caller's own hub login (`hub login`, an app token allowed to launch) is
|
|
14
|
+
// what the SDK client authenticates with. Every wait polls with the verification's abort signal.
|
|
15
|
+
import { randomBytes } from "node:crypto"
|
|
16
|
+
import type { LaunchSpec, LaunchView, Launches } from "../../../sdk/src/index.ts"
|
|
17
|
+
import type { LaunchDriver } from "../bootstrap/adapters/node-services/launch.ts"
|
|
18
|
+
import { txFail, txOk } from "../bootstrap/transaction/result.ts"
|
|
19
|
+
|
|
20
|
+
export const VERIFY_PROMPT = "This is a hubctl setup check: reply with the single word OK and change nothing."
|
|
21
|
+
|
|
22
|
+
export interface HubLaunchDriverOptions {
|
|
23
|
+
readonly launches: Pick<Launches, "create" | "get" | "stop" | "list">
|
|
24
|
+
/** This machine's ID (the stored machine credential's); undefined: not enrolled. */
|
|
25
|
+
readonly machineID: () => Promise<string | undefined>
|
|
26
|
+
readonly pollMs?: number
|
|
27
|
+
readonly sleep?: (ms: number, signal: AbortSignal) => Promise<void>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const ACTIVE = ["queued", "placing", "provisioning", "running"] as const
|
|
31
|
+
|
|
32
|
+
const abortableSleep = (ms: number, signal: AbortSignal) =>
|
|
33
|
+
new Promise<void>((resolve, reject) => {
|
|
34
|
+
if (signal.aborted) return reject(new Error("aborted"))
|
|
35
|
+
const t = setTimeout(resolve, ms)
|
|
36
|
+
signal.addEventListener("abort", () => (clearTimeout(t), reject(new Error("aborted"))), { once: true })
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const message = (error: unknown) => (error instanceof Error ? error.message : String(error))
|
|
40
|
+
|
|
41
|
+
export function hubLaunchDriver(options: HubLaunchDriverOptions): LaunchDriver {
|
|
42
|
+
const pollMs = options.pollMs ?? 1000
|
|
43
|
+
const sleep = options.sleep ?? abortableSleep
|
|
44
|
+
const get = async (id: string): Promise<LaunchView> => (await options.launches.get(id)).launch
|
|
45
|
+
|
|
46
|
+
/** Polls until `done` says so (true: ok; a string: failed with it). */
|
|
47
|
+
async function until(id: string, signal: AbortSignal, done: (l: LaunchView) => true | string | undefined) {
|
|
48
|
+
try {
|
|
49
|
+
for (;;) {
|
|
50
|
+
const r = done(await get(id))
|
|
51
|
+
if (r === true) return txOk(undefined)
|
|
52
|
+
if (typeof r === "string") return txFail("harness_not_ready", r)
|
|
53
|
+
await sleep(pollMs, signal)
|
|
54
|
+
}
|
|
55
|
+
} catch (error) {
|
|
56
|
+
return txFail(signal.aborted ? "cancelled" : "offline", signal.aborted ? "cancelled" : `the hub could not be asked about launch ${id} (${message(error)})`)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
async start(request, signal) {
|
|
62
|
+
const machine = await options.machineID().catch(() => undefined)
|
|
63
|
+
if (!machine) return txFail("unauthorized", "this machine is not enrolled (run `hubctl machine enroll`)")
|
|
64
|
+
const spec: LaunchSpec = {
|
|
65
|
+
key: `hubctl-verify-${randomBytes(8).toString("hex")}`,
|
|
66
|
+
harness: request.harness,
|
|
67
|
+
workspace: { repo: request.workspace, mode: "checkout" },
|
|
68
|
+
placement: { machine, mode: "require" },
|
|
69
|
+
prompt: VERIFY_PROMPT,
|
|
70
|
+
labels: { "hub.setup": "verify" },
|
|
71
|
+
end: { when: "idle", timeout: `${Math.max(1, Math.ceil(request.maxDurationS / 60))}m`, cleanup: "remove" },
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const created = await options.launches.create(spec, { signal })
|
|
75
|
+
return txOk({ launchID: created.launch.id })
|
|
76
|
+
} catch (error) {
|
|
77
|
+
return txFail("harness_not_ready", `the hub refused the verification launch (${message(error)}); it needs \`hub login --scope sessions:launch\` and a grant with verb launch`)
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
awaitResponse: (id, signal) => until(id, signal, (l) => (l.state === "running" && l.sessionID ? true : l.state === "ended" ? `the launch ended ${l.outcome ?? ""} before its session answered`.trim() : undefined)),
|
|
81
|
+
awaitIdle: (id, signal) => until(id, signal, (l) => (l.state !== "ended" ? undefined : l.outcome === "idle" ? true : `the launch ended ${l.outcome ?? "without an outcome"}, not idle`)),
|
|
82
|
+
async inspect(id) {
|
|
83
|
+
try {
|
|
84
|
+
const a = (await get(id)).artifacts
|
|
85
|
+
const leftovers = [...(a?.dirty ? ["uncommitted changes"] : []), ...(a?.commits ? [`${a.commits} commit(s) on the launch branch`] : [])]
|
|
86
|
+
return txOk({ leftovers })
|
|
87
|
+
} catch (error) {
|
|
88
|
+
return txFail("offline", `the hub could not be asked about launch ${id} (${message(error)})`)
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
cleanup: async () => txOk(undefined),
|
|
92
|
+
async cancel(id) {
|
|
93
|
+
try {
|
|
94
|
+
if ((await get(id)).state !== "ended") await options.launches.stop(id)
|
|
95
|
+
return txOk(undefined)
|
|
96
|
+
} catch (error) {
|
|
97
|
+
return txFail("offline", `launch ${id} could not be stopped (${message(error)})`)
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
async slotFree(id) {
|
|
101
|
+
try {
|
|
102
|
+
const { launches } = await options.launches.list({ state: [...ACTIVE] })
|
|
103
|
+
return txOk(!launches.some((l) => l.id === id))
|
|
104
|
+
} catch (error) {
|
|
105
|
+
return txFail("offline", `the hub could not list launches (${message(error)})`)
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
}
|