@ours.network/fleet 1.1.0-nightly.4 → 1.1.0-nightly.6
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/dist/application/role-creation-service.js +11 -0
- package/dist/briefing.js +6 -2
- package/dist/build-info.json +4 -4
- package/dist/cli.js +95 -8
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +15 -5
- package/dist/fleet-command-audit.d.ts +131 -0
- package/dist/fleet-command-audit.js +375 -0
- package/dist/fleet-proxy.d.ts +2 -0
- package/dist/harness/codex.js +6 -2
- package/dist/owner-channel/channel.d.ts +20 -4
- package/dist/owner-channel/channel.js +55 -15
- package/dist/rooms-tasks/cli.js +121 -18
- package/dist/rooms-tasks/external-worker.d.ts +1 -0
- package/dist/rooms-tasks/external-worker.js +14 -9
- package/dist/runner.d.ts +1 -1
- package/dist/runner.js +7 -26
- package/dist/session/control.d.ts +14 -1
- package/dist/session/control.js +35 -0
- package/package.json +1 -1
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { replaceFileAtomically } from './atomic-file.js';
|
|
4
|
+
import { isSensitiveConfigKey } from './sensitive-config.js';
|
|
5
|
+
export class FleetCliExit extends Error {
|
|
6
|
+
exitCode;
|
|
7
|
+
outcomeClass;
|
|
8
|
+
effect;
|
|
9
|
+
constructor(exitCode, outcomeClass = 'runtime', effect = 'unknown') {
|
|
10
|
+
super(`fleet CLI exited ${exitCode}`);
|
|
11
|
+
this.exitCode = exitCode;
|
|
12
|
+
this.outcomeClass = outcomeClass;
|
|
13
|
+
this.effect = effect;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
let collection;
|
|
17
|
+
export function beginFleetAuditCollection() { collection = { resourceIds: {} }; }
|
|
18
|
+
export function recordFleetAuditResource(kind, id) {
|
|
19
|
+
if (id && collection)
|
|
20
|
+
collection.resourceIds[kind] = id;
|
|
21
|
+
}
|
|
22
|
+
export function recordFleetAuditPresentation(value) {
|
|
23
|
+
if (collection)
|
|
24
|
+
collection.presentation = structuredClone(value);
|
|
25
|
+
}
|
|
26
|
+
export function recordFleetAuditFailure(failure) { if (collection)
|
|
27
|
+
collection.failure = failure; }
|
|
28
|
+
export function consumeFleetAuditCollection() {
|
|
29
|
+
const current = collection;
|
|
30
|
+
collection = undefined;
|
|
31
|
+
return {
|
|
32
|
+
...(current && Object.keys(current.resourceIds).length ? { resourceIds: current.resourceIds } : {}),
|
|
33
|
+
...(current?.presentation ? { presentation: current.presentation } : {}),
|
|
34
|
+
...(current?.failure ? { failure: current.failure } : {}),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const SAFE_READ = new Set(['docs', 'version', 'config', 'ls', 'peek', 'logs', 'status', 'doctor']);
|
|
38
|
+
const AGENT_SURFACES = {
|
|
39
|
+
spawn: new Set(['<none>']),
|
|
40
|
+
template: new Set(['list', 'show', 'validate']),
|
|
41
|
+
task: new Set(['create', 'list', 'lists', 'list-create', 'list-rename', 'list-delete', 'move',
|
|
42
|
+
'show', 'start', 'block', 'unblock', 'review', 'done', 'cancel', 'delete', 'recover', 'work', 'finish']),
|
|
43
|
+
room: new Set(['create', 'list', 'show', 'open', 'members', 'delete', 'close', 'recover']),
|
|
44
|
+
};
|
|
45
|
+
export const fleetProxyCommandInventory = Object.freeze(Object.fromEntries(Object.entries(AGENT_SURFACES).map(([surface, commands]) => [surface, [...commands]])));
|
|
46
|
+
const DENIED = new Set([
|
|
47
|
+
'up', 'down', 'restart', 'force-restart', 'attach', 'send', 'loops', 'owner-channel',
|
|
48
|
+
'watchdog-run', 'watchdog-report', 'rm', 'init', 'web',
|
|
49
|
+
]);
|
|
50
|
+
export const fleetProxyTopLevelInventory = Object.freeze({
|
|
51
|
+
safeRead: [...SAFE_READ], agent: Object.keys(AGENT_SURFACES), denied: [...DENIED],
|
|
52
|
+
hidden: ['_run', '_run-temp', '_run-watchdog', '_run-watchdogs'], aliases: ['man'],
|
|
53
|
+
});
|
|
54
|
+
const globalValueOptions = new Set(['-c', '--configuration']);
|
|
55
|
+
/** Classify before Commander parsing. Unknown and internal paths fail closed. */
|
|
56
|
+
export function classifyFleetArgv(argv) {
|
|
57
|
+
let command = '';
|
|
58
|
+
let commandIndex = -1;
|
|
59
|
+
for (let index = 0; index < argv.length; index++) {
|
|
60
|
+
const arg = argv[index];
|
|
61
|
+
if (arg === '--') {
|
|
62
|
+
command = argv[index + 1] ?? '';
|
|
63
|
+
commandIndex = index + 1;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
if (globalValueOptions.has(arg)) {
|
|
67
|
+
index++;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (arg.startsWith('--configuration=') || arg.startsWith('-c='))
|
|
71
|
+
continue;
|
|
72
|
+
if (arg.startsWith('-c') && arg.length > 2)
|
|
73
|
+
continue;
|
|
74
|
+
if (arg === '--help' || arg === '-h' || arg === '--version' || arg === '-V')
|
|
75
|
+
return { command: arg, route: 'supervisor-proxy/read-only', decision: 'allow' };
|
|
76
|
+
if (!arg.startsWith('-')) {
|
|
77
|
+
command = arg;
|
|
78
|
+
commandIndex = index;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (!command)
|
|
83
|
+
return { command: '<none>', route: 'supervisor-proxy/unsupported', decision: 'unsupported' };
|
|
84
|
+
if (command.startsWith('_'))
|
|
85
|
+
return { command, route: 'supervisor-proxy/internal', decision: 'deny' };
|
|
86
|
+
if (SAFE_READ.has(command) || command === 'man')
|
|
87
|
+
return { command, route: 'supervisor-proxy/read-only', decision: 'allow' };
|
|
88
|
+
if (AGENT_SURFACES[command]) {
|
|
89
|
+
const sub = argv[commandIndex + 1] ?? '<none>';
|
|
90
|
+
if (sub === '--help' || sub === '-h')
|
|
91
|
+
return { command: `${command} ${sub}`, route: 'supervisor-proxy/read-only', decision: 'allow' };
|
|
92
|
+
if (sub.startsWith('_'))
|
|
93
|
+
return { command: `${command} ${sub}`, route: 'supervisor-proxy/internal', decision: 'deny' };
|
|
94
|
+
if (command !== 'spawn' && !AGENT_SURFACES[command].has(sub))
|
|
95
|
+
return { command: `${command} ${sub}`, route: 'supervisor-proxy/unsupported', decision: 'unsupported' };
|
|
96
|
+
return { command: sub.startsWith('-') ? command : `${command} ${sub}`,
|
|
97
|
+
route: `supervisor-proxy/${command}`, decision: 'allow' };
|
|
98
|
+
}
|
|
99
|
+
if (DENIED.has(command))
|
|
100
|
+
return { command, route: 'supervisor-proxy/permission-denied', decision: 'deny' };
|
|
101
|
+
return { command, route: 'supervisor-proxy/unsupported', decision: 'unsupported' };
|
|
102
|
+
}
|
|
103
|
+
const marker = (value) => value === '' ? '[REDACTED:empty]' : '[REDACTED:value]';
|
|
104
|
+
const sensitiveValueFlags = new Set([
|
|
105
|
+
'--identity', '--invite', '--token', '--api-token', '--password', '--password-file',
|
|
106
|
+
'--env', '--brief', '--brief-file', '--bio-file', '--persona-file', '--isolation-file',
|
|
107
|
+
'--configuration', '-c', '--public-invite', '--public-invite-file', '--invite-file',
|
|
108
|
+
'--summary-file', '--text', '--message', '--summary', '--reason', '--goal', '--cwd',
|
|
109
|
+
'--identity-cid', '--owner-cid', '--contact-cid', '--codex-config', '--add-dir',
|
|
110
|
+
]);
|
|
111
|
+
function redactUrl(value) {
|
|
112
|
+
try {
|
|
113
|
+
const url = new URL(value);
|
|
114
|
+
let changed = false;
|
|
115
|
+
if (url.username || url.password) {
|
|
116
|
+
url.username = 'REDACTED';
|
|
117
|
+
url.password = 'REDACTED';
|
|
118
|
+
changed = true;
|
|
119
|
+
}
|
|
120
|
+
for (const key of [...url.searchParams.keys()])
|
|
121
|
+
if (isSensitiveConfigKey(key)) {
|
|
122
|
+
url.searchParams.set(key, marker(url.searchParams.get(key) ?? ''));
|
|
123
|
+
changed = true;
|
|
124
|
+
}
|
|
125
|
+
return changed ? url.toString() : value;
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return value;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function redactInline(value) {
|
|
132
|
+
if (!value.startsWith('inline:'))
|
|
133
|
+
return redactUrl(value);
|
|
134
|
+
const source = value.slice('inline:'.length);
|
|
135
|
+
try {
|
|
136
|
+
const parsed = JSON.parse(source);
|
|
137
|
+
const walk = (node, key) => {
|
|
138
|
+
if (key && isSensitiveConfigKey(key))
|
|
139
|
+
return typeof node === 'string' ? marker(node) : '[REDACTED:value]';
|
|
140
|
+
if (Array.isArray(node))
|
|
141
|
+
return node.map(child => walk(child));
|
|
142
|
+
if (node && typeof node === 'object')
|
|
143
|
+
return Object.fromEntries(Object.entries(node).map(([childKey, child]) => [childKey, walk(child, childKey)]));
|
|
144
|
+
return typeof node === 'string' ? redactUrl(node) : node;
|
|
145
|
+
};
|
|
146
|
+
return `inline:${JSON.stringify(walk(parsed))}`;
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return 'inline:[REDACTED:invalid-definition]';
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/** Faithful ordered argv with deterministic structural redaction. */
|
|
153
|
+
export function redactFleetArgv(argv) {
|
|
154
|
+
const result = [...argv];
|
|
155
|
+
for (let index = 0; index < result.length; index++) {
|
|
156
|
+
const arg = result[index];
|
|
157
|
+
if (arg.startsWith('-c=') || (arg.startsWith('-c') && arg.length > 2 && !arg.startsWith('--'))) {
|
|
158
|
+
const prefix = arg.startsWith('-c=') ? '-c=' : '-c';
|
|
159
|
+
result[index] = `${prefix}${marker(arg.slice(prefix.length))}`;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const equal = /^(--[^=]+)=(.*)$/su.exec(arg);
|
|
163
|
+
if (equal && sensitiveValueFlags.has(equal[1])) {
|
|
164
|
+
result[index] = `${equal[1]}=${marker(equal[2])}`;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (sensitiveValueFlags.has(arg) && index + 1 < result.length) {
|
|
168
|
+
result[index + 1] = marker(result[index + 1]);
|
|
169
|
+
index++;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
result[index] = redactInline(arg);
|
|
173
|
+
}
|
|
174
|
+
// `send <role> <text...>` has positional content. Locate it using the same
|
|
175
|
+
// global-option scan as classification, never absolute argv offsets.
|
|
176
|
+
let commandIndex = -1;
|
|
177
|
+
for (let index = 0; index < result.length; index++) {
|
|
178
|
+
const arg = result[index];
|
|
179
|
+
if (globalValueOptions.has(arg)) {
|
|
180
|
+
index++;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (arg.startsWith('--configuration=') || arg.startsWith('-c=') || (arg.startsWith('-c') && arg.length > 2))
|
|
184
|
+
continue;
|
|
185
|
+
if (!arg.startsWith('-')) {
|
|
186
|
+
commandIndex = index;
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (result[commandIndex] === 'send')
|
|
191
|
+
for (let index = commandIndex + 2; index < result.length; index++)
|
|
192
|
+
result[index] = marker(result[index]);
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
/** Durable write-once attempt ledger; contains redacted argv only. */
|
|
196
|
+
export class FleetCommandAuditStore {
|
|
197
|
+
path;
|
|
198
|
+
deps;
|
|
199
|
+
attempts = [];
|
|
200
|
+
constructor(path, deps = { now: () => new Date(), uuid: () => randomUUID() }) {
|
|
201
|
+
this.path = path;
|
|
202
|
+
this.deps = deps;
|
|
203
|
+
if (!existsSync(path))
|
|
204
|
+
return;
|
|
205
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
206
|
+
if (parsed.version !== 1 || !Array.isArray(parsed.attempts))
|
|
207
|
+
throw new Error('invalid fleet command audit ledger');
|
|
208
|
+
this.attempts = parsed.attempts;
|
|
209
|
+
let recovered = false;
|
|
210
|
+
for (const attempt of this.attempts) {
|
|
211
|
+
if (attempt.invocation === 'sending') {
|
|
212
|
+
attempt.invocation = 'uncertain';
|
|
213
|
+
recovered = true;
|
|
214
|
+
}
|
|
215
|
+
if (attempt.outcome?.delivery === 'sending') {
|
|
216
|
+
attempt.outcome.delivery = 'uncertain';
|
|
217
|
+
recovered = true;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (recovered)
|
|
221
|
+
this.persist();
|
|
222
|
+
}
|
|
223
|
+
list() { return this.attempts.map(item => structuredClone(item)); }
|
|
224
|
+
begin(requestId, caller, argv) {
|
|
225
|
+
const existing = this.attempts.find(item => item.caller === caller && item.requestId === requestId);
|
|
226
|
+
if (existing) {
|
|
227
|
+
if (JSON.stringify(existing.argv) !== JSON.stringify(redactFleetArgv(argv)))
|
|
228
|
+
throw new Error('fleet command request ID was reused with different argv');
|
|
229
|
+
return structuredClone(existing);
|
|
230
|
+
}
|
|
231
|
+
const attempt = { version: 1, correlationId: this.deps.uuid(), requestId,
|
|
232
|
+
caller, invokedAt: this.deps.now().toISOString(), classification: classifyFleetArgv(argv),
|
|
233
|
+
argv: redactFleetArgv(argv), invocation: 'sending' };
|
|
234
|
+
this.attempts.push(attempt);
|
|
235
|
+
this.persist();
|
|
236
|
+
return structuredClone(attempt);
|
|
237
|
+
}
|
|
238
|
+
invocation(correlationId, caller, delivery) {
|
|
239
|
+
const attempt = this.owned(correlationId, caller);
|
|
240
|
+
if (attempt.invocation !== 'sending' && attempt.invocation !== delivery)
|
|
241
|
+
throw new Error('conflicting fleet command invocation delivery state');
|
|
242
|
+
attempt.invocation = delivery;
|
|
243
|
+
this.persist();
|
|
244
|
+
return structuredClone(attempt);
|
|
245
|
+
}
|
|
246
|
+
finish(correlationId, caller, outcome) {
|
|
247
|
+
const attempt = this.owned(correlationId, caller);
|
|
248
|
+
if (!attempt.outcome)
|
|
249
|
+
attempt.outcome = { ...outcome, completedAt: this.deps.now().toISOString(), delivery: 'sending' };
|
|
250
|
+
else {
|
|
251
|
+
const prior = { class: attempt.outcome.class, effect: attempt.outcome.effect,
|
|
252
|
+
...(attempt.outcome.exitCode === undefined ? {} : { exitCode: attempt.outcome.exitCode }),
|
|
253
|
+
...(attempt.outcome.resourceIds ? { resourceIds: attempt.outcome.resourceIds } : {}),
|
|
254
|
+
...(attempt.outcome.presentation ? { presentation: attempt.outcome.presentation } : {}) };
|
|
255
|
+
if (JSON.stringify(prior) !== JSON.stringify(outcome))
|
|
256
|
+
throw new Error('conflicting fleet command outcome for existing correlation');
|
|
257
|
+
}
|
|
258
|
+
this.persist();
|
|
259
|
+
return structuredClone(attempt);
|
|
260
|
+
}
|
|
261
|
+
outcome(correlationId, caller, delivery) {
|
|
262
|
+
const attempt = this.owned(correlationId, caller);
|
|
263
|
+
if (!attempt.outcome)
|
|
264
|
+
throw new Error('fleet command audit outcome was not recorded');
|
|
265
|
+
if (attempt.outcome.delivery !== 'sending' && attempt.outcome.delivery !== delivery)
|
|
266
|
+
throw new Error('conflicting fleet command outcome delivery state');
|
|
267
|
+
attempt.outcome.delivery = delivery;
|
|
268
|
+
this.persist();
|
|
269
|
+
return structuredClone(attempt);
|
|
270
|
+
}
|
|
271
|
+
owned(correlationId, caller) {
|
|
272
|
+
const attempt = this.attempts.find(item => item.correlationId === correlationId);
|
|
273
|
+
if (!attempt || attempt.caller !== caller)
|
|
274
|
+
throw new Error('unknown or cross-role fleet command correlation');
|
|
275
|
+
return attempt;
|
|
276
|
+
}
|
|
277
|
+
persist() {
|
|
278
|
+
replaceFileAtomically(this.path, `${JSON.stringify({ version: 1, attempts: this.attempts })}\n`, 0o600);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
|
|
282
|
+
const SAFE_RESOURCE_ID = /^[\p{L}\p{N}][\p{L}\p{N}._:@-]{0,159}$/u;
|
|
283
|
+
const OUTCOMES = new Set(['success', 'validation', 'denied', 'runtime', 'timeout', 'proxy', 'delivery']);
|
|
284
|
+
const EFFECTS = new Set(['not_started', 'completed', 'unknown']);
|
|
285
|
+
export function validateFleetAuditBegin(value) {
|
|
286
|
+
const input = value;
|
|
287
|
+
if (!input || Object.keys(input).some(key => !['requestId', 'argv'].includes(key))
|
|
288
|
+
|| typeof input.requestId !== 'string' || !UUID.test(input.requestId)
|
|
289
|
+
|| !Array.isArray(input.argv) || input.argv.length > 256
|
|
290
|
+
|| !input.argv.every(arg => typeof arg === 'string' && Buffer.byteLength(arg) <= 16_384)
|
|
291
|
+
|| Buffer.byteLength(JSON.stringify(input.argv)) > 48 * 1024)
|
|
292
|
+
throw new Error('invalid fleet audit begin fields');
|
|
293
|
+
}
|
|
294
|
+
export function validateFleetAuditFinish(value) {
|
|
295
|
+
const input = value;
|
|
296
|
+
const allowed = ['correlationId', 'class', 'exitCode', 'effect', 'resourceIds', 'presentation'];
|
|
297
|
+
const resources = input?.resourceIds;
|
|
298
|
+
if (!input || Object.keys(input).some(key => !allowed.includes(key))
|
|
299
|
+
|| typeof input.correlationId !== 'string' || !UUID.test(input.correlationId)
|
|
300
|
+
|| !OUTCOMES.has(input.class) || !EFFECTS.has(String(input.effect))
|
|
301
|
+
|| (input.exitCode !== undefined && (!Number.isSafeInteger(input.exitCode) || Number(input.exitCode) < 0 || Number(input.exitCode) > 255))
|
|
302
|
+
|| (resources !== undefined && (!resources || typeof resources !== 'object' || Array.isArray(resources)
|
|
303
|
+
|| Object.entries(resources).some(([key, id]) => !['agent', 'task', 'room'].includes(key)
|
|
304
|
+
|| typeof id !== 'string' || !SAFE_RESOURCE_ID.test(id))))
|
|
305
|
+
|| (input.presentation !== undefined && !validPresentation(input.presentation)))
|
|
306
|
+
throw new Error('invalid fleet audit finish fields');
|
|
307
|
+
}
|
|
308
|
+
function validPresentation(value) {
|
|
309
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
310
|
+
return false;
|
|
311
|
+
const p = value;
|
|
312
|
+
const safe = (v) => typeof v === 'string' && SAFE_RESOURCE_ID.test(v);
|
|
313
|
+
const text = (v) => typeof v === 'string' && v.length <= 256
|
|
314
|
+
&& !/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/u.test(v);
|
|
315
|
+
if (p.kind === 'agent_started')
|
|
316
|
+
return safe(p.name) && ['permanent', 'temporary'].includes(String(p.lifetime))
|
|
317
|
+
&& text(p.brain) && text(p.role) && safe(p.harness) && p.session === 'acp' && safe(p.parent) && safe(p.actionId)
|
|
318
|
+
&& (p.model === undefined || text(p.model)) && (p.permissions === undefined || text(p.permissions))
|
|
319
|
+
&& Array.isArray(p.inherited) && p.inherited.every(text);
|
|
320
|
+
if (p.kind === 'task')
|
|
321
|
+
return safe(p.operation) && safe(p.id) && safe(p.newState)
|
|
322
|
+
&& (p.title === undefined || text(p.title)) && (p.previousState === undefined || safe(p.previousState))
|
|
323
|
+
&& (p.template === undefined || text(p.template)) && (p.roomId === undefined || safe(p.roomId))
|
|
324
|
+
&& Array.isArray(p.agents) && p.agents.length <= 64
|
|
325
|
+
&& p.agents.every(a => a && typeof a === 'object' && safe(a.name)
|
|
326
|
+
&& text(a.role)
|
|
327
|
+
&& (a.brain === undefined || text(a.brain)));
|
|
328
|
+
if (p.kind === 'room')
|
|
329
|
+
return safe(p.operation) && safe(p.id) && safe(p.newState)
|
|
330
|
+
&& (p.previousState === undefined || safe(p.previousState))
|
|
331
|
+
&& (p.template === undefined || text(p.template))
|
|
332
|
+
&& Array.isArray(p.participants) && p.participants.length <= 64
|
|
333
|
+
&& p.participants.every(a => a && typeof a === 'object' && safe(a.name)
|
|
334
|
+
&& text(a.role));
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
export function renderFleetAuditInvocation(attempt) {
|
|
338
|
+
return ['🧾 Fleet command invoked', `Correlation: ${attempt.correlationId}`, `Agent: ${attempt.caller}`,
|
|
339
|
+
`Route: ${attempt.classification.route}`, `Fleet action: ${attempt.classification.command}`,
|
|
340
|
+
`Decision: ${attempt.classification.decision}`, `Invoked: ${attempt.invokedAt}`,
|
|
341
|
+
`Raw argv (redacted): ${JSON.stringify(attempt.argv)}`].join('\n');
|
|
342
|
+
}
|
|
343
|
+
export function renderFleetAuditOutcome(attempt) {
|
|
344
|
+
if (!attempt.outcome)
|
|
345
|
+
throw new Error('fleet command audit outcome is absent');
|
|
346
|
+
return ['🧾 Fleet command outcome', `Correlation: ${attempt.correlationId}`, `Agent: ${attempt.caller}`,
|
|
347
|
+
`Route: ${attempt.classification.route}`, `Fleet action: ${attempt.classification.command}`,
|
|
348
|
+
`Result: ${attempt.outcome.class}`, `Effect: ${attempt.outcome.effect}`,
|
|
349
|
+
...(attempt.outcome.exitCode === undefined ? [] : [`Exit: ${attempt.outcome.exitCode}`]),
|
|
350
|
+
...Object.entries(attempt.outcome.resourceIds ?? {}).sort(([a], [b]) => a.localeCompare(b))
|
|
351
|
+
.map(([kind, id]) => `${kind[0]?.toUpperCase()}${kind.slice(1)} ID: ${id}`),
|
|
352
|
+
`Completed: ${attempt.outcome.completedAt}`, `Raw argv (redacted): ${JSON.stringify(attempt.argv)}`,
|
|
353
|
+
...renderPresentation(attempt.outcome.presentation)].join('\n');
|
|
354
|
+
}
|
|
355
|
+
function renderPresentation(value) {
|
|
356
|
+
if (!value)
|
|
357
|
+
return [];
|
|
358
|
+
if (value.kind === 'agent_started')
|
|
359
|
+
return ['Structured result: Agent started', `Agent: ${value.name}`,
|
|
360
|
+
`Brain: ${value.brain}`, `Role: ${value.role}`,
|
|
361
|
+
`Runtime: ${value.harness}/${value.session}${value.model ? ` model=${value.model}` : ''}`,
|
|
362
|
+
...(value.permissions ? [`Permissions: ${value.permissions}`] : []), `Parent: ${value.parent}`,
|
|
363
|
+
`Agent action ID: ${value.actionId}`,
|
|
364
|
+
`Inheritance: ${value.inherited.length ? value.inherited.join(', ') : 'none'}`];
|
|
365
|
+
if (value.kind === 'task')
|
|
366
|
+
return ['Structured result: Task', `Operation: ${value.operation}`,
|
|
367
|
+
`Task: ${value.title ? `${value.title} (` : ''}${value.id}${value.title ? ')' : ''}`,
|
|
368
|
+
`Status: ${value.previousState ?? 'unresolved'} -> ${value.newState}`,
|
|
369
|
+
...(value.template ? [`Template: ${value.template}`] : []), ...(value.roomId ? [`Room: ${value.roomId}`] : []),
|
|
370
|
+
`Responsible Agents: ${value.agents.length ? value.agents.map(a => `${a.name} [Brain ${a.brain ?? 'unresolved'}; Role ${a.role}]`).join('; ') : 'none'}`];
|
|
371
|
+
return ['Structured result: Room', `Operation: ${value.operation}`, `Room: ${value.id}`,
|
|
372
|
+
`Status: ${value.previousState ?? 'unresolved'} -> ${value.newState}`,
|
|
373
|
+
...(value.template ? [`Template: ${value.template}`] : []),
|
|
374
|
+
`Participants: ${value.participants.length ? value.participants.map(a => `${a.name} [Role ${a.role}]`).join('; ') : 'none'}`];
|
|
375
|
+
}
|
package/dist/fleet-proxy.d.ts
CHANGED
package/dist/harness/codex.js
CHANGED
|
@@ -274,8 +274,12 @@ export function makeCodexAdapter(exec = realExec, transport) {
|
|
|
274
274
|
? { adapterState: { permissionMetadataSource } } : {}),
|
|
275
275
|
};
|
|
276
276
|
},
|
|
277
|
-
sessionConfigSelections: role =>
|
|
278
|
-
|
|
277
|
+
sessionConfigSelections: role => [
|
|
278
|
+
...(typeof role.model === 'string'
|
|
279
|
+
? [{ configId: 'model', value: role.model }] : []),
|
|
280
|
+
...(typeof role.effort === 'string'
|
|
281
|
+
? [{ configId: 'reasoning_effort', value: role.effort }] : []),
|
|
282
|
+
],
|
|
279
283
|
permissionModeId: role => acpAgentMode(role),
|
|
280
284
|
mcpServers: () => undefined,
|
|
281
285
|
sessionMeta: () => undefined,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type OwnerChannelConfig } from '../config.js';
|
|
2
2
|
import { type AgentSession } from '../session/types.js';
|
|
3
3
|
import { type OwnerFleetOps } from './commands.js';
|
|
4
|
-
import type
|
|
4
|
+
import { type FleetAuditAttempt, type FleetAuditPresentation, type FleetCommandOutcomeClass } from '../fleet-command-audit.js';
|
|
5
5
|
import { type OursOps } from './ours-client.js';
|
|
6
6
|
import { type OwnerUpdatePhase } from './notices.js';
|
|
7
7
|
import { type OwnerEntry } from './state.js';
|
|
@@ -33,8 +33,15 @@ export interface OwnerChannelHandle {
|
|
|
33
33
|
drain(): Promise<void>;
|
|
34
34
|
close(): Promise<void>;
|
|
35
35
|
manage(request: OwnerChannelManagementRequest): Promise<OwnerChannelManagementResult>;
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
beginFleetCommandAudit?(requestId: string, argv: string[]): Promise<FleetAuditAttempt>;
|
|
37
|
+
finishFleetCommandAudit?(input: {
|
|
38
|
+
correlationId: string;
|
|
39
|
+
class: FleetCommandOutcomeClass;
|
|
40
|
+
exitCode?: number;
|
|
41
|
+
effect: 'not_started' | 'completed' | 'unknown';
|
|
42
|
+
resourceIds?: Record<string, string>;
|
|
43
|
+
presentation?: FleetAuditPresentation;
|
|
44
|
+
}): Promise<FleetAuditAttempt>;
|
|
38
45
|
}
|
|
39
46
|
export type OwnerChannelManagementRequest = {
|
|
40
47
|
action: 'contact_list';
|
|
@@ -167,12 +174,21 @@ export declare class OwnerChannel implements OwnerChannelHandle {
|
|
|
167
174
|
private binderOwnedInternally;
|
|
168
175
|
private readonly fleetOps;
|
|
169
176
|
private readonly prepareRestart;
|
|
177
|
+
private readonly commandAudits;
|
|
170
178
|
constructor(options: OwnerChannelOptions);
|
|
171
179
|
start(): Promise<void>;
|
|
172
180
|
drain(): Promise<void>;
|
|
173
181
|
close(): Promise<void>;
|
|
174
182
|
manage(request: OwnerChannelManagementRequest): Promise<OwnerChannelManagementResult>;
|
|
175
|
-
|
|
183
|
+
beginFleetCommandAudit(requestId: string, argv: string[]): Promise<FleetAuditAttempt>;
|
|
184
|
+
finishFleetCommandAudit(input: {
|
|
185
|
+
correlationId: string;
|
|
186
|
+
class: FleetCommandOutcomeClass;
|
|
187
|
+
exitCode?: number;
|
|
188
|
+
effect: 'not_started' | 'completed' | 'unknown';
|
|
189
|
+
resourceIds?: Record<string, string>;
|
|
190
|
+
presentation?: FleetAuditPresentation;
|
|
191
|
+
}): Promise<FleetAuditAttempt>;
|
|
176
192
|
private manageNow;
|
|
177
193
|
/**
|
|
178
194
|
* The daemon reports established contacts and pending introductions as two
|
|
@@ -14,6 +14,7 @@ import { ACP_CANCEL_DEADLINE_EXCEEDED, SessionControlError, } from '../session/t
|
|
|
14
14
|
import { VERSION } from '../version.js';
|
|
15
15
|
import { renderMarkdownFailure, renderMarkdownResult, roomStatus, taskStatus, } from '../rooms-tasks/markdown.js';
|
|
16
16
|
import { dispatchOwnerCommand, fleetCliOps, isOwnerCommandText, } from './commands.js';
|
|
17
|
+
import { FleetCommandAuditStore, renderFleetAuditInvocation, renderFleetAuditOutcome, } from '../fleet-command-audit.js';
|
|
17
18
|
import { OURS_BOUND_ELSEWHERE, OursSdkClient, oursErrorCode, } from './ours-client.js';
|
|
18
19
|
import { ownerNotices, } from './notices.js';
|
|
19
20
|
import { DuplicateSendError, OwnerAuthorizationState, OwnerChannelState, OwnerConversationState, } from './state.js';
|
|
@@ -81,6 +82,7 @@ export class OwnerChannel {
|
|
|
81
82
|
binderOwnedInternally = false;
|
|
82
83
|
fleetOps;
|
|
83
84
|
prepareRestart;
|
|
85
|
+
commandAudits;
|
|
84
86
|
constructor(options) {
|
|
85
87
|
this.options = options;
|
|
86
88
|
this.client = options.client ?? new OursSdkClient(options.env, line => options.log(`[${options.role}] owner channel ${line}`));
|
|
@@ -96,6 +98,7 @@ export class OwnerChannel {
|
|
|
96
98
|
await lifecycle.prepareRestart({ roleIds: [role], mode });
|
|
97
99
|
});
|
|
98
100
|
this.state = new OwnerChannelState(join(options.stateDir, '.owner-channel-state.json'));
|
|
101
|
+
this.commandAudits = new FleetCommandAuditStore(join(options.stateDir, '.owner-channel-command-audit.json'));
|
|
99
102
|
this.authorizations = new OwnerAuthorizationState(join(options.stateDir, '.owner-channel-owners.json'), options.config.owners);
|
|
100
103
|
this.conversations = new OwnerConversationState(join(options.stateDir, '.owner-channel-conversations.json'));
|
|
101
104
|
this.tasks = new OwnerTaskState(join(options.stateDir, '.owner-channel-tasks.json'));
|
|
@@ -203,22 +206,59 @@ export class OwnerChannel {
|
|
|
203
206
|
this.managementTail = run.then(() => undefined, () => undefined);
|
|
204
207
|
return run;
|
|
205
208
|
}
|
|
206
|
-
|
|
209
|
+
beginFleetCommandAudit(requestId, argv) {
|
|
207
210
|
const run = this.managementTail.then(async () => {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
211
|
+
let attempt = this.commandAudits.begin(requestId, this.options.role, argv);
|
|
212
|
+
if (attempt.invocation === 'delivered')
|
|
213
|
+
return attempt;
|
|
214
|
+
if (attempt.invocation === 'uncertain')
|
|
215
|
+
throw new Error('fleet command invocation delivery is uncertain; execution is denied');
|
|
216
|
+
if (!this.ready || this.stopping) {
|
|
217
|
+
this.commandAudits.invocation(attempt.correlationId, this.options.role, 'uncertain');
|
|
218
|
+
this.options.log(`[${this.options.role}] fleet command invocation ${attempt.correlationId} `
|
|
219
|
+
+ 'delivery unavailable; execution denied');
|
|
220
|
+
throw new Error('owner-channel MCP client is unavailable; invocation recorded uncertain');
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
await this.sendProactiveMessage(renderFleetAuditInvocation(attempt), `fleet-command-invocation\0${attempt.correlationId}`, 0);
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
this.commandAudits.invocation(attempt.correlationId, this.options.role, 'uncertain');
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
attempt = this.commandAudits.invocation(attempt.correlationId, this.options.role, 'delivered');
|
|
230
|
+
return attempt;
|
|
231
|
+
});
|
|
232
|
+
this.managementTail = run.then(() => undefined, () => undefined);
|
|
233
|
+
return run;
|
|
234
|
+
}
|
|
235
|
+
finishFleetCommandAudit(input) {
|
|
236
|
+
const run = this.managementTail.then(async () => {
|
|
237
|
+
let attempt = this.commandAudits.finish(input.correlationId, this.options.role, {
|
|
238
|
+
class: input.class, effect: input.effect,
|
|
239
|
+
...(input.exitCode === undefined ? {} : { exitCode: input.exitCode }),
|
|
240
|
+
...(input.resourceIds ? { resourceIds: input.resourceIds } : {}),
|
|
241
|
+
...(input.presentation ? { presentation: input.presentation } : {}),
|
|
242
|
+
});
|
|
243
|
+
if (attempt.outcome?.delivery === 'delivered')
|
|
244
|
+
return attempt;
|
|
245
|
+
if (attempt.outcome?.delivery === 'uncertain')
|
|
246
|
+
throw new Error(`fleet command outcome delivery is uncertain after execution; effect status=${attempt.outcome.effect}`);
|
|
247
|
+
if (!this.ready || this.stopping) {
|
|
248
|
+
this.commandAudits.outcome(attempt.correlationId, this.options.role, 'uncertain');
|
|
249
|
+
this.options.log(`[${this.options.role}] fleet command outcome ${attempt.correlationId} `
|
|
250
|
+
+ `delivery unavailable after execution; effect status=${attempt.outcome?.effect}`);
|
|
251
|
+
throw new Error(`owner-channel unavailable after execution; effect status=${attempt.outcome?.effect}`);
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
await this.sendProactiveMessage(renderFleetAuditOutcome(attempt), `fleet-command-outcome\0${attempt.correlationId}`, 0);
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
this.commandAudits.outcome(attempt.correlationId, this.options.role, 'uncertain');
|
|
258
|
+
throw new Error(`fleet command outcome delivery is uncertain after execution; effect status=${attempt.outcome?.effect}`, { cause: error });
|
|
259
|
+
}
|
|
260
|
+
attempt = this.commandAudits.outcome(attempt.correlationId, this.options.role, 'delivered');
|
|
261
|
+
return attempt;
|
|
222
262
|
});
|
|
223
263
|
this.managementTail = run.then(() => undefined, () => undefined);
|
|
224
264
|
return run;
|