@ours.network/fleet 2.3.1 → 2.4.0-nightly.1
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/build-info.json +5 -5
- package/dist/channels/identity-reads.d.ts +69 -0
- package/dist/channels/identity-reads.js +47 -0
- package/dist/channels/indexer.d.ts +55 -0
- package/dist/channels/indexer.js +214 -0
- package/dist/channels/runtime.d.ts +30 -0
- package/dist/channels/runtime.js +125 -0
- package/dist/channels/service.d.ts +61 -0
- package/dist/channels/service.js +88 -0
- package/dist/channels/sources.d.ts +46 -0
- package/dist/channels/sources.js +68 -0
- package/dist/channels/store.d.ts +103 -0
- package/dist/channels/store.js +204 -0
- package/dist/rooms-tasks/task-state.js +7 -2
- package/dist/web/fleet-config-service.d.ts +8 -0
- package/dist/web/fleet-config-service.js +23 -8
- package/dist/web/resource-views.d.ts +3 -1
- package/dist/web/resource-views.js +33 -16
- package/dist/web/runtime.js +13 -0
- package/dist/web/server.d.ts +3 -0
- package/dist/web/server.js +26 -0
- package/package.json +5 -5
|
@@ -155,7 +155,7 @@ export function canonicalResourceDescriptor(input) {
|
|
|
155
155
|
if (!input || typeof input !== 'object' || Array.isArray(input))
|
|
156
156
|
throw Error('invalid resource descriptor');
|
|
157
157
|
const value = input;
|
|
158
|
-
if (!/^[A-Za-z0-9_-]{1,128}$/.test(value.viewId) || !['directory', 'roles', 'tasks', 'task-lists', 'configuration', 'previews', 'task-layout', 'creation-action', 'action', 'member-addition', 'login', 'onboarding-health', 'subscriptions'].includes(value.resource)
|
|
158
|
+
if (!/^[A-Za-z0-9_-]{1,128}$/.test(value.viewId) || !['directory', 'roles', 'tasks', 'task-lists', 'configuration', 'previews', 'task-layout', 'creation-action', 'action', 'member-addition', 'login', 'onboarding-health', 'subscriptions', 'channels'].includes(value.resource)
|
|
159
159
|
|| Object.keys(value).some(key => !['viewId', 'resource', 'includeTemporary', 'includeDefinitions', 'ids', 'state', 'list', 'groupByList', 'includeDeleting', 'entityId', 'receiptId', 'provider', 'activityOnly'].includes(key)))
|
|
160
160
|
throw Error('invalid resource descriptor');
|
|
161
161
|
if (value.activityOnly !== undefined && value.resource !== 'previews')
|
|
@@ -364,22 +364,31 @@ export class FleetResourceViews {
|
|
|
364
364
|
view.qualifiedAt = now;
|
|
365
365
|
} });
|
|
366
366
|
}
|
|
367
|
+
/** Whether `path` is, contains or lies within the manifest, its split definitions or another configuration input. */
|
|
368
|
+
configurationDependency(path) {
|
|
369
|
+
const manifest = resolve(this.scope.configPath ?? defaultConfigPath());
|
|
370
|
+
const root = resolve(splitRootFor(manifest)), changed = resolve(path);
|
|
371
|
+
const within = (candidate, parent) => {
|
|
372
|
+
const local = relative(parent, candidate);
|
|
373
|
+
return local === '' || (local !== '..' && !local.startsWith('..' + sep));
|
|
374
|
+
};
|
|
375
|
+
// Configuration and provenance read the manifest and split definitions;
|
|
376
|
+
// loadConfig also rejects legacy YAML files in the global fleet.d root.
|
|
377
|
+
// Ancestor replacement/unknown directory wakes remain conservative.
|
|
378
|
+
const legacy = resolve(fleetDDir());
|
|
379
|
+
return within(changed, root) || within(root, changed) || within(manifest, changed)
|
|
380
|
+
|| within(changed, legacy) || within(legacy, changed)
|
|
381
|
+
|| this.configurationInputs().some(input => within(resolve(input), changed));
|
|
382
|
+
}
|
|
367
383
|
affects(descriptor, path, structural = false) {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
// loadConfig also rejects legacy YAML files in the global fleet.d root.
|
|
377
|
-
// Ancestor replacement/unknown directory wakes remain conservative.
|
|
378
|
-
const legacy = resolve(fleetDDir());
|
|
379
|
-
return within(changed, root) || within(root, changed) || within(manifest, changed)
|
|
380
|
-
|| within(changed, legacy) || within(legacy, changed)
|
|
381
|
-
|| this.configurationInputs().some(input => within(resolve(input), changed));
|
|
382
|
-
}
|
|
384
|
+
// The Channels projection commits only its own private database; no other view reads it.
|
|
385
|
+
const channelsRoot = resolve(stateRoot(), 'private-channels'), channelsPath = path === channelsRoot || path.startsWith(channelsRoot + sep);
|
|
386
|
+
if (descriptor.resource === 'channels')
|
|
387
|
+
return channelsPath || channelsRoot.startsWith(resolve(path) + sep);
|
|
388
|
+
if (channelsPath)
|
|
389
|
+
return false;
|
|
390
|
+
if (descriptor.resource === 'configuration')
|
|
391
|
+
return this.configurationDependency(path);
|
|
383
392
|
// An explicitly selected manifest may use any pathname, including one
|
|
384
393
|
// otherwise reserved for an independent task receipt.
|
|
385
394
|
if (this.scope.configPath && resolve(path) === resolve(this.scope.configPath))
|
|
@@ -394,6 +403,14 @@ export class FleetResourceViews {
|
|
|
394
403
|
if (roleProjection && !inRoleRoot && (/^(?:history\.jsonl|models_cache\.json|(?:thread_history|state|logs|queue|goals)_\d+\.sqlite(?:-wal|-shm|-journal)?)$/.test(relative(resolve(homedir(), '.codex'), path))
|
|
395
404
|
|| /^(?:workspace-devices\.db(?:-wal|-shm|-journal)?|audit[/\\]audit\.jsonl)$/.test(relative(resolve(stateRoot(), 'web'), path))))
|
|
396
405
|
return false;
|
|
406
|
+
// Task and room workspaces hold their agents' own execution artifacts (checkouts,
|
|
407
|
+
// builds, test logs); role projections read nothing there except declared role
|
|
408
|
+
// roots and configuration (manifest, split definitions), which stay fenced. Working
|
|
409
|
+
// agents write there continuously, which otherwise fences every directory capture
|
|
410
|
+
// until it resyncs. The workspace directories themselves and their ancestors remain full wakes.
|
|
411
|
+
if (roleProjection && !inRoleRoot && /^(?:tasks|rooms)[/\\][^/\\]+[/\\]/.test(relative(resolve(stateRoot(), 'workspaces'), path))
|
|
412
|
+
&& !this.configurationDependency(path))
|
|
413
|
+
return false;
|
|
397
414
|
// Preview selection reads role config/state and conversation ledgers, never
|
|
398
415
|
// task layout run/operation/brief receipts. A worker may commit several of
|
|
399
416
|
// these while that independent canonical preview read is in flight.
|
package/dist/web/runtime.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createChannels } from '../channels/runtime.js';
|
|
1
2
|
import { ConversationRoleObserver } from '../application/conversation-role-observer.js';
|
|
2
3
|
import { replaceFileAtomically } from '../atomic-file.js';
|
|
3
4
|
import { validateAccountOrigin } from '../account-origin.js';
|
|
@@ -196,6 +197,15 @@ export async function startWebConsole(options) {
|
|
|
196
197
|
const taskRooms = new TaskRoomApplicationService(options.configPath, { binPath: () => options.binPath });
|
|
197
198
|
// One-time, lock-guarded mission key migration before serving; failure leaves it to the next mutation.
|
|
198
199
|
await taskRooms.prepareMissionKeys().catch(error => console.warn(`mission key migration deferred: ${error.message}`));
|
|
200
|
+
// Ours Channels runs only where a daemon client profile exists.
|
|
201
|
+
let channels;
|
|
202
|
+
try {
|
|
203
|
+
channels = createChannels({ configPath: configuration.path, repository });
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
console.warn(`Ours Channels unavailable: ${error.message}`);
|
|
207
|
+
channels = undefined;
|
|
208
|
+
}
|
|
199
209
|
let server;
|
|
200
210
|
try {
|
|
201
211
|
server = await buildWebServer({
|
|
@@ -219,6 +229,7 @@ export async function startWebConsole(options) {
|
|
|
219
229
|
});
|
|
220
230
|
},
|
|
221
231
|
codexModels: createCodexModelDiscovery(),
|
|
232
|
+
...(channels ? { channels: channels.service } : {}),
|
|
222
233
|
subscriptions: new SubscriptionService({
|
|
223
234
|
agents: async () => (await query.list(true)).map(({ role, status }) => ({
|
|
224
235
|
roleId: role.id, stateDir: repository.stateDir(role), running: status.supervisor.liveness === 'running',
|
|
@@ -241,6 +252,7 @@ export async function startWebConsole(options) {
|
|
|
241
252
|
}, { origin: `http://127.0.0.1:${requestedPort}`, host: `127.0.0.1:${requestedPort}` }, { auth });
|
|
242
253
|
}
|
|
243
254
|
catch (error) {
|
|
255
|
+
channels?.close();
|
|
244
256
|
lock.release();
|
|
245
257
|
throw error;
|
|
246
258
|
}
|
|
@@ -336,6 +348,7 @@ export async function startWebConsole(options) {
|
|
|
336
348
|
await control?.close();
|
|
337
349
|
await gateway?.close();
|
|
338
350
|
await server.close();
|
|
351
|
+
channels?.close();
|
|
339
352
|
}
|
|
340
353
|
finally {
|
|
341
354
|
lock.release();
|
package/dist/web/server.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ import type { PresetProvenance } from '../application/preset-provenance.js';
|
|
|
24
24
|
import type { OfferedModels } from '../subscriptions/cli.js';
|
|
25
25
|
import type { SubscriptionService } from '../subscriptions/service.js';
|
|
26
26
|
import type { LayoutBindingGrants } from '../rooms-tasks/layout-binding-grants.js';
|
|
27
|
+
import type { ChannelsService } from '../channels/service.js';
|
|
27
28
|
export interface WebServices {
|
|
28
29
|
resourceScope?: FleetResourceScope;
|
|
29
30
|
invalidateResourceCaches?(): void;
|
|
@@ -48,6 +49,8 @@ export interface WebServices {
|
|
|
48
49
|
presetProvenance?: PresetProvenance;
|
|
49
50
|
oursTools?: Pick<SupervisorOursTools, 'list' | 'call'>;
|
|
50
51
|
subscriptions?: SubscriptionService;
|
|
52
|
+
/** Ours Channels: Owner-only cross-identity external contacts. Absent: not configured. */
|
|
53
|
+
channels?: ChannelsService;
|
|
51
54
|
/** First-time setup from the packaged presets; the account App supplies only the model choices. */
|
|
52
55
|
onboardingSetup?(answers: InitAnswers): Promise<void>;
|
|
53
56
|
/** The Codex models the runtime that sessions will use offers the active account. Absent: this server cannot tell, and filters nothing. */
|
package/dist/web/server.js
CHANGED
|
@@ -424,6 +424,9 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
424
424
|
const project = async (descriptor, signal, owner = '') => {
|
|
425
425
|
signal.throwIfAborted();
|
|
426
426
|
services.invalidateResourceCaches?.();
|
|
427
|
+
// Role projections load the configuration: fence this capture on its external inputs too.
|
|
428
|
+
if (['directory', 'roles', 'previews'].includes(descriptor.resource))
|
|
429
|
+
services.configuration?.observeValidationInputs();
|
|
427
430
|
switch (descriptor.resource) {
|
|
428
431
|
case 'task-layout':
|
|
429
432
|
try {
|
|
@@ -456,6 +459,10 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
456
459
|
throw error;
|
|
457
460
|
}
|
|
458
461
|
case 'subscriptions': return { providers: await subscriptions().list(true) };
|
|
462
|
+
case 'channels':
|
|
463
|
+
if (!services.channels)
|
|
464
|
+
throw new FleetError('capability_unavailable', 'Ours Channels is not configured on this server');
|
|
465
|
+
return services.channels.snapshot();
|
|
459
466
|
case 'onboarding-health': {
|
|
460
467
|
const revision = services.configuration?.read().revision;
|
|
461
468
|
if (onboardingObserved && onboardingObserved.revision !== revision)
|
|
@@ -552,6 +559,25 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
552
559
|
});
|
|
553
560
|
for (const resource of ['task-layout', 'creation-action', 'action', 'member-addition', 'login', 'onboarding-health'])
|
|
554
561
|
app.get('/api/v1/' + resource, async () => { throw new ResourceBindingError(409, 'resource binding required'); });
|
|
562
|
+
const channels = () => { if (!services.channels)
|
|
563
|
+
throw new FleetError('capability_unavailable', 'Ours Channels is not configured on this server'); return services.channels; };
|
|
564
|
+
const channelCid = (value) => { const cid = String(value ?? ''); if (!/^[0-9A-Fa-f]{64}$/.test(cid))
|
|
565
|
+
throw new FleetError('invalid_request', 'a 64-hex container id is required'); return cid.toUpperCase(); };
|
|
566
|
+
const optionalInt = (value, name, max = Number.MAX_SAFE_INTEGER) => { if (value === undefined || value === '')
|
|
567
|
+
return undefined; const n = Number(value); if (!Number.isSafeInteger(n) || n < 1 || n > max)
|
|
568
|
+
throw new FleetError('invalid_request', `${name} must be an integer from 1 to ${max}`); return n; };
|
|
569
|
+
app.get('/api/v1/channels', async (request) => {
|
|
570
|
+
auth.authenticate(request);
|
|
571
|
+
const query = request.query;
|
|
572
|
+
return channels().list({ ...(query.cursor ? { cursor: String(query.cursor) } : {}), ...(query.q ? { q: String(query.q).slice(0, 200) } : {}), ...(query.root ? { root: channelCid(query.root) } : {}), ...(query.limit ? { limit: optionalInt(query.limit, 'limit', 200) } : {}) });
|
|
573
|
+
});
|
|
574
|
+
app.get('/api/v1/channels-status', async (request) => { auth.authenticate(request); return channels().status(); });
|
|
575
|
+
app.get('/api/v1/channels/:cid', async (request) => { auth.authenticate(request); return channels().get(channelCid(request.params.cid)); });
|
|
576
|
+
app.get('/api/v1/channels/:cid/perspectives/:source/messages', async (request) => {
|
|
577
|
+
auth.authenticate(request);
|
|
578
|
+
const before = optionalInt(request.query.before_seq, 'before_seq'), limit = optionalInt(request.query.limit, 'limit', 200);
|
|
579
|
+
return channels().history(channelCid(request.params.cid), channelCid(request.params.source), { ...(before !== undefined ? { before_seq: before } : {}), ...(limit !== undefined ? { limit } : {}) });
|
|
580
|
+
});
|
|
555
581
|
app.get('/api/v1/directory', async (request) => { auth.authenticate(request); return project({ viewId: 'read', resource: 'directory' }, new AbortController().signal); });
|
|
556
582
|
app.get('/api/v1/previews', async (request) => { auth.authenticate(request); const query = request.query, ids = String(query.ids ?? '').split(',').filter(Boolean); return project(canonicalResourceDescriptor({ viewId: 'read', resource: 'previews', ids, ...(query.activityOnly === 'true' ? { activityOnly: true } : {}) }), new AbortController().signal); });
|
|
557
583
|
app.get('/api/v1/task-lists', async (request) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ours.network/fleet",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0-nightly.1",
|
|
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",
|
|
@@ -49,9 +49,9 @@
|
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@agentclientprotocol/sdk": "^1.3.0",
|
|
51
51
|
"@fastify/websocket": "^11.2.0",
|
|
52
|
-
"@ours.network/cli": "4.2.
|
|
53
|
-
"@ours.network/mcp": "2.0.
|
|
54
|
-
"@ours.network/sdk": "5.2.
|
|
52
|
+
"@ours.network/cli": "4.2.1-nightly.1",
|
|
53
|
+
"@ours.network/mcp": "2.1.0-nightly.1",
|
|
54
|
+
"@ours.network/sdk": "5.2.1-nightly.1",
|
|
55
55
|
"commander": "^12.1.0",
|
|
56
56
|
"fastify": "^5.4.0",
|
|
57
57
|
"qrcode": "1.5.4",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"@agentclientprotocol/codex-acp": "1.10.0"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
|
-
"@ours.network/daemon": "5.2.
|
|
68
|
+
"@ours.network/daemon": "5.2.1-nightly.1",
|
|
69
69
|
"@types/node": "^22.20.4",
|
|
70
70
|
"@types/qrcode": "^1.5.6",
|
|
71
71
|
"@types/ws": "^8.18.1",
|