@ours.network/fleet 1.2.0-nightly.21 → 1.2.0-nightly.23

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.
@@ -17,6 +17,7 @@ import { FleetEventBus } from './events.js';
17
17
  import { ROLE_NAME_RE } from '../config.js';
18
18
  import { TaskListError } from '../rooms-tasks/task-lists.js';
19
19
  import { TaskStateError } from '../rooms-tasks/task-state.js';
20
+ import { isProvider } from '../subscriptions/store.js';
20
21
  const statusFor = (code) => ({
21
22
  role_not_found: 404, resource_not_found: 404, unauthorized: 401, forbidden: 403, conflict: 409,
22
23
  idempotency_conflict: 409, stale_state: 409, rate_limited: 429,
@@ -433,6 +434,86 @@ export async function buildWebServer(services, boundary, options = {}) {
433
434
  });
434
435
  return result;
435
436
  });
437
+ // Claude Code / Codex subscription profiles. Responses carry account metadata
438
+ // only; token values never leave the CLI-owned credential files.
439
+ const subscriptions = () => {
440
+ if (!services.subscriptions)
441
+ throw new FleetError('capability_unavailable', 'subscription management is unavailable');
442
+ return services.subscriptions;
443
+ };
444
+ const provider = (raw) => {
445
+ if (!isProvider(raw))
446
+ throw new FleetError('invalid_request', 'unknown subscription provider');
447
+ return raw;
448
+ };
449
+ const subscriptionsChanged = () => events.publish('subscription.changed', {});
450
+ app.get('/api/v1/subscriptions', async (request) => {
451
+ auth.authenticate(request);
452
+ return { providers: await subscriptions().list() };
453
+ });
454
+ app.post('/api/v1/subscriptions/:provider/logins', async (request, reply) => {
455
+ const session = auth.authenticate(request, true);
456
+ const login = await subscriptions().startLogin(provider(request.params.provider), session.id);
457
+ await audit.record({ requestId: request.id, browser: session.id, action: `subscription.login.${request.params.provider}`, result: login.state });
458
+ reply.code(201);
459
+ return login;
460
+ });
461
+ app.get('/api/v1/subscriptions/:provider/logins/:loginId', async (request) => {
462
+ const session = auth.authenticate(request);
463
+ return subscriptions().login(request.params.loginId, session.id).view();
464
+ });
465
+ app.post('/api/v1/subscriptions/:provider/logins/:loginId/code', async (request) => {
466
+ const session = auth.authenticate(request, true);
467
+ const code = request.body?.code;
468
+ if (typeof code !== 'string')
469
+ throw new FleetError('invalid_request', 'code is required');
470
+ const login = subscriptions().login(request.params.loginId, session.id);
471
+ login.submitCode(code.trim());
472
+ await audit.record({ requestId: request.id, browser: session.id, action: `subscription.login.code.${request.params.provider}`, result: 'submitted' });
473
+ return login.view();
474
+ });
475
+ app.delete('/api/v1/subscriptions/:provider/logins/:loginId', async (request) => {
476
+ const session = auth.authenticate(request, true);
477
+ const login = subscriptions().login(request.params.loginId, session.id);
478
+ login.cancel();
479
+ return login.view();
480
+ });
481
+ app.post('/api/v1/subscriptions/:provider/active', async (request) => {
482
+ const session = auth.authenticate(request, true);
483
+ const profileId = request.body?.profileId;
484
+ if (typeof profileId !== 'string')
485
+ throw new FleetError('invalid_request', 'profileId is required');
486
+ const view = await subscriptions().setActive(provider(request.params.provider), profileId);
487
+ subscriptionsChanged();
488
+ await audit.record({ requestId: request.id, browser: session.id, action: `subscription.activate.${request.params.provider}`, result: 'succeeded' });
489
+ return view;
490
+ });
491
+ app.post('/api/v1/subscriptions/:provider/profiles/:profileId/check', async (request) => {
492
+ auth.authenticate(request, true);
493
+ return subscriptions().check(provider(request.params.provider), request.params.profileId);
494
+ });
495
+ app.post('/api/v1/subscriptions/claude/profiles/:profileId/probe', async (request) => {
496
+ const session = auth.authenticate(request, true);
497
+ const usage = await subscriptions().probeClaudeUsage(request.params.profileId);
498
+ await audit.record({ requestId: request.id, browser: session.id, action: 'subscription.usage.probe.claude', result: usage.source });
499
+ return { usage };
500
+ });
501
+ app.patch('/api/v1/subscriptions/:provider/profiles/:profileId', async (request) => {
502
+ auth.authenticate(request, true);
503
+ const label = request.body?.label;
504
+ if (typeof label !== 'string')
505
+ throw new FleetError('invalid_request', 'label is required');
506
+ await subscriptions().rename(provider(request.params.provider), request.params.profileId, label);
507
+ subscriptionsChanged();
508
+ return { ok: true };
509
+ });
510
+ app.delete('/api/v1/subscriptions/:provider/profiles/:profileId', async (request) => {
511
+ const session = auth.authenticate(request, true);
512
+ await subscriptions().remove(provider(request.params.provider), request.params.profileId);
513
+ subscriptionsChanged();
514
+ await audit.record({ requestId: request.id, browser: session.id, action: `subscription.remove.${request.params.provider}`, result: 'succeeded' });
515
+ return { ok: true };
516
+ });
436
517
  app.get('/api/v1/topology', async (request) => {
437
518
  auth.authenticate(request);
438
519
  if (!services.topology)
@@ -543,13 +624,22 @@ export async function buildWebServer(services, boundary, options = {}) {
543
624
  const control = await services.session(request.params.id);
544
625
  if (!control.conversationPage)
545
626
  throw new FleetError('capability_unavailable', 'this role has no conversation ledger');
546
- const page = await control.conversationPage({
547
- after: request.query.after,
548
- limit: request.query.limit ? Number(request.query.limit) : undefined,
549
- });
627
+ const limit = request.query.limit ? Number(request.query.limit) : undefined;
628
+ // `latest` / `before` page backwards from the newest event; `after` pages forward for live updates.
629
+ const backward = request.query.latest === '1' || request.query.before !== undefined;
630
+ if (backward && request.query.after !== undefined)
631
+ throw new FleetError('invalid_request', 'after cannot be combined with latest or before');
632
+ if (backward && !control.conversationTail)
633
+ throw new FleetError('capability_unavailable', 'this role cannot page its conversation backwards');
550
634
  const role = await services.repository.get(request.params.id);
551
635
  const stateDir = role ? services.repository.stateDir(role) : undefined;
552
- return { ...page, events: page.events.map(event => presentAttachmentEvent(stateDir, request.params.id, event)) };
636
+ const present = (event) => presentAttachmentEvent(stateDir, request.params.id, event);
637
+ if (backward) {
638
+ const page = await control.conversationTail({ before: request.query.before, limit });
639
+ return { ...page, events: page.events.map(present), context: page.context.map(present) };
640
+ }
641
+ const page = await control.conversationPage({ after: request.query.after, limit });
642
+ return { ...page, events: page.events.map(present) };
553
643
  });
554
644
  app.get('/api/v1/roles/:id/attachments/:attachmentId', async (request, reply) => {
555
645
  auth.authenticate(request);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/fleet",
3
- "version": "1.2.0-nightly.21",
3
+ "version": "1.2.0-nightly.23",
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",