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

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 (50) hide show
  1. package/README.md +64 -73
  2. package/dist/application/role-creation-service.js +9 -2
  3. package/dist/application/role-removal-service.js +6 -3
  4. package/dist/application/types.d.ts +1 -1
  5. package/dist/briefing.js +2 -2
  6. package/dist/build-info.json +4 -4
  7. package/dist/cli.js +8 -5
  8. package/dist/config.d.ts +28 -2
  9. package/dist/config.js +307 -23
  10. package/dist/creation.d.ts +1 -1
  11. package/dist/creation.js +2 -2
  12. package/dist/docs.d.ts +1 -1
  13. package/dist/docs.js +31 -69
  14. package/dist/doctor.js +1 -1
  15. package/dist/generated-agent-source.d.ts +9 -0
  16. package/dist/generated-agent-source.js +53 -0
  17. package/dist/harness/agent-session.d.ts +13 -0
  18. package/dist/harness/claude-code-session.d.ts +4 -0
  19. package/dist/harness/claude-code-session.js +4 -0
  20. package/dist/harness/claude-code.js +10 -0
  21. package/dist/harness/codex-session.d.ts +4 -0
  22. package/dist/harness/codex-session.js +4 -0
  23. package/dist/harness/codex.js +14 -0
  24. package/dist/harness/registry.js +4 -0
  25. package/dist/init-guidance.d.ts +1 -0
  26. package/dist/init-guidance.js +5 -0
  27. package/dist/model-env.d.ts +0 -1
  28. package/dist/model-env.js +2 -4
  29. package/dist/ops.js +4 -2
  30. package/dist/resolved-plan.d.ts +3 -2
  31. package/dist/resolved-plan.js +21 -5
  32. package/dist/sensitive-config.d.ts +2 -0
  33. package/dist/sensitive-config.js +4 -0
  34. package/dist/spawn.d.ts +4 -4
  35. package/dist/spawn.js +52 -23
  36. package/dist/web/fleet-config-service.d.ts +11 -8
  37. package/dist/web/fleet-config-service.js +383 -183
  38. package/dist/web/topology-promote.js +17 -14
  39. package/dist/web/topology.js +2 -2
  40. package/dist/web/yaml-document-edit.js +2 -0
  41. package/dist/web-app/assets/index-BbE9EX6n.js +10 -0
  42. package/dist/web-app/index.html +1 -1
  43. package/examples/fleet/agents/Alice.yaml +11 -0
  44. package/examples/fleet/agents/FleetCoordinator.yaml +7 -0
  45. package/examples/fleet/brains/claude-default.yaml +5 -0
  46. package/examples/fleet/roles/coordinator.yaml +5 -0
  47. package/examples/fleet/roles/developer.yaml +3 -0
  48. package/examples/fleet.yaml +74 -0
  49. package/package.json +2 -1
  50. package/dist/web-app/assets/index-DhMDVRU1.js +0 -10
package/dist/model-env.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { getAdapter } from './harness/registry.js';
1
2
  /**
2
3
  * Which environment variable a harness reads to pin the model it RUNS.
3
4
  *
@@ -9,12 +10,9 @@
9
10
  * ACP role, and a fleet-wide `defaults.env.ANTHROPIC_MODEL` silently outranked
10
11
  * an explicitly requested one.
11
12
  */
12
- export const MODEL_ENV_BY_HARNESS = {
13
- 'claude-code': 'ANTHROPIC_MODEL',
14
- };
15
13
  /** The model-pin variable for a harness, or undefined if it pins no model by env. */
16
14
  export function modelEnvVar(harness) {
17
- return harness === undefined ? undefined : MODEL_ENV_BY_HARNESS[harness];
15
+ return harness === undefined ? undefined : getAdapter(harness).agentSession.modelEnvironmentVariable();
18
16
  }
