@ours.network/fleet 0.9.5 → 0.9.7
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 +101 -0
- package/dist/atomic-file.d.ts +30 -0
- package/dist/atomic-file.js +86 -0
- package/dist/briefing.d.ts +6 -0
- package/dist/briefing.js +41 -11
- package/dist/cli.js +95 -21
- package/dist/config.d.ts +15 -1
- package/dist/config.js +47 -2
- package/dist/creation.d.ts +179 -0
- package/dist/creation.js +254 -0
- package/dist/docs.d.ts +28 -1
- package/dist/docs.js +132 -0
- package/dist/doctor.js +74 -16
- package/dist/harness/claude-code.d.ts +39 -3
- package/dist/harness/claude-code.js +126 -24
- package/dist/harness/codex.d.ts +7 -1
- package/dist/harness/codex.js +57 -10
- package/dist/harness/registry.d.ts +2 -0
- package/dist/harness/registry.js +19 -0
- package/dist/harness/types.d.ts +50 -3
- package/dist/isolation/bubblewrap.js +7 -1
- package/dist/isolation/policy.d.ts +34 -5
- package/dist/isolation/policy.js +114 -7
- package/dist/isolation/resources.d.ts +6 -3
- package/dist/isolation/resources.js +6 -3
- package/dist/isolation/types.d.ts +19 -1
- package/dist/monitor.d.ts +30 -3
- package/dist/monitor.js +63 -25
- package/dist/ops.d.ts +15 -2
- package/dist/ops.js +32 -9
- package/dist/permissions.d.ts +70 -0
- package/dist/permissions.js +97 -0
- package/dist/runner.d.ts +65 -2
- package/dist/runner.js +239 -19
- package/dist/session/acp.d.ts +22 -1
- package/dist/session/acp.js +110 -26
- package/dist/session/control.d.ts +49 -1
- package/dist/session/control.js +116 -12
- package/dist/session/tmux.d.ts +8 -1
- package/dist/session/tmux.js +34 -4
- package/dist/session/types.d.ts +92 -1
- package/dist/session/types.js +42 -1
- package/dist/spawn.d.ts +27 -2
- package/dist/spawn.js +153 -15
- package/dist/supervisor/launchd.d.ts +50 -0
- package/dist/supervisor/launchd.js +121 -4
- package/dist/supervisor/none.js +22 -4
- package/dist/supervisor/systemd.d.ts +8 -1
- package/dist/supervisor/systemd.js +94 -4
- package/dist/supervisor/types.d.ts +36 -3
- package/dist/tmux.d.ts +34 -2
- package/dist/tmux.js +48 -11
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Socket } from 'node:net';
|
|
2
|
-
import type { SessionHandle } from './types.js';
|
|
2
|
+
import type { ControlFailureKind, SessionHandle } from './types.js';
|
|
3
3
|
export interface ControlRequest {
|
|
4
4
|
version: 1;
|
|
5
5
|
id: string;
|
|
@@ -16,7 +16,49 @@ export interface ControlResponse {
|
|
|
16
16
|
ok: boolean;
|
|
17
17
|
result?: unknown;
|
|
18
18
|
error?: string;
|
|
19
|
+
/** Why it failed, so the caller does not have to guess from the text. */
|
|
20
|
+
kind?: ControlFailureKind;
|
|
19
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* One line saying what a control failure does — and does not — prove about the
|
|
24
|
+
* agent. Only `offline` is evidence that it is gone; every other kind used to
|
|
25
|
+
* be rendered as "is not running", which is how a busy agent got restarted.
|
|
26
|
+
*/
|
|
27
|
+
export declare function livenessNote(kind: ControlFailureKind, name: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* The result taxonomy an overseer judges a role by (7.2).
|
|
30
|
+
*
|
|
31
|
+
* One console command is not a liveness verdict. `peek` and `send` can fail for
|
|
32
|
+
* five distinct reasons and succeed for one, and only ONE of the six says the
|
|
33
|
+
* agent is gone — collapsing them into "not running" is how busy agents got
|
|
34
|
+
* restarted. This is the single definition of that vocabulary: the generated
|
|
35
|
+
* briefing renders it, and the shipped oversee-agents skills quote it. The
|
|
36
|
+
* per-result wording comes from `livenessNote` rather than being restated, so
|
|
37
|
+
* the words an overseer reads in its instructions are the words the CLI prints.
|
|
38
|
+
*/
|
|
39
|
+
export interface OversightResult {
|
|
40
|
+
/** What the command reported: the one success, or the failure kind. */
|
|
41
|
+
result: 'queued' | ControlFailureKind;
|
|
42
|
+
/** What it proves about the agent. */
|
|
43
|
+
meaning: string;
|
|
44
|
+
/** What the overseer does next. */
|
|
45
|
+
action: string;
|
|
46
|
+
/**
|
|
47
|
+
* Whether this result ALONE justifies restarting the role. True for exactly
|
|
48
|
+
* one result. Every other one requires corroboration before touching a role
|
|
49
|
+
* that may simply be working.
|
|
50
|
+
*/
|
|
51
|
+
restartJustified: boolean;
|
|
52
|
+
}
|
|
53
|
+
/** The taxonomy, in the order generated guidance presents it. */
|
|
54
|
+
export declare function oversightTaxonomy(name?: string): OversightResult[];
|
|
55
|
+
/**
|
|
56
|
+
* The taxonomy as guidance lines, for a briefing or any generated document.
|
|
57
|
+
* The shipped oversee-agents skills carry these same lines, and a test holds
|
|
58
|
+
* them to it — so an overseer reading its briefing and an overseer reading the
|
|
59
|
+
* skill cannot be given different rules.
|
|
60
|
+
*/
|
|
61
|
+
export declare function oversightTaxonomyLines(name?: string): string[];
|
|
20
62
|
export declare const controlSocketPath: (stateDir: string) => string;
|
|
21
63
|
export declare const controlTokenPath: (stateDir: string) => string;
|
|
22
64
|
/** Private, versioned JSONL control plane for CLI and future console frontends. */
|
|
@@ -34,6 +76,12 @@ export declare class RoleControlServer {
|
|
|
34
76
|
private handle;
|
|
35
77
|
private write;
|
|
36
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Send one control request. Every failure mode is classified: a missing token
|
|
81
|
+
* or socket is `control-unavailable`, a silent server is `timeout`, and a
|
|
82
|
+
* response that is not parseable JSON is `backend`. The caller never has to
|
|
83
|
+
* infer liveness from an exception message.
|
|
84
|
+
*/
|
|
37
85
|
export declare function controlRequest(stateDir: string, request: Omit<ControlRequest, 'version' | 'id' | 'token'>, timeoutMs?: number): Promise<ControlResponse>;
|
|
38
86
|
export declare function followControl(stateDir: string, onMessage: (message: Record<string, unknown>) => void): Promise<{
|
|
39
87
|
socket: Socket;
|
package/dist/session/control.js
CHANGED
|
@@ -2,7 +2,84 @@ 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 } from './types.js';
|
|
5
6
|
const MAX_LINE_BYTES = 64 * 1024;
|
|
7
|
+
/**
|
|
8
|
+
* One line saying what a control failure does — and does not — prove about the
|
|
9
|
+
* agent. Only `offline` is evidence that it is gone; every other kind used to
|
|
10
|
+
* be rendered as "is not running", which is how a busy agent got restarted.
|
|
11
|
+
*/
|
|
12
|
+
export function livenessNote(kind, name) {
|
|
13
|
+
switch (kind) {
|
|
14
|
+
case 'offline':
|
|
15
|
+
return `'${name}' is confirmed offline.`;
|
|
16
|
+
case 'control-unavailable':
|
|
17
|
+
return `this says nothing about whether '${name}' is alive — its control plane did not answer; ` +
|
|
18
|
+
`check: ours-fleet status ${name}`;
|
|
19
|
+
case 'timeout':
|
|
20
|
+
return `'${name}' did not answer in time; a busy agent looks exactly like this. ` +
|
|
21
|
+
`Check: ours-fleet status ${name}`;
|
|
22
|
+
case 'rejected':
|
|
23
|
+
return `'${name}' is running and refused the request.`;
|
|
24
|
+
case 'backend':
|
|
25
|
+
return `this is a transport failure, not evidence that '${name}' is gone; ` +
|
|
26
|
+
`check: ours-fleet status ${name}`;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** The taxonomy, in the order generated guidance presents it. */
|
|
30
|
+
export function oversightTaxonomy(name = '<Name>') {
|
|
31
|
+
return [
|
|
32
|
+
{
|
|
33
|
+
result: 'queued',
|
|
34
|
+
meaning: `the session accepted the prompt for '${name}'; a turn already running is not a failure.`,
|
|
35
|
+
action: 'Nothing. Do not resend, and do not read the absence of a reply as a stall — '
|
|
36
|
+
+ `check progress with: ours-fleet peek ${name}`,
|
|
37
|
+
restartJustified: false,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
result: 'timeout',
|
|
41
|
+
meaning: livenessNote('timeout', name),
|
|
42
|
+
action: 'Treat delivery as UNCERTAIN — the request may already have been acted on, so do not '
|
|
43
|
+
+ 'resend it blindly.',
|
|
44
|
+
restartJustified: false,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
result: 'rejected',
|
|
48
|
+
meaning: livenessNote('rejected', name),
|
|
49
|
+
action: 'Fix the request, not the agent. A refusal is proof of life.',
|
|
50
|
+
restartJustified: false,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
result: 'control-unavailable',
|
|
54
|
+
meaning: livenessNote('control-unavailable', name),
|
|
55
|
+
action: 'Read the role logs as well. The control plane and the agent are separate things, '
|
|
56
|
+
+ 'and one being unreachable is not evidence about the other.',
|
|
57
|
+
restartJustified: false,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
result: 'backend',
|
|
61
|
+
meaning: livenessNote('backend', name),
|
|
62
|
+
action: 'Investigate the transport, not the agent.',
|
|
63
|
+
restartJustified: false,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
result: 'offline',
|
|
67
|
+
meaning: livenessNote('offline', name),
|
|
68
|
+
action: `This is the ONLY result that justifies a restart on its own: ours-fleet restart ${name} `
|
|
69
|
+
+ 'for a permanent role. Read the logs first.',
|
|
70
|
+
restartJustified: true,
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The taxonomy as guidance lines, for a briefing or any generated document.
|
|
76
|
+
* The shipped oversee-agents skills carry these same lines, and a test holds
|
|
77
|
+
* them to it — so an overseer reading its briefing and an overseer reading the
|
|
78
|
+
* skill cannot be given different rules.
|
|
79
|
+
*/
|
|
80
|
+
export function oversightTaxonomyLines(name = '<Name>') {
|
|
81
|
+
return oversightTaxonomy(name).map(r => `- **${r.result}** — ${r.meaning} → ${r.action}`);
|
|
82
|
+
}
|
|
6
83
|
export const controlSocketPath = (stateDir) => join(stateDir, '.control.sock');
|
|
7
84
|
export const controlTokenPath = (stateDir) => join(stateDir, '.control-token');
|
|
8
85
|
function sameToken(actual, supplied) {
|
|
@@ -111,19 +188,28 @@ export class RoleControlServer {
|
|
|
111
188
|
return;
|
|
112
189
|
case 'submit_prompt': {
|
|
113
190
|
if (!request.text?.trim())
|
|
114
|
-
throw new
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
191
|
+
throw new SessionControlError('rejected', 'text is required');
|
|
192
|
+
// Answer on QUEUE ACCEPTANCE, not on turn completion. A turn can run
|
|
193
|
+
// for minutes; blocking here made every `send` into a busy agent time
|
|
194
|
+
// out, and the timeout was then reported as a dead agent.
|
|
195
|
+
const queued = await this.session.queuePrompt(request.text);
|
|
196
|
+
this.write(socket, {
|
|
197
|
+
version: 1, id: request.id, ok: true,
|
|
198
|
+
result: {
|
|
199
|
+
state: 'queued', promptId: queued.promptId, queuedBehind: queued.queuedBehind,
|
|
200
|
+
},
|
|
201
|
+
});
|
|
118
202
|
return;
|
|
119
203
|
}
|
|
120
204
|
case 'respond_permission': {
|
|
121
205
|
if (!request.permissionId || !request.optionId)
|
|
122
|
-
throw new
|
|
206
|
+
throw new SessionControlError('rejected', 'permissionId and optionId are required');
|
|
123
207
|
const accepted = this.session.respondPermission(request.permissionId, request.optionId);
|
|
124
208
|
this.write(socket, {
|
|
125
209
|
version: 1, id: request.id, ok: accepted,
|
|
126
|
-
result: { accepted },
|
|
210
|
+
result: { accepted },
|
|
211
|
+
error: accepted ? undefined : 'stale or invalid permission response',
|
|
212
|
+
kind: accepted ? undefined : 'rejected',
|
|
127
213
|
});
|
|
128
214
|
return;
|
|
129
215
|
}
|
|
@@ -146,11 +232,14 @@ export class RoleControlServer {
|
|
|
146
232
|
}
|
|
147
233
|
}
|
|
148
234
|
catch (error) {
|
|
235
|
+
// Carry the session's own classification to the caller. Losing it here is
|
|
236
|
+
// what forced the CLI to invent one.
|
|
149
237
|
this.write(socket, {
|
|
150
238
|
version: 1,
|
|
151
239
|
id: request.id,
|
|
152
240
|
ok: false,
|
|
153
241
|
error: error?.message ?? String(error),
|
|
242
|
+
kind: error instanceof SessionControlError ? error.kind : 'backend',
|
|
154
243
|
});
|
|
155
244
|
}
|
|
156
245
|
}
|
|
@@ -159,18 +248,34 @@ export class RoleControlServer {
|
|
|
159
248
|
socket.write(JSON.stringify(response) + '\n');
|
|
160
249
|
}
|
|
161
250
|
}
|
|
251
|
+
/**
|
|
252
|
+
* Send one control request. Every failure mode is classified: a missing token
|
|
253
|
+
* or socket is `control-unavailable`, a silent server is `timeout`, and a
|
|
254
|
+
* response that is not parseable JSON is `backend`. The caller never has to
|
|
255
|
+
* infer liveness from an exception message.
|
|
256
|
+
*/
|
|
162
257
|
export async function controlRequest(stateDir, request, timeoutMs = 120_000) {
|
|
163
|
-
|
|
258
|
+
let token;
|
|
259
|
+
try {
|
|
260
|
+
token = readFileSync(controlTokenPath(stateDir), 'utf8').trim();
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
throw new SessionControlError('control-unavailable', `cannot read the role control token: ${error?.message ?? String(error)}`);
|
|
264
|
+
}
|
|
164
265
|
const id = randomUUID();
|
|
165
266
|
const socket = createConnection(controlSocketPath(stateDir));
|
|
166
267
|
socket.setEncoding('utf8');
|
|
167
|
-
|
|
268
|
+
return new Promise((resolve, reject) => {
|
|
168
269
|
const timer = setTimeout(() => {
|
|
169
270
|
socket.destroy();
|
|
170
|
-
reject(new
|
|
271
|
+
reject(new SessionControlError('timeout', `the role control plane did not answer '${request.command}' within ${timeoutMs}ms`));
|
|
171
272
|
}, timeoutMs);
|
|
172
273
|
let buffer = '';
|
|
173
|
-
socket.once('error', error => {
|
|
274
|
+
socket.once('error', error => {
|
|
275
|
+
clearTimeout(timer);
|
|
276
|
+
const code = error.code;
|
|
277
|
+
reject(new SessionControlError(code === 'ENOENT' || code === 'ECONNREFUSED' ? 'control-unavailable' : 'backend', `role control socket: ${error.message}`));
|
|
278
|
+
});
|
|
174
279
|
socket.on('data', chunk => {
|
|
175
280
|
buffer += chunk;
|
|
176
281
|
const newline = buffer.indexOf('\n');
|
|
@@ -181,7 +286,7 @@ export async function controlRequest(stateDir, request, timeoutMs = 120_000) {
|
|
|
181
286
|
resolve(JSON.parse(buffer.slice(0, newline)));
|
|
182
287
|
}
|
|
183
288
|
catch (error) {
|
|
184
|
-
reject(error);
|
|
289
|
+
reject(new SessionControlError('backend', `malformed control response: ${error?.message ?? String(error)}`));
|
|
185
290
|
}
|
|
186
291
|
socket.end();
|
|
187
292
|
});
|
|
@@ -189,7 +294,6 @@ export async function controlRequest(stateDir, request, timeoutMs = 120_000) {
|
|
|
189
294
|
version: 1, id, token, ...request,
|
|
190
295
|
}) + '\n'));
|
|
191
296
|
});
|
|
192
|
-
return response;
|
|
193
297
|
}
|
|
194
298
|
export async function followControl(stateDir, onMessage) {
|
|
195
299
|
const token = readFileSync(controlTokenPath(stateDir), 'utf8').trim();
|
package/dist/session/tmux.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Tmux } from '../tmux.js';
|
|
2
|
-
import type { SessionEvent, SessionHandle, SessionSnapshot, TurnResult } from './types.js';
|
|
2
|
+
import type { ExitRecord, QueuedPrompt, SessionEvent, SessionHandle, SessionSnapshot, TurnResult } from './types.js';
|
|
3
3
|
/** SessionHandle adapter for the existing tmux transport. */
|
|
4
4
|
export declare class TmuxSession implements SessionHandle {
|
|
5
5
|
private readonly name;
|
|
@@ -10,11 +10,18 @@ export declare class TmuxSession implements SessionHandle {
|
|
|
10
10
|
constructor(name: string, pid: number, tmux: Tmux, processAlive: (pid: number) => boolean);
|
|
11
11
|
isAlive(): boolean;
|
|
12
12
|
snapshot(): SessionSnapshot;
|
|
13
|
+
queuePrompt(text: string): Promise<QueuedPrompt>;
|
|
13
14
|
submitPrompt(text: string): Promise<TurnResult>;
|
|
14
15
|
interrupt(): Promise<void>;
|
|
15
16
|
respondPermission(): boolean;
|
|
16
17
|
eventsSince(): SessionEvent[];
|
|
17
18
|
subscribe(): () => void;
|
|
18
19
|
setControllerAttached(): void;
|
|
20
|
+
/**
|
|
21
|
+
* A tmux pane's exit is only visible through the record its shell wrapper
|
|
22
|
+
* writes; the runner owns that file and classifies it. Nothing observable
|
|
23
|
+
* from here, so say `null` rather than guess.
|
|
24
|
+
*/
|
|
25
|
+
exitResult(): ExitRecord | null;
|
|
19
26
|
close(): Promise<void>;
|
|
20
27
|
}
|
package/dist/session/tmux.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { SessionControlError, turnResult } from './types.js';
|
|
1
3
|
/** SessionHandle adapter for the existing tmux transport. */
|
|
2
4
|
export class TmuxSession {
|
|
3
5
|
name;
|
|
@@ -21,11 +23,31 @@ export class TmuxSession {
|
|
|
21
23
|
readiness: this.isAlive() ? 'idle' : 'failed',
|
|
22
24
|
};
|
|
23
25
|
}
|
|
24
|
-
async
|
|
26
|
+
async queuePrompt(text) {
|
|
25
27
|
if (!this.isAlive())
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
throw new SessionControlError('offline', `tmux pane for '${this.name}' is offline`);
|
|
29
|
+
try {
|
|
30
|
+
await this.tmux.sendText(this.name, text);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
throw new SessionControlError('backend', error?.message ?? String(error));
|
|
34
|
+
}
|
|
35
|
+
// Keystrokes carry no terminal result: tmux cannot tell us how the turn ended.
|
|
36
|
+
return {
|
|
37
|
+
promptId: randomUUID(),
|
|
38
|
+
queuedBehind: 0,
|
|
39
|
+
completion: Promise.resolve(turnResult(true, 'inconclusive')),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
async submitPrompt(text) {
|
|
43
|
+
try {
|
|
44
|
+
return await (await this.queuePrompt(text)).completion;
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (error instanceof SessionControlError)
|
|
48
|
+
return turnResult(false, 'failed', error.message);
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
29
51
|
}
|
|
30
52
|
async interrupt() {
|
|
31
53
|
await this.tmux.sendKey(this.name, 'C-c');
|
|
@@ -40,6 +62,14 @@ export class TmuxSession {
|
|
|
40
62
|
return () => { };
|
|
41
63
|
}
|
|
42
64
|
setControllerAttached() { }
|
|
65
|
+
/**
|
|
66
|
+
* A tmux pane's exit is only visible through the record its shell wrapper
|
|
67
|
+
* writes; the runner owns that file and classifies it. Nothing observable
|
|
68
|
+
* from here, so say `null` rather than guess.
|
|
69
|
+
*/
|
|
70
|
+
exitResult() {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
43
73
|
async close() {
|
|
44
74
|
await this.tmux.kill(this.name);
|
|
45
75
|
}
|
package/dist/session/types.d.ts
CHANGED
|
@@ -1,10 +1,81 @@
|
|
|
1
1
|
import type { SessionBackendId } from '../config.js';
|
|
2
2
|
export type SessionReadiness = 'starting' | 'idle' | 'running' | 'awaiting_permission' | 'failed';
|
|
3
|
+
export type TurnOutcome = 'completed' | 'refused' | 'cancelled' | 'failed' | 'inconclusive';
|
|
4
|
+
/**
|
|
5
|
+
* Two independent facts about one turn, deliberately kept apart:
|
|
6
|
+
*
|
|
7
|
+
* - `accepted` — the live session took responsibility for the prompt. It says
|
|
8
|
+
* nothing about what the agent then did with it.
|
|
9
|
+
* - `outcome` / `succeeded` — how the turn TERMINATED. Only `completed` is a
|
|
10
|
+
* terminal success. A refusal or a cancellation is a prompt that was
|
|
11
|
+
* delivered and then not carried out; every caller that needs the work
|
|
12
|
+
* actually done (mail delivery, role startup) must treat it as a failure.
|
|
13
|
+
*
|
|
14
|
+
* Collapsing the two is what let a refused wake commit its notification cursor
|
|
15
|
+
* and a refused startup prompt log the role as up.
|
|
16
|
+
*/
|
|
3
17
|
export interface TurnResult {
|
|
4
18
|
accepted: boolean;
|
|
5
|
-
outcome:
|
|
19
|
+
outcome: TurnOutcome;
|
|
20
|
+
succeeded: boolean;
|
|
6
21
|
detail?: string;
|
|
7
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Why a control operation failed. The distinctions exist because collapsing
|
|
25
|
+
* them is what made a busy agent look dead: only `offline` is evidence that the
|
|
26
|
+
* session is gone, and `timeout` explicitly does NOT say the prompt was lost.
|
|
27
|
+
*/
|
|
28
|
+
export type ControlFailureKind = 'offline' | 'control-unavailable' | 'timeout' | 'rejected' | 'backend';
|
|
29
|
+
export declare class SessionControlError extends Error {
|
|
30
|
+
readonly kind: ControlFailureKind;
|
|
31
|
+
constructor(kind: ControlFailureKind, message: string);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A prompt the live session has taken responsibility for. Interactive callers
|
|
35
|
+
* stop here: the session has the prompt, and waiting for the turn to finish is
|
|
36
|
+
* a different question with a different, much longer, timescale.
|
|
37
|
+
*/
|
|
38
|
+
export interface QueuedPrompt {
|
|
39
|
+
promptId: string;
|
|
40
|
+
/** Turns already queued ahead of this one. 0 means it starts immediately. */
|
|
41
|
+
queuedBehind: number;
|
|
42
|
+
/** The turn's terminal result. Never rejects. */
|
|
43
|
+
completion: Promise<TurnResult>;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* How a session's process ended.
|
|
47
|
+
*
|
|
48
|
+
* `unknown` is the honest answer when no evidence was recorded — the previous
|
|
49
|
+
* code wrote the word `crash` there, asserting a failure it had not observed.
|
|
50
|
+
* `session-destroyed` (the console was torn down out from under a live process)
|
|
51
|
+
* and `program-exit` (the program decided to leave) are different events and
|
|
52
|
+
* must not collapse into one another, because they imply different next starts.
|
|
53
|
+
*/
|
|
54
|
+
export type ExitClass = 'clean' | 'program-exit' | 'signal' | 'session-destroyed' | 'unknown';
|
|
55
|
+
export interface ExitRecord {
|
|
56
|
+
version: 1;
|
|
57
|
+
class: ExitClass;
|
|
58
|
+
/** Exit code, when the program exited of its own accord. */
|
|
59
|
+
code?: number;
|
|
60
|
+
/** Signal that killed it, when one did. */
|
|
61
|
+
signal?: string;
|
|
62
|
+
/** Raw wait status as the pane shell saw it (tmux only). */
|
|
63
|
+
status?: number;
|
|
64
|
+
at?: string;
|
|
65
|
+
/** One line an operator can read. */
|
|
66
|
+
detail: string;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Classify a shell `$?`. Above 128 the shell is reporting 128+signal — the only
|
|
70
|
+
* signal evidence a pane wrapper can give us.
|
|
71
|
+
*/
|
|
72
|
+
export declare function classifyShellStatus(status: number): ExitRecord;
|
|
73
|
+
/** Classify a child process exit reported directly by node. */
|
|
74
|
+
export declare function classifyChildExit(code: number | null, signal: string | null): ExitRecord;
|
|
75
|
+
/** The single definition of terminal success. Nothing else may re-derive it. */
|
|
76
|
+
export declare const isTerminalSuccess: (outcome: TurnOutcome) => boolean;
|
|
77
|
+
/** Build a TurnResult with `succeeded` always consistent with `outcome`. */
|
|
78
|
+
export declare function turnResult(accepted: boolean, outcome: TurnOutcome, detail?: string): TurnResult;
|
|
8
79
|
export interface SessionSnapshot {
|
|
9
80
|
backend: SessionBackendId;
|
|
10
81
|
alive: boolean;
|
|
@@ -14,6 +85,8 @@ export interface SessionSnapshot {
|
|
|
14
85
|
pendingPermissionId?: string;
|
|
15
86
|
}
|
|
16
87
|
export type SessionEventKind = 'state' | 'agent_text' | 'thought' | 'tool_call' | 'tool_update' | 'permission' | 'turn_stop' | 'error';
|
|
88
|
+
/** What a settled permission request resolved to. */
|
|
89
|
+
export type PermissionDecision = 'allowed' | 'denied' | 'cancelled';
|
|
17
90
|
export interface SessionEvent {
|
|
18
91
|
version: 1;
|
|
19
92
|
seq: number;
|
|
@@ -31,17 +104,35 @@ export interface SessionEvent {
|
|
|
31
104
|
name: string;
|
|
32
105
|
kind: string;
|
|
33
106
|
}>;
|
|
107
|
+
/** What was decided. */
|
|
108
|
+
decision?: PermissionDecision;
|
|
109
|
+
/** Whether policy decided it, or a human answered the prompt. */
|
|
110
|
+
decisionSource?: 'automatic' | 'manual';
|
|
111
|
+
/** The configured policy that produced an automatic decision. */
|
|
112
|
+
policy?: string;
|
|
113
|
+
/** Why, in one human-readable line. */
|
|
114
|
+
reason?: string;
|
|
115
|
+
/** The option actually selected, when one was. */
|
|
116
|
+
optionId?: string;
|
|
34
117
|
}
|
|
35
118
|
export interface SessionHandle {
|
|
36
119
|
readonly backend: SessionBackendId;
|
|
37
120
|
readonly pid: number;
|
|
38
121
|
isAlive(): boolean;
|
|
39
122
|
snapshot(): SessionSnapshot;
|
|
123
|
+
/**
|
|
124
|
+
* Hand the session a prompt and return as soon as it has accepted
|
|
125
|
+
* responsibility for it. Throws `SessionControlError` if it cannot.
|
|
126
|
+
*/
|
|
127
|
+
queuePrompt(text: string): Promise<QueuedPrompt>;
|
|
128
|
+
/** Queue a prompt and wait for its terminal result. */
|
|
40
129
|
submitPrompt(text: string): Promise<TurnResult>;
|
|
41
130
|
interrupt(): Promise<void>;
|
|
42
131
|
respondPermission(permissionId: string, optionId: string): boolean;
|
|
43
132
|
eventsSince(seq: number): SessionEvent[];
|
|
44
133
|
subscribe(listener: (event: SessionEvent) => void): () => void;
|
|
45
134
|
setControllerAttached(attached: boolean): void;
|
|
135
|
+
/** How the backing process ended, or null while it is still running. */
|
|
136
|
+
exitResult(): ExitRecord | null;
|
|
46
137
|
close(): Promise<void>;
|
|
47
138
|
}
|
package/dist/session/types.js
CHANGED
|
@@ -1 +1,42 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export class SessionControlError extends Error {
|
|
2
|
+
kind;
|
|
3
|
+
constructor(kind, message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.kind = kind;
|
|
6
|
+
this.name = 'SessionControlError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Classify a shell `$?`. Above 128 the shell is reporting 128+signal — the only
|
|
11
|
+
* signal evidence a pane wrapper can give us.
|
|
12
|
+
*/
|
|
13
|
+
export function classifyShellStatus(status) {
|
|
14
|
+
if (!Number.isFinite(status))
|
|
15
|
+
return { version: 1, class: 'unknown', detail: 'pane wrote an unreadable exit status' };
|
|
16
|
+
if (status === 0)
|
|
17
|
+
return { version: 1, class: 'clean', code: 0, status, detail: 'exited cleanly (code 0)' };
|
|
18
|
+
if (status > 128) {
|
|
19
|
+
const signal = status - 128;
|
|
20
|
+
return {
|
|
21
|
+
version: 1, class: 'signal', signal: `SIG${signal}`, status,
|
|
22
|
+
detail: `killed by signal ${signal} (shell status ${status})`,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
return { version: 1, class: 'program-exit', code: status, status, detail: `exited with code ${status}` };
|
|
26
|
+
}
|
|
27
|
+
/** Classify a child process exit reported directly by node. */
|
|
28
|
+
export function classifyChildExit(code, signal) {
|
|
29
|
+
if (signal)
|
|
30
|
+
return { version: 1, class: 'signal', signal, detail: `killed by ${signal}` };
|
|
31
|
+
if (code === 0)
|
|
32
|
+
return { version: 1, class: 'clean', code: 0, detail: 'exited cleanly (code 0)' };
|
|
33
|
+
if (code === null)
|
|
34
|
+
return { version: 1, class: 'unknown', detail: 'the process ended with neither a code nor a signal' };
|
|
35
|
+
return { version: 1, class: 'program-exit', code, detail: `exited with code ${code}` };
|
|
36
|
+
}
|
|
37
|
+
/** The single definition of terminal success. Nothing else may re-derive it. */
|
|
38
|
+
export const isTerminalSuccess = (outcome) => outcome === 'completed';
|
|
39
|
+
/** Build a TurnResult with `succeeded` always consistent with `outcome`. */
|
|
40
|
+
export function turnResult(accepted, outcome, detail) {
|
|
41
|
+
return { accepted, outcome, succeeded: isTerminalSuccess(outcome), detail };
|
|
42
|
+
}
|
package/dist/spawn.d.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
|
+
import type { IsolationConfig } from './isolation/types.js';
|
|
1
2
|
import { type ApprovalMode, type FilesystemMode, type SessionBackendId, type UnattendedMode } from './config.js';
|
|
2
3
|
import { type OpsDeps } from './ops.js';
|
|
4
|
+
import { type CreationDeps, type CreationProvenance } from './creation.js';
|
|
5
|
+
/**
|
|
6
|
+
* The provenance record written by the most recent spawn in this process, so
|
|
7
|
+
* the CLI can print the same summary it persisted rather than rebuilding it.
|
|
8
|
+
*/
|
|
9
|
+
export declare let lastProvenance: CreationProvenance | undefined;
|
|
3
10
|
export interface SpawnOpts {
|
|
4
11
|
name: string;
|
|
5
12
|
temp?: boolean;
|
|
@@ -23,12 +30,30 @@ export interface SpawnOpts {
|
|
|
23
30
|
monitor?: boolean;
|
|
24
31
|
bioFile?: string;
|
|
25
32
|
personaFile?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Path to a file holding exactly the existing `isolation:` mapping — the same
|
|
35
|
+
* schema fleet.yaml uses, not a second policy language. The ONE new operator
|
|
36
|
+
* input in this release (6.3).
|
|
37
|
+
*/
|
|
38
|
+
isolationFile?: string;
|
|
26
39
|
overseeInterval?: string;
|
|
27
40
|
configPath?: string;
|
|
28
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Read and validate an `--isolation-file`. The file is the existing
|
|
44
|
+
* `isolation:` mapping and nothing else — the same schema, the same validator
|
|
45
|
+
* (`validateIsolationConfig`), so a policy written here cannot mean something
|
|
46
|
+
* different from the identical block in fleet.yaml.
|
|
47
|
+
*
|
|
48
|
+
* Called BEFORE the creation transaction reserves anything: an invalid file
|
|
49
|
+
* must fail before any artifact exists.
|
|
50
|
+
*/
|
|
51
|
+
export declare function readIsolationFile(path: string): IsolationConfig;
|
|
52
|
+
/** The ours identity a spawn will bind: explicit, else the role name. */
|
|
53
|
+
export declare const effectiveIdentity: (o: SpawnOpts) => string;
|
|
29
54
|
/** Permanent spawn: persist to ~/fleet.d/<Name>.yaml, then bring it up. */
|
|
30
|
-
export declare function spawnPermanent(o: SpawnOpts, deps: OpsDeps): Promise<string>;
|
|
55
|
+
export declare function spawnPermanent(o: SpawnOpts, deps: OpsDeps, creation?: CreationDeps): Promise<string>;
|
|
31
56
|
/** Launches the detached temp supervisor (`_run-temp <name>`). Injectable for tests. */
|
|
32
57
|
export type SupervisorLauncher = (binPath: string, args: string[], dir: string) => void;
|
|
33
58
|
/** Temp spawn: state under ~/.ours-fleet/tmp, plain tmux, auto-clean on exit. */
|
|
34
|
-
export declare function spawnTemp(o: SpawnOpts, binPath: string, launch?: SupervisorLauncher): Promise<string>;
|
|
59
|
+
export declare function spawnTemp(o: SpawnOpts, binPath: string, launch?: SupervisorLauncher, creation?: CreationDeps): Promise<string>;
|