@ours.network/fleet 1.0.2 → 1.0.4
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 +4 -4
- package/dist/application/role-command-service.d.ts +29 -1
- package/dist/application/role-command-service.js +41 -2
- package/dist/application/role-creation-service.d.ts +32 -1
- package/dist/application/role-creation-service.js +56 -10
- package/dist/application/role-removal-service.d.ts +19 -0
- package/dist/application/role-removal-service.js +13 -3
- package/dist/application/session-mutations.d.ts +7 -0
- package/dist/application/session-mutations.js +8 -0
- package/dist/application/task-room-service.d.ts +227 -0
- package/dist/application/task-room-service.js +529 -0
- package/dist/build-info.json +4 -4
- package/dist/cli.js +39 -15
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +7 -6
- package/dist/harness/codex-app-server-proxy.d.ts +1 -1
- package/dist/harness/codex-app-server-proxy.js +23 -9
- package/dist/harness/types.d.ts +4 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/owner-channel/channel.d.ts +5 -0
- package/dist/owner-channel/channel.js +143 -18
- package/dist/owner-channel/commands.d.ts +26 -1
- package/dist/owner-channel/commands.js +66 -128
- package/dist/rooms-tasks/cli.js +181 -462
- package/dist/rooms-tasks/provision.js +71 -8
- package/dist/rooms-tasks/types.d.ts +2 -0
- package/dist/runner.js +19 -44
- package/dist/session/acp.d.ts +8 -3
- package/dist/session/acp.js +24 -8
- package/dist/session/control.d.ts +10 -1
- package/dist/session/control.js +22 -21
- package/dist/watchdog/query.d.ts +2 -0
- package/dist/watchdog/query.js +7 -3
- package/dist/web/server.js +2 -2
- package/package.json +1 -1
|
@@ -5,6 +5,9 @@ import { advanceSaga, setSagaError, updateMemberSeats, updateMemberStartup, acti
|
|
|
5
5
|
import { activateTask, updateTaskMembers, blockTask, unblockTask, getTask, } from './task-state.js';
|
|
6
6
|
import { spawnTemp } from '../spawn.js';
|
|
7
7
|
import { findRole } from '../config.js';
|
|
8
|
+
import { FLEET_PROXY_CALLER_ENV, FLEET_PROXY_STATE_DIR_ENV, } from '../fleet-proxy.js';
|
|
9
|
+
import { controlRequest } from '../session/control.js';
|
|
10
|
+
import { SessionControlError } from '../session/types.js';
|
|
8
11
|
import { closeManagedRoom } from './close.js';
|
|
9
12
|
import { buildRoomMemberTask, sha256Text } from './member-startup.js';
|
|
10
13
|
import { agentDir } from '../paths.js';
|
|
@@ -18,6 +21,28 @@ export function getBinPath() {
|
|
|
18
21
|
return process.argv[1];
|
|
19
22
|
}
|
|
20
23
|
}
|
|
24
|
+
async function spawnRoomMember(options, binPath) {
|
|
25
|
+
const stateDir = process.env[FLEET_PROXY_STATE_DIR_ENV];
|
|
26
|
+
if (!stateDir)
|
|
27
|
+
return {
|
|
28
|
+
statePath: await spawnTemp(options, binPath),
|
|
29
|
+
creationActionId: options.creationActionId,
|
|
30
|
+
};
|
|
31
|
+
const response = await controlRequest(stateDir, { command: 'fleet_spawn', spawn: options }, 10 * 60_000);
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
throw new SessionControlError(response.kind ?? 'backend', response.error ?? 'managed room member spawn failed');
|
|
34
|
+
}
|
|
35
|
+
const result = response.result;
|
|
36
|
+
const expectedCaller = process.env[FLEET_PROXY_CALLER_ENV];
|
|
37
|
+
if (expectedCaller && result.caller !== expectedCaller) {
|
|
38
|
+
throw new Error(`fleet proxy caller mismatch: expected '${expectedCaller}', got '${result.caller}'`);
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
statePath: result.statePath,
|
|
42
|
+
creationActionId: result.creationActionId,
|
|
43
|
+
callerRole: result.caller,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
21
46
|
function shortId(id) { return id.slice(0, 8); }
|
|
22
47
|
function expandMembers(template, prefix) {
|
|
23
48
|
const result = [];
|
|
@@ -40,11 +65,15 @@ function settingsFor(member, cfg) {
|
|
|
40
65
|
refRole = findRole(cfg, member.roleRef);
|
|
41
66
|
}
|
|
42
67
|
catch { /* no ref role */ }
|
|
68
|
+
const permissions = member.overrides?.permissions;
|
|
43
69
|
return {
|
|
44
70
|
model: member.overrides?.model ?? refRole?.model,
|
|
45
71
|
harness: member.overrides?.harness ?? refRole?.harness,
|
|
46
72
|
cwd: member.overrides?.cwd ?? refRole?.cwd,
|
|
47
73
|
persona: member.overrides?.persona ?? refRole?.persona,
|
|
74
|
+
approval: permissions?.approval,
|
|
75
|
+
filesystem: permissions?.filesystem,
|
|
76
|
+
unattended: permissions?.unattended,
|
|
48
77
|
};
|
|
49
78
|
}
|
|
50
79
|
function roomTask(input, member, settings, members, roomIdentityCid, ownerSeatCid) {
|
|
@@ -96,9 +125,24 @@ async function retainRunningLaunch(input) {
|
|
|
96
125
|
.find(candidate => candidate.role_name === member.name);
|
|
97
126
|
const dir = agentDir(member.name, true);
|
|
98
127
|
const taskSha = sha256Text(task);
|
|
99
|
-
if ((seat.launch?.state === 'intent' || seat.launch?.state === 'launched'
|
|
128
|
+
if ((seat.launch?.state === 'intent' || seat.launch?.state === 'launched'
|
|
129
|
+
|| seat.launch?.state === 'failed') && existsSync(dir)) {
|
|
100
130
|
if (!seat.launch.action_id || !launchMatches(dir, member, seat.launch.action_id, taskSha, provision.roomId, roomIdentityCid, seat.invite_id)) {
|
|
101
|
-
|
|
131
|
+
const provenance = readProvenance(dir);
|
|
132
|
+
const adoptable = (seat.launch.state === 'intent' || seat.launch.state === 'failed')
|
|
133
|
+
&& Boolean(seat.launch.caller_role)
|
|
134
|
+
&& provenance?.surface === 'agent'
|
|
135
|
+
&& provenance.callerRole === seat.launch.caller_role
|
|
136
|
+
&& typeof provenance.creationActionId === 'string'
|
|
137
|
+
&& launchMatches(dir, member, provenance.creationActionId, taskSha, provision.roomId, roomIdentityCid, seat.invite_id);
|
|
138
|
+
if (!adoptable)
|
|
139
|
+
throw new Error(`existing launch for ${member.name} does not match its durable intent`);
|
|
140
|
+
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
141
|
+
...seat.launch, state: 'intent', action_id: provenance.creationActionId,
|
|
142
|
+
updated_at: new Date().toISOString(),
|
|
143
|
+
} });
|
|
144
|
+
seat = getRoomRecord(provision.roomId).member_seats
|
|
145
|
+
.find(candidate => candidate.role_name === member.name);
|
|
102
146
|
}
|
|
103
147
|
const supervisor = readTempSupervisor(dir);
|
|
104
148
|
if (!supervisor || supervisor.role !== member.name)
|
|
@@ -106,16 +150,17 @@ async function retainRunningLaunch(input) {
|
|
|
106
150
|
const live = await tempSupervisorLiveness(dir);
|
|
107
151
|
if (live === 'unknown')
|
|
108
152
|
throw new Error(`existing launch for ${member.name} has unknown liveness`);
|
|
153
|
+
const retainedLaunch = seat.launch;
|
|
109
154
|
if (live === 'running') {
|
|
110
155
|
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
111
|
-
...
|
|
156
|
+
...retainedLaunch, state: 'launched', launch_id: supervisor.launchId,
|
|
112
157
|
updated_at: new Date().toISOString(),
|
|
113
158
|
} });
|
|
114
159
|
return true;
|
|
115
160
|
}
|
|
116
161
|
await secureStoppedTempArchive(member.name, supervisor.launchId);
|
|
117
162
|
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
118
|
-
...
|
|
163
|
+
...retainedLaunch, state: 'stopped', launch_id: supervisor.launchId,
|
|
119
164
|
updated_at: new Date().toISOString(),
|
|
120
165
|
} });
|
|
121
166
|
return false;
|
|
@@ -151,14 +196,18 @@ async function launchMember(input) {
|
|
|
151
196
|
const seat = getRoomRecord(provision.roomId).member_seats
|
|
152
197
|
.find(candidate => candidate.role_name === member.name);
|
|
153
198
|
const actionId = randomUUID();
|
|
199
|
+
let effectiveActionId = actionId;
|
|
154
200
|
const attempt = (seat.launch?.attempt ?? 0) + 1;
|
|
155
201
|
const taskSha = sha256Text(startup.task);
|
|
202
|
+
const proxyCaller = process.env[FLEET_PROXY_STATE_DIR_ENV]
|
|
203
|
+
? process.env[FLEET_PROXY_CALLER_ENV] : undefined;
|
|
156
204
|
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
157
205
|
state: 'intent', attempt, action_id: actionId, mission_sha256: taskSha,
|
|
206
|
+
...(proxyCaller ? { caller_role: proxyCaller } : {}),
|
|
158
207
|
updated_at: new Date().toISOString(),
|
|
159
208
|
} });
|
|
160
209
|
try {
|
|
161
|
-
const
|
|
210
|
+
const launched = await spawnRoomMember({
|
|
162
211
|
name: member.name,
|
|
163
212
|
temp: true,
|
|
164
213
|
identity: member.name,
|
|
@@ -166,22 +215,36 @@ async function launchMember(input) {
|
|
|
166
215
|
model: settings.model,
|
|
167
216
|
harness: settings.harness,
|
|
168
217
|
cwd: settings.cwd,
|
|
218
|
+
approval: settings.approval,
|
|
219
|
+
filesystem: settings.filesystem,
|
|
220
|
+
unattended: settings.unattended,
|
|
169
221
|
surface: 'agent',
|
|
170
222
|
creationActionId: actionId,
|
|
171
223
|
roomMemberStartup: startup,
|
|
172
224
|
}, provision.binPath);
|
|
225
|
+
const launchedDir = launched.statePath;
|
|
226
|
+
effectiveActionId = launched.creationActionId;
|
|
227
|
+
if (launched.creationActionId !== actionId) {
|
|
228
|
+
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
229
|
+
state: 'intent', attempt, action_id: launched.creationActionId,
|
|
230
|
+
mission_sha256: taskSha, updated_at: new Date().toISOString(),
|
|
231
|
+
...(launched.callerRole ? { caller_role: launched.callerRole } : {}),
|
|
232
|
+
} });
|
|
233
|
+
}
|
|
173
234
|
const supervisor = readTempSupervisor(launchedDir);
|
|
174
|
-
if (!supervisor || supervisor.role !== member.name || !launchMatches(launchedDir, member,
|
|
235
|
+
if (!supervisor || supervisor.role !== member.name || !launchMatches(launchedDir, member, launched.creationActionId, taskSha, provision.roomId, startup.room_identity_cid, startup.invite_id)) {
|
|
175
236
|
throw new Error(`new launch for ${member.name} did not persist matching provenance`);
|
|
176
237
|
}
|
|
177
238
|
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
178
|
-
state: 'launched', attempt, action_id:
|
|
239
|
+
state: 'launched', attempt, action_id: launched.creationActionId, mission_sha256: taskSha,
|
|
240
|
+
...(launched.callerRole ? { caller_role: launched.callerRole } : {}),
|
|
179
241
|
launch_id: supervisor.launchId, updated_at: new Date().toISOString(),
|
|
180
242
|
} });
|
|
181
243
|
}
|
|
182
244
|
catch (error) {
|
|
183
245
|
updateMemberStartup(provision.roomId, member.name, { launch: {
|
|
184
|
-
state: 'failed', attempt, action_id:
|
|
246
|
+
state: 'failed', attempt, action_id: effectiveActionId, mission_sha256: taskSha,
|
|
247
|
+
...(proxyCaller ? { caller_role: proxyCaller } : {}),
|
|
185
248
|
updated_at: new Date().toISOString(),
|
|
186
249
|
error: error instanceof Error ? error.message : String(error),
|
|
187
250
|
} });
|
|
@@ -87,6 +87,8 @@ export interface RoomMemberLaunchState {
|
|
|
87
87
|
state: 'pending' | 'intent' | 'launched' | 'stopped' | 'failed';
|
|
88
88
|
attempt: number;
|
|
89
89
|
action_id?: string;
|
|
90
|
+
/** Expected authenticated proxy caller while adopting a post-spawn crash. */
|
|
91
|
+
caller_role?: string;
|
|
90
92
|
mission_sha256?: string;
|
|
91
93
|
launch_id?: string;
|
|
92
94
|
updated_at: string;
|
package/dist/runner.js
CHANGED
|
@@ -23,7 +23,7 @@ import { OwnerChannel } from './owner-channel/channel.js';
|
|
|
23
23
|
import { acquireOwnerBinderLease, OwnerBinderHandoffTimeoutError, } from './owner-channel/binder.js';
|
|
24
24
|
import { RoleTurnArbiter } from './session/arbiter.js';
|
|
25
25
|
import { ScheduledLoopManager, } from './loops/manager.js';
|
|
26
|
-
import { FLEET_PROXY_CALLER_ENV, FLEET_PROXY_STATE_DIR_ENV,
|
|
26
|
+
import { FLEET_PROXY_CALLER_ENV, FLEET_PROXY_STATE_DIR_ENV, } from './fleet-proxy.js';
|
|
27
27
|
import { effectivePermissionMode } from './permissions.js';
|
|
28
28
|
import { assertModelPinReachesChild, effectiveRoleModel, repinModelEnv } from './model-env.js';
|
|
29
29
|
import { archiveTempState, markTempSupervisorActive, requestedTempStopReason, } from './temp-lifecycle.js';
|
|
@@ -61,15 +61,15 @@ const defaultDeps = () => ({
|
|
|
61
61
|
},
|
|
62
62
|
});
|
|
63
63
|
const MONITOR_OWNER_FILE = '.monitor-owner';
|
|
64
|
-
|
|
65
|
-
const FLEET_OURS_AUTOSTART = '0';
|
|
64
|
+
const OBSOLETE_OURS_AUTOSTART_ENV = 'OURS_AUTOSTART';
|
|
66
65
|
/** Environment injected only into the managed harness process. */
|
|
67
66
|
export function managedFleetProxyEnv(role, stateDir) {
|
|
67
|
+
const roleEnv = { ...(role.env ?? {}) };
|
|
68
|
+
// ours-mcp 1.0 treats presence (even "0") as a fatal legacy lifecycle mode.
|
|
69
|
+
// Managed children are daemon clients; the proxy itself never starts one.
|
|
70
|
+
delete roleEnv[OBSOLETE_OURS_AUTOSTART_ENV];
|
|
68
71
|
return {
|
|
69
|
-
...
|
|
70
|
-
// This must win over both inherited/configured auto-start. ACP agents run
|
|
71
|
-
// directly rather than through ours-codex, so the runner owns this fence.
|
|
72
|
-
OURS_AUTOSTART: FLEET_OURS_AUTOSTART,
|
|
72
|
+
...roleEnv,
|
|
73
73
|
[FLEET_PROXY_STATE_DIR_ENV]: stateDir,
|
|
74
74
|
[FLEET_PROXY_CALLER_ENV]: role.name,
|
|
75
75
|
};
|
|
@@ -92,11 +92,6 @@ export function harnessChildEnv(role, launchEnv, stateDir) {
|
|
|
92
92
|
* avoid a runner↔spawn initialization cycle (spawn imports runner constants).
|
|
93
93
|
*/
|
|
94
94
|
async function executeManagedSpawn(caller, configPath, requested, log) {
|
|
95
|
-
const { options, inherited } = inheritCallerSpawnDefaults(caller, requested, configPath);
|
|
96
|
-
const creationActionId = randomUUID();
|
|
97
|
-
options.creationActionId = creationActionId;
|
|
98
|
-
const spawnModule = await import('./spawn.js');
|
|
99
|
-
const preview = spawnModule.spawnDryRun(options).resolvedRole;
|
|
100
95
|
const runtimeBinPath = (() => {
|
|
101
96
|
try {
|
|
102
97
|
return realpathSync(process.argv[1]);
|
|
@@ -105,33 +100,15 @@ async function executeManagedSpawn(caller, configPath, requested, log) {
|
|
|
105
100
|
return process.argv[1];
|
|
106
101
|
}
|
|
107
102
|
})();
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
watchdogService: new WatchdogServiceManager(),
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
const result = {
|
|
121
|
-
caller: caller.name,
|
|
122
|
-
role: options.name,
|
|
123
|
-
lifetime: options.temp ? 'temporary' : 'permanent',
|
|
124
|
-
statePath,
|
|
125
|
-
harness: preview.harness,
|
|
126
|
-
session: preview.session,
|
|
127
|
-
// Read back from the resolved environment, not from the request: the banner
|
|
128
|
-
// must name the model the child will run, not the one that was asked for.
|
|
129
|
-
...(effectiveRoleModel(preview) ? { model: effectiveRoleModel(preview) } : {}),
|
|
130
|
-
monitor: { mode: preview.monitor.mode, interrupt: preview.monitor.interrupt },
|
|
131
|
-
permissionMode: effectivePermissionMode(preview),
|
|
132
|
-
inherited,
|
|
133
|
-
creationActionId,
|
|
134
|
-
};
|
|
103
|
+
const [{ RoleCreationService }, { pickBackend }, { WatchdogServiceManager }] = await Promise.all([
|
|
104
|
+
import('./application/role-creation-service.js'), import('./supervisor/index.js'),
|
|
105
|
+
import('./watchdog/service.js'),
|
|
106
|
+
]);
|
|
107
|
+
const service = new RoleCreationService({ configPath,
|
|
108
|
+
ops: { backend: pickBackend(), binPath: runtimeBinPath, log,
|
|
109
|
+
watchdogService: new WatchdogServiceManager() },
|
|
110
|
+
binPath: runtimeBinPath, journal: false });
|
|
111
|
+
const result = await service.createManaged(caller, requested);
|
|
135
112
|
log(`[${caller.name}] managed fleet proxy spawned ${result.lifetime} role ${result.role} `
|
|
136
113
|
+ `harness=${result.harness} session=${result.session} `
|
|
137
114
|
+ `model=${result.model ?? '(harness default)'} `
|
|
@@ -166,17 +143,14 @@ export function recordMonitorOwner(dir, owner) {
|
|
|
166
143
|
export function buildPaneCommand(launch, roleEnv, exitStatusPath, paneArgv = launch.argv) {
|
|
167
144
|
const env = {
|
|
168
145
|
PATH: process.env.PATH ?? '', COLORTERM: 'truecolor', ...launch.env, ...(roleEnv ?? {}),
|
|
169
|
-
// Tmux roles have the same daemon-client boundary as ACP roles. Keep this
|
|
170
|
-
// last so neither harness preparation nor a role env block can take over
|
|
171
|
-
// the shared daemon lifecycle.
|
|
172
|
-
OURS_AUTOSTART: FLEET_OURS_AUTOSTART,
|
|
173
146
|
};
|
|
147
|
+
delete env[OBSOLETE_OURS_AUTOSTART_ENV];
|
|
174
148
|
// Interactive panes should advertise colour even when the supervisor itself
|
|
175
149
|
// was launched with NO_COLOR. A role may still deliberately opt back in to
|
|
176
150
|
// NO_COLOR (or replace COLORTERM) through its explicit env block.
|
|
177
151
|
const unsetNoColor = Object.prototype.hasOwnProperty.call(roleEnv ?? {}, 'NO_COLOR')
|
|
178
152
|
? '' : '-u NO_COLOR ';
|
|
179
|
-
const envPfx = 'env ' + unsetNoColor
|
|
153
|
+
const envPfx = 'env -u OURS_AUTOSTART ' + unsetNoColor
|
|
180
154
|
+ Object.entries(env).map(([k, v]) => `${k}=${shq(v)}`).join(' ');
|
|
181
155
|
const cmd = paneArgv.map(shq).join(' ');
|
|
182
156
|
// Write a structured record, not a bare number: the wait status alone cannot
|
|
@@ -645,6 +619,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
645
619
|
// role-only adapter hook prevents a PATH fallback or resolver skew from
|
|
646
620
|
// claiming metadata trust for an argv it did not authenticate.
|
|
647
621
|
permissionMetadataSource: launch.permissionMetadataSource,
|
|
622
|
+
scrubObsoleteOursAutostart: true,
|
|
648
623
|
log: deps.log,
|
|
649
624
|
});
|
|
650
625
|
pid = acpSession.pid;
|
package/dist/session/acp.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ export declare const AFTER_TOOL_BOUNDARY_TIMEOUT_MS = 120000;
|
|
|
21
21
|
* live role.
|
|
22
22
|
*/
|
|
23
23
|
export declare const STEERING_OCCUPANCY_IDLE_MS = 150000;
|
|
24
|
+
/** Consumed only by Fleet's authenticated bundled-Codex app-server proxy. */
|
|
25
|
+
export declare const CODEX_DISABLE_INHERITED_MCP_ENV = "OURS_FLEET_CODEX_DISABLE_INHERITED_MCP";
|
|
24
26
|
/** Server-generated typed provenance followed by the exact human-authored body. */
|
|
25
27
|
export declare function promptContentBlocks(text: string, origin?: PromptOrigin): acp.ContentBlock[];
|
|
26
28
|
export declare function runtimeSelector(options: acp.SessionConfigOption[] | null | undefined, category: string): RuntimeSelectorMetadata | undefined;
|
|
@@ -38,10 +40,13 @@ export interface AcpSessionOptions {
|
|
|
38
40
|
permissionMode?: NonNullable<SessionSnapshot['permissionMode']>;
|
|
39
41
|
/** Adapter-authenticated request-metadata vocabulary; never inferred from ACP `_meta`. */
|
|
40
42
|
permissionMetadataSource?: 'codex-acp';
|
|
43
|
+
/** Fleet-managed ours proxies must never receive the obsolete presence-sensitive flag. */
|
|
44
|
+
scrubObsoleteOursAutostart?: boolean;
|
|
41
45
|
/**
|
|
42
46
|
* MCP servers the ROLE declares, for every session/new, resume and load.
|
|
43
|
-
* Omitted preserves inherited configuration
|
|
44
|
-
* every inherited server
|
|
47
|
+
* Omitted preserves inherited configuration (encoded as ACP's required `[]`);
|
|
48
|
+
* an explicit empty array disables every inherited server through the
|
|
49
|
+
* authenticated bundled-adapter compatibility path.
|
|
45
50
|
*/
|
|
46
51
|
mcpServers?: AcpMcpServer[];
|
|
47
52
|
/**
|
|
@@ -258,7 +263,7 @@ export declare class AcpSession implements SessionHandle {
|
|
|
258
263
|
private settlePendingAutomatically;
|
|
259
264
|
exitResult(): ExitRecord | null;
|
|
260
265
|
close(): Promise<void>;
|
|
261
|
-
/**
|
|
266
|
+
/** ACP requires the field. Bundled agents treat [] as no client-added servers. */
|
|
262
267
|
private declaredMcpServers;
|
|
263
268
|
private initialize;
|
|
264
269
|
private captureRuntimeMetadata;
|
package/dist/session/acp.js
CHANGED
|
@@ -32,6 +32,8 @@ export const AFTER_TOOL_BOUNDARY_TIMEOUT_MS = 120_000;
|
|
|
32
32
|
*/
|
|
33
33
|
export const STEERING_OCCUPANCY_IDLE_MS = 150_000;
|
|
34
34
|
const TERMINAL_TOOL_STATUSES = new Set(['completed', 'failed']);
|
|
35
|
+
/** Consumed only by Fleet's authenticated bundled-Codex app-server proxy. */
|
|
36
|
+
export const CODEX_DISABLE_INHERITED_MCP_ENV = 'OURS_FLEET_CODEX_DISABLE_INHERITED_MCP';
|
|
35
37
|
const SCHEDULED_LOOP_REDACTION = '[scheduled-loop content redacted]';
|
|
36
38
|
const OWNER_COMMENTARY_REDACTION = '[assistant commentary redacted]';
|
|
37
39
|
const MAX_CANONICAL_SYMLINK_DEPTH = 40;
|
|
@@ -304,9 +306,25 @@ export class AcpSession {
|
|
|
304
306
|
static async start(options) {
|
|
305
307
|
if (!options.argv.length)
|
|
306
308
|
throw new Error('ACP agent command is empty');
|
|
309
|
+
const disableInheritedCodexMcp = options.permissionMetadataSource === 'codex-acp'
|
|
310
|
+
&& options.mcpServers !== undefined && options.mcpServers.length === 0;
|
|
311
|
+
// Obsolete ours-mcp lifecycle flags are presence-sensitive. The shared
|
|
312
|
+
// daemon remains operator-owned; managed ACP children are clients only.
|
|
313
|
+
const childEnv = {
|
|
314
|
+
...process.env,
|
|
315
|
+
...options.env,
|
|
316
|
+
};
|
|
317
|
+
if (options.scrubObsoleteOursAutostart)
|
|
318
|
+
delete childEnv.OURS_AUTOSTART;
|
|
319
|
+
Object.assign(childEnv,
|
|
320
|
+
// Write both states for the authenticated proxy: a stale ambient `1`
|
|
321
|
+
// must never leak explicit-empty semantics into a later inherited role.
|
|
322
|
+
options.permissionMetadataSource === 'codex-acp'
|
|
323
|
+
? { [CODEX_DISABLE_INHERITED_MCP_ENV]: disableInheritedCodexMcp ? '1' : '0' }
|
|
324
|
+
: {});
|
|
307
325
|
const child = spawn(options.argv[0], options.argv.slice(1), {
|
|
308
326
|
cwd: options.cwd,
|
|
309
|
-
env:
|
|
327
|
+
env: childEnv,
|
|
310
328
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
311
329
|
});
|
|
312
330
|
await new Promise((resolve, reject) => {
|
|
@@ -991,11 +1009,9 @@ export class AcpSession {
|
|
|
991
1009
|
});
|
|
992
1010
|
this.conversation.close();
|
|
993
1011
|
}
|
|
994
|
-
/**
|
|
1012
|
+
/** ACP requires the field. Bundled agents treat [] as no client-added servers. */
|
|
995
1013
|
declaredMcpServers() {
|
|
996
|
-
return this.options.mcpServers
|
|
997
|
-
? {}
|
|
998
|
-
: { mcpServers: this.options.mcpServers };
|
|
1014
|
+
return this.options.mcpServers ?? [];
|
|
999
1015
|
}
|
|
1000
1016
|
async initialize() {
|
|
1001
1017
|
const initialized = await this.connection.agent.request(acp.methods.agent.initialize, {
|
|
@@ -1016,7 +1032,7 @@ export class AcpSession {
|
|
|
1016
1032
|
const resumed = await this.connection.agent.request(acp.methods.agent.session.resume, {
|
|
1017
1033
|
sessionId: persisted,
|
|
1018
1034
|
cwd: this.options.cwd,
|
|
1019
|
-
|
|
1035
|
+
mcpServers: this.declaredMcpServers(),
|
|
1020
1036
|
});
|
|
1021
1037
|
this.captureRuntimeMetadata(resumed.configOptions);
|
|
1022
1038
|
this.sessionId = persisted;
|
|
@@ -1029,7 +1045,7 @@ export class AcpSession {
|
|
|
1029
1045
|
const loaded = await this.connection.agent.request(acp.methods.agent.session.load, {
|
|
1030
1046
|
sessionId: persisted,
|
|
1031
1047
|
cwd: this.options.cwd,
|
|
1032
|
-
|
|
1048
|
+
mcpServers: this.declaredMcpServers(),
|
|
1033
1049
|
});
|
|
1034
1050
|
this.captureRuntimeMetadata(loaded.configOptions);
|
|
1035
1051
|
}
|
|
@@ -1041,7 +1057,7 @@ export class AcpSession {
|
|
|
1041
1057
|
else {
|
|
1042
1058
|
const created = await this.connection.agent.request(acp.methods.agent.session.new, {
|
|
1043
1059
|
cwd: this.options.cwd,
|
|
1044
|
-
|
|
1060
|
+
mcpServers: this.declaredMcpServers(),
|
|
1045
1061
|
...(this.options.sessionMeta ? { _meta: this.options.sessionMeta } : {}),
|
|
1046
1062
|
});
|
|
1047
1063
|
this.sessionId = created.sessionId;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Socket } from 'node:net';
|
|
2
|
-
import type { ControlFailureKind, SessionHandle } from './types.js';
|
|
2
|
+
import type { ControlFailureKind, SessionEvent, SessionHandle, SessionSnapshot } from './types.js';
|
|
3
3
|
import type { OwnerChannelHandle, OwnerChannelManagementRequest } from '../owner-channel/channel.js';
|
|
4
4
|
import type { ScheduledLoopManagerHandle } from '../loops/manager.js';
|
|
5
5
|
import type { SpawnOpts } from '../spawn.js';
|
|
@@ -40,6 +40,15 @@ export interface ControlResponse {
|
|
|
40
40
|
/** Why it failed, so the caller does not have to guess from the text. */
|
|
41
41
|
kind?: ControlFailureKind;
|
|
42
42
|
}
|
|
43
|
+
export interface RetainedEventPage {
|
|
44
|
+
events: SessionEvent[];
|
|
45
|
+
snapshot: SessionSnapshot;
|
|
46
|
+
firstSeq: number;
|
|
47
|
+
lastSeq: number;
|
|
48
|
+
truncated: boolean;
|
|
49
|
+
}
|
|
50
|
+
/** The one retained-range projection shared by polling and live-follow admission. */
|
|
51
|
+
export declare function retainedEventPage(session: SessionHandle, since: number): RetainedEventPage;
|
|
43
52
|
/**
|
|
44
53
|
* One line saying what a control failure does — and does not — prove about the
|
|
45
54
|
* agent. Only `offline` is evidence that it is gone; every other kind used to
|
package/dist/session/control.js
CHANGED
|
@@ -2,13 +2,26 @@ import { randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
|
2
2
|
import { chmodSync, existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { createConnection, createServer } from 'node:net';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
-
import { SessionControlError
|
|
5
|
+
import { SessionControlError } from './types.js';
|
|
6
|
+
import { interruptSession, queueSessionPrompt, respondSessionPermission, respondSessionPermissionV2, } from '../application/session-mutations.js';
|
|
6
7
|
const MAX_LINE_BYTES = 64 * 1024;
|
|
7
8
|
/** Commands that require protocol version 3. */
|
|
8
9
|
const V3_COMMANDS = new Set([
|
|
9
10
|
'conversation_page', 'conversation_follow', 'submit_prompt_v2', 'interrupt_v2',
|
|
10
11
|
'respond_permission_v2',
|
|
11
12
|
]);
|
|
13
|
+
/** The one retained-range projection shared by polling and live-follow admission. */
|
|
14
|
+
export function retainedEventPage(session, since) {
|
|
15
|
+
const events = session.eventsSince(since);
|
|
16
|
+
const all = session.eventsSince(0);
|
|
17
|
+
return {
|
|
18
|
+
events,
|
|
19
|
+
snapshot: session.snapshot(),
|
|
20
|
+
firstSeq: all[0]?.seq ?? 0,
|
|
21
|
+
lastSeq: all.at(-1)?.seq ?? 0,
|
|
22
|
+
truncated: Boolean(all[0] && since > 0 && since < all[0].seq - 1),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
12
25
|
/**
|
|
13
26
|
* One line saying what a control failure does — and does not — prove about the
|
|
14
27
|
* agent. Only `offline` is evidence that it is gone; every other kind used to
|
|
@@ -230,7 +243,7 @@ export class RoleControlServer {
|
|
|
230
243
|
// Answer on QUEUE ACCEPTANCE, not on turn completion. A turn can run
|
|
231
244
|
// for minutes; blocking here made every `send` into a busy agent time
|
|
232
245
|
// out, and the timeout was then reported as a dead agent.
|
|
233
|
-
const queued = await this.session
|
|
246
|
+
const queued = await queueSessionPrompt(this.session, request.text, {
|
|
234
247
|
origin: { kind: 'local-console' },
|
|
235
248
|
});
|
|
236
249
|
this.write(socket, {
|
|
@@ -244,7 +257,7 @@ export class RoleControlServer {
|
|
|
244
257
|
case 'respond_permission': {
|
|
245
258
|
if (!request.permissionId || !request.optionId)
|
|
246
259
|
throw new SessionControlError('rejected', 'permissionId and optionId are required');
|
|
247
|
-
const accepted = this.session
|
|
260
|
+
const accepted = respondSessionPermission(this.session, request.permissionId, request.optionId);
|
|
248
261
|
this.write(socket, {
|
|
249
262
|
version: 1, id: request.id, ok: accepted,
|
|
250
263
|
result: { accepted },
|
|
@@ -256,7 +269,7 @@ export class RoleControlServer {
|
|
|
256
269
|
case 'interrupt': {
|
|
257
270
|
// Forced recovery cancelled the turn just as surely as a cooperative
|
|
258
271
|
// stop did. Report HOW, never as a failed operation.
|
|
259
|
-
const outcome =
|
|
272
|
+
const outcome = await interruptSession(this.session, 'local-console');
|
|
260
273
|
this.write(socket, { version: 1, id: request.id, ok: true, result: outcome });
|
|
261
274
|
return;
|
|
262
275
|
}
|
|
@@ -309,29 +322,17 @@ export class RoleControlServer {
|
|
|
309
322
|
}
|
|
310
323
|
case 'events_since': {
|
|
311
324
|
const since = Number.isFinite(request.since) ? Number(request.since) : 0;
|
|
312
|
-
const events = this.session.eventsSince(since);
|
|
313
|
-
const all = this.session.eventsSince(0);
|
|
314
325
|
this.write(socket, {
|
|
315
326
|
version: 1, id: request.id, ok: true,
|
|
316
|
-
result:
|
|
317
|
-
events, snapshot: this.session.snapshot(),
|
|
318
|
-
firstSeq: all[0]?.seq ?? 0, lastSeq: all.at(-1)?.seq ?? 0,
|
|
319
|
-
truncated: Boolean(all[0] && since > 0 && since < all[0].seq - 1),
|
|
320
|
-
},
|
|
327
|
+
result: retainedEventPage(this.session, since),
|
|
321
328
|
});
|
|
322
329
|
return;
|
|
323
330
|
}
|
|
324
331
|
case 'follow': {
|
|
325
332
|
const since = Number.isFinite(request.since) ? Number(request.since) : 0;
|
|
326
|
-
const events = this.session.eventsSince(since);
|
|
327
|
-
const all = this.session.eventsSince(0);
|
|
328
333
|
this.write(socket, {
|
|
329
334
|
version: 1, id: request.id, ok: true,
|
|
330
|
-
result:
|
|
331
|
-
events, snapshot: this.session.snapshot(),
|
|
332
|
-
firstSeq: all[0]?.seq ?? 0, lastSeq: all.at(-1)?.seq ?? 0,
|
|
333
|
-
truncated: Boolean(all[0] && since > 0 && since < all[0].seq - 1),
|
|
334
|
-
},
|
|
335
|
+
result: retainedEventPage(this.session, since),
|
|
335
336
|
});
|
|
336
337
|
const controller = request.controller !== false;
|
|
337
338
|
if (controller)
|
|
@@ -399,7 +400,7 @@ export class RoleControlServer {
|
|
|
399
400
|
this.write(socket, { version: 1, id: request.id, ok: true, result: existing });
|
|
400
401
|
return;
|
|
401
402
|
}
|
|
402
|
-
const outcome =
|
|
403
|
+
const outcome = await interruptSession(this.session, 'local-console');
|
|
403
404
|
const receipt = {
|
|
404
405
|
accepted: true, commandId: request.commandId, at: new Date().toISOString(),
|
|
405
406
|
...outcome,
|
|
@@ -417,9 +418,9 @@ export class RoleControlServer {
|
|
|
417
418
|
if (!request.commandId?.trim() || !request.permissionId?.trim()
|
|
418
419
|
|| !request.optionId?.trim() || !request.sessionGeneration?.trim())
|
|
419
420
|
throw new SessionControlError('rejected', 'commandId, permissionId, optionId and sessionGeneration are required');
|
|
420
|
-
|
|
421
|
+
const result = respondSessionPermissionV2(this.session, request.permissionId, request.optionId, request.sessionGeneration);
|
|
422
|
+
if (result === 'unavailable')
|
|
421
423
|
throw new SessionControlError('rejected', 'generation-bound permission responses are unavailable for this role');
|
|
422
|
-
const result = this.session.respondPermissionV2(request.permissionId, request.optionId, request.sessionGeneration);
|
|
423
424
|
if (result === 'stale')
|
|
424
425
|
throw new SessionControlError('rejected', 'stale_state: permission is settled, expired, invalid, or belongs to another session generation');
|
|
425
426
|
this.write(socket, {
|
package/dist/watchdog/query.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ export interface WatchdogRoleFinding {
|
|
|
6
6
|
status: WatchdogRoleStatus;
|
|
7
7
|
reason: string;
|
|
8
8
|
}
|
|
9
|
+
/** Shared configured-or-surviving-history addressability rule. */
|
|
10
|
+
export declare function watchdogAddressable(name: string, configured: readonly string[], historyExists?: (validName: string) => boolean): boolean;
|
|
9
11
|
/**
|
|
10
12
|
* Needs-attention integration: worst current finding per role across
|
|
11
13
|
* every configured watchdog, for FleetQueryService.status() to fold into a
|
package/dist/watchdog/query.js
CHANGED
|
@@ -6,6 +6,12 @@ import { watchdogsRoot } from '../paths.js';
|
|
|
6
6
|
import { WATCHDOG_STATUS_RANK } from './alerts.js';
|
|
7
7
|
import { readSchedulerState } from './scheduler.js';
|
|
8
8
|
import { listRuns, readReport } from './store.js';
|
|
9
|
+
/** Shared configured-or-surviving-history addressability rule. */
|
|
10
|
+
export function watchdogAddressable(name, configured, historyExists = validName => existsSync(join(watchdogsRoot(), validName))) {
|
|
11
|
+
if (configured.includes(name))
|
|
12
|
+
return true;
|
|
13
|
+
return ROLE_NAME_RE.test(name) && historyExists(name);
|
|
14
|
+
}
|
|
9
15
|
/**
|
|
10
16
|
* Needs-attention integration: worst current finding per role across
|
|
11
17
|
* every configured watchdog, for FleetQueryService.status() to fold into a
|
|
@@ -115,9 +121,7 @@ export class WatchdogQueryService {
|
|
|
115
121
|
*/
|
|
116
122
|
requireKnown(name) {
|
|
117
123
|
const cfg = this.cfgProvider();
|
|
118
|
-
if (cfg.watchdogs.
|
|
119
|
-
return;
|
|
120
|
-
if (ROLE_NAME_RE.test(name) && existsSync(join(watchdogsRoot(), name)))
|
|
124
|
+
if (watchdogAddressable(name, cfg.watchdogs.map(wd => wd.name)))
|
|
121
125
|
return;
|
|
122
126
|
throw new FleetError('role_not_found', `no such watchdog '${name}'`);
|
|
123
127
|
}
|
package/dist/web/server.js
CHANGED
|
@@ -209,7 +209,7 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
209
209
|
throw new FleetError('invalid_request', 'invalid role name');
|
|
210
210
|
if (!services.removal)
|
|
211
211
|
throw new FleetError('capability_unavailable', 'role removal is unavailable');
|
|
212
|
-
return services.removal.
|
|
212
|
+
return services.removal.previewWeb(request.params.id);
|
|
213
213
|
});
|
|
214
214
|
app.post('/api/v1/roles/:id/remove', async (request) => {
|
|
215
215
|
const session = auth.authenticate(request, true);
|
|
@@ -218,7 +218,7 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
218
218
|
if (!services.removal)
|
|
219
219
|
throw new FleetError('capability_unavailable', 'role removal is unavailable');
|
|
220
220
|
const body = request.body;
|
|
221
|
-
const result = await services.removal.
|
|
221
|
+
const result = await services.removal.removeWeb({ role: request.params.id, ...body });
|
|
222
222
|
events.publish('role.removed', { role: result.role }, result.role);
|
|
223
223
|
await audit.record({ requestId: request.id, browser: session.id, roleId: result.role, action: 'role.remove', result: 'succeeded' });
|
|
224
224
|
return result;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ours.network/fleet",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
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",
|