@ours.network/fleet 1.1.0-nightly.2 → 1.1.0-nightly.20
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 +169 -54
- 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 +426 -78
- 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 +227 -73
- package/dist/config-yaml.d.ts +1 -1
- package/dist/config.d.ts +67 -2
- package/dist/config.js +260 -30
- 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 +171 -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 +543 -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 +118 -0
- package/dist/lifecycle-summary.js +161 -0
- package/dist/loops/config.js +3 -2
- 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 +4 -0
- package/dist/rooms-tasks/cli.d.ts +2 -0
- package/dist/rooms-tasks/cli.js +503 -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 +176 -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 +179 -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 +94 -17
- package/dist/rooms-tasks/types.js +14 -4
- package/dist/runner.d.ts +9 -2
- package/dist/runner.js +107 -40
- 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 +6 -31
- package/dist/spawn.js +57 -234
- 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.d.ts
CHANGED
|
@@ -25,7 +25,8 @@ export declare const NOTIFY_EVENT_TYPES: readonly ["message_received", "file_rec
|
|
|
25
25
|
export type NotifyEventType = (typeof NOTIFY_EVENT_TYPES)[number];
|
|
26
26
|
export type InjectMode = 'notification' | 'full';
|
|
27
27
|
export type MonitorMode = 'fleet' | 'native';
|
|
28
|
-
|
|
28
|
+
/** Concrete transport selected for a role. Keep provider-native transports explicit. */
|
|
29
|
+
export type SessionBackendId = 'acp' | 'codex-app-server';
|
|
29
30
|
/** Public, harness-neutral permission policy. */
|
|
30
31
|
export type FleetPermissionMode = 'ask' | 'auto' | 'allow';
|
|
31
32
|
/** `deny` is a deprecated, fail-closed compatibility alias retained for old fleet files. */
|
|
@@ -44,6 +45,10 @@ export interface SessionOptions {
|
|
|
44
45
|
/** ACP agent command and arguments. Defaults are supplied by the harness adapter. */
|
|
45
46
|
command?: string | string[];
|
|
46
47
|
};
|
|
48
|
+
codex_app_server?: {
|
|
49
|
+
/** App-server command and arguments. Defaults to `codex app-server`. */
|
|
50
|
+
command?: string | string[];
|
|
51
|
+
};
|
|
47
52
|
}
|
|
48
53
|
/** Resolved per-role supervisor-monitor config. */
|
|
49
54
|
export interface MonitorConfig {
|
|
@@ -137,6 +142,34 @@ export interface RoleConfig {
|
|
|
137
142
|
worklog?: WorklogPolicyInput;
|
|
138
143
|
auth_proxy?: Partial<AuthProxyConfig>;
|
|
139
144
|
}
|
|
145
|
+
export type AgentSelection<T extends Record<string, unknown> = Record<string, unknown>> = {
|
|
146
|
+
ref: string;
|
|
147
|
+
} | {
|
|
148
|
+
inline: T;
|
|
149
|
+
};
|
|
150
|
+
/** Canonical authoring contract shared by configured, spawned, and room agents. */
|
|
151
|
+
export interface AgentDefinition {
|
|
152
|
+
role: AgentSelection;
|
|
153
|
+
brain: AgentSelection;
|
|
154
|
+
permissions?: Partial<CommonPermissions>;
|
|
155
|
+
identity?: string;
|
|
156
|
+
cwd?: string;
|
|
157
|
+
coordinator?: string;
|
|
158
|
+
env?: Record<string, string>;
|
|
159
|
+
oversee?: OverseeEntry[];
|
|
160
|
+
isolation?: IsolationConfig;
|
|
161
|
+
monitor?: Partial<MonitorConfig>;
|
|
162
|
+
owner_channel?: OwnerChannelConfigInput;
|
|
163
|
+
worklog?: WorklogPolicyInput;
|
|
164
|
+
auth_proxy?: Partial<AuthProxyConfig>;
|
|
165
|
+
}
|
|
166
|
+
/** Inert, reusable launch definition. It never owns an identity or runtime state. */
|
|
167
|
+
export type AgentTemplateDefinition = Omit<AgentDefinition, 'identity'>;
|
|
168
|
+
/** Explicit persistent instance reusing an inert template. */
|
|
169
|
+
export interface AgentTemplateInstance {
|
|
170
|
+
template: string;
|
|
171
|
+
overrides?: Partial<AgentDefinition>;
|
|
172
|
+
}
|
|
140
173
|
/** Internal first-boot payload for a Fleet-provisioned Cowork room member. */
|
|
141
174
|
export interface RoomMemberStartup {
|
|
142
175
|
room_id: string;
|
|
@@ -147,6 +180,7 @@ export interface RoomMemberStartup {
|
|
|
147
180
|
role: string;
|
|
148
181
|
task: string;
|
|
149
182
|
owner_seat_cid: string | null;
|
|
183
|
+
anonymous?: boolean;
|
|
150
184
|
}
|
|
151
185
|
export interface ResolvedRole extends Omit<RoleConfig, 'model' | 'owner_channel' | 'worklog'> {
|
|
152
186
|
name: string;
|
|
@@ -171,6 +205,13 @@ export interface ResolvedRole extends Omit<RoleConfig, 'model' | 'owner_channel'
|
|
|
171
205
|
provenance?: Record<string, FieldProvenance>;
|
|
172
206
|
/** Internal/transient: never accepted as a user-authored RoleConfig key. */
|
|
173
207
|
roomMemberStartup?: RoomMemberStartup;
|
|
208
|
+
/** Original unresolved selections only; denied operational fields never enter inheritance state. */
|
|
209
|
+
agentSelections?: Pick<AgentDefinition, 'role' | 'brain'>;
|
|
210
|
+
agentTemplate?: {
|
|
211
|
+
id: string;
|
|
212
|
+
sourceFile: string;
|
|
213
|
+
contentHash: string;
|
|
214
|
+
};
|
|
174
215
|
}
|
|
175
216
|
export interface FieldProvenance {
|
|
176
217
|
sourceFile: string;
|
|
@@ -190,12 +231,20 @@ export interface FieldProvenance {
|
|
|
190
231
|
}
|
|
191
232
|
export interface FleetConfig {
|
|
192
233
|
roles: ResolvedRole[];
|
|
234
|
+
/** Canonical, variable-resolved Agent authoring documents keyed by stable Agent ID. */
|
|
235
|
+
agentDefinitions?: Record<string, AgentDefinition>;
|
|
236
|
+
/** Canonical inert templates keyed by stable template ID. */
|
|
237
|
+
agentTemplates?: Record<string, AgentTemplateDefinition>;
|
|
238
|
+
rolePresets?: Record<string, Record<string, unknown>>;
|
|
239
|
+
brainPresets?: Record<string, Record<string, unknown>>;
|
|
240
|
+
/** Required launch-boundary resolver for effective temporary Agent definitions. */
|
|
241
|
+
resolveAgentDefinition?: (id: string, definition: AgentDefinition) => ResolvedRole;
|
|
193
242
|
vars: Record<string, string>;
|
|
194
243
|
defaults: Record<string, unknown>;
|
|
195
244
|
files: string[];
|
|
196
245
|
configMode?: 'split-v2';
|
|
197
246
|
sourceDocuments?: Array<{
|
|
198
|
-
kind: 'Manifest' | 'Agent' | 'Role' | 'Brain';
|
|
247
|
+
kind: 'Manifest' | 'Agent' | 'AgentTemplate' | 'Role' | 'Brain' | 'RoomTemplate';
|
|
199
248
|
id?: string;
|
|
200
249
|
path: string;
|
|
201
250
|
}>;
|
|
@@ -229,11 +278,27 @@ export declare function resolveRoleModel(model: string | null | undefined, harne
|
|
|
229
278
|
*/
|
|
230
279
|
export declare function isolationContextFor(role: ResolvedRole): WrapContext;
|
|
231
280
|
export declare const ROLE_NAME_RE: RegExp;
|
|
281
|
+
export declare const ROLE_PRESET_KEYS: string[];
|
|
282
|
+
export declare const BRAIN_PRESET_KEYS: string[];
|
|
283
|
+
export declare const AGENT_KEYS: string[];
|
|
284
|
+
type BarePreset = Record<string, unknown>;
|
|
285
|
+
export declare function validateRoleValue(value: BarePreset, file: string, pointer: string): void;
|
|
286
|
+
export declare function validateBrainValue(value: BarePreset, file: string, pointer: string): void;
|
|
287
|
+
/** Pure launch-boundary validation for a fully resolved temporary Agent definition. */
|
|
288
|
+
export declare function validateEffectiveAgentTemplate(definition: AgentTemplateDefinition, id?: string): AgentTemplateDefinition;
|
|
289
|
+
export declare function assertBareKeys(value: unknown, allowed: string[], label: string): BarePreset;
|
|
232
290
|
export declare function splitRootFor(base: string): string;
|
|
233
291
|
/** Load a v2 manifest and bare Agent/Role/Brain documents from its stem directory. */
|
|
234
292
|
export declare function loadConfig(configPath?: string, options?: {
|
|
235
293
|
yamlMode?: YamlMode;
|
|
294
|
+
additionalAgent?: {
|
|
295
|
+
id: string;
|
|
296
|
+
definition: AgentDefinition;
|
|
297
|
+
};
|
|
298
|
+
skipWatchdogs?: boolean;
|
|
236
299
|
}): FleetConfig;
|
|
300
|
+
/** Full read-only Agent resolution used by launch dry-runs and room plan validation. */
|
|
301
|
+
export declare function resolveAgentDefinitionDryRun(configPath: string, id: string, definition: AgentDefinition): ResolvedRole;
|
|
237
302
|
/**
|
|
238
303
|
* Canonical form of a 64-hex container ID for authorization decisions. Hex
|
|
239
304
|
* 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,
|
|
@@ -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
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) {
|
|
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,6 +186,40 @@ 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];
|
|
@@ -228,7 +269,7 @@ function assertTrustedPath(path, expected) {
|
|
|
228
269
|
if ((st.mode & 0o022) !== 0)
|
|
229
270
|
throw new ConfigError(`E_FILE_TRUST: ${path} is group/world writable (mode ${(st.mode & 0o777).toString(8)})`);
|
|
230
271
|
}
|
|
231
|
-
function assertBareKeys(value, allowed, label) {
|
|
272
|
+
export function assertBareKeys(value, allowed, label) {
|
|
232
273
|
if (!isPlainObject(value))
|
|
233
274
|
throw new ConfigError(`${label}: E_SCHEMA: must be a mapping`);
|
|
234
275
|
const bad = Object.keys(value).filter(key => !allowed.includes(key));
|
|
@@ -265,6 +306,35 @@ function readKindDirectory(root, kind, required, yamlMode, diagnostics, files) {
|
|
|
265
306
|
}
|
|
266
307
|
return result;
|
|
267
308
|
}
|
|
309
|
+
function readRoomTemplateDirectory(root, yamlMode, diagnostics, files) {
|
|
310
|
+
if (!existsSync(root))
|
|
311
|
+
return {};
|
|
312
|
+
assertTrustedPath(root, 'directory');
|
|
313
|
+
const result = {};
|
|
314
|
+
const sources = new Map();
|
|
315
|
+
const names = readdirSync(root)
|
|
316
|
+
.filter(name => ['.yaml', '.yml'].includes(extname(name).toLowerCase())).sort();
|
|
317
|
+
for (const filename of names) {
|
|
318
|
+
const file = join(root, filename);
|
|
319
|
+
assertTrustedPath(file, 'file');
|
|
320
|
+
const id = basename(filename, extname(filename));
|
|
321
|
+
const previous = sources.get(id);
|
|
322
|
+
if (previous)
|
|
323
|
+
throw new ConfigError(`E_DUPLICATE_ID: room template '${id}' defined by both ${previous} and ${file}`);
|
|
324
|
+
const parsed = parseFleetDocument(file, readFileSync(file, 'utf8'), yamlMode);
|
|
325
|
+
diagnostics.push(...parsed.diagnostics);
|
|
326
|
+
if (parsed.value.override_builtin === true)
|
|
327
|
+
diagnostics.push({
|
|
328
|
+
severity: 'warning', kind: 'deprecated-field', file, line: 1, column: 1,
|
|
329
|
+
message: `${file}: override_builtin is deprecated and ignored for a file-backed Room template`,
|
|
330
|
+
});
|
|
331
|
+
files.push(file);
|
|
332
|
+
const validated = validateRoomTemplatesConfig({ [id]: parsed.value }, file)[id];
|
|
333
|
+
result[id] = { ...validated, sourceFile: file };
|
|
334
|
+
sources.set(id, file);
|
|
335
|
+
}
|
|
336
|
+
return result;
|
|
337
|
+
}
|
|
268
338
|
function selection(raw, kind, agentFile, presets, allowed) {
|
|
269
339
|
if (!isPlainObject(raw))
|
|
270
340
|
schemaError(agentFile, `/${kind}`, 'must be { ref } or { inline }');
|
|
@@ -305,7 +375,23 @@ function deepSubStrict(value, vars, file, pointer) {
|
|
|
305
375
|
]));
|
|
306
376
|
return value;
|
|
307
377
|
}
|
|
308
|
-
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 = {}) {
|
|
309
395
|
if (baseDoc.api_version !== V2_API_VERSION)
|
|
310
396
|
throw new ConfigError(`${base}: legacy fleet configuration is unsupported; run 'ours-fleet doctor' and rewrite it as ${V2_API_VERSION}`);
|
|
311
397
|
if ('roles' in baseDoc)
|
|
@@ -316,11 +402,73 @@ function composeSplitRoles(base, baseDoc, yamlMode, diagnostics, files) {
|
|
|
316
402
|
assertTrustedPath(root, 'directory');
|
|
317
403
|
const brains = readKindDirectory(join(root, 'brains'), 'brain', false, yamlMode, diagnostics, files);
|
|
318
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);
|
|
319
406
|
const agents = readKindDirectory(join(root, 'agents'), 'agent', true, yamlMode, diagnostics, files);
|
|
407
|
+
if (additionalAgent) {
|
|
408
|
+
if (agents.has(additionalAgent.id))
|
|
409
|
+
throw new ConfigError(`agent '${additionalAgent.id}' already exists`);
|
|
410
|
+
agents.set(additionalAgent.id, {
|
|
411
|
+
file: '(spawn request)', value: structuredClone(additionalAgent.definition),
|
|
412
|
+
});
|
|
413
|
+
}
|
|
320
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
|
+
}
|
|
321
446
|
const documents = [];
|
|
322
447
|
for (const [id, agent] of agents) {
|
|
323
|
-
|
|
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
|
+
}
|
|
324
472
|
if (!('role' in a) || !('brain' in a))
|
|
325
473
|
throw new ConfigError(`${agent.file}: agent '${id}' requires role and brain`);
|
|
326
474
|
const role = selection(a.role, 'role', agent.file, roles, ROLE_PRESET_KEYS);
|
|
@@ -392,8 +540,16 @@ function composeSplitRoles(base, baseDoc, yamlMode, diagnostics, files) {
|
|
|
392
540
|
: undefined);
|
|
393
541
|
delete provenance.role;
|
|
394
542
|
delete provenance.brain;
|
|
543
|
+
const canonicalDefinition = {
|
|
544
|
+
role: deepSubStrict(a.role, vars, agent.file, `/agents/${id}/role`),
|
|
545
|
+
brain: deepSubStrict(a.brain, vars, agent.file, `/agents/${id}/brain`),
|
|
546
|
+
...operational,
|
|
547
|
+
};
|
|
395
548
|
documents.push({ file: agent.file,
|
|
396
|
-
doc: { roles: { [id]: { ...brainValue, ...roleValue, ...operational } } }, provenance
|
|
549
|
+
doc: { roles: { [id]: { ...brainValue, ...roleValue, ...operational } } }, provenance,
|
|
550
|
+
agentSelections: structuredClone({ role: canonicalDefinition.role, brain: canonicalDefinition.brain }),
|
|
551
|
+
agentDefinition: canonicalDefinition,
|
|
552
|
+
...(agentTemplate ? { agentTemplate } : {}) });
|
|
397
553
|
}
|
|
398
554
|
return documents;
|
|
399
555
|
}
|
|
@@ -411,6 +567,9 @@ export function loadConfig(configPath, options = {}) {
|
|
|
411
567
|
const base = configPath ?? defaultConfigPath();
|
|
412
568
|
const files = [];
|
|
413
569
|
const diagnostics = [];
|
|
570
|
+
const agentTemplates = {};
|
|
571
|
+
const rolePresets = {};
|
|
572
|
+
const brainPresets = {};
|
|
414
573
|
let docs = [];
|
|
415
574
|
let manifestDoc = {};
|
|
416
575
|
if (existsSync(base)) {
|
|
@@ -429,14 +588,20 @@ export function loadConfig(configPath, options = {}) {
|
|
|
429
588
|
const dd = fleetDDir();
|
|
430
589
|
if (existsSync(dd) && readdirSync(dd).some(f => f.endsWith('.yaml') || f.endsWith('.yml')))
|
|
431
590
|
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)];
|
|
591
|
+
docs = [{ file: base, doc: manifestDoc }, ...composeSplitRoles(base, manifestDoc, options.yamlMode ?? 'compat', diagnostics, files, options.additionalAgent, agentTemplates, rolePresets, brainPresets)];
|
|
433
592
|
const baseDoc = manifestDoc;
|
|
434
593
|
const vars = (baseDoc.vars ?? {});
|
|
435
594
|
const defaults = (baseDoc.defaults ?? {});
|
|
595
|
+
const legacyRuntimeDefaults = ['harness', 'model', 'model_chain', 'effort', 'harness_options',
|
|
596
|
+
'session', 'session_options', 'max_tokens', 'autocompact_pct']
|
|
597
|
+
.filter(key => Object.hasOwn(defaults, key));
|
|
598
|
+
if (legacyRuntimeDefaults.length)
|
|
599
|
+
throw new ConfigError(`${base}: E_LEGACY /defaults: Brain-owned field(s) ${legacyRuntimeDefaults.join(', ')} are unsupported; move them to a Brain definition`);
|
|
436
600
|
const startStaggerMs = resolveStartStaggerMs(baseDoc.start_stagger_ms, base);
|
|
437
601
|
const seen = new Map();
|
|
438
602
|
const roles = [];
|
|
439
|
-
|
|
603
|
+
const agentDefinitions = {};
|
|
604
|
+
for (const { file, doc, provenance, agentSelections, agentDefinition, agentTemplate } of docs) {
|
|
440
605
|
for (const [name, raw] of Object.entries((doc.roles ?? {}))) {
|
|
441
606
|
if (!ROLE_NAME_RE.test(name))
|
|
442
607
|
throw new ConfigError(`${file}: invalid role name '${name}' (allowed: [A-Za-z0-9_-])`);
|
|
@@ -444,6 +609,8 @@ export function loadConfig(configPath, options = {}) {
|
|
|
444
609
|
if (prev)
|
|
445
610
|
throw new ConfigError(`role '${name}' defined in both ${prev} and ${file}`);
|
|
446
611
|
seen.set(name, file);
|
|
612
|
+
if (agentDefinition)
|
|
613
|
+
agentDefinitions[name] = structuredClone(agentDefinition);
|
|
447
614
|
const r = deepSub(raw ?? {}, vars);
|
|
448
615
|
const bad = Object.keys(r).filter(k => !ROLE_KEYS.includes(k));
|
|
449
616
|
if (bad.length)
|
|
@@ -474,6 +641,8 @@ export function loadConfig(configPath, options = {}) {
|
|
|
474
641
|
const worklog = resolveWorklogPolicy(defaults.worklog, r.worklog, file, name);
|
|
475
642
|
const authProxy = resolveAuthProxy(defaults.auth_proxy, r.auth_proxy, file, name);
|
|
476
643
|
const harness = r.harness ?? defaults.harness ?? 'claude-code';
|
|
644
|
+
if (session === 'codex-app-server' && harness !== 'codex')
|
|
645
|
+
throw new ConfigError(`${file}: role '${name}' session: codex-app-server requires harness: codex`);
|
|
477
646
|
const defaultHarness = defaults.harness ?? 'claude-code';
|
|
478
647
|
const inheritsModelDefaults = harness === defaultHarness && r.model !== null;
|
|
479
648
|
if (authProxy && harness !== 'claude-code')
|
|
@@ -530,6 +699,8 @@ export function loadConfig(configPath, options = {}) {
|
|
|
530
699
|
auth_proxy: authProxy,
|
|
531
700
|
env: Object.keys(env).length ? env : undefined,
|
|
532
701
|
loops: [],
|
|
702
|
+
...(agentSelections ? { agentSelections } : {}),
|
|
703
|
+
...(agentTemplate ? { agentTemplate } : {}),
|
|
533
704
|
});
|
|
534
705
|
// Forbidden-path enforcement: a mount that would breach the policy
|
|
535
706
|
// is a configuration error, caught by `config` rather than at launch.
|
|
@@ -545,7 +716,12 @@ export function loadConfig(configPath, options = {}) {
|
|
|
545
716
|
}
|
|
546
717
|
}
|
|
547
718
|
validateOwnerChannelIdentities(roles);
|
|
548
|
-
const watchdogs = resolveWatchdogs(baseDoc, base, roles, vars,
|
|
719
|
+
const watchdogs = options.skipWatchdogs ? [] : resolveWatchdogs(baseDoc, base, roles, vars, agentDefinitions, (name, definition) => {
|
|
720
|
+
const id = `WatchdogAgent${name}`.slice(0, 64);
|
|
721
|
+
return findRole(loadConfig(base, {
|
|
722
|
+
yamlMode: options.yamlMode, additionalAgent: { id, definition }, skipWatchdogs: true,
|
|
723
|
+
}), id);
|
|
724
|
+
});
|
|
549
725
|
const resolvedLoops = resolveLoops(baseDoc.loops, base, roles, vars);
|
|
550
726
|
for (const role of roles)
|
|
551
727
|
role.loops = resolvedLoops.byRole.get(role.name) ?? [];
|
|
@@ -558,6 +734,9 @@ export function loadConfig(configPath, options = {}) {
|
|
|
558
734
|
let ownerInvite;
|
|
559
735
|
let roomTemplates;
|
|
560
736
|
let tasks;
|
|
737
|
+
const fileTemplates = readRoomTemplateDirectory(join(splitRootFor(base), 'room_templates'), options.yamlMode ?? 'compat', diagnostics, files);
|
|
738
|
+
if (Object.keys(fileTemplates).length)
|
|
739
|
+
roomTemplates = fileTemplates;
|
|
561
740
|
for (const { file, doc } of docs) {
|
|
562
741
|
if (doc.rooms !== undefined) {
|
|
563
742
|
if (rooms)
|
|
@@ -570,7 +749,21 @@ export function loadConfig(configPath, options = {}) {
|
|
|
570
749
|
}
|
|
571
750
|
if (doc.room_templates !== undefined) {
|
|
572
751
|
const validated = validateRoomTemplatesConfig(deepSub(doc.room_templates, vars), file);
|
|
573
|
-
|
|
752
|
+
const raw = doc.room_templates;
|
|
753
|
+
for (const [name, template] of Object.entries(validated)) {
|
|
754
|
+
const shadowed = roomTemplates?.[name];
|
|
755
|
+
const marker = raw[name]?.override_builtin;
|
|
756
|
+
if (shadowed && (marker !== true || template.version <= shadowed.version))
|
|
757
|
+
throw new ConfigError(`${file}: room_templates.${name} shadows file preset ${shadowed.sourceFile}; `
|
|
758
|
+
+ 'set override_builtin: true and use a higher version');
|
|
759
|
+
if (marker === true)
|
|
760
|
+
diagnostics.push({
|
|
761
|
+
severity: 'warning', kind: 'deprecated-field', file, line: 1, column: 1,
|
|
762
|
+
message: `${file}: room_templates.${name}.override_builtin is deprecated; `
|
|
763
|
+
+ 'it is retained only as an explicit manifest-over-file migration marker',
|
|
764
|
+
});
|
|
765
|
+
roomTemplates = { ...(roomTemplates ?? {}), [name]: { ...template, sourceFile: file } };
|
|
766
|
+
}
|
|
574
767
|
}
|
|
575
768
|
if (doc.tasks !== undefined) {
|
|
576
769
|
if (tasks)
|
|
@@ -579,7 +772,7 @@ export function loadConfig(configPath, options = {}) {
|
|
|
579
772
|
}
|
|
580
773
|
}
|
|
581
774
|
const result = {
|
|
582
|
-
roles, vars, defaults, files, startStaggerMs, diagnostics, watchdogs,
|
|
775
|
+
roles, agentDefinitions, agentTemplates, rolePresets, brainPresets, vars, defaults, files, startStaggerMs, diagnostics, watchdogs,
|
|
583
776
|
loops: resolvedLoops.loops,
|
|
584
777
|
rooms, roomTemplates, tasks, ownerInviteFingerprint,
|
|
585
778
|
configMode: 'split-v2',
|
|
@@ -588,10 +781,18 @@ export function loadConfig(configPath, options = {}) {
|
|
|
588
781
|
return { kind: 'Manifest', path };
|
|
589
782
|
const parent = basename(dirname(path));
|
|
590
783
|
const kind = parent === 'agents' ? 'Agent'
|
|
591
|
-
: parent === '
|
|
784
|
+
: parent === 'agent_templates' ? 'AgentTemplate'
|
|
785
|
+
: parent === 'roles' ? 'Role'
|
|
786
|
+
: parent === 'room_templates' ? 'RoomTemplate' : 'Brain';
|
|
592
787
|
return { kind, id: basename(path, extname(path)), path };
|
|
593
788
|
}),
|
|
594
789
|
};
|
|
790
|
+
Object.defineProperty(result, 'resolveAgentDefinition', {
|
|
791
|
+
value: (id, definition) => resolveAgentDefinitionDryRun(base, id, definition),
|
|
792
|
+
enumerable: false,
|
|
793
|
+
writable: false,
|
|
794
|
+
configurable: false,
|
|
795
|
+
});
|
|
595
796
|
if (ownerInvite !== undefined) {
|
|
596
797
|
Object.defineProperty(result, 'ownerInvite', {
|
|
597
798
|
value: ownerInvite,
|
|
@@ -602,6 +803,12 @@ export function loadConfig(configPath, options = {}) {
|
|
|
602
803
|
}
|
|
603
804
|
return result;
|
|
604
805
|
}
|
|
806
|
+
/** Full read-only Agent resolution used by launch dry-runs and room plan validation. */
|
|
807
|
+
export function resolveAgentDefinitionDryRun(configPath, id, definition) {
|
|
808
|
+
return findRole(loadConfig(configPath, {
|
|
809
|
+
additionalAgent: { id, definition }, skipWatchdogs: true,
|
|
810
|
+
}), id);
|
|
811
|
+
}
|
|
605
812
|
/**
|
|
606
813
|
* Canonical form of a 64-hex container ID for authorization decisions. Hex
|
|
607
814
|
* case is not identity: two casings of one CID are the same peer, so every
|
|
@@ -685,8 +892,8 @@ export function resolveOwnerChannelConfig(defaults, role, session, file = 'confi
|
|
|
685
892
|
const maxRequestBytes = attachments.max_request_bytes ?? 20 * 1024 * 1024;
|
|
686
893
|
if (maxRequestBytes < maxFileBytes)
|
|
687
894
|
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
|
|
895
|
+
if (session !== 'acp' && session !== 'codex-app-server')
|
|
896
|
+
throw new ConfigError(`${file}: role '${name}' owner_channel requires a managed session with correlated final replies`);
|
|
690
897
|
return {
|
|
691
898
|
identity: merged.identity.trim(),
|
|
692
899
|
owners,
|
|
@@ -706,7 +913,13 @@ export function resolveOwnerChannelConfig(defaults, role, session, file = 'confi
|
|
|
706
913
|
};
|
|
707
914
|
}
|
|
708
915
|
function validateOwnerChannelIdentities(roles) {
|
|
709
|
-
const roleIdentities = new Map(
|
|
916
|
+
const roleIdentities = new Map();
|
|
917
|
+
for (const role of roles) {
|
|
918
|
+
const prior = roleIdentities.get(role.identity);
|
|
919
|
+
if (prior)
|
|
920
|
+
throw new ConfigError(`persistent Agents '${prior}' and '${role.name}' resolve to duplicate identity '${role.identity}'`);
|
|
921
|
+
roleIdentities.set(role.identity, role.name);
|
|
922
|
+
}
|
|
710
923
|
const channels = new Map();
|
|
711
924
|
for (const role of roles) {
|
|
712
925
|
const identity = role.owner_channel?.identity;
|
|
@@ -812,8 +1025,8 @@ function resolveSession(raw, file, name) {
|
|
|
812
1025
|
if (value === 'tmux')
|
|
813
1026
|
throw new ConfigError(`${file}: role '${name}' session: tmux is no longer supported; use session: acp `
|
|
814
1027
|
+ 'with the Codex or Claude Code adapter');
|
|
815
|
-
if (value !== 'acp')
|
|
816
|
-
throw new ConfigError(`${file}: role '${name}' session: must be: acp`);
|
|
1028
|
+
if (value !== 'acp' && value !== 'codex-app-server')
|
|
1029
|
+
throw new ConfigError(`${file}: role '${name}' session: must be: acp, codex-app-server`);
|
|
817
1030
|
return value;
|
|
818
1031
|
}
|
|
819
1032
|
function resolveSessionOptions(defaults, role, session, file, name) {
|
|
@@ -828,26 +1041,40 @@ function resolveSessionOptions(defaults, role, session, file, name) {
|
|
|
828
1041
|
...((defaults?.acp) ?? {}),
|
|
829
1042
|
...(role?.acp ?? {}),
|
|
830
1043
|
},
|
|
1044
|
+
codex_app_server: {
|
|
1045
|
+
...((defaults?.codex_app_server) ?? {}),
|
|
1046
|
+
...(role?.codex_app_server ?? {}),
|
|
1047
|
+
},
|
|
831
1048
|
};
|
|
832
1049
|
if (defaults?.tmux !== undefined
|
|
833
1050
|
|| role?.tmux !== undefined)
|
|
834
1051
|
throw new ConfigError(`${file}: role '${name}' session_options.tmux is no longer supported; `
|
|
835
1052
|
+ 'use session: acp with session_options.acp');
|
|
836
|
-
const bad = Object.keys(merged).filter(k => k !== 'acp');
|
|
1053
|
+
const bad = Object.keys(merged).filter(k => k !== 'acp' && k !== 'codex_app_server');
|
|
837
1054
|
if (bad.length)
|
|
838
1055
|
throw new ConfigError(`${file}: role '${name}' session_options: unknown key(s) ${bad.join(', ')}`);
|
|
839
1056
|
if (!isPlainObject(merged.acp))
|
|
840
|
-
throw new ConfigError(`${file}: role '${name}' session_options
|
|
1057
|
+
throw new ConfigError(`${file}: role '${name}' session_options.acp must be a map`);
|
|
1058
|
+
if (!isPlainObject(merged.codex_app_server))
|
|
1059
|
+
throw new ConfigError(`${file}: role '${name}' session_options.codex_app_server must be a map`);
|
|
841
1060
|
const acpBad = Object.keys(merged.acp).filter(k => k !== 'command');
|
|
842
1061
|
if (acpBad.length)
|
|
843
1062
|
throw new ConfigError(`${file}: role '${name}' session_options.acp: unknown key(s) ${acpBad.join(', ')}`);
|
|
844
|
-
const
|
|
845
|
-
if (
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
1063
|
+
const nativeBad = Object.keys(merged.codex_app_server).filter(k => k !== 'command');
|
|
1064
|
+
if (nativeBad.length)
|
|
1065
|
+
throw new ConfigError(`${file}: role '${name}' session_options.codex_app_server: unknown key(s) ${nativeBad.join(', ')}`);
|
|
1066
|
+
for (const [key, command] of [
|
|
1067
|
+
['acp', merged.acp.command],
|
|
1068
|
+
['codex_app_server', merged.codex_app_server.command],
|
|
1069
|
+
]) {
|
|
1070
|
+
if (command !== undefined
|
|
1071
|
+
&& !(typeof command === 'string' && command.trim())
|
|
1072
|
+
&& !(Array.isArray(command) && command.length > 0
|
|
1073
|
+
&& command.every(v => typeof v === 'string' && v.length > 0)))
|
|
1074
|
+
throw new ConfigError(`${file}: role '${name}' session_options.${key}.command must be a non-empty string or string list`);
|
|
1075
|
+
}
|
|
1076
|
+
return Object.keys(merged.acp).length || Object.keys(merged.codex_app_server).length
|
|
1077
|
+
? merged : undefined;
|
|
851
1078
|
}
|
|
852
1079
|
export function resolvePermissions(defaults, role, file = 'config', name = 'role') {
|
|
853
1080
|
if (defaults !== undefined && !isPlainObject(defaults))
|
|
@@ -932,7 +1159,10 @@ export function resolveMonitorConfig(defMonitor, roleMonitor, labels = {}) {
|
|
|
932
1159
|
}
|
|
933
1160
|
export function findRole(cfg, name) {
|
|
934
1161
|
const r = cfg.roles.find(r => r.name === name);
|
|
935
|
-
if (!r)
|
|
1162
|
+
if (!r) {
|
|
1163
|
+
if (cfg.agentTemplates?.[name])
|
|
1164
|
+
throw new ConfigError(`'${name}' is an inert Agent Template, not a persistent Agent; lifecycle commands target only fleet/agents instances`);
|
|
936
1165
|
throw new ConfigError(`no such role '${name}' in ${cfg.files.join(', ') || 'config'}`);
|
|
1166
|
+
}
|
|
937
1167
|
return r;
|
|
938
1168
|
}
|