@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
package/dist/rooms-tasks/cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
2
3
|
import { loadConfig, ConfigError } from '../config.js';
|
|
3
4
|
import { provisionMembers, getBinPath } from './provision.js';
|
|
4
5
|
import { launchFleetWorker } from './external-worker.js';
|
|
@@ -9,6 +10,7 @@ import { createCoworkAdapter, CoworkProtocolError } from './cowork-adapter.js';
|
|
|
9
10
|
import { markdownCode, markdownProse, renderMarkdownFailure, renderMarkdownList, renderMarkdownResult, roomStatus, taskStatus, } from './markdown.js';
|
|
10
11
|
import { TaskRoomApplicationError, TaskRoomApplicationService } from '../application/task-room-service.js';
|
|
11
12
|
import { TaskListError } from './task-lists.js';
|
|
13
|
+
import { recordFleetAuditFailure, recordFleetAuditPresentation, recordFleetAuditResource, } from '../fleet-command-audit.js';
|
|
12
14
|
class TaskRoomPublicError extends Error {
|
|
13
15
|
code;
|
|
14
16
|
fields;
|
|
@@ -89,8 +91,29 @@ function die(e) {
|
|
|
89
91
|
const msg = e instanceof TaskRoomPublicError
|
|
90
92
|
? taskRoomPublicFailure(e).legacy : e instanceof Error ? e.message : String(e);
|
|
91
93
|
process.stderr.write(`error: ${msg}\n`);
|
|
94
|
+
recordFleetAuditFailure(classifyTaskRoomFailure(e));
|
|
92
95
|
process.exit(1);
|
|
93
96
|
}
|
|
97
|
+
function classifyTaskRoomFailure(e) {
|
|
98
|
+
if (e instanceof TaskRoomApplicationError)
|
|
99
|
+
e = taskRoomPublicError(e.code, e.fields);
|
|
100
|
+
if (e instanceof TaskRoomPublicError || e instanceof TaskListError || e instanceof ConfigError)
|
|
101
|
+
return { class: 'validation', effect: 'not_started' };
|
|
102
|
+
if (e instanceof TaskStateError) {
|
|
103
|
+
const known = /^task not found: [A-Za-z0-9_-]{1,128}$/u.test(e.message)
|
|
104
|
+
|| e.message === 'invalid task ID: expected the canonical 17-character lowercase ID'
|
|
105
|
+
|| isKnownTaskStateMessage(e.message);
|
|
106
|
+
return known ? { class: 'validation', effect: 'not_started' }
|
|
107
|
+
: { class: 'runtime', effect: 'unknown' };
|
|
108
|
+
}
|
|
109
|
+
if (e instanceof RoomStateError) {
|
|
110
|
+
const known = /^room not found: [A-Za-z0-9_-]{1,128}$/u.test(e.message)
|
|
111
|
+
|| isKnownRoomStateMessage(e.message);
|
|
112
|
+
return known ? { class: 'validation', effect: 'not_started' }
|
|
113
|
+
: { class: 'runtime', effect: 'unknown' };
|
|
114
|
+
}
|
|
115
|
+
return { class: 'runtime', effect: 'unknown' };
|
|
116
|
+
}
|
|
94
117
|
const TASK_STATE_WORD = '(?:backlog|provisioning|active|review|done|cancelled|failed)';
|
|
95
118
|
const SAFE_ID_WORD = '[A-Za-z0-9_-]{1,128}';
|
|
96
119
|
function isKnownTaskStateMessage(message) {
|
|
@@ -127,6 +150,7 @@ function dieTaskRoom(e) {
|
|
|
127
150
|
action: failure.action,
|
|
128
151
|
});
|
|
129
152
|
process.stderr.write(`${output}\n`);
|
|
153
|
+
recordFleetAuditFailure({ class: 'validation', effect: 'not_started' });
|
|
130
154
|
process.exit(1);
|
|
131
155
|
}
|
|
132
156
|
if (e instanceof TaskListError) {
|
|
@@ -142,6 +166,7 @@ function dieTaskRoom(e) {
|
|
|
142
166
|
: 'Use a valid NFC-normalized name of at most 64 Unicode code points.',
|
|
143
167
|
});
|
|
144
168
|
process.stderr.write(`${output}\n`);
|
|
169
|
+
recordFleetAuditFailure({ class: 'validation', effect: 'not_started' });
|
|
145
170
|
process.exit(1);
|
|
146
171
|
}
|
|
147
172
|
const taskMissing = e instanceof TaskStateError
|
|
@@ -167,6 +192,7 @@ function dieTaskRoom(e) {
|
|
|
167
192
|
: 'Retry once; if it repeats, run ours-fleet doctor and inspect the role logs.',
|
|
168
193
|
});
|
|
169
194
|
process.stderr.write(`${output}\n`);
|
|
195
|
+
recordFleetAuditFailure(classifyTaskRoomFailure(e));
|
|
170
196
|
process.exit(1);
|
|
171
197
|
}
|
|
172
198
|
const taskListMarkdown = (tasks, title = 'Tasks') => renderMarkdownList({
|
|
@@ -175,23 +201,61 @@ const taskListMarkdown = (tasks, title = 'Tasks') => renderMarkdownList({
|
|
|
175
201
|
+ ` — List ${markdownCode(t.list_name ?? 'default')}`
|
|
176
202
|
+ (t.blocked ? ` — 🚧 Blocked: ${markdownProse(t.blocked.reason)}` : '')),
|
|
177
203
|
});
|
|
178
|
-
const taskActionMarkdown = (title, task, fields = []) =>
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
204
|
+
const taskActionMarkdown = (title, task, fields = []) => {
|
|
205
|
+
recordFleetAuditResource('task', task.task_id);
|
|
206
|
+
if (task.room_id)
|
|
207
|
+
recordFleetAuditResource('room', task.room_id);
|
|
208
|
+
return renderMarkdownResult({
|
|
209
|
+
icon: '📋', title,
|
|
210
|
+
fields: [
|
|
211
|
+
{ label: 'ID', value: task.task_id, kind: 'code' },
|
|
212
|
+
{ label: 'Status', value: taskStatus(task.state), kind: 'markdown' },
|
|
213
|
+
{ label: 'List', value: task.list_name ?? 'default', kind: 'code' },
|
|
214
|
+
...fields,
|
|
215
|
+
],
|
|
216
|
+
});
|
|
217
|
+
};
|
|
218
|
+
function safeBrainSummary(definition) {
|
|
219
|
+
const brain = definition?.brain;
|
|
220
|
+
if (!brain || typeof brain !== 'object' || Array.isArray(brain))
|
|
221
|
+
return 'unresolved';
|
|
222
|
+
const selection = brain;
|
|
223
|
+
if (typeof selection.ref === 'string')
|
|
224
|
+
return `ref:${selection.ref}`;
|
|
225
|
+
if (!Object.hasOwn(selection, 'inline'))
|
|
226
|
+
return 'unresolved';
|
|
227
|
+
const fingerprint = createHash('sha256').update(JSON.stringify(selection.inline)).digest('hex').slice(0, 16);
|
|
228
|
+
return `inline:sha256:${fingerprint}`;
|
|
229
|
+
}
|
|
230
|
+
function auditTask(operation, task, previousState, newState = task.state) {
|
|
231
|
+
recordFleetAuditResource('task', task.task_id);
|
|
232
|
+
if (task.room_id)
|
|
233
|
+
recordFleetAuditResource('room', task.room_id);
|
|
234
|
+
const room = task.room_id ? getRoomRecord(task.room_id) : undefined;
|
|
235
|
+
const brains = new Map(room?.member_seats.map(seat => [seat.role_name, safeBrainSummary(seat.launch?.agent_definition)]) ?? []);
|
|
236
|
+
recordFleetAuditPresentation({ kind: 'task', operation, id: task.task_id, title: task.title,
|
|
237
|
+
previousState, newState,
|
|
238
|
+
template: task.template ? `${task.template.name}@${task.template.version}` : undefined,
|
|
239
|
+
roomId: task.room_id, agents: task.member_roles.map(member => ({ name: member.name,
|
|
240
|
+
brain: brains.get(member.name) ?? 'unresolved', role: member.cowork_role })) });
|
|
241
|
+
}
|
|
242
|
+
function auditRoom(operation, room, previousState, newState = room.state) {
|
|
243
|
+
recordFleetAuditResource('room', room.room_id);
|
|
244
|
+
recordFleetAuditPresentation({ kind: 'room', operation, id: room.room_id, previousState, newState,
|
|
245
|
+
template: room.template_snapshot ? `${room.template_snapshot.name}@${room.template_snapshot.version}` : undefined,
|
|
246
|
+
participants: room.member_seats.map(member => ({ name: member.role_name, role: member.cowork_role })) });
|
|
247
|
+
}
|
|
248
|
+
const roomActionMarkdown = (title, room, fields = []) => {
|
|
249
|
+
recordFleetAuditResource('room', room.room_id);
|
|
250
|
+
return renderMarkdownResult({
|
|
251
|
+
icon: '🏠', title,
|
|
252
|
+
fields: [
|
|
253
|
+
{ label: 'ID', value: room.room_id, kind: 'code' },
|
|
254
|
+
{ label: 'Status', value: roomStatus(room.state), kind: 'markdown' },
|
|
255
|
+
...fields,
|
|
256
|
+
],
|
|
257
|
+
});
|
|
258
|
+
};
|
|
195
259
|
const PUBLIC_SETTLE_WAIT_MS = 60_000;
|
|
196
260
|
const PUBLIC_SETTLE_POLL_MS = 100;
|
|
197
261
|
const sleep = (ms) => new Promise(resolve => { setTimeout(resolve, ms); });
|
|
@@ -465,6 +529,7 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
465
529
|
idempotencyKey: opts.idempotencyKey, origin: { type: 'cli' },
|
|
466
530
|
list: opts.list,
|
|
467
531
|
});
|
|
532
|
+
auditTask('create', record, 'none');
|
|
468
533
|
if (opts.json) {
|
|
469
534
|
console.log(JSON.stringify({ schema_version: 1, task: record }, null, 2));
|
|
470
535
|
return;
|
|
@@ -651,9 +716,11 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
651
716
|
.option('--json', 'JSON output')
|
|
652
717
|
.action(async (id, opts) => {
|
|
653
718
|
try {
|
|
719
|
+
const previous = getTask(id).state;
|
|
654
720
|
const t = await taskRoomService(opts.configuration).startTask({
|
|
655
721
|
actor: { kind: 'local_control', surface: 'cli' }, taskId: id,
|
|
656
722
|
});
|
|
723
|
+
auditTask('start', t, previous);
|
|
657
724
|
if (opts.json) {
|
|
658
725
|
console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
|
|
659
726
|
return;
|
|
@@ -677,9 +744,11 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
677
744
|
.option('--json', 'JSON output')
|
|
678
745
|
.action((id, opts) => {
|
|
679
746
|
try {
|
|
747
|
+
const previous = getTask(id).state;
|
|
680
748
|
const t = taskRoomService().blockTask({
|
|
681
749
|
actor: { kind: 'local_control', surface: 'cli' }, taskId: id, reason: opts.reason,
|
|
682
750
|
});
|
|
751
|
+
auditTask('block', t, previous);
|
|
683
752
|
if (opts.json) {
|
|
684
753
|
console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
|
|
685
754
|
return;
|
|
@@ -697,9 +766,11 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
697
766
|
.option('--json', 'JSON output')
|
|
698
767
|
.action((id, opts) => {
|
|
699
768
|
try {
|
|
769
|
+
const previous = getTask(id).state;
|
|
700
770
|
const t = taskRoomService().unblockTask({
|
|
701
771
|
actor: { kind: 'local_control', surface: 'cli' }, taskId: id,
|
|
702
772
|
});
|
|
773
|
+
auditTask('unblock', t, previous);
|
|
703
774
|
if (opts.json) {
|
|
704
775
|
console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
|
|
705
776
|
return;
|
|
@@ -717,9 +788,11 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
717
788
|
.option('--json', 'JSON output')
|
|
718
789
|
.action((id, opts) => {
|
|
719
790
|
try {
|
|
791
|
+
const previous = getTask(id).state;
|
|
720
792
|
const t = taskRoomService().reviewTask({
|
|
721
793
|
actor: { kind: 'local_control', surface: 'cli' }, taskId: id,
|
|
722
794
|
});
|
|
795
|
+
auditTask('review', t, previous);
|
|
723
796
|
if (opts.json) {
|
|
724
797
|
console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
|
|
725
798
|
return;
|
|
@@ -739,6 +812,7 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
739
812
|
.option('--json', 'JSON output')
|
|
740
813
|
.action(async (id, opts) => {
|
|
741
814
|
try {
|
|
815
|
+
const previous = getTask(id).state;
|
|
742
816
|
let summary = opts.summary;
|
|
743
817
|
if (opts.summaryFile)
|
|
744
818
|
summary = readFileSync(opts.summaryFile, 'utf8');
|
|
@@ -750,6 +824,7 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
750
824
|
? await launchTaskSettleWorker(id, opts.configuration)
|
|
751
825
|
: { task: plan.task, timedOut: false };
|
|
752
826
|
const t = settled.task;
|
|
827
|
+
auditTask('done', t, previous);
|
|
753
828
|
if (opts.json) {
|
|
754
829
|
console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
|
|
755
830
|
return;
|
|
@@ -777,6 +852,7 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
777
852
|
try {
|
|
778
853
|
if (id !== confirmId)
|
|
779
854
|
throw taskRoomPublicError('task_confirmation_mismatch');
|
|
855
|
+
const previous = getTask(id).state;
|
|
780
856
|
const plan = await taskRoomService(opts.configuration).cancelTask({
|
|
781
857
|
actor: { kind: 'local_control', surface: 'cli' }, taskId: id,
|
|
782
858
|
});
|
|
@@ -784,6 +860,7 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
784
860
|
? await launchTaskSettleWorker(id, opts.configuration)
|
|
785
861
|
: { task: plan.task, timedOut: false };
|
|
786
862
|
const t = settled.task;
|
|
863
|
+
auditTask('cancel', t, previous);
|
|
787
864
|
if (opts.json) {
|
|
788
865
|
console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
|
|
789
866
|
return;
|
|
@@ -811,9 +888,16 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
811
888
|
try {
|
|
812
889
|
if (id !== confirmId)
|
|
813
890
|
throw taskRoomPublicError('task_confirmation_mismatch');
|
|
891
|
+
let prior;
|
|
892
|
+
try {
|
|
893
|
+
prior = getTask(id);
|
|
894
|
+
}
|
|
895
|
+
catch { /* idempotent already-absent delete */ }
|
|
814
896
|
const deleted = taskRoomService().deleteTask({
|
|
815
897
|
actor: { kind: 'local_control', surface: 'cli' }, taskId: id,
|
|
816
898
|
});
|
|
899
|
+
if (prior)
|
|
900
|
+
auditTask('delete', prior, prior.state, 'deleted');
|
|
817
901
|
if (opts.json) {
|
|
818
902
|
console.log(JSON.stringify({ schema_version: 1, task_id: id, deleted }, null, 2));
|
|
819
903
|
return;
|
|
@@ -834,6 +918,7 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
834
918
|
.option('--json', 'JSON output')
|
|
835
919
|
.action(async (id, opts) => {
|
|
836
920
|
try {
|
|
921
|
+
const previous = getTask(id).state;
|
|
837
922
|
const app = taskRoomService(opts.configuration);
|
|
838
923
|
const actor = { kind: 'local_control', surface: 'cli' };
|
|
839
924
|
const begin = await app.beginTaskRecovery({ actor, taskId: id });
|
|
@@ -844,6 +929,7 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
844
929
|
})()
|
|
845
930
|
: begin.result;
|
|
846
931
|
const t = recovered.task;
|
|
932
|
+
auditTask('recover', t, previous);
|
|
847
933
|
const room = recovered.room;
|
|
848
934
|
const recoveryActions = recovered.issues.map(issue => {
|
|
849
935
|
if (issue.code === 'terminal_pending')
|
|
@@ -951,10 +1037,12 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
951
1037
|
.option('--json', 'JSON output')
|
|
952
1038
|
.action(async (id, opts) => {
|
|
953
1039
|
try {
|
|
1040
|
+
const previous = getTask(id).state;
|
|
954
1041
|
const result = await taskRoomService(opts.configuration).ensureTaskWork({
|
|
955
1042
|
actor: { kind: 'local_control', surface: 'cli' }, taskId: id, template: opts.template,
|
|
956
1043
|
});
|
|
957
1044
|
const t = result.task;
|
|
1045
|
+
auditTask('work', t, previous);
|
|
958
1046
|
if (result.status === 'already_active') {
|
|
959
1047
|
if (opts.json) {
|
|
960
1048
|
console.log(JSON.stringify({ schema_version: 1, task: t, status: 'already_active' }, null, 2));
|
|
@@ -993,6 +1081,7 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
993
1081
|
.option('--json', 'JSON output')
|
|
994
1082
|
.action(async (id, opts) => {
|
|
995
1083
|
try {
|
|
1084
|
+
const previous = getTask(id).state;
|
|
996
1085
|
let summary = opts.summary;
|
|
997
1086
|
if (opts.summaryFile)
|
|
998
1087
|
summary = readFileSync(opts.summaryFile, 'utf8');
|
|
@@ -1004,6 +1093,7 @@ export function registerTaskCommands(parent, cOpt) {
|
|
|
1004
1093
|
? await launchTaskSettleWorker(id, opts.configuration)
|
|
1005
1094
|
: { task: plan.task, timedOut: false };
|
|
1006
1095
|
const t = settled.task;
|
|
1096
|
+
auditTask('finish', t, previous);
|
|
1007
1097
|
if (opts.json) {
|
|
1008
1098
|
console.log(JSON.stringify({ schema_version: 1, task: t }, null, 2));
|
|
1009
1099
|
return;
|
|
@@ -1043,6 +1133,7 @@ export function registerRoomCommands(parent, cOpt) {
|
|
|
1043
1133
|
actor: { kind: 'local_control', surface: 'cli' }, name: opts.name,
|
|
1044
1134
|
template: opts.template, goal: opts.goal, brief: opts.brief, briefFile: opts.briefFile,
|
|
1045
1135
|
});
|
|
1136
|
+
auditRoom('create', record, 'none');
|
|
1046
1137
|
if (opts.json) {
|
|
1047
1138
|
console.log(JSON.stringify({ schema_version: 1, room: record }, null, 2));
|
|
1048
1139
|
return;
|
|
@@ -1127,7 +1218,9 @@ export function registerRoomCommands(parent, cOpt) {
|
|
|
1127
1218
|
.option('--json', 'JSON output')
|
|
1128
1219
|
.action(async (id, opts) => {
|
|
1129
1220
|
try {
|
|
1130
|
-
const { room } = await taskRoomService(opts.configuration).getRoomDetail(id);
|
|
1221
|
+
const { room, orchestration } = await taskRoomService(opts.configuration).getRoomDetail(id);
|
|
1222
|
+
if (orchestration)
|
|
1223
|
+
auditRoom('open', orchestration, orchestration.state);
|
|
1131
1224
|
const url = `http://localhost:4460/room/${id}`;
|
|
1132
1225
|
if (opts.json) {
|
|
1133
1226
|
console.log(JSON.stringify({ schema_version: 1, room_id: id, url, room_name: room.room_name }, null, 2));
|
|
@@ -1154,6 +1247,8 @@ export function registerRoomCommands(parent, cOpt) {
|
|
|
1154
1247
|
.action(async (id, opts) => {
|
|
1155
1248
|
try {
|
|
1156
1249
|
const { orchestration: r, members } = await taskRoomService(opts.configuration).getRoomMembers(id);
|
|
1250
|
+
if (r)
|
|
1251
|
+
auditRoom('members', r, r.state);
|
|
1157
1252
|
if (opts.json) {
|
|
1158
1253
|
console.log(JSON.stringify({
|
|
1159
1254
|
schema_version: 1,
|
|
@@ -1187,10 +1282,13 @@ export function registerRoomCommands(parent, cOpt) {
|
|
|
1187
1282
|
try {
|
|
1188
1283
|
if (id !== confirmId)
|
|
1189
1284
|
throw taskRoomPublicError('room_confirmation_mismatch');
|
|
1285
|
+
const previous = getRoomRecord(id);
|
|
1190
1286
|
await taskRoomService(opts.configuration).requestRoomDeletion({
|
|
1191
1287
|
actor: { kind: 'local_control', surface: 'cli' }, roomId: id,
|
|
1192
1288
|
});
|
|
1193
1289
|
const settled = await launchRoomDeleteWorker(id, opts.configuration);
|
|
1290
|
+
if (previous)
|
|
1291
|
+
auditRoom('delete', previous, previous.state, settled.deleted ? 'deleted' : 'closing');
|
|
1194
1292
|
if (opts.json) {
|
|
1195
1293
|
console.log(JSON.stringify({ schema_version: 1, room_id: id, deleted: settled.deleted }, null, 2));
|
|
1196
1294
|
return;
|
|
@@ -1227,11 +1325,14 @@ export function registerRoomCommands(parent, cOpt) {
|
|
|
1227
1325
|
.option('--json', 'JSON output')
|
|
1228
1326
|
.action(async (id, opts) => {
|
|
1229
1327
|
try {
|
|
1328
|
+
const previous = getRoomRecord(id);
|
|
1230
1329
|
const recovered = await taskRoomService(opts.configuration).recoverRoom({
|
|
1231
1330
|
actor: { kind: 'local_control', surface: 'cli' }, roomId: id,
|
|
1232
1331
|
});
|
|
1233
1332
|
if (recovered.kind === 'deletion_worker_required') {
|
|
1234
1333
|
const settled = await launchRoomDeleteWorker(id, opts.configuration);
|
|
1334
|
+
if (previous)
|
|
1335
|
+
auditRoom('recover', previous, previous.state, settled.deleted ? 'deleted' : 'closing');
|
|
1235
1336
|
if (opts.json) {
|
|
1236
1337
|
console.log(JSON.stringify({
|
|
1237
1338
|
schema_version: 1, room: null, orchestration: null,
|
|
@@ -1252,6 +1353,8 @@ export function registerRoomCommands(parent, cOpt) {
|
|
|
1252
1353
|
return;
|
|
1253
1354
|
}
|
|
1254
1355
|
const { room: cowork, orchestration: r, issues: actions } = recovered;
|
|
1356
|
+
if (r)
|
|
1357
|
+
auditRoom('recover', r, previous?.state ?? 'unresolved', r.state);
|
|
1255
1358
|
if (opts.json) {
|
|
1256
1359
|
console.log(JSON.stringify({ schema_version: 1, room: cowork, orchestration: r ?? null, recovery_actions: actions }, null, 2));
|
|
1257
1360
|
return;
|
|
@@ -1,2 +1,3 @@
|
|
|
1
|
+
export declare function fleetWorkerEnv(source?: NodeJS.ProcessEnv): Record<string, string>;
|
|
1
2
|
/** Launch a Fleet CLI operation outside the caller's supervisor/session lifecycle. */
|
|
2
3
|
export declare function launchFleetWorker(args: string[], operation: string, configPath?: string): Promise<void>;
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
import { execFile, spawn } from 'node:child_process';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
-
|
|
4
|
-
export function
|
|
5
|
-
const cli = [process.argv[1], ...args, ...(configPath ? ['-c', configPath] : [])];
|
|
3
|
+
import { FLEET_PROXY_CALLER_ENV, FLEET_PROXY_STATE_DIR_ENV } from '../fleet-proxy.js';
|
|
4
|
+
export function fleetWorkerEnv(source = process.env) {
|
|
6
5
|
const inheritedKeys = [
|
|
7
6
|
'HOME', 'PATH', 'XDG_RUNTIME_DIR', 'OURS_FLEET_HOME', 'OURS_CONFIG',
|
|
8
7
|
'OURS_PORT', 'OURS_STATE_DIR', 'OURS_API_TOKEN', 'OURS_COWORK_CONFIG',
|
|
9
8
|
];
|
|
10
|
-
const
|
|
11
|
-
|
|
9
|
+
const env = Object.fromEntries(inheritedKeys.flatMap(key => source[key] === undefined ? [] : [[key, source[key]]]));
|
|
10
|
+
// Explicit defense: trusted settle/recover/delete/close workers are effects
|
|
11
|
+
// nested inside the outer audited command, never fresh agent attempts.
|
|
12
|
+
delete env[FLEET_PROXY_STATE_DIR_ENV];
|
|
13
|
+
delete env[FLEET_PROXY_CALLER_ENV];
|
|
14
|
+
return env;
|
|
15
|
+
}
|
|
16
|
+
/** Launch a Fleet CLI operation outside the caller's supervisor/session lifecycle. */
|
|
17
|
+
export function launchFleetWorker(args, operation, configPath) {
|
|
18
|
+
const cli = [process.argv[1], ...args, ...(configPath ? ['-c', configPath] : [])];
|
|
19
|
+
const env = fleetWorkerEnv();
|
|
20
|
+
const inherited = Object.entries(env).map(([key, value]) => `${key}=${value}`);
|
|
12
21
|
const suffix = randomUUID().slice(0, 8);
|
|
13
22
|
const safeOperation = operation.replace(/[^A-Za-z0-9_.-]/g, '-');
|
|
14
23
|
return new Promise((resolve, reject) => {
|
|
@@ -29,10 +38,6 @@ export function launchFleetWorker(args, operation, configPath) {
|
|
|
29
38
|
], { timeout: 15_000 }, error => error ? reject(error) : resolve());
|
|
30
39
|
return;
|
|
31
40
|
}
|
|
32
|
-
const env = Object.fromEntries(inherited.map(value => {
|
|
33
|
-
const split = value.indexOf('=');
|
|
34
|
-
return [value.slice(0, split), value.slice(split + 1)];
|
|
35
|
-
}));
|
|
36
41
|
const child = spawn(process.execPath, cli, { detached: true, stdio: 'ignore', env });
|
|
37
42
|
child.once('error', reject);
|
|
38
43
|
child.once('spawn', () => { child.unref(); resolve(); });
|
package/dist/runner.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export interface RunnerDeps {
|
|
|
23
23
|
/** Neutral construction seam for deterministic runner lifecycle tests. */
|
|
24
24
|
startAgentSession(adapter: AgentSessionAdapter, options: AgentSessionStartOptions): Promise<AgentSession>;
|
|
25
25
|
/** Construct the authenticated role control route (injectable where sockets are unavailable). */
|
|
26
|
-
createControlServer(stateDir: string, session: AgentSession, log: (line: string) => void): Pick<RoleControlServer, 'start' | 'close' | 'setFleetSpawner' | 'setOwnerChannel' | 'setConfigReloader' | 'setLoopManager'>;
|
|
26
|
+
createControlServer(stateDir: string, session: AgentSession, log: (line: string) => void): Pick<RoleControlServer, 'start' | 'close' | 'setFleetSpawner' | 'setFleetAuditor' | 'setOwnerChannel' | 'setConfigReloader' | 'setLoopManager'>;
|
|
27
27
|
/** Acquire the cross-process owner-channel binder lease before replacing the control socket. */
|
|
28
28
|
acquireOwnerBinder(stateDir: string, role: string, identity: string): Promise<OwnerBinderLease>;
|
|
29
29
|
/** Ask the still-authenticated predecessor to emit the fixed recovery notice. */
|
package/dist/runner.js
CHANGED
|
@@ -544,7 +544,6 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
544
544
|
let sessionClosed = false;
|
|
545
545
|
let ownerChannel;
|
|
546
546
|
let ownerBinder;
|
|
547
|
-
const pendingFleetSpawnNotices = [];
|
|
548
547
|
let loopManager;
|
|
549
548
|
let arbiter;
|
|
550
549
|
let reloadLoopConfig;
|
|
@@ -607,21 +606,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
607
606
|
throw error;
|
|
608
607
|
}
|
|
609
608
|
control.setFleetSpawner(async (requested) => {
|
|
610
|
-
|
|
611
|
-
if (!role.owner_channel)
|
|
612
|
-
return event;
|
|
613
|
-
if (ownerChannel?.notifyFleetSpawn) {
|
|
614
|
-
try {
|
|
615
|
-
await ownerChannel.notifyFleetSpawn(event);
|
|
616
|
-
}
|
|
617
|
-
catch (error) {
|
|
618
|
-
deps.log(`[${name}] spawned-agent owner notice failed: `
|
|
619
|
-
+ `${error?.message ?? String(error)}`);
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
else
|
|
623
|
-
pendingFleetSpawnNotices.push(event);
|
|
624
|
-
return event;
|
|
609
|
+
return executeManagedSpawn(role, configPath, requested, deps.log);
|
|
625
610
|
});
|
|
626
611
|
resolvedMonitorDeps.delivery = {
|
|
627
612
|
// A wake is only delivered when its turn TERMINATES successfully. A
|
|
@@ -743,18 +728,14 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
743
728
|
throw new Error(`[${name}] owner channel failed to start: `
|
|
744
729
|
+ `${error?.message ?? String(error)}`);
|
|
745
730
|
}
|
|
746
|
-
for (const event of pendingFleetSpawnNotices.splice(0)) {
|
|
747
|
-
try {
|
|
748
|
-
await ownerChannel.notifyFleetSpawn?.(event);
|
|
749
|
-
}
|
|
750
|
-
catch (error) {
|
|
751
|
-
deps.log(`[${name}] deferred spawned-agent owner notice failed: `
|
|
752
|
-
+ `${error?.message ?? String(error)}`);
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
731
|
}
|
|
756
|
-
if (ownerChannel)
|
|
732
|
+
if (ownerChannel) {
|
|
757
733
|
control.setOwnerChannel(ownerChannel);
|
|
734
|
+
control.setFleetAuditor({
|
|
735
|
+
begin: (requestId, argv) => ownerChannel.beginFleetCommandAudit(requestId, argv),
|
|
736
|
+
finish: input => ownerChannel.finishFleetCommandAudit(input),
|
|
737
|
+
});
|
|
738
|
+
}
|
|
758
739
|
reloadLoopConfig = async () => {
|
|
759
740
|
const nextRole = findRole(loadConfig(configPath), name);
|
|
760
741
|
const definitions = nextRole.loops ?? [];
|
|
@@ -4,11 +4,12 @@ import type { OwnerChannelHandle, OwnerChannelManagementRequest } from '../owner
|
|
|
4
4
|
import type { ScheduledLoopManagerHandle } from '../loops/manager.js';
|
|
5
5
|
import type { SpawnOpts } from '../spawn.js';
|
|
6
6
|
import type { ManagedFleetSpawnResult } from '../fleet-proxy.js';
|
|
7
|
+
import { type FleetAuditPresentation, type FleetCommandOutcomeClass } from '../fleet-command-audit.js';
|
|
7
8
|
export interface ControlRequest {
|
|
8
9
|
version: 1 | 2 | 3;
|
|
9
10
|
id: string;
|
|
10
11
|
token: string;
|
|
11
|
-
command: 'status' | 'snapshot' | 'submit_prompt' | 'respond_permission' | 'interrupt' | 'follow' | 'events_since' | 'owner_channel_manage' | 'loop_status' | 'loop_run_now' | 'loop_disable' | 'loop_enable' | 'reload_config' | 'conversation_page' | 'conversation_follow' | 'submit_prompt_v2' | 'interrupt_v2' | 'respond_permission_v2' | 'fleet_spawn';
|
|
12
|
+
command: 'status' | 'snapshot' | 'submit_prompt' | 'respond_permission' | 'interrupt' | 'follow' | 'events_since' | 'owner_channel_manage' | 'loop_status' | 'loop_run_now' | 'loop_disable' | 'loop_enable' | 'reload_config' | 'conversation_page' | 'conversation_follow' | 'submit_prompt_v2' | 'interrupt_v2' | 'respond_permission_v2' | 'fleet_spawn' | 'fleet_audit_begin' | 'fleet_audit_finish';
|
|
12
13
|
text?: string;
|
|
13
14
|
permissionId?: string;
|
|
14
15
|
optionId?: string;
|
|
@@ -30,6 +31,16 @@ export interface ControlRequest {
|
|
|
30
31
|
sessionGeneration?: string;
|
|
31
32
|
/** Typed managed-spawn request accepted only over the authenticated role control plane. */
|
|
32
33
|
spawn?: SpawnOpts;
|
|
34
|
+
audit?: {
|
|
35
|
+
requestId?: string;
|
|
36
|
+
argv?: string[];
|
|
37
|
+
correlationId?: string;
|
|
38
|
+
class?: FleetCommandOutcomeClass;
|
|
39
|
+
exitCode?: number;
|
|
40
|
+
effect?: 'not_started' | 'completed' | 'unknown';
|
|
41
|
+
resourceIds?: Record<string, string>;
|
|
42
|
+
presentation?: FleetAuditPresentation;
|
|
43
|
+
};
|
|
33
44
|
}
|
|
34
45
|
export interface ControlResponse {
|
|
35
46
|
version: 1;
|
|
@@ -105,6 +116,7 @@ export declare class RoleControlServer {
|
|
|
105
116
|
private loopManager?;
|
|
106
117
|
private reloadConfig?;
|
|
107
118
|
private fleetSpawner?;
|
|
119
|
+
private fleetAuditor?;
|
|
108
120
|
constructor(stateDir: string, session: AgentSession, log: (line: string) => void);
|
|
109
121
|
start(): Promise<void>;
|
|
110
122
|
close(): Promise<void>;
|
|
@@ -113,6 +125,7 @@ export declare class RoleControlServer {
|
|
|
113
125
|
setLoopManager(loopManager: ScheduledLoopManagerHandle | undefined): void;
|
|
114
126
|
setConfigReloader(reloadConfig: (() => Promise<unknown>) | undefined): void;
|
|
115
127
|
setFleetSpawner(fleetSpawner: ((options: SpawnOpts) => Promise<ManagedFleetSpawnResult>) | undefined): void;
|
|
128
|
+
setFleetAuditor(auditor: RoleControlServer['fleetAuditor']): void;
|
|
116
129
|
private accept;
|
|
117
130
|
private handle;
|
|
118
131
|
/** Version and capability gate shared by every conversation v3 command. */
|
package/dist/session/control.js
CHANGED
|
@@ -3,6 +3,7 @@ import { chmodSync, existsSync, readFileSync, rmSync, writeFileSync } from 'node
|
|
|
3
3
|
import { createConnection, createServer } from 'node:net';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { SessionControlError } from './types.js';
|
|
6
|
+
import { validateFleetAuditBegin, validateFleetAuditFinish, } from '../fleet-command-audit.js';
|
|
6
7
|
import { interruptSession, queueSessionPrompt, respondSessionPermission, respondSessionPermissionV2, } from '../application/session-mutations.js';
|
|
7
8
|
const MAX_LINE_BYTES = 64 * 1024;
|
|
8
9
|
/** Commands that require protocol version 3. */
|
|
@@ -119,6 +120,7 @@ export class RoleControlServer {
|
|
|
119
120
|
loopManager;
|
|
120
121
|
reloadConfig;
|
|
121
122
|
fleetSpawner;
|
|
123
|
+
fleetAuditor;
|
|
122
124
|
constructor(stateDir, session, log) {
|
|
123
125
|
this.session = session;
|
|
124
126
|
this.log = log;
|
|
@@ -164,6 +166,7 @@ export class RoleControlServer {
|
|
|
164
166
|
setFleetSpawner(fleetSpawner) {
|
|
165
167
|
this.fleetSpawner = fleetSpawner;
|
|
166
168
|
}
|
|
169
|
+
setFleetAuditor(auditor) { this.fleetAuditor = auditor; }
|
|
167
170
|
accept(socket) {
|
|
168
171
|
this.sockets.add(socket);
|
|
169
172
|
socket.setEncoding('utf8');
|
|
@@ -311,6 +314,38 @@ export class RoleControlServer {
|
|
|
311
314
|
this.write(socket, { version: 1, id: request.id, ok: true, result });
|
|
312
315
|
return;
|
|
313
316
|
}
|
|
317
|
+
case 'fleet_audit_begin': {
|
|
318
|
+
if (!this.fleetAuditor)
|
|
319
|
+
throw new SessionControlError('rejected', 'fleet auditing is unavailable');
|
|
320
|
+
try {
|
|
321
|
+
validateFleetAuditBegin(request.audit);
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
throw new SessionControlError('rejected', 'valid fleet audit begin fields are required');
|
|
325
|
+
}
|
|
326
|
+
const result = await this.fleetAuditor.begin(request.audit.requestId, request.audit.argv);
|
|
327
|
+
this.write(socket, { version: 1, id: request.id, ok: true, result });
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
case 'fleet_audit_finish': {
|
|
331
|
+
if (!this.fleetAuditor)
|
|
332
|
+
throw new SessionControlError('rejected', 'fleet auditing is unavailable');
|
|
333
|
+
try {
|
|
334
|
+
validateFleetAuditFinish(request.audit);
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
throw new SessionControlError('rejected', 'valid fleet audit finish fields are required');
|
|
338
|
+
}
|
|
339
|
+
const audit = request.audit;
|
|
340
|
+
const result = await this.fleetAuditor.finish({
|
|
341
|
+
correlationId: audit.correlationId, class: audit.class, effect: audit.effect,
|
|
342
|
+
...(audit.exitCode === undefined ? {} : { exitCode: audit.exitCode }),
|
|
343
|
+
...(audit.resourceIds ? { resourceIds: audit.resourceIds } : {}),
|
|
344
|
+
...(audit.presentation ? { presentation: audit.presentation } : {}),
|
|
345
|
+
});
|
|
346
|
+
this.write(socket, { version: 1, id: request.id, ok: true, result });
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
314
349
|
case 'owner_channel_manage': {
|
|
315
350
|
if (!request.ownerChannel || typeof request.ownerChannel.action !== 'string')
|
|
316
351
|
throw new SessionControlError('rejected', 'owner-channel management action is required');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ours.network/fleet",
|
|
3
|
-
"version": "1.1.0-nightly.
|
|
3
|
+
"version": "1.1.0-nightly.6",
|
|
4
4
|
"description": "Harness-agnostic fleet of persistent, identity-bound AI agents. Declarative fleet.yaml, ACP sessions, supervision, and ours.network messaging.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "FSL-1.1-Apache-2.0",
|