@ours.network/fleet 1.1.5 → 1.2.0-nightly.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +163 -21
  2. package/dist/agent-ours/bridge.d.ts +3 -0
  3. package/dist/agent-ours/bridge.js +178 -0
  4. package/dist/agent-ours/controller.d.ts +14 -0
  5. package/dist/agent-ours/controller.js +60 -0
  6. package/dist/agent-ours/file-rpc.d.ts +23 -0
  7. package/dist/agent-ours/file-rpc.js +123 -0
  8. package/dist/agent-ours/harness.d.ts +14 -0
  9. package/dist/agent-ours/harness.js +64 -0
  10. package/dist/agent-ours/mcp-endpoint.d.ts +15 -0
  11. package/dist/agent-ours/mcp-endpoint.js +111 -0
  12. package/dist/agent-ours/runtime.d.ts +53 -0
  13. package/dist/agent-ours/runtime.js +247 -0
  14. package/dist/agent-ours/service.d.ts +23 -0
  15. package/dist/agent-ours/service.js +364 -0
  16. package/dist/agent-ours/state.d.ts +45 -0
  17. package/dist/agent-ours/state.js +95 -0
  18. package/dist/agent-ours/wire.d.ts +14 -0
  19. package/dist/agent-ours/wire.js +55 -0
  20. package/dist/agent-recovery-gate.js +1 -1
  21. package/dist/application/legacy-supervisor-identity.d.ts +7 -0
  22. package/dist/application/legacy-supervisor-identity.js +43 -0
  23. package/dist/application/supervisor-ours-tools.d.ts +159 -0
  24. package/dist/application/supervisor-ours-tools.js +121 -0
  25. package/dist/application/task-room-service.js +55 -19
  26. package/dist/briefing.js +10 -59
  27. package/dist/build-info.json +6 -5
  28. package/dist/capabilities.d.ts +2 -1
  29. package/dist/capabilities.js +2 -0
  30. package/dist/cli.js +75 -8
  31. package/dist/client-profile.d.ts +18 -0
  32. package/dist/client-profile.js +130 -0
  33. package/dist/config.d.ts +1 -0
  34. package/dist/creation.d.ts +1 -1
  35. package/dist/creation.js +13 -21
  36. package/dist/daemon-recovery.d.ts +2 -0
  37. package/dist/daemon-recovery.js +53 -2
  38. package/dist/docs.d.ts +3 -3
  39. package/dist/docs.js +32 -23
  40. package/dist/doctor.d.ts +1 -1
  41. package/dist/doctor.js +70 -14
  42. package/dist/fleet-command-audit.js +3 -2
  43. package/dist/harness/agent-session.d.ts +2 -0
  44. package/dist/harness/claude-code-session.js +4 -2
  45. package/dist/harness/codex-session.js +9 -2
  46. package/dist/harness/codex.d.ts +3 -2
  47. package/dist/harness/codex.js +8 -3
  48. package/dist/harness/hermes-session.js +7 -1
  49. package/dist/init-wizard.d.ts +11 -6
  50. package/dist/init-wizard.js +33 -0
  51. package/dist/monitor.d.ts +34 -3
  52. package/dist/monitor.js +174 -74
  53. package/dist/owner-channel/channel.d.ts +3 -0
  54. package/dist/owner-channel/channel.js +28 -6
  55. package/dist/owner-channel/ours-client.d.ts +12 -5
  56. package/dist/owner-channel/ours-client.js +48 -12
  57. package/dist/rooms-tasks/archived-absence.d.ts +8 -0
  58. package/dist/rooms-tasks/archived-absence.js +48 -0
  59. package/dist/rooms-tasks/cli.js +1 -1
  60. package/dist/rooms-tasks/close.d.ts +3 -0
  61. package/dist/rooms-tasks/close.js +95 -17
  62. package/dist/rooms-tasks/cowork-adapter.d.ts +4 -0
  63. package/dist/rooms-tasks/cowork-adapter.js +10 -2
  64. package/dist/rooms-tasks/cowork-http.d.ts +3 -0
  65. package/dist/rooms-tasks/cowork-http.js +70 -0
  66. package/dist/rooms-tasks/deletion.js +40 -3
  67. package/dist/rooms-tasks/member-startup.d.ts +1 -0
  68. package/dist/rooms-tasks/member-startup.js +2 -0
  69. package/dist/rooms-tasks/provision.d.ts +11 -0
  70. package/dist/rooms-tasks/provision.js +138 -26
  71. package/dist/rooms-tasks/room-state.js +13 -0
  72. package/dist/rooms-tasks/task-state.d.ts +13 -3
  73. package/dist/rooms-tasks/task-state.js +42 -6
  74. package/dist/rooms-tasks/types.d.ts +12 -0
  75. package/dist/rooms-tasks/workspace-artifacts.d.ts +6 -0
  76. package/dist/rooms-tasks/workspace-artifacts.js +93 -0
  77. package/dist/rooms-tasks/workspace.d.ts +26 -0
  78. package/dist/rooms-tasks/workspace.js +195 -0
  79. package/dist/runner.d.ts +14 -0
  80. package/dist/runner.js +540 -494
  81. package/dist/session/codex-app-server-transport.js +1 -1
  82. package/dist/session/codex-app-server.js +31 -5
  83. package/dist/spawn.js +19 -16
  84. package/dist/startup-readiness.d.ts +13 -0
  85. package/dist/startup-readiness.js +38 -0
  86. package/dist/supervisor/launchd.js +8 -1
  87. package/dist/supervisor/systemd.js +5 -0
  88. package/dist/temp-supervisor-recovery.d.ts +3 -0
  89. package/dist/temp-supervisor-recovery.js +46 -0
  90. package/dist/watchdog/briefing.js +3 -16
  91. package/dist/watchdog/run.js +19 -5
  92. package/dist/web/server.d.ts +2 -0
  93. package/dist/web/server.js +18 -1
  94. package/docs/task-workspaces.md +98 -0
  95. package/package.json +8 -4
  96. package/presets/fleet/roles/Coordinator.yaml +22 -0
