@ours.network/fleet 1.1.0-nightly.2 → 1.1.0-nightly.3
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 +14 -22
- package/dist/application/role-creation-service.d.ts +2 -21
- package/dist/application/role-creation-service.js +15 -71
- package/dist/application/task-room-service.js +7 -2
- package/dist/briefing.js +3 -3
- package/dist/build-info.json +4 -4
- package/dist/cli.js +40 -33
- package/dist/config.d.ts +37 -0
- package/dist/config.js +40 -12
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +19 -30
- package/dist/doctor.js +3 -3
- package/dist/fleet-proxy.js +30 -8
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/model-env.d.ts +2 -2
- package/dist/model-env.js +1 -1
- package/dist/owner-channel/commands.js +2 -1
- package/dist/rooms-tasks/cli.js +2 -1
- package/dist/rooms-tasks/config.js +24 -9
- package/dist/rooms-tasks/provision.js +71 -36
- package/dist/rooms-tasks/templates.js +6 -6
- package/dist/rooms-tasks/types.d.ts +8 -15
- package/dist/rooms-tasks/types.js +1 -4
- package/dist/spawn.d.ts +6 -31
- package/dist/spawn.js +45 -233
- 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/topology-promote.js +8 -1
- package/dist/web-app/assets/index-BbTc5KtZ.js +10 -0
- package/dist/web-app/index.html +1 -1
- package/package.json +1 -1
- package/dist/web-app/assets/index-BbE9EX6n.js +0 -10
package/dist/spawn.js
CHANGED
|
@@ -4,8 +4,7 @@ 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,46 @@ 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
|
+
throw new Error('canonical agentDefinition conflicts with separate Agent fields');
|
|
27
|
+
const definition = structuredClone(o.agentDefinition);
|
|
28
|
+
if (o.identity)
|
|
29
|
+
definition.identity = o.identity;
|
|
30
|
+
return definition;
|
|
31
|
+
}
|
|
32
|
+
if (!o.role)
|
|
33
|
+
throw new Error('--role is required (declared ID or inline mapping)');
|
|
34
|
+
if (!o.brain)
|
|
35
|
+
throw new Error('--brain is required (declared ID or inline mapping)');
|
|
36
|
+
const permissions = o.approval || o.filesystem || o.unattended ? {
|
|
37
|
+
...(o.approval ? { approval: o.approval } : {}),
|
|
38
|
+
...(o.filesystem ? { filesystem: o.filesystem } : {}),
|
|
39
|
+
...(o.unattended ? { unattended: o.unattended } : {}),
|
|
40
|
+
} : undefined;
|
|
28
41
|
return {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
42
|
+
role: structuredClone(o.role), brain: structuredClone(o.brain),
|
|
43
|
+
...(o.identity ? { identity: o.identity } : {}),
|
|
44
|
+
...(o.cwd ? { cwd: o.cwd } : {}),
|
|
45
|
+
...(o.coordinator ? { coordinator: o.coordinator } : {}),
|
|
46
|
+
...(permissions ? { permissions } : {}),
|
|
47
|
+
...(o.isolationFile ? { isolation: readIsolationFile(o.isolationFile) } : {}),
|
|
48
|
+
...(o.monitorConfig ? { monitor: structuredClone(o.monitorConfig) } : {}),
|
|
33
49
|
};
|
|
34
50
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
if (harness)
|
|
40
|
-
r.harness = harness;
|
|
41
|
-
if (o.session)
|
|
42
|
-
r.session = o.session;
|
|
43
|
-
if (o.identity)
|
|
44
|
-
r.identity = o.identity;
|
|
45
|
-
if (o.cwd)
|
|
46
|
-
r.cwd = o.cwd;
|
|
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;
|
|
51
|
+
function resolvedSpawn(o) {
|
|
52
|
+
const definition = agentDefinitionFromSpawn(o);
|
|
53
|
+
const cfg = loadConfig(o.configPath, { additionalAgent: { id: o.name, definition } });
|
|
54
|
+
return { definition, role: findRole(cfg, o.name) };
|
|
96
55
|
}
|
|
97
56
|
/**
|
|
98
57
|
* Read and validate an `--isolation-file`. The file is the existing
|
|
@@ -120,12 +79,6 @@ export function readIsolationFile(path) {
|
|
|
120
79
|
return cfg;
|
|
121
80
|
}
|
|
122
81
|
export function validateSpawnOpts(o) {
|
|
123
|
-
if (o.mission !== undefined && o.missionFile)
|
|
124
|
-
throw new Error('--mission and --mission-file are mutually exclusive');
|
|
125
|
-
if (o.session === 'tmux')
|
|
126
|
-
throw new Error("invalid --session 'tmux'; tmux is no longer supported; use --session acp");
|
|
127
|
-
if (o.session && o.session !== 'acp')
|
|
128
|
-
throw new Error(`invalid --session '${o.session}'; allowed: acp`);
|
|
129
82
|
if (o.approval && !['ask', 'auto', 'allow', 'deny'].includes(o.approval))
|
|
130
83
|
throw new Error(`invalid --approval '${o.approval}'; allowed: ask, auto, allow (deprecated alias: deny)`);
|
|
131
84
|
if (o.filesystem && !['read-only', 'workspace', 'unrestricted'].includes(o.filesystem))
|
|
@@ -136,23 +89,12 @@ export function validateSpawnOpts(o) {
|
|
|
136
89
|
throw new Error(`invalid role name '${o.name}'`);
|
|
137
90
|
if (o.identity && !/^[A-Za-z0-9_-]+$/.test(o.identity))
|
|
138
91
|
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
92
|
if (o.monitorConfig) {
|
|
142
93
|
const problems = validateMonitorConfig(o.monitorConfig);
|
|
143
94
|
if (problems.length)
|
|
144
95
|
throw new Error(problems.join('; '));
|
|
145
96
|
}
|
|
146
97
|
}
|
|
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
98
|
/**
|
|
157
99
|
* Reject names that are already USED. This is a precondition, not a claim: it
|
|
158
100
|
* runs INSIDE the creation transaction, after both names are reserved, so the
|
|
@@ -173,23 +115,6 @@ function assertNameFree(o) {
|
|
|
173
115
|
}
|
|
174
116
|
/** The ours identity a spawn will bind: explicit, else the role name. */
|
|
175
117
|
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
118
|
/**
|
|
194
119
|
* Validate and resolve a spawn without reserving names, contacting the daemon,
|
|
195
120
|
* or writing state. Collision checks are necessarily a point-in-time snapshot.
|
|
@@ -198,68 +123,13 @@ export function spawnDryRun(o) {
|
|
|
198
123
|
validateSpawnOpts(o);
|
|
199
124
|
if (o.isolationFile)
|
|
200
125
|
readIsolationFile(o.isolationFile);
|
|
201
|
-
if (o.missionFile)
|
|
202
|
-
readMissionFile(o.missionFile);
|
|
203
126
|
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('; '));
|
|
127
|
+
const resolved = resolvedSpawn(o);
|
|
128
|
+
const resolvedRole = resolved.role;
|
|
259
129
|
return {
|
|
260
130
|
schemaVersion: 1,
|
|
261
131
|
warning: 'collision checks are a snapshot; a real spawn reserves names atomically',
|
|
262
|
-
roleDocument:
|
|
132
|
+
roleDocument: structuredClone(resolved.definition),
|
|
263
133
|
resolvedRole,
|
|
264
134
|
};
|
|
265
135
|
}
|
|
@@ -275,22 +145,15 @@ function provenanceSettings(o, defaults) {
|
|
|
275
145
|
const perms = (defaults.permissions ?? {});
|
|
276
146
|
const callerDefaults = new Set(o.inheritedFromCaller ?? []);
|
|
277
147
|
const tagged = (key, entry) => callerDefaults.has(key) ? { ...entry, source: 'caller-role' } : entry;
|
|
278
|
-
const
|
|
279
|
-
const inheritedModel = resolveRoleModel(undefined, o.harness, defaults);
|
|
148
|
+
const selection = (value) => value && 'ref' in value ? `ref:${value.ref}` : value ? 'inline' : undefined;
|
|
280
149
|
return {
|
|
281
|
-
|
|
282
|
-
|
|
150
|
+
brain: tagged('brain', provenanceOf(selection(o.brain), undefined, undefined)),
|
|
151
|
+
role: tagged('role', provenanceOf(selection(o.role), undefined, undefined)),
|
|
283
152
|
identity: o.identity
|
|
284
153
|
? { value: o.identity, source: 'cli' }
|
|
285
154
|
: { value: o.name, source: 'built-in' }, // defaults to the role name
|
|
286
155
|
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
156
|
coordinator: tagged('coordinator', provenanceOf(o.coordinator, undefined, undefined)),
|
|
293
|
-
permission_mode: provenanceOf(o.permissionMode, undefined, undefined),
|
|
294
157
|
approval: tagged('approval', provenanceOf(o.approval, perms.approval, 'ask')),
|
|
295
158
|
filesystem: tagged('filesystem', provenanceOf(o.filesystem, perms.filesystem, 'workspace')),
|
|
296
159
|
unattended: tagged('unattended', provenanceOf(o.unattended, perms.unattended, 'deny')),
|
|
@@ -305,8 +168,7 @@ export async function spawnPermanent(o, deps, creation = {}) {
|
|
|
305
168
|
validateSpawnOpts(o);
|
|
306
169
|
if (o.isolationFile)
|
|
307
170
|
readIsolationFile(o.isolationFile); // fail before reserving
|
|
308
|
-
|
|
309
|
-
readMissionFile(o.missionFile); // fail before reserving
|
|
171
|
+
const prepared = resolvedSpawn(o); // canonical validation before mutation
|
|
310
172
|
// Reserve name and identity together before anything is written or started.
|
|
311
173
|
// A loser of the race creates no config, state, or service.
|
|
312
174
|
creation.onStage?.('reserving');
|
|
@@ -316,7 +178,7 @@ export async function spawnPermanent(o, deps, creation = {}) {
|
|
|
316
178
|
// Establish the identity BEFORE the service is enabled, and record
|
|
317
179
|
// what was actually guaranteed so the briefing can say something true.
|
|
318
180
|
creation.onStage?.('checking_identity');
|
|
319
|
-
const guarantee = await ensureIdentity(effectiveIdentity(o),
|
|
181
|
+
const guarantee = await ensureIdentity(effectiveIdentity(o), { bio: prepared.role.bio, persona: prepared.role.persona }, creation.identityProvisioner ?? deps.identityProvisioner ?? daemonIdentityProvisioner(), deps.log);
|
|
320
182
|
creation.onStage?.('checking_identity', {
|
|
321
183
|
result: guarantee.evidence, guarantee: guarantee.state,
|
|
322
184
|
});
|
|
@@ -333,7 +195,7 @@ export async function spawnPermanent(o, deps, creation = {}) {
|
|
|
333
195
|
mkdirSync(agentRoot, { recursive: true });
|
|
334
196
|
creation.onStage?.('writing_role');
|
|
335
197
|
const file = join(agentRoot, `${o.name}.yaml`);
|
|
336
|
-
writeRoleFile(tx, file, stringify(
|
|
198
|
+
writeRoleFile(tx, file, stringify(agentDefinitionFromSpawn(o)));
|
|
337
199
|
// `up` materialises the state dir and registers the service. Journal the
|
|
338
200
|
// dir before it exists so a failure leaves the name genuinely reusable
|
|
339
201
|
// rather than blocked by a half-built directory.
|
|
@@ -402,8 +264,7 @@ export async function spawnTemp(o, binPath, launch = independentSupervisor, crea
|
|
|
402
264
|
validateSpawnOpts(o);
|
|
403
265
|
if (o.isolationFile)
|
|
404
266
|
readIsolationFile(o.isolationFile); // fail before reserving
|
|
405
|
-
|
|
406
|
-
readMissionFile(o.missionFile); // fail before reserving
|
|
267
|
+
const prepared = resolvedSpawn(o); // canonical validation before mutation
|
|
407
268
|
// Retire only supervisors whose recorded owner is definitively stopped. This
|
|
408
269
|
// bounded pass keeps the active roster clean without deleting old evidence.
|
|
409
270
|
await reclaimStaleTempState();
|
|
@@ -416,63 +277,14 @@ export async function spawnTemp(o, binPath, launch = independentSupervisor, crea
|
|
|
416
277
|
creation.onStage?.('checking_identity', {
|
|
417
278
|
result: 'unknown', guarantee: 'unverified',
|
|
418
279
|
});
|
|
419
|
-
return spawnTempInner(o, binPath, launch, tx, creation.onStage);
|
|
280
|
+
return spawnTempInner(o, prepared.role, binPath, launch, tx, creation.onStage);
|
|
420
281
|
}, creation);
|
|
421
282
|
}
|
|
422
|
-
async function spawnTempInner(o, binPath, launch, tx, onStage) {
|
|
283
|
+
async function spawnTempInner(o, preparedRole, binPath, launch, tx, onStage) {
|
|
423
284
|
const cfg = loadConfig(o.configPath);
|
|
424
|
-
const defaultHarness = cfg.defaults.harness;
|
|
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';
|
|
451
285
|
const role = {
|
|
452
|
-
...
|
|
453
|
-
name: o.name,
|
|
454
|
-
harness,
|
|
455
|
-
session,
|
|
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)',
|
|
471
|
-
roomMemberStartup: o.roomMemberStartup,
|
|
286
|
+
...preparedRole, sourceFile: '(temp)', loops: undefined, roomMemberStartup: o.roomMemberStartup,
|
|
472
287
|
};
|
|
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
288
|
onStage?.('writing_role');
|
|
477
289
|
const dir = applyRole(role, { temp: true, identityGuarantee: 'unverified' });
|
|
478
290
|
const provenance = buildProvenance({
|
|
@@ -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;
|
package/dist/watchdog/run.js
CHANGED
|
@@ -10,7 +10,7 @@ import { applyRole } from '../ops.js';
|
|
|
10
10
|
import { daemonIdentityInventoryProvisioner, ensureIdentity, } from '../creation.js';
|
|
11
11
|
import { runOnce, START_STAGGER_FILE } from '../runner.js';
|
|
12
12
|
import { agentDir, tmpRoot } from '../paths.js';
|
|
13
|
-
import { loadConfig,
|
|
13
|
+
import { loadConfig, ROLE_NAME_RE, } from '../config.js';
|
|
14
14
|
import { getAdapter } from '../harness/registry.js';
|
|
15
15
|
import { redactLogLine } from '../application/log-service.js';
|
|
16
16
|
import { controlRequest, controlSocketPath } from '../session/control.js';
|
|
@@ -84,19 +84,10 @@ function readTail(runDir) {
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
/** The temp role every watchdog-family run launches under. Isolation is opt-in. */
|
|
87
|
-
function buildWatchdogRole(wd,
|
|
87
|
+
function buildWatchdogRole(wd, _cfg) {
|
|
88
88
|
return {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
identity: wd.identity, model: wd.model,
|
|
92
|
-
// Watchdogs are observe-only by contract, but their sanctioned status commands
|
|
93
|
-
// must reach host control sockets. Keep approvals/unattended escalation denied
|
|
94
|
-
// while disabling the harness's native filesystem/network sandbox.
|
|
95
|
-
permissions: { approval: 'deny', filesystem: 'unrestricted', unattended: 'deny' },
|
|
96
|
-
permissionsDeclared: true,
|
|
97
|
-
monitor: resolveMonitorConfig(cfg.defaults.monitor, undefined),
|
|
98
|
-
worklog: resolveWorklogPolicy(cfg.defaults.worklog, undefined),
|
|
99
|
-
isolation: wd.isolation,
|
|
89
|
+
...structuredClone(wd.resolvedAgent),
|
|
90
|
+
name: wd.identity, sourceFile: '(watchdog)', identity: wd.identity,
|
|
100
91
|
};
|
|
101
92
|
}
|
|
102
93
|
/** Discover temporary sessions that are live now, not merely stale dirs on disk. */
|
|
@@ -126,7 +126,14 @@ function agentEntry(node) {
|
|
|
126
126
|
*/
|
|
127
127
|
function watchdogEntry(node, merged) {
|
|
128
128
|
const scoped = linked(merged, node.id, 'watches');
|
|
129
|
-
return {
|
|
129
|
+
return {
|
|
130
|
+
...pick(node, WATCHDOG_FIELDS),
|
|
131
|
+
agent: {
|
|
132
|
+
role: { inline: {} },
|
|
133
|
+
brain: { inline: { harness: 'claude-code', ...pick(node, BRAIN_FIELDS) } },
|
|
134
|
+
},
|
|
135
|
+
...(scoped.length ? { watch: scoped } : {}),
|
|
136
|
+
};
|
|
130
137
|
}
|
|
131
138
|
function loopEntry(node, merged) {
|
|
132
139
|
const roles = linked(merged, node.id, 'targets');
|