@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/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, inheritCallerSpawnDefaults, } from './fleet-proxy.js';
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
- let statePath;
109
- if (options.temp) {
110
- statePath = await spawnModule.spawnTemp(options, runtimeBinPath);
111
- }
112
- else {
113
- const { pickBackend } = await import('./supervisor/index.js');
114
- const { WatchdogServiceManager } = await import('./watchdog/service.js');
115
- statePath = await spawnModule.spawnPermanent(options, {
116
- backend: pickBackend(), binPath: runtimeBinPath, log,
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
@@ -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, interruptOutcome } from './types.js';
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.queuePrompt(request.text, {
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.respondPermission(request.permissionId, request.optionId);
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 = interruptOutcome(await this.session.interrupt('local-console'));
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 = interruptOutcome(await this.session.interrupt('local-console'));
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
- if (!this.session.respondPermissionV2)
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, {
@@ -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
@@ -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.some(wd => wd.name === name))
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
  }
@@ -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.preview(request.params.id);
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.remove({ role: request.params.id, ...body });
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",
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",