@ours.network/fleet 1.1.0-nightly.10 → 1.1.0-nightly.12
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 +42 -6
- package/dist/application/fleet-query-service.js +2 -1
- package/dist/application/role-command-service.js +9 -0
- package/dist/application/task-room-service.d.ts +6 -0
- package/dist/application/task-room-service.js +150 -88
- package/dist/build-info.json +4 -4
- package/dist/canonical-json.d.ts +2 -0
- package/dist/canonical-json.js +10 -0
- package/dist/cli.js +42 -23
- package/dist/config.d.ts +23 -1
- package/dist/config.js +149 -10
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +14 -4
- package/dist/doctor.d.ts +2 -0
- package/dist/doctor.js +23 -4
- package/dist/fleet-command-audit.js +5 -1
- package/dist/init-guidance.d.ts +1 -1
- package/dist/init-guidance.js +1 -1
- package/dist/ops.js +2 -17
- package/dist/owner-channel/commands.js +1 -1
- package/dist/preset-migration.d.ts +27 -0
- package/dist/preset-migration.js +200 -0
- package/dist/resolved-plan.js +4 -0
- package/dist/rooms-tasks/cli.d.ts +2 -0
- package/dist/rooms-tasks/cli.js +119 -27
- package/dist/rooms-tasks/close.d.ts +1 -1
- package/dist/rooms-tasks/close.js +10 -1
- package/dist/rooms-tasks/config.js +11 -24
- package/dist/rooms-tasks/launch-snapshot.d.ts +10 -0
- package/dist/rooms-tasks/launch-snapshot.js +159 -0
- package/dist/rooms-tasks/member-overrides.d.ts +27 -0
- package/dist/rooms-tasks/member-overrides.js +170 -0
- package/dist/rooms-tasks/provision.js +33 -45
- package/dist/rooms-tasks/room-state.js +4 -0
- package/dist/rooms-tasks/task-state.d.ts +2 -0
- package/dist/rooms-tasks/task-state.js +19 -1
- package/dist/rooms-tasks/templates.d.ts +4 -1
- package/dist/rooms-tasks/templates.js +34 -3
- package/dist/rooms-tasks/types.d.ts +31 -6
- package/dist/rooms-tasks/types.js +2 -1
- package/dist/spawn.js +12 -1
- package/dist/web/fleet-config-service.d.ts +1 -0
- package/dist/web/fleet-config-service.js +25 -7
- package/dist/web-app/assets/{index-BbTc5KtZ.js → index-C0h3ALvs.js} +8 -8
- package/dist/web-app/index.html +1 -1
- package/package.json +1 -1
- package/presets/brain-catalog.json +16 -0
- package/presets/fleet/agents/FleetCoordinator.yaml +4 -0
- package/presets/fleet/brains/claude-fable-5-high.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-low.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-max.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-medium.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-high.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-low.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-max.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-medium.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-high.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-low.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-max.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-medium.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +5 -0
- package/presets/fleet/room_templates/pair.yaml +2 -2
- package/presets/fleet/room_templates/single.yaml +1 -1
- package/presets/fleet/room_templates/team.yaml +3 -3
- package/presets/manifest.json +2 -1
- /package/presets/fleet/{agents → agent_templates}/Agent.yaml +0 -0
- /package/presets/fleet/{agents → agent_templates}/Architect.yaml +0 -0
- /package/presets/fleet/{agents → agent_templates}/Critic.yaml +0 -0
- /package/presets/fleet/{agents → agent_templates}/Developer.yaml +0 -0
- /package/presets/fleet/{agents → agent_templates}/Secretary.yaml +0 -0
- /package/presets/fleet/{agents → agent_templates}/Tester.yaml +0 -0
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn as spawnChild } from 'node:child_process';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
|
-
import { existsSync, mkdirSync, readFileSync,
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, statSync } from 'node:fs';
|
|
5
5
|
import { realpathSync } from 'node:fs';
|
|
6
6
|
import { join as joinPath, resolve as resolvePath } from 'node:path';
|
|
7
7
|
import { createInterface } from 'node:readline';
|
|
@@ -9,8 +9,9 @@ import { Command } from 'commander';
|
|
|
9
9
|
import { VERSION } from './version.js';
|
|
10
10
|
import { INIT_COMPLETION_GUIDANCE } from './init-guidance.js';
|
|
11
11
|
import { bootstrapPresets } from './preset-bootstrap.js';
|
|
12
|
+
import { migrateLegacyStarterPresets } from './preset-migration.js';
|
|
12
13
|
import { analyzeInstalls, buildInfo, buildLabel, discoverInstalls, runningLabel, } from './provenance.js';
|
|
13
|
-
import { agentDir, agentsRoot, tmpRoot, logsRoot, deriveXdgRuntimeDir } from './paths.js';
|
|
14
|
+
import { agentDir, agentsRoot, tmpRoot, logsRoot, deriveXdgRuntimeDir, defaultConfigPath } from './paths.js';
|
|
14
15
|
import { findRole, loadConfig, ROLE_NAME_RE } from './config.js';
|
|
15
16
|
import { formatDuration } from './duration.js';
|
|
16
17
|
import { redactSensitive, resolvedPlan } from './resolved-plan.js';
|
|
@@ -394,29 +395,26 @@ cOpt(program.command('force-restart [names...]').description('re-sync + bounce F
|
|
|
394
395
|
die(e);
|
|
395
396
|
}
|
|
396
397
|
});
|
|
397
|
-
program.command('ls').description('list running
|
|
398
|
-
.action(async () => {
|
|
398
|
+
cOpt(program.command('ls').description('list running declared persistent Agent sessions'))
|
|
399
|
+
.action(async (opts) => {
|
|
399
400
|
const acp = [];
|
|
400
|
-
|
|
401
|
-
|
|
401
|
+
const cfg = loadConfig(opts.configuration);
|
|
402
|
+
for (const { name } of cfg.roles) {
|
|
403
|
+
const stateDir = agentDir(name);
|
|
404
|
+
if (!existsSync(controlSocketPath(stateDir)))
|
|
402
405
|
continue;
|
|
403
|
-
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
// steered wake turn (FLEET-002), so `ls` reports what was observed.
|
|
413
|
-
const observed = classifyActivity(result.activity);
|
|
414
|
-
acp.push(`${name}: acp${observed.state === 'active' ? ' (working)'
|
|
415
|
-
: observed.state === 'quiet' ? ' (no recent agent activity)' : ''}`);
|
|
416
|
-
}
|
|
406
|
+
try {
|
|
407
|
+
const response = await controlRequest(stateDir, { command: 'status' }, 2_000);
|
|
408
|
+
const result = response.result;
|
|
409
|
+
if (response.ok && result?.alive) {
|
|
410
|
+
// Activity, not readiness: an idle-readiness role may be running a
|
|
411
|
+
// steered wake turn (FLEET-002), so `ls` reports what was observed.
|
|
412
|
+
const observed = classifyActivity(result.activity);
|
|
413
|
+
acp.push(`${name}: acp${observed.state === 'active' ? ' (working)'
|
|
414
|
+
: observed.state === 'quiet' ? ' (no recent agent activity)' : ''}`);
|
|
417
415
|
}
|
|
418
|
-
catch { /* ignore stale sockets */ }
|
|
419
416
|
}
|
|
417
|
+
catch { /* ignore stale sockets */ }
|
|
420
418
|
}
|
|
421
419
|
console.log(acp.join('\n') || '(none)');
|
|
422
420
|
});
|
|
@@ -526,8 +524,9 @@ program.command('logs <name>').description('show the role log').option('-f, --fo
|
|
|
526
524
|
const { cmd, args } = pickBackend().logsArgs(name, opts.follow === true);
|
|
527
525
|
process.exit(await passthrough(cmd, args));
|
|
528
526
|
});
|
|
529
|
-
program.command('status <name>').description('unit/
|
|
530
|
-
.action(async (name) => {
|
|
527
|
+
cOpt(program.command('status <name>').description('declared persistent Agent unit/session state'))
|
|
528
|
+
.action(async (name, opts) => {
|
|
529
|
+
findRole(loadConfig(opts.configuration), name);
|
|
531
530
|
console.log(await pickBackend().status(name));
|
|
532
531
|
// A role outlives the artifact that created it. If a different build is
|
|
533
532
|
// reporting now, its settings may resolve differently than at creation.
|
|
@@ -1269,6 +1268,26 @@ cOpt(program.command('init').description('idempotent host setup plus missing pac
|
|
|
1269
1268
|
console.log(`Created ${seeded.created.length}; preserved ${seeded.preserved.length} existing file(s).`);
|
|
1270
1269
|
console.log(INIT_COMPLETION_GUIDANCE);
|
|
1271
1270
|
});
|
|
1271
|
+
cOpt(program.command('migrate-agent-templates')
|
|
1272
|
+
.description('dry-run the exact legacy starter split; add --write to apply atomically')
|
|
1273
|
+
.option('--write', 'apply the reviewed migration and retain a private recovery backup'))
|
|
1274
|
+
.action((opts) => {
|
|
1275
|
+
try {
|
|
1276
|
+
const result = migrateLegacyStarterPresets(opts.configuration ?? defaultConfigPath(), { write: opts.write });
|
|
1277
|
+
console.log(result.write ? 'Applied legacy starter migration.' : 'Dry run only; no files were changed.');
|
|
1278
|
+
for (const move of result.moves)
|
|
1279
|
+
console.log(`Move ${move.from} -> ${move.to}`);
|
|
1280
|
+
for (const addition of result.additions)
|
|
1281
|
+
console.log(`Add ${addition}`);
|
|
1282
|
+
console.log(`Staging path: ${result.stagingPath}`);
|
|
1283
|
+
console.log(`Recovery backup: ${result.backupPath}`);
|
|
1284
|
+
if (!result.write)
|
|
1285
|
+
console.log('Re-run with --write to apply this exact migration class.');
|
|
1286
|
+
}
|
|
1287
|
+
catch (error) {
|
|
1288
|
+
die(error);
|
|
1289
|
+
}
|
|
1290
|
+
});
|
|
1272
1291
|
const webCommand = cOpt(program.command('web').description('start or open the secure localhost fleet web console'))
|
|
1273
1292
|
.enablePositionalOptions()
|
|
1274
1293
|
.option('--port <port>', 'loopback service port (default: 49271)', value => {
|
package/dist/config.d.ts
CHANGED
|
@@ -158,6 +158,13 @@ export interface AgentDefinition {
|
|
|
158
158
|
worklog?: WorklogPolicyInput;
|
|
159
159
|
auth_proxy?: Partial<AuthProxyConfig>;
|
|
160
160
|
}
|
|
161
|
+
/** Inert, reusable launch definition. It never owns an identity or runtime state. */
|
|
162
|
+
export type AgentTemplateDefinition = Omit<AgentDefinition, 'identity'>;
|
|
163
|
+
/** Explicit persistent instance reusing an inert template. */
|
|
164
|
+
export interface AgentTemplateInstance {
|
|
165
|
+
template: string;
|
|
166
|
+
overrides?: Partial<AgentDefinition>;
|
|
167
|
+
}
|
|
161
168
|
/** Internal first-boot payload for a Fleet-provisioned Cowork room member. */
|
|
162
169
|
export interface RoomMemberStartup {
|
|
163
170
|
room_id: string;
|
|
@@ -194,6 +201,11 @@ export interface ResolvedRole extends Omit<RoleConfig, 'model' | 'owner_channel'
|
|
|
194
201
|
roomMemberStartup?: RoomMemberStartup;
|
|
195
202
|
/** Original unresolved selections only; denied operational fields never enter inheritance state. */
|
|
196
203
|
agentSelections?: Pick<AgentDefinition, 'role' | 'brain'>;
|
|
204
|
+
agentTemplate?: {
|
|
205
|
+
id: string;
|
|
206
|
+
sourceFile: string;
|
|
207
|
+
contentHash: string;
|
|
208
|
+
};
|
|
197
209
|
}
|
|
198
210
|
export interface FieldProvenance {
|
|
199
211
|
sourceFile: string;
|
|
@@ -215,12 +227,18 @@ export interface FleetConfig {
|
|
|
215
227
|
roles: ResolvedRole[];
|
|
216
228
|
/** Canonical, variable-resolved Agent authoring documents keyed by stable Agent ID. */
|
|
217
229
|
agentDefinitions?: Record<string, AgentDefinition>;
|
|
230
|
+
/** Canonical inert templates keyed by stable template ID. */
|
|
231
|
+
agentTemplates?: Record<string, AgentTemplateDefinition>;
|
|
232
|
+
rolePresets?: Record<string, Record<string, unknown>>;
|
|
233
|
+
brainPresets?: Record<string, Record<string, unknown>>;
|
|
234
|
+
/** Required launch-boundary resolver for effective temporary Agent definitions. */
|
|
235
|
+
resolveAgentDefinition?: (id: string, definition: AgentDefinition) => ResolvedRole;
|
|
218
236
|
vars: Record<string, string>;
|
|
219
237
|
defaults: Record<string, unknown>;
|
|
220
238
|
files: string[];
|
|
221
239
|
configMode?: 'split-v2';
|
|
222
240
|
sourceDocuments?: Array<{
|
|
223
|
-
kind: 'Manifest' | 'Agent' | 'Role' | 'Brain' | 'RoomTemplate';
|
|
241
|
+
kind: 'Manifest' | 'Agent' | 'AgentTemplate' | 'Role' | 'Brain' | 'RoomTemplate';
|
|
224
242
|
id?: string;
|
|
225
243
|
path: string;
|
|
226
244
|
}>;
|
|
@@ -260,6 +278,8 @@ export declare const AGENT_KEYS: string[];
|
|
|
260
278
|
type BarePreset = Record<string, unknown>;
|
|
261
279
|
export declare function validateRoleValue(value: BarePreset, file: string, pointer: string): void;
|
|
262
280
|
export declare function validateBrainValue(value: BarePreset, file: string, pointer: string): void;
|
|
281
|
+
/** Pure launch-boundary validation for a fully resolved temporary Agent definition. */
|
|
282
|
+
export declare function validateEffectiveAgentTemplate(definition: AgentTemplateDefinition, id?: string): AgentTemplateDefinition;
|
|
263
283
|
export declare function assertBareKeys(value: unknown, allowed: string[], label: string): BarePreset;
|
|
264
284
|
export declare function splitRootFor(base: string): string;
|
|
265
285
|
/** Load a v2 manifest and bare Agent/Role/Brain documents from its stem directory. */
|
|
@@ -271,6 +291,8 @@ export declare function loadConfig(configPath?: string, options?: {
|
|
|
271
291
|
};
|
|
272
292
|
skipWatchdogs?: boolean;
|
|
273
293
|
}): FleetConfig;
|
|
294
|
+
/** Full read-only Agent resolution used by launch dry-runs and room plan validation. */
|
|
295
|
+
export declare function resolveAgentDefinitionDryRun(configPath: string, id: string, definition: AgentDefinition): ResolvedRole;
|
|
274
296
|
/**
|
|
275
297
|
* Canonical form of a 64-hex container ID for authorization decisions. Hex
|
|
276
298
|
* case is not identity: two casings of one CID are the same peer, so every
|
package/dist/config.js
CHANGED
|
@@ -11,6 +11,8 @@ import { validateRoomsConfig, validateTasksConfig, validateRoomTemplatesConfig,
|
|
|
11
11
|
import { CAPABILITIES, CAP_MONITOR_INTERRUPT_AFTER_TOOL } from './capabilities.js';
|
|
12
12
|
import { runningLabel } from './provenance.js';
|
|
13
13
|
import { parseDuration } from './duration.js';
|
|
14
|
+
import { createHash } from 'node:crypto';
|
|
15
|
+
import { canonicalJson } from './canonical-json.js';
|
|
14
16
|
/** Conservative built-in policy; `worklog: false` is the explicit opt-out. */
|
|
15
17
|
export const DEFAULT_WORKLOG_POLICY = Object.freeze({
|
|
16
18
|
max_kb: 1024,
|
|
@@ -142,6 +144,11 @@ export const AGENT_KEYS = [
|
|
|
142
144
|
'role', 'brain', 'permissions', 'identity', 'cwd', 'coordinator', 'env', 'oversee',
|
|
143
145
|
'isolation', 'monitor', 'owner_channel', 'worklog', 'auth_proxy',
|
|
144
146
|
];
|
|
147
|
+
const AGENT_INSTANCE_KEYS = ['template', 'overrides'];
|
|
148
|
+
const TEMPLATE_FORBIDDEN_KEYS = ['identity'];
|
|
149
|
+
const MERGED_MAP_FIELDS = new Set([
|
|
150
|
+
'permissions', 'env', 'isolation', 'monitor', 'owner_channel', 'worklog', 'auth_proxy',
|
|
151
|
+
]);
|
|
145
152
|
const V2_API_VERSION = 'ours.network/fleet/v2';
|
|
146
153
|
const utf8Bytes = (value) => Buffer.byteLength(value, 'utf8');
|
|
147
154
|
function schemaError(file, pointer, summary) {
|
|
@@ -179,6 +186,40 @@ export function validateBrainValue(value, file, pointer) {
|
|
|
179
186
|
schemaError(file, `${pointer}/${key}`, 'must be a mapping');
|
|
180
187
|
}
|
|
181
188
|
}
|
|
189
|
+
/** Pure launch-boundary validation for a fully resolved temporary Agent definition. */
|
|
190
|
+
export function validateEffectiveAgentTemplate(definition, id = 'room-member') {
|
|
191
|
+
const value = structuredClone(definition);
|
|
192
|
+
const bad = Object.keys(value).filter(key => !AGENT_KEYS.includes(key));
|
|
193
|
+
if (bad.length)
|
|
194
|
+
throw new ConfigError(`member '${id}' has unknown Agent key(s) ${bad.join(', ')}`);
|
|
195
|
+
if (Object.hasOwn(value, 'identity'))
|
|
196
|
+
throw new ConfigError(`member '${id}' cannot override identity`);
|
|
197
|
+
if (!isPlainObject(value.role) || !isPlainObject(value.role.inline)
|
|
198
|
+
|| Object.keys(value.role).some(key => key !== 'inline'))
|
|
199
|
+
throw new ConfigError(`member '${id}' role must be a resolved inline definition`);
|
|
200
|
+
if (!isPlainObject(value.brain) || !isPlainObject(value.brain.inline)
|
|
201
|
+
|| Object.keys(value.brain).some(key => key !== 'inline'))
|
|
202
|
+
throw new ConfigError(`member '${id}' brain must be a resolved inline definition`);
|
|
203
|
+
const role = value.role.inline;
|
|
204
|
+
const brain = value.brain.inline;
|
|
205
|
+
assertBareKeys(role, ROLE_PRESET_KEYS, `member '${id}' Role`);
|
|
206
|
+
assertBareKeys(brain, BRAIN_PRESET_KEYS, `member '${id}' Brain`);
|
|
207
|
+
validateRoleValue(role, '(member override)', `/members/${id}/role`);
|
|
208
|
+
validateBrainValue(brain, '(member override)', `/members/${id}/brain`);
|
|
209
|
+
validateAgentScalars(Object.fromEntries(Object.entries(value)
|
|
210
|
+
.filter(([key]) => key !== 'role' && key !== 'brain')), '(member override)', id);
|
|
211
|
+
resolvePermissions(undefined, value.permissions, '(member override)', id);
|
|
212
|
+
if (value.isolation !== undefined) {
|
|
213
|
+
const problems = validateIsolationConfig(value.isolation);
|
|
214
|
+
if (problems.length)
|
|
215
|
+
throw new ConfigError(`member '${id}' ${problems.join('; ')}`);
|
|
216
|
+
}
|
|
217
|
+
const adapter = getAdapter(brain.harness);
|
|
218
|
+
adapter.agentSession.resolveBrain({ model: brain.model,
|
|
219
|
+
effort: brain.effort,
|
|
220
|
+
harnessOptions: brain.harness_options });
|
|
221
|
+
return value;
|
|
222
|
+
}
|
|
182
223
|
function validateAgentScalars(value, file, id) {
|
|
183
224
|
for (const key of ['identity', 'cwd']) {
|
|
184
225
|
const current = value[key];
|
|
@@ -334,7 +375,23 @@ function deepSubStrict(value, vars, file, pointer) {
|
|
|
334
375
|
]));
|
|
335
376
|
return value;
|
|
336
377
|
}
|
|
337
|
-
function
|
|
378
|
+
function mergeTemplateDefinition(template, overrides, file, id) {
|
|
379
|
+
const merge = (base, overlay, key) => {
|
|
380
|
+
if (overlay === null && key !== 'model')
|
|
381
|
+
schemaError(file, `/agents/${id}/overrides/${key}`, 'null deletion is not supported');
|
|
382
|
+
if (MERGED_MAP_FIELDS.has(key) && isPlainObject(base) && isPlainObject(overlay)) {
|
|
383
|
+
return Object.fromEntries([...new Set([...Object.keys(base), ...Object.keys(overlay)])]
|
|
384
|
+
.map(child => [child, Object.hasOwn(overlay, child)
|
|
385
|
+
? merge(base[child], overlay[child], key) : structuredClone(base[child])]));
|
|
386
|
+
}
|
|
387
|
+
return structuredClone(overlay);
|
|
388
|
+
};
|
|
389
|
+
const result = structuredClone(template);
|
|
390
|
+
for (const [key, value] of Object.entries(overrides))
|
|
391
|
+
result[key] = merge(result[key], value, key);
|
|
392
|
+
return result;
|
|
393
|
+
}
|
|
394
|
+
function composeSplitRoles(base, baseDoc, yamlMode, diagnostics, files, additionalAgent, templateSink = {}, roleSink = {}, brainSink = {}) {
|
|
338
395
|
if (baseDoc.api_version !== V2_API_VERSION)
|
|
339
396
|
throw new ConfigError(`${base}: legacy fleet configuration is unsupported; run 'ours-fleet doctor' and rewrite it as ${V2_API_VERSION}`);
|
|
340
397
|
if ('roles' in baseDoc)
|
|
@@ -345,6 +402,7 @@ function composeSplitRoles(base, baseDoc, yamlMode, diagnostics, files, addition
|
|
|
345
402
|
assertTrustedPath(root, 'directory');
|
|
346
403
|
const brains = readKindDirectory(join(root, 'brains'), 'brain', false, yamlMode, diagnostics, files);
|
|
347
404
|
const roles = readKindDirectory(join(root, 'roles'), 'role', false, yamlMode, diagnostics, files);
|
|
405
|
+
const agentTemplates = readKindDirectory(join(root, 'agent_templates'), 'agent template', false, yamlMode, diagnostics, files);
|
|
348
406
|
const agents = readKindDirectory(join(root, 'agents'), 'agent', true, yamlMode, diagnostics, files);
|
|
349
407
|
if (additionalAgent) {
|
|
350
408
|
if (agents.has(additionalAgent.id))
|
|
@@ -354,9 +412,63 @@ function composeSplitRoles(base, baseDoc, yamlMode, diagnostics, files, addition
|
|
|
354
412
|
});
|
|
355
413
|
}
|
|
356
414
|
const vars = (baseDoc.vars ?? {});
|
|
415
|
+
for (const [id, preset] of roles) {
|
|
416
|
+
const value = assertBareKeys(preset.value, ROLE_PRESET_KEYS, `${preset.file}: Role '${id}'`);
|
|
417
|
+
validateRoleValue(value, preset.file, `/roles/${id}`);
|
|
418
|
+
roleSink[id] = deepSubStrict(value, vars, preset.file, `/roles/${id}`);
|
|
419
|
+
}
|
|
420
|
+
for (const [id, preset] of brains) {
|
|
421
|
+
const value = assertBareKeys(preset.value, BRAIN_PRESET_KEYS, `${preset.file}: Brain '${id}'`);
|
|
422
|
+
validateBrainValue(value, preset.file, `/brains/${id}`);
|
|
423
|
+
brainSink[id] = deepSubStrict(value, vars, preset.file, `/brains/${id}`);
|
|
424
|
+
}
|
|
425
|
+
const canonicalTemplates = new Map();
|
|
426
|
+
for (const [id, template] of agentTemplates) {
|
|
427
|
+
const value = assertBareKeys(template.value, AGENT_KEYS, `${template.file}: agent template '${id}'`);
|
|
428
|
+
if (!('role' in value) || !('brain' in value))
|
|
429
|
+
throw new ConfigError(`${template.file}: agent template '${id}' requires role and brain`);
|
|
430
|
+
const forbidden = TEMPLATE_FORBIDDEN_KEYS.filter(key => Object.hasOwn(value, key));
|
|
431
|
+
if (forbidden.length)
|
|
432
|
+
schemaError(template.file, `/agent_templates/${id}/${forbidden[0]}`, 'templates cannot own instance identity');
|
|
433
|
+
const templateRole = selection(value.role, 'role', template.file, roles, ROLE_PRESET_KEYS);
|
|
434
|
+
const templateBrain = selection(value.brain, 'brain', template.file, brains, BRAIN_PRESET_KEYS);
|
|
435
|
+
validateRoleValue(templateRole.value, templateRole.file, templateRole.pointer);
|
|
436
|
+
validateBrainValue(templateBrain.value, templateBrain.file, templateBrain.pointer);
|
|
437
|
+
validateAgentScalars(Object.fromEntries(Object.entries(value).filter(([key]) => key !== 'role' && key !== 'brain')), template.file, id);
|
|
438
|
+
canonicalTemplates.set(id, { file: template.file, value });
|
|
439
|
+
const operational = deepSubStrict(Object.fromEntries(Object.entries(value).filter(([key]) => key !== 'role' && key !== 'brain')), vars, template.file, `/agent_templates/${id}`);
|
|
440
|
+
templateSink[id] = {
|
|
441
|
+
role: { inline: deepSubStrict(templateRole.value, vars, templateRole.file, templateRole.pointer) },
|
|
442
|
+
brain: { inline: deepSubStrict(templateBrain.value, vars, templateBrain.file, templateBrain.pointer) },
|
|
443
|
+
...operational,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
357
446
|
const documents = [];
|
|
358
447
|
for (const [id, agent] of agents) {
|
|
359
|
-
|
|
448
|
+
let a;
|
|
449
|
+
let agentTemplate;
|
|
450
|
+
const instanceKeys = Object.keys(agent.value);
|
|
451
|
+
if (instanceKeys.includes('template')) {
|
|
452
|
+
assertBareKeys(agent.value, AGENT_INSTANCE_KEYS, `${agent.file}: agent '${id}'`);
|
|
453
|
+
if (typeof agent.value.template !== 'string' || !ROLE_NAME_RE.test(agent.value.template))
|
|
454
|
+
schemaError(agent.file, `/agents/${id}/template`, 'must be a valid Agent Template ID');
|
|
455
|
+
const template = canonicalTemplates.get(agent.value.template);
|
|
456
|
+
if (!template)
|
|
457
|
+
throw new ConfigError(`${agent.file}: E_REF_MISSING: agent template '${agent.value.template}' not found`);
|
|
458
|
+
if (agent.value.overrides !== undefined && !isPlainObject(agent.value.overrides))
|
|
459
|
+
schemaError(agent.file, `/agents/${id}/overrides`, 'must be a mapping');
|
|
460
|
+
const overrides = assertBareKeys((agent.value.overrides ?? {}), AGENT_KEYS, `${agent.file}: agent '${id}' overrides`);
|
|
461
|
+
a = mergeTemplateDefinition(template.value, overrides, agent.file, id);
|
|
462
|
+
const resolvedTemplate = templateSink[agent.value.template];
|
|
463
|
+
agentTemplate = {
|
|
464
|
+
id: agent.value.template,
|
|
465
|
+
sourceFile: template.file,
|
|
466
|
+
contentHash: createHash('sha256').update(canonicalJson(resolvedTemplate)).digest('hex'),
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
a = assertBareKeys(agent.value, AGENT_KEYS, `${agent.file}: agent '${id}'`);
|
|
471
|
+
}
|
|
360
472
|
if (!('role' in a) || !('brain' in a))
|
|
361
473
|
throw new ConfigError(`${agent.file}: agent '${id}' requires role and brain`);
|
|
362
474
|
const role = selection(a.role, 'role', agent.file, roles, ROLE_PRESET_KEYS);
|
|
@@ -436,7 +548,8 @@ function composeSplitRoles(base, baseDoc, yamlMode, diagnostics, files, addition
|
|
|
436
548
|
documents.push({ file: agent.file,
|
|
437
549
|
doc: { roles: { [id]: { ...brainValue, ...roleValue, ...operational } } }, provenance,
|
|
438
550
|
agentSelections: structuredClone({ role: canonicalDefinition.role, brain: canonicalDefinition.brain }),
|
|
439
|
-
agentDefinition: canonicalDefinition
|
|
551
|
+
agentDefinition: canonicalDefinition,
|
|
552
|
+
...(agentTemplate ? { agentTemplate } : {}) });
|
|
440
553
|
}
|
|
441
554
|
return documents;
|
|
442
555
|
}
|
|
@@ -454,6 +567,9 @@ export function loadConfig(configPath, options = {}) {
|
|
|
454
567
|
const base = configPath ?? defaultConfigPath();
|
|
455
568
|
const files = [];
|
|
456
569
|
const diagnostics = [];
|
|
570
|
+
const agentTemplates = {};
|
|
571
|
+
const rolePresets = {};
|
|
572
|
+
const brainPresets = {};
|
|
457
573
|
let docs = [];
|
|
458
574
|
let manifestDoc = {};
|
|
459
575
|
if (existsSync(base)) {
|
|
@@ -472,7 +588,7 @@ export function loadConfig(configPath, options = {}) {
|
|
|
472
588
|
const dd = fleetDDir();
|
|
473
589
|
if (existsSync(dd) && readdirSync(dd).some(f => f.endsWith('.yaml') || f.endsWith('.yml')))
|
|
474
590
|
throw new ConfigError(`${dd}: legacy fleet.d configuration is unsupported; run 'ours-fleet doctor' and move agents to ${splitRootFor(base)}/agents/`);
|
|
475
|
-
docs = [{ file: base, doc: manifestDoc }, ...composeSplitRoles(base, manifestDoc, options.yamlMode ?? 'compat', diagnostics, files, options.additionalAgent)];
|
|
591
|
+
docs = [{ file: base, doc: manifestDoc }, ...composeSplitRoles(base, manifestDoc, options.yamlMode ?? 'compat', diagnostics, files, options.additionalAgent, agentTemplates, rolePresets, brainPresets)];
|
|
476
592
|
const baseDoc = manifestDoc;
|
|
477
593
|
const vars = (baseDoc.vars ?? {});
|
|
478
594
|
const defaults = (baseDoc.defaults ?? {});
|
|
@@ -485,7 +601,7 @@ export function loadConfig(configPath, options = {}) {
|
|
|
485
601
|
const seen = new Map();
|
|
486
602
|
const roles = [];
|
|
487
603
|
const agentDefinitions = {};
|
|
488
|
-
for (const { file, doc, provenance, agentSelections, agentDefinition } of docs) {
|
|
604
|
+
for (const { file, doc, provenance, agentSelections, agentDefinition, agentTemplate } of docs) {
|
|
489
605
|
for (const [name, raw] of Object.entries((doc.roles ?? {}))) {
|
|
490
606
|
if (!ROLE_NAME_RE.test(name))
|
|
491
607
|
throw new ConfigError(`${file}: invalid role name '${name}' (allowed: [A-Za-z0-9_-])`);
|
|
@@ -582,6 +698,7 @@ export function loadConfig(configPath, options = {}) {
|
|
|
582
698
|
env: Object.keys(env).length ? env : undefined,
|
|
583
699
|
loops: [],
|
|
584
700
|
...(agentSelections ? { agentSelections } : {}),
|
|
701
|
+
...(agentTemplate ? { agentTemplate } : {}),
|
|
585
702
|
});
|
|
586
703
|
// Forbidden-path enforcement: a mount that would breach the policy
|
|
587
704
|
// is a configuration error, caught by `config` rather than at launch.
|
|
@@ -653,7 +770,7 @@ export function loadConfig(configPath, options = {}) {
|
|
|
653
770
|
}
|
|
654
771
|
}
|
|
655
772
|
const result = {
|
|
656
|
-
roles, agentDefinitions, vars, defaults, files, startStaggerMs, diagnostics, watchdogs,
|
|
773
|
+
roles, agentDefinitions, agentTemplates, rolePresets, brainPresets, vars, defaults, files, startStaggerMs, diagnostics, watchdogs,
|
|
657
774
|
loops: resolvedLoops.loops,
|
|
658
775
|
rooms, roomTemplates, tasks, ownerInviteFingerprint,
|
|
659
776
|
configMode: 'split-v2',
|
|
@@ -662,11 +779,18 @@ export function loadConfig(configPath, options = {}) {
|
|
|
662
779
|
return { kind: 'Manifest', path };
|
|
663
780
|
const parent = basename(dirname(path));
|
|
664
781
|
const kind = parent === 'agents' ? 'Agent'
|
|
665
|
-
: parent === '
|
|
666
|
-
: parent === '
|
|
782
|
+
: parent === 'agent_templates' ? 'AgentTemplate'
|
|
783
|
+
: parent === 'roles' ? 'Role'
|
|
784
|
+
: parent === 'room_templates' ? 'RoomTemplate' : 'Brain';
|
|
667
785
|
return { kind, id: basename(path, extname(path)), path };
|
|
668
786
|
}),
|
|
669
787
|
};
|
|
788
|
+
Object.defineProperty(result, 'resolveAgentDefinition', {
|
|
789
|
+
value: (id, definition) => resolveAgentDefinitionDryRun(base, id, definition),
|
|
790
|
+
enumerable: false,
|
|
791
|
+
writable: false,
|
|
792
|
+
configurable: false,
|
|
793
|
+
});
|
|
670
794
|
if (ownerInvite !== undefined) {
|
|
671
795
|
Object.defineProperty(result, 'ownerInvite', {
|
|
672
796
|
value: ownerInvite,
|
|
@@ -677,6 +801,12 @@ export function loadConfig(configPath, options = {}) {
|
|
|
677
801
|
}
|
|
678
802
|
return result;
|
|
679
803
|
}
|
|
804
|
+
/** Full read-only Agent resolution used by launch dry-runs and room plan validation. */
|
|
805
|
+
export function resolveAgentDefinitionDryRun(configPath, id, definition) {
|
|
806
|
+
return findRole(loadConfig(configPath, {
|
|
807
|
+
additionalAgent: { id, definition }, skipWatchdogs: true,
|
|
808
|
+
}), id);
|
|
809
|
+
}
|
|
680
810
|
/**
|
|
681
811
|
* Canonical form of a 64-hex container ID for authorization decisions. Hex
|
|
682
812
|
* case is not identity: two casings of one CID are the same peer, so every
|
|
@@ -781,7 +911,13 @@ export function resolveOwnerChannelConfig(defaults, role, session, file = 'confi
|
|
|
781
911
|
};
|
|
782
912
|
}
|
|
783
913
|
function validateOwnerChannelIdentities(roles) {
|
|
784
|
-
const roleIdentities = new Map(
|
|
914
|
+
const roleIdentities = new Map();
|
|
915
|
+
for (const role of roles) {
|
|
916
|
+
const prior = roleIdentities.get(role.identity);
|
|
917
|
+
if (prior)
|
|
918
|
+
throw new ConfigError(`persistent Agents '${prior}' and '${role.name}' resolve to duplicate identity '${role.identity}'`);
|
|
919
|
+
roleIdentities.set(role.identity, role.name);
|
|
920
|
+
}
|
|
785
921
|
const channels = new Map();
|
|
786
922
|
for (const role of roles) {
|
|
787
923
|
const identity = role.owner_channel?.identity;
|
|
@@ -1007,7 +1143,10 @@ export function resolveMonitorConfig(defMonitor, roleMonitor, labels = {}) {
|
|
|
1007
1143
|
}
|
|
1008
1144
|
export function findRole(cfg, name) {
|
|
1009
1145
|
const r = cfg.roles.find(r => r.name === name);
|
|
1010
|
-
if (!r)
|
|
1146
|
+
if (!r) {
|
|
1147
|
+
if (cfg.agentTemplates?.[name])
|
|
1148
|
+
throw new ConfigError(`'${name}' is an inert Agent Template, not a persistent Agent; lifecycle commands target only fleet/agents instances`);
|
|
1011
1149
|
throw new ConfigError(`no such role '${name}' in ${cfg.files.join(', ') || 'config'}`);
|
|
1150
|
+
}
|
|
1012
1151
|
return r;
|
|
1013
1152
|
}
|