@ours.network/fleet 1.0.5 → 1.1.0-nightly.10

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 (159) hide show
  1. package/README.md +122 -133
  2. package/dist/agent-recovery-gate.d.ts +17 -0
  3. package/dist/agent-recovery-gate.js +135 -0
  4. package/dist/application/capabilities.d.ts +0 -1
  5. package/dist/application/capabilities.js +3 -13
  6. package/dist/application/fleet-query-service.d.ts +0 -3
  7. package/dist/application/fleet-query-service.js +14 -25
  8. package/dist/application/role-creation-service.d.ts +5 -24
  9. package/dist/application/role-creation-service.js +27 -67
  10. package/dist/application/role-removal-service.js +6 -3
  11. package/dist/application/role-repository.d.ts +1 -2
  12. package/dist/application/role-repository.js +7 -7
  13. package/dist/application/session-control.d.ts +0 -12
  14. package/dist/application/session-control.js +0 -42
  15. package/dist/application/session-mutations.d.ts +5 -5
  16. package/dist/application/task-room-service.d.ts +1 -1
  17. package/dist/application/task-room-service.js +154 -16
  18. package/dist/application/types.d.ts +1 -8
  19. package/dist/briefing.js +12 -11
  20. package/dist/build-info.json +5 -5
  21. package/dist/cli.js +143 -63
  22. package/dist/config-yaml.d.ts +1 -1
  23. package/dist/config.d.ts +66 -6
  24. package/dist/config.js +398 -43
  25. package/dist/creation.d.ts +1 -1
  26. package/dist/creation.js +2 -2
  27. package/dist/daemon-recovery.d.ts +93 -0
  28. package/dist/daemon-recovery.js +328 -0
  29. package/dist/docs.d.ts +1 -1
  30. package/dist/docs.js +103 -127
  31. package/dist/doctor.js +18 -11
  32. package/dist/fleet-command-audit.d.ts +160 -0
  33. package/dist/fleet-command-audit.js +433 -0
  34. package/dist/fleet-proxy.d.ts +3 -1
  35. package/dist/fleet-proxy.js +30 -8
  36. package/dist/generated-agent-source.d.ts +9 -0
  37. package/dist/generated-agent-source.js +53 -0
  38. package/dist/harness/acp-session-transport.d.ts +8 -0
  39. package/dist/harness/acp-session-transport.js +3 -0
  40. package/dist/harness/agent-session.d.ts +51 -0
  41. package/dist/harness/agent-session.js +1 -0
  42. package/dist/harness/claude-code-session.d.ts +24 -0
  43. package/dist/harness/claude-code-session.js +36 -0
  44. package/dist/harness/claude-code.d.ts +2 -1
  45. package/dist/harness/claude-code.js +41 -81
  46. package/dist/harness/codex-session.d.ts +24 -0
  47. package/dist/harness/codex-session.js +36 -0
  48. package/dist/harness/codex.d.ts +2 -1
  49. package/dist/harness/codex.js +55 -45
  50. package/dist/harness/registry.js +4 -0
  51. package/dist/harness/types.d.ts +6 -38
  52. package/dist/index.d.ts +4 -6
  53. package/dist/index.js +2 -4
  54. package/dist/init-guidance.d.ts +1 -0
  55. package/dist/init-guidance.js +6 -0
  56. package/dist/isolation/resources.d.ts +3 -7
  57. package/dist/isolation/resources.js +3 -7
  58. package/dist/model-env.d.ts +4 -6
  59. package/dist/model-env.js +5 -8
  60. package/dist/monitor.d.ts +1 -24
  61. package/dist/monitor.js +5 -140
  62. package/dist/ops.js +4 -2
  63. package/dist/owner-channel/channel.d.ts +58 -2
  64. package/dist/owner-channel/channel.js +269 -29
  65. package/dist/owner-channel/commands.d.ts +2 -0
  66. package/dist/owner-channel/commands.js +31 -13
  67. package/dist/owner-channel/state.d.ts +2 -0
  68. package/dist/owner-channel/state.js +8 -2
  69. package/dist/preset-bootstrap.d.ts +10 -0
  70. package/dist/preset-bootstrap.js +85 -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.d.ts +3 -2
  84. package/dist/resolved-plan.js +21 -5
  85. package/dist/rooms-tasks/cli.js +225 -40
  86. package/dist/rooms-tasks/config.js +24 -13
  87. package/dist/rooms-tasks/external-worker.d.ts +1 -0
  88. package/dist/rooms-tasks/external-worker.js +14 -9
  89. package/dist/rooms-tasks/provision.js +98 -36
  90. package/dist/rooms-tasks/templates.d.ts +0 -1
  91. package/dist/rooms-tasks/templates.js +9 -76
  92. package/dist/rooms-tasks/types.d.ts +10 -16
  93. package/dist/rooms-tasks/types.js +1 -4
  94. package/dist/runner.d.ts +13 -16
  95. package/dist/runner.js +110 -122
  96. package/dist/sensitive-config.d.ts +2 -0
  97. package/dist/sensitive-config.js +4 -0
  98. package/dist/session/acp.d.ts +7 -2
  99. package/dist/session/acp.js +29 -6
  100. package/dist/session/activity.d.ts +2 -3
  101. package/dist/session/activity.js +2 -3
  102. package/dist/session/arbiter.d.ts +4 -4
  103. package/dist/session/control.d.ts +17 -4
  104. package/dist/session/control.js +35 -0
  105. package/dist/session/types.d.ts +11 -5
  106. package/dist/spawn.d.ts +8 -33
  107. package/dist/spawn.js +53 -213
  108. package/dist/supervisor/none.d.ts +1 -1
  109. package/dist/supervisor/none.js +2 -2
  110. package/dist/temp-lifecycle.d.ts +1 -1
  111. package/dist/tmux.d.ts +1 -14
  112. package/dist/tmux.js +1 -51
  113. package/dist/watchdog/config.d.ts +10 -7
  114. package/dist/watchdog/config.js +30 -18
  115. package/dist/watchdog/run.js +12 -36
  116. package/dist/web/auth.d.ts +1 -1
  117. package/dist/web/fleet-config-service.d.ts +11 -8
  118. package/dist/web/fleet-config-service.js +384 -183
  119. package/dist/web/runtime.js +4 -15
  120. package/dist/web/server.d.ts +1 -3
  121. package/dist/web/server.js +3 -14
  122. package/dist/web/topology-promote.js +26 -19
  123. package/dist/web/topology.js +2 -2
  124. package/dist/web/yaml-document-edit.js +2 -0
  125. package/dist/web-app/assets/index-59GF-gsZ.css +1 -0
  126. package/dist/web-app/assets/index-BbTc5KtZ.js +10 -0
  127. package/dist/web-app/index.html +2 -2
  128. package/examples/fleet/agents/Alice.yaml +11 -0
  129. package/examples/fleet/agents/FleetCoordinator.yaml +7 -0
  130. package/examples/fleet/brains/claude-default.yaml +5 -0
  131. package/examples/fleet/roles/coordinator.yaml +5 -0
  132. package/examples/fleet/roles/developer.yaml +3 -0
  133. package/examples/fleet.yaml +74 -0
  134. package/package.json +5 -8
  135. package/presets/fleet/agents/Agent.yaml +3 -0
  136. package/presets/fleet/agents/Architect.yaml +3 -0
  137. package/presets/fleet/agents/Critic.yaml +3 -0
  138. package/presets/fleet/agents/Developer.yaml +3 -0
  139. package/presets/fleet/agents/Secretary.yaml +3 -0
  140. package/presets/fleet/agents/Tester.yaml +3 -0
  141. package/presets/fleet/brains/claude-default.yaml +5 -0
  142. package/presets/fleet/roles/Agent.yaml +6 -0
  143. package/presets/fleet/roles/Architect.yaml +6 -0
  144. package/presets/fleet/roles/Critic.yaml +6 -0
  145. package/presets/fleet/roles/Developer.yaml +6 -0
  146. package/presets/fleet/roles/Secretary.yaml +6 -0
  147. package/presets/fleet/roles/Tester.yaml +6 -0
  148. package/presets/fleet/room_templates/pair.yaml +11 -0
  149. package/presets/fleet/room_templates/single.yaml +8 -0
  150. package/presets/fleet/room_templates/team.yaml +12 -0
  151. package/presets/fleet.yaml +7 -0
  152. package/presets/manifest.json +5 -0
  153. package/dist/session/tmux.d.ts +0 -28
  154. package/dist/session/tmux.js +0 -80
  155. package/dist/web/terminal/bridge.d.ts +0 -27
  156. package/dist/web/terminal/bridge.js +0 -317
  157. package/dist/web-app/assets/TerminalView-C_G1ID2P.js +0 -9
  158. package/dist/web-app/assets/index-BCBK78hw.js +0 -10
  159. package/dist/web-app/assets/index-DuC-xnX4.css +0 -1
