@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.
- package/README.md +49 -13
- package/dist/application/fleet-query-service.js +3 -0
- package/dist/application/model-catalog.d.ts +20 -0
- package/dist/application/model-catalog.js +57 -0
- package/dist/application/role-creation-service.d.ts +7 -0
- package/dist/application/role-creation-service.js +21 -4
- package/dist/application/role-removal-service.d.ts +32 -0
- package/dist/application/role-removal-service.js +87 -0
- package/dist/application/role-repository.js +13 -1
- package/dist/application/session-control.d.ts +74 -0
- package/dist/application/session-control.js +66 -1
- package/dist/application/types.d.ts +18 -0
- package/dist/briefing.js +21 -1
- package/dist/cli.js +39 -7
- package/dist/config.d.ts +4 -1
- package/dist/config.js +3 -2
- package/dist/creation.d.ts +6 -3
- package/dist/creation.js +5 -1
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +47 -11
- package/dist/fleet-proxy.d.ts +25 -0
- package/dist/fleet-proxy.js +38 -0
- package/dist/harness/claude-code.js +20 -3
- package/dist/harness/codex.js +14 -2
- package/dist/harness/types.d.ts +6 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/owner-channel/channel.d.ts +13 -0
- package/dist/owner-channel/channel.js +191 -9
- package/dist/owner-channel/state.d.ts +7 -1
- package/dist/owner-channel/state.js +41 -4
- package/dist/permissions.d.ts +5 -0
- package/dist/permissions.js +7 -0
- package/dist/runner.d.ts +2 -0
- package/dist/runner.js +86 -2
- package/dist/session/acp.d.ts +61 -1
- package/dist/session/acp.js +398 -20
- package/dist/session/arbiter.d.ts +10 -1
- package/dist/session/arbiter.js +24 -0
- package/dist/session/control.d.ts +33 -2
- package/dist/session/control.js +158 -5
- package/dist/session/conversation-normalizer.d.ts +34 -0
- package/dist/session/conversation-normalizer.js +356 -0
- package/dist/session/conversation-store.d.ts +88 -0
- package/dist/session/conversation-store.js +347 -0
- package/dist/session/conversation-types.d.ts +274 -0
- package/dist/session/conversation-types.js +1 -0
- package/dist/session/types.d.ts +40 -0
- package/dist/spawn.d.ts +6 -1
- package/dist/spawn.js +23 -16
- package/dist/web/auth.d.ts +1 -1
- package/dist/web/fleet-config-service.d.ts +47 -0
- package/dist/web/fleet-config-service.js +204 -0
- package/dist/web/runtime.js +14 -1
- package/dist/web/server.d.ts +6 -0
- package/dist/web/server.js +181 -9
- package/dist/web/topology.d.ts +31 -0
- package/dist/web/topology.js +61 -0
- package/dist/web-app/assets/{TerminalView-DMoT8udI.js → TerminalView-hZpyUFY_.js} +1 -1
- package/dist/web-app/assets/index-COg4Azq1.css +1 -0
- package/dist/web-app/assets/index-Cde9auW0.js +10 -0
- package/dist/web-app/index.html +2 -2
- package/package.json +1 -1
- package/dist/web-app/assets/index-B-jtLAkp.css +0 -1
- package/dist/web-app/assets/index-B6T8JLSd.js +0 -9
package/dist/permissions.d.ts
CHANGED
|
@@ -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. */
|
package/dist/permissions.js
CHANGED
|
@@ -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
|
|
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);
|
package/dist/session/acp.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
+
import * as acp from '@agentclientprotocol/sdk';
|
|
1
2
|
import type { CommonPermissions } from '../config.js';
|
|
2
|
-
import
|
|
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
|
}
|