@ours.network/fleet 1.2.0-nightly.6 → 1.2.0-nightly.60
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 +287 -65
- package/dist/account-origin.d.ts +2 -0
- package/dist/account-origin.js +6 -0
- package/dist/agent-ours/bridge.js +35 -9
- 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 +12 -0
- package/dist/agent-ours/file-rpc.js +18 -4
- package/dist/agent-ours/harness.js +9 -1
- package/dist/agent-ours/mcp-endpoint.d.ts +4 -0
- package/dist/agent-ours/mcp-endpoint.js +5 -0
- package/dist/agent-ours/runtime.d.ts +9 -1
- package/dist/agent-ours/runtime.js +50 -2
- package/dist/agent-ours/service.d.ts +21 -1
- package/dist/agent-ours/service.js +94 -14
- package/dist/application/codex-models.d.ts +15 -0
- package/dist/application/codex-models.js +56 -0
- package/dist/application/conversation-history.d.ts +27 -0
- package/dist/application/conversation-history.js +212 -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/log-service.js +8 -8
- 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 +3 -2
- package/dist/application/role-removal-service.js +9 -1
- package/dist/application/role-repository.js +19 -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 +30 -0
- package/dist/application/session-control.js +72 -3
- package/dist/application/supervisor-ours-tools.d.ts +159 -0
- package/dist/application/supervisor-ours-tools.js +115 -0
- package/dist/application/task-layouts.d.ts +91 -0
- package/dist/application/task-layouts.js +259 -0
- package/dist/application/task-room-service.d.ts +99 -3
- package/dist/application/task-room-service.js +285 -32
- package/dist/application/types.d.ts +2 -0
- package/dist/atomic-file.d.ts +5 -0
- package/dist/atomic-file.js +62 -1
- package/dist/briefing.d.ts +3 -1
- package/dist/briefing.js +70 -30
- package/dist/build-info.json +7 -4
- package/dist/capabilities.d.ts +6 -1
- package/dist/capabilities.js +8 -0
- package/dist/cli.js +407 -37
- package/dist/client-profile.d.ts +5 -17
- package/dist/client-profile.js +4 -127
- package/dist/config.d.ts +45 -0
- package/dist/config.js +57 -6
- package/dist/creation.d.ts +1 -1
- package/dist/creation.js +4 -4
- package/dist/daemon-recovery.d.ts +1 -8
- package/dist/daemon-recovery.js +8 -113
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +123 -19
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +55 -60
- package/dist/erased-resources.d.ts +9 -0
- package/dist/erased-resources.js +87 -0
- package/dist/file-delivery/reader.d.ts +15 -0
- package/dist/file-delivery/reader.js +63 -0
- package/dist/file-delivery/store.d.ts +15 -0
- package/dist/file-delivery/store.js +124 -0
- package/dist/file-delivery/types.d.ts +19 -0
- package/dist/file-delivery/types.js +1 -0
- package/dist/fleet-command-audit.d.ts +7 -0
- package/dist/fleet-command-audit.js +26 -9
- package/dist/harness/agent-session.d.ts +2 -0
- package/dist/harness/claude-code-session.js +1 -1
- package/dist/harness/claude-code.js +20 -11
- 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-reasoning.d.ts +5 -0
- package/dist/harness/codex-reasoning.js +12 -0
- package/dist/harness/codex-runtime.d.ts +11 -1
- package/dist/harness/codex-runtime.js +31 -4
- package/dist/harness/codex-session.js +2 -2
- package/dist/harness/codex.js +23 -12
- package/dist/init-wizard.d.ts +2 -0
- package/dist/init-wizard.js +38 -19
- package/dist/isolation/policy.js +6 -1
- package/dist/isolation/types.d.ts +5 -0
- package/dist/managed-cli.d.ts +253 -0
- package/dist/managed-cli.js +923 -0
- package/dist/managed-recovery.d.ts +13 -0
- package/dist/managed-recovery.js +31 -0
- package/dist/minimal-setup.d.ts +2 -0
- package/dist/minimal-setup.js +44 -0
- package/dist/monitor.d.ts +8 -34
- package/dist/monitor.js +89 -152
- package/dist/notifications/fleet-producer.d.ts +19 -0
- package/dist/notifications/fleet-producer.js +208 -0
- package/dist/notifications/outbox.d.ts +46 -0
- package/dist/notifications/outbox.js +158 -0
- package/dist/notifications/target-cleanup.d.ts +8 -0
- package/dist/notifications/target-cleanup.js +41 -0
- package/dist/notifications/task-producer.d.ts +18 -0
- package/dist/notifications/task-producer.js +67 -0
- package/dist/ops.d.ts +2 -0
- package/dist/ops.js +20 -3
- package/dist/owner-channel/channel.js +17 -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/resolved-plan.js +1 -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 +65 -5
- package/dist/rooms-tasks/close.d.ts +6 -2
- package/dist/rooms-tasks/close.js +246 -37
- 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 +2 -4
- package/dist/rooms-tasks/cowork-adapter.js +7 -121
- package/dist/rooms-tasks/cowork-http.js +29 -1
- package/dist/rooms-tasks/deletion.d.ts +1 -1
- package/dist/rooms-tasks/deletion.js +84 -5
- package/dist/rooms-tasks/erasure.d.ts +6 -0
- package/dist/rooms-tasks/erasure.js +173 -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-member-retirement.d.ts +10 -0
- package/dist/rooms-tasks/layout-member-retirement.js +90 -0
- package/dist/rooms-tasks/layout-service.d.ts +43 -0
- package/dist/rooms-tasks/layout-service.js +203 -0
- package/dist/rooms-tasks/layout.d.ts +121 -0
- package/dist/rooms-tasks/layout.js +285 -0
- package/dist/rooms-tasks/legacy-task-member.d.ts +19 -0
- package/dist/rooms-tasks/legacy-task-member.js +116 -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-ownership.d.ts +5 -0
- package/dist/rooms-tasks/member-ownership.js +40 -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 +3 -0
- package/dist/rooms-tasks/provision.js +101 -49
- package/dist/rooms-tasks/recovery.d.ts +19 -0
- package/dist/rooms-tasks/recovery.js +110 -0
- package/dist/rooms-tasks/room-state.d.ts +4 -2
- package/dist/rooms-tasks/room-state.js +47 -8
- package/dist/rooms-tasks/task-service-retirement.d.ts +11 -0
- package/dist/rooms-tasks/task-service-retirement.js +21 -0
- package/dist/rooms-tasks/task-state.d.ts +51 -8
- package/dist/rooms-tasks/task-state.js +245 -25
- package/dist/rooms-tasks/types.d.ts +40 -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 +35 -0
- package/dist/rooms-tasks/workspace.js +325 -0
- package/dist/runner.d.ts +25 -3
- package/dist/runner.js +291 -49
- package/dist/session/acp.d.ts +53 -1
- package/dist/session/acp.js +216 -40
- package/dist/session/arbiter.d.ts +11 -0
- package/dist/session/arbiter.js +43 -2
- package/dist/session/codex-app-server.d.ts +32 -0
- package/dist/session/codex-app-server.js +137 -23
- package/dist/session/control.d.ts +12 -1
- package/dist/session/control.js +68 -3
- package/dist/session/conversation-normalizer.d.ts +2 -1
- package/dist/session/conversation-normalizer.js +1 -1
- package/dist/session/conversation-store.d.ts +1 -1
- package/dist/session/conversation-store.js +7 -11
- package/dist/session/conversation-types.d.ts +28 -3
- package/dist/session/prompt-batch.d.ts +63 -0
- package/dist/session/prompt-batch.js +86 -0
- package/dist/session/task-notice.d.ts +14 -0
- package/dist/session/task-notice.js +13 -0
- package/dist/session/types.d.ts +14 -0
- package/dist/setup-tunnel-preflight.d.ts +34 -0
- package/dist/setup-tunnel-preflight.js +62 -0
- package/dist/socket-path.d.ts +2 -0
- package/dist/socket-path.js +22 -0
- package/dist/spawn.d.ts +5 -1
- package/dist/spawn.js +40 -4
- package/dist/startup-prompt.d.ts +4 -0
- package/dist/startup-prompt.js +36 -0
- package/dist/startup-readiness.d.ts +13 -0
- package/dist/startup-readiness.js +38 -0
- package/dist/startup-telemetry.d.ts +16 -0
- package/dist/startup-telemetry.js +46 -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/adoption.d.ts +8 -0
- package/dist/supervisor/adoption.js +78 -0
- package/dist/supervisor/catalog.d.ts +62 -0
- package/dist/supervisor/catalog.js +254 -0
- package/dist/supervisor/fleet.d.ts +8 -0
- package/dist/supervisor/fleet.js +89 -0
- package/dist/supervisor/index.js +3 -6
- package/dist/supervisor/launchd.d.ts +1 -1
- package/dist/supervisor/launchd.js +26 -13
- package/dist/supervisor/legacy.d.ts +35 -0
- package/dist/supervisor/legacy.js +524 -0
- package/dist/supervisor/manager.d.ts +15 -0
- package/dist/supervisor/manager.js +270 -0
- package/dist/supervisor/scope.d.ts +4 -0
- package/dist/supervisor/scope.js +10 -0
- package/dist/supervisor/systemd.d.ts +2 -2
- package/dist/supervisor/systemd.js +19 -33
- package/dist/task-supervision.d.ts +9 -0
- package/dist/task-supervision.js +145 -0
- package/dist/temp-idle.d.ts +17 -0
- package/dist/temp-idle.js +42 -0
- package/dist/temp-lifecycle.d.ts +23 -8
- package/dist/temp-lifecycle.js +113 -65
- package/dist/temp-supervisor-recovery.d.ts +1 -1
- package/dist/temp-supervisor-recovery.js +2 -2
- package/dist/tunnel-setup-completion.d.ts +7 -0
- package/dist/tunnel-setup-completion.js +13 -0
- package/dist/tunnel-setup-input.d.ts +19 -0
- package/dist/tunnel-setup-input.js +124 -0
- package/dist/web/agent-attachments.d.ts +29 -0
- package/dist/web/agent-attachments.js +162 -0
- package/dist/web/auth.d.ts +63 -2
- package/dist/web/auth.js +191 -10
- 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 +32 -0
- package/dist/web/prefix-gateway.js +238 -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 +654 -55
- package/dist/web/service.d.ts +11 -2
- package/dist/web/service.js +30 -4
- package/dist/web/workspace-devices.d.ts +107 -0
- package/dist/web/workspace-devices.js +277 -0
- package/dist/web/workspace-tunnel.d.ts +1 -0
- package/dist/web/workspace-tunnel.js +34 -0
- package/dist/workspace-enrollment.d.ts +82 -0
- package/dist/workspace-enrollment.js +566 -0
- package/dist/workspace-replacement.d.ts +66 -0
- package/dist/workspace-replacement.js +220 -0
- package/docs/acp-files.md +146 -0
- package/docs/fleet-migration.md +54 -0
- package/docs/live-voice.md +13 -0
- package/docs/room-layouts.md +227 -0
- package/docs/task-workspaces.md +225 -0
- package/docs/validation/managed-cli-permissions.md +421 -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 +20 -19
- 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/control.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { socketPath as privateSocketPath } from '../socket-path.js';
|
|
1
2
|
import { chmodSync, mkdirSync, rmSync } from 'node:fs';
|
|
2
3
|
import { createConnection, createServer } from 'node:net';
|
|
3
4
|
import { join } from 'node:path';
|
|
4
5
|
import { FleetError, safeLine } from '../application/errors.js';
|
|
5
6
|
import { stateRoot } from '../paths.js';
|
|
6
|
-
export const webControlPath = (dir = join(stateRoot(), 'web')) => join(dir, 'control.sock');
|
|
7
|
+
export const webControlPath = (dir = join(stateRoot(), 'web')) => privateSocketPath(join(dir, 'control.sock'));
|
|
7
8
|
export async function startWebControlServer(options) {
|
|
8
9
|
const dir = options.dir ?? join(stateRoot(), 'web');
|
|
9
10
|
const path = webControlPath(dir);
|
|
@@ -34,13 +35,14 @@ export async function startWebControlServer(options) {
|
|
|
34
35
|
if (used > rateLimit)
|
|
35
36
|
throw new FleetError('rate_limited', 'local web control rate limit exceeded');
|
|
36
37
|
const parsed = JSON.parse(body.slice(0, body.indexOf('\n')));
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
let result;
|
|
39
|
+
if (parsed.command === 'link-device' && options.onLinkDevice)
|
|
40
|
+
result = await options.onLinkDevice();
|
|
39
41
|
else if (parsed.command === 'revoke-all')
|
|
40
42
|
await options.onRevokeAll();
|
|
41
43
|
else
|
|
42
44
|
throw new FleetError('invalid_request', 'unknown local web control command');
|
|
43
|
-
socket.end(JSON.stringify({ ok: true }) + '\n');
|
|
45
|
+
socket.end(JSON.stringify({ ok: true, result }) + '\n');
|
|
44
46
|
}
|
|
45
47
|
catch (error) {
|
|
46
48
|
const code = error instanceof FleetError ? error.code : 'internal';
|
|
@@ -65,7 +67,7 @@ export async function startWebControlServer(options) {
|
|
|
65
67
|
};
|
|
66
68
|
}
|
|
67
69
|
export async function requestWebControl(command, path = webControlPath(), timeoutMs = 5_000) {
|
|
68
|
-
await new Promise((resolve, reject) => {
|
|
70
|
+
return await new Promise((resolve, reject) => {
|
|
69
71
|
const socket = createConnection(path);
|
|
70
72
|
let response = '';
|
|
71
73
|
let settled = false;
|
|
@@ -73,7 +75,7 @@ export async function requestWebControl(command, path = webControlPath(), timeou
|
|
|
73
75
|
socket.destroy();
|
|
74
76
|
reject(new FleetError('timeout', 'local web control request timed out'));
|
|
75
77
|
}, timeoutMs);
|
|
76
|
-
const finish = (error) => {
|
|
78
|
+
const finish = (error, result) => {
|
|
77
79
|
if (settled)
|
|
78
80
|
return;
|
|
79
81
|
settled = true;
|
|
@@ -81,7 +83,7 @@ export async function requestWebControl(command, path = webControlPath(), timeou
|
|
|
81
83
|
if (error)
|
|
82
84
|
reject(error);
|
|
83
85
|
else
|
|
84
|
-
resolve();
|
|
86
|
+
resolve(result);
|
|
85
87
|
};
|
|
86
88
|
socket.once('connect', () => socket.write(JSON.stringify({ command }) + '\n'));
|
|
87
89
|
socket.on('data', chunk => { response += chunk.toString('utf8'); });
|
|
@@ -91,7 +93,7 @@ export async function requestWebControl(command, path = webControlPath(), timeou
|
|
|
91
93
|
const parsed = JSON.parse(response);
|
|
92
94
|
if (!parsed.ok)
|
|
93
95
|
throw new FleetError(parsed.error?.code === 'rate_limited' ? 'rate_limited' : 'rejected', parsed.error?.message ?? 'local web control request was rejected');
|
|
94
|
-
finish();
|
|
96
|
+
finish(undefined, parsed.result);
|
|
95
97
|
}
|
|
96
98
|
catch (error) {
|
|
97
99
|
finish(error);
|
|
@@ -4,6 +4,9 @@ export interface EditableFleetModel {
|
|
|
4
4
|
manifest: Record<string, unknown>;
|
|
5
5
|
agents: Record<string, Record<string, unknown>>;
|
|
6
6
|
agent_templates: Record<string, Record<string, unknown>>;
|
|
7
|
+
roles?: Record<string, Record<string, unknown>>;
|
|
8
|
+
brains?: Record<string, Record<string, unknown>>;
|
|
9
|
+
room_templates?: Record<string, Record<string, unknown>>;
|
|
7
10
|
}
|
|
8
11
|
export interface ConfigReadResult {
|
|
9
12
|
path: string;
|
|
@@ -45,7 +48,7 @@ export declare class FleetConfigService {
|
|
|
45
48
|
private readonly preflight;
|
|
46
49
|
private readonly beforeMutation?;
|
|
47
50
|
constructor(options?: FleetConfigServiceOptions);
|
|
48
|
-
read(): ConfigReadResult;
|
|
51
|
+
read(includeDefinitions?: boolean): ConfigReadResult;
|
|
49
52
|
preview(baseRevision: string, input: unknown): Promise<ConfigPreviewResult>;
|
|
50
53
|
write(baseRevision: string, input: unknown): Promise<ConfigWriteResult>;
|
|
51
54
|
}
|
|
@@ -23,7 +23,7 @@ export class FleetConfigService {
|
|
|
23
23
|
this.preflight = options.preflight ?? (async () => emptyReport());
|
|
24
24
|
this.beforeMutation = options.beforeMutation;
|
|
25
25
|
}
|
|
26
|
-
read() {
|
|
26
|
+
read(includeDefinitions = false) {
|
|
27
27
|
const sources = readSources(this.path);
|
|
28
28
|
try {
|
|
29
29
|
loadConfig(this.path, { yamlMode: 'strict' });
|
|
@@ -31,7 +31,7 @@ export class FleetConfigService {
|
|
|
31
31
|
catch (error) {
|
|
32
32
|
throw safeError(error, sourceSensitiveValues(sources));
|
|
33
33
|
}
|
|
34
|
-
const raw = modelFrom(sources);
|
|
34
|
+
const raw = modelFrom(sources, includeDefinitions);
|
|
35
35
|
const redacted = redactModel(raw);
|
|
36
36
|
return {
|
|
37
37
|
path: basename(this.path), exists: true, firstRun: false,
|
|
@@ -41,7 +41,7 @@ export class FleetConfigService {
|
|
|
41
41
|
async preview(baseRevision, input) {
|
|
42
42
|
const current = readSources(this.path);
|
|
43
43
|
assertRevision(baseRevision, current);
|
|
44
|
-
const currentModel = modelFrom(current);
|
|
44
|
+
const currentModel = modelFrom(current, !!input && typeof input === 'object' && 'roles' in input);
|
|
45
45
|
const next = restoreRedactions(assertModel(input), currentModel, new Set(redactModel(currentModel).paths));
|
|
46
46
|
const proposal = renderProposal(current, next);
|
|
47
47
|
const staged = stageProposal(this.path, current, proposal);
|
|
@@ -63,7 +63,7 @@ export class FleetConfigService {
|
|
|
63
63
|
return withFileLock(`${splitRootFor(this.path)}.web-edit.lock`, async () => {
|
|
64
64
|
const current = readSources(this.path);
|
|
65
65
|
assertRevision(baseRevision, current);
|
|
66
|
-
const currentModel = modelFrom(current);
|
|
66
|
+
const currentModel = modelFrom(current, !!input && typeof input === 'object' && 'roles' in input);
|
|
67
67
|
const next = restoreRedactions(assertModel(input), currentModel, new Set(redactModel(currentModel).paths));
|
|
68
68
|
const proposal = renderProposal(current, next);
|
|
69
69
|
const changed = changedDocuments(current, proposal);
|
|
@@ -105,6 +105,8 @@ export class FleetConfigService {
|
|
|
105
105
|
rmSync(target, { force: true });
|
|
106
106
|
else {
|
|
107
107
|
assertWritableTarget(target, current.documents.has(rel));
|
|
108
|
+
mkdirSync(dirname(target), { recursive: true, mode: 0o700 });
|
|
109
|
+
trustedDirectory(dirname(target));
|
|
108
110
|
replaceFileAtomically(target, contents, 0o600);
|
|
109
111
|
chmodSync(target, 0o600);
|
|
110
112
|
}
|
|
@@ -184,7 +186,7 @@ function readSources(manifest) {
|
|
|
184
186
|
}
|
|
185
187
|
return { manifest, root, documents };
|
|
186
188
|
}
|
|
187
|
-
function modelFrom(sources) {
|
|
189
|
+
function modelFrom(sources, includeDefinitions = false) {
|
|
188
190
|
const manifestKey = basename(sources.manifest);
|
|
189
191
|
const manifest = parseFleetDocument(sources.manifest, sources.documents.get(manifestKey), 'strict').value;
|
|
190
192
|
const agents = {};
|
|
@@ -199,15 +201,24 @@ function modelFrom(sources) {
|
|
|
199
201
|
const id = basename(rel, extname(rel));
|
|
200
202
|
agent_templates[id] = parseFleetDocument(absoluteFor(sources.manifest, rel), source, 'strict').value;
|
|
201
203
|
}
|
|
202
|
-
|
|
204
|
+
const model = { manifest, agents, agent_templates };
|
|
205
|
+
if (includeDefinitions)
|
|
206
|
+
for (const kind of ['roles', 'brains', 'room_templates']) {
|
|
207
|
+
model[kind] = {};
|
|
208
|
+
for (const [rel, source] of sources.documents)
|
|
209
|
+
if (rel.startsWith(kind + '/') && ['.yaml', '.yml'].includes(extname(rel))) {
|
|
210
|
+
model[kind][basename(rel, extname(rel))] = parseFleetDocument(absoluteFor(sources.manifest, rel), source, 'strict').value;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return model;
|
|
203
214
|
}
|
|
204
215
|
function assertModel(value) {
|
|
205
216
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
206
217
|
throw new FleetError('invalid_request', 'model must be an object');
|
|
207
218
|
const input = value;
|
|
208
|
-
if (Object.keys(input).sort().join(',')
|
|
209
|
-
throw new FleetError('invalid_request', 'model
|
|
210
|
-
for (const key of
|
|
219
|
+
if (!['agent_templates,agents,manifest', 'agent_templates,agents,brains,manifest,roles,room_templates'].includes(Object.keys(input).sort().join(',')))
|
|
220
|
+
throw new FleetError('invalid_request', 'model requires manifest, agents, agent_templates and optionally all definition collections');
|
|
221
|
+
for (const key of Object.keys(input))
|
|
211
222
|
if (!input[key] || typeof input[key] !== 'object' || Array.isArray(input[key]))
|
|
212
223
|
throw new FleetError('invalid_request', `${key} must be an object`);
|
|
213
224
|
for (const [id, doc] of Object.entries(input.agents)) {
|
|
@@ -222,6 +233,14 @@ function assertModel(value) {
|
|
|
222
233
|
if (!doc || typeof doc !== 'object' || Array.isArray(doc))
|
|
223
234
|
throw new FleetError('invalid_request', `Agent Template '${id}' must be an object`);
|
|
224
235
|
}
|
|
236
|
+
for (const kind of ['roles', 'brains', 'room_templates']) {
|
|
237
|
+
for (const [id, doc] of Object.entries((input[kind] ?? {}))) {
|
|
238
|
+
if (!ROLE_NAME_RE.test(id))
|
|
239
|
+
throw new FleetError('invalid_request', `invalid definition ID '${id}'`);
|
|
240
|
+
if (!doc || typeof doc !== 'object' || Array.isArray(doc))
|
|
241
|
+
throw new FleetError('invalid_request', `definition '${id}' must be an object`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
225
244
|
return structuredClone(input);
|
|
226
245
|
}
|
|
227
246
|
function renderProposal(current, model) {
|
|
@@ -240,9 +259,21 @@ function renderProposal(current, model) {
|
|
|
240
259
|
next.set(oldRel ?? join('agent_templates', `${id}.yaml`), oldRel
|
|
241
260
|
? render(current.documents.get(oldRel), doc) : stringify(doc));
|
|
242
261
|
}
|
|
243
|
-
for (const [
|
|
244
|
-
if (
|
|
245
|
-
|
|
262
|
+
for (const kind of ['roles', 'brains', 'room_templates']) {
|
|
263
|
+
if (model[kind] === undefined) {
|
|
264
|
+
for (const [rel, source] of current.documents)
|
|
265
|
+
if (rel.startsWith(kind + '/'))
|
|
266
|
+
next.set(rel, source);
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
for (const [rel, source] of current.documents)
|
|
270
|
+
if (rel.startsWith(kind + '/') && !['.yaml', '.yml'].includes(extname(rel)))
|
|
271
|
+
next.set(rel, source);
|
|
272
|
+
for (const [id, doc] of Object.entries(model[kind])) {
|
|
273
|
+
const oldRel = [...current.documents.keys()].find(rel => rel.startsWith(kind + '/') && ['.yaml', '.yml'].includes(extname(rel)) && basename(rel, extname(rel)) === id);
|
|
274
|
+
next.set(oldRel ?? join(kind, `${id}.yaml`), oldRel ? render(current.documents.get(oldRel), doc) : stringify(doc));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
246
277
|
return next;
|
|
247
278
|
}
|
|
248
279
|
function render(source, model) {
|
|
@@ -395,9 +426,9 @@ function sourceDiff(before, after, label, secrets) {
|
|
|
395
426
|
...left.slice(left.length - tail, left.length - tail + trailing).map(x => ` ${x}`), ''].join('\n');
|
|
396
427
|
}
|
|
397
428
|
function proposalDiff(current, proposal) {
|
|
398
|
-
const secrets = sensitiveValues(modelFrom(current));
|
|
429
|
+
const secrets = sensitiveValues(modelFrom(current, true));
|
|
399
430
|
try {
|
|
400
|
-
secrets.push(...sensitiveValues(modelFrom({ ...current, documents: proposal })));
|
|
431
|
+
secrets.push(...sensitiveValues(modelFrom({ ...current, documents: proposal }, true)));
|
|
401
432
|
}
|
|
402
433
|
catch { /* validation reports later */ }
|
|
403
434
|
return [...new Set([...current.documents.keys(), ...proposal.keys()])].sort().map(rel => sourceDiff(current.documents.get(rel) ?? '', proposal.get(rel) ?? '', rel, secrets)).filter(Boolean).join('\n');
|
|
@@ -405,7 +436,7 @@ function proposalDiff(current, proposal) {
|
|
|
405
436
|
function splitLines(source) { return source.replace(/\n$/, '').split('\n'); }
|
|
406
437
|
function restartImpact(before, after) {
|
|
407
438
|
const roles = [...new Set([...Object.keys(before.agents), ...Object.keys(after.agents)])]
|
|
408
|
-
.filter(id => JSON.stringify(before.agents[id]) !== JSON.stringify(after.agents[id])).sort();
|
|
439
|
+
.filter(id => JSON.stringify(before.agents[id]) !== JSON.stringify(after.agents[id]) || JSON.stringify(before.roles) !== JSON.stringify(after.roles) || JSON.stringify(before.brains) !== JSON.stringify(after.brains)).sort();
|
|
409
440
|
const watchdogScheduler = JSON.stringify(before.manifest.watchdogs) !== JSON.stringify(after.manifest.watchdogs);
|
|
410
441
|
const scheduledLoops = JSON.stringify(before.manifest.loops) !== JSON.stringify(after.manifest.loops);
|
|
411
442
|
const required = roles.length > 0 || watchdogScheduler || scheduledLoops;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { AuditSink } from './audit.js';
|
|
2
|
+
import { type IncomingMessage } from 'node:http';
|
|
3
|
+
import type { WebAuth } from './auth.js';
|
|
4
|
+
/** Each entry exposes one dedicated backend listener. Endpoint dispatch stays upstream. */
|
|
5
|
+
export interface ServiceTarget {
|
|
6
|
+
prefix: string;
|
|
7
|
+
origin: string;
|
|
8
|
+
upstreamPrefix?: string;
|
|
9
|
+
/** Server-side service credentials; never taken from browser headers. */
|
|
10
|
+
headers?: Record<string, string>;
|
|
11
|
+
/** Dedicated credential-authenticated services reject direct browser context. */
|
|
12
|
+
stripBrowserContext?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Service paths (below the prefix) where the account App may open a WebSocket with its device
|
|
15
|
+
* credential. A browser cannot set Authorization on a WebSocket, so only there it is accepted
|
|
16
|
+
* as a subprotocol.
|
|
17
|
+
*/
|
|
18
|
+
deviceSocketPaths?: string[];
|
|
19
|
+
}
|
|
20
|
+
export interface GatewayOptions {
|
|
21
|
+
/** Backend mode is for a separate loopback machine-client listener; browsers are rejected. */
|
|
22
|
+
auth: WebAuth | 'backend';
|
|
23
|
+
fleetOrigin: string;
|
|
24
|
+
services: ServiceTarget[];
|
|
25
|
+
/** Bounded unauthorized-response diagnostics: fixed service/action enums only. */
|
|
26
|
+
audit?: Pick<AuditSink, 'record'>;
|
|
27
|
+
}
|
|
28
|
+
/** Raw transport deliberately runs outside Fastify's JSON parser and body limits. */
|
|
29
|
+
export declare function createPrefixGateway(options: GatewayOptions): {
|
|
30
|
+
server: import("http").Server<typeof IncomingMessage, typeof import("http").ServerResponse>;
|
|
31
|
+
close(): Promise<void>;
|
|
32
|
+
};
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { WorkspaceDeviceAuthError } from './workspace-devices.js';
|
|
3
|
+
import { createServer, request as httpRequest } from 'node:http';
|
|
4
|
+
import { request as httpsRequest } from 'node:https';
|
|
5
|
+
/** The App offers exactly these two subprotocols; only the marker is ever answered or forwarded. */
|
|
6
|
+
const DEVICE_SOCKET_PROTOCOL = 'ours.workspace.v1';
|
|
7
|
+
const DEVICE_SOCKET_BEARER = 'ours.workspace.bearer.';
|
|
8
|
+
/** The device credential a WebSocket offered as a subprotocol; undefined when it offered none of ours. */
|
|
9
|
+
function deviceSocketCredential(req) {
|
|
10
|
+
const offered = String(req.headers['sec-websocket-protocol'] ?? '').split(',').map(value => value.trim()).filter(Boolean);
|
|
11
|
+
if (!offered.some(value => value === DEVICE_SOCKET_PROTOCOL || value.startsWith(DEVICE_SOCKET_BEARER)))
|
|
12
|
+
return undefined;
|
|
13
|
+
const credentials = offered.filter(value => value.startsWith(DEVICE_SOCKET_BEARER));
|
|
14
|
+
const token = credentials[0]?.slice(DEVICE_SOCKET_BEARER.length) ?? '';
|
|
15
|
+
if (offered.length !== 2 || credentials.length !== 1 || !offered.includes(DEVICE_SOCKET_PROTOCOL) || !/^[A-Za-z0-9_.-]{16,256}$/.test(token))
|
|
16
|
+
throw new Error('invalid workspace socket subprotocol');
|
|
17
|
+
return token;
|
|
18
|
+
}
|
|
19
|
+
const hopHeaders = new Set(['connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailer', 'transfer-encoding', 'upgrade']);
|
|
20
|
+
function headersFor(message) {
|
|
21
|
+
const denied = new Set([...hopHeaders, ...(message.headers.connection ?? '').split(',').map(x => x.trim().toLowerCase())]);
|
|
22
|
+
return Object.fromEntries(Object.entries(message.headers).filter(([key]) => !denied.has(key)));
|
|
23
|
+
}
|
|
24
|
+
/** Raw transport deliberately runs outside Fastify's JSON parser and body limits. */
|
|
25
|
+
export function createPrefixGateway(options) {
|
|
26
|
+
const auth = options.auth === 'backend' ? undefined : options.auth;
|
|
27
|
+
let auditWindow = Date.now(), auditCount = 0;
|
|
28
|
+
const rejected = (requestId, prefix, result) => {
|
|
29
|
+
if (Date.now() - auditWindow >= 60_000) {
|
|
30
|
+
auditWindow = Date.now();
|
|
31
|
+
auditCount = 0;
|
|
32
|
+
}
|
|
33
|
+
if (auditCount++ >= 60)
|
|
34
|
+
return;
|
|
35
|
+
void options.audit?.record({ requestId, action: 'gateway' + prefix.replaceAll('/', '.'), result, errorCode: 'unauthorized' }).catch(() => { });
|
|
36
|
+
};
|
|
37
|
+
const services = options.services.map(target => {
|
|
38
|
+
if (!/^\/[a-z][a-z0-9-]*$/.test(target.prefix) || target.prefix === '/fleet')
|
|
39
|
+
throw new Error('invalid service prefix');
|
|
40
|
+
const origin = new URL(target.origin);
|
|
41
|
+
if (target.upstreamPrefix && !/^\/(?:[A-Za-z0-9_-]+\/)*[A-Za-z0-9_-]*$/.test(target.upstreamPrefix))
|
|
42
|
+
throw new Error('invalid upstream service prefix');
|
|
43
|
+
if (!['http:', 'https:'].includes(origin.protocol) || origin.pathname !== '/' || origin.search || origin.hash || origin.username || origin.password)
|
|
44
|
+
throw new Error('service target must be an HTTP origin');
|
|
45
|
+
return { ...target, origin: origin.origin };
|
|
46
|
+
});
|
|
47
|
+
if (new Set(services.map(x => x.prefix)).size !== services.length)
|
|
48
|
+
throw new Error('duplicate service prefix');
|
|
49
|
+
const select = (req) => {
|
|
50
|
+
const raw = req.url ?? '/';
|
|
51
|
+
if (!raw.startsWith('/') || raw.startsWith('//'))
|
|
52
|
+
throw new Error('invalid request target');
|
|
53
|
+
const path = raw.split('?')[0];
|
|
54
|
+
const service = services.find(x => path === x.prefix || path.startsWith(x.prefix + '/'));
|
|
55
|
+
const fleet = path === '/fleet/api' || path.startsWith('/fleet/api/');
|
|
56
|
+
return { service, origin: service?.origin ?? options.fleetOrigin,
|
|
57
|
+
path: service ? (service.upstreamPrefix || '') + (raw.slice(service.prefix.length) || '/').replace(/^\?/, '/?') : fleet ? raw.slice('/fleet'.length) : raw };
|
|
58
|
+
};
|
|
59
|
+
const prepare = (req, upgrade = false) => {
|
|
60
|
+
const route = select(req);
|
|
61
|
+
// The device credential of an App WebSocket: accepted only on an upgrade, from the exact
|
|
62
|
+
// account origin, on a path the service declares, and never beside another credential.
|
|
63
|
+
const socketCredential = upgrade && auth ? deviceSocketCredential(req) : undefined;
|
|
64
|
+
if (socketCredential !== undefined) {
|
|
65
|
+
const path = route.service ? (req.url ?? '/').split('?')[0].slice(route.service.prefix.length) : undefined;
|
|
66
|
+
if (!route.service?.deviceSocketPaths?.includes(path ?? '') || req.headers.authorization !== undefined || req.headers.cookie !== undefined
|
|
67
|
+
|| !req.headers.origin || auth.accountOrigin(req) !== req.headers.origin)
|
|
68
|
+
throw new Error('workspace socket credential is not accepted here');
|
|
69
|
+
}
|
|
70
|
+
// Authentication sees the credential as the bearer it is; the request's own headers are untouched.
|
|
71
|
+
const authHeaders = socketCredential !== undefined ? { ...req.headers, authorization: `Bearer ${socketCredential}` } : req.headers;
|
|
72
|
+
const request = { headers: authHeaders, method: req.method, url: route.path };
|
|
73
|
+
if (!auth && (req.headers.origin || req.headers['sec-fetch-site'] || !route.service))
|
|
74
|
+
throw new Error('machine-client listener rejects browser requests');
|
|
75
|
+
auth?.validateBoundary(request, upgrade);
|
|
76
|
+
const session = route.service && auth ? auth.authenticate(request, !upgrade && !['GET', 'HEAD', 'OPTIONS'].includes(req.method ?? 'GET')) : undefined;
|
|
77
|
+
const headers = headersFor(req);
|
|
78
|
+
// Forward only the explicit browser context, never an attacker-selected forwarding chain.
|
|
79
|
+
for (const key of Object.keys(headers))
|
|
80
|
+
if (key.startsWith('x-forwarded-') || key === 'forwarded')
|
|
81
|
+
delete headers[key];
|
|
82
|
+
delete headers['x-ours-workspace-auth'];
|
|
83
|
+
delete headers['x-ours-request-id'];
|
|
84
|
+
if (route.service) {
|
|
85
|
+
if (auth)
|
|
86
|
+
for (const name of ['cookie', 'authorization', 'x-csrf-token', 'x-ours-api-token'])
|
|
87
|
+
delete headers[name];
|
|
88
|
+
// The server gateway lets this header choose a producer credential; only machine producers send it directly there.
|
|
89
|
+
delete headers['x-ours-notifications-producer'];
|
|
90
|
+
if (route.service.stripBrowserContext) {
|
|
91
|
+
for (const name of ['origin', 'sec-fetch-site', 'sec-fetch-mode', 'sec-fetch-dest', 'sec-fetch-user'])
|
|
92
|
+
delete headers[name];
|
|
93
|
+
}
|
|
94
|
+
Object.assign(headers, route.service.headers);
|
|
95
|
+
headers.host = new URL(route.origin).host;
|
|
96
|
+
}
|
|
97
|
+
if (upgrade) {
|
|
98
|
+
headers.connection = 'Upgrade';
|
|
99
|
+
headers.upgrade = 'websocket';
|
|
100
|
+
}
|
|
101
|
+
// The credential subprotocol ends here: the service is not offered it, on any request.
|
|
102
|
+
if (socketCredential !== undefined || String(headers['sec-websocket-protocol'] ?? '').includes(DEVICE_SOCKET_BEARER))
|
|
103
|
+
delete headers['sec-websocket-protocol'];
|
|
104
|
+
return { ...route, headers, session, authHeaders, deviceSocket: socketCredential !== undefined };
|
|
105
|
+
};
|
|
106
|
+
const track = (authHeaders, session, close) => {
|
|
107
|
+
if (!session)
|
|
108
|
+
return () => { };
|
|
109
|
+
// The session authenticated for this request, with its own deadline, not whatever is current later.
|
|
110
|
+
const unbind = auth.bindTransport(session, close);
|
|
111
|
+
const timer = setInterval(() => { try {
|
|
112
|
+
auth.authenticate({ headers: authHeaders });
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
close();
|
|
116
|
+
} }, 30_000);
|
|
117
|
+
timer.unref();
|
|
118
|
+
return () => { clearInterval(timer); unbind(); };
|
|
119
|
+
};
|
|
120
|
+
// The account App calls workspace services directly from its own origin with a device
|
|
121
|
+
// credential. Only that exact configured origin is answered; the reply grants nothing by
|
|
122
|
+
// itself, every request is still authenticated below.
|
|
123
|
+
const accountCors = (req) => {
|
|
124
|
+
const origin = auth?.accountOrigin(req);
|
|
125
|
+
return origin ? { 'access-control-allow-origin': origin, vary: 'Origin',
|
|
126
|
+
'access-control-expose-headers': 'Accept-Ranges, Content-Disposition, Content-Length, Content-Range, Content-Type, ETag, X-Ours-Workspace-Auth, X-Ours-Request-Id' } : {};
|
|
127
|
+
};
|
|
128
|
+
const server = createServer((req, res) => {
|
|
129
|
+
const cors = accountCors(req);
|
|
130
|
+
const requestId = randomUUID();
|
|
131
|
+
let route;
|
|
132
|
+
try {
|
|
133
|
+
if (req.method === 'OPTIONS' && cors.vary && req.headers['access-control-request-method'] && select(req).service) {
|
|
134
|
+
auth.validateBoundary({ headers: req.headers, method: req.method, url: req.url }, false);
|
|
135
|
+
res.writeHead(204, { ...cors, 'cache-control': 'no-store', 'access-control-max-age': '600',
|
|
136
|
+
'access-control-allow-methods': 'GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS',
|
|
137
|
+
'access-control-allow-headers': 'Authorization, Content-Type, Idempotency-Key, If-None-Match, Range, X-CSRF-Token, X-Ours-Messenger-CSRF, X-Voice-Duration' });
|
|
138
|
+
res.end();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
route = prepare(req);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
const code = error.code;
|
|
145
|
+
const deviceRejected = error instanceof WorkspaceDeviceAuthError;
|
|
146
|
+
if (code === 'unauthorized')
|
|
147
|
+
rejected(requestId, '/auth', deviceRejected ? 'device_rejected' : 'gateway_rejected');
|
|
148
|
+
res.writeHead(code === 'unauthorized' ? 401 : 403, { ...cors, 'content-type': 'application/json', 'cache-control': 'no-store',
|
|
149
|
+
...(code === 'unauthorized' ? { 'x-ours-request-id': requestId } : {}),
|
|
150
|
+
...(deviceRejected ? { 'x-ours-workspace-auth': 'rejected' } : {}) });
|
|
151
|
+
res.end(JSON.stringify({ error: { code: code ?? 'forbidden', message: error.message } }));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const upstream = (route.origin.startsWith('https:') ? httpsRequest : httpRequest)(route.origin, {
|
|
155
|
+
method: req.method, path: route.path, headers: route.headers,
|
|
156
|
+
}, response => {
|
|
157
|
+
const headers = headersFor(response);
|
|
158
|
+
if (route.service) {
|
|
159
|
+
delete headers['set-cookie'];
|
|
160
|
+
// Only Fleet may assert device rejection; never trust a service's marker or correlation id.
|
|
161
|
+
delete headers['x-ours-workspace-auth'];
|
|
162
|
+
delete headers['x-ours-request-id'];
|
|
163
|
+
if (response.statusCode === 401) {
|
|
164
|
+
headers['x-ours-request-id'] = requestId;
|
|
165
|
+
headers['x-ours-workspace-auth'] = 'service';
|
|
166
|
+
rejected(requestId, route.service.prefix, 'upstream_401');
|
|
167
|
+
}
|
|
168
|
+
for (const key of Object.keys(headers))
|
|
169
|
+
if (key.startsWith('access-control-'))
|
|
170
|
+
delete headers[key];
|
|
171
|
+
Object.assign(headers, cors);
|
|
172
|
+
}
|
|
173
|
+
res.writeHead(response.statusCode ?? 502, headers);
|
|
174
|
+
// Streaming headers, including exact-origin CORS and upstream errors,
|
|
175
|
+
// must reach the client before the first event or body byte arrives.
|
|
176
|
+
res.flushHeaders();
|
|
177
|
+
response.on('error', () => res.destroy());
|
|
178
|
+
response.on('aborted', () => res.destroy());
|
|
179
|
+
response.on('close', () => { if (!response.complete)
|
|
180
|
+
res.destroy(); });
|
|
181
|
+
response.pipe(res);
|
|
182
|
+
res.on('close', () => response.destroy());
|
|
183
|
+
});
|
|
184
|
+
upstream.on('error', () => {
|
|
185
|
+
if (!res.headersSent)
|
|
186
|
+
res.writeHead(502, { ...(route.service ? cors : {}), 'content-type': 'application/json' });
|
|
187
|
+
res.end(JSON.stringify({ error: { code: 'upstream_unavailable', message: 'Service is unavailable' } }));
|
|
188
|
+
});
|
|
189
|
+
const untrack = track(route.authHeaders, route.session, () => { upstream.destroy(); res.destroy(); });
|
|
190
|
+
res.on('close', untrack);
|
|
191
|
+
req.on('aborted', () => upstream.destroy());
|
|
192
|
+
res.on('close', () => upstream.destroy());
|
|
193
|
+
req.pipe(upstream);
|
|
194
|
+
});
|
|
195
|
+
const sockets = new Set();
|
|
196
|
+
server.on('connection', socket => { sockets.add(socket); socket.on('close', () => sockets.delete(socket)); });
|
|
197
|
+
server.on('upgrade', (req, socket, head) => {
|
|
198
|
+
let route;
|
|
199
|
+
try {
|
|
200
|
+
route = prepare(req, true);
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
socket.end('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n');
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const upstream = (route.origin.startsWith('https:') ? httpsRequest : httpRequest)(route.origin, {
|
|
207
|
+
method: req.method, path: route.path, headers: route.headers,
|
|
208
|
+
});
|
|
209
|
+
upstream.on('upgrade', (response, peer, upstreamHead) => {
|
|
210
|
+
const headers = headersFor(response);
|
|
211
|
+
headers.connection = 'Upgrade';
|
|
212
|
+
headers.upgrade = 'websocket';
|
|
213
|
+
delete headers['set-cookie'];
|
|
214
|
+
// A browser that offered subprotocols needs one answered; only the marker, never the credential.
|
|
215
|
+
if (route.deviceSocket)
|
|
216
|
+
headers['sec-websocket-protocol'] = DEVICE_SOCKET_PROTOCOL;
|
|
217
|
+
socket.write(`HTTP/1.1 101 Switching Protocols\r\n${Object.entries(headers).flatMap(([k, v]) => (Array.isArray(v) ? v : [v]).map(value => `${k}: ${value}\r\n`)).join('')}\r\n`);
|
|
218
|
+
if (head.length)
|
|
219
|
+
peer.write(head);
|
|
220
|
+
if (upstreamHead.length)
|
|
221
|
+
socket.write(upstreamHead);
|
|
222
|
+
const untrack = track(route.authHeaders, route.session, () => { peer.destroy(); socket.destroy(); });
|
|
223
|
+
socket.on('close', untrack);
|
|
224
|
+
peer.pipe(socket);
|
|
225
|
+
socket.pipe(peer);
|
|
226
|
+
peer.on('error', () => socket.destroy());
|
|
227
|
+
socket.on('error', () => peer.destroy());
|
|
228
|
+
socket.on('close', () => peer.destroy());
|
|
229
|
+
peer.on('close', () => socket.destroy());
|
|
230
|
+
});
|
|
231
|
+
upstream.on('response', response => { socket.end(`HTTP/1.1 ${response.statusCode ?? 502} Rejected\r\nConnection: close\r\n\r\n`); response.destroy(); });
|
|
232
|
+
upstream.on('error', () => socket.destroy());
|
|
233
|
+
socket.on('close', () => upstream.destroy());
|
|
234
|
+
upstream.end();
|
|
235
|
+
});
|
|
236
|
+
return { server, async close() { for (const socket of sockets)
|
|
237
|
+
socket.destroy(); await new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve())); } };
|
|
238
|
+
}
|
package/dist/web/runtime.d.ts
CHANGED
|
@@ -2,8 +2,10 @@ import { type WebServer } from './server.js';
|
|
|
2
2
|
import { type WebAccessConfig } from './access.js';
|
|
3
3
|
export interface StartWebOptions {
|
|
4
4
|
configPath?: string;
|
|
5
|
+
/** Separate console state for an independently launched gateway. */
|
|
6
|
+
webStateDir?: string;
|
|
7
|
+
control?: boolean;
|
|
5
8
|
port?: number;
|
|
6
|
-
open?: boolean;
|
|
7
9
|
binPath: string;
|
|
8
10
|
log?(line: string): void;
|
|
9
11
|
bind?: string;
|