@ours.network/fleet 0.11.0 → 0.12.0
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 +41 -0
- package/dist/briefing.js +15 -0
- package/dist/cli.js +6 -0
- package/dist/config.d.ts +15 -1
- package/dist/config.js +58 -1
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +43 -1
- package/dist/harness/codex.js +16 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/isolation/policy.js +5 -0
- package/dist/isolation/runtime.d.ts +16 -0
- package/dist/isolation/runtime.js +136 -0
- package/dist/isolation/types.d.ts +2 -0
- package/dist/owner-channel/channel.d.ts +55 -0
- package/dist/owner-channel/channel.js +295 -0
- package/dist/owner-channel/mcp.d.ts +24 -0
- package/dist/owner-channel/mcp.js +123 -0
- package/dist/owner-channel/state.d.ts +10 -0
- package/dist/owner-channel/state.js +37 -0
- package/dist/resolved-plan.js +2 -0
- package/dist/runner.d.ts +3 -0
- package/dist/runner.js +39 -5
- package/dist/session/acp.d.ts +1 -0
- package/dist/session/acp.js +17 -2
- package/dist/session/types.d.ts +3 -1
- package/dist/session/types.js +2 -2
- package/dist/spawn.js +7 -3
- package/dist/watchdog/config.d.ts +4 -0
- package/dist/watchdog/config.js +9 -3
- package/dist/watchdog/run.d.ts +2 -0
- package/dist/watchdog/run.js +50 -23
- package/package.json +1 -1
package/dist/session/acp.d.ts
CHANGED
|
@@ -38,6 +38,7 @@ export declare class AcpSession implements SessionHandle {
|
|
|
38
38
|
private steeringSupported;
|
|
39
39
|
private capabilities?;
|
|
40
40
|
private controllerCount;
|
|
41
|
+
private activeTurn?;
|
|
41
42
|
private constructor();
|
|
42
43
|
static start(options: AcpSessionOptions): Promise<AcpSession>;
|
|
43
44
|
isAlive(): boolean;
|
package/dist/session/acp.js
CHANGED
|
@@ -40,6 +40,7 @@ export class AcpSession {
|
|
|
40
40
|
steeringSupported = false;
|
|
41
41
|
capabilities;
|
|
42
42
|
controllerCount = 0;
|
|
43
|
+
activeTurn;
|
|
43
44
|
constructor(options, child, connection) {
|
|
44
45
|
this.options = options;
|
|
45
46
|
this.child = child;
|
|
@@ -162,6 +163,7 @@ export class AcpSession {
|
|
|
162
163
|
this.pendingPermissions.delete(permissionId);
|
|
163
164
|
pending.resolve({ outcome: { outcome: 'selected', optionId } });
|
|
164
165
|
this.events.emit('permission', {
|
|
166
|
+
turnId: this.activeTurn?.id,
|
|
165
167
|
permissionId,
|
|
166
168
|
status: 'completed',
|
|
167
169
|
decision: chosen.kind.startsWith('reject') ? 'denied' : 'allowed',
|
|
@@ -241,6 +243,7 @@ export class AcpSession {
|
|
|
241
243
|
if (!this.sessionId || !this.isAlive())
|
|
242
244
|
return turnResult(false, 'failed', this.lastError ?? 'ACP session is offline');
|
|
243
245
|
this.readiness = 'running';
|
|
246
|
+
this.activeTurn = { id: turnId, output: '' };
|
|
244
247
|
this.events.emit('state', { turnId, status: 'running' });
|
|
245
248
|
try {
|
|
246
249
|
const response = await this.connection.agent.request(acp.methods.agent.session.prompt, {
|
|
@@ -252,7 +255,7 @@ export class AcpSession {
|
|
|
252
255
|
this.events.emit('state', { status: 'idle' });
|
|
253
256
|
// The prompt was accepted either way — the agent answered. Whether the
|
|
254
257
|
// turn SUCCEEDED is a separate question, and only `stopReason` answers it.
|
|
255
|
-
return turnResult(true, classifyStopReason(response.stopReason), response.stopReason);
|
|
258
|
+
return turnResult(true, classifyStopReason(response.stopReason), response.stopReason, this.activeTurn?.id === turnId ? this.activeTurn.output : undefined);
|
|
256
259
|
}
|
|
257
260
|
catch (error) {
|
|
258
261
|
this.lastError = error?.message ?? String(error);
|
|
@@ -260,7 +263,11 @@ export class AcpSession {
|
|
|
260
263
|
this.events.emit('error', { turnId, text: this.lastError });
|
|
261
264
|
if (this.isAlive())
|
|
262
265
|
this.events.emit('state', { status: 'idle' });
|
|
263
|
-
return turnResult(false, 'failed', this.lastError);
|
|
266
|
+
return turnResult(false, 'failed', this.lastError, this.activeTurn?.id === turnId ? this.activeTurn.output : undefined);
|
|
267
|
+
}
|
|
268
|
+
finally {
|
|
269
|
+
if (this.activeTurn?.id === turnId)
|
|
270
|
+
this.activeTurn = undefined;
|
|
264
271
|
}
|
|
265
272
|
}
|
|
266
273
|
async steerPrompt(text) {
|
|
@@ -312,6 +319,7 @@ export class AcpSession {
|
|
|
312
319
|
const permissionId = randomUUID();
|
|
313
320
|
this.readiness = 'awaiting_permission';
|
|
314
321
|
this.events.emit('permission', {
|
|
322
|
+
turnId: this.activeTurn?.id,
|
|
315
323
|
permissionId,
|
|
316
324
|
toolCallId: params.toolCall.toolCallId,
|
|
317
325
|
title: params.toolCall.title ?? 'Permission requested',
|
|
@@ -332,6 +340,7 @@ export class AcpSession {
|
|
|
332
340
|
settleAutomatically(params, option, decision, policy, reason) {
|
|
333
341
|
const settled = option ? decision : 'cancelled';
|
|
334
342
|
this.events.emit('permission', {
|
|
343
|
+
turnId: this.activeTurn?.id,
|
|
335
344
|
permissionId: randomUUID(),
|
|
336
345
|
toolCallId: params.toolCall.toolCallId,
|
|
337
346
|
title: params.toolCall.title ?? 'Permission requested',
|
|
@@ -366,17 +375,22 @@ export class AcpSession {
|
|
|
366
375
|
recordUpdate(update) {
|
|
367
376
|
switch (update.sessionUpdate) {
|
|
368
377
|
case 'agent_message_chunk':
|
|
378
|
+
if (this.activeTurn && update.content.type === 'text')
|
|
379
|
+
this.activeTurn.output += update.content.text;
|
|
369
380
|
this.events.emit('agent_text', {
|
|
381
|
+
turnId: this.activeTurn?.id,
|
|
370
382
|
text: update.content.type === 'text' ? update.content.text : `[${update.content.type}]`,
|
|
371
383
|
});
|
|
372
384
|
break;
|
|
373
385
|
case 'agent_thought_chunk':
|
|
374
386
|
this.events.emit('thought', {
|
|
387
|
+
turnId: this.activeTurn?.id,
|
|
375
388
|
text: update.content.type === 'text' ? update.content.text : `[${update.content.type}]`,
|
|
376
389
|
});
|
|
377
390
|
break;
|
|
378
391
|
case 'tool_call':
|
|
379
392
|
this.events.emit('tool_call', {
|
|
393
|
+
turnId: this.activeTurn?.id,
|
|
380
394
|
toolCallId: update.toolCallId,
|
|
381
395
|
title: update.title,
|
|
382
396
|
status: update.status,
|
|
@@ -384,6 +398,7 @@ export class AcpSession {
|
|
|
384
398
|
break;
|
|
385
399
|
case 'tool_call_update':
|
|
386
400
|
this.events.emit('tool_update', {
|
|
401
|
+
turnId: this.activeTurn?.id,
|
|
387
402
|
toolCallId: update.toolCallId,
|
|
388
403
|
title: update.title ?? undefined,
|
|
389
404
|
status: update.status ?? undefined,
|
package/dist/session/types.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ export interface TurnResult {
|
|
|
19
19
|
outcome: TurnOutcome;
|
|
20
20
|
succeeded: boolean;
|
|
21
21
|
detail?: string;
|
|
22
|
+
/** Final assistant text captured structurally by a backend, when available. */
|
|
23
|
+
output?: string;
|
|
22
24
|
}
|
|
23
25
|
/**
|
|
24
26
|
* Why a control operation failed. The distinctions exist because collapsing
|
|
@@ -75,7 +77,7 @@ export declare function classifyChildExit(code: number | null, signal: string |
|
|
|
75
77
|
/** The single definition of terminal success. Nothing else may re-derive it. */
|
|
76
78
|
export declare const isTerminalSuccess: (outcome: TurnOutcome) => boolean;
|
|
77
79
|
/** Build a TurnResult with `succeeded` always consistent with `outcome`. */
|
|
78
|
-
export declare function turnResult(accepted: boolean, outcome: TurnOutcome, detail?: string): TurnResult;
|
|
80
|
+
export declare function turnResult(accepted: boolean, outcome: TurnOutcome, detail?: string, output?: string): TurnResult;
|
|
79
81
|
export interface SubmitPromptOptions {
|
|
80
82
|
/** Cancel active work before delivering this prompt. */
|
|
81
83
|
interrupt?: boolean;
|
package/dist/session/types.js
CHANGED
|
@@ -37,6 +37,6 @@ export function classifyChildExit(code, signal) {
|
|
|
37
37
|
/** The single definition of terminal success. Nothing else may re-derive it. */
|
|
38
38
|
export const isTerminalSuccess = (outcome) => outcome === 'completed';
|
|
39
39
|
/** Build a TurnResult with `succeeded` always consistent with `outcome`. */
|
|
40
|
-
export function turnResult(accepted, outcome, detail) {
|
|
41
|
-
return { accepted, outcome, succeeded: isTerminalSuccess(outcome), detail };
|
|
40
|
+
export function turnResult(accepted, outcome, detail, output) {
|
|
41
|
+
return { accepted, outcome, succeeded: isTerminalSuccess(outcome), detail, output };
|
|
42
42
|
}
|
package/dist/spawn.js
CHANGED
|
@@ -4,7 +4,7 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { parse, stringify } from 'yaml';
|
|
5
5
|
import { agentDir, fleetDDir } from './paths.js';
|
|
6
6
|
import { validateIsolationConfig } from './isolation/policy.js';
|
|
7
|
-
import { loadConfig, resolveAuthProxy, resolveModelChain, resolveMonitorConfig, resolvePermissions, resolveRoleModel, resolveWorklogPolicy, validateMonitorConfig, } from './config.js';
|
|
7
|
+
import { loadConfig, resolveAuthProxy, resolveModelChain, resolveMonitorConfig, resolveOwnerChannelConfig, resolvePermissions, resolveRoleModel, resolveWorklogPolicy, validateMonitorConfig, } from './config.js';
|
|
8
8
|
import { applyRole, up } from './ops.js';
|
|
9
9
|
import { START_STAGGER_FILE } from './runner.js';
|
|
10
10
|
import { buildProvenance, daemonIdentityProvisioner, ensureIdentity, provenanceOf, withCreationTransaction, writeProvenance, writeRoleFile, } from './creation.js';
|
|
@@ -186,12 +186,13 @@ export function spawnDryRun(o) {
|
|
|
186
186
|
const defaultHarness = cfg.defaults.harness ?? 'claude-code';
|
|
187
187
|
const inheritsModelDefaults = harness === defaultHarness && raw.model !== null;
|
|
188
188
|
const model = resolveRoleModel(raw.model, raw.harness, cfg.defaults);
|
|
189
|
+
const session = raw.session ?? cfg.defaults.session ?? 'tmux';
|
|
189
190
|
const resolvedRole = {
|
|
190
191
|
...raw,
|
|
191
192
|
name: o.name,
|
|
192
193
|
sourceFile: o.temp ? '(temp dry-run)' : join(fleetDDir(), `${o.name}.yaml`),
|
|
193
194
|
harness,
|
|
194
|
-
session
|
|
195
|
+
session,
|
|
195
196
|
session_options: raw.session_options,
|
|
196
197
|
permissions: resolvePermissions(cfg.defaults.permissions, raw.permissions),
|
|
197
198
|
permissionsDeclared: raw.permissions !== undefined || cfg.defaults.permissions !== undefined,
|
|
@@ -203,6 +204,7 @@ export function spawnDryRun(o) {
|
|
|
203
204
|
harness_options: Object.keys(harnessOptions).length ? harnessOptions : undefined,
|
|
204
205
|
isolation: raw.isolation ?? cfg.defaults.isolation,
|
|
205
206
|
monitor: resolveMonitorConfig(cfg.defaults.monitor, raw.monitor),
|
|
207
|
+
owner_channel: resolveOwnerChannelConfig(cfg.defaults.owner_channel, raw.owner_channel, session),
|
|
206
208
|
worklog: resolveWorklogPolicy(cfg.defaults.worklog, raw.worklog),
|
|
207
209
|
auth_proxy: resolveAuthProxy(cfg.defaults.auth_proxy, raw.auth_proxy),
|
|
208
210
|
};
|
|
@@ -378,11 +380,12 @@ async function spawnTempInner(o, binPath, launch, tx, guarantee, onStage) {
|
|
|
378
380
|
const harness = o.harness ?? defaultHarness ?? 'claude-code';
|
|
379
381
|
const inheritsModelDefaults = harness === (defaultHarness ?? 'claude-code') && o.model !== null;
|
|
380
382
|
const model = resolveRoleModel(o.model, o.harness, cfg.defaults);
|
|
383
|
+
const session = o.session ?? cfg.defaults.session ?? 'tmux';
|
|
381
384
|
const role = {
|
|
382
385
|
...fromOpts, // includes `isolation` when --isolation-file was given
|
|
383
386
|
name: o.name,
|
|
384
387
|
harness,
|
|
385
|
-
session
|
|
388
|
+
session,
|
|
386
389
|
identity: o.identity ?? o.name,
|
|
387
390
|
model,
|
|
388
391
|
model_chain: resolveModelChain(model, fromOpts.model_chain ?? (inheritsModelDefaults
|
|
@@ -393,6 +396,7 @@ async function spawnTempInner(o, binPath, launch, tx, guarantee, onStage) {
|
|
|
393
396
|
permissionsDeclared: fromOpts.permissions !== undefined || cfg.defaults.permissions !== undefined,
|
|
394
397
|
// Temp agents inherit the fleet-wide monitor defaults via the snapshot (design §2).
|
|
395
398
|
monitor: resolveMonitorConfig(cfg.defaults.monitor, fromOpts.monitor),
|
|
399
|
+
owner_channel: resolveOwnerChannelConfig(cfg.defaults.owner_channel, fromOpts.owner_channel, session),
|
|
396
400
|
worklog: resolveWorklogPolicy(cfg.defaults.worklog, fromOpts.worklog),
|
|
397
401
|
auth_proxy: resolveAuthProxy(cfg.defaults.auth_proxy, fromOpts.auth_proxy),
|
|
398
402
|
sourceFile: '(temp)',
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { FleetConfig, ResolvedRole, SessionBackendId } from '../config.js';
|
|
2
|
+
import type { IsolationConfig } from '../isolation/types.js';
|
|
2
3
|
export interface WatchdogConfig {
|
|
3
4
|
coordinator?: string;
|
|
4
5
|
enabled?: boolean;
|
|
@@ -12,6 +13,7 @@ export interface WatchdogConfig {
|
|
|
12
13
|
keep_reports?: number;
|
|
13
14
|
alert_cooldown?: string;
|
|
14
15
|
prompt_file?: string;
|
|
16
|
+
isolation?: IsolationConfig;
|
|
15
17
|
}
|
|
16
18
|
export interface ResolvedWatchdog {
|
|
17
19
|
name: string;
|
|
@@ -19,6 +21,7 @@ export interface ResolvedWatchdog {
|
|
|
19
21
|
enabled: boolean;
|
|
20
22
|
intervalMs: number;
|
|
21
23
|
watch: string[];
|
|
24
|
+
watchExplicit: boolean;
|
|
22
25
|
harness: string;
|
|
23
26
|
session: SessionBackendId;
|
|
24
27
|
model?: string;
|
|
@@ -27,6 +30,7 @@ export interface ResolvedWatchdog {
|
|
|
27
30
|
keepReports: number;
|
|
28
31
|
alertCooldownMs: number;
|
|
29
32
|
promptFile?: string;
|
|
33
|
+
isolation?: IsolationConfig;
|
|
30
34
|
sourceFile: string;
|
|
31
35
|
}
|
|
32
36
|
export declare const WATCHDOG_DEFAULT_INTERVAL_MS = 600000;
|
package/dist/watchdog/config.js
CHANGED
|
@@ -2,9 +2,10 @@ import { existsSync } from 'node:fs';
|
|
|
2
2
|
import { isAbsolute } from 'node:path';
|
|
3
3
|
import { parseDuration } from '../duration.js';
|
|
4
4
|
import { ConfigError, ROLE_NAME_RE, resolveRoleModel } from '../config.js';
|
|
5
|
+
import { validateIsolationConfig } from '../isolation/policy.js';
|
|
5
6
|
const WATCHDOG_KEYS = [
|
|
6
7
|
'coordinator', 'enabled', 'interval', 'watch', 'harness', 'model', 'session',
|
|
7
|
-
'identity', 'timeout', 'keep_reports', 'alert_cooldown', 'prompt_file',
|
|
8
|
+
'identity', 'timeout', 'keep_reports', 'alert_cooldown', 'prompt_file', 'isolation',
|
|
8
9
|
];
|
|
9
10
|
export const WATCHDOG_DEFAULT_INTERVAL_MS = 600_000;
|
|
10
11
|
export const WATCHDOG_MIN_INTERVAL_MS = 60_000;
|
|
@@ -69,6 +70,11 @@ export function resolveWatchdogs(baseDoc, baseFile, roles, vars, defaults) {
|
|
|
69
70
|
if (!existsSync(w.prompt_file))
|
|
70
71
|
throw new ConfigError(`${where}: prompt_file not found: ${w.prompt_file}`);
|
|
71
72
|
}
|
|
73
|
+
if (w.isolation !== undefined) {
|
|
74
|
+
const problems = validateIsolationConfig(w.isolation);
|
|
75
|
+
if (problems.length)
|
|
76
|
+
throw new ConfigError(`${where} ${problems.join('; ')}`);
|
|
77
|
+
}
|
|
72
78
|
const dur = (v, key, fallback, minMs) => {
|
|
73
79
|
if (v === undefined)
|
|
74
80
|
return fallback;
|
|
@@ -88,13 +94,13 @@ export function resolveWatchdogs(baseDoc, baseFile, roles, vars, defaults) {
|
|
|
88
94
|
name, coordinator: w.coordinator.trim(),
|
|
89
95
|
enabled: w.enabled ?? true,
|
|
90
96
|
intervalMs: dur(w.interval, 'interval', WATCHDOG_DEFAULT_INTERVAL_MS, WATCHDOG_MIN_INTERVAL_MS),
|
|
91
|
-
watch, harness, session: sessionRaw,
|
|
97
|
+
watch, watchExplicit: w.watch !== undefined, harness, session: sessionRaw,
|
|
92
98
|
model: resolveRoleModel(w.model, w.harness, defaults),
|
|
93
99
|
identity,
|
|
94
100
|
timeoutMs: dur(w.timeout, 'timeout', WATCHDOG_DEFAULT_TIMEOUT_MS),
|
|
95
101
|
keepReports,
|
|
96
102
|
alertCooldownMs: dur(w.alert_cooldown, 'alert_cooldown', WATCHDOG_DEFAULT_COOLDOWN_MS),
|
|
97
|
-
promptFile: w.prompt_file, sourceFile: baseFile,
|
|
103
|
+
promptFile: w.prompt_file, isolation: w.isolation, sourceFile: baseFile,
|
|
98
104
|
});
|
|
99
105
|
}
|
|
100
106
|
return out;
|
package/dist/watchdog/run.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ export interface WatchdogRunDeps {
|
|
|
21
21
|
launchChild?(binPath: string, roleName: string, runDir: string): WatchdogChildHandle;
|
|
22
22
|
/** Pre-loaded config (defaults inheritance). Falls back to `loadConfig()`. */
|
|
23
23
|
cfg?: FleetConfig;
|
|
24
|
+
/** Live temporary-role discovery override for focused tests. */
|
|
25
|
+
discoverLiveTemporaryRoles?(): Promise<string[]>;
|
|
24
26
|
}
|
|
25
27
|
export interface WatchdogRunOutcome {
|
|
26
28
|
report: WatchdogReport;
|
package/dist/watchdog/run.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { closeSync, existsSync, openSync, readFileSync, rmSync, statSync, writeFileSync, } from 'node:fs';
|
|
1
|
+
import { closeSync, existsSync, openSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync, } from 'node:fs';
|
|
2
2
|
import { spawn as spawnChild, execFile } from 'node:child_process';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { promisify } from 'node:util';
|
|
@@ -10,10 +10,12 @@ import { computeDigest, reconcileLedger, readLedger, writeLedger } from './alert
|
|
|
10
10
|
import { applyRole } from '../ops.js';
|
|
11
11
|
import { daemonIdentityProvisioner, ensureIdentity, } from '../creation.js';
|
|
12
12
|
import { runOnce, START_STAGGER_FILE } from '../runner.js';
|
|
13
|
-
import { agentDir } from '../paths.js';
|
|
14
|
-
import { loadConfig, resolveMonitorConfig, resolveWorklogPolicy, } from '../config.js';
|
|
13
|
+
import { agentDir, tmpRoot } from '../paths.js';
|
|
14
|
+
import { loadConfig, resolveMonitorConfig, resolveWorklogPolicy, ROLE_NAME_RE, } from '../config.js';
|
|
15
15
|
import { getAdapter } from '../harness/registry.js';
|
|
16
16
|
import { redactLogLine } from '../application/log-service.js';
|
|
17
|
+
import { controlRequest, controlSocketPath } from '../session/control.js';
|
|
18
|
+
import { Tmux } from '../tmux.js';
|
|
17
19
|
const execFileAsync = promisify(execFile);
|
|
18
20
|
/** How often the deadline loop polls for a completed report or a dead child. */
|
|
19
21
|
const POLL_MS = 1000;
|
|
@@ -92,33 +94,49 @@ function readTail(runDir) {
|
|
|
92
94
|
return undefined;
|
|
93
95
|
}
|
|
94
96
|
}
|
|
95
|
-
/**
|
|
96
|
-
* The temp role every watchdog-family run (inspection and notifier alike) launches under.
|
|
97
|
-
* `network: 'broker'` keeps ours messaging available; no write binds beyond stateDir/cwd, which
|
|
98
|
-
* resolveIsolation adds itself. ~/fleet.yaml and fleet.d are deliberately NOT bound — they're on
|
|
99
|
-
* the isolation blocklist, and everything either run flavor needs is written into its own dir.
|
|
100
|
-
*
|
|
101
|
-
* Read access is scoped to exactly `wd.watch` (finding #3): a watchdog configured to watch one
|
|
102
|
-
* role must not be able to read every other role's state dir just because they all live under
|
|
103
|
-
* the same agents root. `wd.watch` defaults to every role (watchdog/config.ts's
|
|
104
|
-
* resolveWatchdogs), so a watchdog that watches everything still sees everything — this only
|
|
105
|
-
* narrows visibility for a watchdog scoped to fewer roles. A watched role whose state dir doesn't
|
|
106
|
-
* exist (e.g. a role removed after the watchdog was configured) is simply absent from the
|
|
107
|
-
* bwrap ro-bind-try set — the agent reports it unreachable from status evidence, same as any
|
|
108
|
-
* other missing state dir.
|
|
109
|
-
*/
|
|
97
|
+
/** The temp role every watchdog-family run launches under. Isolation is opt-in. */
|
|
110
98
|
function buildWatchdogRole(wd, cfg) {
|
|
111
99
|
return {
|
|
112
100
|
name: wd.identity, sourceFile: '(watchdog)',
|
|
113
101
|
harness: wd.harness, session: wd.session,
|
|
114
102
|
identity: wd.identity, model: wd.model,
|
|
115
|
-
|
|
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' },
|
|
116
107
|
permissionsDeclared: true,
|
|
117
108
|
monitor: resolveMonitorConfig(cfg.defaults.monitor, undefined),
|
|
118
109
|
worklog: resolveWorklogPolicy(cfg.defaults.worklog, undefined),
|
|
119
|
-
isolation:
|
|
110
|
+
isolation: wd.isolation,
|
|
120
111
|
};
|
|
121
112
|
}
|
|
113
|
+
/** Discover temporary sessions that are live now, not merely stale dirs on disk. */
|
|
114
|
+
async function discoverLiveTemporaryRoles() {
|
|
115
|
+
let names;
|
|
116
|
+
try {
|
|
117
|
+
names = readdirSync(tmpRoot(), { withFileTypes: true })
|
|
118
|
+
.filter(entry => entry.isDirectory() && !entry.isSymbolicLink() && ROLE_NAME_RE.test(entry.name))
|
|
119
|
+
.map(entry => entry.name);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
const tmux = new Tmux();
|
|
125
|
+
const live = await Promise.all(names.map(async (name) => {
|
|
126
|
+
const dir = agentDir(name, true);
|
|
127
|
+
const [acpAlive, tmuxAlive] = await Promise.all([
|
|
128
|
+
existsSync(controlSocketPath(dir))
|
|
129
|
+
? controlRequest(dir, { command: 'status' }, 2_000)
|
|
130
|
+
.then(response => response.ok
|
|
131
|
+
&& response.result?.alive === true)
|
|
132
|
+
.catch(() => false)
|
|
133
|
+
: Promise.resolve(false),
|
|
134
|
+
tmux.has(name).catch(() => false),
|
|
135
|
+
]);
|
|
136
|
+
return acpAlive || tmuxAlive ? name : undefined;
|
|
137
|
+
}));
|
|
138
|
+
return live.filter((name) => name !== undefined);
|
|
139
|
+
}
|
|
122
140
|
/**
|
|
123
141
|
* Run one watchdog agent end-to-end in a clean-context temp state dir: provision
|
|
124
142
|
* identity, materialize the run's contract (briefing/manifest/role snapshot),
|
|
@@ -147,14 +165,20 @@ export async function executeWatchdogRun(wd, deps) {
|
|
|
147
165
|
try {
|
|
148
166
|
const guarantee = await ensureIdentity(wd.identity, {}, deps.identityProvisioner ?? daemonIdentityProvisioner(), deps.log);
|
|
149
167
|
const cfg = deps.cfg ?? loadConfig();
|
|
150
|
-
const
|
|
168
|
+
const discovered = wd.watchExplicit
|
|
169
|
+
? []
|
|
170
|
+
: await (deps.discoverLiveTemporaryRoles ?? discoverLiveTemporaryRoles)();
|
|
171
|
+
const watch = [...new Set([...wd.watch, ...discovered])]
|
|
172
|
+
.filter(name => name !== wd.identity);
|
|
173
|
+
const runWd = { ...wd, watch };
|
|
174
|
+
const role = buildWatchdogRole(runWd, cfg);
|
|
151
175
|
const dir = applyRole(role, { temp: true, identityGuarantee: guarantee.state });
|
|
152
176
|
const reportPath = join(dir, 'report.json');
|
|
153
177
|
const manifestPath = join(dir, 'watch.json');
|
|
154
178
|
const promptFocus = wd.promptFile ? readFileSync(wd.promptFile, 'utf8') : undefined;
|
|
155
179
|
// applyRole wrote the generic role briefing; the watchdog contract replaces it.
|
|
156
180
|
writeFileSync(join(dir, 'briefing.md'), generateWatchdogBriefing({
|
|
157
|
-
wd, manifestPath, reportPath,
|
|
181
|
+
wd: runWd, manifestPath, reportPath,
|
|
158
182
|
vocabulary: getAdapter(wd.harness).vocabulary,
|
|
159
183
|
identityGuarantee: guarantee.state,
|
|
160
184
|
promptFocus,
|
|
@@ -163,7 +187,10 @@ export async function executeWatchdogRun(wd, deps) {
|
|
|
163
187
|
writeFileSync(join(dir, 'role.yaml'), stringify(role));
|
|
164
188
|
const manifest = {
|
|
165
189
|
watchdog: wd.name, run_id: runId, coordinator: wd.coordinator, started_at: startedAt,
|
|
166
|
-
roles:
|
|
190
|
+
roles: runWd.watch.map(r => ({
|
|
191
|
+
name: r,
|
|
192
|
+
stateDir: wd.watch.includes(r) ? agentDir(r) : agentDir(r, true),
|
|
193
|
+
})),
|
|
167
194
|
digest: computeDigest(ledger, wd.alertCooldownMs, now()),
|
|
168
195
|
};
|
|
169
196
|
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ours.network/fleet",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Harness-agnostic fleet of persistent, identity-bound AI agents. Declarative fleet.yaml, tmux or ACP sessions, supervision, and ours.network messaging.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "FSL-1.1-Apache-2.0",
|