@ours.network/fleet 1.1.0-nightly.2 → 1.1.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 (202) hide show
  1. package/README.md +204 -55
  2. package/dist/agent-recovery-gate.d.ts +17 -0
  3. package/dist/agent-recovery-gate.js +135 -0
  4. package/dist/application/capabilities.js +18 -6
  5. package/dist/application/fleet-query-service.js +20 -5
  6. package/dist/application/role-command-service.js +9 -0
  7. package/dist/application/role-creation-service.d.ts +6 -25
  8. package/dist/application/role-creation-service.js +34 -72
  9. package/dist/application/role-repository.d.ts +4 -2
  10. package/dist/application/role-repository.js +6 -3
  11. package/dist/application/session-control.d.ts +3 -1
  12. package/dist/application/session-control.js +4 -2
  13. package/dist/application/task-room-service.d.ts +58 -6
  14. package/dist/application/task-room-service.js +435 -79
  15. package/dist/briefing.js +74 -24
  16. package/dist/build-info.json +4 -4
  17. package/dist/canonical-json.d.ts +2 -0
  18. package/dist/canonical-json.js +10 -0
  19. package/dist/cli.js +241 -76
  20. package/dist/config-yaml.d.ts +1 -1
  21. package/dist/config.d.ts +75 -3
  22. package/dist/config.js +274 -34
  23. package/dist/creation.d.ts +1 -1
  24. package/dist/creation.js +3 -2
  25. package/dist/daemon-recovery.d.ts +93 -0
  26. package/dist/daemon-recovery.js +328 -0
  27. package/dist/docs.d.ts +1 -1
  28. package/dist/docs.js +192 -66
  29. package/dist/doctor.d.ts +2 -0
  30. package/dist/doctor.js +54 -4
  31. package/dist/fleet-command-audit.d.ts +165 -0
  32. package/dist/fleet-command-audit.js +555 -0
  33. package/dist/fleet-proxy.d.ts +7 -2
  34. package/dist/fleet-proxy.js +30 -8
  35. package/dist/harness/agent-session.d.ts +5 -0
  36. package/dist/harness/claude-code-session.d.ts +2 -0
  37. package/dist/harness/claude-code-session.js +6 -1
  38. package/dist/harness/claude-code.js +2 -0
  39. package/dist/harness/codex-app-server-proxy.d.ts +40 -0
  40. package/dist/harness/codex-app-server-proxy.js +348 -14
  41. package/dist/harness/codex-session.d.ts +11 -2
  42. package/dist/harness/codex-session.js +22 -2
  43. package/dist/harness/codex.d.ts +5 -1
  44. package/dist/harness/codex.js +110 -10
  45. package/dist/harness/types.d.ts +1 -1
  46. package/dist/index.d.ts +5 -4
  47. package/dist/index.js +4 -3
  48. package/dist/init-guidance.d.ts +1 -1
  49. package/dist/init-guidance.js +4 -3
  50. package/dist/init-wizard.d.ts +98 -0
  51. package/dist/init-wizard.js +629 -0
  52. package/dist/lifecycle-summary.d.ts +134 -0
  53. package/dist/lifecycle-summary.js +179 -0
  54. package/dist/loops/config.d.ts +10 -1
  55. package/dist/loops/config.js +38 -5
  56. package/dist/model-env.d.ts +2 -2
  57. package/dist/model-env.js +1 -1
  58. package/dist/model-recovery.d.ts +1 -1
  59. package/dist/ops.js +2 -17
  60. package/dist/owner-channel/channel.d.ts +58 -0
  61. package/dist/owner-channel/channel.js +321 -42
  62. package/dist/owner-channel/commands.d.ts +6 -1
  63. package/dist/owner-channel/commands.js +34 -19
  64. package/dist/owner-channel/state.d.ts +2 -0
  65. package/dist/owner-channel/state.js +8 -2
  66. package/dist/permissions.js +1 -1
  67. package/dist/preset-bootstrap.d.ts +10 -0
  68. package/dist/preset-bootstrap.js +85 -0
  69. package/dist/preset-migration.d.ts +42 -0
  70. package/dist/preset-migration.js +414 -0
  71. package/dist/reports/artifact.d.ts +10 -0
  72. package/dist/reports/artifact.js +32 -0
  73. package/dist/reports/index.d.ts +5 -0
  74. package/dist/reports/index.js +5 -0
  75. package/dist/reports/render.d.ts +6 -0
  76. package/dist/reports/render.js +240 -0
  77. package/dist/reports/service.d.ts +42 -0
  78. package/dist/reports/service.js +57 -0
  79. package/dist/reports/tasks.d.ts +52 -0
  80. package/dist/reports/tasks.js +177 -0
  81. package/dist/reports/types.d.ts +315 -0
  82. package/dist/reports/types.js +6 -0
  83. package/dist/resolved-plan.js +13 -1
  84. package/dist/rooms-tasks/cli.d.ts +2 -0
  85. package/dist/rooms-tasks/cli.js +514 -75
  86. package/dist/rooms-tasks/close.d.ts +9 -1
  87. package/dist/rooms-tasks/close.js +20 -4
  88. package/dist/rooms-tasks/config.js +16 -14
  89. package/dist/rooms-tasks/cowork-adapter.d.ts +1 -0
  90. package/dist/rooms-tasks/cowork-adapter.js +6 -0
  91. package/dist/rooms-tasks/deletion.d.ts +51 -0
  92. package/dist/rooms-tasks/deletion.js +217 -0
  93. package/dist/rooms-tasks/external-worker.d.ts +1 -0
  94. package/dist/rooms-tasks/external-worker.js +14 -9
  95. package/dist/rooms-tasks/launch-snapshot.d.ts +19 -0
  96. package/dist/rooms-tasks/launch-snapshot.js +181 -0
  97. package/dist/rooms-tasks/member-overrides.d.ts +30 -0
  98. package/dist/rooms-tasks/member-overrides.js +204 -0
  99. package/dist/rooms-tasks/provision.js +184 -49
  100. package/dist/rooms-tasks/room-state.d.ts +1 -0
  101. package/dist/rooms-tasks/room-state.js +12 -1
  102. package/dist/rooms-tasks/task-state.d.ts +115 -3
  103. package/dist/rooms-tasks/task-state.js +361 -11
  104. package/dist/rooms-tasks/templates.d.ts +4 -2
  105. package/dist/rooms-tasks/templates.js +42 -78
  106. package/dist/rooms-tasks/terminal.d.ts +3 -0
  107. package/dist/rooms-tasks/terminal.js +7 -3
  108. package/dist/rooms-tasks/types.d.ts +96 -17
  109. package/dist/rooms-tasks/types.js +14 -4
  110. package/dist/runner.d.ts +12 -2
  111. package/dist/runner.js +124 -43
  112. package/dist/session/acp.d.ts +9 -1
  113. package/dist/session/acp.js +35 -9
  114. package/dist/session/arbiter.d.ts +2 -1
  115. package/dist/session/arbiter.js +2 -0
  116. package/dist/session/codex-app-server-transport.d.ts +53 -0
  117. package/dist/session/codex-app-server-transport.js +184 -0
  118. package/dist/session/codex-app-server.d.ts +122 -0
  119. package/dist/session/codex-app-server.js +1031 -0
  120. package/dist/session/control.d.ts +14 -1
  121. package/dist/session/control.js +38 -0
  122. package/dist/session/conversation-types.d.ts +4 -4
  123. package/dist/session/types.d.ts +24 -6
  124. package/dist/session/types.js +28 -0
  125. package/dist/spawn.d.ts +15 -31
  126. package/dist/spawn.js +117 -237
  127. package/dist/temp-lifecycle.d.ts +1 -1
  128. package/dist/watchdog/config.d.ts +10 -7
  129. package/dist/watchdog/config.js +29 -19
  130. package/dist/watchdog/run.js +4 -13
  131. package/dist/web/fleet-config-service.d.ts +1 -0
  132. package/dist/web/fleet-config-service.js +27 -8
  133. package/dist/web/runtime.js +11 -6
  134. package/dist/web/server.js +28 -1
  135. package/dist/web/topology-promote.js +8 -1
  136. package/dist/web-app/assets/index-C0h3ALvs.js +10 -0
  137. package/dist/web-app/index.html +1 -1
  138. package/package.json +3 -2
  139. package/presets/brain-catalog.json +16 -0
  140. package/presets/fleet/agent_templates/Critic.yaml +4 -0
  141. package/presets/fleet/agent_templates/Developer.yaml +4 -0
  142. package/presets/fleet/agent_templates/LocalCoordinator.yaml +4 -0
  143. package/presets/fleet/agents/FleetCoordinator.yaml +3 -0
  144. package/presets/fleet/brains/claude-default.yaml +5 -0
  145. package/presets/fleet/brains/claude-fable-5-high.yaml +5 -0
  146. package/presets/fleet/brains/claude-fable-5-low.yaml +5 -0
  147. package/presets/fleet/brains/claude-fable-5-max.yaml +5 -0
  148. package/presets/fleet/brains/claude-fable-5-medium.yaml +5 -0
  149. package/presets/fleet/brains/claude-fable-5-xhigh.yaml +5 -0
  150. package/presets/fleet/brains/claude-opus-5-high.yaml +5 -0
  151. package/presets/fleet/brains/claude-opus-5-low.yaml +5 -0
  152. package/presets/fleet/brains/claude-opus-5-max.yaml +5 -0
  153. package/presets/fleet/brains/claude-opus-5-medium.yaml +5 -0
  154. package/presets/fleet/brains/claude-opus-5-xhigh.yaml +5 -0
  155. package/presets/fleet/brains/claude-sonnet-5-high.yaml +5 -0
  156. package/presets/fleet/brains/claude-sonnet-5-low.yaml +5 -0
  157. package/presets/fleet/brains/claude-sonnet-5-max.yaml +5 -0
  158. package/presets/fleet/brains/claude-sonnet-5-medium.yaml +5 -0
  159. package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +5 -0
  160. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +5 -0
  161. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +5 -0
  162. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +5 -0
  163. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +5 -0
  164. package/presets/fleet/brains/codex-gpt-5-4-high.yaml +5 -0
  165. package/presets/fleet/brains/codex-gpt-5-4-low.yaml +5 -0
  166. package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +5 -0
  167. package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +5 -0
  168. package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +5 -0
  169. package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +5 -0
  170. package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +5 -0
  171. package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +5 -0
  172. package/presets/fleet/brains/codex-gpt-5-5-high.yaml +5 -0
  173. package/presets/fleet/brains/codex-gpt-5-5-low.yaml +5 -0
  174. package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +5 -0
  175. package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +5 -0
  176. package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +5 -0
  177. package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +5 -0
  178. package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +5 -0
  179. package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +5 -0
  180. package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +5 -0
  181. package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +5 -0
  182. package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +5 -0
  183. package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +5 -0
  184. package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +5 -0
  185. package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +5 -0
  186. package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +5 -0
  187. package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +5 -0
  188. package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +5 -0
  189. package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +5 -0
  190. package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +5 -0
  191. package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +5 -0
  192. package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +5 -0
  193. package/presets/fleet/roles/Coordinator.yaml +30 -0
  194. package/presets/fleet/roles/Critic.yaml +19 -0
  195. package/presets/fleet/roles/Developer.yaml +19 -0
  196. package/presets/fleet/roles/LocalCoordinator.yaml +25 -0
  197. package/presets/fleet/room_templates/pair.yaml +10 -0
  198. package/presets/fleet/room_templates/single.yaml +7 -0
  199. package/presets/fleet/room_templates/team.yaml +12 -0
  200. package/presets/fleet.yaml +7 -0
  201. package/presets/manifest.json +6 -0
  202. package/dist/web-app/assets/index-BbE9EX6n.js +0 -10
