@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.
@@ -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
- if (descriptor.resource === 'configuration') {
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
- }
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.
@@ -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();
@@ -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. */
@@ -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.1",
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.0",
53
- "@ours.network/mcp": "2.0.3",
54
- "@ours.network/sdk": "5.2.0",
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.0",
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",