@ours.network/fleet 1.2.0-nightly.34 → 1.2.0-nightly.36
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 +28 -49
- package/dist/application/codex-models.d.ts +15 -0
- package/dist/application/codex-models.js +56 -0
- package/dist/build-info.json +4 -4
- package/dist/cli.js +11 -41
- package/dist/config.d.ts +2 -0
- package/dist/config.js +1 -1
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +10 -15
- package/dist/harness/codex-runtime.d.ts +11 -1
- package/dist/harness/codex-runtime.js +31 -4
- package/dist/harness/codex.js +4 -2
- package/dist/init-wizard.js +2 -1
- package/dist/minimal-setup.js +2 -1
- package/dist/paths.d.ts +2 -0
- package/dist/paths.js +2 -0
- package/dist/rooms-tasks/config.d.ts +1 -1
- package/dist/rooms-tasks/config.js +8 -3
- package/dist/subscriptions/cli.d.ts +15 -2
- package/dist/subscriptions/cli.js +41 -4
- package/dist/web/auth.d.ts +1 -1
- package/dist/web/auth.js +2 -5
- package/dist/web/control.d.ts +1 -2
- package/dist/web/control.js +0 -2
- package/dist/web/prefix-gateway.js +2 -7
- package/dist/web/runtime.d.ts +0 -2
- package/dist/web/runtime.js +3 -18
- package/dist/web/server.d.ts +3 -1
- package/dist/web/server.js +14 -35
- package/dist/workspace-enrollment.d.ts +17 -0
- package/dist/workspace-enrollment.js +117 -9
- package/package.json +2 -3
- package/presets/fleet/agent_templates/Critic.yaml +1 -1
- package/presets/fleet/agent_templates/Developer.yaml +1 -1
- package/presets/fleet/agent_templates/Engineer.yaml +1 -1
- package/presets/fleet/agent_templates/LocalCoordinator.yaml +1 -1
- package/presets/fleet/agents/FleetCoordinator.yaml +1 -1
- package/presets/fleet/roles/Coordinator.yaml +6 -0
- package/presets/manifest.json +1 -1
- package/dist/web-app/assets/FleetApp-D58F4xje.css +0 -1
- package/dist/web-app/assets/FleetApp-Gjz3h8uS.js +0 -302
- package/dist/web-app/assets/FleetApp-Gjz3h8uS.js.map +0 -1
- package/dist/web-app/assets/abnfDiagram-VCTEODGH-t4h5V8Cs.js +0 -2
- package/dist/web-app/assets/abnfDiagram-VCTEODGH-t4h5V8Cs.js.map +0 -1
- package/dist/web-app/assets/arc-DbufDTF6.js +0 -2
- package/dist/web-app/assets/arc-DbufDTF6.js.map +0 -1
- package/dist/web-app/assets/architectureDiagram-5GKGNRK7-D6K8jMN5.js +0 -37
- package/dist/web-app/assets/architectureDiagram-5GKGNRK7-D6K8jMN5.js.map +0 -1
- package/dist/web-app/assets/blockDiagram-I7D4REHJ-CWvkET2x.js +0 -130
- package/dist/web-app/assets/blockDiagram-I7D4REHJ-CWvkET2x.js.map +0 -1
- package/dist/web-app/assets/browser-B7y08DsC.js +0 -9
- package/dist/web-app/assets/browser-B7y08DsC.js.map +0 -1
- package/dist/web-app/assets/c4Diagram-7LVT6UL2-RNWA7VnD.js +0 -39
- package/dist/web-app/assets/c4Diagram-7LVT6UL2-RNWA7VnD.js.map +0 -1
- package/dist/web-app/assets/channel-BF3U9sI8.js +0 -2
- package/dist/web-app/assets/channel-BF3U9sI8.js.map +0 -1
- package/dist/web-app/assets/chunk-2Q5K7J3B-pdEuY4vj.js +0 -2
- package/dist/web-app/assets/chunk-2Q5K7J3B-pdEuY4vj.js.map +0 -1
- package/dist/web-app/assets/chunk-5VM5RSS4-DuAUtdUC.js +0 -16
- package/dist/web-app/assets/chunk-5VM5RSS4-DuAUtdUC.js.map +0 -1
- package/dist/web-app/assets/chunk-F27PBJKO-6kpb80Xt.js +0 -2
- package/dist/web-app/assets/chunk-F27PBJKO-6kpb80Xt.js.map +0 -1
- package/dist/web-app/assets/chunk-IMKFNOWR-DWC84P6A.js +0 -232
- package/dist/web-app/assets/chunk-IMKFNOWR-DWC84P6A.js.map +0 -1
- package/dist/web-app/assets/chunk-JWPE2WC7-FHO-M3vi.js +0 -2
- package/dist/web-app/assets/chunk-JWPE2WC7-FHO-M3vi.js.map +0 -1
- package/dist/web-app/assets/chunk-POPQ4Y6H-CjnlKvmR.js +0 -2
- package/dist/web-app/assets/chunk-POPQ4Y6H-CjnlKvmR.js.map +0 -1
- package/dist/web-app/assets/chunk-SVP7TREG-WHYWJ6xq.js +0 -89
- package/dist/web-app/assets/chunk-SVP7TREG-WHYWJ6xq.js.map +0 -1
- package/dist/web-app/assets/chunk-TICWLB2K-POupMpyL.js +0 -207
- package/dist/web-app/assets/chunk-TICWLB2K-POupMpyL.js.map +0 -1
- package/dist/web-app/assets/chunk-XXDRQBXY-Cw5HHqH-.js +0 -2
- package/dist/web-app/assets/chunk-XXDRQBXY-Cw5HHqH-.js.map +0 -1
- package/dist/web-app/assets/classDiagram-ZZMXUADV-1hdMzIme.js +0 -2
- package/dist/web-app/assets/classDiagram-ZZMXUADV-1hdMzIme.js.map +0 -1
- package/dist/web-app/assets/classDiagram-v2-VYDZK3BY-1hdMzIme.js +0 -2
- package/dist/web-app/assets/classDiagram-v2-VYDZK3BY-1hdMzIme.js.map +0 -1
- package/dist/web-app/assets/cose-bilkent-JH36ORCC-Cp6Hjine.js +0 -2
- package/dist/web-app/assets/cose-bilkent-JH36ORCC-Cp6Hjine.js.map +0 -1
- package/dist/web-app/assets/cynefin-OW5HDTMX-DI-DeNUk.js +0 -179
- package/dist/web-app/assets/cynefin-OW5HDTMX-DI-DeNUk.js.map +0 -1
- package/dist/web-app/assets/cynefinDiagram-5FMLGOSQ-todBCANI.js +0 -63
- package/dist/web-app/assets/cynefinDiagram-5FMLGOSQ-todBCANI.js.map +0 -1
- package/dist/web-app/assets/cytoscape.esm-CtNdOvlO.js +0 -332
- package/dist/web-app/assets/cytoscape.esm-CtNdOvlO.js.map +0 -1
- package/dist/web-app/assets/dagre-GXQ25YYZ-BvIVo7eh.js +0 -5
- package/dist/web-app/assets/dagre-GXQ25YYZ-BvIVo7eh.js.map +0 -1
- package/dist/web-app/assets/defaultLocale-DX6XiGOO.js +0 -2
- package/dist/web-app/assets/defaultLocale-DX6XiGOO.js.map +0 -1
- package/dist/web-app/assets/diagram-S7CK7UJ4-k9vDGlB5.js +0 -31
- package/dist/web-app/assets/diagram-S7CK7UJ4-k9vDGlB5.js.map +0 -1
- package/dist/web-app/assets/diagram-UQ7AKVKN-Bv7BT1dE.js +0 -42
- package/dist/web-app/assets/diagram-UQ7AKVKN-Bv7BT1dE.js.map +0 -1
- package/dist/web-app/assets/diagram-VSXAHHWV-m9I9AumX.js +0 -4
- package/dist/web-app/assets/diagram-VSXAHHWV-m9I9AumX.js.map +0 -1
- package/dist/web-app/assets/diagram-VX7I27RA-4VJ2qozS.js +0 -25
- package/dist/web-app/assets/diagram-VX7I27RA-4VJ2qozS.js.map +0 -1
- package/dist/web-app/assets/diagram-Z3DM3KII-CvH8JIEd.js +0 -25
- package/dist/web-app/assets/diagram-Z3DM3KII-CvH8JIEd.js.map +0 -1
- package/dist/web-app/assets/ebnfDiagram-PWID7BFC-DAGs23hd.js +0 -2
- package/dist/web-app/assets/ebnfDiagram-PWID7BFC-DAGs23hd.js.map +0 -1
- package/dist/web-app/assets/erDiagram-RLTQ6QDP-CEMyWKIa.js +0 -100
- package/dist/web-app/assets/erDiagram-RLTQ6QDP-CEMyWKIa.js.map +0 -1
- package/dist/web-app/assets/flowDiagram-HODETNUW-BnraxJTV.js +0 -169
- package/dist/web-app/assets/flowDiagram-HODETNUW-BnraxJTV.js.map +0 -1
- package/dist/web-app/assets/ganttDiagram-EL5Y4UJY-V6d4ICW0.js +0 -293
- package/dist/web-app/assets/ganttDiagram-EL5Y4UJY-V6d4ICW0.js.map +0 -1
- package/dist/web-app/assets/gitGraphDiagram-WWUBYQGX-Bj_gqcba.js +0 -107
- package/dist/web-app/assets/gitGraphDiagram-WWUBYQGX-Bj_gqcba.js.map +0 -1
- package/dist/web-app/assets/index-C4BdeFB9.css +0 -1
- package/dist/web-app/assets/index-i9rKiqYX.js +0 -430
- package/dist/web-app/assets/index-i9rKiqYX.js.map +0 -1
- package/dist/web-app/assets/infoDiagram-27XIBGKW-Dfm89NLl.js +0 -3
- package/dist/web-app/assets/infoDiagram-27XIBGKW-Dfm89NLl.js.map +0 -1
- package/dist/web-app/assets/init-Gi6I4Gst.js +0 -2
- package/dist/web-app/assets/init-Gi6I4Gst.js.map +0 -1
- package/dist/web-app/assets/ishikawaDiagram-5VMMS53U-D-ISiJTS.js +0 -71
- package/dist/web-app/assets/ishikawaDiagram-5VMMS53U-D-ISiJTS.js.map +0 -1
- package/dist/web-app/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/dist/web-app/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/dist/web-app/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/dist/web-app/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/dist/web-app/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/dist/web-app/assets/journeyDiagram-3NMN7TZE-TTySrzws.js +0 -140
- package/dist/web-app/assets/journeyDiagram-3NMN7TZE-TTySrzws.js.map +0 -1
- package/dist/web-app/assets/kanban-definition-UXKFOSKX-BxntsBft.js +0 -90
- package/dist/web-app/assets/kanban-definition-UXKFOSKX-BxntsBft.js.map +0 -1
- package/dist/web-app/assets/katex-HP8lGamR.js +0 -258
- package/dist/web-app/assets/katex-HP8lGamR.js.map +0 -1
- package/dist/web-app/assets/layout-0sZak8Ho.js +0 -2
- package/dist/web-app/assets/layout-0sZak8Ho.js.map +0 -1
- package/dist/web-app/assets/linear-D4xk4kUi.js +0 -2
- package/dist/web-app/assets/linear-D4xk4kUi.js.map +0 -1
- package/dist/web-app/assets/mermaid.core-CNKolAgz.js +0 -313
- package/dist/web-app/assets/mermaid.core-CNKolAgz.js.map +0 -1
- package/dist/web-app/assets/mindmap-definition-YA3MSWOX-DLJijY_l.js +0 -97
- package/dist/web-app/assets/mindmap-definition-YA3MSWOX-DLJijY_l.js.map +0 -1
- package/dist/web-app/assets/ordinal-Cboi1Yqb.js +0 -2
- package/dist/web-app/assets/ordinal-Cboi1Yqb.js.map +0 -1
- package/dist/web-app/assets/pegDiagram-XKGWAZYB-xjrh9rBd.js +0 -2
- package/dist/web-app/assets/pegDiagram-XKGWAZYB-xjrh9rBd.js.map +0 -1
- package/dist/web-app/assets/pieDiagram-E7YTZNPT-CHE8ZMXN.js +0 -40
- package/dist/web-app/assets/pieDiagram-E7YTZNPT-CHE8ZMXN.js.map +0 -1
- package/dist/web-app/assets/quadrantDiagram-AXDQQJYC-CDlEe9sg.js +0 -8
- package/dist/web-app/assets/quadrantDiagram-AXDQQJYC-CDlEe9sg.js.map +0 -1
- package/dist/web-app/assets/railroadDiagram-O6MQD6OU-Dv7lFZ_v.js +0 -2
- package/dist/web-app/assets/railroadDiagram-O6MQD6OU-Dv7lFZ_v.js.map +0 -1
- package/dist/web-app/assets/requirementDiagram-BXWQKSXE-BetEel-k.js +0 -85
- package/dist/web-app/assets/requirementDiagram-BXWQKSXE-BetEel-k.js.map +0 -1
- package/dist/web-app/assets/sankeyDiagram-P5KCCOFB-DCQgLfvK.js +0 -41
- package/dist/web-app/assets/sankeyDiagram-P5KCCOFB-DCQgLfvK.js.map +0 -1
- package/dist/web-app/assets/sequenceDiagram-WJ2MYXX4-CsGOoqu_.js +0 -163
- package/dist/web-app/assets/sequenceDiagram-WJ2MYXX4-CsGOoqu_.js.map +0 -1
- package/dist/web-app/assets/sizeCapture-INFHLROL-CLQRMiEc.js +0 -2
- package/dist/web-app/assets/sizeCapture-INFHLROL-CLQRMiEc.js.map +0 -1
- package/dist/web-app/assets/stateDiagram-D77RDMKH-D2MRdC5Q.js +0 -2
- package/dist/web-app/assets/stateDiagram-D77RDMKH-D2MRdC5Q.js.map +0 -1
- package/dist/web-app/assets/stateDiagram-v2-MP3YSRHH-CToAT5fD.js +0 -2
- package/dist/web-app/assets/stateDiagram-v2-MP3YSRHH-CToAT5fD.js.map +0 -1
- package/dist/web-app/assets/swimlanes-42K2YHIH-BGjHfR8x.js +0 -2
- package/dist/web-app/assets/swimlanes-42K2YHIH-BGjHfR8x.js.map +0 -1
- package/dist/web-app/assets/swimlanesDiagram-VR7AAH4N-bLHg88Vk.js +0 -9
- package/dist/web-app/assets/swimlanesDiagram-VR7AAH4N-bLHg88Vk.js.map +0 -1
- package/dist/web-app/assets/timeline-definition-24CTP7MA-DhGtHQ0p.js +0 -121
- package/dist/web-app/assets/timeline-definition-24CTP7MA-DhGtHQ0p.js.map +0 -1
- package/dist/web-app/assets/vennDiagram-4TSXK5OY-Dv5irf00.js +0 -35
- package/dist/web-app/assets/vennDiagram-4TSXK5OY-Dv5irf00.js.map +0 -1
- package/dist/web-app/assets/wardleyDiagram-VM6X3IG4-JGiUTj5x.js +0 -79
- package/dist/web-app/assets/wardleyDiagram-VM6X3IG4-JGiUTj5x.js.map +0 -1
- package/dist/web-app/assets/xychartDiagram-S5SC5T6Z-Dw5ev7_Q.js +0 -8
- package/dist/web-app/assets/xychartDiagram-S5SC5T6Z-Dw5ev7_Q.js.map +0 -1
- package/dist/web-app/fleet-index.html +0 -19
- package/dist/web-app/fleet.webmanifest +0 -37
- package/dist/web-app/icons/apple-touch-icon.png +0 -0
- package/dist/web-app/icons/favicon-16.png +0 -0
- package/dist/web-app/icons/favicon-32.png +0 -0
- package/dist/web-app/icons/icon-192-maskable.png +0 -0
- package/dist/web-app/icons/icon-192.png +0 -0
- package/dist/web-app/icons/icon-512-maskable.png +0 -0
- package/dist/web-app/icons/icon-512.png +0 -0
- package/dist/web-app/index.html +0 -19
- package/dist/web-app/manifest.webmanifest +0 -17
- package/dist/web-app/sw.js +0 -221
- package/dist/web-app/version.json +0 -1
- package/dist/web-app/workspace-install.sh +0 -148
- package/dist/web-app/workspace-tunnel.sh +0 -40
|
@@ -81,11 +81,18 @@ export class CodexAppServer {
|
|
|
81
81
|
this.pending.clear();
|
|
82
82
|
});
|
|
83
83
|
}
|
|
84
|
-
static async start(bin, env) {
|
|
84
|
+
static async start(bin, env, initializeTimeoutMs = 20_000) {
|
|
85
85
|
const child = spawn(bin, ['app-server'], { env, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
86
86
|
await new Promise((resolve, reject) => { child.once('spawn', resolve); child.once('error', reject); });
|
|
87
87
|
const server = new CodexAppServer(child);
|
|
88
|
-
|
|
88
|
+
// A server that does not answer is not handed out, so nobody else can close it: end it here.
|
|
89
|
+
try {
|
|
90
|
+
await server.call('initialize', { clientInfo: { name: 'ours-fleet', title: 'Ours Fleet', version: '1' } }, initializeTimeoutMs);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
server.close();
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
89
96
|
child.stdin.write(`${JSON.stringify({ method: 'initialized' })}\n`);
|
|
90
97
|
return server;
|
|
91
98
|
}
|
|
@@ -140,8 +147,8 @@ export class CodexAppServer {
|
|
|
140
147
|
}
|
|
141
148
|
}
|
|
142
149
|
/** One-shot use of an app-server for a profile. */
|
|
143
|
-
export async function withCodex(bin, env, fn) {
|
|
144
|
-
const server = await CodexAppServer.start(bin, env);
|
|
150
|
+
export async function withCodex(bin, env, fn, initializeTimeoutMs) {
|
|
151
|
+
const server = await CodexAppServer.start(bin, env, initializeTimeoutMs);
|
|
145
152
|
try {
|
|
146
153
|
return await fn(server);
|
|
147
154
|
}
|
|
@@ -149,6 +156,36 @@ export async function withCodex(bin, env, fn) {
|
|
|
149
156
|
server.close();
|
|
150
157
|
}
|
|
151
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* Ask this Codex executable, with this profile's environment, which models it offers (`model/list`,
|
|
161
|
+
* the call the ACP adapter itself uses). Nothing is inferred when it does not answer.
|
|
162
|
+
*/
|
|
163
|
+
export async function codexOfferedModels(bin, env, budgetMs = 30_000) {
|
|
164
|
+
// One budget for the whole question: onboarding waits for this answer.
|
|
165
|
+
const deadline = Date.now() + budgetMs;
|
|
166
|
+
const left = () => Math.max(1, deadline - Date.now());
|
|
167
|
+
try {
|
|
168
|
+
return await withCodex(bin, env, async (server) => {
|
|
169
|
+
const models = [];
|
|
170
|
+
let cursor = null;
|
|
171
|
+
while (Date.now() < deadline) {
|
|
172
|
+
const answer = await server.call('model/list', { cursor, limit: null }, left());
|
|
173
|
+
if (!Array.isArray(answer?.data))
|
|
174
|
+
return { state: 'unknown', reason: 'Codex did not return a model list' };
|
|
175
|
+
for (const model of answer.data)
|
|
176
|
+
if (model && typeof model.id === 'string' && model.id && model.hidden !== true)
|
|
177
|
+
models.push(model.id);
|
|
178
|
+
cursor = typeof answer.nextCursor === 'string' && answer.nextCursor ? answer.nextCursor : null;
|
|
179
|
+
if (!cursor)
|
|
180
|
+
return { state: 'known', models: [...new Set(models)] };
|
|
181
|
+
}
|
|
182
|
+
return { state: 'unknown', reason: 'Codex did not finish listing its models in time' };
|
|
183
|
+
}, left());
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
return { state: 'unknown', reason: error instanceof Error ? error.message : String(error) };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
152
189
|
/** Flatten `account/rateLimits/read` into display windows. */
|
|
153
190
|
export function codexUsageWindows(result) {
|
|
154
191
|
const snapshots = result.rateLimitsByLimitId && Object.keys(result.rateLimitsByLimitId).length
|
package/dist/web/auth.d.ts
CHANGED
|
@@ -54,7 +54,7 @@ export declare class WebAuth {
|
|
|
54
54
|
mintBootstrap(): string;
|
|
55
55
|
/** The configured account origin, when this request comes from it and workspace devices are enabled. */
|
|
56
56
|
accountOrigin(request: Pick<FastifyRequest, 'headers'>): string | undefined;
|
|
57
|
-
validateBoundary(request: FastifyRequest, requireOrigin: boolean
|
|
57
|
+
validateBoundary(request: FastifyRequest, requireOrigin: boolean): void;
|
|
58
58
|
exchange(request: FastifyRequest): AuthResult;
|
|
59
59
|
login(request: FastifyRequest, password: string): AuthResult;
|
|
60
60
|
anonymous(request: FastifyRequest): BrowserSession;
|
package/dist/web/auth.js
CHANGED
|
@@ -58,7 +58,7 @@ export class WebAuth {
|
|
|
58
58
|
accountOrigin(request) {
|
|
59
59
|
return this.workspaceDevices && request.headers.origin === this.appOrigin ? this.appOrigin : undefined;
|
|
60
60
|
}
|
|
61
|
-
validateBoundary(request, requireOrigin
|
|
61
|
+
validateBoundary(request, requireOrigin) {
|
|
62
62
|
const host = request.headers.host;
|
|
63
63
|
const hosts = this.allowedHosts.size ? this.allowedHosts : new Set([this.host]);
|
|
64
64
|
const origins = this.allowedOrigins.size ? this.allowedOrigins : new Set([this.origin]);
|
|
@@ -71,10 +71,7 @@ export class WebAuth {
|
|
|
71
71
|
if (requireOrigin && !workspaceRequest && (!request.headers.origin || !origins.has(request.headers.origin)))
|
|
72
72
|
throw new FleetError('forbidden', 'request Origin does not match the configured control-panel origin');
|
|
73
73
|
const fetchSite = request.headers['sec-fetch-site'];
|
|
74
|
-
|
|
75
|
-
&& request.headers['sec-fetch-mode'] === 'navigate'
|
|
76
|
-
&& (request.headers['sec-fetch-dest'] === 'document' || (Boolean(this.workspaceDevices) && request.headers['sec-fetch-dest'] === 'iframe' && new URL(request.url || '/', this.origin).searchParams.get('workspace-frame') === '1'));
|
|
77
|
-
if (!workspaceRequest && !pageNavigation && fetchSite && !['same-origin', 'none'].includes(String(fetchSite)))
|
|
74
|
+
if (!workspaceRequest && fetchSite && !['same-origin', 'none'].includes(String(fetchSite)))
|
|
78
75
|
throw new FleetError('forbidden', 'cross-site request rejected');
|
|
79
76
|
}
|
|
80
77
|
exchange(request) {
|
package/dist/web/control.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type WebControlCommand = '
|
|
1
|
+
export type WebControlCommand = 'revoke-all' | 'link-device';
|
|
2
2
|
export declare const webControlPath: (dir?: string) => string;
|
|
3
3
|
export interface WebControlServer {
|
|
4
4
|
path: string;
|
|
@@ -6,7 +6,6 @@ export interface WebControlServer {
|
|
|
6
6
|
}
|
|
7
7
|
export declare function startWebControlServer(options: {
|
|
8
8
|
dir?: string;
|
|
9
|
-
onOpen(): void | Promise<void>;
|
|
10
9
|
onLinkDevice?(): Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
11
10
|
onRevokeAll(): void | Promise<void>;
|
|
12
11
|
now?: () => number;
|
package/dist/web/control.js
CHANGED
|
@@ -38,8 +38,6 @@ export async function startWebControlServer(options) {
|
|
|
38
38
|
let result;
|
|
39
39
|
if (parsed.command === 'link-device' && options.onLinkDevice)
|
|
40
40
|
result = await options.onLinkDevice();
|
|
41
|
-
else if (parsed.command === 'open')
|
|
42
|
-
await options.onOpen();
|
|
43
41
|
else if (parsed.command === 'revoke-all')
|
|
44
42
|
await options.onRevokeAll();
|
|
45
43
|
else
|
|
@@ -27,7 +27,7 @@ export function createPrefixGateway(options) {
|
|
|
27
27
|
const path = raw.split('?')[0];
|
|
28
28
|
const service = services.find(x => path === x.prefix || path.startsWith(x.prefix + '/'));
|
|
29
29
|
const fleet = path === '/fleet/api' || path.startsWith('/fleet/api/');
|
|
30
|
-
return { service,
|
|
30
|
+
return { service, origin: service?.origin ?? options.fleetOrigin,
|
|
31
31
|
path: service ? (service.upstreamPrefix || '') + (raw.slice(service.prefix.length) || '/').replace(/^\?/, '/?') : fleet ? raw.slice('/fleet'.length) : raw };
|
|
32
32
|
};
|
|
33
33
|
const prepare = (req, upgrade = false) => {
|
|
@@ -35,7 +35,7 @@ export function createPrefixGateway(options) {
|
|
|
35
35
|
const request = { headers: req.headers, method: req.method, url: route.path };
|
|
36
36
|
if (!auth && (req.headers.origin || req.headers['sec-fetch-site'] || !route.service))
|
|
37
37
|
throw new Error('machine-client listener rejects browser requests');
|
|
38
|
-
auth?.validateBoundary(request, upgrade
|
|
38
|
+
auth?.validateBoundary(request, upgrade);
|
|
39
39
|
const session = route.service && auth ? auth.authenticate(request, !upgrade && !['GET', 'HEAD', 'OPTIONS'].includes(req.method ?? 'GET')) : undefined;
|
|
40
40
|
const headers = headersFor(req);
|
|
41
41
|
// Forward only the explicit browser context, never an attacker-selected forwarding chain.
|
|
@@ -100,11 +100,6 @@ export function createPrefixGateway(options) {
|
|
|
100
100
|
res.end(JSON.stringify({ error: { code: code ?? 'forbidden', message: error.message } }));
|
|
101
101
|
return;
|
|
102
102
|
}
|
|
103
|
-
if (req.url === '/') {
|
|
104
|
-
res.writeHead(302, { location: '/fleet' });
|
|
105
|
-
res.end();
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
103
|
const upstream = (route.origin.startsWith('https:') ? httpsRequest : httpRequest)(route.origin, {
|
|
109
104
|
method: req.method, path: route.path, headers: route.headers,
|
|
110
105
|
}, response => {
|
package/dist/web/runtime.d.ts
CHANGED
|
@@ -5,9 +5,7 @@ export interface StartWebOptions {
|
|
|
5
5
|
/** Separate console state for an independently launched gateway. */
|
|
6
6
|
webStateDir?: string;
|
|
7
7
|
control?: boolean;
|
|
8
|
-
staticRoot?: string;
|
|
9
8
|
port?: number;
|
|
10
|
-
open?: boolean;
|
|
11
9
|
binPath: string;
|
|
12
10
|
log?(line: string): void;
|
|
13
11
|
bind?: string;
|
package/dist/web/runtime.js
CHANGED
|
@@ -6,10 +6,10 @@ import { readClientProfile } from '../client-profile.js';
|
|
|
6
6
|
import { readPrivateFile } from '@ours.network/sdk/connector';
|
|
7
7
|
import { LayoutBindingGrants } from '../rooms-tasks/layout-binding-grants.js';
|
|
8
8
|
import { RoomLayoutService } from '../rooms-tasks/layout-service.js';
|
|
9
|
-
import { spawn } from 'node:child_process';
|
|
10
9
|
import { FleetNotificationProducer } from '../notifications/fleet-producer.js';
|
|
11
10
|
import { producerConfig } from '../notifications/outbox.js';
|
|
12
11
|
import { SubscriptionService } from '../subscriptions/service.js';
|
|
12
|
+
import { createCodexModelDiscovery } from '../application/codex-models.js';
|
|
13
13
|
import { existsSync, mkdirSync, realpathSync, readFileSync } from 'node:fs';
|
|
14
14
|
import { resolve } from 'node:path';
|
|
15
15
|
import { loadConfig } from '../config.js';
|
|
@@ -202,6 +202,7 @@ export async function startWebConsole(options) {
|
|
|
202
202
|
publish: publishSetup,
|
|
203
203
|
});
|
|
204
204
|
},
|
|
205
|
+
codexModels: createCodexModelDiscovery(),
|
|
205
206
|
subscriptions: new SubscriptionService({
|
|
206
207
|
agents: async () => (await query.list(true)).map(({ role, status }) => ({
|
|
207
208
|
roleId: role.id, stateDir: repository.stateDir(role), running: status.supervisor.liveness === 'running',
|
|
@@ -221,7 +222,7 @@ export async function startWebConsole(options) {
|
|
|
221
222
|
}
|
|
222
223
|
throw new FleetError('capability_unavailable', 'role session backend is unavailable');
|
|
223
224
|
},
|
|
224
|
-
}, { origin: `http://127.0.0.1:${requestedPort}`, host: `127.0.0.1:${requestedPort}` }, { auth
|
|
225
|
+
}, { origin: `http://127.0.0.1:${requestedPort}`, host: `127.0.0.1:${requestedPort}` }, { auth });
|
|
225
226
|
}
|
|
226
227
|
catch (error) {
|
|
227
228
|
lock.release();
|
|
@@ -282,11 +283,6 @@ export async function startWebConsole(options) {
|
|
|
282
283
|
if (options.control !== false)
|
|
283
284
|
control = await startWebControlServer({
|
|
284
285
|
dir: webDir,
|
|
285
|
-
onOpen() {
|
|
286
|
-
const url = access.mode === 'pairing'
|
|
287
|
-
? `${browserOrigin}/#bootstrap=${server.auth.mintBootstrap()}` : `${browserOrigin}/`;
|
|
288
|
-
openBrowser(url);
|
|
289
|
-
},
|
|
290
286
|
onLinkDevice() { return { version: 1, origin: browserOrigin, ...server.auth.workspaceEnrollment() }; },
|
|
291
287
|
onRevokeAll() { server.auth.revokeAllTrustedDevices(); },
|
|
292
288
|
});
|
|
@@ -297,9 +293,6 @@ export async function startWebConsole(options) {
|
|
|
297
293
|
lock.release();
|
|
298
294
|
throw error;
|
|
299
295
|
}
|
|
300
|
-
if (options.open !== false)
|
|
301
|
-
openBrowser(access.mode === 'pairing'
|
|
302
|
-
? `${browserOrigin}/#bootstrap=${server.auth.bootstrapSecret}` : `${browserOrigin}/`);
|
|
303
296
|
let stopTunnel;
|
|
304
297
|
try {
|
|
305
298
|
stopTunnel = startWorkspaceTunnel(browserOrigin);
|
|
@@ -335,11 +328,3 @@ export async function startWebConsole(options) {
|
|
|
335
328
|
function isLoopback(host) {
|
|
336
329
|
return ['127.0.0.1', 'localhost', '::1'].includes(host);
|
|
337
330
|
}
|
|
338
|
-
function openBrowser(url) {
|
|
339
|
-
const command = process.platform === 'darwin' ? 'open'
|
|
340
|
-
: process.platform === 'win32' ? 'cmd' : 'xdg-open';
|
|
341
|
-
const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
342
|
-
const child = spawn(command, args, { stdio: 'ignore', detached: true, shell: false });
|
|
343
|
-
child.on('error', () => undefined);
|
|
344
|
-
child.unref();
|
|
345
|
-
}
|
package/dist/web/server.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ import type { RoleRemovalService } from '../application/role-removal-service.js'
|
|
|
19
19
|
import type { TaskRoomApplicationService } from '../application/task-room-service.js';
|
|
20
20
|
import type { RoomLayoutDefinitions } from '../application/room-layout-definitions.js';
|
|
21
21
|
import type { PresetProvenance } from '../application/preset-provenance.js';
|
|
22
|
+
import type { OfferedModels } from '../subscriptions/cli.js';
|
|
22
23
|
import type { SubscriptionService } from '../subscriptions/service.js';
|
|
23
24
|
import type { LayoutBindingGrants } from '../rooms-tasks/layout-binding-grants.js';
|
|
24
25
|
export interface WebServices {
|
|
@@ -44,6 +45,8 @@ export interface WebServices {
|
|
|
44
45
|
subscriptions?: SubscriptionService;
|
|
45
46
|
/** First-time setup from the packaged presets; the account App supplies only the model choices. */
|
|
46
47
|
onboardingSetup?(answers: InitAnswers): Promise<void>;
|
|
48
|
+
/** The Codex models the runtime that sessions will use offers the active account. Absent: this server cannot tell, and filters nothing. */
|
|
49
|
+
codexModels?(): Promise<OfferedModels>;
|
|
47
50
|
}
|
|
48
51
|
export interface WebServer {
|
|
49
52
|
app: FastifyInstance;
|
|
@@ -57,5 +60,4 @@ export declare function buildWebServer(services: WebServices, boundary: {
|
|
|
57
60
|
host: string;
|
|
58
61
|
}, options?: {
|
|
59
62
|
auth?: WebAuth;
|
|
60
|
-
staticRoot?: string;
|
|
61
63
|
}): Promise<WebServer>;
|
package/dist/web/server.js
CHANGED
|
@@ -6,11 +6,8 @@ import { storeAgentAttachment, attachmentPrompt, MAX_ATTACHMENT_BODY, readAgentA
|
|
|
6
6
|
import { readChatIdle } from '../temp-idle.js';
|
|
7
7
|
import { agentDir } from '../paths.js';
|
|
8
8
|
import { SupervisorOursTools } from '../application/supervisor-ours-tools.js';
|
|
9
|
-
import {
|
|
10
|
-
import { dirname, join } from 'node:path';
|
|
11
|
-
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { join } from 'node:path';
|
|
12
10
|
import Fastify from 'fastify';
|
|
13
|
-
import fastifyStatic from '@fastify/static';
|
|
14
11
|
import fastifyWebsocket from '@fastify/websocket';
|
|
15
12
|
import { createHmac, randomBytes } from 'node:crypto';
|
|
16
13
|
import { FleetError, normalizeError } from '../application/errors.js';
|
|
@@ -44,10 +41,8 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
44
41
|
reply.header('X-Content-Type-Options', 'nosniff');
|
|
45
42
|
reply.header('Referrer-Policy', 'no-referrer');
|
|
46
43
|
reply.header('Permissions-Policy', 'camera=(), microphone=(self), geolocation=(), payment=()');
|
|
47
|
-
//
|
|
48
|
-
reply.header('Content-Security-Policy',
|
|
49
|
-
`connect-src 'self' ${auth.secureCookies ? 'wss' : 'ws'}://${auth.host}; object-src 'none'; base-uri 'none'; ` +
|
|
50
|
-
`frame-ancestors 'self' ${auth.appOrigin}; form-action 'self'; manifest-src 'self'; worker-src 'self'`);
|
|
44
|
+
// Fleet serves an API only: nothing it returns may run, load or be framed as a page.
|
|
45
|
+
reply.header('Content-Security-Policy', "default-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'");
|
|
51
46
|
if (request.url.startsWith('/api/'))
|
|
52
47
|
reply.header('Cache-Control', 'no-store');
|
|
53
48
|
if (request.headers.origin === auth.appOrigin && request.url.startsWith('/api/')) {
|
|
@@ -59,15 +54,7 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
59
54
|
return reply.code(204).send();
|
|
60
55
|
}
|
|
61
56
|
}
|
|
62
|
-
|
|
63
|
-
auth.validateBoundary(request, false, !request.url.startsWith('/api/'));
|
|
64
|
-
}
|
|
65
|
-
catch (error) {
|
|
66
|
-
if (request.url.startsWith('/api/'))
|
|
67
|
-
throw error;
|
|
68
|
-
const message = normalizeError(error).message;
|
|
69
|
-
return reply.code(421).type('text/html').send(`<!doctype html><html><head><title>Fleet console address</title></head><body><main><h1>This fleet-console address is not configured</h1><p>${escapeHtml(message)}</p><p>For nginx or VPS access, configure an explicit public origin. The console does not guess proxy hosts.</p></main></body></html>`);
|
|
70
|
-
}
|
|
57
|
+
auth.validateBoundary(request, false);
|
|
71
58
|
});
|
|
72
59
|
app.post('/api/v1/layout-bindings/:id/control', async (request) => {
|
|
73
60
|
// Initial support is direct same-host Fleet-to-Fleet transport.
|
|
@@ -166,7 +153,11 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
166
153
|
}
|
|
167
154
|
}));
|
|
168
155
|
const catalog = validateCatalog(JSON.parse(readFileSync(join(packagedPresetRoot(), 'brain-catalog.json'), 'utf8')));
|
|
169
|
-
|
|
156
|
+
// Codex models are offered only when the runtime itself listed them for the active account.
|
|
157
|
+
// When it could not be asked, none is offered and the reason is reported.
|
|
158
|
+
const codexModels = services.codexModels ? await services.codexModels() : undefined;
|
|
159
|
+
const offered = (model) => model.harness !== 'codex' || !codexModels || (codexModels.state === 'known' && codexModels.models.includes(model.model));
|
|
160
|
+
return { harnesses, catalog: catalog.models.filter(offered), ...(codexModels ? { codexModels: codexModels.state === 'known' ? { state: 'known' } : codexModels } : {}), providers: services.subscriptions ? await services.subscriptions.list() : [], coordinator: 'FleetCoordinator', configured: onboardingConfigured() };
|
|
170
161
|
});
|
|
171
162
|
// First-time setup is what creates the persistent coordinator. A configuration that loads and
|
|
172
163
|
// has it is set up, whatever its brains are called; its models are then changed by editing it.
|
|
@@ -186,7 +177,9 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
186
177
|
if (!services.configuration || !services.onboardingSetup)
|
|
187
178
|
throw new FleetError('capability_unavailable', 'first-time Fleet setup is unavailable');
|
|
188
179
|
const requested = request.body?.models;
|
|
189
|
-
const
|
|
180
|
+
const codexModels = services.codexModels ? await services.codexModels() : undefined;
|
|
181
|
+
const supported = validateCatalog(JSON.parse(readFileSync(join(packagedPresetRoot(), 'brain-catalog.json'), 'utf8'))).models
|
|
182
|
+
.filter(model => model.harness !== 'codex' || !codexModels || (codexModels.state === 'known' && codexModels.models.includes(model.model)));
|
|
190
183
|
const models = {};
|
|
191
184
|
for (const work of ONBOARDING_WORK) {
|
|
192
185
|
const matches = supported.filter(model => model.harness === requested?.[work]?.harness && model.model === requested?.[work]?.model);
|
|
@@ -1022,19 +1015,8 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
1022
1015
|
socket.on('close', () => { clearInterval(heartbeat); follow.close(); });
|
|
1023
1016
|
});
|
|
1024
1017
|
});
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
await app.register(fastifyStatic, { root: staticRoot, prefix: '/' });
|
|
1028
|
-
app.setNotFoundHandler((request, reply) => {
|
|
1029
|
-
if (request.url.startsWith('/api/'))
|
|
1030
|
-
return reply.code(404).send({ error: 'not found' });
|
|
1031
|
-
const path = request.url.split('?')[0];
|
|
1032
|
-
if ((path === '/chats' || path.startsWith('/chats/')) && existsSync(join(staticRoot, 'fleet-index.html')))
|
|
1033
|
-
return reply.redirect((request.url.includes('?') ? '/fleet/chats' : '/fleet') + (request.url.includes('?') ? request.url.slice(request.url.indexOf('?')) : ''));
|
|
1034
|
-
const fleetEntry = (path === '/fleet' || path.startsWith('/fleet/')) && existsSync(join(staticRoot, 'fleet-index.html'));
|
|
1035
|
-
return reply.sendFile(fleetEntry ? 'fleet-index.html' : 'index.html');
|
|
1036
|
-
});
|
|
1037
|
-
}
|
|
1018
|
+
// Fleet has no pages of its own; every screen belongs to the App.
|
|
1019
|
+
app.setNotFoundHandler((_request, reply) => reply.code(404).send({ error: 'not found' }));
|
|
1038
1020
|
return {
|
|
1039
1021
|
app, auth, audit, events,
|
|
1040
1022
|
async close() {
|
|
@@ -1071,9 +1053,6 @@ function promoteRequest(body) {
|
|
|
1071
1053
|
};
|
|
1072
1054
|
}
|
|
1073
1055
|
function cryptoRandomId() { return randomBytes(12).toString('hex'); }
|
|
1074
|
-
function escapeHtml(value) {
|
|
1075
|
-
return value.replace(/[&<>"']/g, char => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char]));
|
|
1076
|
-
}
|
|
1077
1056
|
function requireSubprotocol(request, expected) {
|
|
1078
1057
|
const protocols = String(request.headers['sec-websocket-protocol'] ?? '')
|
|
1079
1058
|
.split(',').map(value => value.trim());
|
|
@@ -16,6 +16,11 @@ export interface WorkspacePayload {
|
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
18
|
export declare function readWorkspacePayload(file: string): WorkspacePayload;
|
|
19
|
+
/**
|
|
20
|
+
* Check the Fleet configuration tunnel setup will use, before the one-time command is used.
|
|
21
|
+
* Reads only; an existing Owner is compared with this host's own Messenger identity.
|
|
22
|
+
*/
|
|
23
|
+
export declare function checkWorkspaceConfiguration(configuration?: string): Promise<void>;
|
|
19
24
|
export declare function enrollWorkspace(payload: WorkspacePayload, configuration?: string, options?: {
|
|
20
25
|
migrateAppOrigin?: boolean;
|
|
21
26
|
preserveProfile?: boolean;
|
|
@@ -25,4 +30,16 @@ export declare function enrollWorkspace(payload: WorkspacePayload, configuration
|
|
|
25
30
|
rootCid: any;
|
|
26
31
|
}>;
|
|
27
32
|
/** Only the existing signed-root receipt authorizes the scoped tunnel target. */
|
|
33
|
+
/**
|
|
34
|
+
* The enrollment server contact exists only to carry the signed binding. Once the account has confirmed
|
|
35
|
+
* that binding, remove exactly that contact from this host's Messenger, so it does not stay in the
|
|
36
|
+
* person's chats. The server identity is the one recorded with the binding; nothing else is touched.
|
|
37
|
+
*/
|
|
38
|
+
export declare function removeEnrollmentContact(): Promise<boolean>;
|
|
28
39
|
export declare function configureWorkspacePort(payload: Pick<WorkspacePayload, 'appOrigin' | 'challenge'>, hostWorkspaceId: string, rootCid: string, port: number): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Point the tunnel at this host's port. The account answers only for a confirmed signed binding, and
|
|
42
|
+
* only after that answer is the enrollment contact removed; a refused or unconfirmed target leaves it,
|
|
43
|
+
* so the setup can be finished later. A removal that fails is reported and does not undo the setup.
|
|
44
|
+
*/
|
|
45
|
+
export declare function confirmWorkspaceTarget(payload: Pick<WorkspacePayload, 'appOrigin' | 'challenge'>, hostWorkspaceId: string, rootCid: string, port: number, cleanupFailed?: (error: unknown) => void): Promise<void>;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { validateAccountOrigin } from './account-origin.js';
|
|
2
|
-
import { lstatSync, readFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
2
|
+
import { lstatSync, readFileSync, readdirSync, mkdirSync, existsSync } from 'node:fs';
|
|
3
3
|
import { join, resolve } from 'node:path';
|
|
4
4
|
import { readClientProfile } from './client-profile.js';
|
|
5
|
-
import { stateRoot, defaultConfigPath } from './paths.js';
|
|
5
|
+
import { stateRoot, defaultConfigPath, workspaceOwnerInvite } from './paths.js';
|
|
6
6
|
import { replaceFileAtomically } from './atomic-file.js';
|
|
7
7
|
import { loadConfig, splitRootFor } from './config.js';
|
|
8
|
-
import { stringify } from 'yaml';
|
|
8
|
+
import { parse, stringify } from 'yaml';
|
|
9
|
+
import { preflightInitPaths } from './init-wizard.js';
|
|
9
10
|
import { WebAccessStore } from './web/access.js';
|
|
10
11
|
import { WorkspaceDeviceStore } from './web/workspace-devices.js';
|
|
11
12
|
export function readWorkspacePayload(file) {
|
|
@@ -26,17 +27,85 @@ export function readWorkspacePayload(file) {
|
|
|
26
27
|
throw Error('Invalid workspace payload');
|
|
27
28
|
return p;
|
|
28
29
|
}
|
|
29
|
-
|
|
30
|
+
const GENERATED_VERSION = 'ours.network/fleet/v2';
|
|
31
|
+
/** The rooms section tunnel setup writes for the enrolled Human root. */
|
|
32
|
+
const generatedRooms = (rootCid, inviteFile) => ({ rooms: { owner: { provider: 'messenger-server', expected_cid: rootCid, public_invite_file: inviteFile, role: 'Owner' }, defaults: { attach_owner: true, close_when_task_done: true } } });
|
|
33
|
+
/**
|
|
34
|
+
* True only when this configuration is, key for key, what tunnel setup generated itself and nothing
|
|
35
|
+
* else was added beside it. Only then may its Owner entry be treated as the record of an enrollment.
|
|
36
|
+
*/
|
|
37
|
+
function generatedConfigurationOnly(configuration) {
|
|
38
|
+
let document;
|
|
39
|
+
try {
|
|
40
|
+
document = parse(readFileSync(configuration, 'utf8'));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
const only = (value, keys) => !!value && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).sort().join() === [...keys].sort().join();
|
|
46
|
+
if (!only(document, ['api_version', 'rooms']) || document.api_version !== GENERATED_VERSION)
|
|
47
|
+
return false;
|
|
48
|
+
const rooms = document.rooms;
|
|
49
|
+
if (!only(rooms, ['owner', 'defaults']) || !only(rooms.owner, ['provider', 'expected_cid', 'public_invite_file', 'role']) || !only(rooms.defaults, ['attach_owner', 'close_when_task_done']))
|
|
50
|
+
return false;
|
|
51
|
+
if (rooms.owner.provider !== 'messenger-server' || rooms.owner.role !== 'Owner' || rooms.owner.public_invite_file !== workspaceOwnerInvite() || typeof rooms.owner.expected_cid !== 'string' || !/^[a-f0-9]{64}$/i.test(rooms.owner.expected_cid)
|
|
52
|
+
|| rooms.defaults.attach_owner !== true || rooms.defaults.close_when_task_done !== true)
|
|
53
|
+
return false;
|
|
54
|
+
const split = splitRootFor(configuration);
|
|
55
|
+
if (!existsSync(split))
|
|
56
|
+
return true;
|
|
57
|
+
const entries = readdirSync(split);
|
|
58
|
+
return entries.every(entry => entry === 'agents') && (!entries.length || (lstatSync(join(split, 'agents')).isDirectory() && readdirSync(join(split, 'agents')).length === 0));
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The Owner entry of a generated configuration records an enrollment whose host state is gone:
|
|
62
|
+
* no binding and no invitation remain. Nothing else is ever replaced.
|
|
63
|
+
*/
|
|
64
|
+
const recordsLostEnrollment = (configuration) => !existsSync(join(stateRoot(), 'workspace', 'binding.json')) && !existsSync(workspaceOwnerInvite()) && generatedConfigurationOnly(configuration);
|
|
65
|
+
function hostMessenger() {
|
|
30
66
|
const profile = readClientProfile();
|
|
31
67
|
const base = new URL(profile.endpoint);
|
|
32
68
|
base.pathname = base.pathname.replace(/\/daemon\/?$/, '/messenger/');
|
|
33
69
|
if (!base.pathname.endsWith('/messenger/'))
|
|
34
70
|
throw Error('Workspace setup requires the supported gateway client profile');
|
|
35
71
|
const secret = readFileSync(profile.credentialPath, 'utf8').trim();
|
|
36
|
-
|
|
72
|
+
return async (path, value) => { const response = await fetch(new URL('api/' + path, base), { method: value === undefined ? 'GET' : 'POST', headers: { 'X-Ours-Api-Token': secret, ...(value === undefined ? {} : { 'Content-Type': 'application/json', 'Origin': base.origin, 'X-Ours-Messenger-CSRF': '1' }) }, ...(value === undefined ? {} : { body: JSON.stringify(value) }), signal: AbortSignal.timeout(value === undefined ? 15000 : 60000), redirect: 'error' }); if (!response.ok) {
|
|
37
73
|
await response.body?.cancel();
|
|
38
74
|
throw Error('Workspace Messenger enrollment failed (HTTP ' + response.status + '); request a fresh setup payload before retrying');
|
|
39
75
|
} return response.json(); };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Check the Fleet configuration tunnel setup will use, before the one-time command is used.
|
|
79
|
+
* Reads only; an existing Owner is compared with this host's own Messenger identity.
|
|
80
|
+
*/
|
|
81
|
+
export async function checkWorkspaceConfiguration(configuration = defaultConfigPath()) {
|
|
82
|
+
const paths = preflightInitPaths(configuration);
|
|
83
|
+
if (!paths.manifestExisted) {
|
|
84
|
+
if (paths.rootExisted)
|
|
85
|
+
throw Error(`${paths.splitRoot} exists without ${paths.configPath}. Restore that file or move the directory away.`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const loaded = loadConfig(paths.configPath, { yamlMode: 'strict', deferredOwnerInviteFile: workspaceOwnerInvite() });
|
|
89
|
+
if (!loaded.rooms)
|
|
90
|
+
return;
|
|
91
|
+
if (loaded.rooms.defaults?.attach_owner === false)
|
|
92
|
+
throw Error(`${paths.configPath}: rooms.defaults.attach_owner is disabled; enable it before tunnel setup.`);
|
|
93
|
+
if (!loaded.rooms.owner.public_invite_file)
|
|
94
|
+
throw Error(`${paths.configPath}: rooms.owner must use public_invite_file before tunnel setup.`);
|
|
95
|
+
let cid;
|
|
96
|
+
try {
|
|
97
|
+
cid = (await hostMessenger()('workspace/enrollment-identity')).cid;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
cid = undefined;
|
|
101
|
+
}
|
|
102
|
+
if (typeof cid !== 'string' || !/^[a-f0-9]{64}$/i.test(cid))
|
|
103
|
+
throw Error(`${paths.configPath} names a room Owner, and this host's Messenger identity cannot be read to compare with it. Start Messenger on this host first.`);
|
|
104
|
+
if (loaded.rooms.owner.expected_cid.toLowerCase() !== cid.toLowerCase() && !recordsLostEnrollment(paths.configPath))
|
|
105
|
+
throw Error(`${paths.configPath}: rooms.owner.expected_cid names a different Owner than this host's Messenger identity. Tunnel setup does not replace an existing Owner.`);
|
|
106
|
+
}
|
|
107
|
+
export async function enrollWorkspace(payload, configuration = defaultConfigPath(), options = {}) {
|
|
108
|
+
const request = hostMessenger();
|
|
40
109
|
if (new WebAccessStore().read().mode === 'none')
|
|
41
110
|
throw Error('Workspace enrollment requires protected web access; enable pairing or password before setup');
|
|
42
111
|
const dir = join(stateRoot(), 'workspace');
|
|
@@ -55,7 +124,9 @@ export async function enrollWorkspace(payload, configuration = defaultConfigPath
|
|
|
55
124
|
if (migrating && previous.serverCid && previous.serverCid.toUpperCase() !== payload.serverCid.toUpperCase())
|
|
56
125
|
throw Error('Account origin migration must retain the enrollment server identity');
|
|
57
126
|
}
|
|
58
|
-
const loaded = loadConfig(configuration);
|
|
127
|
+
const loaded = loadConfig(configuration, { deferredOwnerInviteFile: workspaceOwnerInvite() });
|
|
128
|
+
// Decided before anything is sent or written: the Owner entry is replaced only when it records an enrollment this host has lost.
|
|
129
|
+
const lost = Boolean(loaded.rooms) && recordsLostEnrollment(resolve(configuration));
|
|
59
130
|
if (loaded.rooms?.defaults?.attach_owner === false)
|
|
60
131
|
throw Error('Existing rooms.defaults.attach_owner is disabled; enable automatic Human Owner admission before workspace setup');
|
|
61
132
|
if (loaded.rooms && !loaded.rooms.owner.public_invite_file)
|
|
@@ -65,7 +136,7 @@ export async function enrollWorkspace(payload, configuration = defaultConfigPath
|
|
|
65
136
|
throw Error('Bound Messenger identity unavailable');
|
|
66
137
|
if (options.preserveProfile && identity.preserveProfile !== true)
|
|
67
138
|
throw Error('Existing-host setup requires Messenger profile-preservation support; update the host Messenger before retrying');
|
|
68
|
-
if (loaded.rooms && loaded.rooms.owner.expected_cid.toLowerCase() !== identity.cid.toLowerCase())
|
|
139
|
+
if (loaded.rooms && !lost && loaded.rooms.owner.expected_cid.toLowerCase() !== identity.cid.toLowerCase())
|
|
69
140
|
throw Error('Retained room owner CID conflicts with the Human root');
|
|
70
141
|
const inviteFile = loaded.rooms?.owner.public_invite_file || join(dir, 'owner.invite');
|
|
71
142
|
if (existsSync(inviteFile)) {
|
|
@@ -80,7 +151,10 @@ export async function enrollWorkspace(payload, configuration = defaultConfigPath
|
|
|
80
151
|
const result = await request('workspace/enroll', options.preserveProfile ? { ...proofPayload, hostWorkspaceId, preserveProfile: true } : { ...payload, hostWorkspaceId });
|
|
81
152
|
if (!result.submitted || !/^[a-f0-9]{64}$/i.test(result.rootCid) || typeof result.ownerInvite !== 'string')
|
|
82
153
|
throw Error('Malformed workspace enrollment response');
|
|
83
|
-
|
|
154
|
+
// Whatever the configuration said before, what is recorded from here on is this host's own root.
|
|
155
|
+
if (result.rootCid.toLowerCase() !== identity.cid.toLowerCase())
|
|
156
|
+
throw Error('Workspace enrollment answered for a different root than this host identity; local configuration is unchanged');
|
|
157
|
+
if (loaded.rooms && !lost && loaded.rooms.owner.expected_cid.toLowerCase() !== result.rootCid.toLowerCase())
|
|
84
158
|
throw Error('Retained room owner CID conflicts with the Human root');
|
|
85
159
|
if (migrating) {
|
|
86
160
|
const deadline = Date.now() + 15000;
|
|
@@ -102,7 +176,9 @@ export async function enrollWorkspace(payload, configuration = defaultConfigPath
|
|
|
102
176
|
const split = splitRootFor(configuration);
|
|
103
177
|
mkdirSync(split, { recursive: true, mode: 0o700 });
|
|
104
178
|
if (!loaded.rooms)
|
|
105
|
-
replaceFileAtomically(configuration, readFileSync(configuration, 'utf8') + '\n' + stringify(
|
|
179
|
+
replaceFileAtomically(configuration, readFileSync(configuration, 'utf8') + '\n' + stringify(generatedRooms(result.rootCid, inviteFile)), 0o600);
|
|
180
|
+
else if (lost && loaded.rooms.owner.expected_cid.toLowerCase() !== result.rootCid.toLowerCase())
|
|
181
|
+
replaceFileAtomically(configuration, `api_version: ${GENERATED_VERSION}\n\n` + stringify(generatedRooms(result.rootCid, inviteFile)), 0o600);
|
|
106
182
|
replaceFileAtomically(join(dir, 'connector'), payload.connectorToken + '\n', 0o600);
|
|
107
183
|
const origin = `https://${payload.hostname}`;
|
|
108
184
|
replaceFileAtomically(join(dir, 'tunnel.json'), JSON.stringify({ origin, tokenFile: join(dir, 'connector') }) + '\n', 0o600);
|
|
@@ -111,6 +187,29 @@ export async function enrollWorkspace(payload, configuration = defaultConfigPath
|
|
|
111
187
|
return { origin, hostWorkspaceId, rootCid: result.rootCid };
|
|
112
188
|
}
|
|
113
189
|
/** Only the existing signed-root receipt authorizes the scoped tunnel target. */
|
|
190
|
+
/**
|
|
191
|
+
* The enrollment server contact exists only to carry the signed binding. Once the account has confirmed
|
|
192
|
+
* that binding, remove exactly that contact from this host's Messenger, so it does not stay in the
|
|
193
|
+
* person's chats. The server identity is the one recorded with the binding; nothing else is touched.
|
|
194
|
+
*/
|
|
195
|
+
export async function removeEnrollmentContact() {
|
|
196
|
+
let serverCid;
|
|
197
|
+
try {
|
|
198
|
+
serverCid = JSON.parse(readFileSync(join(stateRoot(), 'workspace', 'binding.json'), 'utf8')).serverCid;
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
if (typeof serverCid !== 'string' || !/^[A-F0-9]{64}$/i.test(serverCid))
|
|
204
|
+
return false;
|
|
205
|
+
try {
|
|
206
|
+
await hostMessenger()('contacts/remove', { contact: serverCid.toUpperCase() });
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
throw Error('Messenger did not remove it');
|
|
210
|
+
}
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
114
213
|
export async function configureWorkspacePort(payload, hostWorkspaceId, rootCid, port) {
|
|
115
214
|
const deadline = Date.now() + 15000;
|
|
116
215
|
while (true) {
|
|
@@ -127,3 +226,12 @@ export async function configureWorkspacePort(payload, hostWorkspaceId, rootCid,
|
|
|
127
226
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
128
227
|
}
|
|
129
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* Point the tunnel at this host's port. The account answers only for a confirmed signed binding, and
|
|
231
|
+
* only after that answer is the enrollment contact removed; a refused or unconfirmed target leaves it,
|
|
232
|
+
* so the setup can be finished later. A removal that fails is reported and does not undo the setup.
|
|
233
|
+
*/
|
|
234
|
+
export async function confirmWorkspaceTarget(payload, hostWorkspaceId, rootCid, port, cleanupFailed = () => { }) {
|
|
235
|
+
await configureWorkspacePort(payload, hostWorkspaceId, rootCid, port);
|
|
236
|
+
await removeEnrollmentContact().catch(cleanupFailed);
|
|
237
|
+
}
|
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.36",
|
|
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",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"scripts": {
|
|
29
29
|
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
30
|
-
"build": "npm run clean && tsc -p tsconfig.json && node scripts/
|
|
30
|
+
"build": "npm run clean && tsc -p tsconfig.json && node scripts/build-info.mjs",
|
|
31
31
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
32
32
|
"lint": "npm run typecheck",
|
|
33
33
|
"test": "vitest run",
|
|
@@ -39,7 +39,6 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@agentclientprotocol/sdk": "^1.3.0",
|
|
42
|
-
"@fastify/static": "^10.1.2",
|
|
43
42
|
"@fastify/websocket": "^11.2.0",
|
|
44
43
|
"@ours.network/cli": "2.8.1-nightly.13",
|
|
45
44
|
"@ours.network/mcp": "1.2.0-nightly.10",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
role: { ref: Critic }
|
|
2
2
|
brain: { ref: claude-default }
|
|
3
3
|
coordinator: FleetCoordinator
|
|
4
|
-
permissions: { approval:
|
|
4
|
+
permissions: { approval: allow, filesystem: unrestricted, unattended: deny }
|
|
5
5
|
monitor: { mode: fleet, interrupt: after_tool }
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
role: { ref: Developer }
|
|
2
2
|
brain: { ref: claude-default }
|
|
3
3
|
coordinator: FleetCoordinator
|
|
4
|
-
permissions: { approval:
|
|
4
|
+
permissions: { approval: allow, filesystem: unrestricted, unattended: deny }
|
|
5
5
|
monitor: { mode: fleet, interrupt: after_tool }
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
role: { ref: Engineer }
|
|
2
2
|
brain: { ref: claude-default }
|
|
3
3
|
coordinator: FleetCoordinator
|
|
4
|
-
permissions: { approval:
|
|
4
|
+
permissions: { approval: allow, filesystem: unrestricted, unattended: deny }
|
|
5
5
|
monitor: { mode: fleet, interrupt: after_tool }
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
role: { ref: LocalCoordinator }
|
|
2
2
|
brain: { ref: claude-default }
|
|
3
3
|
coordinator: FleetCoordinator
|
|
4
|
-
permissions: { approval:
|
|
4
|
+
permissions: { approval: allow, filesystem: unrestricted, unattended: deny }
|
|
5
5
|
monitor: { mode: fleet, interrupt: after_tool }
|
|
6
6
|
loops:
|
|
7
7
|
continuity:
|