@ours.network/fleet 0.15.1 → 0.15.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.
Files changed (65) hide show
  1. package/README.md +49 -13
  2. package/dist/application/fleet-query-service.js +3 -0
  3. package/dist/application/model-catalog.d.ts +20 -0
  4. package/dist/application/model-catalog.js +57 -0
  5. package/dist/application/role-creation-service.d.ts +7 -0
  6. package/dist/application/role-creation-service.js +21 -4
  7. package/dist/application/role-removal-service.d.ts +32 -0
  8. package/dist/application/role-removal-service.js +87 -0
  9. package/dist/application/role-repository.js +13 -1
  10. package/dist/application/session-control.d.ts +74 -0
  11. package/dist/application/session-control.js +66 -1
  12. package/dist/application/types.d.ts +18 -0
  13. package/dist/briefing.js +21 -1
  14. package/dist/cli.js +39 -7
  15. package/dist/config.d.ts +4 -1
  16. package/dist/config.js +3 -2
  17. package/dist/creation.d.ts +6 -3
  18. package/dist/creation.js +5 -1
  19. package/dist/docs.d.ts +1 -1
  20. package/dist/docs.js +47 -11
  21. package/dist/fleet-proxy.d.ts +25 -0
  22. package/dist/fleet-proxy.js +38 -0
  23. package/dist/harness/claude-code.js +20 -3
  24. package/dist/harness/codex.js +14 -2
  25. package/dist/harness/types.d.ts +6 -1
  26. package/dist/index.d.ts +2 -1
  27. package/dist/index.js +1 -0
  28. package/dist/owner-channel/channel.d.ts +13 -0
  29. package/dist/owner-channel/channel.js +191 -9
  30. package/dist/owner-channel/state.d.ts +7 -1
  31. package/dist/owner-channel/state.js +41 -4
  32. package/dist/permissions.d.ts +5 -0
  33. package/dist/permissions.js +7 -0
  34. package/dist/runner.d.ts +2 -0
  35. package/dist/runner.js +86 -2
  36. package/dist/session/acp.d.ts +61 -1
  37. package/dist/session/acp.js +398 -20
  38. package/dist/session/arbiter.d.ts +10 -1
  39. package/dist/session/arbiter.js +24 -0
  40. package/dist/session/control.d.ts +33 -2
  41. package/dist/session/control.js +158 -5
  42. package/dist/session/conversation-normalizer.d.ts +34 -0
  43. package/dist/session/conversation-normalizer.js +356 -0
  44. package/dist/session/conversation-store.d.ts +88 -0
  45. package/dist/session/conversation-store.js +347 -0
  46. package/dist/session/conversation-types.d.ts +274 -0
  47. package/dist/session/conversation-types.js +1 -0
  48. package/dist/session/types.d.ts +40 -0
  49. package/dist/spawn.d.ts +6 -1
  50. package/dist/spawn.js +23 -16
  51. package/dist/web/auth.d.ts +1 -1
  52. package/dist/web/fleet-config-service.d.ts +47 -0
  53. package/dist/web/fleet-config-service.js +204 -0
  54. package/dist/web/runtime.js +14 -1
  55. package/dist/web/server.d.ts +6 -0
  56. package/dist/web/server.js +181 -9
  57. package/dist/web/topology.d.ts +31 -0
  58. package/dist/web/topology.js +61 -0
  59. package/dist/web-app/assets/{TerminalView-DMoT8udI.js → TerminalView-hZpyUFY_.js} +1 -1
  60. package/dist/web-app/assets/index-COg4Azq1.css +1 -0
  61. package/dist/web-app/assets/index-Cde9auW0.js +10 -0
  62. package/dist/web-app/index.html +2 -2
  63. package/package.json +1 -1
  64. package/dist/web-app/assets/index-B-jtLAkp.css +0 -1
  65. package/dist/web-app/assets/index-B6T8JLSd.js +0 -9
@@ -62,6 +62,11 @@ export interface PermissionConflict {
62
62
  /** The role-named line commands print. */
63
63
  warning: string;
64
64
  }
65
+ /** Resolve the effective portable policy after harness-native overrides win. */
66
+ export declare function effectivePermissionMode(role: ResolvedRole): {
67
+ fleetMode: import('./config.js').FleetPermissionMode;
68
+ nativeMode: string;
69
+ };
65
70
  /** Resolve one role's permissions through its adapter. Never throws. */
66
71
  export declare function analyzeRolePermissions(role: ResolvedRole): RolePermissionAnalysis;
67
72
  /** Every line a command should show for a role: translation, conflicts, floor. */
@@ -23,6 +23,13 @@ export function checkUnattendedFloor(granted, required = UNATTENDED_FLOOR) {
23
23
  const missing = required.filter(c => !granted.includes(c));
24
24
  return { meets: missing.length === 0, missing };
25
25
  }
