@ours.network/fleet 0.18.0 → 0.19.0-nightly.1

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 (69) hide show
  1. package/README.md +57 -121
  2. package/dist/application/fleet-query-service.js +0 -12
  3. package/dist/application/role-creation-service.js +10 -7
  4. package/dist/application/types.d.ts +0 -11
  5. package/dist/briefing.js +6 -15
  6. package/dist/build-info.json +6 -7
  7. package/dist/capabilities.d.ts +1 -3
  8. package/dist/capabilities.js +0 -3
  9. package/dist/cli.js +7 -83
  10. package/dist/config.d.ts +3 -11
  11. package/dist/config.js +15 -40
  12. package/dist/creation.d.ts +38 -22
  13. package/dist/creation.js +111 -24
  14. package/dist/docs.d.ts +1 -1
  15. package/dist/docs.js +46 -120
  16. package/dist/doctor.d.ts +5 -1
  17. package/dist/doctor.js +18 -11
  18. package/dist/fleet-proxy.d.ts +0 -5
  19. package/dist/harness/acp-agent.js +6 -11
  20. package/dist/harness/claude-code.js +11 -204
  21. package/dist/harness/codex.d.ts +1 -4
  22. package/dist/harness/codex.js +12 -74
  23. package/dist/harness/types.d.ts +4 -54
  24. package/dist/index.d.ts +0 -2
  25. package/dist/index.js +0 -1
  26. package/dist/loops/manager.d.ts +1 -30
  27. package/dist/loops/manager.js +6 -69
  28. package/dist/loops/state.d.ts +0 -18
  29. package/dist/loops/state.js +0 -4
  30. package/dist/monitor.js +1 -1
  31. package/dist/ops.d.ts +3 -0
  32. package/dist/ops.js +8 -3
  33. package/dist/owner-channel/attachments.d.ts +25 -2
  34. package/dist/owner-channel/attachments.js +61 -5
  35. package/dist/owner-channel/channel.d.ts +29 -30
  36. package/dist/owner-channel/channel.js +291 -291
  37. package/dist/owner-channel/message-recovery.d.ts +25 -0
  38. package/dist/owner-channel/message-recovery.js +114 -0
  39. package/dist/owner-channel/notices.d.ts +0 -7
  40. package/dist/owner-channel/notices.js +0 -9
  41. package/dist/owner-channel/ours-client.d.ts +148 -0
  42. package/dist/owner-channel/ours-client.js +231 -0
  43. package/dist/resolved-plan.js +0 -1
  44. package/dist/runner.d.ts +0 -48
  45. package/dist/runner.js +94 -236
  46. package/dist/session/acp.d.ts +0 -104
  47. package/dist/session/acp.js +10 -213
  48. package/dist/session/conversation-normalizer.d.ts +0 -6
  49. package/dist/session/conversation-normalizer.js +10 -153
  50. package/dist/session/conversation-types.d.ts +4 -23
  51. package/dist/session/types.d.ts +0 -35
  52. package/dist/spawn.js +26 -33
  53. package/dist/supervisor/systemd.js +29 -2
  54. package/dist/watchdog/briefing.js +0 -7
  55. package/dist/watchdog/run.js +3 -3
  56. package/dist/web-app/assets/{TerminalView-C_G1ID2P.js → TerminalView-BAVk1Bot.js} +1 -1
  57. package/dist/web-app/assets/{index-BCBK78hw.js → index-C3S-xFRU.js} +5 -5
  58. package/dist/web-app/index.html +1 -1
  59. package/dist/worklog.d.ts +1 -7
  60. package/dist/worklog.js +39 -191
  61. package/package.json +3 -1
  62. package/dist/harness-plugins.d.ts +0 -48
  63. package/dist/harness-plugins.js +0 -309
  64. package/dist/model-env.d.ts +0 -71
  65. package/dist/model-env.js +0 -106
  66. package/dist/owner-channel/mcp.d.ts +0 -24
  67. package/dist/owner-channel/mcp.js +0 -145
  68. package/dist/session/activity.d.ts +0 -31
  69. package/dist/session/activity.js +0 -48
package/dist/cli.js CHANGED
@@ -25,9 +25,8 @@ import { stringify } from 'yaml';
25
25
  import { resolvedRolePlan } from './resolved-plan.js';
26
26
  import { creationBuildNote, formatProvenance, readProvenance } from './creation.js';
27
27
  import { doctor } from './doctor.js';
28
- import { allWarnings, analyzeFleetPermissions, effectivePermissionMode, formatNative, } from './permissions.js';
28
+ import { allWarnings, analyzeFleetPermissions, formatNative } from './permissions.js';
29
29
  import { AI_DOCS } from './docs.js';
30
- import { classifyActivity, describeSessionState } from './session/activity.js';
31
30
  import { controlRequest, controlSocketPath, followControl, livenessNote, } from './session/control.js';
32
31
  import { SessionControlError } from './session/types.js';
33
32
  import { readScheduledLoops, storedLoopHealth } from './loops/state.js';
@@ -36,7 +35,6 @@ import { requestWebControl } from './web/control.js';
36
35
  import { WebServiceManager } from './web/service.js';
