@ours.network/fleet 1.2.0-nightly.19 → 1.2.0-nightly.20
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/README.md +10 -1
- package/dist/agent-ours/correspondence.d.ts +10 -0
- package/dist/agent-ours/correspondence.js +1 -0
- package/dist/agent-ours/runtime.d.ts +6 -0
- package/dist/agent-ours/runtime.js +36 -0
- package/dist/agent-ours/service.js +2 -1
- package/dist/application/conversation-history.d.ts +6 -0
- package/dist/application/conversation-history.js +65 -0
- package/dist/application/fleet-query-service.d.ts +1 -1
- package/dist/application/fleet-query-service.js +2 -2
- package/dist/application/preset-provenance.d.ts +19 -0
- package/dist/application/preset-provenance.js +59 -0
- package/dist/application/room-layout-definitions.d.ts +45 -0
- package/dist/application/room-layout-definitions.js +130 -0
- package/dist/application/session-control.d.ts +8 -0
- package/dist/application/session-control.js +21 -3
- package/dist/application/task-layouts.d.ts +89 -0
- package/dist/application/task-layouts.js +248 -0
- package/dist/application/task-room-service.d.ts +82 -0
- package/dist/application/task-room-service.js +197 -9
- package/dist/build-info.json +4 -4
- package/dist/cli.js +2 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +7 -0
- package/dist/init-wizard.js +4 -2
- package/dist/rooms-tasks/add-member.d.ts +48 -0
- package/dist/rooms-tasks/add-member.js +83 -0
- package/dist/rooms-tasks/cli.js +20 -0
- package/dist/rooms-tasks/external-worker.d.ts +1 -1
- package/dist/rooms-tasks/external-worker.js +3 -3
- package/dist/rooms-tasks/layout-cli.d.ts +2 -0
- package/dist/rooms-tasks/layout-cli.js +81 -0
- package/dist/rooms-tasks/layout-config.d.ts +11 -0
- package/dist/rooms-tasks/layout-config.js +89 -0
- package/dist/rooms-tasks/layout-control.d.ts +21 -0
- package/dist/rooms-tasks/layout-control.js +52 -0
- package/dist/rooms-tasks/layout-service.d.ts +39 -0
- package/dist/rooms-tasks/layout-service.js +173 -0
- package/dist/rooms-tasks/layout.d.ts +102 -0
- package/dist/rooms-tasks/layout.js +227 -0
- package/dist/rooms-tasks/live-readiness.js +2 -1
- package/dist/rooms-tasks/provision.d.ts +3 -0
- package/dist/rooms-tasks/provision.js +44 -22
- package/dist/rooms-tasks/room-state.d.ts +1 -1
- package/dist/rooms-tasks/room-state.js +3 -1
- package/dist/rooms-tasks/task-state.d.ts +19 -0
- package/dist/rooms-tasks/task-state.js +58 -6
- package/dist/rooms-tasks/types.d.ts +14 -0
- package/dist/runner.d.ts +7 -2
- package/dist/runner.js +52 -4
- package/dist/session/acp.d.ts +3 -0
- package/dist/session/acp.js +7 -0
- package/dist/session/arbiter.d.ts +3 -0
- package/dist/session/arbiter.js +20 -2
- package/dist/session/codex-app-server.d.ts +3 -0
- package/dist/session/codex-app-server.js +25 -1
- package/dist/session/control.d.ts +10 -1
- package/dist/session/control.js +36 -2
- package/dist/session/conversation-normalizer.d.ts +2 -1
- package/dist/session/conversation-normalizer.js +1 -1
- package/dist/session/conversation-types.d.ts +5 -3
- package/dist/session/task-notice.d.ts +14 -0
- package/dist/session/task-notice.js +13 -0
- package/dist/session/types.d.ts +3 -0
- package/dist/socket-path.d.ts +2 -0
- package/dist/socket-path.js +22 -0
- package/dist/temp-idle.d.ts +17 -0
- package/dist/temp-idle.js +42 -0
- package/dist/temp-lifecycle.d.ts +1 -1
- package/dist/temp-lifecycle.js +1 -1
- package/dist/web/agent-attachments.d.ts +29 -0
- package/dist/web/agent-attachments.js +162 -0
- package/dist/web/auth.d.ts +4 -1
- package/dist/web/auth.js +23 -2
- package/dist/web/control.js +2 -1
- package/dist/web/fleet-config-service.d.ts +4 -1
- package/dist/web/fleet-config-service.js +46 -15
- package/dist/web/prefix-gateway.d.ts +20 -0
- package/dist/web/prefix-gateway.js +150 -0
- package/dist/web/runtime.d.ts +4 -0
- package/dist/web/runtime.js +19 -14
- package/dist/web/server.d.ts +5 -0
- package/dist/web/server.js +275 -24
- package/docs/room-layouts.md +92 -0
- package/examples/room-layouts/fresh.yaml +27 -0
- package/examples/room-layouts/shared.yaml +19 -0
- package/package.json +14 -13
- package/presets/fleet/room_layouts/engineering.yaml +25 -0
- package/presets/fleet/room_layouts/pair.yaml +24 -0
- package/presets/fleet/room_layouts/single.yaml +17 -0
- package/presets/fleet/room_layouts/team.yaml +30 -0
- package/dist/web-app/assets/index-59GF-gsZ.css +0 -1
- package/dist/web-app/assets/index-C0h3ALvs.js +0 -10
- package/dist/web-app/icons/ours-fleet-maskable.svg +0 -4
- package/dist/web-app/icons/ours-fleet.svg +0 -4
- package/dist/web-app/index.html +0 -17
- package/dist/web-app/manifest.webmanifest +0 -15
- package/dist/web-app/offline.html +0 -18
- package/dist/web-app/sw.js +0 -51
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { createServer, request as httpRequest } from 'node:http';
|
|
2
|
+
import { request as httpsRequest } from 'node:https';
|
|
3
|
+
const hopHeaders = new Set(['connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailer', 'transfer-encoding', 'upgrade']);
|
|
4
|
+
function headersFor(message) {
|
|
5
|
+
const denied = new Set([...hopHeaders, ...(message.headers.connection ?? '').split(',').map(x => x.trim().toLowerCase())]);
|
|
6
|
+
return Object.fromEntries(Object.entries(message.headers).filter(([key]) => !denied.has(key)));
|
|
7
|
+
}
|
|
8
|
+
/** Raw transport deliberately runs outside Fastify's JSON parser and body limits. */
|
|
9
|
+
export function createPrefixGateway(options) {
|
|
10
|
+
const auth = options.auth === 'backend' ? undefined : options.auth;
|
|
11
|
+
const services = options.services.map(target => {
|
|
12
|
+
if (!/^\/[a-z][a-z0-9-]*$/.test(target.prefix) || target.prefix === '/fleet')
|
|
13
|
+
throw new Error('invalid service prefix');
|
|
14
|
+
const origin = new URL(target.origin);
|
|
15
|
+
if (!['http:', 'https:'].includes(origin.protocol) || origin.pathname !== '/' || origin.search || origin.hash || origin.username || origin.password)
|
|
16
|
+
throw new Error('service target must be an HTTP origin');
|
|
17
|
+
return { ...target, origin: origin.origin };
|
|
18
|
+
});
|
|
19
|
+
if (new Set(services.map(x => x.prefix)).size !== services.length)
|
|
20
|
+
throw new Error('duplicate service prefix');
|
|
21
|
+
const select = (req) => {
|
|
22
|
+
const raw = req.url ?? '/';
|
|
23
|
+
if (!raw.startsWith('/') || raw.startsWith('//'))
|
|
24
|
+
throw new Error('invalid request target');
|
|
25
|
+
const path = raw.split('?')[0];
|
|
26
|
+
const service = services.find(x => path === x.prefix || path.startsWith(x.prefix + '/'));
|
|
27
|
+
const fleet = path === '/fleet/api' || path.startsWith('/fleet/api/');
|
|
28
|
+
return { service, page: !service && !fleet && !path.startsWith('/api/'), origin: service?.origin ?? options.fleetOrigin,
|
|
29
|
+
path: service ? (raw.slice(service.prefix.length) || '/').replace(/^\?/, '/?') : fleet ? raw.slice('/fleet'.length) : raw };
|
|
30
|
+
};
|
|
31
|
+
const prepare = (req, upgrade = false) => {
|
|
32
|
+
const route = select(req);
|
|
33
|
+
const request = { headers: req.headers, method: req.method };
|
|
34
|
+
if (!auth && (req.headers.origin || req.headers['sec-fetch-site'] || !route.service))
|
|
35
|
+
throw new Error('machine-client listener rejects browser requests');
|
|
36
|
+
auth?.validateBoundary(request, upgrade, route.page);
|
|
37
|
+
const session = route.service && auth ? auth.authenticate(request, !upgrade && !['GET', 'HEAD', 'OPTIONS'].includes(req.method ?? 'GET')) : undefined;
|
|
38
|
+
const headers = headersFor(req);
|
|
39
|
+
// Forward only the explicit browser context, never an attacker-selected forwarding chain.
|
|
40
|
+
for (const key of Object.keys(headers))
|
|
41
|
+
if (key.startsWith('x-forwarded-') || key === 'forwarded')
|
|
42
|
+
delete headers[key];
|
|
43
|
+
if (route.service) {
|
|
44
|
+
if (auth)
|
|
45
|
+
for (const name of ['cookie', 'authorization', 'x-csrf-token', 'x-ours-api-token'])
|
|
46
|
+
delete headers[name];
|
|
47
|
+
Object.assign(headers, route.service.headers);
|
|
48
|
+
headers.host = new URL(route.origin).host;
|
|
49
|
+
}
|
|
50
|
+
if (upgrade) {
|
|
51
|
+
headers.connection = 'Upgrade';
|
|
52
|
+
headers.upgrade = 'websocket';
|
|
53
|
+
}
|
|
54
|
+
return { ...route, headers, session };
|
|
55
|
+
};
|
|
56
|
+
const track = (req, session, close) => {
|
|
57
|
+
if (!session)
|
|
58
|
+
return () => { };
|
|
59
|
+
const unbind = auth.bindTransport(session.id, close);
|
|
60
|
+
const timer = setInterval(() => { try {
|
|
61
|
+
auth.authenticate({ headers: req.headers });
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
close();
|
|
65
|
+
} }, 30_000);
|
|
66
|
+
timer.unref();
|
|
67
|
+
return () => { clearInterval(timer); unbind(); };
|
|
68
|
+
};
|
|
69
|
+
const server = createServer((req, res) => {
|
|
70
|
+
let route;
|
|
71
|
+
try {
|
|
72
|
+
route = prepare(req);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
const code = error.code;
|
|
76
|
+
res.writeHead(code === 'unauthorized' ? 401 : 403, { 'content-type': 'application/json', 'cache-control': 'no-store' });
|
|
77
|
+
res.end(JSON.stringify({ error: { code: code ?? 'forbidden', message: error.message } }));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (req.url === '/') {
|
|
81
|
+
res.writeHead(302, { location: '/fleet' });
|
|
82
|
+
res.end();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const upstream = (route.origin.startsWith('https:') ? httpsRequest : httpRequest)(route.origin, {
|
|
86
|
+
method: req.method, path: route.path, headers: route.headers,
|
|
87
|
+
}, response => {
|
|
88
|
+
const headers = headersFor(response);
|
|
89
|
+
if (route.service)
|
|
90
|
+
delete headers['set-cookie'];
|
|
91
|
+
res.writeHead(response.statusCode ?? 502, headers);
|
|
92
|
+
response.on('error', () => res.destroy());
|
|
93
|
+
response.on('aborted', () => res.destroy());
|
|
94
|
+
response.on('close', () => { if (!response.complete)
|
|
95
|
+
res.destroy(); });
|
|
96
|
+
response.pipe(res);
|
|
97
|
+
res.on('close', () => response.destroy());
|
|
98
|
+
});
|
|
99
|
+
upstream.on('error', () => {
|
|
100
|
+
if (!res.headersSent)
|
|
101
|
+
res.writeHead(502, { 'content-type': 'application/json' });
|
|
102
|
+
res.end(JSON.stringify({ error: { code: 'upstream_unavailable', message: 'Service is unavailable' } }));
|
|
103
|
+
});
|
|
104
|
+
const untrack = track(req, route.session, () => { upstream.destroy(); res.destroy(); });
|
|
105
|
+
res.on('close', untrack);
|
|
106
|
+
req.on('aborted', () => upstream.destroy());
|
|
107
|
+
res.on('close', () => upstream.destroy());
|
|
108
|
+
req.pipe(upstream);
|
|
109
|
+
});
|
|
110
|
+
const sockets = new Set();
|
|
111
|
+
server.on('connection', socket => { sockets.add(socket); socket.on('close', () => sockets.delete(socket)); });
|
|
112
|
+
server.on('upgrade', (req, socket, head) => {
|
|
113
|
+
let route;
|
|
114
|
+
try {
|
|
115
|
+
route = prepare(req, true);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
socket.end('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n');
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const upstream = (route.origin.startsWith('https:') ? httpsRequest : httpRequest)(route.origin, {
|
|
122
|
+
method: req.method, path: route.path, headers: route.headers,
|
|
123
|
+
});
|
|
124
|
+
upstream.on('upgrade', (response, peer, upstreamHead) => {
|
|
125
|
+
const headers = headersFor(response);
|
|
126
|
+
headers.connection = 'Upgrade';
|
|
127
|
+
headers.upgrade = 'websocket';
|
|
128
|
+
delete headers['set-cookie'];
|
|
129
|
+
socket.write(`HTTP/1.1 101 Switching Protocols\r\n${Object.entries(headers).flatMap(([k, v]) => (Array.isArray(v) ? v : [v]).map(value => `${k}: ${value}\r\n`)).join('')}\r\n`);
|
|
130
|
+
if (head.length)
|
|
131
|
+
peer.write(head);
|
|
132
|
+
if (upstreamHead.length)
|
|
133
|
+
socket.write(upstreamHead);
|
|
134
|
+
const untrack = track(req, route.session, () => { peer.destroy(); socket.destroy(); });
|
|
135
|
+
socket.on('close', untrack);
|
|
136
|
+
peer.pipe(socket);
|
|
137
|
+
socket.pipe(peer);
|
|
138
|
+
peer.on('error', () => socket.destroy());
|
|
139
|
+
socket.on('error', () => peer.destroy());
|
|
140
|
+
socket.on('close', () => peer.destroy());
|
|
141
|
+
peer.on('close', () => socket.destroy());
|
|
142
|
+
});
|
|
143
|
+
upstream.on('response', response => { socket.end(`HTTP/1.1 ${response.statusCode ?? 502} Rejected\r\nConnection: close\r\n\r\n`); response.destroy(); });
|
|
144
|
+
upstream.on('error', () => socket.destroy());
|
|
145
|
+
socket.on('close', () => upstream.destroy());
|
|
146
|
+
upstream.end();
|
|
147
|
+
});
|
|
148
|
+
return { server, async close() { for (const socket of sockets)
|
|
149
|
+
socket.destroy(); await new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve())); } };
|
|
150
|
+
}
|
package/dist/web/runtime.d.ts
CHANGED
|
@@ -2,6 +2,10 @@ import { type WebServer } from './server.js';
|
|
|
2
2
|
import { type WebAccessConfig } from './access.js';
|
|
3
3
|
export interface StartWebOptions {
|
|
4
4
|
configPath?: string;
|
|
5
|
+
/** Separate console state for an independently launched gateway. */
|
|
6
|
+
webStateDir?: string;
|
|
7
|
+
control?: boolean;
|
|
8
|
+
staticRoot?: string;
|
|
5
9
|
port?: number;
|
|
6
10
|
open?: boolean;
|
|
7
11
|
binPath: string;
|
package/dist/web/runtime.js
CHANGED
|
@@ -21,6 +21,8 @@ import { FleetConfigService } from './fleet-config-service.js';
|
|
|
21
21
|
import { mergeTopology } from './topology-model.js';
|
|
22
22
|
import { TopologyDraftStore } from './topology-draft-store.js';
|
|
23
23
|
import { TopologyPromoteService } from './topology-promote.js';
|
|
24
|
+
import { RoomLayoutDefinitions } from '../application/room-layout-definitions.js';
|
|
25
|
+
import { PresetProvenance } from '../application/preset-provenance.js';
|
|
24
26
|
import { doctor } from '../doctor.js';
|
|
25
27
|
import { acquireWebServerLock } from './lock.js';
|
|
26
28
|
import { TrustedDeviceStore } from './device-store.js';
|
|
@@ -50,8 +52,8 @@ export async function startWebConsole(options) {
|
|
|
50
52
|
const requestedPort = options.port ?? 49_271;
|
|
51
53
|
if (!Number.isInteger(requestedPort) || requestedPort < 0 || requestedPort > 65_535)
|
|
52
54
|
throw new FleetError('invalid_request', 'port must be between 0 and 65535');
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
+
const webDir = options.webStateDir ?? resolve(stateRoot(), 'web');
|
|
56
|
+
const lock = acquireWebServerLock(webDir);
|
|
55
57
|
const bind = options.bind ?? '127.0.0.1';
|
|
56
58
|
const publicOrigin = options.publicOrigin ? validatePublicOrigin(options.publicOrigin) : undefined;
|
|
57
59
|
if (!isLoopback(bind) && !publicOrigin) {
|
|
@@ -159,7 +161,9 @@ export async function startWebConsole(options) {
|
|
|
159
161
|
try {
|
|
160
162
|
server = await buildWebServer({
|
|
161
163
|
query, repository, logs, commands, creation, removal, audit, events, watchdogs, configuration,
|
|
162
|
-
taskRooms: new TaskRoomApplicationService(options.configPath),
|
|
164
|
+
taskRooms: new TaskRoomApplicationService(options.configPath, { binPath: () => options.binPath }),
|
|
165
|
+
roomLayouts: new RoomLayoutDefinitions(options.configPath),
|
|
166
|
+
presetProvenance: new PresetProvenance(options.configPath),
|
|
163
167
|
topology: readTopology, topologyDrafts, topologyPromote,
|
|
164
168
|
async session(roleId) {
|
|
165
169
|
const role = await repository.get(roleId);
|
|
@@ -174,7 +178,7 @@ export async function startWebConsole(options) {
|
|
|
174
178
|
}
|
|
175
179
|
throw new FleetError('capability_unavailable', 'role session backend is unavailable');
|
|
176
180
|
},
|
|
177
|
-
}, { origin: `http://127.0.0.1:${requestedPort}`, host: `127.0.0.1:${requestedPort}` }, { auth });
|
|
181
|
+
}, { origin: `http://127.0.0.1:${requestedPort}`, host: `127.0.0.1:${requestedPort}` }, { auth, staticRoot: options.staticRoot });
|
|
178
182
|
}
|
|
179
183
|
catch (error) {
|
|
180
184
|
lock.release();
|
|
@@ -203,15 +207,16 @@ export async function startWebConsole(options) {
|
|
|
203
207
|
});
|
|
204
208
|
let control;
|
|
205
209
|
try {
|
|
206
|
-
control
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
210
|
+
if (options.control !== false)
|
|
211
|
+
control = await startWebControlServer({
|
|
212
|
+
dir: webDir,
|
|
213
|
+
onOpen() {
|
|
214
|
+
const url = access.mode === 'pairing'
|
|
215
|
+
? `${browserOrigin}/#bootstrap=${server.auth.mintBootstrap()}` : `${browserOrigin}/`;
|
|
216
|
+
openBrowser(url);
|
|
217
|
+
},
|
|
218
|
+
onRevokeAll() { server.auth.revokeAllTrustedDevices(); },
|
|
219
|
+
});
|
|
215
220
|
}
|
|
216
221
|
catch (error) {
|
|
217
222
|
await server.close();
|
|
@@ -225,7 +230,7 @@ export async function startWebConsole(options) {
|
|
|
225
230
|
...server, address: browserOrigin,
|
|
226
231
|
async close() {
|
|
227
232
|
try {
|
|
228
|
-
await control
|
|
233
|
+
await control?.close();
|
|
229
234
|
await server.close();
|
|
230
235
|
}
|
|
231
236
|
finally {
|
package/dist/web/server.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ import type { TopologyDraftStore } from './topology-draft-store.js';
|
|
|
16
16
|
import type { TopologyPromoteService } from './topology-promote.js';
|
|
17
17
|
import type { RoleRemovalService } from '../application/role-removal-service.js';
|
|
18
18
|
import type { TaskRoomApplicationService } from '../application/task-room-service.js';
|
|
19
|
+
import type { RoomLayoutDefinitions } from '../application/room-layout-definitions.js';
|
|
20
|
+
import type { PresetProvenance } from '../application/preset-provenance.js';
|
|
19
21
|
export interface WebServices {
|
|
20
22
|
query: FleetQueryService;
|
|
21
23
|
repository: RoleRepository;
|
|
@@ -32,6 +34,8 @@ export interface WebServices {
|
|
|
32
34
|
topologyPromote?: TopologyPromoteService;
|
|
33
35
|
removal?: RoleRemovalService;
|
|
34
36
|
taskRooms?: TaskRoomApplicationService;
|
|
37
|
+
roomLayouts?: RoomLayoutDefinitions;
|
|
38
|
+
presetProvenance?: PresetProvenance;
|
|
35
39
|
oursTools?: Pick<SupervisorOursTools, 'list' | 'call'>;
|
|
36
40
|
}
|
|
37
41
|
export interface WebServer {
|
|
@@ -46,4 +50,5 @@ export declare function buildWebServer(services: WebServices, boundary: {
|
|
|
46
50
|
host: string;
|
|
47
51
|
}, options?: {
|
|
48
52
|
auth?: WebAuth;
|
|
53
|
+
staticRoot?: string;
|
|
49
54
|
}): Promise<WebServer>;
|