@@ -0,0 +1,181 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, unlinkSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { replaceFileAtomically } from '../atomic-file.js';
5
+ import { stateRoot } from '../paths.js';
6
+ import { canonicalJson } from '../canonical-json.js';
7
+ import { randomUUID } from 'node:crypto';
8
+ const launchSnapshotDir = () => join(stateRoot(), 'launch-snapshots');
9
+ const launchSnapshotPath = (hash) => join(launchSnapshotDir(), `${hash}.json`);
10
+ const launchSnapshotLock = () => join(stateRoot(), '.launch-snapshot.lock');
11
+ function assertPrivateDirectory(path) {
12
+ const stat = lstatSync(path);
13
+ const uid = process.getuid?.();
14
+ if (!stat.isDirectory() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0
15
+ || (uid !== undefined && stat.uid !== uid))
16
+ throw new Error('launch snapshot directory has unsafe ownership or permissions');
17
+ }
18
+ /** Cross-process exclusion used from seal through durable Room publication. */
19
+ export function acquireLaunchSnapshotLock(options = {}) {
20
+ mkdirSync(stateRoot(), { recursive: true, mode: 0o700 });
21
+ const path = launchSnapshotLock();
22
+ const token = randomUUID();
23
+ let reclaimed = false;
24
+ for (;;) {
25
+ const claim = `${path}.claim.${process.pid}.${randomUUID()}`;
26
+ try {
27
+ mkdirSync(claim, { mode: 0o700 });
28
+ writeFileSync(join(claim, 'owner.json'), JSON.stringify({ pid: process.pid, token }), { mode: 0o600 });
29
+ options.beforePublish?.(claim, path);
30
+ renameSync(claim, path);
31
+ if (reclaimed)
32
+ sweepUnreferencedSnapshotsLocked();
33
+ return () => {
34
+ try {
35
+ const owner = JSON.parse(readFileSync(join(path, 'owner.json'), 'utf8'));
36
+ if (owner.token === token)
37
+ rmSync(path, { recursive: true, force: true });
38
+ }
39
+ catch { /* ownership cannot be proved; never remove a successor */ }
40
+ };
41
+ }
42
+ catch (error) {
43
+ rmSync(claim, { recursive: true, force: true });
44
+ if (!['EEXIST', 'ENOTEMPTY'].includes(error.code ?? ''))
45
+ throw error;
46
+ let alive = true;
47
+ try {
48
+ const owner = JSON.parse(readFileSync(join(path, 'owner.json'), 'utf8'));
49
+ if (typeof owner.pid !== 'number')
50
+ alive = false;
51
+ else
52
+ try {
53
+ process.kill(owner.pid, 0);
54
+ }
55
+ catch {
56
+ alive = false;
57
+ }
58
+ }
59
+ catch {
60
+ alive = false;
61
+ }
62
+ if (!alive) {
63
+ rmSync(path, { recursive: true, force: true });
64
+ reclaimed = true;
65
+ continue;
66
+ }
67
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
68
+ }
69
+ }
70
+ }
71
+ function referencedByRetainedState(hash, excludeTaskFile) {
72
+ for (const directory of ['tasks', 'rooms']) {
73
+ const root = join(stateRoot(), directory);
74
+ if (!existsSync(root))
75
+ continue;
76
+ for (const name of readdirSync(root).filter(entry => entry.endsWith('.json'))) {
77
+ if (directory === 'tasks' && excludeTaskFile !== undefined && name === excludeTaskFile)
78
+ continue;
79
+ try {
80
+ if (readFileSync(join(root, name), 'utf8').includes(hash))
81
+ return true;
82
+ }
83
+ catch {
84
+ return true;
85
+ }
86
+ }
87
+ }
88
+ return false;
89
+ }
90
+ function sweepUnreferencedSnapshotsLocked() {
91
+ const dir = launchSnapshotDir();
92
+ if (!existsSync(dir))
93
+ return;
94
+ assertPrivateDirectory(dir);
95
+ for (const name of readdirSync(dir).filter(entry => /^[a-f0-9]{64}\.json$/.test(entry))) {
96
+ const hash = name.slice(0, -5);
97
+ if (!referencedByRetainedState(hash))
98
+ unlinkSync(join(dir, name));
99
+ }
100
+ }
101
+ export function redactLaunchDefinition(value, key = '') {
102
+ if (key === 'prompt' && typeof value === 'string')
103
+ return {
104
+ bytes: Buffer.byteLength(value, 'utf8'),
105
+ sha256: createHash('sha256').update(value).digest('hex'),
106
+ };
107
+ if (['env', 'harness_options', 'session_options', 'owner_channel', 'auth_proxy'].includes(key))
108
+ return '<redacted>';
109
+ if (Array.isArray(value))
110
+ return value.map(item => redactLaunchDefinition(item));
111
+ if (value && typeof value === 'object')
112
+ return Object.fromEntries(Object.entries(value)
113
+ .map(([child, item]) => [child, redactLaunchDefinition(item, child)]));
114
+ return value;
115
+ }
116
+ export function sealLaunchSnapshot(definitions) {
117
+ const payload = { schema_version: 1, definitions: structuredClone(definitions) };
118
+ const bytes = `${canonicalJson(payload)}\n`;
119
+ const hash = createHash('sha256').update(bytes).digest('hex');
120
+ const path = launchSnapshotPath(hash);
121
+ const dir = launchSnapshotDir();
122
+ if (!existsSync(dir))
123
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
124
+ assertPrivateDirectory(dir);
125
+ if (!existsSync(path))
126
+ replaceFileAtomically(path, bytes, 0o600);
127
+ else
128
+ readLaunchSnapshot(hash);
129
+ return hash;
130
+ }
131
+ export function readLaunchSnapshot(hash) {
132
+ if (!/^[a-f0-9]{64}$/.test(hash))
133
+ throw new Error('invalid launch snapshot hash');
134
+ const dir = launchSnapshotDir();
135
+ const path = launchSnapshotPath(hash);
136
+ const dirStat = lstatSync(dir);
137
+ const fileStat = lstatSync(path);
138
+ const uid = process.getuid?.();
139
+ if (!dirStat.isDirectory() || dirStat.isSymbolicLink() || (dirStat.mode & 0o077) !== 0
140
+ || (uid !== undefined && dirStat.uid !== uid))
141
+ throw new Error('launch snapshot directory has unsafe ownership or permissions');
142
+ if (!fileStat.isFile() || fileStat.isSymbolicLink() || (fileStat.mode & 0o077) !== 0
143
+ || (uid !== undefined && fileStat.uid !== uid))
144
+ throw new Error(`launch snapshot ${hash} has unsafe ownership or permissions`);
145
+ const bytes = readFileSync(path, 'utf8');
146
+ if (createHash('sha256').update(bytes).digest('hex') !== hash)
147
+ throw new Error(`launch snapshot ${hash} failed integrity verification`);
148
+ const parsed = JSON.parse(bytes);
149
+ if (parsed.schema_version !== 1 || !parsed.definitions || typeof parsed.definitions !== 'object')
150
+ throw new Error(`launch snapshot ${hash} has unsupported schema`);
151
+ return parsed.definitions;
152
+ }
153
+ /** Delete a sealed snapshot only after no retained Task/Room record references it. */
154
+ export function releaseLaunchSnapshot(hash) {
155
+ const release = acquireLaunchSnapshotLock();
156
+ try {
157
+ if (referencedByRetainedState(hash))
158
+ return;
159
+ const path = launchSnapshotPath(hash);
160
+ if (existsSync(path))
161
+ unlinkSync(path);
162
+ }
163
+ finally {
164
+ release();
165
+ }
166
+ }
167
+ /**
168
+ * Deletion-finalization variant: the caller already holds the global
169
+ * launch-snapshot lock and is about to unlink the deletion-pending task
170
+ * `taskId`. The reference scan excludes exactly that task file — every other
171
+ * retained Task/Room reference still protects the snapshot. Deleting the
172
+ * snapshot BEFORE the task unlink keeps every crash seam recoverable: the
173
+ * hidden intent survives, and a retry sees the snapshot already absent.
174
+ */
175
+ export function releaseLaunchSnapshotForDeletingTask(hash, taskId) {
176
+ if (referencedByRetainedState(hash, `${taskId}.json`))
177
+ return;
178
+ const path = launchSnapshotPath(hash);
179
+ if (existsSync(path))
180
+ unlinkSync(path);
181
+ }
@@ -0,0 +1,30 @@
1
+ import { type AgentDefinition, type AgentTemplateDefinition, type FleetConfig } from '../config.js';
2
+ import type { TemplateDefinition, TemplateSnapshot } from './types.js';
3
+ import type { AgentLoopsConfig } from '../loops/config.js';
4
+ export interface MemberOverride {
5
+ agent_template?: string;
6
+ brain?: string;
7
+ role?: string;
8
+ approval?: 'ask' | 'auto' | 'allow';
9
+ filesystem?: 'read-only' | 'workspace' | 'unrestricted';
10
+ unattended?: 'deny' | 'wait';
11
+ cwd?: string;
12
+ model?: string;
13
+ effort?: string;
14
+ /** Whole-block override; false is the explicit no-loops policy. */
15
+ loops?: AgentLoopsConfig | false;
16
+ overrides?: Partial<AgentDefinition>;
17
+ }
18
+ export type MemberOverrides = Record<string, MemberOverride>;
19
+ export declare function hashMemberOverrides(overrides: MemberOverrides): string;
20
+ /** Parse only the ordered member-option subsequence supplied by the CLI action. */
21
+ export declare function parseGroupedMemberArgs(argv: string[]): MemberOverrides;
22
+ export declare function readMembersFile(path: string): MemberOverrides;
23
+ export interface PreparedExecutionPlan {
24
+ snapshot: TemplateSnapshot;
25
+ launchDefinitions: Record<string, AgentTemplateDefinition>;
26
+ overrides: MemberOverrides;
27
+ planHash: string;
28
+ overridesHash: string;
29
+ }
30
+ export declare function prepareExecutionPlan(template: TemplateDefinition, cfg: FleetConfig, overrides?: MemberOverrides): PreparedExecutionPlan;
@@ -0,0 +1,204 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { lstatSync, readFileSync } from 'node:fs';
3
+ import { parseFleetDocument } from '../config-yaml.js';
4
+ import { validateEffectiveAgentTemplate } from '../config.js';
5
+ import { analyzeRolePermissions } from '../permissions.js';
6
+ import { canonicalJson } from '../canonical-json.js';
7
+ import { redactLaunchDefinition } from './launch-snapshot.js';
8
+ import { snapshotTemplate } from './templates.js';
9
+ import { readLoopsFile } from '../spawn.js';
10
+ import { canonicalAgentLoops, resolveAgentLoops } from '../loops/config.js';
11
+ export function hashMemberOverrides(overrides) {
12
+ return createHash('sha256').update(canonicalJson(overrides)).digest('hex');
13
+ }
14
+ const OPTION_FIELDS = {
15
+ '--agent-template': 'agent_template', '--brain': 'brain', '--role': 'role',
16
+ '--approval': 'approval', '--filesystem': 'filesystem',
17
+ '--unattended': 'unattended', '--cwd': 'cwd', '--model': 'model', '--effort': 'effort',
18
+ };
19
+ /** Parse only the ordered member-option subsequence supplied by the CLI action. */
20
+ export function parseGroupedMemberArgs(argv) {
21
+ const result = {};
22
+ let current;
23
+ for (let index = 0; index < argv.length;) {
24
+ const flag = argv[index];
25
+ if (flag === '--no-loops') {
26
+ if (!current)
27
+ throw new Error(`${flag} must follow --member <slot>`);
28
+ if (result[current].loops !== undefined)
29
+ throw new Error(`duplicate loop override for member '${current}'`);
30
+ result[current].loops = false;
31
+ index += 1;
32
+ continue;
33
+ }
34
+ const value = argv[index + 1];
35
+ if (!value || value.startsWith('--'))
36
+ throw new Error(`${flag}: value required`);
37
+ if (flag === '--member') {
38
+ if (result[value])
39
+ throw new Error(`duplicate member slot '${value}'`);
40
+ current = value;
41
+ result[current] = {};
42
+ index += 2;
43
+ continue;
44
+ }
45
+ if (flag === '--loops-file') {
46
+ if (!current)
47
+ throw new Error(`${flag} must follow --member <slot>`);
48
+ if (result[current].loops !== undefined)
49
+ throw new Error(`duplicate loop override for member '${current}'`);
50
+ result[current].loops = readLoopsFile(value);
51
+ index += 2;
52
+ continue;
53
+ }
54
+ const field = OPTION_FIELDS[flag];
55
+ if (!field)
56
+ throw new Error(`unknown member option '${flag}'`);
57
+ if (!current)
58
+ throw new Error(`${flag} must follow --member <slot>`);
59
+ if (result[current][field] !== undefined)
60
+ throw new Error(`duplicate ${flag} for member '${current}'`);
61
+ result[current][field] = value;
62
+ index += 2;
63
+ }
64
+ for (const [slot, value] of Object.entries(result))
65
+ validateMemberOverride(value, `member '${slot}'`);
66
+ return result;
67
+ }
68
+ export function readMembersFile(path) {
69
+ const stat = lstatSync(path);
70
+ const uid = process.getuid?.();
71
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1_000_000
72
+ || (uid !== undefined && stat.uid !== uid) || (stat.mode & 0o077) !== 0)
73
+ throw new Error(`${path}: members file must be a trusted regular file no larger than 1 MB`);
74
+ const raw = parseFleetDocument(path, readFileSync(path, 'utf8'), 'strict').value;
75
+ if (!raw.members || typeof raw.members !== 'object' || Array.isArray(raw.members)
76
+ || Object.keys(raw).some(key => key !== 'members'))
77
+ throw new Error(`${path}: expected exactly a members: mapping`);
78
+ const members = structuredClone(raw.members);
79
+ for (const [slot, value] of Object.entries(members))
80
+ validateMemberOverride(value, `${path}: member '${slot}'`);
81
+ return members;
82
+ }
83
+ const MEMBER_KEYS = new Set(['agent_template', 'brain', 'role', 'approval', 'filesystem', 'unattended', 'cwd', 'model', 'effort', 'overrides', 'loops']);
84
+ function validateMemberOverride(value, where) {
85
+ if (!value || typeof value !== 'object' || Array.isArray(value))
86
+ throw new Error(`${where}: must be a mapping`);
87
+ const item = value;
88
+ const unknown = Object.keys(item).filter(key => !MEMBER_KEYS.has(key));
89
+ if (unknown.length)
90
+ throw new Error(`${where}: unknown key '${unknown[0]}'`);
91
+ const allowed = {
92
+ approval: ['ask', 'auto', 'allow'], filesystem: ['read-only', 'workspace', 'unrestricted'],
93
+ unattended: ['deny', 'wait'],
94
+ };
95
+ for (const [key, choices] of Object.entries(allowed))
96
+ if (item[key] !== undefined && !choices.includes(item[key]))
97
+ throw new Error(`${where}: ${key} must be one of ${choices.join(', ')}`);
98
+ for (const key of ['agent_template', 'brain', 'role', 'cwd', 'model', 'effort'])
99
+ if (item[key] !== undefined
100
+ && (typeof item[key] !== 'string' || !item[key].trim()))
101
+ throw new Error(`${where}: ${key} must be a non-blank string`);
102
+ if (item.overrides !== undefined && (!item.overrides || typeof item.overrides !== 'object' || Array.isArray(item.overrides)))
103
+ throw new Error(`${where}: overrides must be a mapping`);
104
+ if (item.overrides && Object.hasOwn(item.overrides, 'loops'))
105
+ throw new Error(`${where}: overrides.loops is unsupported; use the top-level loops member override`);
106
+ if (item.loops !== undefined && item.loops !== false
107
+ && (!item.loops || typeof item.loops !== 'object' || Array.isArray(item.loops)))
108
+ throw new Error(`${where}: loops must be a mapping or false`);
109
+ }
110
+ const MAPS = new Set(['permissions', 'env', 'isolation', 'monitor', 'owner_channel', 'worklog', 'auth_proxy']);
111
+ function merge(base, overlay) {
112
+ const out = structuredClone(base);
113
+ for (const [key, value] of Object.entries(overlay)) {
114
+ if (value === null)
115
+ throw new Error(`member override ${key}: null deletion is not supported`);
116
+ out[key] = MAPS.has(key) && out[key] && typeof out[key] === 'object' && !Array.isArray(out[key])
117
+ && typeof value === 'object' && !Array.isArray(value)
118
+ ? merge(out[key], value) : structuredClone(value);
119
+ }
120
+ return out;
121
+ }
122
+ export function prepareExecutionPlan(template, cfg, overrides = {}) {
123
+ for (const [slot, value] of Object.entries(overrides))
124
+ validateMemberOverride(value, `member '${slot}'`);
125
+ const slots = new Set(template.members.map(member => member.slot));
126
+ for (const slot of Object.keys(overrides))
127
+ if (!slots.has(slot))
128
+ throw new Error(`unknown member slot '${slot}'`);
129
+ const snapshot = snapshotTemplate(template, cfg.agentTemplates);
130
+ const launchDefinitions = {};
131
+ snapshot.members = snapshot.members.map(member => {
132
+ const input = overrides[member.slot] ?? {};
133
+ const source = input.agent_template ?? member.agent_template;
134
+ const base = cfg.agentTemplates?.[source];
135
+ if (!base)
136
+ throw new Error(`Agent Template '${source}' not found`);
137
+ let definition = merge(base, (input.overrides ?? {}));
138
+ if (input.loops === false)
139
+ delete definition.loops;
140
+ else if (input.loops !== undefined)
141
+ definition.loops = structuredClone(input.loops);
142
+ const rolePreset = input.role ? cfg.rolePresets?.[input.role] : undefined;
143
+ if (input.role && !rolePreset)
144
+ throw new Error(`Role '${input.role}' not found`);
145
+ const brainPreset = input.brain ? cfg.brainPresets?.[input.brain] : undefined;
146
+ if (input.brain && !brainPreset)
147
+ throw new Error(`Brain '${input.brain}' not found`);
148
+ if (rolePreset)
149
+ definition.role = { inline: structuredClone(rolePreset) };
150
+ if (brainPreset)
151
+ definition.brain = { inline: structuredClone(brainPreset) };
152
+ const permissions = { ...(definition.permissions ?? {}) };
153
+ if (input.approval)
154
+ permissions.approval = input.approval;
155
+ if (input.filesystem)
156
+ permissions.filesystem = input.filesystem;
157
+ if (input.unattended)
158
+ permissions.unattended = input.unattended;
159
+ if (Object.keys(permissions).length)
160
+ definition.permissions = permissions;
161
+ if (input.cwd)
162
+ definition.cwd = input.cwd;
163
+ if (input.model || input.effort) {
164
+ const brain = 'inline' in definition.brain ? structuredClone(definition.brain.inline) : {};
165
+ if (input.model)
166
+ brain.model = input.model;
167
+ if (input.effort)
168
+ brain.effort = input.effort;
169
+ definition.brain = { inline: brain };
170
+ }
171
+ definition = validateEffectiveAgentTemplate(definition, member.slot);
172
+ {
173
+ if (!cfg.resolveAgentDefinition)
174
+ throw new Error('configuration has no effective Agent resolver');
175
+ const role = cfg.resolveAgentDefinition(`RoomMember_${member.slot}`, {
176
+ ...definition, identity: `RoomMember_${member.slot}`,
177
+ });
178
+ if (definition.loops !== undefined)
179
+ definition.loops = canonicalAgentLoops(resolveAgentLoops(definition.loops, role, `(member '${member.slot}' resolved loops)`));
180
+ const analysis = analyzeRolePermissions(role);
181
+ if (analysis.conflicts?.length)
182
+ throw new Error(`member '${member.slot}' has native permission conflicts: ${analysis.conflicts.map(item => item.warning).join('; ')}`);
183
+ if (analysis.floor && !analysis.floor.meets && analysis.floorSeverity === 'fail')
184
+ throw new Error(`member '${member.slot}' fails unattended floor: ${analysis.floor.missing.join(', ')}`);
185
+ }
186
+ const id = `${member.slot}:${source}`;
187
+ launchDefinitions[id] = definition;
188
+ const hash = createHash('sha256').update(canonicalJson(definition)).digest('hex');
189
+ return { ...member, agent_template: source, launch_definition_id: id,
190
+ loop_source: input.loops !== undefined ? 'cli' : base.loops !== undefined ? 'agent-template' : 'omitted',
191
+ agent_template_hash: hash,
192
+ ...(input.role ? { role_preset: { id: input.role, hash: createHash('sha256').update(canonicalJson(rolePreset)).digest('hex') } } : {}),
193
+ ...(input.brain ? { brain_preset: { id: input.brain, hash: createHash('sha256').update(canonicalJson(brainPreset)).digest('hex') } } : {}),
194
+ agent_projection: redactLaunchDefinition(definition) };
195
+ });
196
+ const planHash = createHash('sha256').update(canonicalJson({
197
+ template: snapshot.content_hash, members: snapshot.members.map(member => ({
198
+ slot: member.slot, source: member.agent_template, hash: member.agent_template_hash,
199
+ })), overrides,
200
+ })).digest('hex');
201
+ snapshot.content_hash = planHash;
202
+ return { snapshot, launchDefinitions, overridesHash: hashMemberOverrides(overrides),
203
+ overrides: redactLaunchDefinition(overrides), planHash };
204
+ }