@ours.network/fleet 1.1.0-nightly.8 → 1.1.0
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 +231 -46
- package/dist/application/capabilities.js +18 -6
- package/dist/application/fleet-query-service.js +7 -4
- 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 +9 -2
- 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 +92 -39
- package/dist/application/task-room-service.js +579 -173
- package/dist/briefing.js +85 -28
- package/dist/build-info.json +5 -5
- package/dist/canonical-json.d.ts +2 -0
- package/dist/canonical-json.js +10 -0
- package/dist/cli.js +130 -55
- package/dist/config.d.ts +38 -3
- package/dist/config.js +193 -28
- package/dist/creation.d.ts +1 -1
- package/dist/creation.js +7 -4
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +193 -55
- package/dist/doctor.d.ts +2 -0
- package/dist/doctor.js +40 -4
- package/dist/fleet-command-audit.d.ts +59 -9
- package/dist/fleet-command-audit.js +426 -63
- package/dist/fleet-proxy.d.ts +5 -2
- package/dist/harness/claude-code-session.js +2 -1
- 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 +9 -2
- package/dist/harness/codex-session.js +18 -2
- package/dist/harness/codex.d.ts +5 -1
- package/dist/harness/codex.js +104 -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 +1 -1
- 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 +21 -5
- package/dist/owner-channel/channel.js +312 -138
- package/dist/owner-channel/commands.d.ts +28 -6
- package/dist/owner-channel/commands.js +109 -27
- 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-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 +434 -279
- package/dist/rooms-tasks/close.d.ts +9 -1
- package/dist/rooms-tasks/close.js +23 -7
- package/dist/rooms-tasks/config.js +16 -25
- 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 +8 -0
- package/dist/rooms-tasks/external-worker.js +38 -0
- 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 -1
- package/dist/rooms-tasks/templates.js +34 -3
- package/dist/rooms-tasks/terminal.d.ts +3 -0
- package/dist/rooms-tasks/terminal.js +10 -4
- package/dist/rooms-tasks/types.d.ts +92 -7
- package/dist/rooms-tasks/types.js +14 -1
- package/dist/runner.d.ts +3 -0
- package/dist/runner.js +92 -33
- package/dist/session/acp.d.ts +4 -1
- package/dist/session/acp.js +29 -13
- 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 +2 -2
- package/dist/session/control.js +18 -2
- 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/web/fleet-config-service.d.ts +1 -0
- package/dist/web/fleet-config-service.js +25 -7
- 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 +4 -4
- package/presets/brain-catalog.json +17 -0
- package/presets/fleet/{agents → agent_templates}/Critic.yaml +2 -0
- package/presets/fleet/{agents/Agent.yaml → agent_templates/Developer.yaml} +3 -1
- package/presets/fleet/agent_templates/LocalCoordinator.yaml +50 -0
- package/presets/fleet/agents/{Developer.yaml → FleetCoordinator.yaml} +1 -1
- 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 +37 -0
- package/presets/fleet/roles/Critic.yaml +23 -1
- package/presets/fleet/roles/Developer.yaml +27 -5
- package/presets/fleet/roles/LocalCoordinator.yaml +35 -0
- package/presets/fleet/room_templates/pair.yaml +9 -7
- package/presets/fleet/room_templates/single.yaml +5 -4
- package/presets/fleet/room_templates/team.yaml +12 -8
- package/presets/manifest.json +2 -1
- package/presets/fleet/agents/Architect.yaml +0 -3
- package/presets/fleet/agents/Secretary.yaml +0 -3
- package/presets/fleet/agents/Tester.yaml +0 -3
- package/presets/fleet/roles/Agent.yaml +0 -6
- package/presets/fleet/roles/Architect.yaml +0 -6
- package/presets/fleet/roles/Secretary.yaml +0 -6
- package/presets/fleet/roles/Tester.yaml +0 -6
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' | 'RoomTemplate';
|
|
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()))
|
|
@@ -334,7 +379,23 @@ function deepSubStrict(value, vars, file, pointer) {
|
|
|
334
379
|
]));
|
|
335
380
|
return value;
|
|
336
381
|
}
|
|
337
|
-
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 = {}) {
|
|
338
399
|
if (baseDoc.api_version !== V2_API_VERSION)
|
|
339
400
|
throw new ConfigError(`${base}: legacy fleet configuration is unsupported; run 'ours-fleet doctor' and rewrite it as ${V2_API_VERSION}`);
|
|
340
401
|
if ('roles' in baseDoc)
|
|
@@ -345,6 +406,7 @@ function composeSplitRoles(base, baseDoc, yamlMode, diagnostics, files, addition
|
|
|
345
406
|
assertTrustedPath(root, 'directory');
|
|
346
407
|
const brains = readKindDirectory(join(root, 'brains'), 'brain', false, yamlMode, diagnostics, files);
|
|
347
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);
|
|
348
410
|
const agents = readKindDirectory(join(root, 'agents'), 'agent', true, yamlMode, diagnostics, files);
|
|
349
411
|
if (additionalAgent) {
|
|
350
412
|
if (agents.has(additionalAgent.id))
|
|
@@ -354,11 +416,67 @@ function composeSplitRoles(base, baseDoc, yamlMode, diagnostics, files, addition
|
|
|
354
416
|
});
|
|
355
417
|
}
|
|
356
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
|
+
}
|
|
357
450
|
const documents = [];
|
|
358
451
|
for (const [id, agent] of agents) {
|
|
359
|
-
|
|
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
|
+
}
|
|
360
476
|
if (!('role' in a) || !('brain' in a))
|
|
361
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');
|
|
362
480
|
const role = selection(a.role, 'role', agent.file, roles, ROLE_PRESET_KEYS);
|
|
363
481
|
const brain = selection(a.brain, 'brain', agent.file, brains, BRAIN_PRESET_KEYS);
|
|
364
482
|
const operational = deepSubStrict(Object.fromEntries(Object.entries(a).filter(([key]) => key !== 'role' && key !== 'brain')), vars, agent.file, `/agents/${id}`);
|
|
@@ -433,10 +551,12 @@ function composeSplitRoles(base, baseDoc, yamlMode, diagnostics, files, addition
|
|
|
433
551
|
brain: deepSubStrict(a.brain, vars, agent.file, `/agents/${id}/brain`),
|
|
434
552
|
...operational,
|
|
435
553
|
};
|
|
554
|
+
const { loops: _temporaryLoops, ...runtimeOperational } = operational;
|
|
436
555
|
documents.push({ file: agent.file,
|
|
437
|
-
doc: { roles: { [id]: { ...brainValue, ...roleValue, ...
|
|
556
|
+
doc: { roles: { [id]: { ...brainValue, ...roleValue, ...runtimeOperational } } }, provenance,
|
|
438
557
|
agentSelections: structuredClone({ role: canonicalDefinition.role, brain: canonicalDefinition.brain }),
|
|
439
|
-
agentDefinition: canonicalDefinition
|
|
558
|
+
agentDefinition: canonicalDefinition,
|
|
559
|
+
...(agentTemplate ? { agentTemplate } : {}) });
|
|
440
560
|
}
|
|
441
561
|
return documents;
|
|
442
562
|
}
|
|
@@ -454,6 +574,9 @@ export function loadConfig(configPath, options = {}) {
|
|
|
454
574
|
const base = configPath ?? defaultConfigPath();
|
|
455
575
|
const files = [];
|
|
456
576
|
const diagnostics = [];
|
|
577
|
+
const agentTemplates = {};
|
|
578
|
+
const rolePresets = {};
|
|
579
|
+
const brainPresets = {};
|
|
457
580
|
let docs = [];
|
|
458
581
|
let manifestDoc = {};
|
|
459
582
|
if (existsSync(base)) {
|
|
@@ -472,7 +595,7 @@ export function loadConfig(configPath, options = {}) {
|
|
|
472
595
|
const dd = fleetDDir();
|
|
473
596
|
if (existsSync(dd) && readdirSync(dd).some(f => f.endsWith('.yaml') || f.endsWith('.yml')))
|
|
474
597
|
throw new ConfigError(`${dd}: legacy fleet.d configuration is unsupported; run 'ours-fleet doctor' and move agents to ${splitRootFor(base)}/agents/`);
|
|
475
|
-
docs = [{ file: base, doc: manifestDoc }, ...composeSplitRoles(base, manifestDoc, options.yamlMode ?? 'compat', diagnostics, files, options.additionalAgent)];
|
|
598
|
+
docs = [{ file: base, doc: manifestDoc }, ...composeSplitRoles(base, manifestDoc, options.yamlMode ?? 'compat', diagnostics, files, options.additionalAgent, agentTemplates, rolePresets, brainPresets)];
|
|
476
599
|
const baseDoc = manifestDoc;
|
|
477
600
|
const vars = (baseDoc.vars ?? {});
|
|
478
601
|
const defaults = (baseDoc.defaults ?? {});
|
|
@@ -485,7 +608,7 @@ export function loadConfig(configPath, options = {}) {
|
|
|
485
608
|
const seen = new Map();
|
|
486
609
|
const roles = [];
|
|
487
610
|
const agentDefinitions = {};
|
|
488
|
-
for (const { file, doc, provenance, agentSelections, agentDefinition } of docs) {
|
|
611
|
+
for (const { file, doc, provenance, agentSelections, agentDefinition, agentTemplate } of docs) {
|
|
489
612
|
for (const [name, raw] of Object.entries((doc.roles ?? {}))) {
|
|
490
613
|
if (!ROLE_NAME_RE.test(name))
|
|
491
614
|
throw new ConfigError(`${file}: invalid role name '${name}' (allowed: [A-Za-z0-9_-])`);
|
|
@@ -525,6 +648,8 @@ export function loadConfig(configPath, options = {}) {
|
|
|
525
648
|
const worklog = resolveWorklogPolicy(defaults.worklog, r.worklog, file, name);
|
|
526
649
|
const authProxy = resolveAuthProxy(defaults.auth_proxy, r.auth_proxy, file, name);
|
|
527
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`);
|
|
528
653
|
const defaultHarness = defaults.harness ?? 'claude-code';
|
|
529
654
|
const inheritsModelDefaults = harness === defaultHarness && r.model !== null;
|
|
530
655
|
if (authProxy && harness !== 'claude-code')
|
|
@@ -559,7 +684,7 @@ export function loadConfig(configPath, options = {}) {
|
|
|
559
684
|
defaulted('identity', r.identity !== undefined, r.identity === undefined);
|
|
560
685
|
defaulted('permissions', defaults.permissions !== undefined, defaults.permissions === undefined);
|
|
561
686
|
defaulted('monitor', defaults.monitor !== undefined, defaults.monitor === undefined);
|
|
562
|
-
|
|
687
|
+
const resolvedRole = {
|
|
563
688
|
...r,
|
|
564
689
|
name,
|
|
565
690
|
sourceFile: file,
|
|
@@ -582,7 +707,11 @@ export function loadConfig(configPath, options = {}) {
|
|
|
582
707
|
env: Object.keys(env).length ? env : undefined,
|
|
583
708
|
loops: [],
|
|
584
709
|
...(agentSelections ? { agentSelections } : {}),
|
|
585
|
-
|
|
710
|
+
...(agentTemplate ? { agentTemplate } : {}),
|
|
711
|
+
};
|
|
712
|
+
if (agentDefinition?.loops !== undefined)
|
|
713
|
+
resolvedRole.temporaryLoops = resolveAgentLoops(agentDefinition.loops, resolvedRole, file);
|
|
714
|
+
roles.push(resolvedRole);
|
|
586
715
|
// Forbidden-path enforcement: a mount that would breach the policy
|
|
587
716
|
// is a configuration error, caught by `config` rather than at launch.
|
|
588
717
|
if (isolation !== undefined) {
|
|
@@ -653,7 +782,7 @@ export function loadConfig(configPath, options = {}) {
|
|
|
653
782
|
}
|
|
654
783
|
}
|
|
655
784
|
const result = {
|
|
656
|
-
roles, agentDefinitions, vars, defaults, files, startStaggerMs, diagnostics, watchdogs,
|
|
785
|
+
roles, agentDefinitions, agentTemplates, rolePresets, brainPresets, vars, defaults, files, startStaggerMs, diagnostics, watchdogs,
|
|
657
786
|
loops: resolvedLoops.loops,
|
|
658
787
|
rooms, roomTemplates, tasks, ownerInviteFingerprint,
|
|
659
788
|
configMode: 'split-v2',
|
|
@@ -662,11 +791,18 @@ export function loadConfig(configPath, options = {}) {
|
|
|
662
791
|
return { kind: 'Manifest', path };
|
|
663
792
|
const parent = basename(dirname(path));
|
|
664
793
|
const kind = parent === 'agents' ? 'Agent'
|
|
665
|
-
: parent === '
|
|
666
|
-
: parent === '
|
|
794
|
+
: parent === 'agent_templates' ? 'AgentTemplate'
|
|
795
|
+
: parent === 'roles' ? 'Role'
|
|
796
|
+
: parent === 'room_templates' ? 'RoomTemplate' : 'Brain';
|
|
667
797
|
return { kind, id: basename(path, extname(path)), path };
|
|
668
798
|
}),
|
|
669
799
|
};
|
|
800
|
+
Object.defineProperty(result, 'resolveAgentDefinition', {
|
|
801
|
+
value: (id, definition) => resolveAgentDefinitionDryRun(base, id, definition),
|
|
802
|
+
enumerable: false,
|
|
803
|
+
writable: false,
|
|
804
|
+
configurable: false,
|
|
805
|
+
});
|
|
670
806
|
if (ownerInvite !== undefined) {
|
|
671
807
|
Object.defineProperty(result, 'ownerInvite', {
|
|
672
808
|
value: ownerInvite,
|
|
@@ -677,6 +813,12 @@ export function loadConfig(configPath, options = {}) {
|
|
|
677
813
|
}
|
|
678
814
|
return result;
|
|
679
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
|
+
}
|
|
680
822
|
/**
|
|
681
823
|
* Canonical form of a 64-hex container ID for authorization decisions. Hex
|
|
682
824
|
* case is not identity: two casings of one CID are the same peer, so every
|
|
@@ -760,8 +902,8 @@ export function resolveOwnerChannelConfig(defaults, role, session, file = 'confi
|
|
|
760
902
|
const maxRequestBytes = attachments.max_request_bytes ?? 20 * 1024 * 1024;
|
|
761
903
|
if (maxRequestBytes < maxFileBytes)
|
|
762
904
|
throw new ConfigError(`${file}: role '${name}' owner_channel.attachments.max_request_bytes must be at least max_file_bytes`);
|
|
763
|
-
if (session !== 'acp')
|
|
764
|
-
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`);
|
|
765
907
|
return {
|
|
766
908
|
identity: merged.identity.trim(),
|
|
767
909
|
owners,
|
|
@@ -781,7 +923,13 @@ export function resolveOwnerChannelConfig(defaults, role, session, file = 'confi
|
|
|
781
923
|
};
|
|
782
924
|
}
|
|
783
925
|
function validateOwnerChannelIdentities(roles) {
|
|
784
|
-
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
|
+
}
|
|
785
933
|
const channels = new Map();
|
|
786
934
|
for (const role of roles) {
|
|
787
935
|
const identity = role.owner_channel?.identity;
|
|
@@ -887,8 +1035,8 @@ function resolveSession(raw, file, name) {
|
|
|
887
1035
|
if (value === 'tmux')
|
|
888
1036
|
throw new ConfigError(`${file}: role '${name}' session: tmux is no longer supported; use session: acp `
|
|
889
1037
|
+ 'with the Codex or Claude Code adapter');
|
|
890
|
-
if (value !== 'acp')
|
|
891
|
-
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`);
|
|
892
1040
|
return value;
|
|
893
1041
|
}
|
|
894
1042
|
function resolveSessionOptions(defaults, role, session, file, name) {
|
|
@@ -903,26 +1051,40 @@ function resolveSessionOptions(defaults, role, session, file, name) {
|
|
|
903
1051
|
...((defaults?.acp) ?? {}),
|
|
904
1052
|
...(role?.acp ?? {}),
|
|
905
1053
|
},
|
|
1054
|
+
codex_app_server: {
|
|
1055
|
+
...((defaults?.codex_app_server) ?? {}),
|
|
1056
|
+
...(role?.codex_app_server ?? {}),
|
|
1057
|
+
},
|
|
906
1058
|
};
|
|
907
1059
|
if (defaults?.tmux !== undefined
|
|
908
1060
|
|| role?.tmux !== undefined)
|
|
909
1061
|
throw new ConfigError(`${file}: role '${name}' session_options.tmux is no longer supported; `
|
|
910
1062
|
+ 'use session: acp with session_options.acp');
|
|
911
|
-
const bad = Object.keys(merged).filter(k => k !== 'acp');
|
|
1063
|
+
const bad = Object.keys(merged).filter(k => k !== 'acp' && k !== 'codex_app_server');
|
|
912
1064
|
if (bad.length)
|
|
913
1065
|
throw new ConfigError(`${file}: role '${name}' session_options: unknown key(s) ${bad.join(', ')}`);
|
|
914
1066
|
if (!isPlainObject(merged.acp))
|
|
915
|
-
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`);
|
|
916
1070
|
const acpBad = Object.keys(merged.acp).filter(k => k !== 'command');
|
|
917
1071
|
if (acpBad.length)
|
|
918
1072
|
throw new ConfigError(`${file}: role '${name}' session_options.acp: unknown key(s) ${acpBad.join(', ')}`);
|
|
919
|
-
const
|
|
920
|
-
if (
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
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;
|
|
926
1088
|
}
|
|
927
1089
|
export function resolvePermissions(defaults, role, file = 'config', name = 'role') {
|
|
928
1090
|
if (defaults !== undefined && !isPlainObject(defaults))
|
|
@@ -1007,7 +1169,10 @@ export function resolveMonitorConfig(defMonitor, roleMonitor, labels = {}) {
|
|
|
1007
1169
|
}
|
|
1008
1170
|
export function findRole(cfg, name) {
|
|
1009
1171
|
const r = cfg.roles.find(r => r.name === name);
|
|
1010
|
-
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`);
|
|
1011
1175
|
throw new ConfigError(`no such role '${name}' in ${cfg.files.join(', ') || 'config'}`);
|
|
1176
|
+
}
|
|
1012
1177
|
return r;
|
|
1013
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) {
|