@ours.network/fleet 1.1.0-nightly.9 → 1.1.1
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 +285 -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 +8 -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 +92 -39
- package/dist/application/task-room-service.js +562 -289
- package/dist/briefing.js +79 -22
- 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 +129 -47
- package/dist/config.d.ts +42 -3
- package/dist/config.js +202 -29
- package/dist/creation.d.ts +1 -1
- package/dist/creation.js +7 -4
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +195 -41
- package/dist/doctor.d.ts +2 -0
- package/dist/doctor.js +40 -4
- package/dist/fleet-command-audit.d.ts +22 -1
- package/dist/fleet-command-audit.js +355 -50
- package/dist/fleet-proxy.d.ts +5 -2
- package/dist/harness/claude-code-session.js +5 -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 +21 -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 +19 -3
- package/dist/owner-channel/channel.js +301 -120
- 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/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 +361 -285
- 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 -83
- 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 +97 -47
- package/dist/session/acp.d.ts +27 -1
- package/dist/session/acp.js +307 -17
- 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 +1 -1
- package/dist/session/control.js +17 -1
- package/dist/session/conversation-types.d.ts +4 -4
- package/dist/session/stall-watchdog.d.ts +60 -0
- package/dist/session/stall-watchdog.js +210 -0
- package/dist/session/types.d.ts +29 -8
- 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 {
|
|
@@ -63,6 +68,10 @@ export interface MonitorConfig {
|
|
|
63
68
|
* a positive integer; resolved default is 3. Optional so old snapshots resolve.
|
|
64
69
|
*/
|
|
65
70
|
turn_fail_threshold?: number;
|
|
71
|
+
/** Opt-in bounded ACP no-progress recovery; independent of mail interruption policy. */
|
|
72
|
+
stall_recovery?: boolean;
|
|
73
|
+
/** Progress silence window; default 15 minutes. Generic silence requires two windows. */
|
|
74
|
+
stall_timeout_ms?: number;
|
|
66
75
|
}
|
|
67
76
|
/** A trusted, fleet-owned ours mailbox which is never bound inside the agent. */
|
|
68
77
|
export interface OwnerChannelConfig {
|
|
@@ -157,6 +166,15 @@ export interface AgentDefinition {
|
|
|
157
166
|
owner_channel?: OwnerChannelConfigInput;
|
|
158
167
|
worklog?: WorklogPolicyInput;
|
|
159
168
|
auth_proxy?: Partial<AuthProxyConfig>;
|
|
169
|
+
/** Scheduled turns scoped only to temporary launches; persistent Agents reject this field. */
|
|
170
|
+
loops?: AgentLoopsConfig;
|
|
171
|
+
}
|
|
172
|
+
/** Inert, reusable launch definition. It never owns an identity or runtime state. */
|
|
173
|
+
export type AgentTemplateDefinition = Omit<AgentDefinition, 'identity'>;
|
|
174
|
+
/** Explicit persistent instance reusing an inert template. */
|
|
175
|
+
export interface AgentTemplateInstance {
|
|
176
|
+
template: string;
|
|
177
|
+
overrides?: Partial<AgentDefinition>;
|
|
160
178
|
}
|
|
161
179
|
/** Internal first-boot payload for a Fleet-provisioned Cowork room member. */
|
|
162
180
|
export interface RoomMemberStartup {
|
|
@@ -168,6 +186,7 @@ export interface RoomMemberStartup {
|
|
|
168
186
|
role: string;
|
|
169
187
|
task: string;
|
|
170
188
|
owner_seat_cid: string | null;
|
|
189
|
+
anonymous?: boolean;
|
|
171
190
|
}
|
|
172
191
|
export interface ResolvedRole extends Omit<RoleConfig, 'model' | 'owner_channel' | 'worklog'> {
|
|
173
192
|
name: string;
|
|
@@ -189,11 +208,20 @@ export interface ResolvedRole extends Omit<RoleConfig, 'model' | 'owner_channel'
|
|
|
189
208
|
worklog?: WorklogPolicy;
|
|
190
209
|
auth_proxy?: AuthProxyConfig;
|
|
191
210
|
loops?: ResolvedRoleLoop[];
|
|
211
|
+
/** Agent Template-local loops, kept separate from manifest loops until a temp launch. */
|
|
212
|
+
temporaryLoops?: ResolvedRoleLoop[];
|
|
213
|
+
/** Durable source of the temporary loop policy; internal and never user-authored. */
|
|
214
|
+
temporaryLoopSource?: 'agent-template' | 'cli' | 'omitted';
|
|
192
215
|
provenance?: Record<string, FieldProvenance>;
|
|
193
216
|
/** Internal/transient: never accepted as a user-authored RoleConfig key. */
|
|
194
217
|
roomMemberStartup?: RoomMemberStartup;
|
|
195
218
|
/** Original unresolved selections only; denied operational fields never enter inheritance state. */
|
|
196
219
|
agentSelections?: Pick<AgentDefinition, 'role' | 'brain'>;
|
|
220
|
+
agentTemplate?: {
|
|
221
|
+
id: string;
|
|
222
|
+
sourceFile: string;
|
|
223
|
+
contentHash: string;
|
|
224
|
+
};
|
|
197
225
|
}
|
|
198
226
|
export interface FieldProvenance {
|
|
199
227
|
sourceFile: string;
|
|
@@ -215,12 +243,18 @@ export interface FleetConfig {
|
|
|
215
243
|
roles: ResolvedRole[];
|
|
216
244
|
/** Canonical, variable-resolved Agent authoring documents keyed by stable Agent ID. */
|
|
217
245
|
agentDefinitions?: Record<string, AgentDefinition>;
|
|
246
|
+
/** Canonical inert templates keyed by stable template ID. */
|
|
247
|
+
agentTemplates?: Record<string, AgentTemplateDefinition>;
|
|
248
|
+
rolePresets?: Record<string, Record<string, unknown>>;
|
|
249
|
+
brainPresets?: Record<string, Record<string, unknown>>;
|
|
250
|
+
/** Required launch-boundary resolver for effective temporary Agent definitions. */
|
|
251
|
+
resolveAgentDefinition?: (id: string, definition: AgentDefinition) => ResolvedRole;
|
|
218
252
|
vars: Record<string, string>;
|
|
219
253
|
defaults: Record<string, unknown>;
|
|
220
254
|
files: string[];
|
|
221
255
|
configMode?: 'split-v2';
|
|
222
256
|
sourceDocuments?: Array<{
|
|
223
|
-
kind: 'Manifest' | 'Agent' | 'Role' | 'Brain' | 'RoomTemplate';
|
|
257
|
+
kind: 'Manifest' | 'Agent' | 'AgentTemplate' | 'Role' | 'Brain' | 'RoomTemplate';
|
|
224
258
|
id?: string;
|
|
225
259
|
path: string;
|
|
226
260
|
}>;
|
|
@@ -260,6 +294,8 @@ export declare const AGENT_KEYS: string[];
|
|
|
260
294
|
type BarePreset = Record<string, unknown>;
|
|
261
295
|
export declare function validateRoleValue(value: BarePreset, file: string, pointer: string): void;
|
|
262
296
|
export declare function validateBrainValue(value: BarePreset, file: string, pointer: string): void;
|
|
297
|
+
/** Pure launch-boundary validation for a fully resolved temporary Agent definition. */
|
|
298
|
+
export declare function validateEffectiveAgentTemplate(definition: AgentTemplateDefinition, id?: string): AgentTemplateDefinition;
|
|
263
299
|
export declare function assertBareKeys(value: unknown, allowed: string[], label: string): BarePreset;
|
|
264
300
|
export declare function splitRootFor(base: string): string;
|
|
265
301
|
/** Load a v2 manifest and bare Agent/Role/Brain documents from its stem directory. */
|
|
@@ -268,9 +304,12 @@ export declare function loadConfig(configPath?: string, options?: {
|
|
|
268
304
|
additionalAgent?: {
|
|
269
305
|
id: string;
|
|
270
306
|
definition: AgentDefinition;
|
|
307
|
+
temporary?: boolean;
|
|
271
308
|
};
|
|
272
309
|
skipWatchdogs?: boolean;
|
|
273
310
|
}): FleetConfig;
|
|
311
|
+
/** Full read-only Agent resolution used by launch dry-runs and room plan validation. */
|
|
312
|
+
export declare function resolveAgentDefinitionDryRun(configPath: string, id: string, definition: AgentDefinition): ResolvedRole;
|
|
274
313
|
/**
|
|
275
314
|
* Canonical form of a 64-hex container ID for authorization decisions. Hex
|
|
276
315
|
* 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,
|
|
@@ -25,7 +27,7 @@ export const NOTIFY_EVENT_TYPES = [
|
|
|
25
27
|
/** Default wake sources when a role does not list its own. */
|
|
26
28
|
export const DEFAULT_WAKE_SOURCES = ['message_received', 'file_received', 'local_contact_request', 'pending_message'];
|
|
27
29
|
const MONITOR_KEYS = [
|
|
28
|
-
'mode', 'enabled', 'wake_sources', 'batch_ms', 'inject', 'interrupt', 'turn_fail_threshold',
|
|
30
|
+
'mode', 'enabled', 'wake_sources', 'batch_ms', 'inject', 'interrupt', 'turn_fail_threshold', 'stall_recovery', 'stall_timeout_ms',
|
|
29
31
|
];
|
|
30
32
|
const INJECT_MODES = ['notification', 'full'];
|
|
31
33
|
const MONITOR_MODES = ['fleet', 'native'];
|
|
@@ -72,6 +74,12 @@ export function validateMonitorConfig(raw, capabilities = CAPABILITIES) {
|
|
|
72
74
|
&& (typeof m.turn_fail_threshold !== 'number' || !Number.isInteger(m.turn_fail_threshold)
|
|
73
75
|
|| m.turn_fail_threshold < 1))
|
|
74
76
|
problems.push('monitor.turn_fail_threshold: must be a positive integer');
|
|
77
|
+
if (m.stall_recovery !== undefined && typeof m.stall_recovery !== 'boolean')
|
|
78
|
+
problems.push('monitor.stall_recovery: must be true or false');
|
|
79
|
+
if (m.stall_timeout_ms !== undefined
|
|
80
|
+
&& (typeof m.stall_timeout_ms !== 'number' || !Number.isSafeInteger(m.stall_timeout_ms)
|
|
81
|
+
|| m.stall_timeout_ms < 60_000 || m.stall_timeout_ms > 86_400_000))
|
|
82
|
+
problems.push('monitor.stall_timeout_ms: must be an integer between 60000 and 86400000');
|
|
75
83
|
if (m.wake_sources !== undefined) {
|
|
76
84
|
if (!Array.isArray(m.wake_sources))
|
|
77
85
|
problems.push('monitor.wake_sources: must be a list');
|
|
@@ -140,8 +148,13 @@ export const BRAIN_PRESET_KEYS = [
|
|
|
140
148
|
];
|
|
141
149
|
export const AGENT_KEYS = [
|
|
142
150
|
'role', 'brain', 'permissions', 'identity', 'cwd', 'coordinator', 'env', 'oversee',
|
|
143
|
-
'isolation', 'monitor', 'owner_channel', 'worklog', 'auth_proxy',
|
|
151
|
+
'isolation', 'monitor', 'owner_channel', 'worklog', 'auth_proxy', 'loops',
|
|
144
152
|
];
|
|
153
|
+
const AGENT_INSTANCE_KEYS = ['template', 'overrides'];
|
|
154
|
+
const TEMPLATE_FORBIDDEN_KEYS = ['identity'];
|
|
155
|
+
const MERGED_MAP_FIELDS = new Set([
|
|
156
|
+
'permissions', 'env', 'isolation', 'monitor', 'owner_channel', 'worklog', 'auth_proxy', 'loops',
|
|
157
|
+
]);
|
|
145
158
|
const V2_API_VERSION = 'ours.network/fleet/v2';
|
|
146
159
|
const utf8Bytes = (value) => Buffer.byteLength(value, 'utf8');
|
|
147
160
|
function schemaError(file, pointer, summary) {
|
|
@@ -179,7 +192,45 @@ export function validateBrainValue(value, file, pointer) {
|
|
|
179
192
|
schemaError(file, `${pointer}/${key}`, 'must be a mapping');
|
|
180
193
|
}
|
|
181
194
|
}
|
|
195
|
+
/** Pure launch-boundary validation for a fully resolved temporary Agent definition. */
|
|
196
|
+
export function validateEffectiveAgentTemplate(definition, id = 'room-member') {
|
|
197
|
+
const value = structuredClone(definition);
|
|
198
|
+
const bad = Object.keys(value).filter(key => !AGENT_KEYS.includes(key));
|
|
199
|
+
if (bad.length)
|
|
200
|
+
throw new ConfigError(`member '${id}' has unknown Agent key(s) ${bad.join(', ')}`);
|
|
201
|
+
if (Object.hasOwn(value, 'identity'))
|
|
202
|
+
throw new ConfigError(`member '${id}' cannot override identity`);
|
|
203
|
+
if (!isPlainObject(value.role) || !isPlainObject(value.role.inline)
|
|
204
|
+
|| Object.keys(value.role).some(key => key !== 'inline'))
|
|
205
|
+
throw new ConfigError(`member '${id}' role must be a resolved inline definition`);
|
|
206
|
+
if (!isPlainObject(value.brain) || !isPlainObject(value.brain.inline)
|
|
207
|
+
|| Object.keys(value.brain).some(key => key !== 'inline'))
|
|
208
|
+
throw new ConfigError(`member '${id}' brain must be a resolved inline definition`);
|
|
209
|
+
const role = value.role.inline;
|
|
210
|
+
const brain = value.brain.inline;
|
|
211
|
+
assertBareKeys(role, ROLE_PRESET_KEYS, `member '${id}' Role`);
|
|
212
|
+
assertBareKeys(brain, BRAIN_PRESET_KEYS, `member '${id}' Brain`);
|
|
213
|
+
validateRoleValue(role, '(member override)', `/members/${id}/role`);
|
|
214
|
+
validateBrainValue(brain, '(member override)', `/members/${id}/brain`);
|
|
215
|
+
validateAgentScalars(Object.fromEntries(Object.entries(value)
|
|
216
|
+
.filter(([key]) => key !== 'role' && key !== 'brain')), '(member override)', id);
|
|
217
|
+
resolvePermissions(undefined, value.permissions, '(member override)', id);
|
|
218
|
+
if (value.isolation !== undefined) {
|
|
219
|
+
const problems = validateIsolationConfig(value.isolation);
|
|
220
|
+
if (problems.length)
|
|
221
|
+
throw new ConfigError(`member '${id}' ${problems.join('; ')}`);
|
|
222
|
+
}
|
|
223
|
+
const adapter = getAdapter(brain.harness);
|
|
224
|
+
adapter.agentSession.resolveBrain({ model: brain.model,
|
|
225
|
+
effort: brain.effort,
|
|
226
|
+
harnessOptions: brain.harness_options });
|
|
227
|
+
return value;
|
|
228
|
+
}
|
|
182
229
|
function validateAgentScalars(value, file, id) {
|
|
230
|
+
if (value.loops !== undefined)
|
|
231
|
+
resolveAgentLoops(value.loops, {
|
|
232
|
+
name: id, harness: 'codex', session: 'acp', permissions: {},
|
|
233
|
+
}, file);
|
|
183
234
|
for (const key of ['identity', 'cwd']) {
|
|
184
235
|
const current = value[key];
|
|
185
236
|
if (current !== undefined && (typeof current !== 'string' || !current.trim()))
|
|
@@ -334,7 +385,23 @@ function deepSubStrict(value, vars, file, pointer) {
|
|
|
334
385
|
]));
|
|
335
386
|
return value;
|
|
336
387
|
}
|
|
337
|
-
function
|
|
388
|
+
function mergeTemplateDefinition(template, overrides, file, id) {
|
|
389
|
+
const merge = (base, overlay, key) => {
|
|
390
|
+
if (overlay === null && key !== 'model')
|
|
391
|
+
schemaError(file, `/agents/${id}/overrides/${key}`, 'null deletion is not supported');
|
|
392
|
+
if (MERGED_MAP_FIELDS.has(key) && isPlainObject(base) && isPlainObject(overlay)) {
|
|
393
|
+
return Object.fromEntries([...new Set([...Object.keys(base), ...Object.keys(overlay)])]
|
|
394
|
+
.map(child => [child, Object.hasOwn(overlay, child)
|
|
395
|
+
? merge(base[child], overlay[child], key) : structuredClone(base[child])]));
|
|
396
|
+
}
|
|
397
|
+
return structuredClone(overlay);
|
|
398
|
+
};
|
|
399
|
+
const result = structuredClone(template);
|
|
400
|
+
for (const [key, value] of Object.entries(overrides))
|
|
401
|
+
result[key] = merge(result[key], value, key);
|
|
402
|
+
return result;
|
|
403
|
+
}
|
|
404
|
+
function composeSplitRoles(base, baseDoc, yamlMode, diagnostics, files, additionalAgent, templateSink = {}, roleSink = {}, brainSink = {}) {
|
|
338
405
|
if (baseDoc.api_version !== V2_API_VERSION)
|
|
339
406
|
throw new ConfigError(`${base}: legacy fleet configuration is unsupported; run 'ours-fleet doctor' and rewrite it as ${V2_API_VERSION}`);
|
|
340
407
|
if ('roles' in baseDoc)
|
|
@@ -345,6 +412,7 @@ function composeSplitRoles(base, baseDoc, yamlMode, diagnostics, files, addition
|
|
|
345
412
|
assertTrustedPath(root, 'directory');
|
|
346
413
|
const brains = readKindDirectory(join(root, 'brains'), 'brain', false, yamlMode, diagnostics, files);
|
|
347
414
|
const roles = readKindDirectory(join(root, 'roles'), 'role', false, yamlMode, diagnostics, files);
|
|
415
|
+
const agentTemplates = readKindDirectory(join(root, 'agent_templates'), 'agent template', false, yamlMode, diagnostics, files);
|
|
348
416
|
const agents = readKindDirectory(join(root, 'agents'), 'agent', true, yamlMode, diagnostics, files);
|
|
349
417
|
if (additionalAgent) {
|
|
350
418
|
if (agents.has(additionalAgent.id))
|
|
@@ -354,11 +422,67 @@ function composeSplitRoles(base, baseDoc, yamlMode, diagnostics, files, addition
|
|
|
354
422
|
});
|
|
355
423
|
}
|
|
356
424
|
const vars = (baseDoc.vars ?? {});
|
|
425
|
+
for (const [id, preset] of roles) {
|
|
426
|
+
const value = assertBareKeys(preset.value, ROLE_PRESET_KEYS, `${preset.file}: Role '${id}'`);
|
|
427
|
+
validateRoleValue(value, preset.file, `/roles/${id}`);
|
|
428
|
+
roleSink[id] = deepSubStrict(value, vars, preset.file, `/roles/${id}`);
|
|
429
|
+
}
|
|
430
|
+
for (const [id, preset] of brains) {
|
|
431
|
+
const value = assertBareKeys(preset.value, BRAIN_PRESET_KEYS, `${preset.file}: Brain '${id}'`);
|
|
432
|
+
validateBrainValue(value, preset.file, `/brains/${id}`);
|
|
433
|
+
brainSink[id] = deepSubStrict(value, vars, preset.file, `/brains/${id}`);
|
|
434
|
+
}
|
|
435
|
+
const canonicalTemplates = new Map();
|
|
436
|
+
for (const [id, template] of agentTemplates) {
|
|
437
|
+
const value = assertBareKeys(template.value, AGENT_KEYS, `${template.file}: agent template '${id}'`);
|
|
438
|
+
if (!('role' in value) || !('brain' in value))
|
|
439
|
+
throw new ConfigError(`${template.file}: agent template '${id}' requires role and brain`);
|
|
440
|
+
const forbidden = TEMPLATE_FORBIDDEN_KEYS.filter(key => Object.hasOwn(value, key));
|
|
441
|
+
if (forbidden.length)
|
|
442
|
+
schemaError(template.file, `/agent_templates/${id}/${forbidden[0]}`, 'templates cannot own instance identity');
|
|
443
|
+
const templateRole = selection(value.role, 'role', template.file, roles, ROLE_PRESET_KEYS);
|
|
444
|
+
const templateBrain = selection(value.brain, 'brain', template.file, brains, BRAIN_PRESET_KEYS);
|
|
445
|
+
validateRoleValue(templateRole.value, templateRole.file, templateRole.pointer);
|
|
446
|
+
validateBrainValue(templateBrain.value, templateBrain.file, templateBrain.pointer);
|
|
447
|
+
validateAgentScalars(Object.fromEntries(Object.entries(value).filter(([key]) => key !== 'role' && key !== 'brain')), template.file, id);
|
|
448
|
+
canonicalTemplates.set(id, { file: template.file, value });
|
|
449
|
+
const operational = deepSubStrict(Object.fromEntries(Object.entries(value).filter(([key]) => key !== 'role' && key !== 'brain')), vars, template.file, `/agent_templates/${id}`);
|
|
450
|
+
templateSink[id] = {
|
|
451
|
+
role: { inline: deepSubStrict(templateRole.value, vars, templateRole.file, templateRole.pointer) },
|
|
452
|
+
brain: { inline: deepSubStrict(templateBrain.value, vars, templateBrain.file, templateBrain.pointer) },
|
|
453
|
+
...operational,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
357
456
|
const documents = [];
|
|
358
457
|
for (const [id, agent] of agents) {
|
|
359
|
-
|
|
458
|
+
let a;
|
|
459
|
+
let agentTemplate;
|
|
460
|
+
const instanceKeys = Object.keys(agent.value);
|
|
461
|
+
if (instanceKeys.includes('template')) {
|
|
462
|
+
assertBareKeys(agent.value, AGENT_INSTANCE_KEYS, `${agent.file}: agent '${id}'`);
|
|
463
|
+
if (typeof agent.value.template !== 'string' || !ROLE_NAME_RE.test(agent.value.template))
|
|
464
|
+
schemaError(agent.file, `/agents/${id}/template`, 'must be a valid Agent Template ID');
|
|
465
|
+
const template = canonicalTemplates.get(agent.value.template);
|
|
466
|
+
if (!template)
|
|
467
|
+
throw new ConfigError(`${agent.file}: E_REF_MISSING: agent template '${agent.value.template}' not found`);
|
|
468
|
+
if (agent.value.overrides !== undefined && !isPlainObject(agent.value.overrides))
|
|
469
|
+
schemaError(agent.file, `/agents/${id}/overrides`, 'must be a mapping');
|
|
470
|
+
const overrides = assertBareKeys((agent.value.overrides ?? {}), AGENT_KEYS, `${agent.file}: agent '${id}' overrides`);
|
|
471
|
+
a = mergeTemplateDefinition(template.value, overrides, agent.file, id);
|
|
472
|
+
const resolvedTemplate = templateSink[agent.value.template];
|
|
473
|
+
agentTemplate = {
|
|
474
|
+
id: agent.value.template,
|
|
475
|
+
sourceFile: template.file,
|
|
476
|
+
contentHash: createHash('sha256').update(canonicalJson(resolvedTemplate)).digest('hex'),
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
a = assertBareKeys(agent.value, AGENT_KEYS, `${agent.file}: agent '${id}'`);
|
|
481
|
+
}
|
|
360
482
|
if (!('role' in a) || !('brain' in a))
|
|
361
483
|
throw new ConfigError(`${agent.file}: agent '${id}' requires role and brain`);
|
|
484
|
+
if (Object.hasOwn(a, 'loops') && !(additionalAgent?.temporary && additionalAgent.id === id))
|
|
485
|
+
schemaError(agent.file, `/agents/${id}/loops`, 'is supported only for temporary Agent launches and inert Agent Templates');
|
|
362
486
|
const role = selection(a.role, 'role', agent.file, roles, ROLE_PRESET_KEYS);
|
|
363
487
|
const brain = selection(a.brain, 'brain', agent.file, brains, BRAIN_PRESET_KEYS);
|
|
364
488
|
const operational = deepSubStrict(Object.fromEntries(Object.entries(a).filter(([key]) => key !== 'role' && key !== 'brain')), vars, agent.file, `/agents/${id}`);
|
|
@@ -433,10 +557,12 @@ function composeSplitRoles(base, baseDoc, yamlMode, diagnostics, files, addition
|
|
|
433
557
|
brain: deepSubStrict(a.brain, vars, agent.file, `/agents/${id}/brain`),
|
|
434
558
|
...operational,
|
|
435
559
|
};
|
|
560
|
+
const { loops: _temporaryLoops, ...runtimeOperational } = operational;
|
|
436
561
|
documents.push({ file: agent.file,
|
|
437
|
-
doc: { roles: { [id]: { ...brainValue, ...roleValue, ...
|
|
562
|
+
doc: { roles: { [id]: { ...brainValue, ...roleValue, ...runtimeOperational } } }, provenance,
|
|
438
563
|
agentSelections: structuredClone({ role: canonicalDefinition.role, brain: canonicalDefinition.brain }),
|
|
439
|
-
agentDefinition: canonicalDefinition
|
|
564
|
+
agentDefinition: canonicalDefinition,
|
|
565
|
+
...(agentTemplate ? { agentTemplate } : {}) });
|
|
440
566
|
}
|
|
441
567
|
return documents;
|
|
442
568
|
}
|
|
@@ -454,6 +580,9 @@ export function loadConfig(configPath, options = {}) {
|
|
|
454
580
|
const base = configPath ?? defaultConfigPath();
|
|
455
581
|
const files = [];
|
|
456
582
|
const diagnostics = [];
|
|
583
|
+
const agentTemplates = {};
|
|
584
|
+
const rolePresets = {};
|
|
585
|
+
const brainPresets = {};
|
|
457
586
|
let docs = [];
|
|
458
587
|
let manifestDoc = {};
|
|
459
588
|
if (existsSync(base)) {
|
|
@@ -472,7 +601,7 @@ export function loadConfig(configPath, options = {}) {
|
|
|
472
601
|
const dd = fleetDDir();
|
|
473
602
|
if (existsSync(dd) && readdirSync(dd).some(f => f.endsWith('.yaml') || f.endsWith('.yml')))
|
|
474
603
|
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)];
|
|
604
|
+
docs = [{ file: base, doc: manifestDoc }, ...composeSplitRoles(base, manifestDoc, options.yamlMode ?? 'compat', diagnostics, files, options.additionalAgent, agentTemplates, rolePresets, brainPresets)];
|
|
476
605
|
const baseDoc = manifestDoc;
|
|
477
606
|
const vars = (baseDoc.vars ?? {});
|
|
478
607
|
const defaults = (baseDoc.defaults ?? {});
|
|
@@ -485,7 +614,7 @@ export function loadConfig(configPath, options = {}) {
|
|
|
485
614
|
const seen = new Map();
|
|
486
615
|
const roles = [];
|
|
487
616
|
const agentDefinitions = {};
|
|
488
|
-
for (const { file, doc, provenance, agentSelections, agentDefinition } of docs) {
|
|
617
|
+
for (const { file, doc, provenance, agentSelections, agentDefinition, agentTemplate } of docs) {
|
|
489
618
|
for (const [name, raw] of Object.entries((doc.roles ?? {}))) {
|
|
490
619
|
if (!ROLE_NAME_RE.test(name))
|
|
491
620
|
throw new ConfigError(`${file}: invalid role name '${name}' (allowed: [A-Za-z0-9_-])`);
|
|
@@ -525,6 +654,8 @@ export function loadConfig(configPath, options = {}) {
|
|
|
525
654
|
const worklog = resolveWorklogPolicy(defaults.worklog, r.worklog, file, name);
|
|
526
655
|
const authProxy = resolveAuthProxy(defaults.auth_proxy, r.auth_proxy, file, name);
|
|
527
656
|
const harness = r.harness ?? defaults.harness ?? 'claude-code';
|
|
657
|
+
if (session === 'codex-app-server' && harness !== 'codex')
|
|
658
|
+
throw new ConfigError(`${file}: role '${name}' session: codex-app-server requires harness: codex`);
|
|
528
659
|
const defaultHarness = defaults.harness ?? 'claude-code';
|
|
529
660
|
const inheritsModelDefaults = harness === defaultHarness && r.model !== null;
|
|
530
661
|
if (authProxy && harness !== 'claude-code')
|
|
@@ -559,7 +690,7 @@ export function loadConfig(configPath, options = {}) {
|
|
|
559
690
|
defaulted('identity', r.identity !== undefined, r.identity === undefined);
|
|
560
691
|
defaulted('permissions', defaults.permissions !== undefined, defaults.permissions === undefined);
|
|
561
692
|
defaulted('monitor', defaults.monitor !== undefined, defaults.monitor === undefined);
|
|
562
|
-
|
|
693
|
+
const resolvedRole = {
|
|
563
694
|
...r,
|
|
564
695
|
name,
|
|
565
696
|
sourceFile: file,
|
|
@@ -582,7 +713,11 @@ export function loadConfig(configPath, options = {}) {
|
|
|
582
713
|
env: Object.keys(env).length ? env : undefined,
|
|
583
714
|
loops: [],
|
|
584
715
|
...(agentSelections ? { agentSelections } : {}),
|
|
585
|
-
|
|
716
|
+
...(agentTemplate ? { agentTemplate } : {}),
|
|
717
|
+
};
|
|
718
|
+
if (agentDefinition?.loops !== undefined)
|
|
719
|
+
resolvedRole.temporaryLoops = resolveAgentLoops(agentDefinition.loops, resolvedRole, file);
|
|
720
|
+
roles.push(resolvedRole);
|
|
586
721
|
// Forbidden-path enforcement: a mount that would breach the policy
|
|
587
722
|
// is a configuration error, caught by `config` rather than at launch.
|
|
588
723
|
if (isolation !== undefined) {
|
|
@@ -653,7 +788,7 @@ export function loadConfig(configPath, options = {}) {
|
|
|
653
788
|
}
|
|
654
789
|
}
|
|
655
790
|
const result = {
|
|
656
|
-
roles, agentDefinitions, vars, defaults, files, startStaggerMs, diagnostics, watchdogs,
|
|
791
|
+
roles, agentDefinitions, agentTemplates, rolePresets, brainPresets, vars, defaults, files, startStaggerMs, diagnostics, watchdogs,
|
|
657
792
|
loops: resolvedLoops.loops,
|
|
658
793
|
rooms, roomTemplates, tasks, ownerInviteFingerprint,
|
|
659
794
|
configMode: 'split-v2',
|
|
@@ -662,11 +797,18 @@ export function loadConfig(configPath, options = {}) {
|
|
|
662
797
|
return { kind: 'Manifest', path };
|
|
663
798
|
const parent = basename(dirname(path));
|
|
664
799
|
const kind = parent === 'agents' ? 'Agent'
|
|
665
|
-
: parent === '
|
|
666
|
-
: parent === '
|
|
800
|
+
: parent === 'agent_templates' ? 'AgentTemplate'
|
|
801
|
+
: parent === 'roles' ? 'Role'
|
|
802
|
+
: parent === 'room_templates' ? 'RoomTemplate' : 'Brain';
|
|
667
803
|
return { kind, id: basename(path, extname(path)), path };
|
|
668
804
|
}),
|
|
669
805
|
};
|
|
806
|
+
Object.defineProperty(result, 'resolveAgentDefinition', {
|
|
807
|
+
value: (id, definition) => resolveAgentDefinitionDryRun(base, id, definition),
|
|
808
|
+
enumerable: false,
|
|
809
|
+
writable: false,
|
|
810
|
+
configurable: false,
|
|
811
|
+
});
|
|
670
812
|
if (ownerInvite !== undefined) {
|
|
671
813
|
Object.defineProperty(result, 'ownerInvite', {
|
|
672
814
|
value: ownerInvite,
|
|
@@ -677,6 +819,12 @@ export function loadConfig(configPath, options = {}) {
|
|
|
677
819
|
}
|
|
678
820
|
return result;
|
|
679
821
|
}
|
|
822
|
+
/** Full read-only Agent resolution used by launch dry-runs and room plan validation. */
|
|
823
|
+
export function resolveAgentDefinitionDryRun(configPath, id, definition) {
|
|
824
|
+
return findRole(loadConfig(configPath, {
|
|
825
|
+
additionalAgent: { id, definition, temporary: true }, skipWatchdogs: true,
|
|
826
|
+
}), id);
|
|
827
|
+
}
|
|
680
828
|
/**
|
|
681
829
|
* Canonical form of a 64-hex container ID for authorization decisions. Hex
|
|
682
830
|
* case is not identity: two casings of one CID are the same peer, so every
|
|
@@ -760,8 +908,8 @@ export function resolveOwnerChannelConfig(defaults, role, session, file = 'confi
|
|
|
760
908
|
const maxRequestBytes = attachments.max_request_bytes ?? 20 * 1024 * 1024;
|
|
761
909
|
if (maxRequestBytes < maxFileBytes)
|
|
762
910
|
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
|
|
911
|
+
if (session !== 'acp' && session !== 'codex-app-server')
|
|
912
|
+
throw new ConfigError(`${file}: role '${name}' owner_channel requires a managed session with correlated final replies`);
|
|
765
913
|
return {
|
|
766
914
|
identity: merged.identity.trim(),
|
|
767
915
|
owners,
|
|
@@ -781,7 +929,13 @@ export function resolveOwnerChannelConfig(defaults, role, session, file = 'confi
|
|
|
781
929
|
};
|
|
782
930
|
}
|
|
783
931
|
function validateOwnerChannelIdentities(roles) {
|
|
784
|
-
const roleIdentities = new Map(
|
|
932
|
+
const roleIdentities = new Map();
|
|
933
|
+
for (const role of roles) {
|
|
934
|
+
const prior = roleIdentities.get(role.identity);
|
|
935
|
+
if (prior)
|
|
936
|
+
throw new ConfigError(`persistent Agents '${prior}' and '${role.name}' resolve to duplicate identity '${role.identity}'`);
|
|
937
|
+
roleIdentities.set(role.identity, role.name);
|
|
938
|
+
}
|
|
785
939
|
const channels = new Map();
|
|
786
940
|
for (const role of roles) {
|
|
787
941
|
const identity = role.owner_channel?.identity;
|
|
@@ -887,8 +1041,8 @@ function resolveSession(raw, file, name) {
|
|
|
887
1041
|
if (value === 'tmux')
|
|
888
1042
|
throw new ConfigError(`${file}: role '${name}' session: tmux is no longer supported; use session: acp `
|
|
889
1043
|
+ 'with the Codex or Claude Code adapter');
|
|
890
|
-
if (value !== 'acp')
|
|
891
|
-
throw new ConfigError(`${file}: role '${name}' session: must be: acp`);
|
|
1044
|
+
if (value !== 'acp' && value !== 'codex-app-server')
|
|
1045
|
+
throw new ConfigError(`${file}: role '${name}' session: must be: acp, codex-app-server`);
|
|
892
1046
|
return value;
|
|
893
1047
|
}
|
|
894
1048
|
function resolveSessionOptions(defaults, role, session, file, name) {
|
|
@@ -903,26 +1057,40 @@ function resolveSessionOptions(defaults, role, session, file, name) {
|
|
|
903
1057
|
...((defaults?.acp) ?? {}),
|
|
904
1058
|
...(role?.acp ?? {}),
|
|
905
1059
|
},
|
|
1060
|
+
codex_app_server: {
|
|
1061
|
+
...((defaults?.codex_app_server) ?? {}),
|
|
1062
|
+
...(role?.codex_app_server ?? {}),
|
|
1063
|
+
},
|
|
906
1064
|
};
|
|
907
1065
|
if (defaults?.tmux !== undefined
|
|
908
1066
|
|| role?.tmux !== undefined)
|
|
909
1067
|
throw new ConfigError(`${file}: role '${name}' session_options.tmux is no longer supported; `
|
|
910
1068
|
+ 'use session: acp with session_options.acp');
|
|
911
|
-
const bad = Object.keys(merged).filter(k => k !== 'acp');
|
|
1069
|
+
const bad = Object.keys(merged).filter(k => k !== 'acp' && k !== 'codex_app_server');
|
|
912
1070
|
if (bad.length)
|
|
913
1071
|
throw new ConfigError(`${file}: role '${name}' session_options: unknown key(s) ${bad.join(', ')}`);
|
|
914
1072
|
if (!isPlainObject(merged.acp))
|
|
915
|
-
throw new ConfigError(`${file}: role '${name}' session_options
|
|
1073
|
+
throw new ConfigError(`${file}: role '${name}' session_options.acp must be a map`);
|
|
1074
|
+
if (!isPlainObject(merged.codex_app_server))
|
|
1075
|
+
throw new ConfigError(`${file}: role '${name}' session_options.codex_app_server must be a map`);
|
|
916
1076
|
const acpBad = Object.keys(merged.acp).filter(k => k !== 'command');
|
|
917
1077
|
if (acpBad.length)
|
|
918
1078
|
throw new ConfigError(`${file}: role '${name}' session_options.acp: unknown key(s) ${acpBad.join(', ')}`);
|
|
919
|
-
const
|
|
920
|
-
if (
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
1079
|
+
const nativeBad = Object.keys(merged.codex_app_server).filter(k => k !== 'command');
|
|
1080
|
+
if (nativeBad.length)
|
|
1081
|
+
throw new ConfigError(`${file}: role '${name}' session_options.codex_app_server: unknown key(s) ${nativeBad.join(', ')}`);
|
|
1082
|
+
for (const [key, command] of [
|
|
1083
|
+
['acp', merged.acp.command],
|
|
1084
|
+
['codex_app_server', merged.codex_app_server.command],
|
|
1085
|
+
]) {
|
|
1086
|
+
if (command !== undefined
|
|
1087
|
+
&& !(typeof command === 'string' && command.trim())
|
|
1088
|
+
&& !(Array.isArray(command) && command.length > 0
|
|
1089
|
+
&& command.every(v => typeof v === 'string' && v.length > 0)))
|
|
1090
|
+
throw new ConfigError(`${file}: role '${name}' session_options.${key}.command must be a non-empty string or string list`);
|
|
1091
|
+
}
|
|
1092
|
+
return Object.keys(merged.acp).length || Object.keys(merged.codex_app_server).length
|
|
1093
|
+
? merged : undefined;
|
|
926
1094
|
}
|
|
927
1095
|
export function resolvePermissions(defaults, role, file = 'config', name = 'role') {
|
|
928
1096
|
if (defaults !== undefined && !isPlainObject(defaults))
|
|
@@ -1002,12 +1170,17 @@ export function resolveMonitorConfig(defMonitor, roleMonitor, labels = {}) {
|
|
|
1002
1170
|
batch_ms: merged.batch_ms ?? MONITOR_DEFAULT_BATCH_MS,
|
|
1003
1171
|
inject: merged.inject ?? 'notification',
|
|
1004
1172
|
interrupt: merged.interrupt ?? false,
|
|
1173
|
+
...(merged.stall_recovery !== undefined ? { stall_recovery: merged.stall_recovery } : {}),
|
|
1174
|
+
...(merged.stall_timeout_ms !== undefined ? { stall_timeout_ms: merged.stall_timeout_ms } : {}),
|
|
1005
1175
|
turn_fail_threshold: merged.turn_fail_threshold ?? MONITOR_DEFAULT_TURN_FAIL_THRESHOLD,
|
|
1006
1176
|
};
|
|
1007
1177
|
}
|
|
1008
1178
|
export function findRole(cfg, name) {
|
|
1009
1179
|
const r = cfg.roles.find(r => r.name === name);
|
|
1010
|
-
if (!r)
|
|
1180
|
+
if (!r) {
|
|
1181
|
+
if (cfg.agentTemplates?.[name])
|
|
1182
|
+
throw new ConfigError(`'${name}' is an inert Agent Template, not a persistent Agent; lifecycle commands target only fleet/agents instances`);
|
|
1011
1183
|
throw new ConfigError(`no such role '${name}' in ${cfg.files.join(', ') || 'config'}`);
|
|
1184
|
+
}
|
|
1012
1185
|
return r;
|
|
1013
1186
|
}
|
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) {
|