@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.
- package/README.md +57 -121
- package/dist/application/fleet-query-service.js +0 -12
- package/dist/application/role-creation-service.js +10 -7
- package/dist/application/types.d.ts +0 -11
- package/dist/briefing.js +6 -15
- package/dist/build-info.json +6 -7
- package/dist/capabilities.d.ts +1 -3
- package/dist/capabilities.js +0 -3
- package/dist/cli.js +7 -83
- package/dist/config.d.ts +3 -11
- package/dist/config.js +15 -40
- package/dist/creation.d.ts +38 -22
- package/dist/creation.js +111 -24
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +46 -120
- package/dist/doctor.d.ts +5 -1
- package/dist/doctor.js +18 -11
- package/dist/fleet-proxy.d.ts +0 -5
- package/dist/harness/acp-agent.js +6 -11
- package/dist/harness/claude-code.js +11 -204
- package/dist/harness/codex.d.ts +1 -4
- package/dist/harness/codex.js +12 -74
- package/dist/harness/types.d.ts +4 -54
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -1
- package/dist/loops/manager.d.ts +1 -30
- package/dist/loops/manager.js +6 -69
- package/dist/loops/state.d.ts +0 -18
- package/dist/loops/state.js +0 -4
- package/dist/monitor.js +1 -1
- package/dist/ops.d.ts +3 -0
- package/dist/ops.js +8 -3
- package/dist/owner-channel/attachments.d.ts +25 -2
- package/dist/owner-channel/attachments.js +61 -5
- package/dist/owner-channel/channel.d.ts +29 -30
- package/dist/owner-channel/channel.js +291 -291
- package/dist/owner-channel/message-recovery.d.ts +25 -0
- package/dist/owner-channel/message-recovery.js +114 -0
- package/dist/owner-channel/notices.d.ts +0 -7
- package/dist/owner-channel/notices.js +0 -9
- package/dist/owner-channel/ours-client.d.ts +148 -0
- package/dist/owner-channel/ours-client.js +231 -0
- package/dist/resolved-plan.js +0 -1
- package/dist/runner.d.ts +0 -48
- package/dist/runner.js +94 -236
- package/dist/session/acp.d.ts +0 -104
- package/dist/session/acp.js +10 -213
- package/dist/session/conversation-normalizer.d.ts +0 -6
- package/dist/session/conversation-normalizer.js +10 -153
- package/dist/session/conversation-types.d.ts +4 -23
- package/dist/session/types.d.ts +0 -35
- package/dist/spawn.js +26 -33
- package/dist/supervisor/systemd.js +29 -2
- package/dist/watchdog/briefing.js +0 -7
- package/dist/watchdog/run.js +3 -3
- package/dist/web-app/assets/{TerminalView-C_G1ID2P.js → TerminalView-BAVk1Bot.js} +1 -1
- package/dist/web-app/assets/{index-BCBK78hw.js → index-C3S-xFRU.js} +5 -5
- package/dist/web-app/index.html +1 -1
- package/dist/worklog.d.ts +1 -7
- package/dist/worklog.js +39 -191
- package/package.json +3 -1
- package/dist/harness-plugins.d.ts +0 -48
- package/dist/harness-plugins.js +0 -309
- package/dist/model-env.d.ts +0 -71
- package/dist/model-env.js +0 -106
- package/dist/owner-channel/mcp.d.ts +0 -24
- package/dist/owner-channel/mcp.js +0 -145
- package/dist/session/activity.d.ts +0 -31
- 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,
|
|
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
|
-
|
|
414
|
-
|
|
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 (
|
|
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?:
|
|
133
|
+
worklog?: WorklogPolicy;
|
|
138
134
|
auth_proxy?: Partial<AuthProxyConfig>;
|
|
139
135
|
}
|
|
140
|
-
export interface ResolvedRole extends Omit<RoleConfig, 'model' | 'owner_channel'
|
|
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:
|
|
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
|
-
|
|
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,
|
|
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 (
|
|
464
|
+
if (defaults === undefined && role === undefined)
|
|
489
465
|
return undefined;
|
|
490
|
-
if (defaults !== undefined &&
|
|
491
|
-
throw new ConfigError(`${file}: defaults.worklog must be a map
|
|
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
|
|
469
|
+
throw new ConfigError(`${file}: role '${name}' worklog must be a map`);
|
|
494
470
|
const merged = {
|
|
495
|
-
...
|
|
496
|
-
...(
|
|
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)
|
package/dist/creation.d.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
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
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
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
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
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
|
-
|
|
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.
|
|
178
|
-
//
|
|
179
|
-
log(`identity '${name}' does not exist and this
|
|
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
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
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) {
|