@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/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
+ }>;
@@ -0,0 +1,124 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { constants, mkdirSync, readFileSync, readdirSync, lstatSync, openSync, fstatSync, closeSync, renameSync, writeFileSync } from 'node:fs';
3
+ import { open, unlink } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+ import { stateRoot } from '../paths.js';
6
+ import { openDirectory, MAX_FILE_BYTES } from './reader.js';
7
+ export function artifactDirectory(stateDir) {
8
+ const incarnation = readFileSync(join(stateDir, '.session-id'), 'utf8').trim();
9
+ if (!/^[a-f0-9-]{36}$/.test(incarnation))
10
+ throw Error('INVALID_ROLE_INCARNATION');
11
+ const key = createHash('sha256').update(JSON.stringify([stateDir, incarnation])).digest('hex');
12
+ return join(stateRoot(), 'private-file-delivery', key);
13
+ }
14
+ function privateDirectory(path) {
15
+ mkdirSync(path, { recursive: true, mode: 0o700 });
16
+ const stat = lstatSync(path);
17
+ if (!stat.isDirectory() || stat.isSymbolicLink() || stat.uid !== process.getuid?.() || stat.mode & 0o077)
18
+ throw Error('INVALID_ARTIFACT_DIRECTORY');
19
+ }
20
+ export class ArtifactStore {
21
+ stateDir;
22
+ constructor(stateDir) {
23
+ this.stateDir = stateDir;
24
+ }
25
+ async copy(source, name, mimeType, binding, signal) {
26
+ if (!Number.isSafeInteger(source.size) || source.size < 0 || source.size > MAX_FILE_BYTES)
27
+ throw Error('FILE_EXCEEDS_20_MIB');
28
+ const path = artifactDirectory(this.stateDir);
29
+ privateDirectory(join(stateRoot(), 'private-file-delivery'));
30
+ privateDirectory(path);
31
+ const root = await openDirectory(path);
32
+ const id = randomUUID(), base = `/proc/self/fd/${root.fd}/`, partial = base + id + '.part';
33
+ let file;
34
+ const reader = source.body.getReader();
35
+ try {
36
+ // Orphans count toward the same bounded quota. They are never republished.
37
+ const entries = readdirSync(base).filter(p => /\.(data|part)$/.test(p));
38
+ const bytes = entries.reduce((sum, p) => sum + lstatSync(base + p).size, 0);
39
+ if (entries.length >= 100 || bytes + source.size > 200 * 1024 * 1024)
40
+ throw Error('ARTIFACT_QUOTA_EXCEEDED');
41
+ file = await open(partial, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
42
+ const hash = createHash('sha256');
43
+ let size = 0;
44
+ for (;;) {
45
+ signal.throwIfAborted();
46
+ const chunk = await reader.read();
47
+ if (chunk.done)
48
+ break;
49
+ size += chunk.value.length;
50
+ if (size > source.size || size > MAX_FILE_BYTES)
51
+ throw Error('SOURCE_SIZE_CHANGED');
52
+ hash.update(chunk.value);
53
+ let offset = 0;
54
+ while (offset < chunk.value.length) {
55
+ const { bytesWritten } = await file.write(chunk.value, offset, chunk.value.length - offset);
56
+ if (!bytesWritten)
57
+ throw Error('SHORT_ARTIFACT_WRITE');
58
+ offset += bytesWritten;
59
+ }
60
+ }
61
+ if (size !== source.size)
62
+ throw Error('SOURCE_SIZE_CHANGED');
63
+ signal.throwIfAborted();
64
+ await file.chmod(0o400);
65
+ await file.sync();
66
+ await file.close();
67
+ file = undefined;
68
+ const record = { id, name, mimeType, size, sha256: hash.digest('hex'), ...binding };
69
+ renameSync(partial, base + id + '.data');
70
+ writeFileSync(base + id + '.json', JSON.stringify(record), { flag: 'wx', mode: 0o400, flush: true });
71
+ // No pending-send record or recovery: a later failure leaves only unreferenced stored bytes.
72
+ await root.sync();
73
+ // The newly created incarnation directory and store root must also survive
74
+ // a crash before a durable conversation event can reference this file.
75
+ for (const parent of [join(stateRoot(), 'private-file-delivery'), stateRoot()]) {
76
+ const handle = await openDirectory(parent);
77
+ try {
78
+ await handle.sync();
79
+ }
80
+ finally {
81
+ await handle.close();
82
+ }
83
+ }
84
+ return record;
85
+ }
86
+ finally {
87
+ await file?.close();
88
+ await reader.cancel().catch(() => { });
89
+ await source.close();
90
+ await unlink(partial).catch(() => { });
91
+ await root.close();
92
+ }
93
+ }
94
+ }
95
+ export async function readDeliveredFile(stateDir, id) {
96
+ if (!/^[a-f0-9-]{36}$/.test(id))
97
+ throw Error('ARTIFACT_NOT_FOUND');
98
+ const root = await openDirectory(artifactDirectory(stateDir));
99
+ const read = (suffix, max) => {
100
+ const fd = openSync(`/proc/self/fd/${root.fd}/${id}.${suffix}`, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
101
+ try {
102
+ const stat = fstatSync(fd);
103
+ if (!stat.isFile() || stat.nlink !== 1 || stat.uid !== process.getuid?.() || stat.size > max)
104
+ throw Error('INVALID_STORED_ARTIFACT');
105
+ const bytes = readFileSync(fd);
106
+ if (bytes.length > max)
107
+ throw Error('ARTIFACT_TOO_LARGE');
108
+ return bytes;
109
+ }
110
+ finally {
111
+ closeSync(fd);
112
+ }
113
+ };
114
+ try {
115
+ const record = JSON.parse(read('json', 4096).toString());
116
+ const bytes = read('data', MAX_FILE_BYTES);
117
+ if (record.id !== id || record.size !== bytes.length || record.sha256 !== createHash('sha256').update(bytes).digest('hex') || typeof record.name !== 'string')
118
+ throw Error('ARTIFACT_INTEGRITY_FAILED');
119
+ return { record, bytes };
120
+ }
121
+ finally {
122
+ await root.close();
123
+ }
124
+ }
@@ -0,0 +1,19 @@
1
+ export interface FileDeliveryBinding {
2
+ sessionGeneration: string;
3
+ acpSessionId: string;
4
+ turnId: string;
5
+ }
6
+ export interface DeliveredFile extends FileDeliveryBinding {
7
+ id: string;
8
+ name: string;
9
+ mimeType: string;
10
+ size: number;
11
+ sha256: string;
12
+ }
13
+ export interface FileDeliveryInput {
14
+ path: string;
15
+ filename?: string;
16
+ mime?: string;
17
+ }
18
+ export type CopyChatFile = (binding: FileDeliveryBinding) => Promise<DeliveredFile>;
19
+ export type SendChatFile = (input: FileDeliveryInput, signal: AbortSignal, copy: CopyChatFile) => Promise<DeliveredFile>;
@@ -0,0 +1 @@
1
+ export {};
@@ -385,16 +385,17 @@ export function makeClaudeCodeAdapter(exec = realExec, transport) {
385
385
  supervisedWakeNote: () => 'Your mail wake-ups are delivered by the fleet supervisor directly into this console as ' +
386
386
  '`[fleet-monitor]` lines — do NOT arm an in-session Monitor. When such a line appears, run ' +
387
387
  '**get_messages** to drain the mail.',
388
- launchNote: name => `You were launched with \`--remote-control ${name}\`. Confirm you are running.`,
389
- restartPrompt: (id, worklog, role) => `Session restarted. Re-bind your ours identity now (choose_identity name "${id}" force=true), ` +
390
- (role?.monitor?.mode === 'fleet'
391
- ? 'then continue from '
392
- : `then ${armMonitor(id)}, then continue from `) +
393
- `${worklog}. Do not re-run whatever crashed you.` +
394
- (role?.monitor?.mode === 'fleet'
395
- ? ' Your mail wakes arrive as `[fleet-monitor]` console lines from the supervisor — ' +
396
- 'do NOT arm an in-session Monitor.'
397
- : ''),
388
+ launchNote: name => `You were launched as Fleet role \`${name}\` under a Claude session. Announce console readiness before task details.`,
389
+ restartPrompt: (id, worklog, role) => role?.monitor?.mode === 'fleet'
390
+ ? `Session restarted. The supervisor has verified assigned identity "${id}"; ` +
391
+ 'accept that readiness and continue without rebinding or profile reconciliation. ' +
392
+ 'Your mail wakes arrive as `[fleet-monitor]` console lines from the supervisor — ' +
393
+ 'do NOT arm an in-session Monitor. ' +
394
+ 'Announce console readiness first without tools, then end that readiness turn. ' +
395
+ `For a separate continuation task, read ${worklog} once for continuity and reuse it in that wake. ` +
396
+ 'Do not re-run whatever crashed you.'
397
+ : `Session restarted. Re-bind your ours identity now (choose_identity name "${id}" force=true), ` +
398
+ `then ${armMonitor(id)}, then continue from ${worklog}. Do not re-run whatever crashed you.`,
398
399
  },
399
400
  exitPolicy: { cleanExitIsFresh: true, fastFailSecs: 20 },
400
401
  };
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { CodexFileInventory } from './codex-file-inventory.js';
2
3
  import { spawn } from 'node:child_process';
3
4
  import { createRequire } from 'node:module';
4
5
  import { createInterface } from 'node:readline';
@@ -446,6 +447,7 @@ export function runCodexAppServerProxy() {
446
447
  });
