@ours.network/fleet 1.2.0-nightly.7 → 1.2.0-nightly.71
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 +279 -68
- package/dist/account-origin.d.ts +2 -0
- package/dist/account-origin.js +6 -0
- package/dist/agent-ours/bridge.js +26 -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 +19 -0
- package/dist/agent-ours/mcp-endpoint.js +98 -25
- package/dist/agent-ours/runtime.d.ts +9 -1
- package/dist/agent-ours/runtime.js +50 -2
- package/dist/agent-ours/service.d.ts +24 -1
- package/dist/agent-ours/service.js +97 -13
- package/dist/application/codex-models.d.ts +15 -0
- package/dist/application/codex-models.js +56 -0
- package/dist/application/conversation-history.d.ts +34 -0
- package/dist/application/conversation-history.js +280 -0
- package/dist/application/conversation-role-observer.d.ts +62 -0
- package/dist/application/conversation-role-observer.js +206 -0
- package/dist/application/fleet-query-service.d.ts +1 -1
- package/dist/application/fleet-query-service.js +3 -3
- 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/resource-observer.d.ts +40 -0
- package/dist/application/resource-observer.js +278 -0
- package/dist/application/role-creation-service.js +3 -2
- package/dist/application/role-removal-service.js +17 -3
- package/dist/application/role-repository.d.ts +1 -1
- package/dist/application/role-repository.js +30 -7
- 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 +61 -17
- package/dist/application/session-control.js +207 -23
- package/dist/application/supervisor-ours-tools.js +5 -11
- package/dist/application/task-layouts.d.ts +91 -0
- package/dist/application/task-layouts.js +261 -0
- package/dist/application/task-room-service.d.ts +101 -3
- package/dist/application/task-room-service.js +286 -33
- package/dist/application/types.d.ts +2 -0
- package/dist/atomic-file.d.ts +34 -1
- package/dist/atomic-file.js +161 -18
- package/dist/briefing.d.ts +3 -1
- package/dist/briefing.js +97 -31
- package/dist/build-info.json +9 -4
- package/dist/capabilities.d.ts +9 -1
- package/dist/capabilities.js +13 -0
- package/dist/cli.js +400 -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 +14 -9
- package/dist/daemon-recovery.js +124 -115
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +207 -19
- package/dist/doctor.d.ts +4 -1
- package/dist/doctor.js +75 -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 +24 -8
- package/dist/harness/agent-session.d.ts +4 -0
- package/dist/harness/claude-code-session.js +1 -1
- package/dist/harness/claude-code.js +21 -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 +24 -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 +9 -34
- package/dist/monitor.js +90 -153
- package/dist/notifications/fleet-producer.d.ts +22 -0
- package/dist/notifications/fleet-producer.js +237 -0
- package/dist/notifications/outbox.d.ts +46 -0
- package/dist/notifications/outbox.js +158 -0
- package/dist/notifications/target-cleanup.d.ts +11 -0
- package/dist/notifications/target-cleanup.js +63 -0
- package/dist/notifications/task-producer.d.ts +20 -0
- package/dist/notifications/task-producer.js +100 -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 +43 -0
- package/dist/process-incarnation.d.ts +23 -0
- package/dist/process-incarnation.js +42 -0
- package/dist/reminders/manager.d.ts +75 -0
- package/dist/reminders/manager.js +278 -0
- package/dist/reminders/runtime.d.ts +27 -0
- package/dist/reminders/runtime.js +32 -0
- package/dist/reminders/schedule.d.ts +37 -0
- package/dist/reminders/schedule.js +104 -0
- package/dist/reminders/store.d.ts +341 -0
- package/dist/reminders/store.js +762 -0
- package/dist/reminders/tools.d.ts +20 -0
- package/dist/reminders/tools.js +114 -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 +84 -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 +12 -5
- package/dist/rooms-tasks/cowork-adapter.js +15 -121
- package/dist/rooms-tasks/cowork-http.js +56 -6
- 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 +286 -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 +52 -8
- package/dist/rooms-tasks/task-state.js +251 -26
- 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 +33 -4
- package/dist/runner.js +410 -62
- package/dist/session/acp.d.ts +75 -2
- package/dist/session/acp.js +345 -82
- package/dist/session/arbiter.d.ts +98 -0
- package/dist/session/arbiter.js +272 -3
- package/dist/session/codex-app-server.d.ts +45 -1
- package/dist/session/codex-app-server.js +225 -32
- package/dist/session/control.d.ts +17 -3
- package/dist/session/control.js +213 -23
- package/dist/session/conversation-normalizer.d.ts +2 -1
- package/dist/session/conversation-normalizer.js +1 -1
- package/dist/session/conversation-observation.d.ts +25 -0
- package/dist/session/conversation-observation.js +100 -0
- package/dist/session/conversation-replay.d.ts +50 -0
- package/dist/session/conversation-replay.js +203 -0
- package/dist/session/conversation-store.d.ts +15 -1
- package/dist/session/conversation-store.js +206 -26
- package/dist/session/conversation-types.d.ts +57 -4
- package/dist/session/prompt-batch.d.ts +79 -0
- package/dist/session/prompt-batch.js +120 -0
- package/dist/session/task-notice.d.ts +14 -0
- package/dist/session/task-notice.js +13 -0
- package/dist/session/types.d.ts +42 -1
- 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 +15 -0
- package/dist/startup-prompt.js +71 -0
- package/dist/startup-readiness.d.ts +32 -0
- package/dist/startup-readiness.js +101 -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 +77 -0
- package/dist/subscriptions/login.js +215 -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 +143 -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/registration-refresh.d.ts +45 -0
- package/dist/supervisor/registration-refresh.js +266 -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 +123 -66
- 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/watchdog/query.d.ts +3 -1
- package/dist/watchdog/query.js +2 -1
- package/dist/web/agent-attachments.d.ts +29 -0
- package/dist/web/agent-attachments.js +162 -0
- package/dist/web/auth.d.ts +65 -2
- package/dist/web/auth.js +201 -11
- 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/conversation-stream-codec.d.ts +23 -0
- package/dist/web/conversation-stream-codec.js +110 -0
- package/dist/web/conversation-stream.d.ts +25 -0
- package/dist/web/conversation-stream.js +231 -0
- package/dist/web/events.d.ts +2 -0
- package/dist/web/events.js +9 -0
- 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 +248 -0
- package/dist/web/resource-views.d.ts +124 -0
- package/dist/web/resource-views.js +424 -0
- package/dist/web/runtime.d.ts +3 -1
- package/dist/web/runtime.js +132 -34
- package/dist/web/server.d.ts +19 -0
- package/dist/web/server.js +802 -67
- 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 +83 -0
- package/dist/workspace-enrollment.js +580 -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/conversation-stream.md +196 -0
- package/docs/fleet-migration.md +54 -0
- package/docs/live-voice.md +13 -0
- package/docs/room-layouts.md +227 -0
- package/docs/self-reminders.md +374 -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 +24 -20
- 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 +4 -2
- 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 +12 -11
- package/presets/fleet/roles/Critic.yaml +13 -0
- package/presets/fleet/roles/Developer.yaml +13 -0
- package/presets/fleet/roles/Engineer.yaml +53 -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
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { constants, closeSync, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { FleetError } from '../application/errors.js';
|
|
5
|
+
export const MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024;
|
|
6
|
+
export const MAX_ATTACHMENT_BODY = Math.ceil(MAX_ATTACHMENT_BYTES / 3) * 4 + 4096;
|
|
7
|
+
function fail(message) { throw new FleetError('invalid_request', message); }
|
|
8
|
+
const digest = (data) => createHash('sha256').update(data).digest('hex');
|
|
9
|
+
function directory(path, create = false, privateStorage = true) {
|
|
10
|
+
if (create && !existsSync(path))
|
|
11
|
+
mkdirSync(path, { mode: 0o700 });
|
|
12
|
+
const stat = lstatSync(path);
|
|
13
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || stat.uid !== process.getuid?.() || (stat.mode & (privateStorage ? 0o022 : 0o002)))
|
|
14
|
+
fail('Attachment directory is not a private owned directory');
|
|
15
|
+
}
|
|
16
|
+
function read(path, max) {
|
|
17
|
+
const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
18
|
+
try {
|
|
19
|
+
const stat = fstatSync(fd);
|
|
20
|
+
if (!stat.isFile() || stat.nlink !== 1 || stat.uid !== process.getuid?.() || stat.size > max)
|
|
21
|
+
fail('Attachment is not a bounded owned regular file');
|
|
22
|
+
return readFileSync(fd);
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
closeSync(fd);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function immutable(path, bytes) {
|
|
29
|
+
try {
|
|
30
|
+
writeFileSync(path, bytes, { flag: 'wx', mode: 0o400 });
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (error.code !== 'EEXIST')
|
|
34
|
+
throw error;
|
|
35
|
+
if (!read(path, bytes.length).equals(bytes))
|
|
36
|
+
fail('Attachment content changed');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function filename(name) {
|
|
40
|
+
const chars = Array.from(name.replace(/[^\p{L}\p{N}._-]/gu, '_').replace(/^\.+/, '_'));
|
|
41
|
+
while (Buffer.byteLength(chars.join('')) > 120)
|
|
42
|
+
chars.pop();
|
|
43
|
+
return chars.join('') || 'file';
|
|
44
|
+
}
|
|
45
|
+
export function storeAgentAttachment(stateDir, generation, input) {
|
|
46
|
+
if (!input || typeof input !== 'object')
|
|
47
|
+
fail('File data is required');
|
|
48
|
+
const body = input;
|
|
49
|
+
if (typeof body.name !== 'string' || !body.name.trim() || body.name.length > 240 || /[\x00-\x1f]/u.test(body.name))
|
|
50
|
+
fail('A file name of 1–240 characters is required');
|
|
51
|
+
if (typeof body.mimeType !== 'string' || !/^[a-zA-Z0-9.+-]+\/[a-zA-Z0-9.+-]+$/.test(body.mimeType))
|
|
52
|
+
fail('A valid media type is required');
|
|
53
|
+
if (typeof body.data !== 'string' || body.data.length > Math.ceil(MAX_ATTACHMENT_BYTES / 3) * 4 || /[^A-Za-z0-9+/=]/.test(body.data))
|
|
54
|
+
fail('Invalid base64 file data or file exceeds 20 MiB');
|
|
55
|
+
const bytes = Buffer.from(body.data, 'base64');
|
|
56
|
+
if (bytes.length > MAX_ATTACHMENT_BYTES || bytes.toString('base64') !== body.data)
|
|
57
|
+
fail('Invalid file data or file exceeds 20 MiB');
|
|
58
|
+
const sha256 = digest(bytes), name = body.name, mimeType = body.mimeType;
|
|
59
|
+
const id = digest(JSON.stringify({ generation, name, mimeType, sha256 }));
|
|
60
|
+
const record = { id, name, mimeType, size: bytes.length, sha256, generation };
|
|
61
|
+
// Fleet runtime state directories may use the account group (0775). Upload storage remains private.
|
|
62
|
+
directory(stateDir, false, false);
|
|
63
|
+
const root = join(stateDir, 'web-attachments');
|
|
64
|
+
directory(root, true);
|
|
65
|
+
const dir = join(root, id);
|
|
66
|
+
directory(dir, true);
|
|
67
|
+
immutable(join(dir, 'content-' + filename(name)), bytes);
|
|
68
|
+
immutable(join(dir, 'metadata.json'), Buffer.from(JSON.stringify(record)));
|
|
69
|
+
return record;
|
|
70
|
+
}
|
|
71
|
+
export function attachmentPrompt(stateDir, generation, ids, text) {
|
|
72
|
+
if (!Array.isArray(ids) || ids.length < 1 || ids.length > 5 || new Set(ids).size !== ids.length)
|
|
73
|
+
fail('Select between 1 and 5 distinct attachments');
|
|
74
|
+
// Fleet runtime state directories may use the account group (0775). Upload storage remains private.
|
|
75
|
+
directory(stateDir, false, false);
|
|
76
|
+
const root = join(stateDir, 'web-attachments');
|
|
77
|
+
directory(root);
|
|
78
|
+
const files = ids.map(id => {
|
|
79
|
+
if (typeof id !== 'string' || !/^[a-f0-9]{64}$/.test(id))
|
|
80
|
+
fail('Invalid attachment ID');
|
|
81
|
+
const dir = join(root, id);
|
|
82
|
+
directory(dir);
|
|
83
|
+
const record = JSON.parse(read(join(dir, 'metadata.json'), 4096).toString());
|
|
84
|
+
if (record.id !== id || record.generation !== generation)
|
|
85
|
+
throw new FleetError('stale_state', 'Attachment belongs to a different agent session');
|
|
86
|
+
const file = join(dir, 'content-' + filename(record.name)), bytes = read(file, MAX_ATTACHMENT_BYTES);
|
|
87
|
+
if (bytes.length !== record.size || digest(bytes) !== record.sha256 || digest(JSON.stringify({ generation, name: record.name, mimeType: record.mimeType, sha256: record.sha256 })) !== id)
|
|
88
|
+
fail('Attachment content changed');
|
|
89
|
+
return { name: record.name, mimeType: record.mimeType, size: record.size, sha256: record.sha256, path: file };
|
|
90
|
+
});
|
|
91
|
+
return `${text}${text ? '\n\n' : ''}Attached files (available on this host; open the files to inspect their contents, and use the image viewer for images):\n${JSON.stringify(files, null, 2)}`;
|
|
92
|
+
}
|
|
93
|
+
/** Authenticated content access reuses the same ownership and integrity checks as admission. */
|
|
94
|
+
function attachmentMetadata(stateDir, id) {
|
|
95
|
+
if (!/^[a-f0-9]{64}$/.test(id))
|
|
96
|
+
fail('Invalid attachment ID');
|
|
97
|
+
directory(stateDir, false, false);
|
|
98
|
+
const root = join(stateDir, 'web-attachments');
|
|
99
|
+
directory(root);
|
|
100
|
+
const dir = join(root, id);
|
|
101
|
+
directory(dir);
|
|
102
|
+
const record = JSON.parse(read(join(dir, 'metadata.json'), 4096).toString());
|
|
103
|
+
if (record.id !== id || typeof record.name !== 'string' || typeof record.mimeType !== 'string')
|
|
104
|
+
fail('Invalid attachment metadata');
|
|
105
|
+
if (digest(JSON.stringify({ generation: record.generation, name: record.name, mimeType: record.mimeType, sha256: record.sha256 })) !== id)
|
|
106
|
+
fail('Attachment metadata changed');
|
|
107
|
+
return record;
|
|
108
|
+
}
|
|
109
|
+
export function readAgentAttachment(stateDir, id) {
|
|
110
|
+
let record;
|
|
111
|
+
try {
|
|
112
|
+
record = attachmentMetadata(stateDir, id);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (error.code === 'ENOENT')
|
|
116
|
+
throw new FleetError('resource_not_found', 'Attachment not found');
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
const bytes = read(join(stateDir, 'web-attachments', id, 'content-' + filename(record.name)), MAX_ATTACHMENT_BYTES);
|
|
120
|
+
if (bytes.length !== record.size || digest(bytes) !== record.sha256)
|
|
121
|
+
fail('Attachment content changed');
|
|
122
|
+
return { record, bytes };
|
|
123
|
+
}
|
|
124
|
+
/** Written before admission so a lost HTTP response cannot lose the visible message. */
|
|
125
|
+
export function storeAttachmentPresentation(stateDir, value) {
|
|
126
|
+
directory(stateDir, false, false);
|
|
127
|
+
const root = join(stateDir, 'web-attachment-messages');
|
|
128
|
+
directory(root, true);
|
|
129
|
+
immutable(join(root, digest(JSON.stringify([value.generation, value.commandId])) + '.json'), Buffer.from(JSON.stringify(value)));
|
|
130
|
+
}
|
|
131
|
+
export function prepareAttachmentPresentation(stateDir, generation, commandId, ids, text, transport) {
|
|
132
|
+
storeAttachmentPresentation(stateDir, { generation, commandId, ids, text, transportDigest: digest(transport) });
|
|
133
|
+
}
|
|
134
|
+
/** Never infer attachments by stripping user text or interpreting a marker. */
|
|
135
|
+
export function presentAttachmentEvent(stateDir, roleId, event) {
|
|
136
|
+
const e = event;
|
|
137
|
+
if (!stateDir || e.kind !== 'prompt.admitted' || e.source !== 'owner_admin_console' || !e.commandId || e.payload?.text?.redacted)
|
|
138
|
+
return event;
|
|
139
|
+
const root = join(stateDir, 'web-attachment-messages');
|
|
140
|
+
if (!existsSync(root))
|
|
141
|
+
return event;
|
|
142
|
+
const path = join(root, digest(JSON.stringify([e.sessionGeneration, e.commandId])) + '.json');
|
|
143
|
+
if (!existsSync(path))
|
|
144
|
+
return event;
|
|
145
|
+
try {
|
|
146
|
+
directory(stateDir, false, false);
|
|
147
|
+
directory(root);
|
|
148
|
+
const value = JSON.parse(read(path, 1024 * 1024).toString());
|
|
149
|
+
if (value.generation !== e.sessionGeneration || value.commandId !== e.commandId || value.transportDigest !== digest(e.payload.text.text))
|
|
150
|
+
return event;
|
|
151
|
+
const attachments = value.ids.map(id => {
|
|
152
|
+
const record = attachmentMetadata(stateDir, id);
|
|
153
|
+
if (record.generation !== value.generation)
|
|
154
|
+
fail('Attachment session changed');
|
|
155
|
+
return { id, name: record.name, mimeType: record.mimeType, size: record.size, url: `/api/v1/roles/${encodeURIComponent(roleId)}/attachments/${id}` };
|
|
156
|
+
});
|
|
157
|
+
return { ...e, payload: { ...e.payload, text: { type: 'text', text: value.text, bytes: Buffer.byteLength(value.text) }, displayText: { type: 'text', text: value.text, bytes: Buffer.byteLength(value.text) }, attachments } };
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return event;
|
|
161
|
+
}
|
|
162
|
+
}
|
package/dist/web/auth.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { WorkspaceDeviceStore } from './workspace-devices.js';
|
|
1
2
|
import type { FastifyRequest } from 'fastify';
|
|
2
3
|
import type { WebSocket } from 'ws';
|
|
3
4
|
import { TrustedDeviceStore, type TrustedDeviceIssue } from './device-store.js';
|
|
@@ -8,6 +9,8 @@ export interface BrowserSession {
|
|
|
8
9
|
createdAt: number;
|
|
9
10
|
lastSeenAt: number;
|
|
10
11
|
absoluteExpiresAt: number;
|
|
12
|
+
/** A workspace access credential, which cannot create further device links. */
|
|
13
|
+
rotating?: boolean;
|
|
11
14
|
}
|
|
12
15
|
export interface AuthResult {
|
|
13
16
|
session: BrowserSession;
|
|
@@ -19,6 +22,8 @@ interface Ticket {
|
|
|
19
22
|
purpose: 'events' | 'conversation';
|
|
20
23
|
roleId?: string;
|
|
21
24
|
expiresAt: number;
|
|
25
|
+
/** When the credential that minted this ticket stops authorizing; a socket opened with it ends then. */
|
|
26
|
+
authorizedUntil: number;
|
|
22
27
|
}
|
|
23
28
|
export declare class WebAuth {
|
|
24
29
|
private _origin;
|
|
@@ -26,6 +31,8 @@ export declare class WebAuth {
|
|
|
26
31
|
private readonly now;
|
|
27
32
|
private readonly devices;
|
|
28
33
|
private readonly access;
|
|
34
|
+
private readonly workspaceDevices?;
|
|
35
|
+
readonly appOrigin: string;
|
|
29
36
|
private _bootstrapSecret;
|
|
30
37
|
private bootstrapExpiresAt;
|
|
31
38
|
private bootstrapUsed;
|
|
@@ -33,8 +40,10 @@ export declare class WebAuth {
|
|
|
33
40
|
private readonly sessionDevices;
|
|
34
41
|
private readonly tickets;
|
|
35
42
|
private readonly rates;
|
|
43
|
+
private readonly transports;
|
|
44
|
+
private readonly renewals;
|
|
36
45
|
private readonly sockets;
|
|
37
|
-
constructor(_origin: string, _host: string, now?: () => number, devices?: TrustedDeviceStore, access?: WebAccessConfig);
|
|
46
|
+
constructor(_origin: string, _host: string, now?: () => number, devices?: TrustedDeviceStore, access?: WebAccessConfig, workspaceDevices?: WorkspaceDeviceStore | undefined, appOrigin?: string);
|
|
38
47
|
get bootstrapSecret(): string;
|
|
39
48
|
get origin(): string;
|
|
40
49
|
get host(): string;
|
|
@@ -48,21 +57,75 @@ export declare class WebAuth {
|
|
|
48
57
|
}): void;
|
|
49
58
|
/** Mint a replacement for an operator-triggered reauthentication ceremony. */
|
|
50
59
|
mintBootstrap(): string;
|
|
60
|
+
/** The configured account origin, when this request comes from it and workspace devices are enabled. */
|
|
61
|
+
accountOrigin(request: Pick<FastifyRequest, 'headers'>): string | undefined;
|
|
51
62
|
validateBoundary(request: FastifyRequest, requireOrigin: boolean): void;
|
|
52
63
|
exchange(request: FastifyRequest): AuthResult;
|
|
53
64
|
login(request: FastifyRequest, password: string): AuthResult;
|
|
54
65
|
anonymous(request: FastifyRequest): BrowserSession;
|
|
55
66
|
resume(request: FastifyRequest): AuthResult;
|
|
56
67
|
authenticate(request: FastifyRequest, mutation?: boolean): BrowserSession;
|
|
68
|
+
/** Check a transport's opening lease without refreshing cookie idle activity. */
|
|
69
|
+
sessionValid(session: Pick<BrowserSession, 'id' | 'absoluteExpiresAt'>): boolean;
|
|
57
70
|
logout(request: FastifyRequest): void;
|
|
58
71
|
mintTicket(request: FastifyRequest, purpose: Ticket['purpose'], roleId?: string): {
|
|
59
72
|
ticket: string;
|
|
60
73
|
expiresAt: string;
|
|
61
74
|
};
|
|
62
75
|
consumeTicket(request: FastifyRequest, value: string, purpose: Ticket['purpose'], roleId?: string): BrowserSession;
|
|
63
|
-
|
|
76
|
+
/**
|
|
77
|
+
* `session` is what authenticate() or consumeTicket() returned for this connection. A workspace
|
|
78
|
+
* device shares one session id across credentials, so the socket ends at that credential's own
|
|
79
|
+
* deadline; revocation still ends everything the device has open.
|
|
80
|
+
*/
|
|
81
|
+
bindSocket(session: Pick<BrowserSession, 'id' | 'absoluteExpiresAt'>, socket: WebSocket): void;
|
|
82
|
+
/** Raw service streams share the same logout/revocation boundary as native sockets. */
|
|
83
|
+
bindTransport(session: Pick<BrowserSession, 'id' | 'absoluteExpiresAt'>, close: () => void, renewal?: string): () => void;
|
|
84
|
+
/**
|
|
85
|
+
* Keep one open transport past the credential that opened it. Only its own reader knows the
|
|
86
|
+
* name it was given, and it must authenticate with a newer credential of the same device while
|
|
87
|
+
* the transport is still open: a copied credential cannot renew after it has expired.
|
|
88
|
+
*/
|
|
89
|
+
renewTransport(request: FastifyRequest, renewal: string): {
|
|
90
|
+
authorizedUntil: number;
|
|
91
|
+
};
|
|
64
92
|
clearSessions(): void;
|
|
65
93
|
revokeAllTrustedDevices(): number;
|
|
94
|
+
workspaceIdentity(): {
|
|
95
|
+
workspaceId: string;
|
|
96
|
+
};
|
|
97
|
+
workspaceEnrollment(issuer?: string | null): {
|
|
98
|
+
enrollment: string;
|
|
99
|
+
workspaceId: string;
|
|
100
|
+
expiresAt: number;
|
|
101
|
+
};
|
|
102
|
+
enrollWorkspace(request: FastifyRequest, input: {
|
|
103
|
+
enrollment: string;
|
|
104
|
+
workspaceId: string;
|
|
105
|
+
label: string;
|
|
106
|
+
credential?: unknown;
|
|
107
|
+
}): import("./workspace-devices.js").WorkspaceCredentials | {
|
|
108
|
+
token: string;
|
|
109
|
+
device: import("./workspace-devices.js").WorkspaceDevice;
|
|
110
|
+
workspaceId: string;
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* Refresh credentials are exchanged between the account server and this host only. A browser
|
|
114
|
+
* always states where a request comes from, so a request that does is refused: no page can be
|
|
115
|
+
* handed refresh material, whichever origin it runs on.
|
|
116
|
+
*/
|
|
117
|
+
private machineOnly;
|
|
118
|
+
refreshWorkspace(request: FastifyRequest, refreshToken: string): import("./workspace-devices.js").WorkspaceCredentials;
|
|
119
|
+
exchangeWorkspace(request: FastifyRequest): import("./workspace-devices.js").WorkspaceCredentials;
|
|
120
|
+
revokeWorkspaceRefresh(request: FastifyRequest, refreshHash: string): boolean;
|
|
121
|
+
/** A link code for another browser, for the issuing link's own current refresh credential, which it rotates. */
|
|
122
|
+
linkWorkspaceDevice(request: FastifyRequest, refreshToken: string): import("./workspace-devices.js").WorkspaceCredentials & {
|
|
123
|
+
enrollment: string;
|
|
124
|
+
enrollmentExpiresAt: number;
|
|
125
|
+
};
|
|
126
|
+
listWorkspaceDevices(): import("./workspace-devices.js").WorkspaceDevice[];
|
|
127
|
+
revokeWorkspaceDevice(id: string): void;
|
|
128
|
+
private requireWorkspaceDevices;
|
|
66
129
|
shutdown(): void;
|
|
67
130
|
private createSession;
|
|
68
131
|
private removeSession;
|
package/dist/web/auth.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { WorkspaceDeviceAuthError, WorkspaceRefreshReuseError } from './workspace-devices.js';
|
|
1
2
|
import { randomBytes, timingSafeEqual } from 'node:crypto';
|
|
2
3
|
import { FleetError } from '../application/errors.js';
|
|
3
4
|
import { TrustedDeviceStore } from './device-store.js';
|
|
@@ -14,6 +15,8 @@ export class WebAuth {
|
|
|
14
15
|
now;
|
|
15
16
|
devices;
|
|
16
17
|
access;
|
|
18
|
+
workspaceDevices;
|
|
19
|
+
appOrigin;
|
|
17
20
|
_bootstrapSecret = token(32);
|
|
18
21
|
bootstrapExpiresAt = Date.now() + 5 * 60_000;
|
|
19
22
|
bootstrapUsed = false;
|
|
@@ -21,13 +24,17 @@ export class WebAuth {
|
|
|
21
24
|
sessionDevices = new Map();
|
|
22
25
|
tickets = new Map();
|
|
23
26
|
rates = new Map();
|
|
27
|
+
transports = new Map();
|
|
28
|
+
renewals = new Map();
|
|
24
29
|
sockets = new Map();
|
|
25
|
-
constructor(_origin, _host, now = Date.now, devices = new TrustedDeviceStore(), access = { version: 1, mode: 'pairing' }) {
|
|
30
|
+
constructor(_origin, _host, now = Date.now, devices = new TrustedDeviceStore(), access = { version: 1, mode: 'pairing' }, workspaceDevices, appOrigin = 'https://app.ours.network') {
|
|
26
31
|
this._origin = _origin;
|
|
27
32
|
this._host = _host;
|
|
28
33
|
this.now = now;
|
|
29
34
|
this.devices = devices;
|
|
30
35
|
this.access = access;
|
|
36
|
+
this.workspaceDevices = workspaceDevices;
|
|
37
|
+
this.appOrigin = appOrigin;
|
|
31
38
|
}
|
|
32
39
|
get bootstrapSecret() { return this._bootstrapSecret; }
|
|
33
40
|
get origin() { return this._origin; }
|
|
@@ -49,16 +56,24 @@ export class WebAuth {
|
|
|
49
56
|
this.bootstrapUsed = false;
|
|
50
57
|
return this._bootstrapSecret;
|
|
51
58
|
}
|
|
59
|
+
/** The configured account origin, when this request comes from it and workspace devices are enabled. */
|
|
60
|
+
accountOrigin(request) {
|
|
61
|
+
return this.workspaceDevices && request.headers.origin === this.appOrigin ? this.appOrigin : undefined;
|
|
62
|
+
}
|
|
52
63
|
validateBoundary(request, requireOrigin) {
|
|
53
64
|
const host = request.headers.host;
|
|
54
65
|
const hosts = this.allowedHosts.size ? this.allowedHosts : new Set([this.host]);
|
|
55
66
|
const origins = this.allowedOrigins.size ? this.allowedOrigins : new Set([this.origin]);
|
|
56
67
|
if (!host || !hosts.has(host))
|
|
57
68
|
throw new FleetError('forbidden', `This address is not configured for the fleet console. Open ${this.origin} or set --public-origin.`);
|
|
58
|
-
if (
|
|
69
|
+
if (this.workspaceDevices && String(request.headers.authorization ?? '').startsWith('Bearer ') && request.headers.origin && request.headers.origin !== this.appOrigin && !origins.has(request.headers.origin))
|
|
70
|
+
throw new FleetError('forbidden', 'workspace request Origin does not match configured account origin');
|
|
71
|
+
const workspaceRequest = Boolean(this.workspaceDevices) && request.headers.origin === this.appOrigin
|
|
72
|
+
&& (String(request.headers.authorization ?? '').startsWith('Bearer ') || (request.url ?? '').split('?')[0] === '/api/v1/devices/enroll' || request.method === 'OPTIONS');
|
|
73
|
+
if (requireOrigin && !workspaceRequest && (!request.headers.origin || !origins.has(request.headers.origin)))
|
|
59
74
|
throw new FleetError('forbidden', 'request Origin does not match the configured control-panel origin');
|
|
60
75
|
const fetchSite = request.headers['sec-fetch-site'];
|
|
61
|
-
if (fetchSite && !['same-origin', 'none'].includes(String(fetchSite)))
|
|
76
|
+
if (!workspaceRequest && fetchSite && !['same-origin', 'none'].includes(String(fetchSite)))
|
|
62
77
|
throw new FleetError('forbidden', 'cross-site request rejected');
|
|
63
78
|
}
|
|
64
79
|
exchange(request) {
|
|
@@ -100,11 +115,28 @@ export class WebAuth {
|
|
|
100
115
|
return { session: this.createSession(device.id), device };
|
|
101
116
|
}
|
|
102
117
|
authenticate(request, mutation = false) {
|
|
118
|
+
const authorization = String(request.headers.authorization ?? '');
|
|
119
|
+
if (authorization.startsWith('Bearer ')) {
|
|
120
|
+
this.validateBoundary(request, mutation);
|
|
121
|
+
if (!this.workspaceDevices)
|
|
122
|
+
throw new WorkspaceDeviceAuthError('workspace bearer credential is invalid');
|
|
123
|
+
const device = this.requireWorkspaceDevices().authenticate(authorization.slice(7));
|
|
124
|
+
const id = `workspace:${device.id}`;
|
|
125
|
+
// A transport opened with this credential ends when this credential does, whatever replaces it.
|
|
126
|
+
const session = { id, csrf: '', createdAt: device.createdAt, lastSeenAt: this.now(), absoluteExpiresAt: device.authorizedUntil, rotating: device.rotating };
|
|
127
|
+
this.sessions.set(id, session);
|
|
128
|
+
this.sessionDevices.set(id, id);
|
|
129
|
+
if (mutation)
|
|
130
|
+
this.consumeRate(`mutation:${id}`, 120, 60000);
|
|
131
|
+
return session;
|
|
132
|
+
}
|
|
103
133
|
this.validateBoundary(request, mutation);
|
|
134
|
+
if (request.headers.origin === this.appOrigin && !this.allowedOrigins.has(this.appOrigin))
|
|
135
|
+
throw new FleetError('unauthorized', 'workspace bearer credential required');
|
|
104
136
|
const id = parseCookies(request.headers.cookie ?? '').ofs_session;
|
|
105
137
|
const session = id ? this.sessions.get(id) : undefined;
|
|
106
138
|
const now = this.now();
|
|
107
|
-
if (!session ||
|
|
139
|
+
if (!session || !this.sessionValid(session)) {
|
|
108
140
|
if (id)
|
|
109
141
|
this.removeSession(id);
|
|
110
142
|
throw new FleetError('unauthorized', 'browser session is missing or expired');
|
|
@@ -118,10 +150,21 @@ export class WebAuth {
|
|
|
118
150
|
session.lastSeenAt = now;
|
|
119
151
|
return session;
|
|
120
152
|
}
|
|
153
|
+
/** Check a transport's opening lease without refreshing cookie idle activity. */
|
|
154
|
+
sessionValid(session) {
|
|
155
|
+
const current = this.sessions.get(session.id), now = this.now();
|
|
156
|
+
if (!current || now >= session.absoluteExpiresAt)
|
|
157
|
+
return false;
|
|
158
|
+
if (session.id.startsWith('workspace:'))
|
|
159
|
+
return Boolean(this.workspaceDevices?.valid(session.id.slice(10)));
|
|
160
|
+
return now < current.absoluteExpiresAt && now - current.lastSeenAt < 30 * 60_000;
|
|
161
|
+
}
|
|
121
162
|
logout(request) {
|
|
122
163
|
const session = this.authenticate(request, true);
|
|
123
164
|
const deviceId = this.sessionDevices.get(session.id);
|
|
124
|
-
if (
|
|
165
|
+
if (session.id.startsWith('workspace:'))
|
|
166
|
+
this.workspaceDevices?.revoke(session.id.slice(10));
|
|
167
|
+
else if (deviceId && deviceId !== 'unprotected')
|
|
125
168
|
this.devices.revokeId(deviceId);
|
|
126
169
|
this.removeSession(session.id);
|
|
127
170
|
}
|
|
@@ -130,7 +173,8 @@ export class WebAuth {
|
|
|
130
173
|
const value = token();
|
|
131
174
|
const ticket = {
|
|
132
175
|
value, sessionId: session.id, purpose, roleId,
|
|
133
|
-
expiresAt: this.now() + 30_000,
|
|
176
|
+
expiresAt: Math.min(this.now() + 30_000, session.absoluteExpiresAt),
|
|
177
|
+
authorizedUntil: session.absoluteExpiresAt,
|
|
134
178
|
};
|
|
135
179
|
this.tickets.set(value, ticket);
|
|
136
180
|
return { ticket: value, expiresAt: new Date(ticket.expiresAt).toISOString() };
|
|
@@ -143,20 +187,82 @@ export class WebAuth {
|
|
|
143
187
|
|| ticket.roleId !== roleId)
|
|
144
188
|
throw new FleetError('unauthorized', 'WebSocket ticket is invalid, expired, or already used');
|
|
145
189
|
const session = this.sessions.get(ticket.sessionId);
|
|
146
|
-
if (!session)
|
|
190
|
+
if (!session || (session.id.startsWith('workspace:') && !this.workspaceDevices?.valid(session.id.slice(10))))
|
|
147
191
|
throw new FleetError('unauthorized', 'browser session expired');
|
|
148
|
-
|
|
192
|
+
if (!session.id.startsWith('workspace:'))
|
|
193
|
+
return session;
|
|
194
|
+
// A workspace device's session entry is replaced by each newer credential; the ticket carries
|
|
195
|
+
// the deadline of the credential that minted it, and nothing newer extends it.
|
|
196
|
+
if (ticket.authorizedUntil <= this.now())
|
|
197
|
+
throw new FleetError('unauthorized', 'WebSocket ticket is invalid, expired, or already used');
|
|
198
|
+
return { ...session, absoluteExpiresAt: ticket.authorizedUntil };
|
|
149
199
|
}
|
|
150
|
-
|
|
200
|
+
/**
|
|
201
|
+
* `session` is what authenticate() or consumeTicket() returned for this connection. A workspace
|
|
202
|
+
* device shares one session id across credentials, so the socket ends at that credential's own
|
|
203
|
+
* deadline; revocation still ends everything the device has open.
|
|
204
|
+
*/
|
|
205
|
+
bindSocket(session, socket) {
|
|
206
|
+
const sessionId = session.id, deadline = sessionId.startsWith('workspace:') ? session.absoluteExpiresAt : undefined;
|
|
151
207
|
let sockets = this.sockets.get(sessionId);
|
|
152
208
|
if (!sockets) {
|
|
153
209
|
sockets = new Set();
|
|
154
210
|
this.sockets.set(sessionId, sockets);
|
|
155
211
|
}
|
|
156
212
|
sockets.add(socket);
|
|
157
|
-
|
|
213
|
+
const expiry = setInterval(() => { const current = this.sessions.get(sessionId); if (!current || (deadline !== undefined ? !this.workspaceDevices?.valid(sessionId.slice(10)) : current.absoluteExpiresAt <= this.now()))
|
|
214
|
+
this.removeSession(sessionId);
|
|
215
|
+
else if (deadline !== undefined && deadline <= this.now()) {
|
|
216
|
+
clearInterval(expiry);
|
|
217
|
+
socket.close(4401, 'authentication expired');
|
|
218
|
+
} }, 1000);
|
|
219
|
+
expiry.unref();
|
|
220
|
+
socket.once('close', () => { clearInterval(expiry); sockets?.delete(socket); });
|
|
221
|
+
}
|
|
222
|
+
/** Raw service streams share the same logout/revocation boundary as native sockets. */
|
|
223
|
+
bindTransport(session, close, renewal) {
|
|
224
|
+
const sessionId = session.id, workspace = sessionId.startsWith('workspace:');
|
|
225
|
+
let transports = this.transports.get(sessionId);
|
|
226
|
+
if (!transports) {
|
|
227
|
+
transports = new Set();
|
|
228
|
+
this.transports.set(sessionId, transports);
|
|
229
|
+
}
|
|
230
|
+
transports.add(close);
|
|
231
|
+
const lease = { sessionId, deadline: session.absoluteExpiresAt };
|
|
232
|
+
if (workspace && renewal)
|
|
233
|
+
this.renewals.set(renewal, lease);
|
|
234
|
+
const expiry = setInterval(() => { const current = this.sessions.get(sessionId); if (!current || (workspace ? !this.workspaceDevices?.valid(sessionId.slice(10)) : current.absoluteExpiresAt <= this.now()))
|
|
235
|
+
this.removeSession(sessionId);
|
|
236
|
+
else if (workspace && lease.deadline <= this.now()) {
|
|
237
|
+
clearInterval(expiry);
|
|
238
|
+
transports.delete(close);
|
|
239
|
+
if (renewal)
|
|
240
|
+
this.renewals.delete(renewal);
|
|
241
|
+
close();
|
|
242
|
+
} }, 1000);
|
|
243
|
+
expiry.unref();
|
|
244
|
+
return () => { clearInterval(expiry); transports.delete(close); if (renewal)
|
|
245
|
+
this.renewals.delete(renewal); if (!transports.size)
|
|
246
|
+
this.transports.delete(sessionId); };
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Keep one open transport past the credential that opened it. Only its own reader knows the
|
|
250
|
+
* name it was given, and it must authenticate with a newer credential of the same device while
|
|
251
|
+
* the transport is still open: a copied credential cannot renew after it has expired.
|
|
252
|
+
*/
|
|
253
|
+
renewTransport(request, renewal) {
|
|
254
|
+
const session = this.authenticate(request, true), lease = this.renewals.get(renewal);
|
|
255
|
+
if (!lease || lease.sessionId !== session.id || lease.deadline <= this.now())
|
|
256
|
+
throw new FleetError('resource_not_found', 'transport is not open');
|
|
257
|
+
lease.deadline = Math.max(lease.deadline, session.absoluteExpiresAt);
|
|
258
|
+
return { authorizedUntil: lease.deadline };
|
|
158
259
|
}
|
|
159
260
|
clearSessions() {
|
|
261
|
+
for (const transports of this.transports.values())
|
|
262
|
+
for (const close of transports)
|
|
263
|
+
close();
|
|
264
|
+
this.transports.clear();
|
|
265
|
+
this.renewals.clear();
|
|
160
266
|
for (const sockets of this.sockets.values())
|
|
161
267
|
for (const socket of sockets)
|
|
162
268
|
socket.close(4401, 'authentication revoked');
|
|
@@ -167,10 +273,88 @@ export class WebAuth {
|
|
|
167
273
|
}
|
|
168
274
|
revokeAllTrustedDevices() {
|
|
169
275
|
const count = this.devices.revokeAll();
|
|
276
|
+
for (const device of this.workspaceDevices?.list() ?? [])
|
|
277
|
+
this.workspaceDevices?.revoke(device.id);
|
|
170
278
|
this.clearSessions();
|
|
171
279
|
return count;
|
|
172
280
|
}
|
|
173
|
-
|
|
281
|
+
workspaceIdentity() { return { workspaceId: this.requireWorkspaceDevices().workspaceId }; }
|
|
282
|
+
workspaceEnrollment(issuer = null) { return this.requireWorkspaceDevices().mint(issuer?.startsWith('workspace:') ? issuer.slice(10) : null); }
|
|
283
|
+
enrollWorkspace(request, input) {
|
|
284
|
+
if (input.credential === 2) {
|
|
285
|
+
this.machineOnly(request);
|
|
286
|
+
this.consumeRate('workspace-enrollment', 30, 60000);
|
|
287
|
+
return this.requireWorkspaceDevices().enroll(input.enrollment, input.workspaceId, input.label, true);
|
|
288
|
+
}
|
|
289
|
+
this.validateBoundary(request, true);
|
|
290
|
+
this.consumeRate('workspace-enrollment', 30, 60000);
|
|
291
|
+
return this.requireWorkspaceDevices().enroll(input.enrollment, input.workspaceId, input.label);
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Refresh credentials are exchanged between the account server and this host only. A browser
|
|
295
|
+
* always states where a request comes from, so a request that does is refused: no page can be
|
|
296
|
+
* handed refresh material, whichever origin it runs on.
|
|
297
|
+
*/
|
|
298
|
+
machineOnly(request) {
|
|
299
|
+
this.validateBoundary(request, false);
|
|
300
|
+
if (request.headers.origin !== undefined || Object.keys(request.headers).some(name => name.startsWith('sec-fetch-')) || request.headers.cookie !== undefined)
|
|
301
|
+
throw new FleetError('forbidden', 'this endpoint does not accept browser requests');
|
|
302
|
+
}
|
|
303
|
+
refreshWorkspace(request, refreshToken) {
|
|
304
|
+
this.machineOnly(request);
|
|
305
|
+
this.consumeRate('workspace-refresh', 240, 60000);
|
|
306
|
+
try {
|
|
307
|
+
return this.requireWorkspaceDevices().refresh(refreshToken);
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
if (error instanceof WorkspaceRefreshReuseError)
|
|
311
|
+
for (const id of error.revoked)
|
|
312
|
+
this.removeSession(`workspace:${id}`);
|
|
313
|
+
throw error;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
exchangeWorkspace(request) {
|
|
317
|
+
this.machineOnly(request);
|
|
318
|
+
this.consumeRate('workspace-enrollment', 30, 60000);
|
|
319
|
+
const authorization = String(request.headers.authorization ?? '');
|
|
320
|
+
const issued = this.requireWorkspaceDevices().exchange(authorization.startsWith('Bearer ') ? authorization.slice(7) : '');
|
|
321
|
+
// Whatever the fixed bearer had open ends with it.
|
|
322
|
+
this.removeSession(`workspace:${issued.device.id}`);
|
|
323
|
+
return issued;
|
|
324
|
+
}
|
|
325
|
+
revokeWorkspaceRefresh(request, refreshHash) {
|
|
326
|
+
this.machineOnly(request);
|
|
327
|
+
this.consumeRate('workspace-revoke', 120, 60000);
|
|
328
|
+
const id = this.requireWorkspaceDevices().revokeByRefreshHash(refreshHash);
|
|
329
|
+
if (id)
|
|
330
|
+
this.removeSession(`workspace:${id}`);
|
|
331
|
+
return Boolean(id);
|
|
332
|
+
}
|
|
333
|
+
/** A link code for another browser, for the issuing link's own current refresh credential, which it rotates. */
|
|
334
|
+
linkWorkspaceDevice(request, refreshToken) {
|
|
335
|
+
this.machineOnly(request);
|
|
336
|
+
this.consumeRate('workspace-link', 10, 60000);
|
|
337
|
+
try {
|
|
338
|
+
return this.requireWorkspaceDevices().link(refreshToken);
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
if (error instanceof WorkspaceRefreshReuseError)
|
|
342
|
+
for (const id of error.revoked)
|
|
343
|
+
this.removeSession(`workspace:${id}`);
|
|
344
|
+
throw error;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
listWorkspaceDevices() { return this.requireWorkspaceDevices().list(); }
|
|
348
|
+
revokeWorkspaceDevice(id) {
|
|
349
|
+
this.requireWorkspaceDevices().revoke(id);
|
|
350
|
+
this.removeSession(`workspace:${id}`);
|
|
351
|
+
}
|
|
352
|
+
requireWorkspaceDevices() {
|
|
353
|
+
if (!this.workspaceDevices)
|
|
354
|
+
throw new FleetError('capability_unavailable', 'workspace device access is not configured');
|
|
355
|
+
return this.workspaceDevices;
|
|
356
|
+
}
|
|
357
|
+
shutdown() { this.clearSessions(); this.workspaceDevices?.close(); }
|
|
174
358
|
createSession(deviceId) {
|
|
175
359
|
const now = this.now();
|
|
176
360
|
const session = {
|
|
@@ -182,6 +366,12 @@ export class WebAuth {
|
|
|
182
366
|
return session;
|
|
183
367
|
}
|
|
184
368
|
removeSession(id) {
|
|
369
|
+
for (const close of this.transports.get(id) ?? [])
|
|
370
|
+
close();
|
|
371
|
+
this.transports.delete(id);
|
|
372
|
+
for (const [renewal, lease] of this.renewals)
|
|
373
|
+
if (lease.sessionId === id)
|
|
374
|
+
this.renewals.delete(renewal);
|
|
185
375
|
this.sessions.delete(id);
|
|
186
376
|
this.sessionDevices.delete(id);
|
|
187
377
|
for (const [ticket, value] of this.tickets)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createServer } from 'node:net';
|
|
2
|
+
/** Bind the preferred loopback port; only EADDRINUSE permits an OS-selected replacement. */
|
|
3
|
+
export async function availableWebPort(preferred) {
|
|
4
|
+
const server = createServer();
|
|
5
|
+
const listen = (port) => new Promise((resolve, reject) => {
|
|
6
|
+
server.once('error', reject);
|
|
7
|
+
server.listen(port, '127.0.0.1', () => { server.removeListener('error', reject); resolve(); });
|
|
8
|
+
});
|
|
9
|
+
try {
|
|
10
|
+
await listen(preferred);
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
if (error.code !== 'EADDRINUSE')
|
|
14
|
+
throw error;
|
|
15
|
+
await listen(0);
|
|
16
|
+
}
|
|
17
|
+
const address = server.address();
|
|
18
|
+
if (!address || typeof address === 'string')
|
|
19
|
+
throw Error('No loopback port selected');
|
|
20
|
+
const port = address.port;
|
|
21
|
+
await new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve()));
|
|
22
|
+
return port;
|
|
23
|
+
}
|
package/dist/web/control.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type WebControlCommand = '
|
|
1
|
+
export type WebControlCommand = 'revoke-all' | 'link-device';
|
|
2
2
|
export declare const webControlPath: (dir?: string) => string;
|
|
3
3
|
export interface WebControlServer {
|
|
4
4
|
path: string;
|
|
@@ -6,9 +6,9 @@ export interface WebControlServer {
|
|
|
6
6
|
}
|
|
7
7
|
export declare function startWebControlServer(options: {
|
|
8
8
|
dir?: string;
|
|
9
|
-
|
|
9
|
+
onLinkDevice?(): Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
10
10
|
onRevokeAll(): void | Promise<void>;
|
|
11
11
|
now?: () => number;
|
|
12
12
|
rateLimit?: number;
|
|
13
13
|
}): Promise<WebControlServer>;
|
|
14
|
-
export declare function requestWebControl(command: WebControlCommand, path?: string, timeoutMs?: number): Promise<void>;
|
|
14
|
+
export declare function requestWebControl(command: WebControlCommand, path?: string, timeoutMs?: number): Promise<Record<string, unknown> | void>;
|