@ours.network/fleet 1.1.0-nightly.2 → 1.1.0-nightly.4

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 (43) hide show
  1. package/README.md +14 -22
  2. package/dist/application/role-creation-service.d.ts +2 -21
  3. package/dist/application/role-creation-service.js +15 -71
  4. package/dist/application/task-room-service.js +7 -2
  5. package/dist/briefing.js +3 -3
  6. package/dist/build-info.json +4 -4
  7. package/dist/cli.js +40 -33
  8. package/dist/config.d.ts +37 -0
  9. package/dist/config.js +40 -12
  10. package/dist/docs.d.ts +1 -1
  11. package/dist/docs.js +19 -30
  12. package/dist/doctor.js +3 -3
  13. package/dist/fleet-proxy.js +30 -8
  14. package/dist/harness/agent-session.d.ts +5 -0
  15. package/dist/harness/claude-code-session.d.ts +2 -0
  16. package/dist/harness/claude-code-session.js +4 -0
  17. package/dist/harness/claude-code.js +2 -0
  18. package/dist/harness/codex-session.d.ts +2 -0
  19. package/dist/harness/codex-session.js +4 -0
  20. package/dist/harness/codex.js +2 -0
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.js +1 -1
  23. package/dist/model-env.d.ts +2 -2
  24. package/dist/model-env.js +1 -1
  25. package/dist/owner-channel/commands.js +2 -1
  26. package/dist/rooms-tasks/cli.js +2 -1
  27. package/dist/rooms-tasks/config.js +24 -9
  28. package/dist/rooms-tasks/provision.js +71 -36
  29. package/dist/rooms-tasks/templates.js +6 -6
  30. package/dist/rooms-tasks/types.d.ts +8 -15
  31. package/dist/rooms-tasks/types.js +1 -4
  32. package/dist/session/acp.d.ts +5 -0
  33. package/dist/session/acp.js +27 -3
  34. package/dist/spawn.d.ts +6 -31
  35. package/dist/spawn.js +45 -233
  36. package/dist/watchdog/config.d.ts +10 -7
  37. package/dist/watchdog/config.js +29 -19
  38. package/dist/watchdog/run.js +4 -13
  39. package/dist/web/topology-promote.js +8 -1
  40. package/dist/web-app/assets/index-BbTc5KtZ.js +10 -0
  41. package/dist/web-app/index.html +1 -1
  42. package/package.json +1 -1
  43. package/dist/web-app/assets/index-BbE9EX6n.js +0 -10
