@ours.network/fleet 1.1.0-nightly.2 → 1.1.0-nightly.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +204 -55
- package/dist/agent-recovery-gate.d.ts +17 -0
- package/dist/agent-recovery-gate.js +135 -0
- package/dist/application/capabilities.js +18 -6
- package/dist/application/fleet-query-service.js +20 -5
- package/dist/application/role-command-service.js +9 -0
- package/dist/application/role-creation-service.d.ts +6 -25
- package/dist/application/role-creation-service.js +34 -72
- package/dist/application/role-repository.d.ts +4 -2
- package/dist/application/role-repository.js +6 -3
- package/dist/application/session-control.d.ts +3 -1
- package/dist/application/session-control.js +4 -2
- package/dist/application/task-room-service.d.ts +58 -6
- package/dist/application/task-room-service.js +435 -79
- package/dist/briefing.js +74 -24
- package/dist/build-info.json +4 -4
- package/dist/canonical-json.d.ts +2 -0
- package/dist/canonical-json.js +10 -0
- package/dist/cli.js +241 -76
- package/dist/config-yaml.d.ts +1 -1
- package/dist/config.d.ts +75 -3
- package/dist/config.js +274 -34
- package/dist/creation.d.ts +1 -1
- package/dist/creation.js +3 -2
- package/dist/daemon-recovery.d.ts +93 -0
- package/dist/daemon-recovery.js +328 -0
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +192 -66
- package/dist/doctor.d.ts +2 -0
- package/dist/doctor.js +54 -4
- package/dist/fleet-command-audit.d.ts +165 -0
- package/dist/fleet-command-audit.js +555 -0
- package/dist/fleet-proxy.d.ts +7 -2
- package/dist/fleet-proxy.js +30 -8
- package/dist/harness/agent-session.d.ts +5 -0
- package/dist/harness/claude-code-session.d.ts +2 -0
- package/dist/harness/claude-code-session.js +6 -1
- package/dist/harness/claude-code.js +2 -0
- package/dist/harness/codex-app-server-proxy.d.ts +40 -0
- package/dist/harness/codex-app-server-proxy.js +348 -14
- package/dist/harness/codex-session.d.ts +11 -2
- package/dist/harness/codex-session.js +22 -2
- package/dist/harness/codex.d.ts +5 -1
- package/dist/harness/codex.js +110 -10
- package/dist/harness/types.d.ts +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +4 -3
- package/dist/init-guidance.d.ts +1 -1
- package/dist/init-guidance.js +4 -3
- package/dist/init-wizard.d.ts +98 -0
- package/dist/init-wizard.js +629 -0
- package/dist/lifecycle-summary.d.ts +134 -0
- package/dist/lifecycle-summary.js +179 -0
- package/dist/loops/config.d.ts +10 -1
- package/dist/loops/config.js +38 -5
- package/dist/model-env.d.ts +2 -2
- package/dist/model-env.js +1 -1
- package/dist/model-recovery.d.ts +1 -1
- package/dist/ops.js +2 -17
- package/dist/owner-channel/channel.d.ts +58 -0
- package/dist/owner-channel/channel.js +321 -42
- package/dist/owner-channel/commands.d.ts +6 -1
- package/dist/owner-channel/commands.js +34 -19
- package/dist/owner-channel/state.d.ts +2 -0
- package/dist/owner-channel/state.js +8 -2
- package/dist/permissions.js +1 -1
- package/dist/preset-bootstrap.d.ts +10 -0
- package/dist/preset-bootstrap.js +85 -0
- package/dist/preset-migration.d.ts +42 -0
- package/dist/preset-migration.js +414 -0
- package/dist/reports/artifact.d.ts +10 -0
- package/dist/reports/artifact.js +32 -0
- package/dist/reports/index.d.ts +5 -0
- package/dist/reports/index.js +5 -0
- package/dist/reports/render.d.ts +6 -0
- package/dist/reports/render.js +240 -0
- package/dist/reports/service.d.ts +42 -0
- package/dist/reports/service.js +57 -0
- package/dist/reports/tasks.d.ts +52 -0
- package/dist/reports/tasks.js +177 -0
- package/dist/reports/types.d.ts +315 -0
- package/dist/reports/types.js +6 -0
- package/dist/resolved-plan.js +13 -1
- package/dist/rooms-tasks/cli.d.ts +2 -0
- package/dist/rooms-tasks/cli.js +514 -75
- package/dist/rooms-tasks/close.d.ts +9 -1
- package/dist/rooms-tasks/close.js +20 -4
- package/dist/rooms-tasks/config.js +16 -14
- package/dist/rooms-tasks/cowork-adapter.d.ts +1 -0
- package/dist/rooms-tasks/cowork-adapter.js +6 -0
- package/dist/rooms-tasks/deletion.d.ts +51 -0
- package/dist/rooms-tasks/deletion.js +217 -0
- package/dist/rooms-tasks/external-worker.d.ts +1 -0
- package/dist/rooms-tasks/external-worker.js +14 -9
- package/dist/rooms-tasks/launch-snapshot.d.ts +19 -0
- package/dist/rooms-tasks/launch-snapshot.js +181 -0
- package/dist/rooms-tasks/member-overrides.d.ts +30 -0
- package/dist/rooms-tasks/member-overrides.js +204 -0
- package/dist/rooms-tasks/provision.js +184 -49
- package/dist/rooms-tasks/room-state.d.ts +1 -0
- package/dist/rooms-tasks/room-state.js +12 -1
- package/dist/rooms-tasks/task-state.d.ts +115 -3
- package/dist/rooms-tasks/task-state.js +361 -11
- package/dist/rooms-tasks/templates.d.ts +4 -2
- package/dist/rooms-tasks/templates.js +42 -78
- package/dist/rooms-tasks/terminal.d.ts +3 -0
- package/dist/rooms-tasks/terminal.js +7 -3
- package/dist/rooms-tasks/types.d.ts +96 -17
- package/dist/rooms-tasks/types.js +14 -4
- package/dist/runner.d.ts +12 -2
- package/dist/runner.js +124 -43
- package/dist/session/acp.d.ts +9 -1
- package/dist/session/acp.js +35 -9
- package/dist/session/arbiter.d.ts +2 -1
- package/dist/session/arbiter.js +2 -0
- package/dist/session/codex-app-server-transport.d.ts +53 -0
- package/dist/session/codex-app-server-transport.js +184 -0
- package/dist/session/codex-app-server.d.ts +122 -0
- package/dist/session/codex-app-server.js +1031 -0
- package/dist/session/control.d.ts +14 -1
- package/dist/session/control.js +38 -0
- package/dist/session/conversation-types.d.ts +4 -4
- package/dist/session/types.d.ts +24 -6
- package/dist/session/types.js +28 -0
- package/dist/spawn.d.ts +15 -31
- package/dist/spawn.js +117 -237
- package/dist/temp-lifecycle.d.ts +1 -1
- package/dist/watchdog/config.d.ts +10 -7
- package/dist/watchdog/config.js +29 -19
- package/dist/watchdog/run.js +4 -13
- package/dist/web/fleet-config-service.d.ts +1 -0
- package/dist/web/fleet-config-service.js +27 -8
- package/dist/web/runtime.js +11 -6
- package/dist/web/server.js +28 -1
- package/dist/web/topology-promote.js +8 -1
- package/dist/web-app/assets/index-C0h3ALvs.js +10 -0
- package/dist/web-app/index.html +1 -1
- package/package.json +3 -2
- package/presets/brain-catalog.json +16 -0
- package/presets/fleet/agent_templates/Critic.yaml +4 -0
- package/presets/fleet/agent_templates/Developer.yaml +4 -0
- package/presets/fleet/agent_templates/LocalCoordinator.yaml +4 -0
- package/presets/fleet/agents/FleetCoordinator.yaml +3 -0
- package/presets/fleet/brains/claude-default.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-high.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-low.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-max.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-medium.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-high.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-low.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-max.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-medium.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-high.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-low.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-max.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-medium.yaml +5 -0
- package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +5 -0
- package/presets/fleet/roles/Coordinator.yaml +30 -0
- package/presets/fleet/roles/Critic.yaml +19 -0
- package/presets/fleet/roles/Developer.yaml +19 -0
- package/presets/fleet/roles/LocalCoordinator.yaml +25 -0
- package/presets/fleet/room_templates/pair.yaml +10 -0
- package/presets/fleet/room_templates/single.yaml +7 -0
- package/presets/fleet/room_templates/team.yaml +12 -0
- package/presets/fleet.yaml +7 -0
- package/presets/manifest.json +6 -0
- package/dist/web-app/assets/index-BbE9EX6n.js +0 -10
package/dist/spawn.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { spawn as spawnChild } from 'node:child_process';
|
|
2
|
-
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { parse, stringify } from 'yaml';
|
|
5
5
|
import { agentDir, defaultConfigPath } from './paths.js';
|
|
6
6
|
import { validateIsolationConfig } from './isolation/policy.js';
|
|
7
|
-
import { loadConfig,
|
|
8
|
-
import { resolveRoleModelEnv } from './model-env.js';
|
|
7
|
+
import { loadConfig, findRole, splitRootFor, validateMonitorConfig, } from './config.js';
|
|
9
8
|
import { applyRole, up } from './ops.js';
|
|
10
9
|
import { START_STAGGER_FILE } from './runner.js';
|
|
11
10
|
import { buildProvenance, daemonIdentityProvisioner, ensureIdentity, provenanceOf, withCreationTransaction, writeProvenance, writeRoleFile, } from './creation.js';
|
|
@@ -13,86 +12,58 @@ import { VERSION } from './version.js';
|
|
|
13
12
|
import { recordGeneratedAgentSource } from './generated-agent-source.js';
|
|
14
13
|
import './harness/claude-code.js';
|
|
15
14
|
import './harness/codex.js';
|
|
16
|
-
import { getAdapter } from './harness/registry.js';
|
|
17
15
|
import { archiveTempState, makeTempSupervisorLauncher, prepareTempSupervisor, reclaimStaleTempState, } from './temp-lifecycle.js';
|
|
18
16
|
/**
|
|
19
17
|
* The provenance record written by the most recent spawn in this process, so
|
|
20
18
|
* the CLI can print the same summary it persisted rather than rebuilding it.
|
|
21
19
|
*/
|
|
22
20
|
export let lastProvenance;
|
|
23
|
-
export function
|
|
24
|
-
if (o.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
export function agentDefinitionFromSpawn(o) {
|
|
22
|
+
if (o.agentDefinition) {
|
|
23
|
+
if (o.brain !== undefined || o.role !== undefined || o.cwd !== undefined
|
|
24
|
+
|| o.coordinator !== undefined || o.approval !== undefined || o.filesystem !== undefined
|
|
25
|
+
|| o.unattended !== undefined || o.isolationFile !== undefined || o.monitorConfig !== undefined
|
|
26
|
+
|| o.loopsFile !== undefined)
|
|
27
|
+
throw new Error('canonical agentDefinition conflicts with separate Agent fields');
|
|
28
|
+
const definition = structuredClone(o.agentDefinition);
|
|
29
|
+
if (o.noLoops === true && definition.loops !== undefined)
|
|
30
|
+
throw new Error('canonical agentDefinition loops conflict with explicit no-loops policy');
|
|
31
|
+
if (o.identity)
|
|
32
|
+
definition.identity = o.identity;
|
|
33
|
+
return definition;
|
|
34
|
+
}
|
|
35
|
+
if (!o.role)
|
|
36
|
+
throw new Error('--role is required (declared ID or inline mapping)');
|
|
37
|
+
if (!o.brain)
|
|
38
|
+
throw new Error('--brain is required (declared ID or inline mapping)');
|
|
39
|
+
const permissions = o.approval || o.filesystem || o.unattended ? {
|
|
40
|
+
...(o.approval ? { approval: o.approval } : {}),
|
|
41
|
+
...(o.filesystem ? { filesystem: o.filesystem } : {}),
|
|
42
|
+
...(o.unattended ? { unattended: o.unattended } : {}),
|
|
43
|
+
} : undefined;
|
|
28
44
|
return {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
45
|
+
role: structuredClone(o.role), brain: structuredClone(o.brain),
|
|
46
|
+
...(o.identity ? { identity: o.identity } : {}),
|
|
47
|
+
...(o.cwd ? { cwd: o.cwd } : {}),
|
|
48
|
+
...(o.coordinator ? { coordinator: o.coordinator } : {}),
|
|
49
|
+
...(permissions ? { permissions } : {}),
|
|
50
|
+
...(o.isolationFile ? { isolation: readIsolationFile(o.isolationFile) } : {}),
|
|
51
|
+
...(o.monitorConfig ? { monitor: structuredClone(o.monitorConfig) } : {}),
|
|
52
|
+
...(o.loopsFile ? { loops: readLoopsFile(o.loopsFile) } : {}),
|
|
33
53
|
};
|
|
34
54
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (o.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (o.coordinator)
|
|
48
|
-
r.coordinator = o.coordinator;
|
|
49
|
-
if (o.missionFile)
|
|
50
|
-
r.mission = readMissionFile(o.missionFile);
|
|
51
|
-
else if (o.mission !== undefined)
|
|
52
|
-
r.mission = o.mission;
|
|
53
|
-
if (o.model === null)
|
|
54
|
-
r.model = null;
|
|
55
|
-
else if (o.model?.trim())
|
|
56
|
-
r.model = o.model.trim();
|
|
57
|
-
if (o.reasoningEffort?.trim())
|
|
58
|
-
r.effort = o.reasoningEffort.trim();
|
|
59
|
-
const harnessOptions = {};
|
|
60
|
-
if (o.permissionMode)
|
|
61
|
-
harnessOptions[harness === 'claude-code' ? 'permission_mode' : 'approval'] = o.permissionMode;
|
|
62
|
-
if (o.sandbox)
|
|
63
|
-
harnessOptions.sandbox = o.sandbox;
|
|
64
|
-
if (o.profile)
|
|
65
|
-
harnessOptions.profile = o.profile;
|
|
66
|
-
if (o.launcher)
|
|
67
|
-
harnessOptions.launcher = o.launcher;
|
|
68
|
-
if (o.search === true)
|
|
69
|
-
harnessOptions.search = true;
|
|
70
|
-
const codexConfig = { ...(o.codexConfig ?? {}) };
|
|
71
|
-
if (Object.keys(codexConfig).length)
|
|
72
|
-
harnessOptions.config = codexConfig;
|
|
73
|
-
if (o.addDirs?.length)
|
|
74
|
-
harnessOptions.add_dirs = o.addDirs;
|
|
75
|
-
if (o.monitor === true)
|
|
76
|
-
harnessOptions.monitor = true;
|
|
77
|
-
if (Object.keys(harnessOptions).length)
|
|
78
|
-
r.harness_options = harnessOptions;
|
|
79
|
-
if (o.approval || o.filesystem || o.unattended) {
|
|
80
|
-
r.permissions = {
|
|
81
|
-
...(o.approval ? { approval: o.approval } : {}),
|
|
82
|
-
...(o.filesystem ? { filesystem: o.filesystem } : {}),
|
|
83
|
-
...(o.unattended ? { unattended: o.unattended } : {}),
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
const profile = profileValues(o);
|
|
87
|
-
if (profile.bio)
|
|
88
|
-
r.bio = profile.bio;
|
|
89
|
-
if (profile.persona)
|
|
90
|
-
r.persona = profile.persona;
|
|
91
|
-
if (o.isolationFile)
|
|
92
|
-
r.isolation = readIsolationFile(o.isolationFile);
|
|
93
|
-
if (o.monitorConfig)
|
|
94
|
-
r.monitor = { ...o.monitorConfig };
|
|
95
|
-
return r;
|
|
55
|
+
function resolvedSpawn(o) {
|
|
56
|
+
const definition = agentDefinitionFromSpawn(o);
|
|
57
|
+
const cfg = loadConfig(o.configPath, {
|
|
58
|
+
additionalAgent: { id: o.name, definition, temporary: o.temp === true },
|
|
59
|
+
});
|
|
60
|
+
const role = findRole(cfg, o.name);
|
|
61
|
+
if (o.temp)
|
|
62
|
+
role.temporaryLoopSource = o.loopSource
|
|
63
|
+
?? (role.temporaryLoops?.length ? 'agent-template' : 'omitted');
|
|
64
|
+
if (o.noLoops === true)
|
|
65
|
+
role.temporaryLoops = [];
|
|
66
|
+
return { definition, role };
|
|
96
67
|
}
|
|
97
68
|
/**
|
|
98
69
|
* Read and validate an `--isolation-file`. The file is the existing
|
|
@@ -119,13 +90,37 @@ export function readIsolationFile(path) {
|
|
|
119
90
|
throw new Error(`--isolation-file ${path}: ${problems.join('; ')}`);
|
|
120
91
|
return cfg;
|
|
121
92
|
}
|
|
93
|
+
/** Read a private canonical temporary-loop override before any creation side effect. */
|
|
94
|
+
export function readLoopsFile(path) {
|
|
95
|
+
const stat = lstatSync(path);
|
|
96
|
+
const uid = process.getuid?.();
|
|
97
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1_000_000
|
|
98
|
+
|| (uid !== undefined && stat.uid !== uid) || (stat.mode & 0o777) !== 0o600)
|
|
99
|
+
throw new Error(`${path}: loops file must be an owner-only regular file no larger than 1 MB`);
|
|
100
|
+
let raw;
|
|
101
|
+
try {
|
|
102
|
+
raw = parse(readFileSync(path, 'utf8'));
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
throw new Error(`--loops-file ${path}: ${error.message}`);
|
|
106
|
+
}
|
|
107
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)
|
|
108
|
+
|| !Object.hasOwn(raw, 'loops')
|
|
109
|
+
|| Object.keys(raw).some(key => key !== 'loops'))
|
|
110
|
+
throw new Error(`${path}: expected exactly a top-level loops: mapping`);
|
|
111
|
+
const loops = raw.loops;
|
|
112
|
+
if (!loops || typeof loops !== 'object' || Array.isArray(loops)
|
|
113
|
+
|| !Object.keys(loops).length)
|
|
114
|
+
throw new Error(`${path}: loops must be a non-empty mapping; use --no-loops to disable loops`);
|
|
115
|
+
return structuredClone(loops);
|
|
116
|
+
}
|
|
122
117
|
export function validateSpawnOpts(o) {
|
|
123
|
-
if (o.
|
|
124
|
-
throw new Error('--
|
|
125
|
-
if (o.
|
|
126
|
-
throw new Error(
|
|
127
|
-
if (o.
|
|
128
|
-
|
|
118
|
+
if (o.loopsFile && o.noLoops === true)
|
|
119
|
+
throw new Error('--loops-file and --no-loops are mutually exclusive');
|
|
120
|
+
if ((o.loopsFile || o.noLoops === true || o.agentDefinition?.loops !== undefined) && !o.temp)
|
|
121
|
+
throw new Error('temporary Agent loops require --temp');
|
|
122
|
+
if (o.loopsFile)
|
|
123
|
+
readLoopsFile(o.loopsFile);
|
|
129
124
|
if (o.approval && !['ask', 'auto', 'allow', 'deny'].includes(o.approval))
|
|
130
125
|
throw new Error(`invalid --approval '${o.approval}'; allowed: ask, auto, allow (deprecated alias: deny)`);
|
|
131
126
|
if (o.filesystem && !['read-only', 'workspace', 'unrestricted'].includes(o.filesystem))
|
|
@@ -136,23 +131,12 @@ export function validateSpawnOpts(o) {
|
|
|
136
131
|
throw new Error(`invalid role name '${o.name}'`);
|
|
137
132
|
if (o.identity && !/^[A-Za-z0-9_-]+$/.test(o.identity))
|
|
138
133
|
throw new Error(`invalid identity name '${o.identity}'`);
|
|
139
|
-
if (o.model && (o.model.length > 128 || /[\0-\x1f\x7f]/.test(o.model)))
|
|
140
|
-
throw new Error('model must be printable and at most 128 characters');
|
|
141
134
|
if (o.monitorConfig) {
|
|
142
135
|
const problems = validateMonitorConfig(o.monitorConfig);
|
|
143
136
|
if (problems.length)
|
|
144
137
|
throw new Error(problems.join('; '));
|
|
145
138
|
}
|
|
146
139
|
}
|
|
147
|
-
/** Read mission text without trimming or newline rewriting. */
|
|
148
|
-
export function readMissionFile(path) {
|
|
149
|
-
try {
|
|
150
|
-
return readFileSync(path, 'utf8');
|
|
151
|
-
}
|
|
152
|
-
catch (e) {
|
|
153
|
-
throw new Error(`--mission-file ${path}: ${e.message}`);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
140
|
/**
|
|
157
141
|
* Reject names that are already USED. This is a precondition, not a claim: it
|
|
158
142
|
* runs INSIDE the creation transaction, after both names are reserved, so the
|
|
@@ -160,7 +144,15 @@ export function readMissionFile(path) {
|
|
|
160
144
|
* by the reservation rather than by this function.
|
|
161
145
|
*/
|
|
162
146
|
function assertNameFree(o) {
|
|
163
|
-
|
|
147
|
+
let cfg;
|
|
148
|
+
try {
|
|
149
|
+
cfg = loadConfig(o.configPath);
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
if (error instanceof Error && error.message.includes(`resolve to duplicate identity '${effectiveIdentity(o)}'`))
|
|
153
|
+
throw new Error(`ours identity '${effectiveIdentity(o)}' is already taken or being created right now`);
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
164
156
|
if (cfg.roles.some(r => r.name === o.name))
|
|
165
157
|
throw new Error(`role '${o.name}' already exists (${cfg.roles.find(r => r.name === o.name).sourceFile})`);
|
|
166
158
|
if (existsSync(agentDir(o.name)) || existsSync(agentDir(o.name, true)))
|
|
@@ -173,23 +165,6 @@ function assertNameFree(o) {
|
|
|
173
165
|
}
|
|
174
166
|
/** The ours identity a spawn will bind: explicit, else the role name. */
|
|
175
167
|
export const effectiveIdentity = (o) => o.identity ?? o.name;
|
|
176
|
-
/** Bare Agent document written by v2 spawn: inline Role × inline Brain + operations. */
|
|
177
|
-
export function buildAgentDocument(raw) {
|
|
178
|
-
const role = Object.fromEntries(['mission', 'persona', 'bio', 'briefing_file']
|
|
179
|
-
.filter(key => raw[key] !== undefined)
|
|
180
|
-
.map(key => [key, raw[key]]));
|
|
181
|
-
const brain = Object.fromEntries([
|
|
182
|
-
'harness', 'session', 'session_options', 'model', 'model_chain', 'max_tokens',
|
|
183
|
-
'autocompact_pct', 'harness_options', 'effort',
|
|
184
|
-
].filter(key => raw[key] !== undefined)
|
|
185
|
-
.map(key => [key, raw[key]]));
|
|
186
|
-
const operational = Object.fromEntries([
|
|
187
|
-
'permissions', 'identity', 'cwd', 'coordinator', 'env', 'oversee', 'isolation',
|
|
188
|
-
'monitor', 'owner_channel', 'worklog', 'auth_proxy',
|
|
189
|
-
].filter(key => raw[key] !== undefined)
|
|
190
|
-
.map(key => [key, raw[key]]));
|
|
191
|
-
return { role: { inline: role }, brain: { inline: brain }, ...operational };
|
|
192
|
-
}
|
|
193
168
|
/**
|
|
194
169
|
* Validate and resolve a spawn without reserving names, contacting the daemon,
|
|
195
170
|
* or writing state. Collision checks are necessarily a point-in-time snapshot.
|
|
@@ -198,68 +173,13 @@ export function spawnDryRun(o) {
|
|
|
198
173
|
validateSpawnOpts(o);
|
|
199
174
|
if (o.isolationFile)
|
|
200
175
|
readIsolationFile(o.isolationFile);
|
|
201
|
-
if (o.missionFile)
|
|
202
|
-
readMissionFile(o.missionFile);
|
|
203
176
|
assertNameFree(o);
|
|
204
|
-
const
|
|
205
|
-
const
|
|
206
|
-
const harnessOptions = {
|
|
207
|
-
...(cfg.defaults.harness_options ?? {}),
|
|
208
|
-
...(raw.harness_options ?? {}),
|
|
209
|
-
};
|
|
210
|
-
const harness = raw.harness ?? cfg.defaults.harness ?? 'claude-code';
|
|
211
|
-
const defaultHarness = cfg.defaults.harness ?? 'claude-code';
|
|
212
|
-
const inheritsModelDefaults = harness === defaultHarness && raw.model !== null;
|
|
213
|
-
const authProxy = resolveAuthProxy(cfg.defaults.auth_proxy, raw.auth_proxy);
|
|
214
|
-
// One resolution for the environment and the model it pins (src/model-env.ts).
|
|
215
|
-
const modelEnv = resolveRoleModelEnv({
|
|
216
|
-
harness,
|
|
217
|
-
model: resolveRoleModel(raw.model, raw.harness, cfg.defaults),
|
|
218
|
-
modelWasExplicit: raw.model !== undefined,
|
|
219
|
-
defaultsEnv: (cfg.defaults.env ?? {}),
|
|
220
|
-
roleEnv: raw.env,
|
|
221
|
-
...(authProxy ? { authProxyBaseUrl: authProxy.base_url } : {}),
|
|
222
|
-
});
|
|
223
|
-
const adapter = getAdapter(harness);
|
|
224
|
-
const brain = adapter.agentSession.resolveBrain({
|
|
225
|
-
model: modelEnv.model,
|
|
226
|
-
effort: raw.effort,
|
|
227
|
-
harnessOptions: Object.keys(harnessOptions).length ? harnessOptions : undefined,
|
|
228
|
-
});
|
|
229
|
-
const model = brain.model ?? undefined;
|
|
230
|
-
const session = raw.session ?? cfg.defaults.session ?? 'acp';
|
|
231
|
-
const resolvedRole = {
|
|
232
|
-
...raw,
|
|
233
|
-
name: o.name,
|
|
234
|
-
sourceFile: o.temp ? '(temp dry-run)' : join(splitRootFor(o.configPath ?? defaultConfigPath()), 'agents', `${o.name}.yaml`),
|
|
235
|
-
harness,
|
|
236
|
-
session,
|
|
237
|
-
session_options: raw.session_options,
|
|
238
|
-
permissions: resolvePermissions(cfg.defaults.permissions, raw.permissions),
|
|
239
|
-
permissionsDeclared: raw.permissions !== undefined || cfg.defaults.permissions !== undefined,
|
|
240
|
-
identity: effectiveIdentity(o),
|
|
241
|
-
effort: raw.effort,
|
|
242
|
-
model,
|
|
243
|
-
model_chain: resolveModelChain(model, raw.model_chain ?? (inheritsModelDefaults
|
|
244
|
-
? cfg.defaults.model_chain
|
|
245
|
-
: undefined)),
|
|
246
|
-
harness_options: brain.harnessOptions,
|
|
247
|
-
isolation: raw.isolation ?? cfg.defaults.isolation,
|
|
248
|
-
monitor: resolveMonitorConfig(cfg.defaults.monitor, raw.monitor),
|
|
249
|
-
owner_channel: resolveOwnerChannelConfig(cfg.defaults.owner_channel, raw.owner_channel, session),
|
|
250
|
-
worklog: resolveWorklogPolicy(cfg.defaults.worklog, raw.worklog),
|
|
251
|
-
auth_proxy: authProxy,
|
|
252
|
-
};
|
|
253
|
-
resolvedRole.env = modelEnv.env;
|
|
254
|
-
if (resolvedRole.auth_proxy && resolvedRole.harness !== 'claude-code')
|
|
255
|
-
throw new Error('auth_proxy is supported only by claude-code');
|
|
256
|
-
const optionProblems = adapter.validateOptions(resolvedRole.harness_options, resolvedRole);
|
|
257
|
-
if (optionProblems.length)
|
|
258
|
-
throw new Error(optionProblems.map(problem => `${problem.path}: ${problem.message}`).join('; '));
|
|
177
|
+
const resolved = resolvedSpawn(o);
|
|
178
|
+
const resolvedRole = resolved.role;
|
|
259
179
|
return {
|
|
260
180
|
schemaVersion: 1,
|
|
261
181
|
warning: 'collision checks are a snapshot; a real spawn reserves names atomically',
|
|
262
|
-
roleDocument:
|
|
182
|
+
roleDocument: structuredClone(resolved.definition),
|
|
263
183
|
resolvedRole,
|
|
264
184
|
};
|
|
265
185
|
}
|
|
@@ -271,26 +191,19 @@ export function spawnDryRun(o) {
|
|
|
271
191
|
* `env`, `bio`, `persona` and `harness_options` are deliberately absent: the
|
|
272
192
|
* record exists to be read, and must not become a place credentials collect.
|
|
273
193
|
*/
|
|
274
|
-
function provenanceSettings(o, defaults) {
|
|
194
|
+
function provenanceSettings(o, defaults, resolvedLoops = []) {
|
|
275
195
|
const perms = (defaults.permissions ?? {});
|
|
276
196
|
const callerDefaults = new Set(o.inheritedFromCaller ?? []);
|
|
277
197
|
const tagged = (key, entry) => callerDefaults.has(key) ? { ...entry, source: 'caller-role' } : entry;
|
|
278
|
-
const
|
|
279
|
-
const inheritedModel = resolveRoleModel(undefined, o.harness, defaults);
|
|
198
|
+
const selection = (value) => value && 'ref' in value ? `ref:${value.ref}` : value ? 'inline' : undefined;
|
|
280
199
|
return {
|
|
281
|
-
|
|
282
|
-
|
|
200
|
+
brain: tagged('brain', provenanceOf(selection(o.brain), undefined, undefined)),
|
|
201
|
+
role: tagged('role', provenanceOf(selection(o.role), undefined, undefined)),
|
|
283
202
|
identity: o.identity
|
|
284
203
|
? { value: o.identity, source: 'cli' }
|
|
285
204
|
: { value: o.name, source: 'built-in' }, // defaults to the role name
|
|
286
205
|
cwd: tagged('cwd', provenanceOf(o.cwd, undefined, undefined)),
|
|
287
|
-
model: tagged('model', o.model === null
|
|
288
|
-
? { value: undefined, source: 'cli' }
|
|
289
|
-
: explicitModel
|
|
290
|
-
? { value: explicitModel, source: 'cli' }
|
|
291
|
-
: { value: inheritedModel, source: inheritedModel ? 'fleet-default' : 'built-in' }),
|
|
292
206
|
coordinator: tagged('coordinator', provenanceOf(o.coordinator, undefined, undefined)),
|
|
293
|
-
permission_mode: provenanceOf(o.permissionMode, undefined, undefined),
|
|
294
207
|
approval: tagged('approval', provenanceOf(o.approval, perms.approval, 'ask')),
|
|
295
208
|
filesystem: tagged('filesystem', provenanceOf(o.filesystem, perms.filesystem, 'workspace')),
|
|
296
209
|
unattended: tagged('unattended', provenanceOf(o.unattended, perms.unattended, 'deny')),
|
|
@@ -298,6 +211,16 @@ function provenanceSettings(o, defaults) {
|
|
|
298
211
|
? { value: 'declared via --isolation-file', source: 'cli' }
|
|
299
212
|
: { value: defaults.isolation ? 'from fleet defaults' : undefined, source: defaults.isolation ? 'fleet-default' : 'built-in' },
|
|
300
213
|
monitor: tagged('monitorConfig', provenanceOf(o.monitorConfig, defaults.monitor, { mode: 'fleet' })),
|
|
214
|
+
loops: {
|
|
215
|
+
value: o.noLoops ? { enabled: false, loops: [] }
|
|
216
|
+
: resolvedLoops.length ? resolvedLoops.map(loop => ({
|
|
217
|
+
name: loop.name, enabled: loop.enabled, intervalMs: loop.intervalMs,
|
|
218
|
+
initialDelayMs: loop.initialDelayMs, jitterMs: loop.jitterMs,
|
|
219
|
+
prompt: { bytes: loop.promptBytes, sha256: loop.promptHash },
|
|
220
|
+
})) : 'omitted (legacy no temporary loops)',
|
|
221
|
+
source: o.loopSource === 'agent-template' ? 'agent-template'
|
|
222
|
+
: o.loopsFile || o.noLoops || o.loopSource === 'cli' ? 'cli' : 'built-in',
|
|
223
|
+
},
|
|
301
224
|
};
|
|
302
225
|
}
|
|
303
226
|
/** Permanent spawn: persist one bare Agent document under the selected v2 root. */
|
|
@@ -305,8 +228,10 @@ export async function spawnPermanent(o, deps, creation = {}) {
|
|
|
305
228
|
validateSpawnOpts(o);
|
|
306
229
|
if (o.isolationFile)
|
|
307
230
|
readIsolationFile(o.isolationFile); // fail before reserving
|
|
308
|
-
|
|
309
|
-
|
|
231
|
+
// Preserve the detailed owner/source diagnostic for an existing identity.
|
|
232
|
+
// The check is repeated inside the reservation boundary to close races.
|
|
233
|
+
assertNameFree(o);
|
|
234
|
+
const prepared = resolvedSpawn(o); // canonical validation before mutation
|
|
310
235
|
// Reserve name and identity together before anything is written or started.
|
|
311
236
|
// A loser of the race creates no config, state, or service.
|
|
312
237
|
creation.onStage?.('reserving');
|
|
@@ -316,7 +241,7 @@ export async function spawnPermanent(o, deps, creation = {}) {
|
|
|
316
241
|
// Establish the identity BEFORE the service is enabled, and record
|
|
317
242
|
// what was actually guaranteed so the briefing can say something true.
|
|
318
243
|
creation.onStage?.('checking_identity');
|
|
319
|
-
const guarantee = await ensureIdentity(effectiveIdentity(o),
|
|
244
|
+
const guarantee = await ensureIdentity(effectiveIdentity(o), { bio: prepared.role.bio, persona: prepared.role.persona }, creation.identityProvisioner ?? deps.identityProvisioner ?? daemonIdentityProvisioner(), deps.log);
|
|
320
245
|
creation.onStage?.('checking_identity', {
|
|
321
246
|
result: guarantee.evidence, guarantee: guarantee.state,
|
|
322
247
|
});
|
|
@@ -333,7 +258,7 @@ export async function spawnPermanent(o, deps, creation = {}) {
|
|
|
333
258
|
mkdirSync(agentRoot, { recursive: true });
|
|
334
259
|
creation.onStage?.('writing_role');
|
|
335
260
|
const file = join(agentRoot, `${o.name}.yaml`);
|
|
336
|
-
writeRoleFile(tx, file, stringify(
|
|
261
|
+
writeRoleFile(tx, file, stringify(agentDefinitionFromSpawn(o)));
|
|
337
262
|
// `up` materialises the state dir and registers the service. Journal the
|
|
338
263
|
// dir before it exists so a failure leaves the name genuinely reusable
|
|
339
264
|
// rather than blocked by a half-built directory.
|
|
@@ -359,7 +284,7 @@ export async function spawnPermanent(o, deps, creation = {}) {
|
|
|
359
284
|
// launch still records how it was asked for.
|
|
360
285
|
const provenance = buildProvenance({
|
|
361
286
|
role: o.name, lifetime: 'permanent', fleetVersion: VERSION,
|
|
362
|
-
settings: provenanceSettings(o, cfg.defaults),
|
|
287
|
+
settings: provenanceSettings(o, cfg.defaults, prepared.role.temporaryLoops),
|
|
363
288
|
surface: o.surface, creationActionId: o.creationActionId, callerRole: o.callerRole,
|
|
364
289
|
});
|
|
365
290
|
mkdirSync(agentDir(o.name), { recursive: true });
|
|
@@ -402,8 +327,7 @@ export async function spawnTemp(o, binPath, launch = independentSupervisor, crea
|
|
|
402
327
|
validateSpawnOpts(o);
|
|
403
328
|
if (o.isolationFile)
|
|
404
329
|
readIsolationFile(o.isolationFile); // fail before reserving
|
|
405
|
-
|
|
406
|
-
readMissionFile(o.missionFile); // fail before reserving
|
|
330
|
+
const prepared = resolvedSpawn(o); // canonical validation before mutation
|
|
407
331
|
// Retire only supervisors whose recorded owner is definitively stopped. This
|
|
408
332
|
// bounded pass keeps the active roster clean without deleting old evidence.
|
|
409
333
|
await reclaimStaleTempState();
|
|
@@ -416,68 +340,24 @@ export async function spawnTemp(o, binPath, launch = independentSupervisor, crea
|
|
|
416
340
|
creation.onStage?.('checking_identity', {
|
|
417
341
|
result: 'unknown', guarantee: 'unverified',
|
|
418
342
|
});
|
|
419
|
-
return spawnTempInner(o, binPath, launch, tx, creation.onStage);
|
|
343
|
+
return spawnTempInner(o, prepared.role, binPath, launch, tx, creation.onStage);
|
|
420
344
|
}, creation);
|
|
421
345
|
}
|
|
422
|
-
async function spawnTempInner(o, binPath, launch, tx, onStage) {
|
|
346
|
+
async function spawnTempInner(o, preparedRole, binPath, launch, tx, onStage) {
|
|
423
347
|
const cfg = loadConfig(o.configPath);
|
|
424
|
-
const
|
|
425
|
-
const fromOpts = buildRoleConfig(o, defaultHarness);
|
|
426
|
-
const mergedHarnessOptions = {
|
|
427
|
-
...(cfg.defaults.harness_options ?? {}),
|
|
428
|
-
...(fromOpts.harness_options ?? {}),
|
|
429
|
-
};
|
|
430
|
-
const harness = o.harness ?? defaultHarness ?? 'claude-code';
|
|
431
|
-
const inheritsModelDefaults = harness === (defaultHarness ?? 'claude-code') && o.model !== null;
|
|
432
|
-
const tempAuthProxy = resolveAuthProxy(cfg.defaults.auth_proxy, fromOpts.auth_proxy);
|
|
433
|
-
// An explicitly requested --model must reach the child, not just the banner
|
|
434
|
-
// (src/model-env.ts).
|
|
435
|
-
const modelEnv = resolveRoleModelEnv({
|
|
436
|
-
harness,
|
|
437
|
-
model: resolveRoleModel(o.model, o.harness, cfg.defaults),
|
|
438
|
-
modelWasExplicit: o.model !== undefined,
|
|
439
|
-
defaultsEnv: (cfg.defaults.env ?? {}),
|
|
440
|
-
roleEnv: fromOpts.env,
|
|
441
|
-
...(tempAuthProxy ? { authProxyBaseUrl: tempAuthProxy.base_url } : {}),
|
|
442
|
-
});
|
|
443
|
-
const adapter = getAdapter(harness);
|
|
444
|
-
const brain = adapter.agentSession.resolveBrain({
|
|
445
|
-
model: modelEnv.model,
|
|
446
|
-
effort: fromOpts.effort,
|
|
447
|
-
harnessOptions: Object.keys(mergedHarnessOptions).length ? mergedHarnessOptions : undefined,
|
|
448
|
-
});
|
|
449
|
-
const model = brain.model ?? undefined;
|
|
450
|
-
const session = o.session ?? cfg.defaults.session ?? 'acp';
|
|
348
|
+
const { temporaryLoops, ...launchRole } = preparedRole;
|
|
451
349
|
const role = {
|
|
452
|
-
...
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
identity: o.identity ?? o.name,
|
|
457
|
-
effort: fromOpts.effort,
|
|
458
|
-
model,
|
|
459
|
-
model_chain: resolveModelChain(model, fromOpts.model_chain ?? (inheritsModelDefaults
|
|
460
|
-
? cfg.defaults.model_chain
|
|
461
|
-
: undefined)),
|
|
462
|
-
harness_options: brain.harnessOptions,
|
|
463
|
-
permissions: resolvePermissions(cfg.defaults.permissions, fromOpts.permissions),
|
|
464
|
-
permissionsDeclared: fromOpts.permissions !== undefined || cfg.defaults.permissions !== undefined,
|
|
465
|
-
// Temp agents inherit the fleet-wide monitor defaults via the snapshot.
|
|
466
|
-
monitor: resolveMonitorConfig(cfg.defaults.monitor, fromOpts.monitor),
|
|
467
|
-
owner_channel: resolveOwnerChannelConfig(cfg.defaults.owner_channel, fromOpts.owner_channel, session),
|
|
468
|
-
worklog: resolveWorklogPolicy(cfg.defaults.worklog, fromOpts.worklog),
|
|
469
|
-
auth_proxy: tempAuthProxy,
|
|
470
|
-
sourceFile: '(temp)',
|
|
350
|
+
...launchRole, sourceFile: '(temp)',
|
|
351
|
+
...(o.noLoops === true ? { loops: [] }
|
|
352
|
+
: temporaryLoops?.length ? { loops: temporaryLoops } : { loops: undefined }),
|
|
353
|
+
temporaryLoopSource: o.loopSource ?? (temporaryLoops?.length ? 'agent-template' : 'omitted'),
|
|
471
354
|
roomMemberStartup: o.roomMemberStartup,
|
|
472
355
|
};
|
|
473
|
-
role.env = modelEnv.env;
|
|
474
|
-
if (role.auth_proxy && role.harness !== 'claude-code')
|
|
475
|
-
throw new Error('auth_proxy is supported only by claude-code');
|
|
476
356
|
onStage?.('writing_role');
|
|
477
357
|
const dir = applyRole(role, { temp: true, identityGuarantee: 'unverified' });
|
|
478
358
|
const provenance = buildProvenance({
|
|
479
359
|
role: o.name, lifetime: 'temporary', fleetVersion: VERSION,
|
|
480
|
-
settings: provenanceSettings(o, cfg.defaults),
|
|
360
|
+
settings: provenanceSettings(o, cfg.defaults, preparedRole.temporaryLoops),
|
|
481
361
|
surface: o.surface, creationActionId: o.creationActionId, callerRole: o.callerRole,
|
|
482
362
|
});
|
|
483
363
|
writeProvenance(dir, provenance);
|
package/dist/temp-lifecycle.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export declare const TEMP_STOP_REQUEST_FILE = ".temp-stop-request.json";
|
|
|
5
5
|
export declare const TEMP_RECLAIM_BATCH = 32;
|
|
6
6
|
export declare const TEMP_LAUNCH_GRACE_MS = 60000;
|
|
7
7
|
export type TempSupervisorKind = 'systemd-transient' | 'launchd-transient' | 'detached';
|
|
8
|
-
export type TempTerminationReason = 'identity-closed' | 'session-ended' | 'operator-stop' | 'supervisor-signal' | 'startup-failure' | 'stale-supervisor';
|
|
8
|
+
export type TempTerminationReason = 'identity-closed' | 'session-ended' | 'operator-stop' | 'supervisor-signal' | 'supervisor-recycle' | 'startup-failure' | 'stale-supervisor';
|
|
9
9
|
export interface TempSupervisorRecord {
|
|
10
10
|
version: 1;
|
|
11
11
|
role: string;
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import type { FleetConfig, ResolvedRole
|
|
1
|
+
import type { AgentDefinition, FleetConfig, ResolvedRole } from '../config.js';
|
|
2
|
+
import type { SessionBackendId } from '../config.js';
|
|
2
3
|
import type { IsolationConfig } from '../isolation/types.js';
|
|
3
4
|
export interface WatchdogConfig {
|
|
4
5
|
coordinator?: string;
|
|
5
6
|
enabled?: boolean;
|
|
6
7
|
interval?: string;
|
|
7
8
|
watch?: string[];
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
agent?: {
|
|
10
|
+
ref: string;
|
|
11
|
+
} | AgentDefinition;
|
|
11
12
|
identity?: string;
|
|
12
13
|
timeout?: string;
|
|
13
14
|
keep_reports?: number;
|
|
14
15
|
alert_cooldown?: string;
|
|
15
16
|
prompt_file?: string;
|
|
16
|
-
isolation?: IsolationConfig;
|
|
17
17
|
}
|
|
18
18
|
export interface ResolvedWatchdog {
|
|
19
19
|
name: string;
|
|
@@ -22,15 +22,18 @@ export interface ResolvedWatchdog {
|
|
|
22
22
|
intervalMs: number;
|
|
23
23
|
watch: string[];
|
|
24
24
|
watchExplicit: boolean;
|
|
25
|
+
agentDefinition: AgentDefinition;
|
|
26
|
+
resolvedAgent: ResolvedRole;
|
|
27
|
+
/** Resolved/read-only runtime facts; never independent authoring fields. */
|
|
25
28
|
harness: string;
|
|
26
29
|
session: SessionBackendId;
|
|
27
30
|
model?: string;
|
|
31
|
+
isolation?: IsolationConfig;
|
|
28
32
|
identity: string;
|
|
29
33
|
timeoutMs: number;
|
|
30
34
|
keepReports: number;
|
|
31
35
|
alertCooldownMs: number;
|
|
32
36
|
promptFile?: string;
|
|
33
|
-
isolation?: IsolationConfig;
|
|
34
37
|
sourceFile: string;
|
|
35
38
|
}
|
|
36
39
|
export declare const WATCHDOG_DEFAULT_INTERVAL_MS = 600000;
|
|
@@ -38,7 +41,7 @@ export declare const WATCHDOG_MIN_INTERVAL_MS = 60000;
|
|
|
38
41
|
export declare const WATCHDOG_DEFAULT_TIMEOUT_MS = 300000;
|
|
39
42
|
export declare const WATCHDOG_DEFAULT_KEEP_REPORTS = 50;
|
|
40
43
|
export declare const WATCHDOG_DEFAULT_COOLDOWN_MS = 3600000;
|
|
41
|
-
export declare function resolveWatchdogs(baseDoc: Record<string, unknown>, baseFile: string, roles: ResolvedRole[], vars: Record<string, string>,
|
|
44
|
+
export declare function resolveWatchdogs(baseDoc: Record<string, unknown>, baseFile: string, roles: ResolvedRole[], vars: Record<string, string>, agentDefinitions: Record<string, AgentDefinition>, resolveInline: (name: string, definition: AgentDefinition) => ResolvedRole): ResolvedWatchdog[];
|
|
42
45
|
/**
|
|
43
46
|
* Split `restart`'s argument names into watchdogs (a release, handled
|
|
44
47
|
* directly) and roles (handed to restartRoles). Watchdog names can't collide
|
package/dist/watchdog/config.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { isAbsolute } from 'node:path';
|
|
3
3
|
import { parseDuration } from '../duration.js';
|
|
4
|
-
import { ConfigError, ROLE_NAME_RE
|
|
5
|
-
import { validateIsolationConfig } from '../isolation/policy.js';
|
|
4
|
+
import { ConfigError, ROLE_NAME_RE } from '../config.js';
|
|
6
5
|
const WATCHDOG_KEYS = [
|
|
7
|
-
'coordinator', 'enabled', 'interval', 'watch', '
|
|
8
|
-
'identity', 'timeout', 'keep_reports', 'alert_cooldown', 'prompt_file',
|
|
6
|
+
'coordinator', 'enabled', 'interval', 'watch', 'agent',
|
|
7
|
+
'identity', 'timeout', 'keep_reports', 'alert_cooldown', 'prompt_file',
|
|
9
8
|
];
|
|
10
9
|
export const WATCHDOG_DEFAULT_INTERVAL_MS = 600_000;
|
|
11
10
|
export const WATCHDOG_MIN_INTERVAL_MS = 60_000;
|
|
@@ -21,7 +20,7 @@ function deepSub(v, vars) {
|
|
|
21
20
|
return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, deepSub(x, vars)]));
|
|
22
21
|
return v;
|
|
23
22
|
}
|
|
24
|
-
export function resolveWatchdogs(baseDoc, baseFile, roles, vars,
|
|
23
|
+
export function resolveWatchdogs(baseDoc, baseFile, roles, vars, agentDefinitions, resolveInline) {
|
|
25
24
|
const block = baseDoc.watchdogs;
|
|
26
25
|
if (block === undefined || block === null)
|
|
27
26
|
return [];
|
|
@@ -42,6 +41,9 @@ export function resolveWatchdogs(baseDoc, baseFile, roles, vars, defaults) {
|
|
|
42
41
|
if (roleNames.has(name))
|
|
43
42
|
throw new ConfigError(`${baseFile}: watchdog '${name}' collides with a role name`);
|
|
44
43
|
const w = deepSub(rawEntry ?? {}, vars);
|
|
44
|
+
const legacy = ['harness', 'model', 'session', 'isolation'].filter(key => Object.hasOwn(w, key));
|
|
45
|
+
if (legacy.length)
|
|
46
|
+
throw new ConfigError(`${where}: E_LEGACY Brain/Agent-owned field(s) ${legacy.join(', ')} are unsupported; select a canonical Agent with agent`);
|
|
45
47
|
const bad = Object.keys(w).filter(k => !WATCHDOG_KEYS.includes(k));
|
|
46
48
|
if (bad.length)
|
|
47
49
|
throw new ConfigError(`${where} has unknown key(s) ${bad.join(', ')}; allowed: ${WATCHDOG_KEYS.join(', ')}`);
|
|
@@ -60,23 +62,29 @@ export function resolveWatchdogs(baseDoc, baseFile, roles, vars, defaults) {
|
|
|
60
62
|
if (collidingWatchdog !== undefined)
|
|
61
63
|
throw new ConfigError(`${where}: identity '${identity}' collides with watchdog '${collidingWatchdog}'`);
|
|
62
64
|
watchdogIdentities.set(identity, name);
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (
|
|
68
|
-
|
|
65
|
+
if (!w.agent || typeof w.agent !== 'object' || Array.isArray(w.agent))
|
|
66
|
+
throw new ConfigError(`${where}: agent is required (Agent ref or canonical Agent definition)`);
|
|
67
|
+
let agentDefinition;
|
|
68
|
+
let resolvedAgent;
|
|
69
|
+
if ('ref' in w.agent) {
|
|
70
|
+
const ref = w.agent.ref;
|
|
71
|
+
if (typeof ref !== 'string' || !ref)
|
|
72
|
+
throw new ConfigError(`${where}: agent.ref must be a declared Agent ID`);
|
|
73
|
+
agentDefinition = agentDefinitions[ref];
|
|
74
|
+
resolvedAgent = roles.find(role => role.name === ref);
|
|
75
|
+
if (!agentDefinition || !resolvedAgent)
|
|
76
|
+
throw new ConfigError(`${where}: Agent ref '${ref}' was not found`);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
agentDefinition = structuredClone(w.agent);
|
|
80
|
+
resolvedAgent = resolveInline(name, agentDefinition);
|
|
81
|
+
}
|
|
69
82
|
if (w.prompt_file !== undefined) {
|
|
70
83
|
if (typeof w.prompt_file !== 'string' || !isAbsolute(w.prompt_file))
|
|
71
84
|
throw new ConfigError(`${where}: prompt_file must be an absolute path`);
|
|
72
85
|
if (!existsSync(w.prompt_file))
|
|
73
86
|
throw new ConfigError(`${where}: prompt_file not found: ${w.prompt_file}`);
|
|
74
87
|
}
|
|
75
|
-
if (w.isolation !== undefined) {
|
|
76
|
-
const problems = validateIsolationConfig(w.isolation);
|
|
77
|
-
if (problems.length)
|
|
78
|
-
throw new ConfigError(`${where} ${problems.join('; ')}`);
|
|
79
|
-
}
|
|
80
88
|
const dur = (v, key, fallback, minMs) => {
|
|
81
89
|
if (v === undefined)
|
|
82
90
|
return fallback;
|
|
@@ -96,13 +104,15 @@ export function resolveWatchdogs(baseDoc, baseFile, roles, vars, defaults) {
|
|
|
96
104
|
name, coordinator: w.coordinator.trim(),
|
|
97
105
|
enabled: w.enabled ?? true,
|
|
98
106
|
intervalMs: dur(w.interval, 'interval', WATCHDOG_DEFAULT_INTERVAL_MS, WATCHDOG_MIN_INTERVAL_MS),
|
|
99
|
-
watch, watchExplicit: w.watch !== undefined,
|
|
100
|
-
|
|
107
|
+
watch, watchExplicit: w.watch !== undefined,
|
|
108
|
+
agentDefinition: structuredClone(agentDefinition), resolvedAgent: structuredClone(resolvedAgent),
|
|
109
|
+
harness: resolvedAgent.harness, session: resolvedAgent.session,
|
|
110
|
+
model: resolvedAgent.model, isolation: resolvedAgent.isolation,
|
|
101
111
|
identity,
|
|
102
112
|
timeoutMs: dur(w.timeout, 'timeout', WATCHDOG_DEFAULT_TIMEOUT_MS),
|
|
103
113
|
keepReports,
|
|
104
114
|
alertCooldownMs: dur(w.alert_cooldown, 'alert_cooldown', WATCHDOG_DEFAULT_COOLDOWN_MS),
|
|
105
|
-
promptFile: w.prompt_file,
|
|
115
|
+
promptFile: w.prompt_file, sourceFile: baseFile,
|
|
106
116
|
});
|
|
107
117
|
}
|
|
108
118
|
return out;
|