@ours.network/fleet 1.0.3 → 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/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/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/runner.js +10 -33
- 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
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';
|
|
@@ -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)'} `
|
|
@@ -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",
|