@ours.network/fleet 1.2.0-nightly.53 → 1.2.0-nightly.55

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 (108) hide show
  1. package/README.md +22 -12
  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/service.d.ts +6 -1
  9. package/dist/agent-ours/service.js +73 -8
  10. package/dist/application/conversation-history.d.ts +2 -0
  11. package/dist/application/conversation-history.js +25 -2
  12. package/dist/application/log-service.js +8 -8
  13. package/dist/application/role-creation-service.js +2 -1
  14. package/dist/application/role-removal-service.js +7 -1
  15. package/dist/application/role-repository.js +17 -0
  16. package/dist/application/supervisor-ours-tools.js +3 -1
  17. package/dist/application/task-layouts.js +12 -2
  18. package/dist/application/task-room-service.js +19 -17
  19. package/dist/application/types.d.ts +2 -0
  20. package/dist/build-info.json +4 -4
  21. package/dist/cli.js +31 -3
  22. package/dist/config.d.ts +11 -0
  23. package/dist/config.js +13 -2
  24. package/dist/file-delivery/reader.d.ts +17 -0
  25. package/dist/file-delivery/reader.js +94 -0
  26. package/dist/file-delivery/store.d.ts +15 -0
  27. package/dist/file-delivery/store.js +124 -0
  28. package/dist/file-delivery/types.d.ts +19 -0
  29. package/dist/file-delivery/types.js +1 -0
  30. package/dist/harness/agent-session.d.ts +2 -0
  31. package/dist/harness/claude-code-session.js +1 -1
  32. package/dist/harness/codex-app-server-proxy.js +6 -2
  33. package/dist/harness/codex-file-inventory.d.ts +15 -0
  34. package/dist/harness/codex-file-inventory.js +113 -0
  35. package/dist/harness/codex-session.js +2 -2
  36. package/dist/harness/codex.js +2 -1
  37. package/dist/isolation/policy.js +4 -1
  38. package/dist/managed-recovery.d.ts +13 -0
  39. package/dist/managed-recovery.js +31 -0
  40. package/dist/ops.d.ts +2 -0
  41. package/dist/ops.js +15 -2
  42. package/dist/rooms-tasks/cli.js +21 -0
  43. package/dist/rooms-tasks/close.d.ts +2 -0
  44. package/dist/rooms-tasks/close.js +66 -1
  45. package/dist/rooms-tasks/deletion.js +10 -0
  46. package/dist/rooms-tasks/erasure.js +4 -2
  47. package/dist/rooms-tasks/layout-member-retirement.d.ts +10 -0
  48. package/dist/rooms-tasks/layout-member-retirement.js +90 -0
  49. package/dist/rooms-tasks/layout-service.d.ts +1 -0
  50. package/dist/rooms-tasks/layout-service.js +31 -13
  51. package/dist/rooms-tasks/layout.d.ts +7 -0
  52. package/dist/rooms-tasks/layout.js +35 -0
  53. package/dist/rooms-tasks/legacy-task-member.d.ts +19 -0
  54. package/dist/rooms-tasks/legacy-task-member.js +116 -0
  55. package/dist/rooms-tasks/member-ownership.d.ts +5 -0
  56. package/dist/rooms-tasks/member-ownership.js +40 -0
  57. package/dist/rooms-tasks/provision.js +26 -1
  58. package/dist/rooms-tasks/recovery.d.ts +19 -0
  59. package/dist/rooms-tasks/recovery.js +110 -0
  60. package/dist/rooms-tasks/task-service-retirement.d.ts +11 -0
  61. package/dist/rooms-tasks/task-service-retirement.js +21 -0
  62. package/dist/rooms-tasks/task-state.d.ts +1 -0
  63. package/dist/rooms-tasks/task-state.js +16 -1
  64. package/dist/rooms-tasks/types.d.ts +3 -0
  65. package/dist/rooms-tasks/workspace.d.ts +2 -0
  66. package/dist/rooms-tasks/workspace.js +5 -0
  67. package/dist/runner.d.ts +6 -0
  68. package/dist/runner.js +161 -22
  69. package/dist/session/acp.d.ts +5 -0
  70. package/dist/session/acp.js +68 -8
  71. package/dist/session/arbiter.d.ts +5 -0
  72. package/dist/session/arbiter.js +16 -2
  73. package/dist/session/codex-app-server.d.ts +1 -0
  74. package/dist/session/codex-app-server.js +5 -0
  75. package/dist/session/conversation-types.d.ts +4 -2
  76. package/dist/session/types.d.ts +3 -0
  77. package/dist/spawn.d.ts +5 -1
  78. package/dist/spawn.js +32 -3
  79. package/dist/startup-readiness.d.ts +1 -1
  80. package/dist/startup-readiness.js +3 -3
  81. package/dist/supervisor/adoption.d.ts +8 -0
  82. package/dist/supervisor/adoption.js +78 -0
  83. package/dist/supervisor/catalog.d.ts +62 -0
  84. package/dist/supervisor/catalog.js +254 -0
  85. package/dist/supervisor/fleet.d.ts +8 -0
  86. package/dist/supervisor/fleet.js +89 -0
  87. package/dist/supervisor/index.js +3 -6
  88. package/dist/supervisor/launchd.d.ts +1 -1
  89. package/dist/supervisor/launchd.js +24 -12
  90. package/dist/supervisor/legacy.d.ts +35 -0
  91. package/dist/supervisor/legacy.js +524 -0
  92. package/dist/supervisor/manager.d.ts +15 -0
  93. package/dist/supervisor/manager.js +258 -0
  94. package/dist/supervisor/scope.d.ts +4 -0
  95. package/dist/supervisor/scope.js +10 -0
  96. package/dist/supervisor/systemd.d.ts +2 -2
  97. package/dist/supervisor/systemd.js +14 -14
  98. package/dist/task-supervision.d.ts +9 -0
  99. package/dist/task-supervision.js +145 -0
  100. package/dist/temp-lifecycle.d.ts +17 -7
  101. package/dist/temp-lifecycle.js +85 -53
  102. package/dist/temp-supervisor-recovery.d.ts +1 -1
  103. package/dist/temp-supervisor-recovery.js +2 -2
  104. package/dist/web/server.js +24 -0
  105. package/docs/acp-files.md +102 -0
  106. package/docs/fleet-migration.md +54 -0
  107. package/docs/task-workspaces.md +26 -0
  108. package/package.json +7 -3
