@ours.network/fleet 1.2.0-nightly.4 → 1.2.0-nightly.41
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 +165 -53
- package/dist/account-origin.d.ts +2 -0
- package/dist/account-origin.js +6 -0
- package/dist/agent-ours/bridge.js +9 -0
- package/dist/agent-ours/correspondence.d.ts +10 -0
- package/dist/agent-ours/correspondence.js +1 -0
- package/dist/agent-ours/harness.js +8 -0
- package/dist/agent-ours/runtime.d.ts +6 -0
- package/dist/agent-ours/runtime.js +36 -0
- package/dist/agent-ours/service.js +33 -13
- package/dist/application/codex-models.d.ts +15 -0
- package/dist/application/codex-models.js +56 -0
- package/dist/application/conversation-history.d.ts +25 -0
- package/dist/application/conversation-history.js +189 -0
- package/dist/application/fleet-query-service.d.ts +1 -1
- package/dist/application/fleet-query-service.js +2 -2
- package/dist/application/legacy-supervisor-identity.d.ts +7 -0
- package/dist/application/legacy-supervisor-identity.js +43 -0
- package/dist/application/model-catalog.d.ts +1 -1
- package/dist/application/model-catalog.js +5 -2
- package/dist/application/preset-provenance.d.ts +19 -0
- package/dist/application/preset-provenance.js +59 -0
- package/dist/application/role-creation-service.js +1 -1
- package/dist/application/role-repository.js +2 -2
- 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 +18 -0
- package/dist/application/session-control.js +54 -3
- package/dist/application/supervisor-ours-tools.d.ts +159 -0
- package/dist/application/supervisor-ours-tools.js +113 -0
- package/dist/application/task-layouts.d.ts +91 -0
- package/dist/application/task-layouts.js +249 -0
- package/dist/application/task-room-service.d.ts +99 -3
- package/dist/application/task-room-service.js +296 -31
- package/dist/atomic-file.d.ts +5 -0
- package/dist/atomic-file.js +62 -1
- package/dist/build-info.json +6 -4
- package/dist/capabilities.d.ts +3 -1
- package/dist/capabilities.js +4 -0
- package/dist/cli.js +186 -34
- package/dist/client-profile.d.ts +5 -16
- package/dist/client-profile.js +4 -112
- package/dist/config.d.ts +18 -0
- package/dist/config.js +28 -4
- package/dist/creation.js +2 -2
- package/dist/daemon-recovery.d.ts +1 -8
- package/dist/daemon-recovery.js +8 -113
- package/dist/docs.d.ts +3 -3
- package/dist/docs.js +44 -22
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +46 -59
- package/dist/erased-resources.d.ts +9 -0
- package/dist/erased-resources.js +87 -0
- package/dist/fleet-command-audit.d.ts +1 -0
- package/dist/fleet-command-audit.js +11 -6
- package/dist/harness/claude-code.js +9 -1
- package/dist/harness/claude-runtime.d.ts +12 -0
- package/dist/harness/claude-runtime.js +43 -0
- package/dist/harness/codex-app-server-proxy.d.ts +4 -1
- package/dist/harness/codex-app-server-proxy.js +41 -5
- package/dist/harness/codex-runtime.d.ts +11 -1
- package/dist/harness/codex-runtime.js +31 -4
- package/dist/harness/codex.d.ts +3 -2
- package/dist/harness/codex.js +8 -5
- package/dist/init-wizard.d.ts +2 -0
- package/dist/init-wizard.js +38 -19
- package/dist/isolation/policy.js +2 -0
- package/dist/isolation/types.d.ts +5 -0
- package/dist/minimal-setup.d.ts +2 -0
- package/dist/minimal-setup.js +44 -0
- package/dist/monitor.d.ts +1 -34
- package/dist/monitor.js +24 -151
- package/dist/notifications/fleet-producer.d.ts +18 -0
- package/dist/notifications/fleet-producer.js +129 -0
- package/dist/notifications/outbox.d.ts +42 -0
- package/dist/notifications/outbox.js +149 -0
- package/dist/owner-channel/channel.d.ts +3 -0
- package/dist/owner-channel/channel.js +34 -6
- package/dist/owner-channel/commands.d.ts +4 -0
- package/dist/owner-channel/commands.js +8 -0
- package/dist/owner-channel/lifecycle-outbox.js +4 -3
- package/dist/owner-channel/ours-client.js +2 -17
- package/dist/paths.d.ts +2 -0
- package/dist/paths.js +2 -0
- package/dist/preset-migration.js +25 -0
- package/dist/rooms-tasks/add-member.d.ts +48 -0
- package/dist/rooms-tasks/add-member.js +83 -0
- package/dist/rooms-tasks/archived-absence.d.ts +8 -0
- package/dist/rooms-tasks/archived-absence.js +48 -0
- package/dist/rooms-tasks/cli.js +34 -5
- package/dist/rooms-tasks/close.d.ts +5 -2
- package/dist/rooms-tasks/close.js +208 -28
- package/dist/rooms-tasks/config.d.ts +1 -1
- package/dist/rooms-tasks/config.js +11 -4
- package/dist/rooms-tasks/cowork-adapter.d.ts +6 -4
- package/dist/rooms-tasks/cowork-adapter.js +11 -117
- package/dist/rooms-tasks/cowork-http.d.ts +3 -0
- package/dist/rooms-tasks/cowork-http.js +70 -0
- package/dist/rooms-tasks/deletion.d.ts +1 -1
- package/dist/rooms-tasks/deletion.js +74 -5
- package/dist/rooms-tasks/erasure.d.ts +6 -0
- package/dist/rooms-tasks/erasure.js +169 -0
- package/dist/rooms-tasks/external-worker.d.ts +1 -1
- package/dist/rooms-tasks/external-worker.js +3 -4
- package/dist/rooms-tasks/layout-binding-client.d.ts +11 -0
- package/dist/rooms-tasks/layout-binding-client.js +56 -0
- package/dist/rooms-tasks/layout-binding-grants.d.ts +21 -0
- package/dist/rooms-tasks/layout-binding-grants.js +128 -0
- package/dist/rooms-tasks/layout-cli.d.ts +4 -0
- package/dist/rooms-tasks/layout-cli.js +107 -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 +42 -0
- package/dist/rooms-tasks/layout-service.js +185 -0
- package/dist/rooms-tasks/layout.d.ts +114 -0
- package/dist/rooms-tasks/layout.js +250 -0
- package/dist/rooms-tasks/live-readiness.d.ts +18 -0
- package/dist/rooms-tasks/live-readiness.js +85 -0
- package/dist/rooms-tasks/member-startup.d.ts +1 -0
- package/dist/rooms-tasks/member-startup.js +2 -0
- package/dist/rooms-tasks/provision.d.ts +14 -0
- package/dist/rooms-tasks/provision.js +173 -46
- package/dist/rooms-tasks/room-state.d.ts +4 -2
- package/dist/rooms-tasks/room-state.js +53 -8
- package/dist/rooms-tasks/task-state.d.ts +42 -7
- package/dist/rooms-tasks/task-state.js +153 -23
- package/dist/rooms-tasks/types.d.ts +28 -0
- package/dist/rooms-tasks/workspace-artifacts.d.ts +6 -0
- package/dist/rooms-tasks/workspace-artifacts.js +93 -0
- package/dist/rooms-tasks/workspace.d.ts +26 -0
- package/dist/rooms-tasks/workspace.js +195 -0
- package/dist/runner.d.ts +19 -3
- package/dist/runner.js +86 -29
- package/dist/session/acp.d.ts +7 -1
- package/dist/session/acp.js +21 -6
- 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 +26 -3
- package/dist/session/control.d.ts +10 -1
- package/dist/session/control.js +37 -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/setup-tunnel-preflight.d.ts +32 -0
- package/dist/setup-tunnel-preflight.js +58 -0
- package/dist/socket-path.d.ts +2 -0
- package/dist/socket-path.js +22 -0
- package/dist/spawn.js +7 -0
- package/dist/startup-readiness.d.ts +13 -0
- package/dist/startup-readiness.js +38 -0
- package/dist/subscriptions/cli.d.ts +91 -0
- package/dist/subscriptions/cli.js +223 -0
- package/dist/subscriptions/launch.d.ts +37 -0
- package/dist/subscriptions/launch.js +93 -0
- package/dist/subscriptions/login.d.ts +75 -0
- package/dist/subscriptions/login.js +182 -0
- package/dist/subscriptions/observed.d.ts +7 -0
- package/dist/subscriptions/observed.js +44 -0
- package/dist/subscriptions/probe.d.ts +10 -0
- package/dist/subscriptions/probe.js +73 -0
- package/dist/subscriptions/service.d.ts +102 -0
- package/dist/subscriptions/service.js +364 -0
- package/dist/subscriptions/store.d.ts +71 -0
- package/dist/subscriptions/store.js +140 -0
- package/dist/supervisor/launchd.js +2 -1
- package/dist/supervisor/systemd.js +9 -23
- package/dist/temp-idle.d.ts +17 -0
- package/dist/temp-idle.js +42 -0
- package/dist/temp-lifecycle.d.ts +6 -1
- package/dist/temp-lifecycle.js +29 -13
- package/dist/web/agent-attachments.d.ts +29 -0
- package/dist/web/agent-attachments.js +162 -0
- package/dist/web/auth.d.ts +29 -1
- package/dist/web/auth.js +81 -7
- package/dist/web/available-port.d.ts +2 -0
- package/dist/web/available-port.js +23 -0
- package/dist/web/control.d.ts +3 -3
- package/dist/web/control.js +10 -8
- 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 +29 -0
- package/dist/web/prefix-gateway.js +206 -0
- package/dist/web/runtime.d.ts +3 -1
- package/dist/web/runtime.js +116 -31
- package/dist/web/server.d.ts +16 -0
- package/dist/web/server.js +501 -53
- package/dist/web/service.d.ts +11 -2
- package/dist/web/service.js +30 -4
- package/dist/web/workspace-devices.d.ts +29 -0
- package/dist/web/workspace-devices.js +84 -0
- package/dist/web/workspace-tunnel.d.ts +1 -0
- package/dist/web/workspace-tunnel.js +32 -0
- package/dist/workspace-enrollment.d.ts +53 -0
- package/dist/workspace-enrollment.js +370 -0
- package/docs/room-layouts.md +227 -0
- package/docs/task-workspaces.md +130 -0
- package/examples/room-layouts/fresh.yaml +27 -0
- package/examples/room-layouts/scoped.yaml +21 -0
- package/examples/room-layouts/shared.yaml +19 -0
- package/package.json +13 -18
- package/presets/README.md +78 -0
- package/presets/brain-catalog.json +8 -2
- 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 +5 -0
- package/presets/fleet/agent_templates/LocalCoordinator.yaml +1 -1
- package/presets/fleet/agents/FleetCoordinator.yaml +1 -1
- package/presets/fleet/brains/claude-fable-5-1-high.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-1-low.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-1-max.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-1-medium.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-1-xhigh.yaml +5 -0
- package/presets/fleet/brains/claude-fable-5-high.yaml +1 -1
- package/presets/fleet/brains/claude-fable-5-low.yaml +1 -1
- package/presets/fleet/brains/claude-fable-5-max.yaml +1 -1
- package/presets/fleet/brains/claude-fable-5-medium.yaml +1 -1
- package/presets/fleet/brains/claude-fable-5-xhigh.yaml +1 -1
- package/presets/fleet/brains/claude-haiku-4-5-20251001-default.yaml +4 -0
- package/presets/fleet/brains/claude-opus-5-5-high.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-5-low.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-5-max.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-5-medium.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-5-xhigh.yaml +5 -0
- package/presets/fleet/brains/claude-opus-5-high.yaml +1 -1
- package/presets/fleet/brains/claude-opus-5-low.yaml +1 -1
- package/presets/fleet/brains/claude-opus-5-max.yaml +1 -1
- package/presets/fleet/brains/claude-opus-5-medium.yaml +1 -1
- package/presets/fleet/brains/claude-opus-5-xhigh.yaml +1 -1
- package/presets/fleet/brains/claude-sonnet-5-high.yaml +1 -1
- package/presets/fleet/brains/claude-sonnet-5-low.yaml +1 -1
- package/presets/fleet/brains/claude-sonnet-5-max.yaml +1 -1
- package/presets/fleet/brains/claude-sonnet-5-medium.yaml +1 -1
- package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-5-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-5-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-1-sol-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-1-sol-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-1-sol-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-1-sol-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-1-sol-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-1-sol-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-astra-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-astra-max.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-astra-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-astra-ultra.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-astra-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-luna-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-luna-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-luna-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-luna-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-luna-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-sol-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-sol-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-sol-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-sol-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-sol-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-sol-xhigh.yaml +5 -0
- package/presets/fleet/roles/Coordinator.yaml +34 -11
- package/presets/fleet/roles/Engineer.yaml +40 -0
- 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/presets/fleet/room_templates/engineering.yaml +19 -0
- package/presets/manifest.json +2 -2
- 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
package/dist/web/server.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { packagedPresetRoot } from '../preset-bootstrap.js';
|
|
3
|
+
import { validateCatalog } from '../init-wizard.js';
|
|
4
|
+
import { getAdapter } from '../harness/registry.js';
|
|
5
|
+
import { storeAgentAttachment, attachmentPrompt, MAX_ATTACHMENT_BODY, readAgentAttachment, prepareAttachmentPresentation, presentAttachmentEvent } from './agent-attachments.js';
|
|
6
|
+
import { readChatIdle } from '../temp-idle.js';
|
|
7
|
+
import { agentDir } from '../paths.js';
|
|
8
|
+
import { SupervisorOursTools } from '../application/supervisor-ours-tools.js';
|
|
9
|
+
import { join } from 'node:path';
|
|
4
10
|
import Fastify from 'fastify';
|
|
5
|
-
import fastifyStatic from '@fastify/static';
|
|
6
11
|
import fastifyWebsocket from '@fastify/websocket';
|
|
7
12
|
import { createHmac, randomBytes } from 'node:crypto';
|
|
8
13
|
import { FleetError, normalizeError } from '../application/errors.js';
|
|
@@ -13,6 +18,8 @@ import { FleetEventBus } from './events.js';
|
|
|
13
18
|
import { ROLE_NAME_RE } from '../config.js';
|
|
14
19
|
import { TaskListError } from '../rooms-tasks/task-lists.js';
|
|
15
20
|
import { TaskStateError } from '../rooms-tasks/task-state.js';
|
|
21
|
+
import { isProvider } from '../subscriptions/store.js';
|
|
22
|
+
const ONBOARDING_WORK = ['coordination', 'development', 'review'];
|
|
16
23
|
const statusFor = (code) => ({
|
|
17
24
|
role_not_found: 404, resource_not_found: 404, unauthorized: 401, forbidden: 403, conflict: 409,
|
|
18
25
|
idempotency_conflict: 409, stale_state: 409, rate_limited: 429,
|
|
@@ -33,25 +40,40 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
33
40
|
app.addHook('onRequest', async (request, reply) => {
|
|
34
41
|
reply.header('X-Content-Type-Options', 'nosniff');
|
|
35
42
|
reply.header('Referrer-Policy', 'no-referrer');
|
|
36
|
-
reply.header('Permissions-Policy', 'camera=(), microphone=(), geolocation=(), payment=()');
|
|
37
|
-
|
|
38
|
-
reply.header('Content-Security-Policy',
|
|
39
|
-
`connect-src 'self' ${auth.secureCookies ? 'wss' : 'ws'}://${auth.host}; object-src 'none'; base-uri 'none'; ` +
|
|
40
|
-
`frame-ancestors 'none'; form-action 'self'; manifest-src 'self'; worker-src 'self'`);
|
|
43
|
+
reply.header('Permissions-Policy', 'camera=(), microphone=(self), geolocation=(), payment=()');
|
|
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'");
|
|
41
46
|
if (request.url.startsWith('/api/'))
|
|
42
47
|
reply.header('Cache-Control', 'no-store');
|
|
43
|
-
|
|
44
|
-
auth.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (request.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
if (request.headers.origin === auth.appOrigin && request.url.startsWith('/api/')) {
|
|
49
|
+
reply.header('Access-Control-Allow-Origin', auth.appOrigin).header('Vary', 'Origin');
|
|
50
|
+
reply.header('Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE,OPTIONS');
|
|
51
|
+
reply.header('Access-Control-Allow-Headers', 'Authorization,Content-Type,X-CSRF-Token,Idempotency-Key');
|
|
52
|
+
if (request.method === 'OPTIONS') {
|
|
53
|
+
auth.validateBoundary(request, false);
|
|
54
|
+
return reply.code(204).send();
|
|
55
|
+
}
|
|
51
56
|
}
|
|
57
|
+
auth.validateBoundary(request, false);
|
|
58
|
+
});
|
|
59
|
+
app.post('/api/v1/layout-bindings/:id/control', async (request) => {
|
|
60
|
+
// Initial support is direct same-host Fleet-to-Fleet transport.
|
|
61
|
+
const forwarded = Object.keys(request.headers).some(name => name === 'forwarded' || name.startsWith('x-forwarded-') || name === 'x-real-ip' || name === 'via');
|
|
62
|
+
if (!['127.0.0.1', '::1', '::ffff:127.0.0.1'].includes(request.ip) || forwarded)
|
|
63
|
+
throw new FleetError('forbidden', 'layout bindings require direct loopback access');
|
|
64
|
+
// Machine credentials cannot authenticate browser or lifecycle routes.
|
|
65
|
+
if (request.headers.origin)
|
|
66
|
+
auth.validateBoundary(request, true);
|
|
67
|
+
if (!services.layoutBindings)
|
|
68
|
+
throw new FleetError('capability_unavailable', 'layout bindings unavailable');
|
|
69
|
+
return services.layoutBindings.control(request.params.id, request.headers.authorization, request.body);
|
|
52
70
|
});
|
|
53
71
|
app.setErrorHandler(async (error, request, reply) => {
|
|
54
|
-
const
|
|
72
|
+
const privateToolParseError = ['/api/v1/roles/:id/ours/call', '/api/v1/layout-bindings/:id/control'].includes(request.routeOptions.url ?? '')
|
|
73
|
+
&& error instanceof Error && 'code' in error
|
|
74
|
+
&& typeof error.code === 'string' && error.code.startsWith('FST_ERR_CTP_');
|
|
75
|
+
const fleetError = normalizeError(privateToolParseError
|
|
76
|
+
? new FleetError('invalid_request', 'expected a valid JSON tool request') : error, request.id);
|
|
55
77
|
await audit.record({
|
|
56
78
|
requestId: request.id, action: `${request.method} ${request.routeOptions.url ?? request.url}`,
|
|
57
79
|
result: 'rejected', errorCode: fleetError.code,
|
|
@@ -96,6 +118,20 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
96
118
|
const session = auth.authenticate(request);
|
|
97
119
|
return { csrfToken: session.csrf, expiresAt: new Date(session.absoluteExpiresAt).toISOString() };
|
|
98
120
|
});
|
|
121
|
+
app.get('/api/v1/workspace/identity', async () => auth.workspaceIdentity());
|
|
122
|
+
app.post('/api/v1/devices/enroll', async (request, reply) => {
|
|
123
|
+
reply.header('Cache-Control', 'no-store');
|
|
124
|
+
const input = request.body;
|
|
125
|
+
if (!input)
|
|
126
|
+
throw new FleetError('invalid_request', 'device enrollment body required');
|
|
127
|
+
return auth.enrollWorkspace(request, input);
|
|
128
|
+
});
|
|
129
|
+
app.post('/api/v1/devices/link', async (request) => {
|
|
130
|
+
const session = auth.authenticate(request, true);
|
|
131
|
+
return { version: 1, origin: auth.origin, ...auth.workspaceEnrollment(session.id) };
|
|
132
|
+
});
|
|
133
|
+
app.get('/api/v1/devices', async (request) => { auth.authenticate(request); return { devices: auth.listWorkspaceDevices() }; });
|
|
134
|
+
app.delete('/api/v1/devices/:id', async (request) => { auth.authenticate(request, true); auth.revokeWorkspaceDevice(request.params.id); return { revoked: true }; });
|
|
99
135
|
app.get('/api/v1/meta', async (request) => {
|
|
100
136
|
auth.authenticate(request);
|
|
101
137
|
return {
|
|
@@ -106,6 +142,79 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
106
142
|
auditDegraded: audit.degraded,
|
|
107
143
|
};
|
|
108
144
|
});
|
|
145
|
+
app.get('/api/v1/onboarding', async (request) => {
|
|
146
|
+
auth.authenticate(request);
|
|
147
|
+
const harnesses = await Promise.all(['codex', 'claude-code'].map(async (harness) => {
|
|
148
|
+
try {
|
|
149
|
+
return { harness, ...await getAdapter(harness).checkPrereqs() };
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return { harness, ok: false, checks: [{ name: 'availability', ok: false, detail: 'Harness checks unavailable' }] };
|
|
153
|
+
}
|
|
154
|
+
}));
|
|
155
|
+
const catalog = validateCatalog(JSON.parse(readFileSync(join(packagedPresetRoot(), 'brain-catalog.json'), 'utf8')));
|
|
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() };
|
|
161
|
+
});
|
|
162
|
+
// First-time setup is what creates the persistent coordinator. A configuration that loads and
|
|
163
|
+
// has it is set up, whatever its brains are called; its models are then changed by editing it.
|
|
164
|
+
const onboardingConfigured = () => {
|
|
165
|
+
if (!services.configuration)
|
|
166
|
+
return false;
|
|
167
|
+
try {
|
|
168
|
+
return Boolean(services.configuration.read(false).model.agents.FleetCoordinator);
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
let onboardingSetupQueue = Promise.resolve();
|
|
175
|
+
app.post('/api/v1/onboarding/setup', async (request) => {
|
|
176
|
+
auth.authenticate(request, true);
|
|
177
|
+
if (!services.configuration || !services.onboardingSetup)
|
|
178
|
+
throw new FleetError('capability_unavailable', 'first-time Fleet setup is unavailable');
|
|
179
|
+
const requested = request.body?.models;
|
|
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)));
|
|
183
|
+
const models = {};
|
|
184
|
+
for (const work of ONBOARDING_WORK) {
|
|
185
|
+
const matches = supported.filter(model => model.harness === requested?.[work]?.harness && model.model === requested?.[work]?.model);
|
|
186
|
+
if (matches.length !== 1)
|
|
187
|
+
throw new FleetError('invalid_request', `choose a supported model for ${work}`);
|
|
188
|
+
models[work] = matches[0];
|
|
189
|
+
}
|
|
190
|
+
const chosen = Object.values(models);
|
|
191
|
+
const subscriptions = [...new Set(chosen.map(model => model.harness === 'codex' ? 'codex' : 'claude'))];
|
|
192
|
+
const oneModel = new Set(chosen.map(model => `${model.harness}\0${model.model}`)).size === 1;
|
|
193
|
+
const configuration = services.configuration, setup = services.onboardingSetup;
|
|
194
|
+
// One setup at a time: the first-time check and the publication it guards must not interleave.
|
|
195
|
+
const run = onboardingSetupQueue.then(async () => {
|
|
196
|
+
if (onboardingConfigured())
|
|
197
|
+
throw new FleetError('conflict', 'Fleet is already set up; change its models in the configuration');
|
|
198
|
+
try {
|
|
199
|
+
await setup({ subscriptions, assignmentStrategy: oneModel ? 'one-model' : 'per-job', models, reasoning: 'balanced' });
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
throw new FleetError('backend_failure', `Fleet setup did not complete: ${error.message}`);
|
|
203
|
+
}
|
|
204
|
+
// Setup preserves files it finds, so confirm the published models are the ones this request chose.
|
|
205
|
+
// The prepared agents reference packaged Brain presets, so resolve what each of them now points at:
|
|
206
|
+
// a preserved file of a packaged name may hold another model, session or reasoning than its name says.
|
|
207
|
+
const published = configuration.read(true).model, brains = published.brains ?? {};
|
|
208
|
+
const brainOf = (holder) => brains[String(holder?.brain?.ref ?? '')];
|
|
209
|
+
const assigned = { coordination: brainOf(published.agents.FleetCoordinator), development: brainOf(published.agent_templates?.Developer), review: brainOf(published.agent_templates?.Critic) };
|
|
210
|
+
for (const work of ONBOARDING_WORK)
|
|
211
|
+
if (assigned[work]?.harness !== models[work].harness || assigned[work]?.model !== models[work].model || assigned[work]?.session !== models[work].session || assigned[work]?.effort !== 'medium')
|
|
212
|
+
throw new FleetError('conflict', 'Fleet was set up with different models; review its configuration');
|
|
213
|
+
return { configured: true };
|
|
214
|
+
});
|
|
215
|
+
onboardingSetupQueue = run.catch(() => { });
|
|
216
|
+
return run;
|
|
217
|
+
});
|
|
109
218
|
app.get('/api/v1/creation-capabilities', async (request) => {
|
|
110
219
|
auth.authenticate(request);
|
|
111
220
|
return services.creation.capabilities();
|
|
@@ -128,7 +237,8 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
128
237
|
});
|
|
129
238
|
app.get('/api/v1/roles', async (request) => {
|
|
130
239
|
auth.authenticate(request);
|
|
131
|
-
|
|
240
|
+
const query = request.query;
|
|
241
|
+
return { roles: await services.query.list(query.includeTemporary === 'true') };
|
|
132
242
|
});
|
|
133
243
|
const taskApi = async (fn) => {
|
|
134
244
|
try {
|
|
@@ -141,6 +251,8 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
141
251
|
? 'conflict' : 'invalid_request';
|
|
142
252
|
throw new FleetError(code, error.message);
|
|
143
253
|
}
|
|
254
|
+
if (error instanceof TaskStateError && 'accepted' in error && error.accepted === false)
|
|
255
|
+
return Promise.reject(Object.assign(new FleetError('invalid_request', error.message), { memberNotAccepted: true }));
|
|
144
256
|
if (error instanceof TaskStateError) {
|
|
145
257
|
throw new FleetError(error.message.startsWith('task not found:') ? 'resource_not_found' : 'conflict', error.message);
|
|
146
258
|
}
|
|
@@ -188,9 +300,10 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
188
300
|
const state = query.state && query.state !== 'all' ? query.state : undefined;
|
|
189
301
|
const filter = { ...(state ? { state } : {}), ...(query.list ? { list: query.list } : {}),
|
|
190
302
|
...(query.includeDeleting === 'true' ? { includeDeleting: true } : {}) };
|
|
303
|
+
const api = requireTaskRooms();
|
|
191
304
|
return taskApi(() => query.groupByList === 'true'
|
|
192
|
-
? { groups:
|
|
193
|
-
: { tasks:
|
|
305
|
+
? { groups: api.groupedTasks(filter).map(group => ({ ...group, tasks: group.tasks.map(task => api.withLayoutRooms(task)) })) }
|
|
306
|
+
: { tasks: api.listTasks(filter).map(task => api.withLayoutRooms(task)) });
|
|
194
307
|
});
|
|
195
308
|
app.delete('/api/v1/tasks/:id', async (request, reply) => {
|
|
196
309
|
auth.authenticate(request, true);
|
|
@@ -221,12 +334,17 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
221
334
|
const body = request.body;
|
|
222
335
|
if (typeof body?.title !== 'string' || !body.title)
|
|
223
336
|
throw new FleetError('invalid_request', 'title is required');
|
|
337
|
+
// Every task runs a saved Room Layout; rooms are never defined inline or from room templates here.
|
|
338
|
+
if (typeof body.layout !== 'string' || !body.layout)
|
|
339
|
+
throw new FleetError('invalid_request', 'layout is required: choose a room layout from Settings › Room layouts');
|
|
340
|
+
if (body.template !== undefined || body.noRoom !== undefined)
|
|
341
|
+
throw new FleetError('invalid_request', 'tasks use room layouts; template and noRoom are not accepted');
|
|
224
342
|
const task = await taskApi(() => requireTaskRooms().createTask({
|
|
225
343
|
actor: { kind: 'local_control', surface: 'web' }, title: body.title,
|
|
226
344
|
brief: typeof body.brief === 'string' ? body.brief : undefined,
|
|
227
|
-
|
|
228
|
-
backlog: body.backlog === true, noRoom: body.noRoom === true,
|
|
345
|
+
backlog: body.backlog === true,
|
|
229
346
|
list: typeof body.list === 'string' ? body.list : undefined,
|
|
347
|
+
layout: body.layout,
|
|
230
348
|
idempotencyKey: typeof request.headers['idempotency-key'] === 'string'
|
|
231
349
|
? request.headers['idempotency-key'] : undefined,
|
|
232
350
|
origin: { type: 'web' },
|
|
@@ -240,11 +358,162 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
240
358
|
const list = String(request.body?.list ?? '');
|
|
241
359
|
return { task: await taskApi(() => requireTaskRooms().moveTask({ actor: { kind: 'local_control', surface: 'web' }, taskId, list })) };
|
|
242
360
|
});
|
|
361
|
+
app.patch('/api/v1/tasks/:id/description', async (request) => {
|
|
362
|
+
auth.authenticate(request, true);
|
|
363
|
+
const body = request.body;
|
|
364
|
+
if (typeof body?.brief !== 'string' || typeof body.expectedBrief !== 'string' || body.brief.length > 100_000)
|
|
365
|
+
throw new FleetError('invalid_request', 'brief and expectedBrief must be strings; brief maximum is 100000 characters');
|
|
366
|
+
return { task: await taskApi(() => requireTaskRooms().editTaskDescription({
|
|
367
|
+
actor: { kind: 'local_control', surface: 'web' }, taskId: request.params.id,
|
|
368
|
+
brief: body.brief, expectedBrief: body.expectedBrief,
|
|
369
|
+
})) };
|
|
370
|
+
});
|
|
371
|
+
app.get('/api/v1/tasks/:id', async (request) => {
|
|
372
|
+
auth.authenticate(request);
|
|
373
|
+
return taskApi(() => requireTaskRooms().getTask(request.params.id));
|
|
374
|
+
});
|
|
375
|
+
app.post('/api/v1/tasks/:id/members', async (request, reply) => {
|
|
376
|
+
auth.authenticate(request, true);
|
|
377
|
+
const body = request.body;
|
|
378
|
+
try {
|
|
379
|
+
const result = await taskApi(() => requireTaskRooms().addMember(request.params.id, body));
|
|
380
|
+
reply.code(result.state === 'running' ? 202 : 200);
|
|
381
|
+
return result;
|
|
382
|
+
}
|
|
383
|
+
catch (error) {
|
|
384
|
+
if (error instanceof Error && 'memberNotAccepted' in error)
|
|
385
|
+
return reply.code(409).send({ error: { code: 'member_not_accepted', message: error.message }, accepted: false });
|
|
386
|
+
throw error;
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
app.get('/api/v1/tasks/:id/member-additions/:requestId', async (request) => {
|
|
390
|
+
auth.authenticate(request);
|
|
391
|
+
const { id, requestId } = request.params;
|
|
392
|
+
return taskApi(() => requireTaskRooms().memberAddition(id, requestId));
|
|
393
|
+
});
|
|
394
|
+
app.patch('/api/v1/tasks/:id/layout', async (request) => {
|
|
395
|
+
const session = auth.authenticate(request, true);
|
|
396
|
+
const body = request.body;
|
|
397
|
+
const name = (v) => typeof v === 'string' && v.length > 0;
|
|
398
|
+
if (!name(body?.layout) || !(body.expectedLayout === null || name(body.expectedLayout)))
|
|
399
|
+
throw new FleetError('invalid_request', 'layout must be a layout name; expectedLayout a layout name or null');
|
|
400
|
+
const task = await taskApi(() => requireTaskRooms().setTaskLayout({ actor: { kind: 'local_control', surface: 'web' },
|
|
401
|
+
taskId: request.params.id, layout: body.layout, expectedLayout: body.expectedLayout }));
|
|
402
|
+
await audit.record({ requestId: request.id, browser: session.id, action: 'task.layout.set', result: 'succeeded' });
|
|
403
|
+
return { task: requireTaskRooms().withLayoutRooms(task) };
|
|
404
|
+
});
|
|
405
|
+
app.get('/api/v1/tasks/:id/layout', async (request) => {
|
|
406
|
+
auth.authenticate(request);
|
|
407
|
+
return { layout: await taskApi(async () => requireTaskRooms().taskLayout(request.params.id)) };
|
|
408
|
+
});
|
|
409
|
+
for (const operation of ['open', 'close']) {
|
|
410
|
+
app.post(`/api/v1/tasks/:id/layout/rooms/:room/${operation}`, async (request, reply) => {
|
|
411
|
+
const session = auth.authenticate(request, true);
|
|
412
|
+
const { id, room } = request.params;
|
|
413
|
+
const accepted = await taskApi(() => requireTaskRooms().launchTaskLayoutOperation({
|
|
414
|
+
actor: { kind: 'local_control', surface: 'web' }, taskId: id, room,
|
|
415
|
+
operation: operation === 'open' ? 'open' : 'close-room',
|
|
416
|
+
}));
|
|
417
|
+
await audit.record({ requestId: request.id, browser: session.id, action: `task.layout.${operation}`, result: 'succeeded' });
|
|
418
|
+
reply.code(202);
|
|
419
|
+
return { operation: accepted };
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
app.post('/api/v1/tasks/:id/layout/cleanup', async (request, reply) => {
|
|
423
|
+
auth.authenticate(request, true);
|
|
424
|
+
const accepted = await taskApi(() => requireTaskRooms().retryTaskLayoutCleanup({
|
|
425
|
+
actor: { kind: 'local_control', surface: 'web' }, taskId: request.params.id,
|
|
426
|
+
}));
|
|
427
|
+
reply.code(202);
|
|
428
|
+
return { operation: accepted };
|
|
429
|
+
});
|
|
430
|
+
app.get('/api/v1/task-templates', async (request) => {
|
|
431
|
+
auth.authenticate(request);
|
|
432
|
+
return { templates: requireTaskRooms().listTemplates() };
|
|
433
|
+
});
|
|
434
|
+
const layouts = () => {
|
|
435
|
+
if (!services.roomLayouts)
|
|
436
|
+
throw new FleetError('capability_unavailable', 'room layout editing is unavailable');
|
|
437
|
+
return services.roomLayouts;
|
|
438
|
+
};
|
|
439
|
+
app.get('/api/v1/room-layouts', async (request) => {
|
|
440
|
+
auth.authenticate(request);
|
|
441
|
+
const list = layouts().list(), defaultLayout = layouts().defaultLayout(list);
|
|
442
|
+
return { layouts: list, ...(defaultLayout ? { default_layout: defaultLayout } : {}) };
|
|
443
|
+
});
|
|
444
|
+
app.post('/api/v1/room-layouts/validate', async (request) => {
|
|
445
|
+
auth.authenticate(request, true);
|
|
446
|
+
return layouts().validate(request.body?.definition);
|
|
447
|
+
});
|
|
448
|
+
app.put('/api/v1/room-layouts/:name', async (request) => {
|
|
449
|
+
const session = auth.authenticate(request, true);
|
|
450
|
+
const body = request.body;
|
|
451
|
+
if (typeof body?.revision !== 'string')
|
|
452
|
+
throw new FleetError('invalid_request', 'revision is required');
|
|
453
|
+
const result = await layouts().save(request.params.name, body.revision, body.definition);
|
|
454
|
+
events.publish('room_layouts.changed', { name: result.name, revision: result.revision });
|
|
455
|
+
await audit.record({ requestId: request.id, browser: session.id, action: 'room_layout.save', result: 'succeeded' });
|
|
456
|
+
return result;
|
|
457
|
+
});
|
|
458
|
+
app.delete('/api/v1/room-layouts/:name', async (request) => {
|
|
459
|
+
const session = auth.authenticate(request, true);
|
|
460
|
+
const revision = request.query.revision;
|
|
461
|
+
if (typeof revision !== 'string' || !revision)
|
|
462
|
+
throw new FleetError('invalid_request', 'revision is required');
|
|
463
|
+
const result = await layouts().remove(request.params.name, revision);
|
|
464
|
+
events.publish('room_layouts.changed', { name: result.name });
|
|
465
|
+
await audit.record({ requestId: request.id, browser: session.id, action: 'room_layout.delete', result: 'succeeded' });
|
|
466
|
+
return result;
|
|
467
|
+
});
|
|
468
|
+
for (const action of ['start', 'block', 'unblock', 'review', 'finish', 'cancel']) {
|
|
469
|
+
app.post(`/api/v1/tasks/:id/${action}`, async (request, reply) => {
|
|
470
|
+
auth.authenticate(request, true);
|
|
471
|
+
const taskId = request.params.id;
|
|
472
|
+
const body = (request.body ?? {});
|
|
473
|
+
const input = { actor: { kind: 'local_control', surface: 'web' }, taskId };
|
|
474
|
+
const api = requireTaskRooms();
|
|
475
|
+
return taskApi(async () => {
|
|
476
|
+
switch (action) {
|
|
477
|
+
case 'start': {
|
|
478
|
+
// A task's layout can be chosen or changed but never removed, so this check cannot go stale.
|
|
479
|
+
if (!api.getTask(taskId).task.layout)
|
|
480
|
+
throw new FleetError('conflict', 'choose a room layout for this task before starting it');
|
|
481
|
+
const task = await api.startTask(input);
|
|
482
|
+
const provisioning = await api.launchTaskProvisioning(taskId);
|
|
483
|
+
if (provisioning.kind !== 'ready')
|
|
484
|
+
reply.code(202);
|
|
485
|
+
return { task, provisioning };
|
|
486
|
+
}
|
|
487
|
+
case 'block':
|
|
488
|
+
if (typeof body.reason !== 'string' || !body.reason.trim())
|
|
489
|
+
throw new FleetError('invalid_request', 'reason is required');
|
|
490
|
+
return { task: api.blockTask({ ...input, reason: body.reason }) };
|
|
491
|
+
case 'unblock': return { task: api.unblockTask(input) };
|
|
492
|
+
case 'review': return { task: api.reviewTask(input) };
|
|
493
|
+
case 'finish':
|
|
494
|
+
case 'cancel': {
|
|
495
|
+
const plan = action === 'finish' ? await api.finishTask(input) : await api.cancelTask(input);
|
|
496
|
+
const task = plan.settlementRequired ? await api.launchTaskSettlement(taskId) : plan.task;
|
|
497
|
+
const pending = task.terminal_intent?.status === 'pending';
|
|
498
|
+
if (pending)
|
|
499
|
+
reply.code(202);
|
|
500
|
+
return { task, pending };
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
app.get('/api/v1/configuration/provenance', async (request) => {
|
|
507
|
+
auth.authenticate(request);
|
|
508
|
+
if (!services.presetProvenance)
|
|
509
|
+
throw new FleetError('capability_unavailable', 'preset provenance is unavailable');
|
|
510
|
+
return { provenance: services.presetProvenance.read() };
|
|
511
|
+
});
|
|
243
512
|
app.get('/api/v1/configuration', async (request) => {
|
|
244
513
|
auth.authenticate(request);
|
|
245
514
|
if (!services.configuration)
|
|
246
515
|
throw new FleetError('capability_unavailable', 'fleet configuration editing is unavailable');
|
|
247
|
-
return services.configuration.read();
|
|
516
|
+
return services.configuration.read(request.query.includeDefinitions === 'true');
|
|
248
517
|
});
|
|
249
518
|
app.post('/api/v1/configuration/preview', async (request) => {
|
|
250
519
|
auth.authenticate(request, true);
|
|
@@ -265,6 +534,86 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
265
534
|
});
|
|
266
535
|
return result;
|
|
267
536
|
});
|
|
537
|
+
// Claude Code / Codex subscription profiles. Responses carry account metadata
|
|
538
|
+
// only; token values never leave the CLI-owned credential files.
|
|
539
|
+
const subscriptions = () => {
|
|
540
|
+
if (!services.subscriptions)
|
|
541
|
+
throw new FleetError('capability_unavailable', 'subscription management is unavailable');
|
|
542
|
+
return services.subscriptions;
|
|
543
|
+
};
|
|
544
|
+
const provider = (raw) => {
|
|
545
|
+
if (!isProvider(raw))
|
|
546
|
+
throw new FleetError('invalid_request', 'unknown subscription provider');
|
|
547
|
+
return raw;
|
|
548
|
+
};
|
|
549
|
+
const subscriptionsChanged = () => events.publish('subscription.changed', {});
|
|
550
|
+
app.get('/api/v1/subscriptions', async (request) => {
|
|
551
|
+
auth.authenticate(request);
|
|
552
|
+
return { providers: await subscriptions().list() };
|
|
553
|
+
});
|
|
554
|
+
app.post('/api/v1/subscriptions/:provider/logins', async (request, reply) => {
|
|
555
|
+
const session = auth.authenticate(request, true);
|
|
556
|
+
const login = await subscriptions().startLogin(provider(request.params.provider), session.id);
|
|
557
|
+
await audit.record({ requestId: request.id, browser: session.id, action: `subscription.login.${request.params.provider}`, result: login.state });
|
|
558
|
+
reply.code(201);
|
|
559
|
+
return login;
|
|
560
|
+
});
|
|
561
|
+
app.get('/api/v1/subscriptions/:provider/logins/:loginId', async (request) => {
|
|
562
|
+
const session = auth.authenticate(request);
|
|
563
|
+
return subscriptions().login(request.params.loginId, session.id).view();
|
|
564
|
+
});
|
|
565
|
+
app.post('/api/v1/subscriptions/:provider/logins/:loginId/code', async (request) => {
|
|
566
|
+
const session = auth.authenticate(request, true);
|
|
567
|
+
const code = request.body?.code;
|
|
568
|
+
if (typeof code !== 'string')
|
|
569
|
+
throw new FleetError('invalid_request', 'code is required');
|
|
570
|
+
const login = subscriptions().login(request.params.loginId, session.id);
|
|
571
|
+
login.submitCode(code.trim());
|
|
572
|
+
await audit.record({ requestId: request.id, browser: session.id, action: `subscription.login.code.${request.params.provider}`, result: 'submitted' });
|
|
573
|
+
return login.view();
|
|
574
|
+
});
|
|
575
|
+
app.delete('/api/v1/subscriptions/:provider/logins/:loginId', async (request) => {
|
|
576
|
+
const session = auth.authenticate(request, true);
|
|
577
|
+
const login = subscriptions().login(request.params.loginId, session.id);
|
|
578
|
+
login.cancel();
|
|
579
|
+
return login.view();
|
|
580
|
+
});
|
|
581
|
+
app.post('/api/v1/subscriptions/:provider/active', async (request) => {
|
|
582
|
+
const session = auth.authenticate(request, true);
|
|
583
|
+
const profileId = request.body?.profileId;
|
|
584
|
+
if (typeof profileId !== 'string')
|
|
585
|
+
throw new FleetError('invalid_request', 'profileId is required');
|
|
586
|
+
const view = await subscriptions().setActive(provider(request.params.provider), profileId);
|
|
587
|
+
subscriptionsChanged();
|
|
588
|
+
await audit.record({ requestId: request.id, browser: session.id, action: `subscription.activate.${request.params.provider}`, result: 'succeeded' });
|
|
589
|
+
return view;
|
|
590
|
+
});
|
|
591
|
+
app.post('/api/v1/subscriptions/:provider/profiles/:profileId/check', async (request) => {
|
|
592
|
+
auth.authenticate(request, true);
|
|
593
|
+
return subscriptions().check(provider(request.params.provider), request.params.profileId);
|
|
594
|
+
});
|
|
595
|
+
app.post('/api/v1/subscriptions/claude/profiles/:profileId/probe', async (request) => {
|
|
596
|
+
const session = auth.authenticate(request, true);
|
|
597
|
+
const usage = await subscriptions().probeClaudeUsage(request.params.profileId);
|
|
598
|
+
await audit.record({ requestId: request.id, browser: session.id, action: 'subscription.usage.probe.claude', result: usage.source });
|
|
599
|
+
return { usage };
|
|
600
|
+
});
|
|
601
|
+
app.patch('/api/v1/subscriptions/:provider/profiles/:profileId', async (request) => {
|
|
602
|
+
auth.authenticate(request, true);
|
|
603
|
+
const label = request.body?.label;
|
|
604
|
+
if (typeof label !== 'string')
|
|
605
|
+
throw new FleetError('invalid_request', 'label is required');
|
|
606
|
+
await subscriptions().rename(provider(request.params.provider), request.params.profileId, label);
|
|
607
|
+
subscriptionsChanged();
|
|
608
|
+
return { ok: true };
|
|
609
|
+
});
|
|
610
|
+
app.delete('/api/v1/subscriptions/:provider/profiles/:profileId', async (request) => {
|
|
611
|
+
const session = auth.authenticate(request, true);
|
|
612
|
+
await subscriptions().remove(provider(request.params.provider), request.params.profileId);
|
|
613
|
+
subscriptionsChanged();
|
|
614
|
+
await audit.record({ requestId: request.id, browser: session.id, action: `subscription.remove.${request.params.provider}`, result: 'succeeded' });
|
|
615
|
+
return { ok: true };
|
|
616
|
+
});
|
|
268
617
|
app.get('/api/v1/topology', async (request) => {
|
|
269
618
|
auth.authenticate(request);
|
|
270
619
|
if (!services.topology)
|
|
@@ -311,9 +660,36 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
311
660
|
});
|
|
312
661
|
return result;
|
|
313
662
|
});
|
|
663
|
+
const oursTools = services.oursTools ?? new SupervisorOursTools();
|
|
664
|
+
app.get('/api/v1/roles/:id/contacts', async (request) => {
|
|
665
|
+
auth.authenticate(request);
|
|
666
|
+
const control = await services.session(request.params.id);
|
|
667
|
+
if (!control.agentContacts)
|
|
668
|
+
throw new FleetError('capability_unavailable', 'Supervisor contacts API is unavailable');
|
|
669
|
+
return control.agentContacts();
|
|
670
|
+
});
|
|
671
|
+
app.get('/api/v1/roles/:id/messages', async (request) => {
|
|
672
|
+
auth.authenticate(request);
|
|
673
|
+
const control = await services.session(request.params.id);
|
|
674
|
+
if (!control.agentHistory)
|
|
675
|
+
throw new FleetError('capability_unavailable', 'Supervisor history API is unavailable');
|
|
676
|
+
return control.agentHistory({ peer_cid: request.query.peer_cid, ...(request.query.limit !== undefined ? { limit: Number(request.query.limit) } : {}), ...(request.query.before_seq !== undefined ? { before_seq: Number(request.query.before_seq) } : {}) });
|
|
677
|
+
});
|
|
678
|
+
app.get('/api/v1/roles/:id/ours/tools', async (request) => {
|
|
679
|
+
auth.authenticate(request);
|
|
680
|
+
return oursTools.list(request.params.id);
|
|
681
|
+
});
|
|
682
|
+
app.post('/api/v1/roles/:id/ours/call', async (request) => {
|
|
683
|
+
const session = auth.authenticate(request, true);
|
|
684
|
+
const result = await oursTools.call(request.params.id, request.body);
|
|
685
|
+
await audit.record({ requestId: request.id, browser: session.id,
|
|
686
|
+
action: 'ours.call', result: result.result.isError === true ? 'tool_error' : 'succeeded' });
|
|
687
|
+
return result;
|
|
688
|
+
});
|
|
314
689
|
app.get('/api/v1/roles/:id', async (request) => {
|
|
315
690
|
auth.authenticate(request);
|
|
316
|
-
|
|
691
|
+
const detail = await services.query.detail(request.params.id);
|
|
692
|
+
return { ...detail, chatIdle: detail.role.lifetime === 'temporary' ? readChatIdle(agentDir(request.params.id, true)) ?? null : null };
|
|
317
693
|
});
|
|
318
694
|
app.get('/api/v1/roles/:id/removal-preview', async (request) => {
|
|
319
695
|
auth.authenticate(request);
|
|
@@ -348,15 +724,79 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
348
724
|
const control = await services.session(request.params.id);
|
|
349
725
|
if (!control.conversationPage)
|
|
350
726
|
throw new FleetError('capability_unavailable', 'this role has no conversation ledger');
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
727
|
+
const limit = request.query.limit ? Number(request.query.limit) : undefined;
|
|
728
|
+
// `latest` / `before` page backwards from the newest event; `after` pages forward for live updates.
|
|
729
|
+
const backward = request.query.latest === '1' || request.query.before !== undefined;
|
|
730
|
+
if (backward && request.query.after !== undefined)
|
|
731
|
+
throw new FleetError('invalid_request', 'after cannot be combined with latest or before');
|
|
732
|
+
if (backward && !control.conversationTail)
|
|
733
|
+
throw new FleetError('capability_unavailable', 'this role cannot page its conversation backwards');
|
|
734
|
+
const role = await services.repository.get(request.params.id);
|
|
735
|
+
const stateDir = role ? services.repository.stateDir(role) : undefined;
|
|
736
|
+
const present = (event) => presentAttachmentEvent(stateDir, request.params.id, event);
|
|
737
|
+
if (backward) {
|
|
738
|
+
const page = await control.conversationTail({ before: request.query.before, limit });
|
|
739
|
+
return { ...page, events: page.events.map(present), context: page.context.map(present) };
|
|
740
|
+
}
|
|
741
|
+
const page = await control.conversationPage({ after: request.query.after, limit });
|
|
742
|
+
return { ...page, events: page.events.map(present) };
|
|
743
|
+
});
|
|
744
|
+
app.get('/api/v1/roles/:id/attachments/:attachmentId', async (request, reply) => {
|
|
745
|
+
auth.authenticate(request);
|
|
746
|
+
const role = await services.repository.get(request.params.id);
|
|
747
|
+
if (!role)
|
|
748
|
+
throw new FleetError('role_not_found', 'Agent not found');
|
|
749
|
+
const stateDir = services.repository.stateDir(role);
|
|
750
|
+
if (!stateDir)
|
|
751
|
+
throw new FleetError('capability_unavailable', 'Agent file storage is unavailable');
|
|
752
|
+
const { record, bytes } = readAgentAttachment(stateDir, request.params.attachmentId);
|
|
753
|
+
const image = /^image\/(png|jpeg|gif|webp|avif)$/.test(record.mimeType);
|
|
754
|
+
reply.header('Cache-Control', 'private, no-store');
|
|
755
|
+
reply.header('X-Content-Type-Options', 'nosniff');
|
|
756
|
+
reply.header('Content-Security-Policy', "default-src 'none'; sandbox");
|
|
757
|
+
reply.header('Content-Disposition', `${image ? 'inline' : 'attachment'}; filename*=UTF-8''${encodeURIComponent(record.name).replace(/'/g, '%27')}`);
|
|
758
|
+
return reply.type(image ? record.mimeType : 'application/octet-stream').send(bytes);
|
|
759
|
+
});
|
|
760
|
+
app.post('/api/v1/roles/:id/attachments', { bodyLimit: MAX_ATTACHMENT_BODY }, async (request) => {
|
|
761
|
+
auth.authenticate(request, true);
|
|
762
|
+
const role = await services.repository.get(request.params.id);
|
|
763
|
+
if (!role)
|
|
764
|
+
throw new FleetError('role_not_found', 'Agent not found');
|
|
765
|
+
const stateDir = services.repository.stateDir(role);
|
|
766
|
+
if (!stateDir)
|
|
767
|
+
throw new FleetError('capability_unavailable', 'Agent file storage is unavailable');
|
|
768
|
+
const body = request.body;
|
|
769
|
+
if (typeof body?.expectedSessionGeneration !== 'string' || !body.expectedSessionGeneration.trim())
|
|
770
|
+
throw new FleetError('invalid_request', 'expectedSessionGeneration is required for attachments');
|
|
771
|
+
const control = await services.session(request.params.id);
|
|
772
|
+
if (!control.conversationPage || !control.submitPromptV2)
|
|
773
|
+
throw new FleetError('capability_unavailable', 'Session-bound attachments are unavailable');
|
|
774
|
+
const page = await control.conversationPage({ limit: 1 });
|
|
775
|
+
if (page.snapshot.sessionGeneration !== body.expectedSessionGeneration)
|
|
776
|
+
throw new FleetError('stale_state', 'Agent session changed. Select the files again.');
|
|
777
|
+
return storeAgentAttachment(stateDir, body.expectedSessionGeneration, request.body);
|
|
355
778
|
});
|
|
356
779
|
app.post('/api/v1/roles/:id/input', async (request, reply) => {
|
|
357
780
|
const session = auth.authenticate(request, true);
|
|
358
781
|
const body = request.body;
|
|
359
|
-
|
|
782
|
+
if (body?.expectedSessionGeneration !== undefined && (typeof body.expectedSessionGeneration !== 'string' || !body.expectedSessionGeneration.trim()))
|
|
783
|
+
throw new FleetError('invalid_request', 'expectedSessionGeneration must be a nonempty string');
|
|
784
|
+
let text = String(body?.text ?? '');
|
|
785
|
+
if (body?.attachments !== undefined) {
|
|
786
|
+
if (typeof body.commandId !== 'string' || !body.commandId.trim())
|
|
787
|
+
throw new FleetError('invalid_request', 'commandId is required for attachments');
|
|
788
|
+
if (typeof body.expectedSessionGeneration !== 'string' || !body.expectedSessionGeneration.trim())
|
|
789
|
+
throw new FleetError('invalid_request', 'expectedSessionGeneration is required for attachments');
|
|
790
|
+
const role = await services.repository.get(request.params.id);
|
|
791
|
+
if (!role)
|
|
792
|
+
throw new FleetError('role_not_found', 'Agent not found');
|
|
793
|
+
const stateDir = services.repository.stateDir(role);
|
|
794
|
+
if (!stateDir)
|
|
795
|
+
throw new FleetError('capability_unavailable', 'Agent file storage is unavailable');
|
|
796
|
+
const originalText = text;
|
|
797
|
+
text = attachmentPrompt(stateDir, body.expectedSessionGeneration, body.attachments, text);
|
|
798
|
+
prepareAttachmentPresentation(stateDir, body.expectedSessionGeneration, body.commandId, body.attachments, originalText, text);
|
|
799
|
+
}
|
|
360
800
|
const commandId = typeof body?.commandId === 'string' && body.commandId.trim()
|
|
361
801
|
? body.commandId : undefined;
|
|
362
802
|
const control = await services.session(request.params.id);
|
|
@@ -367,6 +807,7 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
367
807
|
const admittedCommandId = commandId ?? randomBytes(16).toString('hex');
|
|
368
808
|
const receipt = await control.submitPromptV2({
|
|
369
809
|
commandId: admittedCommandId, text, source: 'owner_admin_console',
|
|
810
|
+
expectedSessionGeneration: body.expectedSessionGeneration,
|
|
370
811
|
actorBrowserSession: createHmac('sha256', digestKey).update(session.id).digest('hex').slice(0, 24),
|
|
371
812
|
});
|
|
372
813
|
await audit.record({
|
|
@@ -378,6 +819,8 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
378
819
|
reply.code(202);
|
|
379
820
|
return receipt;
|
|
380
821
|
}
|
|
822
|
+
if (body.expectedSessionGeneration !== undefined)
|
|
823
|
+
throw new FleetError('capability_unavailable', 'session-bound voice input is unavailable for this role');
|
|
381
824
|
const receipt = await control.sendText(text);
|
|
382
825
|
await audit.record({
|
|
383
826
|
requestId: request.id, browser: session.id, roleId: request.params.id,
|
|
@@ -443,10 +886,22 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
443
886
|
const body = request.body;
|
|
444
887
|
if (!body.action || !['start', 'stop', 'restart_resume', 'restart_fresh'].includes(body.action))
|
|
445
888
|
throw new FleetError('invalid_request', 'invalid lifecycle action');
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
889
|
+
let receipt;
|
|
890
|
+
try {
|
|
891
|
+
receipt = await services.commands.execute({
|
|
892
|
+
roleId: request.params.id, action: body.action,
|
|
893
|
+
actionId: body.actionId, confirmation: body.confirmation,
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
catch (error) {
|
|
897
|
+
// An existing receipt always wins: a conflicting/repeated action is not a
|
|
898
|
+
// proof that the original operation was never admitted.
|
|
899
|
+
if (typeof body.actionId === 'string' && !services.commands.get(body.actionId)) {
|
|
900
|
+
const normalized = normalizeError(error);
|
|
901
|
+
return reply.code(409).send({ error: { code: 'action_not_accepted', message: normalized.message }, accepted: false });
|
|
902
|
+
}
|
|
903
|
+
throw error;
|
|
904
|
+
}
|
|
450
905
|
events.publish('action.changed', receipt, request.params.id);
|
|
451
906
|
reply.code(202);
|
|
452
907
|
return receipt;
|
|
@@ -534,6 +989,9 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
534
989
|
}
|
|
535
990
|
socket.send(JSON.stringify(payload));
|
|
536
991
|
};
|
|
992
|
+
const role = await services.repository.get(request.params.id);
|
|
993
|
+
const stateDir = role ? services.repository.stateDir(role) : undefined;
|
|
994
|
+
const present = (event) => presentAttachmentEvent(stateDir, request.params.id, event);
|
|
537
995
|
const follow = await control.followConversation({
|
|
538
996
|
after,
|
|
539
997
|
onPage: page => {
|
|
@@ -544,11 +1002,11 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
544
1002
|
lastCursor: page.nextCursor ?? after,
|
|
545
1003
|
});
|
|
546
1004
|
if (page.events.length)
|
|
547
|
-
guardedSend({ type: 'events', events: page.events });
|
|
1005
|
+
guardedSend({ type: 'events', events: page.events.map(present) });
|
|
548
1006
|
if (page.hasMore)
|
|
549
1007
|
guardedSend({ type: 'resync.required' });
|
|
550
1008
|
},
|
|
551
|
-
onEvent: event => guardedSend({ type: 'events', events: [event] }),
|
|
1009
|
+
onEvent: event => guardedSend({ type: 'events', events: [present(event)] }),
|
|
552
1010
|
onClose: reason => {
|
|
553
1011
|
if (socket.readyState === socket.OPEN)
|
|
554
1012
|
socket.close(4409, (reason ?? 'conversation stream ended').slice(0, 120));
|
|
@@ -561,15 +1019,8 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
561
1019
|
socket.on('close', () => { clearInterval(heartbeat); follow.close(); });
|
|
562
1020
|
});
|
|
563
1021
|
});
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
await app.register(fastifyStatic, { root: staticRoot, prefix: '/' });
|
|
567
|
-
app.setNotFoundHandler((request, reply) => {
|
|
568
|
-
if (request.url.startsWith('/api/'))
|
|
569
|
-
return reply.code(404).send({ error: 'not found' });
|
|
570
|
-
return reply.sendFile('index.html');
|
|
571
|
-
});
|
|
572
|
-
}
|
|
1022
|
+
// Fleet has no pages of its own; every screen belongs to the App.
|
|
1023
|
+
app.setNotFoundHandler((_request, reply) => reply.code(404).send({ error: 'not found' }));
|
|
573
1024
|
return {
|
|
574
1025
|
app, auth, audit, events,
|
|
575
1026
|
async close() {
|
|
@@ -582,17 +1033,17 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
582
1033
|
function setAuthCookies(reply, session, device, secure = false) {
|
|
583
1034
|
const suffix = secure ? '; Secure' : '';
|
|
584
1035
|
reply.header('Set-Cookie', [
|
|
585
|
-
`ofs_session=${encodeURIComponent(session)}; HttpOnly; SameSite=Strict; Path
|
|
586
|
-
`ofs_device=${encodeURIComponent(device)}; HttpOnly; SameSite=Strict; Path
|
|
1036
|
+
`ofs_session=${encodeURIComponent(session)}; HttpOnly; SameSite=Strict; Path=/; Max-Age=28800${suffix}`,
|
|
1037
|
+
`ofs_device=${encodeURIComponent(device)}; HttpOnly; SameSite=Strict; Path=/; Max-Age=2592000${suffix}`,
|
|
587
1038
|
]);
|
|
588
1039
|
}
|
|
589
1040
|
function setSessionCookie(reply, session, secure = false) {
|
|
590
|
-
reply.header('Set-Cookie', `ofs_session=${encodeURIComponent(session)}; HttpOnly; SameSite=Strict; Path
|
|
1041
|
+
reply.header('Set-Cookie', `ofs_session=${encodeURIComponent(session)}; HttpOnly; SameSite=Strict; Path=/; Max-Age=28800${secure ? '; Secure' : ''}`);
|
|
591
1042
|
}
|
|
592
1043
|
function clearAuthCookies(reply) {
|
|
593
1044
|
reply.header('Set-Cookie', [
|
|
594
|
-
'ofs_session=; HttpOnly; SameSite=Strict; Path
|
|
595
|
-
'ofs_device=; HttpOnly; SameSite=Strict; Path
|
|
1045
|
+
'ofs_session=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0',
|
|
1046
|
+
'ofs_device=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0',
|
|
596
1047
|
]);
|
|
597
1048
|
}
|
|
598
1049
|
function promoteRequest(body) {
|
|
@@ -606,9 +1057,6 @@ function promoteRequest(body) {
|
|
|
606
1057
|
};
|
|
607
1058
|
}
|
|
608
1059
|
function cryptoRandomId() { return randomBytes(12).toString('hex'); }
|
|
609
|
-
function escapeHtml(value) {
|
|
610
|
-
return value.replace(/[&<>"']/g, char => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char]));
|
|
611
|
-
}
|
|
612
1060
|
function requireSubprotocol(request, expected) {
|
|
613
1061
|
const protocols = String(request.headers['sec-websocket-protocol'] ?? '')
|
|
614
1062
|
.split(',').map(value => value.trim());
|