@ours.network/fleet 1.2.0-nightly.2 → 1.2.0-nightly.21

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 (277) hide show
  1. package/README.md +152 -29
  2. package/dist/agent-ours/bridge.d.ts +3 -0
  3. package/dist/agent-ours/bridge.js +178 -0
  4. package/dist/agent-ours/controller.d.ts +14 -0
  5. package/dist/agent-ours/controller.js +60 -0
  6. package/dist/agent-ours/correspondence.d.ts +10 -0
  7. package/dist/agent-ours/correspondence.js +1 -0
  8. package/dist/agent-ours/file-rpc.d.ts +23 -0
  9. package/dist/agent-ours/file-rpc.js +123 -0
  10. package/dist/agent-ours/harness.d.ts +14 -0
  11. package/dist/agent-ours/harness.js +72 -0
  12. package/dist/agent-ours/mcp-endpoint.d.ts +15 -0
  13. package/dist/agent-ours/mcp-endpoint.js +111 -0
  14. package/dist/agent-ours/runtime.d.ts +59 -0
  15. package/dist/agent-ours/runtime.js +283 -0
  16. package/dist/agent-ours/service.d.ts +23 -0
  17. package/dist/agent-ours/service.js +372 -0
  18. package/dist/agent-ours/state.d.ts +45 -0
  19. package/dist/agent-ours/state.js +95 -0
  20. package/dist/agent-ours/wire.d.ts +14 -0
  21. package/dist/agent-ours/wire.js +55 -0
  22. package/dist/agent-recovery-gate.js +1 -1
  23. package/dist/application/conversation-history.d.ts +6 -0
  24. package/dist/application/conversation-history.js +65 -0
  25. package/dist/application/fleet-query-service.d.ts +1 -1
  26. package/dist/application/fleet-query-service.js +2 -2
  27. package/dist/application/legacy-supervisor-identity.d.ts +7 -0
  28. package/dist/application/legacy-supervisor-identity.js +43 -0
  29. package/dist/application/model-catalog.d.ts +1 -1
  30. package/dist/application/model-catalog.js +5 -2
  31. package/dist/application/preset-provenance.d.ts +19 -0
  32. package/dist/application/preset-provenance.js +59 -0
  33. package/dist/application/room-layout-definitions.d.ts +45 -0
  34. package/dist/application/room-layout-definitions.js +130 -0
  35. package/dist/application/session-control.d.ts +8 -0
  36. package/dist/application/session-control.js +21 -3
  37. package/dist/application/supervisor-ours-tools.d.ts +159 -0
  38. package/dist/application/supervisor-ours-tools.js +113 -0
  39. package/dist/application/task-layouts.d.ts +89 -0
  40. package/dist/application/task-layouts.js +248 -0
  41. package/dist/application/task-room-service.d.ts +99 -3
  42. package/dist/application/task-room-service.js +296 -31
  43. package/dist/atomic-file.d.ts +5 -0
  44. package/dist/atomic-file.js +62 -1
  45. package/dist/briefing.js +10 -59
  46. package/dist/build-info.json +5 -4
  47. package/dist/capabilities.d.ts +2 -1
  48. package/dist/capabilities.js +2 -0
  49. package/dist/cli.js +65 -2
  50. package/dist/client-profile.d.ts +5 -16
  51. package/dist/client-profile.js +4 -112
  52. package/dist/config.d.ts +4 -0
  53. package/dist/config.js +7 -0
  54. package/dist/creation.d.ts +1 -1
  55. package/dist/creation.js +5 -16
  56. package/dist/daemon-recovery.d.ts +1 -8
  57. package/dist/daemon-recovery.js +8 -113
  58. package/dist/docs.d.ts +3 -3
  59. package/dist/docs.js +43 -23
  60. package/dist/doctor.d.ts +1 -1
  61. package/dist/doctor.js +46 -59
  62. package/dist/erased-resources.d.ts +9 -0
  63. package/dist/erased-resources.js +87 -0
  64. package/dist/fleet-command-audit.d.ts +1 -0
  65. package/dist/fleet-command-audit.js +12 -7
  66. package/dist/harness/agent-session.d.ts +2 -0
  67. package/dist/harness/claude-code-session.js +4 -2
  68. package/dist/harness/codex-app-server-proxy.d.ts +4 -1
  69. package/dist/harness/codex-app-server-proxy.js +41 -5
  70. package/dist/harness/codex-session.js +9 -2
  71. package/dist/harness/codex.d.ts +3 -2
  72. package/dist/harness/codex.js +4 -3
  73. package/dist/harness/hermes-session.js +7 -1
  74. package/dist/init-wizard.js +20 -9
  75. package/dist/monitor.d.ts +1 -34
  76. package/dist/monitor.js +24 -151
  77. package/dist/owner-channel/channel.d.ts +3 -0
  78. package/dist/owner-channel/channel.js +34 -6
  79. package/dist/owner-channel/commands.d.ts +4 -0
  80. package/dist/owner-channel/commands.js +8 -0
  81. package/dist/owner-channel/lifecycle-outbox.js +4 -3
  82. package/dist/owner-channel/ours-client.js +2 -17
  83. package/dist/preset-migration.js +15 -0
  84. package/dist/rooms-tasks/add-member.d.ts +48 -0
  85. package/dist/rooms-tasks/add-member.js +83 -0
  86. package/dist/rooms-tasks/archived-absence.d.ts +8 -0
  87. package/dist/rooms-tasks/archived-absence.js +48 -0
  88. package/dist/rooms-tasks/cli.js +34 -5
  89. package/dist/rooms-tasks/close.d.ts +5 -2
  90. package/dist/rooms-tasks/close.js +208 -28
  91. package/dist/rooms-tasks/config.js +3 -1
  92. package/dist/rooms-tasks/cowork-adapter.d.ts +6 -4
  93. package/dist/rooms-tasks/cowork-adapter.js +11 -117
  94. package/dist/rooms-tasks/cowork-http.d.ts +3 -0
  95. package/dist/rooms-tasks/cowork-http.js +70 -0
  96. package/dist/rooms-tasks/deletion.d.ts +1 -1
  97. package/dist/rooms-tasks/deletion.js +74 -5
  98. package/dist/rooms-tasks/erasure.d.ts +6 -0
  99. package/dist/rooms-tasks/erasure.js +169 -0
  100. package/dist/rooms-tasks/external-worker.d.ts +1 -1
  101. package/dist/rooms-tasks/external-worker.js +3 -4
  102. package/dist/rooms-tasks/layout-cli.d.ts +2 -0
  103. package/dist/rooms-tasks/layout-cli.js +81 -0
  104. package/dist/rooms-tasks/layout-config.d.ts +11 -0
  105. package/dist/rooms-tasks/layout-config.js +89 -0
  106. package/dist/rooms-tasks/layout-control.d.ts +21 -0
  107. package/dist/rooms-tasks/layout-control.js +52 -0
  108. package/dist/rooms-tasks/layout-service.d.ts +39 -0
  109. package/dist/rooms-tasks/layout-service.js +173 -0
  110. package/dist/rooms-tasks/layout.d.ts +102 -0
  111. package/dist/rooms-tasks/layout.js +227 -0
  112. package/dist/rooms-tasks/live-readiness.d.ts +18 -0
  113. package/dist/rooms-tasks/live-readiness.js +85 -0
  114. package/dist/rooms-tasks/member-startup.d.ts +1 -0
  115. package/dist/rooms-tasks/member-startup.js +2 -0
  116. package/dist/rooms-tasks/provision.d.ts +14 -0
  117. package/dist/rooms-tasks/provision.js +182 -48
  118. package/dist/rooms-tasks/room-state.d.ts +4 -2
  119. package/dist/rooms-tasks/room-state.js +53 -8
  120. package/dist/rooms-tasks/task-state.d.ts +42 -7
  121. package/dist/rooms-tasks/task-state.js +153 -23
  122. package/dist/rooms-tasks/types.d.ts +28 -0
  123. package/dist/rooms-tasks/workspace-artifacts.d.ts +6 -0
  124. package/dist/rooms-tasks/workspace-artifacts.js +93 -0
  125. package/dist/rooms-tasks/workspace.d.ts +26 -0
  126. package/dist/rooms-tasks/workspace.js +195 -0
  127. package/dist/runner.d.ts +23 -3
  128. package/dist/runner.js +574 -494
  129. package/dist/session/acp.d.ts +3 -0
  130. package/dist/session/acp.js +7 -0
  131. package/dist/session/arbiter.d.ts +3 -0
  132. package/dist/session/arbiter.js +20 -2
  133. package/dist/session/codex-app-server-transport.js +1 -1
  134. package/dist/session/codex-app-server.d.ts +3 -0
  135. package/dist/session/codex-app-server.js +25 -1
  136. package/dist/session/control.d.ts +10 -1
  137. package/dist/session/control.js +36 -2
  138. package/dist/session/conversation-normalizer.d.ts +2 -1
  139. package/dist/session/conversation-normalizer.js +1 -1
  140. package/dist/session/conversation-types.d.ts +5 -3
  141. package/dist/session/task-notice.d.ts +14 -0
  142. package/dist/session/task-notice.js +13 -0
  143. package/dist/session/types.d.ts +3 -0
  144. package/dist/socket-path.d.ts +2 -0
  145. package/dist/socket-path.js +22 -0
  146. package/dist/spawn.js +19 -16
  147. package/dist/startup-readiness.d.ts +13 -0
  148. package/dist/startup-readiness.js +38 -0
  149. package/dist/supervisor/launchd.js +2 -1
  150. package/dist/supervisor/systemd.js +9 -23
  151. package/dist/temp-idle.d.ts +17 -0
  152. package/dist/temp-idle.js +42 -0
  153. package/dist/temp-lifecycle.d.ts +6 -1
  154. package/dist/temp-lifecycle.js +29 -13
  155. package/dist/temp-supervisor-recovery.d.ts +3 -0
  156. package/dist/temp-supervisor-recovery.js +46 -0
  157. package/dist/watchdog/briefing.js +3 -16
  158. package/dist/watchdog/run.js +19 -5
  159. package/dist/web/agent-attachments.d.ts +29 -0
  160. package/dist/web/agent-attachments.js +162 -0
  161. package/dist/web/auth.d.ts +4 -1
  162. package/dist/web/auth.js +23 -2
  163. package/dist/web/control.js +2 -1
  164. package/dist/web/fleet-config-service.d.ts +4 -1
  165. package/dist/web/fleet-config-service.js +46 -15
  166. package/dist/web/prefix-gateway.d.ts +20 -0
  167. package/dist/web/prefix-gateway.js +150 -0
  168. package/dist/web/runtime.d.ts +4 -0
  169. package/dist/web/runtime.js +19 -14
  170. package/dist/web/server.d.ts +7 -0
  171. package/dist/web/server.js +293 -25
  172. package/docs/room-layouts.md +92 -0
  173. package/docs/task-workspaces.md +130 -0
  174. package/examples/room-layouts/fresh.yaml +27 -0
  175. package/examples/room-layouts/shared.yaml +19 -0
  176. package/package.json +17 -13
  177. package/presets/README.md +78 -0
  178. package/presets/brain-catalog.json +8 -2
  179. package/presets/fleet/agent_templates/Engineer.yaml +5 -0
  180. package/presets/fleet/brains/claude-fable-5-1-high.yaml +5 -0
  181. package/presets/fleet/brains/claude-fable-5-1-low.yaml +5 -0
  182. package/presets/fleet/brains/claude-fable-5-1-max.yaml +5 -0
  183. package/presets/fleet/brains/claude-fable-5-1-medium.yaml +5 -0
  184. package/presets/fleet/brains/claude-fable-5-1-xhigh.yaml +5 -0
  185. package/presets/fleet/brains/claude-fable-5-high.yaml +1 -1
  186. package/presets/fleet/brains/claude-fable-5-low.yaml +1 -1
  187. package/presets/fleet/brains/claude-fable-5-max.yaml +1 -1
  188. package/presets/fleet/brains/claude-fable-5-medium.yaml +1 -1
  189. package/presets/fleet/brains/claude-fable-5-xhigh.yaml +1 -1
  190. package/presets/fleet/brains/claude-haiku-4-5-20251001-default.yaml +4 -0
  191. package/presets/fleet/brains/claude-opus-5-5-high.yaml +5 -0
  192. package/presets/fleet/brains/claude-opus-5-5-low.yaml +5 -0
  193. package/presets/fleet/brains/claude-opus-5-5-max.yaml +5 -0
  194. package/presets/fleet/brains/claude-opus-5-5-medium.yaml +5 -0
  195. package/presets/fleet/brains/claude-opus-5-5-xhigh.yaml +5 -0
  196. package/presets/fleet/brains/claude-opus-5-high.yaml +1 -1
  197. package/presets/fleet/brains/claude-opus-5-low.yaml +1 -1
  198. package/presets/fleet/brains/claude-opus-5-max.yaml +1 -1
  199. package/presets/fleet/brains/claude-opus-5-medium.yaml +1 -1
  200. package/presets/fleet/brains/claude-opus-5-xhigh.yaml +1 -1
  201. package/presets/fleet/brains/claude-sonnet-5-high.yaml +1 -1
  202. package/presets/fleet/brains/claude-sonnet-5-low.yaml +1 -1
  203. package/presets/fleet/brains/claude-sonnet-5-max.yaml +1 -1
  204. package/presets/fleet/brains/claude-sonnet-5-medium.yaml +1 -1
  205. package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +1 -1
  206. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +1 -1
  207. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +1 -1
  208. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +1 -1
  209. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +1 -1
  210. package/presets/fleet/brains/codex-gpt-5-4-high.yaml +1 -1
  211. package/presets/fleet/brains/codex-gpt-5-4-low.yaml +1 -1
  212. package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +1 -1
  213. package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +1 -1
  214. package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +1 -1
  215. package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +1 -1
  216. package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +1 -1
  217. package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +1 -1
  218. package/presets/fleet/brains/codex-gpt-5-5-high.yaml +1 -1
  219. package/presets/fleet/brains/codex-gpt-5-5-low.yaml +1 -1
  220. package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +1 -1
  221. package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +1 -1
  222. package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +1 -1
  223. package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +1 -1
  224. package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +1 -1
  225. package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +1 -1
  226. package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +1 -1
  227. package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +1 -1
  228. package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +1 -1
  229. package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +1 -1
  230. package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +1 -1
  231. package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +1 -1
  232. package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +1 -1
  233. package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +1 -1
  234. package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +1 -1
  235. package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +1 -1
  236. package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +1 -1
  237. package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +1 -1
  238. package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +1 -1
  239. package/presets/fleet/brains/codex-gpt-6-1-sol-high.yaml +5 -0
  240. package/presets/fleet/brains/codex-gpt-6-1-sol-low.yaml +5 -0
  241. package/presets/fleet/brains/codex-gpt-6-1-sol-max.yaml +5 -0
  242. package/presets/fleet/brains/codex-gpt-6-1-sol-medium.yaml +5 -0
  243. package/presets/fleet/brains/codex-gpt-6-1-sol-ultra.yaml +5 -0
  244. package/presets/fleet/brains/codex-gpt-6-1-sol-xhigh.yaml +5 -0
  245. package/presets/fleet/brains/codex-gpt-6-astra-high.yaml +1 -1
  246. package/presets/fleet/brains/codex-gpt-6-astra-low.yaml +1 -1
  247. package/presets/fleet/brains/codex-gpt-6-astra-max.yaml +1 -1
  248. package/presets/fleet/brains/codex-gpt-6-astra-medium.yaml +1 -1
  249. package/presets/fleet/brains/codex-gpt-6-astra-ultra.yaml +1 -1
  250. package/presets/fleet/brains/codex-gpt-6-astra-xhigh.yaml +1 -1
  251. package/presets/fleet/brains/codex-gpt-6-luna-high.yaml +5 -0
  252. package/presets/fleet/brains/codex-gpt-6-luna-low.yaml +5 -0
  253. package/presets/fleet/brains/codex-gpt-6-luna-max.yaml +5 -0
  254. package/presets/fleet/brains/codex-gpt-6-luna-medium.yaml +5 -0
  255. package/presets/fleet/brains/codex-gpt-6-luna-xhigh.yaml +5 -0
  256. package/presets/fleet/brains/codex-gpt-6-sol-high.yaml +5 -0
  257. package/presets/fleet/brains/codex-gpt-6-sol-low.yaml +5 -0
  258. package/presets/fleet/brains/codex-gpt-6-sol-max.yaml +5 -0
  259. package/presets/fleet/brains/codex-gpt-6-sol-medium.yaml +5 -0
  260. package/presets/fleet/brains/codex-gpt-6-sol-ultra.yaml +5 -0
  261. package/presets/fleet/brains/codex-gpt-6-sol-xhigh.yaml +5 -0
  262. package/presets/fleet/roles/Coordinator.yaml +22 -0
  263. package/presets/fleet/roles/Engineer.yaml +40 -0
  264. package/presets/fleet/room_layouts/engineering.yaml +25 -0
  265. package/presets/fleet/room_layouts/pair.yaml +24 -0
  266. package/presets/fleet/room_layouts/single.yaml +17 -0
  267. package/presets/fleet/room_layouts/team.yaml +30 -0
  268. package/presets/fleet/room_templates/engineering.yaml +19 -0
  269. package/presets/manifest.json +2 -2
  270. package/dist/web-app/assets/index-59GF-gsZ.css +0 -1
  271. package/dist/web-app/assets/index-C0h3ALvs.js +0 -10
  272. package/dist/web-app/icons/ours-fleet-maskable.svg +0 -4
  273. package/dist/web-app/icons/ours-fleet.svg +0 -4
  274. package/dist/web-app/index.html +0 -17
  275. package/dist/web-app/manifest.webmanifest +0 -15
  276. package/dist/web-app/offline.html +0 -18
  277. package/dist/web-app/sw.js +0 -51
