@ours.network/fleet 1.0.5 → 1.1.0-nightly.10
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 +122 -133
- package/dist/agent-recovery-gate.d.ts +17 -0
- package/dist/agent-recovery-gate.js +135 -0
- package/dist/application/capabilities.d.ts +0 -1
- package/dist/application/capabilities.js +3 -13
- package/dist/application/fleet-query-service.d.ts +0 -3
- package/dist/application/fleet-query-service.js +14 -25
- package/dist/application/role-creation-service.d.ts +5 -24
- package/dist/application/role-creation-service.js +27 -67
- package/dist/application/role-removal-service.js +6 -3
- package/dist/application/role-repository.d.ts +1 -2
- package/dist/application/role-repository.js +7 -7
- package/dist/application/session-control.d.ts +0 -12
- package/dist/application/session-control.js +0 -42
- package/dist/application/session-mutations.d.ts +5 -5
- package/dist/application/task-room-service.d.ts +1 -1
- package/dist/application/task-room-service.js +154 -16
- package/dist/application/types.d.ts +1 -8
- package/dist/briefing.js +12 -11
- package/dist/build-info.json +5 -5
- package/dist/cli.js +143 -63
- package/dist/config-yaml.d.ts +1 -1
- package/dist/config.d.ts +66 -6
- package/dist/config.js +398 -43
- package/dist/creation.d.ts +1 -1
- package/dist/creation.js +2 -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 +103 -127
- package/dist/doctor.js +18 -11
- package/dist/fleet-command-audit.d.ts +160 -0
- package/dist/fleet-command-audit.js +433 -0
- package/dist/fleet-proxy.d.ts +3 -1
- package/dist/fleet-proxy.js +30 -8
- package/dist/generated-agent-source.d.ts +9 -0
- package/dist/generated-agent-source.js +53 -0
- package/dist/harness/acp-session-transport.d.ts +8 -0
- package/dist/harness/acp-session-transport.js +3 -0
- package/dist/harness/agent-session.d.ts +51 -0
- package/dist/harness/agent-session.js +1 -0
- package/dist/harness/claude-code-session.d.ts +24 -0
- package/dist/harness/claude-code-session.js +36 -0
- package/dist/harness/claude-code.d.ts +2 -1
- package/dist/harness/claude-code.js +41 -81
- package/dist/harness/codex-session.d.ts +24 -0
- package/dist/harness/codex-session.js +36 -0
- package/dist/harness/codex.d.ts +2 -1
- package/dist/harness/codex.js +55 -45
- package/dist/harness/registry.js +4 -0
- package/dist/harness/types.d.ts +6 -38
- package/dist/index.d.ts +4 -6
- package/dist/index.js +2 -4
- package/dist/init-guidance.d.ts +1 -0
- package/dist/init-guidance.js +6 -0
- package/dist/isolation/resources.d.ts +3 -7
- package/dist/isolation/resources.js +3 -7
- package/dist/model-env.d.ts +4 -6
- package/dist/model-env.js +5 -8
- package/dist/monitor.d.ts +1 -24
- package/dist/monitor.js +5 -140
- package/dist/ops.js +4 -2
- package/dist/owner-channel/channel.d.ts +58 -2
- package/dist/owner-channel/channel.js +269 -29
- package/dist/owner-channel/commands.d.ts +2 -0
- package/dist/owner-channel/commands.js +31 -13
- package/dist/owner-channel/state.d.ts +2 -0
- package/dist/owner-channel/state.js +8 -2
- package/dist/preset-bootstrap.d.ts +10 -0
- package/dist/preset-bootstrap.js +85 -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.d.ts +3 -2
- package/dist/resolved-plan.js +21 -5
- package/dist/rooms-tasks/cli.js +225 -40
- package/dist/rooms-tasks/config.js +24 -13
- package/dist/rooms-tasks/external-worker.d.ts +1 -0
- package/dist/rooms-tasks/external-worker.js +14 -9
- package/dist/rooms-tasks/provision.js +98 -36
- package/dist/rooms-tasks/templates.d.ts +0 -1
- package/dist/rooms-tasks/templates.js +9 -76
- package/dist/rooms-tasks/types.d.ts +10 -16
- package/dist/rooms-tasks/types.js +1 -4
- package/dist/runner.d.ts +13 -16
- package/dist/runner.js +110 -122
- package/dist/sensitive-config.d.ts +2 -0
- package/dist/sensitive-config.js +4 -0
- package/dist/session/acp.d.ts +7 -2
- package/dist/session/acp.js +29 -6
- package/dist/session/activity.d.ts +2 -3
- package/dist/session/activity.js +2 -3
- package/dist/session/arbiter.d.ts +4 -4
- package/dist/session/control.d.ts +17 -4
- package/dist/session/control.js +35 -0
- package/dist/session/types.d.ts +11 -5
- package/dist/spawn.d.ts +8 -33
- package/dist/spawn.js +53 -213
- package/dist/supervisor/none.d.ts +1 -1
- package/dist/supervisor/none.js +2 -2
- package/dist/temp-lifecycle.d.ts +1 -1
- package/dist/tmux.d.ts +1 -14
- package/dist/tmux.js +1 -51
- package/dist/watchdog/config.d.ts +10 -7
- package/dist/watchdog/config.js +30 -18
- package/dist/watchdog/run.js +12 -36
- package/dist/web/auth.d.ts +1 -1
- package/dist/web/fleet-config-service.d.ts +11 -8
- package/dist/web/fleet-config-service.js +384 -183
- package/dist/web/runtime.js +4 -15
- package/dist/web/server.d.ts +1 -3
- package/dist/web/server.js +3 -14
- package/dist/web/topology-promote.js +26 -19
- package/dist/web/topology.js +2 -2
- package/dist/web/yaml-document-edit.js +2 -0
- package/dist/web-app/assets/index-59GF-gsZ.css +1 -0
- package/dist/web-app/assets/index-BbTc5KtZ.js +10 -0
- package/dist/web-app/index.html +2 -2
- package/examples/fleet/agents/Alice.yaml +11 -0
- package/examples/fleet/agents/FleetCoordinator.yaml +7 -0
- package/examples/fleet/brains/claude-default.yaml +5 -0
- package/examples/fleet/roles/coordinator.yaml +5 -0
- package/examples/fleet/roles/developer.yaml +3 -0
- package/examples/fleet.yaml +74 -0
- package/package.json +5 -8
- package/presets/fleet/agents/Agent.yaml +3 -0
- package/presets/fleet/agents/Architect.yaml +3 -0
- package/presets/fleet/agents/Critic.yaml +3 -0
- package/presets/fleet/agents/Developer.yaml +3 -0
- package/presets/fleet/agents/Secretary.yaml +3 -0
- package/presets/fleet/agents/Tester.yaml +3 -0
- package/presets/fleet/brains/claude-default.yaml +5 -0
- package/presets/fleet/roles/Agent.yaml +6 -0
- package/presets/fleet/roles/Architect.yaml +6 -0
- package/presets/fleet/roles/Critic.yaml +6 -0
- package/presets/fleet/roles/Developer.yaml +6 -0
- package/presets/fleet/roles/Secretary.yaml +6 -0
- package/presets/fleet/roles/Tester.yaml +6 -0
- package/presets/fleet/room_templates/pair.yaml +11 -0
- package/presets/fleet/room_templates/single.yaml +8 -0
- package/presets/fleet/room_templates/team.yaml +12 -0
- package/presets/fleet.yaml +7 -0
- package/presets/manifest.json +5 -0
- package/dist/session/tmux.d.ts +0 -28
- package/dist/session/tmux.js +0 -80
- package/dist/web/terminal/bridge.d.ts +0 -27
- package/dist/web/terminal/bridge.js +0 -317
- package/dist/web-app/assets/TerminalView-C_G1ID2P.js +0 -9
- package/dist/web-app/assets/index-BCBK78hw.js +0 -10
- package/dist/web-app/assets/index-DuC-xnX4.css +0 -1
package/dist/config.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
1
|
+
import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, extname, join, resolve, sep } from 'node:path';
|
|
3
3
|
import { agentDir, defaultConfigPath, fleetDDir, home } from './paths.js';
|
|
4
4
|
import { parseFleetDocument, } from './config-yaml.js';
|
|
5
5
|
import { harnessRuntimeDir, resolveIsolation, validateIsolationConfig, } from './isolation/policy.js';
|
|
@@ -10,6 +10,7 @@ import { 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
|
+
import { parseDuration } from './duration.js';
|
|
13
14
|
/** Conservative built-in policy; `worklog: false` is the explicit opt-out. */
|
|
14
15
|
export const DEFAULT_WORKLOG_POLICY = Object.freeze({
|
|
15
16
|
max_kb: 1024,
|
|
@@ -126,12 +127,319 @@ export function isolationContextFor(role) {
|
|
|
126
127
|
harnessSharedPaths: split?.shared,
|
|
127
128
|
};
|
|
128
129
|
}
|
|
129
|
-
export const ROLE_NAME_RE = /^[A-Za-z0-9_-]
|
|
130
|
+
export const ROLE_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
|
|
130
131
|
const ROLE_KEYS = [
|
|
131
132
|
'harness', 'session', 'session_options', 'permissions', 'identity', 'cwd', 'coordinator', 'mission', 'persona', 'bio',
|
|
132
|
-
'briefing_file', 'model', 'model_chain', 'max_tokens', 'autocompact_pct', 'env', 'oversee', 'harness_options',
|
|
133
|
+
'briefing_file', 'model', 'effort', 'model_chain', 'max_tokens', 'autocompact_pct', 'env', 'oversee', 'harness_options',
|
|
133
134
|
'isolation', 'monitor', 'owner_channel', 'worklog', 'auth_proxy',
|
|
134
135
|
];
|
|
136
|
+
export const ROLE_PRESET_KEYS = ['mission', 'persona', 'bio', 'briefing_file'];
|
|
137
|
+
export const BRAIN_PRESET_KEYS = [
|
|
138
|
+
'harness', 'session', 'session_options', 'model', 'model_chain', 'max_tokens',
|
|
139
|
+
'autocompact_pct', 'harness_options', 'effort',
|
|
140
|
+
];
|
|
141
|
+
export const AGENT_KEYS = [
|
|
142
|
+
'role', 'brain', 'permissions', 'identity', 'cwd', 'coordinator', 'env', 'oversee',
|
|
143
|
+
'isolation', 'monitor', 'owner_channel', 'worklog', 'auth_proxy',
|
|
144
|
+
];
|
|
145
|
+
const V2_API_VERSION = 'ours.network/fleet/v2';
|
|
146
|
+
const utf8Bytes = (value) => Buffer.byteLength(value, 'utf8');
|
|
147
|
+
function schemaError(file, pointer, summary) {
|
|
148
|
+
throw new ConfigError(`${file}: E_SCHEMA ${pointer}: ${summary}`);
|
|
149
|
+
}
|
|
150
|
+
export function validateRoleValue(value, file, pointer) {
|
|
151
|
+
const limits = {
|
|
152
|
+
mission: { bytes: 16 * 1024, nonblank: true },
|
|
153
|
+
persona: { bytes: 16 * 1024 },
|
|
154
|
+
bio: { bytes: 4 * 1024 },
|
|
155
|
+
};
|
|
156
|
+
for (const [key, rule] of Object.entries(limits)) {
|
|
157
|
+
const current = value[key];
|
|
158
|
+
if (current === undefined)
|
|
159
|
+
continue;
|
|
160
|
+
if (typeof current !== 'string')
|
|
161
|
+
schemaError(file, `${pointer}/${key}`, 'must be a string');
|
|
162
|
+
if (rule.nonblank && !current.trim())
|
|
163
|
+
schemaError(file, `${pointer}/${key}`, 'must be non-blank');
|
|
164
|
+
if (utf8Bytes(current) > rule.bytes)
|
|
165
|
+
schemaError(file, `${pointer}/${key}`, `must be at most ${rule.bytes} UTF-8 bytes`);
|
|
166
|
+
}
|
|
167
|
+
if (value.briefing_file !== undefined
|
|
168
|
+
&& (typeof value.briefing_file !== 'string' || !value.briefing_file.trim()))
|
|
169
|
+
schemaError(file, `${pointer}/briefing_file`, 'must be a non-blank relative path');
|
|
170
|
+
}
|
|
171
|
+
export function validateBrainValue(value, file, pointer) {
|
|
172
|
+
if (typeof value.harness !== 'string' || !value.harness.trim())
|
|
173
|
+
schemaError(file, `${pointer}/harness`, 'is required and must be a registered non-blank harness ID');
|
|
174
|
+
if (value.model !== undefined && value.model !== null
|
|
175
|
+
&& (typeof value.model !== 'string' || !value.model.trim()))
|
|
176
|
+
schemaError(file, `${pointer}/model`, 'must be a non-blank string or null');
|
|
177
|
+
for (const key of ['harness_options', 'session_options']) {
|
|
178
|
+
if (value[key] !== undefined && !isPlainObject(value[key]))
|
|
179
|
+
schemaError(file, `${pointer}/${key}`, 'must be a mapping');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function validateAgentScalars(value, file, id) {
|
|
183
|
+
for (const key of ['identity', 'cwd']) {
|
|
184
|
+
const current = value[key];
|
|
185
|
+
if (current !== undefined && (typeof current !== 'string' || !current.trim()))
|
|
186
|
+
schemaError(file, `/agents/${id}/${key}`, 'must be a non-blank string');
|
|
187
|
+
}
|
|
188
|
+
if (value.coordinator !== undefined
|
|
189
|
+
&& (typeof value.coordinator !== 'string' || !ROLE_NAME_RE.test(value.coordinator)))
|
|
190
|
+
schemaError(file, `/agents/${id}/coordinator`, 'must be a valid Agent ID');
|
|
191
|
+
if (value.oversee !== undefined) {
|
|
192
|
+
if (!Array.isArray(value.oversee))
|
|
193
|
+
schemaError(file, `/agents/${id}/oversee`, 'must be an array');
|
|
194
|
+
const seen = new Set();
|
|
195
|
+
value.oversee.forEach((entry, index) => {
|
|
196
|
+
const pointer = `/agents/${id}/oversee/${index}`;
|
|
197
|
+
if (!isPlainObject(entry))
|
|
198
|
+
schemaError(file, pointer, 'must be a mapping');
|
|
199
|
+
const keys = Object.keys(entry);
|
|
200
|
+
const bad = keys.filter(key => key !== 'agent' && key !== 'interval');
|
|
201
|
+
if (bad.length || keys.length !== 2 || !keys.includes('agent') || !keys.includes('interval'))
|
|
202
|
+
schemaError(file, pointer, 'must contain exactly agent and interval');
|
|
203
|
+
if (typeof entry.agent !== 'string' || !ROLE_NAME_RE.test(entry.agent))
|
|
204
|
+
schemaError(file, `${pointer}/agent`, 'must be a valid Agent ID');
|
|
205
|
+
if (typeof entry.interval !== 'string')
|
|
206
|
+
schemaError(file, `${pointer}/interval`, 'must be a valid duration');
|
|
207
|
+
try {
|
|
208
|
+
parseDuration(entry.interval, { name: 'oversee interval' });
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
schemaError(file, `${pointer}/interval`, 'must be a valid duration such as 5m');
|
|
212
|
+
}
|
|
213
|
+
if (seen.has(entry.agent))
|
|
214
|
+
schemaError(file, `${pointer}/agent`, 'must be unique');
|
|
215
|
+
seen.add(entry.agent);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function assertTrustedPath(path, expected) {
|
|
220
|
+
const st = lstatSync(path);
|
|
221
|
+
if (st.isSymbolicLink())
|
|
222
|
+
throw new ConfigError(`E_SYMLINK: trusted configuration path is a symlink: ${path}`);
|
|
223
|
+
if (expected === 'file' ? !st.isFile() : !st.isDirectory())
|
|
224
|
+
throw new ConfigError(`E_FILE_TRUST: expected ${expected}: ${path}`);
|
|
225
|
+
const uid = process.getuid?.();
|
|
226
|
+
if (uid !== undefined && st.uid !== uid)
|
|
227
|
+
throw new ConfigError(`E_FILE_TRUST: ${path} is owned by uid ${st.uid}; required uid ${uid}`);
|
|
228
|
+
if ((st.mode & 0o022) !== 0)
|
|
229
|
+
throw new ConfigError(`E_FILE_TRUST: ${path} is group/world writable (mode ${(st.mode & 0o777).toString(8)})`);
|
|
230
|
+
}
|
|
231
|
+
export function assertBareKeys(value, allowed, label) {
|
|
232
|
+
if (!isPlainObject(value))
|
|
233
|
+
throw new ConfigError(`${label}: E_SCHEMA: must be a mapping`);
|
|
234
|
+
const bad = Object.keys(value).filter(key => !allowed.includes(key));
|
|
235
|
+
if (bad.length)
|
|
236
|
+
throw new ConfigError(`${label}: E_UNKNOWN_KEY: unknown key(s) ${bad.join(', ')}; allowed: ${allowed.join(', ')}`);
|
|
237
|
+
return value;
|
|
238
|
+
}
|
|
239
|
+
export function splitRootFor(base) {
|
|
240
|
+
const extension = extname(base).toLowerCase();
|
|
241
|
+
return extension === '.yaml' || extension === '.yml' ? base.slice(0, -extension.length) : `${base}.d`;
|
|
242
|
+
}
|
|
243
|
+
function readKindDirectory(root, kind, required, yamlMode, diagnostics, files) {
|
|
244
|
+
if (!existsSync(root)) {
|
|
245
|
+
if (required)
|
|
246
|
+
throw new ConfigError(`E_ROOT_MISSING: required ${kind} root not found: ${root}`);
|
|
247
|
+
return new Map();
|
|
248
|
+
}
|
|
249
|
+
assertTrustedPath(root, 'directory');
|
|
250
|
+
const result = new Map();
|
|
251
|
+
const names = readdirSync(root).filter(name => ['.yaml', '.yml'].includes(extname(name).toLowerCase())).sort();
|
|
252
|
+
for (const name of names) {
|
|
253
|
+
const file = join(root, name);
|
|
254
|
+
assertTrustedPath(file, 'file');
|
|
255
|
+
const id = basename(name, extname(name));
|
|
256
|
+
if (!ROLE_NAME_RE.test(id))
|
|
257
|
+
throw new ConfigError(`${file}: invalid ${kind} id '${id}' (allowed: [A-Za-z0-9_-])`);
|
|
258
|
+
const previous = result.get(id);
|
|
259
|
+
if (previous)
|
|
260
|
+
throw new ConfigError(`E_DUPLICATE_ID: ${kind} '${id}' defined by both ${previous.file} and ${file}`);
|
|
261
|
+
const parsed = parseFleetDocument(file, readFileSync(file, 'utf8'), yamlMode);
|
|
262
|
+
diagnostics.push(...parsed.diagnostics);
|
|
263
|
+
files.push(file);
|
|
264
|
+
result.set(id, { file, value: parsed.value });
|
|
265
|
+
}
|
|
266
|
+
return result;
|
|
267
|
+
}
|
|
268
|
+
function readRoomTemplateDirectory(root, yamlMode, diagnostics, files) {
|
|
269
|
+
if (!existsSync(root))
|
|
270
|
+
return {};
|
|
271
|
+
assertTrustedPath(root, 'directory');
|
|
272
|
+
const result = {};
|
|
273
|
+
const sources = new Map();
|
|
274
|
+
const names = readdirSync(root)
|
|
275
|
+
.filter(name => ['.yaml', '.yml'].includes(extname(name).toLowerCase())).sort();
|
|
276
|
+
for (const filename of names) {
|
|
277
|
+
const file = join(root, filename);
|
|
278
|
+
assertTrustedPath(file, 'file');
|
|
279
|
+
const id = basename(filename, extname(filename));
|
|
280
|
+
const previous = sources.get(id);
|
|
281
|
+
if (previous)
|
|
282
|
+
throw new ConfigError(`E_DUPLICATE_ID: room template '${id}' defined by both ${previous} and ${file}`);
|
|
283
|
+
const parsed = parseFleetDocument(file, readFileSync(file, 'utf8'), yamlMode);
|
|
284
|
+
diagnostics.push(...parsed.diagnostics);
|
|
285
|
+
if (parsed.value.override_builtin === true)
|
|
286
|
+
diagnostics.push({
|
|
287
|
+
severity: 'warning', kind: 'deprecated-field', file, line: 1, column: 1,
|
|
288
|
+
message: `${file}: override_builtin is deprecated and ignored for a file-backed Room template`,
|
|
289
|
+
});
|
|
290
|
+
files.push(file);
|
|
291
|
+
const validated = validateRoomTemplatesConfig({ [id]: parsed.value }, file)[id];
|
|
292
|
+
result[id] = { ...validated, sourceFile: file };
|
|
293
|
+
sources.set(id, file);
|
|
294
|
+
}
|
|
295
|
+
return result;
|
|
296
|
+
}
|
|
297
|
+
function selection(raw, kind, agentFile, presets, allowed) {
|
|
298
|
+
if (!isPlainObject(raw))
|
|
299
|
+
schemaError(agentFile, `/${kind}`, 'must be { ref } or { inline }');
|
|
300
|
+
const keys = Object.keys(raw);
|
|
301
|
+
if (keys.length !== 1 || (keys[0] !== 'ref' && keys[0] !== 'inline'))
|
|
302
|
+
throw new ConfigError(`${agentFile}: E_UNION /${kind}: must contain exactly one of ref or inline`);
|
|
303
|
+
if ('ref' in raw) {
|
|
304
|
+
if (typeof raw.ref !== 'string' || !ROLE_NAME_RE.test(raw.ref))
|
|
305
|
+
schemaError(agentFile, `/${kind}/ref`, 'must be a valid case-sensitive ID');
|
|
306
|
+
const preset = presets.get(raw.ref);
|
|
307
|
+
if (!preset)
|
|
308
|
+
throw new ConfigError(`${agentFile}: E_REF_MISSING: ${kind} '${raw.ref}' not found`);
|
|
309
|
+
return {
|
|
310
|
+
value: assertBareKeys(preset.value, allowed, `${preset.file}: ${kind} '${raw.ref}'`),
|
|
311
|
+
file: preset.file,
|
|
312
|
+
pointer: `/${kind}s/${raw.ref}`,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
value: assertBareKeys(raw.inline, allowed, `${agentFile}: agent.${kind}.inline`),
|
|
317
|
+
file: agentFile,
|
|
318
|
+
pointer: `/${kind}/inline`,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
function deepSubStrict(value, vars, file, pointer) {
|
|
322
|
+
if (typeof value === 'string') {
|
|
323
|
+
return value.replace(/\$\{(\w+)\}/g, (_match, key) => {
|
|
324
|
+
if (!(key in vars))
|
|
325
|
+
schemaError(file, pointer, `unknown variable \${${key}}`);
|
|
326
|
+
return String(vars[key]);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
if (Array.isArray(value))
|
|
330
|
+
return value.map((item, index) => deepSubStrict(item, vars, file, `${pointer}/${index}`));
|
|
331
|
+
if (isPlainObject(value))
|
|
332
|
+
return Object.fromEntries(Object.entries(value).map(([key, child]) => [
|
|
333
|
+
key, deepSubStrict(child, vars, file, `${pointer}/${key}`),
|
|
334
|
+
]));
|
|
335
|
+
return value;
|
|
336
|
+
}
|
|
337
|
+
function composeSplitRoles(base, baseDoc, yamlMode, diagnostics, files, additionalAgent) {
|
|
338
|
+
if (baseDoc.api_version !== V2_API_VERSION)
|
|
339
|
+
throw new ConfigError(`${base}: legacy fleet configuration is unsupported; run 'ours-fleet doctor' and rewrite it as ${V2_API_VERSION}`);
|
|
340
|
+
if ('roles' in baseDoc)
|
|
341
|
+
throw new ConfigError(`${base}: legacy top-level roles: is unsupported; define bare Agent files under ${splitRootFor(base)}/agents/`);
|
|
342
|
+
const root = splitRootFor(base);
|
|
343
|
+
if (!existsSync(root))
|
|
344
|
+
throw new ConfigError(`E_ROOT_MISSING: required configuration root not found: ${root}`);
|
|
345
|
+
assertTrustedPath(root, 'directory');
|
|
346
|
+
const brains = readKindDirectory(join(root, 'brains'), 'brain', false, yamlMode, diagnostics, files);
|
|
347
|
+
const roles = readKindDirectory(join(root, 'roles'), 'role', false, yamlMode, diagnostics, files);
|
|
348
|
+
const agents = readKindDirectory(join(root, 'agents'), 'agent', true, yamlMode, diagnostics, files);
|
|
349
|
+
if (additionalAgent) {
|
|
350
|
+
if (agents.has(additionalAgent.id))
|
|
351
|
+
throw new ConfigError(`agent '${additionalAgent.id}' already exists`);
|
|
352
|
+
agents.set(additionalAgent.id, {
|
|
353
|
+
file: '(spawn request)', value: structuredClone(additionalAgent.definition),
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
const vars = (baseDoc.vars ?? {});
|
|
357
|
+
const documents = [];
|
|
358
|
+
for (const [id, agent] of agents) {
|
|
359
|
+
const a = assertBareKeys(agent.value, AGENT_KEYS, `${agent.file}: agent '${id}'`);
|
|
360
|
+
if (!('role' in a) || !('brain' in a))
|
|
361
|
+
throw new ConfigError(`${agent.file}: agent '${id}' requires role and brain`);
|
|
362
|
+
const role = selection(a.role, 'role', agent.file, roles, ROLE_PRESET_KEYS);
|
|
363
|
+
const brain = selection(a.brain, 'brain', agent.file, brains, BRAIN_PRESET_KEYS);
|
|
364
|
+
const operational = deepSubStrict(Object.fromEntries(Object.entries(a).filter(([key]) => key !== 'role' && key !== 'brain')), vars, agent.file, `/agents/${id}`);
|
|
365
|
+
validateAgentScalars(operational, agent.file, id);
|
|
366
|
+
const brainValue = deepSubStrict({ ...brain.value }, vars, brain.file, brain.pointer);
|
|
367
|
+
validateBrainValue(brainValue, brain.file, brain.pointer);
|
|
368
|
+
const effort = brainValue.effort;
|
|
369
|
+
delete brainValue.effort;
|
|
370
|
+
const harness = brainValue.harness;
|
|
371
|
+
{
|
|
372
|
+
let adapter;
|
|
373
|
+
try {
|
|
374
|
+
adapter = getAdapter(harness);
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
schemaError(brain.file, `${brain.pointer}/harness`, `unregistered harness '${harness}'`);
|
|
378
|
+
}
|
|
379
|
+
let resolved;
|
|
380
|
+
try {
|
|
381
|
+
resolved = adapter.agentSession.resolveBrain({
|
|
382
|
+
model: brainValue.model,
|
|
383
|
+
effort: effort,
|
|
384
|
+
harnessOptions: brainValue.harness_options,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
schemaError(brain.file, `${brain.pointer}/effort`, error.message);
|
|
389
|
+
}
|
|
390
|
+
brainValue.model = resolved.model;
|
|
391
|
+
brainValue.harness_options = resolved.harnessOptions;
|
|
392
|
+
brainValue.effort = effort;
|
|
393
|
+
}
|
|
394
|
+
const roleValue = deepSubStrict({ ...role.value }, vars, role.file, role.pointer);
|
|
395
|
+
validateRoleValue(roleValue, role.file, role.pointer);
|
|
396
|
+
if (typeof roleValue.briefing_file === 'string') {
|
|
397
|
+
const containmentRoot = realpathSync(dirname(role.file));
|
|
398
|
+
const briefing = resolve(dirname(role.file), roleValue.briefing_file);
|
|
399
|
+
if (briefing !== containmentRoot && !briefing.startsWith(`${containmentRoot}${sep}`))
|
|
400
|
+
throw new ConfigError(`${role.file}: E_PATH_ESCAPE: briefing_file escapes its ${role.file === agent.file ? 'Agent' : 'Role'} root`);
|
|
401
|
+
assertTrustedPath(briefing, 'file');
|
|
402
|
+
const real = realpathSync(briefing);
|
|
403
|
+
if (real !== containmentRoot && !real.startsWith(`${containmentRoot}${sep}`))
|
|
404
|
+
throw new ConfigError(`${role.file}: E_PATH_ESCAPE: briefing_file realpath escapes its root`);
|
|
405
|
+
roleValue.briefing_file = real;
|
|
406
|
+
}
|
|
407
|
+
const provenance = {};
|
|
408
|
+
const record = (values, source, kind, via) => {
|
|
409
|
+
for (const [key, raw] of Object.entries(values))
|
|
410
|
+
provenance[key] = {
|
|
411
|
+
sourceFile: source.file, sourcePointer: `${source.pointer}/${key}`,
|
|
412
|
+
sourceKind: kind, sourceId: via?.id ?? id, origin: 'explicit',
|
|
413
|
+
...(via ? { viaReference: via } : {}),
|
|
414
|
+
transforms: [
|
|
415
|
+
...(JSON.stringify(raw).includes('${')
|
|
416
|
+
? [{ kind: 'substitution', detail: 'manifest variable substitution' }] : []),
|
|
417
|
+
...(['model', 'harness_options', 'effort'].includes(key)
|
|
418
|
+
? [{ kind: 'adapter-normalization', detail: 'AgentSession Brain resolution' }] : []),
|
|
419
|
+
],
|
|
420
|
+
};
|
|
421
|
+
};
|
|
422
|
+
record(a, { file: agent.file, pointer: `/agents/${id}` }, 'Agent');
|
|
423
|
+
record(role.value, { file: role.file, pointer: role.pointer }, 'Role', 'ref' in a.role
|
|
424
|
+
? { sourcePointer: `/agents/${id}/role/ref`, kind: 'Role', id: String(a.role.ref) }
|
|
425
|
+
: undefined);
|
|
426
|
+
record(brain.value, { file: brain.file, pointer: brain.pointer }, 'Brain', 'ref' in a.brain
|
|
427
|
+
? { sourcePointer: `/agents/${id}/brain/ref`, kind: 'Brain', id: String(a.brain.ref) }
|
|
428
|
+
: undefined);
|
|
429
|
+
delete provenance.role;
|
|
430
|
+
delete provenance.brain;
|
|
431
|
+
const canonicalDefinition = {
|
|
432
|
+
role: deepSubStrict(a.role, vars, agent.file, `/agents/${id}/role`),
|
|
433
|
+
brain: deepSubStrict(a.brain, vars, agent.file, `/agents/${id}/brain`),
|
|
434
|
+
...operational,
|
|
435
|
+
};
|
|
436
|
+
documents.push({ file: agent.file,
|
|
437
|
+
doc: { roles: { [id]: { ...brainValue, ...roleValue, ...operational } } }, provenance,
|
|
438
|
+
agentSelections: structuredClone({ role: canonicalDefinition.role, brain: canonicalDefinition.brain }),
|
|
439
|
+
agentDefinition: canonicalDefinition });
|
|
440
|
+
}
|
|
441
|
+
return documents;
|
|
442
|
+
}
|
|
135
443
|
function deepSub(v, vars) {
|
|
136
444
|
if (typeof v === 'string')
|
|
137
445
|
return v.replace(/\$\{(\w+)\}/g, (m, k) => (k in vars ? String(vars[k]) : m));
|
|
@@ -141,43 +449,43 @@ function deepSub(v, vars) {
|
|
|
141
449
|
return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, deepSub(x, vars)]));
|
|
142
450
|
return v;
|
|
143
451
|
}
|
|
144
|
-
/** Load
|
|
452
|
+
/** Load a v2 manifest and bare Agent/Role/Brain documents from its stem directory. */
|
|
145
453
|
export function loadConfig(configPath, options = {}) {
|
|
146
454
|
const base = configPath ?? defaultConfigPath();
|
|
147
455
|
const files = [];
|
|
148
456
|
const diagnostics = [];
|
|
149
|
-
|
|
457
|
+
let docs = [];
|
|
458
|
+
let manifestDoc = {};
|
|
150
459
|
if (existsSync(base)) {
|
|
460
|
+
assertTrustedPath(base, 'file');
|
|
151
461
|
const parsed = parseFleetDocument(base, readFileSync(base, 'utf8'), options.yamlMode);
|
|
152
|
-
|
|
462
|
+
manifestDoc = parsed.value;
|
|
153
463
|
diagnostics.push(...parsed.diagnostics);
|
|
154
464
|
files.push(base);
|
|
155
465
|
}
|
|
156
466
|
else if (configPath) {
|
|
157
467
|
throw new ConfigError(`config not found: ${base}`);
|
|
158
468
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
for (const f of readdirSync(dd).filter(f => f.endsWith('.yaml') || f.endsWith('.yml')).sort()) {
|
|
162
|
-
const p = join(dd, f);
|
|
163
|
-
const parsed = parseFleetDocument(p, readFileSync(p, 'utf8'), options.yamlMode);
|
|
164
|
-
const doc = parsed.value;
|
|
165
|
-
diagnostics.push(...parsed.diagnostics);
|
|
166
|
-
const FLEET_D_ALLOWED = ['roles', 'rooms', 'room_templates', 'tasks'];
|
|
167
|
-
const extra = Object.keys(doc).filter(k => !FLEET_D_ALLOWED.includes(k));
|
|
168
|
-
if (extra.length)
|
|
169
|
-
throw new ConfigError(`${p}: fleet.d files may only define ${FLEET_D_ALLOWED.join(', ')}: (found: ${extra.join(', ')})`);
|
|
170
|
-
docs.push({ file: p, doc });
|
|
171
|
-
files.push(p);
|
|
172
|
-
}
|
|
469
|
+
else {
|
|
470
|
+
throw new ConfigError(`config not found: ${base}`);
|
|
173
471
|
}
|
|
174
|
-
const
|
|
472
|
+
const dd = fleetDDir();
|
|
473
|
+
if (existsSync(dd) && readdirSync(dd).some(f => f.endsWith('.yaml') || f.endsWith('.yml')))
|
|
474
|
+
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)];
|
|
476
|
+
const baseDoc = manifestDoc;
|
|
175
477
|
const vars = (baseDoc.vars ?? {});
|
|
176
478
|
const defaults = (baseDoc.defaults ?? {});
|
|
479
|
+
const legacyRuntimeDefaults = ['harness', 'model', 'model_chain', 'effort', 'harness_options',
|
|
480
|
+
'session', 'session_options', 'max_tokens', 'autocompact_pct']
|
|
481
|
+
.filter(key => Object.hasOwn(defaults, key));
|
|
482
|
+
if (legacyRuntimeDefaults.length)
|
|
483
|
+
throw new ConfigError(`${base}: E_LEGACY /defaults: Brain-owned field(s) ${legacyRuntimeDefaults.join(', ')} are unsupported; move them to a Brain definition`);
|
|
177
484
|
const startStaggerMs = resolveStartStaggerMs(baseDoc.start_stagger_ms, base);
|
|
178
485
|
const seen = new Map();
|
|
179
486
|
const roles = [];
|
|
180
|
-
|
|
487
|
+
const agentDefinitions = {};
|
|
488
|
+
for (const { file, doc, provenance, agentSelections, agentDefinition } of docs) {
|
|
181
489
|
for (const [name, raw] of Object.entries((doc.roles ?? {}))) {
|
|
182
490
|
if (!ROLE_NAME_RE.test(name))
|
|
183
491
|
throw new ConfigError(`${file}: invalid role name '${name}' (allowed: [A-Za-z0-9_-])`);
|
|
@@ -185,6 +493,8 @@ export function loadConfig(configPath, options = {}) {
|
|
|
185
493
|
if (prev)
|
|
186
494
|
throw new ConfigError(`role '${name}' defined in both ${prev} and ${file}`);
|
|
187
495
|
seen.set(name, file);
|
|
496
|
+
if (agentDefinition)
|
|
497
|
+
agentDefinitions[name] = structuredClone(agentDefinition);
|
|
188
498
|
const r = deepSub(raw ?? {}, vars);
|
|
189
499
|
const bad = Object.keys(r).filter(k => !ROLE_KEYS.includes(k));
|
|
190
500
|
if (bad.length)
|
|
@@ -234,6 +544,21 @@ export function loadConfig(configPath, options = {}) {
|
|
|
234
544
|
const modelChain = resolveModelChain(model, r.model_chain ?? (inheritsModelDefaults
|
|
235
545
|
? defaults.model_chain
|
|
236
546
|
: undefined), file, name);
|
|
547
|
+
const fieldProvenance = { ...(provenance ?? {}) };
|
|
548
|
+
const defaulted = (key, explicit, builtIn = false) => {
|
|
549
|
+
if (fieldProvenance[key])
|
|
550
|
+
return;
|
|
551
|
+
fieldProvenance[key] = {
|
|
552
|
+
sourceFile: builtIn ? '(built-in)' : base,
|
|
553
|
+
sourcePointer: builtIn ? `/${key}` : `/defaults/${key}`,
|
|
554
|
+
sourceKind: builtIn ? 'built-in' : 'Manifest',
|
|
555
|
+
origin: builtIn ? 'built-in' : explicit ? 'explicit' : 'typed-default',
|
|
556
|
+
transforms: [{ kind: 'validation-default', detail: builtIn ? 'built-in default' : 'manifest operational default' }],
|
|
557
|
+
};
|
|
558
|
+
};
|
|
559
|
+
defaulted('identity', r.identity !== undefined, r.identity === undefined);
|
|
560
|
+
defaulted('permissions', defaults.permissions !== undefined, defaults.permissions === undefined);
|
|
561
|
+
defaulted('monitor', defaults.monitor !== undefined, defaults.monitor === undefined);
|
|
237
562
|
roles.push({
|
|
238
563
|
...r,
|
|
239
564
|
name,
|
|
@@ -252,9 +577,11 @@ export function loadConfig(configPath, options = {}) {
|
|
|
252
577
|
monitor,
|
|
253
578
|
owner_channel: ownerChannel,
|
|
254
579
|
worklog,
|
|
580
|
+
provenance: fieldProvenance,
|
|
255
581
|
auth_proxy: authProxy,
|
|
256
582
|
env: Object.keys(env).length ? env : undefined,
|
|
257
583
|
loops: [],
|
|
584
|
+
...(agentSelections ? { agentSelections } : {}),
|
|
258
585
|
});
|
|
259
586
|
// Forbidden-path enforcement: a mount that would breach the policy
|
|
260
587
|
// is a configuration error, caught by `config` rather than at launch.
|
|
@@ -270,7 +597,12 @@ export function loadConfig(configPath, options = {}) {
|
|
|
270
597
|
}
|
|
271
598
|
}
|
|
272
599
|
validateOwnerChannelIdentities(roles);
|
|
273
|
-
const watchdogs = resolveWatchdogs(baseDoc, base, roles, vars,
|
|
600
|
+
const watchdogs = options.skipWatchdogs ? [] : resolveWatchdogs(baseDoc, base, roles, vars, agentDefinitions, (name, definition) => {
|
|
601
|
+
const id = `WatchdogAgent${name}`.slice(0, 64);
|
|
602
|
+
return findRole(loadConfig(base, {
|
|
603
|
+
yamlMode: options.yamlMode, additionalAgent: { id, definition }, skipWatchdogs: true,
|
|
604
|
+
}), id);
|
|
605
|
+
});
|
|
274
606
|
const resolvedLoops = resolveLoops(baseDoc.loops, base, roles, vars);
|
|
275
607
|
for (const role of roles)
|
|
276
608
|
role.loops = resolvedLoops.byRole.get(role.name) ?? [];
|
|
@@ -283,6 +615,9 @@ export function loadConfig(configPath, options = {}) {
|
|
|
283
615
|
let ownerInvite;
|
|
284
616
|
let roomTemplates;
|
|
285
617
|
let tasks;
|
|
618
|
+
const fileTemplates = readRoomTemplateDirectory(join(splitRootFor(base), 'room_templates'), options.yamlMode ?? 'compat', diagnostics, files);
|
|
619
|
+
if (Object.keys(fileTemplates).length)
|
|
620
|
+
roomTemplates = fileTemplates;
|
|
286
621
|
for (const { file, doc } of docs) {
|
|
287
622
|
if (doc.rooms !== undefined) {
|
|
288
623
|
if (rooms)
|
|
@@ -295,7 +630,21 @@ export function loadConfig(configPath, options = {}) {
|
|
|
295
630
|
}
|
|
296
631
|
if (doc.room_templates !== undefined) {
|
|
297
632
|
const validated = validateRoomTemplatesConfig(deepSub(doc.room_templates, vars), file);
|
|
298
|
-
|
|
633
|
+
const raw = doc.room_templates;
|
|
634
|
+
for (const [name, template] of Object.entries(validated)) {
|
|
635
|
+
const shadowed = roomTemplates?.[name];
|
|
636
|
+
const marker = raw[name]?.override_builtin;
|
|
637
|
+
if (shadowed && (marker !== true || template.version <= shadowed.version))
|
|
638
|
+
throw new ConfigError(`${file}: room_templates.${name} shadows file preset ${shadowed.sourceFile}; `
|
|
639
|
+
+ 'set override_builtin: true and use a higher version');
|
|
640
|
+
if (marker === true)
|
|
641
|
+
diagnostics.push({
|
|
642
|
+
severity: 'warning', kind: 'deprecated-field', file, line: 1, column: 1,
|
|
643
|
+
message: `${file}: room_templates.${name}.override_builtin is deprecated; `
|
|
644
|
+
+ 'it is retained only as an explicit manifest-over-file migration marker',
|
|
645
|
+
});
|
|
646
|
+
roomTemplates = { ...(roomTemplates ?? {}), [name]: { ...template, sourceFile: file } };
|
|
647
|
+
}
|
|
299
648
|
}
|
|
300
649
|
if (doc.tasks !== undefined) {
|
|
301
650
|
if (tasks)
|
|
@@ -304,9 +653,19 @@ export function loadConfig(configPath, options = {}) {
|
|
|
304
653
|
}
|
|
305
654
|
}
|
|
306
655
|
const result = {
|
|
307
|
-
roles, vars, defaults, files, startStaggerMs, diagnostics, watchdogs,
|
|
656
|
+
roles, agentDefinitions, vars, defaults, files, startStaggerMs, diagnostics, watchdogs,
|
|
308
657
|
loops: resolvedLoops.loops,
|
|
309
658
|
rooms, roomTemplates, tasks, ownerInviteFingerprint,
|
|
659
|
+
configMode: 'split-v2',
|
|
660
|
+
sourceDocuments: files.map(path => {
|
|
661
|
+
if (path === base)
|
|
662
|
+
return { kind: 'Manifest', path };
|
|
663
|
+
const parent = basename(dirname(path));
|
|
664
|
+
const kind = parent === 'agents' ? 'Agent'
|
|
665
|
+
: parent === 'roles' ? 'Role'
|
|
666
|
+
: parent === 'room_templates' ? 'RoomTemplate' : 'Brain';
|
|
667
|
+
return { kind, id: basename(path, extname(path)), path };
|
|
668
|
+
}),
|
|
310
669
|
};
|
|
311
670
|
if (ownerInvite !== undefined) {
|
|
312
671
|
Object.defineProperty(result, 'ownerInvite', {
|
|
@@ -524,9 +883,12 @@ export function resolveWorklogPolicy(defaults, role, file = 'config', name = 'ro
|
|
|
524
883
|
return merged;
|
|
525
884
|
}
|
|
526
885
|
function resolveSession(raw, file, name) {
|
|
527
|
-
const value = raw ?? '
|
|
528
|
-
if (value
|
|
529
|
-
throw new ConfigError(`${file}: role '${name}' session:
|
|
886
|
+
const value = raw ?? 'acp';
|
|
887
|
+
if (value === 'tmux')
|
|
888
|
+
throw new ConfigError(`${file}: role '${name}' session: tmux is no longer supported; use session: acp `
|
|
889
|
+
+ 'with the Codex or Claude Code adapter');
|
|
890
|
+
if (value !== 'acp')
|
|
891
|
+
throw new ConfigError(`${file}: role '${name}' session: must be: acp`);
|
|
530
892
|
return value;
|
|
531
893
|
}
|
|
532
894
|
function resolveSessionOptions(defaults, role, session, file, name) {
|
|
@@ -541,33 +903,26 @@ function resolveSessionOptions(defaults, role, session, file, name) {
|
|
|
541
903
|
...((defaults?.acp) ?? {}),
|
|
542
904
|
...(role?.acp ?? {}),
|
|
543
905
|
},
|
|
544
|
-
tmux: {
|
|
545
|
-
...((defaults?.tmux) ?? {}),
|
|
546
|
-
...(role?.tmux ?? {}),
|
|
547
|
-
},
|
|
548
906
|
};
|
|
549
|
-
|
|
907
|
+
if (defaults?.tmux !== undefined
|
|
908
|
+
|| role?.tmux !== undefined)
|
|
909
|
+
throw new ConfigError(`${file}: role '${name}' session_options.tmux is no longer supported; `
|
|
910
|
+
+ 'use session: acp with session_options.acp');
|
|
911
|
+
const bad = Object.keys(merged).filter(k => k !== 'acp');
|
|
550
912
|
if (bad.length)
|
|
551
913
|
throw new ConfigError(`${file}: role '${name}' session_options: unknown key(s) ${bad.join(', ')}`);
|
|
552
|
-
if (!isPlainObject(merged.acp)
|
|
914
|
+
if (!isPlainObject(merged.acp))
|
|
553
915
|
throw new ConfigError(`${file}: role '${name}' session_options.${session} must be a map`);
|
|
554
916
|
const acpBad = Object.keys(merged.acp).filter(k => k !== 'command');
|
|
555
|
-
const tmuxBad = Object.keys(merged.tmux).filter(k => k !== 'boot_grace_ms');
|
|
556
917
|
if (acpBad.length)
|
|
557
918
|
throw new ConfigError(`${file}: role '${name}' session_options.acp: unknown key(s) ${acpBad.join(', ')}`);
|
|
558
|
-
if (tmuxBad.length)
|
|
559
|
-
throw new ConfigError(`${file}: role '${name}' session_options.tmux: unknown key(s) ${tmuxBad.join(', ')}`);
|
|
560
919
|
const command = merged.acp.command;
|
|
561
920
|
if (command !== undefined
|
|
562
921
|
&& !(typeof command === 'string' && command.trim())
|
|
563
922
|
&& !(Array.isArray(command) && command.length > 0
|
|
564
923
|
&& command.every(v => typeof v === 'string' && v.length > 0)))
|
|
565
924
|
throw new ConfigError(`${file}: role '${name}' session_options.acp.command must be a non-empty string or string list`);
|
|
566
|
-
|
|
567
|
-
if (grace !== undefined
|
|
568
|
-
&& (typeof grace !== 'number' || !Number.isFinite(grace) || grace < 0))
|
|
569
|
-
throw new ConfigError(`${file}: role '${name}' session_options.tmux.boot_grace_ms must be a non-negative number`);
|
|
570
|
-
return Object.keys(merged.acp).length || Object.keys(merged.tmux).length ? merged : undefined;
|
|
925
|
+
return Object.keys(merged.acp).length ? merged : undefined;
|
|
571
926
|
}
|
|
572
927
|
export function resolvePermissions(defaults, role, file = 'config', name = 'role') {
|
|
573
928
|
if (defaults !== undefined && !isPlainObject(defaults))
|
package/dist/creation.d.ts
CHANGED
|
@@ -153,7 +153,7 @@ export interface DaemonIdentityProvisionerOptions {
|
|
|
153
153
|
export declare function daemonIdentityProvisioner(env?: NodeJS.ProcessEnv, attachClient?: (options: AttachOursClientOptions) => Promise<IdentityInventoryClient>, options?: DaemonIdentityProvisionerOptions): IdentityProvisioner;
|
|
154
154
|
/** Inventory-only variant for session-owned temporary identity lifecycles. */
|
|
155
155
|
export declare function daemonIdentityInventoryProvisioner(env?: NodeJS.ProcessEnv, attachClient?: (options: AttachOursClientOptions) => Promise<IdentityInventoryClient>): IdentityProvisioner;
|
|
156
|
-
/** Atomically write a
|
|
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
159
|
export type ProvenanceSource = 'cli' | 'fleet-default' | 'caller-role' | 'built-in';
|
package/dist/creation.js
CHANGED
|
@@ -300,12 +300,12 @@ export function daemonIdentityInventoryProvisioner(env = process.env, attachClie
|
|
|
300
300
|
acceptTemporaryExisting: true,
|
|
301
301
|
});
|
|
302
302
|
}
|
|
303
|
-
/** Atomically write a
|
|
303
|
+
/** Atomically write a bare Agent file, journalling it for rollback. */
|
|
304
304
|
export function writeRoleFile(tx, file, contents) {
|
|
305
305
|
const existed = existsSync(file);
|
|
306
306
|
replaceFileAtomically(file, contents, 0o644);
|
|
307
307
|
tx.record({
|
|
308
|
-
stage: `
|
|
308
|
+
stage: `Agent file ${file}`,
|
|
309
309
|
// Only remove what THIS transaction created; never delete a file the
|
|
310
310
|
// operator already had.
|
|
311
311
|
undo: () => { if (!existed)
|