@ours.network/fleet 1.1.0-nightly.8 → 1.1.0-nightly.9
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 +1 -1
- package/dist/application/task-room-service.js +147 -14
- package/dist/briefing.js +6 -6
- package/dist/build-info.json +4 -4
- package/dist/cli.js +2 -9
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +13 -15
- package/dist/fleet-command-audit.d.ts +37 -8
- package/dist/fleet-command-audit.js +120 -62
- package/dist/owner-channel/channel.d.ts +2 -2
- package/dist/owner-channel/channel.js +31 -38
- package/dist/owner-channel/state.d.ts +2 -0
- package/dist/owner-channel/state.js +8 -2
- package/dist/rooms-tasks/cli.js +117 -38
- package/dist/rooms-tasks/provision.js +28 -1
- package/dist/runner.js +13 -1
- package/dist/session/control.d.ts +1 -1
- package/dist/session/control.js +1 -1
- package/package.json +1 -1
|
@@ -8,6 +8,8 @@ export declare class FleetCliExit extends Error {
|
|
|
8
8
|
}
|
|
9
9
|
export type FleetAuditPresentation = {
|
|
10
10
|
kind: 'agent_started';
|
|
11
|
+
eventId: string;
|
|
12
|
+
id: string;
|
|
11
13
|
name: string;
|
|
12
14
|
lifetime: 'permanent' | 'temporary';
|
|
13
15
|
brain: string;
|
|
@@ -21,40 +23,65 @@ export type FleetAuditPresentation = {
|
|
|
21
23
|
inherited: string[];
|
|
22
24
|
} | {
|
|
23
25
|
kind: 'task';
|
|
24
|
-
operation:
|
|
26
|
+
operation: 'create' | 'start' | 'work' | 'block' | 'unblock' | 'review' | 'done' | 'cancel' | 'finish' | 'delete' | 'settling';
|
|
27
|
+
eventId: string;
|
|
25
28
|
id: string;
|
|
26
29
|
title?: string;
|
|
27
30
|
previousState?: string;
|
|
28
31
|
newState: string;
|
|
29
32
|
template?: string;
|
|
30
33
|
roomId?: string;
|
|
34
|
+
revision?: string;
|
|
35
|
+
list?: string;
|
|
31
36
|
agents: Array<{
|
|
32
37
|
name: string;
|
|
33
38
|
brain?: string;
|
|
34
39
|
role: string;
|
|
40
|
+
permissions?: string;
|
|
35
41
|
}>;
|
|
36
42
|
} | {
|
|
37
43
|
kind: 'room';
|
|
38
|
-
operation:
|
|
44
|
+
operation: 'create' | 'activate' | 'close' | 'delete';
|
|
45
|
+
eventId: string;
|
|
39
46
|
id: string;
|
|
40
47
|
previousState?: string;
|
|
41
48
|
newState: string;
|
|
49
|
+
revision?: string;
|
|
50
|
+
name?: string;
|
|
42
51
|
template?: string;
|
|
52
|
+
taskId?: string;
|
|
43
53
|
participants: Array<{
|
|
44
54
|
name: string;
|
|
55
|
+
id?: string;
|
|
56
|
+
brain?: string;
|
|
45
57
|
role: string;
|
|
58
|
+
permissions?: string;
|
|
46
59
|
}>;
|
|
60
|
+
} | {
|
|
61
|
+
kind: 'lifecycle_failure';
|
|
62
|
+
eventId: string;
|
|
63
|
+
resource: 'Agent' | 'Task' | 'Room';
|
|
64
|
+
id: string;
|
|
65
|
+
state: string;
|
|
66
|
+
category: 'provision_failed' | 'provision_pending' | 'readiness_failed' | 'settlement_failed' | 'settlement_pending' | 'cleanup_failed' | 'cleanup_pending';
|
|
47
67
|
};
|
|
68
|
+
/** Commands are Owner-silent unless they record one of these semantic lifecycle kinds. */
|
|
69
|
+
export declare const FLEET_LIFECYCLE_EVENT_KINDS: readonly ["agent_started", "task", "room", "lifecycle_failure"];
|
|
70
|
+
/** Stable semantic dedupe basis: resource kind + stable resource ID + transition/action ID. */
|
|
71
|
+
export declare function lifecycleEventDigestBasis(value: FleetAuditPresentation): string;
|
|
48
72
|
export declare function beginFleetAuditCollection(): void;
|
|
49
73
|
export declare function recordFleetAuditResource(kind: 'agent' | 'task' | 'room', id: string | undefined): void;
|
|
50
|
-
|
|
74
|
+
type FleetAuditPresentationInput = FleetAuditPresentation extends infer T ? T extends FleetAuditPresentation ? Omit<T, 'eventId'> & {
|
|
75
|
+
eventId?: string;
|
|
76
|
+
} : never : never;
|
|
77
|
+
export declare function recordFleetAuditPresentation(value: FleetAuditPresentationInput): void;
|
|
51
78
|
export declare function recordFleetAuditFailure(failure: {
|
|
52
79
|
class: FleetCommandOutcomeClass;
|
|
53
80
|
effect: 'not_started' | 'completed' | 'unknown';
|
|
54
81
|
}): void;
|
|
55
82
|
export declare function consumeFleetAuditCollection(): {
|
|
56
83
|
resourceIds?: Record<string, string>;
|
|
57
|
-
|
|
84
|
+
presentations?: FleetAuditPresentation[];
|
|
58
85
|
failure?: {
|
|
59
86
|
class: FleetCommandOutcomeClass;
|
|
60
87
|
effect: 'not_started' | 'completed' | 'unknown';
|
|
@@ -81,7 +108,7 @@ export interface FleetAuditAttempt {
|
|
|
81
108
|
effect: 'not_started' | 'completed' | 'unknown';
|
|
82
109
|
delivery: 'sending' | 'delivered' | 'uncertain';
|
|
83
110
|
resourceIds?: Record<string, string>;
|
|
84
|
-
|
|
111
|
+
presentations?: FleetAuditPresentation[];
|
|
85
112
|
};
|
|
86
113
|
}
|
|
87
114
|
export declare const fleetProxyCommandInventory: Readonly<{
|
|
@@ -90,6 +117,7 @@ export declare const fleetProxyCommandInventory: Readonly<{
|
|
|
90
117
|
export declare const fleetProxyTopLevelInventory: Readonly<{
|
|
91
118
|
safeRead: string[];
|
|
92
119
|
agent: string[];
|
|
120
|
+
public: string[];
|
|
93
121
|
denied: string[];
|
|
94
122
|
hidden: string[];
|
|
95
123
|
aliases: string[];
|
|
@@ -125,7 +153,8 @@ export declare function validateFleetAuditFinish(value: unknown): asserts value
|
|
|
125
153
|
exitCode?: number;
|
|
126
154
|
effect: 'not_started' | 'completed' | 'unknown';
|
|
127
155
|
resourceIds?: Record<string, string>;
|
|
128
|
-
|
|
156
|
+
presentations?: FleetAuditPresentation[];
|
|
129
157
|
};
|
|
130
|
-
|
|
131
|
-
export declare function
|
|
158
|
+
/** Compact Owner presentation. It intentionally has no command, argv, environment, or correlation data. */
|
|
159
|
+
export declare function renderFleetLifecycleEvent(value: FleetAuditPresentation): string;
|
|
160
|
+
export {};
|
|
@@ -13,15 +13,35 @@ export class FleetCliExit extends Error {
|
|
|
13
13
|
this.effect = effect;
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
|
+
/** Commands are Owner-silent unless they record one of these semantic lifecycle kinds. */
|
|
17
|
+
export const FLEET_LIFECYCLE_EVENT_KINDS = Object.freeze([
|
|
18
|
+
'agent_started', 'task', 'room', 'lifecycle_failure',
|
|
19
|
+
]);
|
|
20
|
+
/** Stable semantic dedupe basis: resource kind + stable resource ID + transition/action ID. */
|
|
21
|
+
export function lifecycleEventDigestBasis(value) {
|
|
22
|
+
const resourceKind = value.kind === 'lifecycle_failure' ? value.resource : value.kind;
|
|
23
|
+
return `${resourceKind}\0${value.id}\0${value.eventId}`;
|
|
24
|
+
}
|
|
16
25
|
let collection;
|
|
17
|
-
export function beginFleetAuditCollection() { collection = { resourceIds: {} }; }
|
|
26
|
+
export function beginFleetAuditCollection() { collection = { resourceIds: {}, presentations: [] }; }
|
|
18
27
|
export function recordFleetAuditResource(kind, id) {
|
|
19
28
|
if (id && collection)
|
|
20
29
|
collection.resourceIds[kind] = id;
|
|
21
30
|
}
|
|
22
31
|
export function recordFleetAuditPresentation(value) {
|
|
23
|
-
if (collection)
|
|
24
|
-
|
|
32
|
+
if (collection && FLEET_LIFECYCLE_EVENT_KINDS.includes(value.kind)) {
|
|
33
|
+
const basis = value.eventId ?? (value.kind === 'agent_started' ? value.actionId
|
|
34
|
+
: value.kind === 'task' || value.kind === 'room'
|
|
35
|
+
? `${value.operation}:${value.revision ?? `${value.previousState ?? 'none'}:${value.newState}`}`
|
|
36
|
+
: `${value.category}:${value.state}`);
|
|
37
|
+
collection.presentations.push(structuredClone({ ...value, eventId: basis }));
|
|
38
|
+
if (value.kind === 'task')
|
|
39
|
+
collection.resourceIds.task = value.id;
|
|
40
|
+
if (value.kind === 'room')
|
|
41
|
+
collection.resourceIds.room = value.id;
|
|
42
|
+
if (value.kind === 'agent_started')
|
|
43
|
+
collection.resourceIds.agent = value.id;
|
|
44
|
+
}
|
|
25
45
|
}
|
|
26
46
|
export function recordFleetAuditFailure(failure) { if (collection)
|
|
27
47
|
collection.failure = failure; }
|
|
@@ -30,7 +50,7 @@ export function consumeFleetAuditCollection() {
|
|
|
30
50
|
collection = undefined;
|
|
31
51
|
return {
|
|
32
52
|
...(current && Object.keys(current.resourceIds).length ? { resourceIds: current.resourceIds } : {}),
|
|
33
|
-
...(current?.
|
|
53
|
+
...(current?.presentations.length ? { presentations: current.presentations } : {}),
|
|
34
54
|
...(current?.failure ? { failure: current.failure } : {}),
|
|
35
55
|
};
|
|
36
56
|
}
|
|
@@ -43,12 +63,11 @@ const AGENT_SURFACES = {
|
|
|
43
63
|
room: new Set(['create', 'list', 'show', 'open', 'members', 'delete', 'close', 'recover']),
|
|
44
64
|
};
|
|
45
65
|
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
66
|
export const fleetProxyTopLevelInventory = Object.freeze({
|
|
51
|
-
safeRead: [...SAFE_READ], agent: Object.keys(AGENT_SURFACES),
|
|
67
|
+
safeRead: [...SAFE_READ], agent: Object.keys(AGENT_SURFACES), public: [
|
|
68
|
+
'up', 'down', 'restart', 'force-restart', 'attach', 'send', 'loops', 'owner-channel',
|
|
69
|
+
'watchdog-run', 'watchdog-report', 'rm', 'init', 'web',
|
|
70
|
+
], denied: [],
|
|
52
71
|
hidden: ['_run', '_run-temp', '_run-watchdog', '_run-watchdogs'], aliases: ['man'],
|
|
53
72
|
});
|
|
54
73
|
const globalValueOptions = new Set(['-c', '--configuration']);
|
|
@@ -92,13 +111,14 @@ export function classifyFleetArgv(argv) {
|
|
|
92
111
|
if (sub.startsWith('_'))
|
|
93
112
|
return { command: `${command} ${sub}`, route: 'supervisor-proxy/internal', decision: 'deny' };
|
|
94
113
|
if (command !== 'spawn' && !AGENT_SURFACES[command].has(sub))
|
|
95
|
-
return { command: `${command} ${sub}`, route:
|
|
114
|
+
return { command: `${command} ${sub}`, route: `supervisor-proxy/${command}`, decision: 'allow' };
|
|
96
115
|
return { command: sub.startsWith('-') ? command : `${command} ${sub}`,
|
|
97
116
|
route: `supervisor-proxy/${command}`, decision: 'allow' };
|
|
98
117
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
118
|
+
// The proxy is attribution and routing, not a product permission boundary.
|
|
119
|
+
// Commander validates every public command. Hidden process entry points above
|
|
120
|
+
// remain guarded because recursively invoking them breaks the supervisor protocol.
|
|
121
|
+
return { command, route: 'supervisor-proxy/public', decision: 'allow' };
|
|
102
122
|
}
|
|
103
123
|
const marker = (value) => value === '' ? '[REDACTED:empty]' : '[REDACTED:value]';
|
|
104
124
|
const sensitiveValueFlags = new Set([
|
|
@@ -251,7 +271,7 @@ export class FleetCommandAuditStore {
|
|
|
251
271
|
const prior = { class: attempt.outcome.class, effect: attempt.outcome.effect,
|
|
252
272
|
...(attempt.outcome.exitCode === undefined ? {} : { exitCode: attempt.outcome.exitCode }),
|
|
253
273
|
...(attempt.outcome.resourceIds ? { resourceIds: attempt.outcome.resourceIds } : {}),
|
|
254
|
-
...(attempt.outcome.
|
|
274
|
+
...(attempt.outcome.presentations ? { presentations: attempt.outcome.presentations } : {}) };
|
|
255
275
|
if (JSON.stringify(prior) !== JSON.stringify(outcome))
|
|
256
276
|
throw new Error('conflicting fleet command outcome for existing correlation');
|
|
257
277
|
}
|
|
@@ -293,7 +313,7 @@ export function validateFleetAuditBegin(value) {
|
|
|
293
313
|
}
|
|
294
314
|
export function validateFleetAuditFinish(value) {
|
|
295
315
|
const input = value;
|
|
296
|
-
const allowed = ['correlationId', 'class', 'exitCode', 'effect', 'resourceIds', '
|
|
316
|
+
const allowed = ['correlationId', 'class', 'exitCode', 'effect', 'resourceIds', 'presentations'];
|
|
297
317
|
const resources = input?.resourceIds;
|
|
298
318
|
if (!input || Object.keys(input).some(key => !allowed.includes(key))
|
|
299
319
|
|| typeof input.correlationId !== 'string' || !UUID.test(input.correlationId)
|
|
@@ -302,7 +322,8 @@ export function validateFleetAuditFinish(value) {
|
|
|
302
322
|
|| (resources !== undefined && (!resources || typeof resources !== 'object' || Array.isArray(resources)
|
|
303
323
|
|| Object.entries(resources).some(([key, id]) => !['agent', 'task', 'room'].includes(key)
|
|
304
324
|
|| typeof id !== 'string' || !SAFE_RESOURCE_ID.test(id))))
|
|
305
|
-
|| (input.
|
|
325
|
+
|| (input.presentations !== undefined && (!Array.isArray(input.presentations)
|
|
326
|
+
|| input.presentations.length > 128 || !input.presentations.every(validPresentation))))
|
|
306
327
|
throw new Error('invalid fleet audit finish fields');
|
|
307
328
|
}
|
|
308
329
|
function validPresentation(value) {
|
|
@@ -312,64 +333,101 @@ function validPresentation(value) {
|
|
|
312
333
|
const safe = (v) => typeof v === 'string' && SAFE_RESOURCE_ID.test(v);
|
|
313
334
|
const text = (v) => typeof v === 'string' && v.length <= 256
|
|
314
335
|
&& !/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/u.test(v);
|
|
336
|
+
const exact = (record, keys) => Object.keys(record).every(key => keys.includes(key));
|
|
337
|
+
const transition = (allowed) => typeof p.operation === 'string' && typeof p.newState === 'string'
|
|
338
|
+
&& allowed[p.operation]?.includes(`${String(p.previousState ?? 'none')}→${p.newState}`);
|
|
315
339
|
if (p.kind === 'agent_started')
|
|
316
|
-
return
|
|
340
|
+
return exact(p, ['kind', 'eventId', 'id', 'name', 'lifetime', 'brain', 'role',
|
|
341
|
+
'harness', 'session', 'model', 'permissions', 'parent', 'actionId', 'inherited'])
|
|
342
|
+
&& safe(p.eventId) && safe(p.id) && safe(p.name) && ['permanent', 'temporary'].includes(String(p.lifetime))
|
|
317
343
|
&& text(p.brain) && text(p.role) && safe(p.harness) && p.session === 'acp' && safe(p.parent) && safe(p.actionId)
|
|
318
344
|
&& (p.model === undefined || text(p.model)) && (p.permissions === undefined || text(p.permissions))
|
|
319
345
|
&& Array.isArray(p.inherited) && p.inherited.every(text);
|
|
320
346
|
if (p.kind === 'task')
|
|
321
|
-
return
|
|
347
|
+
return exact(p, ['kind', 'operation', 'eventId', 'id', 'title', 'previousState',
|
|
348
|
+
'newState', 'template', 'roomId', 'revision', 'list', 'agents'])
|
|
349
|
+
&& ['create', 'start', 'work', 'block', 'unblock', 'review', 'done',
|
|
350
|
+
'cancel', 'finish', 'delete', 'settling'].includes(String(p.operation)) && safe(p.eventId) && safe(p.id) && safe(p.newState)
|
|
351
|
+
&& transition({ create: ['none→backlog', 'none→provisioning', 'none→active'],
|
|
352
|
+
start: ['backlog→provisioning'], work: ['provisioning→active'],
|
|
353
|
+
block: ['backlog→backlog', 'provisioning→provisioning', 'active→active', 'review→review'],
|
|
354
|
+
unblock: ['backlog→backlog', 'provisioning→provisioning', 'active→active', 'review→review'],
|
|
355
|
+
review: ['active→review'], done: ['review→done'], finish: ['active→done', 'review→done'],
|
|
356
|
+
cancel: ['backlog→cancelled', 'provisioning→cancelled', 'active→cancelled', 'review→cancelled'],
|
|
357
|
+
delete: ['done→deleted'], settling: ['active→active', 'review→review', 'backlog→backlog',
|
|
358
|
+
'provisioning→provisioning'] })
|
|
322
359
|
&& (p.title === undefined || text(p.title)) && (p.previousState === undefined || safe(p.previousState))
|
|
323
360
|
&& (p.template === undefined || text(p.template)) && (p.roomId === undefined || safe(p.roomId))
|
|
361
|
+
&& (p.list === undefined || text(p.list)) && (p.revision === undefined || text(p.revision))
|
|
324
362
|
&& Array.isArray(p.agents) && p.agents.length <= 64
|
|
325
|
-
&& p.agents.every(a => a && typeof a === 'object'
|
|
363
|
+
&& p.agents.every(a => a && typeof a === 'object'
|
|
364
|
+
&& exact(a, ['name', 'brain', 'role', 'permissions'])
|
|
365
|
+
&& safe(a.name)
|
|
326
366
|
&& text(a.role)
|
|
327
|
-
&& (a.brain === undefined || text(a.brain))
|
|
367
|
+
&& (a.brain === undefined || text(a.brain))
|
|
368
|
+
&& (a.permissions === undefined || text(a.permissions)));
|
|
328
369
|
if (p.kind === 'room')
|
|
329
|
-
return
|
|
370
|
+
return exact(p, ['kind', 'operation', 'eventId', 'id', 'previousState', 'newState',
|
|
371
|
+
'revision', 'name', 'template', 'taskId', 'participants'])
|
|
372
|
+
&& ['create', 'activate', 'close', 'delete'].includes(String(p.operation))
|
|
373
|
+
&& safe(p.eventId) && safe(p.id) && safe(p.newState)
|
|
374
|
+
&& transition({ create: ['none→provisioning'], activate: ['provisioning→active'],
|
|
375
|
+
close: ['active→closing', 'provisioning→closing'],
|
|
376
|
+
delete: ['closing→deleted'] })
|
|
330
377
|
&& (p.previousState === undefined || safe(p.previousState))
|
|
331
|
-
&& (p.template === undefined || text(p.template))
|
|
378
|
+
&& (p.name === undefined || text(p.name)) && (p.template === undefined || text(p.template))
|
|
379
|
+
&& (p.taskId === undefined || safe(p.taskId)) && (p.revision === undefined || text(p.revision))
|
|
332
380
|
&& Array.isArray(p.participants) && p.participants.length <= 64
|
|
333
|
-
&& p.participants.every(a => a && typeof a === 'object'
|
|
334
|
-
&&
|
|
381
|
+
&& p.participants.every(a => a && typeof a === 'object'
|
|
382
|
+
&& exact(a, ['name', 'id', 'brain', 'role', 'permissions'])
|
|
383
|
+
&& safe(a.name)
|
|
384
|
+
&& text(a.role)
|
|
385
|
+
&& (a.id === undefined || safe(a.id))
|
|
386
|
+
&& (a.brain === undefined || text(a.brain))
|
|
387
|
+
&& (a.permissions === undefined || text(a.permissions)));
|
|
388
|
+
if (p.kind === 'lifecycle_failure')
|
|
389
|
+
return exact(p, ['kind', 'eventId', 'resource', 'id', 'state', 'category'])
|
|
390
|
+
&& ['Agent', 'Task', 'Room'].includes(String(p.resource))
|
|
391
|
+
&& safe(p.eventId) && safe(p.id) && safe(p.state) && ['provision_failed', 'provision_pending', 'readiness_failed', 'settlement_failed',
|
|
392
|
+
'settlement_pending', 'cleanup_failed', 'cleanup_pending'].includes(String(p.category));
|
|
335
393
|
return false;
|
|
336
394
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
if (value.kind === '
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
395
|
+
/** Compact Owner presentation. It intentionally has no command, argv, environment, or correlation data. */
|
|
396
|
+
export function renderFleetLifecycleEvent(value) {
|
|
397
|
+
if (value.kind === 'lifecycle_failure') {
|
|
398
|
+
const actions = {
|
|
399
|
+
provision_failed: 'Inspect Fleet service logs, correct configuration, then recover the resource.',
|
|
400
|
+
provision_pending: 'Run Task or Room recover after checking member readiness.',
|
|
401
|
+
readiness_failed: 'Inspect the Agent log, correct its configuration, then retry creation.',
|
|
402
|
+
settlement_failed: 'Inspect Fleet service logs, then run Task recover.',
|
|
403
|
+
settlement_pending: 'Run Task recover to continue settlement.',
|
|
404
|
+
cleanup_failed: 'Inspect Fleet service logs, then run Room recover.',
|
|
405
|
+
cleanup_pending: 'Run Room recover to continue cleanup.',
|
|
406
|
+
};
|
|
407
|
+
const lifecycle = value.category.endsWith('_pending') ? 'lifecycle pending' : 'lifecycle failure';
|
|
408
|
+
return `⚠️ ${value.resource} ${value.id} ${lifecycle} (${value.category}); `
|
|
409
|
+
+ `state ${value.state}. Action: ${actions[value.category]}`;
|
|
410
|
+
}
|
|
411
|
+
if (value.kind === 'agent_started') {
|
|
412
|
+
const permission = value.permissions ? `; permissions ${value.permissions}` : '';
|
|
413
|
+
return `🧑💻 ${value.parent} spawned ${value.lifetime} Agent ${value.name} (${value.id}) — ready. `
|
|
414
|
+
+ `Brain ${value.brain ?? 'unresolved'}; Role ${value.role ?? 'unresolved'}${permission}.`;
|
|
415
|
+
}
|
|
416
|
+
if (value.kind === 'task') {
|
|
417
|
+
const title = value.title ? ` “${value.title}”` : '';
|
|
418
|
+
const context = [value.list ? `List ${value.list}` : undefined,
|
|
419
|
+
value.template ? `template ${value.template}` : undefined,
|
|
420
|
+
value.roomId ? `Room ${value.roomId}` : undefined].filter(Boolean).join('; ');
|
|
421
|
+
const agents = value.agents.length ? ` Agents: ${value.agents.map(agent => `${agent.name} [Brain ${agent.brain ?? 'unresolved'}; Role ${agent.role}`
|
|
422
|
+
+ `${agent.permissions ? `; permissions ${agent.permissions}` : ''}]`).join('; ')}.` : '';
|
|
423
|
+
return `📋 Task${title} (${value.id}) ${value.operation}: `
|
|
424
|
+
+ `${value.previousState ?? 'none'} → ${value.newState}.${context ? ` ${context}.` : ''}${agents}`;
|
|
425
|
+
}
|
|
426
|
+
const context = [value.template ? `template ${value.template}` : undefined,
|
|
427
|
+
value.taskId ? `Task ${value.taskId}` : undefined].filter(Boolean).join('; ');
|
|
428
|
+
const participants = value.participants.length ? ` Participants: ${value.participants.map(participant => `${participant.name}${participant.id ? ` (${participant.id})` : ''} [Role ${participant.role}`
|
|
429
|
+
+ `${participant.brain ? `; Brain ${participant.brain}` : ''}`
|
|
430
|
+
+ `${participant.permissions ? `; permissions ${participant.permissions}` : ''}]`).join('; ')}.` : '';
|
|
431
|
+
return `🏠 Room${value.name ? ` “${value.name}”` : ''} (${value.id}) ${value.operation}: `
|
|
432
|
+
+ `${value.previousState ?? 'none'} → ${value.newState}.${context ? ` ${context}.` : ''}${participants}`;
|
|
375
433
|
}
|
|
@@ -53,7 +53,7 @@ export interface OwnerChannelHandle {
|
|
|
53
53
|
exitCode?: number;
|
|
54
54
|
effect: 'not_started' | 'completed' | 'unknown';
|
|
55
55
|
resourceIds?: Record<string, string>;
|
|
56
|
-
|
|
56
|
+
presentations?: FleetAuditPresentation[];
|
|
57
57
|
}): Promise<FleetAuditAttempt>;
|
|
58
58
|
}
|
|
59
59
|
export declare const OWNER_RECOVERY_QUEUE_CAPACITY = 32;
|
|
@@ -223,7 +223,7 @@ export declare class OwnerChannel implements OwnerChannelHandle {
|
|
|
223
223
|
exitCode?: number;
|
|
224
224
|
effect: 'not_started' | 'completed' | 'unknown';
|
|
225
225
|
resourceIds?: Record<string, string>;
|
|
226
|
-
|
|
226
|
+
presentations?: FleetAuditPresentation[];
|
|
227
227
|
}): Promise<FleetAuditAttempt>;
|
|
228
228
|
recover(epoch: string): Promise<void>;
|
|
229
229
|
private recoveryStage;
|
|
@@ -15,7 +15,7 @@ import { ACP_CANCEL_DEADLINE_EXCEEDED, SessionControlError, } from '../session/t
|
|
|
15
15
|
import { VERSION } from '../version.js';
|
|
16
16
|
import { renderMarkdownFailure, renderMarkdownResult, roomStatus, taskStatus, } from '../rooms-tasks/markdown.js';
|
|
17
17
|
import { dispatchOwnerCommand, fleetCliOps, isOwnerCommandText, } from './commands.js';
|
|
18
|
-
import { FleetCommandAuditStore,
|
|
18
|
+
import { FleetCommandAuditStore, lifecycleEventDigestBasis, renderFleetLifecycleEvent, } from '../fleet-command-audit.js';
|
|
19
19
|
import { OURS_BOUND_ELSEWHERE, OursSdkClient, oursErrorCode, } from './ours-client.js';
|
|
20
20
|
import { ownerNotices, } from './notices.js';
|
|
21
21
|
import { DuplicateSendError, OwnerAuthorizationState, OwnerChannelState, OwnerConversationState, } from './state.js';
|
|
@@ -263,18 +263,14 @@ export class OwnerChannel {
|
|
|
263
263
|
return this.queueManagement(async () => {
|
|
264
264
|
if (!this.ready || this.stopping)
|
|
265
265
|
throw new Error('owner-channel MCP client is unavailable');
|
|
266
|
-
const model = event.model ? `, model ${event.model}` : '';
|
|
267
|
-
const monitorPolicy = event.monitor.interrupt === true
|
|
268
|
-
? ' with interruption'
|
|
269
|
-
: event.monitor.interrupt === 'after_tool' ? ' with after-tool steering' : '';
|
|
270
|
-
const monitor = `${event.monitor.mode} monitor${monitorPolicy}`;
|
|
271
266
|
const permission = event.permissionMode
|
|
272
|
-
?
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
267
|
+
? `${event.permissionMode.fleetMode}/${event.permissionMode.nativeMode}` : undefined;
|
|
268
|
+
await this.sendProactiveMessage(renderFleetLifecycleEvent({ kind: 'agent_started', eventId: event.creationActionId,
|
|
269
|
+
id: event.role, name: event.role,
|
|
270
|
+
lifetime: event.lifetime, brain: event.brainSummary, role: event.roleSummary,
|
|
271
|
+
harness: event.harness, session: event.session, model: event.model,
|
|
272
|
+
permissions: permission, parent: event.caller, actionId: event.creationActionId,
|
|
273
|
+
inherited: event.inherited }), `fleet-spawn\0${event.creationActionId}`, 0);
|
|
278
274
|
});
|
|
279
275
|
}
|
|
280
276
|
beginFleetCommandAudit(requestId, argv) {
|
|
@@ -283,20 +279,9 @@ export class OwnerChannel {
|
|
|
283
279
|
if (attempt.invocation === 'delivered')
|
|
284
280
|
return attempt;
|
|
285
281
|
if (attempt.invocation === 'uncertain')
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
this.options.log(`[${this.options.role}] fleet command invocation ${attempt.correlationId} `
|
|
290
|
-
+ 'delivery unavailable; execution denied');
|
|
291
|
-
throw new Error('owner-channel MCP client is unavailable; invocation recorded uncertain');
|
|
292
|
-
}
|
|
293
|
-
try {
|
|
294
|
-
await this.sendProactiveMessage(renderFleetAuditInvocation(attempt), `fleet-command-invocation\0${attempt.correlationId}`, 0);
|
|
295
|
-
}
|
|
296
|
-
catch (error) {
|
|
297
|
-
this.commandAudits.invocation(attempt.correlationId, this.options.role, 'uncertain');
|
|
298
|
-
throw error;
|
|
299
|
-
}
|
|
282
|
+
return attempt;
|
|
283
|
+
this.options.log(`[${this.options.role}] fleet proxy command ${attempt.correlationId} `
|
|
284
|
+
+ `route=${attempt.classification.route} decision=${attempt.classification.decision}`);
|
|
300
285
|
attempt = this.commandAudits.invocation(attempt.correlationId, this.options.role, 'delivered');
|
|
301
286
|
return attempt;
|
|
302
287
|
});
|
|
@@ -309,26 +294,34 @@ export class OwnerChannel {
|
|
|
309
294
|
class: input.class, effect: input.effect,
|
|
310
295
|
...(input.exitCode === undefined ? {} : { exitCode: input.exitCode }),
|
|
311
296
|
...(input.resourceIds ? { resourceIds: input.resourceIds } : {}),
|
|
312
|
-
...(input.
|
|
297
|
+
...(input.presentations ? { presentations: input.presentations } : {}),
|
|
313
298
|
});
|
|
314
299
|
if (attempt.outcome?.delivery === 'delivered')
|
|
315
300
|
return attempt;
|
|
316
301
|
if (attempt.outcome?.delivery === 'uncertain')
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
this.commandAudits.outcome(attempt.correlationId, this.options.role, 'uncertain');
|
|
320
|
-
this.options.log(`[${this.options.role}] fleet command outcome ${attempt.correlationId} `
|
|
321
|
-
+ `delivery unavailable after execution; effect status=${attempt.outcome?.effect}`);
|
|
322
|
-
throw new Error(`owner-channel unavailable after execution; effect status=${attempt.outcome?.effect}`);
|
|
323
|
-
}
|
|
302
|
+
return attempt;
|
|
303
|
+
let uncertain = false;
|
|
324
304
|
try {
|
|
325
|
-
|
|
305
|
+
for (const event of attempt.outcome?.presentations ?? []) {
|
|
306
|
+
try {
|
|
307
|
+
await this.sendProactiveMessage(renderFleetLifecycleEvent(event), `fleet-lifecycle\0${lifecycleEventDigestBasis(event)}`, 0);
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
if (error instanceof DuplicateSendError) {
|
|
311
|
+
if (error.status !== 'delivered')
|
|
312
|
+
uncertain = true;
|
|
313
|
+
this.options.log(`[${this.options.role}] duplicate Fleet lifecycle event suppressed`);
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
throw error;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
326
319
|
}
|
|
327
320
|
catch (error) {
|
|
328
|
-
|
|
329
|
-
|
|
321
|
+
uncertain = true;
|
|
322
|
+
this.logError(`fleet lifecycle delivery uncertain correlation=${attempt.correlationId}`, error);
|
|
330
323
|
}
|
|
331
|
-
attempt = this.commandAudits.outcome(attempt.correlationId, this.options.role, 'delivered');
|
|
324
|
+
attempt = this.commandAudits.outcome(attempt.correlationId, this.options.role, uncertain ? 'uncertain' : 'delivered');
|
|
332
325
|
return attempt;
|
|
333
326
|
});
|
|
334
327
|
this.managementTail = run.then(() => undefined, () => undefined);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** A send whose (dedupe-scoped) digest was already recorded: it must not repeat. */
|
|
2
2
|
export declare class DuplicateSendError extends Error {
|
|
3
|
+
readonly status: OwnerProactiveSend['status'];
|
|
4
|
+
constructor(status: OwnerProactiveSend['status']);
|
|
3
5
|
}
|
|
4
6
|
/** Durable bounded dedupe containing wire IDs only — never message or reply plaintext. */
|
|
5
7
|
export declare class OwnerChannelState {
|
|
@@ -5,6 +5,11 @@ import { replaceFileAtomically } from '../atomic-file.js';
|
|
|
5
5
|
import { canonicalCid } from '../config.js';
|
|
6
6
|
/** A send whose (dedupe-scoped) digest was already recorded: it must not repeat. */
|
|
7
7
|
export class DuplicateSendError extends Error {
|
|
8
|
+
status;
|
|
9
|
+
constructor(status) {
|
|
10
|
+
super('duplicate proactive owner message refused');
|
|
11
|
+
this.status = status;
|
|
12
|
+
}
|
|
8
13
|
}
|
|
9
14
|
/** Durable bounded dedupe containing wire IDs only — never message or reply plaintext. */
|
|
10
15
|
export class OwnerChannelState {
|
|
@@ -184,8 +189,9 @@ export class OwnerConversationState {
|
|
|
184
189
|
// 'all' scope serves wire-keyed idempotency: a crash replay must not
|
|
185
190
|
// deliver the same wire to a second owner after the route moved.
|
|
186
191
|
const scope = dedupe === 'all' ? this.sends : recent;
|
|
187
|
-
|
|
188
|
-
|
|
192
|
+
const duplicate = scope.find(send => send.digest === digest);
|
|
193
|
+
if (duplicate)
|
|
194
|
+
throw new DuplicateSendError(duplicate.status);
|
|
189
195
|
const last = recent.at(-1);
|
|
190
196
|
if (last && now - last.at < minIntervalMs)
|
|
191
197
|
throw new Error(`proactive owner messages are rate-limited to one every ${minIntervalMs}ms`);
|