19
17
  /**
20
18
  * Resolve a role's environment and its runtime model TOGETHER, so the two can
package/dist/ops.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync, rmSync, unlinkSync, } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { randomUUID, createHash } from 'node:crypto';
4
- import { agentDir, fleetDDir, watchdogsRoot } from './paths.js';
4
+ import { agentDir, watchdogsRoot } from './paths.js';
5
5
  import { findRole } from './config.js';
6
6
  import { getAdapter } from './harness/registry.js';
7
7
  import { generateBriefing } from './briefing.js';
@@ -9,6 +9,7 @@ import { resetRestartLedger } from './runner.js';
9
9
  import { archiveTempState, stopTempSupervisor, tempSupervisorLiveness, } from './temp-lifecycle.js';
10
10
  import { realExec } from './exec.js';
11
11
  import { daemonIdentityProvisioner, reconcilePermanentRoleIdentities, } from './creation.js';
12
+ import { provesGeneratedAgentSource } from './generated-agent-source.js';
12
13
  // Launch staggering now lives at the harness-launch point (the runner's start
13
14
  // gate, driven by `start_stagger_ms`), so it covers systemd host-boot too — not
14
15
  // just the `up`/`restart` command loop below. The old in-loop FLEET_START_STAGGER
@@ -264,9 +265,10 @@ export async function rmRole(cfg, name, deps) {
264
265
  return;
265
266
  }
266
267
  const role = findRole(cfg, name);
268
+ const generatedSource = provesGeneratedAgentSource(agentDir(name), cfg.files[0], role.sourceFile);
267
269
  await deps.backend.uninstall(name);
268
270
  rmSync(agentDir(name), { recursive: true, force: true });
269
- if (role.sourceFile.startsWith(fleetDDir() + '/')) {
271
+ if (generatedSource && existsSync(role.sourceFile)) {
270
272
  unlinkSync(role.sourceFile);
271
273
  deps.log(`removed ${role.sourceFile}`);
272
274
  }
@@ -1,5 +1,6 @@
1
1
  import type { FleetConfig, ResolvedRole } from './config.js';
2
- export declare const RESOLVED_PLAN_SCHEMA_VERSION = 1;
2
+ export declare const RESOLVED_PLAN_SCHEMA_VERSION = 2;
3
+ export declare const redactSensitive: (value: unknown, key?: string) => unknown;
3
4
  /** Stable, secret-safe machine contract consumed by config JSON and spawn dry-run. */
4
5
  export declare function resolvedPlan(cfg: FleetConfig): Record<string, unknown>;
5
- export declare function resolvedRolePlan(role: ResolvedRole): Record<string, unknown>;
6
+ export declare function resolvedRolePlan(role: ResolvedRole, manifestDir?: string): Record<string, unknown>;
@@ -1,8 +1,10 @@
1
1
  import { analyzeRolePermissions } from './permissions.js';
2
- export const RESOLVED_PLAN_SCHEMA_VERSION = 1;
2
+ import { dirname, relative } from 'node:path';
3
+ import { isSensitiveConfigKey } from './sensitive-config.js';
4
+ export const RESOLVED_PLAN_SCHEMA_VERSION = 2;
3
5
  const sortedObject = (value) => Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)));
