@ours.network/fleet 1.2.0-nightly.2 → 1.2.0-nightly.21
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 +152 -29
- package/dist/agent-ours/bridge.d.ts +3 -0
- package/dist/agent-ours/bridge.js +178 -0
- package/dist/agent-ours/controller.d.ts +14 -0
- package/dist/agent-ours/controller.js +60 -0
- package/dist/agent-ours/correspondence.d.ts +10 -0
- package/dist/agent-ours/correspondence.js +1 -0
- package/dist/agent-ours/file-rpc.d.ts +23 -0
- package/dist/agent-ours/file-rpc.js +123 -0
- package/dist/agent-ours/harness.d.ts +14 -0
- package/dist/agent-ours/harness.js +72 -0
- package/dist/agent-ours/mcp-endpoint.d.ts +15 -0
- package/dist/agent-ours/mcp-endpoint.js +111 -0
- package/dist/agent-ours/runtime.d.ts +59 -0
- package/dist/agent-ours/runtime.js +283 -0
- package/dist/agent-ours/service.d.ts +23 -0
- package/dist/agent-ours/service.js +372 -0
- package/dist/agent-ours/state.d.ts +45 -0
- package/dist/agent-ours/state.js +95 -0
- package/dist/agent-ours/wire.d.ts +14 -0
- package/dist/agent-ours/wire.js +55 -0
- package/dist/agent-recovery-gate.js +1 -1
- package/dist/application/conversation-history.d.ts +6 -0
- package/dist/application/conversation-history.js +65 -0
- package/dist/application/fleet-query-service.d.ts +1 -1
- package/dist/application/fleet-query-service.js +2 -2
- package/dist/application/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/room-layout-definitions.d.ts +45 -0
- package/dist/application/room-layout-definitions.js +130 -0
- package/dist/application/session-control.d.ts +8 -0
- package/dist/application/session-control.js +21 -3
- package/dist/application/supervisor-ours-tools.d.ts +159 -0
- package/dist/application/supervisor-ours-tools.js +113 -0
- package/dist/application/task-layouts.d.ts +89 -0
- package/dist/application/task-layouts.js +248 -0
- package/dist/application/task-room-service.d.ts +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/briefing.js +10 -59
- package/dist/build-info.json +5 -4
- package/dist/capabilities.d.ts +2 -1
- package/dist/capabilities.js +2 -0
- package/dist/cli.js +65 -2
- package/dist/client-profile.d.ts +5 -16
- package/dist/client-profile.js +4 -112
- package/dist/config.d.ts +4 -0
- package/dist/config.js +7 -0
- package/dist/creation.d.ts +1 -1
- package/dist/creation.js +5 -16
- 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 +43 -23
- 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 +12 -7
- package/dist/harness/agent-session.d.ts +2 -0
- package/dist/harness/claude-code-session.js +4 -2
- 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-session.js +9 -2
- package/dist/harness/codex.d.ts +3 -2
- package/dist/harness/codex.js +4 -3
- package/dist/harness/hermes-session.js +7 -1
- package/dist/init-wizard.js +20 -9
- package/dist/monitor.d.ts +1 -34
- package/dist/monitor.js +24 -151
- 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/preset-migration.js +15 -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.js +3 -1
- 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-cli.d.ts +2 -0
- package/dist/rooms-tasks/layout-cli.js +81 -0
- package/dist/rooms-tasks/layout-config.d.ts +11 -0
- package/dist/rooms-tasks/layout-config.js +89 -0
- package/dist/rooms-tasks/layout-control.d.ts +21 -0
- package/dist/rooms-tasks/layout-control.js +52 -0
- package/dist/rooms-tasks/layout-service.d.ts +39 -0
- package/dist/rooms-tasks/layout-service.js +173 -0
- package/dist/rooms-tasks/layout.d.ts +102 -0
- package/dist/rooms-tasks/layout.js +227 -0
- package/dist/rooms-tasks/live-readiness.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 +182 -48
- 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 +23 -3
- package/dist/runner.js +574 -494
- package/dist/session/acp.d.ts +3 -0
- package/dist/session/acp.js +7 -0
- package/dist/session/arbiter.d.ts +3 -0
- package/dist/session/arbiter.js +20 -2
- package/dist/session/codex-app-server-transport.js +1 -1
- package/dist/session/codex-app-server.d.ts +3 -0
- package/dist/session/codex-app-server.js +25 -1
- package/dist/session/control.d.ts +10 -1
- package/dist/session/control.js +36 -2
- package/dist/session/conversation-normalizer.d.ts +2 -1
- package/dist/session/conversation-normalizer.js +1 -1
- package/dist/session/conversation-types.d.ts +5 -3
- package/dist/session/task-notice.d.ts +14 -0
- package/dist/session/task-notice.js +13 -0
- package/dist/session/types.d.ts +3 -0
- package/dist/socket-path.d.ts +2 -0
- package/dist/socket-path.js +22 -0
- package/dist/spawn.js +19 -16
- package/dist/startup-readiness.d.ts +13 -0
- package/dist/startup-readiness.js +38 -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/temp-supervisor-recovery.d.ts +3 -0
- package/dist/temp-supervisor-recovery.js +46 -0
- package/dist/watchdog/briefing.js +3 -16
- package/dist/watchdog/run.js +19 -5
- package/dist/web/agent-attachments.d.ts +29 -0
- package/dist/web/agent-attachments.js +162 -0
- package/dist/web/auth.d.ts +4 -1
- package/dist/web/auth.js +23 -2
- package/dist/web/control.js +2 -1
- package/dist/web/fleet-config-service.d.ts +4 -1
- package/dist/web/fleet-config-service.js +46 -15
- package/dist/web/prefix-gateway.d.ts +20 -0
- package/dist/web/prefix-gateway.js +150 -0
- package/dist/web/runtime.d.ts +4 -0
- package/dist/web/runtime.js +19 -14
- package/dist/web/server.d.ts +7 -0
- package/dist/web/server.js +293 -25
- package/docs/room-layouts.md +92 -0
- package/docs/task-workspaces.md +130 -0
- package/examples/room-layouts/fresh.yaml +27 -0
- package/examples/room-layouts/shared.yaml +19 -0
- package/package.json +17 -13
- package/presets/README.md +78 -0
- package/presets/brain-catalog.json +8 -2
- package/presets/fleet/agent_templates/Engineer.yaml +5 -0
- 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 +22 -0
- 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,3 +1,7 @@
|
|
|
1
|
+
import { storeAgentAttachment, attachmentPrompt, MAX_ATTACHMENT_BODY, readAgentAttachment, prepareAttachmentPresentation, presentAttachmentEvent } from './agent-attachments.js';
|
|
2
|
+
import { readChatIdle } from '../temp-idle.js';
|
|
3
|
+
import { agentDir } from '../paths.js';
|
|
4
|
+
import { SupervisorOursTools } from '../application/supervisor-ours-tools.js';
|
|
1
5
|
import { existsSync } from 'node:fs';
|
|
2
6
|
import { dirname, join } from 'node:path';
|
|
3
7
|
import { fileURLToPath } from 'node:url';
|
|
@@ -33,15 +37,15 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
33
37
|
app.addHook('onRequest', async (request, reply) => {
|
|
34
38
|
reply.header('X-Content-Type-Options', 'nosniff');
|
|
35
39
|
reply.header('Referrer-Policy', 'no-referrer');
|
|
36
|
-
reply.header('Permissions-Policy', 'camera=(), microphone=(), geolocation=(), payment=()');
|
|
40
|
+
reply.header('Permissions-Policy', 'camera=(), microphone=(self), geolocation=(), payment=()');
|
|
37
41
|
reply.header('X-Frame-Options', 'DENY');
|
|
38
|
-
reply.header('Content-Security-Policy', `default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; ` +
|
|
42
|
+
reply.header('Content-Security-Policy', `default-src 'self'; script-src 'self'; style-src 'self'; style-src-attr 'unsafe-inline'; img-src 'self' data: blob:; media-src 'self' blob:; frame-src 'self' blob:; ` +
|
|
39
43
|
`connect-src 'self' ${auth.secureCookies ? 'wss' : 'ws'}://${auth.host}; object-src 'none'; base-uri 'none'; ` +
|
|
40
44
|
`frame-ancestors 'none'; form-action 'self'; manifest-src 'self'; worker-src 'self'`);
|
|
41
45
|
if (request.url.startsWith('/api/'))
|
|
42
46
|
reply.header('Cache-Control', 'no-store');
|
|
43
47
|
try {
|
|
44
|
-
auth.validateBoundary(request, false);
|
|
48
|
+
auth.validateBoundary(request, false, !request.url.startsWith('/api/'));
|
|
45
49
|
}
|
|
46
50
|
catch (error) {
|
|
47
51
|
if (request.url.startsWith('/api/'))
|
|
@@ -51,7 +55,11 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
51
55
|
}
|
|
52
56
|
});
|
|
53
57
|
app.setErrorHandler(async (error, request, reply) => {
|
|
54
|
-
const
|
|
58
|
+
const privateToolParseError = request.routeOptions.url === '/api/v1/roles/:id/ours/call'
|
|
59
|
+
&& error instanceof Error && 'code' in error
|
|
60
|
+
&& typeof error.code === 'string' && error.code.startsWith('FST_ERR_CTP_');
|
|
61
|
+
const fleetError = normalizeError(privateToolParseError
|
|
62
|
+
? new FleetError('invalid_request', 'expected a valid JSON tool request') : error, request.id);
|
|
55
63
|
await audit.record({
|
|
56
64
|
requestId: request.id, action: `${request.method} ${request.routeOptions.url ?? request.url}`,
|
|
57
65
|
result: 'rejected', errorCode: fleetError.code,
|
|
@@ -128,7 +136,8 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
128
136
|
});
|
|
129
137
|
app.get('/api/v1/roles', async (request) => {
|
|
130
138
|
auth.authenticate(request);
|
|
131
|
-
|
|
139
|
+
const query = request.query;
|
|
140
|
+
return { roles: await services.query.list(query.includeTemporary === 'true') };
|
|
132
141
|
});
|
|
133
142
|
const taskApi = async (fn) => {
|
|
134
143
|
try {
|
|
@@ -141,6 +150,8 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
141
150
|
? 'conflict' : 'invalid_request';
|
|
142
151
|
throw new FleetError(code, error.message);
|
|
143
152
|
}
|
|
153
|
+
if (error instanceof TaskStateError && 'accepted' in error && error.accepted === false)
|
|
154
|
+
return Promise.reject(Object.assign(new FleetError('invalid_request', error.message), { memberNotAccepted: true }));
|
|
144
155
|
if (error instanceof TaskStateError) {
|
|
145
156
|
throw new FleetError(error.message.startsWith('task not found:') ? 'resource_not_found' : 'conflict', error.message);
|
|
146
157
|
}
|
|
@@ -188,9 +199,10 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
188
199
|
const state = query.state && query.state !== 'all' ? query.state : undefined;
|
|
189
200
|
const filter = { ...(state ? { state } : {}), ...(query.list ? { list: query.list } : {}),
|
|
190
201
|
...(query.includeDeleting === 'true' ? { includeDeleting: true } : {}) };
|
|
202
|
+
const api = requireTaskRooms();
|
|
191
203
|
return taskApi(() => query.groupByList === 'true'
|
|
192
|
-
? { groups:
|
|
193
|
-
: { tasks:
|
|
204
|
+
? { groups: api.groupedTasks(filter).map(group => ({ ...group, tasks: group.tasks.map(task => api.withLayoutRooms(task)) })) }
|
|
205
|
+
: { tasks: api.listTasks(filter).map(task => api.withLayoutRooms(task)) });
|
|
194
206
|
});
|
|
195
207
|
app.delete('/api/v1/tasks/:id', async (request, reply) => {
|
|
196
208
|
auth.authenticate(request, true);
|
|
@@ -221,12 +233,17 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
221
233
|
const body = request.body;
|
|
222
234
|
if (typeof body?.title !== 'string' || !body.title)
|
|
223
235
|
throw new FleetError('invalid_request', 'title is required');
|
|
236
|
+
// Every task runs a saved Room Layout; rooms are never defined inline or from room templates here.
|
|
237
|
+
if (typeof body.layout !== 'string' || !body.layout)
|
|
238
|
+
throw new FleetError('invalid_request', 'layout is required: choose a room layout from Settings › Room layouts');
|
|
239
|
+
if (body.template !== undefined || body.noRoom !== undefined)
|
|
240
|
+
throw new FleetError('invalid_request', 'tasks use room layouts; template and noRoom are not accepted');
|
|
224
241
|
const task = await taskApi(() => requireTaskRooms().createTask({
|
|
225
242
|
actor: { kind: 'local_control', surface: 'web' }, title: body.title,
|
|
226
243
|
brief: typeof body.brief === 'string' ? body.brief : undefined,
|
|
227
|
-
|
|
228
|
-
backlog: body.backlog === true, noRoom: body.noRoom === true,
|
|
244
|
+
backlog: body.backlog === true,
|
|
229
245
|
list: typeof body.list === 'string' ? body.list : undefined,
|
|
246
|
+
layout: body.layout,
|
|
230
247
|
idempotencyKey: typeof request.headers['idempotency-key'] === 'string'
|
|
231
248
|
? request.headers['idempotency-key'] : undefined,
|
|
232
249
|
origin: { type: 'web' },
|
|
@@ -240,11 +257,162 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
240
257
|
const list = String(request.body?.list ?? '');
|
|
241
258
|
return { task: await taskApi(() => requireTaskRooms().moveTask({ actor: { kind: 'local_control', surface: 'web' }, taskId, list })) };
|
|
242
259
|
});
|
|
260
|
+
app.patch('/api/v1/tasks/:id/description', async (request) => {
|
|
261
|
+
auth.authenticate(request, true);
|
|
262
|
+
const body = request.body;
|
|
263
|
+
if (typeof body?.brief !== 'string' || typeof body.expectedBrief !== 'string' || body.brief.length > 100_000)
|
|
264
|
+
throw new FleetError('invalid_request', 'brief and expectedBrief must be strings; brief maximum is 100000 characters');
|
|
265
|
+
return { task: await taskApi(() => requireTaskRooms().editTaskDescription({
|
|
266
|
+
actor: { kind: 'local_control', surface: 'web' }, taskId: request.params.id,
|
|
267
|
+
brief: body.brief, expectedBrief: body.expectedBrief,
|
|
268
|
+
})) };
|
|
269
|
+
});
|
|
270
|
+
app.get('/api/v1/tasks/:id', async (request) => {
|
|
271
|
+
auth.authenticate(request);
|
|
272
|
+
return taskApi(() => requireTaskRooms().getTask(request.params.id));
|
|
273
|
+
});
|
|
274
|
+
app.post('/api/v1/tasks/:id/members', async (request, reply) => {
|
|
275
|
+
auth.authenticate(request, true);
|
|
276
|
+
const body = request.body;
|
|
277
|
+
try {
|
|
278
|
+
const result = await taskApi(() => requireTaskRooms().addMember(request.params.id, body));
|
|
279
|
+
reply.code(result.state === 'running' ? 202 : 200);
|
|
280
|
+
return result;
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
if (error instanceof Error && 'memberNotAccepted' in error)
|
|
284
|
+
return reply.code(409).send({ error: { code: 'member_not_accepted', message: error.message }, accepted: false });
|
|
285
|
+
throw error;
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
app.get('/api/v1/tasks/:id/member-additions/:requestId', async (request) => {
|
|
289
|
+
auth.authenticate(request);
|
|
290
|
+
const { id, requestId } = request.params;
|
|
291
|
+
return taskApi(() => requireTaskRooms().memberAddition(id, requestId));
|
|
292
|
+
});
|
|
293
|
+
app.patch('/api/v1/tasks/:id/layout', async (request) => {
|
|
294
|
+
const session = auth.authenticate(request, true);
|
|
295
|
+
const body = request.body;
|
|
296
|
+
const name = (v) => typeof v === 'string' && v.length > 0;
|
|
297
|
+
if (!name(body?.layout) || !(body.expectedLayout === null || name(body.expectedLayout)))
|
|
298
|
+
throw new FleetError('invalid_request', 'layout must be a layout name; expectedLayout a layout name or null');
|
|
299
|
+
const task = await taskApi(() => requireTaskRooms().setTaskLayout({ actor: { kind: 'local_control', surface: 'web' },
|
|
300
|
+
taskId: request.params.id, layout: body.layout, expectedLayout: body.expectedLayout }));
|
|
301
|
+
await audit.record({ requestId: request.id, browser: session.id, action: 'task.layout.set', result: 'succeeded' });
|
|
302
|
+
return { task: requireTaskRooms().withLayoutRooms(task) };
|
|
303
|
+
});
|
|
304
|
+
app.get('/api/v1/tasks/:id/layout', async (request) => {
|
|
305
|
+
auth.authenticate(request);
|
|
306
|
+
return { layout: await taskApi(async () => requireTaskRooms().taskLayout(request.params.id)) };
|
|
307
|
+
});
|
|
308
|
+
for (const operation of ['open', 'close']) {
|
|
309
|
+
app.post(`/api/v1/tasks/:id/layout/rooms/:room/${operation}`, async (request, reply) => {
|
|
310
|
+
const session = auth.authenticate(request, true);
|
|
311
|
+
const { id, room } = request.params;
|
|
312
|
+
const accepted = await taskApi(() => requireTaskRooms().launchTaskLayoutOperation({
|
|
313
|
+
actor: { kind: 'local_control', surface: 'web' }, taskId: id, room,
|
|
314
|
+
operation: operation === 'open' ? 'open' : 'close-room',
|
|
315
|
+
}));
|
|
316
|
+
await audit.record({ requestId: request.id, browser: session.id, action: `task.layout.${operation}`, result: 'succeeded' });
|
|
317
|
+
reply.code(202);
|
|
318
|
+
return { operation: accepted };
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
app.post('/api/v1/tasks/:id/layout/cleanup', async (request, reply) => {
|
|
322
|
+
auth.authenticate(request, true);
|
|
323
|
+
const accepted = await taskApi(() => requireTaskRooms().retryTaskLayoutCleanup({
|
|
324
|
+
actor: { kind: 'local_control', surface: 'web' }, taskId: request.params.id,
|
|
325
|
+
}));
|
|
326
|
+
reply.code(202);
|
|
327
|
+
return { operation: accepted };
|
|
328
|
+
});
|
|
329
|
+
app.get('/api/v1/task-templates', async (request) => {
|
|
330
|
+
auth.authenticate(request);
|
|
331
|
+
return { templates: requireTaskRooms().listTemplates() };
|
|
332
|
+
});
|
|
333
|
+
const layouts = () => {
|
|
334
|
+
if (!services.roomLayouts)
|
|
335
|
+
throw new FleetError('capability_unavailable', 'room layout editing is unavailable');
|
|
336
|
+
return services.roomLayouts;
|
|
337
|
+
};
|
|
338
|
+
app.get('/api/v1/room-layouts', async (request) => {
|
|
339
|
+
auth.authenticate(request);
|
|
340
|
+
const list = layouts().list(), defaultLayout = layouts().defaultLayout(list);
|
|
341
|
+
return { layouts: list, ...(defaultLayout ? { default_layout: defaultLayout } : {}) };
|
|
342
|
+
});
|
|
343
|
+
app.post('/api/v1/room-layouts/validate', async (request) => {
|
|
344
|
+
auth.authenticate(request, true);
|
|
345
|
+
return layouts().validate(request.body?.definition);
|
|
346
|
+
});
|
|
347
|
+
app.put('/api/v1/room-layouts/:name', async (request) => {
|
|
348
|
+
const session = auth.authenticate(request, true);
|
|
349
|
+
const body = request.body;
|
|
350
|
+
if (typeof body?.revision !== 'string')
|
|
351
|
+
throw new FleetError('invalid_request', 'revision is required');
|
|
352
|
+
const result = await layouts().save(request.params.name, body.revision, body.definition);
|
|
353
|
+
events.publish('room_layouts.changed', { name: result.name, revision: result.revision });
|
|
354
|
+
await audit.record({ requestId: request.id, browser: session.id, action: 'room_layout.save', result: 'succeeded' });
|
|
355
|
+
return result;
|
|
356
|
+
});
|
|
357
|
+
app.delete('/api/v1/room-layouts/:name', async (request) => {
|
|
358
|
+
const session = auth.authenticate(request, true);
|
|
359
|
+
const revision = request.query.revision;
|
|
360
|
+
if (typeof revision !== 'string' || !revision)
|
|
361
|
+
throw new FleetError('invalid_request', 'revision is required');
|
|
362
|
+
const result = await layouts().remove(request.params.name, revision);
|
|
363
|
+
events.publish('room_layouts.changed', { name: result.name });
|
|
364
|
+
await audit.record({ requestId: request.id, browser: session.id, action: 'room_layout.delete', result: 'succeeded' });
|
|
365
|
+
return result;
|
|
366
|
+
});
|
|
367
|
+
for (const action of ['start', 'block', 'unblock', 'review', 'finish', 'cancel']) {
|
|
368
|
+
app.post(`/api/v1/tasks/:id/${action}`, async (request, reply) => {
|
|
369
|
+
auth.authenticate(request, true);
|
|
370
|
+
const taskId = request.params.id;
|
|
371
|
+
const body = (request.body ?? {});
|
|
372
|
+
const input = { actor: { kind: 'local_control', surface: 'web' }, taskId };
|
|
373
|
+
const api = requireTaskRooms();
|
|
374
|
+
return taskApi(async () => {
|
|
375
|
+
switch (action) {
|
|
376
|
+
case 'start': {
|
|
377
|
+
// A task's layout can be chosen or changed but never removed, so this check cannot go stale.
|
|
378
|
+
if (!api.getTask(taskId).task.layout)
|
|
379
|
+
throw new FleetError('conflict', 'choose a room layout for this task before starting it');
|
|
380
|
+
const task = await api.startTask(input);
|
|
381
|
+
const provisioning = await api.launchTaskProvisioning(taskId);
|
|
382
|
+
if (provisioning.kind !== 'ready')
|
|
383
|
+
reply.code(202);
|
|
384
|
+
return { task, provisioning };
|
|
385
|
+
}
|
|
386
|
+
case 'block':
|
|
387
|
+
if (typeof body.reason !== 'string' || !body.reason.trim())
|
|
388
|
+
throw new FleetError('invalid_request', 'reason is required');
|
|
389
|
+
return { task: api.blockTask({ ...input, reason: body.reason }) };
|
|
390
|
+
case 'unblock': return { task: api.unblockTask(input) };
|
|
391
|
+
case 'review': return { task: api.reviewTask(input) };
|
|
392
|
+
case 'finish':
|
|
393
|
+
case 'cancel': {
|
|
394
|
+
const plan = action === 'finish' ? await api.finishTask(input) : await api.cancelTask(input);
|
|
395
|
+
const task = plan.settlementRequired ? await api.launchTaskSettlement(taskId) : plan.task;
|
|
396
|
+
const pending = task.terminal_intent?.status === 'pending';
|
|
397
|
+
if (pending)
|
|
398
|
+
reply.code(202);
|
|
399
|
+
return { task, pending };
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
app.get('/api/v1/configuration/provenance', async (request) => {
|
|
406
|
+
auth.authenticate(request);
|
|
407
|
+
if (!services.presetProvenance)
|
|
408
|
+
throw new FleetError('capability_unavailable', 'preset provenance is unavailable');
|
|
409
|
+
return { provenance: services.presetProvenance.read() };
|
|
410
|
+
});
|
|
243
411
|
app.get('/api/v1/configuration', async (request) => {
|
|
244
412
|
auth.authenticate(request);
|
|
245
413
|
if (!services.configuration)
|
|
246
414
|
throw new FleetError('capability_unavailable', 'fleet configuration editing is unavailable');
|
|
247
|
-
return services.configuration.read();
|
|
415
|
+
return services.configuration.read(request.query.includeDefinitions === 'true');
|
|
248
416
|
});
|
|
249
417
|
app.post('/api/v1/configuration/preview', async (request) => {
|
|
250
418
|
auth.authenticate(request, true);
|
|
@@ -311,9 +479,36 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
311
479
|
});
|
|
312
480
|
return result;
|
|
313
481
|
});
|
|
482
|
+
const oursTools = services.oursTools ?? new SupervisorOursTools();
|
|
483
|
+
app.get('/api/v1/roles/:id/contacts', async (request) => {
|
|
484
|
+
auth.authenticate(request);
|
|
485
|
+
const control = await services.session(request.params.id);
|
|
486
|
+
if (!control.agentContacts)
|
|
487
|
+
throw new FleetError('capability_unavailable', 'Supervisor contacts API is unavailable');
|
|
488
|
+
return control.agentContacts();
|
|
489
|
+
});
|
|
490
|
+
app.get('/api/v1/roles/:id/messages', async (request) => {
|
|
491
|
+
auth.authenticate(request);
|
|
492
|
+
const control = await services.session(request.params.id);
|
|
493
|
+
if (!control.agentHistory)
|
|
494
|
+
throw new FleetError('capability_unavailable', 'Supervisor history API is unavailable');
|
|
495
|
+
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) } : {}) });
|
|
496
|
+
});
|
|
497
|
+
app.get('/api/v1/roles/:id/ours/tools', async (request) => {
|
|
498
|
+
auth.authenticate(request);
|
|
499
|
+
return oursTools.list(request.params.id);
|
|
500
|
+
});
|
|
501
|
+
app.post('/api/v1/roles/:id/ours/call', async (request) => {
|
|
502
|
+
const session = auth.authenticate(request, true);
|
|
503
|
+
const result = await oursTools.call(request.params.id, request.body);
|
|
504
|
+
await audit.record({ requestId: request.id, browser: session.id,
|
|
505
|
+
action: 'ours.call', result: result.result.isError === true ? 'tool_error' : 'succeeded' });
|
|
506
|
+
return result;
|
|
507
|
+
});
|
|
314
508
|
app.get('/api/v1/roles/:id', async (request) => {
|
|
315
509
|
auth.authenticate(request);
|
|
316
|
-
|
|
510
|
+
const detail = await services.query.detail(request.params.id);
|
|
511
|
+
return { ...detail, chatIdle: detail.role.lifetime === 'temporary' ? readChatIdle(agentDir(request.params.id, true)) ?? null : null };
|
|
317
512
|
});
|
|
318
513
|
app.get('/api/v1/roles/:id/removal-preview', async (request) => {
|
|
319
514
|
auth.authenticate(request);
|
|
@@ -348,15 +543,70 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
348
543
|
const control = await services.session(request.params.id);
|
|
349
544
|
if (!control.conversationPage)
|
|
350
545
|
throw new FleetError('capability_unavailable', 'this role has no conversation ledger');
|
|
351
|
-
|
|
546
|
+
const page = await control.conversationPage({
|
|
352
547
|
after: request.query.after,
|
|
353
548
|
limit: request.query.limit ? Number(request.query.limit) : undefined,
|
|
354
549
|
});
|
|
550
|
+
const role = await services.repository.get(request.params.id);
|
|
551
|
+
const stateDir = role ? services.repository.stateDir(role) : undefined;
|
|
552
|
+
return { ...page, events: page.events.map(event => presentAttachmentEvent(stateDir, request.params.id, event)) };
|
|
553
|
+
});
|
|
554
|
+
app.get('/api/v1/roles/:id/attachments/:attachmentId', async (request, reply) => {
|
|
555
|
+
auth.authenticate(request);
|
|
556
|
+
const role = await services.repository.get(request.params.id);
|
|
557
|
+
if (!role)
|
|
558
|
+
throw new FleetError('role_not_found', 'Agent not found');
|
|
559
|
+
const stateDir = services.repository.stateDir(role);
|
|
560
|
+
if (!stateDir)
|
|
561
|
+
throw new FleetError('capability_unavailable', 'Agent file storage is unavailable');
|
|
562
|
+
const { record, bytes } = readAgentAttachment(stateDir, request.params.attachmentId);
|
|
563
|
+
const image = /^image\/(png|jpeg|gif|webp|avif)$/.test(record.mimeType);
|
|
564
|
+
reply.header('Cache-Control', 'private, no-store');
|
|
565
|
+
reply.header('X-Content-Type-Options', 'nosniff');
|
|
566
|
+
reply.header('Content-Security-Policy', "default-src 'none'; sandbox");
|
|
567
|
+
reply.header('Content-Disposition', `${image ? 'inline' : 'attachment'}; filename*=UTF-8''${encodeURIComponent(record.name).replace(/'/g, '%27')}`);
|
|
568
|
+
return reply.type(image ? record.mimeType : 'application/octet-stream').send(bytes);
|
|
569
|
+
});
|
|
570
|
+
app.post('/api/v1/roles/:id/attachments', { bodyLimit: MAX_ATTACHMENT_BODY }, async (request) => {
|
|
571
|
+
auth.authenticate(request, true);
|
|
572
|
+
const role = await services.repository.get(request.params.id);
|
|
573
|
+
if (!role)
|
|
574
|
+
throw new FleetError('role_not_found', 'Agent not found');
|
|
575
|
+
const stateDir = services.repository.stateDir(role);
|
|
576
|
+
if (!stateDir)
|
|
577
|
+
throw new FleetError('capability_unavailable', 'Agent file storage is unavailable');
|
|
578
|
+
const body = request.body;
|
|
579
|
+
if (typeof body?.expectedSessionGeneration !== 'string' || !body.expectedSessionGeneration.trim())
|
|
580
|
+
throw new FleetError('invalid_request', 'expectedSessionGeneration is required for attachments');
|
|
581
|
+
const control = await services.session(request.params.id);
|
|
582
|
+
if (!control.conversationPage || !control.submitPromptV2)
|
|
583
|
+
throw new FleetError('capability_unavailable', 'Session-bound attachments are unavailable');
|
|
584
|
+
const page = await control.conversationPage({ limit: 1 });
|
|
585
|
+
if (page.snapshot.sessionGeneration !== body.expectedSessionGeneration)
|
|
586
|
+
throw new FleetError('stale_state', 'Agent session changed. Select the files again.');
|
|
587
|
+
return storeAgentAttachment(stateDir, body.expectedSessionGeneration, request.body);
|
|
355
588
|
});
|
|
356
589
|
app.post('/api/v1/roles/:id/input', async (request, reply) => {
|
|
357
590
|
const session = auth.authenticate(request, true);
|
|
358
591
|
const body = request.body;
|
|
359
|
-
|
|
592
|
+
if (body?.expectedSessionGeneration !== undefined && (typeof body.expectedSessionGeneration !== 'string' || !body.expectedSessionGeneration.trim()))
|
|
593
|
+
throw new FleetError('invalid_request', 'expectedSessionGeneration must be a nonempty string');
|
|
594
|
+
let text = String(body?.text ?? '');
|
|
595
|
+
if (body?.attachments !== undefined) {
|
|
596
|
+
if (typeof body.commandId !== 'string' || !body.commandId.trim())
|
|
597
|
+
throw new FleetError('invalid_request', 'commandId is required for attachments');
|
|
598
|
+
if (typeof body.expectedSessionGeneration !== 'string' || !body.expectedSessionGeneration.trim())
|
|
599
|
+
throw new FleetError('invalid_request', 'expectedSessionGeneration is required for attachments');
|
|
600
|
+
const role = await services.repository.get(request.params.id);
|
|
601
|
+
if (!role)
|
|
602
|
+
throw new FleetError('role_not_found', 'Agent not found');
|
|
603
|
+
const stateDir = services.repository.stateDir(role);
|
|
604
|
+
if (!stateDir)
|
|
605
|
+
throw new FleetError('capability_unavailable', 'Agent file storage is unavailable');
|
|
606
|
+
const originalText = text;
|
|
607
|
+
text = attachmentPrompt(stateDir, body.expectedSessionGeneration, body.attachments, text);
|
|
608
|
+
prepareAttachmentPresentation(stateDir, body.expectedSessionGeneration, body.commandId, body.attachments, originalText, text);
|
|
609
|
+
}
|
|
360
610
|
const commandId = typeof body?.commandId === 'string' && body.commandId.trim()
|
|
361
611
|
? body.commandId : undefined;
|
|
362
612
|
const control = await services.session(request.params.id);
|
|
@@ -367,6 +617,7 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
367
617
|
const admittedCommandId = commandId ?? randomBytes(16).toString('hex');
|
|
368
618
|
const receipt = await control.submitPromptV2({
|
|
369
619
|
commandId: admittedCommandId, text, source: 'owner_admin_console',
|
|
620
|
+
expectedSessionGeneration: body.expectedSessionGeneration,
|
|
370
621
|
actorBrowserSession: createHmac('sha256', digestKey).update(session.id).digest('hex').slice(0, 24),
|
|
371
622
|
});
|
|
372
623
|
await audit.record({
|
|
@@ -378,6 +629,8 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
378
629
|
reply.code(202);
|
|
379
630
|
return receipt;
|
|
380
631
|
}
|
|
632
|
+
if (body.expectedSessionGeneration !== undefined)
|
|
633
|
+
throw new FleetError('capability_unavailable', 'session-bound voice input is unavailable for this role');
|
|
381
634
|
const receipt = await control.sendText(text);
|
|
382
635
|
await audit.record({
|
|
383
636
|
requestId: request.id, browser: session.id, roleId: request.params.id,
|
|
@@ -443,10 +696,22 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
443
696
|
const body = request.body;
|
|
444
697
|
if (!body.action || !['start', 'stop', 'restart_resume', 'restart_fresh'].includes(body.action))
|
|
445
698
|
throw new FleetError('invalid_request', 'invalid lifecycle action');
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
699
|
+
let receipt;
|
|
700
|
+
try {
|
|
701
|
+
receipt = await services.commands.execute({
|
|
702
|
+
roleId: request.params.id, action: body.action,
|
|
703
|
+
actionId: body.actionId, confirmation: body.confirmation,
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
catch (error) {
|
|
707
|
+
// An existing receipt always wins: a conflicting/repeated action is not a
|
|
708
|
+
// proof that the original operation was never admitted.
|
|
709
|
+
if (typeof body.actionId === 'string' && !services.commands.get(body.actionId)) {
|
|
710
|
+
const normalized = normalizeError(error);
|
|
711
|
+
return reply.code(409).send({ error: { code: 'action_not_accepted', message: normalized.message }, accepted: false });
|
|
712
|
+
}
|
|
713
|
+
throw error;
|
|
714
|
+
}
|
|
450
715
|
events.publish('action.changed', receipt, request.params.id);
|
|
451
716
|
reply.code(202);
|
|
452
717
|
return receipt;
|
|
@@ -534,6 +799,9 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
534
799
|
}
|
|
535
800
|
socket.send(JSON.stringify(payload));
|
|
536
801
|
};
|
|
802
|
+
const role = await services.repository.get(request.params.id);
|
|
803
|
+
const stateDir = role ? services.repository.stateDir(role) : undefined;
|
|
804
|
+
const present = (event) => presentAttachmentEvent(stateDir, request.params.id, event);
|
|
537
805
|
const follow = await control.followConversation({
|
|
538
806
|
after,
|
|
539
807
|
onPage: page => {
|
|
@@ -544,11 +812,11 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
544
812
|
lastCursor: page.nextCursor ?? after,
|
|
545
813
|
});
|
|
546
814
|
if (page.events.length)
|
|
547
|
-
guardedSend({ type: 'events', events: page.events });
|
|
815
|
+
guardedSend({ type: 'events', events: page.events.map(present) });
|
|
548
816
|
if (page.hasMore)
|
|
549
817
|
guardedSend({ type: 'resync.required' });
|
|
550
818
|
},
|
|
551
|
-
onEvent: event => guardedSend({ type: 'events', events: [event] }),
|
|
819
|
+
onEvent: event => guardedSend({ type: 'events', events: [present(event)] }),
|
|
552
820
|
onClose: reason => {
|
|
553
821
|
if (socket.readyState === socket.OPEN)
|
|
554
822
|
socket.close(4409, (reason ?? 'conversation stream ended').slice(0, 120));
|
|
@@ -561,7 +829,7 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
561
829
|
socket.on('close', () => { clearInterval(heartbeat); follow.close(); });
|
|
562
830
|
});
|
|
563
831
|
});
|
|
564
|
-
const staticRoot = join(dirname(fileURLToPath(import.meta.url)), '..', 'web-app');
|
|
832
|
+
const staticRoot = options.staticRoot ?? join(dirname(fileURLToPath(import.meta.url)), '..', 'web-app');
|
|
565
833
|
if (existsSync(staticRoot)) {
|
|
566
834
|
await app.register(fastifyStatic, { root: staticRoot, prefix: '/' });
|
|
567
835
|
app.setNotFoundHandler((request, reply) => {
|
|
@@ -582,17 +850,17 @@ export async function buildWebServer(services, boundary, options = {}) {
|
|
|
582
850
|
function setAuthCookies(reply, session, device, secure = false) {
|
|
583
851
|
const suffix = secure ? '; Secure' : '';
|
|
584
852
|
reply.header('Set-Cookie', [
|
|
585
|
-
`ofs_session=${encodeURIComponent(session)}; HttpOnly; SameSite=Strict; Path
|
|
586
|
-
`ofs_device=${encodeURIComponent(device)}; HttpOnly; SameSite=Strict; Path
|
|
853
|
+
`ofs_session=${encodeURIComponent(session)}; HttpOnly; SameSite=Strict; Path=/; Max-Age=28800${suffix}`,
|
|
854
|
+
`ofs_device=${encodeURIComponent(device)}; HttpOnly; SameSite=Strict; Path=/; Max-Age=2592000${suffix}`,
|
|
587
855
|
]);
|
|
588
856
|
}
|
|
589
857
|
function setSessionCookie(reply, session, secure = false) {
|
|
590
|
-
reply.header('Set-Cookie', `ofs_session=${encodeURIComponent(session)}; HttpOnly; SameSite=Strict; Path
|
|
858
|
+
reply.header('Set-Cookie', `ofs_session=${encodeURIComponent(session)}; HttpOnly; SameSite=Strict; Path=/; Max-Age=28800${secure ? '; Secure' : ''}`);
|
|
591
859
|
}
|
|
592
860
|
function clearAuthCookies(reply) {
|
|
593
861
|
reply.header('Set-Cookie', [
|
|
594
|
-
'ofs_session=; HttpOnly; SameSite=Strict; Path
|
|
595
|
-
'ofs_device=; HttpOnly; SameSite=Strict; Path
|
|
862
|
+
'ofs_session=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0',
|
|
863
|
+
'ofs_device=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0',
|
|
596
864
|
]);
|
|
597
865
|
}
|
|
598
866
|
function promoteRequest(body) {
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# YAML Room Layouts
|
|
2
|
+
|
|
3
|
+
Room Layout describes room goals, participants and their shared or independent
|
|
4
|
+
sessions. Rooms and agents have independent lifecycles.
|
|
5
|
+
|
|
6
|
+
Place a layout at `fleet/room_layouts/<name>.yaml` beside the split configuration
|
|
7
|
+
for `fleet.yaml`. Agent Templates remain in `fleet/agent_templates/`.
|
|
8
|
+
|
|
9
|
+
```yaml
|
|
10
|
+
version: 1
|
|
11
|
+
participants:
|
|
12
|
+
product: {agent_template: Product}
|
|
13
|
+
developer: {agent_template: Developer}
|
|
14
|
+
qa: {agent_template: QA}
|
|
15
|
+
rooms:
|
|
16
|
+
discovery:
|
|
17
|
+
goal: Clarify the problem and acceptance criteria
|
|
18
|
+
members: [product, developer, qa]
|
|
19
|
+
design:
|
|
20
|
+
goal: Review implementation choices
|
|
21
|
+
members: [developer, qa]
|
|
22
|
+
amigos:
|
|
23
|
+
goal: Resolve a concrete acceptance question
|
|
24
|
+
members: [product, developer, qa]
|
|
25
|
+
roles: {product: product, developer: developer, qa: qa}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Repeating a participant key reuses its exact running instance and session.
|
|
29
|
+
Different keys using the same Agent Template create independent instances.
|
|
30
|
+
See the [shared](../examples/room-layouts/shared.yaml) and
|
|
31
|
+
[fresh](../examples/room-layouts/fresh.yaml) examples. Both choices can coexist.
|
|
32
|
+
Fresh agents receive their room context. Supply relevant evidence through ordinary
|
|
33
|
+
room messages or existing project documents when needed.
|
|
34
|
+
|
|
35
|
+
A participant without `agent_template` requires a binding. `layout instance`
|
|
36
|
+
returns a local exact reference (supervisor, agent, launch, CID and session).
|
|
37
|
+
Pass participant-to-reference YAML to `create --bindings`. A stopped, replaced
|
|
38
|
+
or remote instance is rejected.
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
ours-fleet layout validate work
|
|
42
|
+
ours-fleet layout create work --id example
|
|
43
|
+
ours-fleet layout open example design
|
|
44
|
+
ours-fleet layout open example discovery
|
|
45
|
+
ours-fleet layout open example amigos
|
|
46
|
+
ours-fleet layout close-room example amigos
|
|
47
|
+
ours-fleet layout status example
|
|
48
|
+
ours-fleet layout close example
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Any declared room may open first. `create` only saves the definition and resolved
|
|
52
|
+
Agent Templates; `open` lazily starts missing participants and admits all members
|
|
53
|
+
before sending their room context. The membership notification is informational. Existing room rules govern when
|
|
54
|
+
an agent contributes. Each room opens through an explicit command.
|
|
55
|
+
|
|
56
|
+
`close-room` archives a room and keeps agents available to other rooms. `close`
|
|
57
|
+
archives opened rooms and stops only temporary participants created by this layout
|
|
58
|
+
instance. Explicitly bound participants survive. Cleanup processes existing resources. A 3-Amigos room is an ordinary room with
|
|
59
|
+
complementary members.
|
|
60
|
+
|
|
61
|
+
Supported room fields: `goal`, `members`, optional `roles`, `contract`,
|
|
62
|
+
`quiet_membership`, `anonymous`. `contract` is briefing text.
|
|
63
|
+
Top-level fields: `version: 1`, optional `description`, `participants`, `rooms`.
|
|
64
|
+
Unknown fields, ambiguous YAML and invalid member references are rejected.
|
|
65
|
+
Existing templates and tasks keep their behavior.
|
|
66
|
+
|
|
67
|
+
Only local Fleet control is supported. The existing authenticated control socket,
|
|
68
|
+
agent runtime, `spawnTemp` and session queue perform operations. Private state
|
|
69
|
+
snapshots template definitions; public status hides their environment values.
|
|
70
|
+
Existing owner settings apply; owner CID/invite fingerprint are pinned. Unknown
|
|
71
|
+
mutation outcomes stop new work for inspection rather than blindly replaying a
|
|
72
|
+
spawn or room creation. `close` and `close-room` can still clean known resources.
|
|
73
|
+
Cleanup continues after individual errors and does not stop replacement or borrowed
|
|
74
|
+
instances. Confirmed stopped workers need no live control socket. An unknown
|
|
75
|
+
original outcome remains recorded, and cleanup reports incomplete until inspected;
|
|
76
|
+
it never claims that unknown resources were found or removed. Retrying cleanup
|
|
77
|
+
checks known resources and skips completed cleanup. Reopening an active layout room
|
|
78
|
+
checks its native state, CID and required seats and reports drift without repairing it.
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
Shared sessions retain available conversation context, not guaranteed perfect
|
|
82
|
+
recall or isolation between rooms. Compaction and model judgment still matter.
|
|
83
|
+
Rooms keep their existing turn scheduling.
|
|
84
|
+
|
|
85
|
+
Each layout instance creates distinct native room names, so several instances can
|
|
86
|
+
open the same declared room key. Participant roles must differ from the attached
|
|
87
|
+
owner role; a collision is rejected before creating resources.
|
|
88
|
+
|
|
89
|
+
Definitions are loaded for authoring commands. Retained instances use their saved
|
|
90
|
+
layout and Agent Templates. Status and cleanup operate on saved resources even
|
|
91
|
+
when the source YAML is broken or removed. New agent launches continue to use
|
|
92
|
+
Fleet's normal configuration and launch checks.
|