@ours.network/fleet 1.2.0-nightly.8 → 1.2.0-nightly.80
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 +5 -0
- package/dist/account-origin.js +31 -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 +294 -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 +43 -0
- package/dist/application/resource-observer.js +323 -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 +63 -17
- package/dist/application/session-control.js +221 -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 +31 -0
- package/dist/notifications/fleet-producer.js +351 -0
- package/dist/notifications/outbox.d.ts +59 -0
- package/dist/notifications/outbox.js +199 -0
- package/dist/notifications/target-cleanup.d.ts +16 -0
- package/dist/notifications/target-cleanup.js +75 -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 +5 -0
- package/dist/paths.js +5 -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 +4 -0
- package/dist/rooms-tasks/archived-absence.js +11 -0
- package/dist/rooms-tasks/cli.js +65 -5
- package/dist/rooms-tasks/close.d.ts +5 -2
- package/dist/rooms-tasks/close.js +224 -36
- 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 +63 -10
- 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 +43 -5
- package/dist/rooms-tasks/task-state.js +244 -22
- package/dist/rooms-tasks/types.d.ts +35 -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 +219 -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 +213 -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 +130 -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 +26 -0
- package/dist/web/conversation-stream.js +239 -0
- package/dist/web/events.d.ts +2 -0
- package/dist/web/events.js +9 -0
- package/dist/web/fleet-config-service.d.ts +7 -1
- package/dist/web/fleet-config-service.js +61 -16
- package/dist/web/prefix-gateway.d.ts +32 -0
- package/dist/web/prefix-gateway.js +248 -0
- package/dist/web/resource-views.d.ts +135 -0
- package/dist/web/resource-views.js +486 -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 +821 -68
- 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 +605 -0
- package/dist/workspace-replacement.d.ts +66 -0
- package/dist/workspace-replacement.js +238 -0
- package/docs/acp-files.md +146 -0
- package/docs/conversation-stream.md +216 -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,351 @@
|
|
|
1
|
+
import { observeFleetResources } from '../application/resource-observer.js';
|
|
2
|
+
import { roomsDir } from '../rooms-tasks/room-state.js';
|
|
3
|
+
import { cleanupDir, drainNotificationTargetCleanup, notificationAgentId, hasPendingNotificationTargetCleanup } from './target-cleanup.js';
|
|
4
|
+
import { closeSync, existsSync, lstatSync, openSync, readSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
5
|
+
import { basename, join, relative, resolve, sep } from 'node:path';
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
import { NotificationOutbox } from './outbox.js';
|
|
8
|
+
const BATCH_BYTES = 256 * 1024;
|
|
9
|
+
const TURN_ROLES = 4, TURN_MS = 8;
|
|
10
|
+
const RUNTIME_FILES = new Set(['.monitor-status', '.monitor-state.json', '.notify-cursor', '.session-events.jsonl', '.session-events.jsonl.1', '.subscription-usage.json']);
|
|
11
|
+
/** Unlike a UI inventory, unread classification must not treat unreadable records as absence. */
|
|
12
|
+
function taskRoleAssociations() {
|
|
13
|
+
const dir = roomsDir(), result = new Map();
|
|
14
|
+
let files;
|
|
15
|
+
try {
|
|
16
|
+
files = readdirSync(dir);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (error.code === 'ENOENT')
|
|
20
|
+
return result;
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
for (const name of files.filter(name => name.endsWith('.json'))) {
|
|
24
|
+
const file = join(dir, name), stat = lstatSync(file);
|
|
25
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1024 * 1024)
|
|
26
|
+
throw Error('Unreadable room association');
|
|
27
|
+
const room = JSON.parse(readFileSync(file, 'utf8'));
|
|
28
|
+
if (!room || typeof room.room_id !== 'string' || !Array.isArray(room.member_seats))
|
|
29
|
+
throw Error('Invalid room association');
|
|
30
|
+
if (room.task_id === undefined)
|
|
31
|
+
continue;
|
|
32
|
+
if (typeof room.task_id !== 'string' || !/^[A-Za-z0-9_-]{1,120}$/.test(room.task_id))
|
|
33
|
+
throw Error('Invalid task association');
|
|
34
|
+
for (const seat of room.member_seats) {
|
|
35
|
+
if (typeof seat.role_name !== 'string')
|
|
36
|
+
throw Error('Invalid member association');
|
|
37
|
+
if (result.has(seat.role_name) && result.get(seat.role_name) !== room.task_id)
|
|
38
|
+
throw Error('Ambiguous task association');
|
|
39
|
+
result.set(seat.role_name, room.task_id);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
/** Read durable supervisor ledgers without restarting agents or attaching a
|
|
45
|
+
* conversation controller. Each tick reads at most 256 KiB per role, from its
|
|
46
|
+
* checkpoint; it never loads a complete transcript or requests model history. */
|
|
47
|
+
export class FleetNotificationProducer {
|
|
48
|
+
roots;
|
|
49
|
+
stateDir;
|
|
50
|
+
config;
|
|
51
|
+
warn;
|
|
52
|
+
queues = new Map();
|
|
53
|
+
observation = new AbortController();
|
|
54
|
+
continuation;
|
|
55
|
+
continuationAt = Infinity;
|
|
56
|
+
dirty = new Set();
|
|
57
|
+
retryAt = new Map();
|
|
58
|
+
discoveryNeeded = true;
|
|
59
|
+
taskRoles;
|
|
60
|
+
stopped = false;
|
|
61
|
+
existing = new Map();
|
|
62
|
+
cleanup;
|
|
63
|
+
constructor(roots, stateDir, config, warn = () => { }) {
|
|
64
|
+
this.roots = roots;
|
|
65
|
+
this.stateDir = stateDir;
|
|
66
|
+
this.config = config;
|
|
67
|
+
this.warn = warn;
|
|
68
|
+
let healthy = true;
|
|
69
|
+
observeFleetResources({ retryWatchFailures: true, roots, paths: [...roots, roomsDir(), cleanupDir()],
|
|
70
|
+
onInvalidated: path => this.accept(path) }, state => {
|
|
71
|
+
if (state.health !== 'healthy') {
|
|
72
|
+
healthy = false;
|
|
73
|
+
this.warn('Notification discovery observation unavailable; checkpoint retained');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (!healthy || this.dirty.size || this.discoveryNeeded) {
|
|
77
|
+
// Unknown wakes and watch reinstallation advance coverage without a
|
|
78
|
+
// pathname. Keep one conservative discovery pass for those barriers.
|
|
79
|
+
if (!healthy)
|
|
80
|
+
this.discoveryNeeded = true;
|
|
81
|
+
healthy = true;
|
|
82
|
+
this.resume();
|
|
83
|
+
}
|
|
84
|
+
}, this.observation.signal);
|
|
85
|
+
// Freeze startup history boundaries before deferring any role's content
|
|
86
|
+
// work. Later appends/segments must not become part of the ignored history.
|
|
87
|
+
// This pass reads directory/file metadata only, never ledger contents.
|
|
88
|
+
for (const root of roots)
|
|
89
|
+
for (const name of this.names(root)) {
|
|
90
|
+
const roleDir = join(root, name), dir = join(roleDir, '.conversation');
|
|
91
|
+
try {
|
|
92
|
+
if (!existsSync(dir) || lstatSync(dir).isSymbolicLink())
|
|
93
|
+
continue;
|
|
94
|
+
const segment = this.segments(dir).at(-1);
|
|
95
|
+
if (segment)
|
|
96
|
+
this.existing.set(roleDir, { segment, offset: statSync(join(dir, segment)).size, prompts: [] });
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
this.existing.set(roleDir, null);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
this.poll();
|
|
103
|
+
}
|
|
104
|
+
names(root) {
|
|
105
|
+
try {
|
|
106
|
+
return readdirSync(root, { withFileTypes: true }).filter(d => d.isDirectory() && /^[a-zA-Z0-9_-]+$/.test(d.name)).map(d => d.name);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
segments(dir) {
|
|
113
|
+
return readdirSync(dir).filter(n => /^events-\d{6}\.jsonl$/.test(n) && !lstatSync(join(dir, n)).isSymbolicLink()).sort();
|
|
114
|
+
}
|
|
115
|
+
poll() {
|
|
116
|
+
if (this.stopped)
|
|
117
|
+
return;
|
|
118
|
+
this.discoveryNeeded = true;
|
|
119
|
+
this.flush();
|
|
120
|
+
}
|
|
121
|
+
accept(path) {
|
|
122
|
+
if (path === undefined) {
|
|
123
|
+
this.discoveryNeeded = true;
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
for (const root of this.roots) {
|
|
127
|
+
const local = relative(resolve(root), path), parts = local.split(sep);
|
|
128
|
+
if (local === '..' || local.startsWith('..' + sep))
|
|
129
|
+
continue;
|
|
130
|
+
if (parts.length === 2 && RUNTIME_FILES.has(parts[1]))
|
|
131
|
+
return false;
|
|
132
|
+
if (parts.length >= 2 && parts[1] === '.conversation') {
|
|
133
|
+
if (parts.length <= 3) {
|
|
134
|
+
this.dirty.add(join(root, parts[0]));
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Role replacement, unknown paths and metadata retain discovery. Room
|
|
139
|
+
// association and target-cleanup paths take the same conservative route.
|
|
140
|
+
this.discoveryNeeded = true;
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
this.discoveryNeeded = true;
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
flush() {
|
|
147
|
+
if (this.stopped)
|
|
148
|
+
return;
|
|
149
|
+
this.cleanup ??= drainNotificationTargetCleanup(this.config, undefined, undefined, { beforeDelete: async (target) => {
|
|
150
|
+
const name = notificationAgentId(target);
|
|
151
|
+
if (!name || !/^[A-Za-z0-9_-]+$/.test(name))
|
|
152
|
+
return false;
|
|
153
|
+
// A recreated or retained role still has a valid target. Filesystem errors cannot prove removal.
|
|
154
|
+
for (const root of this.roots) {
|
|
155
|
+
try {
|
|
156
|
+
lstatSync(join(root, name));
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
if (error.code !== 'ENOENT')
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
for (const root of this.roots) {
|
|
165
|
+
const roleDir = join(root, name), queue = this.queues.get(roleDir);
|
|
166
|
+
if (queue) {
|
|
167
|
+
await queue.retire();
|
|
168
|
+
this.queues.delete(roleDir);
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
const file = join(this.stateDir, createHash('sha256').update(roleDir).digest('hex') + '.json');
|
|
172
|
+
if (existsSync(file)) {
|
|
173
|
+
const queue = new NotificationOutbox(file, this.config, async () => { throw Error('retired target'); });
|
|
174
|
+
try {
|
|
175
|
+
await queue.retire();
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
await queue.close();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return true;
|
|
184
|
+
} }).catch(() => { }).finally(() => {
|
|
185
|
+
this.cleanup = undefined;
|
|
186
|
+
try {
|
|
187
|
+
if (hasPendingNotificationTargetCleanup())
|
|
188
|
+
this.resume(3000);
|
|
189
|
+
}
|
|
190
|
+
catch { }
|
|
191
|
+
});
|
|
192
|
+
if (this.discoveryNeeded) {
|
|
193
|
+
this.discoveryNeeded = false;
|
|
194
|
+
try {
|
|
195
|
+
this.taskRoles = taskRoleAssociations();
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
this.taskRoles = undefined;
|
|
199
|
+
this.warn('Notification task association unavailable; delivering unclassified events');
|
|
200
|
+
}
|
|
201
|
+
for (const root of this.roots)
|
|
202
|
+
for (const name of this.names(root))
|
|
203
|
+
this.dirty.add(join(root, name));
|
|
204
|
+
}
|
|
205
|
+
// A turn reads at most four 256KiB batches (1MiB). The time bound is checked
|
|
206
|
+
// between roles; a synchronous filesystem operation cannot be preempted.
|
|
207
|
+
const started = performance.now();
|
|
208
|
+
let roles = 0;
|
|
209
|
+
for (const roleDir of this.dirty) {
|
|
210
|
+
if (roles >= TURN_ROLES || (roles && performance.now() - started >= TURN_MS))
|
|
211
|
+
break;
|
|
212
|
+
if ((this.retryAt.get(roleDir) ?? 0) > Date.now())
|
|
213
|
+
continue;
|
|
214
|
+
this.dirty.delete(roleDir);
|
|
215
|
+
this.retryAt.delete(roleDir);
|
|
216
|
+
roles++;
|
|
217
|
+
this.scanRole(roleDir, started + TURN_MS);
|
|
218
|
+
}
|
|
219
|
+
if (this.dirty.size) {
|
|
220
|
+
let next = Infinity;
|
|
221
|
+
for (const roleDir of this.dirty)
|
|
222
|
+
next = Math.min(next, this.retryAt.get(roleDir) ?? Date.now());
|
|
223
|
+
this.resume(Math.max(0, next - Date.now()));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
scanRole(roleDir, deadline) {
|
|
227
|
+
const roleId = basename(roleDir), dir = join(roleDir, '.conversation');
|
|
228
|
+
try {
|
|
229
|
+
if (!existsSync(dir) || lstatSync(dir).isSymbolicLink())
|
|
230
|
+
return;
|
|
231
|
+
const segments = this.segments(dir);
|
|
232
|
+
if (!segments.length)
|
|
233
|
+
return;
|
|
234
|
+
let outbox = this.queues.get(roleDir);
|
|
235
|
+
if (!outbox) {
|
|
236
|
+
const key = createHash('sha256').update(roleDir).digest('hex');
|
|
237
|
+
outbox = new NotificationOutbox(join(this.stateDir, key + '.json'), this.config, async (value, eventId) => ({ eventId, title: `${roleId}: agent completed`, body: 'Your agent finished responding. Open the chat to view the result.',
|
|
238
|
+
...value, target: { kind: 'agent', agentId: roleId } }), this.warn);
|
|
239
|
+
this.queues.set(roleDir, outbox);
|
|
240
|
+
}
|
|
241
|
+
if (!outbox.checkpoint) {
|
|
242
|
+
const initial = this.existing.get(roleDir);
|
|
243
|
+
if (initial === null)
|
|
244
|
+
throw Error('Notification startup ledger boundary unavailable');
|
|
245
|
+
outbox.saveCheckpoint(initial ?? { segment: segments[0], offset: 0, prompts: [] });
|
|
246
|
+
}
|
|
247
|
+
const saved = outbox.checkpoint;
|
|
248
|
+
const current = { ...saved, prompts: [...saved.prompts], messages: { ...saved.messages } };
|
|
249
|
+
let index = segments.indexOf(current.segment);
|
|
250
|
+
if (index < 0) {
|
|
251
|
+
this.warn('Notification ledger segment unavailable; checkpoint retained');
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const file = join(dir, current.segment), size = statSync(file).size;
|
|
255
|
+
if (size < current.offset) {
|
|
256
|
+
this.warn('Notification ledger truncated; checkpoint retained');
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
let remaining = size - current.offset;
|
|
260
|
+
while (remaining === 0 && index < segments.length - 1) {
|
|
261
|
+
current.segment = segments[++index];
|
|
262
|
+
current.offset = 0;
|
|
263
|
+
remaining = statSync(join(dir, current.segment)).size;
|
|
264
|
+
}
|
|
265
|
+
if (remaining === 0) {
|
|
266
|
+
if (current.segment !== saved.segment)
|
|
267
|
+
outbox.saveCheckpoint(current);
|
|
268
|
+
return; // Latest EOF: no ledger open, allocation or read.
|
|
269
|
+
}
|
|
270
|
+
const fd = openSync(join(dir, current.segment), 'r'), buffer = Buffer.alloc(Math.min(BATCH_BYTES, remaining));
|
|
271
|
+
let count;
|
|
272
|
+
try {
|
|
273
|
+
count = readSync(fd, buffer, 0, buffer.length, current.offset);
|
|
274
|
+
}
|
|
275
|
+
finally {
|
|
276
|
+
closeSync(fd);
|
|
277
|
+
}
|
|
278
|
+
let position = 0, blocked = false;
|
|
279
|
+
while (position < count) {
|
|
280
|
+
const end = buffer.indexOf(10, position);
|
|
281
|
+
if (end < 0 || end >= count) {
|
|
282
|
+
// Oversized model/media events are irrelevant to prompt/completion
|
|
283
|
+
// correlation. Skip in bounded chunks; retain partial small lines.
|
|
284
|
+
if (current.discarding || (position === 0 && count === BATCH_BYTES)) {
|
|
285
|
+
current.offset += count - position;
|
|
286
|
+
current.discarding = true;
|
|
287
|
+
this.warn('Notification observer skipping oversized ledger record');
|
|
288
|
+
}
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
if (current.discarding) {
|
|
292
|
+
current.offset += end - position + 1;
|
|
293
|
+
position = end + 1;
|
|
294
|
+
current.discarding = false;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
const event = JSON.parse(buffer.subarray(position, end).toString('utf8'));
|
|
298
|
+
if (event.schemaVersion !== 1 || event.roleId !== roleId)
|
|
299
|
+
throw new Error('invalid ledger event');
|
|
300
|
+
const prompt = `${event.sessionGeneration}:${event.promptId}`;
|
|
301
|
+
if (event.kind === 'prompt.admitted' && event.promptId && ['browser', 'owner_admin_console', 'owner_channel'].includes(event.source ?? '')) {
|
|
302
|
+
if (!current.prompts.includes(prompt))
|
|
303
|
+
current.prompts.push(prompt);
|
|
304
|
+
}
|
|
305
|
+
if (['message.chunk', 'message.replace'].includes(event.kind) && event.promptId && event.messageId && current.prompts.includes(prompt))
|
|
306
|
+
current.messages[prompt] = event.messageId;
|
|
307
|
+
if (event.kind === 'turn.completed' && event.promptId && current.prompts.includes(prompt)) {
|
|
308
|
+
if (!outbox.enqueue(`${roleId}:${event.sessionGeneration}:${event.seq}`, { ...(this.taskRoles ? { taskId: this.taskRoles.get(roleId) ?? null } : {}), anchor: { kind: 'fleet-message', messageId: current.messages[prompt] ?? event.promptId } })) {
|
|
309
|
+
blocked = true;
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
current.prompts = current.prompts.filter(p => p !== prompt);
|
|
313
|
+
delete current.messages[prompt];
|
|
314
|
+
}
|
|
315
|
+
current.offset += end - position + 1;
|
|
316
|
+
position = end + 1;
|
|
317
|
+
if (performance.now() >= deadline)
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
if (current.segment !== saved.segment || current.offset !== saved.offset)
|
|
321
|
+
outbox.saveCheckpoint(current);
|
|
322
|
+
if (blocked) {
|
|
323
|
+
this.retryAt.set(roleDir, Date.now() + 1000);
|
|
324
|
+
this.dirty.add(roleDir);
|
|
325
|
+
}
|
|
326
|
+
else if ((current.segment !== saved.segment || current.offset !== saved.offset)
|
|
327
|
+
&& (current.offset < statSync(join(dir, current.segment)).size || index < segments.length - 1))
|
|
328
|
+
this.dirty.add(roleDir);
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
this.warn('Notification ledger admission pending; checkpoint will replay');
|
|
332
|
+
// Retained, not-yet-admitted ledger work needs its own next-due wake;
|
|
333
|
+
// the outbox sender can retry only events that were already admitted.
|
|
334
|
+
this.retryAt.set(roleDir, Date.now() + 1000);
|
|
335
|
+
this.dirty.add(roleDir);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
resume(delay = 0) {
|
|
339
|
+
if (this.stopped)
|
|
340
|
+
return;
|
|
341
|
+
const at = Date.now() + delay;
|
|
342
|
+
if (this.continuation && this.continuationAt <= at)
|
|
343
|
+
return;
|
|
344
|
+
clearTimeout(this.continuation);
|
|
345
|
+
this.continuationAt = at;
|
|
346
|
+
this.continuation = setTimeout(() => { this.continuation = undefined; this.continuationAt = Infinity; this.flush(); }, delay);
|
|
347
|
+
this.continuation.unref();
|
|
348
|
+
}
|
|
349
|
+
async drain() { await Promise.all([...this.queues.values()].map(q => q.drain())); }
|
|
350
|
+
async close() { this.stopped = true; this.observation.abort(); clearTimeout(this.continuation); await this.cleanup; await Promise.all([...this.queues.values()].map(q => q.close())); }
|
|
351
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export type NotificationTarget = {
|
|
2
|
+
kind: 'agent';
|
|
3
|
+
agentId: string;
|
|
4
|
+
} | {
|
|
5
|
+
kind: 'task' | 'task-deleted';
|
|
6
|
+
taskId: string;
|
|
7
|
+
};
|
|
8
|
+
export interface NotificationInput {
|
|
9
|
+
eventId: string;
|
|
10
|
+
title: string;
|
|
11
|
+
body: string;
|
|
12
|
+
target: NotificationTarget;
|
|
13
|
+
anchor?: {
|
|
14
|
+
kind: 'fleet-message';
|
|
15
|
+
messageId: string;
|
|
16
|
+
};
|
|
17
|
+
taskId?: string | null;
|
|
18
|
+
}
|
|
19
|
+
/** Only the closed typed contract is sent; any other payload is never sent or converted. */
|
|
20
|
+
export declare function isNotificationInput(value: unknown): value is NotificationInput;
|
|
21
|
+
export interface ProducerConfig {
|
|
22
|
+
/** Service base URL. With a gateway credential it is the server gateway's notifications route. */
|
|
23
|
+
origin: string;
|
|
24
|
+
token: string;
|
|
25
|
+
/** Daemon API credential that authenticates this client to the server gateway. */
|
|
26
|
+
gatewayCredential?: string;
|
|
27
|
+
}
|
|
28
|
+
type ProfileLocation = {
|
|
29
|
+
serverUrl: string;
|
|
30
|
+
expectedInstanceId: string;
|
|
31
|
+
credentialPath: string;
|
|
32
|
+
};
|
|
33
|
+
/** The installer delivers the producer credential beside the profile credential, bound to that server installation. */
|
|
34
|
+
export declare const PRODUCER_CREDENTIAL_FILE = "notifications-producer.json";
|
|
35
|
+
export declare function producerConfig(env?: NodeJS.ProcessEnv, profile?: () => ProfileLocation | undefined, warn?: (message: string) => void): ProducerConfig | undefined;
|
|
36
|
+
/** Durable producer queue. Service dedupe makes crash-after-acceptance replay safe. */
|
|
37
|
+
export declare class NotificationOutbox {
|
|
38
|
+
private readonly file;
|
|
39
|
+
private readonly config;
|
|
40
|
+
private readonly project;
|
|
41
|
+
private readonly warn;
|
|
42
|
+
private entries;
|
|
43
|
+
cursor?: number;
|
|
44
|
+
checkpoint?: unknown;
|
|
45
|
+
private running?;
|
|
46
|
+
private retired;
|
|
47
|
+
private timer;
|
|
48
|
+
constructor(file: string, config: ProducerConfig, project: (value: unknown, id: string) => Promise<NotificationInput>, warn?: (message: string) => void);
|
|
49
|
+
private persist;
|
|
50
|
+
advance(cursor: number): void;
|
|
51
|
+
saveCheckpoint(checkpoint: unknown): void;
|
|
52
|
+
enqueue(id: string, value: unknown, cursor?: number | undefined): boolean;
|
|
53
|
+
drain(): Promise<void>;
|
|
54
|
+
private drainOnce;
|
|
55
|
+
/** Wait for the current send before clearing queued sends and retiring its target. */
|
|
56
|
+
retire(): Promise<void>;
|
|
57
|
+
close(): Promise<void>;
|
|
58
|
+
}
|
|
59
|
+
export {};
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { readPrivateFile } from '@ours.network/sdk/connector';
|
|
4
|
+
import { readClientProfile } from '../client-profile.js';
|
|
5
|
+
const object = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
|
|
6
|
+
const closed = (value, keys) => Object.keys(value).every(key => keys.includes(key));
|
|
7
|
+
const domainId = (id) => typeof id === 'string' && /^[A-Za-z0-9_-]{1,120}$/.test(id);
|
|
8
|
+
const text = (value, max) => typeof value === 'string' && !!value.trim() && value.length <= max;
|
|
9
|
+
/** Only the closed typed contract is sent; any other payload is never sent or converted. */
|
|
10
|
+
export function isNotificationInput(value) {
|
|
11
|
+
if (!object(value) || !closed(value, ['eventId', 'title', 'body', 'target', 'anchor', 'taskId'])
|
|
12
|
+
|| !text(value.eventId, 512) || !text(value.title, 160) || !text(value.body, 512) || !object(value.target))
|
|
13
|
+
return false;
|
|
14
|
+
const target = value.target;
|
|
15
|
+
if (target.kind === 'agent') {
|
|
16
|
+
if (!closed(target, ['kind', 'agentId']) || !domainId(target.agentId))
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
else if (target.kind === 'task' || target.kind === 'task-deleted') {
|
|
20
|
+
if (!closed(target, ['kind', 'taskId']) || !domainId(target.taskId) || (value.taskId !== undefined && value.taskId !== target.taskId))
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
else
|
|
24
|
+
return false;
|
|
25
|
+
if (value.taskId !== undefined && value.taskId !== null && !domainId(value.taskId))
|
|
26
|
+
return false;
|
|
27
|
+
return value.anchor === undefined || (target.kind === 'agent' && object(value.anchor)
|
|
28
|
+
&& closed(value.anchor, ['kind', 'messageId']) && value.anchor.kind === 'fleet-message' && text(value.anchor.messageId, 1024) && !/[\x00-\x1f\x7f]/.test(value.anchor.messageId));
|
|
29
|
+
}
|
|
30
|
+
const has = (entry, key) => Object.hasOwn(entry, key);
|
|
31
|
+
// An entry is sent only when it is an object with a string ID and its frozen payload is the closed
|
|
32
|
+
// typed contract or, with no payload field at all, it carries the current version marker. Anything
|
|
33
|
+
// else, including null or scalar entries, is never read further, projected, sent or changed.
|
|
34
|
+
const supported = (entry) => object(entry) && typeof entry.id === 'string' && !has(entry, 'operation')
|
|
35
|
+
&& (has(entry, 'payload') ? isNotificationInput(entry.payload) : entry.targetVersion === 1);
|
|
36
|
+
const same = (id) => (entry) => supported(entry) && entry.id === id;
|
|
37
|
+
function defaultProfile() {
|
|
38
|
+
try {
|
|
39
|
+
return readClientProfile();
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** The installer delivers the producer credential beside the profile credential, bound to that server installation. */
|
|
46
|
+
export const PRODUCER_CREDENTIAL_FILE = 'notifications-producer.json';
|
|
47
|
+
const loopback = (url) => ['127.0.0.1', '[::1]', 'localhost'].includes(url.hostname);
|
|
48
|
+
export function producerConfig(env = process.env, profile = defaultProfile, warn = () => { }) {
|
|
49
|
+
const origin = env.OURS_NOTIFICATIONS_ORIGIN, token = env.OURS_NOTIFICATIONS_PRODUCER_TOKEN;
|
|
50
|
+
if (origin || token) {
|
|
51
|
+
if (!origin || !token || token.length < 32)
|
|
52
|
+
throw new Error('notifications producer requires origin and scoped token');
|
|
53
|
+
const url = new URL(origin);
|
|
54
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.origin !== origin || url.username || url.password)
|
|
55
|
+
throw new Error('invalid notification service origin');
|
|
56
|
+
if (url.protocol === 'http:' && !loopback(url))
|
|
57
|
+
throw new Error('notification credentials require HTTPS or loopback');
|
|
58
|
+
return { origin, token };
|
|
59
|
+
}
|
|
60
|
+
const selected = profile();
|
|
61
|
+
if (!selected)
|
|
62
|
+
return;
|
|
63
|
+
const file = join(dirname(selected.credentialPath), PRODUCER_CREDENTIAL_FILE);
|
|
64
|
+
if (!existsSync(file))
|
|
65
|
+
return;
|
|
66
|
+
// Reasons are fixed text: parser and file errors can quote the secret-bearing contents.
|
|
67
|
+
let reason = 'it is not a private owner-only file';
|
|
68
|
+
try {
|
|
69
|
+
const text = readPrivateFile(file, 4096).toString('utf8');
|
|
70
|
+
reason = 'it is not valid producer JSON';
|
|
71
|
+
const bound = JSON.parse(text);
|
|
72
|
+
reason = 'it belongs to another server installation';
|
|
73
|
+
if (!bound || bound.schema !== 1 || bound.serverUrl !== selected.serverUrl || bound.expectedInstanceId !== selected.expectedInstanceId)
|
|
74
|
+
throw new Error();
|
|
75
|
+
reason = 'its credential is invalid';
|
|
76
|
+
if (typeof bound.token !== 'string' || bound.token.length < 32)
|
|
77
|
+
throw new Error();
|
|
78
|
+
reason = 'the server requires HTTPS or loopback';
|
|
79
|
+
const server = new URL(selected.serverUrl);
|
|
80
|
+
if (server.protocol !== 'https:' && !(server.protocol === 'http:' && loopback(server)))
|
|
81
|
+
throw new Error();
|
|
82
|
+
reason = 'the client credential is unreadable';
|
|
83
|
+
const gatewayCredential = readPrivateFile(selected.credentialPath, 4096).toString('utf8').trim();
|
|
84
|
+
return { origin: selected.serverUrl.replace(/\/$/, '') + '/notifications', token: bound.token, gatewayCredential };
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
warn(`Notifications are not produced: ${file} is unusable (${reason}). Re-run the installer to repair it.`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** Durable producer queue. Service dedupe makes crash-after-acceptance replay safe. */
|
|
92
|
+
export class NotificationOutbox {
|
|
93
|
+
file;
|
|
94
|
+
config;
|
|
95
|
+
project;
|
|
96
|
+
warn;
|
|
97
|
+
entries;
|
|
98
|
+
cursor;
|
|
99
|
+
checkpoint;
|
|
100
|
+
running;
|
|
101
|
+
retired = false;
|
|
102
|
+
timer;
|
|
103
|
+
constructor(file, config, project, warn = () => { }) {
|
|
104
|
+
this.file = file;
|
|
105
|
+
this.config = config;
|
|
106
|
+
this.project = project;
|
|
107
|
+
this.warn = warn;
|
|
108
|
+
mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
|
|
109
|
+
const saved = existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) : { entries: [] };
|
|
110
|
+
// Only the current state shape is read; anything else is refused and left unchanged on disk.
|
|
111
|
+
if (!saved || typeof saved !== 'object' || Array.isArray(saved) || !Array.isArray(saved.entries))
|
|
112
|
+
throw new Error('Unsupported notification outbox state');
|
|
113
|
+
this.entries = saved.entries;
|
|
114
|
+
this.cursor = saved.cursor;
|
|
115
|
+
this.checkpoint = saved.checkpoint;
|
|
116
|
+
this.timer = setInterval(() => { void this.drain(); }, 3000);
|
|
117
|
+
this.timer.unref();
|
|
118
|
+
}
|
|
119
|
+
persist(entries, cursor = this.cursor, checkpoint = this.checkpoint) {
|
|
120
|
+
const tmp = this.file + '.tmp', fd = openSync(tmp, 'w', 0o600);
|
|
121
|
+
try {
|
|
122
|
+
writeFileSync(fd, JSON.stringify({ entries, cursor, checkpoint }));
|
|
123
|
+
fsyncSync(fd);
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
closeSync(fd);
|
|
127
|
+
}
|
|
128
|
+
renameSync(tmp, this.file);
|
|
129
|
+
const dir = openSync(dirname(this.file), 'r');
|
|
130
|
+
try {
|
|
131
|
+
fsyncSync(dir);
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
closeSync(dir);
|
|
135
|
+
}
|
|
136
|
+
this.entries = entries;
|
|
137
|
+
this.cursor = cursor;
|
|
138
|
+
this.checkpoint = checkpoint;
|
|
139
|
+
}
|
|
140
|
+
advance(cursor) { this.persist(this.entries, cursor); }
|
|
141
|
+
saveCheckpoint(checkpoint) { this.persist(this.entries, this.cursor, checkpoint); }
|
|
142
|
+
enqueue(id, value, cursor = this.cursor) {
|
|
143
|
+
if (this.retired)
|
|
144
|
+
return false;
|
|
145
|
+
if (this.entries.some(same(id))) {
|
|
146
|
+
if (cursor !== this.cursor)
|
|
147
|
+
this.advance(cursor);
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
if (this.entries.filter(supported).length >= 2000)
|
|
151
|
+
return false;
|
|
152
|
+
this.persist([...this.entries, { id, value, targetVersion: 1 }], cursor);
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
drain() {
|
|
156
|
+
if (this.retired)
|
|
157
|
+
return Promise.resolve();
|
|
158
|
+
return this.running ??= this.drainOnce().finally(() => { this.running = undefined; });
|
|
159
|
+
}
|
|
160
|
+
async drainOnce() {
|
|
161
|
+
let attempts = 0;
|
|
162
|
+
for (const entry of [...this.entries]) {
|
|
163
|
+
if (!supported(entry))
|
|
164
|
+
continue;
|
|
165
|
+
if (attempts >= 32)
|
|
166
|
+
break;
|
|
167
|
+
if (this.retired)
|
|
168
|
+
break;
|
|
169
|
+
try {
|
|
170
|
+
const value = entry.payload ?? await this.project(entry.value, entry.id);
|
|
171
|
+
if (!isNotificationInput(value))
|
|
172
|
+
continue;
|
|
173
|
+
attempts++;
|
|
174
|
+
// Freeze the canonical projection before any network attempt. A crash
|
|
175
|
+
// after service acceptance must replay the identical dedupe payload.
|
|
176
|
+
if (!entry.payload)
|
|
177
|
+
this.persist(this.entries.map(e => same(entry.id)(e) ? { ...e, payload: value } : e));
|
|
178
|
+
const response = await fetch(this.config.origin + '/api/v1/send', {
|
|
179
|
+
method: 'POST', redirect: 'error', signal: AbortSignal.timeout(10_000),
|
|
180
|
+
headers: { ...(this.config.gatewayCredential
|
|
181
|
+
? { 'X-Ours-Api-Token': this.config.gatewayCredential, 'X-Ours-Notifications-Producer': this.config.token }
|
|
182
|
+
: { Authorization: `Bearer ${this.config.token}` }), 'Content-Type': 'application/json' },
|
|
183
|
+
body: JSON.stringify(value),
|
|
184
|
+
});
|
|
185
|
+
await response.body?.cancel();
|
|
186
|
+
if (!response.ok)
|
|
187
|
+
throw new Error('notification service rejected event');
|
|
188
|
+
this.persist(this.entries.filter(e => !same(entry.id)(e)));
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
this.warn('Notification producer delivery pending; retrying from durable outbox');
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/** Wait for the current send before clearing queued sends and retiring its target. */
|
|
197
|
+
async retire() { this.retired = true; clearInterval(this.timer); await this.running; this.persist(this.entries.filter(entry => !supported(entry))); }
|
|
198
|
+
async close() { clearInterval(this.timer); await this.running; }
|
|
199
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type ProducerConfig } from './outbox.js';
|
|
2
|
+
export declare const cleanupDir: () => string;
|
|
3
|
+
export type AgentTarget = {
|
|
4
|
+
kind: 'agent';
|
|
5
|
+
agentId: string;
|
|
6
|
+
};
|
|
7
|
+
/** Only a closed agent identity authorizes this producer's role-target cleanup. */
|
|
8
|
+
export declare function notificationAgentId(target: unknown): string | null;
|
|
9
|
+
export declare function hasPendingNotificationTargetCleanup(dir?: string): boolean;
|
|
10
|
+
/** Queue only after the owning lifecycle has established deletion, never on a failed inventory read. */
|
|
11
|
+
export declare function retireNotificationTarget(target: AgentTarget, config?: ProducerConfig | undefined, dir?: string, request?: typeof fetch): Promise<void>;
|
|
12
|
+
/** Persisted retries also run from the ordinary web notification producer after restart. */
|
|
13
|
+
export declare function drainNotificationTargetCleanup(config: ProducerConfig, dir?: string, request?: typeof fetch, options?: {
|
|
14
|
+
retain?: boolean;
|
|
15
|
+
beforeDelete?: (target: AgentTarget) => Promise<boolean>;
|
|
16
|
+
}): Promise<void>;
|