@ours.network/fleet 1.1.0-nightly.20 → 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 +36 -2
- package/dist/application/task-room-service.js +16 -8
- package/dist/build-info.json +4 -4
- package/dist/cli.js +15 -4
- package/dist/config.d.ts +8 -1
- package/dist/config.js +17 -7
- package/dist/creation.d.ts +1 -1
- package/dist/creation.js +3 -2
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +21 -0
- package/dist/fleet-command-audit.js +14 -2
- package/dist/lifecycle-summary.d.ts +16 -0
- package/dist/lifecycle-summary.js +18 -0
- package/dist/loops/config.d.ts +10 -1
- package/dist/loops/config.js +35 -3
- package/dist/resolved-plan.js +9 -1
- package/dist/rooms-tasks/cli.js +14 -3
- package/dist/rooms-tasks/launch-snapshot.js +5 -0
- package/dist/rooms-tasks/member-overrides.d.ts +3 -0
- package/dist/rooms-tasks/member-overrides.js +39 -5
- package/dist/rooms-tasks/provision.js +5 -0
- package/dist/rooms-tasks/types.d.ts +2 -0
- package/dist/runner.d.ts +3 -0
- package/dist/runner.js +17 -3
- package/dist/spawn.d.ts +9 -0
- package/dist/spawn.js +65 -8
- package/package.json +1 -1
package/dist/docs.js
CHANGED
|
@@ -31,6 +31,12 @@ selects one inline/ref Role and Brain and carries its operational fields.
|
|
|
31
31
|
Agent Templates under \`~/fleet/agent_templates\` are inert reusable launch definitions;
|
|
32
32
|
only explicit files under \`~/fleet/agents\` are persistent lifecycle instances.
|
|
33
33
|
Room members use \`agent_template\` and receive immutable content-addressed snapshots.
|
|
34
|
+
An Agent Template may declare up to 64 temporary-only named \`loops\`; persistent Agent
|
|
35
|
+
instances reject them. Each loop requires \`interval\` (1m..30d) and bounded
|
|
36
|
+
nonblank \`prompt\`, with optional \`enabled\` (default true), \`initial_delay\`
|
|
37
|
+
(default interval, 0s..30d), and \`jitter\` (default 0s, < interval, <=1h).
|
|
38
|
+
Execution is fixed skip-if-busy with no ordinary missed-tick backlog/replay;
|
|
39
|
+
restart recovery may preserve at most one recent late occurrence.
|
|
34
40
|
Legacy top-level \`roles:\` and \`fleet.d\` are rejected. Validate the complete
|
|
35
41
|
trusted source set with \`config\` and \`doctor\` before starting or restarting.
|
|
36
42
|
|
|
@@ -318,8 +324,23 @@ ours-fleet template show team [-c FILE]
|
|
|
318
324
|
ours-fleet task create --title "Solo task" --template single [-c FILE]
|
|
319
325
|
ours-fleet task create --title "Reviewed change" --template pair [-c FILE]
|
|
320
326
|
ours-fleet task create --title "Phased delivery" --template team [-c FILE]
|
|
327
|
+
ours-fleet spawn --temp Scout --role SELECTION --brain SELECTION --loops-file PRIVATE.yaml
|
|
328
|
+
ours-fleet task start TASK --member developer --loops-file PRIVATE.yaml
|
|
329
|
+
ours-fleet task start TASK --member critic --no-loops
|
|
321
330
|
\`\`\`
|
|
322
331
|
|
|
332
|
+
\`--loops-file\` must be an owner-only, non-symlink regular file <=1 MB containing
|
|
333
|
+
exactly one top-level non-empty \`loops:\` mapping. \`--no-loops\` explicitly
|
|
334
|
+
disables loops. They are mutually exclusive and rejected for permanent spawn.
|
|
335
|
+
For a grouped room member the whole CLI block overrides its Agent Template;
|
|
336
|
+
the template overrides omission. Omission preserves historical no-loop behavior
|
|
337
|
+
and never inherits manifest wildcard loops. Fleet validates before side effects,
|
|
338
|
+
seals normalized timings plus exact private prompts, and reuses that snapshot for
|
|
339
|
+
idempotent start, retry, recovery, and replacement. Trusted authoring and the
|
|
340
|
+
private sealed runtime retain exact prompts; resolved launch, task, room,
|
|
341
|
+
provenance, and audit presentations show source, policy, timing, prompt bytes,
|
|
342
|
+
and prompt SHA-256 instead of prompt text.
|
|
343
|
+
|
|
323
344
|
An alternate manifest \`-c /path/custom.yaml\` uses \`/path/custom/\` as its split
|
|
324
345
|
root. Repeated init only fills missing files and never adopts a newer default.
|
|
325
346
|
Revision-3 packaged-bootstrap and generated role defaults have an exact-semantic,
|
|
@@ -131,7 +131,7 @@ export function classifyFleetArgv(argv) {
|
|
|
131
131
|
const marker = (value) => value === '' ? '[REDACTED:empty]' : '[REDACTED:value]';
|
|
132
132
|
const sensitiveValueFlags = new Set([
|
|
133
133
|
'--identity', '--invite', '--token', '--api-token', '--password', '--password-file',
|
|
134
|
-
'--env', '--brief', '--brief-file', '--bio-file', '--persona-file', '--isolation-file',
|
|
134
|
+
'--env', '--brief', '--brief-file', '--bio-file', '--persona-file', '--isolation-file', '--loops-file',
|
|
135
135
|
'--configuration', '-c', '--public-invite', '--public-invite-file', '--invite-file',
|
|
136
136
|
'--summary-file', '--text', '--message', '--summary', '--reason', '--goal', '--cwd',
|
|
137
137
|
'--identity-cid', '--owner-cid', '--contact-cid', '--codex-config', '--add-dir',
|
|
@@ -364,7 +364,7 @@ function validConfiguration(value) {
|
|
|
364
364
|
const count = (v) => v === undefined
|
|
365
365
|
|| (Number.isSafeInteger(v) && Number(v) >= 0 && Number(v) <= 4_096);
|
|
366
366
|
return exact(c, ['version', 'template', 'role', 'brain', 'harness', 'session', 'model',
|
|
367
|
-
'effort', 'mission', 'approval', 'filesystem', 'unattended', 'permissionMode', 'monitor', 'isolation'])
|
|
367
|
+
'effort', 'mission', 'approval', 'filesystem', 'unattended', 'permissionMode', 'monitor', 'isolation', 'loops'])
|
|
368
368
|
&& c.version === 1 && origin(c.role) && origin(c.brain)
|
|
369
369
|
&& text(c.harness, 64) && SESSION_BACKENDS.has(c.session)
|
|
370
370
|
&& (c.model === null || text(c.model, 160))
|
|
@@ -389,6 +389,18 @@ function validConfiguration(value) {
|
|
|
389
389
|
&& (c.isolation.on_unavailable === undefined || ['warn', 'strict'].includes(String(c.isolation.on_unavailable)))
|
|
390
390
|
&& (c.isolation.network === undefined || ['broker', 'deny', 'allow', 'allowlist'].includes(String(c.isolation.network)))
|
|
391
391
|
&& count(c.isolation.read_mounts) && count(c.isolation.write_mounts)))
|
|
392
|
+
&& (c.loops === undefined || (record(c.loops)
|
|
393
|
+
&& exact(c.loops, ['source', 'policy', 'entries'])
|
|
394
|
+
&& ['agent-template', 'cli', 'omitted'].includes(String(c.loops.source))
|
|
395
|
+
&& c.loops.policy === 'skip-if-busy'
|
|
396
|
+
&& Array.isArray(c.loops.entries) && c.loops.entries.length <= 64
|
|
397
|
+
&& c.loops.entries.every(entry => record(entry)
|
|
398
|
+
&& exact(entry, ['name', 'enabled', 'intervalMs', 'initialDelayMs', 'jitterMs', 'prompt'])
|
|
399
|
+
&& text(entry.name, 64) && typeof entry.enabled === 'boolean'
|
|
400
|
+
&& [entry.intervalMs, entry.initialDelayMs, entry.jitterMs].every(Number.isSafeInteger)
|
|
401
|
+
&& record(entry.prompt) && exact(entry.prompt, ['bytes', 'sha256'])
|
|
402
|
+
&& Number.isSafeInteger(entry.prompt.bytes) && Number(entry.prompt.bytes) >= 0
|
|
403
|
+
&& typeof entry.prompt.sha256 === 'string' && /^[a-f0-9]{64}$/.test(entry.prompt.sha256))))
|
|
392
404
|
// Escaping and code-fence growth can push a per-field-valid configuration
|
|
393
405
|
// past the line budget; a v1 configuration whose complete mandatory
|
|
394
406
|
// rendering cannot fit is invalid, never silently trimmed.
|
|
@@ -47,6 +47,22 @@ export interface AgentLaunchConfiguration {
|
|
|
47
47
|
read_mounts?: number;
|
|
48
48
|
write_mounts?: number;
|
|
49
49
|
};
|
|
50
|
+
/** Present only for temporary agents; prompt bodies are never presented. */
|
|
51
|
+
loops?: {
|
|
52
|
+
source: 'agent-template' | 'cli' | 'omitted';
|
|
53
|
+
policy: 'skip-if-busy';
|
|
54
|
+
entries: Array<{
|
|
55
|
+
name: string;
|
|
56
|
+
enabled: boolean;
|
|
57
|
+
intervalMs: number;
|
|
58
|
+
initialDelayMs: number;
|
|
59
|
+
jitterMs: number;
|
|
60
|
+
prompt: {
|
|
61
|
+
bytes: number;
|
|
62
|
+
sha256: string;
|
|
63
|
+
};
|
|
64
|
+
}>;
|
|
65
|
+
};
|
|
50
66
|
}
|
|
51
67
|
/** Where a Brain/Role selection came from; labels are human, hashes secondary. */
|
|
52
68
|
export type SelectionOrigin = {
|
|
@@ -58,6 +58,15 @@ export function summarizeResolvedLaunch(role, origins) {
|
|
|
58
58
|
...(fs?.read?.length ? { read_mounts: fs.read.length } : {}),
|
|
59
59
|
...(fs?.write?.length ? { write_mounts: fs.write.length } : {}),
|
|
60
60
|
} } : {}),
|
|
61
|
+
...(role.temporaryLoopSource ? { loops: {
|
|
62
|
+
source: role.temporaryLoopSource,
|
|
63
|
+
policy: 'skip-if-busy',
|
|
64
|
+
entries: (role.temporaryLoops ?? role.loops ?? []).map(loop => ({
|
|
65
|
+
name: loop.name, enabled: loop.enabled, intervalMs: loop.intervalMs,
|
|
66
|
+
initialDelayMs: loop.initialDelayMs, jitterMs: loop.jitterMs,
|
|
67
|
+
prompt: { bytes: loop.promptBytes, sha256: loop.promptHash },
|
|
68
|
+
})),
|
|
69
|
+
} } : {}),
|
|
61
70
|
};
|
|
62
71
|
// A configuration whose complete mandatory rendering cannot fit must never
|
|
63
72
|
// be produced; real resolved values sit far below the budget, so this is a
|
|
@@ -114,6 +123,15 @@ function optionalComponents(configuration) {
|
|
|
114
123
|
+ `${configuration.isolation.read_mounts || configuration.isolation.write_mounts
|
|
115
124
|
? `, mounts +${configuration.isolation.read_mounts ?? 0}ro/+${configuration.isolation.write_mounts ?? 0}rw` : ''}`
|
|
116
125
|
: undefined,
|
|
126
|
+
configuration.loops
|
|
127
|
+
? configuration.loops.source === 'omitted'
|
|
128
|
+
? 'temporary loops omitted (legacy behavior)'
|
|
129
|
+
: configuration.loops.entries.length === 0
|
|
130
|
+
? `temporary loops disabled (source ${configuration.loops.source})`
|
|
131
|
+
: `temporary loops ${configuration.loops.entries.map(loop => `${loop.name}:${loop.enabled ? 'enabled' : 'disabled'}`
|
|
132
|
+
+ ` interval=${loop.intervalMs}ms delay=${loop.initialDelayMs}ms jitter=${loop.jitterMs}ms`
|
|
133
|
+
+ ` prompt=${loop.prompt.bytes}B/${loop.prompt.sha256.slice(0, 12)}`).join(', ')}; policy skip-if-busy; source ${configuration.loops.source}`
|
|
134
|
+
: undefined,
|
|
117
135
|
].filter((part) => Boolean(part));
|
|
118
136
|
}
|
|
119
137
|
/**
|
package/dist/loops/config.d.ts
CHANGED
|
@@ -7,6 +7,9 @@ export interface LoopConfig {
|
|
|
7
7
|
initial_delay?: string;
|
|
8
8
|
jitter?: string;
|
|
9
9
|
}
|
|
10
|
+
/** Agent Template-local loops are implicitly scoped to the temporary agent. */
|
|
11
|
+
export type AgentLoopConfig = Omit<LoopConfig, 'roles'>;
|
|
12
|
+
export type AgentLoopsConfig = Record<string, AgentLoopConfig>;
|
|
10
13
|
export interface ResolvedLoop {
|
|
11
14
|
name: string;
|
|
12
15
|
selectors: string[];
|
|
@@ -24,7 +27,13 @@ export interface ResolvedRoleLoop extends Omit<ResolvedLoop, 'selectors' | 'role
|
|
|
24
27
|
role: string;
|
|
25
28
|
definitionHash: string;
|
|
26
29
|
}
|
|
27
|
-
export declare function resolveLoops(block: unknown, baseFile: string, roles: ResolvedRole[], vars: Record<string, string
|
|
30
|
+
export declare function resolveLoops(block: unknown, baseFile: string, roles: ResolvedRole[], vars: Record<string, string>, options?: {
|
|
31
|
+
assertTrustedSource?: boolean;
|
|
32
|
+
}): {
|
|
28
33
|
loops: ResolvedLoop[];
|
|
29
34
|
byRole: Map<string, ResolvedRoleLoop[]>;
|
|
30
35
|
};
|
|
36
|
+
/** Resolve the canonical Agent Template schema without inventing a second loop language. */
|
|
37
|
+
export declare function resolveAgentLoops(block: unknown, role: ResolvedRole, sourceFile?: string): ResolvedRoleLoop[];
|
|
38
|
+
/** Fully explicit private snapshot form; prompt text is retained for deterministic recovery. */
|
|
39
|
+
export declare function canonicalAgentLoops(loops: ResolvedRoleLoop[]): AgentLoopsConfig;
|
package/dist/loops/config.js
CHANGED
|
@@ -2,13 +2,14 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { lstatSync } from 'node:fs';
|
|
3
3
|
import { ConfigError, ROLE_NAME_RE } from '../config.js';
|
|
4
4
|
import { sessionBackendCapabilities } from '../session/types.js';
|
|
5
|
-
import { parseDuration } from '../duration.js';
|
|
5
|
+
import { formatDuration, parseDuration } from '../duration.js';
|
|
6
6
|
const LOOP_KEYS = ['roles', 'interval', 'prompt', 'enabled', 'initial_delay', 'jitter'];
|
|
7
7
|
const MIN_INTERVAL_MS = 60_000;
|
|
8
8
|
const MAX_DURATION_MS = 30 * 24 * 60 * 60 * 1_000;
|
|
9
9
|
const MAX_JITTER_MS = 60 * 60 * 1_000;
|
|
10
10
|
const MAX_PROMPT_BYTES = 16 * 1024;
|
|
11
11
|
const MAX_PROMPT_SCALARS = 12_000;
|
|
12
|
+
const MAX_AGENT_LOOPS = 64;
|
|
12
13
|
function substitute(value, vars) {
|
|
13
14
|
if (typeof value === 'string')
|
|
14
15
|
return value.replace(/\$\{(\w+)\}/g, (match, key) => key in vars ? String(vars[key]) : match);
|
|
@@ -48,7 +49,7 @@ function normalizePrompt(raw, where) {
|
|
|
48
49
|
function digest(value) {
|
|
49
50
|
return createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
50
51
|
}
|
|
51
|
-
export function resolveLoops(block, baseFile, roles, vars) {
|
|
52
|
+
export function resolveLoops(block, baseFile, roles, vars, options = {}) {
|
|
52
53
|
const byRole = new Map(roles.map(role => [role.name, []]));
|
|
53
54
|
if (block === undefined || block === null)
|
|
54
55
|
return { loops: [], byRole };
|
|
@@ -114,10 +115,41 @@ export function resolveLoops(block, baseFile, roles, vars) {
|
|
|
114
115
|
for (const values of byRole.values())
|
|
115
116
|
values.sort((a, b) => a.name.localeCompare(b.name));
|
|
116
117
|
out.sort((a, b) => a.name.localeCompare(b.name));
|
|
117
|
-
if (out.some(loop => loop.enabled))
|
|
118
|
+
if (out.some(loop => loop.enabled) && options.assertTrustedSource !== false)
|
|
118
119
|
assertSafeLoopConfig(baseFile);
|
|
119
120
|
return { loops: out, byRole };
|
|
120
121
|
}
|
|
122
|
+
/** Resolve the canonical Agent Template schema without inventing a second loop language. */
|
|
123
|
+
export function resolveAgentLoops(block, role, sourceFile = '(temporary Agent loop)') {
|
|
124
|
+
if (block === undefined)
|
|
125
|
+
return [];
|
|
126
|
+
if (!block || typeof block !== 'object' || Array.isArray(block))
|
|
127
|
+
throw new ConfigError(`${sourceFile}: Agent loops must be a non-empty map`);
|
|
128
|
+
if (!Object.keys(block).length)
|
|
129
|
+
throw new ConfigError(`${sourceFile}: Agent loops must be non-empty; use an explicit no-loops override to disable template loops`);
|
|
130
|
+
if (Object.keys(block).length > MAX_AGENT_LOOPS)
|
|
131
|
+
throw new ConfigError(`${sourceFile}: Agent loops may contain at most ${MAX_AGENT_LOOPS} entries`);
|
|
132
|
+
const scoped = Object.fromEntries(Object.entries(block).map(([name, raw]) => {
|
|
133
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
134
|
+
return [name, raw];
|
|
135
|
+
const value = raw;
|
|
136
|
+
if (Object.hasOwn(value, 'roles'))
|
|
137
|
+
throw new ConfigError(`${sourceFile}: Agent loop '${name}' has unknown key roles; Agent loops are implicitly scoped`);
|
|
138
|
+
return [name, { ...value, roles: [role.name] }];
|
|
139
|
+
}));
|
|
140
|
+
return resolveLoops(scoped, sourceFile, [role], {}, { assertTrustedSource: false })
|
|
141
|
+
.byRole.get(role.name) ?? [];
|
|
142
|
+
}
|
|
143
|
+
/** Fully explicit private snapshot form; prompt text is retained for deterministic recovery. */
|
|
144
|
+
export function canonicalAgentLoops(loops) {
|
|
145
|
+
return Object.fromEntries(loops.map(loop => [loop.name, {
|
|
146
|
+
enabled: loop.enabled,
|
|
147
|
+
interval: formatDuration(loop.intervalMs),
|
|
148
|
+
initial_delay: formatDuration(loop.initialDelayMs),
|
|
149
|
+
jitter: formatDuration(loop.jitterMs),
|
|
150
|
+
prompt: loop.prompt,
|
|
151
|
+
}]));
|
|
152
|
+
}
|
|
121
153
|
function assertSafeLoopConfig(path) {
|
|
122
154
|
let stat;
|
|
123
155
|
try {
|
package/dist/resolved-plan.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { analyzeRolePermissions } from './permissions.js';
|
|
2
2
|
import { dirname, relative } from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
3
4
|
import { isSensitiveConfigKey } from './sensitive-config.js';
|
|
4
5
|
export const RESOLVED_PLAN_SCHEMA_VERSION = 2;
|
|
5
6
|
const sortedObject = (value) => Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)));
|
|
6
7
|
export const redactSensitive = (value, key = '') => {
|
|
8
|
+
if (key === 'prompt' && typeof value === 'string')
|
|
9
|
+
return {
|
|
10
|
+
bytes: Buffer.byteLength(value, 'utf8'),
|
|
11
|
+
sha256: createHash('sha256').update(value).digest('hex'),
|
|
12
|
+
};
|
|
7
13
|
if (isSensitiveConfigKey(key))
|
|
8
14
|
return '<redacted>';
|
|
9
15
|
if (Array.isArray(value))
|
|
@@ -48,6 +54,7 @@ export function resolvedPlan(cfg) {
|
|
|
48
54
|
}
|
|
49
55
|
export function resolvedRolePlan(role, manifestDir = dirname(role.sourceFile)) {
|
|
50
56
|
const analysis = analyzeRolePermissions(role);
|
|
57
|
+
const effectiveLoops = role.temporaryLoopSource ? (role.temporaryLoops ?? []) : (role.loops ?? []);
|
|
51
58
|
return {
|
|
52
59
|
name: role.name,
|
|
53
60
|
sourceFile: role.sourceFile,
|
|
@@ -87,7 +94,8 @@ export function resolvedRolePlan(role, manifestDir = dirname(role.sourceFile)) {
|
|
|
87
94
|
},
|
|
88
95
|
monitor: role.monitor,
|
|
89
96
|
ownerChannel: role.owner_channel ?? null,
|
|
90
|
-
|
|
97
|
+
temporaryLoopSource: role.temporaryLoopSource ?? null,
|
|
98
|
+
loops: effectiveLoops.map(loop => ({
|
|
91
99
|
name: loop.name, enabled: loop.enabled, intervalMs: loop.intervalMs,
|
|
92
100
|
initialDelayMs: loop.initialDelayMs, jitterMs: loop.jitterMs,
|
|
93
101
|
definitionHash: loop.definitionHash, prompt: { bytes: loop.promptBytes, sha256: loop.promptHash },
|
package/dist/rooms-tasks/cli.js
CHANGED
|
@@ -8,14 +8,17 @@ import { acquireLaunchSnapshotLock, releaseLaunchSnapshot } from './launch-snaps
|
|
|
8
8
|
import { parseGroupedMemberArgs, readMembersFile } from './member-overrides.js';
|
|
9
9
|
const MEMBER_ARG_FLAGS = new Set([
|
|
10
10
|
'--member', '--agent-template', '--brain', '--role', '--approval', '--filesystem', '--unattended',
|
|
11
|
-
'--cwd', '--model', '--effort',
|
|
11
|
+
'--cwd', '--model', '--effort', '--loops-file', '--no-loops',
|
|
12
12
|
]);
|
|
13
13
|
export function cliMemberOverrides(membersFile, argv = process.argv.slice(2)) {
|
|
14
14
|
const grouped = [];
|
|
15
15
|
for (let i = 0; i < argv.length; i++)
|
|
16
16
|
if (MEMBER_ARG_FLAGS.has(argv[i])) {
|
|
17
|
-
grouped.push(argv[i]
|
|
18
|
-
i
|
|
17
|
+
grouped.push(argv[i]);
|
|
18
|
+
if (argv[i] !== '--no-loops') {
|
|
19
|
+
grouped.push(argv[i + 1]);
|
|
20
|
+
i += 1;
|
|
21
|
+
}
|
|
19
22
|
}
|
|
20
23
|
if (membersFile && grouped.length)
|
|
21
24
|
throw new Error('--members-file cannot be combined with grouped --member options');
|
|
@@ -712,6 +715,8 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
712
715
|
.option('--cwd <path>', 'working directory for the current member block')
|
|
713
716
|
.option('--model <id>', 'model for the current member block')
|
|
714
717
|
.option('--effort <level>', 'reasoning effort for the current member block')
|
|
718
|
+
.option('--loops-file <path>', 'owner-only loops: YAML for the current temporary member')
|
|
719
|
+
.option('--no-loops', 'disable loops for the current temporary member')
|
|
715
720
|
.option('--json', 'JSON output')
|
|
716
721
|
.action(async (opts, command) => {
|
|
717
722
|
try {
|
|
@@ -926,6 +931,8 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
926
931
|
.option('--cwd <path>', 'working directory for current member')
|
|
927
932
|
.option('--model <id>', 'model for current member')
|
|
928
933
|
.option('--effort <level>', 'reasoning effort for current member')
|
|
934
|
+
.option('--loops-file <path>', 'owner-only loops: YAML for current member')
|
|
935
|
+
.option('--no-loops', 'disable loops for current member')
|
|
929
936
|
.option('--json', 'JSON output')
|
|
930
937
|
.action(async (id, opts, command) => {
|
|
931
938
|
try {
|
|
@@ -1336,6 +1343,8 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
1336
1343
|
.option('--cwd <path>', 'working directory for current member')
|
|
1337
1344
|
.option('--model <id>', 'model for current member')
|
|
1338
1345
|
.option('--effort <level>', 'reasoning effort for current member')
|
|
1346
|
+
.option('--loops-file <path>', 'owner-only loops: YAML for current member')
|
|
1347
|
+
.option('--no-loops', 'disable loops for current member')
|
|
1339
1348
|
.option('--json', 'JSON output')
|
|
1340
1349
|
.action(async (id, opts, command) => {
|
|
1341
1350
|
try {
|
|
@@ -1446,6 +1455,8 @@ export function registerRoomCommands(parent, cOpt) {
|
|
|
1446
1455
|
.option('--cwd <path>', 'working directory for current member')
|
|
1447
1456
|
.option('--model <id>', 'model for current member')
|
|
1448
1457
|
.option('--effort <level>', 'reasoning effort for current member')
|
|
1458
|
+
.option('--loops-file <path>', 'owner-only loops: YAML for current member')
|
|
1459
|
+
.option('--no-loops', 'disable loops for current member')
|
|
1449
1460
|
.option('--json', 'JSON output')
|
|
1450
1461
|
.action(async (opts, command) => {
|
|
1451
1462
|
try {
|
|
@@ -99,6 +99,11 @@ function sweepUnreferencedSnapshotsLocked() {
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
export function redactLaunchDefinition(value, key = '') {
|
|
102
|
+
if (key === 'prompt' && typeof value === 'string')
|
|
103
|
+
return {
|
|
104
|
+
bytes: Buffer.byteLength(value, 'utf8'),
|
|
105
|
+
sha256: createHash('sha256').update(value).digest('hex'),
|
|
106
|
+
};
|
|
102
107
|
if (['env', 'harness_options', 'session_options', 'owner_channel', 'auth_proxy'].includes(key))
|
|
103
108
|
return '<redacted>';
|
|
104
109
|
if (Array.isArray(value))
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type AgentDefinition, type AgentTemplateDefinition, type FleetConfig } from '../config.js';
|
|
2
2
|
import type { TemplateDefinition, TemplateSnapshot } from './types.js';
|
|
3
|
+
import type { AgentLoopsConfig } from '../loops/config.js';
|
|
3
4
|
export interface MemberOverride {
|
|
4
5
|
agent_template?: string;
|
|
5
6
|
brain?: string;
|
|
@@ -10,6 +11,8 @@ export interface MemberOverride {
|
|
|
10
11
|
cwd?: string;
|
|
11
12
|
model?: string;
|
|
12
13
|
effort?: string;
|
|
14
|
+
/** Whole-block override; false is the explicit no-loops policy. */
|
|
15
|
+
loops?: AgentLoopsConfig | false;
|
|
13
16
|
overrides?: Partial<AgentDefinition>;
|
|
14
17
|
}
|
|
15
18
|
export type MemberOverrides = Record<string, MemberOverride>;
|
|
@@ -6,6 +6,8 @@ import { analyzeRolePermissions } from '../permissions.js';
|
|
|
6
6
|
import { canonicalJson } from '../canonical-json.js';
|
|
7
7
|
import { redactLaunchDefinition } from './launch-snapshot.js';
|
|
8
8
|
import { snapshotTemplate } from './templates.js';
|
|
9
|
+
import { readLoopsFile } from '../spawn.js';
|
|
10
|
+
import { canonicalAgentLoops, resolveAgentLoops } from '../loops/config.js';
|
|
9
11
|
export function hashMemberOverrides(overrides) {
|
|
10
12
|
return createHash('sha256').update(canonicalJson(overrides)).digest('hex');
|
|
11
13
|
}
|
|
@@ -16,20 +18,37 @@ const OPTION_FIELDS = {
|
|
|
16
18
|
};
|
|
17
19
|
/** Parse only the ordered member-option subsequence supplied by the CLI action. */
|
|
18
20
|
export function parseGroupedMemberArgs(argv) {
|
|
19
|
-
if (argv.length % 2)
|
|
20
|
-
throw new Error(`${argv.at(-1)}: value required`);
|
|
21
21
|
const result = {};
|
|
22
22
|
let current;
|
|
23
|
-
for (let index = 0; index < argv.length;
|
|
23
|
+
for (let index = 0; index < argv.length;) {
|
|
24
24
|
const flag = argv[index];
|
|
25
|
+
if (flag === '--no-loops') {
|
|
26
|
+
if (!current)
|
|
27
|
+
throw new Error(`${flag} must follow --member <slot>`);
|
|
28
|
+
if (result[current].loops !== undefined)
|
|
29
|
+
throw new Error(`duplicate loop override for member '${current}'`);
|
|
30
|
+
result[current].loops = false;
|
|
31
|
+
index += 1;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
25
34
|
const value = argv[index + 1];
|
|
26
|
-
if (!value)
|
|
35
|
+
if (!value || value.startsWith('--'))
|
|
27
36
|
throw new Error(`${flag}: value required`);
|
|
28
37
|
if (flag === '--member') {
|
|
29
38
|
if (result[value])
|
|
30
39
|
throw new Error(`duplicate member slot '${value}'`);
|
|
31
40
|
current = value;
|
|
32
41
|
result[current] = {};
|
|
42
|
+
index += 2;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (flag === '--loops-file') {
|
|
46
|
+
if (!current)
|
|
47
|
+
throw new Error(`${flag} must follow --member <slot>`);
|
|
48
|
+
if (result[current].loops !== undefined)
|
|
49
|
+
throw new Error(`duplicate loop override for member '${current}'`);
|
|
50
|
+
result[current].loops = readLoopsFile(value);
|
|
51
|
+
index += 2;
|
|
33
52
|
continue;
|
|
34
53
|
}
|
|
35
54
|
const field = OPTION_FIELDS[flag];
|
|
@@ -40,6 +59,7 @@ export function parseGroupedMemberArgs(argv) {
|
|
|
40
59
|
if (result[current][field] !== undefined)
|
|
41
60
|
throw new Error(`duplicate ${flag} for member '${current}'`);
|
|
42
61
|
result[current][field] = value;
|
|
62
|
+
index += 2;
|
|
43
63
|
}
|
|
44
64
|
for (const [slot, value] of Object.entries(result))
|
|
45
65
|
validateMemberOverride(value, `member '${slot}'`);
|
|
@@ -60,7 +80,7 @@ export function readMembersFile(path) {
|
|
|
60
80
|
validateMemberOverride(value, `${path}: member '${slot}'`);
|
|
61
81
|
return members;
|
|
62
82
|
}
|
|
63
|
-
const MEMBER_KEYS = new Set(['agent_template', 'brain', 'role', 'approval', 'filesystem', 'unattended', 'cwd', 'model', 'effort', 'overrides']);
|
|
83
|
+
const MEMBER_KEYS = new Set(['agent_template', 'brain', 'role', 'approval', 'filesystem', 'unattended', 'cwd', 'model', 'effort', 'overrides', 'loops']);
|
|
64
84
|
function validateMemberOverride(value, where) {
|
|
65
85
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
66
86
|
throw new Error(`${where}: must be a mapping`);
|
|
@@ -81,6 +101,11 @@ function validateMemberOverride(value, where) {
|
|
|
81
101
|
throw new Error(`${where}: ${key} must be a non-blank string`);
|
|
82
102
|
if (item.overrides !== undefined && (!item.overrides || typeof item.overrides !== 'object' || Array.isArray(item.overrides)))
|
|
83
103
|
throw new Error(`${where}: overrides must be a mapping`);
|
|
104
|
+
if (item.overrides && Object.hasOwn(item.overrides, 'loops'))
|
|
105
|
+
throw new Error(`${where}: overrides.loops is unsupported; use the top-level loops member override`);
|
|
106
|
+
if (item.loops !== undefined && item.loops !== false
|
|
107
|
+
&& (!item.loops || typeof item.loops !== 'object' || Array.isArray(item.loops)))
|
|
108
|
+
throw new Error(`${where}: loops must be a mapping or false`);
|
|
84
109
|
}
|
|
85
110
|
const MAPS = new Set(['permissions', 'env', 'isolation', 'monitor', 'owner_channel', 'worklog', 'auth_proxy']);
|
|
86
111
|
function merge(base, overlay) {
|
|
@@ -95,6 +120,8 @@ function merge(base, overlay) {
|
|
|
95
120
|
return out;
|
|
96
121
|
}
|
|
97
122
|
export function prepareExecutionPlan(template, cfg, overrides = {}) {
|
|
123
|
+
for (const [slot, value] of Object.entries(overrides))
|
|
124
|
+
validateMemberOverride(value, `member '${slot}'`);
|
|
98
125
|
const slots = new Set(template.members.map(member => member.slot));
|
|
99
126
|
for (const slot of Object.keys(overrides))
|
|
100
127
|
if (!slots.has(slot))
|
|
@@ -108,6 +135,10 @@ export function prepareExecutionPlan(template, cfg, overrides = {}) {
|
|
|
108
135
|
if (!base)
|
|
109
136
|
throw new Error(`Agent Template '${source}' not found`);
|
|
110
137
|
let definition = merge(base, (input.overrides ?? {}));
|
|
138
|
+
if (input.loops === false)
|
|
139
|
+
delete definition.loops;
|
|
140
|
+
else if (input.loops !== undefined)
|
|
141
|
+
definition.loops = structuredClone(input.loops);
|
|
111
142
|
const rolePreset = input.role ? cfg.rolePresets?.[input.role] : undefined;
|
|
112
143
|
if (input.role && !rolePreset)
|
|
113
144
|
throw new Error(`Role '${input.role}' not found`);
|
|
@@ -144,6 +175,8 @@ export function prepareExecutionPlan(template, cfg, overrides = {}) {
|
|
|
144
175
|
const role = cfg.resolveAgentDefinition(`RoomMember_${member.slot}`, {
|
|
145
176
|
...definition, identity: `RoomMember_${member.slot}`,
|
|
146
177
|
});
|
|
178
|
+
if (definition.loops !== undefined)
|
|
179
|
+
definition.loops = canonicalAgentLoops(resolveAgentLoops(definition.loops, role, `(member '${member.slot}' resolved loops)`));
|
|
147
180
|
const analysis = analyzeRolePermissions(role);
|
|
148
181
|
if (analysis.conflicts?.length)
|
|
149
182
|
throw new Error(`member '${member.slot}' has native permission conflicts: ${analysis.conflicts.map(item => item.warning).join('; ')}`);
|
|
@@ -154,6 +187,7 @@ export function prepareExecutionPlan(template, cfg, overrides = {}) {
|
|
|
154
187
|
launchDefinitions[id] = definition;
|
|
155
188
|
const hash = createHash('sha256').update(canonicalJson(definition)).digest('hex');
|
|
156
189
|
return { ...member, agent_template: source, launch_definition_id: id,
|
|
190
|
+
loop_source: input.loops !== undefined ? 'cli' : base.loops !== undefined ? 'agent-template' : 'omitted',
|
|
157
191
|
agent_template_hash: hash,
|
|
158
192
|
...(input.role ? { role_preset: { id: input.role, hash: createHash('sha256').update(canonicalJson(rolePreset)).digest('hex') } } : {}),
|
|
159
193
|
...(input.brain ? { brain_preset: { id: input.brain, hash: createHash('sha256').update(canonicalJson(brainPreset)).digest('hex') } } : {}),
|
|
@@ -100,6 +100,7 @@ function expandMembers(template, prefix) {
|
|
|
100
100
|
agentTemplate: slot.agent_template,
|
|
101
101
|
launchDefinitionId: slot.launch_definition_id ?? slot.agent_template,
|
|
102
102
|
agentTemplateHash: slot.agent_template_hash,
|
|
103
|
+
loopSource: slot.loop_source ?? 'omitted',
|
|
103
104
|
});
|
|
104
105
|
}
|
|
105
106
|
}
|
|
@@ -121,6 +122,7 @@ function settingsFor(member, cfg, sealed) {
|
|
|
121
122
|
template: member.agentTemplate,
|
|
122
123
|
templateHash: member.agentTemplateHash
|
|
123
124
|
?? createHash('sha256').update(canonicalJson(definition)).digest('hex'),
|
|
125
|
+
loopSource: member.loopSource,
|
|
124
126
|
};
|
|
125
127
|
}
|
|
126
128
|
function roomTask(input, member, settings, members, roomIdentityCid, ownerSeatCid) {
|
|
@@ -286,6 +288,9 @@ async function launchMemberUnlocked(input) {
|
|
|
286
288
|
temp: true,
|
|
287
289
|
identity: member.name,
|
|
288
290
|
agentDefinition: settings.definition,
|
|
291
|
+
...(settings.loopSource === 'cli' && settings.definition.loops === undefined
|
|
292
|
+
? { noLoops: true } : {}),
|
|
293
|
+
loopSource: settings.loopSource,
|
|
289
294
|
surface: 'agent',
|
|
290
295
|
creationActionId: actionId,
|
|
291
296
|
roomMemberStartup: startup,
|
|
@@ -276,6 +276,8 @@ export interface TemplateDefinition {
|
|
|
276
276
|
members: TemplateMemberSlot[];
|
|
277
277
|
}
|
|
278
278
|
export interface TemplateSnapshotMember extends TemplateMemberSlot {
|
|
279
|
+
/** Source of the effective temporary-loop policy, independent of its sealed value. */
|
|
280
|
+
loop_source?: 'agent-template' | 'cli' | 'omitted';
|
|
279
281
|
/** Secret-safe public projection; launch material lives in the sealed snapshot. */
|
|
280
282
|
agent_projection?: Record<string, unknown>;
|
|
281
283
|
agent_template_hash?: string;
|
package/dist/runner.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { AgentSession, ExitRecord, TurnResult } from './session/types.js';
|
|
|
7
7
|
import type { AgentSessionAdapter, AgentSessionStartOptions } from './harness/agent-session.js';
|
|
8
8
|
import { type OwnerChannelHandle, type OwnerChannelOptions } from './owner-channel/channel.js';
|
|
9
9
|
import { type OwnerBinderLease } from './owner-channel/binder.js';
|
|
10
|
+
import { ScheduledLoopManager, type ScheduledLoopManagerHandle } from './loops/manager.js';
|
|
10
11
|
export interface RunnerDeps {
|
|
11
12
|
exec: Exec;
|
|
12
13
|
cpuDelegated(): boolean;
|
|
@@ -26,6 +27,8 @@ export interface RunnerDeps {
|
|
|
26
27
|
startAgentSession(adapter: AgentSessionAdapter, options: AgentSessionStartOptions): Promise<AgentSession>;
|
|
27
28
|
/** Construct the authenticated role control route (injectable where sockets are unavailable). */
|
|
28
29
|
createControlServer(stateDir: string, session: AgentSession, log: (line: string) => void): Pick<RoleControlServer, 'start' | 'close' | 'setFleetSpawner' | 'setFleetAuditor' | 'setOwnerChannel' | 'setConfigReloader' | 'setLoopManager'>;
|
|
30
|
+
/** Construct scheduled-loop execution (injectable for fail-closed startup tests). */
|
|
31
|
+
createLoopManager(...args: ConstructorParameters<typeof ScheduledLoopManager>): ScheduledLoopManagerHandle;
|
|
29
32
|
/** Acquire the cross-process owner-channel binder lease before replacing the control socket. */
|
|
30
33
|
acquireOwnerBinder(stateDir: string, role: string, identity: string): Promise<OwnerBinderLease>;
|
|
31
34
|
/** Ask the still-authenticated predecessor to emit the fixed recovery notice. */
|
package/dist/runner.js
CHANGED
|
@@ -53,6 +53,7 @@ const defaultDeps = () => ({
|
|
|
53
53
|
createOwnerChannel: opts => new OwnerChannel(opts),
|
|
54
54
|
startAgentSession: (adapter, options) => adapter.start(options),
|
|
55
55
|
createControlServer: (stateDir, session, log) => new RoleControlServer(stateDir, session, log),
|
|
56
|
+
createLoopManager: (...args) => new ScheduledLoopManager(...args),
|
|
56
57
|
acquireOwnerBinder: (stateDir, role, identity) => acquireOwnerBinderLease(stateDir, role, identity),
|
|
57
58
|
reportOwnerStartupFailure: async (stateDir) => {
|
|
58
59
|
const response = await controlRequest(stateDir, {
|
|
@@ -774,7 +775,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
774
775
|
if (generation === loopGeneration && (loopManager || !definitions.length))
|
|
775
776
|
return { changed: false, loops: definitions.length };
|
|
776
777
|
if (!loopManager && definitions.length) {
|
|
777
|
-
loopManager =
|
|
778
|
+
loopManager = deps.createLoopManager(name, definitions, dir, arbiter, {
|
|
778
779
|
now: deps.now,
|
|
779
780
|
setTimer: (callback, ms) => setTimeout(callback, ms),
|
|
780
781
|
clearTimer: timer => clearTimeout(timer),
|
|
@@ -790,10 +791,12 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
790
791
|
deps.log(`[${name}] scheduled loops reloaded (${definitions.length} definitions)`);
|
|
791
792
|
return { changed: true, loops: definitions.length };
|
|
792
793
|
};
|
|
793
|
-
|
|
794
|
+
// Temporary agents are immutable launch snapshots: never re-resolve mutable Fleet YAML.
|
|
795
|
+
if (!temp)
|
|
796
|
+
control.setConfigReloader(reloadLoopConfig);
|
|
794
797
|
if (role.loops?.length) {
|
|
795
798
|
try {
|
|
796
|
-
loopManager =
|
|
799
|
+
loopManager = deps.createLoopManager(name, role.loops, dir, arbiter, {
|
|
797
800
|
now: deps.now,
|
|
798
801
|
setTimer: (callback, ms) => setTimeout(callback, ms),
|
|
799
802
|
clearTimer: timer => clearTimeout(timer),
|
|
@@ -803,6 +806,17 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
803
806
|
loopManager.start();
|
|
804
807
|
}
|
|
805
808
|
catch (error) {
|
|
809
|
+
if (temp) {
|
|
810
|
+
monitor?.stop();
|
|
811
|
+
if (monitorLoop)
|
|
812
|
+
await monitorLoop;
|
|
813
|
+
await control.close();
|
|
814
|
+
ownerBinder?.release();
|
|
815
|
+
await agentSession.close();
|
|
816
|
+
unsubscribeRecovery?.();
|
|
817
|
+
throw new Error(`[${name}] configured temporary loop manager failed to start: `
|
|
818
|
+
+ `${error?.message ?? String(error)}`);
|
|
819
|
+
}
|
|
806
820
|
deps.log(`[${name}] scheduled loop manager unavailable: ${error?.name ?? 'Error'}`);
|
|
807
821
|
}
|
|
808
822
|
}
|
package/dist/spawn.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { IsolationConfig } from './isolation/types.js';
|
|
2
|
+
import type { AgentLoopsConfig } from './loops/config.js';
|
|
2
3
|
import { type ApprovalMode, type FilesystemMode, type ResolvedRole, type MonitorConfig, type UnattendedMode, type RoomMemberStartup, type AgentDefinition, type AgentSelection } from './config.js';
|
|
3
4
|
import { type OpsDeps } from './ops.js';
|
|
4
5
|
import { type CreationDeps, type CreationProvenance } from './creation.js';
|
|
@@ -40,6 +41,12 @@ export interface SpawnOpts {
|
|
|
40
41
|
* input in this release.
|
|
41
42
|
*/
|
|
42
43
|
isolationFile?: string;
|
|
44
|
+
/** Trusted YAML containing exactly a top-level `loops:` map. Temporary launches only. */
|
|
45
|
+
loopsFile?: string;
|
|
46
|
+
/** Explicitly override any selected Agent Template loops with none. */
|
|
47
|
+
noLoops?: boolean;
|
|
48
|
+
/** Internal source label retained after room-member plan resolution. */
|
|
49
|
+
loopSource?: 'agent-template' | 'cli' | 'omitted';
|
|
43
50
|
overseeInterval?: string;
|
|
44
51
|
configPath?: string;
|
|
45
52
|
dryRun?: boolean;
|
|
@@ -56,6 +63,8 @@ export declare function agentDefinitionFromSpawn(o: SpawnOpts): AgentDefinition;
|
|
|
56
63
|
* must fail before any artifact exists.
|
|
57
64
|
*/
|
|
58
65
|
export declare function readIsolationFile(path: string): IsolationConfig;
|
|
66
|
+
/** Read a private canonical temporary-loop override before any creation side effect. */
|
|
67
|
+
export declare function readLoopsFile(path: string): AgentLoopsConfig;
|
|
59
68
|
export declare function validateSpawnOpts(o: SpawnOpts): void;
|
|
60
69
|
/** The ours identity a spawn will bind: explicit, else the role name. */
|
|
61
70
|
export declare const effectiveIdentity: (o: SpawnOpts) => string;
|