package/dist/cli.js CHANGED
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import { waitForRoleDaemon } from './startup-readiness.js';
3
+ import { SupervisorOursTools } from './application/supervisor-ours-tools.js';
4
+ import { runTempSupervisor, TEMP_RECYCLE_EXIT } from './temp-supervisor-recovery.js';
2
5
  import { spawn as spawnChild } from 'node:child_process';
3
6
  import { randomUUID } from 'node:crypto';
4
7
  import { existsSync, mkdirSync, readFileSync, statSync } from 'node:fs';
@@ -9,7 +12,7 @@ import { Command } from 'commander';
9
12
  import { VERSION } from './version.js';
10
13
  import { INIT_COMPLETION_GUIDANCE } from './init-guidance.js';
11
14
  import { migrateLegacyStarterPresets, migratePackagedRoleDefaults } from './preset-migration.js';
12
- import { executeInitWizard, isInteractiveTerminal, publishSetup, TerminalPrompter, } from './init-wizard.js';
15
+ import { executeInitAnswers, executeInitWizard, isInteractiveTerminal, publishSetup, readInitSettings, TerminalPrompter, } from './init-wizard.js';
13
16
  import { analyzeInstalls, buildInfo, buildLabel, discoverInstalls, runningLabel, } from './provenance.js';
14
17
  import { agentDir, agentsRoot, tmpRoot, logsRoot, deriveXdgRuntimeDir, defaultConfigPath } from './paths.js';
15
18
  import { findRole, loadConfig, ROLE_NAME_RE } from './config.js';
@@ -17,7 +20,7 @@ import { formatDuration } from './duration.js';
17
20
  import { redactSensitive, resolvedPlan, resolvedRolePlan } from './resolved-plan.js';
18
21
  import { pickBackend } from './supervisor/index.js';
19
22
  import { up, down } from './ops.js';