37
36
  import { WebAccessStore, passwordAccess, validatePublicOrigin } from './web/access.js';
38
37
  import { FLEET_PROXY_CALLER_ENV, FLEET_PROXY_STATE_DIR_ENV, } from './fleet-proxy.js';
39
- import { installHarnessPlugin, readHarnessPluginLock, selectedHarnessPluginIds, } from './harness-plugins.js';
40
38
  import './harness/claude-code.js'; // registers the claude-code adapter
41
39
  import './harness/codex.js'; // registers the codex adapter
42
40
  // sudo/su shells lack XDG_RUNTIME_DIR, breaking every systemctl/journalctl
@@ -67,51 +65,6 @@ const program = new Command()
67
65
  .version(VERSION);
68
66
  const cOpt = (cmd) => cmd.option('-c, --configuration <file>', 'config file (default: ~/fleet.yaml + ~/fleet.d/)');
69
67
  const collect = (value, previous) => [...previous, value];
70
- const pluginsCommand = program.command('plugins')
71
- .description('install exact, persisted stable/nightly harness plugins');
72
- const changeHarnessPlugins = (update) => async (names, opts) => {
73
- try {
74
- const cfg = loadConfig(opts.configuration);
75
- for (const harness of selectedHarnessPluginIds(names)) {
76
- const channel = cfg.harnessPlugins[harness].plugin_channel;
77
- const result = await installHarnessPlugin(harness, channel, { update });
78
- console.log(`${harness}: ${result.lock.package}@${result.lock.version} `
79
- + `(${channel}, ${result.resolved ? 'resolved and locked' : 'reused lock'})`);
80
- console.log(` lock: ${result.lockPath}`);
81
- console.log(` marketplace: ${result.marketplacePath}`);
82
- }
83
- }
84
- catch (e) {
85
- die(e);
86
- }
87
- };
88
- cOpt(pluginsCommand.command('install [harnesses...]')
89
- .description('install from the configured channel; reuse an existing same-channel lock'))
90
- .action(changeHarnessPlugins(false));
91
- cOpt(pluginsCommand.command('update [harnesses...]')
92
- .description('resolve configured channels once, persist exact versions, and install them'))
93
- .action(changeHarnessPlugins(true));
94
- cOpt(pluginsCommand.command('status [harnesses...]')
95
- .description('show configured channels and persisted exact versions'))
96
- .action((names, opts) => {
97
- try {
98
- const cfg = loadConfig(opts.configuration);
99
- for (const harness of selectedHarnessPluginIds(names)) {
100
- const configured = cfg.harnessPlugins[harness].plugin_channel;
101
- const lock = readHarnessPluginLock(harness);
102
- if (!lock) {
103
- console.log(`${harness}: configured=${configured}, unlocked `
104
- + `(run \`ours-fleet plugins install ${harness}\`)`);
105
- continue;
106
- }
107
- const mismatch = lock.channel === configured ? '' : `, CONFIG MISMATCH (locked=${lock.channel})`;
108
- console.log(`${harness}: configured=${configured}, ${lock.package}@${lock.version}${mismatch}`);
109
- }
110
- }
111
- catch (e) {
112
- die(e);
113
- }
114
- });
115
68
  program.command('docs')
116
69
  .alias('man')
117
70
  .description('print the complete AI-friendly command and configuration reference')
@@ -410,14 +363,8 @@ program.command('ls').description('list running fleet sessions')
410
363
  continue;
411
364
  try {
412
365
  const response = await controlRequest(stateDir, { command: 'status' }, 2_000);
413
- const result = response.result;
414
- if (response.ok && result?.alive) {
415
- // Activity, not readiness: an idle-readiness role may be running a
416
- // steered wake turn (FLEET-002), so `ls` reports what was observed.
417
- const observed = classifyActivity(result.activity);
418
- acp.push(`${name}: acp${observed.state === 'active' ? ' (working)'
419
- : observed.state === 'quiet' ? ' (no recent agent activity)' : ''}`);
420
- }
366
+ if (response.ok && response.result?.alive)
367
+ acp.push(`${name}: acp`);
421
368
  }
422
369
  catch { /* ignore stale sockets */ }
423
370
  }
@@ -558,14 +505,6 @@ program.command('status <name>').description('unit/agent state')
558
505
  else if (ledger.consecutiveImmediateFailures > 0)
559
506
  console.log(`restarts: ${ledger.consecutiveImmediateFailures} consecutive immediate `
560
507
  + `failures, next delay ${ledger.nextDelayMs}ms (${ledger.lastReason})`);
561
- // A kill the supervisor never saw leaves no other trace here. Say it out
562
- // loud rather than letting the absence of a failure streak read as "fine".
563
- if (ledger.lastTermination?.class === 'abrupt')
564
- console.log(`last termination: ABRUPT — the previous supervisor run (started `
565
- + `${ledger.lastTermination.runStartedAt ?? 'unknown'}) died without an orderly exit; `
566
- + `observed ${ledger.lastTermination.observedAt}`
567
- + `\n abrupt terminations recorded: ${ledger.abruptTerminations ?? 1}`
568
- + `\n cause (signal / OOM-kill): journalctl --user -u ours-fleet-agent@${name}.service`);
569
508
  const modelStatus = joinPath(agentDir(name), '.model-status');