@@ -15,6 +15,9 @@ export class CodexAgentSessionAdapter {
15
15
  prepareLaunch(role, prep) {
16
16
  return this.strategy.prepareLaunch(role, prep);
17
17
  }
18
+ sessionConfigSelections(role) {
19
+ return this.strategy.sessionConfigSelections(role);
20
+ }
18
21
  start(options) {
19
22
  const { role, prep, launch } = options;
20
23
  return this.transport({
@@ -23,6 +26,7 @@ export class CodexAgentSessionAdapter {
23
26
  modeId: this.strategy.permissionModeId(role),
24
27
  mcpServers: this.strategy.mcpServers(role),
25
28
  sessionMeta: this.strategy.sessionMeta(role, prep),
29
+ configSelections: this.strategy.sessionConfigSelections(role),
26
30
  permissionMode: options.permissionMode,
27
31
  permissionMetadataSource: acpAdapterState(launch.adapterState).permissionMetadataSource,
28
32
  scrubObsoleteOursAutostart: true,
@@ -274,6 +274,8 @@ export function makeCodexAdapter(exec = realExec, transport) {
274
274
  ? { adapterState: { permissionMetadataSource } } : {}),
275
275
  };
276
276
  },
277
+ sessionConfigSelections: role => typeof role.model === 'string'
278
+ ? [{ configId: 'model', value: role.model }] : [],
277
279
  permissionModeId: role => acpAgentMode(role),
278
280
  mcpServers: () => undefined,
279
281
  sessionMeta: () => undefined,
package/dist/index.d.ts CHANGED
@@ -13,7 +13,7 @@ export { effectivePermissionMode } from './permissions.js';
13
13
  export { pickBackend } from './supervisor/index.js';
14
14
  export type { SupervisorBackend } from './supervisor/types.js';
15
15
  export { up, down, restartRoles, rmRole, applyRole } from './ops.js';
16
- export { spawnPermanent, spawnTemp, buildRoleConfig, profileValues, validateSpawnOpts, } from './spawn.js';
16
+ export { spawnPermanent, spawnTemp, agentDefinitionFromSpawn, validateSpawnOpts, } from './spawn.js';
17
17
  export { RoleRepository } from './application/role-repository.js';
18
18
  export { FleetQueryService } from './application/fleet-query-service.js';
19
19
  export { AcpRoleSessionAdapter } from './application/session-control.js';
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ export { generateBriefing } from './briefing.js';
9
9
  export { effectivePermissionMode } from './permissions.js';
10
10
  export { pickBackend } from './supervisor/index.js';
11
11
  export { up, down, restartRoles, rmRole, applyRole } from './ops.js';
12
- export { spawnPermanent, spawnTemp, buildRoleConfig, profileValues, validateSpawnOpts, } from './spawn.js';
12
+ export { spawnPermanent, spawnTemp, agentDefinitionFromSpawn, validateSpawnOpts, } from './spawn.js';
13
13
  export { RoleRepository } from './application/role-repository.js';
14
14
  export { FleetQueryService } from './application/fleet-query-service.js';
15
15
  export { AcpRoleSessionAdapter } from './application/session-control.js';
@@ -16,7 +16,7 @@ export interface RoleModelEnvInput {
16
16
  harness: string;
17
17
  /** Already resolved by `resolveRoleModel` — may come from the fleet default. */
18
18
  model: string | undefined;
19
- /** True when the role (or `--model`) named a model, including `model: null`. */
19
+ /** True when the resolved Brain named a model, including `model: null`. */
20
20
  modelWasExplicit: boolean;
21
21
  defaultsEnv?: Record<string, string>;
22
22
  roleEnv?: Record<string, string>;
@@ -35,7 +35,7 @@ export interface RoleModelEnv {
35
35
  * never disagree.
36
36
  *
37
37
  * Precedence, highest first:
38
- * 1. an explicit `model:` / `--model` on the role
38
+ * 1. an explicit `model:` on the resolved Brain
39
39
  * 2. the role's own `env:` pin
40
40
  * 3. the fleet `defaults.model`
41
41
  * 4. the fleet `defaults.env` pin
package/dist/model-env.js CHANGED
@@ -19,7 +19,7 @@ export function modelEnvVar(harness) {
19
19
  * never disagree.
20
20
  *
21
21
  * Precedence, highest first:
22
- * 1. an explicit `model:` / `--model` on the role
22
+ * 1. an explicit `model:` on the resolved Brain
23
23
  * 2. the role's own `env:` pin
24
24
  * 3. the fleet `defaults.model`
25
25
  * 4. the fleet `defaults.env` pin
@@ -630,7 +630,8 @@ export const ownerCommands = [
630
630
  ...(t.builtin ? ['Source: built-in'] : []),
631
631
  ...(t.contract ? [`Contract: ${t.contract}`] : []),
632
632
  'Members:',
633
- ...t.members.map(m => ` ${m.slot} (${m.role}) ×${m.count} → role_ref: ${m.role_ref}`),
633
+ ...t.members.map(m => ` ${m.slot} (${m.role}) ×${m.count} → `
634
+ + `${'ref' in m.agent ? `Agent ref: ${m.agent.ref}` : 'inline Agent'}`),
634
635
  ];
635
636
  await ctx.reply(lines.join('\n'));
636
637
  };
@@ -409,7 +409,8 @@ export function registerTemplateCommands(parent, cOpt) {
409
409
  console.log(`\nContract:\n${t.contract}`);
410
410
  console.log(`\nMembers:`);
411
411
  for (const m of t.members)
412
- console.log(` ${m.slot}: ${m.count}× ${m.role} (ref: ${m.role_ref})`);
412
+ console.log(` ${m.slot}: ${m.count}× ${m.role} (`
413
+ + `${'ref' in m.agent ? `agent ref: ${m.agent.ref}` : 'inline Agent'})`);
413
414
  console.log(`\nContent hash: ${t.content_hash}`);
414
415
  }
415
416
  catch (e) {
@@ -1,7 +1,8 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import { createHash } from 'node:crypto';
3
- import { ROOMS_KEYS as RK, ROOMS_OWNER_KEYS as ROK, ROOMS_COWORK_KEYS as RCK, ROOMS_DEFAULTS_KEYS as RDK, TASKS_KEYS as TK, TEMPLATE_KEYS as TPK, TEMPLATE_MEMBER_KEYS as TMK, TEMPLATE_OVERRIDE_KEYS as TOK, } from './types.js';
3
+ import { ROOMS_KEYS as RK, ROOMS_OWNER_KEYS as ROK, ROOMS_COWORK_KEYS as RCK, ROOMS_DEFAULTS_KEYS as RDK, TASKS_KEYS as TK, TEMPLATE_KEYS as TPK, TEMPLATE_MEMBER_KEYS as TMK, } from './types.js';
4
4
  import { BUILTIN_TEMPLATES } from './templates.js';
5
+ import { isSensitiveConfigKey } from '../sensitive-config.js';
5
6
  const isPlainObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
6
7
  const CID_RE = /^[0-9a-fA-F]{64}$/;
7
8
  export class RoomsTasksConfigError extends Error {
@@ -136,19 +137,33 @@ export function validateRoomTemplatesConfig(raw, path) {
136
137
  throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].role: required string`);
137
138
  if (!Number.isInteger(m.count) || m.count < 1)
138
139
  throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].count: required positive integer`);
139
- if (!m.role_ref || typeof m.role_ref !== 'string')
140
- throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].role_ref: required string`);
141
- if (m.overrides !== undefined) {
142
- if (!isPlainObject(m.overrides))
143
- throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].overrides: must be a mapping`);
144
- rejectUnknown(m.overrides, TOK, path, `room_templates.${name}.members[${i}].overrides`);
140
+ if (!isPlainObject(m.agent))
141
+ throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].agent: required mapping`);
142
+ const agentKeys = Object.keys(m.agent);
143
+ const isRef = agentKeys.length === 1 && typeof m.agent.ref === 'string';
144
+ const isDefinition = agentKeys.includes('role') && agentKeys.includes('brain');
145
+ if (!isRef && !isDefinition)
146
+ throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].agent: must be {ref} or canonical {role, brain, ...}`);
147
+ const containsSensitive = (value) => Array.isArray(value)
148
+ ? value.some(containsSensitive)
149
+ : Boolean(value && typeof value === 'object' && Object.entries(value)
150
+ .some(([key, child]) => isSensitiveConfigKey(key) || containsSensitive(child)));
151
+ if (isDefinition && containsSensitive(m.agent))
152
+ throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].agent: inline sensitive configuration cannot be persisted; use references`);
153
+ if (isDefinition) {
154
+ const definition = m.agent;
155
+ const brain = isPlainObject(definition.brain) && isPlainObject(definition.brain.inline)
156
+ ? definition.brain.inline : undefined;
157
+ if (definition.env !== undefined || definition.owner_channel !== undefined
158
+ || definition.auth_proxy !== undefined || brain?.harness_options !== undefined
159
+ || brain?.session_options !== undefined)
160
+ throw new RoomsTasksConfigError(path, `room_templates.${name}.members[${i}].agent: secret-capable inline fields cannot be persisted; use references`);
145
161
  }
146
162
  return {
147
163
  slot: m.slot,
148
164
  role: m.role,
149
165
  count: m.count,
150
- role_ref: m.role_ref,
151
- overrides: m.overrides,
166
+ agent: structuredClone(m.agent),
152
167
  };
153
168
  });
154
169
  let room;
@@ -1,10 +1,10 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { existsSync, readFileSync, realpathSync } from 'node:fs';
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
3
3
  import { parse } from 'yaml';
4
4
  import { advanceSaga, setSagaError, updateMemberSeats, updateMemberStartup, activateRoom, getRoomRecord, } from './room-state.js';
5
5
  import { activateTask, updateTaskMembers, blockTask, unblockTask, getTask, } from './task-state.js';
6
6
  import { spawnTemp } from '../spawn.js';
7
- import { findRole } from '../config.js';
7
+ import { findRole, loadConfig } from '../config.js';
8
8
  import { FLEET_PROXY_CALLER_ENV, FLEET_PROXY_STATE_DIR_ENV, } from '../fleet-proxy.js';
9
9
  import { controlRequest } from '../session/control.js';
10
10
  import { SessionControlError } from '../session/types.js';
@@ -21,6 +21,26 @@ export function getBinPath() {
21
21
  return process.argv[1];
22
22
  }
23
23
  }
24
+ function canonicalJson(value) {
25
+ if (Array.isArray(value))
26
+ return `[${value.map(canonicalJson).join(',')}]`;
27
+ if (value && typeof value === 'object')
28
+ return `{${Object.entries(value)
29
+ .sort(([a], [b]) => a.localeCompare(b))
30
+ .map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`).join(',')}}`;
31
+ return JSON.stringify(value);
32
+ }
33
+ function launchDefinition(definition) {
34
+ const selectionEvidence = (selection) => 'ref' in selection ? { kind: 'ref', id: selection.ref } : { kind: 'inline' };
35
+ return {
36
+ projection: {
37
+ brain: selectionEvidence(definition.brain), role: selectionEvidence(definition.role),
38
+ operationalFields: Object.keys(definition)
39
+ .filter(key => key !== 'brain' && key !== 'role').sort(),
40
+ },
41
+ fingerprint: createHash('sha256').update(canonicalJson(definition)).digest('hex'),
42
+ };
43
+ }
24
44
  async function spawnRoomMember(options, binPath) {
25
45
  const stateDir = process.env[FLEET_PROXY_STATE_DIR_ENV];
26
46
  if (!stateDir)
@@ -52,28 +72,31 @@ function expandMembers(template, prefix) {
52
72
  name: `${prefix}-${slot.slot}-${i}`,
53
73
  slot: slot.slot,
54
74
  coworkRole: slot.role,
55
- roleRef: slot.role_ref,
56
- overrides: slot.overrides,
75
+ agent: structuredClone(slot.agent),
57
76
  });
58
77
  }
59
78
  }