26
+ /** Resolve the effective portable policy after harness-native overrides win. */
27
+ export function effectivePermissionMode(role) {
28
+ const adapter = getAdapter(role.harness);
29
+ if (!adapter.effectivePermissionMode)
30
+ throw new Error(`harness '${role.harness}' cannot report an effective ask|auto|allow permission mode`);
31
+ return adapter.effectivePermissionMode(role);
32
+ }
26
33
  /**
27
34
  * Find native settings that contradict the neutral block. Only fires when the
28
35
  * operator wrote BOTH — a role that states its intent once, neutrally or
package/dist/runner.d.ts CHANGED
@@ -27,6 +27,8 @@ export interface RunnerDeps {
27
27
  /** Lets a test (or a shutdown path) end the supervised restart loop. */
28
28
  shouldStop?(): boolean;
29
29
  }
30
+ /** Environment injected only into the managed harness process. */
31
+ export declare function managedFleetProxyEnv(role: ResolvedRole, stateDir: string): Record<string, string>;
30
32
  /**
31
33
  * Record who owns wake delivery for this run. Returning true means a fleet
32
34
  * monitor is taking ownership back from a native harness and must start at the
package/dist/runner.js CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync, writeFileSync, rmSync, mkdirSync } from 'node:fs';
1
+ import { existsSync, readFileSync, writeFileSync, rmSync, mkdirSync, realpathSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { randomUUID } from 'node:crypto';
4
4
  import { parse } from 'yaml';
@@ -22,6 +22,8 @@ import { OwnerChannel } from './owner-channel/channel.js';
22
22
  import { acquireOwnerBinderLease, OwnerBinderHandoffTimeoutError, } from './owner-channel/binder.js';
23
23
  import { RoleTurnArbiter } from './session/arbiter.js';
24
24
  import { ScheduledLoopManager, } from './loops/manager.js';
25
+ import { FLEET_PROXY_CALLER_ENV, FLEET_PROXY_STATE_DIR_ENV, inheritCallerSpawnDefaults, } from './fleet-proxy.js';
26
+ import { effectivePermissionMode } from './permissions.js';
25
27
  const defaultDeps = () => ({
26
28
  tmux: new Tmux(),
27
29
  exec: realExec,
@@ -54,6 +56,60 @@ const defaultDeps = () => ({
54
56
  },
55
57
  });
56
58
  const MONITOR_OWNER_FILE = '.monitor-owner';
59
+ /** Environment injected only into the managed harness process. */
60
+ export function managedFleetProxyEnv(role, stateDir) {
61
+ return {
62
+ ...(role.env ?? {}),
63
+ [FLEET_PROXY_STATE_DIR_ENV]: stateDir,
64
+ [FLEET_PROXY_CALLER_ENV]: role.name,
65
+ };
66
+ }
67
+ /**
68
+ * Execute a typed proxy request in the caller's supervisor. Dynamic imports
69
+ * avoid a runner↔spawn initialization cycle (spawn imports runner constants).
70
+ */
71
+ async function executeManagedSpawn(caller, configPath, requested, log) {
72
+ const { options, inherited } = inheritCallerSpawnDefaults(caller, requested, configPath);
73
+ const creationActionId = randomUUID();
74
+ options.creationActionId = creationActionId;
75
+ const spawnModule = await import('./spawn.js');
76
+ const preview = spawnModule.spawnDryRun(options).resolvedRole;
77
+ const runtimeBinPath = (() => {
78
+ try {
79
+ return realpathSync(process.argv[1]);
80
+ }
81
+ catch {
82
+ return process.argv[1];
83
+ }
84
+ })();
85
+ let statePath;
86
+ if (options.temp) {
87
+ statePath = await spawnModule.spawnTemp(options, runtimeBinPath);
88
+ }
89
+ else {
90
+ const { pickBackend } = await import('./supervisor/index.js');
91
+ const { WatchdogServiceManager } = await import('./watchdog/service.js');
92
+ statePath = await spawnModule.spawnPermanent(options, {
93
+ backend: pickBackend(), binPath: runtimeBinPath, log,
94
+ watchdogService: new WatchdogServiceManager(),
95
+ });
96
+ }
97
+ const result = {
98
+ caller: caller.name,
99
+ role: options.name,
100
+ lifetime: options.temp ? 'temporary' : 'permanent',
101
+ statePath,
102
+ harness: preview.harness,
103
+ session: preview.session,
104
+ ...(preview.model ? { model: preview.model } : {}),
105
+ monitor: { mode: preview.monitor.mode, interrupt: preview.monitor.interrupt },
106
+ inherited,
107
+ creationActionId,
108
+ };
109
+ log(`[${caller.name}] managed fleet proxy spawned ${result.lifetime} role ${result.role} `
110
+ + `harness=${result.harness} session=${result.session}`);
111
+ return result;
112
+ }
57
113
  /**
58
114
  * Record who owns wake delivery for this run. Returning true means a fleet
59
115
  * monitor is taking ownership back from a native harness and must start at the
@@ -435,6 +491,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
435
491
  let acpStartupComplete = false;
436
492
  let ownerChannel;
437
493
  let ownerBinder;
494
+ const pendingFleetSpawnNotices = [];
438
495
  let loopManager;
439
496
  let arbiter;
440
497
  let reloadLoopConfig;
@@ -453,11 +510,12 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
453
510
  name,
454
511
  argv: wrappedArgv,
455
512
  cwd: runCwd,
456
- env: { ...launch.env, ...(role.env ?? {}) },
513
+ env: { ...launch.env, ...managedFleetProxyEnv(role, dir) },
457
514
  stateDir: dir,
458
515
  mode,
459
516
  permissions: perms,
460
517
  modeId: adapter.acpPermissionModeId?.(role),
518
+ permissionMode: effectivePermissionMode(role),
461
519
  log: deps.log,
462
520
  });
463
521
  pid = acpSession.pid;
@@ -501,6 +559,23 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
501
559
  unsubscribeRecovery?.();
502
560
  throw error;
503
561
  }
562
+ control.setFleetSpawner(async (requested) => {
563
+ const event = await executeManagedSpawn(role, configPath, requested, deps.log);
564
+ if (!role.owner_channel)
565
+ return event;
566
+ if (ownerChannel?.notifyFleetSpawn) {
567
+ try {
568
+ await ownerChannel.notifyFleetSpawn(event);
569
+ }
570
+ catch (error) {
571
+ deps.log(`[${name}] spawned-agent owner notice failed: `
572
+ + `${error?.message ?? String(error)}`);
573
+ }
574
+ }
575
+ else
576
+ pendingFleetSpawnNotices.push(event);
577
+ return event;
578
+ });
504
579
  resolvedMonitorDeps.delivery = {
505
580
  // A wake is only delivered when its turn TERMINATES successfully. A
506
581
  // refusal or a cancellation reached the agent and was not acted on, so
@@ -589,6 +664,15 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
589
664
  throw new Error(`[${name}] owner channel failed to start: `
590
665
  + `${error?.message ?? String(error)}`);
591
666
  }
667
+ for (const event of pendingFleetSpawnNotices.splice(0)) {
668
+ try {
669
+ await ownerChannel.notifyFleetSpawn?.(event);
670
+ }
671
+ catch (error) {
672
+ deps.log(`[${name}] deferred spawned-agent owner notice failed: `
673
+ + `${error?.message ?? String(error)}`);
674
+ }
675
+ }
592
676
  }
593
677
  if (ownerChannel)
594
678
  control.setOwnerChannel(ownerChannel);
@@ -1,5 +1,11 @@
1
+ import * as acp from '@agentclientprotocol/sdk';
1
2
  import type { CommonPermissions } from '../config.js';
2
- import type { ExitRecord, QueuedPrompt, SessionEvent, SessionHandle, SessionSnapshot, SubmitPromptOptions, TurnCancellationSource, TurnOutcome, TurnResult } from './types.js';
3
+ import { ConversationEventStore } from './conversation-store.js';
4
+ import type { ConversationSnapshot, PromptOrigin, PromptReceipt, SubmitPromptCommand } from './conversation-types.js';
5
+ import type { ConversationHandlePage, ExitRecord, QueuedPrompt, SessionEvent, RuntimeSelectorMetadata, SessionHandle, SessionSnapshot, SubmitPromptOptions, TurnCancellationSource, TurnOutcome, TurnResult } from './types.js';
6
+ /** Server-generated typed provenance followed by the exact human-authored body. */
7
+ export declare function promptContentBlocks(text: string, origin?: PromptOrigin): acp.ContentBlock[];
8
+ export declare function runtimeSelector(options: acp.SessionConfigOption[] | null | undefined, category: string): RuntimeSelectorMetadata | undefined;
3
9
  export interface AcpSessionOptions {
4
10
  name: string;
5
11
  argv: string[];
@@ -10,9 +16,15 @@ export interface AcpSessionOptions {
10
16
  permissions: CommonPermissions;
11
17
  /** Native permission-mode id to request via session/set_mode; undefined keeps the agent default. */
12
18
  modeId?: string;
19
+ /** Adapter-resolved live permission policy; separate from ACP agent-specific session modes. */
20
+ permissionMode?: NonNullable<SessionSnapshot['permissionMode']>;
13
21
  log(line: string): void;
14
22
  /** Test seam for the cancel-escalation grace period; production uses the default. */
15
23
  cancelGraceMs?: number;
24
+ /** How long a pending permission may wait for a human before it expires. */
25
+ permissionTimeoutMs?: number;
26
+ /** Grace after the last controller detaches before the unattended policy applies. */
27
+ controllerGraceMs?: number;
16
28
  }
17
29
  /**
18
30
  * Classify an ACP `stopReason` into a terminal outcome. A refusal and a
@@ -30,6 +42,11 @@ export declare class AcpSession implements SessionHandle {
30
42
  readonly pid: number;
31
43
  private readonly child;
32
44
  private readonly events;
45
+ private readonly conversation;
46
+ /** New on every runner start; permission/turn IDs from prior generations are stale. */
47
+ private readonly sessionGeneration;
48
+ /** True while `session/load` replays history as ordinary updates. */
49
+ private replaying;
33
50
  private readonly sessionFile;
34
51
  private readonly pendingPermissions;
35
52
  private connection;
@@ -41,11 +58,22 @@ export declare class AcpSession implements SessionHandle {
41
58
  private exit;
42
59
  private steeringSupported;
43
60
  private capabilities?;
61
+ private runtimeModel?;
62
+ private reasoningEffort?;
44
63
  private controllerCount;
64
+ /** Armed when the last controller detaches; unattended policy applies on fire. */
65
+ private controllerGrace?;
45
66
  private cancelEscalation?;
46
67
  private activeTurn?;
47
68
  private constructor();
48
69
  static start(options: AcpSessionOptions): Promise<AcpSession>;
70
+ /**
71
+ * Honest restart recovery (spec §5.3): a prompt that was admitted but never
72
+ * started is safe to dispatch again; a turn that had already started may
73
+ * have caused side effects, so it is closed as `unknown_after_restart` —
74
+ * never silently replayed.
75
+ */
76
+ private recoverOpenPrompts;
49
77
  isAlive(): boolean;
50
78
  snapshot(): SessionSnapshot;
51
79
  /**
@@ -54,16 +82,40 @@ export declare class AcpSession implements SessionHandle {
54
82
  * for it is what turned a busy agent into a timeout and then into "dead".
55
83
  */
56
84
  queuePrompt(text: string, options?: SubmitPromptOptions): Promise<QueuedPrompt>;
85
+ /**
86
+ * Durably record a prompt admission BEFORE acceptance is returned. Browser
87
+ * admissions are transactional — a prompt the ledger cannot hold is refused,
88
+ * because an acknowledged-then-lost prompt is worse than an error. Every
89
+ * other source degrades to best-effort so the agent keeps working (§5.3).
90
+ */
91
+ private admitToLedger;
92
+ /** Idempotent browser prompt admission (control v3 `submit_prompt_v2`). */
93
+ submitPromptBrowser(command: SubmitPromptCommand): Promise<PromptReceipt>;
57
94
  submitPrompt(text: string, options?: SubmitPromptOptions): Promise<TurnResult>;
58
95
  interrupt(source?: TurnCancellationSource): Promise<void>;
59
96
  private cancelActive;
60
97
  respondPermission(permissionId: string, optionId: string): boolean;
98
+ /**
99
+ * A v2 decision binds to the session generation it was shown under. A stale
100
+ * generation, an already-settled request, or an unknown option are all the
101
+ * same answer: someone else's decision (or a restart) got there first.
102
+ */
103
+ respondPermissionV2(permissionId: string, optionId: string, sessionGeneration: string): 'accepted' | 'stale';
61
104
  eventsSince(seq: number): SessionEvent[];
62
105
  subscribe(listener: (event: SessionEvent) => void): () => void;
63
106
  setControllerAttached(attached: boolean): void;
107
+ private armControllerGrace;
108
+ /**
109
+ * Settle one pending request without a human decision — unattended policy,
110
+ * expiry, or cancellation — and leave the same durable evidence a manual
111
+ * decision would. A denial selects the agent's own one-shot reject option;
112
+ * everything else resolves as cancelled toward the agent.
113
+ */
114
+ private settlePendingAutomatically;
64
115
  exitResult(): ExitRecord | null;
65
116
  close(): Promise<void>;
66
117
  private initialize;
118
+ private captureRuntimeMetadata;
67
119
  private runPrompt;
68
120
  private steerPrompt;
69
121
  private requestPermission;
@@ -75,5 +127,13 @@ export declare class AcpSession implements SessionHandle {
75
127
  private settleAutomatically;
76
128
  private withinAutomaticBoundary;
77
129
  private recordUpdate;
130
+ /** Normalize every ACP update losslessly into the durable ledger. */
131
+ private recordConversationUpdate;
132
+ conversationPage(request?: {
133
+ after?: string;
134
+ limit?: number;
135
+ }): ConversationHandlePage;
136
+ conversationSnapshot(): ConversationSnapshot;
137
+ subscribeConversation(listener: Parameters<ConversationEventStore['subscribe']>[0]): () => void;
78
138
  private fail;
79
139
  }