@huynle/hub 0.6.0 → 0.7.1
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 +800 -0
- package/Dockerfile +30 -0
- package/README.md +870 -66
- package/package.json +45 -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 +155 -0
- package/packages/cli/dist/hub-cli.js +10162 -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 +122 -0
- package/packages/codex/src/paths.ts +65 -0
- package/packages/codex/src/sessions.ts +578 -0
- package/packages/codex/src/spoke.ts +249 -0
- package/packages/codex/src/stdio.ts +101 -0
- package/packages/codex/src/websocket.ts +206 -0
- package/packages/codex/src/wrapper.ts +100 -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 +60 -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 +502 -0
- package/packages/sdk/src/envelope.ts +108 -0
- package/packages/sdk/src/errors.ts +106 -0
- package/packages/sdk/src/index.ts +10 -0
- package/packages/sdk/src/sse.ts +67 -0
- package/packages/sdk/src/types.ts +458 -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 +79 -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 +142 -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 +185 -0
- package/src/central/agent/directory.ts +473 -0
- package/src/central/agent/envelope.ts +51 -0
- package/src/central/agent/errors.ts +72 -0
- package/src/central/agent/escalation.ts +219 -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 +389 -0
- package/src/central/agent/routes.ts +278 -0
- package/src/central/agent/session-tokens.ts +227 -0
- package/src/central/agent/versioned.ts +26 -0
- package/src/central/agent/watch.ts +221 -0
- package/src/central/agent/webhook-routes.ts +78 -0
- package/src/central/agent/webhooks.ts +449 -0
- package/src/central/agent-data.ts +177 -0
- package/src/central/auth.ts +224 -121
- package/src/central/backend/file-state.ts +240 -0
- package/src/central/backend/index.ts +113 -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 +244 -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 +1067 -171
- package/src/central/import.ts +318 -0
- package/src/central/main.ts +211 -12
- 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 +572 -189
- package/src/central/token-keys.ts +165 -0
- package/src/config.ts +49 -1
- package/src/index.ts +165 -12
- 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 +68 -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/assets/highlight-y4rJE6Tt.js +5 -0
- package/web/dist/assets/index-D9zMbxSg.css +1 -0
- package/web/dist/assets/index-db1z4duA.js +20 -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/index.html +9 -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,106 @@
|
|
|
1
|
+
// The hub's error envelope (design §8): every /api/agent failure answers {error: {code, message, hint?, ...extra}}.
|
|
2
|
+
// The SDK turns it into a HubError; failures without an envelope (hub unreachable, a proxy's HTML, a body that is not
|
|
3
|
+
// JSON, a broken stream) are HubTransportErrors. Neither ever carries the token.
|
|
4
|
+
import type { Evaluation } from "./types.ts"
|
|
5
|
+
|
|
6
|
+
/** The codes the hub answers with (src/central/agent/errors.ts); later phases may add more, so any string is kept. */
|
|
7
|
+
export type ErrorCode =
|
|
8
|
+
| "invalid"
|
|
9
|
+
| "unauthorized"
|
|
10
|
+
| "token_expired"
|
|
11
|
+
| "needs_user"
|
|
12
|
+
| "forbidden"
|
|
13
|
+
| "not_found"
|
|
14
|
+
| "ambiguous"
|
|
15
|
+
| "name_taken"
|
|
16
|
+
| "gone"
|
|
17
|
+
| "unsupported"
|
|
18
|
+
| "paused"
|
|
19
|
+
| "rate_limited"
|
|
20
|
+
| "internal"
|
|
21
|
+
| "offline"
|
|
22
|
+
| (string & {})
|
|
23
|
+
|
|
24
|
+
export interface Envelope {
|
|
25
|
+
readonly code: ErrorCode
|
|
26
|
+
readonly message: string
|
|
27
|
+
readonly hint?: string
|
|
28
|
+
/**
|
|
29
|
+
* More fields, by code: candidates (ambiguous), retryAfter (rate_limited, seconds), feature/harness (unsupported),
|
|
30
|
+
* evaluation/decisionID (needs_user; see needsUserOf).
|
|
31
|
+
*/
|
|
32
|
+
readonly [extra: string]: unknown
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A failure the hub reported in its envelope. */
|
|
36
|
+
export class HubError extends Error {
|
|
37
|
+
override readonly name = "HubError"
|
|
38
|
+
readonly code: ErrorCode
|
|
39
|
+
readonly hint?: string
|
|
40
|
+
|
|
41
|
+
constructor(
|
|
42
|
+
/** HTTP status of the answer. */
|
|
43
|
+
readonly status: number,
|
|
44
|
+
readonly envelope: Envelope,
|
|
45
|
+
) {
|
|
46
|
+
super(envelope.message)
|
|
47
|
+
this.code = envelope.code
|
|
48
|
+
if (typeof envelope.hint === "string") this.hint = envelope.hint
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Seconds to wait before retrying (rate_limited), when the hub said. */
|
|
52
|
+
get retryAfter(): number | undefined {
|
|
53
|
+
const value = this.envelope.retryAfter
|
|
54
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* A failure without the hub's envelope: `unreachable` (no answer), `http` (a non-OK answer without the envelope, or
|
|
60
|
+
* a redirect), `bad_response` (an OK answer that is not what was expected), `stream` (a watch stream that broke).
|
|
61
|
+
*/
|
|
62
|
+
export type TransportKind = "unreachable" | "http" | "bad_response" | "stream"
|
|
63
|
+
|
|
64
|
+
export class HubTransportError extends Error {
|
|
65
|
+
override readonly name = "HubTransportError"
|
|
66
|
+
/** HTTP status, for `http`. */
|
|
67
|
+
readonly status?: number
|
|
68
|
+
|
|
69
|
+
constructor(
|
|
70
|
+
readonly kind: TransportKind,
|
|
71
|
+
message: string,
|
|
72
|
+
options: { readonly status?: number; readonly cause?: unknown } = {},
|
|
73
|
+
) {
|
|
74
|
+
super(message, options.cause !== undefined ? { cause: options.cause } : undefined)
|
|
75
|
+
if (options.status !== undefined) this.status = options.status
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** True for a HubError (with one of `codes`, when given). */
|
|
80
|
+
export const isHubError = (error: unknown, ...codes: ErrorCode[]): error is HubError =>
|
|
81
|
+
error instanceof HubError && (codes.length === 0 || codes.includes(error.code))
|
|
82
|
+
|
|
83
|
+
/** What a needs_user failure carries: why (the evaluation) and the Decision it opened for the user, if any. */
|
|
84
|
+
export interface NeedsUser {
|
|
85
|
+
readonly evaluation?: Evaluation
|
|
86
|
+
readonly decisionID?: string
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The needs_user details of an error (undefined for any other error). */
|
|
90
|
+
export function needsUserOf(error: unknown): NeedsUser | undefined {
|
|
91
|
+
if (!isHubError(error, "needs_user")) return undefined
|
|
92
|
+
const { evaluation, decisionID } = error.envelope
|
|
93
|
+
return {
|
|
94
|
+
...(typeof evaluation === "object" && evaluation !== null ? { evaluation: evaluation as Evaluation } : {}),
|
|
95
|
+
...(typeof decisionID === "string" ? { decisionID } : {}),
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The envelope inside a parsed body, if it is one (also the console routes' {error: message, code, hint?}). */
|
|
100
|
+
export function envelopeOf(body: unknown): Envelope | undefined {
|
|
101
|
+
const raw = body as { error?: unknown; code?: unknown; hint?: unknown } | null
|
|
102
|
+
const error = raw?.error as Envelope | undefined
|
|
103
|
+
if (typeof error === "object" && error !== null && typeof error.code === "string" && typeof error.message === "string") return error
|
|
104
|
+
if (typeof raw?.error === "string" && typeof raw.code === "string") return { code: raw.code, message: raw.error, ...(typeof raw.hint === "string" ? { hint: raw.hint } : {}) }
|
|
105
|
+
return undefined
|
|
106
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// The hub's API client (@huynle/hub-sdk): a typed client for /api/agent (HubClient), the error envelope as typed
|
|
2
|
+
// errors (HubError, HubTransportError), the answer types, the SSE parser of the watch stream, and the pure
|
|
3
|
+
// envelope renderer/sanitiser and address parser shared with the hub, launches, and webhook verification.
|
|
4
|
+
export * from "./address.ts"
|
|
5
|
+
export * from "./client.ts"
|
|
6
|
+
export * from "./envelope.ts"
|
|
7
|
+
export * from "./errors.ts"
|
|
8
|
+
export * from "./sse.ts"
|
|
9
|
+
export * from "./webhook.ts"
|
|
10
|
+
export type * from "./types.ts"
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Server-sent events (GET /api/agent/watch): an incremental parser, and the event stream of a response.
|
|
2
|
+
import { HubTransportError } from "./errors.ts"
|
|
3
|
+
import type { WatchEvent } from "./types.ts"
|
|
4
|
+
|
|
5
|
+
/** Parses SSE text incrementally: feed chunks, get complete events (events without data are skipped). */
|
|
6
|
+
export class SseParser {
|
|
7
|
+
#buffer = ""
|
|
8
|
+
|
|
9
|
+
push(chunk: string): WatchEvent[] {
|
|
10
|
+
this.#buffer += chunk.replace(/\r\n?/g, "\n")
|
|
11
|
+
const out: WatchEvent[] = []
|
|
12
|
+
let end: number
|
|
13
|
+
while ((end = this.#buffer.indexOf("\n\n")) >= 0) {
|
|
14
|
+
const block = this.#buffer.slice(0, end)
|
|
15
|
+
this.#buffer = this.#buffer.slice(end + 2)
|
|
16
|
+
let event = "message"
|
|
17
|
+
const data: string[] = []
|
|
18
|
+
for (const line of block.split("\n")) {
|
|
19
|
+
if (line.startsWith(":")) continue
|
|
20
|
+
const colon = line.indexOf(":")
|
|
21
|
+
const field = colon < 0 ? line : line.slice(0, colon)
|
|
22
|
+
const value = colon < 0 ? "" : line.slice(colon + 1).replace(/^ /, "")
|
|
23
|
+
if (field === "event") event = value
|
|
24
|
+
else if (field === "data") data.push(value)
|
|
25
|
+
}
|
|
26
|
+
if (data.length) out.push({ event, data: data.join("\n") })
|
|
27
|
+
}
|
|
28
|
+
return out
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The named events of a heartbeat the hub sends to keep the stream open. */
|
|
33
|
+
const HEARTBEAT = "ping"
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The events of an SSE response until it ends (heartbeats skipped). Ends quietly once `signal` aborts; a stream that
|
|
37
|
+
* breaks otherwise is a `stream` HubTransportError.
|
|
38
|
+
*/
|
|
39
|
+
export async function* sseEvents(response: Response, signal?: AbortSignal): AsyncGenerator<WatchEvent, void, undefined> {
|
|
40
|
+
if (!response.body) throw new HubTransportError("bad_response", "the hub sent no watch stream")
|
|
41
|
+
const reader = response.body.getReader()
|
|
42
|
+
const decoder = new TextDecoder()
|
|
43
|
+
const parser = new SseParser()
|
|
44
|
+
const stop = () => void reader.cancel().catch(() => {})
|
|
45
|
+
signal?.addEventListener("abort", stop, { once: true })
|
|
46
|
+
try {
|
|
47
|
+
for (;;) {
|
|
48
|
+
if (signal?.aborted) return
|
|
49
|
+
let chunk: Awaited<ReturnType<typeof reader.read>>
|
|
50
|
+
try {
|
|
51
|
+
chunk = await reader.read()
|
|
52
|
+
} catch (cause) {
|
|
53
|
+
if (signal?.aborted) return
|
|
54
|
+
throw new HubTransportError("stream", `the watch stream failed (${cause instanceof Error ? cause.message : String(cause)})`, { cause })
|
|
55
|
+
}
|
|
56
|
+
if (chunk.done || signal?.aborted) return
|
|
57
|
+
for (const event of parser.push(decoder.decode(chunk.value, { stream: true }))) {
|
|
58
|
+
if (event.event === HEARTBEAT) continue
|
|
59
|
+
yield event
|
|
60
|
+
if (signal?.aborted) return
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} finally {
|
|
64
|
+
signal?.removeEventListener("abort", stop)
|
|
65
|
+
stop()
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
// The shapes of the hub's /api/agent answers (src/central/agent/*). Fields are those the hub sends today; clients
|
|
2
|
+
// should ignore unknown ones, and later hubs may add fields.
|
|
3
|
+
|
|
4
|
+
/** The caller, as whoami and status (`you`) report it. */
|
|
5
|
+
export type Actor =
|
|
6
|
+
| { readonly kind: "user"; readonly userID: string; readonly deviceID: string; readonly via: "cookie" | "device-token" }
|
|
7
|
+
| {
|
|
8
|
+
readonly kind: "session"
|
|
9
|
+
readonly userID: string
|
|
10
|
+
readonly machineID: string
|
|
11
|
+
readonly sessionID: string
|
|
12
|
+
/** Which of the session's tokens: its shell token, or the act token only the hub tool has (missing: shell). */
|
|
13
|
+
readonly via?: "shell" | "act"
|
|
14
|
+
}
|
|
15
|
+
| { readonly kind: "app"; readonly userID: string; readonly tokenID: string; readonly scopes: readonly string[]; readonly label?: string }
|
|
16
|
+
|
|
17
|
+
export interface UserView {
|
|
18
|
+
readonly id: string
|
|
19
|
+
readonly name?: string
|
|
20
|
+
readonly email?: string
|
|
21
|
+
readonly role?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface Whoami {
|
|
25
|
+
readonly actor: Actor
|
|
26
|
+
readonly user: UserView
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface Blocker {
|
|
30
|
+
readonly kind: string
|
|
31
|
+
readonly id?: string
|
|
32
|
+
readonly summary: string
|
|
33
|
+
readonly sessionID?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** One session in status and sessions answers. */
|
|
37
|
+
export interface SessionView {
|
|
38
|
+
readonly id: string
|
|
39
|
+
readonly name?: string
|
|
40
|
+
readonly roles?: readonly string[]
|
|
41
|
+
readonly title?: string
|
|
42
|
+
readonly state: string
|
|
43
|
+
readonly harness?: string
|
|
44
|
+
readonly parentID?: string
|
|
45
|
+
readonly busySince?: number
|
|
46
|
+
readonly now?: string
|
|
47
|
+
readonly blockers?: readonly Blocker[]
|
|
48
|
+
readonly children?: number
|
|
49
|
+
readonly labels?: Readonly<Record<string, string>>
|
|
50
|
+
/** In sessions answers. */
|
|
51
|
+
readonly machineID?: string
|
|
52
|
+
readonly project?: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface AttentionItem {
|
|
56
|
+
readonly kind: string
|
|
57
|
+
readonly sessionID: string
|
|
58
|
+
readonly name?: string
|
|
59
|
+
readonly id?: string
|
|
60
|
+
readonly summary: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** GET /api/agent/status: machines → projects → sessions, and what needs attention. */
|
|
64
|
+
export interface Status {
|
|
65
|
+
readonly machines: readonly {
|
|
66
|
+
readonly id: string
|
|
67
|
+
readonly name?: string
|
|
68
|
+
readonly online: boolean
|
|
69
|
+
readonly harnesses?: readonly string[]
|
|
70
|
+
readonly projects: readonly { readonly root: string; readonly name: string; readonly sessions: readonly SessionView[] }[]
|
|
71
|
+
}[]
|
|
72
|
+
readonly attention: readonly AttentionItem[]
|
|
73
|
+
readonly you?: Actor
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** GET /api/agent/sessions/:addr: one session in detail. */
|
|
77
|
+
export interface SessionDetail extends SessionView {
|
|
78
|
+
readonly online?: boolean
|
|
79
|
+
readonly directory?: string
|
|
80
|
+
readonly lastSeen?: number
|
|
81
|
+
readonly summary?: {
|
|
82
|
+
readonly branch?: string
|
|
83
|
+
readonly context?: { readonly used: number; readonly limit?: number; readonly percent?: number; readonly model?: string }
|
|
84
|
+
readonly changes?: { readonly additions: number; readonly deletions: number; readonly totalFiles: number }
|
|
85
|
+
readonly mcp?: readonly { readonly name: string; readonly status: string }[]
|
|
86
|
+
}
|
|
87
|
+
readonly unsupported?: readonly { readonly feature?: string; readonly harness?: string }[]
|
|
88
|
+
readonly failed?: readonly { readonly feature?: string }[]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** One timeline row (newest first). */
|
|
92
|
+
export interface LogRow {
|
|
93
|
+
readonly kind: string
|
|
94
|
+
readonly text?: string
|
|
95
|
+
readonly name?: string
|
|
96
|
+
readonly state?: string
|
|
97
|
+
readonly title?: string
|
|
98
|
+
readonly at?: number
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface RoleView {
|
|
102
|
+
readonly role: string
|
|
103
|
+
readonly project?: string
|
|
104
|
+
readonly generation?: number
|
|
105
|
+
readonly exclusive?: boolean
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** One session in directory answers (who). */
|
|
109
|
+
export interface DirectoryEntry {
|
|
110
|
+
readonly sessionID: string
|
|
111
|
+
readonly name?: string
|
|
112
|
+
readonly aliases?: readonly string[]
|
|
113
|
+
readonly machineID?: string
|
|
114
|
+
readonly parentID?: string
|
|
115
|
+
readonly project?: string
|
|
116
|
+
readonly title?: string
|
|
117
|
+
readonly state?: string
|
|
118
|
+
readonly online: boolean
|
|
119
|
+
readonly lastSeen?: number
|
|
120
|
+
readonly archived?: boolean
|
|
121
|
+
readonly roles: readonly RoleView[]
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface Named {
|
|
125
|
+
readonly sessionID: string
|
|
126
|
+
readonly name: string
|
|
127
|
+
/** The previous name, kept as an alias for 24 h. */
|
|
128
|
+
readonly alias?: string
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface RoleClaim extends RoleView {
|
|
132
|
+
readonly sessionID: string
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface RoleRelease {
|
|
136
|
+
readonly sessionID: string
|
|
137
|
+
readonly role: string
|
|
138
|
+
readonly project?: string
|
|
139
|
+
readonly released?: boolean
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface AppTokenView {
|
|
143
|
+
readonly id: string
|
|
144
|
+
readonly label?: string
|
|
145
|
+
readonly scopes: readonly string[]
|
|
146
|
+
readonly created?: number
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** POST /api/agent/app-tokens: the token is shown once. */
|
|
150
|
+
export interface CreatedAppToken extends AppTokenView {
|
|
151
|
+
readonly token: string
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Who sends or receives a message: a session, the user (`@me`), or an app token. */
|
|
155
|
+
export type Party =
|
|
156
|
+
| { readonly kind: "session"; readonly sessionID: string; readonly name?: string; readonly harness?: string; readonly machine?: string }
|
|
157
|
+
| { readonly kind: "user" }
|
|
158
|
+
| { readonly kind: "app"; readonly tokenID: string; readonly label?: string }
|
|
159
|
+
|
|
160
|
+
export type MessageKind = "msg" | "ask" | "reply" | "decline"
|
|
161
|
+
/** `sent` for msg/reply/decline; asks are open until answered, declined or expired. */
|
|
162
|
+
export type MessageState = "sent" | "open" | "answered" | "declined" | "expired"
|
|
163
|
+
/** How a message reached its recipient session. */
|
|
164
|
+
export type MessageDelivery = "steer" | "turn" | "queued" | "retained" | "pending"
|
|
165
|
+
|
|
166
|
+
/** A stored agent message. */
|
|
167
|
+
export interface AgentMessage {
|
|
168
|
+
readonly id: string
|
|
169
|
+
readonly kind: MessageKind
|
|
170
|
+
/** The authenticated sender. */
|
|
171
|
+
readonly from: Party
|
|
172
|
+
/** The address as the sender wrote it. */
|
|
173
|
+
readonly to: string
|
|
174
|
+
/** The party it reached. */
|
|
175
|
+
readonly recipient: Party
|
|
176
|
+
/** The role holder generation it reached (role addresses). */
|
|
177
|
+
readonly generation?: number
|
|
178
|
+
readonly text: string
|
|
179
|
+
readonly replyTo?: string
|
|
180
|
+
readonly state: MessageState
|
|
181
|
+
readonly at: number
|
|
182
|
+
readonly hop: number
|
|
183
|
+
readonly expiresAt?: number
|
|
184
|
+
readonly delivery?: MessageDelivery
|
|
185
|
+
readonly deliveredAt?: number
|
|
186
|
+
/** The reply or decline that closed an ask. */
|
|
187
|
+
readonly answer?: { readonly id: string; readonly at: number }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** POST /api/agent/msg and /ask: the stored message and how it was delivered (none: stored only). */
|
|
191
|
+
export interface Sent {
|
|
192
|
+
readonly message: AgentMessage
|
|
193
|
+
readonly delivery?: MessageDelivery
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** POST /api/agent/msg or /ask with all: one per session. */
|
|
197
|
+
export interface SentAll {
|
|
198
|
+
readonly sent: readonly Sent[]
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** POST /api/agent/ask (after any wait) and GET /api/agent/messages/:id: the message and, once closed, its answer. */
|
|
202
|
+
export interface Asked extends Sent {
|
|
203
|
+
readonly answer?: AgentMessage
|
|
204
|
+
/** An ask to @me: the Decision it opened. */
|
|
205
|
+
readonly decision?: Decision
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** One choice a Decision offers; onTimeout may pick only a safe one. */
|
|
209
|
+
export interface DecisionOption {
|
|
210
|
+
readonly id: string
|
|
211
|
+
readonly label: string
|
|
212
|
+
readonly safe?: boolean
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export type DecisionUrgency = "low" | "normal" | "high"
|
|
216
|
+
/** confirm: any of the user's devices; tap: only the signed-in console. */
|
|
217
|
+
export type DecisionTier = "confirm" | "tap"
|
|
218
|
+
/** closed: ended without an answer because what it asked about went away (answered elsewhere, ended). */
|
|
219
|
+
export type DecisionState = "open" | "answered" | "expired" | "closed"
|
|
220
|
+
|
|
221
|
+
/** What an ask to @me opens (design §12.1); only the user answers it. */
|
|
222
|
+
export interface Decision {
|
|
223
|
+
readonly id: string
|
|
224
|
+
/** The ask (to @me) carrying it. */
|
|
225
|
+
readonly messageID: string
|
|
226
|
+
readonly from: Party
|
|
227
|
+
/** Session IDs it concerns. */
|
|
228
|
+
readonly about: readonly string[]
|
|
229
|
+
readonly sessionID?: string
|
|
230
|
+
readonly question: string
|
|
231
|
+
readonly options: readonly DecisionOption[]
|
|
232
|
+
readonly recommendation?: { readonly option: string; readonly why?: string }
|
|
233
|
+
readonly urgency: DecisionUrgency
|
|
234
|
+
readonly tier: DecisionTier
|
|
235
|
+
readonly deadline?: number
|
|
236
|
+
readonly onTimeout?: string
|
|
237
|
+
readonly state: DecisionState
|
|
238
|
+
readonly at: number
|
|
239
|
+
readonly expiresAt: number
|
|
240
|
+
readonly escalatedAt?: number
|
|
241
|
+
readonly answer?: { readonly option?: string; readonly text?: string; readonly answeredBy: "user" | "timeout"; readonly at: number; readonly deviceID?: string }
|
|
242
|
+
/** Why it closed without an answer (state closed). */
|
|
243
|
+
readonly closed?: { readonly at: number; readonly reason: string }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** POST /api/decisions/:id/answer. */
|
|
247
|
+
export interface DecisionAnswered {
|
|
248
|
+
readonly decision: Decision
|
|
249
|
+
/** The user's reply to the asker. */
|
|
250
|
+
readonly reply?: AgentMessage
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** POST /api/agent/reply and /decline: the answer and the message it answered. */
|
|
254
|
+
export interface Replied extends Sent {
|
|
255
|
+
readonly original: AgentMessage
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** GET /api/agent/inbox. */
|
|
259
|
+
export interface Inbox {
|
|
260
|
+
/** Open asks to the caller, newest first. */
|
|
261
|
+
readonly asks: readonly AgentMessage[]
|
|
262
|
+
/** The caller's own open asks. */
|
|
263
|
+
readonly waiting: readonly AgentMessage[]
|
|
264
|
+
/** Recent messages to the caller. */
|
|
265
|
+
readonly messages: readonly AgentMessage[]
|
|
266
|
+
/** Session IDs the caller muted. */
|
|
267
|
+
readonly muted: readonly string[]
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** One server-sent event of GET /api/agent/watch (data is the raw text, JSON for hub events). */
|
|
271
|
+
export interface WatchEvent {
|
|
272
|
+
readonly event: string
|
|
273
|
+
readonly data: string
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Acting (POST /api/agent/sessions/:addr/{approve,deny,answer,prompt,stop}; src/central/agent/acting.ts).
|
|
277
|
+
|
|
278
|
+
/** OpenCode's reply to a permission request. */
|
|
279
|
+
export type PermissionDecision = "once" | "always" | "reject"
|
|
280
|
+
|
|
281
|
+
/** How an agent's prompt enters a busy session (the spoke picks when unset). */
|
|
282
|
+
export type PromptDelivery = "queue" | "steer"
|
|
283
|
+
|
|
284
|
+
/** An answer to a form (question): field key → value. */
|
|
285
|
+
export type FormAnswer = Readonly<Record<string, string | number | boolean | readonly string[]>>
|
|
286
|
+
|
|
287
|
+
/** Why an act verb needs the user (the needs_user envelope's `evaluation`). */
|
|
288
|
+
export interface Evaluation {
|
|
289
|
+
readonly verdict: "outside"
|
|
290
|
+
readonly reason: string
|
|
291
|
+
/** Set by the hub: confirm (any of the user's devices) or tap (the signed-in console). */
|
|
292
|
+
readonly tier: DecisionTier
|
|
293
|
+
/** The usable grants it was evaluated against. */
|
|
294
|
+
readonly grants: readonly string[]
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export interface Approved {
|
|
298
|
+
readonly sessionID: string
|
|
299
|
+
readonly requestID: string
|
|
300
|
+
readonly decision: PermissionDecision
|
|
301
|
+
/** The grant it acted under (agents). */
|
|
302
|
+
readonly grant?: string
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export interface Denied {
|
|
306
|
+
readonly sessionID: string
|
|
307
|
+
readonly requestID: string
|
|
308
|
+
readonly grant?: string
|
|
309
|
+
/** With a why: whether the harness got the reason. */
|
|
310
|
+
readonly reasonSent?: boolean
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export interface FormAnswered {
|
|
314
|
+
readonly sessionID: string
|
|
315
|
+
readonly formID: string
|
|
316
|
+
readonly grant?: string
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export interface Stopped {
|
|
320
|
+
readonly sessionID: string
|
|
321
|
+
readonly grant?: string
|
|
322
|
+
/** The spoke's answer. */
|
|
323
|
+
readonly result: unknown
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** An agent's prompt (delivered under its provenance), or the user's (steered into the tree, or a new turn). */
|
|
327
|
+
export type Prompted =
|
|
328
|
+
| { readonly sessionID: string; readonly messageID: string; readonly delivered: string; readonly inboxID?: string; readonly grant: string }
|
|
329
|
+
| { readonly sessionID: string; readonly mode: "steer"; readonly inboxID: string }
|
|
330
|
+
| { readonly sessionID: string; readonly mode: "turn" }
|
|
331
|
+
|
|
332
|
+
// Grants (/api/agent/grants; src/central/agent/grants.ts): created, edited and revoked only by the user.
|
|
333
|
+
|
|
334
|
+
export type GrantVerb = "approve" | "deny" | "answer" | "prompt" | "stop" | "roles" | "launch"
|
|
335
|
+
export type GrantState = "active" | "expired" | "revoked"
|
|
336
|
+
|
|
337
|
+
/** What the user grants (create and update bodies). */
|
|
338
|
+
export interface GrantSpec {
|
|
339
|
+
/** A session (ses_… or @name, pinned at creation), a reserved role (@@orchestrator…), or app:<token id>. */
|
|
340
|
+
readonly holder: string
|
|
341
|
+
readonly scope: { readonly projects: readonly string[] }
|
|
342
|
+
readonly allow: readonly GrantVerb[]
|
|
343
|
+
readonly permissions?: { readonly allow?: readonly string[]; readonly never?: readonly string[] }
|
|
344
|
+
readonly allowAlways?: boolean
|
|
345
|
+
/** Seconds, or 30m, 8h, 2d (1 minute to 30 days). */
|
|
346
|
+
readonly expires?: number | string
|
|
347
|
+
readonly maxActionsPerHour?: number
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export interface Grant {
|
|
351
|
+
readonly id: string
|
|
352
|
+
readonly holder: string
|
|
353
|
+
readonly holderSessionID?: string
|
|
354
|
+
readonly scope: { readonly projects: readonly string[] }
|
|
355
|
+
readonly allow: readonly GrantVerb[]
|
|
356
|
+
readonly permissions: { readonly allow: readonly string[]; readonly never: readonly string[] }
|
|
357
|
+
readonly allowAlways: boolean
|
|
358
|
+
readonly maxActionsPerHour: number
|
|
359
|
+
readonly created: number
|
|
360
|
+
readonly expiresAt: number
|
|
361
|
+
readonly updated?: number
|
|
362
|
+
readonly revokedAt?: number
|
|
363
|
+
readonly state: GrantState
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** POST /api/agent/grants/requests: what an agent asks the user for (the holder defaults to the requester). */
|
|
367
|
+
export type GrantRequestSpec = Omit<GrantSpec, "holder"> & {
|
|
368
|
+
/** Only the requester itself (ses_…, its @name, app:<own token id>) or a reserved @@role the session holds. */
|
|
369
|
+
readonly holder?: string
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** POST /api/agent/grants/requests: the ask to @me and the tap Decision it opened (approve creates the grant). */
|
|
373
|
+
export interface GrantRequested {
|
|
374
|
+
readonly message: AgentMessage
|
|
375
|
+
readonly decision: Decision
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Launches (design §6.1; packages/protocol/src/launch.ts is the wire source of truth, kept in step by the tests).
|
|
379
|
+
|
|
380
|
+
export type LaunchState = "queued" | "placing" | "provisioning" | "running" | "ended"
|
|
381
|
+
export type LaunchOutcome = "idle" | "needs_user" | "failed" | "timeout" | "stopped" | "lost"
|
|
382
|
+
export type WorkspaceMode = "worktree" | "checkout" | "clone"
|
|
383
|
+
export type RuntimeKind = "host" | "openshell"
|
|
384
|
+
export type PlacementMode = "require" | "prefer"
|
|
385
|
+
export type LaunchEndWhen = "idle" | "asked" | "never"
|
|
386
|
+
export type CleanupPolicy = "keep" | "remove"
|
|
387
|
+
|
|
388
|
+
/** POST /api/agent/launches: what to start, where, and when it ends. Unset fields take the hub's defaults. */
|
|
389
|
+
export interface LaunchSpec {
|
|
390
|
+
/** Idempotency key: the same key and body return the same launch (another body: invalid). */
|
|
391
|
+
readonly key?: string
|
|
392
|
+
/** The session's @name (without the @). */
|
|
393
|
+
readonly name?: string
|
|
394
|
+
readonly roles?: readonly string[]
|
|
395
|
+
/** Default opencode. */
|
|
396
|
+
readonly harness?: string
|
|
397
|
+
readonly agent?: string
|
|
398
|
+
/** "provider/model". */
|
|
399
|
+
readonly model?: string
|
|
400
|
+
readonly workspace: {
|
|
401
|
+
/** Absolute path on the node: the repository (worktree, checkout) or where the clone goes. */
|
|
402
|
+
readonly repo: string
|
|
403
|
+
/** Default worktree (`<repo>/.worktrees/<branch>`; needs branch). */
|
|
404
|
+
readonly mode?: WorkspaceMode
|
|
405
|
+
readonly branch?: string
|
|
406
|
+
readonly base?: string
|
|
407
|
+
/** https or ssh; required for clone. */
|
|
408
|
+
readonly remote?: string
|
|
409
|
+
}
|
|
410
|
+
readonly runtime?: { readonly kind: RuntimeKind; readonly profile?: string }
|
|
411
|
+
readonly placement?: { readonly labels?: Readonly<Record<string, string>>; readonly machine?: string; readonly mode?: PlacementMode }
|
|
412
|
+
readonly prompt?: string
|
|
413
|
+
/** Free-form; never interpreted by the hub (find launches and sessions by them). */
|
|
414
|
+
readonly labels?: Readonly<Record<string, string>>
|
|
415
|
+
readonly end?: { readonly when?: LaunchEndWhen; readonly timeout?: string; readonly cleanup?: CleanupPolicy }
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export interface LaunchStep {
|
|
419
|
+
readonly state: LaunchState
|
|
420
|
+
readonly reason: string
|
|
421
|
+
readonly at: number
|
|
422
|
+
readonly outcome?: LaunchOutcome
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
export interface LaunchArtifacts {
|
|
426
|
+
readonly branch?: string
|
|
427
|
+
readonly base?: string
|
|
428
|
+
readonly head?: string
|
|
429
|
+
readonly commits?: number
|
|
430
|
+
readonly dirty?: boolean
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** A launch as the hub reports it (the spec with its defaults filled in). */
|
|
434
|
+
export interface LaunchView {
|
|
435
|
+
readonly id: string
|
|
436
|
+
readonly state: LaunchState
|
|
437
|
+
readonly reason: string
|
|
438
|
+
readonly spec: LaunchSpec
|
|
439
|
+
/** ms epoch */
|
|
440
|
+
readonly created: number
|
|
441
|
+
/** When it entered its state. */
|
|
442
|
+
readonly updated: number
|
|
443
|
+
/** Who launched it: `user`, `app:<token id>` or a session ID. */
|
|
444
|
+
readonly by?: string
|
|
445
|
+
readonly machineID?: string
|
|
446
|
+
readonly directory?: string
|
|
447
|
+
readonly sessionID?: string
|
|
448
|
+
readonly name?: string
|
|
449
|
+
readonly outcome?: LaunchOutcome
|
|
450
|
+
readonly artifacts?: LaunchArtifacts
|
|
451
|
+
readonly history: readonly LaunchStep[]
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** POST /api/agent/launches: the launch, and whether it is new (false: the same key again). */
|
|
455
|
+
export interface LaunchCreated {
|
|
456
|
+
readonly launch: LaunchView
|
|
457
|
+
readonly created: boolean
|
|
458
|
+
}
|