@ours.network/fleet 1.2.0-nightly.70 → 1.2.0-nightly.71
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/conversation-history.d.ts +7 -0
- package/dist/application/conversation-history.js +65 -0
- package/dist/application/fleet-query-service.d.ts +1 -1
- package/dist/application/fleet-query-service.js +2 -2
- package/dist/application/resource-observer.d.ts +40 -0
- package/dist/application/resource-observer.js +278 -0
- package/dist/application/role-repository.d.ts +1 -1
- package/dist/application/role-repository.js +11 -5
- package/dist/application/task-layouts.js +2 -0
- package/dist/application/task-room-service.d.ts +2 -0
- package/dist/application/task-room-service.js +2 -2
- package/dist/build-info.json +4 -4
- package/dist/daemon-recovery.d.ts +13 -1
- package/dist/daemon-recovery.js +117 -3
- package/dist/monitor.d.ts +1 -0
- package/dist/monitor.js +1 -1
- package/dist/notifications/fleet-producer.d.ts +4 -1
- package/dist/notifications/fleet-producer.js +36 -7
- package/dist/notifications/target-cleanup.d.ts +1 -0
- package/dist/notifications/target-cleanup.js +1 -1
- package/dist/notifications/task-producer.d.ts +3 -1
- package/dist/notifications/task-producer.js +38 -5
- package/dist/rooms-tasks/add-member.js +2 -1
- package/dist/rooms-tasks/layout.js +2 -1
- package/dist/rooms-tasks/task-state.d.ts +1 -0
- package/dist/rooms-tasks/task-state.js +6 -1
- package/dist/runner.js +22 -7
- package/dist/subscriptions/login.d.ts +4 -3
- package/dist/subscriptions/login.js +14 -6
- package/dist/subscriptions/service.d.ts +1 -1
- package/dist/subscriptions/service.js +4 -4
- package/dist/subscriptions/store.js +3 -0
- package/dist/watchdog/query.d.ts +3 -1
- package/dist/watchdog/query.js +2 -1
- package/dist/web/events.d.ts +2 -0
- package/dist/web/events.js +9 -0
- package/dist/web/prefix-gateway.js +20 -10
- package/dist/web/resource-views.d.ts +124 -0
- package/dist/web/resource-views.js +424 -0
- package/dist/web/runtime.js +4 -1
- package/dist/web/server.d.ts +3 -0
- package/dist/web/server.js +170 -19
- package/docs/conversation-stream.md +6 -0
- package/package.json +1 -1
package/dist/web/server.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { FleetResourceViews, ResourceBindingError, canonicalResourceDescriptor } from './resource-views.js';
|
|
2
|
+
import { publishFleetResourceCommit } from '../application/resource-observer.js';
|
|
3
|
+
import { stateRoot, defaultConfigPath } from '../paths.js';
|
|
4
|
+
import { conversationPreview } from '../application/conversation-history.js';
|
|
1
5
|
import { registerConversationStream } from './conversation-stream.js';
|
|
2
6
|
import { conversationHasAttachment } from '../application/conversation-history.js';
|
|
3
7
|
import { readDeliveredFile } from '../file-delivery/store.js';
|
|
@@ -61,6 +65,12 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
61
65
|
}
|
|
62
66
|
auth.validateBoundary(request, false);
|
|
63
67
|
});
|
|
68
|
+
app.addHook('onResponse', async (request, reply) => {
|
|
69
|
+
if (reply.statusCode < 400 && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(request.method) && !request.url.includes('/resource-subscriptions')) {
|
|
70
|
+
publishFleetResourceCommit(services.configuration?.path ?? defaultConfigPath());
|
|
71
|
+
publishFleetResourceCommit(stateRoot());
|
|
72
|
+
}
|
|
73
|
+
});
|
|
64
74
|
app.post('/api/v1/layout-bindings/:id/control', async (request) => {
|
|
65
75
|
// Initial support is direct same-host Fleet-to-Fleet transport.
|
|
66
76
|
const forwarded = Object.keys(request.headers).some(name => name === 'forwarded' || name.startsWith('x-forwarded-') || name === 'x-real-ip' || name === 'via');
|
|
@@ -74,6 +84,8 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
74
84
|
return services.layoutBindings.control(request.params.id, request.headers.authorization, request.body);
|
|
75
85
|
});
|
|
76
86
|
app.setErrorHandler(async (error, request, reply) => {
|
|
87
|
+
if (error instanceof ResourceBindingError)
|
|
88
|
+
return reply.code(error.status).send({ error: { code: 'resource_binding', message: error.message } });
|
|
77
89
|
const privateToolParseError = ['/api/v1/roles/:id/ours/call', '/api/v1/layout-bindings/:id/control'].includes(request.routeOptions.url ?? '')
|
|
78
90
|
&& error instanceof Error && 'code' in error
|
|
79
91
|
&& typeof error.code === 'string' && error.code.startsWith('FST_ERR_CTP_');
|
|
@@ -183,8 +195,10 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
183
195
|
auditDegraded: audit.degraded,
|
|
184
196
|
};
|
|
185
197
|
});
|
|
198
|
+
let onboardingObserved;
|
|
186
199
|
app.get('/api/v1/onboarding', async (request) => {
|
|
187
|
-
auth.authenticate(request);
|
|
200
|
+
const browser = auth.authenticate(request);
|
|
201
|
+
const revision = services.configuration?.read().revision;
|
|
188
202
|
// Opening an existing workspace needs configuration state, not CLI/account discovery.
|
|
189
203
|
// Keep the full report for the setup screens and older App clients.
|
|
190
204
|
if (request.query.view === 'state')
|
|
@@ -202,7 +216,12 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
202
216
|
// When it could not be asked, none is offered and the reason is reported.
|
|
203
217
|
const codexModels = services.codexModels ? await services.codexModels() : undefined;
|
|
204
218
|
const offered = (model) => model.harness !== 'codex' || !codexModels || (codexModels.state === 'known' && codexModels.models.includes(model.model));
|
|
205
|
-
|
|
219
|
+
const payload = { harnesses, catalog: catalog.models.filter(offered), ...(codexModels ? { codexModels: codexModels.state === 'known' ? { state: 'known' } : codexModels } : {}), providers: services.subscriptions ? await services.subscriptions.list() : [], coordinator: 'FleetCoordinator', configured: onboardingConfigured() };
|
|
220
|
+
if (revision === services.configuration?.read().revision) {
|
|
221
|
+
onboardingObserved = { owner: browser.id, revision, payload };
|
|
222
|
+
events.publish('onboarding.changed', {});
|
|
223
|
+
}
|
|
224
|
+
return payload;
|
|
206
225
|
});
|
|
207
226
|
// First-time setup is what creates the persistent coordinator. A configuration that loads and
|
|
208
227
|
// has it is set up, whatever its brains are called; its models are then changed by editing it.
|
|
@@ -283,6 +302,9 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
283
302
|
app.get('/api/v1/roles', async (request) => {
|
|
284
303
|
auth.authenticate(request);
|
|
285
304
|
const query = request.query;
|
|
305
|
+
// Unbound REST retains the existing problem-bearing inventory. Bound reads
|
|
306
|
+
// are handled by the strict resource preHandler before this route.
|
|
307
|
+
services.invalidateResourceCaches?.();
|
|
286
308
|
return { roles: await services.query.list(query.includeTemporary === 'true') };
|
|
287
309
|
});
|
|
288
310
|
const taskApi = async (fn) => {
|
|
@@ -309,9 +331,152 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
309
331
|
throw new FleetError('capability_unavailable', 'task operations are unavailable');
|
|
310
332
|
return services.taskRooms;
|
|
311
333
|
};
|
|
334
|
+
const projectTasks = async (query, strict = true) => {
|
|
335
|
+
const states = ['backlog', 'provisioning', 'active', 'review', 'done', 'cancelled', 'failed'];
|
|
336
|
+
if (query.state && query.state !== 'all' && !states.includes(query.state))
|
|
337
|
+
throw new FleetError('invalid_request', 'invalid task state filter');
|
|
338
|
+
if (query.groupByList !== undefined && !['true', 'false'].includes(query.groupByList))
|
|
339
|
+
throw new FleetError('invalid_request', 'groupByList must be true or false');
|
|
340
|
+
if (query.includeDeleting !== undefined && !['true', 'false'].includes(query.includeDeleting))
|
|
341
|
+
throw new FleetError('invalid_request', 'includeDeleting must be true or false');
|
|
342
|
+
const state = query.state && query.state !== 'all' ? query.state : undefined;
|
|
343
|
+
const filter = { strict, ...(state ? { state } : {}), ...(query.list ? { list: query.list } : {}),
|
|
344
|
+
...(query.includeDeleting === 'true' ? { includeDeleting: true } : {}) };
|
|
345
|
+
const api = requireTaskRooms();
|
|
346
|
+
return taskApi(() => query.groupByList === 'true'
|
|
347
|
+
? { groups: api.groupedTasks(filter).map(group => ({ ...group, tasks: group.tasks.map(task => api.withLayoutRooms(task)) })) }
|
|
348
|
+
: { tasks: api.listTasks(filter).map(task => api.withLayoutRooms(task)) });
|
|
349
|
+
};
|
|
350
|
+
const project = async (descriptor, signal, owner = '') => {
|
|
351
|
+
signal.throwIfAborted();
|
|
352
|
+
services.invalidateResourceCaches?.();
|
|
353
|
+
switch (descriptor.resource) {
|
|
354
|
+
case 'task-layout':
|
|
355
|
+
try {
|
|
356
|
+
return { layout: await taskApi(() => requireTaskRooms().taskLayout(descriptor.entityId)) };
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
if (error?.code === 'task_not_found' || error?.code === 'resource_not_found')
|
|
360
|
+
return { deleted: true, id: descriptor.entityId };
|
|
361
|
+
throw error;
|
|
362
|
+
}
|
|
363
|
+
case 'creation-action': return services.creation.get(descriptor.entityId) ?? { deleted: true, id: descriptor.entityId };
|
|
364
|
+
case 'action': return services.commands.get(descriptor.entityId) ?? { deleted: true, id: descriptor.entityId };
|
|
365
|
+
case 'member-addition':
|
|
366
|
+
try {
|
|
367
|
+
return await taskApi(() => requireTaskRooms().memberAddition(descriptor.entityId, descriptor.receiptId));
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
if (['task_not_found', 'resource_not_found'].includes(error?.code) || error?.message === 'Member request not found')
|
|
371
|
+
return { deleted: true, id: descriptor.receiptId };
|
|
372
|
+
throw error;
|
|
373
|
+
}
|
|
374
|
+
case 'login':
|
|
375
|
+
// A foreign login must never be delivered, including its URL/device code.
|
|
376
|
+
try {
|
|
377
|
+
return subscriptions().login(descriptor.entityId, owner).view();
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
if (error?.code === 'resource_not_found')
|
|
381
|
+
return { deleted: true, id: descriptor.entityId };
|
|
382
|
+
throw error;
|
|
383
|
+
}
|
|
384
|
+
case 'subscriptions': return { providers: await subscriptions().list(true) };
|
|
385
|
+
case 'onboarding-health': {
|
|
386
|
+
const revision = services.configuration?.read().revision;
|
|
387
|
+
if (onboardingObserved && onboardingObserved.revision !== revision)
|
|
388
|
+
onboardingObserved = undefined;
|
|
389
|
+
const observed = onboardingObserved?.owner === owner ? onboardingObserved : undefined;
|
|
390
|
+
return { ...observed?.payload, observationCurrent: !!observed, configured: onboardingConfigured(), providers: services.subscriptions ? await services.subscriptions.list(true) : [] };
|
|
391
|
+
}
|
|
392
|
+
case 'roles': return { roles: await services.query.list(descriptor.includeTemporary ?? false, true) };
|
|
393
|
+
case 'tasks': return projectTasks({ state: descriptor.state, list: descriptor.list, groupByList: String(descriptor.groupByList ?? false), includeDeleting: String(descriptor.includeDeleting ?? false) });
|
|
394
|
+
case 'task-lists': return { lists: requireTaskRooms().listTaskLists() };
|
|
395
|
+
case 'configuration':
|
|
396
|
+
if (!services.configuration)
|
|
397
|
+
throw new FleetError('capability_unavailable', 'fleet configuration editing is unavailable');
|
|
398
|
+
return { ...services.configuration.read(descriptor.includeDefinitions ?? false), provenance: services.presetProvenance?.read() ?? null };
|
|
399
|
+
case 'directory': {
|
|
400
|
+
const [roles, tasks] = await Promise.all([services.query.list(true, true), projectTasks({})]);
|
|
401
|
+
return { roles, tasks: tasks.tasks, lists: requireTaskRooms().listTaskLists() };
|
|
402
|
+
}
|
|
403
|
+
case 'previews': {
|
|
404
|
+
const roles = await services.repository.list(true), selected = new Map(roles.map(role => [role.id, role]));
|
|
405
|
+
const previews = {};
|
|
406
|
+
for (const id of descriptor.ids ?? []) {
|
|
407
|
+
signal.throwIfAborted();
|
|
408
|
+
const role = selected.get(id), dir = role && services.repository.stateDir(role);
|
|
409
|
+
if (!role)
|
|
410
|
+
continue;
|
|
411
|
+
try {
|
|
412
|
+
previews[id] = dir ? await conversationPreview(dir, signal) : { text: 'No recent agent output', at: '' };
|
|
413
|
+
}
|
|
414
|
+
catch {
|
|
415
|
+
signal.throwIfAborted();
|
|
416
|
+
// A damaged/unreadable ledger is unavailable, not empty or removed.
|
|
417
|
+
// Keep this selected row observable so a repair can refresh it,
|
|
418
|
+
// without retiring previews for the other selected roles.
|
|
419
|
+
previews[id] = { text: 'Preview unavailable', at: '', unavailable: true };
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return { previews };
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
const resources = new FleetResourceViews(services.resourceScope ?? {}, project);
|
|
427
|
+
app.addHook('preClose', async () => resources.close());
|
|
428
|
+
app.get('/api/v1/resource-events', async (request, reply) => {
|
|
429
|
+
const browser = auth.authenticate(request);
|
|
430
|
+
reply.hijack();
|
|
431
|
+
reply.raw.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache, no-transform', Connection: 'keep-alive', 'X-Accel-Buffering': 'no' });
|
|
432
|
+
reply.raw.flushHeaders();
|
|
433
|
+
const opened = resources.open(reply.raw, browser.id, () => auth.sessionValid(browser));
|
|
434
|
+
const binding = auth.bindTransport(browser, opened.close);
|
|
435
|
+
reply.raw.once('close', () => binding());
|
|
436
|
+
});
|
|
437
|
+
app.post('/api/v1/resource-subscriptions', async (request, reply) => {
|
|
438
|
+
const browser = auth.authenticate(request, true), body = request.body;
|
|
439
|
+
const binding = resources.register(browser.id, String(body?.connectionId ?? ''), String(body?.viewId ?? ''), body?.subscriptionEpoch, body?.descriptor);
|
|
440
|
+
const retire = () => {
|
|
441
|
+
if (reply.raw.writableFinished)
|
|
442
|
+
return;
|
|
443
|
+
try {
|
|
444
|
+
resources.remove(browser.id, String(body.connectionId), String(body.viewId), body.subscriptionEpoch);
|
|
445
|
+
}
|
|
446
|
+
catch { /* Connection may already be closed. */ }
|
|
447
|
+
};
|
|
448
|
+
reply.raw.once('close', retire);
|
|
449
|
+
reply.raw.once('finish', () => reply.raw.off('close', retire));
|
|
450
|
+
if (reply.raw.destroyed)
|
|
451
|
+
retire();
|
|
452
|
+
return binding;
|
|
453
|
+
});
|
|
454
|
+
app.delete('/api/v1/resource-subscriptions', async (request) => {
|
|
455
|
+
const browser = auth.authenticate(request, true), body = request.body;
|
|
456
|
+
resources.remove(browser.id, String(body?.connectionId ?? ''), String(body?.viewId ?? ''), body?.subscriptionEpoch);
|
|
457
|
+
return { removed: true };
|
|
458
|
+
});
|
|
459
|
+
// Bound reads return precisely the registered initial capture, never an unrelated GET.
|
|
460
|
+
app.addHook('preHandler', async (request, reply) => {
|
|
461
|
+
if (request.method !== 'GET' || !request.headers['x-resource-connection'])
|
|
462
|
+
return;
|
|
463
|
+
const browser = auth.authenticate(request), headers = request.headers;
|
|
464
|
+
const baseline = await resources.bound(browser.id, String(headers['x-resource-connection']), String(headers['x-resource-view']), Number(headers['x-resource-epoch']), String(headers['x-resource-baseline']));
|
|
465
|
+
const expected = '/api/v1/' + baseline.descriptor.resource;
|
|
466
|
+
if (request.url.split('?')[0] !== expected)
|
|
467
|
+
throw new ResourceBindingError(409, 'resource route mismatch');
|
|
468
|
+
for (const name of ['Connection', 'View', 'Epoch', 'Baseline'])
|
|
469
|
+
reply.header('X-Resource-' + name, String(headers['x-resource-' + name.toLowerCase()]));
|
|
470
|
+
reply.header('X-Resource-Producer', baseline.producerGeneration).header('X-Resource-Revision', baseline.revision).header('X-Resource-Source-Generation', baseline.source.producerGeneration).header('X-Resource-Source-Position', baseline.source.position);
|
|
471
|
+
return reply.send(baseline.payload);
|
|
472
|
+
});
|
|
473
|
+
for (const resource of ['task-layout', 'creation-action', 'action', 'member-addition', 'login', 'onboarding-health'])
|
|
474
|
+
app.get('/api/v1/' + resource, async () => { throw new ResourceBindingError(409, 'resource binding required'); });
|
|
475
|
+
app.get('/api/v1/directory', async (request) => { auth.authenticate(request); return project({ viewId: 'read', resource: 'directory' }, new AbortController().signal); });
|
|
476
|
+
app.get('/api/v1/previews', async (request) => { auth.authenticate(request); const ids = String(request.query.ids ?? '').split(',').filter(Boolean); return project(canonicalResourceDescriptor({ viewId: 'read', resource: 'previews', ids }), new AbortController().signal); });
|
|
312
477
|
app.get('/api/v1/task-lists', async (request) => {
|
|
313
478
|
auth.authenticate(request);
|
|
314
|
-
return {
|
|
479
|
+
return project({ viewId: 'read', resource: 'task-lists' }, new AbortController().signal);
|
|
315
480
|
});
|
|
316
481
|
app.post('/api/v1/task-lists', async (request, reply) => {
|
|
317
482
|
auth.authenticate(request, true);
|
|
@@ -334,21 +499,7 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
334
499
|
});
|
|
335
500
|
app.get('/api/v1/tasks', async (request) => {
|
|
336
501
|
auth.authenticate(request);
|
|
337
|
-
|
|
338
|
-
const states = ['backlog', 'provisioning', 'active', 'review', 'done', 'cancelled', 'failed'];
|
|
339
|
-
if (query.state && query.state !== 'all' && !states.includes(query.state))
|
|
340
|
-
throw new FleetError('invalid_request', 'invalid task state filter');
|
|
341
|
-
if (query.groupByList !== undefined && !['true', 'false'].includes(query.groupByList))
|
|
342
|
-
throw new FleetError('invalid_request', 'groupByList must be true or false');
|
|
343
|
-
if (query.includeDeleting !== undefined && !['true', 'false'].includes(query.includeDeleting))
|
|
344
|
-
throw new FleetError('invalid_request', 'includeDeleting must be true or false');
|
|
345
|
-
const state = query.state && query.state !== 'all' ? query.state : undefined;
|
|
346
|
-
const filter = { ...(state ? { state } : {}), ...(query.list ? { list: query.list } : {}),
|
|
347
|
-
...(query.includeDeleting === 'true' ? { includeDeleting: true } : {}) };
|
|
348
|
-
const api = requireTaskRooms();
|
|
349
|
-
return taskApi(() => query.groupByList === 'true'
|
|
350
|
-
? { groups: api.groupedTasks(filter).map(group => ({ ...group, tasks: group.tasks.map(task => api.withLayoutRooms(task)) })) }
|
|
351
|
-
: { tasks: api.listTasks(filter).map(task => api.withLayoutRooms(task)) });
|
|
502
|
+
return projectTasks(request.query, false);
|
|
352
503
|
});
|
|
353
504
|
app.delete('/api/v1/tasks/:id', async (request, reply) => {
|
|
354
505
|
auth.authenticate(request, true);
|
|
@@ -558,7 +709,7 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
558
709
|
auth.authenticate(request);
|
|
559
710
|
if (!services.configuration)
|
|
560
711
|
throw new FleetError('capability_unavailable', 'fleet configuration editing is unavailable');
|
|
561
|
-
return
|
|
712
|
+
return project({ viewId: 'read', resource: 'configuration', includeDefinitions: request.query.includeDefinitions === 'true' }, new AbortController().signal);
|
|
562
713
|
});
|
|
563
714
|
app.post('/api/v1/configuration/preview', async (request) => {
|
|
564
715
|
auth.authenticate(request, true);
|
|
@@ -188,3 +188,9 @@ explicit recovery and unsupported-host fallback. DOM mutation and fixture latenc
|
|
|
188
188
|
measurements have no claim about real-provider inference or physical-device audio.
|
|
189
189
|
The App and Fleet changes require coordinated upstream delivery; source qualification
|
|
190
190
|
does not establish that installed or serving packages contain them.
|
|
191
|
+
|
|
192
|
+
## Full resource delivery qualification
|
|
193
|
+
|
|
194
|
+
Directory, configuration/definitions, previews, provider/status, layout and admitted-operation views share the authenticated ephemeral resource connection with per-view epochs. Complete ready requires the canonical subscribed payload; a heartbeat does not certify a failed upstream watcher. User commands remain HTTP. Controller, voice and selected-daemon health are independent owners. The selected-daemon health deadline passes its cancellation signal through the public SDK selection/request API, cancelling underlying HTTP I/O as well as retiring the result.
|
|
195
|
+
|
|
196
|
+
The matching App source qualification composes the existing ACP/Codex browser fixture and records sixty-second healthy windows separately from initial/history/recovery/command/health/lease/long-poll traffic. Unit and HTTP producer checks are not proof of every rendered resource combination or installed deployment. Preserve existing room-layout suite sources and qualify released service/dependency versions separately.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ours.network/fleet",
|
|
3
|
-
"version": "1.2.0-nightly.
|
|
3
|
+
"version": "1.2.0-nightly.71",
|
|
4
4
|
"description": "Harness-agnostic fleet of persistent, identity-bound AI agents. Declarative fleet.yaml, managed native/ACP sessions, supervision, and ours.network messaging.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "FSL-1.1-Apache-2.0",
|