20
- import { readRestartLedger, runSupervised, runTemp } from './runner.js';
23
+ import { readRestartLedger, runSupervised, runTemp, SupervisorRecycleRequiredError } from './runner.js';
21
24
  import { executeWatchdogRun, runWatchdogAgent } from './watchdog/run.js';
22
25
  import { readSchedulerState, resetSchedulerState, runScheduler } from './watchdog/scheduler.js';
23
26
  import { partitionRestartNames } from './watchdog/config.js';
@@ -495,6 +498,40 @@ program.command('peek <name> [lines]').description('pane snapshot without attach
495
498
  die(controlFailure(name, 'peek', e));
496
499
  }
497
500
  });
501
+ const oursToolsCommand = program.command('ours').description('invoke tools through a named agent supervisor and its fixed identity');
502
+ oursToolsCommand.command('tools <agent>').description('list the same tools and schemas exposed to the agent MCP')
503
+ .action(async (agent) => {
504
+ try {
505
+ console.log(JSON.stringify(await new SupervisorOursTools().list(agent), null, 2));
506
+ }
507
+ catch (error) {
508
+ die(error);
509
+ }
510
+ });
511
+ oursToolsCommand.command('call <agent> <tool>').description('call a supervisor MCP tool without prompting the agent')
512
+ .option('--args-file <path>', 'JSON object arguments from a private file; omitted means {}')
513
+ .action(async (agent, tool, options) => {
514
+ try {
515
+ let args = {};
516
+ if (options.argsFile) {
517
+ try {
518
+ args = JSON.parse(readFileSync(options.argsFile, 'utf8'));
519
+ }
520
+ catch {
521
+ throw new Error('cannot read tool arguments: expected a readable JSON object file');
522
+ }
523
+ }
524
+ const response = await new SupervisorOursTools().call(agent, { tool, arguments: args });
525
+ console.log(JSON.stringify(response, null, 2));
526
+ if ('isError' in response.result && response.result.isError)
527
+ throw new FleetCliExit(1, 'runtime', 'unknown');
528
+ }
529
+ catch (error) {
530
+ if (error instanceof FleetCliExit)
531
+ throw error;
532
+ die(error);
533
+ }
534
+ });
498
535
  program.command('send <name> [text...]').description("type into the agent's console")
499
536
  .action(async (name, text, opts) => {
500
537
  const stateDir = acpStateDir(name);
@@ -1269,13 +1306,14 @@ cOpt(program.command('doctor').description('prerequisite report'))
1269
1306
  if (!rep.ok)
1270
1307
  throw new FleetCliExit(1);
1271
1308
  });