@@ -16,6 +16,9 @@ import { launchFleetWorker } from '../rooms-tasks/external-worker.js';
16
16
  import { layoutSupervisorId } from '../rooms-tasks/layout-control.js';
17
17
  import { layoutRunPath, RoomLayoutService } from '../rooms-tasks/layout-service.js';
18
18
  import { FleetError } from './errors.js';
19
+ import { ownedLayoutRetirementSeats } from '../rooms-tasks/layout-member-retirement.js';
20
+ import { eraseMemberArtifacts } from '../rooms-tasks/erasure.js';
21
+ import { assertTaskRegistrationsAbsent } from '../supervisor/catalog.js';
19
22
  /** Hash of the authorable definition only (never the source file path). */
20
23
  export function layoutDefinitionHash(definition) {
21
24
  const { version, description, participants, rooms } = definition;
@@ -238,8 +241,15 @@ export class TaskLayouts {
238
241
  const link = task.layout;
239
242
  if (!link)
240
243
  return;
241
- if (this.runExists(link))
242
- await this.service().open(link.run_id, true).close(layoutSupervisorId());
244
+ if (this.runExists(link)) {
245
+ const engine = this.service().open(link.run_id, true);
246
+ await engine.delete(layoutSupervisorId());
247
+ const state = engine.snapshot();
248
+ const rooms = Object.values(state.rooms).flatMap(room => room.native ? [room.native.room_id] : []);
249
+ assertTaskRegistrationsAbsent(task.task_id);
250
+ await eraseMemberArtifacts('task', task.task_id, ownedLayoutRetirementSeats(state, link.run_id), rooms);
251
+ }
252
+ assertTaskRegistrationsAbsent(task.task_id);
243
253
  await this.withTaskLock(link.run_id, async () => {
244
254
  for (const path of [layoutRunPath(link.run_id), this.provenancePath(link.run_id), taskBriefPath(task)])
245
255
  rmSync(path, { force: true });
@@ -378,23 +378,25 @@ export class TaskRoomApplicationService {
378
378
  const ready = task.state === 'active' && room?.state === 'active'
379
379
  && active === expected && launched === expected;
380
380
  const blocker = task.outcome?.summary ?? task.blocked?.reason ?? room?.saga.error;
381
- const nextAction = task.terminal_intent
382
- ? `Complete the accepted ${task.terminal_intent.kind} operation; do not restart provisioning.`
383
- : room?.state === 'closing' || room?.state === 'closed'
384
- ? `Complete room cleanup; do not restart provisioning.`
385
- : room?.provisioning_detail === 'member_failed'
386
- ? `Inspect the failed launch, then run ours-fleet task start ${task.task_id}.`
387
- : room?.provisioning_detail === 'waiting_owner_authorization'
388
- ? `Ensure ours-cowork 1.3.0 or newer is running and available, then run ours-fleet task start ${task.task_id}.`
389
- : room?.provisioning_detail === 'waiting_owner_invite'
390
- ? `Rotate rooms.owner.public_invite, then run ours-fleet task start ${task.task_id}.`
391
- : room?.provisioning_detail === 'owner_cid_mismatch'
392
- ? `Verify rooms.owner.expected_cid, rotate the Owner invite, then run ours-fleet task start ${task.task_id}.`
393
- : room?.provisioning_detail === 'waiting_cowork'
394
- ? `Restore ours-cowork, then run ours-fleet task start ${task.task_id}.`
395
- : failed
396
- ? `Correct the blocker, then run ours-fleet task start ${task.task_id}.`
397
- : undefined;
381
+ const nextAction = blocker === 'FLEET_SERVICE_NOT_INSTALLED: run ours-fleet init'
382
+ ? `Run ours-fleet init, then retry ours-fleet task start ${task.task_id}.`
383
+ : task.terminal_intent
384
+ ? `Complete the accepted ${task.terminal_intent.kind} operation; do not restart provisioning.`
385
+ : room?.state === 'closing' || room?.state === 'closed'
386
+ ? `Complete room cleanup; do not restart provisioning.`
387
+ : room?.provisioning_detail === 'member_failed'
388
+ ? `Inspect the failed launch, then run ours-fleet task start ${task.task_id}.`
389
+ : room?.provisioning_detail === 'waiting_owner_authorization'
390
+ ? `Ensure ours-cowork 1.3.0 or newer is running and available, then run ours-fleet task start ${task.task_id}.`
391
+ : room?.provisioning_detail === 'waiting_owner_invite'
392
+ ? `Rotate rooms.owner.public_invite, then run ours-fleet task start ${task.task_id}.`
393
+ : room?.provisioning_detail === 'owner_cid_mismatch'
394
+ ? `Verify rooms.owner.expected_cid, rotate the Owner invite, then run ours-fleet task start ${task.task_id}.`
395
+ : room?.provisioning_detail === 'waiting_cowork'
396
+ ? `Restore ours-cowork, then run ours-fleet task start ${task.task_id}.`
397
+ : failed
398
+ ? `Correct the blocker, then run ours-fleet task start ${task.task_id}.`
399
+ : undefined;
398
400
  return {
399
401
  kind: failed ? 'failed' : ready ? 'ready' : 'in_progress', task, room,
400
402
  launch: {
@@ -26,6 +26,8 @@ export interface RoleRecord {
26
26
  id: string;
27
27
  lifetime: RoleLifetime;
28
28
  configured: boolean;
29
+ /** Read-only display ownership; never authorizes lifecycle operations. */
30
+ taskId?: string;
29
31
  config?: ResolvedRoleView;
30
32
  stateRef?: {
31
33
  lifetime: 'permanent' | 'temporary';
@@ -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.55",
3
+ "buildId": "6e4199ab6d73",
4
+ "commit": "a66362bc99a9896700192f5ff2c8454a0e209766",
5
5
  "dirty": true,
6
- "builtAt": "2026-10-07T22:04:37.157Z",
6
+ "builtAt": "2026-10-09T12:16:42.572Z",
7
7
  "capabilities": [
8
8
  "managed-cli.codex-reasoning-effort-v1",
9
9
  "managed-cli.setup-v1",
package/dist/cli.js CHANGED
@@ -617,7 +617,7 @@ cOpt(program.command('status <name>').description('declared persistent Agent uni
617
617
  + `${ledger.lastTermination.runStartedAt ?? 'unknown'}) died without an orderly exit; `
618
618
  + `observed ${ledger.lastTermination.observedAt}`
619
619
  + `\n abrupt terminations recorded: ${ledger.abruptTerminations ?? 1}`
620
- + `\n cause (signal / OOM-kill): journalctl --user -u ours-fleet-agent@${name}.service`);
620
+ + `\n inspect member output: ours-fleet logs ${name}`);
621
621
  const modelStatus = joinPath(agentDir(name), '.model-status');
622
622
  if (existsSync(modelStatus)) {
623
623
  try {
@@ -1867,6 +1867,33 @@ program.command('_wait-daemon <name>', { hidden: true }).description('internal:
1867
1867
  die(e);
1868
1868
  }
1869
1869
  });
1870
+ program.command('_wait-temp-daemon <name>', { hidden: true })
1871
+ .action(async (name) => {
1872
+ try {
1873
+ await waitForRoleDaemon(name, undefined, {}, true);
1874
+ }
1875
+ catch (error) {
1876
+ die(error);
1877
+ }
1878
+ });
1879
+ program.command('_run-fleet', { hidden: true }).action(async () => {
1880
+ try {
1881
+ const { runFleetManager } = await import('./supervisor/manager.js');
1882
+ await runFleetManager(process.argv[1]);
1883
+ }
1884
+ catch (error) {
1885
+ die(error);
1886
+ }
1887
+ });
1888
+ program.command('_run-managed <key> <generation>', { hidden: true }).action(async (key, generation) => {
1889
+ try {
1890
+ const { runManagedMember } = await import('./supervisor/manager.js');
1891
+ await runManagedMember(key, generation);
1892
+ }
1893
+ catch (error) {
1894
+ die(error);
1895
+ }
1896
+ });
1870
1897
  program.command('_run <name>', { hidden: true }).description('internal: supervisor entrypoint')
1871
1898
  .option('-c, --configuration <file>')
1872
1899
  .action(async (name, opts) => {
@@ -1889,9 +1916,10 @@ program.command('_run-temp <name>', { hidden: true }).description('internal: tem
1889
1916
  }
1890
1917
  });
1891
1918
  program.command('_run-temp-worker <name>', { hidden: true })
1892
- .action(async (name) => {
1919
+ .option('--suspend-on-stop')
1920
+ .action(async (name, options) => {
1893
1921
  try {
1894
- await runTemp(name);
1922
+ await runTemp(name, { suspendOnStop: options.suspendOnStop === true });
1895
1923
  }
1896
1924
  catch (error) {
1897
1925
  if (error instanceof SupervisorRecycleRequiredError)
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
  }
@@ -189,6 +198,8 @@ export interface AgentTemplateInstance {
189
198
  }
190
199
  /** Internal first-boot payload for a Fleet-provisioned Cowork room member. */
191
200
  export interface RoomMemberStartup {
201
+ /** Task ownership; absent for standalone rooms and legacy snapshots. */
202
+ task_id?: string;
192
203
  workspace?: import('./rooms-tasks/workspace.js').OwnedWorkspace;
193
204
  room_id: string;
194
205
  room_identity_cid: string;
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 {};
@@ -30,6 +30,8 @@ export interface AgentSessionStartOptions {
30
30
  cwd: string;
31
31
  stateDir: string;
32
32
  mode: 'fresh' | 'resume';
33
+ /** Durable task context must never silently fall back to a fresh session. */
34
+ requireResume?: boolean;
33
35
  permissions: CommonPermissions;
34
36
  permissionMode: {
35
37
  fleetMode: FleetPermissionMode;
@@ -24,7 +24,7 @@ export class ClaudeCodeAgentSessionAdapter {
24
24
  return this.transport({
25
25
  name: role.name, harness: role.harness,
26
26
  argv: launch.argv, cwd: options.cwd, env: launch.env,
27
- stateDir: options.stateDir, mode: options.mode, permissions: options.permissions,
27
+ stateDir: options.stateDir, mode: options.mode, requireResume: options.requireResume, permissions: options.permissions,
28
28
  modeId: this.strategy.permissionModeId(role),
29
29
  mcpServers: options.managedOurs ? [...(this.strategy.mcpServers(role) ?? []).filter(s => s.name !== 'ours'), options.managedOurs.server] : this.strategy.mcpServers(role),
30
30
  inheritEnvironment: options.managedOurs ? false : undefined,
@@ -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
+ }