@ours.network/fleet 0.18.1 → 0.19.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 +86 -90
- 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 +5 -5
- package/dist/cli.js +12 -37
- package/dist/config.d.ts +14 -6
- package/dist/config.js +58 -31
- package/dist/creation.d.ts +38 -22
- package/dist/creation.js +111 -24
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +71 -94
- package/dist/doctor.d.ts +5 -1
- package/dist/doctor.js +143 -12
- package/dist/fleet-proxy.d.ts +0 -5
- package/dist/harness/acp-agent.js +6 -11
- package/dist/harness/claude-code.js +11 -200
- package/dist/harness/codex.d.ts +1 -4
- package/dist/harness/codex.js +12 -70
- package/dist/harness/types.d.ts +4 -54
- 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 +36 -30
- package/dist/owner-channel/channel.js +338 -291
- package/dist/owner-channel/commands.d.ts +9 -0
- package/dist/owner-channel/commands.js +393 -0
- 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/rooms-tasks/cli.d.ts +4 -0
- package/dist/rooms-tasks/cli.js +1102 -0
- package/dist/rooms-tasks/close.d.ts +21 -0
- package/dist/rooms-tasks/close.js +159 -0
- package/dist/rooms-tasks/config.d.ts +15 -0
- package/dist/rooms-tasks/config.js +174 -0
- package/dist/rooms-tasks/cowork-adapter.d.ts +77 -0
- package/dist/rooms-tasks/cowork-adapter.js +272 -0
- package/dist/rooms-tasks/external-worker.d.ts +2 -0
- package/dist/rooms-tasks/external-worker.js +40 -0
- package/dist/rooms-tasks/index.d.ts +10 -0
- package/dist/rooms-tasks/index.js +10 -0
- package/dist/rooms-tasks/provision.d.ts +21 -0
- package/dist/rooms-tasks/provision.js +243 -0
- package/dist/rooms-tasks/room-state.d.ts +28 -0
- package/dist/rooms-tasks/room-state.js +221 -0
- package/dist/rooms-tasks/task-state.d.ts +43 -0
- package/dist/rooms-tasks/task-state.js +277 -0
- package/dist/rooms-tasks/templates.d.ts +6 -0
- package/dist/rooms-tasks/templates.js +94 -0
- package/dist/rooms-tasks/terminal.d.ts +23 -0
- package/dist/rooms-tasks/terminal.js +53 -0
- package/dist/rooms-tasks/types.d.ts +187 -0
- package/dist/rooms-tasks/types.js +20 -0
- 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/temp-lifecycle.d.ts +9 -2
- package/dist/temp-lifecycle.js +61 -0
- 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/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/config.d.ts
CHANGED
|
@@ -11,9 +11,6 @@ export interface WorklogPolicy {
|
|
|
11
11
|
keep_tail_kb: number;
|
|
12
12
|
max_archives: number;
|
|
13
13
|
}
|
|
14
|
-
export type WorklogPolicyInput = Partial<WorklogPolicy> | false;
|
|
15
|
-
/** Conservative built-in policy; `worklog: false` is the explicit opt-out. */
|
|
16
|
-
export declare const DEFAULT_WORKLOG_POLICY: Readonly<WorklogPolicy>;
|
|
17
14
|
export interface AuthProxyConfig {
|
|
18
15
|
kind: 'anthropic';
|
|
19
16
|
base_url: string;
|
|
@@ -133,10 +130,10 @@ export interface RoleConfig {
|
|
|
133
130
|
isolation?: IsolationConfig;
|
|
134
131
|
monitor?: Partial<MonitorConfig>;
|
|
135
132
|
owner_channel?: OwnerChannelConfigInput;
|
|
136
|
-
worklog?:
|
|
133
|
+
worklog?: WorklogPolicy;
|
|
137
134
|
auth_proxy?: Partial<AuthProxyConfig>;
|
|
138
135
|
}
|
|
139
|
-
export interface ResolvedRole extends Omit<RoleConfig, 'model' | 'owner_channel'
|
|
136
|
+
export interface ResolvedRole extends Omit<RoleConfig, 'model' | 'owner_channel'> {
|
|
140
137
|
name: string;
|
|
141
138
|
harness: string;
|
|
142
139
|
session: SessionBackendId;
|
|
@@ -168,6 +165,17 @@ export interface FleetConfig {
|
|
|
168
165
|
diagnostics: ConfigDiagnostic[];
|
|
169
166
|
watchdogs: ResolvedWatchdog[];
|
|
170
167
|
loops: ResolvedLoop[];
|
|
168
|
+
rooms?: import('./rooms-tasks/types.js').RoomsConfig;
|
|
169
|
+
roomTemplates?: import('./rooms-tasks/types.js').RoomTemplatesConfig;
|
|
170
|
+
tasks?: import('./rooms-tasks/types.js').TasksConfig;
|
|
171
|
+
/** SHA-256 fingerprint of the resolved owner invite (never the invite itself). */
|
|
172
|
+
ownerInviteFingerprint?: string;
|
|
173
|
+
/**
|
|
174
|
+
* Resolved owner invite for immediate private-IPC use only. This property is
|
|
175
|
+
* deliberately non-enumerable so config dumps and JSON output cannot expose
|
|
176
|
+
* the bearer credential.
|
|
177
|
+
*/
|
|
178
|
+
ownerInvite?: string;
|
|
171
179
|
}
|
|
172
180
|
export declare class ConfigError extends Error {
|
|
173
181
|
}
|
|
@@ -196,7 +204,7 @@ export declare function canonicalCid(value: string): string;
|
|
|
196
204
|
export declare function resolveOwnerChannelConfig(defaults: unknown, role: OwnerChannelConfigInput | undefined, session: SessionBackendId, file?: string, name?: string): OwnerChannelConfig | undefined;
|
|
197
205
|
export declare function resolveModelChain(model: string | undefined, chain: string[] | undefined, file?: string, name?: string): string[] | undefined;
|
|
198
206
|
export declare function resolveAuthProxy(defaults: unknown, role: Partial<AuthProxyConfig> | undefined, file?: string, name?: string): AuthProxyConfig | undefined;
|
|
199
|
-
export declare function resolveWorklogPolicy(defaults: unknown, role:
|
|
207
|
+
export declare function resolveWorklogPolicy(defaults: unknown, role: WorklogPolicy | undefined, file?: string, name?: string): WorklogPolicy | undefined;
|
|
200
208
|
export declare function resolvePermissions(defaults: unknown, role: Partial<CommonPermissions> | undefined, file?: string, name?: string): CommonPermissions;
|
|
201
209
|
/**
|
|
202
210
|
* Merge `defaults.monitor` under the role's own `monitor:` key-by-key, validate the
|
package/dist/config.js
CHANGED
|
@@ -4,17 +4,11 @@ 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';
|
|
9
|
+
import { validateRoomsConfig, validateTasksConfig, validateRoomTemplatesConfig, } from './rooms-tasks/config.js';
|
|
10
10
|
import { CAPABILITIES, CAP_MONITOR_INTERRUPT_AFTER_TOOL } from './capabilities.js';
|
|
11
11
|
import { runningLabel } from './provenance.js';
|
|
12
|
-
/** Conservative built-in policy; `worklog: false` is the explicit opt-out. */
|
|
13
|
-
export const DEFAULT_WORKLOG_POLICY = Object.freeze({
|
|
14
|
-
max_kb: 1024,
|
|
15
|
-
keep_tail_kb: 256,
|
|
16
|
-
max_archives: 12,
|
|
17
|
-
});
|
|
18
12
|
/** The 8 content-free event types the ours daemon appends to notifications.log. */
|
|
19
13
|
export const NOTIFY_EVENT_TYPES = [
|
|
20
14
|
'message_received', 'file_received', 'sibling_contact_added', 'local_contact_request',
|
|
@@ -171,9 +165,10 @@ export function loadConfig(configPath, options = {}) {
|
|
|
171
165
|
const parsed = parseFleetDocument(p, readFileSync(p, 'utf8'), options.yamlMode);
|
|
172
166
|
const doc = parsed.value;
|
|
173
167
|
diagnostics.push(...parsed.diagnostics);
|
|
174
|
-
const
|
|
168
|
+
const FLEET_D_ALLOWED = ['roles', 'rooms', 'room_templates', 'tasks'];
|
|
169
|
+
const extra = Object.keys(doc).filter(k => !FLEET_D_ALLOWED.includes(k));
|
|
175
170
|
if (extra.length)
|
|
176
|
-
throw new ConfigError(`${p}: fleet.d files may only define
|
|
171
|
+
throw new ConfigError(`${p}: fleet.d files may only define ${FLEET_D_ALLOWED.join(', ')}: (found: ${extra.join(', ')})`);
|
|
177
172
|
docs.push({ file: p, doc });
|
|
178
173
|
files.push(p);
|
|
179
174
|
}
|
|
@@ -224,23 +219,17 @@ export function loadConfig(configPath, options = {}) {
|
|
|
224
219
|
const harness = r.harness ?? defaults.harness ?? 'claude-code';
|
|
225
220
|
const defaultHarness = defaults.harness ?? 'claude-code';
|
|
226
221
|
const inheritsModelDefaults = harness === defaultHarness && r.model !== null;
|
|
227
|
-
|
|
228
|
-
throw new ConfigError(`${file}: role '${name}' auth_proxy is supported only by claude-code`);
|
|
229
|
-
// Environment and runtime model are resolved together so `model:` and the
|
|
230
|
-
// harness's model pin can never disagree (see src/model-env.ts).
|
|
231
|
-
const modelEnv = resolveRoleModelEnv({
|
|
232
|
-
harness,
|
|
233
|
-
model: resolveRoleModel(r.model, r.harness, defaults),
|
|
234
|
-
modelWasExplicit: r.model !== undefined,
|
|
235
|
-
defaultsEnv: (defaults.env ?? {}),
|
|
236
|
-
roleEnv: r.env,
|
|
237
|
-
...(authProxy ? { authProxyBaseUrl: authProxy.base_url } : {}),
|
|
238
|
-
}, message => new ConfigError(`${file}: role '${name}' ${message}`));
|
|
239
|
-
const env = modelEnv.env;
|
|
240
|
-
const model = modelEnv.model;
|
|
222
|
+
const model = resolveRoleModel(r.model, r.harness, defaults);
|
|
241
223
|
const modelChain = resolveModelChain(model, r.model_chain ?? (inheritsModelDefaults
|
|
242
224
|
? defaults.model_chain
|
|
243
225
|
: undefined), file, name);
|
|
226
|
+
if (authProxy && harness !== 'claude-code')
|
|
227
|
+
throw new ConfigError(`${file}: role '${name}' auth_proxy is supported only by claude-code`);
|
|
228
|
+
const env = {
|
|
229
|
+
...(defaults.env ?? {}),
|
|
230
|
+
...(r.env ?? {}),
|
|
231
|
+
...(authProxy ? { ANTHROPIC_BASE_URL: authProxy.base_url } : {}),
|
|
232
|
+
};
|
|
244
233
|
roles.push({
|
|
245
234
|
...r,
|
|
246
235
|
name,
|
|
@@ -281,10 +270,49 @@ export function loadConfig(configPath, options = {}) {
|
|
|
281
270
|
const resolvedLoops = resolveLoops(baseDoc.loops, base, roles, vars);
|
|
282
271
|
for (const role of roles)
|
|
283
272
|
role.loops = resolvedLoops.byRole.get(role.name) ?? [];
|
|
284
|
-
|
|
273
|
+
// ── Rooms, room_templates, tasks (split-config merge) ──────────────
|
|
274
|
+
// These sections may appear in the base fleet.yaml and/or in fleet.d files.
|
|
275
|
+
// Base provides defaults; fleet.d extends. Only one source may define rooms/tasks
|
|
276
|
+
// top-level (room_templates merge by name, last writer wins).
|
|
277
|
+
let rooms;
|
|
278
|
+
let ownerInviteFingerprint;
|
|
279
|
+
let ownerInvite;
|
|
280
|
+
let roomTemplates;
|
|
281
|
+
let tasks;
|
|
282
|
+
for (const { file, doc } of docs) {
|
|
283
|
+
if (doc.rooms !== undefined) {
|
|
284
|
+
if (rooms)
|
|
285
|
+
throw new ConfigError(`rooms: defined in multiple files; last: ${file}`);
|
|
286
|
+
const validated = validateRoomsConfig(deepSub(doc.rooms, vars), vars, file);
|
|
287
|
+
ownerInviteFingerprint = validated._invite?.fingerprint;
|
|
288
|
+
ownerInvite = validated._invite?.value;
|
|
289
|
+
const { _invite: _, ...clean } = validated;
|
|
290
|
+
rooms = clean;
|
|
291
|
+
}
|
|
292
|
+
if (doc.room_templates !== undefined) {
|
|
293
|
+
const validated = validateRoomTemplatesConfig(deepSub(doc.room_templates, vars), file);
|
|
294
|
+
roomTemplates = { ...(roomTemplates ?? {}), ...validated };
|
|
295
|
+
}
|
|
296
|
+
if (doc.tasks !== undefined) {
|
|
297
|
+
if (tasks)
|
|
298
|
+
throw new ConfigError(`tasks: defined in multiple files; last: ${file}`);
|
|
299
|
+
tasks = validateTasksConfig(deepSub(doc.tasks, vars), file);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
const result = {
|
|
285
303
|
roles, vars, defaults, files, startStaggerMs, diagnostics, watchdogs,
|
|
286
304
|
loops: resolvedLoops.loops,
|
|
305
|
+
rooms, roomTemplates, tasks, ownerInviteFingerprint,
|
|
287
306
|
};
|
|
307
|
+
if (ownerInvite !== undefined) {
|
|
308
|
+
Object.defineProperty(result, 'ownerInvite', {
|
|
309
|
+
value: ownerInvite,
|
|
310
|
+
enumerable: false,
|
|
311
|
+
writable: false,
|
|
312
|
+
configurable: false,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
return result;
|
|
288
316
|
}
|
|
289
317
|
/**
|
|
290
318
|
* Canonical form of a 64-hex container ID for authorization decisions. Hex
|
|
@@ -474,16 +502,15 @@ export function resolveAuthProxy(defaults, role, file = 'config', name = 'role')
|
|
|
474
502
|
};
|
|
475
503
|
}
|
|
476
504
|
export function resolveWorklogPolicy(defaults, role, file = 'config', name = 'role') {
|
|
477
|
-
if (
|
|
505
|
+
if (defaults === undefined && role === undefined)
|
|
478
506
|
return undefined;
|
|
479
|
-
if (defaults !== undefined &&
|
|
480
|
-
throw new ConfigError(`${file}: defaults.worklog must be a map
|
|
507
|
+
if (defaults !== undefined && !isPlainObject(defaults))
|
|
508
|
+
throw new ConfigError(`${file}: defaults.worklog must be a map`);
|
|
481
509
|
if (role !== undefined && !isPlainObject(role))
|
|
482
|
-
throw new ConfigError(`${file}: role '${name}' worklog must be a map
|
|
510
|
+
throw new ConfigError(`${file}: role '${name}' worklog must be a map`);
|
|
483
511
|
const merged = {
|
|
484
|
-
...
|
|
485
|
-
...(
|
|
486
|
-
...(role === undefined ? {} : role),
|
|
512
|
+
...(defaults ?? {}),
|
|
513
|
+
...(role ?? {}),
|
|
487
514
|
};
|
|
488
515
|
const bad = Object.keys(merged).filter(key => !['max_kb', 'keep_tail_kb', 'max_archives'].includes(key));
|
|
489
516
|
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) {
|