@huynle/hub 0.6.1 → 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.
Files changed (219) hide show
  1. package/CHANGELOG.md +800 -0
  2. package/Dockerfile +30 -0
  3. package/README.md +870 -66
  4. package/package.json +45 -2
  5. package/packages/claude-code/package.json +12 -0
  6. package/packages/claude-code/src/bridge.ts +79 -0
  7. package/packages/claude-code/src/daemon.ts +339 -0
  8. package/packages/claude-code/src/hook-input.ts +44 -0
  9. package/packages/claude-code/src/ids.ts +27 -0
  10. package/packages/claude-code/src/index.ts +18 -0
  11. package/packages/claude-code/src/launcher.ts +265 -0
  12. package/packages/claude-code/src/main.ts +127 -0
  13. package/packages/claude-code/src/mcp.ts +109 -0
  14. package/packages/claude-code/src/paths.ts +34 -0
  15. package/packages/claude-code/src/plugin.ts +99 -0
  16. package/packages/claude-code/src/references.ts +51 -0
  17. package/packages/claude-code/src/sessions.ts +600 -0
  18. package/packages/claude-code/src/tools.ts +66 -0
  19. package/packages/claude-code/src/wire.ts +71 -0
  20. package/packages/claude-code/src/wrapper.ts +155 -0
  21. package/packages/cli/dist/hub-cli.js +10162 -0
  22. package/packages/codex/package.json +12 -0
  23. package/packages/codex/src/app-server.ts +44 -0
  24. package/packages/codex/src/ids.ts +28 -0
  25. package/packages/codex/src/index.ts +11 -0
  26. package/packages/codex/src/jsonrpc.ts +168 -0
  27. package/packages/codex/src/main.ts +122 -0
  28. package/packages/codex/src/paths.ts +65 -0
  29. package/packages/codex/src/sessions.ts +578 -0
  30. package/packages/codex/src/spoke.ts +249 -0
  31. package/packages/codex/src/stdio.ts +101 -0
  32. package/packages/codex/src/websocket.ts +206 -0
  33. package/packages/codex/src/wrapper.ts +100 -0
  34. package/packages/node/package.json +12 -0
  35. package/packages/node/src/config.ts +184 -0
  36. package/packages/node/src/connection.ts +283 -0
  37. package/packages/node/src/daemon.ts +281 -0
  38. package/packages/node/src/git.ts +107 -0
  39. package/packages/node/src/harnesses.ts +60 -0
  40. package/packages/node/src/host.ts +319 -0
  41. package/packages/node/src/index.ts +17 -0
  42. package/packages/node/src/main.ts +11 -0
  43. package/packages/node/src/openshell.ts +73 -0
  44. package/packages/node/src/run.ts +106 -0
  45. package/packages/preset/package.json +12 -0
  46. package/packages/preset/presets/aider.json +8 -0
  47. package/packages/preset/presets/goose.json +8 -0
  48. package/packages/preset/src/index.ts +2 -0
  49. package/packages/preset/src/main.ts +195 -0
  50. package/packages/preset/src/presets.ts +87 -0
  51. package/packages/preset/src/spoke.ts +251 -0
  52. package/packages/preset/src/tmux.ts +160 -0
  53. package/packages/protocol/package.json +12 -0
  54. package/packages/protocol/src/harness-events.ts +207 -0
  55. package/packages/protocol/src/index.ts +1215 -0
  56. package/packages/protocol/src/launch.ts +172 -0
  57. package/packages/protocol/src/node.ts +409 -0
  58. package/packages/protocol/src/preset.ts +144 -0
  59. package/packages/protocol/src/webhook.ts +62 -0
  60. package/packages/protocol/src/wire.ts +54 -0
  61. package/packages/runtime-openshell/image/Dockerfile +36 -0
  62. package/packages/runtime-openshell/image/README.md +78 -0
  63. package/packages/runtime-openshell/image/build.ts +203 -0
  64. package/packages/runtime-openshell/image/entry.sh +163 -0
  65. package/packages/runtime-openshell/image/image.ts +119 -0
  66. package/packages/runtime-openshell/image/start.sh +24 -0
  67. package/packages/runtime-openshell/package.json +12 -0
  68. package/packages/runtime-openshell/src/cli.ts +584 -0
  69. package/packages/runtime-openshell/src/index.ts +6 -0
  70. package/packages/runtime-openshell/src/policy.ts +155 -0
  71. package/packages/runtime-openshell/src/profiles.ts +181 -0
  72. package/packages/runtime-openshell/src/runtime.ts +475 -0
  73. package/packages/runtime-sdk/package.json +12 -0
  74. package/packages/runtime-sdk/src/index.ts +87 -0
  75. package/packages/sdk/package.json +12 -0
  76. package/packages/sdk/src/address.ts +25 -0
  77. package/packages/sdk/src/client.ts +502 -0
  78. package/packages/sdk/src/envelope.ts +108 -0
  79. package/packages/sdk/src/errors.ts +106 -0
  80. package/packages/sdk/src/index.ts +10 -0
  81. package/packages/sdk/src/sse.ts +67 -0
  82. package/packages/sdk/src/types.ts +458 -0
  83. package/packages/sdk/src/webhook.ts +102 -0
  84. package/packages/spoke-sdk/package.json +12 -0
  85. package/packages/spoke-sdk/src/activity.ts +3 -0
  86. package/packages/spoke-sdk/src/connection.ts +295 -0
  87. package/packages/spoke-sdk/src/delivery.ts +119 -0
  88. package/packages/spoke-sdk/src/enroll.ts +79 -0
  89. package/packages/spoke-sdk/src/harness-hello.ts +79 -0
  90. package/packages/spoke-sdk/src/harness-spoke.ts +263 -0
  91. package/packages/spoke-sdk/src/index.ts +12 -0
  92. package/packages/spoke-sdk/src/prompt-refs.ts +154 -0
  93. package/packages/spoke-sdk/src/prompt-source.ts +8 -0
  94. package/packages/spoke-sdk/src/session-ids.ts +49 -0
  95. package/packages/spoke-sdk/src/session-tokens.ts +78 -0
  96. package/scripts/hub-claude-code.service +34 -0
  97. package/scripts/hub-codex-spoke.service +34 -0
  98. package/scripts/hub-node.service +35 -0
  99. package/scripts/io.github.huynle.hub-claude-code.plist +53 -0
  100. package/scripts/io.github.huynle.hub-codex-spoke.plist +53 -0
  101. package/scripts/io.github.huynle.hub-node.plist +52 -0
  102. package/src/central/actions.ts +233 -0
  103. package/src/central/agent/acting-routes.ts +133 -0
  104. package/src/central/agent/acting-switch.ts +93 -0
  105. package/src/central/agent/acting.ts +599 -0
  106. package/src/central/agent/actor.ts +193 -0
  107. package/src/central/agent/app-tokens.ts +142 -0
  108. package/src/central/agent/audit.ts +185 -0
  109. package/src/central/agent/decisions.ts +530 -0
  110. package/src/central/agent/delivery.ts +678 -0
  111. package/src/central/agent/directory-routes.ts +185 -0
  112. package/src/central/agent/directory.ts +473 -0
  113. package/src/central/agent/envelope.ts +51 -0
  114. package/src/central/agent/errors.ts +72 -0
  115. package/src/central/agent/escalation.ts +219 -0
  116. package/src/central/agent/grant-requests.ts +221 -0
  117. package/src/central/agent/grant-routes.ts +70 -0
  118. package/src/central/agent/grants.ts +235 -0
  119. package/src/central/agent/launch-engine.ts +1025 -0
  120. package/src/central/agent/launch-routes.ts +252 -0
  121. package/src/central/agent/launch-spec.ts +289 -0
  122. package/src/central/agent/launch-store.ts +264 -0
  123. package/src/central/agent/launch-tokens.ts +206 -0
  124. package/src/central/agent/message-limits.ts +123 -0
  125. package/src/central/agent/message-routes.ts +385 -0
  126. package/src/central/agent/messages.ts +442 -0
  127. package/src/central/agent/names.ts +86 -0
  128. package/src/central/agent/placement.ts +419 -0
  129. package/src/central/agent/policy.ts +378 -0
  130. package/src/central/agent/read-routes.ts +389 -0
  131. package/src/central/agent/routes.ts +278 -0
  132. package/src/central/agent/session-tokens.ts +227 -0
  133. package/src/central/agent/versioned.ts +26 -0
  134. package/src/central/agent/watch.ts +221 -0
  135. package/src/central/agent/webhook-routes.ts +78 -0
  136. package/src/central/agent/webhooks.ts +449 -0
  137. package/src/central/agent-data.ts +177 -0
  138. package/src/central/auth.ts +224 -121
  139. package/src/central/backend/file-state.ts +240 -0
  140. package/src/central/backend/index.ts +113 -0
  141. package/src/central/backend/memory-bus.ts +93 -0
  142. package/src/central/backend/memory-catalog.ts +90 -0
  143. package/src/central/backend/memory-counters.ts +102 -0
  144. package/src/central/backend/memory-events.ts +45 -0
  145. package/src/central/backend/memory-kv.ts +38 -0
  146. package/src/central/backend/memory-state.ts +77 -0
  147. package/src/central/backend/postgres-migrate.ts +100 -0
  148. package/src/central/backend/postgres-state.ts +197 -0
  149. package/src/central/backend/types.ts +244 -0
  150. package/src/central/backend/valkey-bus.ts +425 -0
  151. package/src/central/backend/valkey-catalog.ts +190 -0
  152. package/src/central/backend/valkey-client.ts +36 -0
  153. package/src/central/backend/valkey-counters.ts +38 -0
  154. package/src/central/backend/valkey-events.ts +168 -0
  155. package/src/central/backend/valkey-kv.ts +54 -0
  156. package/src/central/backend/valkey-locks.ts +92 -0
  157. package/src/central/cluster/directory.ts +141 -0
  158. package/src/central/cluster/feed.ts +84 -0
  159. package/src/central/cluster/leader.ts +232 -0
  160. package/src/central/cluster/live.ts +151 -0
  161. package/src/central/cluster/recommend.ts +59 -0
  162. package/src/central/cluster/rpc.ts +150 -0
  163. package/src/central/cluster/view.ts +232 -0
  164. package/src/central/config.ts +197 -3
  165. package/src/central/done.ts +96 -0
  166. package/src/central/events-log.ts +100 -0
  167. package/src/central/http.ts +1067 -171
  168. package/src/central/import.ts +318 -0
  169. package/src/central/main.ts +211 -12
  170. package/src/central/nodes.ts +314 -0
  171. package/src/central/oidc.ts +34 -14
  172. package/src/central/push-target.ts +118 -18
  173. package/src/central/push.ts +256 -82
  174. package/src/central/registry.ts +902 -92
  175. package/src/central/say-stream.ts +128 -0
  176. package/src/central/session-token-push.ts +233 -0
  177. package/src/central/speech/openai.ts +21 -14
  178. package/src/central/speech/openrouter.ts +27 -10
  179. package/src/central/speech/provider.ts +54 -0
  180. package/src/central/speech/service.ts +22 -7
  181. package/src/central/spoke-ws.ts +522 -44
  182. package/src/central/store-shared.ts +518 -0
  183. package/src/central/store.ts +572 -189
  184. package/src/central/token-keys.ts +165 -0
  185. package/src/config.ts +49 -1
  186. package/src/index.ts +165 -12
  187. package/src/protocol.ts +3 -650
  188. package/src/rpc.ts +27 -2
  189. package/src/spoke/client.ts +3 -272
  190. package/src/spoke/conversation.ts +16 -12
  191. package/src/spoke/deliver.ts +75 -0
  192. package/src/spoke/enroll.ts +2 -79
  193. package/src/spoke/env-inject.ts +160 -0
  194. package/src/spoke/hub-rpc.ts +115 -0
  195. package/src/spoke/hub-tool.ts +409 -0
  196. package/src/spoke/launch-source.ts +22 -0
  197. package/src/spoke/launchers.ts +68 -0
  198. package/src/spoke/locations.ts +3 -2
  199. package/src/spoke/node-roots.ts +61 -0
  200. package/src/spoke/prompt-refs.ts +116 -0
  201. package/src/spoke/session-tokens.ts +2 -0
  202. package/src/spoke/spoke.ts +465 -28
  203. package/src/spoke/taskboard.ts +243 -0
  204. package/src/spoke/timeline.ts +184 -6
  205. package/src/spoke/tracker.ts +79 -4
  206. package/src/status.ts +203 -10
  207. package/src/tui-commands.ts +13 -6
  208. package/src/tui-messaging.ts +93 -0
  209. package/src/tui.ts +15 -0
  210. package/web/dist/assets/highlight-y4rJE6Tt.js +5 -0
  211. package/web/dist/assets/index-D9zMbxSg.css +1 -0
  212. package/web/dist/assets/index-db1z4duA.js +20 -0
  213. package/web/dist/assets/ort-wasm-simd-threaded-CxTQ5xH-.wasm +0 -0
  214. package/web/dist/assets/silero-worker-C8-8vLVS.js +11 -0
  215. package/web/dist/assets/silero_vad-Dt9-bTDn.onnx +0 -0
  216. package/web/dist/index.html +9 -2
  217. package/web/dist/sw.js +79 -15
  218. package/web/dist/assets/index-DClBvfLL.css +0 -1
  219. 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
+ }