@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.
Files changed (52) hide show
  1. package/README.md +2 -0
  2. package/dist/agent-ours/bridge.js +10 -3
  3. package/dist/agent-ours/file-rpc.d.ts +8 -0
  4. package/dist/agent-ours/file-rpc.js +11 -1
  5. package/dist/agent-ours/harness.js +47 -3
  6. package/dist/agent-ours/mcp-endpoint.d.ts +5 -0
  7. package/dist/agent-ours/mcp-endpoint.js +4 -0
  8. package/dist/agent-ours/runtime.d.ts +3 -1
  9. package/dist/agent-ours/runtime.js +14 -2
  10. package/dist/agent-ours/service.d.ts +2 -0
  11. package/dist/agent-ours/service.js +44 -2
  12. package/dist/application/conversation-history.d.ts +2 -0
  13. package/dist/application/conversation-history.js +25 -2
  14. package/dist/application/supervisor-ours-tools.js +3 -1
  15. package/dist/briefing.d.ts +1 -1
  16. package/dist/briefing.js +52 -29
  17. package/dist/build-info.json +4 -4
  18. package/dist/config.d.ts +9 -0
  19. package/dist/config.js +13 -2
  20. package/dist/file-delivery/reader.d.ts +17 -0
  21. package/dist/file-delivery/reader.js +94 -0
  22. package/dist/file-delivery/store.d.ts +15 -0
  23. package/dist/file-delivery/store.js +124 -0
  24. package/dist/file-delivery/types.d.ts +19 -0
  25. package/dist/file-delivery/types.js +1 -0
  26. package/dist/harness/claude-code.js +11 -10
  27. package/dist/harness/codex-app-server-proxy.js +6 -2
  28. package/dist/harness/codex-file-inventory.d.ts +15 -0
  29. package/dist/harness/codex-file-inventory.js +113 -0
  30. package/dist/harness/codex.js +9 -5
  31. package/dist/isolation/policy.js +4 -1
  32. package/dist/monitor.d.ts +7 -0
  33. package/dist/monitor.js +65 -1
  34. package/dist/rooms-tasks/cli.js +12 -2
  35. package/dist/rooms-tasks/cowork-http.js +29 -1
  36. package/dist/rooms-tasks/erasure.js +5 -3
  37. package/dist/rooms-tasks/workspace-artifacts.js +1 -1
  38. package/dist/rooms-tasks/workspace.d.ts +8 -1
  39. package/dist/rooms-tasks/workspace.js +168 -43
  40. package/dist/runner.js +32 -9
  41. package/dist/session/acp.d.ts +4 -0
  42. package/dist/session/acp.js +61 -8
  43. package/dist/session/conversation-types.d.ts +4 -2
  44. package/dist/session/types.d.ts +1 -0
  45. package/dist/startup-prompt.d.ts +4 -0
  46. package/dist/startup-prompt.js +36 -0
  47. package/dist/startup-telemetry.d.ts +16 -0
  48. package/dist/startup-telemetry.js +46 -0
  49. package/dist/web/server.js +24 -0
  50. package/docs/acp-files.md +102 -0
  51. package/docs/task-workspaces.md +31 -0
  52. 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
- 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)
@@ -10,7 +10,9 @@ export interface AssignedIdentity {
10
10
  action: string;
11
11
  expectedCid?: string;
12
12
  allowCreate: boolean;
13
- bio: string;
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
- 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)
@@ -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
- * Temporary roles always create their own session-owned identity.
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. Start the task above using the available messaging, file and history tools.');
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(`6. ${wake}`);
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('If `' + opts.routinesPath + '` exists, re-read it at the START of every wake before acting.');
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. Read the worklog and continue.');
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(`3. RECONCILE your profile (idempotent): call **${v.currentIdentityTool}** and read your`);
132
- L.push(' current bio and persona, so you only write below when they actually differ.');
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(`6. ${wakeNote}`);
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(`7. ANNOUNCE yourself: call **${v.sendTool}** to contact "${role.coordinator}" with text:`);
213
- L.push(` "${role.name} online — identity '${id}' bound, ready."`);
214
- L.push(`8. Await messages. When the monitor wakes you (or the owner requests a manual check),`);
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(`7. Await messages. When the monitor wakes you (or the owner requests a manual check),`);
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(`If \`${opts.routinesPath}\` exists, re-read it at the START of every wake — before acting`);
265
- L.push('on messages, timers, or prompts — and follow it for recurring or scheduled work. It may');
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. Continue from your worklog.');
269
- L.push(`${wakeNote} Then continue from your WORKLOG.`);
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.');
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "1.2.0-nightly.52",
3
- "buildId": "46344889ffd4",
4
- "commit": "b7d24b24d1a5618370afb64d9d2d1f5e50a39c6f",
2
+ "version": "1.2.0-nightly.54",
3
+ "buildId": "b10c893ceda0",
4
+ "commit": "4da7d4782c1caf686836806bc8606fc50887edc5",
5
5
  "dirty": true,
6
- "builtAt": "2026-10-07T14:59:54.103Z",
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
  }