@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,168 @@
|
|
|
1
|
+
// JSON-RPC as Codex's app-server speaks it (codex app-server, P7-T0 spike v0hsdqhm): one JSON object per message,
|
|
2
|
+
// without a "jsonrpc" member. Requests both ways: the client calls methods (thread/start, turn/start …) and the
|
|
3
|
+
// server asks its clients (approvals), each side numbering its own ids. Notifications carry the thread's events.
|
|
4
|
+
// The channel is any transport of whole text messages: lines over a child's stdio (stdio.ts) or WebSocket text frames
|
|
5
|
+
// over the shared daemon's Unix socket (websocket.ts).
|
|
6
|
+
|
|
7
|
+
/** A transport of whole text messages. */
|
|
8
|
+
export interface Channel {
|
|
9
|
+
send(text: string): void
|
|
10
|
+
close(): void
|
|
11
|
+
onMessage(cb: (text: string) => void): void
|
|
12
|
+
/** Called once, when the channel closed (either side). */
|
|
13
|
+
onClose(cb: (reason: string) => void): void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type RpcID = number | string
|
|
17
|
+
|
|
18
|
+
export class RpcError extends Error {
|
|
19
|
+
constructor(
|
|
20
|
+
message: string,
|
|
21
|
+
readonly code: number,
|
|
22
|
+
readonly method?: string,
|
|
23
|
+
) {
|
|
24
|
+
super(message)
|
|
25
|
+
this.name = "RpcError"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** JSON-RPC's code for a request that could not be sent or answered (this side's own failures). */
|
|
30
|
+
export const LOCAL_ERROR = -32000
|
|
31
|
+
|
|
32
|
+
export interface PeerHandlers {
|
|
33
|
+
onNotification?(method: string, params: unknown): void
|
|
34
|
+
onRequest?(id: RpcID, method: string, params: unknown): void
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const DEFAULT_TIMEOUT_MS = 60_000
|
|
38
|
+
|
|
39
|
+
interface Pending {
|
|
40
|
+
readonly method: string
|
|
41
|
+
resolve(value: unknown): void
|
|
42
|
+
reject(error: Error): void
|
|
43
|
+
readonly timer: ReturnType<typeof setTimeout>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const isObj = (v: unknown): v is Record<string, unknown> => typeof v === "object" && v !== null && !Array.isArray(v)
|
|
47
|
+
const isID = (v: unknown): v is RpcID => typeof v === "number" || typeof v === "string"
|
|
48
|
+
|
|
49
|
+
export class JsonRpcPeer {
|
|
50
|
+
readonly closed: Promise<string>
|
|
51
|
+
#closedReason: string | undefined
|
|
52
|
+
#next = 0
|
|
53
|
+
readonly #pending = new Map<RpcID, Pending>()
|
|
54
|
+
readonly #channel: Channel
|
|
55
|
+
readonly #handlers: PeerHandlers
|
|
56
|
+
|
|
57
|
+
constructor(channel: Channel, handlers: PeerHandlers = {}) {
|
|
58
|
+
this.#channel = channel
|
|
59
|
+
this.#handlers = handlers
|
|
60
|
+
let done!: (reason: string) => void
|
|
61
|
+
this.closed = new Promise((resolve) => (done = resolve))
|
|
62
|
+
channel.onMessage((text) => this.#receive(text))
|
|
63
|
+
channel.onClose((reason) => {
|
|
64
|
+
if (this.#closedReason !== undefined) return
|
|
65
|
+
this.#closedReason = reason || "closed"
|
|
66
|
+
for (const [id, call] of this.#pending) {
|
|
67
|
+
clearTimeout(call.timer)
|
|
68
|
+
call.reject(new RpcError(`The app-server connection closed before it answered ${call.method} (${this.#closedReason})`, LOCAL_ERROR, call.method))
|
|
69
|
+
this.#pending.delete(id)
|
|
70
|
+
}
|
|
71
|
+
done(this.#closedReason)
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
get isClosed(): boolean {
|
|
76
|
+
return this.#closedReason !== undefined
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
request(method: string, params?: unknown, options: { readonly timeoutMs?: number } = {}): Promise<any> {
|
|
80
|
+
if (this.#closedReason !== undefined) return Promise.reject(new RpcError(`The app-server connection is closed (${this.#closedReason})`, LOCAL_ERROR, method))
|
|
81
|
+
const id = ++this.#next
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
84
|
+
const timer = setTimeout(() => {
|
|
85
|
+
this.#pending.delete(id)
|
|
86
|
+
reject(new RpcError(`${method}: the app-server did not answer within ${timeoutMs} ms`, LOCAL_ERROR, method))
|
|
87
|
+
}, timeoutMs)
|
|
88
|
+
this.#pending.set(id, { method, resolve, reject, timer })
|
|
89
|
+
this.#send(params === undefined ? { id, method } : { id, method, params })
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
notify(method: string, params?: unknown): void {
|
|
94
|
+
this.#send(params === undefined ? { method } : { method, params })
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Answers a server request. */
|
|
98
|
+
respond(id: RpcID, result: unknown): void {
|
|
99
|
+
this.#send({ id, result })
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
respondError(id: RpcID, code: number, message: string): void {
|
|
103
|
+
this.#send({ id, error: { code, message } })
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
close(): void {
|
|
107
|
+
this.#channel.close()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
#send(message: object): void {
|
|
111
|
+
if (this.#closedReason !== undefined) return
|
|
112
|
+
try {
|
|
113
|
+
this.#channel.send(JSON.stringify(message))
|
|
114
|
+
} catch {
|
|
115
|
+
// The channel reports its own close; a request then fails there.
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
#receive(text: string): void {
|
|
120
|
+
let message: unknown
|
|
121
|
+
try {
|
|
122
|
+
message = JSON.parse(text)
|
|
123
|
+
} catch {
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
if (!isObj(message)) return
|
|
127
|
+
if (typeof message.method === "string") {
|
|
128
|
+
if (isID(message.id)) this.#handlers.onRequest?.(message.id, message.method, message.params)
|
|
129
|
+
else this.#handlers.onNotification?.(message.method, message.params)
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
if (!isID(message.id)) return
|
|
133
|
+
const call = this.#pending.get(message.id)
|
|
134
|
+
if (!call) return
|
|
135
|
+
this.#pending.delete(message.id)
|
|
136
|
+
clearTimeout(call.timer)
|
|
137
|
+
if (isObj(message.error)) {
|
|
138
|
+
const code = typeof message.error.code === "number" ? message.error.code : LOCAL_ERROR
|
|
139
|
+
call.reject(new RpcError(`${call.method}: ${typeof message.error.message === "string" ? message.error.message : "error"}`, code, call.method))
|
|
140
|
+
} else call.resolve(message.result)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Two connected in-memory channels (tests, and in-process fakes). */
|
|
145
|
+
export function memoryChannelPair(): [Channel, Channel] {
|
|
146
|
+
type End = { listeners: ((t: string) => void)[]; closers: ((r: string) => void)[]; closed: boolean }
|
|
147
|
+
const ends: [End, End] = [
|
|
148
|
+
{ listeners: [], closers: [], closed: false },
|
|
149
|
+
{ listeners: [], closers: [], closed: false },
|
|
150
|
+
]
|
|
151
|
+
const close = (reason: string) => {
|
|
152
|
+
for (const end of ends) {
|
|
153
|
+
if (end.closed) continue
|
|
154
|
+
end.closed = true
|
|
155
|
+
for (const cb of end.closers) cb(reason)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const channel = (self: End, other: End): Channel => ({
|
|
159
|
+
send(text) {
|
|
160
|
+
if (self.closed) throw new Error("channel closed")
|
|
161
|
+
for (const cb of other.listeners) cb(text)
|
|
162
|
+
},
|
|
163
|
+
close: () => close("closed"),
|
|
164
|
+
onMessage: (cb) => void self.listeners.push(cb),
|
|
165
|
+
onClose: (cb) => void self.closers.push(cb),
|
|
166
|
+
})
|
|
167
|
+
return [channel(ends[0], ends[1]), channel(ends[1], ends[0])]
|
|
168
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// `hub codex-spoke serve`: the per-user Codex spoke service until SIGINT/SIGTERM (main.ts aborts io.signal), started
|
|
2
|
+
// in the background by `hub codex` and never typed by hand in normal use. It needs an enrolled machine (the machine
|
|
3
|
+
// token in the spoke's config.json, read-only), keeps a pid file in <hub state dir>/codex/spoke.pid (one service per
|
|
4
|
+
// user: a second one refuses while the first runs) and attaches to the shared Codex daemon's socket when present.
|
|
5
|
+
//
|
|
6
|
+
// Settings (environment): HUB_CODEX_BIN (default: the Codex `hubctl setup` installed, else `codex` on PATH), HUB_CODEX_DAEMON_SOCKET (another socket, or
|
|
7
|
+
// "off"), HUB_CODEX_CONFIG (Codex config overrides for the app-server the spoke starts, one key=value per line,
|
|
8
|
+
// passed as -c; Codex's files are never written).
|
|
9
|
+
import { spawnSync } from "node:child_process"
|
|
10
|
+
import { openSync, closeSync, readFileSync, rmSync, writeSync } from "node:fs"
|
|
11
|
+
import { configPath } from "../../../src/config.ts"
|
|
12
|
+
import { readSpokeCredentials } from "../../node/src/config.ts"
|
|
13
|
+
import { codexBinary } from "../../node/src/harnesses.ts"
|
|
14
|
+
import pkg from "../package.json" with { type: "json" }
|
|
15
|
+
import { codexStateDir, ensurePrivateDir, daemonSocketPath, pidFile, readServicePid } from "./paths.ts"
|
|
16
|
+
import { startCodexSpoke } from "./spoke.ts"
|
|
17
|
+
|
|
18
|
+
export const VERSION: string = pkg.version
|
|
19
|
+
|
|
20
|
+
export interface CodexMainIo {
|
|
21
|
+
readonly env: Readonly<Record<string, string | undefined>>
|
|
22
|
+
readonly home: string
|
|
23
|
+
stdout(s: string): void
|
|
24
|
+
stderr(s: string): void
|
|
25
|
+
readonly signal?: AbortSignal
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const CODEX_SPOKE_USAGE = "Usage: hub codex-spoke serve | install [--dry-run] | uninstall [--dry-run] | status"
|
|
29
|
+
|
|
30
|
+
/** `codex --version` → "0.160.0", or "" when it cannot be told. */
|
|
31
|
+
export function codexVersion(bin: string, env: CodexMainIo["env"]): string {
|
|
32
|
+
try {
|
|
33
|
+
const r = spawnSync(bin, ["--version"], { encoding: "utf8", timeout: 5000, env: env as NodeJS.ProcessEnv })
|
|
34
|
+
return /\d+\.\d+\.\d+[\w.+-]*/.exec(r.stdout ?? "")?.[0]?.slice(0, 100) ?? ""
|
|
35
|
+
} catch {
|
|
36
|
+
return ""
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** HUB_CODEX_CONFIG's lines as `-c key=value` arguments. */
|
|
41
|
+
export function configOverrides(value: string | undefined): string[] {
|
|
42
|
+
return (value ?? "")
|
|
43
|
+
.split("\n")
|
|
44
|
+
.map((line) => line.trim())
|
|
45
|
+
.filter((line) => line !== "" && line.includes("="))
|
|
46
|
+
.flatMap((line) => ["-c", line])
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Claims the pid file for this process; false when another live service holds it. */
|
|
50
|
+
function claimPidFile(file: string, env: CodexMainIo["env"], home: string): boolean {
|
|
51
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
52
|
+
try {
|
|
53
|
+
const fd = openSync(file, "wx", 0o600)
|
|
54
|
+
try {
|
|
55
|
+
writeSync(fd, `${process.pid}\n`)
|
|
56
|
+
} finally {
|
|
57
|
+
closeSync(fd)
|
|
58
|
+
}
|
|
59
|
+
return true
|
|
60
|
+
} catch (cause) {
|
|
61
|
+
if ((cause as NodeJS.ErrnoException).code !== "EEXIST") throw cause
|
|
62
|
+
if (readServicePid(env, home) !== undefined) return false
|
|
63
|
+
rmSync(file, { force: true }) // a stale file: its process is gone
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return false
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function releasePidFile(file: string): void {
|
|
70
|
+
try {
|
|
71
|
+
if (readFileSync(file, "utf8").trim() === String(process.pid)) rmSync(file, { force: true })
|
|
72
|
+
} catch {}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function serve(io: CodexMainIo): Promise<number> {
|
|
76
|
+
const credentials = readSpokeCredentials(io.env, io.home)
|
|
77
|
+
if (!credentials) {
|
|
78
|
+
io.stderr(`hub codex-spoke serve: this machine is not enrolled with a hub (no machine token in ${configPath(io.env, io.home)})\n`)
|
|
79
|
+
return 1
|
|
80
|
+
}
|
|
81
|
+
const log = (line: string) => io.stderr(`[hub codex-spoke] ${new Date().toISOString()} ${line}\n`)
|
|
82
|
+
ensurePrivateDir(codexStateDir(io.env, io.home))
|
|
83
|
+
const file = pidFile(io.env, io.home)
|
|
84
|
+
if (!claimPidFile(file, io.env, io.home)) {
|
|
85
|
+
io.stderr(`hub codex-spoke serve: already running (pid ${readServicePid(io.env, io.home) ?? "?"})\n`)
|
|
86
|
+
return 1
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const codexBin = codexBinary(io.env, io.home)
|
|
90
|
+
const env: Record<string, string | undefined> = { ...io.env }
|
|
91
|
+
delete env.BUN_BE_BUN
|
|
92
|
+
const daemonSocket = daemonSocketPath(io.env, io.home)
|
|
93
|
+
const running = startCodexSpoke({
|
|
94
|
+
server: credentials.server,
|
|
95
|
+
token: credentials.token,
|
|
96
|
+
harnessVersion: codexVersion(codexBin, env),
|
|
97
|
+
pluginVersion: VERSION,
|
|
98
|
+
directory: io.home,
|
|
99
|
+
codexBin,
|
|
100
|
+
codexArgs: configOverrides(io.env.HUB_CODEX_CONFIG),
|
|
101
|
+
env,
|
|
102
|
+
...(daemonSocket ? { daemonSocket } : {}),
|
|
103
|
+
log,
|
|
104
|
+
})
|
|
105
|
+
log(daemonSocket ? `serving; the Codex daemon socket is ${daemonSocket}` : "serving; not attaching to a Codex daemon")
|
|
106
|
+
await new Promise<void>((resolve) => {
|
|
107
|
+
if (!io.signal) return
|
|
108
|
+
if (io.signal.aborted) return resolve()
|
|
109
|
+
io.signal.addEventListener("abort", () => resolve(), { once: true })
|
|
110
|
+
})
|
|
111
|
+
await running.stop()
|
|
112
|
+
return 0
|
|
113
|
+
} finally {
|
|
114
|
+
releasePidFile(file)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function codexMain(argv: readonly string[], io: CodexMainIo): Promise<number> {
|
|
119
|
+
const [command] = argv
|
|
120
|
+
if (command === "serve") return serve(io)
|
|
121
|
+
io.stderr(`${CODEX_SPOKE_USAGE}\n`)
|
|
122
|
+
return 2
|
|
123
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Where the Codex spoke looks and keeps its files. It only reads Codex's home (CODEX_HOME, default ~/.codex) to find
|
|
2
|
+
// the shared daemon's socket; its own files live in <hub state dir>/codex (the hub state dir is $HUB_STATE_DIR, else
|
|
3
|
+
// ~/.local/state/hub), a private (0700) directory holding the service's pid file (spoke.pid) and its log.
|
|
4
|
+
// $HUB_CODEX_DAEMON_SOCKET names another daemon socket, or "off" for none.
|
|
5
|
+
import { chmodSync, lstatSync, mkdirSync, readFileSync } from "node:fs"
|
|
6
|
+
import { homedir } from "node:os"
|
|
7
|
+
import path from "node:path"
|
|
8
|
+
import { hubEnv, type Env } from "../../../src/env.ts"
|
|
9
|
+
import { APP_DIR, stateBase } from "../../../src/migrate.ts"
|
|
10
|
+
|
|
11
|
+
export function codexHome(env: Env = process.env, home: string = homedir()): string {
|
|
12
|
+
return env.CODEX_HOME || path.join(home, ".codex")
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** The shared daemon's control socket, or undefined when attaching to it is turned off. */
|
|
16
|
+
export function daemonSocketPath(env: Env = process.env, home: string = homedir()): string | undefined {
|
|
17
|
+
const override = env.HUB_CODEX_DAEMON_SOCKET
|
|
18
|
+
if (override === "off") return undefined
|
|
19
|
+
return override || path.join(codexHome(env, home), "app-server-control", "app-server-control.sock")
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function codexStateDir(env: Env = process.env, home: string = homedir()): string {
|
|
23
|
+
return path.join(hubEnv(env).HUB_STATE_DIR || path.join(stateBase(home), APP_DIR), "codex")
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const pidFile = (env: Env = process.env, home: string = homedir()) => path.join(codexStateDir(env, home), "spoke.pid")
|
|
27
|
+
|
|
28
|
+
/** A usable process ID: an integer above 1 (0 and negative IDs signal process groups or every process). */
|
|
29
|
+
export const isPid = (n: unknown): n is number => typeof n === "number" && Number.isInteger(n) && n > 1
|
|
30
|
+
|
|
31
|
+
export function isAlive(pid: number): boolean {
|
|
32
|
+
if (!isPid(pid)) return false
|
|
33
|
+
try {
|
|
34
|
+
process.kill(pid, 0)
|
|
35
|
+
return true
|
|
36
|
+
} catch (cause) {
|
|
37
|
+
return (cause as NodeJS.ErrnoException).code === "EPERM"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The running service's pid from its pid file, or undefined when there is none or that process is gone. */
|
|
42
|
+
export function readServicePid(env: Env = process.env, home: string = homedir()): number | undefined {
|
|
43
|
+
let text: string
|
|
44
|
+
try {
|
|
45
|
+
text = readFileSync(pidFile(env, home), "utf8").trim()
|
|
46
|
+
} catch {
|
|
47
|
+
return undefined
|
|
48
|
+
}
|
|
49
|
+
const pid = /^\d+$/.test(text) ? Number(text) : NaN
|
|
50
|
+
return isPid(pid) && isAlive(pid) ? pid : undefined
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Makes `dir` (and its parents) and checks it is a real directory of this user that nobody else can reach: a
|
|
55
|
+
* symlink or someone else's directory is refused, group or other permissions are removed (0700).
|
|
56
|
+
*/
|
|
57
|
+
export function ensurePrivateDir(dir: string): void {
|
|
58
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 })
|
|
59
|
+
const stat = lstatSync(dir)
|
|
60
|
+
if (stat.isSymbolicLink()) throw new Error(`${dir} is a symlink; the Codex spoke needs a real directory there`)
|
|
61
|
+
if (!stat.isDirectory()) throw new Error(`${dir} is not a directory`)
|
|
62
|
+
const uid = process.getuid?.()
|
|
63
|
+
if (uid !== undefined && stat.uid !== uid) throw new Error(`${dir} belongs to another user`)
|
|
64
|
+
if ((stat.mode & 0o777) !== 0o700) chmodSync(dir, 0o700)
|
|
65
|
+
}
|