@ours.network/fleet 1.1.0-nightly.3 → 1.1.0-nightly.30
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 +244 -40
- 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 +4 -4
- package/dist/application/role-creation-service.js +19 -1
- 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 +93 -40
- package/dist/application/task-room-service.js +579 -173
- package/dist/briefing.js +85 -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 +217 -51
- package/dist/config-yaml.d.ts +1 -1
- package/dist/config.d.ts +38 -3
- package/dist/config.js +240 -28
- package/dist/creation.d.ts +1 -1
- package/dist/creation.js +7 -4
- 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 +213 -42
- package/dist/doctor.d.ts +2 -0
- package/dist/doctor.js +54 -4
- package/dist/fleet-command-audit.d.ts +181 -0
- package/dist/fleet-command-audit.js +738 -0
- package/dist/fleet-proxy.d.ts +7 -2
- 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 +4 -3
- package/dist/index.js +3 -2
- 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 +631 -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-recovery.d.ts +1 -1
- package/dist/ops.js +2 -17
- package/dist/owner-channel/channel.d.ts +75 -3
- package/dist/owner-channel/channel.js +531 -123
- package/dist/owner-channel/commands.d.ts +28 -6
- package/dist/owner-channel/commands.js +112 -36
- package/dist/owner-channel/lifecycle-outbox.d.ts +24 -0
- package/dist/owner-channel/lifecycle-outbox.js +87 -0
- package/dist/owner-channel/ours-client.d.ts +19 -0
- package/dist/owner-channel/ours-client.js +3 -0
- 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 +450 -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 +519 -261
- package/dist/rooms-tasks/close.d.ts +9 -1
- package/dist/rooms-tasks/close.js +23 -7
- package/dist/rooms-tasks/config.js +16 -29
- package/dist/rooms-tasks/cowork-adapter.d.ts +5 -0
- package/dist/rooms-tasks/cowork-adapter.js +13 -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 +9 -0
- package/dist/rooms-tasks/external-worker.js +52 -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/member-startup.d.ts +1 -0
- package/dist/rooms-tasks/member-startup.js +9 -2
- package/dist/rooms-tasks/provision.js +146 -56
- package/dist/rooms-tasks/room-state.d.ts +1 -0
- package/dist/rooms-tasks/room-state.js +12 -1
- package/dist/rooms-tasks/task-room-name.d.ts +12 -0
- package/dist/rooms-tasks/task-room-name.js +61 -0
- 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 +10 -4
- package/dist/rooms-tasks/types.d.ts +94 -8
- package/dist/rooms-tasks/types.js +14 -1
- package/dist/runner.d.ts +12 -2
- package/dist/runner.js +167 -56
- 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 +51 -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 +9 -0
- package/dist/spawn.js +77 -9
- package/dist/temp-lifecycle.d.ts +1 -1
- 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-app/assets/{index-BbTc5KtZ.js → index-C0h3ALvs.js} +8 -8
- package/dist/web-app/index.html +1 -1
- package/package.json +5 -4
- package/presets/brain-catalog.json +17 -0
- package/presets/fleet/agent_templates/Critic.yaml +5 -0
- package/presets/fleet/agent_templates/Developer.yaml +5 -0
- package/presets/fleet/agent_templates/LocalCoordinator.yaml +50 -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/brains/codex-gpt-6-astra-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-xhigh.yaml +5 -0
- package/presets/fleet/roles/Coordinator.yaml +30 -0
- package/presets/fleet/roles/Critic.yaml +28 -0
- package/presets/fleet/roles/Developer.yaml +28 -0
- package/presets/fleet/roles/LocalCoordinator.yaml +35 -0
- package/presets/fleet/room_templates/pair.yaml +13 -0
- package/presets/fleet/room_templates/single.yaml +9 -0
- package/presets/fleet/room_templates/team.yaml +16 -0
- package/presets/fleet.yaml +7 -0
- package/presets/manifest.json +6 -0
package/dist/config.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type ConfigDiagnostic, type YamlMode } from './config-yaml.js';
|
|
2
2
|
import type { IsolationConfig, WrapContext } from './isolation/types.js';
|
|
3
3
|
import type { ResolvedWatchdog } from './watchdog/config.js';
|
|
4
|
-
import type { ResolvedLoop, ResolvedRoleLoop } from './loops/config.js';
|
|
4
|
+
import type { AgentLoopsConfig, ResolvedLoop, ResolvedRoleLoop } from './loops/config.js';
|
|
5
5
|
export interface OverseeEntry {
|
|
6
6
|
agent: string;
|
|
7
7
|
interval: string;
|
|
@@ -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 {
|
|
@@ -157,6 +162,15 @@ export interface AgentDefinition {
|
|
|
157
162
|
owner_channel?: OwnerChannelConfigInput;
|
|
158
163
|
worklog?: WorklogPolicyInput;
|
|
159
164
|
auth_proxy?: Partial<AuthProxyConfig>;
|
|
165
|
+
/** Scheduled turns scoped only to temporary launches; persistent Agents reject this field. */
|
|
166
|
+
loops?: AgentLoopsConfig;
|
|
167
|
+
}
|
|
168
|
+
/** Inert, reusable launch definition. It never owns an identity or runtime state. */
|
|
169
|
+
export type AgentTemplateDefinition = Omit<AgentDefinition, 'identity'>;
|
|
170
|
+
/** Explicit persistent instance reusing an inert template. */
|
|
171
|
+
export interface AgentTemplateInstance {
|
|
172
|
+
template: string;
|
|
173
|
+
overrides?: Partial<AgentDefinition>;
|
|
160
174
|
}
|
|
161
175
|
/** Internal first-boot payload for a Fleet-provisioned Cowork room member. */
|
|
162
176
|
export interface RoomMemberStartup {
|
|
@@ -168,6 +182,7 @@ export interface RoomMemberStartup {
|
|
|
168
182
|
role: string;
|
|
169
183
|
task: string;
|
|
170
184
|
owner_seat_cid: string | null;
|
|
185
|
+
anonymous?: boolean;
|
|
171
186
|
}
|
|
172
187
|
export interface ResolvedRole extends Omit<RoleConfig, 'model' | 'owner_channel' | 'worklog'> {
|
|
173
188
|
name: string;
|
|
@@ -189,11 +204,20 @@ export interface ResolvedRole extends Omit<RoleConfig, 'model' | 'owner_channel'
|
|
|
189
204
|
worklog?: WorklogPolicy;
|
|
190
205
|
auth_proxy?: AuthProxyConfig;
|
|
191
206
|
loops?: ResolvedRoleLoop[];
|
|
207
|
+
/** Agent Template-local loops, kept separate from manifest loops until a temp launch. */
|
|
208
|
+
temporaryLoops?: ResolvedRoleLoop[];
|
|
209
|
+
/** Durable source of the temporary loop policy; internal and never user-authored. */
|
|
210
|
+
temporaryLoopSource?: 'agent-template' | 'cli' | 'omitted';
|
|
192
211
|
provenance?: Record<string, FieldProvenance>;
|
|
193
212
|
/** Internal/transient: never accepted as a user-authored RoleConfig key. */
|
|
194
213
|
roomMemberStartup?: RoomMemberStartup;
|
|
195
214
|
/** Original unresolved selections only; denied operational fields never enter inheritance state. */
|
|
196
215
|
agentSelections?: Pick<AgentDefinition, 'role' | 'brain'>;
|
|
216
|
+
agentTemplate?: {
|
|
217
|
+
id: string;
|
|
218
|
+
sourceFile: string;
|
|
219
|
+
contentHash: string;
|
|
220
|
+
};
|
|
197
221
|
}
|
|
198
222
|
export interface FieldProvenance {
|
|
199
223
|
sourceFile: string;
|
|
@@ -215,12 +239,18 @@ export interface FleetConfig {
|
|
|
215
239
|
roles: ResolvedRole[];
|
|
216
240
|
/** Canonical, variable-resolved Agent authoring documents keyed by stable Agent ID. */
|
|
217
241
|
agentDefinitions?: Record<string, AgentDefinition>;
|
|
242
|
+
/** Canonical inert templates keyed by stable template ID. */
|
|
243
|
+
agentTemplates?: Record<string, AgentTemplateDefinition>;
|
|
244
|
+
rolePresets?: Record<string, Record<string, unknown>>;
|
|
245
|
+
brainPresets?: Record<string, Record<string, unknown>>;
|
|
246
|
+
/** Required launch-boundary resolver for effective temporary Agent definitions. */
|
|
247
|
+
resolveAgentDefinition?: (id: string, definition: AgentDefinition) => ResolvedRole;
|
|
218
248
|
vars: Record<string, string>;
|
|
219
249
|
defaults: Record<string, unknown>;
|
|
220
250
|
files: string[];
|
|
221
251
|
configMode?: 'split-v2';
|
|
222
252
|
sourceDocuments?: Array<{
|
|
223
|
-
kind: 'Manifest' | 'Agent' | 'Role' | 'Brain';
|
|
253
|
+
kind: 'Manifest' | 'Agent' | 'AgentTemplate' | 'Role' | 'Brain' | 'RoomTemplate';
|
|
224
254
|
id?: string;
|
|
225
255
|
path: string;
|
|
226
256
|
}>;
|
|
@@ -260,6 +290,8 @@ export declare const AGENT_KEYS: string[];
|
|
|
260
290
|
type BarePreset = Record<string, unknown>;
|
|
261
291
|
export declare function validateRoleValue(value: BarePreset, file: string, pointer: string): void;
|
|
262
292
|
export declare function validateBrainValue(value: BarePreset, file: string, pointer: string): void;
|
|
293
|
+
/** Pure launch-boundary validation for a fully resolved temporary Agent definition. */
|
|
294
|
+
export declare function validateEffectiveAgentTemplate(definition: AgentTemplateDefinition, id?: string): AgentTemplateDefinition;
|
|
263
295
|
export declare function assertBareKeys(value: unknown, allowed: string[], label: string): BarePreset;
|
|
264
296
|
export declare function splitRootFor(base: string): string;
|
|
265
297
|
/** Load a v2 manifest and bare Agent/Role/Brain documents from its stem directory. */
|
|
@@ -268,9 +300,12 @@ export declare function loadConfig(configPath?: string, options?: {
|
|
|
268
300
|
additionalAgent?: {
|
|
269
301
|
id: string;
|
|
270
302
|
definition: AgentDefinition;
|
|
303
|
+
temporary?: boolean;
|
|
271
304
|
};
|
|
272
305
|
skipWatchdogs?: boolean;
|
|
273
306
|
}): FleetConfig;
|
|
307
|
+
/** Full read-only Agent resolution used by launch dry-runs and room plan validation. */
|
|
308
|
+
export declare function resolveAgentDefinitionDryRun(configPath: string, id: string, definition: AgentDefinition): ResolvedRole;
|
|
274
309
|
/**
|
|
275
310
|
* Canonical form of a 64-hex container ID for authorization decisions. Hex
|
|
276
311
|
* case is not identity: two casings of one CID are the same peer, so every
|
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,
|
|
@@ -140,8 +142,13 @@ export const BRAIN_PRESET_KEYS = [
|
|
|
140
142
|
];
|
|
141
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) {
|
|
@@ -179,7 +186,45 @@ export function validateBrainValue(value, file, pointer) {
|
|
|
179
186
|
schemaError(file, `${pointer}/${key}`, 'must be a mapping');
|
|
180
187
|
}
|
|
181
188
|
}
|
|
189
|
+
/** Pure launch-boundary validation for a fully resolved temporary Agent definition. */
|
|
190
|
+
export function validateEffectiveAgentTemplate(definition, id = 'room-member') {
|
|
191
|
+
const value = structuredClone(definition);
|
|
192
|
+
const bad = Object.keys(value).filter(key => !AGENT_KEYS.includes(key));
|
|
193
|
+
if (bad.length)
|
|
194
|
+
throw new ConfigError(`member '${id}' has unknown Agent key(s) ${bad.join(', ')}`);
|
|
195
|
+
if (Object.hasOwn(value, 'identity'))
|
|
196
|
+
throw new ConfigError(`member '${id}' cannot override identity`);
|
|
197
|
+
if (!isPlainObject(value.role) || !isPlainObject(value.role.inline)
|
|
198
|
+
|| Object.keys(value.role).some(key => key !== 'inline'))
|
|
199
|
+
throw new ConfigError(`member '${id}' role must be a resolved inline definition`);
|
|
200
|
+
if (!isPlainObject(value.brain) || !isPlainObject(value.brain.inline)
|
|
201
|
+
|| Object.keys(value.brain).some(key => key !== 'inline'))
|
|
202
|
+
throw new ConfigError(`member '${id}' brain must be a resolved inline definition`);
|
|
203
|
+
const role = value.role.inline;
|
|
204
|
+
const brain = value.brain.inline;
|
|
205
|
+
assertBareKeys(role, ROLE_PRESET_KEYS, `member '${id}' Role`);
|
|
206
|
+
assertBareKeys(brain, BRAIN_PRESET_KEYS, `member '${id}' Brain`);
|
|
207
|
+
validateRoleValue(role, '(member override)', `/members/${id}/role`);
|
|
208
|
+
validateBrainValue(brain, '(member override)', `/members/${id}/brain`);
|
|
209
|
+
validateAgentScalars(Object.fromEntries(Object.entries(value)
|
|
210
|
+
.filter(([key]) => key !== 'role' && key !== 'brain')), '(member override)', id);
|
|
211
|
+
resolvePermissions(undefined, value.permissions, '(member override)', id);
|
|
212
|
+
if (value.isolation !== undefined) {
|
|
213
|
+
const problems = validateIsolationConfig(value.isolation);
|
|
214
|
+
if (problems.length)
|
|
215
|
+
throw new ConfigError(`member '${id}' ${problems.join('; ')}`);
|
|
216
|
+
}
|
|
217
|
+
const adapter = getAdapter(brain.harness);
|
|
218
|
+
adapter.agentSession.resolveBrain({ model: brain.model,
|
|
219
|
+
effort: brain.effort,
|
|
220
|
+
harnessOptions: brain.harness_options });
|
|
221
|
+
return value;
|
|
222
|
+
}
|
|
182
223
|
function validateAgentScalars(value, file, id) {
|
|
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()))
|
|
@@ -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,6 +406,7 @@ function composeSplitRoles(base, baseDoc, yamlMode, diagnostics, files, addition
|
|
|
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);
|
|
320
411
|
if (additionalAgent) {
|
|
321
412
|
if (agents.has(additionalAgent.id))
|
|
@@ -325,11 +416,67 @@ function composeSplitRoles(base, baseDoc, yamlMode, diagnostics, files, addition
|
|
|
325
416
|
});
|
|
326
417
|
}
|
|
327
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
|
+
}
|
|
328
450
|
const documents = [];
|
|
329
451
|
for (const [id, agent] of agents) {
|
|
330
|
-
|
|
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
|
+
}
|
|
331
476
|
if (!('role' in a) || !('brain' in a))
|
|
332
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');
|
|
333
480
|
const role = selection(a.role, 'role', agent.file, roles, ROLE_PRESET_KEYS);
|
|
334
481
|
const brain = selection(a.brain, 'brain', agent.file, brains, BRAIN_PRESET_KEYS);
|
|
335
482
|
const operational = deepSubStrict(Object.fromEntries(Object.entries(a).filter(([key]) => key !== 'role' && key !== 'brain')), vars, agent.file, `/agents/${id}`);
|
|
@@ -404,10 +551,12 @@ function composeSplitRoles(base, baseDoc, yamlMode, diagnostics, files, addition
|
|
|
404
551
|
brain: deepSubStrict(a.brain, vars, agent.file, `/agents/${id}/brain`),
|
|
405
552
|
...operational,
|
|
406
553
|
};
|
|
554
|
+
const { loops: _temporaryLoops, ...runtimeOperational } = operational;
|
|
407
555
|
documents.push({ file: agent.file,
|
|
408
|
-
doc: { roles: { [id]: { ...brainValue, ...roleValue, ...
|
|
556
|
+
doc: { roles: { [id]: { ...brainValue, ...roleValue, ...runtimeOperational } } }, provenance,
|
|
409
557
|
agentSelections: structuredClone({ role: canonicalDefinition.role, brain: canonicalDefinition.brain }),
|
|
410
|
-
agentDefinition: canonicalDefinition
|
|
558
|
+
agentDefinition: canonicalDefinition,
|
|
559
|
+
...(agentTemplate ? { agentTemplate } : {}) });
|
|
411
560
|
}
|
|
412
561
|
return documents;
|
|
413
562
|
}
|
|
@@ -425,6 +574,9 @@ export function loadConfig(configPath, options = {}) {
|
|
|
425
574
|
const base = configPath ?? defaultConfigPath();
|
|
426
575
|
const files = [];
|
|
427
576
|
const diagnostics = [];
|
|
577
|
+
const agentTemplates = {};
|
|
578
|
+
const rolePresets = {};
|
|
579
|
+
const brainPresets = {};
|
|
428
580
|
let docs = [];
|
|
429
581
|
let manifestDoc = {};
|
|
430
582
|
if (existsSync(base)) {
|
|
@@ -443,7 +595,7 @@ export function loadConfig(configPath, options = {}) {
|
|
|
443
595
|
const dd = fleetDDir();
|
|
444
596
|
if (existsSync(dd) && readdirSync(dd).some(f => f.endsWith('.yaml') || f.endsWith('.yml')))
|
|
445
597
|
throw new ConfigError(`${dd}: legacy fleet.d configuration is unsupported; run 'ours-fleet doctor' and move agents to ${splitRootFor(base)}/agents/`);
|
|
446
|
-
docs = [{ file: base, doc: manifestDoc }, ...composeSplitRoles(base, manifestDoc, options.yamlMode ?? 'compat', diagnostics, files, options.additionalAgent)];
|
|
598
|
+
docs = [{ file: base, doc: manifestDoc }, ...composeSplitRoles(base, manifestDoc, options.yamlMode ?? 'compat', diagnostics, files, options.additionalAgent, agentTemplates, rolePresets, brainPresets)];
|
|
447
599
|
const baseDoc = manifestDoc;
|
|
448
600
|
const vars = (baseDoc.vars ?? {});
|
|
449
601
|
const defaults = (baseDoc.defaults ?? {});
|
|
@@ -456,7 +608,7 @@ export function loadConfig(configPath, options = {}) {
|
|
|
456
608
|
const seen = new Map();
|
|
457
609
|
const roles = [];
|
|
458
610
|
const agentDefinitions = {};
|
|
459
|
-
for (const { file, doc, provenance, agentSelections, agentDefinition } of docs) {
|
|
611
|
+
for (const { file, doc, provenance, agentSelections, agentDefinition, agentTemplate } of docs) {
|
|
460
612
|
for (const [name, raw] of Object.entries((doc.roles ?? {}))) {
|
|
461
613
|
if (!ROLE_NAME_RE.test(name))
|
|
462
614
|
throw new ConfigError(`${file}: invalid role name '${name}' (allowed: [A-Za-z0-9_-])`);
|
|
@@ -496,6 +648,8 @@ export function loadConfig(configPath, options = {}) {
|
|
|
496
648
|
const worklog = resolveWorklogPolicy(defaults.worklog, r.worklog, file, name);
|
|
497
649
|
const authProxy = resolveAuthProxy(defaults.auth_proxy, r.auth_proxy, file, name);
|
|
498
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`);
|
|
499
653
|
const defaultHarness = defaults.harness ?? 'claude-code';
|
|
500
654
|
const inheritsModelDefaults = harness === defaultHarness && r.model !== null;
|
|
501
655
|
if (authProxy && harness !== 'claude-code')
|
|
@@ -530,7 +684,7 @@ export function loadConfig(configPath, options = {}) {
|
|
|
530
684
|
defaulted('identity', r.identity !== undefined, r.identity === undefined);
|
|
531
685
|
defaulted('permissions', defaults.permissions !== undefined, defaults.permissions === undefined);
|
|
532
686
|
defaulted('monitor', defaults.monitor !== undefined, defaults.monitor === undefined);
|
|
533
|
-
|
|
687
|
+
const resolvedRole = {
|
|
534
688
|
...r,
|
|
535
689
|
name,
|
|
536
690
|
sourceFile: file,
|
|
@@ -553,7 +707,11 @@ export function loadConfig(configPath, options = {}) {
|
|
|
553
707
|
env: Object.keys(env).length ? env : undefined,
|
|
554
708
|
loops: [],
|
|
555
709
|
...(agentSelections ? { agentSelections } : {}),
|
|
556
|
-
|
|
710
|
+
...(agentTemplate ? { agentTemplate } : {}),
|
|
711
|
+
};
|
|
712
|
+
if (agentDefinition?.loops !== undefined)
|
|
713
|
+
resolvedRole.temporaryLoops = resolveAgentLoops(agentDefinition.loops, resolvedRole, file);
|
|
714
|
+
roles.push(resolvedRole);
|
|
557
715
|
// Forbidden-path enforcement: a mount that would breach the policy
|
|
558
716
|
// is a configuration error, caught by `config` rather than at launch.
|
|
559
717
|
if (isolation !== undefined) {
|
|
@@ -586,6 +744,9 @@ export function loadConfig(configPath, options = {}) {
|
|
|
586
744
|
let ownerInvite;
|
|
587
745
|
let roomTemplates;
|
|
588
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;
|
|
589
750
|
for (const { file, doc } of docs) {
|
|
590
751
|
if (doc.rooms !== undefined) {
|
|
591
752
|
if (rooms)
|
|
@@ -598,7 +759,21 @@ export function loadConfig(configPath, options = {}) {
|
|
|
598
759
|
}
|
|
599
760
|
if (doc.room_templates !== undefined) {
|
|
600
761
|
const validated = validateRoomTemplatesConfig(deepSub(doc.room_templates, vars), file);
|
|
601
|
-
|
|
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
|
+
}
|
|
602
777
|
}
|
|
603
778
|
if (doc.tasks !== undefined) {
|
|
604
779
|
if (tasks)
|
|
@@ -607,7 +782,7 @@ export function loadConfig(configPath, options = {}) {
|
|
|
607
782
|
}
|
|
608
783
|
}
|
|
609
784
|
const result = {
|
|
610
|
-
roles, agentDefinitions, vars, defaults, files, startStaggerMs, diagnostics, watchdogs,
|
|
785
|
+
roles, agentDefinitions, agentTemplates, rolePresets, brainPresets, vars, defaults, files, startStaggerMs, diagnostics, watchdogs,
|
|
611
786
|
loops: resolvedLoops.loops,
|
|
612
787
|
rooms, roomTemplates, tasks, ownerInviteFingerprint,
|
|
613
788
|
configMode: 'split-v2',
|
|
@@ -616,10 +791,18 @@ export function loadConfig(configPath, options = {}) {
|
|
|
616
791
|
return { kind: 'Manifest', path };
|
|
617
792
|
const parent = basename(dirname(path));
|
|
618
793
|
const kind = parent === 'agents' ? 'Agent'
|
|
619
|
-
: parent === '
|
|
794
|
+
: parent === 'agent_templates' ? 'AgentTemplate'
|
|
795
|
+
: parent === 'roles' ? 'Role'
|
|
796
|
+
: parent === 'room_templates' ? 'RoomTemplate' : 'Brain';
|
|
620
797
|
return { kind, id: basename(path, extname(path)), path };
|
|
621
798
|
}),
|
|
622
799
|
};
|
|
800
|
+
Object.defineProperty(result, 'resolveAgentDefinition', {
|
|
801
|
+
value: (id, definition) => resolveAgentDefinitionDryRun(base, id, definition),
|
|
802
|
+
enumerable: false,
|
|
803
|
+
writable: false,
|
|
804
|
+
configurable: false,
|
|
805
|
+
});
|
|
623
806
|
if (ownerInvite !== undefined) {
|
|
624
807
|
Object.defineProperty(result, 'ownerInvite', {
|
|
625
808
|
value: ownerInvite,
|
|
@@ -630,6 +813,12 @@ export function loadConfig(configPath, options = {}) {
|
|
|
630
813
|
}
|
|
631
814
|
return result;
|
|
632
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
|
+
}
|
|
633
822
|
/**
|
|
634
823
|
* Canonical form of a 64-hex container ID for authorization decisions. Hex
|
|
635
824
|
* case is not identity: two casings of one CID are the same peer, so every
|
|
@@ -713,8 +902,8 @@ export function resolveOwnerChannelConfig(defaults, role, session, file = 'confi
|
|
|
713
902
|
const maxRequestBytes = attachments.max_request_bytes ?? 20 * 1024 * 1024;
|
|
714
903
|
if (maxRequestBytes < maxFileBytes)
|
|
715
904
|
throw new ConfigError(`${file}: role '${name}' owner_channel.attachments.max_request_bytes must be at least max_file_bytes`);
|
|
716
|
-
if (session !== 'acp')
|
|
717
|
-
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`);
|
|
718
907
|
return {
|
|
719
908
|
identity: merged.identity.trim(),
|
|
720
909
|
owners,
|
|
@@ -734,7 +923,13 @@ export function resolveOwnerChannelConfig(defaults, role, session, file = 'confi
|
|
|
734
923
|
};
|
|
735
924
|
}
|
|
736
925
|
function validateOwnerChannelIdentities(roles) {
|
|
737
|
-
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
|
+
}
|
|
738
933
|
const channels = new Map();
|
|
739
934
|
for (const role of roles) {
|
|
740
935
|
const identity = role.owner_channel?.identity;
|
|
@@ -840,8 +1035,8 @@ function resolveSession(raw, file, name) {
|
|
|
840
1035
|
if (value === 'tmux')
|
|
841
1036
|
throw new ConfigError(`${file}: role '${name}' session: tmux is no longer supported; use session: acp `
|
|
842
1037
|
+ 'with the Codex or Claude Code adapter');
|
|
843
|
-
if (value !== 'acp')
|
|
844
|
-
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`);
|
|
845
1040
|
return value;
|
|
846
1041
|
}
|
|
847
1042
|
function resolveSessionOptions(defaults, role, session, file, name) {
|
|
@@ -856,26 +1051,40 @@ function resolveSessionOptions(defaults, role, session, file, name) {
|
|
|
856
1051
|
...((defaults?.acp) ?? {}),
|
|
857
1052
|
...(role?.acp ?? {}),
|
|
858
1053
|
},
|
|
1054
|
+
codex_app_server: {
|
|
1055
|
+
...((defaults?.codex_app_server) ?? {}),
|
|
1056
|
+
...(role?.codex_app_server ?? {}),
|
|
1057
|
+
},
|
|
859
1058
|
};
|
|
860
1059
|
if (defaults?.tmux !== undefined
|
|
861
1060
|
|| role?.tmux !== undefined)
|
|
862
1061
|
throw new ConfigError(`${file}: role '${name}' session_options.tmux is no longer supported; `
|
|
863
1062
|
+ 'use session: acp with session_options.acp');
|
|
864
|
-
const bad = Object.keys(merged).filter(k => k !== 'acp');
|
|
1063
|
+
const bad = Object.keys(merged).filter(k => k !== 'acp' && k !== 'codex_app_server');
|
|
865
1064
|
if (bad.length)
|
|
866
1065
|
throw new ConfigError(`${file}: role '${name}' session_options: unknown key(s) ${bad.join(', ')}`);
|
|
867
1066
|
if (!isPlainObject(merged.acp))
|
|
868
|
-
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`);
|
|
869
1070
|
const acpBad = Object.keys(merged.acp).filter(k => k !== 'command');
|
|
870
1071
|
if (acpBad.length)
|
|
871
1072
|
throw new ConfigError(`${file}: role '${name}' session_options.acp: unknown key(s) ${acpBad.join(', ')}`);
|
|
872
|
-
const
|
|
873
|
-
if (
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
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;
|
|
879
1088
|
}
|
|
880
1089
|
export function resolvePermissions(defaults, role, file = 'config', name = 'role') {
|
|
881
1090
|
if (defaults !== undefined && !isPlainObject(defaults))
|
|
@@ -960,7 +1169,10 @@ export function resolveMonitorConfig(defMonitor, roleMonitor, labels = {}) {
|
|
|
960
1169
|
}
|
|
961
1170
|
export function findRole(cfg, name) {
|
|
962
1171
|
const r = cfg.roles.find(r => r.name === name);
|
|
963
|
-
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`);
|
|
964
1175
|
throw new ConfigError(`no such role '${name}' in ${cfg.files.join(', ') || 'config'}`);
|
|
1176
|
+
}
|
|
965
1177
|
return r;
|
|
966
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
|
@@ -258,11 +258,13 @@ export function daemonIdentityProvisioner(env = process.env, attachClient = atta
|
|
|
258
258
|
const before = await client.listIdentities();
|
|
259
259
|
const existing = before.find(identity => identity.name === name);
|
|
260
260
|
if (existing) {
|
|
261
|
+
if (!('kind' in existing))
|
|
262
|
+
throw new Error(`identity '${name}' is quarantined; refusing to replace it`);
|
|
261
263
|
if (existing.temp)
|
|
262
264
|
throw new Error(`identity '${name}' exists but is temporary; refusing to convert or adopt it`);
|
|
263
265
|
return; // another reconciler won the create race
|
|
264
266
|
}
|
|
265
|
-
if (!before.some(identity => identity.kind === 'root'))
|
|
267
|
+
if (!before.some(identity => 'kind' in identity && identity.kind === 'root'))
|
|
266
268
|
throw new Error(`cannot create role identity '${name}': this host has no Human identity; run ours onboarding first`);
|
|
267
269
|
let createdHere = false;
|
|
268
270
|
try {
|
|
@@ -279,7 +281,7 @@ export function daemonIdentityProvisioner(env = process.env, attachClient = atta
|
|
|
279
281
|
// accept only the compatible permanent identity now visible.
|
|
280
282
|
const after = await client.listIdentities();
|
|
281
283
|
const raced = after.find(identity => identity.name === name);
|
|
282
|
-
if (!raced || raced.temp)
|
|
284
|
+
if (!raced || !('kind' in raced) || raced.temp)
|
|
283
285
|
throw error;
|
|
284
286
|
}
|
|
285
287
|
if (createdHere && profile.persona && client.setPersona)
|
|
@@ -374,11 +376,12 @@ export function creationBuildNote(p) {
|
|
|
374
376
|
export function formatProvenance(p) {
|
|
375
377
|
const mark = {
|
|
376
378
|
cli: 'explicit', 'fleet-default': 'fleet default', 'caller-role': 'caller role',
|
|
377
|
-
'built-in': 'built-in',
|
|
379
|
+
'agent-template': 'Agent Template', 'built-in': 'built-in',
|
|
378
380
|
};
|
|
379
381
|
return Object.entries(p.settings)
|
|
380
382
|
.filter(([, e]) => e.value !== undefined)
|
|
381
|
-
.map(([k, e]) => ` ${k.padEnd(12)} ${
|
|
383
|
+
.map(([k, e]) => ` ${k.padEnd(12)} ${e.value && typeof e.value === 'object'
|
|
384
|
+
? JSON.stringify(e.value) : String(e.value)} (${mark[e.source]})`);
|
|
382
385
|
}
|
|
383
386
|
/** Classify one setting: an explicit CLI value, a fleet default, or built-in. */
|
|
384
387
|
export function provenanceOf(cliValue, fleetDefault, builtIn) {
|