@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.
- package/README.md +22 -12
- package/dist/agent-ours/bridge.js +10 -3
- package/dist/agent-ours/file-rpc.d.ts +8 -0
- package/dist/agent-ours/file-rpc.js +11 -1
- package/dist/agent-ours/harness.js +47 -3
- package/dist/agent-ours/mcp-endpoint.d.ts +5 -0
- package/dist/agent-ours/mcp-endpoint.js +4 -0
- package/dist/agent-ours/service.d.ts +6 -1
- package/dist/agent-ours/service.js +73 -8
- package/dist/application/conversation-history.d.ts +2 -0
- package/dist/application/conversation-history.js +25 -2
- package/dist/application/log-service.js +8 -8
- package/dist/application/role-creation-service.js +2 -1
- package/dist/application/role-removal-service.js +7 -1
- package/dist/application/role-repository.js +17 -0
- package/dist/application/supervisor-ours-tools.js +3 -1
- package/dist/application/task-layouts.js +12 -2
- package/dist/application/task-room-service.js +19 -17
- package/dist/application/types.d.ts +2 -0
- package/dist/build-info.json +4 -4
- package/dist/cli.js +31 -3
- package/dist/config.d.ts +11 -0
- package/dist/config.js +13 -2
- package/dist/file-delivery/reader.d.ts +17 -0
- package/dist/file-delivery/reader.js +94 -0
- package/dist/file-delivery/store.d.ts +15 -0
- package/dist/file-delivery/store.js +124 -0
- package/dist/file-delivery/types.d.ts +19 -0
- package/dist/file-delivery/types.js +1 -0
- package/dist/harness/agent-session.d.ts +2 -0
- package/dist/harness/claude-code-session.js +1 -1
- package/dist/harness/codex-app-server-proxy.js +6 -2
- package/dist/harness/codex-file-inventory.d.ts +15 -0
- package/dist/harness/codex-file-inventory.js +113 -0
- package/dist/harness/codex-session.js +2 -2
- package/dist/harness/codex.js +2 -1
- package/dist/isolation/policy.js +4 -1
- package/dist/managed-recovery.d.ts +13 -0
- package/dist/managed-recovery.js +31 -0
- package/dist/ops.d.ts +2 -0
- package/dist/ops.js +15 -2
- package/dist/rooms-tasks/cli.js +21 -0
- package/dist/rooms-tasks/close.d.ts +2 -0
- package/dist/rooms-tasks/close.js +66 -1
- package/dist/rooms-tasks/deletion.js +10 -0
- package/dist/rooms-tasks/erasure.js +4 -2
- package/dist/rooms-tasks/layout-member-retirement.d.ts +10 -0
- package/dist/rooms-tasks/layout-member-retirement.js +90 -0
- package/dist/rooms-tasks/layout-service.d.ts +1 -0
- package/dist/rooms-tasks/layout-service.js +31 -13
- package/dist/rooms-tasks/layout.d.ts +7 -0
- package/dist/rooms-tasks/layout.js +35 -0
- package/dist/rooms-tasks/legacy-task-member.d.ts +19 -0
- package/dist/rooms-tasks/legacy-task-member.js +116 -0
- package/dist/rooms-tasks/member-ownership.d.ts +5 -0
- package/dist/rooms-tasks/member-ownership.js +40 -0
- package/dist/rooms-tasks/provision.js +26 -1
- package/dist/rooms-tasks/recovery.d.ts +19 -0
- package/dist/rooms-tasks/recovery.js +110 -0
- package/dist/rooms-tasks/task-service-retirement.d.ts +11 -0
- package/dist/rooms-tasks/task-service-retirement.js +21 -0
- package/dist/rooms-tasks/task-state.d.ts +1 -0
- package/dist/rooms-tasks/task-state.js +16 -1
- package/dist/rooms-tasks/types.d.ts +3 -0
- package/dist/rooms-tasks/workspace.d.ts +2 -0
- package/dist/rooms-tasks/workspace.js +5 -0
- package/dist/runner.d.ts +6 -0
- package/dist/runner.js +161 -22
- package/dist/session/acp.d.ts +5 -0
- package/dist/session/acp.js +68 -8
- package/dist/session/arbiter.d.ts +5 -0
- package/dist/session/arbiter.js +16 -2
- package/dist/session/codex-app-server.d.ts +1 -0
- package/dist/session/codex-app-server.js +5 -0
- package/dist/session/conversation-types.d.ts +4 -2
- package/dist/session/types.d.ts +3 -0
- package/dist/spawn.d.ts +5 -1
- package/dist/spawn.js +32 -3
- package/dist/startup-readiness.d.ts +1 -1
- package/dist/startup-readiness.js +3 -3
- package/dist/supervisor/adoption.d.ts +8 -0
- package/dist/supervisor/adoption.js +78 -0
- package/dist/supervisor/catalog.d.ts +62 -0
- package/dist/supervisor/catalog.js +254 -0
- package/dist/supervisor/fleet.d.ts +8 -0
- package/dist/supervisor/fleet.js +89 -0
- package/dist/supervisor/index.js +3 -6
- package/dist/supervisor/launchd.d.ts +1 -1
- package/dist/supervisor/launchd.js +24 -12
- package/dist/supervisor/legacy.d.ts +35 -0
- package/dist/supervisor/legacy.js +524 -0
- package/dist/supervisor/manager.d.ts +15 -0
- package/dist/supervisor/manager.js +258 -0
- package/dist/supervisor/scope.d.ts +4 -0
- package/dist/supervisor/scope.js +10 -0
- package/dist/supervisor/systemd.d.ts +2 -2
- package/dist/supervisor/systemd.js +14 -14
- package/dist/task-supervision.d.ts +9 -0
- package/dist/task-supervision.js +145 -0
- package/dist/temp-lifecycle.d.ts +17 -7
- package/dist/temp-lifecycle.js +85 -53
- package/dist/temp-supervisor-recovery.d.ts +1 -1
- package/dist/temp-supervisor-recovery.js +2 -2
- package/dist/web/server.js +24 -0
- package/docs/acp-files.md +102 -0
- package/docs/fleet-migration.md +54 -0
- package/docs/task-workspaces.md +26 -0
- 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
|
-
|
|
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 =
|
|
382
|
-
? `
|
|
383
|
-
:
|
|
384
|
-
? `Complete
|
|
385
|
-
: room?.
|
|
386
|
-
? `
|
|
387
|
-
: room?.provisioning_detail === '
|
|
388
|
-
? `
|
|
389
|
-
: room?.provisioning_detail === '
|
|
390
|
-
? `
|
|
391
|
-
: room?.provisioning_detail === '
|
|
392
|
-
? `
|
|
393
|
-
: room?.provisioning_detail === '
|
|
394
|
-
? `
|
|
395
|
-
:
|
|
396
|
-
? `
|
|
397
|
-
:
|
|
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';
|
package/dist/build-info.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.2.0-nightly.
|
|
3
|
-
"buildId": "
|
|
4
|
-
"commit": "
|
|
2
|
+
"version": "1.2.0-nightly.55",
|
|
3
|
+
"buildId": "6e4199ab6d73",
|
|
4
|
+
"commit": "a66362bc99a9896700192f5ff2c8454a0e209766",
|
|
5
5
|
"dirty": true,
|
|
6
|
-
"builtAt": "2026-10-
|
|
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
|
|
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
|
-
.
|
|
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
|
-
|
|
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
|
+
}
|