4
- const redactSensitive = (value, key = '') => {
5
- if (/(?:secret|token|password|authorization|api[_-]?key)/i.test(key))
6
+ export const redactSensitive = (value, key = '') => {
7
+ if (isSensitiveConfigKey(key))
6
8
  return '<redacted>';
7
9
  if (Array.isArray(value))
8
10
  return value.map(item => redactSensitive(item));
@@ -14,12 +16,17 @@ const redactSensitive = (value, key = '') => {
14
16
  };
15
17
  /** Stable, secret-safe machine contract consumed by config JSON and spawn dry-run. */
16
18
  export function resolvedPlan(cfg) {
19
+ const manifestDir = dirname(cfg.files[0] ?? '.');
17
20
  return {
18
21
  schemaVersion: RESOLVED_PLAN_SCHEMA_VERSION,
22
+ configMode: cfg.configMode ?? 'split-v2',
19
23
  sourceFiles: [...cfg.files],
24
+ sourceDocuments: (cfg.sourceDocuments ?? []).map(document => ({
25
+ ...document, path: relative(manifestDir, document.path) || document.path,
26
+ })),
20
27
  startStaggerMs: cfg.startStaggerMs,
21
28
  diagnostics: cfg.diagnostics.map(diagnostic => ({ ...diagnostic })),
22
- roles: cfg.roles.map(resolvedRolePlan),
29
+ roles: cfg.roles.map(role => resolvedRolePlan(role, manifestDir)),
23
30
  loops: cfg.loops.map(loop => sortedObject({
24
31
  name: loop.name, selectors: [...loop.selectors], roles: [...loop.roleNames],
25
32
  intervalMs: loop.intervalMs, initialDelayMs: loop.initialDelayMs, jitterMs: loop.jitterMs,
@@ -36,16 +43,25 @@ export function resolvedPlan(cfg) {
36
43
  })),
37
44
  };
38
45
  }
39
- export function resolvedRolePlan(role) {
46
+ export function resolvedRolePlan(role, manifestDir = dirname(role.sourceFile)) {
40
47
  const analysis = analyzeRolePermissions(role);
41
48
  return {
42
49
  name: role.name,
43
50
  sourceFile: role.sourceFile,
51
+ references: Object.values(role.provenance ?? {}).flatMap(item => item.viaReference ? [item.viaReference] : [])
52
+ .filter((item, index, all) => all.findIndex(other => JSON.stringify(other) === JSON.stringify(item)) === index)
53
+ .sort((a, b) => a.kind.localeCompare(b.kind) || a.id.localeCompare(b.id)
54
+ || a.sourcePointer.localeCompare(b.sourcePointer)),
55
+ provenance: sortedObject(Object.fromEntries(Object.entries(role.provenance ?? {}).map(([field, item]) => [
56
+ field, { ...item, sourceFile: item.sourceKind === 'built-in'
57
+ ? item.sourceFile : relative(manifestDir, item.sourceFile) || item.sourceFile },
58
+ ]))),
44
59
  identity: role.identity,
45
60
  harness: role.harness,
46
61
  session: role.session,
47
62
  sessionOptions: role.session_options ?? null,
48
63
  model: role.model ?? null,
64
+ effort: role.effort ?? null,
49
65
  modelChain: role.model_chain ?? null,
50
66
  cwd: role.cwd ?? null,
51
67
  coordinator: role.coordinator ?? null,
@@ -0,0 +1,2 @@
1
+ /** Shared recursive configuration redaction boundary for operator-facing models. */
2
+ export declare function isSensitiveConfigKey(key: string): boolean;
@@ -0,0 +1,4 @@
1
+ /** Shared recursive configuration redaction boundary for operator-facing models. */
2
+ export function isSensitiveConfigKey(key) {
3
+ return /(?:secret|token|password|credential|private[_-]?key|auth|invite|api[_-]?key)/i.test(key);
4
+ }
package/dist/spawn.d.ts CHANGED
@@ -85,17 +85,17 @@ export declare const effectiveIdentity: (o: SpawnOpts) => string;
85
85
  export interface SpawnDryRun {
86
86
  schemaVersion: 1;
87
87
  warning: string;
88
- roleDocument: {
89
- roles: Record<string, RoleConfig>;
90
- };
88
+ roleDocument: Record<string, unknown>;
91
89
  resolvedRole: ResolvedRole;
92
90
  }
91
+ /** Bare Agent document written by v2 spawn: inline Role × inline Brain + operations. */
92
+ export declare function buildAgentDocument(raw: RoleConfig): Record<string, unknown>;
93
93
  /**
94
94
  * Validate and resolve a spawn without reserving names, contacting the daemon,
95
95
  * or writing state. Collision checks are necessarily a point-in-time snapshot.
96
96
  */
97
97
  export declare function spawnDryRun(o: SpawnOpts): SpawnDryRun;
98
- /** Permanent spawn: persist to ~/fleet.d/<Name>.yaml, then bring it up. */
98
+ /** Permanent spawn: persist one bare Agent document under the selected v2 root. */
99
99
  export declare function spawnPermanent(o: SpawnOpts, deps: OpsDeps, creation?: CreationDeps): Promise<string>;
100
100
  export type { SupervisorLauncher } from './temp-lifecycle.js';
101
101
  /** Temp spawn: live state under ~/.ours-fleet/tmp, independent transient supervision. */
package/dist/spawn.js CHANGED
@@ -2,14 +2,15 @@ import { spawn as spawnChild } from 'node:child_process';
2
2
  import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { parse, stringify } from 'yaml';
5
- import { agentDir, fleetDDir } from './paths.js';
5
+ import { agentDir, defaultConfigPath } from './paths.js';
6
6
  import { validateIsolationConfig } from './isolation/policy.js';
7
- import { loadConfig, resolveAuthProxy, resolveModelChain, resolveMonitorConfig, resolveOwnerChannelConfig, resolvePermissions, resolveRoleModel, resolveWorklogPolicy, validateMonitorConfig, } from './config.js';
7
+ import { loadConfig, resolveAuthProxy, resolveModelChain, resolveMonitorConfig, resolveOwnerChannelConfig, resolvePermissions, resolveRoleModel, resolveWorklogPolicy, splitRootFor, validateMonitorConfig, } from './config.js';
8
8
  import { resolveRoleModelEnv } from './model-env.js';
9
9
  import { applyRole, up } from './ops.js';
10
10
  import { START_STAGGER_FILE } from './runner.js';
11
11
  import { buildProvenance, daemonIdentityProvisioner, ensureIdentity, provenanceOf, withCreationTransaction, writeProvenance, writeRoleFile, } from './creation.js';
12
12
  import { VERSION } from './version.js';
13
+ import { recordGeneratedAgentSource } from './generated-agent-source.js';
13
14
  import './harness/claude-code.js';
14
15
  import './harness/codex.js';
15
16
  import { getAdapter } from './harness/registry.js';
@@ -34,8 +35,9 @@ export function profileValues(o) {
34
35
  /** Pure option-to-role mapping shared by CLI and application services. */
35
36
  export function buildRoleConfig(o, defaultHarness) {
36
37
  const r = {};
37
- if (o.harness)
38
- r.harness = o.harness;
38
+ const harness = o.harness ?? defaultHarness;
39
+ if (harness)
40
+ r.harness = harness;
39
41
  if (o.session)
40
42
  r.session = o.session;
41
43
  if (o.identity)
@@ -52,7 +54,8 @@ export function buildRoleConfig(o, defaultHarness) {
52
54
  r.model = null;
53
55
  else if (o.model?.trim())
54
56
  r.model = o.model.trim();
55
- const harness = o.harness ?? defaultHarness;
57
+ if (o.reasoningEffort?.trim())
58
+ r.effort = o.reasoningEffort.trim();
56
59
  const harnessOptions = {};
57
60
  if (o.permissionMode)
58
61
  harnessOptions[harness === 'claude-code' ? 'permission_mode' : 'approval'] = o.permissionMode;
@@ -65,12 +68,8 @@ export function buildRoleConfig(o, defaultHarness) {
65
68
  if (o.search === true)
66
69
  harnessOptions.search = true;
67
70
  const codexConfig = { ...(o.codexConfig ?? {}) };
68
- if (o.reasoningEffort && harness === 'codex')
69
- codexConfig.model_reasoning_effort = o.reasoningEffort;
70
71
  if (Object.keys(codexConfig).length)
71
72
  harnessOptions.config = codexConfig;
72
- if (o.reasoningEffort && harness === 'claude-code')
73
- harnessOptions.effort = o.reasoningEffort;
74
73
  if (o.addDirs?.length)
75
74
  harnessOptions.add_dirs = o.addDirs;
76
75
  if (o.monitor === true)
@@ -174,6 +173,23 @@ function assertNameFree(o) {
174
173
  }
175
174
  /** The ours identity a spawn will bind: explicit, else the role name. */
176
175
  export const effectiveIdentity = (o) => o.identity ?? o.name;
176
+ /** Bare Agent document written by v2 spawn: inline Role × inline Brain + operations. */
177
+ export function buildAgentDocument(raw) {
178
+ const role = Object.fromEntries(['mission', 'persona', 'bio', 'briefing_file']
179
+ .filter(key => raw[key] !== undefined)
180
+ .map(key => [key, raw[key]]));
181
+ const brain = Object.fromEntries([
182
+ 'harness', 'session', 'session_options', 'model', 'model_chain', 'max_tokens',
183
+ 'autocompact_pct', 'harness_options', 'effort',
184
+ ].filter(key => raw[key] !== undefined)
185
+ .map(key => [key, raw[key]]));
186
+ const operational = Object.fromEntries([
187
+ 'permissions', 'identity', 'cwd', 'coordinator', 'env', 'oversee', 'isolation',
188
+ 'monitor', 'owner_channel', 'worklog', 'auth_proxy',
189
+ ].filter(key => raw[key] !== undefined)
190
+ .map(key => [key, raw[key]]));
191
+ return { role: { inline: role }, brain: { inline: brain }, ...operational };
192
+ }
177
193
  /**
178
194
  * Validate and resolve a spawn without reserving names, contacting the daemon,
179
195
  * or writing state. Collision checks are necessarily a point-in-time snapshot.
@@ -186,7 +202,7 @@ export function spawnDryRun(o) {
186
202
  readMissionFile(o.missionFile);
187
203
  assertNameFree(o);
188
204
  const cfg = loadConfig(o.configPath);
189
- const raw = buildRoleConfig(o, cfg.defaults.harness);
205
+ const raw = buildRoleConfig(o, cfg.defaults.harness ?? 'claude-code');
190
206
  const harnessOptions = {
191
207
  ...(cfg.defaults.harness_options ?? {}),
192
208
  ...(raw.harness_options ?? {}),
@@ -204,23 +220,30 @@ export function spawnDryRun(o) {
204
220
  roleEnv: raw.env,
205
221
  ...(authProxy ? { authProxyBaseUrl: authProxy.base_url } : {}),
206
222
  });
207
- const model = modelEnv.model;
223
+ const adapter = getAdapter(harness);
224
+ const brain = adapter.agentSession.resolveBrain({
225
+ model: modelEnv.model,
226
+ effort: raw.effort,
227
+ harnessOptions: Object.keys(harnessOptions).length ? harnessOptions : undefined,
228
+ });
229
+ const model = brain.model ?? undefined;
208
230
  const session = raw.session ?? cfg.defaults.session ?? 'acp';
209
231
  const resolvedRole = {
210
232
  ...raw,
211
233
  name: o.name,
212
- sourceFile: o.temp ? '(temp dry-run)' : join(fleetDDir(), `${o.name}.yaml`),
234
+ sourceFile: o.temp ? '(temp dry-run)' : join(splitRootFor(o.configPath ?? defaultConfigPath()), 'agents', `${o.name}.yaml`),
213
235
  harness,
214
236
  session,
215
237
  session_options: raw.session_options,
216
238
  permissions: resolvePermissions(cfg.defaults.permissions, raw.permissions),
217
239
  permissionsDeclared: raw.permissions !== undefined || cfg.defaults.permissions !== undefined,
218
240
  identity: effectiveIdentity(o),
241
+ effort: raw.effort,
219
242
  model,
220
243
  model_chain: resolveModelChain(model, raw.model_chain ?? (inheritsModelDefaults
221
244
  ? cfg.defaults.model_chain
222
245
  : undefined)),
223
- harness_options: Object.keys(harnessOptions).length ? harnessOptions : undefined,
246
+ harness_options: brain.harnessOptions,
224
247
  isolation: raw.isolation ?? cfg.defaults.isolation,
225
248
  monitor: resolveMonitorConfig(cfg.defaults.monitor, raw.monitor),
226
249
  owner_channel: resolveOwnerChannelConfig(cfg.defaults.owner_channel, raw.owner_channel, session),
@@ -228,7 +251,6 @@ export function spawnDryRun(o) {
228
251
  auth_proxy: authProxy,
229
252
  };
230
253
  resolvedRole.env = modelEnv.env;
231
- const adapter = getAdapter(resolvedRole.harness);
232
254
  if (resolvedRole.auth_proxy && resolvedRole.harness !== 'claude-code')
233
255
  throw new Error('auth_proxy is supported only by claude-code');
234
256
  const optionProblems = adapter.validateOptions(resolvedRole.harness_options, resolvedRole);
@@ -237,7 +259,7 @@ export function spawnDryRun(o) {
237
259
  return {
238
260
  schemaVersion: 1,
239
261
  warning: 'collision checks are a snapshot; a real spawn reserves names atomically',
240
- roleDocument: { roles: { [o.name]: raw } },
262
+ roleDocument: buildAgentDocument(raw),
241
263
  resolvedRole,
242
264
  };
243
265
  }
@@ -278,7 +300,7 @@ function provenanceSettings(o, defaults) {
278
300
  monitor: tagged('monitorConfig', provenanceOf(o.monitorConfig, defaults.monitor, { mode: 'fleet' })),
279
301
  };
280
302
  }
281
- /** Permanent spawn: persist to ~/fleet.d/<Name>.yaml, then bring it up. */
303
+ /** Permanent spawn: persist one bare Agent document under the selected v2 root. */
282
304
  export async function spawnPermanent(o, deps, creation = {}) {
283
305
  validateSpawnOpts(o);
284
306
  if (o.isolationFile)
@@ -307,12 +329,11 @@ export async function spawnPermanent(o, deps, creation = {}) {
307
329
  await creation.identityProvisioner?.remove?.(effectiveIdentity(o));
308
330
  },
309
331
  });
310
- mkdirSync(fleetDDir(), { recursive: true });
332
+ const agentRoot = join(splitRootFor(o.configPath ?? defaultConfigPath()), 'agents');
333
+ mkdirSync(agentRoot, { recursive: true });
311
334
  creation.onStage?.('writing_role');
312
- const file = join(fleetDDir(), `${o.name}.yaml`);
313
- writeRoleFile(tx, file, stringify({
314
- roles: { [o.name]: buildRoleConfig(o, cfg.defaults.harness) },
315
- }));
335
+ const file = join(agentRoot, `${o.name}.yaml`);
336
+ writeRoleFile(tx, file, stringify(buildAgentDocument(buildRoleConfig(o, cfg.defaults.harness ?? 'claude-code'))));
316
337
  // `up` materialises the state dir and registers the service. Journal the
317
338
  // dir before it exists so a failure leaves the name genuinely reusable
318
339
  // rather than blocked by a half-built directory.
@@ -343,6 +364,7 @@ export async function spawnPermanent(o, deps, creation = {}) {
343
364
  });
344
365
  mkdirSync(agentDir(o.name), { recursive: true });
345
366
  writeProvenance(agentDir(o.name), provenance);
367
+ recordGeneratedAgentSource(agentDir(o.name), o.configPath ?? defaultConfigPath(), file);
346
368
  creation.onStage?.('registering_supervisor');
347
369
  await up(loadConfig(o.configPath), [o.name], {
348
370
  ...deps,
@@ -418,7 +440,13 @@ async function spawnTempInner(o, binPath, launch, tx, onStage) {
418
440
  roleEnv: fromOpts.env,
419
441
  ...(tempAuthProxy ? { authProxyBaseUrl: tempAuthProxy.base_url } : {}),
420
442
  });
421
- const model = modelEnv.model;
443
+ const adapter = getAdapter(harness);
444
+ const brain = adapter.agentSession.resolveBrain({
445
+ model: modelEnv.model,
446
+ effort: fromOpts.effort,
447
+ harnessOptions: Object.keys(mergedHarnessOptions).length ? mergedHarnessOptions : undefined,
448
+ });
449
+ const model = brain.model ?? undefined;
422
450
  const session = o.session ?? cfg.defaults.session ?? 'acp';
423
451
  const role = {
424
452
  ...fromOpts, // includes `isolation` when --isolation-file was given
@@ -426,11 +454,12 @@ async function spawnTempInner(o, binPath, launch, tx, onStage) {
426
454
  harness,
427
455
  session,
428
456
  identity: o.identity ?? o.name,
457
+ effort: fromOpts.effort,
429
458
  model,
430
459
  model_chain: resolveModelChain(model, fromOpts.model_chain ?? (inheritsModelDefaults
431
460
  ? cfg.defaults.model_chain
432
461
  : undefined)),
433
- harness_options: Object.keys(mergedHarnessOptions).length ? mergedHarnessOptions : undefined,
462
+ harness_options: brain.harnessOptions,
434
463
  permissions: resolvePermissions(cfg.defaults.permissions, fromOpts.permissions),
435
464
  permissionsDeclared: fromOpts.permissions !== undefined || cfg.defaults.permissions !== undefined,
436
465
  // Temp agents inherit the fleet-wide monitor defaults via the snapshot.
@@ -1,10 +1,13 @@
1
1
  import type { PrereqReport } from '../harness/types.js';
2
2
  export declare const REDACTED_ENV_VALUE = "__OURS_FLEET_SECRET_REDACTED__";
3
- export type EditableFleetModel = Record<string, unknown>;
3
+ export interface EditableFleetModel {
4
+ manifest: Record<string, unknown>;
5
+ agents: Record<string, Record<string, unknown>>;
6
+ }
4
7
  export interface ConfigReadResult {
5
8
  path: string;
6
- exists: boolean;
7
- firstRun: boolean;
9
+ exists: true;
10
+ firstRun: false;
8
11
  revision: string;
9
12
  model: EditableFleetModel;
10
13
  redactions: string[];
@@ -33,15 +36,15 @@ export interface ConfigWriteResult extends ConfigPreviewResult {
33
36
  export interface FleetConfigServiceOptions {
34
37
  configPath?: string;
35
38
  preflight?(configPath: string): Promise<PrereqReport>;
39
+ /** Fault-injection seam; called under lock immediately before each mutation. */
40
+ beforeMutation?(relativePath: string, index: number): void;
36
41
  }
37
42
  export declare class FleetConfigService {
38
43
  readonly path: string;
39
44
  private readonly preflight;
45
+ private readonly beforeMutation?;
40
46
  constructor(options?: FleetConfigServiceOptions);
41
47
  read(): ConfigReadResult;
42
- preview(baseRevision: string, model: unknown): Promise<ConfigPreviewResult>;
43
- write(baseRevision: string, model: unknown): Promise<ConfigWriteResult>;
44
- private readSource;
45
- private assertRevision;
46
- private validateCandidate;
48
+ preview(baseRevision: string, input: unknown): Promise<ConfigPreviewResult>;
49
+ write(baseRevision: string, input: unknown): Promise<ConfigWriteResult>;
47
50
  }