@ours.network/fleet 1.2.0-nightly.57 → 1.2.0-nightly.59
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 +1 -1
- package/dist/agent-ours/bridge.js +21 -11
- package/dist/agent-ours/file-rpc.d.ts +6 -2
- package/dist/agent-ours/file-rpc.js +11 -7
- package/dist/agent-ours/harness.js +2 -46
- package/dist/agent-ours/mcp-endpoint.d.ts +0 -1
- package/dist/agent-ours/mcp-endpoint.js +3 -2
- package/dist/agent-ours/service.d.ts +15 -0
- package/dist/agent-ours/service.js +41 -35
- package/dist/briefing.js +10 -0
- package/dist/build-info.json +4 -4
- package/dist/config.d.ts +10 -4
- package/dist/config.js +8 -3
- package/dist/file-delivery/reader.d.ts +10 -12
- package/dist/file-delivery/reader.js +33 -64
- package/dist/harness/codex-app-server-proxy.js +2 -6
- package/dist/harness/codex.js +1 -2
- package/dist/owner-channel/channel.js +7 -4
- package/dist/session/acp.d.ts +39 -0
- package/dist/session/acp.js +120 -29
- package/dist/session/arbiter.d.ts +1 -0
- package/dist/session/arbiter.js +1 -0
- package/dist/session/codex-app-server.d.ts +26 -0
- package/dist/session/codex-app-server.js +98 -22
- package/dist/session/conversation-types.d.ts +19 -0
- package/dist/session/prompt-batch.d.ts +63 -0
- package/dist/session/prompt-batch.js +86 -0
- package/dist/session/types.d.ts +5 -0
- package/docs/acp-files.md +99 -55
- package/package.json +1 -1
- package/dist/harness/codex-file-inventory.d.ts +0 -15
- package/dist/harness/codex-file-inventory.js +0 -113
|
@@ -5,6 +5,7 @@ import { isAbsolute, join, resolve } from 'node:path';
|
|
|
5
5
|
import { boundedJson } from './conversation-normalizer.js';
|
|
6
6
|
import { ConversationEventStore } from './conversation-store.js';
|
|
7
7
|
import { SessionEvents } from './events.js';
|
|
8
|
+
import { memberAuthority, memberBindings, pendingInventory, takeBatch, } from './prompt-batch.js';
|
|
8
9
|
import { CodexAppServerTransport } from './codex-app-server-transport.js';
|
|
9
10
|
import { CODEX_APP_SERVER_CANCEL_DEADLINE_EXCEEDED, SessionControlError, sessionBackendCapabilities, turnResult, } from './types.js';
|
|
10
11
|
const PERMISSION_TIMEOUT_MS = 10 * 60_000;
|
|
@@ -75,6 +76,31 @@ export function nativePromptAdditionalContext(origin) {
|
|
|
75
76
|
};
|
|
76
77
|
return undefined;
|
|
77
78
|
}
|
|
79
|
+
const PROMPT_BATCH_CONTEXT_KEY = 'ours-fleet://prompt-batch';
|
|
80
|
+
/**
|
|
81
|
+
* Application context for a turn that carries several admitted prompts.
|
|
82
|
+
*
|
|
83
|
+
* The single-prompt context says "the accompanying user text", which would
|
|
84
|
+
* cover every member. A shared turn therefore carries this one entry instead:
|
|
85
|
+
* each member is its own user text item, in order, and its route and exact
|
|
86
|
+
* bytes are stated here by the server. Text imitating this wording is still
|
|
87
|
+
* only user text; it cannot add an application-context entry.
|
|
88
|
+
*/
|
|
89
|
+
export function nativeBatchAdditionalContext(members) {
|
|
90
|
+
if (members.length === 1)
|
|
91
|
+
return nativePromptAdditionalContext(members[0].origin);
|
|
92
|
+
const bindings = memberBindings(members, origin => sourceFor(origin).source);
|
|
93
|
+
return {
|
|
94
|
+
[PROMPT_BATCH_CONTEXT_KEY]: {
|
|
95
|
+
kind: 'application',
|
|
96
|
+
value: `Fleet queued prompt batch: ${members.length} separately admitted prompts in submission order, `
|
|
97
|
+
+ 'one user text item each. Server-generated provenance per item, describing only that item: '
|
|
98
|
+
+ bindings.map(binding => `item ${binding.index} of ${binding.of} `
|
|
99
|
+
+ `(source=${binding.source}, bytes=${binding.bytes}, sha256=${binding.sha256}): `
|
|
100
|
+
+ `${memberAuthority(binding.source)}`).join('; ') + '.',
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
78
104
|
function toolItem(item) {
|
|
79
105
|
switch (item.type) {
|
|
80
106
|
case 'commandExecution': return {
|
|
@@ -123,6 +149,10 @@ export class CodexAppServerSession {
|
|
|
123
149
|
exit = null;
|
|
124
150
|
queueDepth = 0;
|
|
125
151
|
promptTail = Promise.resolve();
|
|
152
|
+
/** Admitted prompts not yet handed to the app-server, in admission order. */
|
|
153
|
+
pendingPrompts = [];
|
|
154
|
+
/** Admitted prompt → the turn that carries it, while that turn is open. */
|
|
155
|
+
sharedTurns = new Map();
|
|
126
156
|
activeTurn;
|
|
127
157
|
controllerCount = 0;
|
|
128
158
|
controllerGrace;
|
|
@@ -173,7 +203,9 @@ export class CodexAppServerSession {
|
|
|
173
203
|
alive: this.isAlive(),
|
|
174
204
|
readiness: this.readiness,
|
|
175
205
|
sessionId: this.threadId,
|
|
176
|
-
|
|
206
|
+
// A shared turn is a human console turn if any member is: inbox traffic must not cut it short.
|
|
207
|
+
...(this.activeTurn?.origin ? { activePromptSource: this.activeTurn.console
|
|
208
|
+
? 'owner-admin-console' : this.activeTurn.origin.kind } : {}),
|
|
177
209
|
lastError: this.lastError,
|
|
178
210
|
pendingPermissionId: this.pendingPermissions.keys().next().value,
|
|
179
211
|
runtimeModel: this.runtimeModel,
|
|
@@ -204,8 +236,7 @@ export class CodexAppServerSession {
|
|
|
204
236
|
const queuedBehind = this.queueDepth;
|
|
205
237
|
this.admit(promptId, text, queuedBehind, options);
|
|
206
238
|
this.queueDepth++;
|
|
207
|
-
const run = this.
|
|
208
|
-
this.promptTail = run.then(() => undefined, () => undefined);
|
|
239
|
+
const run = this.enqueue(promptId, text, options.origin);
|
|
209
240
|
const completion = run.then(result => {
|
|
210
241
|
this.queueDepth = Math.max(0, this.queueDepth - 1);
|
|
211
242
|
return result;
|
|
@@ -218,6 +249,42 @@ export class CodexAppServerSession {
|
|
|
218
249
|
delivery: delivery ?? (queuedBehind ? 'queued' : 'started'),
|
|
219
250
|
};
|
|
220
251
|
}
|
|
252
|
+
/** The turn whose events and result answer this admitted prompt; itself unless a release shared it. */
|
|
253
|
+
sharedTurnId(promptId) {
|
|
254
|
+
return this.sharedTurns.get(promptId);
|
|
255
|
+
}
|
|
256
|
+
/** Each prompt keeps its own step in the chain; the step it reaches releases a whole snapshot. */
|
|
257
|
+
enqueue(promptId, text, origin) {
|
|
258
|
+
const entry = {
|
|
259
|
+
promptId, text, origin, ...sourceFor(origin), admittedAt: new Date().toISOString(),
|
|
260
|
+
// The one text the native session runs as an operation instead of a turn.
|
|
261
|
+
command: text.trim() === '/compact',
|
|
262
|
+
};
|
|
263
|
+
this.pendingPrompts.push(entry);
|
|
264
|
+
const run = this.promptTail.then(() => this.release(entry));
|
|
265
|
+
this.promptTail = run.then(() => undefined, () => undefined);
|
|
266
|
+
return run;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* The turn boundary: the first waiting prompt to get here synchronously
|
|
270
|
+
* takes every pending prompt delivered with it into one turn. A prompt
|
|
271
|
+
* admitted afterwards stays pending for the next boundary, and a step whose
|
|
272
|
+
* prompt was already taken reports that turn's result.
|
|
273
|
+
*/
|
|
274
|
+
release(entry) {
|
|
275
|
+
if (entry.released)
|
|
276
|
+
return entry.released;
|
|
277
|
+
const members = takeBatch(this.pendingPrompts, entry);
|
|
278
|
+
for (const member of members)
|
|
279
|
+
this.sharedTurns.set(member.promptId, members[0].promptId);
|
|
280
|
+
const run = this.runPrompt(members).finally(() => {
|
|
281
|
+
for (const member of members)
|
|
282
|
+
this.sharedTurns.delete(member.promptId);
|
|
283
|
+
});
|
|
284
|
+
for (const member of members)
|
|
285
|
+
member.released = run;
|
|
286
|
+
return run;
|
|
287
|
+
}
|
|
221
288
|
async submitPrompt(text, options = {}) {
|
|
222
289
|
try {
|
|
223
290
|
return await (await this.queuePrompt(text, options)).completion;
|
|
@@ -232,7 +299,7 @@ export class CodexAppServerSession {
|
|
|
232
299
|
async interruptPrompt(generation, promptId) {
|
|
233
300
|
if (generation !== this.sessionGeneration)
|
|
234
301
|
throw new Error('session_changed: Agent session changed.');
|
|
235
|
-
if (this.activeTurn?.promptId
|
|
302
|
+
if (!this.activeTurn?.promptIds.includes(promptId))
|
|
236
303
|
return { state: 'settled' };
|
|
237
304
|
return this.interrupt('local-console');
|
|
238
305
|
}
|
|
@@ -370,6 +437,7 @@ export class CodexAppServerSession {
|
|
|
370
437
|
sessionGeneration: this.sessionGeneration,
|
|
371
438
|
readiness: this.isAlive() ? this.readiness : 'offline',
|
|
372
439
|
queueDepth: this.queueDepth,
|
|
440
|
+
...pendingInventory(this.pendingPrompts),
|
|
373
441
|
pendingPermissionIds: [...this.pendingPermissions.keys()],
|
|
374
442
|
...(this.conversation.degraded ? { historyDegraded: true } : {}),
|
|
375
443
|
};
|
|
@@ -465,8 +533,7 @@ export class CodexAppServerSession {
|
|
|
465
533
|
const origin = open.commandId
|
|
466
534
|
? { kind: 'owner-admin-console', commandId: open.commandId }
|
|
467
535
|
: undefined;
|
|
468
|
-
const run = this.
|
|
469
|
-
this.promptTail = run.then(() => undefined, () => undefined);
|
|
536
|
+
const run = this.enqueue(open.promptId, open.text, origin);
|
|
470
537
|
void run.finally(() => { this.queueDepth = Math.max(0, this.queueDepth - 1); });
|
|
471
538
|
}
|
|
472
539
|
}
|
|
@@ -498,21 +565,27 @@ export class CodexAppServerSession {
|
|
|
498
565
|
else
|
|
499
566
|
this.conversation.appendSafe(draft);
|
|
500
567
|
}
|
|
501
|
-
async runPrompt(
|
|
568
|
+
async runPrompt(members) {
|
|
569
|
+
const { text, promptId, origin } = members[0];
|
|
502
570
|
if (!this.threadId || !this.transport.isAlive())
|
|
503
571
|
return turnResult(false, 'failed', this.lastError ?? 'Codex app-server session is offline');
|
|
504
572
|
let settle;
|
|
505
573
|
const settled = new Promise(resolveResult => { settle = resolveResult; });
|
|
506
|
-
const active = {
|
|
574
|
+
const active = {
|
|
575
|
+
promptId, origin, output: '', settled, settle,
|
|
576
|
+
promptIds: members.map(member => member.promptId),
|
|
577
|
+
console: members.some(member => member.origin?.kind === 'owner-admin-console'),
|
|
578
|
+
};
|
|
507
579
|
this.activeTurn = active;
|
|
508
580
|
this.finalAnswerObserved = false;
|
|
509
581
|
this.activeItems.clear();
|
|
510
582
|
this.readiness = 'running';
|
|
511
583
|
this.events.emit('state', { turnId: promptId, status: 'running', origin });
|
|
512
|
-
this.conversation.appendSafe({
|
|
584
|
+
members.forEach((member, index) => this.conversation.appendSafe({
|
|
513
585
|
kind: 'prompt.started', sessionGeneration: this.sessionGeneration,
|
|
514
|
-
promptId, turnId: promptId, source: sourceFor(origin).source,
|
|
515
|
-
|
|
586
|
+
promptId: member.promptId, turnId: promptId, source: sourceFor(member.origin).source,
|
|
587
|
+
payload: members.length > 1 ? { batch: { turnId: promptId, index, size: members.length } } : {},
|
|
588
|
+
}));
|
|
516
589
|
try {
|
|
517
590
|
if (text.trim() === '/compact') {
|
|
518
591
|
// The acknowledgement is not completion. Standard turn notifications
|
|
@@ -522,12 +595,13 @@ export class CodexAppServerSession {
|
|
|
522
595
|
}
|
|
523
596
|
const response = await this.transport.request('turn/start', {
|
|
524
597
|
threadId: this.threadId,
|
|
525
|
-
|
|
598
|
+
// One unchanged user text item per admitted prompt, in admission order.
|
|
599
|
+
input: members.map(member => ({ type: 'text', text: member.text, text_elements: [] })),
|
|
526
600
|
approvalPolicy: this.options.approvalPolicy,
|
|
527
601
|
model: this.options.model ?? null,
|
|
528
602
|
effort: this.options.effort ?? null,
|
|
529
|
-
...(
|
|
530
|
-
? { additionalContext:
|
|
603
|
+
...(nativeBatchAdditionalContext(members)
|
|
604
|
+
? { additionalContext: nativeBatchAdditionalContext(members) } : {}),
|
|
531
605
|
});
|
|
532
606
|
const turn = isObject(response.turn) ? response.turn : undefined;
|
|
533
607
|
active.nativeTurnId ??= string(turn?.id);
|
|
@@ -806,14 +880,16 @@ export class CodexAppServerSession {
|
|
|
806
880
|
cancellationSource: active.cancellationSource,
|
|
807
881
|
});
|
|
808
882
|
this.events.emit('state', { status: this.readiness });
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
883
|
+
// Every member closes with the shared outcome, so none is left waiting.
|
|
884
|
+
for (const member of active.promptIds)
|
|
885
|
+
this.conversation.appendSafe({
|
|
886
|
+
kind: 'turn.completed', sessionGeneration: this.sessionGeneration,
|
|
887
|
+
promptId: member, turnId: active.promptId,
|
|
888
|
+
payload: {
|
|
889
|
+
outcome, ...(detail ? { stopReason: detail } : {}),
|
|
890
|
+
...(active.cancellationSource ? { cancellationSource: active.cancellationSource } : {}),
|
|
891
|
+
},
|
|
892
|
+
});
|
|
817
893
|
active.settle(result);
|
|
818
894
|
}
|
|
819
895
|
/**
|
|
@@ -176,6 +176,12 @@ export interface PromptAdmittedPayload {
|
|
|
176
176
|
}
|
|
177
177
|
export interface PromptStartedPayload {
|
|
178
178
|
queuedBehind?: number;
|
|
179
|
+
/** Present when one adapter turn carries several admitted prompts; `turnId` is the shared turn. */
|
|
180
|
+
batch?: {
|
|
181
|
+
turnId: string;
|
|
182
|
+
index: number;
|
|
183
|
+
size: number;
|
|
184
|
+
};
|
|
179
185
|
}
|
|
180
186
|
export interface PromptInterruptRequestedPayload {
|
|
181
187
|
commandId?: string;
|
|
@@ -289,12 +295,25 @@ export interface PermissionDecisionCommand {
|
|
|
289
295
|
sessionGeneration: string;
|
|
290
296
|
optionId: string;
|
|
291
297
|
}
|
|
298
|
+
/** One admitted prompt not yet handed to the agent, as reported by the queue that owns it. */
|
|
299
|
+
export interface PendingPromptSummary {
|
|
300
|
+
promptId: string;
|
|
301
|
+
source: ConversationSource;
|
|
302
|
+
admittedAt: string;
|
|
303
|
+
bytes: number;
|
|
304
|
+
/** A bounded preview, present only for a body the ledger already holds. */
|
|
305
|
+
text?: string;
|
|
306
|
+
textTruncated?: boolean;
|
|
307
|
+
}
|
|
292
308
|
export interface ConversationSnapshot {
|
|
293
309
|
sessionGeneration: string;
|
|
294
310
|
readiness: 'starting' | 'idle' | 'running' | 'awaiting_permission' | 'failed' | 'offline';
|
|
295
311
|
queueDepth: number;
|
|
296
312
|
pendingPermissionIds: string[];
|
|
297
313
|
historyDegraded?: boolean;
|
|
314
|
+
/** Every waiting prompt, in delivery order; `pendingPromptCount` equals its length. */
|
|
315
|
+
pendingPrompts?: PendingPromptSummary[];
|
|
316
|
+
pendingPromptCount?: number;
|
|
298
317
|
}
|
|
299
318
|
export interface ConversationPage {
|
|
300
319
|
events: ConversationEventV1[];
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ConversationSource, PendingPromptSummary } from './conversation-types.js';
|
|
2
|
+
import type { PromptOrigin, TurnResult } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* A prompt the session has admitted and not yet handed to the adapter.
|
|
5
|
+
*
|
|
6
|
+
* Every admitted prompt keeps its own id, ledger records and completion. What
|
|
7
|
+
* changes at a turn boundary is how many of them one adapter turn carries.
|
|
8
|
+
*/
|
|
9
|
+
export interface PendingPrompt {
|
|
10
|
+
promptId: string;
|
|
11
|
+
text: string;
|
|
12
|
+
origin?: PromptOrigin;
|
|
13
|
+
source: ConversationSource;
|
|
14
|
+
/** Whether the ledger already holds this body; an external body is never copied out of the queue. */
|
|
15
|
+
persistBody: boolean;
|
|
16
|
+
admittedAt: string;
|
|
17
|
+
/** Whether the session recognised the text, at admission, as one of its own control operations. */
|
|
18
|
+
command?: boolean;
|
|
19
|
+
/** Set when a release takes this prompt; every member of that turn shares it. */
|
|
20
|
+
released?: Promise<TurnResult>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* How a waiting prompt takes part in a release.
|
|
24
|
+
*
|
|
25
|
+
* - `message`: something a person or a peer route submitted for the agent to
|
|
26
|
+
* read — the paired console, the owner channel, the local console, a
|
|
27
|
+
* monitor wake. Every waiting message is delivered at one boundary, in
|
|
28
|
+
* admission order, whatever its route.
|
|
29
|
+
* - `command`: a control operation the session itself supports — the native
|
|
30
|
+
* `/compact`, a command the ACP adapter advertised. It must reach the
|
|
31
|
+
* adapter as the entire prompt, so it is its own turn and a boundary
|
|
32
|
+
* between the messages before and after it. The session decides this at
|
|
33
|
+
* admission; text that merely starts with a slash is an ordinary message.
|
|
34
|
+
* - `internal`: startup, scheduled-loop and stall-watchdog prompts. They own
|
|
35
|
+
* their turn's lifecycle (and, for a loop, the redaction of its whole
|
|
36
|
+
* transcript), run alone, and never split the messages around them.
|
|
37
|
+
*/
|
|
38
|
+
export type PromptClass = 'message' | 'command' | 'internal';
|
|
39
|
+
export declare function promptClass(prompt: Pick<PendingPrompt, 'origin' | 'command'>): PromptClass;
|
|
40
|
+
/** The command name a prompt invokes, when its whole text has the shape of one. */
|
|
41
|
+
export declare function commandName(text: string): string | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* Detach, in admission order, every pending prompt that is delivered with
|
|
44
|
+
* `head`. Synchronous on purpose: this is the snapshot boundary, and a prompt
|
|
45
|
+
* admitted after it returns belongs to the next release.
|
|
46
|
+
*/
|
|
47
|
+
export declare function takeBatch(pending: PendingPrompt[], head: PendingPrompt): PendingPrompt[];
|
|
48
|
+
/** Exact, server-computed identity of one member's body inside a shared turn. */
|
|
49
|
+
export interface MemberBinding {
|
|
50
|
+
index: number;
|
|
51
|
+
of: number;
|
|
52
|
+
source: ConversationSource;
|
|
53
|
+
bytes: number;
|
|
54
|
+
sha256: string;
|
|
55
|
+
}
|
|
56
|
+
export declare function memberBindings(members: ReadonlyArray<Pick<PendingPrompt, 'text' | 'origin'>>, sourceOf: (origin: PromptOrigin | undefined) => ConversationSource): MemberBinding[];
|
|
57
|
+
/** What the server asserts about a member's route; never derived from the member's text. */
|
|
58
|
+
export declare function memberAuthority(source: ConversationSource): string;
|
|
59
|
+
/** What a client may show for prompts still waiting: all of them, in order, and never an unpersisted body. */
|
|
60
|
+
export declare function pendingInventory(pending: readonly PendingPrompt[]): {
|
|
61
|
+
pendingPrompts: PendingPromptSummary[];
|
|
62
|
+
pendingPromptCount: number;
|
|
63
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* The inventory names every waiting prompt — a client must be able to show the
|
|
4
|
+
* whole queue, not a count of what it could not see. What keeps a snapshot
|
|
5
|
+
* small is the body: each entry carries a short preview, not the prompt.
|
|
6
|
+
*/
|
|
7
|
+
const INVENTORY_TEXT_CHARS = 200;
|
|
8
|
+
const INTERNAL = new Set(['startup', 'scheduled-loop', 'stall-watchdog']);
|
|
9
|
+
export function promptClass(prompt) {
|
|
10
|
+
if (INTERNAL.has(prompt.origin?.kind ?? 'local-console'))
|
|
11
|
+
return 'internal';
|
|
12
|
+
return prompt.command ? 'command' : 'message';
|
|
13
|
+
}
|
|
14
|
+
/** The command name a prompt invokes, when its whole text has the shape of one. */
|
|
15
|
+
export function commandName(text) {
|
|
16
|
+
return /^\/([a-z][a-z0-9_-]*)(?:\s|$)/i.exec(text.trim())?.[1];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Detach, in admission order, every pending prompt that is delivered with
|
|
20
|
+
* `head`. Synchronous on purpose: this is the snapshot boundary, and a prompt
|
|
21
|
+
* admitted after it returns belongs to the next release.
|
|
22
|
+
*/
|
|
23
|
+
export function takeBatch(pending, head) {
|
|
24
|
+
const members = [];
|
|
25
|
+
if (promptClass(head) !== 'message')
|
|
26
|
+
members.push(head);
|
|
27
|
+
else {
|
|
28
|
+
for (const prompt of pending) {
|
|
29
|
+
const kind = promptClass(prompt);
|
|
30
|
+
if (kind === 'command')
|
|
31
|
+
break;
|
|
32
|
+
if (kind === 'message')
|
|
33
|
+
members.push(prompt);
|
|
34
|
+
}
|
|
35
|
+
if (!members.includes(head))
|
|
36
|
+
members.unshift(head);
|
|
37
|
+
}
|
|
38
|
+
for (const member of members) {
|
|
39
|
+
const at = pending.indexOf(member);
|
|
40
|
+
if (at >= 0)
|
|
41
|
+
pending.splice(at, 1);
|
|
42
|
+
}
|
|
43
|
+
return members;
|
|
44
|
+
}
|
|
45
|
+
export function memberBindings(members, sourceOf) {
|
|
46
|
+
return members.map((member, at) => ({
|
|
47
|
+
index: at + 1,
|
|
48
|
+
of: members.length,
|
|
49
|
+
source: sourceOf(member.origin),
|
|
50
|
+
bytes: Buffer.byteLength(member.text),
|
|
51
|
+
sha256: createHash('sha256').update(member.text).digest('hex'),
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
/** What the server asserts about a member's route; never derived from the member's text. */
|
|
55
|
+
export function memberAuthority(source) {
|
|
56
|
+
switch (source) {
|
|
57
|
+
case 'owner_admin_console':
|
|
58
|
+
return 'direct owner input from the server-authenticated paired console';
|
|
59
|
+
case 'owner_channel':
|
|
60
|
+
return 'admitted through the authenticated Fleet owner channel';
|
|
61
|
+
case 'fleet_monitor':
|
|
62
|
+
return 'a Fleet monitor notification, not owner input';
|
|
63
|
+
default:
|
|
64
|
+
return 'local console input, not authenticated owner-console or owner-channel input';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** What a client may show for prompts still waiting: all of them, in order, and never an unpersisted body. */
|
|
68
|
+
export function pendingInventory(pending) {
|
|
69
|
+
return {
|
|
70
|
+
pendingPromptCount: pending.length,
|
|
71
|
+
pendingPrompts: pending.map(prompt => {
|
|
72
|
+
const display = prompt.origin?.kind === 'owner' ? prompt.origin.displayText
|
|
73
|
+
: prompt.persistBody ? prompt.text : undefined;
|
|
74
|
+
return {
|
|
75
|
+
promptId: prompt.promptId,
|
|
76
|
+
source: prompt.source,
|
|
77
|
+
admittedAt: prompt.admittedAt,
|
|
78
|
+
bytes: Buffer.byteLength(prompt.text),
|
|
79
|
+
...(display === undefined ? {} : {
|
|
80
|
+
text: display.slice(0, INVENTORY_TEXT_CHARS),
|
|
81
|
+
...(display.length > INVENTORY_TEXT_CHARS ? { textTruncated: true } : {}),
|
|
82
|
+
}),
|
|
83
|
+
};
|
|
84
|
+
}),
|
|
85
|
+
};
|
|
86
|
+
}
|
package/dist/session/types.d.ts
CHANGED
|
@@ -307,6 +307,11 @@ export interface AgentSession {
|
|
|
307
307
|
* Hand the session a prompt and return as soon as it has accepted
|
|
308
308
|
* responsibility for it. Throws `SessionControlError` if it cannot.
|
|
309
309
|
*/
|
|
310
|
+
/**
|
|
311
|
+
* The turn that carries an admitted prompt when one adapter turn delivered
|
|
312
|
+
* several; absent means the prompt's own id is its turn id.
|
|
313
|
+
*/
|
|
314
|
+
sharedTurnId?(promptId: string): string | undefined;
|
|
310
315
|
queuePrompt(text: string, options?: SubmitPromptOptions): Promise<QueuedPrompt>;
|
|
311
316
|
/** Queue a prompt and wait for its terminal result. */
|
|
312
317
|
submitPrompt(text: string, options?: SubmitPromptOptions): Promise<TurnResult>;
|
package/docs/acp-files.md
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
# Sending files in an ACP chat
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Every managed Agent can attach a file to its current ACP chat with the existing
|
|
4
|
+
Ours `send_file` tool. No Agent setting turns this on and no export directory is
|
|
5
|
+
configured or created:
|
|
4
6
|
|
|
5
7
|
```json
|
|
6
|
-
{"path":"
|
|
8
|
+
{"path":"/home/me/project/build/report.pdf"}
|
|
7
9
|
```
|
|
8
10
|
|
|
9
11
|
Omitting `contact` sends to the current ACP chat. Supplying a non-empty contact
|
|
10
12
|
keeps ordinary Ours delivery:
|
|
11
13
|
|
|
12
14
|
```json
|
|
13
|
-
{"contact":"Alice","path":"
|
|
15
|
+
{"contact":"Alice","path":"build/report.pdf"}
|
|
14
16
|
```
|
|
15
17
|
|
|
16
18
|
Only an absent contact chooses the chat. Null, empty or whitespace-only contacts,
|
|
17
|
-
unknown fields and unknown recipients fail without falling back to chat.
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
unknown fields and unknown recipients fail without falling back to chat.
|
|
20
|
+
Current-chat sending accepts `path`, optional `filename` and `mime`; inline
|
|
21
|
+
base64 and Ours reply fields are unsupported.
|
|
20
22
|
|
|
21
23
|
Each call makes one attempt. Failures are returned to the agent. There is no
|
|
22
24
|
request ID, deduplication, automatic retry or recovery of failed sends. If a user
|
|
@@ -24,52 +26,84 @@ asks to resend, another tool call makes a new attempt and can produce another
|
|
|
24
26
|
attachment. UUIDs identify stored attachments; the stored SHA-256 checks integrity.
|
|
25
27
|
Neither is a retry key.
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
## Paths
|
|
30
|
+
|
|
31
|
+
The Agent passes the path of the file it wants to send. Relative paths resolve
|
|
32
|
+
from the Agent's working directory; `..`, absolute paths, hidden directories and
|
|
33
|
+
symlinked parent directories are all accepted. The file does not have to be
|
|
34
|
+
inside the working directory or any particular subtree. It is opened by the
|
|
35
|
+
Fleet bridge inside the Agent's own OS sandbox, so the Agent can send exactly
|
|
36
|
+
what it can already read.
|
|
37
|
+
|
|
38
|
+
The file itself must pass ordinary integrity checks, each with its own result:
|
|
39
|
+
|
|
40
|
+
| Result | Meaning |
|
|
41
|
+
| --- | --- |
|
|
42
|
+
| `CHAT_FILE_NOT_FOUND` | nothing exists at the path |
|
|
43
|
+
| `CHAT_FILE_NOT_READABLE` | the Agent's OS user or sandbox cannot read it |
|
|
44
|
+
| `CHAT_FILE_IS_SYMLINK` | the named file is itself a symlink; pass its target |
|
|
45
|
+
| `CHAT_FILE_HARDLINKED` | the file has more than one hard link |
|
|
46
|
+
| `CHAT_FILE_NOT_REGULAR` | a directory, FIFO, socket or device |
|
|
47
|
+
| `FILE_EXCEEDS_20_MIB` | larger than the 20 MiB limit |
|
|
48
|
+
| `CHAT_FILE_CHANGED` / `SOURCE_SIZE_CHANGED` | modified while it was being read; finish writing first |
|
|
49
|
+
| `CHAT_FILE_REQUIRES_LINUX` | the Agent's bridge is not running on Linux |
|
|
50
|
+
| `PRIVATE_STATE_CANNOT_BE_SENT` | Fleet's own credentials, connector profile or stored attachments |
|
|
51
|
+
|
|
52
|
+
`PRIVATE_STATE_CANNOT_BE_SENT` covers only Fleet's private Ours runtime
|
|
53
|
+
directory, its attachment store and the connector profile and credential files,
|
|
54
|
+
checked on the provided path before approval and on the opened file's real path
|
|
55
|
+
afterwards. It is not configurable and is not a general path policy.
|
|
56
|
+
|
|
57
|
+
## Supported sessions
|
|
58
|
+
|
|
59
|
+
Current-chat delivery needs Linux, the managed Ours connector and a Codex or
|
|
60
|
+
Claude ACP session with a tracked active turn. Those Agents need no special
|
|
61
|
+
`harness_options`, MCP restriction or clean configuration home: inherited and
|
|
62
|
+
additional MCP connectors stay as they are, and Fleet's bound bridge is the
|
|
63
|
+
`ours` connector as it already is for every managed Agent.
|
|
64
|
+
|
|
65
|
+
Any other backend still sees the optional `contact`, and omitting it returns a
|
|
66
|
+
precise result rather than a missing tool:
|
|
28
67
|
|
|
29
|
-
```yaml
|
|
30
|
-
file_delivery:
|
|
31
|
-
enabled: true
|
|
32
|
-
directory: deliverables
|
|
33
68
|
```
|
|
69
|
+
CURRENT_CHAT_DELIVERY_UNAVAILABLE: sending a file to this chat requires a Codex or Claude ACP session; this Agent runs <harness>/<session>. Pass contact to send through Ours instead
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Idle, scheduled, steered, cancelled and replaced turns return
|
|
73
|
+
`CURRENT_CHAT_DELIVERY_UNAVAILABLE`; a second send while one is in progress
|
|
74
|
+
returns `FILE_DELIVERY_BUSY`.
|
|
75
|
+
|
|
76
|
+
A file is published only for a call that arrives over the Agent's
|
|
77
|
+
capability-authenticated bridge and binds to the turn that is live in that
|
|
78
|
+
Agent's own session. Connector inventory is not consulted, so unrelated or
|
|
79
|
+
similarly named connectors neither enable nor block delivery.
|
|
34
80
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
proxy. Before each turn, its public exact-thread, paginated MCP inventory must
|
|
52
|
-
show a connected managed `ours` with the expected optional-contact schema.
|
|
53
|
-
Ours aliases, filtered aliases, ambiguous identity, failed discovery and an
|
|
54
|
-
incompatible schema make the turn unavailable. Unrelated identified tools are
|
|
55
|
-
preserved. Explicitly disabled Ours is rejected before replacement. Inherited
|
|
56
|
-
TOML MCP settings, including project/profile sources, are conservatively
|
|
57
|
-
unsupported until their disabling semantics can be qualified before replacement.
|
|
58
|
-
Use a clean Codex configuration home for this initial opt-in.
|
|
59
|
-
|
|
60
|
-
Claude ACP 0.63.0's installed wrapper has no public same-session inventory RPC. Initial
|
|
61
|
-
support therefore requires an operator's explicit restricted MCP configuration:
|
|
81
|
+
## Permissions
|
|
82
|
+
|
|
83
|
+
Existing native tool approval and the neutral role permission policy both apply
|
|
84
|
+
before bytes are read. With `approval: allow`, a path inside the role's
|
|
85
|
+
`filesystem` boundary is allowed automatically; a path outside it is put to the
|
|
86
|
+
attached controller like any other out-of-boundary request, and is denied when
|
|
87
|
+
nobody is attached and `unattended: deny`. An `ask` decision needs an attached
|
|
88
|
+
controller; `deny` refuses with `CURRENT_CHAT_DELIVERY_UNAVAILABLE`, and a
|
|
89
|
+
declined request returns `FILE_SEND_DENIED`. A native policy that refuses MCP
|
|
90
|
+
execution (including Codex `never` without a native tool allowance) can still
|
|
91
|
+
refuse before Fleet receives the call.
|
|
92
|
+
|
|
93
|
+
## Retired configuration
|
|
94
|
+
|
|
95
|
+
`file_delivery` is retired. An existing block still loads so manifests need no
|
|
96
|
+
edit, and has no effect:
|
|
62
97
|
|
|
63
98
|
```yaml
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
ours: { command: ours-mcp, args: [proxy] }
|
|
99
|
+
file_delivery:
|
|
100
|
+
enabled: false # ignored: delivery stays available
|
|
101
|
+
directory: deliverables # ignored: not created, not enforced
|
|
68
102
|
```
|
|
69
103
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
104
|
+
Remove it at your convenience. Unknown keys inside it are still rejected.
|
|
105
|
+
|
|
106
|
+
## Storage and download
|
|
73
107
|
|
|
74
108
|
Successful sends copy complete bytes into private immutable storage and append a
|
|
75
109
|
server-origin `file.attached` event containing the actual session generation,
|
|
@@ -90,13 +124,23 @@ saving after a workspace change.
|
|
|
90
124
|
|
|
91
125
|
This source requires the companion MCP implementation's
|
|
92
126
|
`MANAGED_FILE_DELIVERY_VERSION = 1`. Fleet pins published
|
|
93
|
-
`@ours.network/mcp@1.2.0-nightly.13`, which includes it
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
127
|
+
`@ours.network/mcp@1.2.0-nightly.13`, which includes it and already treats the
|
|
128
|
+
export-directory hint as optional, so default delivery needs no MCP change. An
|
|
129
|
+
installed MCP package without the capability fails Agent startup with
|
|
130
|
+
`INCOMPATIBLE_MANAGED_FILE_DELIVERY` instead of quietly requiring a contact.
|
|
131
|
+
Changes to the MCP pin require clean installed-package qualification without a
|
|
132
|
+
built MCP overlay.
|
|
133
|
+
|
|
134
|
+
The deterministic native fixture (`test/file-delivery-native.integration.mjs`)
|
|
135
|
+
uses real ACP adapters, the real managed connector and an isolated daemon. Its
|
|
136
|
+
Agent has no `file_delivery` block and no MCP restriction, and it sends a file
|
|
137
|
+
from a hidden directory outside the working directory. It then downloads the
|
|
138
|
+
generated attachment over the authenticated HTTP route and compares bytes and
|
|
139
|
+
SHA-256, after checking that an anonymous and a forged-cookie request are
|
|
140
|
+
refused. `FLEET_FILE_NATIVE_VARIANT=connectors` adds unrelated MCP connectors
|
|
141
|
+
and requires them to stay usable. A local scripted provider supplies only tool
|
|
142
|
+
intents; approval, read, copy, publication and bytes come from the
|
|
143
|
+
implementation. It does not qualify external-model inference, deployment or
|
|
144
|
+
arbitrary native MCP request-to-ACP tool-call ID mapping. Correlation is the
|
|
145
|
+
authenticated bridge and the actual active Fleet turn, which is revoked by
|
|
146
|
+
cancellation/steering.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ours.network/fleet",
|
|
3
|
-
"version": "1.2.0-nightly.
|
|
3
|
+
"version": "1.2.0-nightly.59",
|
|
4
4
|
"description": "Harness-agnostic fleet of persistent, identity-bound AI agents. Declarative fleet.yaml, managed native/ACP sessions, supervision, and ours.network messaging.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "FSL-1.1-Apache-2.0",
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
export declare class CodexFileInventory {
|
|
2
|
-
private readonly sendToCodex;
|
|
3
|
-
private readonly emitToClient;
|
|
4
|
-
private checks;
|
|
5
|
-
private sequence;
|
|
6
|
-
constructor(sendToCodex: (line: string) => void, emitToClient: (line: string) => void);
|
|
7
|
-
/** Hold only a turn/start; unrelated tools/configuration are preserved. */
|
|
8
|
-
observeClientLine(line: string): boolean;
|
|
9
|
-
observeServerLine(line: string): boolean;
|
|
10
|
-
close(): void;
|
|
11
|
-
private query;
|
|
12
|
-
private qualified;
|
|
13
|
-
private finish;
|
|
14
|
-
private reject;
|
|
15
|
-
}
|