@huynle/hub 0.8.7 → 0.9.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 (213) hide show
  1. package/CHANGELOG.md +152 -0
  2. package/README.md +75 -16
  3. package/package.json +5 -1
  4. package/packages/claude-code/package.json +1 -1
  5. package/packages/claude-code/src/wrapper.ts +2 -2
  6. package/packages/cli/dist/hub-cli.js +1413 -523
  7. package/packages/cli/package.json +1 -1
  8. package/packages/cli/src/bootstrap/PHASE3-CONTRACT.md +112 -0
  9. package/packages/cli/src/bootstrap/adapters/node-services/control.ts +164 -21
  10. package/packages/cli/src/bootstrap/adapters/node-services/plan.ts +24 -7
  11. package/packages/cli/src/bootstrap/adapters/node-services/render.ts +116 -9
  12. package/packages/cli/src/bootstrap/adapters/node-services/validate.ts +8 -2
  13. package/packages/cli/src/bootstrap/contracts/actions.ts +8 -6
  14. package/packages/cli/src/bootstrap/contracts/discovery.ts +4 -3
  15. package/packages/cli/src/bootstrap/contracts/exec.ts +4 -2
  16. package/packages/cli/src/bootstrap/contracts/paths.ts +2 -1
  17. package/packages/cli/src/bootstrap/contracts/platform.ts +5 -5
  18. package/packages/cli/src/bootstrap/discovery/discover.ts +7 -2
  19. package/packages/cli/src/bootstrap/discovery/host.ts +2 -0
  20. package/packages/cli/src/bootstrap/preview/plan.ts +5 -1
  21. package/packages/cli/src/bootstrap/profiles/desired.ts +16 -1
  22. package/packages/cli/src/bootstrap/profiles/history.ts +2 -1
  23. package/packages/cli/src/bootstrap/profiles/upgrade.ts +2 -2
  24. package/packages/cli/src/bootstrap/release/assets.ts +4 -1
  25. package/packages/cli/src/cli.ts +23 -10
  26. package/packages/cli/src/http.ts +1 -0
  27. package/packages/cli/src/launches.ts +5 -1
  28. package/packages/cli/src/mcp.ts +28 -8
  29. package/packages/cli/src/messaging.ts +14 -0
  30. package/packages/cli/src/node.ts +94 -12
  31. package/packages/cli/src/setup/commands.ts +27 -7
  32. package/packages/cli/src/setup/runtime.ts +31 -4
  33. package/packages/cli/src/team-intake-mcp.ts +26 -2
  34. package/packages/codex/package.json +1 -1
  35. package/packages/codex/src/wrapper.ts +2 -2
  36. package/packages/node/package.json +1 -1
  37. package/packages/node/src/config.ts +49 -17
  38. package/packages/node/src/git.ts +16 -2
  39. package/packages/node/src/harnesses.ts +54 -9
  40. package/packages/node/src/host.ts +61 -15
  41. package/packages/node/src/opencode-service.ts +111 -0
  42. package/packages/node/src/opencode-warm.ts +11 -5
  43. package/packages/node/src/run.ts +50 -5
  44. package/packages/preset/package.json +1 -1
  45. package/packages/protocol/package.json +1 -1
  46. package/packages/protocol/schema/app-delivery/v1.schema.json +677 -0
  47. package/packages/protocol/src/artifacts.ts +43 -0
  48. package/packages/protocol/src/credentials.ts +38 -0
  49. package/packages/protocol/src/index.ts +59 -6
  50. package/packages/protocol/src/launch.ts +46 -6
  51. package/packages/protocol/src/markdown.ts +753 -0
  52. package/packages/protocol/src/media-allowlist.ts +240 -0
  53. package/packages/protocol/src/media.ts +120 -0
  54. package/packages/protocol/src/preview-bundle.ts +221 -0
  55. package/packages/protocol/src/voice-recording.ts +8 -0
  56. package/packages/runtime-openshell/package.json +1 -1
  57. package/packages/runtime-sdk/package.json +1 -1
  58. package/packages/sdk/package.json +1 -1
  59. package/packages/sdk/src/client.ts +17 -3
  60. package/packages/sdk/src/errors.ts +1 -0
  61. package/packages/sdk/src/team-intake.ts +8 -0
  62. package/packages/sdk/src/types.ts +16 -2
  63. package/packages/spoke-sdk/package.json +1 -1
  64. package/src/central/agent/delivery.ts +35 -6
  65. package/src/central/agent/errors.ts +2 -0
  66. package/src/central/agent/events.ts +11 -2
  67. package/src/central/agent/launch-engine.ts +65 -4
  68. package/src/central/agent/launch-routes.ts +8 -0
  69. package/src/central/agent/launch-spec.ts +22 -3
  70. package/src/central/agent/launch-store.ts +2 -2
  71. package/src/central/agent/message-routes.ts +49 -11
  72. package/src/central/agent/messages.ts +17 -4
  73. package/src/central/agent/not-after.ts +28 -0
  74. package/src/central/agent/placement.ts +36 -4
  75. package/src/central/agent/routes.ts +3 -1
  76. package/src/central/agent/webhook-routes.ts +21 -9
  77. package/src/central/agent/webhooks.ts +27 -9
  78. package/src/central/artifacts/host.ts +3 -1
  79. package/src/central/artifacts/maintenance.ts +2 -1
  80. package/src/central/artifacts/preview-bundle.ts +81 -0
  81. package/src/central/artifacts/previews.ts +94 -0
  82. package/src/central/artifacts/routes.ts +12 -1
  83. package/src/central/artifacts/store.ts +198 -8
  84. package/src/central/config.ts +34 -0
  85. package/src/central/fetch-budget.ts +59 -0
  86. package/src/central/fetcher.ts +372 -0
  87. package/src/central/http.ts +277 -22
  88. package/src/central/install/CONTRACT.md +4 -4
  89. package/src/central/install/install.ps1 +37 -5
  90. package/src/central/install/render.ts +13 -8
  91. package/src/central/media-remote.ts +248 -0
  92. package/src/central/media-timeline.ts +317 -0
  93. package/src/central/media-unfurl.ts +119 -0
  94. package/src/central/profiles/content.ts +3 -35
  95. package/src/central/recording/routes.ts +2 -2
  96. package/src/central/recording/schema.ts +3 -1
  97. package/src/central/registry.ts +449 -21
  98. package/src/central/spoke-ws.ts +17 -3
  99. package/src/central/store.ts +21 -0
  100. package/src/central/teams/intake-routes.ts +2 -1
  101. package/src/central/teams/intake.ts +200 -15
  102. package/src/central/unfurl-parse.ts +313 -0
  103. package/src/index.ts +68 -8
  104. package/src/speakable.ts +33 -1
  105. package/src/spoke/conversation.ts +7 -4
  106. package/src/spoke/deliver.ts +19 -0
  107. package/src/spoke/media-bytes.ts +171 -0
  108. package/src/spoke/media-policy.ts +394 -0
  109. package/src/spoke/media.ts +189 -0
  110. package/src/spoke/own-server.ts +30 -0
  111. package/src/spoke/preview-bundle.ts +538 -0
  112. package/src/spoke/spoke.ts +387 -17
  113. package/src/spoke/timeline.ts +59 -5
  114. package/src/spoke/voice-front.ts +335 -0
  115. package/web/dist/assets/24BXRY42-Zfr7jm_p.js +2 -0
  116. package/web/dist/assets/4FHPYW2L-C6Z67098.js +2 -0
  117. package/web/dist/assets/4LWW3ZNH-CUiYeygH.js +1 -0
  118. package/web/dist/assets/5THPTBWS-Ce_hS451.js +1 -0
  119. package/web/dist/assets/5XYWFMKW-CW-BCKiK.js +4 -0
  120. package/web/dist/assets/64PMEAB3-BjpCDGU9.js +8 -0
  121. package/web/dist/assets/6BEWHPQE-BpNa96Ze.js +2 -0
  122. package/web/dist/assets/6PIGVMFT-YXn9Hgnb.js +1 -0
  123. package/web/dist/assets/75JW6PN6-BFpDBuGN.js +2 -0
  124. package/web/dist/assets/7WLXJQOR-D6QVYUu9.js +1 -0
  125. package/web/dist/assets/7XWI72E6-BMuIspEy.js +2 -0
  126. package/web/dist/assets/A4UD2L5Z-BQzcD9dU.js +8 -0
  127. package/web/dist/assets/ArtifactPage-BKISN-Qj.js +1 -0
  128. package/web/dist/assets/ArtifactPane-CKOiAUNH.js +1 -0
  129. package/web/dist/assets/ArtifactWorkbench-d7R1o-rs.js +6 -0
  130. package/web/dist/assets/BGW36YJW-CJMhya8S.js +4 -0
  131. package/web/dist/assets/CGIZI67J-DYDSLME8.js +2 -0
  132. package/web/dist/assets/DKTLEL5E-DciMwFJ1.js +1 -0
  133. package/web/dist/assets/DNNCRC2E-BIeKf5t-.js +5 -0
  134. package/web/dist/assets/Diagram-azw2B_QH.js +1 -0
  135. package/web/dist/assets/Diagram-vdpmp3xu.css +1 -0
  136. package/web/dist/assets/E67EBMDD--AucIw59.js +5 -0
  137. package/web/dist/assets/EFYWJBJK-CVywe_D5.js +1 -0
  138. package/web/dist/assets/EXE5EH6O-DR1IHCTr.js +4 -0
  139. package/web/dist/assets/FTXIJQGB-BZVuiBn1.js +1 -0
  140. package/web/dist/assets/G3MDBVQH-BZmnMkFw.js +1 -0
  141. package/web/dist/assets/G6GPDDNC-DEz8F449.js +2 -0
  142. package/web/dist/assets/G76NGGFM-CrB2V34K.js +1 -0
  143. package/web/dist/assets/GH6G3TJC-D3RTYEH4.js +2 -0
  144. package/web/dist/assets/HPNUA7RO-B19NeNOJ.js +1 -0
  145. package/web/dist/assets/HQRGC5D7-Dmpzq6Rn.js +4 -0
  146. package/web/dist/assets/IX6Z2TVR-DphRHnek.js +5 -0
  147. package/web/dist/assets/J77NQRCJ-B7AGcxvw.js +3 -0
  148. package/web/dist/assets/JAD36DTZ-B9-dbsS0.js +5 -0
  149. package/web/dist/assets/LF3NJQAX-koL91UZ9.js +2 -0
  150. package/web/dist/assets/LRZCOFJZ-Cdjasfuf.js +6 -0
  151. package/web/dist/assets/Library-CcjZXsfF.js +2 -0
  152. package/web/dist/assets/Lightbox-C8KNKwOA.css +1 -0
  153. package/web/dist/assets/Lightbox-Du56MXGX.js +1 -0
  154. package/web/dist/assets/MediaImage-Ds_JUx3a.js +4 -0
  155. package/web/dist/assets/N5NF2FGY-OuapYTfr.js +2 -0
  156. package/web/dist/assets/N7OWWQHY-BNLd-Q_U.js +2 -0
  157. package/web/dist/assets/NT5JY5WP-1lJM-aun.js +1 -0
  158. package/web/dist/assets/OT7W3RNY-jDL3drzX.js +4 -0
  159. package/web/dist/assets/P67P66DB-CE-opRb8.js +3 -0
  160. package/web/dist/assets/PFLLLM7K-DDJFgzW3.js +2 -0
  161. package/web/dist/assets/PUAMC4MJ-CtBm-WuU.js +12 -0
  162. package/web/dist/assets/PVFCHRLY-CG5JXv60.js +2 -0
  163. package/web/dist/assets/ProfilesDestination-DdzbzmeK.js +1 -0
  164. package/web/dist/assets/QQW2P742-BbcEkpa4.js +1 -0
  165. package/web/dist/assets/RVARI57O-CgA1WoRv.js +1 -0
  166. package/web/dist/assets/S63OEDPG-CKlhpvl2.js +3 -0
  167. package/web/dist/assets/SG5TVYLW-BBtiwXPL.js +2 -0
  168. package/web/dist/assets/TeamsPage-CDyjrK-I.js +1 -0
  169. package/web/dist/assets/UGRSUGA2-urZ_hx83.js +5 -0
  170. package/web/dist/assets/V44WA6U7-DYfX3OlM.js +1 -0
  171. package/web/dist/assets/V67O4WWV-xYxJ00QW.js +1 -0
  172. package/web/dist/assets/VBUCM3YD-JHGjuJC_.js +1 -0
  173. package/web/dist/assets/VINGRCPA-DZuJa3SH.js +7 -0
  174. package/web/dist/assets/WL6DNKX4-BHqripdA.js +2 -0
  175. package/web/dist/assets/WQ63CJYP-D6G5frDY.js +2 -0
  176. package/web/dist/assets/Watchtower-DYwoUWME.js +2 -0
  177. package/web/dist/assets/XOYAOB3S-L-KeA1EF.js +13 -0
  178. package/web/dist/assets/Y5LQLG3M-dr2-erb8.js +2 -0
  179. package/web/dist/assets/YIK6JTN6--1t-Ud8k.js +2 -0
  180. package/web/dist/assets/artifacts-DU9uRgs1.js +1 -0
  181. package/web/dist/assets/index-C3ci5UtY.css +1 -0
  182. package/web/dist/assets/index-LNMZCqgE.js +36 -0
  183. package/web/dist/assets/lazy-f3CuUNJZ.js +43 -0
  184. package/web/dist/assets/live-ycZ_aQZO.js +5 -0
  185. package/web/dist/assets/mermaid.worker-WDVRAh32.js +1 -0
  186. package/web/dist/assets/pane-layout-0RrLHGDr.js +1 -0
  187. package/web/dist/assets/preload-helper-BLdHbfdN.js +1 -0
  188. package/web/dist/assets/{renderer2d-CYag1CnR.js → renderer2d-BIwbgRoX.js} +1 -1
  189. package/web/dist/assets/{renderer3d-BRB6rgDJ.js → renderer3d-C1a7eYGQ.js} +1 -1
  190. package/web/dist/assets/{safe-area-fOy0AOuG.js → safe-area-DHhPy61Q.js} +1 -1
  191. package/web/dist/assets/{silero-worker-DhlIHyD_.js → silero-worker-e802lB5T.js} +1 -1
  192. package/web/dist/assets/smart-turn-v3.2-cpu-WpeLRbPA.onnx +0 -0
  193. package/web/dist/assets/smart-turn-worker-DaVJPdvJ.js +11 -0
  194. package/web/dist/assets/source-BWlvaODa.js +1 -0
  195. package/web/dist/assets/toast-Cuu2K0hx.js +1 -0
  196. package/web/dist/assets/ui-CFB4wQfF.js +3 -0
  197. package/web/dist/index.html +8 -6
  198. package/web/src/voice/turn/LICENSE +28 -0
  199. package/web/dist/assets/ArtifactPage-Dq7VW-bU.js +0 -1
  200. package/web/dist/assets/ArtifactPane-DKwbMTX9.js +0 -1
  201. package/web/dist/assets/ArtifactWorkbench-vrgIfncX.js +0 -6
  202. package/web/dist/assets/Library-D6-Go6Cu.js +0 -2
  203. package/web/dist/assets/ProfilesDestination-BOML2Dgf.js +0 -1
  204. package/web/dist/assets/TeamsPage-CNBTE8gB.js +0 -1
  205. package/web/dist/assets/Watchtower-Dor1Ubjy.js +0 -2
  206. package/web/dist/assets/hub-CrbItiwP.js +0 -13
  207. package/web/dist/assets/index-82Ce0IAf.css +0 -1
  208. package/web/dist/assets/index-D9SP3_0z.js +0 -22
  209. package/web/dist/assets/live-BNJgy0vg.js +0 -5
  210. package/web/dist/assets/pane-layout-EdR1kRrG.js +0 -1
  211. package/web/dist/assets/preload-helper-DqrJTq0t.js +0 -1
  212. package/web/dist/assets/source-Wbq35qGp.js +0 -1
  213. package/web/dist/assets/ui-j5FFk90B.js +0 -3