60
79
  return result;
61
80
  }
62
81
  function settingsFor(member, cfg) {
63
- let refRole;
64
- try {
65
- refRole = findRole(cfg, member.roleRef);
82
+ if (!('ref' in member.agent)) {
83
+ const role = member.agent.role;
84
+ if (cfg.files[0] && existsSync(cfg.files[0]) && statSync(cfg.files[0]).isFile())
85
+ loadConfig(cfg.files[0], { additionalAgent: {
86
+ id: `RoomPreflight${member.name.replace(/[^A-Za-z0-9_-]/g, '')}`.slice(0, 64),
87
+ definition: member.agent,
88
+ } });
89
+ return { definition: structuredClone(member.agent),
90
+ ...('inline' in role && typeof role.inline.persona === 'string'
91
+ ? { persona: role.inline.persona } : {}) };
66
92
  }
67
- catch { /* no ref role */ }
68
- const permissions = member.overrides?.permissions;
93
+ const refRole = findRole(cfg, member.agent.ref);
94
+ const definition = cfg.agentDefinitions?.[member.agent.ref];
95
+ if (!definition)
96
+ throw new Error(`Agent '${member.agent.ref}' has no canonical definition`);
69
97
  return {
70
- model: member.overrides?.model ?? refRole?.model,
71
- harness: member.overrides?.harness ?? refRole?.harness,
72
- cwd: member.overrides?.cwd ?? refRole?.cwd,
73
- persona: member.overrides?.persona ?? refRole?.persona,
74
- approval: permissions?.approval,
75
- filesystem: permissions?.filesystem,
76
- unattended: permissions?.unattended,
98
+ definition: structuredClone(definition),
99
+ persona: refRole.persona,
77
100
  };
78
101
  }
79
102
  function roomTask(input, member, settings, members, roomIdentityCid, ownerSeatCid) {
@@ -104,13 +127,11 @@ function launchMatches(dir, member, actionId, taskSha, roomId, roomIdentityCid,
104
127
  const role = parse(readFileSync(`${dir}/role.yaml`, 'utf8'));
105
128
  const startup = role.roomMemberStartup;
106
129
  return role.identity === member.name
107
- && typeof role.mission === 'string'
108
- && sha256Text(role.mission) === taskSha
109
130
  && startup?.room_id === roomId
110
131
  && startup.room_identity_cid === roomIdentityCid
111
132
  && startup.identity_name === member.name
112
133
  && startup.role === member.coworkRole
113
- && startup.task === role.mission
134
+ && sha256Text(startup.task ?? '') === taskSha
114
135
  && (expectedInviteId === undefined || startup.invite_id === expectedInviteId)
115
136
  && typeof startup.invite === 'string'
116
137
  && startup.invite.length > 0;
@@ -199,10 +220,13 @@ async function launchMember(input) {
199
220
  let effectiveActionId = actionId;
200
221
  const attempt = (seat.launch?.attempt ?? 0) + 1;
201
222
  const taskSha = sha256Text(startup.task);
223
+ const effectiveAgentDefinition = structuredClone(settings.definition);
224
+ const { projection: agentDefinition, fingerprint: agentFingerprint } = launchDefinition(effectiveAgentDefinition);
202
225
  const proxyCaller = process.env[FLEET_PROXY_STATE_DIR_ENV]
203
226
  ? process.env[FLEET_PROXY_CALLER_ENV] : undefined;
204
227
  updateMemberStartup(provision.roomId, member.name, { launch: {
205
228
  state: 'intent', attempt, action_id: actionId, mission_sha256: taskSha,
229
+ agent_definition: agentDefinition, agent_fingerprint: agentFingerprint,
206
230
  ...(proxyCaller ? { caller_role: proxyCaller } : {}),
207
231
  updated_at: new Date().toISOString(),
208
232
  } });
@@ -211,13 +235,7 @@ async function launchMember(input) {
211
235
  name: member.name,
212
236
  temp: true,
213
237
  identity: member.name,
214
- mission: startup.task,
215
- model: settings.model,
216
- harness: settings.harness,
217
- cwd: settings.cwd,
218
- approval: settings.approval,
219
- filesystem: settings.filesystem,
220
- unattended: settings.unattended,
238
+ agentDefinition: settings.definition,
221
239
  surface: 'agent',
222
240
  creationActionId: actionId,
223
241
  roomMemberStartup: startup,
@@ -227,7 +245,8 @@ async function launchMember(input) {
227
245
  if (launched.creationActionId !== actionId) {
228
246
  updateMemberStartup(provision.roomId, member.name, { launch: {
229
247
  state: 'intent', attempt, action_id: launched.creationActionId,
230
- mission_sha256: taskSha, updated_at: new Date().toISOString(),
248
+ mission_sha256: taskSha, agent_definition: agentDefinition,
249
+ agent_fingerprint: agentFingerprint, updated_at: new Date().toISOString(),
231
250
  ...(launched.callerRole ? { caller_role: launched.callerRole } : {}),
232
251
  } });
233
252
  }
@@ -237,6 +256,7 @@ async function launchMember(input) {
237
256
  }
238
257
  updateMemberStartup(provision.roomId, member.name, { launch: {
239
258
  state: 'launched', attempt, action_id: launched.creationActionId, mission_sha256: taskSha,
259
+ agent_definition: agentDefinition, agent_fingerprint: agentFingerprint,
240
260
  ...(launched.callerRole ? { caller_role: launched.callerRole } : {}),
241
261
  launch_id: supervisor.launchId, updated_at: new Date().toISOString(),
242
262
  } });
@@ -244,6 +264,7 @@ async function launchMember(input) {
244
264
  catch (error) {
245
265
  updateMemberStartup(provision.roomId, member.name, { launch: {
246
266
  state: 'failed', attempt, action_id: effectiveActionId, mission_sha256: taskSha,
267
+ agent_definition: agentDefinition, agent_fingerprint: agentFingerprint,
247
268
  ...(proxyCaller ? { caller_role: proxyCaller } : {}),
248
269
  updated_at: new Date().toISOString(),
249
270
  error: error instanceof Error ? error.message : String(error),
@@ -289,6 +310,8 @@ export async function provisionMembers(input) {
289
310
  const { cfg, cowork, roomId, taskId, template } = input;
290
311
  const prefix = taskId ? shortId(taskId) : `room-${shortId(roomId)}`;
291
312
  const members = expandMembers(template, prefix);
313
+ // Resolve every Agent before persisting launch intent or touching Cowork membership.
314
+ const settings = new Map(members.map(member => [member.name, settingsFor(member, cfg)]));
292
315
  const existing = getRoomRecord(roomId);
293
316
  if (!existing?.room_identity_cid)
294
317
  throw new Error(`room ${roomId} has no pinned room identity CID`);
@@ -299,15 +322,27 @@ export async function provisionMembers(input) {
299
322
  && members.every(member => persistedNames.has(member.name));
300
323
  if (!resuming) {
301
324
  advanceSaga(roomId, 'create_members', 3);
302
- updateMemberSeats(roomId, members.map(member => ({
303
- role_name: member.name,
304
- slot: member.slot,
305
- cowork_role: member.coworkRole,
306
- seat_state: 'pending',
307
- launch: { state: 'pending', attempt: 0, updated_at: new Date().toISOString() },
308
- })));
325
+ updateMemberSeats(roomId, members.map(member => {
326
+ const evidence = launchDefinition(settings.get(member.name).definition);
327
+ return ({
328
+ role_name: member.name,
329
+ slot: member.slot,
330
+ cowork_role: member.coworkRole,
331
+ seat_state: 'pending',
332
+ launch: { state: 'pending', attempt: 0,
333
+ agent_definition: evidence.projection, agent_fingerprint: evidence.fingerprint,
334
+ updated_at: new Date().toISOString() },
335
+ });
336
+ }));
337
+ }
338
+ else {
339
+ for (const member of members) {
340
+ const seat = existing.member_seats.find(candidate => candidate.role_name === member.name);
341
+ const evidence = launchDefinition(settings.get(member.name).definition);
342
+ if (!seat.launch?.agent_fingerprint || seat.launch.agent_fingerprint !== evidence.fingerprint)
343
+ throw new Error(`Agent definition drift for ${member.name}; durable launch intent does not match current configuration`);
344
+ }
309
345
  }
310
- const settings = new Map(members.map(member => [member.name, settingsFor(member, cfg)]));
311
346
  const tasks = new Map(members.map(member => [member.name, roomTask(input, member, settings.get(member.name), members, roomIdentityCid, ownerSeatCid)]));
312
347
  const policy = {
313
348
  timeoutMs: input.startupWait?.timeoutMs ?? 60_000,
@@ -12,9 +12,9 @@ const TEAM = {
12
12
  'Completion requires tester sign-off.',
13
13
  ].join('\n'),
14
14
  members: [
15
- { slot: 'architect', role: 'Architect', count: 1, role_ref: 'Architect' },
16
- { slot: 'developer', role: 'Developer', count: 1, role_ref: 'Developer' },
17
- { slot: 'tester', role: 'Tester', count: 1, role_ref: 'Tester' },
15
+ { slot: 'architect', role: 'Architect', count: 1, agent: { ref: 'Architect' } },
16
+ { slot: 'developer', role: 'Developer', count: 1, agent: { ref: 'Developer' } },
17
+ { slot: 'tester', role: 'Tester', count: 1, agent: { ref: 'Tester' } },
18
18
  ],
19
19
  };
20
20
  const PAIR = {
@@ -30,8 +30,8 @@ const PAIR = {
30
30
  'Completion requires joint sign-off.',
31
31
  ].join('\n'),
32
32
  members: [
33
- { slot: 'secretary', role: 'Secretary', count: 1, role_ref: 'Secretary' },
34
- { slot: 'critic', role: 'Critic', count: 1, role_ref: 'Critic' },
33
+ { slot: 'secretary', role: 'Secretary', count: 1, agent: { ref: 'Secretary' } },
34
+ { slot: 'critic', role: 'Critic', count: 1, agent: { ref: 'Critic' } },
35
35
  ],
36
36
  };
37
37
  const SINGLE = {
@@ -45,7 +45,7 @@ const SINGLE = {
45
45
  'Owner reviews and approves completion.',
46
46
  ].join('\n'),
47
47
  members: [
48
- { slot: 'agent', role: 'Agent', count: 1, role_ref: 'Agent' },
48
+ { slot: 'agent', role: 'Agent', count: 1, agent: { ref: 'Agent' } },
49
49
  ],
50
50
  };
51
51
  export const BUILTIN_TEMPLATES = [TEAM, PAIR, SINGLE];
@@ -100,6 +100,9 @@ export interface RoomMemberLaunchState {
100
100
  /** Expected authenticated proxy caller while adopting a post-spawn crash. */
101
101
  caller_role?: string;
102
102
  mission_sha256?: string;
103
+ /** Redacted, deterministic effective Agent launch definition retained for retry inspection. */
104
+ agent_definition?: Record<string, unknown>;
105
+ agent_fingerprint?: string;
103
106
  launch_id?: string;
104
107
  updated_at: string;
105
108
  error?: string;
@@ -189,19 +192,10 @@ export interface TemplateMemberSlot {
189
192
  slot: string;
190
193
  role: string;
191
194
  count: number;
192
- role_ref: string;
193
- overrides?: TemplateRoleOverrides;
194
- }
195
- export interface TemplateRoleOverrides {
196
- harness?: string;
197
- model?: string;
198
- model_chain?: string[];
199
- permissions?: Record<string, unknown>;
200
- isolation?: Record<string, unknown>;
201
- cwd?: string;
202
- env?: Record<string, string>;
203
- persona?: string;
204
- mission?: string;
195
+ /** Canonical Agent definition, or stable reference to a declared Agent. */
196
+ agent: import('../config.js').AgentDefinition | {
197
+ ref: string;
198
+ };
205
199
  }
206
200
  export interface TemplateRoomConfig {
207
201
  quiet_membership?: boolean;
@@ -252,5 +246,4 @@ export declare const ROOMS_COWORK_KEYS: readonly ["config"];
252
246
  export declare const ROOMS_DEFAULTS_KEYS: readonly ["template", "attach_owner", "close_when_task_done"];
253
247
  export declare const TASKS_KEYS: readonly ["default_room_template", "create_mode", "close_room_on_done", "retain_completed_for"];
254
248
  export declare const TEMPLATE_KEYS: readonly ["version", "description", "room", "contract", "members", "override_builtin"];
255
- export declare const TEMPLATE_MEMBER_KEYS: readonly ["slot", "role", "count", "role_ref", "overrides"];
256
- export declare const TEMPLATE_OVERRIDE_KEYS: readonly ["harness", "model", "model_chain", "permissions", "isolation", "cwd", "env", "persona", "mission"];
249
+ export declare const TEMPLATE_MEMBER_KEYS: readonly ["slot", "role", "count", "agent"];
@@ -14,7 +14,4 @@ export const ROOMS_COWORK_KEYS = ['config'];
14
14
  export const ROOMS_DEFAULTS_KEYS = ['template', 'attach_owner', 'close_when_task_done'];
15
15
  export const TASKS_KEYS = ['default_room_template', 'create_mode', 'close_room_on_done', 'retain_completed_for'];
16
16
  export const TEMPLATE_KEYS = ['version', 'description', 'room', 'contract', 'members', 'override_builtin'];
17
- export const TEMPLATE_MEMBER_KEYS = ['slot', 'role', 'count', 'role_ref', 'overrides'];
18
- export const TEMPLATE_OVERRIDE_KEYS = [
19
- 'harness', 'model', 'model_chain', 'permissions', 'isolation', 'cwd', 'env', 'persona', 'mission',
20
- ];
17
+ export const TEMPLATE_MEMBER_KEYS = ['slot', 'role', 'count', 'agent'];
@@ -36,6 +36,11 @@ export interface AcpSessionOptions {
36
36
  permissions: CommonPermissions;
37
37
  /** Native permission-mode id to request via session/set_mode; undefined keeps the agent default. */
38
38
  modeId?: string;
39
+ /** Ordered explicit Brain choices that must be applied before readiness. */
40
+ configSelections?: Array<{
41
+ configId: string;
42
+ value: string;
43
+ }>;
39
44
  /** Adapter-resolved live permission policy; separate from ACP agent-specific session modes. */
40
45
  permissionMode?: NonNullable<SessionSnapshot['permissionMode']>;
41
46
  /** Adapter-authenticated request-metadata vocabulary; never inferred from ACP `_meta`. */
@@ -1027,13 +1027,15 @@ export class AcpSession {
1027
1027
  const persisted = this.options.mode === 'resume' && existsSync(this.sessionFile)
1028
1028
  ? readFileSync(this.sessionFile, 'utf8').trim()
1029
1029
  : '';
1030
+ let advertisedConfigOptions;
1030
1031
  if (persisted && this.capabilities?.sessionCapabilities?.resume != null) {
1031
1032
  const resumed = await this.connection.agent.request(acp.methods.agent.session.resume, {
1032
1033
  sessionId: persisted,
1033
1034
  cwd: this.options.cwd,
1034
1035
  mcpServers: this.declaredMcpServers(),
1035
1036
  });
1036
- this.captureRuntimeMetadata(resumed.configOptions);
1037
+ advertisedConfigOptions = resumed.configOptions;
1038
+ this.captureRuntimeMetadata(advertisedConfigOptions);
1037
1039
  this.sessionId = persisted;
1038
1040
  }
1039
1041
  else if (persisted && this.capabilities?.loadSession) {
@@ -1046,7 +1048,8 @@ export class AcpSession {
1046
1048
  cwd: this.options.cwd,
1047
1049
  mcpServers: this.declaredMcpServers(),
1048
1050
  });
1049
- this.captureRuntimeMetadata(loaded.configOptions);
1051
+ advertisedConfigOptions = loaded.configOptions;
1052
+ this.captureRuntimeMetadata(advertisedConfigOptions);
1050
1053
  }
1051
1054
  finally {
1052
1055
  this.replaying = false;
@@ -1060,8 +1063,29 @@ export class AcpSession {
1060
1063
  ...(this.options.sessionMeta ? { _meta: this.options.sessionMeta } : {}),
1061
1064
  });
1062
1065
  this.sessionId = created.sessionId;
1063
- this.captureRuntimeMetadata(created.configOptions);
1066
+ advertisedConfigOptions = created.configOptions;
1067
+ this.captureRuntimeMetadata(advertisedConfigOptions);
1064
1068
  }
1069
+ for (const selection of this.options.configSelections ?? []) {
1070
+ if (!advertisedConfigOptions?.some(option => option.id === selection.configId))
1071
+ throw new Error(`ACP agent did not advertise required session config option '${selection.configId}'`);
1072
+ let configured;
1073
+ try {
1074
+ configured = await this.connection.agent.request(acp.methods.agent.session.setConfigOption, { sessionId: this.sessionId, configId: selection.configId, value: selection.value });
1075
+ }
1076
+ catch (error) {
1077
+ throw new Error(`ACP agent refused required session config option '${selection.configId}' value '${selection.value}': `
1078
+ + (error instanceof Error ? error.message : String(error)));
1079
+ }
1080
+ advertisedConfigOptions = configured.configOptions;
1081
+ this.captureRuntimeMetadata(advertisedConfigOptions);
1082
+ const applied = advertisedConfigOptions.find(option => option.id === selection.configId);
1083
+ if (applied?.currentValue !== selection.value)
1084
+ throw new Error(`ACP agent did not apply required session config option '${selection.configId}' value '${selection.value}'`);
1085
+ }
1086
+ // Do not persist a session until every required Brain choice is live. A
1087
+ // failed startup closes the ACP session; persisting its id first would make
1088
+ // a later resume repeatedly target that invalid session.
1065
1089
  writeFileSync(this.sessionFile, this.sessionId + '\n', { mode: 0o600 });
1066
1090
  // Deliver the configured permission mode whichever way the session came up
1067
1091
  // (new, resume or load) — the launch flag never reaches an ACP agent. A
package/dist/spawn.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { IsolationConfig } from './isolation/types.js';
2
- import { type ApprovalMode, type FilesystemMode, type ResolvedRole, type RoleConfig, type MonitorConfig, type SessionBackendId, type UnattendedMode, type RoomMemberStartup } from './config.js';
2
+ import { type ApprovalMode, type FilesystemMode, type ResolvedRole, type MonitorConfig, type UnattendedMode, type RoomMemberStartup, type AgentDefinition, type AgentSelection } from './config.js';
3
3
  import { type OpsDeps } from './ops.js';
4
4
  import { type CreationDeps, type CreationProvenance } from './creation.js';
5
5
  import './harness/claude-code.js';
@@ -13,34 +13,18 @@ export declare let lastProvenance: CreationProvenance | undefined;
13
13
  export interface SpawnOpts {
14
14
  name: string;
15
15
  temp?: boolean;
16
- harness?: string;
17
- session?: SessionBackendId;
18
- mission?: string;
19
- missionFile?: string;
16
+ brain?: AgentSelection;
17
+ role?: AgentSelection;
18
+ /** Trusted canonical definition used by room provisioning; never a second schema. */
19
+ agentDefinition?: AgentDefinition;
20
20
  identity?: string;
21
21
  cwd?: string;
22
22
  coordinator?: string;
23
- /** null explicitly selects the harness default; undefined retains normal fleet inheritance. */
24
- model?: string | null;
25
- permissionMode?: string;
26
23
  approval?: ApprovalMode;
27
24
  filesystem?: FilesystemMode;
28
25
  unattended?: UnattendedMode;
29
- sandbox?: string;
30
- profile?: string;
31
- launcher?: string;
32
- search?: boolean;
33
- codexConfig?: Record<string, string | number | boolean>;
34
- reasoningEffort?: string | null;
35
- addDirs?: string[];
36
- monitor?: boolean;
37
26
  /** Typed external monitor configuration used by trusted creation surfaces. */
38
27
  monitorConfig?: Partial<MonitorConfig>;
39
- bioFile?: string;
40
- personaFile?: string;
41
- /** Inline profile values for trusted typed callers such as the local web service. */
42
- bio?: string;
43
- persona?: string;
44
28
  /** Internal, non-sensitive provenance correlation for typed presentation layers. */
45
29
  surface?: 'cli' | 'web' | 'agent';
46
30
  creationActionId?: string;
@@ -61,12 +45,7 @@ export interface SpawnOpts {
61
45
  dryRun?: boolean;
62
46
  json?: boolean;
63
47
  }
64
- export declare function profileValues(o: SpawnOpts): {
65
- bio?: string;
66
- persona?: string;
67
- };
68
- /** Pure option-to-role mapping shared by CLI and application services. */
69
- export declare function buildRoleConfig(o: SpawnOpts, defaultHarness?: string): RoleConfig;
48
+ export declare function agentDefinitionFromSpawn(o: SpawnOpts): AgentDefinition;
70
49
  /**
71
50
  * Read and validate an `--isolation-file`. The file is the existing
72
51
  * `isolation:` mapping and nothing else — the same schema, the same validator
@@ -78,8 +57,6 @@ export declare function buildRoleConfig(o: SpawnOpts, defaultHarness?: string):
78
57
  */
79
58
  export declare function readIsolationFile(path: string): IsolationConfig;
80
59
  export declare function validateSpawnOpts(o: SpawnOpts): void;
81
- /** Read mission text without trimming or newline rewriting. */
82
- export declare function readMissionFile(path: string): string;
83
60
  /** The ours identity a spawn will bind: explicit, else the role name. */
84
61
  export declare const effectiveIdentity: (o: SpawnOpts) => string;
85
62
  export interface SpawnDryRun {
@@ -88,8 +65,6 @@ export interface SpawnDryRun {
88
65
  roleDocument: Record<string, unknown>;
89
66
  resolvedRole: ResolvedRole;
90
67
  }
91
- /** Bare Agent document written by v2 spawn: inline Role × inline Brain + operations. */
92
- export declare function buildAgentDocument(raw: RoleConfig): Record<string, unknown>;
93
68
  /**
94
69
  * Validate and resolve a spawn without reserving names, contacting the daemon,
95
70
  * or writing state. Collision checks are necessarily a point-in-time snapshot.