@ours.network/fleet 1.2.0-nightly.54 → 1.2.0-nightly.55
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 +20 -12
- package/dist/agent-ours/service.d.ts +4 -1
- package/dist/agent-ours/service.js +31 -7
- package/dist/application/log-service.js +8 -8
- package/dist/application/role-creation-service.js +2 -1
- package/dist/application/role-removal-service.js +7 -1
- package/dist/application/role-repository.js +17 -0
- package/dist/application/task-layouts.js +12 -2
- package/dist/application/task-room-service.js +19 -17
- package/dist/application/types.d.ts +2 -0
- package/dist/build-info.json +4 -4
- package/dist/cli.js +31 -3
- package/dist/config.d.ts +2 -0
- package/dist/harness/agent-session.d.ts +2 -0
- package/dist/harness/claude-code-session.js +1 -1
- package/dist/harness/codex-session.js +2 -2
- package/dist/managed-recovery.d.ts +13 -0
- package/dist/managed-recovery.js +31 -0
- package/dist/ops.d.ts +2 -0
- package/dist/ops.js +15 -2
- package/dist/rooms-tasks/cli.js +21 -0
- package/dist/rooms-tasks/close.d.ts +2 -0
- package/dist/rooms-tasks/close.js +66 -1
- package/dist/rooms-tasks/deletion.js +10 -0
- package/dist/rooms-tasks/erasure.js +4 -2
- package/dist/rooms-tasks/layout-member-retirement.d.ts +10 -0
- package/dist/rooms-tasks/layout-member-retirement.js +90 -0
- package/dist/rooms-tasks/layout-service.d.ts +1 -0
- package/dist/rooms-tasks/layout-service.js +31 -13
- package/dist/rooms-tasks/layout.d.ts +7 -0
- package/dist/rooms-tasks/layout.js +35 -0
- package/dist/rooms-tasks/legacy-task-member.d.ts +19 -0
- package/dist/rooms-tasks/legacy-task-member.js +116 -0
- package/dist/rooms-tasks/member-ownership.d.ts +5 -0
- package/dist/rooms-tasks/member-ownership.js +40 -0
- package/dist/rooms-tasks/provision.js +26 -1
- package/dist/rooms-tasks/recovery.d.ts +19 -0
- package/dist/rooms-tasks/recovery.js +110 -0
- package/dist/rooms-tasks/task-service-retirement.d.ts +11 -0
- package/dist/rooms-tasks/task-service-retirement.js +21 -0
- package/dist/rooms-tasks/task-state.d.ts +1 -0
- package/dist/rooms-tasks/task-state.js +16 -1
- package/dist/rooms-tasks/types.d.ts +3 -0
- package/dist/rooms-tasks/workspace.d.ts +2 -0
- package/dist/rooms-tasks/workspace.js +5 -0
- package/dist/runner.d.ts +6 -0
- package/dist/runner.js +160 -22
- package/dist/session/acp.d.ts +1 -0
- package/dist/session/acp.js +7 -0
- package/dist/session/arbiter.d.ts +5 -0
- package/dist/session/arbiter.js +16 -2
- package/dist/session/codex-app-server.d.ts +1 -0
- package/dist/session/codex-app-server.js +5 -0
- package/dist/session/types.d.ts +2 -0
- package/dist/spawn.d.ts +5 -1
- package/dist/spawn.js +32 -3
- package/dist/startup-readiness.d.ts +1 -1
- package/dist/startup-readiness.js +3 -3
- package/dist/supervisor/adoption.d.ts +8 -0
- package/dist/supervisor/adoption.js +78 -0
- package/dist/supervisor/catalog.d.ts +62 -0
- package/dist/supervisor/catalog.js +254 -0
- package/dist/supervisor/fleet.d.ts +8 -0
- package/dist/supervisor/fleet.js +89 -0
- package/dist/supervisor/index.js +3 -6
- package/dist/supervisor/launchd.d.ts +1 -1
- package/dist/supervisor/launchd.js +24 -12
- package/dist/supervisor/legacy.d.ts +35 -0
- package/dist/supervisor/legacy.js +524 -0
- package/dist/supervisor/manager.d.ts +15 -0
- package/dist/supervisor/manager.js +258 -0
- package/dist/supervisor/scope.d.ts +4 -0
- package/dist/supervisor/scope.js +10 -0
- package/dist/supervisor/systemd.d.ts +2 -2
- package/dist/supervisor/systemd.js +14 -14
- package/dist/task-supervision.d.ts +9 -0
- package/dist/task-supervision.js +145 -0
- package/dist/temp-lifecycle.d.ts +17 -7
- package/dist/temp-lifecycle.js +85 -53
- package/dist/temp-supervisor-recovery.d.ts +1 -1
- package/dist/temp-supervisor-recovery.js +2 -2
- package/docs/fleet-migration.md +54 -0
- package/docs/task-workspaces.md +26 -0
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -10,8 +10,9 @@ turns such sessions into **roles**: long-lived agents that
|
|
|
10
10
|
|
|
11
11
|
- **run through structured managed sessions** behind one provider-neutral
|
|
12
12
|
agent-session interface, which you can inspect or prompt,
|
|
13
|
-
- are **supervised** — systemd (Linux) or
|
|
14
|
-
|
|
13
|
+
- are **supervised** — one Fleet systemd service (Linux) or LaunchAgent (macOS)
|
|
14
|
+
runs their existing agent runners, restarts them on crash, and restores the
|
|
15
|
+
desired running roster at boot or login,
|
|
15
16
|
- **resume their context** across restarts (when the harness supports it),
|
|
16
17
|
- **bind an ours.network identity**, so you — and every other agent — can message
|
|
17
18
|
them by name over an end-to-end-encrypted channel
|
|
@@ -44,11 +45,11 @@ brain: { inline: { harness: codex, session: codex-app-server } }
|
|
|
44
45
|
|
|
45
46
|
```
|
|
46
47
|
~/fleet.yaml + ~/fleet/{agents,agent_templates,roles,brains,room_templates}/*.yaml your declaration
|
|
47
|
-
│ ours-fleet up
|
|
48
|
+
│ ours-fleet init, then up
|
|
48
49
|
▼
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
one Fleet OS service ──► private member catalog + existing agent runners
|
|
51
|
+
│
|
|
52
|
+
briefing.md per member ──► session adapter ──► native app-server or ACP transport
|
|
52
53
|
```
|
|
53
54
|
|
|
54
55
|
Each role gets a state dir (`~/.ours-fleet/agents/<Name>/`) holding its briefing,
|
|
@@ -81,9 +82,10 @@ The state dir contract:
|
|
|
81
82
|
| a harness CLI, logged in | the agent itself | e.g. Claude Code (`claude`) or Codex CLI (`codex`) |
|
|
82
83
|
| `ours` CLI + shared daemon | identity + agent-to-agent messaging | `ours-install client --config /absolute/private/profile.json --integrations fleet --fleet-settings /absolute/fleet-settings.json` |
|
|
83
84
|
|
|
84
|
-
Linux only: `ours-fleet init` enables *linger* so
|
|
85
|
-
and
|
|
86
|
-
equivalent)
|
|
85
|
+
Linux only: `ours-fleet init` enables *linger* so Fleet runs without a login session
|
|
86
|
+
and restores running members after reboot. On macOS the Fleet LaunchAgent starts
|
|
87
|
+
**at login** (no linger equivalent). Member logs live in each member's state
|
|
88
|
+
directory; use `ours-fleet logs <Name>` to read them.
|
|
87
89
|
|
|
88
90
|
## Install
|
|
89
91
|
|
|
@@ -103,6 +105,12 @@ maintained adapters require Node 22 or newer.
|
|
|
103
105
|
Each OS user manages their own fleet — to host roles under a sandboxed account,
|
|
104
106
|
become that account and repeat.
|
|
105
107
|
|
|
108
|
+
When upgrading from per-agent services, coordinate package installation with
|
|
109
|
+
explicit `ours-fleet init`. Preserve operator readiness hooks on the common
|
|
110
|
+
parent before migration; new task/temporary creation requires that parent.
|
|
111
|
+
See the [migration and rollback runbook](docs/fleet-migration.md)
|
|
112
|
+
for changed stop/restart behaviour and legacy task transfer limits.
|
|
113
|
+
|
|
106
114
|
## Quickstart
|
|
107
115
|
|
|
108
116
|
```sh
|
|
@@ -331,9 +339,9 @@ Installs predating the stamp are compared by hashing their `dist/`, so two of
|
|
|
331
339
|
those are still told apart.
|
|
332
340
|
|
|
333
341
|
A permanent role brought up via `-c custom.yaml` remembers that file (`.config-path`
|
|
334
|
-
in its state dir) across supervisor-triggered
|
|
335
|
-
|
|
336
|
-
the default `~/fleet.yaml
|
|
342
|
+
in its state dir and its private member registration) across supervisor-triggered
|
|
343
|
+
restarts. Its runner resolves that original configuration instead of falling back
|
|
344
|
+
to the default `~/fleet.yaml`.
|
|
337
345
|
|
|
338
346
|
## split configuration field reference
|
|
339
347
|
|
|
@@ -22,4 +22,7 @@ export declare function readRoomReadiness(roomCid: string, name: string): {
|
|
|
22
22
|
generation: number;
|
|
23
23
|
} | undefined;
|
|
24
24
|
/** Explicit logical-instance termination, independent of harness/bridge transport. */
|
|
25
|
-
export declare function releaseManagedAgent(role: ResolvedRole
|
|
25
|
+
export declare function releaseManagedAgent(role: ResolvedRole, expected?: {
|
|
26
|
+
cid: string;
|
|
27
|
+
action: string;
|
|
28
|
+
}): Promise<void>;
|
|
@@ -3,10 +3,11 @@ import { ArtifactStore } from '../file-delivery/store.js';
|
|
|
3
3
|
import { resolve, basename, extname } from 'node:path';
|
|
4
4
|
import { socketPath as privateSocketPath } from '../socket-path.js';
|
|
5
5
|
import { randomBytes, randomUUID } from 'node:crypto';
|
|
6
|
-
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
7
7
|
import { join } from 'node:path';
|
|
8
8
|
import { attachOursClient } from '@ours.network/sdk/client';
|
|
9
9
|
import { ApplicationIdentityStore } from '@ours.network/mcp/application-identities';
|
|
10
|
+
import { assertSafeAncestors } from '../rooms-tasks/workspace.js';
|
|
10
11
|
import { readClientProfile } from '../client-profile.js';
|
|
11
12
|
import { stateRoot } from '../paths.js';
|
|
12
13
|
import { createCoworkAdapter } from '../rooms-tasks/cowork-adapter.js';
|
|
@@ -359,24 +360,47 @@ export function readRoomReadiness(roomCid, name) {
|
|
|
359
360
|
}
|
|
360
361
|
}
|
|
361
362
|
/** Explicit logical-instance termination, independent of harness/bridge transport. */
|
|
362
|
-
export async function releaseManagedAgent(role) {
|
|
363
|
+
export async function releaseManagedAgent(role, expected) {
|
|
363
364
|
const profile = readClientProfile({ ...process.env, ...role.env });
|
|
364
365
|
if (!profile)
|
|
365
366
|
throw Error('MANAGED_OURS_REQUIRES_EXPLICIT_DAEMON_PROFILE');
|
|
366
367
|
const root = privateRuntimeRoot(), dir = join(root, binderKey(profile.expectedInstanceId, role.identity));
|
|
367
|
-
if (!existsSync(join(dir, 'instance.json')))
|
|
368
|
+
if (!existsSync(join(dir, 'instance.json'))) {
|
|
369
|
+
if (expected)
|
|
370
|
+
throw Error('MEMBER_RUNTIME_RETIREMENT_MISSING');
|
|
368
371
|
return;
|
|
369
|
-
|
|
372
|
+
}
|
|
373
|
+
const read = (file) => {
|
|
374
|
+
const path = join(dir, file);
|
|
375
|
+
if (expected) {
|
|
376
|
+
assertSafeAncestors(dir);
|
|
377
|
+
const stat = lstatSync(path);
|
|
378
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 256 * 1024)
|
|
379
|
+
throw Error('MEMBER_RUNTIME_UNSAFE_PROOF');
|
|
380
|
+
}
|
|
381
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
382
|
+
};
|
|
383
|
+
const instance = read('instance.json');
|
|
384
|
+
if (expected) {
|
|
385
|
+
read('state.json');
|
|
386
|
+
read('owner.json');
|
|
387
|
+
}
|
|
370
388
|
if (instance.role !== role.name)
|
|
371
389
|
throw Error('IDENTITY_ASSIGNED_TO_OTHER_ROLE');
|
|
372
390
|
const controller = await RuntimeController.acquire(root, profile.expectedInstanceId, role.identity, instance.instance);
|
|
373
391
|
try {
|
|
374
392
|
const state = controller.journal.read();
|
|
375
|
-
if (!state || state.
|
|
393
|
+
if (expected && (!state || state.name !== role.identity || state.daemon !== profile.expectedInstanceId
|
|
394
|
+
|| state.instance !== instance.instance || state.lifetime !== 'temporary' || instance.temporary !== true
|
|
395
|
+
|| state.cid !== expected.cid || state.action !== expected.action))
|
|
396
|
+
throw Error('MEMBER_RUNTIME_RETIREMENT_MISMATCH');
|
|
397
|
+
if (!state)
|
|
376
398
|
return;
|
|
377
|
-
const owner =
|
|
378
|
-
if (owner.instance !== state.instance)
|
|
399
|
+
const owner = read('owner.json');
|
|
400
|
+
if (owner.instance !== state.instance || typeof owner.token !== 'string' || !owner.token)
|
|
379
401
|
throw Error('OWNER_RECORD_MISMATCH');
|
|
402
|
+
if (state.phase === 'RELEASED')
|
|
403
|
+
return;
|
|
380
404
|
const client = await attachOursClient({
|
|
381
405
|
endpoint: profile.endpoint,
|
|
382
406
|
expectedInstanceId: profile.expectedInstanceId,
|
|
@@ -3,7 +3,6 @@ import { createHmac, randomBytes } from 'node:crypto';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { realExec } from '../exec.js';
|
|
5
5
|
import { logsRoot } from '../paths.js';
|
|
6
|
-
import { unitFor } from '../supervisor/systemd.js';
|
|
7
6
|
import { ROLE_NAME_RE } from '../config.js';
|
|
8
7
|
import { FleetError } from './errors.js';
|
|
9
8
|
const STANDARD_REDACTIONS = [
|
|
@@ -78,12 +77,13 @@ export class StructuredLogService {
|
|
|
78
77
|
}
|
|
79
78
|
async tail(roleId, requested, cursor) {
|
|
80
79
|
const limit = Math.min(Math.max(requested, 1), 1_000);
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
80
|
+
const source = this.backend.logsArgs(roleId, false);
|
|
81
|
+
const args = { cmd: source.cmd, args: [...source.args] };
|
|
82
|
+
const count = args.args.indexOf('-n');
|
|
83
|
+
if (count >= 0)
|
|
84
|
+
args.args[count + 1] = String(limit);
|
|
85
|
+
if (args.cmd === 'journalctl')
|
|
86
|
+
args.args.push('-o', 'json', '--no-pager', '-n', String(limit));
|
|
87
87
|
if (!['journalctl', 'tail', 'tmux'].includes(args.cmd))
|
|
88
88
|
throw new FleetError('capability_unavailable', 'log backend is unsupported');
|
|
89
89
|
const result = await this.exec(args.cmd, args.args);
|
|
@@ -93,7 +93,7 @@ export class StructuredLogService {
|
|
|
93
93
|
const parsed = lines.map((line, index) => {
|
|
94
94
|
let text = line;
|
|
95
95
|
let at;
|
|
96
|
-
if (
|
|
96
|
+
if (args.cmd === 'journalctl') {
|
|
97
97
|
try {
|
|
98
98
|
const json = JSON.parse(line);
|
|
99
99
|
text = typeof json.MESSAGE === 'string' ? json.MESSAGE : line;
|
|
@@ -59,7 +59,8 @@ export class RoleCreationService {
|
|
|
59
59
|
}
|
|
60
60
|
async createManaged(caller, requested) {
|
|
61
61
|
const plan = this.previewSpawn({ origin: 'managed', caller, options: requested });
|
|
62
|
-
const creationActionId =
|
|
62
|
+
const creationActionId = requested.roomMemberStartup?.task_id
|
|
63
|
+
? requested.creationActionId ?? randomUUID() : randomUUID();
|
|
63
64
|
plan.options.creationActionId = creationActionId;
|
|
64
65
|
const statePath = await this.launchSync(plan.options);
|
|
65
66
|
const selectionSummary = (kind) => {
|
|
@@ -3,7 +3,7 @@ import { cpSync, existsSync, mkdirSync, readdirSync, renameSync } from 'node:fs'
|
|
|
3
3
|
import { basename, dirname, join } from 'node:path';
|
|
4
4
|
import { loadConfig, ROLE_NAME_RE } from '../config.js';
|
|
5
5
|
import { agentDir, stateRoot } from '../paths.js';
|
|
6
|
-
import { rmRole } from '../ops.js';
|
|
6
|
+
import { assertStandaloneRoleRemoval, rmRole } from '../ops.js';
|
|
7
7
|
import { FleetError } from './errors.js';
|
|
8
8
|
import { provesGeneratedAgentSource } from '../generated-agent-source.js';
|
|
9
9
|
export class RoleRemovalService {
|
|
@@ -19,6 +19,12 @@ export class RoleRemovalService {
|
|
|
19
19
|
if (!ROLE_NAME_RE.test(requested))
|
|
20
20
|
throw new FleetError('invalid_request', 'invalid role name');
|
|
21
21
|
const cfg = loadConfig(this.options.configPath);
|
|
22
|
+
try {
|
|
23
|
+
assertStandaloneRoleRemoval(cfg, requested);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
throw new FleetError('conflict', error instanceof Error ? error.message : String(error));
|
|
27
|
+
}
|
|
22
28
|
const names = new Set(cfg.roles.map(role => role.name));
|
|
23
29
|
for (const root of [dirname(agentDir('_')), dirname(agentDir('_', true))]) {
|
|
24
30
|
try {
|
|
@@ -5,6 +5,21 @@ import { loadConfig, resolveSnapshotMonitor, resolvePermissions, ROLE_NAME_RE, }
|
|
|
5
5
|
import { agentsRoot, tmpRoot } from '../paths.js';
|
|
6
6
|
import { readSpawnLineage } from '../web/topology.js';
|
|
7
7
|
const MAX_SNAPSHOT_BYTES = 256 * 1024;
|
|
8
|
+
function retainedTaskId(dir, name) {
|
|
9
|
+
try {
|
|
10
|
+
const path = join(dir, '.temp-supervisor.json');
|
|
11
|
+
const stat = lstatSync(path);
|
|
12
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 16 * 1024)
|
|
13
|
+
return undefined;
|
|
14
|
+
const record = JSON.parse(readFileSync(path, 'utf8'));
|
|
15
|
+
const id = record?.taskOwner?.taskId;
|
|
16
|
+
return record?.version === 1 && record?.role === name && typeof id === 'string'
|
|
17
|
+
&& /^[0-9a-z]{9}[0-9a-f]{8}$/.test(id) ? id : undefined;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
8
23
|
function safeDirs(root) {
|
|
9
24
|
try {
|
|
10
25
|
return readdirSync(root, { withFileTypes: true })
|
|
@@ -128,10 +143,12 @@ export class RoleRepository {
|
|
|
128
143
|
const stateRef = inTemporary ? { lifetime: 'temporary' }
|
|
129
144
|
: inPermanent ? { lifetime: 'permanent' } : undefined;
|
|
130
145
|
const tempStateDir = inTemporary ? join(temporaryRoot, name) : undefined;
|
|
146
|
+
const taskId = !config && tempStateDir ? retainedTaskId(tempStateDir, name) : undefined;
|
|
131
147
|
return {
|
|
132
148
|
id: name,
|
|
133
149
|
lifetime: config ? 'permanent' : inTemporary ? 'temporary' : 'orphan',
|
|
134
150
|
configured: Boolean(config),
|
|
151
|
+
...(taskId ? { taskId } : {}),
|
|
135
152
|
config: config ? view(config) : tempRole ? view(tempRole) : undefined,
|
|
136
153
|
stateRef,
|
|
137
154
|
stateHealth,
|
|
@@ -16,6 +16,9 @@ import { launchFleetWorker } from '../rooms-tasks/external-worker.js';
|
|
|
16
16
|
import { layoutSupervisorId } from '../rooms-tasks/layout-control.js';
|
|
17
17
|
import { layoutRunPath, RoomLayoutService } from '../rooms-tasks/layout-service.js';
|
|
18
18
|
import { FleetError } from './errors.js';
|
|
19
|
+
import { ownedLayoutRetirementSeats } from '../rooms-tasks/layout-member-retirement.js';
|
|
20
|
+
import { eraseMemberArtifacts } from '../rooms-tasks/erasure.js';
|
|
21
|
+
import { assertTaskRegistrationsAbsent } from '../supervisor/catalog.js';
|
|
19
22
|
/** Hash of the authorable definition only (never the source file path). */
|
|
20
23
|
export function layoutDefinitionHash(definition) {
|
|
21
24
|
const { version, description, participants, rooms } = definition;
|
|
@@ -238,8 +241,15 @@ export class TaskLayouts {
|
|
|
238
241
|
const link = task.layout;
|
|
239
242
|
if (!link)
|
|
240
243
|
return;
|
|
241
|
-
if (this.runExists(link))
|
|
242
|
-
|
|
244
|
+
if (this.runExists(link)) {
|
|
245
|
+
const engine = this.service().open(link.run_id, true);
|
|
246
|
+
await engine.delete(layoutSupervisorId());
|
|
247
|
+
const state = engine.snapshot();
|
|
248
|
+
const rooms = Object.values(state.rooms).flatMap(room => room.native ? [room.native.room_id] : []);
|
|
249
|
+
assertTaskRegistrationsAbsent(task.task_id);
|
|
250
|
+
await eraseMemberArtifacts('task', task.task_id, ownedLayoutRetirementSeats(state, link.run_id), rooms);
|
|
251
|
+
}
|
|
252
|
+
assertTaskRegistrationsAbsent(task.task_id);
|
|
243
253
|
await this.withTaskLock(link.run_id, async () => {
|
|
244
254
|
for (const path of [layoutRunPath(link.run_id), this.provenancePath(link.run_id), taskBriefPath(task)])
|
|
245
255
|
rmSync(path, { force: true });
|
|
@@ -378,23 +378,25 @@ export class TaskRoomApplicationService {
|
|
|
378
378
|
const ready = task.state === 'active' && room?.state === 'active'
|
|
379
379
|
&& active === expected && launched === expected;
|
|
380
380
|
const blocker = task.outcome?.summary ?? task.blocked?.reason ?? room?.saga.error;
|
|
381
|
-
const nextAction =
|
|
382
|
-
? `
|
|
383
|
-
:
|
|
384
|
-
? `Complete
|
|
385
|
-
: room?.
|
|
386
|
-
? `
|
|
387
|
-
: room?.provisioning_detail === '
|
|
388
|
-
? `
|
|
389
|
-
: room?.provisioning_detail === '
|
|
390
|
-
? `
|
|
391
|
-
: room?.provisioning_detail === '
|
|
392
|
-
? `
|
|
393
|
-
: room?.provisioning_detail === '
|
|
394
|
-
? `
|
|
395
|
-
:
|
|
396
|
-
? `
|
|
397
|
-
:
|
|
381
|
+
const nextAction = blocker === 'FLEET_SERVICE_NOT_INSTALLED: run ours-fleet init'
|
|
382
|
+
? `Run ours-fleet init, then retry ours-fleet task start ${task.task_id}.`
|
|
383
|
+
: task.terminal_intent
|
|
384
|
+
? `Complete the accepted ${task.terminal_intent.kind} operation; do not restart provisioning.`
|
|
385
|
+
: room?.state === 'closing' || room?.state === 'closed'
|
|
386
|
+
? `Complete room cleanup; do not restart provisioning.`
|
|
387
|
+
: room?.provisioning_detail === 'member_failed'
|
|
388
|
+
? `Inspect the failed launch, then run ours-fleet task start ${task.task_id}.`
|
|
389
|
+
: room?.provisioning_detail === 'waiting_owner_authorization'
|
|
390
|
+
? `Ensure ours-cowork 1.3.0 or newer is running and available, then run ours-fleet task start ${task.task_id}.`
|
|
391
|
+
: room?.provisioning_detail === 'waiting_owner_invite'
|
|
392
|
+
? `Rotate rooms.owner.public_invite, then run ours-fleet task start ${task.task_id}.`
|
|
393
|
+
: room?.provisioning_detail === 'owner_cid_mismatch'
|
|
394
|
+
? `Verify rooms.owner.expected_cid, rotate the Owner invite, then run ours-fleet task start ${task.task_id}.`
|
|
395
|
+
: room?.provisioning_detail === 'waiting_cowork'
|
|
396
|
+
? `Restore ours-cowork, then run ours-fleet task start ${task.task_id}.`
|
|
397
|
+
: failed
|
|
398
|
+
? `Correct the blocker, then run ours-fleet task start ${task.task_id}.`
|
|
399
|
+
: undefined;
|
|
398
400
|
return {
|
|
399
401
|
kind: failed ? 'failed' : ready ? 'ready' : 'in_progress', task, room,
|
|
400
402
|
launch: {
|
|
@@ -26,6 +26,8 @@ export interface RoleRecord {
|
|
|
26
26
|
id: string;
|
|
27
27
|
lifetime: RoleLifetime;
|
|
28
28
|
configured: boolean;
|
|
29
|
+
/** Read-only display ownership; never authorizes lifecycle operations. */
|
|
30
|
+
taskId?: string;
|
|
29
31
|
config?: ResolvedRoleView;
|
|
30
32
|
stateRef?: {
|
|
31
33
|
lifetime: 'permanent' | 'temporary';
|
package/dist/build-info.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.2.0-nightly.
|
|
3
|
-
"buildId": "
|
|
4
|
-
"commit": "
|
|
2
|
+
"version": "1.2.0-nightly.55",
|
|
3
|
+
"buildId": "6e4199ab6d73",
|
|
4
|
+
"commit": "a66362bc99a9896700192f5ff2c8454a0e209766",
|
|
5
5
|
"dirty": true,
|
|
6
|
-
"builtAt": "2026-10-
|
|
6
|
+
"builtAt": "2026-10-09T12:16:42.572Z",
|
|
7
7
|
"capabilities": [
|
|
8
8
|
"managed-cli.codex-reasoning-effort-v1",
|
|
9
9
|
"managed-cli.setup-v1",
|
package/dist/cli.js
CHANGED
|
@@ -617,7 +617,7 @@ cOpt(program.command('status <name>').description('declared persistent Agent uni
|
|
|
617
617
|
+ `${ledger.lastTermination.runStartedAt ?? 'unknown'}) died without an orderly exit; `
|
|
618
618
|
+ `observed ${ledger.lastTermination.observedAt}`
|
|
619
619
|
+ `\n abrupt terminations recorded: ${ledger.abruptTerminations ?? 1}`
|
|
620
|
-
+ `\n
|
|
620
|
+
+ `\n inspect member output: ours-fleet logs ${name}`);
|
|
621
621
|
const modelStatus = joinPath(agentDir(name), '.model-status');
|
|
622
622
|
if (existsSync(modelStatus)) {
|
|
623
623
|
try {
|
|
@@ -1867,6 +1867,33 @@ program.command('_wait-daemon <name>', { hidden: true }).description('internal:
|
|
|
1867
1867
|
die(e);
|
|
1868
1868
|
}
|
|
1869
1869
|
});
|
|
1870
|
+
program.command('_wait-temp-daemon <name>', { hidden: true })
|
|
1871
|
+
.action(async (name) => {
|
|
1872
|
+
try {
|
|
1873
|
+
await waitForRoleDaemon(name, undefined, {}, true);
|
|
1874
|
+
}
|
|
1875
|
+
catch (error) {
|
|
1876
|
+
die(error);
|
|
1877
|
+
}
|
|
1878
|
+
});
|
|
1879
|
+
program.command('_run-fleet', { hidden: true }).action(async () => {
|
|
1880
|
+
try {
|
|
1881
|
+
const { runFleetManager } = await import('./supervisor/manager.js');
|
|
1882
|
+
await runFleetManager(process.argv[1]);
|
|
1883
|
+
}
|
|
1884
|
+
catch (error) {
|
|
1885
|
+
die(error);
|
|
1886
|
+
}
|
|
1887
|
+
});
|
|
1888
|
+
program.command('_run-managed <key> <generation>', { hidden: true }).action(async (key, generation) => {
|
|
1889
|
+
try {
|
|
1890
|
+
const { runManagedMember } = await import('./supervisor/manager.js');
|
|
1891
|
+
await runManagedMember(key, generation);
|
|
1892
|
+
}
|
|
1893
|
+
catch (error) {
|
|
1894
|
+
die(error);
|
|
1895
|
+
}
|
|
1896
|
+
});
|
|
1870
1897
|
program.command('_run <name>', { hidden: true }).description('internal: supervisor entrypoint')
|
|
1871
1898
|
.option('-c, --configuration <file>')
|
|
1872
1899
|
.action(async (name, opts) => {
|
|
@@ -1889,9 +1916,10 @@ program.command('_run-temp <name>', { hidden: true }).description('internal: tem
|
|
|
1889
1916
|
}
|
|
1890
1917
|
});
|
|
1891
1918
|
program.command('_run-temp-worker <name>', { hidden: true })
|
|
1892
|
-
.
|
|
1919
|
+
.option('--suspend-on-stop')
|
|
1920
|
+
.action(async (name, options) => {
|
|
1893
1921
|
try {
|
|
1894
|
-
await runTemp(name);
|
|
1922
|
+
await runTemp(name, { suspendOnStop: options.suspendOnStop === true });
|
|
1895
1923
|
}
|
|
1896
1924
|
catch (error) {
|
|
1897
1925
|
if (error instanceof SupervisorRecycleRequiredError)
|
package/dist/config.d.ts
CHANGED
|
@@ -198,6 +198,8 @@ export interface AgentTemplateInstance {
|
|
|
198
198
|
}
|
|
199
199
|
/** Internal first-boot payload for a Fleet-provisioned Cowork room member. */
|
|
200
200
|
export interface RoomMemberStartup {
|
|
201
|
+
/** Task ownership; absent for standalone rooms and legacy snapshots. */
|
|
202
|
+
task_id?: string;
|
|
201
203
|
workspace?: import('./rooms-tasks/workspace.js').OwnedWorkspace;
|
|
202
204
|
room_id: string;
|
|
203
205
|
room_identity_cid: string;
|
|
@@ -30,6 +30,8 @@ export interface AgentSessionStartOptions {
|
|
|
30
30
|
cwd: string;
|
|
31
31
|
stateDir: string;
|
|
32
32
|
mode: 'fresh' | 'resume';
|
|
33
|
+
/** Durable task context must never silently fall back to a fresh session. */
|
|
34
|
+
requireResume?: boolean;
|
|
33
35
|
permissions: CommonPermissions;
|
|
34
36
|
permissionMode: {
|
|
35
37
|
fleetMode: FleetPermissionMode;
|
|
@@ -24,7 +24,7 @@ export class ClaudeCodeAgentSessionAdapter {
|
|
|
24
24
|
return this.transport({
|
|
25
25
|
name: role.name, harness: role.harness,
|
|
26
26
|
argv: launch.argv, cwd: options.cwd, env: launch.env,
|
|
27
|
-
stateDir: options.stateDir, mode: options.mode, permissions: options.permissions,
|
|
27
|
+
stateDir: options.stateDir, mode: options.mode, requireResume: options.requireResume, permissions: options.permissions,
|
|
28
28
|
modeId: this.strategy.permissionModeId(role),
|
|
29
29
|
mcpServers: options.managedOurs ? [...(this.strategy.mcpServers(role) ?? []).filter(s => s.name !== 'ours'), options.managedOurs.server] : this.strategy.mcpServers(role),
|
|
30
30
|
inheritEnvironment: options.managedOurs ? false : undefined,
|
|
@@ -32,7 +32,7 @@ export class CodexAgentSessionAdapter {
|
|
|
32
32
|
if (role.session === 'codex-app-server')
|
|
33
33
|
return this.nativeTransport({
|
|
34
34
|
name: role.name, argv: launch.argv, cwd: options.cwd, env: launch.env,
|
|
35
|
-
stateDir: options.stateDir, mode: options.mode, permissions: options.permissions,
|
|
35
|
+
stateDir: options.stateDir, mode: options.mode, requireResume: options.requireResume, permissions: options.permissions,
|
|
36
36
|
permissionMode: options.permissionMode,
|
|
37
37
|
model: role.model, effort: role.effort,
|
|
38
38
|
approvalPolicy: this.strategy.approvalPolicy(role),
|
|
@@ -44,7 +44,7 @@ export class CodexAgentSessionAdapter {
|
|
|
44
44
|
return this.transport({
|
|
45
45
|
name: role.name, harness: role.harness,
|
|
46
46
|
argv: launch.argv, cwd: options.cwd, env: launch.env,
|
|
47
|
-
stateDir: options.stateDir, mode: options.mode, permissions: options.permissions,
|
|
47
|
+
stateDir: options.stateDir, mode: options.mode, requireResume: options.requireResume, permissions: options.permissions,
|
|
48
48
|
modeId: this.strategy.permissionModeId(role),
|
|
49
49
|
mcpServers: options.managedOurs ? [...(this.strategy.mcpServers(role) ?? []).filter(s => s.name !== 'ours'), options.managedOurs.server] : this.strategy.mcpServers(role),
|
|
50
50
|
inheritEnvironment: options.managedOurs ? false : undefined,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Body-free recovery cursor. Dispatching is uncertain after a crash, never
|
|
2
|
+
* permission to replay an initial task which may already have caused effects. */
|
|
3
|
+
export interface ManagedRecovery {
|
|
4
|
+
version: 1;
|
|
5
|
+
session: 'pending' | 'established';
|
|
6
|
+
readiness: boolean;
|
|
7
|
+
initial: 'pending' | 'dispatching' | 'completed';
|
|
8
|
+
active: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare const managedRecoveryPath: (dir: string) => string;
|
|
11
|
+
export declare function readManagedRecovery(dir: string): ManagedRecovery | undefined;
|
|
12
|
+
export declare function writeManagedRecovery(dir: string, value: ManagedRecovery): void;
|
|
13
|
+
export declare function firstManagedSessionMayStart(dir: string): boolean;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { lstatSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { replaceFileAtomically } from './atomic-file.js';
|
|
4
|
+
export const managedRecoveryPath = (dir) => join(dir, '.managed-recovery.json');
|
|
5
|
+
export function readManagedRecovery(dir) {
|
|
6
|
+
const path = managedRecoveryPath(dir);
|
|
7
|
+
let stat;
|
|
8
|
+
try {
|
|
9
|
+
stat = lstatSync(path);
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
if (error.code === 'ENOENT')
|
|
13
|
+
return;
|
|
14
|
+
throw error;
|
|
15
|
+
}
|
|
16
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 4096)
|
|
17
|
+
throw Error('MANAGED_RECOVERY_UNSAFE_PROOF');
|
|
18
|
+
const value = JSON.parse(readFileSync(path, 'utf8'));
|
|
19
|
+
if (value.version !== 1 || !['pending', 'established'].includes(value.session)
|
|
20
|
+
|| !['pending', 'dispatching', 'completed'].includes(value.initial)
|
|
21
|
+
|| typeof value.readiness !== 'boolean' || typeof value.active !== 'boolean')
|
|
22
|
+
throw Error('MANAGED_RECOVERY_INVALID_PROOF');
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
export function writeManagedRecovery(dir, value) {
|
|
26
|
+
replaceFileAtomically(managedRecoveryPath(dir), JSON.stringify(value) + '\n');
|
|
27
|
+
}
|
|
28
|
+
export function firstManagedSessionMayStart(dir) {
|
|
29
|
+
const proof = readManagedRecovery(dir);
|
|
30
|
+
return Boolean(proof?.session === 'pending' && proof.initial === 'pending' && !proof.readiness && !proof.active);
|
|
31
|
+
}
|
package/dist/ops.d.ts
CHANGED
|
@@ -54,5 +54,7 @@ export declare function up(cfg: FleetConfig, names: string[], deps: OpsDeps, con
|
|
|
54
54
|
export declare function down(cfg: FleetConfig, names: string[], deps: OpsDeps): Promise<void>;
|
|
55
55
|
/** Re-sync from config + bounce. mode 'keep' resumes context; 'fresh' wipes it. */
|
|
56
56
|
export declare function restartRoles(cfg: FleetConfig, names: string[], deps: OpsDeps, mode: 'keep' | 'fresh', configPath?: string): Promise<void>;
|
|
57
|
+
/** Read-only admission check shared by CLI removal and web preview, before any archive. */
|
|
58
|
+
export declare function assertStandaloneRoleRemoval(cfg: FleetConfig, name: string): void;
|
|
57
59
|
/** Stop + forget a role: unit, state dir, and its fleet.d file when spawned. */
|
|
58
60
|
export declare function rmRole(cfg: FleetConfig, name: string, deps: OpsDeps): Promise<void>;
|
package/dist/ops.js
CHANGED
|
@@ -7,8 +7,8 @@ import { findRole } from './config.js';
|
|
|
7
7
|
import { getAdapter } from './harness/registry.js';
|
|
8
8
|
import { generateBriefing } from './briefing.js';
|
|
9
9
|
import { managedCliBriefing } from './managed-cli.js';
|
|
10
|
-
import { resetRestartLedger } from './runner.js';
|
|
11
|
-
import { archiveTempState, stopTempSupervisor, tempSupervisorLiveness, } from './temp-lifecycle.js';
|
|
10
|
+
import { resetRestartLedger, loadTempRole } from './runner.js';
|
|
11
|
+
import { archiveTempState, stopTempSupervisor, tempSupervisorLiveness, readTempSupervisor, } from './temp-lifecycle.js';
|
|
12
12
|
import { realExec } from './exec.js';
|
|
13
13
|
import { daemonIdentityProvisioner, reconcilePermanentRoleIdentities, } from './creation.js';
|
|
14
14
|
import { provesGeneratedAgentSource } from './generated-agent-source.js';
|
|
@@ -223,8 +223,21 @@ export async function restartRoles(cfg, names, deps, mode, configPath) {
|
|
|
223
223
|
: `↻ ${role.name} — restarted (resumes; briefing re-synced)`);
|
|
224
224
|
}
|
|
225
225
|
}
|
|
226
|
+
/** Read-only admission check shared by CLI removal and web preview, before any archive. */
|
|
227
|
+
export function assertStandaloneRoleRemoval(cfg, name) {
|
|
228
|
+
if (cfg.roles.some(role => role.name === name) || !/^[A-Za-z0-9_-]+$/.test(name))
|
|
229
|
+
return;
|
|
230
|
+
const dir = agentDir(name, true);
|
|
231
|
+
if (!existsSync(dir))
|
|
232
|
+
return;
|
|
233
|
+
const taskId = readTempSupervisor(dir)?.taskOwner?.taskId
|
|
234
|
+
?? (existsSync(join(dir, 'role.yaml')) ? loadTempRole(name).roomMemberStartup?.task_id : undefined);
|
|
235
|
+
if (taskId)
|
|
236
|
+
throw Error(`TASK_MEMBER_IS_TASK_OWNED: ${name} belongs to task ${taskId}; use the owning task lifecycle; if its record is missing or corrupt, preserve evidence and restore the original records before retrying task delete`);
|
|
237
|
+
}
|
|
226
238
|
/** Stop + forget a role: unit, state dir, and its fleet.d file when spawned. */
|
|
227
239
|
export async function rmRole(cfg, name, deps) {
|
|
240
|
+
assertStandaloneRoleRemoval(cfg, name);
|
|
228
241
|
const temporaryDir = /^[A-Za-z0-9_-]+$/.test(name) ? agentDir(name, true) : '';
|
|
229
242
|
const configured = cfg.roles.find(role => role.name === name);
|
|
230
243
|
if (!configured && temporaryDir && existsSync(temporaryDir)) {
|
package/dist/rooms-tasks/cli.js
CHANGED
|
@@ -891,6 +891,27 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
891
891
|
dieTaskRoom(e);
|
|
892
892
|
}
|
|
893
893
|
});
|
|
894
|
+
cOpt(taskCmd.command('recover-members <id>'))
|
|
895
|
+
.description('resume proven stopped task members in-place; never create identities or replay invites')
|
|
896
|
+
.option('--json', 'JSON output')
|
|
897
|
+
.action(async (id, opts) => {
|
|
898
|
+
try {
|
|
899
|
+
const { recoverTaskMembers } = await import('./recovery.js');
|
|
900
|
+
const task = getTask(id), cfg = loadConfig(opts.configuration);
|
|
901
|
+
const members = await recoverTaskMembers({ taskId: id, binPath: getBinPath(),
|
|
902
|
+
cowork: task.layout ? undefined : createCoworkAdapter({ configPath: cfg.rooms?.cowork?.config }) });
|
|
903
|
+
if (opts.json)
|
|
904
|
+
console.log(JSON.stringify({ schema_version: 1, task_id: id, members }, null, 2));
|
|
905
|
+
else
|
|
906
|
+
for (const member of members)
|
|
907
|
+
console.log(`${member.name}: ${member.status}`);
|
|
908
|
+
}
|
|
909
|
+
catch (e) {
|
|
910
|
+
if (opts.json)
|
|
911
|
+
die(e);
|
|
912
|
+
dieTaskRoom(e);
|
|
913
|
+
}
|
|
914
|
+
});
|
|
894
915
|
cOpt(taskCmd.command('start <id>'))
|
|
895
916
|
.description('idempotently select a plan, provision, and start a task')
|
|
896
917
|
.option('--template <name>', 'room template')
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { retireTaskMemberService } from './task-service-retirement.js';
|
|
1
2
|
import { withFileLock } from '../atomic-file.js';
|
|
2
3
|
import { type TempLifecycleDeps } from '../temp-lifecycle.js';
|
|
3
4
|
import { type CoworkAdapter } from './cowork-adapter.js';
|
|
@@ -5,6 +6,7 @@ import type { RoomMemberSeat, RoomOrchestrationRecord } from './types.js';
|
|
|
5
6
|
export declare const CLOSE_LOCK_STALE_MS: number;
|
|
6
7
|
export declare function roomCloseLockPath(roomId: string): string;
|
|
7
8
|
export interface RoomCloseDeps {
|
|
9
|
+
retireTaskService?: typeof retireTaskMemberService;
|
|
8
10
|
inspectMember?(seat: RoomMemberSeat): Promise<{
|
|
9
11
|
launchId: string;
|
|
10
12
|
}>;
|