@ours.network/fleet 1.2.0-nightly.33 → 1.2.0-nightly.34

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.
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "1.2.0-nightly.33",
3
- "buildId": "c27262dcfdeb",
4
- "commit": "57b2fa27b72ae4f53fbad81e966139474b7a6e03",
2
+ "version": "1.2.0-nightly.34",
3
+ "buildId": "1d0ffc344d7e",
4
+ "commit": "b17e975b8f21b865f6690fc7299851447b53fdad",
5
5
  "dirty": true,
6
- "builtAt": "2026-10-02T10:40:46.562Z",
6
+ "builtAt": "2026-10-02T12:17:07.327Z",
7
7
  "capabilities": [
8
8
  "workspace.enroll.preserve-profile-v1",
9
9
  "cowork.http-management-v1",
@@ -52,6 +52,8 @@ export declare class WebAuth {
52
52
  }): void;
53
53
  /** Mint a replacement for an operator-triggered reauthentication ceremony. */
54
54
  mintBootstrap(): string;
55
+ /** The configured account origin, when this request comes from it and workspace devices are enabled. */
56
+ accountOrigin(request: Pick<FastifyRequest, 'headers'>): string | undefined;
55
57
  validateBoundary(request: FastifyRequest, requireOrigin: boolean, allowPageNavigation?: boolean): void;
56
58
  exchange(request: FastifyRequest): AuthResult;
57
59
  login(request: FastifyRequest, password: string): AuthResult;
package/dist/web/auth.js CHANGED
@@ -54,6 +54,10 @@ export class WebAuth {
54
54
  this.bootstrapUsed = false;
55
55
  return this._bootstrapSecret;
56
56
  }
