@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.
- package/README.md +2 -0
- 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/runtime.d.ts +3 -1
- package/dist/agent-ours/runtime.js +14 -2
- package/dist/agent-ours/service.d.ts +2 -0
- package/dist/agent-ours/service.js +44 -2
- package/dist/application/conversation-history.d.ts +2 -0
- package/dist/application/conversation-history.js +25 -2
- package/dist/application/supervisor-ours-tools.js +3 -1
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +52 -29
- package/dist/build-info.json +4 -4
- package/dist/config.d.ts +9 -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/claude-code.js +11 -10
- 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.js +9 -5
- package/dist/isolation/policy.js +4 -1
- package/dist/monitor.d.ts +7 -0
- package/dist/monitor.js +65 -1
- package/dist/rooms-tasks/cli.js +12 -2
- package/dist/rooms-tasks/cowork-http.js +29 -1
- package/dist/rooms-tasks/erasure.js +5 -3
- package/dist/rooms-tasks/workspace-artifacts.js +1 -1
- package/dist/rooms-tasks/workspace.d.ts +8 -1
- package/dist/rooms-tasks/workspace.js +168 -43
- package/dist/runner.js +32 -9
- package/dist/session/acp.d.ts +4 -0
- package/dist/session/acp.js +61 -8
- package/dist/session/conversation-types.d.ts +4 -2
- package/dist/session/types.d.ts +1 -0
- package/dist/startup-prompt.d.ts +4 -0
- package/dist/startup-prompt.js +36 -0
- package/dist/startup-telemetry.d.ts +16 -0
- package/dist/startup-telemetry.js +46 -0
- package/dist/web/server.js +24 -0
- package/docs/acp-files.md +102 -0
- package/docs/task-workspaces.md +31 -0
- 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
|
|
389
|
-
restartPrompt: (id, worklog, role) =>
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/harness/codex.js
CHANGED
|
@@ -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.
|
|
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.
|
|
628
|
-
'
|
|
629
|
-
`NOT arm arm_monitor/foreground_monitor.
|
|
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
|
package/dist/isolation/policy.js
CHANGED
|
@@ -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;
|