@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.
- package/dist/build-info.json +4 -4
- package/dist/web/auth.d.ts +2 -0
- package/dist/web/auth.js +4 -0
- package/dist/web/prefix-gateway.js +25 -3
- package/dist/web/runtime.js +14 -2
- package/dist/web/server.d.ts +3 -0
- package/dist/web/server.js +52 -1
- package/dist/web-app/version.json +1 -1
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.2.0-nightly.
|
|
3
|
-
"buildId": "
|
|
4
|
-
"commit": "
|
|
2
|
+
"version": "1.2.0-nightly.34",
|
|
3
|
+
"buildId": "1d0ffc344d7e",
|
|
4
|
+
"commit": "b17e975b8f21b865f6690fc7299851447b53fdad",
|
|
5
5
|
"dirty": true,
|
|
6
|
-
"builtAt": "2026-10-
|
|
6
|
+
"builtAt": "2026-10-02T12:17:07.327Z",
|
|
7
7
|
"capabilities": [
|
|
8
8
|
"workspace.enroll.preserve-profile-v1",
|
|
9
9
|
"cowork.http-management-v1",
|
package/dist/web/auth.d.ts
CHANGED
|
@@ -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(); });
|
package/dist/web/runtime.js
CHANGED
|
@@ -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',
|
package/dist/web/server.d.ts
CHANGED
|
@@ -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;
|
package/dist/web/server.js
CHANGED
|
@@ -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-
|
|
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.
|
|
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",
|