@@ -0,0 +1,433 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { replaceFileAtomically } from './atomic-file.js';
4
+ import { isSensitiveConfigKey } from './sensitive-config.js';
5
+ export class FleetCliExit extends Error {
6
+ exitCode;
7
+ outcomeClass;
8
+ effect;
9
+ constructor(exitCode, outcomeClass = 'runtime', effect = 'unknown') {
10
+ super(`fleet CLI exited ${exitCode}`);
11
+ this.exitCode = exitCode;
12
+ this.outcomeClass = outcomeClass;
13
+ this.effect = effect;
14
+ }
15
+ }
16
+ /** Commands are Owner-silent unless they record one of these semantic lifecycle kinds. */
17
+ export const FLEET_LIFECYCLE_EVENT_KINDS = Object.freeze([
18
+ 'agent_started', 'task', 'room', 'lifecycle_failure',
19
+ ]);
20
+ /** Stable semantic dedupe basis: resource kind + stable resource ID + transition/action ID. */
21
+ export function lifecycleEventDigestBasis(value) {
22
+ const resourceKind = value.kind === 'lifecycle_failure' ? value.resource : value.kind;
23
+ return `${resourceKind}\0${value.id}\0${value.eventId}`;
24
+ }
25
+ let collection;
26
+ export function beginFleetAuditCollection() { collection = { resourceIds: {}, presentations: [] }; }
27
+ export function recordFleetAuditResource(kind, id) {
28
+ if (id && collection)
29
+ collection.resourceIds[kind] = id;
30
+ }
31
+ export function recordFleetAuditPresentation(value) {
32
+ if (collection && FLEET_LIFECYCLE_EVENT_KINDS.includes(value.kind)) {
33
+ const basis = value.eventId ?? (value.kind === 'agent_started' ? value.actionId
34
+ : value.kind === 'task' || value.kind === 'room'
35
+ ? `${value.operation}:${value.revision ?? `${value.previousState ?? 'none'}:${value.newState}`}`
36
+ : `${value.category}:${value.state}`);
37
+ collection.presentations.push(structuredClone({ ...value, eventId: basis }));
38
+ if (value.kind === 'task')
39
+ collection.resourceIds.task = value.id;
40
+ if (value.kind === 'room')
41
+ collection.resourceIds.room = value.id;
42
+ if (value.kind === 'agent_started')
43
+ collection.resourceIds.agent = value.id;
44
+ }
45
+ }
46
+ export function recordFleetAuditFailure(failure) { if (collection)
47
+ collection.failure = failure; }
48
+ export function consumeFleetAuditCollection() {
49
+ const current = collection;
50
+ collection = undefined;
51
+ return {
52
+ ...(current && Object.keys(current.resourceIds).length ? { resourceIds: current.resourceIds } : {}),
53
+ ...(current?.presentations.length ? { presentations: current.presentations } : {}),
54
+ ...(current?.failure ? { failure: current.failure } : {}),
55
+ };
56
+ }
57
+ const SAFE_READ = new Set(['docs', 'version', 'config', 'ls', 'peek', 'logs', 'status', 'doctor']);
58
+ const AGENT_SURFACES = {
59
+ spawn: new Set(['<none>']),
60
+ template: new Set(['list', 'show', 'validate']),
61
+ task: new Set(['create', 'list', 'lists', 'list-create', 'list-rename', 'list-delete', 'move',
62
+ 'show', 'start', 'block', 'unblock', 'review', 'done', 'cancel', 'delete', 'recover', 'work', 'finish']),
63
+ room: new Set(['create', 'list', 'show', 'open', 'members', 'delete', 'close', 'recover']),
64
+ };
65
+ export const fleetProxyCommandInventory = Object.freeze(Object.fromEntries(Object.entries(AGENT_SURFACES).map(([surface, commands]) => [surface, [...commands]])));
66
+ export const fleetProxyTopLevelInventory = Object.freeze({
67
+ safeRead: [...SAFE_READ], agent: Object.keys(AGENT_SURFACES), public: [
68
+ 'up', 'down', 'restart', 'force-restart', 'attach', 'send', 'loops', 'owner-channel',
69
+ 'watchdog-run', 'watchdog-report', 'rm', 'init', 'web',
70
+ ], denied: [],
71
+ hidden: ['_run', '_run-temp', '_run-watchdog', '_run-watchdogs'], aliases: ['man'],
72
+ });
73
+ const globalValueOptions = new Set(['-c', '--configuration']);
74
+ /** Classify before Commander parsing. Unknown and internal paths fail closed. */
75
+ export function classifyFleetArgv(argv) {
76
+ let command = '';
77
+ let commandIndex = -1;
78
+ for (let index = 0; index < argv.length; index++) {
79
+ const arg = argv[index];
80
+ if (arg === '--') {
81
+ command = argv[index + 1] ?? '';
82
+ commandIndex = index + 1;
83
+ break;
84
+ }
85
+ if (globalValueOptions.has(arg)) {
86
+ index++;
87
+ continue;
88
+ }
89
+ if (arg.startsWith('--configuration=') || arg.startsWith('-c='))
90
+ continue;
91
+ if (arg.startsWith('-c') && arg.length > 2)
92
+ continue;
93
+ if (arg === '--help' || arg === '-h' || arg === '--version' || arg === '-V')
94
+ return { command: arg, route: 'supervisor-proxy/read-only', decision: 'allow' };
95
+ if (!arg.startsWith('-')) {
96
+ command = arg;
97
+ commandIndex = index;
98
+ break;
99
+ }
100
+ }
101
+ if (!command)
102
+ return { command: '<none>', route: 'supervisor-proxy/unsupported', decision: 'unsupported' };
103
+ if (command.startsWith('_'))
104
+ return { command, route: 'supervisor-proxy/internal', decision: 'deny' };
105
+ if (SAFE_READ.has(command) || command === 'man')
106
+ return { command, route: 'supervisor-proxy/read-only', decision: 'allow' };
107
+ if (AGENT_SURFACES[command]) {
108
+ const sub = argv[commandIndex + 1] ?? '<none>';
109
+ if (sub === '--help' || sub === '-h')
110
+ return { command: `${command} ${sub}`, route: 'supervisor-proxy/read-only', decision: 'allow' };
111
+ if (sub.startsWith('_'))
112
+ return { command: `${command} ${sub}`, route: 'supervisor-proxy/internal', decision: 'deny' };
113
+ if (command !== 'spawn' && !AGENT_SURFACES[command].has(sub))
114
+ return { command: `${command} ${sub}`, route: `supervisor-proxy/${command}`, decision: 'allow' };
115
+ return { command: sub.startsWith('-') ? command : `${command} ${sub}`,
116
+ route: `supervisor-proxy/${command}`, decision: 'allow' };
117
+ }
118
+ // The proxy is attribution and routing, not a product permission boundary.
119
+ // Commander validates every public command. Hidden process entry points above
120
+ // remain guarded because recursively invoking them breaks the supervisor protocol.
121
+ return { command, route: 'supervisor-proxy/public', decision: 'allow' };
122
+ }
123
+ const marker = (value) => value === '' ? '[REDACTED:empty]' : '[REDACTED:value]';
124
+ const sensitiveValueFlags = new Set([
125
+ '--identity', '--invite', '--token', '--api-token', '--password', '--password-file',
126
+ '--env', '--brief', '--brief-file', '--bio-file', '--persona-file', '--isolation-file',
127
+ '--configuration', '-c', '--public-invite', '--public-invite-file', '--invite-file',
128
+ '--summary-file', '--text', '--message', '--summary', '--reason', '--goal', '--cwd',
129
+ '--identity-cid', '--owner-cid', '--contact-cid', '--codex-config', '--add-dir',
130
+ ]);
131
+ function redactUrl(value) {
132
+ try {
133
+ const url = new URL(value);
134
+ let changed = false;
135
+ if (url.username || url.password) {
136
+ url.username = 'REDACTED';
137
+ url.password = 'REDACTED';
138
+ changed = true;
139
+ }
140
+ for (const key of [...url.searchParams.keys()])
141
+ if (isSensitiveConfigKey(key)) {
142
+ url.searchParams.set(key, marker(url.searchParams.get(key) ?? ''));
143
+ changed = true;
144
+ }
145
+ return changed ? url.toString() : value;
146
+ }
147
+ catch {
148
+ return value;
149
+ }
150
+ }
151
+ function redactInline(value) {
152
+ if (!value.startsWith('inline:'))
153
+ return redactUrl(value);
154
+ const source = value.slice('inline:'.length);
155
+ try {
156
+ const parsed = JSON.parse(source);
157
+ const walk = (node, key) => {
158
+ if (key && isSensitiveConfigKey(key))
159
+ return typeof node === 'string' ? marker(node) : '[REDACTED:value]';
160
+ if (Array.isArray(node))
161
+ return node.map(child => walk(child));
162
+ if (node && typeof node === 'object')
163
+ return Object.fromEntries(Object.entries(node).map(([childKey, child]) => [childKey, walk(child, childKey)]));
164
+ return typeof node === 'string' ? redactUrl(node) : node;
165
+ };
166
+ return `inline:${JSON.stringify(walk(parsed))}`;
167
+ }
168
+ catch {
169
+ return 'inline:[REDACTED:invalid-definition]';
170
+ }
171
+ }
172
+ /** Faithful ordered argv with deterministic structural redaction. */
173
+ export function redactFleetArgv(argv) {
174
+ const result = [...argv];
175
+ for (let index = 0; index < result.length; index++) {
176
+ const arg = result[index];
177
+ if (arg.startsWith('-c=') || (arg.startsWith('-c') && arg.length > 2 && !arg.startsWith('--'))) {
178
+ const prefix = arg.startsWith('-c=') ? '-c=' : '-c';
179
+ result[index] = `${prefix}${marker(arg.slice(prefix.length))}`;
180
+ continue;
181
+ }
182
+ const equal = /^(--[^=]+)=(.*)$/su.exec(arg);
183
+ if (equal && sensitiveValueFlags.has(equal[1])) {
184
+ result[index] = `${equal[1]}=${marker(equal[2])}`;
185
+ continue;
186
+ }
187
+ if (sensitiveValueFlags.has(arg) && index + 1 < result.length) {
188
+ result[index + 1] = marker(result[index + 1]);
189
+ index++;
190
+ continue;
191
+ }
192
+ result[index] = redactInline(arg);
193
+ }
194
+ // `send <role> <text...>` has positional content. Locate it using the same
195
+ // global-option scan as classification, never absolute argv offsets.
196
+ let commandIndex = -1;
197
+ for (let index = 0; index < result.length; index++) {
198
+ const arg = result[index];
199
+ if (globalValueOptions.has(arg)) {
200
+ index++;
201
+ continue;
202
+ }
203
+ if (arg.startsWith('--configuration=') || arg.startsWith('-c=') || (arg.startsWith('-c') && arg.length > 2))
204
+ continue;
205
+ if (!arg.startsWith('-')) {
206
+ commandIndex = index;
207
+ break;
208
+ }
209
+ }
210
+ if (result[commandIndex] === 'send')
211
+ for (let index = commandIndex + 2; index < result.length; index++)
212
+ result[index] = marker(result[index]);
213
+ return result;
214
+ }
215
+ /** Durable write-once attempt ledger; contains redacted argv only. */
216
+ export class FleetCommandAuditStore {
217
+ path;
218
+ deps;
219
+ attempts = [];
220
+ constructor(path, deps = { now: () => new Date(), uuid: () => randomUUID() }) {
221
+ this.path = path;
222
+ this.deps = deps;
223
+ if (!existsSync(path))
224
+ return;
225
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
226
+ if (parsed.version !== 1 || !Array.isArray(parsed.attempts))
227
+ throw new Error('invalid fleet command audit ledger');
228
+ this.attempts = parsed.attempts;
229
+ let recovered = false;
230
+ for (const attempt of this.attempts) {
231
+ if (attempt.invocation === 'sending') {
232
+ attempt.invocation = 'uncertain';
233
+ recovered = true;
234
+ }
235
+ if (attempt.outcome?.delivery === 'sending') {
236
+ attempt.outcome.delivery = 'uncertain';
237
+ recovered = true;
238
+ }
239
+ }
240
+ if (recovered)
241
+ this.persist();
242
+ }
243
+ list() { return this.attempts.map(item => structuredClone(item)); }
244
+ begin(requestId, caller, argv) {
245
+ const existing = this.attempts.find(item => item.caller === caller && item.requestId === requestId);
246
+ if (existing) {
247
+ if (JSON.stringify(existing.argv) !== JSON.stringify(redactFleetArgv(argv)))
248
+ throw new Error('fleet command request ID was reused with different argv');
249
+ return structuredClone(existing);
250
+ }
251
+ const attempt = { version: 1, correlationId: this.deps.uuid(), requestId,
252
+ caller, invokedAt: this.deps.now().toISOString(), classification: classifyFleetArgv(argv),
253
+ argv: redactFleetArgv(argv), invocation: 'sending' };
254
+ this.attempts.push(attempt);
255
+ this.persist();
256
+ return structuredClone(attempt);
257
+ }
258
+ invocation(correlationId, caller, delivery) {
259
+ const attempt = this.owned(correlationId, caller);
260
+ if (attempt.invocation !== 'sending' && attempt.invocation !== delivery)
261
+ throw new Error('conflicting fleet command invocation delivery state');
262
+ attempt.invocation = delivery;
263
+ this.persist();
264
+ return structuredClone(attempt);
265
+ }
266
+ finish(correlationId, caller, outcome) {
267
+ const attempt = this.owned(correlationId, caller);
268
+ if (!attempt.outcome)
269
+ attempt.outcome = { ...outcome, completedAt: this.deps.now().toISOString(), delivery: 'sending' };
270
+ else {
271
+ const prior = { class: attempt.outcome.class, effect: attempt.outcome.effect,
272
+ ...(attempt.outcome.exitCode === undefined ? {} : { exitCode: attempt.outcome.exitCode }),
273
+ ...(attempt.outcome.resourceIds ? { resourceIds: attempt.outcome.resourceIds } : {}),
274
+ ...(attempt.outcome.presentations ? { presentations: attempt.outcome.presentations } : {}) };
275
+ if (JSON.stringify(prior) !== JSON.stringify(outcome))
276
+ throw new Error('conflicting fleet command outcome for existing correlation');
277
+ }
278
+ this.persist();
279
+ return structuredClone(attempt);
280
+ }
281
+ outcome(correlationId, caller, delivery) {
282
+ const attempt = this.owned(correlationId, caller);
283
+ if (!attempt.outcome)
284
+ throw new Error('fleet command audit outcome was not recorded');
285
+ if (attempt.outcome.delivery !== 'sending' && attempt.outcome.delivery !== delivery)
286
+ throw new Error('conflicting fleet command outcome delivery state');
287
+ attempt.outcome.delivery = delivery;
288
+ this.persist();
289
+ return structuredClone(attempt);
290
+ }
291
+ owned(correlationId, caller) {
292
+ const attempt = this.attempts.find(item => item.correlationId === correlationId);
293
+ if (!attempt || attempt.caller !== caller)
294
+ throw new Error('unknown or cross-role fleet command correlation');
295
+ return attempt;
296
+ }
297
+ persist() {
298
+ replaceFileAtomically(this.path, `${JSON.stringify({ version: 1, attempts: this.attempts })}\n`, 0o600);
299
+ }
300
+ }
301
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
302
+ const SAFE_RESOURCE_ID = /^[\p{L}\p{N}][\p{L}\p{N}._:@-]{0,159}$/u;
303
+ const OUTCOMES = new Set(['success', 'validation', 'denied', 'runtime', 'timeout', 'proxy', 'delivery']);
304
+ const EFFECTS = new Set(['not_started', 'completed', 'unknown']);
305
+ export function validateFleetAuditBegin(value) {
306
+ const input = value;
307
+ if (!input || Object.keys(input).some(key => !['requestId', 'argv'].includes(key))
308
+ || typeof input.requestId !== 'string' || !UUID.test(input.requestId)
309
+ || !Array.isArray(input.argv) || input.argv.length > 256
310
+ || !input.argv.every(arg => typeof arg === 'string' && Buffer.byteLength(arg) <= 16_384)
311
+ || Buffer.byteLength(JSON.stringify(input.argv)) > 48 * 1024)
312
+ throw new Error('invalid fleet audit begin fields');
313
+ }
314
+ export function validateFleetAuditFinish(value) {
315
+ const input = value;
316
+ const allowed = ['correlationId', 'class', 'exitCode', 'effect', 'resourceIds', 'presentations'];
317
+ const resources = input?.resourceIds;
318
+ if (!input || Object.keys(input).some(key => !allowed.includes(key))
319
+ || typeof input.correlationId !== 'string' || !UUID.test(input.correlationId)
320
+ || !OUTCOMES.has(input.class) || !EFFECTS.has(String(input.effect))
321
+ || (input.exitCode !== undefined && (!Number.isSafeInteger(input.exitCode) || Number(input.exitCode) < 0 || Number(input.exitCode) > 255))
322
+ || (resources !== undefined && (!resources || typeof resources !== 'object' || Array.isArray(resources)
323
+ || Object.entries(resources).some(([key, id]) => !['agent', 'task', 'room'].includes(key)
324
+ || typeof id !== 'string' || !SAFE_RESOURCE_ID.test(id))))
325
+ || (input.presentations !== undefined && (!Array.isArray(input.presentations)
326
+ || input.presentations.length > 128 || !input.presentations.every(validPresentation))))
327
+ throw new Error('invalid fleet audit finish fields');
328
+ }
329
+ function validPresentation(value) {
330
+ if (!value || typeof value !== 'object' || Array.isArray(value))
331
+ return false;
332
+ const p = value;
333
+ const safe = (v) => typeof v === 'string' && SAFE_RESOURCE_ID.test(v);
334
+ const text = (v) => typeof v === 'string' && v.length <= 256
335
+ && !/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/u.test(v);
336
+ const exact = (record, keys) => Object.keys(record).every(key => keys.includes(key));
337
+ const transition = (allowed) => typeof p.operation === 'string' && typeof p.newState === 'string'
338
+ && allowed[p.operation]?.includes(`${String(p.previousState ?? 'none')}→${p.newState}`);
339
+ if (p.kind === 'agent_started')
340
+ return exact(p, ['kind', 'eventId', 'id', 'name', 'lifetime', 'brain', 'role',
341
+ 'harness', 'session', 'model', 'permissions', 'parent', 'actionId', 'inherited'])
342
+ && safe(p.eventId) && safe(p.id) && safe(p.name) && ['permanent', 'temporary'].includes(String(p.lifetime))
343
+ && text(p.brain) && text(p.role) && safe(p.harness) && p.session === 'acp' && safe(p.parent) && safe(p.actionId)
344
+ && (p.model === undefined || text(p.model)) && (p.permissions === undefined || text(p.permissions))
345
+ && Array.isArray(p.inherited) && p.inherited.every(text);
346
+ if (p.kind === 'task')
347
+ return exact(p, ['kind', 'operation', 'eventId', 'id', 'title', 'previousState',
348
+ 'newState', 'template', 'roomId', 'revision', 'list', 'agents'])
349
+ && ['create', 'start', 'work', 'block', 'unblock', 'review', 'done',
350
+ 'cancel', 'finish', 'delete', 'settling'].includes(String(p.operation)) && safe(p.eventId) && safe(p.id) && safe(p.newState)
351
+ && transition({ create: ['none→backlog', 'none→provisioning', 'none→active'],
352
+ start: ['backlog→provisioning'], work: ['provisioning→active'],
353
+ block: ['backlog→backlog', 'provisioning→provisioning', 'active→active', 'review→review'],
354
+ unblock: ['backlog→backlog', 'provisioning→provisioning', 'active→active', 'review→review'],
355
+ review: ['active→review'], done: ['review→done'], finish: ['active→done', 'review→done'],
356
+ cancel: ['backlog→cancelled', 'provisioning→cancelled', 'active→cancelled', 'review→cancelled'],
357
+ delete: ['done→deleted'], settling: ['active→active', 'review→review', 'backlog→backlog',
358
+ 'provisioning→provisioning'] })
359
+ && (p.title === undefined || text(p.title)) && (p.previousState === undefined || safe(p.previousState))
360
+ && (p.template === undefined || text(p.template)) && (p.roomId === undefined || safe(p.roomId))
361
+ && (p.list === undefined || text(p.list)) && (p.revision === undefined || text(p.revision))
362
+ && Array.isArray(p.agents) && p.agents.length <= 64
363
+ && p.agents.every(a => a && typeof a === 'object'
364
+ && exact(a, ['name', 'brain', 'role', 'permissions'])
365
+ && safe(a.name)
366
+ && text(a.role)
367
+ && (a.brain === undefined || text(a.brain))
368
+ && (a.permissions === undefined || text(a.permissions)));
369
+ if (p.kind === 'room')
370
+ return exact(p, ['kind', 'operation', 'eventId', 'id', 'previousState', 'newState',
371
+ 'revision', 'name', 'template', 'taskId', 'participants'])
372
+ && ['create', 'activate', 'close', 'delete'].includes(String(p.operation))
373
+ && safe(p.eventId) && safe(p.id) && safe(p.newState)
374
+ && transition({ create: ['none→provisioning'], activate: ['provisioning→active'],
375
+ close: ['active→closing', 'provisioning→closing'],
376
+ delete: ['closing→deleted'] })
377
+ && (p.previousState === undefined || safe(p.previousState))
378
+ && (p.name === undefined || text(p.name)) && (p.template === undefined || text(p.template))
379
+ && (p.taskId === undefined || safe(p.taskId)) && (p.revision === undefined || text(p.revision))
380
+ && Array.isArray(p.participants) && p.participants.length <= 64
381
+ && p.participants.every(a => a && typeof a === 'object'
382
+ && exact(a, ['name', 'id', 'brain', 'role', 'permissions'])
383
+ && safe(a.name)
384
+ && text(a.role)
385
+ && (a.id === undefined || safe(a.id))
386
+ && (a.brain === undefined || text(a.brain))
387
+ && (a.permissions === undefined || text(a.permissions)));
388
+ if (p.kind === 'lifecycle_failure')
389
+ return exact(p, ['kind', 'eventId', 'resource', 'id', 'state', 'category'])
390
+ && ['Agent', 'Task', 'Room'].includes(String(p.resource))
391
+ && safe(p.eventId) && safe(p.id) && safe(p.state) && ['provision_failed', 'provision_pending', 'readiness_failed', 'settlement_failed',
392
+ 'settlement_pending', 'cleanup_failed', 'cleanup_pending'].includes(String(p.category));
393
+ return false;
394
+ }
395
+ /** Compact Owner presentation. It intentionally has no command, argv, environment, or correlation data. */
396
+ export function renderFleetLifecycleEvent(value) {
397
+ if (value.kind === 'lifecycle_failure') {
398
+ const actions = {
399
+ provision_failed: 'Inspect Fleet service logs, correct configuration, then recover the resource.',
400
+ provision_pending: 'Run Task or Room recover after checking member readiness.',
401
+ readiness_failed: 'Inspect the Agent log, correct its configuration, then retry creation.',
402
+ settlement_failed: 'Inspect Fleet service logs, then run Task recover.',
403
+ settlement_pending: 'Run Task recover to continue settlement.',
404
+ cleanup_failed: 'Inspect Fleet service logs, then run Room recover.',
405
+ cleanup_pending: 'Run Room recover to continue cleanup.',
406
+ };
407
+ const lifecycle = value.category.endsWith('_pending') ? 'lifecycle pending' : 'lifecycle failure';
408
+ return `⚠️ ${value.resource} ${value.id} ${lifecycle} (${value.category}); `
409
+ + `state ${value.state}. Action: ${actions[value.category]}`;
410
+ }
411
+ if (value.kind === 'agent_started') {
412
+ const permission = value.permissions ? `; permissions ${value.permissions}` : '';
413
+ return `🧑‍💻 ${value.parent} spawned ${value.lifetime} Agent ${value.name} (${value.id}) — ready. `
414
+ + `Brain ${value.brain ?? 'unresolved'}; Role ${value.role ?? 'unresolved'}${permission}.`;
415
+ }
416
+ if (value.kind === 'task') {
417
+ const title = value.title ? ` “${value.title}”` : '';
418
+ const context = [value.list ? `List ${value.list}` : undefined,
419
+ value.template ? `template ${value.template}` : undefined,
420
+ value.roomId ? `Room ${value.roomId}` : undefined].filter(Boolean).join('; ');
421
+ const agents = value.agents.length ? ` Agents: ${value.agents.map(agent => `${agent.name} [Brain ${agent.brain ?? 'unresolved'}; Role ${agent.role}`
422
+ + `${agent.permissions ? `; permissions ${agent.permissions}` : ''}]`).join('; ')}.` : '';
423
+ return `📋 Task${title} (${value.id}) ${value.operation}: `
424
+ + `${value.previousState ?? 'none'} → ${value.newState}.${context ? ` ${context}.` : ''}${agents}`;
425
+ }
426
+ const context = [value.template ? `template ${value.template}` : undefined,
427
+ value.taskId ? `Task ${value.taskId}` : undefined].filter(Boolean).join('; ');
428
+ const participants = value.participants.length ? ` Participants: ${value.participants.map(participant => `${participant.name}${participant.id ? ` (${participant.id})` : ''} [Role ${participant.role}`
429
+ + `${participant.brain ? `; Brain ${participant.brain}` : ''}`
430
+ + `${participant.permissions ? `; permissions ${participant.permissions}` : ''}]`).join('; ')}.` : '';
431
+ return `🏠 Room${value.name ? ` “${value.name}”` : ''} (${value.id}) ${value.operation}: `
432
+ + `${value.previousState ?? 'none'} → ${value.newState}.${context ? ` ${context}.` : ''}${participants}`;
433
+ }
@@ -9,7 +9,7 @@ export interface ManagedFleetSpawnResult {
9
9
  lifetime: 'permanent' | 'temporary';
10
10
  statePath: string;
11
11
  harness: string;
12
- session: 'tmux' | 'acp';
12
+ session: 'acp';
13
13
  model?: string;
14
14
  monitor: Pick<MonitorConfig, 'mode' | 'interrupt'>;
15
15
  /** Adapter-resolved portable policy and exact native runtime mode. */
@@ -19,6 +19,8 @@ export interface ManagedFleetSpawnResult {
19
19
  };
20
20
  inherited: string[];
21
21
  creationActionId: string;
22
+ brainSummary: string;
23
+ roleSummary: string;
22
24
  }