@@ -0,0 +1,677 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/huynle/hub/packages/protocol/schema/app-delivery/v1.schema.json",
4
+ "title": "Hub app delivery surface",
5
+ "description": "The app-facing delivery surface of a hub (msg/ask, launches, team intake, webhooks, error bodies), frozen by docs/plans/2026-11-06-app-delivery-contracts.md. Every definition lives in $defs; validate a body against `<$id>#/$defs/<Name>`. Fields marked `(H1)`-`(H6)` in a description were added by those tasks (H1-H5 shipped; H6 not started); they are optional, so bodies of older hubs validate. REST views are open (clients ignore unknown fields); event payloads (`*EventData`, `*Subject`) are closed allowlists: a new field there needs a schema change in the same commit. `schemaVersion` is an annotation keyword (Ajv strict mode: ajv.addVocabulary(['schemaVersion'])).",
6
+ "schemaVersion": 1,
7
+ "$defs": {
8
+ "SchemaVersion": { "description": "The version of this schema document; additive changes keep 1.", "const": 1 },
9
+
10
+ "EpochMs": { "description": "Milliseconds since the Unix epoch.", "type": "integer", "minimum": 0 },
11
+ "NotAfter": {
12
+ "description": "A hard deadline (msg/ask H2, team intake H4 and launches H3, all through parseNotAfter in src/central/agent/not-after.ts), epoch milliseconds (REST and SDK; the CLI and MCP also accept an ISO 8601 date-time and convert it). Must be in the future and at most 30 days (NotAfterLimitMs) out: a value already passed is 422 `expired` (nothing stored or delivered); one further out, or not an integer, is 400 `invalid`. It takes part in idempotency body comparison like any other field.",
13
+ "type": "integer",
14
+ "minimum": 1
15
+ },
16
+ "NotAfterLimitMs": { "description": "The furthest notAfter accepted, relative to the hub's clock: 30 days (NOT_AFTER_MAX_MS).", "const": 2592000000 },
17
+ "ExpiredReason": {
18
+ "description": "Why a delivery expired (ExpiredReason in src/central/agent/not-after.ts): `not_after` (its notAfter passed), `pending_timeout` (the 24 h hold for an offline or busy session, PENDING_MS in src/central/agent/delivery.ts). On a message as `expiredReason`, on message.changed as `reason` (an enum, never text).",
19
+ "enum": ["not_after", "pending_timeout"]
20
+ },
21
+
22
+ "IdempotencyKey": {
23
+ "description": "The `Idempotency-Key` request header (IDEMPOTENCY_KEY in packages/protocol/src/contracts.ts). Same key + same body within IdempotencyTtlMs replays the first answer with 200 and header `idempotent-replayed: true`; another body under a live key is 409 `conflict`. On team intake, workspace/profile creates and (H1, shipped) POST /api/agent/msg and /ask, keyed per sender.",
24
+ "type": "string",
25
+ "pattern": "^[A-Za-z0-9_.:-]{1,200}$"
26
+ },
27
+ "IdempotencyTtlMs": { "description": "How long a used Idempotency-Key replays (IDEMPOTENCY_TTL_MS: 24 h).", "const": 86400000 },
28
+ "ReplayedHeader": { "description": "Value of the response header `idempotent-replayed` on an idempotent replay.", "const": "true" },
29
+
30
+ "ErrorCode": {
31
+ "description": "The /api/agent error codes (AGENT_ERROR_STATUS in src/central/agent/errors.ts; `expired` shipped with H2).",
32
+ "enum": ["invalid", "invalid_cursor", "unauthorized", "token_expired", "needs_user", "forbidden", "not_found", "ambiguous", "name_taken", "conflict", "gone", "unsupported", "expired", "paused", "rate_limited", "internal", "offline", "busy"]
33
+ },
34
+ "ErrorCodeStatus": {
35
+ "description": "The HTTP status of each error code (AGENT_ERROR_STATUS). `expired` is 422 like `unsupported`.",
36
+ "const": {
37
+ "invalid": 400,
38
+ "invalid_cursor": 400,
39
+ "unauthorized": 401,
40
+ "token_expired": 401,
41
+ "needs_user": 403,
42
+ "forbidden": 403,
43
+ "not_found": 404,
44
+ "ambiguous": 409,
45
+ "name_taken": 409,
46
+ "conflict": 409,
47
+ "gone": 410,
48
+ "unsupported": 422,
49
+ "expired": 422,
50
+ "paused": 423,
51
+ "rate_limited": 429,
52
+ "internal": 500,
53
+ "offline": 503,
54
+ "busy": 503
55
+ }
56
+ },
57
+ "ErrorBody": {
58
+ "description": "Every /api/agent error: {error: {code, message, hint?, ...extra}} (src/central/agent/errors.ts agentError). `retryAfter` (seconds) is also sent as the Retry-After header. 409 conflict may carry `currentRevision`; 409 ambiguous `candidates`; 422 unsupported `feature`; 422 expired: a notAfter had already passed at submit (nothing stored, delivered or pushed).",
59
+ "type": "object",
60
+ "required": ["error"],
61
+ "properties": {
62
+ "error": {
63
+ "type": "object",
64
+ "required": ["code", "message"],
65
+ "properties": {
66
+ "code": { "$ref": "#/$defs/ErrorCode" },
67
+ "message": { "type": "string" },
68
+ "hint": { "type": "string" },
69
+ "retryAfter": { "type": "number", "minimum": 0 },
70
+ "currentRevision": { "type": "integer", "minimum": 0 },
71
+ "candidates": { "type": "array" },
72
+ "feature": { "type": "string" },
73
+ "notAfter": { "$ref": "#/$defs/EpochMs" }
74
+ }
75
+ }
76
+ }
77
+ },
78
+
79
+ "Party": {
80
+ "description": "A message sender or recipient (Party in packages/sdk/src/types.ts).",
81
+ "oneOf": [
82
+ {
83
+ "type": "object",
84
+ "required": ["kind", "sessionID"],
85
+ "properties": { "kind": { "const": "session" }, "sessionID": { "type": "string" }, "name": { "type": "string" }, "harness": { "type": "string" }, "machine": { "type": "string" } }
86
+ },
87
+ { "type": "object", "required": ["kind"], "properties": { "kind": { "const": "user" } } },
88
+ {
89
+ "type": "object",
90
+ "required": ["kind", "tokenID"],
91
+ "properties": { "kind": { "const": "app" }, "tokenID": { "type": "string" }, "label": { "type": "string" } }
92
+ }
93
+ ]
94
+ },
95
+ "MessageKind": { "enum": ["msg", "ask", "reply", "decline"] },
96
+ "MessageState": { "description": "`sent` for msg/reply/decline; asks are open until answered, declined or expired.", "enum": ["sent", "open", "answered", "declined", "expired"] },
97
+ "MessageDelivery": {
98
+ "description": "How a message reached its recipient session (Delivery in src/central/agent/messages.ts). `expired`: pending past PENDING_MS or (H2) past its notAfter; never delivered (see expiredReason).",
99
+ "enum": ["steer", "turn", "queued", "retained", "pending", "dropped", "expired"]
100
+ },
101
+ "WakeSkip": { "enum": ["wake_false", "subagent", "hop_cap", "budget"] },
102
+ "Message": {
103
+ "type": "object",
104
+ "required": ["id", "kind", "from", "to", "recipient", "text", "state", "at", "hop"],
105
+ "properties": {
106
+ "id": { "type": "string", "pattern": "^msg_[A-Za-z0-9]{22}$" },
107
+ "kind": { "$ref": "#/$defs/MessageKind" },
108
+ "from": { "$ref": "#/$defs/Party" },
109
+ "to": { "type": "string" },
110
+ "recipient": { "$ref": "#/$defs/Party" },
111
+ "generation": { "type": "integer", "minimum": 0 },
112
+ "text": { "type": "string" },
113
+ "replyTo": { "type": "string" },
114
+ "state": { "$ref": "#/$defs/MessageState" },
115
+ "at": { "$ref": "#/$defs/EpochMs" },
116
+ "hop": { "type": "integer", "minimum": 0 },
117
+ "expiresAt": { "$ref": "#/$defs/EpochMs" },
118
+ "notAfter": { "description": "(H2) Not delivered after this; an ask's expiresAt is at most notAfter.", "$ref": "#/$defs/EpochMs" },
119
+ "delivery": { "$ref": "#/$defs/MessageDelivery" },
120
+ "expiredReason": { "description": "(H2) delivery expired only: which deadline passed.", "$ref": "#/$defs/ExpiredReason" },
121
+ "deliveredAt": { "$ref": "#/$defs/EpochMs" },
122
+ "answer": { "type": "object", "required": ["id", "at"], "properties": { "id": { "type": "string" }, "at": { "$ref": "#/$defs/EpochMs" } } }
123
+ }
124
+ },
125
+ "MsgRequest": {
126
+ "description": "POST /api/agent/msg (scope messages:send). Optional header Idempotency-Key (H1, per sender). The sender is the credential; the hub ignores any other field (listed here as not allowed so clients send only these).",
127
+ "type": "object",
128
+ "required": ["to", "text"],
129
+ "additionalProperties": false,
130
+ "properties": {
131
+ "to": { "type": "string", "minLength": 1 },
132
+ "text": { "type": "string", "minLength": 1, "maxLength": 10000 },
133
+ "wake": { "type": "boolean" },
134
+ "urgent": { "type": "boolean" },
135
+ "all": { "type": "boolean" },
136
+ "notAfter": { "$ref": "#/$defs/NotAfter" }
137
+ }
138
+ },
139
+ "AskRequest": {
140
+ "description": "POST /api/agent/ask (scope messages:send). Optional header Idempotency-Key (H1, per sender). expiresIn and wait are seconds (wait at most 60, never with all). The Decision fields (options … about) are for asks to @me only.",
141
+ "type": "object",
142
+ "required": ["to", "text"],
143
+ "additionalProperties": false,
144
+ "properties": {
145
+ "to": { "type": "string", "minLength": 1 },
146
+ "text": { "type": "string", "minLength": 1, "maxLength": 10000 },
147
+ "wake": { "type": "boolean" },
148
+ "urgent": { "type": "boolean" },
149
+ "all": { "type": "boolean" },
150
+ "expiresIn": { "type": "number", "minimum": 0 },
151
+ "wait": { "type": "number", "minimum": 0, "maximum": 60 },
152
+ "notAfter": { "$ref": "#/$defs/NotAfter" },
153
+ "options": {
154
+ "type": "array",
155
+ "items": { "type": "object", "required": ["id", "label"], "properties": { "id": { "type": "string" }, "label": { "type": "string" }, "safe": { "type": "boolean" } } }
156
+ },
157
+ "recommendation": { "type": "object", "required": ["option"], "properties": { "option": { "type": "string" }, "why": { "type": "string" } } },
158
+ "urgency": { "$ref": "#/$defs/DecisionUrgency" },
159
+ "tier": { "$ref": "#/$defs/DecisionTier" },
160
+ "deadline": { "type": "number", "minimum": 0 },
161
+ "onTimeout": { "type": "string" },
162
+ "about": { "type": "array", "items": { "type": "string" } }
163
+ }
164
+ },
165
+ "Sent": {
166
+ "description": "One stored message and how it was delivered (absent: stored only, e.g. to @me).",
167
+ "type": "object",
168
+ "required": ["message"],
169
+ "properties": {
170
+ "message": { "$ref": "#/$defs/Message" },
171
+ "delivery": { "$ref": "#/$defs/MessageDelivery" },
172
+ "woke": { "type": "boolean" },
173
+ "wakeSkipped": { "$ref": "#/$defs/WakeSkip" }
174
+ }
175
+ },
176
+ "SentAll": {
177
+ "description": "`all: true`: one Sent per session the address names. (H1) An idempotent replay returns the same list.",
178
+ "type": "object",
179
+ "required": ["sent"],
180
+ "properties": { "sent": { "type": "array", "items": { "$ref": "#/$defs/Sent" } } }
181
+ },
182
+ "MsgResponse": {
183
+ "description": "POST /api/agent/msg: 201 (200 on an H1 replay).",
184
+ "oneOf": [{ "$ref": "#/$defs/SentAll" }, { "allOf": [{ "$ref": "#/$defs/Sent" }, { "properties": { "sent": false } }] }]
185
+ },
186
+ "Asked": {
187
+ "description": "An ask after any wait: the message, its answer once closed, and (ask @me) the Decision it opened.",
188
+ "allOf": [
189
+ { "$ref": "#/$defs/Sent" },
190
+ { "type": "object", "properties": { "answer": { "$ref": "#/$defs/Message" }, "decision": { "$ref": "#/$defs/Decision" } } }
191
+ ]
192
+ },
193
+ "AskResponse": {
194
+ "description": "POST /api/agent/ask: 201 (200 on an H1 replay: the first answer as it was, not the ask's current state; read GET /api/agent/messages/:id for that).",
195
+ "oneOf": [{ "$ref": "#/$defs/SentAll" }, { "allOf": [{ "$ref": "#/$defs/Asked" }, { "properties": { "sent": false } }] }]
196
+ },
197
+ "DecisionState": { "enum": ["open", "answered", "expired", "closed"] },
198
+ "DecisionUrgency": { "enum": ["low", "normal", "high"] },
199
+ "DecisionTier": { "enum": ["confirm", "tap"] },
200
+ "Decision": {
201
+ "description": "An ask to @me (Decision in src/central/agent/decisions.ts). Answered only by the user on the console.",
202
+ "type": "object",
203
+ "required": ["id", "messageID", "from", "about", "question", "options", "urgency", "tier", "state", "at", "expiresAt"],
204
+ "properties": {
205
+ "id": { "type": "string", "pattern": "^dec_[A-Za-z0-9]{22}$" },
206
+ "messageID": { "type": "string" },
207
+ "from": { "$ref": "#/$defs/Party" },
208
+ "about": { "type": "array", "items": { "type": "string" } },
209
+ "sessionID": { "type": "string" },
210
+ "question": { "type": "string" },
211
+ "options": {
212
+ "type": "array",
213
+ "items": { "type": "object", "required": ["id", "label"], "properties": { "id": { "type": "string" }, "label": { "type": "string" }, "safe": { "const": true } } }
214
+ },
215
+ "recommendation": { "type": "object", "required": ["option"], "properties": { "option": { "type": "string" }, "why": { "type": "string" } } },
216
+ "urgency": { "$ref": "#/$defs/DecisionUrgency" },
217
+ "tier": { "$ref": "#/$defs/DecisionTier" },
218
+ "deadline": { "$ref": "#/$defs/EpochMs" },
219
+ "onTimeout": { "type": "string" },
220
+ "state": { "$ref": "#/$defs/DecisionState" },
221
+ "at": { "$ref": "#/$defs/EpochMs" },
222
+ "expiresAt": { "$ref": "#/$defs/EpochMs" },
223
+ "escalatedAt": { "$ref": "#/$defs/EpochMs" },
224
+ "answer": {
225
+ "type": "object",
226
+ "required": ["answeredBy", "at"],
227
+ "properties": { "option": { "type": "string" }, "text": { "type": "string" }, "answeredBy": { "enum": ["user", "timeout"] }, "at": { "$ref": "#/$defs/EpochMs" } }
228
+ },
229
+ "closed": { "type": "object", "required": ["at", "reason"], "properties": { "at": { "$ref": "#/$defs/EpochMs" }, "reason": { "type": "string" } } }
230
+ }
231
+ },
232
+
233
+ "LaunchState": { "enum": ["queued", "placing", "provisioning", "running", "ended"] },
234
+ "LaunchOutcome": {
235
+ "description": "How an ended launch ended (LAUNCH_OUTCOMES). `expired` (H3): its notAfter passed while it was still queued or placing; it never started.",
236
+ "enum": ["idle", "needs_user", "failed", "timeout", "stopped", "lost", "expired"]
237
+ },
238
+ "LaunchWait": { "enum": ["capacity", "no_match", "active"] },
239
+ "LaunchSpecRequest": {
240
+ "description": "POST /api/agent/launches (scope sessions:launch, plus a launch grant on the repo for apps and sessions). Unknown fields are 400 invalid. Idempotency is the body field `key` (not the header): same key + equivalent body → 200 with the same launch; another body → 400 invalid.",
241
+ "type": "object",
242
+ "required": ["workspace"],
243
+ "additionalProperties": false,
244
+ "properties": {
245
+ "key": { "type": "string", "pattern": "^[!-~]{1,200}$" },
246
+ "name": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]{1,39}$" },
247
+ "roles": { "type": "array", "maxItems": 10, "items": { "type": "string" } },
248
+ "harness": { "type": "string" },
249
+ "agent": { "type": "string" },
250
+ "model": { "type": "string", "maxLength": 200 },
251
+ "workspace": {
252
+ "type": "object",
253
+ "required": ["repo"],
254
+ "additionalProperties": false,
255
+ "properties": {
256
+ "repo": { "type": "string", "pattern": "^/" },
257
+ "mode": { "enum": ["worktree", "checkout", "clone"] },
258
+ "branch": { "type": "string", "maxLength": 200 },
259
+ "base": { "type": "string", "maxLength": 200 },
260
+ "remote": { "type": "string", "maxLength": 500 }
261
+ }
262
+ },
263
+ "runtime": { "type": "object", "additionalProperties": false, "properties": { "kind": { "enum": ["host", "openshell"] }, "profile": { "type": "string" } } },
264
+ "placement": {
265
+ "type": "object",
266
+ "additionalProperties": false,
267
+ "properties": { "labels": { "type": "object", "additionalProperties": { "type": "string" } }, "machine": { "type": "string" }, "mode": { "enum": ["require", "prefer"] } }
268
+ },
269
+ "prompt": { "type": "string", "maxLength": 10000 },
270
+ "intent": { "type": "string" },
271
+ "labels": { "type": "object", "maxProperties": 32, "additionalProperties": { "type": "string", "maxLength": 256 } },
272
+ "end": {
273
+ "type": "object",
274
+ "additionalProperties": false,
275
+ "properties": { "when": { "enum": ["idle", "asked", "never"] }, "timeout": { "type": ["string", "number"] }, "cleanup": { "enum": ["keep", "remove"] } }
276
+ },
277
+ "agentProfile": {
278
+ "type": "object",
279
+ "required": ["profileID", "version"],
280
+ "additionalProperties": false,
281
+ "properties": { "profileID": { "type": "string" }, "version": { "type": "integer", "minimum": 1 } }
282
+ },
283
+ "notAfter": { "description": "(H3) Start-by deadline: applies while queued or placing only. Already passed on a new launch: 422 expired before the grant check; a key replay is answered even after it.", "$ref": "#/$defs/NotAfter" }
284
+ }
285
+ },
286
+ "LaunchSpecView": {
287
+ "description": "The validated spec as the API shows it (defaults filled in; end.timeout as a duration).",
288
+ "type": "object",
289
+ "required": ["roles", "harness", "workspace", "runtime", "placement", "labels", "end"],
290
+ "properties": {
291
+ "key": { "type": "string" },
292
+ "name": { "type": "string" },
293
+ "roles": { "type": "array", "items": { "type": "string" } },
294
+ "harness": { "type": "string" },
295
+ "agent": { "type": "string" },
296
+ "model": { "type": "string" },
297
+ "workspace": {
298
+ "type": "object",
299
+ "required": ["repo", "mode"],
300
+ "properties": { "repo": { "type": "string" }, "mode": { "enum": ["worktree", "checkout", "clone"] }, "branch": { "type": "string" }, "base": { "type": "string" }, "remote": { "type": "string" } }
301
+ },
302
+ "runtime": { "type": "object", "required": ["kind"], "properties": { "kind": { "enum": ["host", "openshell"] }, "profile": { "type": "string" } } },
303
+ "placement": {
304
+ "type": "object",
305
+ "required": ["labels", "mode"],
306
+ "properties": { "labels": { "type": "object", "additionalProperties": { "type": "string" } }, "machine": { "type": "string" }, "mode": { "enum": ["require", "prefer"] } }
307
+ },
308
+ "prompt": { "type": "string" },
309
+ "intent": { "type": "string" },
310
+ "labels": { "type": "object", "additionalProperties": { "type": "string" } },
311
+ "end": {
312
+ "type": "object",
313
+ "required": ["when", "cleanup"],
314
+ "properties": { "when": { "enum": ["idle", "asked", "never"] }, "timeout": { "type": "string" }, "cleanup": { "enum": ["keep", "remove"] } }
315
+ },
316
+ "agentProfile": { "type": "object", "required": ["profileID", "version"], "properties": { "profileID": { "type": "string" }, "version": { "type": "integer" } } },
317
+ "notAfter": { "description": "(H3) As submitted.", "$ref": "#/$defs/EpochMs" }
318
+ }
319
+ },
320
+ "LaunchStep": {
321
+ "type": "object",
322
+ "required": ["state", "reason", "at"],
323
+ "properties": { "state": { "$ref": "#/$defs/LaunchState" }, "reason": { "type": "string" }, "at": { "$ref": "#/$defs/EpochMs" }, "outcome": { "$ref": "#/$defs/LaunchOutcome" } }
324
+ },
325
+ "Launch": {
326
+ "description": "A launch as the API shows it (launchInfo in src/central/agent/launch-routes.ts). An ended launch carries its outcome; no other state does.",
327
+ "type": "object",
328
+ "required": ["id", "state", "reason", "spec", "created", "updated", "history"],
329
+ "properties": {
330
+ "id": { "type": "string", "pattern": "^l_[0-9a-f]{16}$" },
331
+ "state": { "$ref": "#/$defs/LaunchState" },
332
+ "reason": { "type": "string" },
333
+ "spec": { "$ref": "#/$defs/LaunchSpecView" },
334
+ "created": { "$ref": "#/$defs/EpochMs" },
335
+ "updated": { "$ref": "#/$defs/EpochMs" },
336
+ "by": { "type": "string" },
337
+ "byLabel": { "type": "string" },
338
+ "launcherRevoked": { "const": true },
339
+ "waiting": {
340
+ "type": "object",
341
+ "required": ["wait", "reason", "since"],
342
+ "properties": { "wait": { "$ref": "#/$defs/LaunchWait" }, "reason": { "type": "string" }, "since": { "$ref": "#/$defs/EpochMs" } }
343
+ },
344
+ "machineID": { "type": "string" },
345
+ "directory": { "type": "string" },
346
+ "sessionID": { "type": "string" },
347
+ "name": { "type": "string" },
348
+ "outcome": { "$ref": "#/$defs/LaunchOutcome" },
349
+ "artifacts": { "type": "object" },
350
+ "history": { "type": "array", "items": { "$ref": "#/$defs/LaunchStep" } }
351
+ },
352
+ "if": { "properties": { "state": { "const": "ended" } } },
353
+ "then": { "required": ["outcome"], "properties": { "outcome": { "$ref": "#/$defs/LaunchOutcome" } } },
354
+ "else": { "properties": { "outcome": false } }
355
+ },
356
+ "LaunchResponse": {
357
+ "description": "POST /api/agent/launches (201, or 200 for an idempotent repeat), GET /:id, DELETE /:id.",
358
+ "type": "object",
359
+ "required": ["launch"],
360
+ "properties": { "launch": { "$ref": "#/$defs/Launch" } }
361
+ },
362
+ "LaunchListResponse": { "type": "object", "required": ["launches"], "properties": { "launches": { "type": "array", "items": { "$ref": "#/$defs/Launch" } } } },
363
+
364
+ "IntakePriority": { "enum": ["low", "normal", "high", "urgent"] },
365
+ "IntakeStatus": { "description": "Unchanged by H4: an expired queued item is `cancelled` with reason `not_after` (NOT_AFTER_REASON in src/central/teams/intake.ts).", "enum": ["queued", "routed", "done", "cancelled", "failed"] },
366
+ "IntakeProgress": { "enum": ["open", "in_progress", "blocked"] },
367
+ "IntakeStatusInput": { "enum": ["open", "in_progress", "blocked", "done", "failed", "cancelled"] },
368
+ "IntakeExpiryReason": { "description": "(H4) The exact `reason` (item) and `reasonCode` (team.intake event) of an intake the hub cancelled because its notAfter passed while queued (NOT_AFTER_REASON).", "const": "not_after" },
369
+ "IntakeSource": {
370
+ "oneOf": [
371
+ { "type": "object", "required": ["kind"], "properties": { "kind": { "const": "user" } } },
372
+ { "type": "object", "required": ["kind", "tokenID"], "properties": { "kind": { "const": "app" }, "tokenID": { "type": "string" } } },
373
+ { "type": "object", "required": ["kind", "sessionID"], "properties": { "kind": { "const": "session" }, "sessionID": { "type": "string" } } }
374
+ ]
375
+ },
376
+ "IntakeSubmitRequest": {
377
+ "description": "POST /api/agent/teams/:id/intake (scope teams:submit). Optional header Idempotency-Key (keyed per submitter and team; same body → 200 replay, other body → 409 conflict). Unknown fields are 400 invalid.",
378
+ "type": "object",
379
+ "required": ["title"],
380
+ "additionalProperties": false,
381
+ "properties": {
382
+ "title": { "type": "string", "minLength": 1, "maxLength": 120 },
383
+ "brief": { "type": "string", "maxLength": 16000 },
384
+ "priority": { "$ref": "#/$defs/IntakePriority" },
385
+ "labels": { "type": "array", "maxItems": 32, "uniqueItems": true, "items": { "type": "string", "pattern": "^[a-z0-9-]{2,40}$" } },
386
+ "due": { "description": "Informational due time for the worker, epoch ms (never enforced, unlike notAfter).", "type": "integer", "minimum": 1 },
387
+ "criteria": { "type": "array", "maxItems": 8, "items": { "type": "string", "minLength": 1, "maxLength": 200 } },
388
+ "ref": { "type": "string", "minLength": 1, "maxLength": 200 },
389
+ "notAfter": { "description": "(H4) Route-by deadline: a still-queued item is cancelled (reason not_after) once it passes; a routed item's task message inherits it.", "$ref": "#/$defs/NotAfter" }
390
+ }
391
+ },
392
+ "Intake": {
393
+ "description": "A team intake item as read (StoredIntake in src/central/teams/intake.ts).",
394
+ "type": "object",
395
+ "required": ["v", "id", "teamID", "title", "source", "status", "revision", "createdAt", "priority", "updatedAt"],
396
+ "properties": {
397
+ "v": { "const": 1 },
398
+ "id": { "type": "string", "pattern": "^itk_[A-Za-z0-9]{16,32}$" },
399
+ "teamID": { "type": "string", "pattern": "^team_[A-Za-z0-9]{16,32}$" },
400
+ "title": { "type": "string" },
401
+ "source": { "$ref": "#/$defs/IntakeSource" },
402
+ "ref": { "type": "string" },
403
+ "assignmentID": { "type": "string" },
404
+ "status": { "$ref": "#/$defs/IntakeStatus" },
405
+ "revision": { "type": "integer", "minimum": 1 },
406
+ "createdAt": { "$ref": "#/$defs/EpochMs" },
407
+ "priority": { "$ref": "#/$defs/IntakePriority" },
408
+ "labels": { "type": "array", "items": { "type": "string" } },
409
+ "due": { "$ref": "#/$defs/EpochMs" },
410
+ "criteria": { "type": "array", "items": { "type": "string" } },
411
+ "workspaceID": { "type": "string" },
412
+ "sessionID": { "type": "string" },
413
+ "messageID": { "type": "string" },
414
+ "progress": { "$ref": "#/$defs/IntakeProgress" },
415
+ "reason": { "description": "Why it failed or was cancelled (free text, at most 200; exactly `not_after` when the hub expired it, H4).", "type": "string" },
416
+ "lastError": { "type": "string" },
417
+ "updatedAt": { "$ref": "#/$defs/EpochMs" },
418
+ "routedAt": { "$ref": "#/$defs/EpochMs" },
419
+ "closedAt": { "$ref": "#/$defs/EpochMs" },
420
+ "notAfter": { "description": "(H4) As submitted.", "$ref": "#/$defs/EpochMs" }
421
+ }
422
+ },
423
+ "IntakeResponse": { "type": "object", "required": ["intake"], "properties": { "intake": { "$ref": "#/$defs/Intake" } } },
424
+ "IntakeListResponse": {
425
+ "type": "object",
426
+ "required": ["items"],
427
+ "properties": { "items": { "type": "array", "items": { "$ref": "#/$defs/Intake" } }, "cursor": { "type": "string" } }
428
+ },
429
+ "IntakeStatusRequest": {
430
+ "description": "POST /api/agent/teams/:id/intake/:intake/status.",
431
+ "type": "object",
432
+ "required": ["status", "expectedRevision"],
433
+ "additionalProperties": false,
434
+ "properties": {
435
+ "status": { "$ref": "#/$defs/IntakeStatusInput" },
436
+ "expectedRevision": { "type": "integer", "minimum": 0 },
437
+ "reason": { "type": "string", "minLength": 1, "maxLength": 200 }
438
+ }
439
+ },
440
+ "IntakeStatusResponse": {
441
+ "type": "object",
442
+ "required": ["intake"],
443
+ "properties": { "intake": { "$ref": "#/$defs/Intake" }, "assignment": { "type": "object", "required": ["id", "status", "revision"], "properties": { "id": { "type": "string" }, "status": { "enum": ["open", "in_progress", "blocked", "done", "cancelled"] }, "revision": { "type": "integer", "minimum": 1 } } } }
444
+ },
445
+
446
+ "EventID": { "description": "Journal events: evt_ + 20 hex; legacy launch.<state> webhook events: evt_ + 24 hex.", "type": "string", "pattern": "^evt_[0-9a-f]{20}([0-9a-f]{4})?$" },
447
+ "AgentEvent": {
448
+ "description": "A journal event as GET /api/agent/events returns it (AgentEvent in src/central/agent/events.ts).",
449
+ "type": "object",
450
+ "required": ["id", "at", "type", "subject", "data"],
451
+ "properties": {
452
+ "id": { "$ref": "#/$defs/EventID" },
453
+ "at": { "$ref": "#/$defs/EpochMs" },
454
+ "type": { "type": "string" },
455
+ "subject": { "type": "object", "additionalProperties": { "type": "string" } },
456
+ "data": { "type": "object", "additionalProperties": { "type": ["string", "number", "boolean"] } }
457
+ }
458
+ },
459
+
460
+ "LaunchChangedSubject": {
461
+ "type": "object",
462
+ "required": ["launchID"],
463
+ "additionalProperties": false,
464
+ "properties": { "launchID": { "type": "string" }, "sessionID": { "type": "string" } }
465
+ },
466
+ "LaunchChangedEventData": {
467
+ "description": "launch.changed data (launchEvent in src/central/agent/events.ts): never prompt, reason or labels.",
468
+ "type": "object",
469
+ "required": ["state"],
470
+ "additionalProperties": false,
471
+ "properties": {
472
+ "state": { "$ref": "#/$defs/LaunchState" },
473
+ "outcome": { "$ref": "#/$defs/LaunchOutcome" },
474
+ "name": { "type": "string" },
475
+ "wait": { "$ref": "#/$defs/LaunchWait" }
476
+ }
477
+ },
478
+ "MessageChangedSubject": {
479
+ "type": "object",
480
+ "required": ["messageID"],
481
+ "additionalProperties": false,
482
+ "properties": { "messageID": { "type": "string" }, "sessionID": { "type": "string" }, "senderID": { "type": "string" } }
483
+ },
484
+ "MessageChangedEventData": {
485
+ "description": "message.changed data (messageEvent in src/central/agent/events.ts): never the text.",
486
+ "type": "object",
487
+ "required": ["kind", "state", "from", "to"],
488
+ "additionalProperties": false,
489
+ "properties": {
490
+ "kind": { "$ref": "#/$defs/MessageKind" },
491
+ "state": { "$ref": "#/$defs/MessageState" },
492
+ "from": { "type": "string" },
493
+ "to": { "type": "string" },
494
+ "replyTo": { "type": "string" },
495
+ "delivery": { "$ref": "#/$defs/MessageDelivery" },
496
+ "reason": { "description": "(H2) Only with delivery expired: which deadline passed (an enum, never text).", "$ref": "#/$defs/ExpiredReason" }
497
+ }
498
+ },
499
+ "TeamIntakeSubject": {
500
+ "type": "object",
501
+ "required": ["teamID", "intakeID"],
502
+ "additionalProperties": false,
503
+ "properties": { "teamID": { "type": "string" }, "intakeID": { "type": "string" }, "assignmentID": { "type": "string" }, "workspaceID": { "type": "string" }, "sessionID": { "type": "string" } }
504
+ },
505
+ "TeamIntakeEventData": {
506
+ "description": "team.intake data (intakeEvent in src/central/teams/intake.ts): enums, revision and timestamps only; never title, brief, labels, criteria or the free-text reason.",
507
+ "type": "object",
508
+ "required": ["status", "revision", "source", "priority", "createdAt", "updatedAt"],
509
+ "additionalProperties": false,
510
+ "properties": {
511
+ "status": { "$ref": "#/$defs/IntakeStatus" },
512
+ "revision": { "type": "integer", "minimum": 1 },
513
+ "source": { "enum": ["user", "app", "session"] },
514
+ "priority": { "$ref": "#/$defs/IntakePriority" },
515
+ "progress": { "$ref": "#/$defs/IntakeProgress" },
516
+ "reasonCode": { "description": "A code, never text: the last routing failure's error code (lastError, an ErrorCode) or (H4) `not_after` on an item cancelled because its notAfter passed while queued.", "type": "string", "pattern": "^[a-z][a-z_]{0,39}$" },
517
+ "createdAt": { "$ref": "#/$defs/EpochMs" },
518
+ "updatedAt": { "$ref": "#/$defs/EpochMs" },
519
+ "routedAt": { "$ref": "#/$defs/EpochMs" },
520
+ "closedAt": { "$ref": "#/$defs/EpochMs" }
521
+ }
522
+ },
523
+
524
+ "CanonicalWebhookEventType": {
525
+ "description": "Event types a webhook can select today (CANONICAL_WEBHOOK_EVENT_TYPES in src/central/agent/webhooks.ts). Patterns may also be `<family>.*`, except for ExactOnlyWebhookEventType.",
526
+ "enum": ["session.added", "session.removed", "session.changed", "session.archived", "session.named", "session.pinned", "blocker.opened", "blocker.closed", "machine.changed", "role.claimed", "role.released", "message.changed", "launch.changed", "team.intake"]
527
+ },
528
+ "ExactOnlyWebhookEventType": { "description": "(H5) Types no family wildcard selects: register them by exact name (`team.*` is 400 invalid).", "enum": ["team.intake"] },
529
+ "LegacyLaunchWebhookType": {
530
+ "description": "Pre-canonical registration patterns kept for rollback compatibility: a registration for launch.<state> receives the launch.changed body of that state.",
531
+ "type": "string",
532
+ "pattern": "^launch\\.(queued|placing|provisioning|running|ended)$"
533
+ },
534
+ "WebhookEnvelope": {
535
+ "description": "Every webhook POST body (CloudEvents-shaped; WebhookEvent in src/central/agent/webhooks.ts). Headers: WebhookHeaders.",
536
+ "type": "object",
537
+ "required": ["id", "type", "source", "subject", "time", "user", "schemaVersion", "data"],
538
+ "properties": {
539
+ "id": { "$ref": "#/$defs/EventID" },
540
+ "type": { "type": "string" },
541
+ "source": { "description": "The hub's public URL.", "type": "string" },
542
+ "subject": { "description": "The launch, message, intake, session or machine ID (first present, in that order), else the event ID.", "type": "string" },
543
+ "time": { "description": "ISO 8601 (Date.prototype.toISOString).", "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$" },
544
+ "user": { "type": "string" },
545
+ "schemaVersion": { "const": 1 },
546
+ "data": {}
547
+ }
548
+ },
549
+ "CanonicalWebhookData": {
550
+ "description": "The data of a canonical webhook event: the journal event's subject and data.",
551
+ "type": "object",
552
+ "required": ["subject", "data"],
553
+ "additionalProperties": false,
554
+ "properties": { "subject": { "type": "object" }, "data": { "type": "object" } }
555
+ },
556
+ "LaunchChangedWebhook": {
557
+ "allOf": [
558
+ { "$ref": "#/$defs/WebhookEnvelope" },
559
+ {
560
+ "properties": {
561
+ "type": { "const": "launch.changed" },
562
+ "data": {
563
+ "allOf": [{ "$ref": "#/$defs/CanonicalWebhookData" }, { "properties": { "subject": { "$ref": "#/$defs/LaunchChangedSubject" }, "data": { "$ref": "#/$defs/LaunchChangedEventData" } } }]
564
+ }
565
+ }
566
+ }
567
+ ]
568
+ },
569
+ "LegacyLaunchWebhook": {
570
+ "description": "The pre-canonical `launch.<state>` body (launchEvent in src/central/agent/webhooks.ts: data {launch}). Webhooks.onLaunch builds it but the current hub never calls it (src/central/http.ts publishes canonical events only); kept for receivers of older hubs.",
571
+ "allOf": [
572
+ { "$ref": "#/$defs/WebhookEnvelope" },
573
+ {
574
+ "properties": {
575
+ "type": { "$ref": "#/$defs/LegacyLaunchWebhookType" },
576
+ "data": { "type": "object", "required": ["launch"], "properties": { "launch": { "$ref": "#/$defs/Launch" } } }
577
+ }
578
+ }
579
+ ]
580
+ },
581
+ "MessageChangedWebhook": {
582
+ "allOf": [
583
+ { "$ref": "#/$defs/WebhookEnvelope" },
584
+ {
585
+ "properties": {
586
+ "type": { "const": "message.changed" },
587
+ "data": {
588
+ "allOf": [{ "$ref": "#/$defs/CanonicalWebhookData" }, { "properties": { "subject": { "$ref": "#/$defs/MessageChangedSubject" }, "data": { "$ref": "#/$defs/MessageChangedEventData" } } }]
589
+ }
590
+ }
591
+ }
592
+ ]
593
+ },
594
+ "TeamIntakeWebhook": {
595
+ "description": "(H5) team.intake; envelope subject = the intake ID. A user's registrations get every one; an app's only those of intake that app submitted (event audience by app:<tokenID>).",
596
+ "allOf": [
597
+ { "$ref": "#/$defs/WebhookEnvelope" },
598
+ {
599
+ "properties": {
600
+ "type": { "const": "team.intake" },
601
+ "data": {
602
+ "allOf": [{ "$ref": "#/$defs/CanonicalWebhookData" }, { "properties": { "subject": { "$ref": "#/$defs/TeamIntakeSubject" }, "data": { "$ref": "#/$defs/TeamIntakeEventData" } } }]
603
+ }
604
+ }
605
+ }
606
+ ]
607
+ },
608
+ "HubSignature": {
609
+ "description": "The Hub-Signature header as the hub sends it: t=<unix seconds>,v1=<hex HMAC-SHA256 over `<t>.<raw body>` keyed with the registration's whsec_ secret>; one v1 per active secret while a secret rotates. Receivers accept any matching v1, compare in constant time, refuse |now - t| > 300 s, and ignore unknown schemes (packages/protocol/src/webhook.ts).",
610
+ "type": "string",
611
+ "maxLength": 2048,
612
+ "pattern": "^t=[0-9]{1,12}(,v1=[0-9a-f]{64})+$"
613
+ },
614
+ "WebhookHeaders": {
615
+ "description": "Headers of every webhook POST (names lower-cased). Hub-Delivery is stable across retries: deduplicate on it.",
616
+ "type": "object",
617
+ "required": ["hub-signature", "hub-event", "hub-delivery", "content-type"],
618
+ "properties": {
619
+ "hub-signature": { "$ref": "#/$defs/HubSignature" },
620
+ "hub-event": { "type": "string" },
621
+ "hub-delivery": { "type": "string", "pattern": "^whd_[0-9a-f]{24}$" },
622
+ "content-type": { "const": "application/json" }
623
+ }
624
+ },
625
+ "WebhookRegisterRequest": {
626
+ "description": "POST /api/agent/webhooks. Users; apps need webhookRequiredScopes(events): events:subscribe plus each selected type's scope, except that events [\"team.intake\"] alone needs only teams:submit. events default [\"launch.*\"].",
627
+ "type": "object",
628
+ "required": ["url"],
629
+ "properties": {
630
+ "url": { "type": "string", "minLength": 1, "maxLength": 2048 },
631
+ "events": { "type": "array", "minItems": 1, "maxItems": 20, "items": { "type": "string" } },
632
+ "label": { "type": ["string", "null"], "maxLength": 100 }
633
+ }
634
+ },
635
+ "WebhookView": {
636
+ "type": "object",
637
+ "required": ["id", "url", "events", "by", "created"],
638
+ "properties": {
639
+ "id": { "type": "string", "pattern": "^wh_[0-9a-f]{16}$" },
640
+ "secret": false,
641
+ "url": { "type": "string" },
642
+ "events": { "type": "array", "items": { "type": "string" } },
643
+ "by": { "type": "string", "pattern": "^(user|app:.+)$" },
644
+ "label": { "type": "string" },
645
+ "created": { "$ref": "#/$defs/EpochMs" }
646
+ }
647
+ },
648
+ "WebhookRegisterResponse": {
649
+ "description": "201: the secret is shown only here.",
650
+ "type": "object",
651
+ "required": ["webhook", "secret"],
652
+ "properties": { "webhook": { "$ref": "#/$defs/WebhookView" }, "secret": { "type": "string", "pattern": "^whsec_[A-Za-z0-9_-]{43}$" } }
653
+ },
654
+
655
+ "RelayState": { "description": "(H6) The state of one store-and-forward relay item.", "enum": ["queued", "delivered", "conflicted", "failed", "expired"] },
656
+ "RelayItem": {
657
+ "description": "(H6) A hub-initiated write to a connected app, as the hub shows it (never its body or the app's URL).",
658
+ "type": "object",
659
+ "required": ["id", "connection", "state", "attempts", "created"],
660
+ "properties": {
661
+ "id": { "type": "string", "pattern": "^rly_[0-9a-f]{24}$" },
662
+ "connection": { "type": "string" },
663
+ "state": { "$ref": "#/$defs/RelayState" },
664
+ "attempts": { "type": "integer", "minimum": 0 },
665
+ "created": { "$ref": "#/$defs/EpochMs" },
666
+ "nextAt": { "$ref": "#/$defs/EpochMs" },
667
+ "notAfter": { "$ref": "#/$defs/EpochMs" },
668
+ "expiredReason": { "$ref": "#/$defs/ExpiredReason" },
669
+ "last": {
670
+ "type": "object",
671
+ "required": ["at"],
672
+ "properties": { "at": { "$ref": "#/$defs/EpochMs" }, "status": { "type": "integer" }, "error": { "type": "string", "maxLength": 300 } }
673
+ }
674
+ }
675
+ }
676
+ }
677
+ }