@@ -4,6 +4,9 @@ export interface EditableFleetModel {
4
4
  manifest: Record<string, unknown>;
5
5
  agents: Record<string, Record<string, unknown>>;
6
6
  agent_templates: Record<string, Record<string, unknown>>;
7
+ roles?: Record<string, Record<string, unknown>>;
8
+ brains?: Record<string, Record<string, unknown>>;
9
+ room_templates?: Record<string, Record<string, unknown>>;
7
10
  }
8
11
  export interface ConfigReadResult {
9
12
  path: string;
@@ -45,7 +48,7 @@ export declare class FleetConfigService {
45
48
  private readonly preflight;
46
49
  private readonly beforeMutation?;
47
50
  constructor(options?: FleetConfigServiceOptions);
48
- read(): ConfigReadResult;
51
+ read(includeDefinitions?: boolean): ConfigReadResult;
49
52
  preview(baseRevision: string, input: unknown): Promise<ConfigPreviewResult>;
50
53
  write(baseRevision: string, input: unknown): Promise<ConfigWriteResult>;
51
54
  }
@@ -23,7 +23,7 @@ export class FleetConfigService {
23
23
  this.preflight = options.preflight ?? (async () => emptyReport());
24
24
  this.beforeMutation = options.beforeMutation;
25
25
  }
26
- read() {
26
+ read(includeDefinitions = false) {
27
27
  const sources = readSources(this.path);
28
28
  try {
29
29
  loadConfig(this.path, { yamlMode: 'strict' });
@@ -31,7 +31,7 @@ export class FleetConfigService {
31
31
  catch (error) {
32
32
  throw safeError(error, sourceSensitiveValues(sources));
33
33
  }
34
- const raw = modelFrom(sources);
34
+ const raw = modelFrom(sources, includeDefinitions);
35
35
  const redacted = redactModel(raw);
36
36
  return {
37
37
  path: basename(this.path), exists: true, firstRun: false,
@@ -41,7 +41,7 @@ export class FleetConfigService {
41
41
  async preview(baseRevision, input) {
42
42
  const current = readSources(this.path);
43
43
  assertRevision(baseRevision, current);
44
- const currentModel = modelFrom(current);
44
+ const currentModel = modelFrom(current, !!input && typeof input === 'object' && 'roles' in input);
45
45
  const next = restoreRedactions(assertModel(input), currentModel, new Set(redactModel(currentModel).paths));
46
46
  const proposal = renderProposal(current, next);
47
47
  const staged = stageProposal(this.path, current, proposal);
@@ -63,7 +63,7 @@ export class FleetConfigService {
63
63
  return withFileLock(`${splitRootFor(this.path)}.web-edit.lock`, async () => {
64
64
  const current = readSources(this.path);
65
65
  assertRevision(baseRevision, current);
66
- const currentModel = modelFrom(current);
66
+ const currentModel = modelFrom(current, !!input && typeof input === 'object' && 'roles' in input);
67
67
  const next = restoreRedactions(assertModel(input), currentModel, new Set(redactModel(currentModel).paths));
68
68
  const proposal = renderProposal(current, next);
69
69
  const changed = changedDocuments(current, proposal);
@@ -105,6 +105,8 @@ export class FleetConfigService {
105
105
  rmSync(target, { force: true });
106
106
  else {
107
107
  assertWritableTarget(target, current.documents.has(rel));
108
+ mkdirSync(dirname(target), { recursive: true, mode: 0o700 });
109
+ trustedDirectory(dirname(target));
108
110
  replaceFileAtomically(target, contents, 0o600);
109
111
  chmodSync(target, 0o600);
110
112
  }
@@ -184,7 +186,7 @@ function readSources(manifest) {
184
186
  }
185
187
  return { manifest, root, documents };
186
188
  }
187
- function modelFrom(sources) {
189
+ function modelFrom(sources, includeDefinitions = false) {
188
190
  const manifestKey = basename(sources.manifest);
189
191
  const manifest = parseFleetDocument(sources.manifest, sources.documents.get(manifestKey), 'strict').value;
190
192
  const agents = {};
@@ -199,15 +201,24 @@ function modelFrom(sources) {
199
201
  const id = basename(rel, extname(rel));
200
202
  agent_templates[id] = parseFleetDocument(absoluteFor(sources.manifest, rel), source, 'strict').value;
201
203
  }
202
- return { manifest, agents, agent_templates };
204
+ const model = { manifest, agents, agent_templates };
205
+ if (includeDefinitions)
206
+ for (const kind of ['roles', 'brains', 'room_templates']) {
207
+ model[kind] = {};
208
+ for (const [rel, source] of sources.documents)
209
+ if (rel.startsWith(kind + '/') && ['.yaml', '.yml'].includes(extname(rel))) {
210
+ model[kind][basename(rel, extname(rel))] = parseFleetDocument(absoluteFor(sources.manifest, rel), source, 'strict').value;
211
+ }
212
+ }
213
+ return model;
203
214
  }
204
215
  function assertModel(value) {
205
216
  if (!value || typeof value !== 'object' || Array.isArray(value))
206
217
  throw new FleetError('invalid_request', 'model must be an object');
207
218
  const input = value;
208
- if (Object.keys(input).sort().join(',') !== 'agent_templates,agents,manifest')
209
- throw new FleetError('invalid_request', 'model allows exactly manifest, agents, and agent_templates');
210
- for (const key of ['manifest', 'agents', 'agent_templates'])
219
+ if (!['agent_templates,agents,manifest', 'agent_templates,agents,brains,manifest,roles,room_templates'].includes(Object.keys(input).sort().join(',')))
220
+ throw new FleetError('invalid_request', 'model requires manifest, agents, agent_templates and optionally all definition collections');
221
+ for (const key of Object.keys(input))
211
222
  if (!input[key] || typeof input[key] !== 'object' || Array.isArray(input[key]))
212
223
  throw new FleetError('invalid_request', `${key} must be an object`);
213
224
  for (const [id, doc] of Object.entries(input.agents)) {
@@ -222,6 +233,14 @@ function assertModel(value) {
222
233
  if (!doc || typeof doc !== 'object' || Array.isArray(doc))
223
234
  throw new FleetError('invalid_request', `Agent Template '${id}' must be an object`);
224
235
  }
236
+ for (const kind of ['roles', 'brains', 'room_templates']) {
237
+ for (const [id, doc] of Object.entries((input[kind] ?? {}))) {
238
+ if (!ROLE_NAME_RE.test(id))
239
+ throw new FleetError('invalid_request', `invalid definition ID '${id}'`);
240
+ if (!doc || typeof doc !== 'object' || Array.isArray(doc))
241
+ throw new FleetError('invalid_request', `definition '${id}' must be an object`);
242
+ }
243
+ }
225
244
  return structuredClone(input);
226
245
  }
227
246
  function renderProposal(current, model) {
@@ -240,9 +259,21 @@ function renderProposal(current, model) {
240
259
  next.set(oldRel ?? join('agent_templates', `${id}.yaml`), oldRel
241
260
  ? render(current.documents.get(oldRel), doc) : stringify(doc));
242
261
  }
243
- for (const [rel, source] of current.documents)
244
- if (rel.startsWith('roles/') || rel.startsWith('brains/') || rel.startsWith('room_templates/'))
245
- next.set(rel, source);
262
+ for (const kind of ['roles', 'brains', 'room_templates']) {
263
+ if (model[kind] === undefined) {
264
+ for (const [rel, source] of current.documents)
265
+ if (rel.startsWith(kind + '/'))
266
+ next.set(rel, source);
267
+ continue;
268
+ }
269
+ for (const [rel, source] of current.documents)
270
+ if (rel.startsWith(kind + '/') && !['.yaml', '.yml'].includes(extname(rel)))
271
+ next.set(rel, source);
272
+ for (const [id, doc] of Object.entries(model[kind])) {
273
+ const oldRel = [...current.documents.keys()].find(rel => rel.startsWith(kind + '/') && ['.yaml', '.yml'].includes(extname(rel)) && basename(rel, extname(rel)) === id);
274
+ next.set(oldRel ?? join(kind, `${id}.yaml`), oldRel ? render(current.documents.get(oldRel), doc) : stringify(doc));
275
+ }
276
+ }
246
277
  return next;
247
278
  }
248
279
  function render(source, model) {
@@ -395,9 +426,9 @@ function sourceDiff(before, after, label, secrets) {
395
426
  ...left.slice(left.length - tail, left.length - tail + trailing).map(x => ` ${x}`), ''].join('\n');
396
427
  }
397
428
  function proposalDiff(current, proposal) {
398
- const secrets = sensitiveValues(modelFrom(current));
429
+ const secrets = sensitiveValues(modelFrom(current, true));
399
430
  try {
400
- secrets.push(...sensitiveValues(modelFrom({ ...current, documents: proposal })));
431
+ secrets.push(...sensitiveValues(modelFrom({ ...current, documents: proposal }, true)));
401
432
  }
402
433
  catch { /* validation reports later */ }
403
434
  return [...new Set([...current.documents.keys(), ...proposal.keys()])].sort().map(rel => sourceDiff(current.documents.get(rel) ?? '', proposal.get(rel) ?? '', rel, secrets)).filter(Boolean).join('\n');
@@ -405,7 +436,7 @@ function proposalDiff(current, proposal) {
405
436
  function splitLines(source) { return source.replace(/\n$/, '').split('\n'); }
406
437
  function restartImpact(before, after) {
407
438
  const roles = [...new Set([...Object.keys(before.agents), ...Object.keys(after.agents)])]
408
- .filter(id => JSON.stringify(before.agents[id]) !== JSON.stringify(after.agents[id])).sort();
439
+ .filter(id => JSON.stringify(before.agents[id]) !== JSON.stringify(after.agents[id]) || JSON.stringify(before.roles) !== JSON.stringify(after.roles) || JSON.stringify(before.brains) !== JSON.stringify(after.brains)).sort();
409
440
  const watchdogScheduler = JSON.stringify(before.manifest.watchdogs) !== JSON.stringify(after.manifest.watchdogs);
410
441
  const scheduledLoops = JSON.stringify(before.manifest.loops) !== JSON.stringify(after.manifest.loops);
411
442
  const required = roles.length > 0 || watchdogScheduler || scheduledLoops;
@@ -0,0 +1,20 @@
1
+ import { type IncomingMessage } from 'node:http';
2
+ import type { WebAuth } from './auth.js';
3
+ /** Each entry exposes one dedicated backend listener. Endpoint dispatch stays upstream. */
4
+ export interface ServiceTarget {
5
+ prefix: string;
6
+ origin: string;
7
+ /** Server-side service credentials; never taken from browser headers. */
8
+ headers?: Record<string, string>;
9
+ }
10
+ export interface GatewayOptions {
11
+ /** Backend mode is for a separate loopback machine-client listener; browsers are rejected. */
12
+ auth: WebAuth | 'backend';
13
+ fleetOrigin: string;
14
+ services: ServiceTarget[];
15
+ }
16
+ /** Raw transport deliberately runs outside Fastify's JSON parser and body limits. */
17
+ export declare function createPrefixGateway(options: GatewayOptions): {
18
+ server: import("http").Server<typeof IncomingMessage, typeof import("http").ServerResponse>;
19
+ close(): Promise<void>;
20
+ };
@@ -0,0 +1,150 @@
1
+ import { createServer, request as httpRequest } from 'node:http';
2
+ import { request as httpsRequest } from 'node:https';
3
+ const hopHeaders = new Set(['connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailer', 'transfer-encoding', 'upgrade']);
4
+ function headersFor(message) {
5
+ const denied = new Set([...hopHeaders, ...(message.headers.connection ?? '').split(',').map(x => x.trim().toLowerCase())]);
6
+ return Object.fromEntries(Object.entries(message.headers).filter(([key]) => !denied.has(key)));
7
+ }
8
+ /** Raw transport deliberately runs outside Fastify's JSON parser and body limits. */
9
+ export function createPrefixGateway(options) {
10
+ const auth = options.auth === 'backend' ? undefined : options.auth;
11
+ const services = options.services.map(target => {
12
+ if (!/^\/[a-z][a-z0-9-]*$/.test(target.prefix) || target.prefix === '/fleet')
13
+ throw new Error('invalid service prefix');
14
+ const origin = new URL(target.origin);
15
+ if (!['http:', 'https:'].includes(origin.protocol) || origin.pathname !== '/' || origin.search || origin.hash || origin.username || origin.password)
16
+ throw new Error('service target must be an HTTP origin');
17
+ return { ...target, origin: origin.origin };
18
+ });
19
+ if (new Set(services.map(x => x.prefix)).size !== services.length)
20
+ throw new Error('duplicate service prefix');
21
+ const select = (req) => {
22
+ const raw = req.url ?? '/';
23
+ if (!raw.startsWith('/') || raw.startsWith('//'))
24
+ throw new Error('invalid request target');
25
+ const path = raw.split('?')[0];
26
+ const service = services.find(x => path === x.prefix || path.startsWith(x.prefix + '/'));
27
+ const fleet = path === '/fleet/api' || path.startsWith('/fleet/api/');
28
+ return { service, page: !service && !fleet && !path.startsWith('/api/'), origin: service?.origin ?? options.fleetOrigin,
29
+ path: service ? (raw.slice(service.prefix.length) || '/').replace(/^\?/, '/?') : fleet ? raw.slice('/fleet'.length) : raw };
30
+ };
31
+ const prepare = (req, upgrade = false) => {
32
+ const route = select(req);
33
+ const request = { headers: req.headers, method: req.method };
34
+ if (!auth && (req.headers.origin || req.headers['sec-fetch-site'] || !route.service))
35
+ throw new Error('machine-client listener rejects browser requests');
36
+ auth?.validateBoundary(request, upgrade, route.page);
37
+ const session = route.service && auth ? auth.authenticate(request, !upgrade && !['GET', 'HEAD', 'OPTIONS'].includes(req.method ?? 'GET')) : undefined;
38
+ const headers = headersFor(req);
39
+ // Forward only the explicit browser context, never an attacker-selected forwarding chain.
40
+ for (const key of Object.keys(headers))
41
+ if (key.startsWith('x-forwarded-') || key === 'forwarded')
42
+ delete headers[key];
43
+ if (route.service) {
44
+ if (auth)
45
+ for (const name of ['cookie', 'authorization', 'x-csrf-token', 'x-ours-api-token'])
46
+ delete headers[name];
47
+ Object.assign(headers, route.service.headers);
48
+ headers.host = new URL(route.origin).host;
49
+ }
50
+ if (upgrade) {
51
+ headers.connection = 'Upgrade';
52
+ headers.upgrade = 'websocket';
53
+ }
54
+ return { ...route, headers, session };
55
+ };
56
+ const track = (req, session, close) => {
57
+ if (!session)
58
+ return () => { };
59
+ const unbind = auth.bindTransport(session.id, close);
60
+ const timer = setInterval(() => { try {
61
+ auth.authenticate({ headers: req.headers });
62
+ }
63
+ catch {
64
+ close();
65
+ } }, 30_000);
66
+ timer.unref();
67
+ return () => { clearInterval(timer); unbind(); };
68
+ };
69
+ const server = createServer((req, res) => {
70
+ let route;
71
+ try {
72
+ route = prepare(req);
73
+ }
74
+ catch (error) {
75
+ const code = error.code;
76
+ res.writeHead(code === 'unauthorized' ? 401 : 403, { 'content-type': 'application/json', 'cache-control': 'no-store' });
77
+ res.end(JSON.stringify({ error: { code: code ?? 'forbidden', message: error.message } }));
78
+ return;
79
+ }
80
+ if (req.url === '/') {
81
+ res.writeHead(302, { location: '/fleet' });
82
+ res.end();
83
+ return;
84
+ }
85
+ const upstream = (route.origin.startsWith('https:') ? httpsRequest : httpRequest)(route.origin, {
86
+ method: req.method, path: route.path, headers: route.headers,
87
+ }, response => {
88
+ const headers = headersFor(response);
89
+ if (route.service)
90
+ delete headers['set-cookie'];
91
+ res.writeHead(response.statusCode ?? 502, headers);
92
+ response.on('error', () => res.destroy());
93
+ response.on('aborted', () => res.destroy());
94
+ response.on('close', () => { if (!response.complete)
95
+ res.destroy(); });
96
+ response.pipe(res);
97
+ res.on('close', () => response.destroy());
98
+ });
99
+ upstream.on('error', () => {
100
+ if (!res.headersSent)
101
+ res.writeHead(502, { 'content-type': 'application/json' });
102
+ res.end(JSON.stringify({ error: { code: 'upstream_unavailable', message: 'Service is unavailable' } }));
103
+ });
104
+ const untrack = track(req, route.session, () => { upstream.destroy(); res.destroy(); });
105
+ res.on('close', untrack);
106
+ req.on('aborted', () => upstream.destroy());
107
+ res.on('close', () => upstream.destroy());
108
+ req.pipe(upstream);
109
+ });
110
+ const sockets = new Set();
111
+ server.on('connection', socket => { sockets.add(socket); socket.on('close', () => sockets.delete(socket)); });
112
+ server.on('upgrade', (req, socket, head) => {
113
+ let route;
114
+ try {
115
+ route = prepare(req, true);
116
+ }
117
+ catch {
118
+ socket.end('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n');
119
+ return;
120
+ }
121
+ const upstream = (route.origin.startsWith('https:') ? httpsRequest : httpRequest)(route.origin, {
122
+ method: req.method, path: route.path, headers: route.headers,
123
+ });
124
+ upstream.on('upgrade', (response, peer, upstreamHead) => {
125
+ const headers = headersFor(response);
126
+ headers.connection = 'Upgrade';
127
+ headers.upgrade = 'websocket';
128
+ delete headers['set-cookie'];
129
+ socket.write(`HTTP/1.1 101 Switching Protocols\r\n${Object.entries(headers).flatMap(([k, v]) => (Array.isArray(v) ? v : [v]).map(value => `${k}: ${value}\r\n`)).join('')}\r\n`);
130
+ if (head.length)
131
+ peer.write(head);
132
+ if (upstreamHead.length)
133
+ socket.write(upstreamHead);
134
+ const untrack = track(req, route.session, () => { peer.destroy(); socket.destroy(); });
135
+ socket.on('close', untrack);
136
+ peer.pipe(socket);
137
+ socket.pipe(peer);
138
+ peer.on('error', () => socket.destroy());
139
+ socket.on('error', () => peer.destroy());
140
+ socket.on('close', () => peer.destroy());
141
+ peer.on('close', () => socket.destroy());
142
+ });
143
+ upstream.on('response', response => { socket.end(`HTTP/1.1 ${response.statusCode ?? 502} Rejected\r\nConnection: close\r\n\r\n`); response.destroy(); });
144
+ upstream.on('error', () => socket.destroy());
145
+ socket.on('close', () => upstream.destroy());
146
+ upstream.end();
147
+ });
148
+ return { server, async close() { for (const socket of sockets)
149
+ socket.destroy(); await new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve())); } };
150
+ }
@@ -2,6 +2,10 @@ import { type WebServer } from './server.js';
2
2
  import { type WebAccessConfig } from './access.js';
3
3
  export interface StartWebOptions {
4
4
  configPath?: string;
5
+ /** Separate console state for an independently launched gateway. */
6
+ webStateDir?: string;
7
+ control?: boolean;
8
+ staticRoot?: string;
5
9
  port?: number;
6
10
  open?: boolean;
7
11
  binPath: string;
@@ -21,6 +21,8 @@ import { FleetConfigService } from './fleet-config-service.js';
21
21
  import { mergeTopology } from './topology-model.js';
22
22
  import { TopologyDraftStore } from './topology-draft-store.js';
23
23
  import { TopologyPromoteService } from './topology-promote.js';
24
+ import { RoomLayoutDefinitions } from '../application/room-layout-definitions.js';
25
+ import { PresetProvenance } from '../application/preset-provenance.js';
24
26
  import { doctor } from '../doctor.js';
25
27
  import { acquireWebServerLock } from './lock.js';
26
28
  import { TrustedDeviceStore } from './device-store.js';
@@ -50,8 +52,8 @@ export async function startWebConsole(options) {
50
52
  const requestedPort = options.port ?? 49_271;
51
53
  if (!Number.isInteger(requestedPort) || requestedPort < 0 || requestedPort > 65_535)
52
54
  throw new FleetError('invalid_request', 'port must be between 0 and 65535');
53
- const lock = acquireWebServerLock();
54
- const webDir = resolve(stateRoot(), 'web');
55
+ const webDir = options.webStateDir ?? resolve(stateRoot(), 'web');
56
+ const lock = acquireWebServerLock(webDir);
55
57
  const bind = options.bind ?? '127.0.0.1';
56
58
  const publicOrigin = options.publicOrigin ? validatePublicOrigin(options.publicOrigin) : undefined;
57
59
  if (!isLoopback(bind) && !publicOrigin) {
@@ -159,7 +161,9 @@ export async function startWebConsole(options) {
159
161
  try {
160
162
  server = await buildWebServer({
161
163
  query, repository, logs, commands, creation, removal, audit, events, watchdogs, configuration,
162
- taskRooms: new TaskRoomApplicationService(options.configPath),
164
+ taskRooms: new TaskRoomApplicationService(options.configPath, { binPath: () => options.binPath }),
165
+ roomLayouts: new RoomLayoutDefinitions(options.configPath),
166
+ presetProvenance: new PresetProvenance(options.configPath),
163
167
  topology: readTopology, topologyDrafts, topologyPromote,
164
168
  async session(roleId) {
165
169
  const role = await repository.get(roleId);
@@ -174,7 +178,7 @@ export async function startWebConsole(options) {
174
178
  }
175
179
  throw new FleetError('capability_unavailable', 'role session backend is unavailable');
176
180
  },
177
- }, { origin: `http://127.0.0.1:${requestedPort}`, host: `127.0.0.1:${requestedPort}` }, { auth });
181
+ }, { origin: `http://127.0.0.1:${requestedPort}`, host: `127.0.0.1:${requestedPort}` }, { auth, staticRoot: options.staticRoot });
178
182
  }
179
183
  catch (error) {
180
184
  lock.release();
@@ -203,15 +207,16 @@ export async function startWebConsole(options) {
203
207
  });
204
208
  let control;
205
209
  try {
206
- control = await startWebControlServer({
207
- dir: webDir,
208
- onOpen() {
209
- const url = access.mode === 'pairing'
210
- ? `${browserOrigin}/#bootstrap=${server.auth.mintBootstrap()}` : `${browserOrigin}/`;
211
- openBrowser(url);
212
- },
213
- onRevokeAll() { server.auth.revokeAllTrustedDevices(); },
214
- });
210
+ if (options.control !== false)
211
+ control = await startWebControlServer({
212
+ dir: webDir,
213
+ onOpen() {
214
+ const url = access.mode === 'pairing'
215
+ ? `${browserOrigin}/#bootstrap=${server.auth.mintBootstrap()}` : `${browserOrigin}/`;
216
+ openBrowser(url);
217
+ },
218
+ onRevokeAll() { server.auth.revokeAllTrustedDevices(); },
219
+ });
215
220
  }
216
221
  catch (error) {
217
222
  await server.close();
@@ -225,7 +230,7 @@ export async function startWebConsole(options) {
225
230
  ...server, address: browserOrigin,
226
231
  async close() {
227
232
  try {
228
- await control.close();
233
+ await control?.close();
229
234
  await server.close();
230
235
  }
231
236
  finally {
@@ -1,3 +1,4 @@
1
+ import { SupervisorOursTools } from '../application/supervisor-ours-tools.js';
1
2
  import { type FastifyInstance } from 'fastify';
2
3
  import type { FleetQueryService } from '../application/fleet-query-service.js';
3
4
  import type { RoleRepository } from '../application/role-repository.js';
@@ -15,6 +16,8 @@ import type { TopologyDraftStore } from './topology-draft-store.js';
15
16
  import type { TopologyPromoteService } from './topology-promote.js';
16
17
  import type { RoleRemovalService } from '../application/role-removal-service.js';
17
18
  import type { TaskRoomApplicationService } from '../application/task-room-service.js';
19
+ import type { RoomLayoutDefinitions } from '../application/room-layout-definitions.js';
20
+ import type { PresetProvenance } from '../application/preset-provenance.js';
18
21
  export interface WebServices {
19
22
  query: FleetQueryService;
20
23
  repository: RoleRepository;
@@ -31,6 +34,9 @@ export interface WebServices {
31
34
  topologyPromote?: TopologyPromoteService;
32
35
  removal?: RoleRemovalService;
33
36
  taskRooms?: TaskRoomApplicationService;
37
+ roomLayouts?: RoomLayoutDefinitions;
38
+ presetProvenance?: PresetProvenance;
39
+ oursTools?: Pick<SupervisorOursTools, 'list' | 'call'>;
34
40
  }
35
41
  export interface WebServer {
36
42
  app: FastifyInstance;
@@ -44,4 +50,5 @@ export declare function buildWebServer(services: WebServices, boundary: {
44
50
  host: string;
45
51
  }, options?: {
46
52
  auth?: WebAuth;
53
+ staticRoot?: string;
47
54
  }): Promise<WebServer>;