1272
- cOpt(program.command('init').description('interactively add missing Fleet defaults while preserving existing configuration'))
1309
+ cOpt(program.command('init').description('add missing Fleet defaults while preserving existing configuration')
1310
+ .option('--settings <file>', 'validated InitAnswers JSON; runs without interactive questions'))
1273
1311
  .action(async (opts) => {
1274
1312
  const configuration = opts.configuration ?? defaultConfigPath();
1275
- if (!isInteractiveTerminal(process.stdin, process.stdout))
1313
+ if (!opts.settings && !isInteractiveTerminal(process.stdin, process.stdout))
1276
1314
  die('ours-fleet init requires an interactive terminal; no host setup ran and no configuration changed');
1277
1315
  try {
1278
- const result = await executeInitWizard(new TerminalPrompter(process.stdin, process.stdout), configuration, {
1316
+ const initDeps = {
1279
1317
  async hostSetup() {
1280
1318
  for (const d of [agentsRoot(), tmpRoot(), logsRoot()])
1281
1319
  mkdirSync(d, { recursive: true });
@@ -1283,10 +1321,13 @@ cOpt(program.command('init').description('interactively add missing Fleet defaul
1283
1321
  console.log(message);
1284
1322
  },
1285
1323
  publish: publishSetup,
1286
- });
1324
+ };
1325
+ const result = opts.settings
1326
+ ? await executeInitAnswers(readInitSettings(opts.settings), configuration, initDeps)
1327
+ : await executeInitWizard(new TerminalPrompter(process.stdin, process.stdout), configuration, initDeps);
1287
1328
  if (!result) {
1288
1329
  console.log('Fleet setup cancelled. No host setup ran and no configuration changed.');
1289
- return;
1330
+ throw new FleetCliExit(1);
1290
1331
  }
1291
1332
  const prior = result.manifestExisted && result.rootExisted ? 'manifest and split configuration'
1292
1333
  : result.manifestExisted ? 'manifest only'
@@ -1297,6 +1338,8 @@ cOpt(program.command('init').description('interactively add missing Fleet defaul
1297
1338
  console.log(INIT_COMPLETION_GUIDANCE);
1298
1339
  }
1299
1340
  catch (error) {
1341
+ if (error instanceof FleetCliExit)
1342
+ throw error;
1300
1343
  die(error);
1301
1344
  }
1302
1345
  });
@@ -1537,6 +1580,16 @@ function configureWebAccess(opts) {
1537
1580
  registerTemplateCommands(program, cOpt);
1538
1581
  registerTaskCommands(program, cOpt);
1539
1582
  registerRoomCommands(program, cOpt);
1583
+ program.command('_wait-daemon <name>', { hidden: true }).description('internal: service startup readiness')
1584
+ .option('-c, --configuration <file>')
1585
+ .action(async (name, opts) => {
1586
+ try {
1587
+ await waitForRoleDaemon(name, opts.configuration);
1588
+ }
1589
+ catch (e) {
1590
+ die(e);
1591
+ }
1592
+ });
1540
1593
  program.command('_run <name>', { hidden: true }).description('internal: supervisor entrypoint')
1541
1594
  .option('-c, --configuration <file>')
1542
1595
  .action(async (name, opts) => {
@@ -1552,12 +1605,24 @@ program.command('_run <name>', { hidden: true }).description('internal: supervis
1552
1605
  program.command('_run-temp <name>', { hidden: true }).description('internal: temp-agent entrypoint')
1553
1606
  .action(async (name) => {
1554
1607
  try {
1555
- await runTemp(name);
1608
+ await runTempSupervisor(name, process.argv[1]);
1556
1609
  }
1557
1610
  catch (e) {
1558
1611
  die(e);
1559
1612
  }
1560
1613
  });
1614
+ program.command('_run-temp-worker <name>', { hidden: true })
1615
+ .action(async (name) => {
1616
+ try {
1617
+ await runTemp(name);
1618
+ }
1619
+ catch (error) {
1620
+ if (error instanceof SupervisorRecycleRequiredError)
1621
+ process.exitCode = TEMP_RECYCLE_EXIT;
1622
+ else
1623
+ die(error);
1624
+ }
1625
+ });
1561
1626
  program.command('_run-watchdog <name>', { hidden: true })
1562
1627
  .description('internal: one watchdog agent run (no cleanup — parent harvests)')
1563
1628
  .action(async (name) => {
@@ -1593,6 +1658,8 @@ async function parseFleetCli() {
1593
1658
  await program.parseAsync(process.argv);
1594
1659
  }
1595
1660
  catch (error) {
1661
+ if (error instanceof FleetCliExit)
1662
+ throw error;
1596
1663
  const commander = error;
1597
1664
  if (commander.exitCode === 0)
1598
1665
  return;
@@ -0,0 +1,18 @@
1
+ export interface ExplicitClientProfile {
2
+ readonly endpoint: string;
3
+ readonly expectedInstanceId: string;
4
+ readonly credentialPath: string;
5
+ readonly configPath: string;
6
+ readonly serverUrl?: string;
7
+ }
8
+ export declare class ClientProfileError extends Error {
9
+ constructor(message: string);
10
+ }
11
+ export declare function clientConfigPath(env: NodeJS.ProcessEnv): string;
12
+ /**
13
+ * Read Fleet's selected host-client profile without opening its credential.
14
+ * A legacy daemon config remains legacy only when none of the profile fields is
15
+ * present. Explicit and managed selections fail closed on invalid input.
16
+ */
17
+ export declare function readClientProfile(env: NodeJS.ProcessEnv): ExplicitClientProfile | undefined;
18
+ export declare function clientProfileKey(profile: ExplicitClientProfile): string;
@@ -0,0 +1,130 @@
1
+ import { normalizeDaemonEndpoint } from '@ours.network/sdk/client';
2
+ import { lstatSync, readFileSync, statSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { isAbsolute, join } from 'node:path';
5
+ const PROFILE_FIELDS = ['endpoint', 'expectedInstanceId', 'credentialPath'];
6
+ const LOWERCASE_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
7
+ const LEGACY_CONFIG_FIELDS = ['apiToken', 'stateDir', 'port'];
8
+ const LEGACY_ENV_FIELDS = ['OURS_API_TOKEN', 'OURS_STATE_DIR', 'OURS_PORT', 'OURS_DAEMON_ID'];
9
+ export class ClientProfileError extends Error {
10
+ constructor(message) {
11
+ super(message);
12
+ this.name = 'ClientProfileError';
13
+ }
14
+ }
15
+ export function clientConfigPath(env) {
16
+ if (env.OURS_CONFIG !== undefined)
17
+ return env.OURS_CONFIG;
18
+ const managedPath = join(env.HOME || homedir(), '.ours-client', 'profile.json');
19
+ try {
20
+ lstatSync(managedPath);
21
+ return managedPath;
22
+ }
23
+ catch (error) {
24
+ if (error.code !== 'ENOENT')
25
+ throw invalid(managedPath, 'could not be read');
26
+ return join(homedir(), '.ours', 'config.json');
27
+ }
28
+ }
29
+ function invalid(path, detail) {
30
+ return new ClientProfileError(`ours client profile ${JSON.stringify(path)} ${detail}`);
31
+ }
32
+ /**
33
+ * Read Fleet's selected host-client profile without opening its credential.
34
+ * A legacy daemon config remains legacy only when none of the profile fields is
35
+ * present. Explicit and managed selections fail closed on invalid input.
36
+ */
37
+ export function readClientProfile(env) {
38
+ const configPath = clientConfigPath(env);
39
+ const managed = configPath === join(env.HOME || homedir(), '.ours-client', 'profile.json');
40
+ const named = env.OURS_CONFIG !== undefined || managed;
41
+ let text;
42
+ try {
43
+ text = readFileSync(configPath, 'utf8');
44
+ }
45
+ catch {
46
+ if (named)
47
+ throw invalid(configPath, 'could not be read');
48
+ return undefined;
49
+ }
50
+ let parsed;
51
+ try {
52
+ parsed = JSON.parse(text);
53
+ }
54
+ catch {
55
+ if (named)
56
+ throw invalid(configPath, 'is not valid JSON');
57
+ return undefined;
58
+ }
59
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
60
+ if (named)
61
+ throw invalid(configPath, 'must contain a JSON object');
62
+ return undefined;
63
+ }
64
+ const row = parsed;
65
+ if (!PROFILE_FIELDS.some(field => Object.prototype.hasOwnProperty.call(row, field))) {
66
+ if (managed)
67
+ throw invalid(configPath, 'must contain a complete client profile');
68
+ return undefined;
69
+ }
70
+ let metadata;
71
+ try {
72
+ metadata = statSync(configPath);
73
+ }
74
+ catch {
75
+ throw invalid(configPath, 'could not be inspected');
76
+ }
77
+ if (!metadata.isFile())
78
+ throw invalid(configPath, 'must be a regular file');
79
+ const getuid = process.getuid;
80
+ if (getuid && metadata.uid !== getuid())
81
+ throw invalid(configPath, 'must be owned by this user');
82
+ if ((metadata.mode & 0o077) !== 0)
83
+ throw invalid(configPath, 'must not be accessible by group or other users');
84
+ for (const field of PROFILE_FIELDS)
85
+ if (typeof row[field] !== 'string' || !row[field].trim())
86
+ throw invalid(configPath, `has an invalid or missing ${field}`);
87
+ for (const field of LEGACY_CONFIG_FIELDS)
88
+ if (Object.prototype.hasOwnProperty.call(row, field))
89
+ throw invalid(configPath, `cannot combine ${field} with explicit profile selection`);
90
+ for (const field of LEGACY_ENV_FIELDS)
91
+ if (env[field]?.trim())
92
+ throw invalid(configPath, `cannot combine ${field} with explicit profile selection`);
93
+ const endpoint = row.endpoint.trim();
94
+ let url;
95
+ try {
96
+ url = new URL(endpoint);
97
+ }
98
+ catch {
99
+ throw invalid(configPath, 'has an invalid endpoint');
100
+ }
101
+ if ((url.protocol !== 'http:' && url.protocol !== 'https:') || url.username || url.password
102
+ || url.search || url.hash)
103
+ throw invalid(configPath, 'endpoint must be an HTTP or HTTPS base URL');
104
+ const expectedInstanceId = row.expectedInstanceId.trim();
105
+ if (!LOWERCASE_UUID.test(expectedInstanceId))
106
+ throw invalid(configPath, 'expectedInstanceId must be a lowercase UUID');
107
+ const credentialPath = row.credentialPath.trim();
108
+ if (!isAbsolute(credentialPath))
109
+ throw invalid(configPath, 'credentialPath must be absolute');
110
+ let serverUrl;
111
+ if (Object.hasOwn(row, 'serverUrl')) {
112
+ if (typeof row.serverUrl !== 'string' || /[\s\\?#]/.test(row.serverUrl))
113
+ throw invalid(configPath, 'serverUrl must be a safe HTTP or HTTPS base URL');
114
+ try {
115
+ serverUrl = normalizeDaemonEndpoint(row.serverUrl);
116
+ }
117
+ catch {
118
+ throw invalid(configPath, 'serverUrl must be a safe HTTP or HTTPS base URL');
119
+ }
120
+ if (normalizeDaemonEndpoint(endpoint) !== serverUrl + '/daemon')
121
+ throw invalid(configPath, 'endpoint must select serverUrl/daemon');
122
+ }
123
+ return Object.freeze({
124
+ ...(serverUrl === undefined ? {} : { serverUrl }),
125
+ endpoint: normalizeDaemonEndpoint(endpoint), expectedInstanceId, credentialPath, configPath,
126
+ });
127
+ }
128
+ export function clientProfileKey(profile) {
129
+ return `${profile.endpoint}#${profile.expectedInstanceId}`;
130
+ }
package/dist/config.d.ts CHANGED
@@ -178,6 +178,7 @@ export interface AgentTemplateInstance {
178
178
  }
179
179
  /** Internal first-boot payload for a Fleet-provisioned Cowork room member. */
180
180
  export interface RoomMemberStartup {
181
+ workspace?: import('./rooms-tasks/workspace.js').OwnedWorkspace;
181
182
  room_id: string;
182
183
  room_identity_cid: string;
183
184
  identity_name: string;
@@ -121,7 +121,7 @@ export type IdentityGuarantee = {
121
121
  detail: string;
122
122
  };
123
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'>;
124
+ export declare function reconcilePermanentRoleIdentities(role: ResolvedRole, provisioner?: IdentityProvisioner, log?: (line: string) => void, knownRoleGuarantee?: IdentityGuarantee['state']): Promise<'verified' | 'created' | 'unverified'>;
125
125
  /**
126
126
  * Establish the identity before the role's service is enabled.
127
127
  *
package/dist/creation.js CHANGED
@@ -1,9 +1,11 @@
1
+ import { preparePermanentAssignment } from './agent-ours/service.js';
1
2
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
2
3
  import { join } from 'node:path';
3
4
  import { randomUUID } from 'node:crypto';
4
5
  import { attachOursClient, } from '@ours.network/sdk/client';
5
6
  import { replaceFileAtomically, withFileLock } from './atomic-file.js';
6
7
  import { stateRoot } from './paths.js';
8
+ import { readClientProfile } from './client-profile.js';
7
9
  import { buildInfo, UNKNOWN_BUILD } from './provenance.js';
8
10
  /**
9
11
  * One creation transaction: role name and ours identity reserved together,
@@ -149,19 +151,7 @@ const requireGuaranteedIdentity = (role, identity, result) => {
149
151
  };
150
152
  /** Reconcile every permanent identity a role's supervisor owns before launch. */
151
153
  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);
154
+ const guarantee = await preparePermanentAssignment(role);
165
155
  if (role.owner_channel) {
166
156
  const channelIdentity = role.owner_channel.identity;
167
157
  const channel = await ensureIdentity(channelIdentity, {
@@ -171,7 +161,7 @@ export async function reconcilePermanentRoleIdentities(role, provisioner = daemo
171
161
  }, provisioner, log);
172
162
  requireGuaranteedIdentity(role, channelIdentity, channel);
173
163
  }
174
- return roleGuarantee.state;
164
+ return guarantee;
175
165
  }
176
166
  /**
177
167
  * Establish the identity before the role's service is enabled.
@@ -221,12 +211,17 @@ export async function ensureIdentity(name, profile, provisioner, log = () => { }
221
211
  };
222
212
  }
223
213
  export function daemonIdentityProvisioner(env = process.env, attachClient = attachOursClient, options = {}) {
214
+ const connect = (leaseToken) => {
215
+ const profile = readClientProfile(env);
216
+ return attachClient(profile ? {
217
+ endpoint: profile.endpoint, expectedInstanceId: profile.expectedInstanceId,
218
+ credentialPath: profile.credentialPath, sessionMode: 'external', leaseToken, env: {},
219
+ } : { env, leaseToken, clientPid: process.pid });
220
+ };
224
221
  const provisioner = {
225
222
  async exists(name) {
226
223
  try {
227
- const client = await attachClient({
228
- env, leaseToken: `ours-fleet-identity-preflight-${process.pid}`, clientPid: process.pid,
229
- });
224
+ const client = await connect(`ours-fleet-identity-preflight-${process.pid}`);
230
225
  const identities = await client.identities();
231
226
  if (!Array.isArray(identities))
232
227
  return 'unknown';
@@ -248,10 +243,7 @@ export function daemonIdentityProvisioner(env = process.env, attachClient = atta
248
243
  if (options.createPermanent === false)
249
244
  return provisioner;
250
245
  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
- });
246
+ const client = await connect(`ours-fleet-identity-create-${process.pid}-${randomUUID()}`);
255
247
  if (!client.listIdentities || !client.createIdentity || !client.releaseLease)
256
248
  throw new Error('the selected ours SDK client cannot create permanent identities');
257
249
  try {
@@ -1,3 +1,4 @@
1
+ import { type AttachOursClientOptions, type OursClient } from '@ours.network/sdk/client';
1
2
  import { type FetchLike } from './monitor.js';
2
3
  export declare const DAEMON_RECOVERY_MAX_ATTEMPTS = 6;
3
4
  export declare const DAEMON_RECOVERY_INITIAL_BACKOFF_MS = 1000;
@@ -31,6 +32,7 @@ export type DaemonGenerationObservation = {
31
32
  interface GenerationProbeDeps {
32
33
  readText?(path: string): string;
33
34
  canonicalize?(path: string): string;
35
+ attachClient?(options: AttachOursClientOptions): Pick<OursClient, 'version' | 'identities' | 'close'> | Promise<Pick<OursClient, 'version' | 'identities' | 'close'>>;
34
36
  }
35
37
  /**
36
38
  * Corroborate the loopback daemon's unauthenticated `/info`, its credentialed
@@ -1,7 +1,9 @@
1
1
  import { readFileSync, realpathSync } from 'node:fs';
2
2
  import { join, resolve } from 'node:path';
3
- import { createHash } from 'node:crypto';
4
- import { resolveEndpoint } from './monitor.js';
3
+ import { createHash, randomUUID } from 'node:crypto';
4
+ import { attachOursClient, } from '@ours.network/sdk/client';
5
+ import { readDaemonConfig, resolveEndpoint } from './monitor.js';
6
+ import { readClientProfile } from './client-profile.js';
5
7
  import { replaceFileAtomically } from './atomic-file.js';
6
8
  export const DAEMON_RECOVERY_MAX_ATTEMPTS = 6;
7
9
  export const DAEMON_RECOVERY_INITIAL_BACKOFF_MS = 1_000;
@@ -50,6 +52,9 @@ function canonical(path, deps) {
50
52
  * stale `ready` file can outlive the process that wrote it.
51
53
  */
52
54
  export async function probeDaemonGeneration(fetch, env, deps = {}) {
55
+ const profile = readClientProfile(env);
56
+ if (profile || env.OURS_DAEMON_ID)
57
+ return probeSelectedDaemon(env, deps, profile);
53
58
  const endpoint = resolveEndpoint(env);
54
59
  let response;
55
60
  try {
@@ -141,6 +146,52 @@ export async function probeDaemonGeneration(fetch, env, deps = {}) {
141
146
  },
142
147
  };
143
148
  }
149
+ /** Container selection uses public SDK reads; daemon paths/PIDs remain opaque metadata. */
150
+ async function probeSelectedDaemon(env, deps, profile) {
151
+ // Defer credential-file access to SDK selection, which checks the configured
152
+ // daemon identity/capability before it reads or sends the credential.
153
+ const endpoint = profile ? undefined : resolveEndpoint(env, false);
154
+ const token = profile ? undefined : env.OURS_API_TOKEN?.trim() || readDaemonConfig(env).apiToken;
155
+ let client;
156
+ try {
157
+ const options = profile ? {
158
+ endpoint: profile.endpoint, expectedInstanceId: profile.expectedInstanceId,
159
+ credentialPath: profile.credentialPath,
160
+ sessionMode: 'external', leaseToken: randomUUID(), env: {},
161
+ } : {
162
+ endpoint: endpoint.origin, expectedInstanceId: env.OURS_DAEMON_ID,
163
+ sessionMode: 'external', leaseToken: randomUUID(),
164
+ ...(token ? { token } : { credentialPath: join(endpoint.stateDir, 'daemon-token') }),
165
+ };
166
+ client = await (deps.attachClient?.(options) ?? attachOursClient(options));
167
+ const info = await client.version({ startup: true });
168
+ if (info.name !== 'ours' || !positiveInteger(info.pid)
169
+ || typeof info.stateDir !== 'string' || !info.stateDir)
170
+ return { state: 'unavailable', reason: 'DAEMON_INFO_INVALID' };
171
+ const progress = startupProgress(info.startup);
172
+ if (!progress)
173
+ return { state: 'unavailable', reason: 'DAEMON_PROGRESS_INVALID' };
174
+ if (progress.phase !== 'ready')
175
+ return { state: 'unavailable', reason: 'DAEMON_PROGRESS_NOT_READY' };
176
+ if (progress.pid !== info.pid)
177
+ return { state: 'unavailable', reason: 'DAEMON_GENERATION_MISMATCH' };
178
+ const identities = await client.identities();
179
+ if (!Array.isArray(identities))
180
+ return { state: 'unavailable', reason: 'DAEMON_IDENTITIES_INVALID' };
181
+ if (!identities.length)
182
+ return { state: 'unavailable', reason: 'DAEMON_IDENTITIES_NOT_READY' };
183
+ return { state: 'ready', generation: {
184
+ bootId: progress.bootId, pid: progress.pid, startedAt: progress.startedAt, stateDir: info.stateDir,
185
+ } };
186
+ }
187
+ catch {
188
+ // Failed selection, auth or transport is unavailable, never owner death.
189
+ return { state: 'unavailable', reason: 'DAEMON_SELECTED_PROBE_UNAVAILABLE' };
190
+ }
191
+ finally {
192
+ await client?.close();
193
+ }
194
+ }
144
195
  export class DaemonGenerationObserver {
145
196
  current;
146
197
  unavailable = false;