57
+ /** The configured account origin, when this request comes from it and workspace devices are enabled. */
58
+ accountOrigin(request) {
59
+ return this.workspaceDevices && request.headers.origin === this.appOrigin ? this.appOrigin : undefined;
60
+ }
57
61
  validateBoundary(request, requireOrigin, allowPageNavigation = false) {
58
62
  const host = request.headers.host;
59
63
  const hosts = this.allowedHosts.size ? this.allowedHosts : new Set([this.host]);
@@ -72,14 +72,31 @@ export function createPrefixGateway(options) {
72
72
  timer.unref();
73
73
  return () => { clearInterval(timer); unbind(); };
74
74
  };
75
+ // The account App calls workspace services directly from its own origin with a device
76
+ // credential. Only that exact configured origin is answered; the reply grants nothing by
77
+ // itself, every request is still authenticated below.
78
+ const accountCors = (req) => {
79
+ const origin = auth?.accountOrigin(req);
80
+ return origin ? { 'access-control-allow-origin': origin, vary: 'Origin',
81
+ 'access-control-expose-headers': 'Accept-Ranges, Content-Disposition, Content-Length, Content-Range, Content-Type, ETag' } : {};
82
+ };
75
83
  const server = createServer((req, res) => {
84
+ const cors = accountCors(req);
76
85
  let route;
77
86
  try {
87
+ if (req.method === 'OPTIONS' && cors.vary && req.headers['access-control-request-method'] && select(req).service) {
88
+ auth.validateBoundary({ headers: req.headers, method: req.method, url: req.url }, false);
89
+ res.writeHead(204, { ...cors, 'cache-control': 'no-store', 'access-control-max-age': '600',
90
+ 'access-control-allow-methods': 'GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS',
91
+ 'access-control-allow-headers': 'Authorization, Content-Type, Idempotency-Key, If-None-Match, Range, X-CSRF-Token, X-Ours-Messenger-CSRF, X-Voice-Duration' });
92
+ res.end();
93
+ return;
94
+ }
78
95
  route = prepare(req);
79
96
  }
80
97
  catch (error) {
81
98
  const code = error.code;
82
- res.writeHead(code === 'unauthorized' ? 401 : 403, { 'content-type': 'application/json', 'cache-control': 'no-store' });
99
+ res.writeHead(code === 'unauthorized' ? 401 : 403, { ...cors, 'content-type': 'application/json', 'cache-control': 'no-store' });
83
100
  res.end(JSON.stringify({ error: { code: code ?? 'forbidden', message: error.message } }));
84
101
  return;
85
102
  }
@@ -92,8 +109,13 @@ export function createPrefixGateway(options) {
92
109
  method: req.method, path: route.path, headers: route.headers,
93
110
  }, response => {
94
111
  const headers = headersFor(response);
95
- if (route.service)
112
+ if (route.service) {
96
113
  delete headers['set-cookie'];
114
+ for (const key of Object.keys(headers))
115
+ if (key.startsWith('access-control-'))
116
+ delete headers[key];
117
+ Object.assign(headers, cors);
118
+ }
97
119
  res.writeHead(response.statusCode ?? 502, headers);
98
120
  response.on('error', () => res.destroy());
99
121
  response.on('aborted', () => res.destroy());
@@ -104,7 +126,7 @@ export function createPrefixGateway(options) {
104
126
  });
105
127
  upstream.on('error', () => {
106
128
  if (!res.headersSent)
107
- res.writeHead(502, { 'content-type': 'application/json' });
129
+ res.writeHead(502, { ...(route.service ? cors : {}), 'content-type': 'application/json' });
108
130
  res.end(JSON.stringify({ error: { code: 'upstream_unavailable', message: 'Service is unavailable' } }));
109
131
  });
110
132
  const untrack = track(req, route.session, () => { upstream.destroy(); res.destroy(); });
@@ -10,7 +10,7 @@ import { spawn } from 'node:child_process';
10
10
  import { FleetNotificationProducer } from '../notifications/fleet-producer.js';
11
11
  import { producerConfig } from '../notifications/outbox.js';
12
12
  import { SubscriptionService } from '../subscriptions/service.js';
13
- import { existsSync, realpathSync, readFileSync } from 'node:fs';
13
+ import { existsSync, mkdirSync, realpathSync, readFileSync } from 'node:fs';
14
14
  import { resolve } from 'node:path';
15
15
  import { loadConfig } from '../config.js';
16
16
  import { RoleRepository } from '../application/role-repository.js';
@@ -24,7 +24,8 @@ import { FleetError } from '../application/errors.js';
24
24
  import { controlRequest, controlSocketPath } from '../session/control.js';
25
25
  import { pickBackend } from '../supervisor/index.js';
26
26
  import { realExec } from '../exec.js';
27
- import { home, stateRoot, agentsRoot, tmpRoot } from '../paths.js';
27
+ import { home, stateRoot, agentsRoot, tmpRoot, logsRoot } from '../paths.js';
28
+ import { executeInitAnswers, publishSetup } from '../init-wizard.js';
28
29
  import { AuditSink } from './audit.js';
29
30
  import { FleetEventBus } from './events.js';
30
31
  import { buildWebServer } from './server.js';
@@ -190,6 +191,17 @@ export async function startWebConsole(options) {
190
191
  layoutBindings: new LayoutBindingGrants(new RoomLayoutService(options.configPath).supervisor()),
191
192
  presetProvenance: new PresetProvenance(options.configPath),
192
193
  topology: readTopology, topologyDrafts, topologyPromote,
194
+ // Same boundary as `ours-fleet init --settings`: existing files are preserved, missing defaults are added.
195
+ onboardingSetup: async (answers) => {
196
+ await executeInitAnswers(answers, configuration.path, {
197
+ async hostSetup() {
198
+ for (const directory of [agentsRoot(), tmpRoot(), logsRoot()])
199
+ mkdirSync(directory, { recursive: true });
200
+ await pickBackend().init(options.binPath);
201
+ },
202
+ publish: publishSetup,
203
+ });
204
+ },
193
205
  subscriptions: new SubscriptionService({
194
206
  agents: async () => (await query.list(true)).map(({ role, status }) => ({
195
207
  roleId: role.id, stateDir: repository.stateDir(role), running: status.supervisor.liveness === 'running',
@@ -1,3 +1,4 @@
1
+ import { type InitAnswers } from '../init-wizard.js';
1
2
  import { SupervisorOursTools } from '../application/supervisor-ours-tools.js';
2
3
  import { type FastifyInstance } from 'fastify';
3
4
  import type { FleetQueryService } from '../application/fleet-query-service.js';
@@ -41,6 +42,8 @@ export interface WebServices {
41
42
  presetProvenance?: PresetProvenance;
42
43
  oursTools?: Pick<SupervisorOursTools, 'list' | 'call'>;
43
44
  subscriptions?: SubscriptionService;
45
+ /** First-time setup from the packaged presets; the account App supplies only the model choices. */
46
+ onboardingSetup?(answers: InitAnswers): Promise<void>;
44
47
  }
45
48
  export interface WebServer {
46
49
  app: FastifyInstance;
@@ -22,6 +22,7 @@ import { ROLE_NAME_RE } from '../config.js';
22
22
  import { TaskListError } from '../rooms-tasks/task-lists.js';
23
23
  import { TaskStateError } from '../rooms-tasks/task-state.js';
24
24
  import { isProvider } from '../subscriptions/store.js';
25
+ const ONBOARDING_WORK = ['coordination', 'development', 'review'];
25
26
  const statusFor = (code) => ({
26
27
  role_not_found: 404, resource_not_found: 404, unauthorized: 401, forbidden: 403, conflict: 409,
27
28
  idempotency_conflict: 409, stale_state: 409, rate_limited: 429,
@@ -165,7 +166,57 @@ export async function buildWebServer(services, boundary, options = {}) {
165
166
  }
166
167
  }));
167
168
  const catalog = validateCatalog(JSON.parse(readFileSync(join(packagedPresetRoot(), 'brain-catalog.json'), 'utf8')));
168
- return { harnesses, catalog: catalog.models, providers: services.subscriptions ? await services.subscriptions.list() : [], coordinator: 'FleetCoordinator' };
169
+ return { harnesses, catalog: catalog.models, providers: services.subscriptions ? await services.subscriptions.list() : [], coordinator: 'FleetCoordinator', configured: onboardingConfigured() };
170
+ });
171
+ // First-time setup is what creates the persistent coordinator. A configuration that loads and
172
+ // has it is set up, whatever its brains are called; its models are then changed by editing it.
173
+ const onboardingConfigured = () => {
174
+ if (!services.configuration)
175
+ return false;
176
+ try {
177
+ return Boolean(services.configuration.read(false).model.agents.FleetCoordinator);
178
+ }
179
+ catch {
180
+ return false;
181
+ }
182
+ };
183
+ let onboardingSetupQueue = Promise.resolve();
184
+ app.post('/api/v1/onboarding/setup', async (request) => {
185
+ auth.authenticate(request, true);
186
+ if (!services.configuration || !services.onboardingSetup)
187
+ throw new FleetError('capability_unavailable', 'first-time Fleet setup is unavailable');
188
+ const requested = request.body?.models;
189
+ const supported = validateCatalog(JSON.parse(readFileSync(join(packagedPresetRoot(), 'brain-catalog.json'), 'utf8'))).models;
190
+ const models = {};
191
+ for (const work of ONBOARDING_WORK) {
192
+ const matches = supported.filter(model => model.harness === requested?.[work]?.harness && model.model === requested?.[work]?.model);
193
+ if (matches.length !== 1)
194
+ throw new FleetError('invalid_request', `choose a supported model for ${work}`);
195
+ models[work] = matches[0];
196
+ }
197
+ const chosen = Object.values(models);
198
+ const subscriptions = [...new Set(chosen.map(model => model.harness === 'codex' ? 'codex' : 'claude'))];
199
+ const oneModel = new Set(chosen.map(model => `${model.harness}\0${model.model}`)).size === 1;
200
+ const configuration = services.configuration, setup = services.onboardingSetup;
201
+ // One setup at a time: the first-time check and the publication it guards must not interleave.
202
+ const run = onboardingSetupQueue.then(async () => {
203
+ if (onboardingConfigured())
204
+ throw new FleetError('conflict', 'Fleet is already set up; change its models in the configuration');
205
+ try {
206
+ await setup({ subscriptions, assignmentStrategy: oneModel ? 'one-model' : 'per-job', models, reasoning: 'balanced' });
207
+ }
208
+ catch (error) {
209
+ throw new FleetError('backend_failure', `Fleet setup did not complete: ${error.message}`);
210
+ }
211
+ // Setup preserves files it finds, so confirm the published models are the ones this request chose.
212
+ const brains = configuration.read(true).model.brains ?? {};
213
+ for (const work of ONBOARDING_WORK)
214
+ if (brains[work]?.harness !== models[work].harness || brains[work]?.model !== models[work].model)
215
+ throw new FleetError('conflict', 'Fleet was set up with different models; review its configuration');
216
+ return { configured: true };
217
+ });
218
+ onboardingSetupQueue = run.catch(() => { });
219
+ return run;
169
220
  });
170
221
  app.get('/api/v1/creation-capabilities', async (request) => {
171
222
  auth.authenticate(request);
@@ -1 +1 @@
1
- {"sha":"21b1610c125e41982803ecc8c389c169dd1252c4","time":"2026-10-02T10:38:31.229Z"}
1
+ {"sha":"21b1610c125e41982803ecc8c389c169dd1252c4","time":"2026-10-02T12:14:07.133Z"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/fleet",
3
- "version": "1.2.0-nightly.33",
3
+ "version": "1.2.0-nightly.34",
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",