@ours.network/fleet 1.2.0-nightly.52 → 1.2.0-nightly.54
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 +2 -0
- package/dist/agent-ours/bridge.js +10 -3
- package/dist/agent-ours/file-rpc.d.ts +8 -0
- package/dist/agent-ours/file-rpc.js +11 -1
- package/dist/agent-ours/harness.js +47 -3
- package/dist/agent-ours/mcp-endpoint.d.ts +5 -0
- package/dist/agent-ours/mcp-endpoint.js +4 -0
- package/dist/agent-ours/runtime.d.ts +3 -1
- package/dist/agent-ours/runtime.js +14 -2
- package/dist/agent-ours/service.d.ts +2 -0
- package/dist/agent-ours/service.js +44 -2
- package/dist/application/conversation-history.d.ts +2 -0
- package/dist/application/conversation-history.js +25 -2
- package/dist/application/supervisor-ours-tools.js +3 -1
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +52 -29
- package/dist/build-info.json +4 -4
- package/dist/config.d.ts +9 -0
- package/dist/config.js +13 -2
- package/dist/file-delivery/reader.d.ts +17 -0
- package/dist/file-delivery/reader.js +94 -0
- package/dist/file-delivery/store.d.ts +15 -0
- package/dist/file-delivery/store.js +124 -0
- package/dist/file-delivery/types.d.ts +19 -0
- package/dist/file-delivery/types.js +1 -0
- package/dist/harness/claude-code.js +11 -10
- package/dist/harness/codex-app-server-proxy.js +6 -2
- package/dist/harness/codex-file-inventory.d.ts +15 -0
- package/dist/harness/codex-file-inventory.js +113 -0
- package/dist/harness/codex.js +9 -5
- package/dist/isolation/policy.js +4 -1
- package/dist/monitor.d.ts +7 -0
- package/dist/monitor.js +65 -1
- package/dist/rooms-tasks/cli.js +12 -2
- package/dist/rooms-tasks/cowork-http.js +29 -1
- package/dist/rooms-tasks/erasure.js +5 -3
- package/dist/rooms-tasks/workspace-artifacts.js +1 -1
- package/dist/rooms-tasks/workspace.d.ts +8 -1
- package/dist/rooms-tasks/workspace.js +168 -43
- package/dist/runner.js +32 -9
- package/dist/session/acp.d.ts +4 -0
- package/dist/session/acp.js +61 -8
- package/dist/session/conversation-types.d.ts +4 -2
- package/dist/session/types.d.ts +1 -0
- package/dist/startup-prompt.d.ts +4 -0
- package/dist/startup-prompt.js +36 -0
- package/dist/startup-telemetry.d.ts +16 -0
- package/dist/startup-telemetry.js +46 -0
- package/dist/web/server.js +24 -0
- package/docs/acp-files.md +102 -0
- package/docs/task-workspaces.md +31 -0
- package/package.json +4 -3
package/dist/session/acp.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { FileDeliveryInput, CopyChatFile, DeliveredFile } from '../file-delivery/types.js';
|
|
1
2
|
import { type TaskNoticeBinding, type TaskCreatedNotice } from './task-notice.js';
|
|
2
3
|
import * as acp from '@agentclientprotocol/sdk';
|
|
3
4
|
import type { CommonPermissions } from '../config.js';
|
|
@@ -176,6 +177,7 @@ export declare class AcpSession implements AgentSession {
|
|
|
176
177
|
private stallTimer?;
|
|
177
178
|
private stallAttempt?;
|
|
178
179
|
private readonly toolBoundaryWaiters;
|
|
180
|
+
private fileExportBusy;
|
|
179
181
|
private activeTurn?;
|
|
180
182
|
private constructor();
|
|
181
183
|
static start(options: AcpSessionOptions): Promise<AcpSession>;
|
|
@@ -303,6 +305,8 @@ export declare class AcpSession implements AgentSession {
|
|
|
303
305
|
*/
|
|
304
306
|
private settlePendingAutomatically;
|
|
305
307
|
exitResult(): ExitRecord | null;
|
|
308
|
+
/** One attempt, tied to the managed live turn captured before any approval/read. */
|
|
309
|
+
sendFileToChat(input: FileDeliveryInput, signal: AbortSignal, copy: CopyChatFile, policy: CommonPermissions): Promise<DeliveredFile>;
|
|
306
310
|
close(): Promise<void>;
|
|
307
311
|
/** ACP requires the field. Bundled agents treat [] as no client-added servers. */
|
|
308
312
|
private declaredMcpServers;
|
package/dist/session/acp.js
CHANGED
|
@@ -278,6 +278,7 @@ export class AcpSession {
|
|
|
278
278
|
stallTimer;
|
|
279
279
|
stallAttempt;
|
|
280
280
|
toolBoundaryWaiters = new Set();
|
|
281
|
+
fileExportBusy = false;
|
|
281
282
|
activeTurn;
|
|
282
283
|
constructor(options, child, connection) {
|
|
283
284
|
this.options = options;
|
|
@@ -823,6 +824,8 @@ export class AcpSession {
|
|
|
823
824
|
if (!this.sessionId)
|
|
824
825
|
return;
|
|
825
826
|
const active = this.activeTurn;
|
|
827
|
+
if (active)
|
|
828
|
+
active.fileDeliveryRevoked = true;
|
|
826
829
|
const previousSource = active?.cancellationSource;
|
|
827
830
|
if (active && (source === 'owner' || source === 'local-console' || !previousSource))
|
|
828
831
|
active.cancellationSource = source;
|
|
@@ -1026,6 +1029,54 @@ export class AcpSession {
|
|
|
1026
1029
|
exitResult() {
|
|
1027
1030
|
return this.exit;
|
|
1028
1031
|
}
|
|
1032
|
+
/** One attempt, tied to the managed live turn captured before any approval/read. */
|
|
1033
|
+
async sendFileToChat(input, signal, copy, policy) {
|
|
1034
|
+
const active = this.activeTurn, sessionId = this.sessionId;
|
|
1035
|
+
const check = () => {
|
|
1036
|
+
signal.throwIfAborted();
|
|
1037
|
+
if (!active || !sessionId || this.activeTurn !== active || this.sessionId !== sessionId || active.fileDeliveryRevoked
|
|
1038
|
+
|| active.cancellationSource || this.closing || !this.isAlive() || this.replaying || this.steeringOccupied
|
|
1039
|
+
|| active.origin?.kind === 'scheduled-loop' || policy.approval === 'deny')
|
|
1040
|
+
throw Error('CURRENT_CHAT_DELIVERY_UNAVAILABLE');
|
|
1041
|
+
};
|
|
1042
|
+
check();
|
|
1043
|
+
if (this.fileExportBusy)
|
|
1044
|
+
throw Error('FILE_DELIVERY_BUSY');
|
|
1045
|
+
this.fileExportBusy = true;
|
|
1046
|
+
const toolCallId = 'fleet-file-export-' + randomUUID();
|
|
1047
|
+
try {
|
|
1048
|
+
const onAbort = () => {
|
|
1049
|
+
for (const [id, pending] of this.pendingPermissions)
|
|
1050
|
+
if (pending.toolCallId === toolCallId)
|
|
1051
|
+
this.settlePendingAutomatically(id, pending, 'cancelled', undefined, 'File send cancelled');
|
|
1052
|
+
};
|
|
1053
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
1054
|
+
let answer;
|
|
1055
|
+
try {
|
|
1056
|
+
answer = await this.requestPermission({ sessionId: sessionId, toolCall: {
|
|
1057
|
+
toolCallId, title: 'Send file to this ACP chat: ' + (input.filename ?? input.path), kind: 'read', status: 'pending',
|
|
1058
|
+
locations: [{ path: resolve(this.options.cwd, input.path) }],
|
|
1059
|
+
}, options: [{ optionId: 'send', name: 'Send file', kind: 'allow_once' }, { optionId: 'deny', name: 'Do not send', kind: 'reject_once' }] }, policy);
|
|
1060
|
+
}
|
|
1061
|
+
finally {
|
|
1062
|
+
signal.removeEventListener('abort', onAbort);
|
|
1063
|
+
}
|
|
1064
|
+
check();
|
|
1065
|
+
if (answer.outcome.outcome !== 'selected' || answer.outcome.optionId !== 'send')
|
|
1066
|
+
throw Error('FILE_SEND_DENIED');
|
|
1067
|
+
const attachment = await copy({ sessionGeneration: this.sessionGeneration, acpSessionId: sessionId, turnId: active.id });
|
|
1068
|
+
// Synchronous check + durable append establishes order with cancellation/stop.
|
|
1069
|
+
// No await, pending-send journal, replay or recovery exists after this point.
|
|
1070
|
+
check();
|
|
1071
|
+
this.conversation.append({ kind: 'file.attached', source: 'agent', sessionGeneration: this.sessionGeneration,
|
|
1072
|
+
acpSessionId: sessionId, promptId: active.id, turnId: active.id, payload: { attachment } });
|
|
1073
|
+
return attachment;
|
|
1074
|
+
}
|
|
1075
|
+
finally {
|
|
1076
|
+
this.releaseTool(toolCallId);
|
|
1077
|
+
this.fileExportBusy = false;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1029
1080
|
async close() {
|
|
1030
1081
|
this.closing = true;
|
|
1031
1082
|
if (this.stallTimer)
|
|
@@ -1414,6 +1465,8 @@ export class AcpSession {
|
|
|
1414
1465
|
}
|
|
1415
1466
|
async steerPrompt(text) {
|
|
1416
1467
|
this.steeringWasUsed = true;
|
|
1468
|
+
if (this.activeTurn)
|
|
1469
|
+
this.activeTurn.fileDeliveryRevoked = true;
|
|
1417
1470
|
if (!this.sessionId || !this.isAlive())
|
|
1418
1471
|
return turnResult(false, 'failed', this.lastError ?? 'ACP session is offline');
|
|
1419
1472
|
this.steeringRequests++;
|
|
@@ -1445,7 +1498,7 @@ export class AcpSession {
|
|
|
1445
1498
|
this.steeringRequests--;
|
|
1446
1499
|
}
|
|
1447
1500
|
}
|
|
1448
|
-
requestPermission(params) {
|
|
1501
|
+
requestPermission(params, neutralPolicy) {
|
|
1449
1502
|
// `kinds` is a PRIORITY order. Scanning the agent's option array instead
|
|
1450
1503
|
// (`options.find(o => kinds.includes(o.kind))`) hands the choice to whatever
|
|
1451
1504
|
// order the agent happened to list, which is exactly how an automatic denial
|
|
@@ -1463,7 +1516,7 @@ export class AcpSession {
|
|
|
1463
1516
|
// Permission is part of the tool lifecycle. Reserve before any policy or
|
|
1464
1517
|
// human decision so a monitor wake cannot slip between request and answer.
|
|
1465
1518
|
this.reservePermission(toolCallId, permissionId);
|
|
1466
|
-
if (this.isEffectiveCodexProtectedMcpApproval(params)) {
|
|
1519
|
+
if (!neutralPolicy && this.isEffectiveCodexProtectedMcpApproval(params)) {
|
|
1467
1520
|
// Protected MCP approval is already the tool's narrow gate. Never turn
|
|
1468
1521
|
// this one decision into an adapter-wide standing grant.
|
|
1469
1522
|
const option = choose(['allow_once']);
|
|
@@ -1474,9 +1527,9 @@ export class AcpSession {
|
|
|
1474
1527
|
this.releasePermission(toolCallId, permissionId);
|
|
1475
1528
|
return Promise.resolve(response);
|
|
1476
1529
|
}
|
|
1477
|
-
if (this.options.permissions.approval === 'allow' && this.withinAutomaticBoundary(params)) {
|
|
1530
|
+
if ((neutralPolicy ?? this.options.permissions).approval === 'allow' && this.withinAutomaticBoundary(params, neutralPolicy)) {
|
|
1478
1531
|
const option = choose(['allow_always', 'allow_once']);
|
|
1479
|
-
const response = this.settleAutomatically(params, option, 'allowed', 'permissions.approval=allow', `the request is inside the ${this.options.permissions.filesystem} boundary`);
|
|
1532
|
+
const response = this.settleAutomatically(params, option, 'allowed', 'permissions.approval=allow', `the request is inside the ${(neutralPolicy ?? this.options.permissions).filesystem} boundary`);
|
|
1480
1533
|
if (option)
|
|
1481
1534
|
this.allowPermission(toolCallId, permissionId);
|
|
1482
1535
|
else
|
|
@@ -1486,8 +1539,8 @@ export class AcpSession {
|
|
|
1486
1539
|
// A live grace window still counts as attended: the controller may be
|
|
1487
1540
|
// mid-reconnect, and denying instantly is exactly what the grace prevents.
|
|
1488
1541
|
const unattended = this.controllerCount === 0 && !this.controllerGrace
|
|
1489
|
-
&& this.options.permissions.unattended === 'deny';
|
|
1490
|
-
if (this.options.permissions.approval === 'deny' || unattended) {
|
|
1542
|
+
&& (neutralPolicy ?? this.options.permissions).unattended === 'deny';
|
|
1543
|
+
if ((neutralPolicy ?? this.options.permissions).approval === 'deny' || unattended) {
|
|
1491
1544
|
// reject_once FIRST: `reject_always` teaches the agent a standing rule from
|
|
1492
1545
|
// a decision no human made, so one unattended denial would silently disable
|
|
1493
1546
|
// the tool for the rest of the session.
|
|
@@ -1588,8 +1641,8 @@ export class AcpSession {
|
|
|
1588
1641
|
? { outcome: { outcome: 'selected', optionId: option.optionId } }
|
|
1589
1642
|
: { outcome: { outcome: 'cancelled' } };
|
|
1590
1643
|
}
|
|
1591
|
-
withinAutomaticBoundary(params) {
|
|
1592
|
-
const filesystem = this.options.permissions.filesystem;
|
|
1644
|
+
withinAutomaticBoundary(params, policy) {
|
|
1645
|
+
const filesystem = (policy ?? this.options.permissions).filesystem;
|
|
1593
1646
|
if (filesystem === 'unrestricted')
|
|
1594
1647
|
return true;
|
|
1595
1648
|
if (filesystem === 'read-only' && params.toolCall.kind !== 'read')
|
|
@@ -8,7 +8,7 @@ import type { PromptOrigin, TurnCancellationSource, TurnOutcome } from './types.
|
|
|
8
8
|
* file touches the wire: transport updates are reduced into these shapes by a
|
|
9
9
|
* session adapter, and the store assigns `seq`/`eventId`/timestamps.
|
|
10
10
|
*/
|
|
11
|
-
export type ConversationEventKind = 'prompt.admitted' | 'prompt.started' | 'prompt.interrupt_requested' | 'message.chunk' | 'message.replace' | 'thought.chunk' | 'thought.replace' | 'plan.replace' | 'tool.upsert' | 'tool.content_chunk' | 'permission.requested' | 'permission.resolved' | 'fleet.task_created' | 'monitor.delivery' | 'usage.updated' | 'turn.state' | 'turn.completed' | 'session.state' | 'session.info' | 'capabilities.updated' | 'error'
|
|
11
|
+
export type ConversationEventKind = 'prompt.admitted' | 'prompt.started' | 'prompt.interrupt_requested' | 'message.chunk' | 'message.replace' | 'thought.chunk' | 'thought.replace' | 'plan.replace' | 'tool.upsert' | 'tool.content_chunk' | 'permission.requested' | 'permission.resolved' | 'file.attached' | 'fleet.task_created' | 'monitor.delivery' | 'usage.updated' | 'turn.state' | 'turn.completed' | 'session.state' | 'session.info' | 'capabilities.updated' | 'error'
|
|
12
12
|
/** A well-formed transport update this version cannot represent. Bounded, never a crash. */
|
|
13
13
|
| 'unsupported';
|
|
14
14
|
/** Where a conversation record came from. Typed provenance, never prompt text. */
|
|
@@ -228,7 +228,9 @@ export interface BoundedJson {
|
|
|
228
228
|
digest?: string;
|
|
229
229
|
redacted?: true;
|
|
230
230
|
}
|
|
231
|
-
export type ConversationPayload =
|
|
231
|
+
export type ConversationPayload = {
|
|
232
|
+
attachment: import('../file-delivery/types.js').DeliveredFile;
|
|
233
|
+
} | MessageChunkPayload | ThoughtChunkPayload | PlanReplacePayload | ToolUpsertPayload | UsageUpdatedPayload | SessionStatePayload | SessionInfoPayload | CapabilitiesUpdatedPayload | UnsupportedPayload | PromptAdmittedPayload | PromptStartedPayload | PromptInterruptRequestedPayload | PermissionRequestedPayload | PermissionResolvedPayload | TurnStatePayload | TurnCompletedPayload | MonitorDeliveryPayload | SessionLifecyclePayload | ErrorPayload | TaskCreatedNotice;
|
|
232
234
|
export interface ConversationEventV1 {
|
|
233
235
|
schemaVersion: 1;
|
|
234
236
|
roleId: string;
|
package/dist/session/types.d.ts
CHANGED
|
@@ -284,6 +284,7 @@ export interface ConversationHandlePage {
|
|
|
284
284
|
* on any concrete transport which implements it.
|
|
285
285
|
*/
|
|
286
286
|
export interface AgentSession {
|
|
287
|
+
sendFileToChat?: (input: import('../file-delivery/types.js').FileDeliveryInput, signal: AbortSignal, copy: import('../file-delivery/types.js').CopyChatFile, policy: import('../config.js').CommonPermissions) => Promise<import('../file-delivery/types.js').DeliveredFile>;
|
|
287
288
|
readonly backend: SessionBackendId;
|
|
288
289
|
readonly pid: number;
|
|
289
290
|
/** Live capabilities; optional only for source compatibility with external adapters. */
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Readiness is a separate turn after identity/profile and room admission succeed. */
|
|
2
|
+
export declare function managedStartupPrompt(stateDir: string, mode: 'fresh' | 'resume', hasPriorBoot?: boolean): string;
|
|
3
|
+
/** Configured work/continuity runs after readiness, never inside the fast turn. */
|
|
4
|
+
export declare function managedTaskPrompt(stateDir: string, mode: 'fresh' | 'resume', hasPriorBoot?: boolean): string;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
/** Readiness is a separate turn after identity/profile and room admission succeed. */
|
|
3
|
+
export function managedStartupPrompt(stateDir, mode, hasPriorBoot = false) {
|
|
4
|
+
const briefing = join(stateDir, 'briefing.md');
|
|
5
|
+
const continuity = mode === 'resume' || hasPriorBoot
|
|
6
|
+
? ` For a later continuation task, read ${join(stateDir, 'WORKLOG.md')} once for continuity.`
|
|
7
|
+
: '';
|
|
8
|
+
return 'Fleet readiness check. The supervisor has verified your assigned identity, configured Role profile, '
|
|
9
|
+
+ 'and any required room admission. Reply with a short final console acknowledgement that you are ready, '
|
|
10
|
+
+ 'then end this turn. Announce readiness before any tool calls or file reads. '
|
|
11
|
+
+ 'Do not read briefing, routines, worklog, inbox, history, help or config during this readiness turn; '
|
|
12
|
+
+ 'do not perform identity/profile setup or broad tool discovery. Do not claim task work has been done. '
|
|
13
|
+
+ 'The supervisor delivers configured work separately; otherwise wait for a concrete task or supervisor wake. '
|
|
14
|
+
+ `For a later task or mail wake, read ${briefing} once before acting and reuse it within that wake. `
|
|
15
|
+
+ 'Read routines only for concrete work; an idle launch needs no detail reads. '
|
|
16
|
+
+ 'The supervisor delivers initial unread mail and later arrivals as wakes; do not poll the inbox at startup.'
|
|
17
|
+
+ continuity;
|
|
18
|
+
}
|
|
19
|
+
/** Configured work/continuity runs after readiness, never inside the fast turn. */
|
|
20
|
+
export function managedTaskPrompt(stateDir, mode, hasPriorBoot = false) {
|
|
21
|
+
const briefing = join(stateDir, 'briefing.md');
|
|
22
|
+
const continuity = mode === 'resume' || hasPriorBoot
|
|
23
|
+
? ` Read ${join(stateDir, 'WORKLOG.md')} once for continuity.`
|
|
24
|
+
: '';
|
|
25
|
+
return `Read and follow ${briefing} for the assigned work. Readiness has already been announced; `
|
|
26
|
+
+ 'this is a separate task turn, so do not repeat the readiness acknowledgement.' + continuity + ' '
|
|
27
|
+
+ 'Identify the concrete assignment in the briefing or unfinished work in the continuation log. '
|
|
28
|
+
+ 'A role charter or public bio alone is not a new task. If there is no concrete work, end this turn '
|
|
29
|
+
+ 'and await a task without reading routines, inbox, history, or unrelated files. '
|
|
30
|
+
+ 'For concrete work, read any existing routines once, reuse details already read in this wake, '
|
|
31
|
+
+ 'acknowledge the actual task and next action through its assigned reply route, and proceed. '
|
|
32
|
+
+ (mode === 'fresh' && !hasPriorBoot ? 'On a fresh launch, do not read a worklog for initialization. ' : '')
|
|
33
|
+
+ 'Use inbox/history only when a mail wake, unread pagination, or the actual task needs them. '
|
|
34
|
+
+ 'The supervisor delivers initial unread mail; no model-owned startup poll is needed. '
|
|
35
|
+
+ 'Identity/profile setup, help/config exploration and broad tool discovery are not startup rituals.';
|
|
36
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ConversationEventV1 } from './session/conversation-types.js';
|
|
2
|
+
/** Body-free startup diagnostics. First output is an observation, not proof of useful work. */
|
|
3
|
+
export declare class StartupTelemetry {
|
|
4
|
+
private readonly now;
|
|
5
|
+
private readonly log;
|
|
6
|
+
private readonly started;
|
|
7
|
+
private promptAt?;
|
|
8
|
+
private promptId?;
|
|
9
|
+
private firstOutput;
|
|
10
|
+
private completed;
|
|
11
|
+
private readonly tools;
|
|
12
|
+
constructor(now: () => number, log: (line: string) => void);
|
|
13
|
+
mark(stage: 'supervisor_started' | 'identity_prepare_started' | 'identity_ready' | 'harness_starting' | 'harness_ready' | 'startup_submitted' | 'readiness_turn_completed'): void;
|
|
14
|
+
observe(event: ConversationEventV1): void;
|
|
15
|
+
finish(): void;
|
|
16
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Body-free startup diagnostics. First output is an observation, not proof of useful work. */
|
|
2
|
+
export class StartupTelemetry {
|
|
3
|
+
now;
|
|
4
|
+
log;
|
|
5
|
+
started;
|
|
6
|
+
promptAt;
|
|
7
|
+
promptId;
|
|
8
|
+
firstOutput = false;
|
|
9
|
+
completed = false;
|
|
10
|
+
tools = new Set();
|
|
11
|
+
constructor(now, log) {
|
|
12
|
+
this.now = now;
|
|
13
|
+
this.log = log;
|
|
14
|
+
this.started = now();
|
|
15
|
+
}
|
|
16
|
+
mark(stage) {
|
|
17
|
+
const at = this.now();
|
|
18
|
+
if (stage === 'startup_submitted')
|
|
19
|
+
this.promptAt = at;
|
|
20
|
+
this.log(`startup stage=${stage} elapsed_ms=${Math.max(0, at - this.started)}`);
|
|
21
|
+
}
|
|
22
|
+
observe(event) {
|
|
23
|
+
if (this.promptAt === undefined || this.completed || event.source === 'agent_replay')
|
|
24
|
+
return;
|
|
25
|
+
if (event.kind === 'prompt.started' && event.source === 'startup')
|
|
26
|
+
this.promptId = event.promptId;
|
|
27
|
+
if (this.promptId && event.promptId && event.promptId !== this.promptId)
|
|
28
|
+
return;
|
|
29
|
+
if (event.kind === 'tool.upsert' && 'toolCallId' in event.payload && event.payload.toolCallId)
|
|
30
|
+
this.tools.add(event.payload.toolCallId);
|
|
31
|
+
if (!this.firstOutput && ['message.chunk', 'message.replace'].includes(event.kind)
|
|
32
|
+
&& 'role' in event.payload && event.payload.role === 'assistant'
|
|
33
|
+
&& 'content' in event.payload && event.payload.content.type === 'text'
|
|
34
|
+
&& event.payload.content.text.trim()) {
|
|
35
|
+
this.firstOutput = true;
|
|
36
|
+
const at = this.now();
|
|
37
|
+
this.log(`startup stage=first_assistant_output elapsed_ms=${Math.max(0, at - this.started)} model_and_tools_ms=${Math.max(0, at - this.promptAt)} tools=${this.tools.size}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
finish() {
|
|
41
|
+
if (this.completed)
|
|
42
|
+
return;
|
|
43
|
+
this.completed = true;
|
|
44
|
+
this.log(`startup stage=turn_settled elapsed_ms=${Math.max(0, this.now() - this.started)} tools=${this.tools.size} first_output=${this.firstOutput}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
package/dist/web/server.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { conversationHasAttachment } from '../application/conversation-history.js';
|
|
2
|
+
import { readDeliveredFile } from '../file-delivery/store.js';
|
|
1
3
|
import { WorkspaceDeviceAuthError } from './workspace-devices.js';
|
|
2
4
|
import { readFileSync } from 'node:fs';
|
|
3
5
|
import { packagedPresetRoot } from '../preset-bootstrap.js';
|
|
@@ -756,6 +758,28 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
756
758
|
const page = await control.conversationPage({ after: request.query.after, limit });
|
|
757
759
|
return { ...page, events: page.events.map(present) };
|
|
758
760
|
});
|
|
761
|
+
app.get('/api/v1/roles/:id/artifacts/:artifactId', async (request, reply) => {
|
|
762
|
+
auth.authenticate(request);
|
|
763
|
+
const role = await services.repository.get(request.params.id);
|
|
764
|
+
if (!role)
|
|
765
|
+
throw new FleetError('role_not_found', 'Agent not found');
|
|
766
|
+
const stateDir = services.repository.stateDir(role);
|
|
767
|
+
if (!stateDir)
|
|
768
|
+
throw new FleetError('resource_not_found', 'Agent file not found');
|
|
769
|
+
try {
|
|
770
|
+
const { record, bytes } = await readDeliveredFile(stateDir, request.params.artifactId);
|
|
771
|
+
if (!await conversationHasAttachment(stateDir, record))
|
|
772
|
+
throw Error('ARTIFACT_NOT_PUBLISHED');
|
|
773
|
+
reply.header('Cache-Control', 'private, no-store');
|
|
774
|
+
reply.header('X-Content-Type-Options', 'nosniff');
|
|
775
|
+
reply.header('Content-Security-Policy', "default-src 'none'; sandbox");
|
|
776
|
+
reply.header('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(record.name).replace(/'/g, '%27')}`);
|
|
777
|
+
return reply.type('application/octet-stream').send(bytes);
|
|
778
|
+
}
|
|
779
|
+
catch {
|
|
780
|
+
throw new FleetError('resource_not_found', 'Agent file unavailable');
|
|
781
|
+
}
|
|
782
|
+
});
|
|
759
783
|
app.get('/api/v1/roles/:id/attachments/:attachmentId', async (request, reply) => {
|
|
760
784
|
auth.authenticate(request);
|
|
761
785
|
const role = await services.repository.get(request.params.id);
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Sending files in an ACP chat
|
|
2
|
+
|
|
3
|
+
An opted-in managed Agent uses the existing Ours `send_file` tool:
|
|
4
|
+
|
|
5
|
+
```json
|
|
6
|
+
{"path":"deliverables/report.pdf"}
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Omitting `contact` sends to the current ACP chat. Supplying a non-empty contact
|
|
10
|
+
keeps ordinary Ours delivery:
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{"contact":"Alice","path":"deliverables/report.pdf"}
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
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. Outside
|
|
18
|
+
opt-in, contact remains required. Current-chat sending accepts `path`, optional
|
|
19
|
+
`filename` and `mime`; inline base64 and Ours reply fields are unsupported.
|
|
20
|
+
|
|
21
|
+
Each call makes one attempt. Failures are returned to the agent. There is no
|
|
22
|
+
request ID, deduplication, automatic retry or recovery of failed sends. If a user
|
|
23
|
+
asks to resend, another tool call makes a new attempt and can produce another
|
|
24
|
+
attachment. UUIDs identify stored attachments; the stored SHA-256 checks integrity.
|
|
25
|
+
Neither is a retry key.
|
|
26
|
+
|
|
27
|
+
Set this on the Agent definition, not manifest defaults or a Brain:
|
|
28
|
+
|
|
29
|
+
```yaml
|
|
30
|
+
file_delivery:
|
|
31
|
+
enabled: true
|
|
32
|
+
directory: deliverables
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The tool description advertises this directory and the 20 MiB limit to the Agent.
|
|
36
|
+
The directory is a non-hidden subtree of the Agent's working directory. Both
|
|
37
|
+
routes interpret relative paths from that working directory; absolute paths keep
|
|
38
|
+
their usual meaning. Chat sending additionally enforces the export directory.
|
|
39
|
+
The Agent should finish writing the file before calling `send_file`. Symlinks,
|
|
40
|
+
hardlinks, hidden/credential paths, changed files and files over 20 MiB fail.
|
|
41
|
+
|
|
42
|
+
Initial support requires Linux, the managed Ours connector, a stock Codex or
|
|
43
|
+
Claude ACP adapter, and a tracked active turn. Idle, scheduled, steered, cancelled,
|
|
44
|
+
replaced and unsupported sessions cannot export. Existing native tool approval
|
|
45
|
+
and the neutral role permission policy both apply before bytes are read. An
|
|
46
|
+
`ask` decision needs an attached controller; `deny` refuses. A native policy that
|
|
47
|
+
refuses MCP execution (including Codex `never` without a native tool allowance)
|
|
48
|
+
can still refuse before Fleet receives the call.
|
|
49
|
+
|
|
50
|
+
Codex requires native runtime 0.159.0 or newer and Fleet's managed App Server
|
|
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:
|
|
62
|
+
|
|
63
|
+
```yaml
|
|
64
|
+
harness_options:
|
|
65
|
+
mcp_servers_only: true
|
|
66
|
+
mcp_servers:
|
|
67
|
+
ours: { command: ours-mcp, args: [proxy] }
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Fleet replaces that connector with its bound bridge. Additional explicit MCP
|
|
71
|
+
servers and custom ACP commands are unsupported for this feature. Default-off
|
|
72
|
+
Agents retain their existing configuration.
|
|
73
|
+
|
|
74
|
+
Successful sends copy complete bytes into private immutable storage and append a
|
|
75
|
+
server-origin `file.attached` event containing the actual session generation,
|
|
76
|
+
ACP session and managed turn. One export can run at a time. Cancellation or
|
|
77
|
+
steering before publication revokes it; cancellation after publication does not
|
|
78
|
+
undo it. Unpublished stored bytes are never replayed or republished. Storage is
|
|
79
|
+
bounded to 100 files and 200 MiB per role incarnation, including incomplete and
|
|
80
|
+
orphaned copies. Exhausted storage fails; no automatic recovery is performed.
|
|
81
|
+
|
|
82
|
+
The App renders persisted file cards from these typed events. HTML and images
|
|
83
|
+
also download as files. Downloads require authentication, the matching role
|
|
84
|
+
incarnation, a published conversation event and intact bytes. Deleted, missing,
|
|
85
|
+
changed or orphaned artifacts return unavailable. Remote workspaces fetch bytes
|
|
86
|
+
using their existing device credential, limit the read to 20 MiB and prevent
|
|
87
|
+
saving after a workspace change.
|
|
88
|
+
|
|
89
|
+
## Release prerequisite and qualification
|
|
90
|
+
|
|
91
|
+
This source requires the companion MCP implementation's
|
|
92
|
+
`MANAGED_FILE_DELIVERY_VERSION = 1`. Fleet pins published
|
|
93
|
+
`@ours.network/mcp@1.2.0-nightly.13`, which includes it. Older MCP packages without
|
|
94
|
+
this capability fail closed when file delivery is enabled. Changes to the MCP
|
|
95
|
+
pin require clean installed-package qualification without a built MCP overlay.
|
|
96
|
+
|
|
97
|
+
The deterministic native fixture uses real ACP adapters, the real managed
|
|
98
|
+
connector and an isolated daemon. A local scripted provider supplies only tool
|
|
99
|
+
intents; approval, read, copy, publication and bytes come from the implementation.
|
|
100
|
+
It does not qualify external-model inference, deployment or arbitrary native
|
|
101
|
+
MCP request-to-ACP tool-call ID mapping. Correlation is the authenticated bridge
|
|
102
|
+
and the actual active Fleet turn, which is revoked by cancellation/steering.
|
package/docs/task-workspaces.md
CHANGED
|
@@ -80,6 +80,37 @@ any remaining original archive and still refuse replacement live member state
|
|
|
80
80
|
or a recreated identity even after the archive has been consumed. Failed deletion
|
|
81
81
|
remains retryable with its recorded error.
|
|
82
82
|
|
|
83
|
+
Explicit deletion restores missing owner read/write/traversal bits on real
|
|
84
|
+
directories inside a proven owned workspace or retired archive before the Git
|
|
85
|
+
audit and removal. This handles restrictive artifacts left by tests even when
|
|
86
|
+
they belong to the same Unix user. It adds no group/other access, follows no
|
|
87
|
+
artifact symlinks, crosses no mounted device boundary, and never changes the
|
|
88
|
+
owner. Only Git pointer files that the audit must read may gain owner-read;
|
|
89
|
+
hardlinked unreadable pointers are refused to avoid changing an outside alias.
|
|
90
|
+
Permission changes target a verified open inode. Readable foreign-UID directories
|
|
91
|
+
may be traversed unchanged; missing owner access is never repaired for another
|
|
92
|
+
UID. On Linux, traversal and Git pointer reads use pinned directory/file
|
|
93
|
+
descriptors, and a detected pathname replacement stops cleanup before descending.
|
|
94
|
+
Unreadable entries
|
|
95
|
+
use an `O_PATH` descriptor through `/proc/self/fd`; other platforms retain the
|
|
96
|
+
error if they cannot open the entry safely. A restricted workspace root is
|
|
97
|
+
temporarily accessible to verify its exact ownership marker; failed marker
|
|
98
|
+
verification restores its original mode. Partial token-derived deletion
|
|
99
|
+
tombstones resume directory cleanup without re-reading already-removed Git
|
|
100
|
+
metadata. List, show, and ordinary Git audit do not repair permissions.
|
|
101
|
+
|
|
102
|
+
An accepted deletion is hidden from ordinary task lists while cleanup settles.
|
|
103
|
+
Use `ours-fleet task list --include-deleting --json` (or the API's
|
|
104
|
+
`includeDeleting=true`) to include retained cleanup intents, and `task show ID`
|
|
105
|
+
to see pending/error/retry information. Disappearance from the ordinary list is
|
|
106
|
+
not proof of completed deletion; successful deletion removes the task record
|
|
107
|
+
after workspace and member cleanup.
|
|
108
|
+
Cowork HTTP transport failures record the RPC method, failing stage and a
|
|
109
|
+
bounded error category in the cleanup intent. Credential rejection remains an
|
|
110
|
+
explicit `unauthorized` protocol error. Transport diagnostics do not include
|
|
111
|
+
request bodies, credentials, URLs or raw exception text, and failed mutations
|
|
112
|
+
are never automatically replayed.
|
|
113
|
+
|
|
83
114
|
Deletion rejects traversal, foreign recorded paths, changed markers and symlinked
|
|
84
115
|
workspace ancestors. Ordinary artifact symlinks are unlinked without following
|
|
85
116
|
their targets. Git control symlinks and foreign pointers are refused. As with
|
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.54",
|
|
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",
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"docs/live-voice.md",
|
|
23
23
|
"docs/room-layouts.md",
|
|
24
24
|
"docs/validation/managed-cli-permissions.md",
|
|
25
|
-
"LICENSE"
|
|
25
|
+
"LICENSE",
|
|
26
|
+
"docs/acp-files.md"
|
|
26
27
|
],
|
|
27
28
|
"engines": {
|
|
28
29
|
"node": ">=22.13.0"
|
|
@@ -43,7 +44,7 @@
|
|
|
43
44
|
"@agentclientprotocol/sdk": "^1.3.0",
|
|
44
45
|
"@fastify/websocket": "^11.2.0",
|
|
45
46
|
"@ours.network/cli": "2.8.1-nightly.15",
|
|
46
|
-
"@ours.network/mcp": "1.2.0-nightly.
|
|
47
|
+
"@ours.network/mcp": "1.2.0-nightly.13",
|
|
47
48
|
"@ours.network/sdk": "3.8.1-nightly.17",
|
|
48
49
|
"commander": "^12.1.0",
|
|
49
50
|
"fastify": "^5.4.0",
|