447
448
  }
448
449
  };
450
+ const inventory = process.env.FLEET_CURRENT_CHAT_FILES === '1' ? new CodexFileInventory(writeToChild, writeToClient) : undefined;
449
451
  const recovery = new CodexTerminalRecovery({
450
452
  sendToCodex: writeToChild,
451
453
  emitToClient: writeToClient,
@@ -456,6 +458,7 @@ export function runCodexAppServerProxy() {
456
458
  return;
457
459
  finished = true;
458
460
  recovery.close();
461
+ inventory?.close();
459
462
  input.close();
460
463
  process.stdin.destroy();
461
464
  // stdout/stderr are often pipes under Fleet and tests. Setting exitCode
@@ -488,10 +491,11 @@ export function runCodexAppServerProxy() {
488
491
  return;
489
492
  const rewritten = rewriteCodexAppServerRequest(line, approval, expectedSandbox, disableInheritedMcp, managedOursTools);
490
493
  recovery.observeClientLine(rewritten);
491
- writeToChild(rewritten);
494
+ if (!inventory || inventory.observeClientLine(rewritten))
495
+ writeToChild(rewritten);
492
496
  });
493
497
  output.on('line', line => {
494
- if (!finished && recovery.observeServerLine(line))
498
+ if (!finished && (!inventory || inventory.observeServerLine(line)) && recovery.observeServerLine(line))
495
499
  writeToClient(line);
496
500
  });
497
501
  input.once('close', () => {
@@ -0,0 +1,15 @@
1
+ export declare class CodexFileInventory {
2
+ private readonly sendToCodex;
3
+ private readonly emitToClient;
4
+ private checks;
5
+ private sequence;
6
+ constructor(sendToCodex: (line: string) => void, emitToClient: (line: string) => void);
7
+ /** Hold only a turn/start; unrelated tools/configuration are preserved. */
8
+ observeClientLine(line: string): boolean;
9
+ observeServerLine(line: string): boolean;
10
+ close(): void;
11
+ private query;
12
+ private qualified;
13
+ private finish;
14
+ private reject;
15
+ }
@@ -0,0 +1,113 @@
1
+ const object = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
2
+ const parse = (line) => { try {
3
+ const v = JSON.parse(line);
4
+ return object(v) ? v : undefined;
5
+ }
6
+ catch {
7
+ return undefined;
8
+ } };
9
+ export class CodexFileInventory {
10
+ sendToCodex;
11
+ emitToClient;
12
+ checks = new Map();
13
+ sequence = 0;
14
+ constructor(sendToCodex, emitToClient) {
15
+ this.sendToCodex = sendToCodex;
16
+ this.emitToClient = emitToClient;
17
+ }
18
+ /** Hold only a turn/start; unrelated tools/configuration are preserved. */
19
+ observeClientLine(line) {
20
+ const message = parse(line);
21
+ if (message?.method !== 'turn/start')
22
+ return true;
23
+ const threadId = object(message.params) ? message.params.threadId : undefined;
24
+ if (typeof threadId !== 'string' || !threadId || message.id === undefined || this.checks.size >= 8) {
25
+ this.reject(message);
26
+ return false;
27
+ }
28
+ const check = { request: message, threadId, servers: [], cursors: new Set(), timer: setTimeout(() => {
29
+ for (const [id, c] of this.checks)
30
+ if (c === check)
31
+ this.checks.delete(id);
32
+ this.reject(message);
33
+ }, 30_000) };
34
+ this.query(check);
35
+ return false;
36
+ }
37
+ observeServerLine(line) {
38
+ const message = parse(line), id = message?.id;
39
+ if (typeof id !== 'string')
40
+ return true;
41
+ const check = this.checks.get(id);
42
+ if (!check)
43
+ return true;
44
+ this.checks.delete(id);
45
+ const result = message?.result;
46
+ if (!object(result) || !Array.isArray(result.data) || result.data.some(s => !object(s)) || check.servers.length + result.data.length > 1000) {
47
+ this.finish(check, false);
48
+ return false;
49
+ }
50
+ check.servers.push(...result.data);
51
+ if (result.nextCursor !== null) {
52
+ const cursor = result.nextCursor;
53
+ if (typeof cursor !== 'string' || !cursor || check.cursors.has(cursor) || check.cursors.size >= 10)
54
+ this.finish(check, false);
55
+ else {
56
+ check.cursors.add(cursor);
57
+ this.query(check, cursor);
58
+ }
59
+ return false;
60
+ }
61
+ this.finish(check, this.qualified(check.servers));
62
+ return false;
63
+ }
64
+ close() { for (const c of this.checks.values())
65
+ clearTimeout(c.timer); this.checks.clear(); }
66
+ query(check, cursor) {
67
+ const id = `ours-fleet-file-inventory-${++this.sequence}`;
68
+ this.checks.set(id, check);
69
+ this.sendToCodex(JSON.stringify({ id, method: 'mcpServerStatus/list', params: { threadId: check.threadId, detail: 'full', limit: 100, ...(cursor ? { cursor } : {}) } }));
70
+ }
71
+ qualified(servers) {
72
+ let managed = 0;
73
+ for (const server of servers) {
74
+ // Unknown or failed discovery cannot establish absence of another connector.
75
+ if (server.toolsError !== null || !object(server.tools))
76
+ return false;
77
+ if (!object(server.serverInfo) || typeof server.serverInfo.name !== 'string')
78
+ return false;
79
+ const tools = Object.values(server.tools).filter(object);
80
+ const names = new Set(tools.map(t => t.name));
81
+ if (server.name !== 'ours') {
82
+ if (server.serverInfo.name === 'ours')
83
+ return false;
84
+ if (tools.some(t => t.name === 'send_file' && object(t.inputSchema) && object(t.inputSchema.properties) && 'contact' in t.inputSchema.properties))
85
+ return false;
86
+ if (names.has('current_identity') && names.has('send_file') && names.has('get_messages'))
87
+ return false;
88
+ continue;
89
+ }
90
+ managed++;
91
+ if (server.serverInfo.name !== 'ours' || server.runtimeStatus !== 'connected')
92
+ return false;
93
+ const files = tools.filter(t => t.name === 'send_file');
94
+ if (files.length !== 1 || !object(files[0].inputSchema))
95
+ return false;
96
+ const schema = files[0].inputSchema;
97
+ if (!object(schema.properties) || !object(schema.properties.contact) || schema.additionalProperties !== false
98
+ || (Array.isArray(schema.required) && schema.required.includes('contact')))
99
+ return false;
100
+ }
101
+ return managed === 1;
102
+ }
103
+ finish(check, allowed) {
104
+ clearTimeout(check.timer);
105
+ if (allowed)
106
+ this.sendToCodex(JSON.stringify(check.request));
107
+ else
108
+ this.reject(check.request);
109
+ }
110
+ reject(request) {
111
+ this.emitToClient(JSON.stringify({ id: request.id, error: { code: -32000, message: 'CURRENT_CHAT_DELIVERY_UNAVAILABLE: unqualified MCP inventory' } }));
112
+ }
113
+ }
@@ -262,6 +262,7 @@ function codexAcpEnvironment(role, dirs) {
262
262
  if (!source)
263
263
  throw new Error('Codex app-server proxy is missing; rebuild ours-fleet');
264
264
  writeFileSync(proxyModule, readFileSync(source, 'utf8'), { mode: 0o600 });
265
+ writeFileSync(join(runtimeDir, 'codex-file-inventory.js'), readFileSync(join(dirname(source), 'codex-file-inventory.js'), 'utf8'), { mode: 0o600 });
265
266
  const windows = process.platform === 'win32';
266
267
  const command = join(runtimeDir, windows ? 'codex-app-server-proxy.cmd' : 'codex-app-server-proxy');
267
268
  const script = windows
@@ -482,7 +483,7 @@ export function makeCodexAdapter(exec = realExec, transport, nativeTransport) {
482
483
  catch (error) {
483
484
  throw new Error(`Codex ACP runtime check failed: ${error.message}; check CODEX_PATH or reinstall Fleet with optional dependencies`);
484
485
  }
485
- const minimum = role.model === 'gpt-6-astra' || resolution.version === '1.10.0'
486
+ const minimum = role.file_delivery?.enabled ? '0.159.0' : role.model === 'gpt-6-astra' || resolution.version === '1.10.0'
486
487
  ? '0.153.3' : '0.145.0';
487
488
  if (!codexVersionAtLeast(runtime.version, minimum))
488
489
  throw new Error(`${role.model ?? 'Codex ACP'} requires a newer Codex (>=${minimum}): ACP selects ${runtime.executable} (${runtime.version}); upgrade Fleet or set CODEX_PATH to a supported executable in the agent/service environment`);
@@ -621,12 +622,15 @@ export function makeCodexAdapter(exec = realExec, transport, nativeTransport) {
621
622
  supervisedWakeNote: () => 'Your mail wake-ups are delivered by the fleet supervisor directly into this console as ' +
622
623
  '`[fleet-monitor]` lines — do NOT arm arm_monitor or foreground_monitor. When such a line ' +
623
624
  'appears, call **get_messages**, handle the mail, and reply with send_message.',
624
- launchNote: name => `You were launched as the fleet role \`${name}\` under a Codex session. Confirm you are running.`,
625
+ launchNote: name => `You were launched as the fleet role \`${name}\` under a Codex session. Announce console readiness before task details.`,
625
626
  restartPrompt: (id, worklog, configuredRole) => {
626
627
  if (configuredRole?.monitor?.mode === 'fleet')
627
- return `Session restarted. Re-bind your ours identity now (choose_identity name "${id}" force=true); ` +
628
- 'your mail wakes are delivered by the fleet supervisor as `[fleet-monitor]` console lines, so do ' +
629
- `NOT arm arm_monitor/foreground_monitor. Continue from ${worklog}. Do not re-run whatever crashed you.`;
628
+ return `Session restarted. The supervisor has verified assigned identity "${id}"; ` +
629
+ 'accept that readiness and continue without rebinding or profile reconciliation. ' +
630
+ 'Mail wakes arrive as `[fleet-monitor]` lines; do NOT arm arm_monitor/foreground_monitor. ' +
631
+ 'Announce console readiness first without tools, then end that readiness turn. ' +
632
+ `For a separate continuation task, read ${worklog} once for continuity and reuse it in that wake. ` +
633
+ 'Do not re-run whatever crashed you.';
630
634
  const consented = configuredRole?.harness_options?.monitor === true;
631
635
  return `Session restarted. Re-bind your ours identity now (choose_identity name "${id}" force=true), ` +
632
636
  (consented
@@ -1,3 +1,4 @@
1
+ import { stateRoot } from '../paths.js';
1
2
  import { realpathSync } from 'node:fs';
2
3
  import { basename, dirname, join, resolve, sep } from 'node:path';
3
4
  import { BACKENDS, ON_UNAVAILABLE, NETWORK_MODES, } from './types.js';
@@ -57,7 +58,7 @@ const SYSTEM_RO = ['/usr', '/bin', '/sbin', '/lib', '/lib64', '/etc'];
57
58
  /** Ephemeral scratch mounts. */
58
59
  const scratchTmpfs = (home) => ['/tmp', join(home, '.cache')];
59
60
  /** Home-relative sensitive paths never exposed (the blocklist's teeth). */
60
- const SENSITIVE_HOME = ['.ssh', '.aws', '.docker', '.gnupg', '.ours', 'fleet.yaml', 'fleet.d'];
61
+ const SENSITIVE_HOME = ['.ssh', '.aws', '.docker', '.gnupg', '.ours', '.ours-fleet/private-ours', '.ours-fleet/private-file-delivery', 'fleet.yaml', 'fleet.d'];
61
62
  const ISOLATION_KEYS = ['backend', 'on_unavailable', 'fs', 'network', 'allow_hosts', 'resources', 'secrets'];
62
63
  const FS_KEYS = ['read', 'write'];
63
64
  const RESOURCE_KEYS = ['cpu', 'mem', 'pids'];
@@ -189,6 +190,8 @@ export function resolveIsolation(cfg, ctx) {
189
190
  const agentsRoot = dirname(stateDir);
190
191
  const blocklist = [
191
192
  ...SENSITIVE_HOME.map(p => join(home, p)),
193
+ join(stateRoot(), 'private-ours'),
194
+ join(stateRoot(), 'private-file-delivery'),
192
195
  agentsRoot, // sibling agents' state dirs (this agent's own is explicitly mounted)
193
196
  ];
194
197
  // ENFORCE the list, before anything builds a backend argv. Until now it was
package/dist/monitor.d.ts CHANGED
@@ -28,6 +28,10 @@ export type FetchLike = (url: string, init?: {
28
28
  signal?: AbortSignal;
29
29
  }) => Promise<FetchResponse>;
30
30
  export interface MonitorPageClient {
31
+ /** Body-free snapshot; never consumes messages or changes read status. */
32
+ unread(): Promise<{
33
+ identities: Array<Record<string, unknown>>;
34
+ }>;
31
35
  readNotificationPage(identity: string, options?: {
32
36
  since?: number | 'tip';
33
37
  signal?: AbortSignal;
@@ -179,6 +183,7 @@ export declare class Monitor {
179
183
  private stopped;
180
184
  private bootDeadline;
181
185
  private currentAbort;
186
+ private initialBacklogPending;
182
187
  private apiErrorStreak;
183
188
  private readonly turnFailThreshold;
184
189
  /** Active degradations, keyed by cause. Empty means armed. */
@@ -198,6 +203,7 @@ export declare class Monitor {
198
203
  /** Gather stragglers arriving within batch_ms so a burst lands as one line. */
199
204
  private coalesce;
200
205
  private deliver;
206
+ private deliverLine;
201
207
  /** Update the consecutive-API-error streak and derive `.monitor-status` from it. */
202
208
  private recordTurn;
203
209
  /**
@@ -210,6 +216,7 @@ export declare class Monitor {
210
216
  */
211
217
  private doFetch;
212
218
  private profileClient;
219
+ private readInitialUnread;
213
220
  private disposeProfileClient;
214
221
  private advance;
215
222
  private persistCursor;