@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/supervisor/none.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Tmux, tmuxArgs } from '../tmux.js';
|
|
2
2
|
import { realExec, shq } from '../exec.js';
|
|
3
3
|
/**
|
|
4
|
-
* No supervision:
|
|
4
|
+
* No service supervision: tmux contains the direct runner process, nothing survives a reboot and
|
|
5
5
|
* nothing restarts on crash. Used for temp agents and CI tests
|
|
6
6
|
* (OURS_FLEET_SUPERVISOR=none).
|
|
7
7
|
*/
|
|
@@ -9,7 +9,7 @@ export function makeNoneBackend(exec = realExec) {
|
|
|
9
9
|
const tmux = new Tmux(exec);
|
|
10
10
|
return {
|
|
11
11
|
id: 'none',
|
|
12
|
-
async init() { return ['no supervisor:
|
|
12
|
+
async init() { return ['no service supervisor: direct runner containment only (no reboot survival)']; },
|
|
13
13
|
async install(name, binPath) {
|
|
14
14
|
const existed = await tmux.kill(name); // true when a session was there
|
|
15
15
|
await tmux.newSession(name, process.cwd(), `${shq(binPath)} _run ${shq(name)}`);
|
package/dist/temp-lifecycle.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export declare const TEMP_STOP_REQUEST_FILE = ".temp-stop-request.json";
|
|
|
5
5
|
export declare const TEMP_RECLAIM_BATCH = 32;
|
|
6
6
|
export declare const TEMP_LAUNCH_GRACE_MS = 60000;
|
|
7
7
|
export type TempSupervisorKind = 'systemd-transient' | 'launchd-transient' | 'detached';
|
|
8
|
-
export type TempTerminationReason = 'identity-closed' | 'session-ended' | 'operator-stop' | 'supervisor-signal' | 'startup-failure' | 'stale-supervisor';
|
|
8
|
+
export type TempTerminationReason = 'identity-closed' | 'session-ended' | 'operator-stop' | 'supervisor-signal' | 'supervisor-recycle' | 'startup-failure' | 'stale-supervisor';
|
|
9
9
|
export interface TempSupervisorRecord {
|
|
10
10
|
version: 1;
|
|
11
11
|
role: string;
|
package/dist/tmux.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export declare const tmuxSocket: (session: string) => string;
|
|
|
20
20
|
* the shared default server and re-creates #32.
|
|
21
21
|
*/
|
|
22
22
|
export declare const tmuxArgs: (session: string, args: string[]) => string[];
|
|
23
|
-
/** Thin tmux wrapper
|
|
23
|
+
/** Thin tmux wrapper retained only for supervisor=none process containment. */
|
|
24
24
|
export declare class Tmux {
|
|
25
25
|
private exec;
|
|
26
26
|
constructor(exec?: Exec);
|
|
@@ -32,17 +32,4 @@ export declare class Tmux {
|
|
|
32
32
|
* program inside it exited on its own".
|
|
33
33
|
*/
|
|
34
34
|
kill(name: string): Promise<boolean>;
|
|
35
|
-
capture(name: string, lines?: number): Promise<string>;
|
|
36
|
-
/** Bounded ANSI/history seed for the browser terminal projection. */
|
|
37
|
-
captureHistory(name: string, lines?: number): Promise<string>;
|
|
38
|
-
panePid(name: string): Promise<number | null>;
|
|
39
|
-
/**
|
|
40
|
-
* List the live sessions among `names`, asking each server in turn.
|
|
41
|
-
* There is no fleet-wide `tmux ls` any more: a session per server means the
|
|
42
|
-
* caller says which sessions to ask about. A server that is not running
|
|
43
|
-
* answers non-zero and contributes nothing.
|
|
44
|
-
*/
|
|
45
|
-
list(names: readonly string[]): Promise<string>;
|
|
46
|
-
sendText(name: string, text: string): Promise<void>;
|
|
47
|
-
sendKey(name: string, key: string): Promise<void>;
|
|
48
35
|
}
|
package/dist/tmux.js
CHANGED
|
@@ -20,7 +20,7 @@ export const tmuxSocket = (session) => `${TMUX_SOCKET_PREFIX}${session}`;
|
|
|
20
20
|
* the shared default server and re-creates #32.
|
|
21
21
|
*/
|
|
22
22
|
export const tmuxArgs = (session, args) => ['-L', tmuxSocket(session), ...args];
|
|
23
|
-
/** Thin tmux wrapper
|
|
23
|
+
/** Thin tmux wrapper retained only for supervisor=none process containment. */
|
|
24
24
|
export class Tmux {
|
|
25
25
|
exec;
|
|
26
26
|
constructor(exec = realExec) {
|
|
@@ -42,54 +42,4 @@ export class Tmux {
|
|
|
42
42
|
async kill(name) {
|
|
43
43
|
return (await this.exec('tmux', tmuxArgs(name, ['kill-session', '-t', name]))).code === 0;
|
|
44
44
|
}
|
|
45
|
-
async capture(name, lines = 40) {
|
|
46
|
-
const r = await this.exec('tmux', tmuxArgs(name, ['capture-pane', '-t', name, '-p']));
|
|
47
|
-
if (r.code !== 0)
|
|
48
|
-
throw new Error(`tmux capture-pane '${name}' failed: ${r.stderr.trim()}`);
|
|
49
|
-
const all = r.stdout.replace(/\n+$/, '').split('\n');
|
|
50
|
-
return all.slice(-lines).join('\n');
|
|
51
|
-
}
|
|
52
|
-
/** Bounded ANSI/history seed for the browser terminal projection. */
|
|
53
|
-
async captureHistory(name, lines = 5_000) {
|
|
54
|
-
const bounded = Math.min(Math.max(Math.trunc(lines), 1), 20_000);
|
|
55
|
-
const r = await this.exec('tmux', tmuxArgs(name, ['capture-pane', '-t', name, '-p', '-e', '-J', '-S', `-${bounded}`]));
|
|
56
|
-
if (r.code !== 0)
|
|
57
|
-
throw new Error(`tmux capture-pane '${name}' failed: ${r.stderr.trim()}`);
|
|
58
|
-
return Buffer.from(r.stdout).subarray(0, 4 * 1024 * 1024).toString();
|
|
59
|
-
}
|
|
60
|
-
async panePid(name) {
|
|
61
|
-
const r = await this.exec('tmux', tmuxArgs(name, ['list-panes', '-t', name, '-F', '#{pane_pid}']));
|
|
62
|
-
if (r.code !== 0)
|
|
63
|
-
return null;
|
|
64
|
-
const pid = parseInt(r.stdout.trim().split('\n')[0], 10);
|
|
65
|
-
return Number.isFinite(pid) ? pid : null;
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* List the live sessions among `names`, asking each server in turn.
|
|
69
|
-
* There is no fleet-wide `tmux ls` any more: a session per server means the
|
|
70
|
-
* caller says which sessions to ask about. A server that is not running
|
|
71
|
-
* answers non-zero and contributes nothing.
|
|
72
|
-
*/
|
|
73
|
-
async list(names) {
|
|
74
|
-
const lines = [];
|
|
75
|
-
for (const name of names) {
|
|
76
|
-
const r = await this.exec('tmux', tmuxArgs(name, ['ls']));
|
|
77
|
-
if (r.code === 0 && r.stdout.trim())
|
|
78
|
-
lines.push(r.stdout.trimEnd());
|
|
79
|
-
}
|
|
80
|
-
return lines.join('\n');
|
|
81
|
-
}
|
|
82
|
-
async sendText(name, text) {
|
|
83
|
-
let r = await this.exec('tmux', tmuxArgs(name, ['send-keys', '-t', name, '-l', text]));
|
|
84
|
-
if (r.code !== 0)
|
|
85
|
-
throw new Error(`tmux send-keys '${name}' failed: ${r.stderr.trim()}`);
|
|
86
|
-
r = await this.exec('tmux', tmuxArgs(name, ['send-keys', '-t', name, 'Enter']));
|
|
87
|
-
if (r.code !== 0)
|
|
88
|
-
throw new Error(`tmux send-keys Enter '${name}' failed: ${r.stderr.trim()}`);
|
|
89
|
-
}
|
|
90
|
-
async sendKey(name, key) {
|
|
91
|
-
const r = await this.exec('tmux', tmuxArgs(name, ['send-keys', '-t', name, key]));
|
|
92
|
-
if (r.code !== 0)
|
|
93
|
-
throw new Error(`tmux send-keys '${name}' failed: ${r.stderr.trim()}`);
|
|
94
|
-
}
|
|
95
45
|
}
|
|
@@ -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 [];
|
|
@@ -31,7 +30,7 @@ export function resolveWatchdogs(baseDoc, baseFile, roles, vars, defaults) {
|
|
|
31
30
|
const roleIdentities = new Set(roles.map(r => r.identity));
|
|
32
31
|
// Tracks identity -> owning watchdog name across entries:
|
|
33
32
|
// two watchdogs declaring the same identity would otherwise share a temp
|
|
34
|
-
// dir,
|
|
33
|
+
// dir, agent session, and run lock with no error until they collide at
|
|
35
34
|
// runtime.
|
|
36
35
|
const watchdogIdentities = new Map();
|
|
37
36
|
const out = [];
|
|
@@ -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,21 +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
|
-
|
|
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
|
+
}
|
|
67
82
|
if (w.prompt_file !== undefined) {
|
|
68
83
|
if (typeof w.prompt_file !== 'string' || !isAbsolute(w.prompt_file))
|
|
69
84
|
throw new ConfigError(`${where}: prompt_file must be an absolute path`);
|
|
70
85
|
if (!existsSync(w.prompt_file))
|
|
71
86
|
throw new ConfigError(`${where}: prompt_file not found: ${w.prompt_file}`);
|
|
72
87
|
}
|
|
73
|
-
if (w.isolation !== undefined) {
|
|
74
|
-
const problems = validateIsolationConfig(w.isolation);
|
|
75
|
-
if (problems.length)
|
|
76
|
-
throw new ConfigError(`${where} ${problems.join('; ')}`);
|
|
77
|
-
}
|
|
78
88
|
const dur = (v, key, fallback, minMs) => {
|
|
79
89
|
if (v === undefined)
|
|
80
90
|
return fallback;
|
|
@@ -94,13 +104,15 @@ export function resolveWatchdogs(baseDoc, baseFile, roles, vars, defaults) {
|
|
|
94
104
|
name, coordinator: w.coordinator.trim(),
|
|
95
105
|
enabled: w.enabled ?? true,
|
|
96
106
|
intervalMs: dur(w.interval, 'interval', WATCHDOG_DEFAULT_INTERVAL_MS, WATCHDOG_MIN_INTERVAL_MS),
|
|
97
|
-
watch, watchExplicit: w.watch !== undefined,
|
|
98
|
-
|
|
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,
|
|
99
111
|
identity,
|
|
100
112
|
timeoutMs: dur(w.timeout, 'timeout', WATCHDOG_DEFAULT_TIMEOUT_MS),
|
|
101
113
|
keepReports,
|
|
102
114
|
alertCooldownMs: dur(w.alert_cooldown, 'alert_cooldown', WATCHDOG_DEFAULT_COOLDOWN_MS),
|
|
103
|
-
promptFile: w.prompt_file,
|
|
115
|
+
promptFile: w.prompt_file, sourceFile: baseFile,
|
|
104
116
|
});
|
|
105
117
|
}
|
|
106
118
|
return out;
|
package/dist/watchdog/run.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { closeSync, existsSync, openSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync, } from 'node:fs';
|
|
2
|
-
import { spawn as spawnChild
|
|
2
|
+
import { spawn as spawnChild } from 'node:child_process';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import { promisify } from 'node:util';
|
|
5
4
|
import { stringify } from 'yaml';
|
|
6
5
|
import { errorReport, normalizeWatchdogReport, validateWatchdogReport } from './report.js';
|
|
7
6
|
import { acquireRunLock, formatRunId, pruneReports, releaseRunLock, writeReport } from './store.js';
|
|
@@ -11,12 +10,10 @@ import { applyRole } from '../ops.js';
|
|
|
11
10
|
import { daemonIdentityInventoryProvisioner, ensureIdentity, } from '../creation.js';
|
|
12
11
|
import { runOnce, START_STAGGER_FILE } from '../runner.js';
|
|
13
12
|
import { agentDir, tmpRoot } from '../paths.js';
|
|
14
|
-
import { loadConfig,
|
|
13
|
+
import { loadConfig, ROLE_NAME_RE, } from '../config.js';
|
|
15
14
|
import { getAdapter } from '../harness/registry.js';
|
|
16
15
|
import { redactLogLine } from '../application/log-service.js';
|
|
17
16
|
import { controlRequest, controlSocketPath } from '../session/control.js';
|
|
18
|
-
import { Tmux } from '../tmux.js';
|
|
19
|
-
const execFileAsync = promisify(execFile);
|
|
20
17
|
/** How often the deadline loop polls for a completed report or a dead child. */
|
|
21
18
|
const POLL_MS = 1000;
|
|
22
19
|
/** A `report.json` this old, with no further writes, is treated as finished. */
|
|
@@ -56,7 +53,6 @@ async function killIfNeeded(reason, child, roleName, sleep) {
|
|
|
56
53
|
if (reason === 'stable')
|
|
57
54
|
await sleep(HARVEST_GRACE_MS);
|
|
58
55
|
child.kill();
|
|
59
|
-
await killTmuxSession(roleName);
|
|
60
56
|
}
|
|
61
57
|
}
|
|
62
58
|
function defaultLaunchChild(binPath, roleName, runDir) {
|
|
@@ -77,13 +73,6 @@ function defaultLaunchChild(binPath, roleName, runDir) {
|
|
|
77
73
|
}),
|
|
78
74
|
};
|
|
79
75
|
}
|
|
80
|
-
/** Best-effort: the tmux session outlives the supervisor child (`runOnce` created it). */
|
|
81
|
-
async function killTmuxSession(roleName) {
|
|
82
|
-
try {
|
|
83
|
-
await execFileAsync('tmux', ['kill-session', '-t', roleName]);
|
|
84
|
-
}
|
|
85
|
-
catch { /* best effort */ }
|
|
86
|
-
}
|
|
87
76
|
/** Last 4096 chars of run.log, redacted — attached to error reports as diagnostic tail. */
|
|
88
77
|
function readTail(runDir) {
|
|
89
78
|
try {
|
|
@@ -95,19 +84,10 @@ function readTail(runDir) {
|
|
|
95
84
|
}
|
|
96
85
|
}
|
|
97
86
|
/** The temp role every watchdog-family run launches under. Isolation is opt-in. */
|
|
98
|
-
function buildWatchdogRole(wd,
|
|
87
|
+
function buildWatchdogRole(wd, _cfg) {
|
|
99
88
|
return {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
identity: wd.identity, model: wd.model,
|
|
103
|
-
// Watchdogs are observe-only by contract, but their sanctioned status commands
|
|
104
|
-
// must reach host control sockets. Keep approvals/unattended escalation denied
|
|
105
|
-
// while disabling the harness's native filesystem/network sandbox.
|
|
106
|
-
permissions: { approval: 'deny', filesystem: 'unrestricted', unattended: 'deny' },
|
|
107
|
-
permissionsDeclared: true,
|
|
108
|
-
monitor: resolveMonitorConfig(cfg.defaults.monitor, undefined),
|
|
109
|
-
worklog: resolveWorklogPolicy(cfg.defaults.worklog, undefined),
|
|
110
|
-
isolation: wd.isolation,
|
|
89
|
+
...structuredClone(wd.resolvedAgent),
|
|
90
|
+
name: wd.identity, sourceFile: '(watchdog)', identity: wd.identity,
|
|
111
91
|
};
|
|
112
92
|
}
|
|
113
93
|
/** Discover temporary sessions that are live now, not merely stale dirs on disk. */
|
|
@@ -121,19 +101,15 @@ async function discoverLiveTemporaryRoles() {
|
|
|
121
101
|
catch {
|
|
122
102
|
return [];
|
|
123
103
|
}
|
|
124
|
-
const tmux = new Tmux();
|
|
125
104
|
const live = await Promise.all(names.map(async (name) => {
|
|
126
105
|
const dir = agentDir(name, true);
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
tmux.has(name).catch(() => false),
|
|
135
|
-
]);
|
|
136
|
-
return acpAlive || tmuxAlive ? name : undefined;
|
|
106
|
+
const alive = existsSync(controlSocketPath(dir))
|
|
107
|
+
? await controlRequest(dir, { command: 'status' }, 2_000)
|
|
108
|
+
.then(response => response.ok
|
|
109
|
+
&& response.result?.alive === true)
|
|
110
|
+
.catch(() => false)
|
|
111
|
+
: false;
|
|
112
|
+
return alive ? name : undefined;
|
|
137
113
|
}));
|
|
138
114
|
return live.filter((name) => name !== undefined);
|
|
139
115
|
}
|
package/dist/web/auth.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type { PrereqReport } from '../harness/types.js';
|
|
2
2
|
export declare const REDACTED_ENV_VALUE = "__OURS_FLEET_SECRET_REDACTED__";
|
|
3
|
-
export
|
|
3
|
+
export interface EditableFleetModel {
|
|
4
|
+
manifest: Record<string, unknown>;
|
|
5
|
+
agents: Record<string, Record<string, unknown>>;
|
|
6
|
+
}
|
|
4
7
|
export interface ConfigReadResult {
|
|
5
8
|
path: string;
|
|
6
|
-
exists:
|
|
7
|
-
firstRun:
|
|
9
|
+
exists: true;
|
|
10
|
+
firstRun: false;
|
|
8
11
|
revision: string;
|
|
9
12
|
model: EditableFleetModel;
|
|
10
13
|
redactions: string[];
|
|
@@ -33,15 +36,15 @@ export interface ConfigWriteResult extends ConfigPreviewResult {
|
|
|
33
36
|
export interface FleetConfigServiceOptions {
|
|
34
37
|
configPath?: string;
|
|
35
38
|
preflight?(configPath: string): Promise<PrereqReport>;
|
|
39
|
+
/** Fault-injection seam; called under lock immediately before each mutation. */
|
|
40
|
+
beforeMutation?(relativePath: string, index: number): void;
|
|
36
41
|
}
|
|
37
42
|
export declare class FleetConfigService {
|
|
38
43
|
readonly path: string;
|
|
39
44
|
private readonly preflight;
|
|
45
|
+
private readonly beforeMutation?;
|
|
40
46
|
constructor(options?: FleetConfigServiceOptions);
|
|
41
47
|
read(): ConfigReadResult;
|
|
42
|
-
preview(baseRevision: string,
|
|
43
|
-
write(baseRevision: string,
|
|
44
|
-
private readSource;
|
|
45
|
-
private assertRevision;
|
|
46
|
-
private validateCandidate;
|
|
48
|
+
preview(baseRevision: string, input: unknown): Promise<ConfigPreviewResult>;
|
|
49
|
+
write(baseRevision: string, input: unknown): Promise<ConfigWriteResult>;
|
|
47
50
|
}
|