23
25
  /**
24
26
  * Fill only omitted spawn settings from the live caller. Explicit agent choices
@@ -1,4 +1,5 @@
1
1
  import { inheritedPermissionMode } from './permissions.js';
2
+ import { isSensitiveConfigKey } from './sensitive-config.js';
2
3
  /** Present only inside a managed role process. The CLI treats it as a routing hint, not authority. */
3
4
  export const FLEET_PROXY_STATE_DIR_ENV = 'OURS_FLEET_PROXY_STATE_DIR';
4
5
  export const FLEET_PROXY_CALLER_ENV = 'OURS_FLEET_PROXY_CALLER';
@@ -9,15 +10,41 @@ export const FLEET_PROXY_CALLER_ENV = 'OURS_FLEET_PROXY_CALLER';
9
10
  export function inheritCallerSpawnDefaults(caller, requested, configPath) {
10
11
  const options = { ...requested };
11
12
  const inherited = [];
13
+ if (requested.agentDefinition) {
14
+ options.configPath = configPath;
15
+ options.surface = 'agent';
16
+ options.callerRole = caller.name;
17
+ options.inheritedFromCaller = [];
18
+ return { options, inherited };
19
+ }
12
20
  const take = (key, value) => {
13
21
  if (options[key] !== undefined || value === undefined)
14
22
  return;
15
23
  options[key] = value;
16
24
  inherited.push(String(key));
17
25
  };
18
- const sameHarness = requested.harness === undefined || requested.harness === caller.harness;
19
- take('harness', caller.harness);
20
- take('session', caller.session);
26
+ const sensitive = (value) => {
27
+ if (Array.isArray(value))
28
+ return value.some(sensitive);
29
+ if (!value || typeof value !== 'object')
30
+ return false;
31
+ return Object.entries(value)
32
+ .some(([key, child]) => isSensitiveConfigKey(key) || sensitive(child));
33
+ };
34
+ if (options.brain === undefined) {
35
+ const brain = caller.agentSelections?.brain;
36
+ if (!brain)
37
+ throw new Error('caller has no inspectable Brain selection; pass --brain explicitly');
38
+ if ('inline' in brain && sensitive(brain.inline))
39
+ throw new Error('caller inline Brain contains sensitive configuration; pass --brain explicitly');
40
+ take('brain', structuredClone(brain));
41
+ }
42
+ if (options.role === undefined) {
43
+ const role = caller.agentSelections?.role;
44
+ if (!role)
45
+ throw new Error('caller has no inspectable Role selection; pass --role explicitly');
46
+ take('role', structuredClone(role));
47
+ }
21
48
  take('cwd', caller.cwd);
22
49
  take('coordinator', caller.name);
23
50
  if (options.approval === undefined)
@@ -25,11 +52,6 @@ export function inheritCallerSpawnDefaults(caller, requested, configPath) {
25
52
  take('filesystem', caller.permissions.filesystem);
26
53
  take('unattended', caller.permissions.unattended);
27
54
  take('monitorConfig', structuredClone(caller.monitor));
28
- // A model name and native harness options are not portable across harnesses.
29
- // When the caller explicitly switches harness, let that harness/fleet defaults
30
- // select its model instead of copying (for example) a Codex model into Claude.
31
- if (sameHarness)
32
- take('model', caller.model);
33
55
  options.configPath = configPath;
34
56
  options.surface = 'agent';
35
57
  options.callerRole = caller.name;
@@ -0,0 +1,9 @@
1
+ export declare const GENERATED_AGENT_SOURCE_MARKER = ".generated-agent-source.json";
2
+ /** Durable proof that Fleet itself created one exact Agent document. */
3
+ export declare function recordGeneratedAgentSource(stateDir: string, configPath: string, agentPath: string): void;
4
+ /**
5
+ * Verify exact generated-file ownership. Missing, malformed, or mismatched
6
+ * state fails closed: shared v2 agents/ directories also contain hand-written
7
+ * documents, so directory membership alone is never deletion authority.
8
+ */
9
+ export declare function provesGeneratedAgentSource(stateDir: string, configPath: string | undefined, agentPath: string): boolean;
@@ -0,0 +1,53 @@
1
+ import { chmodSync, lstatSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { join, resolve } from 'node:path';
4
+ export const GENERATED_AGENT_SOURCE_MARKER = '.generated-agent-source.json';
5
+ const normalized = (path) => resolve(path);
6
+ /** Durable proof that Fleet itself created one exact Agent document. */
7
+ export function recordGeneratedAgentSource(stateDir, configPath, agentPath) {
8
+ const marker = {
9
+ version: 1,
10
+ configPath: normalized(configPath),
11
+ agentPath: normalized(agentPath),
12
+ };
13
+ const path = join(stateDir, GENERATED_AGENT_SOURCE_MARKER);
14
+ const temporary = join(stateDir, `.${GENERATED_AGENT_SOURCE_MARKER}.${process.pid}-${randomUUID()}.tmp`);
15
+ try {
16
+ writeFileSync(temporary, `${JSON.stringify(marker)}\n`, { flag: 'wx', mode: 0o600 });
17
+ chmodSync(temporary, 0o600);
18
+ renameSync(temporary, path);
19
+ }
20
+ catch (error) {
21
+ try {
22
+ unlinkSync(temporary);
23
+ }
24
+ catch { /* never hide the original failure */ }
25
+ throw error;
26
+ }
27
+ }
28
+ /**
29
+ * Verify exact generated-file ownership. Missing, malformed, or mismatched
30
+ * state fails closed: shared v2 agents/ directories also contain hand-written
31
+ * documents, so directory membership alone is never deletion authority.
32
+ */
33
+ export function provesGeneratedAgentSource(stateDir, configPath, agentPath) {
34
+ if (!configPath)
35
+ return false;
36
+ const path = join(stateDir, GENERATED_AGENT_SOURCE_MARKER);
37
+ try {
38
+ const stat = lstatSync(path);
39
+ if (!stat.isFile() || stat.isSymbolicLink())
40
+ return false;
41
+ if ((stat.mode & 0o022) !== 0)
42
+ return false;
43
+ if (typeof process.geteuid === 'function' && stat.uid !== process.geteuid())
44
+ return false;
45
+ const marker = JSON.parse(readFileSync(path, 'utf8'));
46
+ return marker.version === 1
47
+ && marker.configPath === normalized(configPath)
48
+ && marker.agentPath === normalized(agentPath);
49
+ }
50
+ catch {
51
+ return false;
52
+ }
53
+ }
@@ -0,0 +1,8 @@
1
+ import type { AcpSessionOptions } from '../session/acp.js';
2
+ import type { AgentSession } from '../session/types.js';
3
+ /** ACP implementation seam kept out of the harness-neutral adapter contract. */
4
+ export type AcpSessionTransport = (options: AcpSessionOptions) => Promise<AgentSession>;
5
+ export interface AcpAdapterState {
6
+ permissionMetadataSource?: AcpSessionOptions['permissionMetadataSource'];
7
+ }
8
+ export declare function acpAdapterState(value: unknown): AcpAdapterState;
@@ -0,0 +1,3 @@
1
+ export function acpAdapterState(value) {
2
+ return (value ?? {});
3
+ }
@@ -0,0 +1,51 @@
1
+ import type { CommonPermissions, FleetPermissionMode, ResolvedRole } from '../config.js';
2
+ import type { AgentSession } from '../session/types.js';
3
+ import type { Launch, SessionPrep } from './types.js';
4
+ /**
5
+ * Command selected by a harness adapter. The runner may wrap argv for its
6
+ * isolation/resource boundary, but never interprets adapter-specific fields.
7
+ */
8
+ export interface PreparedAgentSessionLaunch extends Launch {
9
+ readonly adapterState?: unknown;
10
+ }
11
+ export interface BrainSelection {
12
+ model?: string | null;
13
+ effort?: string;
14
+ harnessOptions?: Record<string, unknown>;
15
+ }
16
+ export interface ResolvedBrainSelection {
17
+ model?: string | null;
18
+ harnessOptions?: Record<string, unknown>;
19
+ }
20
+ export interface AgentSessionConfigSelection {
21
+ configId: string;
22
+ value: string;
23
+ }
24
+ export interface AgentSessionStartOptions {
25
+ role: ResolvedRole;
26
+ prep: SessionPrep;
27
+ launch: PreparedAgentSessionLaunch;
28
+ cwd: string;
29
+ stateDir: string;
30
+ mode: 'fresh' | 'resume';
31
+ permissions: CommonPermissions;
32
+ permissionMode: {
33
+ fleetMode: FleetPermissionMode;
34
+ nativeMode: string;
35
+ };
36
+ log(line: string): void;
37
+ }
38
+ /**
39
+ * The sole harness-specific boundary for creating a live agent session.
40
+ * Lifecycle orchestration remains in the runner; ACP normalization remains in
41
+ * the shared transport implementation.
42
+ */
43
+ export interface AgentSessionAdapter {
44
+ /** Harness-owned translation of neutral Brain selection into launch state. */
45
+ resolveBrain(brain: BrainSelection): ResolvedBrainSelection;
46
+ /** Harness-owned environment channel used to pin/recover a model, if any. */
47
+ modelEnvironmentVariable(): string | undefined;
48
+ prepareLaunch(role: ResolvedRole, prep: SessionPrep): PreparedAgentSessionLaunch;
49
+ sessionConfigSelections(role: ResolvedRole): AgentSessionConfigSelection[];
50
+ start(options: AgentSessionStartOptions): Promise<AgentSession>;
51
+ }
@@ -0,0 +1 @@
1
+ export {};