@ours.network/fleet 1.1.0-nightly.2 → 1.1.0-nightly.21
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 +204 -55
- package/dist/agent-recovery-gate.d.ts +17 -0
- package/dist/agent-recovery-gate.js +135 -0
- package/dist/application/capabilities.js +18 -6
- package/dist/application/fleet-query-service.js +20 -5
- package/dist/application/role-command-service.js +9 -0
- package/dist/application/role-creation-service.d.ts +6 -25
- package/dist/application/role-creation-service.js +34 -72
- package/dist/application/role-repository.d.ts +4 -2
- package/dist/application/role-repository.js +6 -3
- package/dist/application/session-control.d.ts +3 -1
- package/dist/application/session-control.js +4 -2
- package/dist/application/task-room-service.d.ts +58 -6
- package/dist/application/task-room-service.js +435 -79
- package/dist/briefing.js +74 -24
- 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 +241 -76
- package/dist/config-yaml.d.ts +1 -1
- package/dist/config.d.ts +75 -3
- package/dist/config.js +274 -34
- package/dist/creation.d.ts +1 -1
- package/dist/creation.js +3 -2
- package/dist/daemon-recovery.d.ts +93 -0
- package/dist/daemon-recovery.js +328 -0
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +192 -66
- package/dist/doctor.d.ts +2 -0
- package/dist/doctor.js +54 -4
- package/dist/fleet-command-audit.d.ts +165 -0
- package/dist/fleet-command-audit.js +555 -0
- package/dist/fleet-proxy.d.ts +7 -2
- package/dist/fleet-proxy.js +30 -8
- package/dist/harness/agent-session.d.ts +5 -0
- package/dist/harness/claude-code-session.d.ts +2 -0
- package/dist/harness/claude-code-session.js +6 -1
- package/dist/harness/claude-code.js +2 -0
- package/dist/harness/codex-app-server-proxy.d.ts +40 -0
- package/dist/harness/codex-app-server-proxy.js +348 -14
- package/dist/harness/codex-session.d.ts +11 -2
- package/dist/harness/codex-session.js +22 -2
- package/dist/harness/codex.d.ts +5 -1
- package/dist/harness/codex.js +110 -10
- package/dist/harness/types.d.ts +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +4 -3
- package/dist/init-guidance.d.ts +1 -1
- package/dist/init-guidance.js +4 -3
- package/dist/init-wizard.d.ts +98 -0
- package/dist/init-wizard.js +629 -0
- package/dist/lifecycle-summary.d.ts +134 -0
- package/dist/lifecycle-summary.js +179 -0
- package/dist/loops/config.d.ts +10 -1
- package/dist/loops/config.js +38 -5
- package/dist/model-env.d.ts +2 -2
- package/dist/model-env.js +1 -1
- package/dist/model-recovery.d.ts +1 -1
- package/dist/ops.js +2 -17
- package/dist/owner-channel/channel.d.ts +58 -0
- package/dist/owner-channel/channel.js +321 -42
- package/dist/owner-channel/commands.d.ts +6 -1
- package/dist/owner-channel/commands.js +34 -19
- package/dist/owner-channel/state.d.ts +2 -0
- package/dist/owner-channel/state.js +8 -2
- package/dist/permissions.js +1 -1
- package/dist/preset-bootstrap.d.ts +10 -0
- package/dist/preset-bootstrap.js +85 -0
- package/dist/preset-migration.d.ts +42 -0
- package/dist/preset-migration.js +414 -0
- package/dist/reports/artifact.d.ts +10 -0
- package/dist/reports/artifact.js +32 -0
- package/dist/reports/index.d.ts +5 -0
- package/dist/reports/index.js +5 -0
- package/dist/reports/render.d.ts +6 -0
- package/dist/reports/render.js +240 -0
- package/dist/reports/service.d.ts +42 -0
- package/dist/reports/service.js +57 -0
- package/dist/reports/tasks.d.ts +52 -0
- package/dist/reports/tasks.js +177 -0
- package/dist/reports/types.d.ts +315 -0
- package/dist/reports/types.js +6 -0
- package/dist/resolved-plan.js +13 -1
- package/dist/rooms-tasks/cli.d.ts +2 -0
- package/dist/rooms-tasks/cli.js +514 -75
- package/dist/rooms-tasks/close.d.ts +9 -1
- package/dist/rooms-tasks/close.js +20 -4
- package/dist/rooms-tasks/config.js +16 -14
- package/dist/rooms-tasks/cowork-adapter.d.ts +1 -0
- package/dist/rooms-tasks/cowork-adapter.js +6 -0
- package/dist/rooms-tasks/deletion.d.ts +51 -0
- package/dist/rooms-tasks/deletion.js +217 -0
- package/dist/rooms-tasks/external-worker.d.ts +1 -0
- package/dist/rooms-tasks/external-worker.js +14 -9
- package/dist/rooms-tasks/launch-snapshot.d.ts +19 -0
- package/dist/rooms-tasks/launch-snapshot.js +181 -0
- package/dist/rooms-tasks/member-overrides.d.ts +30 -0
- package/dist/rooms-tasks/member-overrides.js +204 -0
- package/dist/rooms-tasks/provision.js +184 -49
- package/dist/rooms-tasks/room-state.d.ts +1 -0
- package/dist/rooms-tasks/room-state.js +12 -1
- package/dist/rooms-tasks/task-state.d.ts +115 -3
- package/dist/rooms-tasks/task-state.js +361 -11
- package/dist/rooms-tasks/templates.d.ts +4 -2
- package/dist/rooms-tasks/templates.js +42 -78
- package/dist/rooms-tasks/terminal.d.ts +3 -0
- package/dist/rooms-tasks/terminal.js +7 -3
- package/dist/rooms-tasks/types.d.ts +96 -17
- package/dist/rooms-tasks/types.js +14 -4
- package/dist/runner.d.ts +12 -2
- package/dist/runner.js +124 -43
- package/dist/session/acp.d.ts +9 -1
- package/dist/session/acp.js +35 -9
- package/dist/session/arbiter.d.ts +2 -1
- package/dist/session/arbiter.js +2 -0
- package/dist/session/codex-app-server-transport.d.ts +53 -0
- package/dist/session/codex-app-server-transport.js +184 -0
- package/dist/session/codex-app-server.d.ts +122 -0
- package/dist/session/codex-app-server.js +1031 -0
- package/dist/session/control.d.ts +14 -1
- package/dist/session/control.js +38 -0
- package/dist/session/conversation-types.d.ts +4 -4
- package/dist/session/types.d.ts +24 -6
- package/dist/session/types.js +28 -0
- package/dist/spawn.d.ts +15 -31
- package/dist/spawn.js +117 -237
- package/dist/temp-lifecycle.d.ts +1 -1
- package/dist/watchdog/config.d.ts +10 -7
- package/dist/watchdog/config.js +29 -19
- package/dist/watchdog/run.js +4 -13
- package/dist/web/fleet-config-service.d.ts +1 -0
- package/dist/web/fleet-config-service.js +27 -8
- package/dist/web/runtime.js +11 -6
- package/dist/web/server.js +28 -1
- package/dist/web/topology-promote.js +8 -1
- package/dist/web-app/assets/index-C0h3ALvs.js +10 -0
- package/dist/web-app/index.html +1 -1
- package/package.json +3 -2
- package/presets/brain-catalog.json +16 -0
- package/presets/fleet/agent_templates/Critic.yaml +4 -0
- package/presets/fleet/agent_templates/Developer.yaml +4 -0
- package/presets/fleet/agent_templates/LocalCoordinator.yaml +4 -0
- package/presets/fleet/agents/FleetCoordinator.yaml +3 -0
- package/presets/fleet/brains/claude-default.yaml +5 -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/roles/Coordinator.yaml +30 -0
- package/presets/fleet/roles/Critic.yaml +19 -0
- package/presets/fleet/roles/Developer.yaml +19 -0
- package/presets/fleet/roles/LocalCoordinator.yaml +25 -0
- package/presets/fleet/room_templates/pair.yaml +10 -0
- package/presets/fleet/room_templates/single.yaml +7 -0
- package/presets/fleet/room_templates/team.yaml +12 -0
- package/presets/fleet.yaml +7 -0
- package/presets/manifest.json +6 -0
- package/dist/web-app/assets/index-BbE9EX6n.js +0 -10
package/dist/config.js
CHANGED
|
@@ -6,11 +6,13 @@ import { harnessRuntimeDir, resolveIsolation, validateIsolationConfig, } from '.
|
|
|
6
6
|
import { getAdapter } from './harness/registry.js';
|
|
7
7
|
import { resolveRoleModelEnv } from './model-env.js';
|
|
8
8
|
import { resolveWatchdogs } from './watchdog/config.js';
|
|
9
|
-
import { resolveLoops } from './loops/config.js';
|
|
9
|
+
import { resolveAgentLoops, resolveLoops } from './loops/config.js';
|
|
10
10
|
import { validateRoomsConfig, validateTasksConfig, validateRoomTemplatesConfig, } from './rooms-tasks/config.js';
|
|
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,
|
|
@@ -133,21 +135,26 @@ const ROLE_KEYS = [
|
|
|
133
135
|
'briefing_file', 'model', 'effort', 'model_chain', 'max_tokens', 'autocompact_pct', 'env', 'oversee', 'harness_options',
|
|
134
136
|
'isolation', 'monitor', 'owner_channel', 'worklog', 'auth_proxy',
|
|
135
137
|
];
|
|
136
|
-
const ROLE_PRESET_KEYS = ['mission', 'persona', 'bio', 'briefing_file'];
|
|
137
|
-
const BRAIN_PRESET_KEYS = [
|
|
138
|
+
export const ROLE_PRESET_KEYS = ['mission', 'persona', 'bio', 'briefing_file'];
|
|
139
|
+
export const BRAIN_PRESET_KEYS = [
|
|
138
140
|
'harness', 'session', 'session_options', 'model', 'model_chain', 'max_tokens',
|
|
139
141
|
'autocompact_pct', 'harness_options', 'effort',
|
|
140
142
|
];
|
|
141
|
-
const AGENT_KEYS = [
|
|
143
|
+
export const AGENT_KEYS = [
|
|
142
144
|
'role', 'brain', 'permissions', 'identity', 'cwd', 'coordinator', 'env', 'oversee',
|
|
143
|
-
'isolation', 'monitor', 'owner_channel', 'worklog', 'auth_proxy',
|
|
145
|
+
'isolation', 'monitor', 'owner_channel', 'worklog', 'auth_proxy', 'loops',
|
|
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', 'loops',
|
|
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) {
|
|
148
155
|
throw new ConfigError(`${file}: E_SCHEMA ${pointer}: ${summary}`);
|
|
149
156
|
}
|
|
150
|
-
function validateRoleValue(value, file, pointer) {
|
|
157
|
+
export function validateRoleValue(value, file, pointer) {
|
|
151
158
|
const limits = {
|
|
152
159
|
mission: { bytes: 16 * 1024, nonblank: true },
|
|
153
160
|
persona: { bytes: 16 * 1024 },
|
|
@@ -168,7 +175,7 @@ function validateRoleValue(value, file, pointer) {
|
|
|
168
175
|
&& (typeof value.briefing_file !== 'string' || !value.briefing_file.trim()))
|
|
169
176
|
schemaError(file, `${pointer}/briefing_file`, 'must be a non-blank relative path');
|
|
170
177
|
}
|
|
171
|
-
function validateBrainValue(value, file, pointer) {
|
|
178
|
+
export function validateBrainValue(value, file, pointer) {
|
|
172
179
|
if (typeof value.harness !== 'string' || !value.harness.trim())
|
|
173
180
|
schemaError(file, `${pointer}/harness`, 'is required and must be a registered non-blank harness ID');
|
|
174
181
|
if (value.model !== undefined && value.model !== null
|
|
@@ -179,7 +186,45 @@ 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) {
|
|
224
|
+
if (value.loops !== undefined)
|
|
225
|
+
resolveAgentLoops(value.loops, {
|
|
226
|
+
name: id, harness: 'codex', session: 'acp', permissions: {},
|
|
227
|
+
}, file);
|
|
183
228
|
for (const key of ['identity', 'cwd']) {
|
|
184
229
|
const current = value[key];
|
|
185
230
|
if (current !== undefined && (typeof current !== 'string' || !current.trim()))
|
|
@@ -228,7 +273,7 @@ function assertTrustedPath(path, expected) {
|
|
|
228
273
|
if ((st.mode & 0o022) !== 0)
|
|
229
274
|
throw new ConfigError(`E_FILE_TRUST: ${path} is group/world writable (mode ${(st.mode & 0o777).toString(8)})`);
|
|
230
275
|
}
|
|
231
|
-
function assertBareKeys(value, allowed, label) {
|
|
276
|
+
export function assertBareKeys(value, allowed, label) {
|
|
232
277
|
if (!isPlainObject(value))
|
|
233
278
|
throw new ConfigError(`${label}: E_SCHEMA: must be a mapping`);
|
|
234
279
|
const bad = Object.keys(value).filter(key => !allowed.includes(key));
|
|
@@ -265,6 +310,35 @@ function readKindDirectory(root, kind, required, yamlMode, diagnostics, files) {
|
|
|
265
310
|
}
|
|
266
311
|
return result;
|
|
267
312
|
}
|
|
313
|
+
function readRoomTemplateDirectory(root, yamlMode, diagnostics, files) {
|
|
314
|
+
if (!existsSync(root))
|
|
315
|
+
return {};
|
|
316
|
+
assertTrustedPath(root, 'directory');
|
|
317
|
+
const result = {};
|
|
318
|
+
const sources = new Map();
|
|
319
|
+
const names = readdirSync(root)
|
|
320
|
+
.filter(name => ['.yaml', '.yml'].includes(extname(name).toLowerCase())).sort();
|
|
321
|
+
for (const filename of names) {
|
|
322
|
+
const file = join(root, filename);
|
|
323
|
+
assertTrustedPath(file, 'file');
|
|
324
|
+
const id = basename(filename, extname(filename));
|
|
325
|
+
const previous = sources.get(id);
|
|
326
|
+
if (previous)
|
|
327
|
+
throw new ConfigError(`E_DUPLICATE_ID: room template '${id}' defined by both ${previous} and ${file}`);
|
|
328
|
+
const parsed = parseFleetDocument(file, readFileSync(file, 'utf8'), yamlMode);
|
|
329
|
+
diagnostics.push(...parsed.diagnostics);
|
|
330
|
+
if (parsed.value.override_builtin === true)
|
|
331
|
+
diagnostics.push({
|
|
332
|
+
severity: 'warning', kind: 'deprecated-field', file, line: 1, column: 1,
|
|
333
|
+
message: `${file}: override_builtin is deprecated and ignored for a file-backed Room template`,
|
|
334
|
+
});
|
|
335
|
+
files.push(file);
|
|
336
|
+
const validated = validateRoomTemplatesConfig({ [id]: parsed.value }, file)[id];
|
|
337
|
+
result[id] = { ...validated, sourceFile: file };
|
|
338
|
+
sources.set(id, file);
|
|
339
|
+
}
|
|
340
|
+
return result;
|
|
341
|
+
}
|
|
268
342
|
function selection(raw, kind, agentFile, presets, allowed) {
|
|
269
343
|
if (!isPlainObject(raw))
|
|
270
344
|
schemaError(agentFile, `/${kind}`, 'must be { ref } or { inline }');
|
|
@@ -305,7 +379,23 @@ function deepSubStrict(value, vars, file, pointer) {
|
|
|
305
379
|
]));
|
|
306
380
|
return value;
|
|
307
381
|
}
|
|
308
|
-
function
|
|
382
|
+
function mergeTemplateDefinition(template, overrides, file, id) {
|
|
383
|
+
const merge = (base, overlay, key) => {
|
|
384
|
+
if (overlay === null && key !== 'model')
|
|
385
|
+
schemaError(file, `/agents/${id}/overrides/${key}`, 'null deletion is not supported');
|
|
386
|
+
if (MERGED_MAP_FIELDS.has(key) && isPlainObject(base) && isPlainObject(overlay)) {
|
|
387
|
+
return Object.fromEntries([...new Set([...Object.keys(base), ...Object.keys(overlay)])]
|
|
388
|
+
.map(child => [child, Object.hasOwn(overlay, child)
|
|
389
|
+
? merge(base[child], overlay[child], key) : structuredClone(base[child])]));
|
|
390
|
+
}
|
|
391
|
+
return structuredClone(overlay);
|
|
392
|
+
};
|
|
393
|
+
const result = structuredClone(template);
|
|
394
|
+
for (const [key, value] of Object.entries(overrides))
|
|
395
|
+
result[key] = merge(result[key], value, key);
|
|
396
|
+
return result;
|
|
397
|
+
}
|
|
398
|
+
function composeSplitRoles(base, baseDoc, yamlMode, diagnostics, files, additionalAgent, templateSink = {}, roleSink = {}, brainSink = {}) {
|
|
309
399
|
if (baseDoc.api_version !== V2_API_VERSION)
|
|
310
400
|
throw new ConfigError(`${base}: legacy fleet configuration is unsupported; run 'ours-fleet doctor' and rewrite it as ${V2_API_VERSION}`);
|
|
311
401
|
if ('roles' in baseDoc)
|
|
@@ -316,13 +406,77 @@ function composeSplitRoles(base, baseDoc, yamlMode, diagnostics, files) {
|
|
|
316
406
|
assertTrustedPath(root, 'directory');
|
|
317
407
|
const brains = readKindDirectory(join(root, 'brains'), 'brain', false, yamlMode, diagnostics, files);
|
|
318
408
|
const roles = readKindDirectory(join(root, 'roles'), 'role', false, yamlMode, diagnostics, files);
|
|
409
|
+
const agentTemplates = readKindDirectory(join(root, 'agent_templates'), 'agent template', false, yamlMode, diagnostics, files);
|
|
319
410
|
const agents = readKindDirectory(join(root, 'agents'), 'agent', true, yamlMode, diagnostics, files);
|
|
411
|
+
if (additionalAgent) {
|
|
412
|
+
if (agents.has(additionalAgent.id))
|
|
413
|
+
throw new ConfigError(`agent '${additionalAgent.id}' already exists`);
|
|
414
|
+
agents.set(additionalAgent.id, {
|
|
415
|
+
file: '(spawn request)', value: structuredClone(additionalAgent.definition),
|
|
416
|
+
});
|
|
417
|
+
}
|
|
320
418
|
const vars = (baseDoc.vars ?? {});
|
|
419
|
+
for (const [id, preset] of roles) {
|
|
420
|
+
const value = assertBareKeys(preset.value, ROLE_PRESET_KEYS, `${preset.file}: Role '${id}'`);
|
|
421
|
+
validateRoleValue(value, preset.file, `/roles/${id}`);
|
|
422
|
+
roleSink[id] = deepSubStrict(value, vars, preset.file, `/roles/${id}`);
|
|
423
|
+
}
|
|
424
|
+
for (const [id, preset] of brains) {
|
|
425
|
+
const value = assertBareKeys(preset.value, BRAIN_PRESET_KEYS, `${preset.file}: Brain '${id}'`);
|
|
426
|
+
validateBrainValue(value, preset.file, `/brains/${id}`);
|
|
427
|
+
brainSink[id] = deepSubStrict(value, vars, preset.file, `/brains/${id}`);
|
|
428
|
+
}
|
|
429
|
+
const canonicalTemplates = new Map();
|
|
430
|
+
for (const [id, template] of agentTemplates) {
|
|
431
|
+
const value = assertBareKeys(template.value, AGENT_KEYS, `${template.file}: agent template '${id}'`);
|
|
432
|
+
if (!('role' in value) || !('brain' in value))
|
|
433
|
+
throw new ConfigError(`${template.file}: agent template '${id}' requires role and brain`);
|
|
434
|
+
const forbidden = TEMPLATE_FORBIDDEN_KEYS.filter(key => Object.hasOwn(value, key));
|
|
435
|
+
if (forbidden.length)
|
|
436
|
+
schemaError(template.file, `/agent_templates/${id}/${forbidden[0]}`, 'templates cannot own instance identity');
|
|
437
|
+
const templateRole = selection(value.role, 'role', template.file, roles, ROLE_PRESET_KEYS);
|
|
438
|
+
const templateBrain = selection(value.brain, 'brain', template.file, brains, BRAIN_PRESET_KEYS);
|
|
439
|
+
validateRoleValue(templateRole.value, templateRole.file, templateRole.pointer);
|
|
440
|
+
validateBrainValue(templateBrain.value, templateBrain.file, templateBrain.pointer);
|
|
441
|
+
validateAgentScalars(Object.fromEntries(Object.entries(value).filter(([key]) => key !== 'role' && key !== 'brain')), template.file, id);
|
|
442
|
+
canonicalTemplates.set(id, { file: template.file, value });
|
|
443
|
+
const operational = deepSubStrict(Object.fromEntries(Object.entries(value).filter(([key]) => key !== 'role' && key !== 'brain')), vars, template.file, `/agent_templates/${id}`);
|
|
444
|
+
templateSink[id] = {
|
|
445
|
+
role: { inline: deepSubStrict(templateRole.value, vars, templateRole.file, templateRole.pointer) },
|
|
446
|
+
brain: { inline: deepSubStrict(templateBrain.value, vars, templateBrain.file, templateBrain.pointer) },
|
|
447
|
+
...operational,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
321
450
|
const documents = [];
|
|
322
451
|
for (const [id, agent] of agents) {
|
|
323
|
-
|
|
452
|
+
let a;
|
|
453
|
+
let agentTemplate;
|
|
454
|
+
const instanceKeys = Object.keys(agent.value);
|
|
455
|
+
if (instanceKeys.includes('template')) {
|
|
456
|
+
assertBareKeys(agent.value, AGENT_INSTANCE_KEYS, `${agent.file}: agent '${id}'`);
|
|
457
|
+
if (typeof agent.value.template !== 'string' || !ROLE_NAME_RE.test(agent.value.template))
|
|
458
|
+
schemaError(agent.file, `/agents/${id}/template`, 'must be a valid Agent Template ID');
|
|
459
|
+
const template = canonicalTemplates.get(agent.value.template);
|
|
460
|
+
if (!template)
|
|
461
|
+
throw new ConfigError(`${agent.file}: E_REF_MISSING: agent template '${agent.value.template}' not found`);
|
|
462
|
+
if (agent.value.overrides !== undefined && !isPlainObject(agent.value.overrides))
|
|
463
|
+
schemaError(agent.file, `/agents/${id}/overrides`, 'must be a mapping');
|
|
464
|
+
const overrides = assertBareKeys((agent.value.overrides ?? {}), AGENT_KEYS, `${agent.file}: agent '${id}' overrides`);
|
|
465
|
+
a = mergeTemplateDefinition(template.value, overrides, agent.file, id);
|
|
466
|
+
const resolvedTemplate = templateSink[agent.value.template];
|
|
467
|
+
agentTemplate = {
|
|
468
|
+
id: agent.value.template,
|
|
469
|
+
sourceFile: template.file,
|
|
470
|
+
contentHash: createHash('sha256').update(canonicalJson(resolvedTemplate)).digest('hex'),
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
a = assertBareKeys(agent.value, AGENT_KEYS, `${agent.file}: agent '${id}'`);
|
|
475
|
+
}
|
|
324
476
|
if (!('role' in a) || !('brain' in a))
|
|
325
477
|
throw new ConfigError(`${agent.file}: agent '${id}' requires role and brain`);
|
|
478
|
+
if (Object.hasOwn(a, 'loops') && !(additionalAgent?.temporary && additionalAgent.id === id))
|
|
479
|
+
schemaError(agent.file, `/agents/${id}/loops`, 'is supported only for temporary Agent launches and inert Agent Templates');
|
|
326
480
|
const role = selection(a.role, 'role', agent.file, roles, ROLE_PRESET_KEYS);
|
|
327
481
|
const brain = selection(a.brain, 'brain', agent.file, brains, BRAIN_PRESET_KEYS);
|
|
328
482
|
const operational = deepSubStrict(Object.fromEntries(Object.entries(a).filter(([key]) => key !== 'role' && key !== 'brain')), vars, agent.file, `/agents/${id}`);
|
|
@@ -392,8 +546,17 @@ function composeSplitRoles(base, baseDoc, yamlMode, diagnostics, files) {
|
|
|
392
546
|
: undefined);
|
|
393
547
|
delete provenance.role;
|
|
394
548
|
delete provenance.brain;
|
|
549
|
+
const canonicalDefinition = {
|
|
550
|
+
role: deepSubStrict(a.role, vars, agent.file, `/agents/${id}/role`),
|
|
551
|
+
brain: deepSubStrict(a.brain, vars, agent.file, `/agents/${id}/brain`),
|
|
552
|
+
...operational,
|
|
553
|
+
};
|
|
554
|
+
const { loops: _temporaryLoops, ...runtimeOperational } = operational;
|
|
395
555
|
documents.push({ file: agent.file,
|
|
396
|
-
doc: { roles: { [id]: { ...brainValue, ...roleValue, ...
|
|
556
|
+
doc: { roles: { [id]: { ...brainValue, ...roleValue, ...runtimeOperational } } }, provenance,
|
|
557
|
+
agentSelections: structuredClone({ role: canonicalDefinition.role, brain: canonicalDefinition.brain }),
|
|
558
|
+
agentDefinition: canonicalDefinition,
|
|
559
|
+
...(agentTemplate ? { agentTemplate } : {}) });
|
|
397
560
|
}
|
|
398
561
|
return documents;
|
|
399
562
|
}
|
|
@@ -411,6 +574,9 @@ export function loadConfig(configPath, options = {}) {
|
|
|
411
574
|
const base = configPath ?? defaultConfigPath();
|
|
412
575
|
const files = [];
|
|
413
576
|
const diagnostics = [];
|
|
577
|
+
const agentTemplates = {};
|
|
578
|
+
const rolePresets = {};
|
|
579
|
+
const brainPresets = {};
|
|
414
580
|
let docs = [];
|
|
415
581
|
let manifestDoc = {};
|
|
416
582
|
if (existsSync(base)) {
|
|
@@ -429,14 +595,20 @@ export function loadConfig(configPath, options = {}) {
|
|
|
429
595
|
const dd = fleetDDir();
|
|
430
596
|
if (existsSync(dd) && readdirSync(dd).some(f => f.endsWith('.yaml') || f.endsWith('.yml')))
|
|
431
597
|
throw new ConfigError(`${dd}: legacy fleet.d configuration is unsupported; run 'ours-fleet doctor' and move agents to ${splitRootFor(base)}/agents/`);
|
|
432
|
-
docs = [{ file: base, doc: manifestDoc }, ...composeSplitRoles(base, manifestDoc, options.yamlMode ?? 'compat', diagnostics, files)];
|
|
598
|
+
docs = [{ file: base, doc: manifestDoc }, ...composeSplitRoles(base, manifestDoc, options.yamlMode ?? 'compat', diagnostics, files, options.additionalAgent, agentTemplates, rolePresets, brainPresets)];
|
|
433
599
|
const baseDoc = manifestDoc;
|
|
434
600
|
const vars = (baseDoc.vars ?? {});
|
|
435
601
|
const defaults = (baseDoc.defaults ?? {});
|
|
602
|
+
const legacyRuntimeDefaults = ['harness', 'model', 'model_chain', 'effort', 'harness_options',
|
|
603
|
+
'session', 'session_options', 'max_tokens', 'autocompact_pct']
|
|
604
|
+
.filter(key => Object.hasOwn(defaults, key));
|
|
605
|
+
if (legacyRuntimeDefaults.length)
|
|
606
|
+
throw new ConfigError(`${base}: E_LEGACY /defaults: Brain-owned field(s) ${legacyRuntimeDefaults.join(', ')} are unsupported; move them to a Brain definition`);
|
|
436
607
|
const startStaggerMs = resolveStartStaggerMs(baseDoc.start_stagger_ms, base);
|
|
437
608
|
const seen = new Map();
|
|
438
609
|
const roles = [];
|
|
439
|
-
|
|
610
|
+
const agentDefinitions = {};
|
|
611
|
+
for (const { file, doc, provenance, agentSelections, agentDefinition, agentTemplate } of docs) {
|
|
440
612
|
for (const [name, raw] of Object.entries((doc.roles ?? {}))) {
|
|
441
613
|
if (!ROLE_NAME_RE.test(name))
|
|
442
614
|
throw new ConfigError(`${file}: invalid role name '${name}' (allowed: [A-Za-z0-9_-])`);
|
|
@@ -444,6 +616,8 @@ export function loadConfig(configPath, options = {}) {
|
|
|
444
616
|
if (prev)
|
|
445
617
|
throw new ConfigError(`role '${name}' defined in both ${prev} and ${file}`);
|
|
446
618
|
seen.set(name, file);
|
|
619
|
+
if (agentDefinition)
|
|
620
|
+
agentDefinitions[name] = structuredClone(agentDefinition);
|
|
447
621
|
const r = deepSub(raw ?? {}, vars);
|
|
448
622
|
const bad = Object.keys(r).filter(k => !ROLE_KEYS.includes(k));
|
|
449
623
|
if (bad.length)
|
|
@@ -474,6 +648,8 @@ export function loadConfig(configPath, options = {}) {
|
|
|
474
648
|
const worklog = resolveWorklogPolicy(defaults.worklog, r.worklog, file, name);
|
|
475
649
|
const authProxy = resolveAuthProxy(defaults.auth_proxy, r.auth_proxy, file, name);
|
|
476
650
|
const harness = r.harness ?? defaults.harness ?? 'claude-code';
|
|
651
|
+
if (session === 'codex-app-server' && harness !== 'codex')
|
|
652
|
+
throw new ConfigError(`${file}: role '${name}' session: codex-app-server requires harness: codex`);
|
|
477
653
|
const defaultHarness = defaults.harness ?? 'claude-code';
|
|
478
654
|
const inheritsModelDefaults = harness === defaultHarness && r.model !== null;
|
|
479
655
|
if (authProxy && harness !== 'claude-code')
|
|
@@ -508,7 +684,7 @@ export function loadConfig(configPath, options = {}) {
|
|
|
508
684
|
defaulted('identity', r.identity !== undefined, r.identity === undefined);
|
|
509
685
|
defaulted('permissions', defaults.permissions !== undefined, defaults.permissions === undefined);
|
|
510
686
|
defaulted('monitor', defaults.monitor !== undefined, defaults.monitor === undefined);
|
|
511
|
-
|
|
687
|
+
const resolvedRole = {
|
|
512
688
|
...r,
|
|
513
689
|
name,
|
|
514
690
|
sourceFile: file,
|
|
@@ -530,7 +706,12 @@ export function loadConfig(configPath, options = {}) {
|
|
|
530
706
|
auth_proxy: authProxy,
|
|
531
707
|
env: Object.keys(env).length ? env : undefined,
|
|
532
708
|
loops: [],
|
|
533
|
-
|
|
709
|
+
...(agentSelections ? { agentSelections } : {}),
|
|
710
|
+
...(agentTemplate ? { agentTemplate } : {}),
|
|
711
|
+
};
|
|
712
|
+
if (agentDefinition?.loops !== undefined)
|
|
713
|
+
resolvedRole.temporaryLoops = resolveAgentLoops(agentDefinition.loops, resolvedRole, file);
|
|
714
|
+
roles.push(resolvedRole);
|
|
534
715
|
// Forbidden-path enforcement: a mount that would breach the policy
|
|
535
716
|
// is a configuration error, caught by `config` rather than at launch.
|
|
536
717
|
if (isolation !== undefined) {
|
|
@@ -545,7 +726,12 @@ export function loadConfig(configPath, options = {}) {
|
|
|
545
726
|
}
|
|
546
727
|
}
|
|
547
728
|
validateOwnerChannelIdentities(roles);
|
|
548
|
-
const watchdogs = resolveWatchdogs(baseDoc, base, roles, vars,
|
|
729
|
+
const watchdogs = options.skipWatchdogs ? [] : resolveWatchdogs(baseDoc, base, roles, vars, agentDefinitions, (name, definition) => {
|
|
730
|
+
const id = `WatchdogAgent${name}`.slice(0, 64);
|
|
731
|
+
return findRole(loadConfig(base, {
|
|
732
|
+
yamlMode: options.yamlMode, additionalAgent: { id, definition }, skipWatchdogs: true,
|
|
733
|
+
}), id);
|
|
734
|
+
});
|
|
549
735
|
const resolvedLoops = resolveLoops(baseDoc.loops, base, roles, vars);
|
|
550
736
|
for (const role of roles)
|
|
551
737
|
role.loops = resolvedLoops.byRole.get(role.name) ?? [];
|
|
@@ -558,6 +744,9 @@ export function loadConfig(configPath, options = {}) {
|
|
|
558
744
|
let ownerInvite;
|
|
559
745
|
let roomTemplates;
|
|
560
746
|
let tasks;
|
|
747
|
+
const fileTemplates = readRoomTemplateDirectory(join(splitRootFor(base), 'room_templates'), options.yamlMode ?? 'compat', diagnostics, files);
|
|
748
|
+
if (Object.keys(fileTemplates).length)
|
|
749
|
+
roomTemplates = fileTemplates;
|
|
561
750
|
for (const { file, doc } of docs) {
|
|
562
751
|
if (doc.rooms !== undefined) {
|
|
563
752
|
if (rooms)
|
|
@@ -570,7 +759,21 @@ export function loadConfig(configPath, options = {}) {
|
|
|
570
759
|
}
|
|
571
760
|
if (doc.room_templates !== undefined) {
|
|
572
761
|
const validated = validateRoomTemplatesConfig(deepSub(doc.room_templates, vars), file);
|
|
573
|
-
|
|
762
|
+
const raw = doc.room_templates;
|
|
763
|
+
for (const [name, template] of Object.entries(validated)) {
|
|
764
|
+
const shadowed = roomTemplates?.[name];
|
|
765
|
+
const marker = raw[name]?.override_builtin;
|
|
766
|
+
if (shadowed && (marker !== true || template.version <= shadowed.version))
|
|
767
|
+
throw new ConfigError(`${file}: room_templates.${name} shadows file preset ${shadowed.sourceFile}; `
|
|
768
|
+
+ 'set override_builtin: true and use a higher version');
|
|
769
|
+
if (marker === true)
|
|
770
|
+
diagnostics.push({
|
|
771
|
+
severity: 'warning', kind: 'deprecated-field', file, line: 1, column: 1,
|
|
772
|
+
message: `${file}: room_templates.${name}.override_builtin is deprecated; `
|
|
773
|
+
+ 'it is retained only as an explicit manifest-over-file migration marker',
|
|
774
|
+
});
|
|
775
|
+
roomTemplates = { ...(roomTemplates ?? {}), [name]: { ...template, sourceFile: file } };
|
|
776
|
+
}
|
|
574
777
|
}
|
|
575
778
|
if (doc.tasks !== undefined) {
|
|
576
779
|
if (tasks)
|
|
@@ -579,7 +782,7 @@ export function loadConfig(configPath, options = {}) {
|
|
|
579
782
|
}
|
|
580
783
|
}
|
|
581
784
|
const result = {
|
|
582
|
-
roles, vars, defaults, files, startStaggerMs, diagnostics, watchdogs,
|
|
785
|
+
roles, agentDefinitions, agentTemplates, rolePresets, brainPresets, vars, defaults, files, startStaggerMs, diagnostics, watchdogs,
|
|
583
786
|
loops: resolvedLoops.loops,
|
|
584
787
|
rooms, roomTemplates, tasks, ownerInviteFingerprint,
|
|
585
788
|
configMode: 'split-v2',
|
|
@@ -588,10 +791,18 @@ export function loadConfig(configPath, options = {}) {
|
|
|
588
791
|
return { kind: 'Manifest', path };
|
|
589
792
|
const parent = basename(dirname(path));
|
|
590
793
|
const kind = parent === 'agents' ? 'Agent'
|
|
591
|
-
: parent === '
|
|
794
|
+
: parent === 'agent_templates' ? 'AgentTemplate'
|
|
795
|
+
: parent === 'roles' ? 'Role'
|
|
796
|
+
: parent === 'room_templates' ? 'RoomTemplate' : 'Brain';
|
|
592
797
|
return { kind, id: basename(path, extname(path)), path };
|
|
593
798
|
}),
|
|
594
799
|
};
|
|
800
|
+
Object.defineProperty(result, 'resolveAgentDefinition', {
|
|
801
|
+
value: (id, definition) => resolveAgentDefinitionDryRun(base, id, definition),
|
|
802
|
+
enumerable: false,
|
|
803
|
+
writable: false,
|
|
804
|
+
configurable: false,
|
|
805
|
+
});
|
|
595
806
|
if (ownerInvite !== undefined) {
|
|
596
807
|
Object.defineProperty(result, 'ownerInvite', {
|
|
597
808
|
value: ownerInvite,
|
|
@@ -602,6 +813,12 @@ export function loadConfig(configPath, options = {}) {
|
|
|
602
813
|
}
|
|
603
814
|
return result;
|
|
604
815
|
}
|
|
816
|
+
/** Full read-only Agent resolution used by launch dry-runs and room plan validation. */
|
|
817
|
+
export function resolveAgentDefinitionDryRun(configPath, id, definition) {
|
|
818
|
+
return findRole(loadConfig(configPath, {
|
|
819
|
+
additionalAgent: { id, definition, temporary: true }, skipWatchdogs: true,
|
|
820
|
+
}), id);
|
|
821
|
+
}
|
|
605
822
|
/**
|
|
606
823
|
* Canonical form of a 64-hex container ID for authorization decisions. Hex
|
|
607
824
|
* case is not identity: two casings of one CID are the same peer, so every
|
|
@@ -685,8 +902,8 @@ export function resolveOwnerChannelConfig(defaults, role, session, file = 'confi
|
|
|
685
902
|
const maxRequestBytes = attachments.max_request_bytes ?? 20 * 1024 * 1024;
|
|
686
903
|
if (maxRequestBytes < maxFileBytes)
|
|
687
904
|
throw new ConfigError(`${file}: role '${name}' owner_channel.attachments.max_request_bytes must be at least max_file_bytes`);
|
|
688
|
-
if (session !== 'acp')
|
|
689
|
-
throw new ConfigError(`${file}: role '${name}' owner_channel requires session
|
|
905
|
+
if (session !== 'acp' && session !== 'codex-app-server')
|
|
906
|
+
throw new ConfigError(`${file}: role '${name}' owner_channel requires a managed session with correlated final replies`);
|
|
690
907
|
return {
|
|
691
908
|
identity: merged.identity.trim(),
|
|
692
909
|
owners,
|
|
@@ -706,7 +923,13 @@ export function resolveOwnerChannelConfig(defaults, role, session, file = 'confi
|
|
|
706
923
|
};
|
|
707
924
|
}
|
|
708
925
|
function validateOwnerChannelIdentities(roles) {
|
|
709
|
-
const roleIdentities = new Map(
|
|
926
|
+
const roleIdentities = new Map();
|
|
927
|
+
for (const role of roles) {
|
|
928
|
+
const prior = roleIdentities.get(role.identity);
|
|
929
|
+
if (prior)
|
|
930
|
+
throw new ConfigError(`persistent Agents '${prior}' and '${role.name}' resolve to duplicate identity '${role.identity}'`);
|
|
931
|
+
roleIdentities.set(role.identity, role.name);
|
|
932
|
+
}
|
|
710
933
|
const channels = new Map();
|
|
711
934
|
for (const role of roles) {
|
|
712
935
|
const identity = role.owner_channel?.identity;
|
|
@@ -812,8 +1035,8 @@ function resolveSession(raw, file, name) {
|
|
|
812
1035
|
if (value === 'tmux')
|
|
813
1036
|
throw new ConfigError(`${file}: role '${name}' session: tmux is no longer supported; use session: acp `
|
|
814
1037
|
+ 'with the Codex or Claude Code adapter');
|
|
815
|
-
if (value !== 'acp')
|
|
816
|
-
throw new ConfigError(`${file}: role '${name}' session: must be: acp`);
|
|
1038
|
+
if (value !== 'acp' && value !== 'codex-app-server')
|
|
1039
|
+
throw new ConfigError(`${file}: role '${name}' session: must be: acp, codex-app-server`);
|
|
817
1040
|
return value;
|
|
818
1041
|
}
|
|
819
1042
|
function resolveSessionOptions(defaults, role, session, file, name) {
|
|
@@ -828,26 +1051,40 @@ function resolveSessionOptions(defaults, role, session, file, name) {
|
|
|
828
1051
|
...((defaults?.acp) ?? {}),
|
|
829
1052
|
...(role?.acp ?? {}),
|
|
830
1053
|
},
|
|
1054
|
+
codex_app_server: {
|
|
1055
|
+
...((defaults?.codex_app_server) ?? {}),
|
|
1056
|
+
...(role?.codex_app_server ?? {}),
|
|
1057
|
+
},
|
|
831
1058
|
};
|
|
832
1059
|
if (defaults?.tmux !== undefined
|
|
833
1060
|
|| role?.tmux !== undefined)
|
|
834
1061
|
throw new ConfigError(`${file}: role '${name}' session_options.tmux is no longer supported; `
|
|
835
1062
|
+ 'use session: acp with session_options.acp');
|
|
836
|
-
const bad = Object.keys(merged).filter(k => k !== 'acp');
|
|
1063
|
+
const bad = Object.keys(merged).filter(k => k !== 'acp' && k !== 'codex_app_server');
|
|
837
1064
|
if (bad.length)
|
|
838
1065
|
throw new ConfigError(`${file}: role '${name}' session_options: unknown key(s) ${bad.join(', ')}`);
|
|
839
1066
|
if (!isPlainObject(merged.acp))
|
|
840
|
-
throw new ConfigError(`${file}: role '${name}' session_options
|
|
1067
|
+
throw new ConfigError(`${file}: role '${name}' session_options.acp must be a map`);
|
|
1068
|
+
if (!isPlainObject(merged.codex_app_server))
|
|
1069
|
+
throw new ConfigError(`${file}: role '${name}' session_options.codex_app_server must be a map`);
|
|
841
1070
|
const acpBad = Object.keys(merged.acp).filter(k => k !== 'command');
|
|
842
1071
|
if (acpBad.length)
|
|
843
1072
|
throw new ConfigError(`${file}: role '${name}' session_options.acp: unknown key(s) ${acpBad.join(', ')}`);
|
|
844
|
-
const
|
|
845
|
-
if (
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
1073
|
+
const nativeBad = Object.keys(merged.codex_app_server).filter(k => k !== 'command');
|
|
1074
|
+
if (nativeBad.length)
|
|
1075
|
+
throw new ConfigError(`${file}: role '${name}' session_options.codex_app_server: unknown key(s) ${nativeBad.join(', ')}`);
|
|
1076
|
+
for (const [key, command] of [
|
|
1077
|
+
['acp', merged.acp.command],
|
|
1078
|
+
['codex_app_server', merged.codex_app_server.command],
|
|
1079
|
+
]) {
|
|
1080
|
+
if (command !== undefined
|
|
1081
|
+
&& !(typeof command === 'string' && command.trim())
|
|
1082
|
+
&& !(Array.isArray(command) && command.length > 0
|
|
1083
|
+
&& command.every(v => typeof v === 'string' && v.length > 0)))
|
|
1084
|
+
throw new ConfigError(`${file}: role '${name}' session_options.${key}.command must be a non-empty string or string list`);
|
|
1085
|
+
}
|
|
1086
|
+
return Object.keys(merged.acp).length || Object.keys(merged.codex_app_server).length
|
|
1087
|
+
? merged : undefined;
|
|
851
1088
|
}
|
|
852
1089
|
export function resolvePermissions(defaults, role, file = 'config', name = 'role') {
|
|
853
1090
|
if (defaults !== undefined && !isPlainObject(defaults))
|
|
@@ -932,7 +1169,10 @@ export function resolveMonitorConfig(defMonitor, roleMonitor, labels = {}) {
|
|
|
932
1169
|
}
|
|
933
1170
|
export function findRole(cfg, name) {
|
|
934
1171
|
const r = cfg.roles.find(r => r.name === name);
|
|
935
|
-
if (!r)
|
|
1172
|
+
if (!r) {
|
|
1173
|
+
if (cfg.agentTemplates?.[name])
|
|
1174
|
+
throw new ConfigError(`'${name}' is an inert Agent Template, not a persistent Agent; lifecycle commands target only fleet/agents instances`);
|
|
936
1175
|
throw new ConfigError(`no such role '${name}' in ${cfg.files.join(', ') || 'config'}`);
|
|
1176
|
+
}
|
|
937
1177
|
return r;
|
|
938
1178
|
}
|
package/dist/creation.d.ts
CHANGED
|
@@ -156,7 +156,7 @@ export declare function daemonIdentityInventoryProvisioner(env?: NodeJS.ProcessE
|
|
|
156
156
|
/** Atomically write a bare Agent file, journalling it for rollback. */
|
|
157
157
|
export declare function writeRoleFile(tx: CreationTransaction, file: string, contents: string): void;
|
|
158
158
|
/** Where a setting's effective value came from. */
|
|
159
|
-
export type ProvenanceSource = 'cli' | 'fleet-default' | 'caller-role' | 'built-in';
|
|
159
|
+
export type ProvenanceSource = 'cli' | 'agent-template' | 'fleet-default' | 'caller-role' | 'built-in';
|
|
160
160
|
export interface ProvenanceEntry {
|
|
161
161
|
value: unknown;
|
|
162
162
|
source: ProvenanceSource;
|
package/dist/creation.js
CHANGED
|
@@ -374,11 +374,12 @@ export function creationBuildNote(p) {
|
|
|
374
374
|
export function formatProvenance(p) {
|
|
375
375
|
const mark = {
|
|
376
376
|
cli: 'explicit', 'fleet-default': 'fleet default', 'caller-role': 'caller role',
|
|
377
|
-
'built-in': 'built-in',
|
|
377
|
+
'agent-template': 'Agent Template', 'built-in': 'built-in',
|
|
378
378
|
};
|
|
379
379
|
return Object.entries(p.settings)
|
|
380
380
|
.filter(([, e]) => e.value !== undefined)
|
|
381
|
-
.map(([k, e]) => ` ${k.padEnd(12)} ${
|
|
381
|
+
.map(([k, e]) => ` ${k.padEnd(12)} ${e.value && typeof e.value === 'object'
|
|
382
|
+
? JSON.stringify(e.value) : String(e.value)} (${mark[e.source]})`);
|
|
382
383
|
}
|
|
383
384
|
/** Classify one setting: an explicit CLI value, a fleet default, or built-in. */
|
|
384
385
|
export function provenanceOf(cliValue, fleetDefault, builtIn) {
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { type FetchLike } from './monitor.js';
|
|
2
|
+
export declare const DAEMON_RECOVERY_MAX_ATTEMPTS = 6;
|
|
3
|
+
export declare const DAEMON_RECOVERY_INITIAL_BACKOFF_MS = 1000;
|
|
4
|
+
export declare const DAEMON_RECOVERY_MAX_BACKOFF_MS = 5000;
|
|
5
|
+
export declare const DAEMON_RECOVERY_DEADLINE_MS = 60000;
|
|
6
|
+
export interface DaemonGeneration {
|
|
7
|
+
bootId: string;
|
|
8
|
+
pid: number;
|
|
9
|
+
startedAt: number;
|
|
10
|
+
stateDir: string;
|
|
11
|
+
}
|
|
12
|
+
export type DaemonGenerationProbe = {
|
|
13
|
+
state: 'ready';
|
|
14
|
+
generation: DaemonGeneration;
|
|
15
|
+
} | {
|
|
16
|
+
state: 'unavailable';
|
|
17
|
+
reason: string;
|
|
18
|
+
};
|
|
19
|
+
export type DaemonGenerationObservation = {
|
|
20
|
+
kind: 'baseline' | 'stable' | 'available';
|
|
21
|
+
generation: DaemonGeneration;
|
|
22
|
+
} | {
|
|
23
|
+
kind: 'changed';
|
|
24
|
+
previous: DaemonGeneration;
|
|
25
|
+
generation: DaemonGeneration;
|
|
26
|
+
} | {
|
|
27
|
+
kind: 'lost' | 'unavailable';
|
|
28
|
+
previous?: DaemonGeneration;
|
|
29
|
+
reason: string;
|
|
30
|
+
};
|
|
31
|
+
interface GenerationProbeDeps {
|
|
32
|
+
readText?(path: string): string;
|
|
33
|
+
canonicalize?(path: string): string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Corroborate the loopback daemon's unauthenticated `/info`, its credentialed
|
|
37
|
+
* identity-index readiness route, and its local boot-generation record. The
|
|
38
|
+
* index enforces auth when the daemon visibility requires it; open visibility
|
|
39
|
+
* deliberately does not. No source is sufficient
|
|
40
|
+
* alone: `/info` has no boot id, the identity index has no generation, and a
|
|
41
|
+
* stale `ready` file can outlive the process that wrote it.
|
|
42
|
+
*/
|
|
43
|
+
export declare function probeDaemonGeneration(fetch: FetchLike, env: NodeJS.ProcessEnv, deps?: GenerationProbeDeps): Promise<DaemonGenerationProbe>;
|
|
44
|
+
export declare class DaemonGenerationObserver {
|
|
45
|
+
private current?;
|
|
46
|
+
private unavailable;
|
|
47
|
+
observe(probe: DaemonGenerationProbe): DaemonGenerationObservation;
|
|
48
|
+
}
|
|
49
|
+
export declare function daemonRecoveryBackoff(attempt: number): number;
|
|
50
|
+
export type RecoveryPath = 'agent' | 'owner';
|
|
51
|
+
export type RecoveryPathResult = {
|
|
52
|
+
ok: true;
|
|
53
|
+
} | {
|
|
54
|
+
ok: false;
|
|
55
|
+
reason: string;
|
|
56
|
+
};
|
|
57
|
+
export interface RoleRecoveryControllerOptions {
|
|
58
|
+
role: string;
|
|
59
|
+
identity: string;
|
|
60
|
+
stateDir: string;
|
|
61
|
+
now(): number;
|
|
62
|
+
sleep(ms: number): Promise<void>;
|
|
63
|
+
recoverAgent(epoch: string): Promise<RecoveryPathResult>;
|
|
64
|
+
recoverOwner(epoch: string): Promise<RecoveryPathResult>;
|
|
65
|
+
log(line: string): void;
|
|
66
|
+
}
|
|
67
|
+
export interface RecoveryStatus {
|
|
68
|
+
version: 1;
|
|
69
|
+
identity: string;
|
|
70
|
+
epoch: string;
|
|
71
|
+
state: 'recovering' | 'recovered' | 'degraded' | 'cancelled';
|
|
72
|
+
paths: Record<RecoveryPath, {
|
|
73
|
+
state: 'pending' | 'recovered' | 'degraded';
|
|
74
|
+
attempts: number;
|
|
75
|
+
reason?: string;
|
|
76
|
+
}>;
|
|
77
|
+
updatedAt: string;
|
|
78
|
+
}
|
|
79
|
+
export declare function readDaemonRecoveryStatus(dir: string): RecoveryStatus | undefined;
|
|
80
|
+
/** Per-role, per-generation bounded recovery with path-level fault isolation. */
|
|
81
|
+
export declare class RoleRecoveryController {
|
|
82
|
+
private readonly options;
|
|
83
|
+
private token;
|
|
84
|
+
private activeEpoch?;
|
|
85
|
+
private active?;
|
|
86
|
+
private status?;
|
|
87
|
+
constructor(options: RoleRecoveryControllerOptions);
|
|
88
|
+
recover(generation: DaemonGeneration): Promise<RecoveryStatus>;
|
|
89
|
+
cancel(): void;
|
|
90
|
+
noteLoss(reason: string): void;
|
|
91
|
+
private write;
|
|
92
|
+
}
|
|
93
|
+
export {};
|