@ours.network/fleet 1.2.0-nightly.53 → 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 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
- handles.set(args.handle, { file, call, write: op === 'write-open' });
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)
@@ -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();
@@ -210,6 +215,18 @@ export async function prepareManagedAgent(role, stateDir, temporary) {
210
215
  const bridgeDir = join(stateDir, '.ours-bridge');
211
216
  mkdirSync(bridgeDir, { recursive: true, mode: 0o700 });
212
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);
213
230
  const endpoint = await startMcpEndpoint({
214
231
  socket,
215
232
  capability,
@@ -218,6 +235,26 @@ export async function prepareManagedAgent(role, stateDir, temporary) {
218
235
  client,
219
236
  identities,
220
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
+ }) } : {}),
221
258
  });
222
259
  partialEndpoint = endpoint;
223
260
  const descriptor = join(bridgeDir, 'descriptor.json');
@@ -225,10 +262,13 @@ export async function prepareManagedAgent(role, stateDir, temporary) {
225
262
  return {
226
263
  runtime,
227
264
  descriptor,
228
- privatePaths: [root, profile.configPath, profile.credentialPath],
265
+ privatePaths: [root, profile.configPath, profile.credentialPath, join(stateRoot(), 'private-file-delivery')],
266
+ setFileDelivery: sender => { fileSender = sender; },
229
267
  close: async (terminal) => {
230
268
  try {
269
+ fileSender = undefined;
231
270
  await endpoint.close();
271
+ await exportRoot?.handle.close();
232
272
  if (terminal)
233
273
  await runtime.terminal();
234
274
  else
@@ -248,6 +288,7 @@ export async function prepareManagedAgent(role, stateDir, temporary) {
248
288
  catch (error) {
249
289
  try {
250
290
  await partialEndpoint?.close();
291
+ await exportRoot?.handle.close();
251
292
  }
252
293
  finally {
253
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
- if (legacy && descriptor.socket !== join(selected.dir, '.ours-bridge', `g${descriptor.generation}.sock`))
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)
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "1.2.0-nightly.53",
3
- "buildId": "bf5ca35d699f",
4
- "commit": "cdf3842ad7869bd41fc843ff3e71859f4ef2f0f0",
2
+ "version": "1.2.0-nightly.54",
3
+ "buildId": "b10c893ceda0",
4
+ "commit": "4da7d4782c1caf686836806bc8606fc50887edc5",
5
5
  "dirty": true,
6
- "builtAt": "2026-10-07T22:04:37.157Z",
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
  }
package/dist/config.js CHANGED
@@ -29,6 +29,15 @@ export const NOTIFY_EVENT_TYPES = [
29
29
  * Agent; never inferred from a role's name or persona, and never inherited.
30
30
  */
31
31
  export const MANAGED_CLI_WORKFLOW_IDS = ['task-workflow'];
32
+ export function validateFileDelivery(value) {
33
+ if (value === undefined)
34
+ return undefined;
35
+ if (!isPlainObject(value) || Object.keys(value).some(k => !['enabled', 'directory'].includes(k))
36
+ || typeof value.enabled !== 'boolean' || typeof value.directory !== 'string'
37
+ || !value.directory || value.directory.split('/').some(p => !p || p.startsWith('.') || /[\\\x00-\x1f]/.test(p)))
38
+ throw new ConfigError('file_delivery requires enabled:boolean and a relative non-hidden directory');
39
+ return value;
40
+ }
32
41
  /** Default wake sources when a role does not list its own. */
33
42
  export const DEFAULT_WAKE_SOURCES = ['message_received', 'file_received', 'local_contact_request', 'pending_message'];
34
43
  const MONITOR_KEYS = [
@@ -144,7 +153,7 @@ export const ROLE_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
144
153
  const ROLE_KEYS = [
145
154
  'harness', 'session', 'session_options', 'permissions', 'identity', 'cwd', 'coordinator', 'mission', 'persona', 'bio',
146
155
  'briefing_file', 'model', 'effort', 'model_chain', 'max_tokens', 'autocompact_pct', 'env', 'oversee', 'harness_options',
147
- 'isolation', 'monitor', 'owner_channel', 'worklog', 'auth_proxy', 'managed_cli',
156
+ 'isolation', 'monitor', 'owner_channel', 'worklog', 'auth_proxy', 'managed_cli', 'file_delivery',
148
157
  ];
149
158
  export const ROLE_PRESET_KEYS = ['mission', 'persona', 'bio', 'briefing_file'];
150
159
  export const BRAIN_PRESET_KEYS = [
@@ -153,7 +162,7 @@ export const BRAIN_PRESET_KEYS = [
153
162
  ];
154
163
  export const AGENT_KEYS = [
155
164
  'role', 'brain', 'permissions', 'identity', 'cwd', 'coordinator', 'env', 'oversee',
156
- 'isolation', 'monitor', 'owner_channel', 'worklog', 'auth_proxy', 'loops', 'managed_cli',
165
+ 'isolation', 'monitor', 'owner_channel', 'worklog', 'auth_proxy', 'loops', 'managed_cli', 'file_delivery',
157
166
  ];
158
167
  const AGENT_INSTANCE_KEYS = ['template', 'overrides'];
159
168
  const TEMPLATE_FORBIDDEN_KEYS = ['identity'];
@@ -199,6 +208,7 @@ export function validateBrainValue(value, file, pointer) {
199
208
  }
200
209
  /** Pure launch-boundary validation for a fully resolved temporary Agent definition. */
201
210
  export function validateEffectiveAgentTemplate(definition, id = 'room-member') {
211
+ validateFileDelivery(definition.file_delivery);
202
212
  const value = structuredClone(definition);
203
213
  const bad = Object.keys(value).filter(key => !AGENT_KEYS.includes(key));
204
214
  if (bad.length)
@@ -646,6 +656,7 @@ export function loadConfig(configPath, options = {}) {
646
656
  const bad = Object.keys(r).filter(k => !ROLE_KEYS.includes(k));
647
657
  if (bad.length)
648
658
  throw new ConfigError(`${file}: role '${name}' has unknown key(s) ${bad.join(', ')}; allowed: ${ROLE_KEYS.join(', ')}`);
659
+ validateFileDelivery(r.file_delivery);
649
660
  const isolation = r.isolation ?? defaults.isolation;
650
661
  const session = resolveSession(r.session ?? defaults.session, file, name);
651
662
  const sessionOptions = resolveSessionOptions(defaults.session_options, r.session_options, session, file, name);
@@ -0,0 +1,17 @@
1
+ import { type FileHandle } from 'node:fs/promises';
2
+ export declare const MAX_FILE_BYTES: number;
3
+ export interface ExportRoot {
4
+ path: string;
5
+ dev: number;
6
+ ino: number;
7
+ }
8
+ /** Pin every component; neither trusted root ancestors nor agent components follow symlinks. */
9
+ export declare function openDirectory(path: string): Promise<FileHandle>;
10
+ export declare function pinExportRoot(path: string): Promise<{
11
+ root: ExportRoot;
12
+ handle: FileHandle;
13
+ }>;
14
+ export declare function exportPath(root: ExportRoot, cwd: string, input: string): string[];
15
+ /** Runs in the harness bridge, never in the supervisor's OS context. */
16
+ export declare function openExportFile(root: ExportRoot, cwd: string, input: string): Promise<FileHandle>;
17
+ export declare function ensureExportDirectory(cwd: string, directory: string): Promise<void>;
@@ -0,0 +1,94 @@
1
+ import { constants } from 'node:fs';
2
+ import { mkdir, open } from 'node:fs/promises';
3
+ import { isAbsolute, relative, resolve } from 'node:path';
4
+ export const MAX_FILE_BYTES = 20 * 1024 * 1024;
5
+ const FLAGS = constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK;
6
+ /** Pin every component; neither trusted root ancestors nor agent components follow symlinks. */
7
+ export async function openDirectory(path) {
8
+ if (process.platform !== 'linux' || !isAbsolute(path) || path !== resolve(path))
9
+ throw Error('FILE_DELIVERY_REQUIRES_LINUX_ROOT');
10
+ let dir = await open('/', FLAGS | constants.O_DIRECTORY);
11
+ try {
12
+ for (const part of path.split('/').filter(Boolean)) {
13
+ const next = await open(`/proc/self/fd/${dir.fd}/${part}`, FLAGS | constants.O_DIRECTORY);
14
+ await dir.close();
15
+ dir = next;
16
+ }
17
+ return dir;
18
+ }
19
+ catch (error) {
20
+ await dir.close();
21
+ throw error;
22
+ }
23
+ }
24
+ export async function pinExportRoot(path) {
25
+ const handle = await openDirectory(path);
26
+ try {
27
+ const stat = await handle.stat();
28
+ if (stat.uid !== process.getuid?.() || (stat.mode & 0o022))
29
+ throw Error('EXPORT_ROOT_NOT_PRIVATE_OWNED');
30
+ return { root: { path, dev: stat.dev, ino: stat.ino }, handle };
31
+ }
32
+ catch (error) {
33
+ await handle.close();
34
+ throw error;
35
+ }
36
+ }
37
+ export function exportPath(root, cwd, input) {
38
+ if (!input || input.length > 4096 || /[\\\x00-\x1f]/.test(input) || input.split('/').some(p => p === '.' || p === '..' || !p) && !isAbsolute(input))
39
+ throw Error('INVALID_EXPORT_PATH');
40
+ if (input.split('/').some(p => p === '..' || p === '.'))
41
+ throw Error('INVALID_EXPORT_PATH');
42
+ const path = relative(root.path, resolve(cwd, input));
43
+ const parts = path.split('/');
44
+ if (!path || isAbsolute(path) || parts.some(p => !p || p === '..' || p === '.' || p.startsWith('.') || /^(credentials?|secrets?|tokens?)(\.|$)/i.test(p)))
45
+ throw Error('OUTSIDE_EXPORT_DIRECTORY');
46
+ return parts;
47
+ }
48
+ /** Runs in the harness bridge, never in the supervisor's OS context. */
49
+ export async function openExportFile(root, cwd, input) {
50
+ const parts = exportPath(root, cwd, input);
51
+ let dir = await openDirectory(root.path);
52
+ try {
53
+ const stat = await dir.stat();
54
+ if (stat.dev !== root.dev || stat.ino !== root.ino)
55
+ throw Error('EXPORT_ROOT_CHANGED');
56
+ for (const part of parts.slice(0, -1)) {
57
+ const next = await open(`/proc/self/fd/${dir.fd}/${part}`, FLAGS | constants.O_DIRECTORY);
58
+ await dir.close();
59
+ dir = next;
60
+ }
61
+ const file = await open(`/proc/self/fd/${dir.fd}/${parts.at(-1)}`, FLAGS);
62
+ try {
63
+ const stat = await file.stat();
64
+ if (!stat.isFile() || stat.uid !== process.getuid?.() || stat.nlink !== 1 || stat.size > MAX_FILE_BYTES)
65
+ throw Error('INVALID_EXPORT_FILE');
66
+ return file;
67
+ }
68
+ catch (error) {
69
+ await file.close();
70
+ throw error;
71
+ }
72
+ }
73
+ finally {
74
+ await dir.close();
75
+ }
76
+ }
77
+ export async function ensureExportDirectory(cwd, directory) {
78
+ let dir = await openDirectory(cwd);
79
+ try {
80
+ for (const part of directory.split('/')) {
81
+ if (!part || part.startsWith('.') || /[\\\x00-\x1f]/.test(part))
82
+ throw Error('INVALID_EXPORT_DIRECTORY');
83
+ const path = `/proc/self/fd/${dir.fd}/${part}`;
84
+ await mkdir(path, { mode: 0o700 }).catch(error => { if (error.code !== 'EEXIST')
85
+ throw error; });
86
+ const next = await open(path, FLAGS | constants.O_DIRECTORY);
87
+ await dir.close();
88
+ dir = next;
89
+ }
90
+ }
91
+ finally {
92
+ await dir.close();
93
+ }
94
+ }
@@ -0,0 +1,15 @@
1
+ import type { DeliveredFile, FileDeliveryBinding } from './types.js';
2
+ export declare function artifactDirectory(stateDir: string): string;
3
+ export declare class ArtifactStore {
4
+ private readonly stateDir;
5
+ constructor(stateDir: string);
6
+ copy(source: {
7
+ size: number;
8
+ body: ReadableStream<Uint8Array>;
9
+ close(): Promise<void>;
10
+ }, name: string, mimeType: string, binding: FileDeliveryBinding, signal: AbortSignal): Promise<DeliveredFile>;
11
+ }
12
+ export declare function readDeliveredFile(stateDir: string, id: string): Promise<{
13
+ record: DeliveredFile;
14
+ bytes: Buffer;
15
+ }>;