570
509
  if (existsSync(modelStatus)) {
571
510
  try {
@@ -584,14 +523,8 @@ program.command('status <name>').description('unit/agent state')
584
523
  if (stateDir) {
585
524
  try {
586
525
  const response = await controlRequest(stateDir, { command: 'status' }, 2_000);
587
- if (response.ok) {
588
- const snapshot = response.result;
526
+ if (response.ok)
589
527
  console.log(`session: ${JSON.stringify(response.result)}`);
590
- // `readiness` is turn occupancy, never an activity claim: a steered
591
- // wake turn runs to completion with readiness pinned at `idle`
592
- // (FLEET-002). Say which question each field answers.
593
- console.log(describeSessionState(snapshot.readiness, snapshot.activity));
594
- }
595
528
  }
596
529
  catch {
597
530
  console.log('session: acp control unavailable');
@@ -702,10 +635,7 @@ function renderLoopRows(role, state, loop) {
702
635
  return Object.entries(state.loops).filter(([name]) => !loop || name === loop).map(([name, item]) => `${role}/${name} ${item.enabled && !item.operatorDisabled ? 'enabled' : 'disabled'} `
703
636
  + `${item.activeRunId ? 'running' : 'idle'} next=${item.nextDueAt} last=${item.lastOutcome ?? 'never'} `
704
637
  + `counts=${item.counts.started}/${item.counts.completed}/${item.counts.failed} `
705
- + `skip=${item.counts.skipped}(busy=${item.counts.skippedBusy},missed=${item.counts.skippedMissed})`
706
- + (item.missedGap
707
- ? ` gap=${item.missedGap.count}@${item.missedGap.fromAt}..${item.missedGap.throughAt}`
708
- : ''));
638
+ + `skip=${item.counts.skipped}(busy=${item.counts.skippedBusy},missed=${item.counts.skippedMissed})`);
709
639
  }
710
640
  cOpt(loopsCommand.command('status [role] [loop]').description('show live or stored loop state'))
711
641
  .option('--json', 'emit stable JSON')
@@ -1159,7 +1089,7 @@ cOpt(program.command('spawn [name]').description('spawn a new agent (permanent b
1159
1089
  .option('--coordinator <name>', 'announce target')
1160
1090
  .option('--model <id>', 'model id to launch on (e.g. claude-fable-5); default: launcher default')
1161
1091
  .option('--permission-mode <mode>', 'harness permission mode (Codex: untrusted|on-request|never; Claude: native values)')
1162
- .option('--approval <mode>', 'fleet permission mode: ask|auto|allow (Codex ACP allow selects agent-full-access; deny is deprecated)')
1092
+ .option('--approval <mode>', 'fleet permission mode: ask|auto|allow (deny is deprecated)')
1163
1093
  .option('--filesystem <mode>', 'common filesystem intent: read-only|workspace|unrestricted')
1164
1094
  .option('--unattended <mode>', 'permission behavior without a console: deny|wait')
1165
1095
  .option('--sandbox <mode>', 'Codex sandbox: read-only|workspace-write|danger-full-access')
@@ -1231,17 +1161,13 @@ cOpt(program.command('spawn [name]').description('spawn a new agent (permanent b
1231
1161
  console.log(`spawned ${result.lifetime} agent '${result.role}' through `
1232
1162
  + `${result.caller}'s fleet proxy (state: ${result.statePath})`);
1233
1163
  console.log(` ${result.harness}/${result.session}`
1234
- + `${result.model ? ` model=${result.model}` : ''}`
1235
- + (result.permissionMode
1236
- ? `; permission=${result.permissionMode.fleetMode} native=${result.permissionMode.nativeMode}`
1237
- : '') + '; '
1164
+ + `${result.model ? ` model=${result.model}` : ''}; `
1238
1165
  + `monitor=${result.monitor.mode} interrupt=${result.monitor.interrupt}`);
1239
1166
  if (result.inherited.length)
1240
1167
  console.log(` inherited omitted defaults from ${result.caller}: ${result.inherited.join(', ')}`);
1241
1168
  console.log(`→ watch it: ours-fleet peek ${result.role} | attach: ours-fleet attach ${result.role}`);
1242
1169
  return;
1243
1170
  }
1244
- const plannedPermissionMode = effectivePermissionMode(spawnDryRun(o).resolvedRole);
1245
1171
  if (o.temp) {
1246
1172
  const dir = await spawnTemp(o, binPath);
1247
1173
  console.log(`spawned temp agent '${roleName}' (state: ${dir}; gone on exit/reboot)`);
@@ -1259,8 +1185,6 @@ cOpt(program.command('spawn [name]').description('spawn a new agent (permanent b
1259
1185
  for (const line of formatProvenance(lastProvenance))
1260
1186
  console.log(line);
1261
1187
  }
1262
- console.log(` permission=${plannedPermissionMode.fleetMode} `
1263
- + `native=${plannedPermissionMode.nativeMode}`);
1264
1188
  console.log(`→ watch it: ours-fleet peek ${roleName} | attach: ours-fleet attach ${roleName}`);
1265
1189
  }
1266
1190
  catch (e) {
package/dist/config.d.ts CHANGED
@@ -2,7 +2,6 @@ import { type ConfigDiagnostic, type YamlMode } from './config-yaml.js';
2
2
  import type { IsolationConfig, WrapContext } from './isolation/types.js';
3
3
  import type { ResolvedWatchdog } from './watchdog/config.js';
4
4
  import type { ResolvedLoop, ResolvedRoleLoop } from './loops/config.js';
5
- import { type HarnessPluginChannel, type HarnessPluginConfigs } from './harness-plugins.js';
6
5
  export interface OverseeEntry {
7
6
  role: string;
8
7
  interval: string;
@@ -12,9 +11,6 @@ export interface WorklogPolicy {
12
11
  keep_tail_kb: number;
13
12
  max_archives: number;
14
13
  }
15
- export type WorklogPolicyInput = Partial<WorklogPolicy> | false;
16
- /** Conservative built-in policy; `worklog: false` is the explicit opt-out. */
17
- export declare const DEFAULT_WORKLOG_POLICY: Readonly<WorklogPolicy>;
18
14
  export interface AuthProxyConfig {
19
15
  kind: 'anthropic';
20
16
  base_url: string;
@@ -134,10 +130,10 @@ export interface RoleConfig {
134
130
  isolation?: IsolationConfig;
135
131
  monitor?: Partial<MonitorConfig>;
136
132
  owner_channel?: OwnerChannelConfigInput;
137
- worklog?: WorklogPolicyInput;
133
+ worklog?: WorklogPolicy;
138
134
  auth_proxy?: Partial<AuthProxyConfig>;
139
135
  }
140
- export interface ResolvedRole extends Omit<RoleConfig, 'model' | 'owner_channel' | 'worklog'> {
136
+ export interface ResolvedRole extends Omit<RoleConfig, 'model' | 'owner_channel'> {
141
137
  name: string;
142
138
  harness: string;
143
139
  session: SessionBackendId;
@@ -157,8 +153,6 @@ export interface ResolvedRole extends Omit<RoleConfig, 'model' | 'owner_channel'
157
153
  worklog?: WorklogPolicy;
158
154
  auth_proxy?: AuthProxyConfig;
159
155
  loops?: ResolvedRoleLoop[];
160
- /** Configured channel for a managed first-party harness plugin. */
161
- harnessPluginChannel?: HarnessPluginChannel;
162
156
  }
163
157
  export interface FleetConfig {
164
158
  roles: ResolvedRole[];
@@ -171,8 +165,6 @@ export interface FleetConfig {
171
165
  diagnostics: ConfigDiagnostic[];
172
166
  watchdogs: ResolvedWatchdog[];
173
167
  loops: ResolvedLoop[];
174
- /** Per-harness plugin channel intent. Exact versions live in host-local locks. */
175
- harnessPlugins: HarnessPluginConfigs;
176
168
  }
177
169
  export declare class ConfigError extends Error {
178
170
  }
@@ -201,7 +193,7 @@ export declare function canonicalCid(value: string): string;
201
193
  export declare function resolveOwnerChannelConfig(defaults: unknown, role: OwnerChannelConfigInput | undefined, session: SessionBackendId, file?: string, name?: string): OwnerChannelConfig | undefined;
202
194
  export declare function resolveModelChain(model: string | undefined, chain: string[] | undefined, file?: string, name?: string): string[] | undefined;
203
195
  export declare function resolveAuthProxy(defaults: unknown, role: Partial<AuthProxyConfig> | undefined, file?: string, name?: string): AuthProxyConfig | undefined;
204
- export declare function resolveWorklogPolicy(defaults: unknown, role: WorklogPolicyInput | undefined, file?: string, name?: string): WorklogPolicy | undefined;
196
+ export declare function resolveWorklogPolicy(defaults: unknown, role: WorklogPolicy | undefined, file?: string, name?: string): WorklogPolicy | undefined;
205
197
  export declare function resolvePermissions(defaults: unknown, role: Partial<CommonPermissions> | undefined, file?: string, name?: string): CommonPermissions;
206
198
  /**
207
199
  * Merge `defaults.monitor` under the role's own `monitor:` key-by-key, validate the
package/dist/config.js CHANGED
@@ -4,18 +4,10 @@ import { agentDir, defaultConfigPath, fleetDDir, home } from './paths.js';
4
4
  import { parseFleetDocument, } from './config-yaml.js';
5
5
  import { harnessRuntimeDir, resolveIsolation, validateIsolationConfig, } from './isolation/policy.js';
6
6
  import { getAdapter } from './harness/registry.js';
7
- import { resolveRoleModelEnv } from './model-env.js';
8
7
  import { resolveWatchdogs } from './watchdog/config.js';
9
8
  import { resolveLoops } from './loops/config.js';
10
9
  import { CAPABILITIES, CAP_MONITOR_INTERRUPT_AFTER_TOOL } from './capabilities.js';
11
10
  import { runningLabel } from './provenance.js';
12
- import { HARNESS_PLUGIN_IDS, resolveHarnessPluginConfigs, } from './harness-plugins.js';
13
- /** Conservative built-in policy; `worklog: false` is the explicit opt-out. */
14
- export const DEFAULT_WORKLOG_POLICY = Object.freeze({
15
- max_kb: 1024,
16
- keep_tail_kb: 256,
17
- max_archives: 12,
18
- });
19
11
  /** The 8 content-free event types the ours daemon appends to notifications.log. */
20
12
  export const NOTIFY_EVENT_TYPES = [
21
13
  'message_received', 'file_received', 'sibling_contact_added', 'local_contact_request',
@@ -183,13 +175,6 @@ export function loadConfig(configPath, options = {}) {
183
175
  const vars = (baseDoc.vars ?? {});
184
176
  const defaults = (baseDoc.defaults ?? {});
185
177
  const startStaggerMs = resolveStartStaggerMs(baseDoc.start_stagger_ms, base);
186
- let harnessPlugins;
187
- try {
188
- harnessPlugins = resolveHarnessPluginConfigs(baseDoc.harnesses, base);
189
- }
190
- catch (error) {
191
- throw new ConfigError(error.message);
192
- }
193
178
  const seen = new Map();
194
179
  const roles = [];
195
180
  for (const { file, doc } of docs) {
@@ -232,31 +217,22 @@ export function loadConfig(configPath, options = {}) {
232
217
  const harness = r.harness ?? defaults.harness ?? 'claude-code';
233
218
  const defaultHarness = defaults.harness ?? 'claude-code';
234
219
  const inheritsModelDefaults = harness === defaultHarness && r.model !== null;
235
- if (authProxy && harness !== 'claude-code')
236
- throw new ConfigError(`${file}: role '${name}' auth_proxy is supported only by claude-code`);
237
- // Environment and runtime model are resolved together so `model:` and the
238
- // harness's model pin can never disagree (see src/model-env.ts).
239
- const modelEnv = resolveRoleModelEnv({
240
- harness,
241
- model: resolveRoleModel(r.model, r.harness, defaults),
242
- modelWasExplicit: r.model !== undefined,
243
- defaultsEnv: (defaults.env ?? {}),
244
- roleEnv: r.env,
245
- ...(authProxy ? { authProxyBaseUrl: authProxy.base_url } : {}),
246
- }, message => new ConfigError(`${file}: role '${name}' ${message}`));
247
- const env = modelEnv.env;
248
- const model = modelEnv.model;
220
+ const model = resolveRoleModel(r.model, r.harness, defaults);
249
221
  const modelChain = resolveModelChain(model, r.model_chain ?? (inheritsModelDefaults
250
222
  ? defaults.model_chain
251
223
  : undefined), file, name);
224
+ if (authProxy && harness !== 'claude-code')
225
+ throw new ConfigError(`${file}: role '${name}' auth_proxy is supported only by claude-code`);
226
+ const env = {
227
+ ...(defaults.env ?? {}),
228
+ ...(r.env ?? {}),
229
+ ...(authProxy ? { ANTHROPIC_BASE_URL: authProxy.base_url } : {}),
230
+ };
252
231
  roles.push({
253
232
  ...r,
254
233
  name,
255
234
  sourceFile: file,
256
235
  harness,
257
- harnessPluginChannel: HARNESS_PLUGIN_IDS.includes(harness)
258
- ? harnessPlugins[harness].plugin_channel
259
- : undefined,
260
236
  session,
261
237
  session_options: sessionOptions,
262
238
  permissions,
@@ -293,7 +269,7 @@ export function loadConfig(configPath, options = {}) {
293
269
  for (const role of roles)
294
270
  role.loops = resolvedLoops.byRole.get(role.name) ?? [];
295
271
  return {
296
- roles, vars, defaults, files, startStaggerMs, diagnostics, watchdogs, harnessPlugins,
272
+ roles, vars, defaults, files, startStaggerMs, diagnostics, watchdogs,
297
273
  loops: resolvedLoops.loops,
298
274
  };
299
275
  }
@@ -485,16 +461,15 @@ export function resolveAuthProxy(defaults, role, file = 'config', name = 'role')
485
461
  };
486
462
  }
487
463
  export function resolveWorklogPolicy(defaults, role, file = 'config', name = 'role') {
488
- if (role === false || (role === undefined && defaults === false))
464
+ if (defaults === undefined && role === undefined)
489
465
  return undefined;
490
- if (defaults !== undefined && defaults !== false && !isPlainObject(defaults))
491
- throw new ConfigError(`${file}: defaults.worklog must be a map or false`);
466
+ if (defaults !== undefined && !isPlainObject(defaults))
467
+ throw new ConfigError(`${file}: defaults.worklog must be a map`);
492
468
  if (role !== undefined && !isPlainObject(role))
493
- throw new ConfigError(`${file}: role '${name}' worklog must be a map or false`);
469
+ throw new ConfigError(`${file}: role '${name}' worklog must be a map`);
494
470
  const merged = {
495
- ...DEFAULT_WORKLOG_POLICY,
496
- ...(defaults === false || defaults === undefined ? {} : defaults),
497
- ...(role === undefined ? {} : role),
471
+ ...(defaults ?? {}),
472
+ ...(role ?? {}),
498
473
  };
499
474
  const bad = Object.keys(merged).filter(key => !['max_kb', 'keep_tail_kb', 'max_archives'].includes(key));
500
475
  if (bad.length)
@@ -1,5 +1,6 @@
1
+ import { type AttachOursClientOptions, type OursClient } from '@ours.network/sdk/client';
1
2
  import { type LockDeps } from './atomic-file.js';
2
- import { type FetchLike } from './monitor.js';
3
+ import type { ResolvedRole } from './config.js';
3
4
  export type ReservationKind = 'role' | 'identity';
4
5
  export interface Reservation {
5
6
  kind: ReservationKind;
@@ -82,26 +83,30 @@ export declare function clearStaleReservations(olderThanMs?: number, now?: numbe
82
83
  * Identity provisioning (7.3). The fleet must know — before the harness starts
83
84
  * — whether the role's identity exists, and create it when it does not.
84
85
  *
85
- * `exists()` is answerable today: the daemon's authenticated `/identities`
86
- * endpoint is already used by doctor. `create()` is NOT: `ours-mcp` exposes only
87
- * `create-root`, and role identities are minted through the MCP `create_identity`
88
- * tool inside an agent session. So creation is a seam, injected by whoever can
89
- * satisfy it, and its absence is reported rather than papered over.
86
+ * `exists()` is answered through the typed SDK daemon inventory. `create()` is
87
+ * intentionally still a seam: fleet preserves application-owned identity
88
+ * creation/binding, especially the session-owned lifetime of temporary roles.
89
+ * Its absence is reported rather than papered over.
90
90
  */
91
91
  export interface IdentityProvisioner {
92
92
  /** Does this identity exist? `unknown` when the daemon could not be asked. */
93
93
  exists(name: string): Promise<boolean | 'unknown'>;
94
94
  /** Create it, publishing bio/persona through the same path. Absent = cannot. */
95
- create?(name: string, profile: {
96
- bio?: string;
97
- persona?: string;
98
- }): Promise<void>;
95
+ create?(name: string, profile: IdentityProvisionProfile): Promise<void>;
99
96
  /**
100
97
  * Undo a `create` during rollback. Only ever called for an identity THIS
101
98
  * transaction created; absent means "cannot", and the orphan is reported.
102
99
  */
103
100
  remove?(name: string): Promise<void>;
104
101
  }
102
+ export interface IdentityProvisionProfile {
103
+ bio?: string;
104
+ persona?: string;
105
+ /** Permanent role identities are locally discoverable; control identities opt out. */
106
+ exposeLocal?: boolean;
107
+ /** Permanent role identities accept sibling introductions; control identities opt out. */
108
+ localAutoAccept?: boolean;
109
+ }
105
110
  export type IdentityGuarantee = {
106
111
  state: 'verified';
107
112
  evidence: 'verified';
@@ -115,6 +120,8 @@ export type IdentityGuarantee = {
115
120
  evidence: 'missing' | 'unknown';
116
121
  detail: string;
117
122
  };
123
+ /** Reconcile every permanent identity a role's supervisor owns before launch. */
124
+ export declare function reconcilePermanentRoleIdentities(role: ResolvedRole, provisioner?: IdentityProvisioner, log?: (line: string) => void, knownRoleGuarantee?: IdentityGuarantee['state']): Promise<'verified' | 'created'>;
118
125
  /**
119
126
  * Establish the identity before the role's service is enabled.
120
127
  *
@@ -123,21 +130,29 @@ export type IdentityGuarantee = {
123
130
  * the failure a real agent hit on its first boot, having been told to bind an
124
131
  * identity that did not exist.
125
132
  */
126
- export declare function ensureIdentity(name: string, profile: {
127
- bio?: string;
128
- persona?: string;
129
- }, provisioner: IdentityProvisioner | undefined, log?: (line: string) => void): Promise<IdentityGuarantee>;
133
+ export declare function ensureIdentity(name: string, profile: IdentityProvisionProfile, provisioner: IdentityProvisioner | undefined, log?: (line: string) => void): Promise<IdentityGuarantee>;
130
134
  /**
131
- * Ask the running ours daemon whether an identity exists, over the same
132
- * authenticated endpoint doctor already probes. Answers `unknown` rather than
133
- * guessing when the daemon cannot be reached — an unreachable daemon is not
134
- * evidence that the identity is missing.
135
+ * Ask the running ours daemon whether an identity exists through SDK 2's
136
+ * coherence-checking attach path. Answers `unknown` rather than guessing when
137
+ * the daemon cannot be reached — an unreachable daemon is not evidence that
138
+ * the identity is missing.
135
139
  *
136
- * It deliberately has no `create()`: role identities are minted through the MCP
137
- * `create_identity` tool, and inventing a daemon endpoint we cannot test is the
138
- * failure mode this release exists to stop.
140
+ * Permanent identities can be created before a harness starts. Creation binds
141
+ * the provisioning lease, so it is always released before the role or channel
142
+ * takes ownership. Temporary callers disable creation: a temporary identity is
143
+ * owned and deleted by the exact connector lease that created it, and there is
144
+ * no safe lease-transfer operation in the current SDK.
139
145
  */
140
- export declare function daemonIdentityProvisioner(env?: NodeJS.ProcessEnv, fetchImpl?: FetchLike): IdentityProvisioner;
146
+ type IdentityInventoryClient = Pick<OursClient, 'identities'> & Partial<Pick<OursClient, 'listIdentities' | 'createIdentity' | 'setPersona' | 'releaseLease'>>;
147
+ export interface DaemonIdentityProvisionerOptions {
148
+ /** False for temporary roles/watchdogs whose connector must own creation. */
149
+ createPermanent?: boolean;
150
+ /** True only for inventory checks performed by a temporary lifecycle. */
151
+ acceptTemporaryExisting?: boolean;
152
+ }
153
+ export declare function daemonIdentityProvisioner(env?: NodeJS.ProcessEnv, attachClient?: (options: AttachOursClientOptions) => Promise<IdentityInventoryClient>, options?: DaemonIdentityProvisionerOptions): IdentityProvisioner;
154
+ /** Inventory-only variant for session-owned temporary identity lifecycles. */
155
+ export declare function daemonIdentityInventoryProvisioner(env?: NodeJS.ProcessEnv, attachClient?: (options: AttachOursClientOptions) => Promise<IdentityInventoryClient>): IdentityProvisioner;
141
156
  /** Atomically write a role's fleet.d file, journalling it for rollback. */
142
157
  export declare function writeRoleFile(tx: CreationTransaction, file: string, contents: string): void;
143
158
  /** Where a setting's effective value came from. */
@@ -207,3 +222,4 @@ export declare function creationBuildNote(p: CreationProvenance): string | undef
207
222
  export declare function formatProvenance(p: CreationProvenance): string[];
208
223
  /** Classify one setting: an explicit CLI value, a fleet default, or built-in. */
209
224
  export declare function provenanceOf(cliValue: unknown, fleetDefault: unknown, builtIn?: unknown): ProvenanceEntry;
225
+ export {};
package/dist/creation.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { attachOursClient, } from '@ours.network/sdk/client';
3
5
  import { replaceFileAtomically, withFileLock } from './atomic-file.js';
4
6
  import { stateRoot } from './paths.js';
5
- import { resolveEndpoint } from './monitor.js';
6
7
  import { buildInfo, UNKNOWN_BUILD } from './provenance.js';
7
8
  /**
8
9
  * One creation transaction: role name and ours identity reserved together,
@@ -141,6 +142,37 @@ function readdirSyncSafe(dir) {
141
142
  return [];
142
143
  }
143
144
  }
145
+ const requireGuaranteedIdentity = (role, identity, result) => {
146
+ if (result.state !== 'unverified')
147
+ return result;
148
+ throw new Error(`role '${role.name}': could not establish permanent ours identity '${identity}' before launch: ${result.detail}`);
149
+ };
150
+ /** Reconcile every permanent identity a role's supervisor owns before launch. */
151
+ export async function reconcilePermanentRoleIdentities(role, provisioner = daemonIdentityProvisioner(), log = () => { }, knownRoleGuarantee) {
152
+ const roleResult = knownRoleGuarantee
153
+ ? {
154
+ state: knownRoleGuarantee,
155
+ evidence: knownRoleGuarantee === 'verified' ? 'verified' : 'missing',
156
+ detail: `identity was ${knownRoleGuarantee} by the creation transaction`,
157
+ }
158
+ : await ensureIdentity(role.identity, {
159
+ bio: role.bio,
160
+ persona: role.persona,
161
+ exposeLocal: true,
162
+ localAutoAccept: true,
163
+ }, provisioner, log);
164
+ const roleGuarantee = requireGuaranteedIdentity(role, role.identity, roleResult);
165
+ if (role.owner_channel) {
166
+ const channelIdentity = role.owner_channel.identity;
167
+ const channel = await ensureIdentity(channelIdentity, {
168
+ bio: `Authenticated owner channel for the ours-fleet ${role.name} role.`,
169
+ exposeLocal: false,
170
+ localAutoAccept: false,
171
+ }, provisioner, log);
172
+ requireGuaranteedIdentity(role, channelIdentity, channel);
173
+ }
174
+ return roleGuarantee.state;
175
+ }
144
176
  /**
145
177
  * Establish the identity before the role's service is enabled.
146
178
  *
@@ -174,10 +206,9 @@ export async function ensureIdentity(name, profile, provisioner, log = () => { }
174
206
  detail: 'the ours daemon could not be asked',
175
207
  };
176
208
  if (!provisioner.create) {
177
- // Loud, and named. The briefing will tell the agent to mint it — which is
178
- // what actually happens today but nobody is told it was "predefined".
179
- log(`identity '${name}' does not exist and this host cannot create one automatically`
180
- + `the role will be told to mint it on first boot. Create it in advance to avoid that.`);
209
+ // Loud, and named. Permanent lifecycle callers fail closed here; temporary
210
+ // callers preserve connector-owned first-boot creation semantics.
211
+ log(`identity '${name}' does not exist and this provisioner cannot create one automatically`);
181
212
  return {
182
213
  state: 'unverified', evidence: 'missing',
183
214
  detail: 'it does not exist and cannot be created here',
@@ -189,29 +220,85 @@ export async function ensureIdentity(name, profile, provisioner, log = () => { }
189
220
  detail: 'created during spawn, with its bio and persona published',
190
221
  };
191
222
  }
192
- /**
193
- * Ask the running ours daemon whether an identity exists, over the same
194
- * authenticated endpoint doctor already probes. Answers `unknown` rather than
195
- * guessing when the daemon cannot be reached — an unreachable daemon is not
196
- * evidence that the identity is missing.
197
- *
198
- * It deliberately has no `create()`: role identities are minted through the MCP
199
- * `create_identity` tool, and inventing a daemon endpoint we cannot test is the
200
- * failure mode this release exists to stop.
201
- */
202
- export function daemonIdentityProvisioner(env = process.env, fetchImpl = (u, i) => globalThis.fetch(u, i)) {
203
- return {
223
+ export function daemonIdentityProvisioner(env = process.env, attachClient = attachOursClient, options = {}) {
224
+ const provisioner = {
204
225
  async exists(name) {
205
- const ep = resolveEndpoint(env);
206
- const resp = await fetchImpl(`${ep.origin}/identities`, { headers: ep.headers });
207
- if (!resp.ok)
208
- return 'unknown';
209
- const body = await resp.json();
210
- if (!Array.isArray(body.identities))
226
+ try {
227
+ const client = await attachClient({
228
+ env, leaseToken: `ours-fleet-identity-preflight-${process.pid}`, clientPid: process.pid,
229
+ });
230
+ const identities = await client.identities();
231
+ if (!Array.isArray(identities))
232
+ return 'unknown';
233
+ const existing = identities.find(identity => identity.name === name);
234
+ if (!existing)
235
+ return false;
236
+ // A session-owned temporary identity cannot satisfy a permanent role
237
+ // invariant. Report it as absent so create() can reject the collision
238
+ // explicitly after inspecting the authoritative identity tree.
239
+ if (existing.temporary && options.acceptTemporaryExisting !== true)
240
+ return false;
241
+ return true;
242
+ }
243
+ catch {
211
244
  return 'unknown';
212
- return body.identities.some(i => (typeof i === 'string' ? i : i?.name) === name);
245
+ }
213
246
  },
214
247
  };
248
+ if (options.createPermanent === false)
249
+ return provisioner;
250
+ provisioner.create = async (name, profile) => {
251
+ const client = await attachClient({
252
+ env, leaseToken: `ours-fleet-identity-create-${process.pid}-${randomUUID()}`,
253
+ clientPid: process.pid,
254
+ });
255
+ if (!client.listIdentities || !client.createIdentity || !client.releaseLease)
256
+ throw new Error('the selected ours SDK client cannot create permanent identities');
257
+ try {
258
+ const before = await client.listIdentities();
259
+ const existing = before.find(identity => identity.name === name);
260
+ if (existing) {
261
+ if (existing.temp)
262
+ throw new Error(`identity '${name}' exists but is temporary; refusing to convert or adopt it`);
263
+ return; // another reconciler won the create race
264
+ }
265
+ if (!before.some(identity => identity.kind === 'root'))
266
+ throw new Error(`cannot create role identity '${name}': this host has no Human identity; run ours onboarding first`);
267
+ let createdHere = false;
268
+ try {
269
+ await client.createIdentity({
270
+ name,
271
+ bio: profile.bio ?? '',
272
+ exposeLocal: profile.exposeLocal ?? true,
273
+ localAutoAccept: profile.localAutoAccept ?? true,
274
+ });
275
+ createdHere = true;
276
+ }
277
+ catch (error) {
278
+ // Creation is daemon-atomic. If a concurrent reconciler won by name,
279
+ // accept only the compatible permanent identity now visible.
280
+ const after = await client.listIdentities();
281
+ const raced = after.find(identity => identity.name === name);
282
+ if (!raced || raced.temp)
283
+ throw error;
284
+ }
285
+ if (createdHere && profile.persona && client.setPersona)
286
+ await client.setPersona({ persona: profile.persona });
287
+ }
288
+ finally {
289
+ // createIdentity binds this lease. The managed role/channel is the next
290
+ // owner, so never leave the short-lived provisioning client holding it.
291
+ await client.releaseLease();
292
+ }
293
+ };
294
+ return provisioner;
295
+ }
296
+ /** Inventory-only variant for session-owned temporary identity lifecycles. */
297
+ export function daemonIdentityInventoryProvisioner(env = process.env, attachClient = attachOursClient) {
298
+ return daemonIdentityProvisioner(env, attachClient, {
299
+ createPermanent: false,
300
+ acceptTemporaryExisting: true,
301
+ });
215
302
  }
216
303
  /** Atomically write a role's fleet.d file, journalling it for rollback. */
217
304
  export function writeRoleFile(tx, file, contents) {