@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.
- package/README.md +163 -21
- package/dist/agent-ours/bridge.d.ts +3 -0
- package/dist/agent-ours/bridge.js +178 -0
- package/dist/agent-ours/controller.d.ts +14 -0
- package/dist/agent-ours/controller.js +60 -0
- package/dist/agent-ours/file-rpc.d.ts +23 -0
- package/dist/agent-ours/file-rpc.js +123 -0
- package/dist/agent-ours/harness.d.ts +14 -0
- package/dist/agent-ours/harness.js +64 -0
- package/dist/agent-ours/mcp-endpoint.d.ts +15 -0
- package/dist/agent-ours/mcp-endpoint.js +111 -0
- package/dist/agent-ours/runtime.d.ts +53 -0
- package/dist/agent-ours/runtime.js +247 -0
- package/dist/agent-ours/service.d.ts +23 -0
- package/dist/agent-ours/service.js +364 -0
- package/dist/agent-ours/state.d.ts +45 -0
- package/dist/agent-ours/state.js +95 -0
- package/dist/agent-ours/wire.d.ts +14 -0
- package/dist/agent-ours/wire.js +55 -0
- package/dist/agent-recovery-gate.js +1 -1
- package/dist/application/legacy-supervisor-identity.d.ts +7 -0
- package/dist/application/legacy-supervisor-identity.js +43 -0
- package/dist/application/supervisor-ours-tools.d.ts +159 -0
- package/dist/application/supervisor-ours-tools.js +121 -0
- package/dist/application/task-room-service.js +55 -19
- package/dist/briefing.js +10 -59
- package/dist/build-info.json +6 -5
- package/dist/capabilities.d.ts +2 -1
- package/dist/capabilities.js +2 -0
- package/dist/cli.js +75 -8
- package/dist/client-profile.d.ts +18 -0
- package/dist/client-profile.js +130 -0
- package/dist/config.d.ts +1 -0
- package/dist/creation.d.ts +1 -1
- package/dist/creation.js +13 -21
- package/dist/daemon-recovery.d.ts +2 -0
- package/dist/daemon-recovery.js +53 -2
- package/dist/docs.d.ts +3 -3
- package/dist/docs.js +32 -23
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +70 -14
- package/dist/fleet-command-audit.js +3 -2
- package/dist/harness/agent-session.d.ts +2 -0
- package/dist/harness/claude-code-session.js +4 -2
- package/dist/harness/codex-session.js +9 -2
- package/dist/harness/codex.d.ts +3 -2
- package/dist/harness/codex.js +8 -3
- package/dist/harness/hermes-session.js +7 -1
- package/dist/init-wizard.d.ts +11 -6
- package/dist/init-wizard.js +33 -0
- package/dist/monitor.d.ts +34 -3
- package/dist/monitor.js +174 -74
- package/dist/owner-channel/channel.d.ts +3 -0
- package/dist/owner-channel/channel.js +28 -6
- package/dist/owner-channel/ours-client.d.ts +12 -5
- package/dist/owner-channel/ours-client.js +48 -12
- package/dist/rooms-tasks/archived-absence.d.ts +8 -0
- package/dist/rooms-tasks/archived-absence.js +48 -0
- package/dist/rooms-tasks/cli.js +1 -1
- package/dist/rooms-tasks/close.d.ts +3 -0
- package/dist/rooms-tasks/close.js +95 -17
- package/dist/rooms-tasks/cowork-adapter.d.ts +4 -0
- package/dist/rooms-tasks/cowork-adapter.js +10 -2
- package/dist/rooms-tasks/cowork-http.d.ts +3 -0
- package/dist/rooms-tasks/cowork-http.js +70 -0
- package/dist/rooms-tasks/deletion.js +40 -3
- package/dist/rooms-tasks/member-startup.d.ts +1 -0
- package/dist/rooms-tasks/member-startup.js +2 -0
- package/dist/rooms-tasks/provision.d.ts +11 -0
- package/dist/rooms-tasks/provision.js +138 -26
- package/dist/rooms-tasks/room-state.js +13 -0
- package/dist/rooms-tasks/task-state.d.ts +13 -3
- package/dist/rooms-tasks/task-state.js +42 -6
- package/dist/rooms-tasks/types.d.ts +12 -0
- package/dist/rooms-tasks/workspace-artifacts.d.ts +6 -0
- package/dist/rooms-tasks/workspace-artifacts.js +93 -0
- package/dist/rooms-tasks/workspace.d.ts +26 -0
- package/dist/rooms-tasks/workspace.js +195 -0
- package/dist/runner.d.ts +14 -0
- package/dist/runner.js +540 -494
- package/dist/session/codex-app-server-transport.js +1 -1
- package/dist/session/codex-app-server.js +31 -5
- package/dist/spawn.js +19 -16
- package/dist/startup-readiness.d.ts +13 -0
- package/dist/startup-readiness.js +38 -0
- package/dist/supervisor/launchd.js +8 -1
- package/dist/supervisor/systemd.js +5 -0
- package/dist/temp-supervisor-recovery.d.ts +3 -0
- package/dist/temp-supervisor-recovery.js +46 -0
- package/dist/watchdog/briefing.js +3 -16
- package/dist/watchdog/run.js +19 -5
- package/dist/web/server.d.ts +2 -0
- package/dist/web/server.js +18 -1
- package/docs/task-workspaces.md +98 -0
- package/package.json +8 -4
- 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('
|
|
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
|
|
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
|
-
|
|
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
|
|
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;
|
package/dist/creation.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
package/dist/daemon-recovery.js
CHANGED
|
@@ -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 {
|
|
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;
|