@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/README.md
CHANGED
|
@@ -1920,3 +1920,5 @@ Prerelease rollout requires this Fleet capability to be published first, then th
|
|
|
1920
1920
|
installer's normal generated version/integrity lock updated, then installer and
|
|
1921
1921
|
App released. The currently pinned Fleet nightly.41 does not support v2 grants.
|
|
1922
1922
|
These source changes do not publish, merge, or deploy any component.
|
|
1923
|
+
|
|
1924
|
+
See [ACP file sending](docs/acp-files.md) for the opt-in same-chat `send_file` flow and supported configurations.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { openExportFile } from '../file-delivery/reader.js';
|
|
2
3
|
import { createHash } from 'node:crypto';
|
|
3
4
|
import { connect } from 'node:net';
|
|
4
5
|
import { access, mkdir, open, readFile } from 'node:fs/promises';
|
|
@@ -71,7 +72,7 @@ export async function runBridge(descriptorPath) {
|
|
|
71
72
|
value = false;
|
|
72
73
|
}
|
|
73
74
|
}
|
|
74
|
-
else if (op === 'read-open' || op === 'write-open') {
|
|
75
|
+
else if (op === 'read-open' || op === 'write-open' || op === 'export-open') {
|
|
75
76
|
if (handles.size >= 32 ||
|
|
76
77
|
typeof args.handle !== 'string' ||
|
|
77
78
|
handles.has(args.handle) ||
|
|
@@ -80,8 +81,9 @@ export async function runBridge(descriptorPath) {
|
|
|
80
81
|
const path = resolve(args.path);
|
|
81
82
|
if (op === 'write-open')
|
|
82
83
|
await mkdir(dirname(path), { recursive: true });
|
|
83
|
-
const file = await open(path, op === 'write-open' ? 'w' : 'r');
|
|
84
|
-
|
|
84
|
+
const file = op === 'export-open' ? await openExportFile(args.root, process.cwd(), args.path) : await open(path, op === 'write-open' ? 'w' : 'r');
|
|
85
|
+
const stat = await file.stat();
|
|
86
|
+
handles.set(args.handle, { file, call, write: op === 'write-open', ...(op === 'export-open' ? { original: { size: stat.size, mtimeMs: stat.mtimeMs, ctimeMs: stat.ctimeMs } } : {}) });
|
|
85
87
|
value =
|
|
86
88
|
op === 'write-open'
|
|
87
89
|
? path
|
|
@@ -105,6 +107,11 @@ export async function runBridge(descriptorPath) {
|
|
|
105
107
|
value = null;
|
|
106
108
|
}
|
|
107
109
|
else if (op === 'read' && !row.write) {
|
|
110
|
+
if (row.original) {
|
|
111
|
+
const stat = await row.file.stat();
|
|
112
|
+
if (!stat.isFile() || stat.nlink !== 1 || stat.size !== row.original.size || stat.mtimeMs !== row.original.mtimeMs || stat.ctimeMs !== row.original.ctimeMs)
|
|
113
|
+
throw Error('EXPORT_SOURCE_CHANGED');
|
|
114
|
+
}
|
|
108
115
|
const buffer = Buffer.alloc(MAX_CHUNK_BYTES);
|
|
109
116
|
const { bytesRead } = await row.file.read(buffer);
|
|
110
117
|
value = bytesRead ? buffer.subarray(0, bytesRead).toString('base64') : null;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ExportRoot } from '../file-delivery/reader.js';
|
|
1
2
|
import type { FileExecutionContext } from '@ours.network/mcp/server';
|
|
2
3
|
import type { ToolRequestExtra } from '@ours.network/mcp/dist/types/mcp/tool.js';
|
|
3
4
|
import { type Wire } from './wire.js';
|
|
@@ -16,6 +17,13 @@ export declare class FileCallbacks implements FileExecutionContext {
|
|
|
16
17
|
body: ReadableStream<Uint8Array<ArrayBufferLike>>;
|
|
17
18
|
close: () => Promise<void>;
|
|
18
19
|
}>;
|
|
20
|
+
readExport(path: string, root: ExportRoot, extra: ToolRequestExtra): Promise<{
|
|
21
|
+
filename: any;
|
|
22
|
+
size: any;
|
|
23
|
+
body: ReadableStream<Uint8Array<ArrayBufferLike>>;
|
|
24
|
+
close: () => Promise<void>;
|
|
25
|
+
}>;
|
|
26
|
+
private openRead;
|
|
19
27
|
write(path: string, body: ReadableStream<Uint8Array>, extra: ToolRequestExtra): Promise<{
|
|
20
28
|
path: string;
|
|
21
29
|
size: number;
|
|
@@ -59,14 +59,24 @@ export class FileCallbacks {
|
|
|
59
59
|
return (await this.rpc(String(extra.requestId), 'probe', { path }, extra.signal)) === true;
|
|
60
60
|
}
|
|
61
61
|
async read(path, extra) {
|
|
62
|
+
return this.openRead(path, extra);
|
|
63
|
+
}
|
|
64
|
+
async readExport(path, root, extra) {
|
|
65
|
+
return this.openRead(path, extra, root);
|
|
66
|
+
}
|
|
67
|
+
async openRead(path, extra, root) {
|
|
62
68
|
const call = String(extra.requestId), handle = randomUUID();
|
|
63
|
-
const meta = await this.rpc(call, 'read-open', { path, handle }, extra.signal);
|
|
69
|
+
const meta = await this.rpc(call, root ? 'export-open' : 'read-open', { path, handle, ...(root ? { root } : {}) }, extra.signal);
|
|
64
70
|
if (!meta ||
|
|
65
71
|
typeof meta.filename !== 'string' ||
|
|
66
72
|
!Number.isSafeInteger(meta.size) ||
|
|
67
73
|
meta.size < 0)
|
|
68
74
|
throw Error('INVALID_FILE_METADATA');
|
|
75
|
+
let closed = false;
|
|
69
76
|
const close = async () => {
|
|
77
|
+
if (closed)
|
|
78
|
+
return;
|
|
79
|
+
closed = true;
|
|
70
80
|
await this.rpc(call, 'close', { handle }, new AbortController().signal).catch(() => { });
|
|
71
81
|
};
|
|
72
82
|
const body = new ReadableStream({
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
2
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { resolveBundledAcpAgent } from '../harness/acp-agent.js';
|
|
3
6
|
/** Do not pass the supervisor's ours connection credentials to harness children. */
|
|
4
7
|
export function managedChildEnvironment(input) {
|
|
5
8
|
const out = {};
|
|
6
9
|
for (const [key, value] of Object.entries(input)) {
|
|
7
|
-
if (value === undefined || (/^OURS_/i.test(key) && !key.startsWith('OURS_FLEET_')))
|
|
10
|
+
if (key === 'FLEET_CURRENT_CHAT_FILES' || value === undefined || (/^OURS_/i.test(key) && !key.startsWith('OURS_FLEET_')))
|
|
8
11
|
continue;
|
|
9
12
|
out[key] = value;
|
|
10
13
|
}
|
|
@@ -13,6 +16,9 @@ export function managedChildEnvironment(input) {
|
|
|
13
16
|
}
|
|
14
17
|
/** Replace ours only; retain the user's ordinary harness configuration and plugins. */
|
|
15
18
|
export function prepareManagedHarness(role, stateDir, cwd, descriptor, env) {
|
|
19
|
+
const child = managedChildEnvironment({ ...process.env, ...env });
|
|
20
|
+
if (role.file_delivery?.enabled)
|
|
21
|
+
qualifyFileHarness(role, cwd, child);
|
|
16
22
|
const bridge = fileURLToPath(new URL('./bridge.js', import.meta.url));
|
|
17
23
|
const server = {
|
|
18
24
|
name: 'ours',
|
|
@@ -37,7 +43,6 @@ export function prepareManagedHarness(role, stateDir, cwd, descriptor, env) {
|
|
|
37
43
|
},
|
|
38
44
|
plugins: { 'ours@ours-codex-marketplace': { enabled: false } },
|
|
39
45
|
};
|
|
40
|
-
const child = managedChildEnvironment({ ...process.env, ...env });
|
|
41
46
|
if (role.harness === 'codex') {
|
|
42
47
|
const config = child.CODEX_CONFIG ? JSON.parse(child.CODEX_CONFIG) : {};
|
|
43
48
|
child.CODEX_CONFIG = JSON.stringify({
|
|
@@ -70,3 +75,42 @@ export function managedClaudeMeta(base) {
|
|
|
70
75
|
},
|
|
71
76
|
};
|
|
72
77
|
}
|
|
78
|
+
/** Bounded stock-adapter support; opt-in never silently erases unrelated tools. */
|
|
79
|
+
function qualifyFileHarness(role, cwd, child) {
|
|
80
|
+
if (process.platform !== 'linux')
|
|
81
|
+
throw Error('CURRENT_CHAT_DELIVERY_UNAVAILABLE: Linux required');
|
|
82
|
+
if (role.session !== 'acp' || role.session_options?.acp?.command)
|
|
83
|
+
throw Error('CURRENT_CHAT_DELIVERY_UNAVAILABLE: stock ACP adapter required');
|
|
84
|
+
if (role.harness === 'claude-code') {
|
|
85
|
+
if (resolveBundledAcpAgent('@agentclientprotocol/claude-agent-acp', 'claude-agent-acp', 'claude-agent-acp').version !== '0.63.0')
|
|
86
|
+
throw Error('CURRENT_CHAT_DELIVERY_UNAVAILABLE: qualified Claude ACP 0.63.0 required');
|
|
87
|
+
const options = role.harness_options ?? {}, servers = options.mcp_servers;
|
|
88
|
+
if (options.mcp_servers_only !== true || !servers || Object.keys(servers).length !== 1 || !servers.ours)
|
|
89
|
+
throw Error('CURRENT_CHAT_DELIVERY_UNAVAILABLE: Claude requires explicit mcp_servers_only and a single ours connector');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (role.harness !== 'codex' || !child.OURS_FLEET_CODEX_ACP_MANIFEST || child.OURS_FLEET_CODEX_DISABLE_INHERITED_MCP === '1')
|
|
93
|
+
throw Error('CURRENT_CHAT_DELIVERY_UNAVAILABLE: managed Codex inventory proxy required');
|
|
94
|
+
const config = JSON.parse(child.CODEX_CONFIG ?? '{}');
|
|
95
|
+
if (config.mcp_servers?.ours?.enabled === false)
|
|
96
|
+
throw Error('CURRENT_CHAT_DELIVERY_UNAVAILABLE: ours MCP disabled');
|
|
97
|
+
// Public inventory sees effective injected config. Before replacing ours, refuse
|
|
98
|
+
// inherited TOML connector configuration, including disabled/project/profile
|
|
99
|
+
// entries. No TOML guessing or override of an operator's explicit disable.
|
|
100
|
+
const home = child.CODEX_HOME ?? join(child.HOME ?? homedir(), '.codex');
|
|
101
|
+
const paths = [join(home, 'config.toml')];
|
|
102
|
+
const profile = role.harness_options?.profile;
|
|
103
|
+
if (typeof profile === 'string')
|
|
104
|
+
paths.push(join(home, profile + '.config.toml'));
|
|
105
|
+
for (let p = resolve(cwd); p !== resolve(child.HOME ?? homedir());) {
|
|
106
|
+
paths.push(join(p, '.codex/config.toml'));
|
|
107
|
+
const next = dirname(p);
|
|
108
|
+
if (next === p)
|
|
109
|
+
break;
|
|
110
|
+
p = next;
|
|
111
|
+
}
|
|
112
|
+
for (const path of new Set(paths))
|
|
113
|
+
if (existsSync(path) && /mcp_servers|mcpServers/.test(readFileSync(path, 'utf8')))
|
|
114
|
+
throw Error('CURRENT_CHAT_DELIVERY_UNAVAILABLE: inherited Codex MCP configuration requires inventory support before replacement');
|
|
115
|
+
child.FLEET_CURRENT_CHAT_FILES = '1';
|
|
116
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import type { FileDeliveryInput, DeliveredFile } from '../file-delivery/types.js';
|
|
2
|
+
import type { ToolRequestExtra } from '@ours.network/mcp/dist/types/mcp/tool.js';
|
|
1
3
|
import type { ApplicationIdentityStore } from '@ours.network/mcp/application-identities';
|
|
2
4
|
import type { OursClient } from '@ours.network/sdk/client';
|
|
5
|
+
import { FileCallbacks } from './file-rpc.js';
|
|
3
6
|
import type { AgentOursRuntime } from './runtime.js';
|
|
4
7
|
export interface EndpointOptions {
|
|
5
8
|
socket: string;
|
|
@@ -9,6 +12,8 @@ export interface EndpointOptions {
|
|
|
9
12
|
client: OursClient;
|
|
10
13
|
identities: ApplicationIdentityStore;
|
|
11
14
|
remoteDaemonFiles: boolean;
|
|
15
|
+
currentChatFileDirectory?: string;
|
|
16
|
+
currentChatFile?: (input: FileDeliveryInput, extra: ToolRequestExtra, files: FileCallbacks) => Promise<DeliveredFile>;
|
|
12
17
|
}
|
|
13
18
|
export declare function startMcpEndpoint(options: EndpointOptions): Promise<{
|
|
14
19
|
close(): Promise<void>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as managedMcp from '@ours.network/mcp/server';
|
|
1
2
|
import { createServer } from 'node:net';
|
|
2
3
|
import { chmod, unlink } from 'node:fs/promises';
|
|
3
4
|
import { timingSafeEqual } from 'node:crypto';
|
|
@@ -6,6 +7,8 @@ import { JSONRPCMessageSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
|
6
7
|
import { FileCallbacks } from './file-rpc.js';
|
|
7
8
|
import { Wire } from './wire.js';
|
|
8
9
|
export async function startMcpEndpoint(options) {
|
|
10
|
+
if (options.currentChatFile && managedMcp.MANAGED_FILE_DELIVERY_VERSION !== 1)
|
|
11
|
+
throw Error('INCOMPATIBLE_MANAGED_FILE_DELIVERY');
|
|
9
12
|
const sockets = new Set();
|
|
10
13
|
const server = createServer((socket) => {
|
|
11
14
|
if (sockets.size >= 4) {
|
|
@@ -33,6 +36,7 @@ export async function startMcpEndpoint(options) {
|
|
|
33
36
|
};
|
|
34
37
|
const mcp = createManagedOursMcpServer(options.client, 'managed-v1', options.identities, {
|
|
35
38
|
remoteDaemonFiles: options.remoteDaemonFiles,
|
|
39
|
+
...(options.currentChatFile ? { currentChatFileDirectory: options.currentChatFileDirectory, currentChatFile: (input, extra) => options.currentChatFile(input, extra, files) } : {}),
|
|
36
40
|
fileContext: files,
|
|
37
41
|
admit: async () => {
|
|
38
42
|
if (!authenticated || inFlight >= 32)
|
|
@@ -10,7 +10,9 @@ export interface AssignedIdentity {
|
|
|
10
10
|
action: string;
|
|
11
11
|
expectedCid?: string;
|
|
12
12
|
allowCreate: boolean;
|
|
13
|
-
|
|
13
|
+
/** Omitted fields preserve an existing identity's custom profile. */
|
|
14
|
+
bio?: string;
|
|
15
|
+
persona?: string;
|
|
14
16
|
}
|
|
15
17
|
export interface RoomAdmission {
|
|
16
18
|
id: string;
|
|
@@ -62,6 +62,7 @@ export class AgentOursRuntime {
|
|
|
62
62
|
actual.isRoot)
|
|
63
63
|
throw Error('IDENTITY_PROOF_MISMATCH');
|
|
64
64
|
this.deps.assertFence();
|
|
65
|
+
return actual;
|
|
65
66
|
}
|
|
66
67
|
async prepare(room, timeoutMs = 30_000) {
|
|
67
68
|
if (['TERMINAL_INTENT', 'CLEANUP_PENDING', 'RELEASED', 'FAILED', 'QUIESCING'].includes(this.state.phase))
|
|
@@ -92,7 +93,7 @@ export class AgentOursRuntime {
|
|
|
92
93
|
this.transition('RECOVERING');
|
|
93
94
|
const args = {
|
|
94
95
|
name: this.state.name,
|
|
95
|
-
bio: this.assignment.bio,
|
|
96
|
+
bio: this.assignment.bio ?? '',
|
|
96
97
|
exposeLocal: false,
|
|
97
98
|
localAutoAccept: true,
|
|
98
99
|
};
|
|
@@ -104,7 +105,18 @@ export class AgentOursRuntime {
|
|
|
104
105
|
this.transition('OWNED', { cid: created.info.cid });
|
|
105
106
|
}
|
|
106
107
|
}
|
|
107
|
-
await this.verify();
|
|
108
|
+
const profile = await this.verify();
|
|
109
|
+
// The supervisor owns profile setup, just like identity binding. Apply only
|
|
110
|
+
// explicitly configured fields, under the verified owner, before publishing
|
|
111
|
+
// readiness or admitting the harness. Repeating a prepare is idempotent.
|
|
112
|
+
if (this.assignment.bio !== undefined && profile.bio !== this.assignment.bio) {
|
|
113
|
+
this.deps.assertFence();
|
|
114
|
+
await this.deps.client.setBio({ bio: this.assignment.bio });
|
|
115
|
+
}
|
|
116
|
+
if (this.assignment.persona !== undefined && profile.persona !== this.assignment.persona) {
|
|
117
|
+
this.deps.assertFence();
|
|
118
|
+
await this.deps.client.setPersona({ persona: this.assignment.persona });
|
|
119
|
+
}
|
|
108
120
|
if (!room && (this.state.room || this.state.admissionIntent))
|
|
109
121
|
throw Error('ROOM_DESCRIPTOR_REQUIRED');
|
|
110
122
|
if (room) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AgentSession } from '../session/types.js';
|
|
1
2
|
import type { ResolvedRole } from '../config.js';
|
|
2
3
|
import { AgentOursRuntime } from './runtime.js';
|
|
3
4
|
export declare const privateRuntimeRoot: () => string;
|
|
@@ -5,6 +6,7 @@ export interface ManagedAgentService {
|
|
|
5
6
|
runtime: AgentOursRuntime;
|
|
6
7
|
descriptor: string;
|
|
7
8
|
privatePaths: string[];
|
|
9
|
+
setFileDelivery?(sender: AgentSession['sendFileToChat']): void;
|
|
8
10
|
close(terminal: boolean): Promise<void>;
|
|
9
11
|
}
|
|
10
12
|
/** Only trusted launch orchestration may write this descriptor, never the child. */
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { pinExportRoot, exportPath, ensureExportDirectory } from '../file-delivery/reader.js';
|
|
2
|
+
import { ArtifactStore } from '../file-delivery/store.js';
|
|
3
|
+
import { resolve, basename, extname } from 'node:path';
|
|
1
4
|
import { socketPath as privateSocketPath } from '../socket-path.js';
|
|
2
5
|
import { randomBytes, randomUUID } from 'node:crypto';
|
|
3
6
|
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
@@ -94,6 +97,8 @@ export async function prepareManagedAgent(role, stateDir, temporary) {
|
|
|
94
97
|
}
|
|
95
98
|
const controller = await RuntimeController.acquire(root, profile.expectedInstanceId, role.identity, instance);
|
|
96
99
|
let closeClient;
|
|
100
|
+
let exportRoot;
|
|
101
|
+
let fileSender;
|
|
97
102
|
let partialEndpoint;
|
|
98
103
|
try {
|
|
99
104
|
let prior = controller.journal.read();
|
|
@@ -146,7 +151,8 @@ export async function prepareManagedAgent(role, stateDir, temporary) {
|
|
|
146
151
|
action: launch?.action ?? prior?.action ?? randomUUID(),
|
|
147
152
|
expectedCid: pin?.cid,
|
|
148
153
|
allowCreate: true,
|
|
149
|
-
bio: role.bio
|
|
154
|
+
bio: role.bio,
|
|
155
|
+
persona: role.persona,
|
|
150
156
|
}, {
|
|
151
157
|
client,
|
|
152
158
|
journal: controller.journal,
|
|
@@ -209,6 +215,18 @@ export async function prepareManagedAgent(role, stateDir, temporary) {
|
|
|
209
215
|
const bridgeDir = join(stateDir, '.ours-bridge');
|
|
210
216
|
mkdirSync(bridgeDir, { recursive: true, mode: 0o700 });
|
|
211
217
|
const capability = randomBytes(32).toString('hex'), socket = privateSocketPath(join(bridgeDir, `g${generation}.sock`));
|
|
218
|
+
const cwd = role.cwd && existsSync(role.cwd) ? resolve(role.cwd) : resolve(stateDir);
|
|
219
|
+
if (role.file_delivery?.enabled) {
|
|
220
|
+
if (role.session !== 'acp' || !['codex', 'claude-code'].includes(role.harness))
|
|
221
|
+
throw Error('FILE_DELIVERY_REQUIRES_CODEX_OR_CLAUDE_ACP');
|
|
222
|
+
const rootPath = resolve(cwd, role.file_delivery.directory);
|
|
223
|
+
const forbidden = [privateRuntimeRoot(), join(stateRoot(), 'private-file-delivery'), profile.configPath, profile.credentialPath];
|
|
224
|
+
if (forbidden.some(p => rootPath === resolve(p) || rootPath.startsWith(resolve(p) + '/') || resolve(p).startsWith(rootPath + '/')))
|
|
225
|
+
throw Error('PRIVATE_STATE_CANNOT_BE_EXPORTED');
|
|
226
|
+
await ensureExportDirectory(cwd, role.file_delivery.directory);
|
|
227
|
+
exportRoot = await pinExportRoot(rootPath);
|
|
228
|
+
}
|
|
229
|
+
const artifactStore = new ArtifactStore(stateDir);
|
|
212
230
|
const endpoint = await startMcpEndpoint({
|
|
213
231
|
socket,
|
|
214
232
|
capability,
|
|
@@ -217,6 +235,26 @@ export async function prepareManagedAgent(role, stateDir, temporary) {
|
|
|
217
235
|
client,
|
|
218
236
|
identities,
|
|
219
237
|
remoteDaemonFiles: true,
|
|
238
|
+
...(exportRoot ? { currentChatFileDirectory: role.file_delivery.directory, currentChatFile: (async (input, extra, files) => {
|
|
239
|
+
if (!fileSender || !exportRoot)
|
|
240
|
+
throw Error('CURRENT_CHAT_DELIVERY_UNAVAILABLE');
|
|
241
|
+
exportPath(exportRoot.root, cwd, input.path); // validate before approval/read
|
|
242
|
+
const name = input.filename ?? basename(input.path);
|
|
243
|
+
if (!name || Buffer.byteLength(name) > 240 || /[\/\\\x00-\x1f]/.test(name) || name === '.' || name === '..')
|
|
244
|
+
throw Error('INVALID_FILENAME');
|
|
245
|
+
const mime = input.mime ?? { '.txt': 'text/plain', '.md': 'text/markdown', '.pdf': 'application/pdf', '.json': 'application/json', '.html': 'text/html', '.png': 'image/png' }[extname(name).toLowerCase()] ?? 'application/octet-stream';
|
|
246
|
+
if (mime.length > 200 || !/^[A-Za-z0-9.+-]+\/[A-Za-z0-9.+-]+$/.test(mime))
|
|
247
|
+
throw Error('INVALID_MIME');
|
|
248
|
+
return fileSender(input, extra.signal, async (binding) => {
|
|
249
|
+
const source = await files.readExport(input.path, exportRoot.root, extra);
|
|
250
|
+
try {
|
|
251
|
+
return await artifactStore.copy(source, name, mime, binding, extra.signal);
|
|
252
|
+
}
|
|
253
|
+
finally {
|
|
254
|
+
await source.close();
|
|
255
|
+
}
|
|
256
|
+
}, role.permissions);
|
|
257
|
+
}) } : {}),
|
|
220
258
|
});
|
|
221
259
|
partialEndpoint = endpoint;
|
|
222
260
|
const descriptor = join(bridgeDir, 'descriptor.json');
|
|
@@ -224,10 +262,13 @@ export async function prepareManagedAgent(role, stateDir, temporary) {
|
|
|
224
262
|
return {
|
|
225
263
|
runtime,
|
|
226
264
|
descriptor,
|
|
227
|
-
privatePaths: [root, profile.configPath, profile.credentialPath],
|
|
265
|
+
privatePaths: [root, profile.configPath, profile.credentialPath, join(stateRoot(), 'private-file-delivery')],
|
|
266
|
+
setFileDelivery: sender => { fileSender = sender; },
|
|
228
267
|
close: async (terminal) => {
|
|
229
268
|
try {
|
|
269
|
+
fileSender = undefined;
|
|
230
270
|
await endpoint.close();
|
|
271
|
+
await exportRoot?.handle.close();
|
|
231
272
|
if (terminal)
|
|
232
273
|
await runtime.terminal();
|
|
233
274
|
else
|
|
@@ -247,6 +288,7 @@ export async function prepareManagedAgent(role, stateDir, temporary) {
|
|
|
247
288
|
catch (error) {
|
|
248
289
|
try {
|
|
249
290
|
await partialEndpoint?.close();
|
|
291
|
+
await exportRoot?.handle.close();
|
|
250
292
|
}
|
|
251
293
|
finally {
|
|
252
294
|
try {
|
|
@@ -23,3 +23,5 @@ export declare function resumedConversationPage(stateDir: string, request: {
|
|
|
23
23
|
after?: string;
|
|
24
24
|
limit?: number;
|
|
25
25
|
}, live: ConversationPageView, expectedSessionId: string): Promise<ConversationPageView>;
|
|
26
|
+
/** Download authorization requires an actual published attachment, never an orphan copy. */
|
|
27
|
+
export declare function conversationHasAttachment(stateDir: string, attachment: import('../file-delivery/types.js').DeliveredFile): Promise<boolean>;
|
|
@@ -2,7 +2,7 @@ import { createReadStream } from 'node:fs';
|
|
|
2
2
|
import { open, readFile, readdir } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { createInterface } from 'node:readline';
|
|
5
|
-
const HISTORICAL_KINDS = ['fleet.task_created', 'prompt.admitted', 'message.chunk', 'message.replace', 'tool.upsert', 'tool.content_chunk', 'turn.completed'];
|
|
5
|
+
const HISTORICAL_KINDS = ['file.attached', 'fleet.task_created', 'prompt.admitted', 'message.chunk', 'message.replace', 'tool.upsert', 'tool.content_chunk', 'turn.completed'];
|
|
6
6
|
/** Whether a ledger event belongs in the conversation: the live generation, or history of the resumed ACP session. */
|
|
7
7
|
export function conversationEventVisible(event, sessionGeneration, resumedSessionId) {
|
|
8
8
|
return event.source !== 'agent_replay' && (event.sessionGeneration === sessionGeneration || (!!resumedSessionId && HISTORICAL_KINDS.includes(event.kind) && event.acpSessionId === resumedSessionId));
|
|
@@ -162,7 +162,7 @@ export async function resumedConversationPage(stateDir, request, live, expectedS
|
|
|
162
162
|
const cursor = Number(event.seq);
|
|
163
163
|
if (!Number.isSafeInteger(cursor) || cursor <= after)
|
|
164
164
|
continue;
|
|
165
|
-
const historical = ['fleet.task_created', 'prompt.admitted', 'message.chunk', 'message.replace', 'tool.upsert', 'tool.content_chunk', 'turn.completed'].includes(event.kind);
|
|
165
|
+
const historical = ['file.attached', 'fleet.task_created', 'prompt.admitted', 'message.chunk', 'message.replace', 'tool.upsert', 'tool.content_chunk', 'turn.completed'].includes(event.kind);
|
|
166
166
|
const visible = event.source !== 'agent_replay' && (event.sessionGeneration === live.snapshot.sessionGeneration || (historical && event.acpSessionId === sessionId));
|
|
167
167
|
if (!visible) {
|
|
168
168
|
next = String(cursor);
|
|
@@ -187,3 +187,26 @@ export async function resumedConversationPage(stateDir, request, live, expectedS
|
|
|
187
187
|
return live;
|
|
188
188
|
return { ...live, events, firstAvailableCursor: first, nextCursor: next, hasMore };
|
|
189
189
|
}
|
|
190
|
+
/** Download authorization requires an actual published attachment, never an orphan copy. */
|
|
191
|
+
export async function conversationHasAttachment(stateDir, attachment) {
|
|
192
|
+
const dir = join(stateDir, '.conversation');
|
|
193
|
+
const names = (await readdir(dir)).filter(n => /^events-\d+\.jsonl$/.test(n)).sort().reverse();
|
|
194
|
+
let found = false;
|
|
195
|
+
for (const name of names) {
|
|
196
|
+
await readLinesBackward(join(dir, name), line => {
|
|
197
|
+
try {
|
|
198
|
+
const e = JSON.parse(line);
|
|
199
|
+
const a = e.payload.attachment;
|
|
200
|
+
if (e.kind === 'file.attached' && e.source === 'agent' && a?.id === attachment.id
|
|
201
|
+
&& e.sessionGeneration === attachment.sessionGeneration && e.acpSessionId === attachment.acpSessionId
|
|
202
|
+
&& e.turnId === attachment.turnId && JSON.stringify(a) === JSON.stringify(attachment))
|
|
203
|
+
found = true;
|
|
204
|
+
}
|
|
205
|
+
catch { /* unreadable lines cannot authorize a download */ }
|
|
206
|
+
return !found;
|
|
207
|
+
});
|
|
208
|
+
if (found)
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
return found;
|
|
212
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { socketPath } from '../socket-path.js';
|
|
1
2
|
import { createHash } from 'node:crypto';
|
|
2
3
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
4
|
import { join } from 'node:path';
|
|
@@ -35,7 +36,8 @@ export class SupervisorOursTools {
|
|
|
35
36
|
const identity = legacy
|
|
36
37
|
? legacySupervisorIdentity(role, identityName, selected.temporary, descriptor.generation)
|
|
37
38
|
: { name: identityName, cid: descriptor.cid, generation: descriptor.generation };
|
|
38
|
-
|
|
39
|
+
const legacySocket = join(selected.dir, '.ours-bridge', `g${descriptor.generation}.sock`);
|
|
40
|
+
if (legacy && descriptor.socket !== legacySocket && descriptor.socket !== socketPath(legacySocket))
|
|
39
41
|
throw new FleetError('capability_unavailable', 'legacy supervisor socket does not match the selected agent');
|
|
40
42
|
if ((!legacy && (descriptor.role !== role || descriptor.identity !== identityName))
|
|
41
43
|
|| !/^[a-f0-9]{64}$/i.test(identity.cid ?? '') || !Number.isSafeInteger(descriptor.generation) || descriptor.generation < 1)
|
package/dist/briefing.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export interface BriefingOpts {
|
|
|
8
8
|
briefingBody?: string;
|
|
9
9
|
/**
|
|
10
10
|
* What spawn established about a persistent role's ours identity.
|
|
11
|
-
*
|
|
11
|
+
* The supervisor provisions temporary identities as well.
|
|
12
12
|
* Defaults to `unverified`, because a briefing generated without that
|
|
13
13
|
* knowledge must not claim one.
|
|
14
14
|
*/
|
package/dist/briefing.js
CHANGED
|
@@ -19,6 +19,38 @@ function adminConsoleAuthority(session) {
|
|
|
19
19
|
];
|
|
20
20
|
return [];
|
|
21
21
|
}
|
|
22
|
+
/** Common guidance for Fleet-owned boot and subsequent wakes; tools stay available. */
|
|
23
|
+
function startupGuidance(v) {
|
|
24
|
+
return [
|
|
25
|
+
'', '## Readiness first and task-driven wakes',
|
|
26
|
+
'On a fresh launch or restart, announce readiness with a short final console response before any tools',
|
|
27
|
+
'or file reads. End that readiness turn; the supervisor delivers configured work in a separate turn.',
|
|
28
|
+
'If readiness was already announced, do not repeat it. An idle agent waits for a concrete task or wake',
|
|
29
|
+
'without reading routines, worklog, inbox, history, or unrelated files. A charter or bio alone is not a task.',
|
|
30
|
+
'For a concrete task or mail wake, read this briefing once before acting. If a curated briefing has no',
|
|
31
|
+
'concrete assignment, stop after that read and await work rather than exploring other files.',
|
|
32
|
+
'For assigned work, read any existing routines once. On a continuation task after restart or resume,',
|
|
33
|
+
'also read the active worklog once to recover unfinished work. Never read a worklog for fresh initialization,',
|
|
34
|
+
'including a fresh temporary agent. Within a wake, reuse details already read; reread only when a file',
|
|
35
|
+
'changed or you need a specific missing detail.',
|
|
36
|
+
'Fleet owns identity binding, readiness, required room admission, and application of the configured Role',
|
|
37
|
+
'bio/persona before the model starts. Accept that launch context; no model-side profile setup is needed.',
|
|
38
|
+
'Avoid startup identity/profile checks, broad tool discovery, CLI help/config exploration, or searching',
|
|
39
|
+
'for your own chat merely to confirm launch. Discover only the tools needed for the next task action.',
|
|
40
|
+
'Identity, profile, and management APIs remain available for an explicit task or a verified failure;',
|
|
41
|
+
'follow the recovery/escalation instructions for a real mismatch rather than hiding it.',
|
|
42
|
+
'The supervisor delivers initial unread mail and subsequent arrivals as mail wakes after readiness.',
|
|
43
|
+
'Do not perform a model-owned inbox poll during startup, even when no task was preassigned.',
|
|
44
|
+
`On a mail wake, use **${v.getMessagesTool}** once to read that arrival and its authenticated provenance.`,
|
|
45
|
+
'Read another batch only if unread messages remain, a new arrival is signalled, or a concrete task',
|
|
46
|
+
'requires a fresh inbox check. Use history only for needed context or pagination, not a startup ritual.',
|
|
47
|
+
'Read routines for concrete work in each later wake, then act on that work. Do not reread the worklog',
|
|
48
|
+
'within the same wake by habit. Supervisor wakes replace a separate model-owned monitor.',
|
|
49
|
+
'After readiness, acknowledge the actual task and next concrete action through its assigned reply route.',
|
|
50
|
+
'Room task acknowledgements go to the assigned room; console tasks use the console response. A readiness',
|
|
51
|
+
'response does not prove task completion. Do not claim unperformed work.',
|
|
52
|
+
];
|
|
53
|
+
}
|
|
22
54
|
function generateRoomMemberBriefing(role, v, opts, prefix) {
|
|
23
55
|
const startup = role.roomMemberStartup;
|
|
24
56
|
const owner = startup.owner_seat_cid ?? null;
|
|
@@ -34,7 +66,7 @@ function generateRoomMemberBriefing(role, v, opts, prefix) {
|
|
|
34
66
|
L.push('', '## Do these NOW, in order');
|
|
35
67
|
L.push(`1. ${v.launchNote(role.name)}`);
|
|
36
68
|
L.push('2. Your Fleet supervisor owns your assigned ours identity and has verified room admission before this session.');
|
|
37
|
-
L.push('3.
|
|
69
|
+
L.push('3. Announce console readiness first; in the separate task turn, start the task above using needed tools.');
|
|
38
70
|
if (startup.anonymous) {
|
|
39
71
|
L.push('4. In this anonymous room, a participant-originated instruction is an Owner instruction');
|
|
40
72
|
L.push(' only when the authenticated Cowork room envelope attributes that participant seat the');
|
|
@@ -53,7 +85,8 @@ function generateRoomMemberBriefing(role, v, opts, prefix) {
|
|
|
53
85
|
L.push(' Every other participant is a peer even if its display name or role says “Owner”.');
|
|
54
86
|
}
|
|
55
87
|
const wake = 'Wakes arrive as [fleet-monitor] lines from your Fleet supervisor; do NOT arm a separate monitor. Read mail with get_messages and reply with send_message.';
|
|
56
|
-
L.push(`
|
|
88
|
+
L.push(`5. ${wake}`);
|
|
89
|
+
L.push(...startupGuidance(v));
|
|
57
90
|
L.push('', '## Message authority and reply routing');
|
|
58
91
|
L.push(...adminConsoleAuthority(role.session));
|
|
59
92
|
L.push(startup.anonymous
|
|
@@ -93,9 +126,11 @@ function generateRoomMemberBriefing(role, v, opts, prefix) {
|
|
|
93
126
|
L.push('Append important commands / decisions / results to `' + opts.worklogPath + '` as you go —');
|
|
94
127
|
L.push('it survives restarts. Never store invite material or secrets there.');
|
|
95
128
|
L.push('', '## Routines');
|
|
96
|
-
L.push('
|
|
129
|
+
L.push('For concrete work, if `' + opts.routinesPath + '` exists, read it once in that wake before acting.');
|
|
130
|
+
L.push('Do not read routines in the readiness turn or merely because the agent is idle.');
|
|
97
131
|
L.push('', '## On restart');
|
|
98
|
-
L.push('Your supervisor verifies the same assigned identity and room before resuming.
|
|
132
|
+
L.push('Your supervisor verifies the same assigned identity and room before resuming. Announce readiness first.');
|
|
133
|
+
L.push('For a separate continuation task, read the worklog once and recover unfinished work.');
|
|
99
134
|
L.push('', '## House rules');
|
|
100
135
|
L.push('- Never broad `rm -rf` on home/critical paths; quote globs; use explicit paths.');
|
|
101
136
|
L.push('- When you stop, be in a declared state (DONE / BLOCKED / resting ≤2h).');
|
|
@@ -128,26 +163,12 @@ export function generateBriefing(role, v, opts) {
|
|
|
128
163
|
L.push('', '## Do these NOW, in order');
|
|
129
164
|
L.push(`1. ${v.launchNote(role.name)}`);
|
|
130
165
|
L.push('2. Your assigned ours identity is owned and verified by the Fleet supervisor before this session starts.');
|
|
131
|
-
L.push(
|
|
132
|
-
L.push(
|
|
133
|
-
if (opts.briefingBody !== undefined) {
|
|
134
|
-
L.push('4. The curated briefing did not declare a profile source. Do not infer one or mutate');
|
|
135
|
-
L.push(' bio/persona from arbitrary headings.');
|
|
136
|
-
L.push('5. Continue with the curated operating instructions without a profile write.');
|
|
137
|
-
}
|
|
138
|
-
else {
|
|
139
|
-
const profileSource = role.persona ? 'Charter' : 'Mission';
|
|
140
|
-
L.push(`4. PUBLISH your public **bio** via **${v.setBioTool}**`);
|
|
141
|
-
L.push(role.bio
|
|
142
|
-
? ' with the **Bio** section above, verbatim. Skip the call if it already matches.'
|
|
143
|
-
: ` with a 1–2 sentence summary of your ${profileSource} above. Skip if it already matches.`);
|
|
144
|
-
L.push(`5. SET your **persona** (local operating contract, never shared in invites) via`);
|
|
145
|
-
L.push(` **${v.setPersonaTool}** with the **${profileSource}** section above, verbatim. Skip if it matches.`);
|
|
146
|
-
}
|
|
166
|
+
L.push('3. Announce console readiness first; in the separate task turn, start assigned work and acknowledge its next action.');
|
|
167
|
+
L.push(...startupGuidance(v));
|
|
147
168
|
// When the supervisor owns the monitor (monitor.mode=fleet), the agent must NOT arm
|
|
148
169
|
// its own in-session watch — wakes are injected as [fleet-monitor] lines.
|
|
149
170
|
const wakeNote = 'Wakes arrive as [fleet-monitor] lines from your Fleet supervisor; do NOT arm a separate monitor. Read mail with get_messages and reply with send_message.';
|
|
150
|
-
L.push(
|
|
171
|
+
L.push(wakeNote);
|
|
151
172
|
if (role.owner_channel || managedSession(role)) {
|
|
152
173
|
L.push('', '## Message authority and reply routing');
|
|
153
174
|
}
|
|
@@ -209,13 +230,13 @@ export function generateBriefing(role, v, opts) {
|
|
|
209
230
|
L.push(...(opts.managedCli ?? []));
|
|
210
231
|
}
|
|
211
232
|
if (role.coordinator) {
|
|
212
|
-
L.push(`
|
|
213
|
-
L.push(
|
|
214
|
-
L.push(`
|
|
233
|
+
L.push(`For an assigned task, ANNOUNCE yourself: call **${v.sendTool}** to contact "${role.coordinator}".`);
|
|
234
|
+
L.push('After console readiness, name the actual task and your next action. An idle launch needs no coordinator call.');
|
|
235
|
+
L.push(`Await messages. When the monitor wakes you (or the owner requests a manual check),`);
|
|
215
236
|
L.push(` call **${v.getMessagesTool}**, act, and reply.`);
|
|
216
237
|
}
|
|
217
238
|
else {
|
|
218
|
-
L.push(`
|
|
239
|
+
L.push(`Await messages. When the monitor wakes you (or the owner requests a manual check),`);
|
|
219
240
|
L.push(` call **${v.getMessagesTool}**, act on them,`);
|
|
220
241
|
L.push(` and reply with ${v.sendTool}. No coordinator is configured — the owner drives you`);
|
|
221
242
|
L.push(` via \`ours-fleet attach ${role.name}\` or by messaging "${id}".`);
|
|
@@ -261,12 +282,14 @@ export function generateBriefing(role, v, opts) {
|
|
|
261
282
|
'Continue writing only WORKLOG.md.');
|
|
262
283
|
}
|
|
263
284
|
L.push('', '## Routines');
|
|
264
|
-
L.push(`
|
|
265
|
-
L.push('on
|
|
285
|
+
L.push(`For concrete work, if \`${opts.routinesPath}\` exists, read it once in that wake before acting`);
|
|
286
|
+
L.push('on that work and follow it for recurring or scheduled tasks. Do not read it in the readiness turn');
|
|
287
|
+
L.push('or merely because the agent is idle. It may');
|
|
266
288
|
L.push('change between wakes without a restart; treat the file, not your memory of it, as current.');
|
|
267
289
|
L.push('', '## On restart (you run under a supervised launcher)');
|
|
268
|
-
L.push('The supervisor verifies your identity and room before resuming.
|
|
269
|
-
L.push(
|
|
290
|
+
L.push('The supervisor verifies your identity and room before resuming. Announce readiness first.');
|
|
291
|
+
L.push('For a separate continuation task, read your worklog once and recover unfinished work.');
|
|
292
|
+
L.push(wakeNote);
|
|
270
293
|
L.push('Do not blindly re-run whatever may have crashed you.');
|
|
271
294
|
L.push('', '## House rules');
|
|
272
295
|
L.push('- Never broad `rm -rf` on home/critical paths; quote globs; use explicit paths.');
|
package/dist/build-info.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.2.0-nightly.
|
|
3
|
-
"buildId": "
|
|
4
|
-
"commit": "
|
|
2
|
+
"version": "1.2.0-nightly.54",
|
|
3
|
+
"buildId": "b10c893ceda0",
|
|
4
|
+
"commit": "4da7d4782c1caf686836806bc8606fc50887edc5",
|
|
5
5
|
"dirty": true,
|
|
6
|
-
"builtAt": "2026-10-
|
|
6
|
+
"builtAt": "2026-10-08T10:22:35.148Z",
|
|
7
7
|
"capabilities": [
|
|
8
8
|
"managed-cli.codex-reasoning-effort-v1",
|
|
9
9
|
"managed-cli.setup-v1",
|
package/dist/config.d.ts
CHANGED
|
@@ -42,6 +42,11 @@ export type MonitorInterrupt = boolean | 'after_tool';
|
|
|
42
42
|
*/
|
|
43
43
|
export declare const MANAGED_CLI_WORKFLOW_IDS: readonly ["task-workflow"];
|
|
44
44
|
export type ManagedCliWorkflowId = (typeof MANAGED_CLI_WORKFLOW_IDS)[number];
|
|
45
|
+
export interface FileDeliveryConfig {
|
|
46
|
+
enabled: boolean;
|
|
47
|
+
directory: string;
|
|
48
|
+
}
|
|
49
|
+
export declare function validateFileDelivery(value: unknown): FileDeliveryConfig | undefined;
|
|
45
50
|
export interface CommonPermissions {
|
|
46
51
|
approval: ApprovalMode;
|
|
47
52
|
filesystem: FilesystemMode;
|
|
@@ -154,6 +159,8 @@ export interface RoleConfig {
|
|
|
154
159
|
auth_proxy?: Partial<AuthProxyConfig>;
|
|
155
160
|
/** Fleet-prepared CLI workflows this role may run outside its command sandbox. */
|
|
156
161
|
managed_cli?: ManagedCliWorkflowId[];
|
|
162
|
+
/** Explicit per-Agent export opt-in; not inherited from manifest defaults. */
|
|
163
|
+
file_delivery?: FileDeliveryConfig;
|
|
157
164
|
}
|
|
158
165
|
export type AgentSelection<T extends Record<string, unknown> = Record<string, unknown>> = {
|
|
159
166
|
ref: string;
|
|
@@ -177,6 +184,8 @@ export interface AgentDefinition {
|
|
|
177
184
|
auth_proxy?: Partial<AuthProxyConfig>;
|
|
178
185
|
/** Fleet-prepared CLI workflows this Agent may run outside its command sandbox. */
|
|
179
186
|
managed_cli?: ManagedCliWorkflowId[];
|
|
187
|
+
/** Explicit per-Agent export opt-in; not inherited from manifest defaults. */
|
|
188
|
+
file_delivery?: FileDeliveryConfig;
|
|
180
189
|
/** Scheduled turns scoped only to temporary launches; persistent Agents reject this field. */
|
|
181
190
|
loops?: AgentLoopsConfig;
|
|
182
191
|
}
|