@moretea-labs/forge 1.6.0 → 1.7.1
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/AGENTS.md +4 -0
- package/CHANGELOG.md +39 -0
- package/CLAUDE.md +4 -0
- package/README.md +2 -2
- package/README.zh-CN.md +1 -1
- package/SKILL.md +2 -2
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/agents/openai.yaml +1 -1
- package/assets/hooks/hook-input.sh +11 -8
- package/assets/hooks/lib/session-state.sh +10 -7
- package/assets/hooks/lib/workflow-state.sh +13 -177
- package/assets/hooks/post-tool-observer.sh +8 -2
- package/assets/hooks/prompt-guard.sh +5 -81
- package/assets/hooks/session-start-context.sh +56 -1
- package/assets/partials/05-workflow.partial.md +1 -1
- package/assets/partials-agents/02-operating-mode.partial.md +2 -0
- package/assets/partials-agents/03-orchestration.partial.md +1 -1
- package/assets/plugin-registry.v1.json +3 -3
- package/assets/reference-configs/document-generation.md +1 -1
- package/assets/reference-configs/harness-overview.md +1 -1
- package/assets/reference-configs/sprint-contracts.md +3 -3
- package/assets/skill-version.json +2 -2
- package/assets/templates/contract.template.md +2 -7
- package/assets/templates/helpers/capture-plan.sh +2 -2
- package/assets/templates/helpers/check-architecture-sync.sh +17 -0
- package/assets/templates/helpers/check-task-workflow.sh +4 -4
- package/assets/templates/helpers/contract-worktree.sh +1 -21
- package/assets/templates/helpers/ensure-task-workflow.sh +11 -36
- package/assets/templates/helpers/inspect-project-state.ts +8 -6
- package/assets/templates/helpers/migrate-project-template.sh +22 -45
- package/assets/templates/helpers/migrate-workflow-docs.ts +9 -7
- package/assets/templates/helpers/new-plan.sh +1 -1
- package/assets/templates/helpers/plan-to-todo.sh +12 -37
- package/assets/templates/helpers/ship-worktrees.sh +2 -29
- package/assets/templates/helpers/verify-contract.sh +1 -111
- package/assets/templates/helpers/verify-sprint.sh +3 -180
- package/assets/templates/helpers/workflow-contract.ts +5 -1
- package/assets/templates/plan.template.md +1 -1
- package/assets/templates/review.template.md +9 -34
- package/assets/workflow-contract.v1.json +10 -19
- package/docs/README.md +10 -10
- package/docs/architecture/CURRENT.md +178 -0
- package/docs/architecture/EVOLUTION.md +42 -0
- package/docs/architecture/current/README.md +4 -156
- package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +2 -2
- package/docs/architecture/decisions/20260823-immediate-controller-relay.md +53 -0
- package/docs/architecture/decisions/20260826-browser-runtime-v2.md +73 -0
- package/docs/architecture/history/README.md +7 -0
- package/docs/architecture/{chatgpt-handoff-facade.md → history/chatgpt-handoff-facade.md} +4 -0
- package/docs/architecture/{global-hook-runtime.md → history/global-hook-runtime.md} +7 -3
- package/docs/architecture/{ios-semantic-automation-provider-v2.md → history/ios-semantic-automation-provider-v2.md} +6 -2
- package/docs/architecture/history.md +3 -34
- package/docs/architecture/index.md +15 -98
- package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +1 -1
- package/docs/architecture/reviews/2026-08-22-semantic-harness-convergence-acceptance.md +39 -0
- package/docs/architecture/versions/1.6.md +38 -0
- package/docs/architecture/versions/1.7.md +41 -0
- package/docs/forge-chatgpt-browser-engine.md +2 -2
- package/docs/forge-chatgpt-mcp-setup.md +4 -28
- package/docs/forge-chatgpt-supervised-automation.md +3 -3
- package/docs/forge-github-issue-launcher.md +1 -1
- package/docs/forge-plugin-management.md +1 -1
- package/docs/forge-runtime-self-healing-loop.md +1 -1
- package/docs/operations/controller-browser-plugin.md +26 -6
- package/docs/operations/controller-performance-and-502.md +143 -0
- package/docs/operations/controller-reliability-runbook.md +1 -1
- package/docs/operations/features.md +2 -2
- package/docs/operations/github-repository.md +1 -1
- package/docs/operations/homebrew.md +2 -2
- package/docs/operations/mcp-tool-exposure.md +5 -3
- package/docs/wiki/Architecture.md +6 -6
- package/docs/wiki/Implementation.md +2 -2
- package/docs/wiki/Integrations.md +2 -2
- package/docs/wiki/Work-Lifecycle.md +1 -1
- package/package.json +33 -11
- package/scripts/activate-source-baseline.ts +17 -13
- package/scripts/architecture-event.ts +2 -820
- package/scripts/architecture-queue.sh +2 -645
- package/scripts/archive-architecture-request.sh +2 -242
- package/scripts/archive-workflow.sh +1 -291
- package/scripts/benchmark-background-check-overlap.ts +228 -0
- package/scripts/benchmark-mcp-host.sh +117 -0
- package/scripts/benchmark-mcp-transport.ts +170 -0
- package/scripts/benchmark-quality-harness.ts +146 -0
- package/scripts/benchmark-route-session-concurrency.ts +207 -6
- package/scripts/benchmark-thin-harness-mcp-e2e.ts +7 -1
- package/scripts/benchmark-typescript-navigation.ts +186 -0
- package/scripts/benchmark-work-efficiency.ts +334 -0
- package/scripts/capability-config.ts +2 -392
- package/scripts/capability-resolver.ts +2 -503
- package/scripts/capture-plan.sh +1 -332
- package/scripts/check-agent-tooling.sh +2 -1558
- package/scripts/check-architecture-sync.sh +2 -371
- package/scripts/check-brain-manifest.sh +2 -253
- package/scripts/check-context-files.sh +2 -47
- package/scripts/check-deploy-sql-order.sh +2 -115
- package/scripts/check-mcp-compatibility.ts +14 -0
- package/scripts/check-runtime-architecture.mjs +349 -79
- package/scripts/check-skill-version.ts +3 -191
- package/scripts/check-source-duplication.mjs +61 -0
- package/scripts/check-stable-baseline.ts +92 -0
- package/scripts/check-task-sync.sh +2 -63
- package/scripts/check-task-workflow.sh +2 -1000
- package/scripts/codex-handoff-resume.sh +2 -267
- package/scripts/context-contract-sync.sh +2 -514
- package/scripts/contract-run.ts +2 -463
- package/scripts/contract-worktree.sh +1 -722
- package/scripts/ensure-task-workflow.sh +1 -1225
- package/scripts/host-temporary-cloud-mcp-e2e.sh +265 -0
- package/scripts/inspect-project-state.ts +4 -279
- package/scripts/install-standalone-recovery.ts +24 -0
- package/scripts/lib/project-init-lib.sh +22 -46
- package/scripts/lib/run-forge-helper.sh +19 -0
- package/scripts/lib/run-forge-helper.ts +26 -0
- package/scripts/migrate-project-template.sh +1 -1189
- package/scripts/migrate-workflow-docs.ts +4 -412
- package/scripts/new-plan.sh +1 -180
- package/scripts/new-spec.sh +1 -47
- package/scripts/new-sprint.sh +1 -46
- package/scripts/plan-to-todo.sh +1 -796
- package/scripts/prepare-codex-handoff.sh +2 -201
- package/scripts/prepare-handoff.sh +1 -78
- package/scripts/refresh-current-status.sh +2 -467
- package/scripts/run-governed-gate.ts +5 -1
- package/scripts/select-agent-context-blocks.sh +2 -64
- package/scripts/ship-worktrees.sh +1 -558
- package/scripts/smoke-ios-physical-device.ts +4 -2
- package/scripts/smoke-runtime-recovery.ts +1 -35
- package/scripts/smoke-schedule-engine.ts +1 -20
- package/scripts/sprint-backlog.sh +1 -834
- package/scripts/stage-runtime-release.ts +2 -1
- package/scripts/summarize-failures.sh +2 -123
- package/scripts/switch-plan.sh +1 -150
- package/scripts/sync-brain-docs.sh +2 -379
- package/scripts/sync-generated-authority.mjs +175 -0
- package/scripts/verify-contract.sh +2 -703
- package/scripts/verify-forge-runtime.sh +0 -1
- package/scripts/verify-sprint.sh +1 -677
- package/scripts/workflow-contract.ts +4 -205
- package/scripts/workstream-sync.sh +2 -261
- package/src/cli/agent-jobs/executor-health.ts +1 -1
- package/src/cli/commands/init-hook.ts +3 -3
- package/src/cli/commands/mcp.ts +9 -5
- package/src/cli/commands/plugin.ts +12 -3
- package/src/cli/commands/recovery.ts +25 -3
- package/src/cli/commands/runtime.ts +55 -3
- package/src/cli/commands/security.ts +12 -1
- package/src/cli/commands/setup-profile.ts +31 -21
- package/src/cli/controller/check-runner.ts +113 -5
- package/src/cli/controller/composite-result.ts +1 -1
- package/src/cli/controller/context/focus.ts +51 -0
- package/src/cli/controller/context/instruction-context.ts +114 -0
- package/src/cli/controller/context/known-paths.ts +76 -0
- package/src/cli/controller/context/query-planning.ts +110 -0
- package/src/cli/controller/context/rule-resolution.ts +532 -0
- package/src/cli/controller/context/source-materializer.ts +278 -0
- package/src/cli/controller/context/structural.ts +16 -0
- package/src/cli/controller/context/types.ts +286 -0
- package/src/cli/controller/context-pack.ts +828 -487
- package/src/cli/controller/launch-agents.ts +77 -2
- package/src/cli/controller/operational-plan.ts +2 -2
- package/src/cli/controller/work-mode.ts +6 -4
- package/src/cli/editing/edit-session.ts +60 -7
- package/src/cli/github/contracts.ts +16 -0
- package/src/cli/github/github.ts +135 -16
- package/src/cli/github/session.ts +2 -1
- package/src/cli/installer/managed-entries.ts +15 -1
- package/src/cli/local-bridge/console-automations.ts +54 -78
- package/src/cli/local-bridge/console-view-models.ts +1 -35
- package/src/cli/local-bridge/facade-api.ts +37 -264
- package/src/cli/local-bridge/frontend/src/App.tsx +3 -3
- package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +23 -13
- package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +37 -4
- package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +6 -4
- package/src/cli/local-bridge/frontend/src/lib/api.ts +1 -6
- package/src/cli/local-bridge/frontend/src/lib/routes.ts +1 -1
- package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +2 -2
- package/src/cli/local-bridge/frontend/src/styles.css +1 -1
- package/src/cli/local-bridge/frontend/src/types.ts +5 -8
- package/src/cli/local-bridge/job-store.ts +1 -490
- package/src/cli/local-bridge/server.ts +2 -515
- package/src/cli/local-bridge/ui-dist/app.css +1 -1
- package/src/cli/local-bridge/ui-dist/app.js +3703 -3757
- package/src/cli/mcp/auth.ts +5 -5
- package/src/cli/mcp/instructions.ts +3 -2
- package/src/cli/mcp/legacy-tool-service.ts +10 -222
- package/src/cli/mcp/multi-repository.ts +16 -5
- package/src/cli/mcp/oauth.ts +175 -26
- package/src/cli/mcp/post-finalize-work-attribution.ts +150 -0
- package/src/cli/mcp/repository-tools.ts +282 -146
- package/src/cli/mcp/server.ts +586 -116
- package/src/cli/mcp/tool-capability-cache.ts +1 -1
- package/src/cli/mcp/tool-contract.ts +14 -0
- package/src/cli/mcp/toolset-selection.ts +1 -1
- package/src/cli/mcp/toolset.ts +44 -7
- package/src/cli/mcp/transports/http.ts +124 -14
- package/src/cli/mcp/transports/session-registry.ts +43 -5
- package/src/cli/mcp/types.ts +3 -3
- package/src/cli/repositories/command-classifier.ts +103 -21
- package/src/cli/repositories/command-executor.ts +78 -526
- package/src/cli/repositories/command-process.ts +108 -0
- package/src/cli/repositories/command-scope.ts +99 -41
- package/src/cli/repositories/controller-home.ts +7 -8
- package/src/cli/repositories/git-command-executor.ts +2 -0
- package/src/cli/repositories/locks.ts +53 -16
- package/src/cli/repositories/repository-snapshot.ts +124 -0
- package/src/cli/repositories/selected-path-actions.ts +17 -9
- package/src/cli/repositories/structured-git.ts +118 -6
- package/src/cli/repository/inspector.ts +563 -65
- package/src/cli/repository/session-cache.ts +58 -17
- package/src/cli/v81-entry.ts +1 -4
- package/src/effects/fs-transaction.ts +85 -17
- package/src/runtime/context/codegraph-read-provider.ts +233 -2
- package/src/runtime/context/codegraph-sidecar.cjs +74 -40
- package/src/runtime/context/generic-lsp-provider.ts +355 -0
- package/src/runtime/context/lsp-client.ts +283 -0
- package/src/runtime/context/semantic-navigation-contract.ts +61 -0
- package/src/runtime/context/semantic-navigation.ts +223 -0
- package/src/runtime/context/swift-navigation.ts +565 -0
- package/src/runtime/context/typescript-navigation.ts +226 -0
- package/src/runtime/control-plane/dispatch-priority.ts +28 -0
- package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +48 -16
- package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +86 -24
- package/src/runtime/control-plane/execution/repository-work-attribution.ts +106 -0
- package/src/runtime/control-plane/execution/retained-work-resume.ts +203 -0
- package/src/runtime/control-plane/execution/session-store.ts +18 -0
- package/src/runtime/control-plane/execution/verification-evidence.ts +58 -3
- package/src/runtime/control-plane/execution/work-completion-authority.ts +58 -0
- package/src/runtime/control-plane/execution/work-evidence-policy.ts +200 -0
- package/src/runtime/control-plane/execution/work-execution-support.ts +386 -0
- package/src/runtime/control-plane/execution/work-finalization-service.ts +1913 -0
- package/src/runtime/control-plane/execution/work-handle-authority.ts +96 -0
- package/src/runtime/control-plane/execution/work-handle-store.ts +27 -2
- package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +258 -0
- package/src/runtime/control-plane/execution/work-operation-service.ts +468 -0
- package/src/runtime/control-plane/execution/work-path-scope.ts +37 -0
- package/src/runtime/control-plane/execution/work-preparation-service.ts +479 -0
- package/src/runtime/control-plane/execution/work-process-evidence.ts +129 -0
- package/src/runtime/control-plane/execution/work-remote-delivery.ts +215 -0
- package/src/runtime/control-plane/execution/work-task-receipt.ts +22 -18
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +156 -11
- package/src/runtime/control-plane/execution/work-validation-reconciler.ts +22 -14
- package/src/runtime/control-plane/execution/work-verification-context.ts +56 -0
- package/src/runtime/control-plane/execution/work-verification-service.ts +388 -0
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +90 -9
- package/src/runtime/control-plane/facade/capability-registry.ts +129 -25
- package/src/runtime/control-plane/facade/controller-round-relay.ts +1118 -0
- package/src/runtime/control-plane/facade/controller-session-store.ts +302 -12
- package/src/runtime/control-plane/facade/goal-workloop-access.ts +17 -1
- package/src/runtime/control-plane/facade/goal-workloop.ts +928 -190
- package/src/runtime/control-plane/facade/index.ts +3 -0
- package/src/runtime/control-plane/facade/plan-contract-store.ts +275 -42
- package/src/runtime/control-plane/facade/repository-work-admission.ts +147 -0
- package/src/runtime/control-plane/facade/requirement-authority.ts +83 -0
- package/src/runtime/control-plane/facade/semantic-admission.ts +205 -0
- package/src/runtime/control-plane/facade/suggested-actions.ts +5 -1
- package/src/runtime/control-plane/facade/types.ts +90 -10
- package/src/runtime/control-plane/facade/work-contract-store.ts +157 -261
- package/src/runtime/control-plane/facade/work-state-machine.ts +250 -0
- package/src/runtime/control-plane/generated-cache-retention.ts +271 -0
- package/src/runtime/control-plane/global-scheduler/config.ts +44 -0
- package/src/runtime/control-plane/global-scheduler/dispatch-capacity.ts +80 -0
- package/src/runtime/control-plane/global-scheduler/durable-admission.ts +46 -0
- package/src/runtime/control-plane/global-scheduler/maintenance.ts +152 -0
- package/src/runtime/control-plane/global-scheduler/projection-refresh.ts +84 -0
- package/src/runtime/control-plane/global-scheduler/reconciliation.ts +3 -3
- package/src/runtime/control-plane/global-scheduler/scheduler.ts +278 -543
- package/src/runtime/control-plane/global-scheduler/source-scan.ts +59 -0
- package/src/runtime/control-plane/global-scheduler/state.ts +88 -0
- package/src/runtime/control-plane/global-scheduler/worker-attachment.ts +57 -0
- package/src/runtime/control-plane/global-scheduler/worker-exit-decision.ts +38 -0
- package/src/runtime/control-plane/global-scheduler/worker-exit-reconciler.ts +175 -0
- package/src/runtime/control-plane/global-scheduler/worker-launch.ts +111 -0
- package/src/runtime/control-plane/global-scheduler/worker-lifecycle-store.ts +64 -0
- package/src/runtime/control-plane/global-scheduler/worker-lifecycle.ts +150 -0
- package/src/runtime/control-plane/global-scheduler/worker-process.ts +119 -0
- package/src/runtime/control-plane/global-scheduler/worker-stderr.ts +60 -0
- package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +63 -0
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +365 -117
- package/src/runtime/control-plane/launcher/launch-reservation-store.ts +89 -0
- package/src/runtime/control-plane/launcher/thin-launcher.ts +244 -11
- package/src/runtime/control-plane/persistence/controller-home-migration.ts +487 -0
- package/src/runtime/control-plane/persistence/requirement-store.ts +0 -12
- package/src/runtime/control-plane/persistence/sqlite-store.ts +33 -2
- package/src/runtime/control-plane/release-retention.ts +62 -2
- package/src/runtime/control-plane/repo-actor/registry.ts +4 -2
- package/src/runtime/control-plane/routing/route-policy.ts +24 -21
- package/src/runtime/control-plane/routing/workspace-admission.ts +70 -0
- package/src/runtime/control-plane/runtime-status-client.ts +0 -2
- package/src/runtime/diagnostics/mcp-timing.ts +13 -0
- package/src/runtime/diagnostics/mcp-transport-benchmark.ts +129 -0
- package/src/runtime/diagnostics/performance.ts +24 -1
- package/src/runtime/evidence/event-ledger.ts +1 -1
- package/src/runtime/evidence/process-check-execution.ts +18 -0
- package/src/runtime/execution/jobs/child-reference.ts +6 -2
- package/src/runtime/execution/jobs/store.ts +58 -4
- package/src/runtime/execution/jobs/types.ts +1 -1
- package/src/runtime/execution/managed-workspace.ts +203 -1
- package/src/runtime/execution/process-runtime/check-facade.ts +51 -4
- package/src/runtime/execution/process-runtime/check-result.ts +88 -0
- package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +7 -1
- package/src/runtime/execution/process-runtime/check-scheduling.ts +114 -0
- package/src/runtime/execution/process-runtime/command-facade.ts +367 -40
- package/src/runtime/execution/process-runtime/gc.ts +38 -6
- package/src/runtime/execution/process-runtime/lightweight-managed.ts +1201 -0
- package/src/runtime/execution/process-runtime/persisted-check.ts +51 -5
- package/src/runtime/execution/process-runtime/resource-claims.ts +10 -6
- package/src/runtime/execution/process-runtime/runtime.ts +53 -3
- package/src/runtime/execution/process-runtime/store.ts +1 -0
- package/src/runtime/execution/process-runtime/types.ts +4 -12
- package/src/runtime/execution/thin-harness/async-process.ts +12 -2
- package/src/runtime/execution/thin-harness/search-worker.ts +54 -5
- package/src/runtime/execution/work-activity.ts +21 -0
- package/src/runtime/execution/workers/executor.ts +18 -61
- package/src/runtime/gateway/mcp/execution-tools.ts +35 -1852
- package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +61 -0
- package/src/runtime/gateway/mcp/process-tools.ts +83 -37
- package/src/runtime/gateway/mcp/router.ts +186 -84
- package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +565 -0
- package/src/runtime/gateway/mcp/runtime-tools.ts +2378 -1947
- package/src/runtime/plugins/app-store-connect-adapter.ts +216 -47
- package/src/runtime/plugins/browser-adapter.ts +2662 -829
- package/src/runtime/plugins/browser-automation-service.ts +10 -0
- package/src/runtime/plugins/browser-handoff-host.ts +2 -25
- package/src/runtime/plugins/browser-handoff.ts +110 -5
- package/src/runtime/plugins/browser-macos-bridge.ts +701 -63
- package/src/runtime/plugins/browser-manifest-surface.ts +603 -0
- package/src/runtime/plugins/browser-native-session-service.ts +222 -0
- package/src/runtime/plugins/browser-node-bridge.ts +6 -34
- package/src/runtime/plugins/browser-provider-registry.ts +109 -0
- package/src/runtime/plugins/browser-runtime-contract.ts +154 -0
- package/src/runtime/plugins/browser-runtime.ts +193 -0
- package/src/runtime/plugins/browser-session-authority.ts +332 -0
- package/src/runtime/plugins/browser-session-store.ts +121 -0
- package/src/runtime/plugins/browser-session-types.ts +98 -0
- package/src/runtime/plugins/capability-authorization-grants.ts +354 -0
- package/src/runtime/plugins/desktop-operator-registration.ts +10 -4
- package/src/runtime/plugins/external-adapter.ts +358 -3
- package/src/runtime/plugins/external-unix-socket.ts +7 -1
- package/src/runtime/plugins/first-party-registry.ts +2 -0
- package/src/runtime/plugins/github-adapter.ts +125 -0
- package/src/runtime/plugins/ios/agent-device-provider.ts +1 -1
- package/src/runtime/plugins/ios/agent-device-typed-provider.ts +52 -9
- package/src/runtime/plugins/ios/remote-xpc-hid.ts +1171 -0
- package/src/runtime/plugins/ios-adapter.ts +106 -16
- package/src/runtime/plugins/ios-agent-device.ts +234 -64
- package/src/runtime/plugins/ios-physical-device.ts +1143 -392
- package/src/runtime/plugins/lightweight-action.ts +153 -0
- package/src/runtime/plugins/local-system-adapter.ts +123 -6
- package/src/runtime/plugins/macos-capability-broker.ts +47 -9
- package/src/runtime/plugins/plugin-action-sidecar.ts +59 -0
- package/src/runtime/plugins/plugin-management-adapter.ts +49 -8
- package/src/runtime/plugins/resend-adapter.ts +1 -1
- package/src/runtime/plugins/store.ts +330 -33
- package/src/runtime/plugins/types.ts +38 -3
- package/src/runtime/plugins/xiaohongshu-publish.ts +515 -0
- package/src/runtime/projections/git-status-sampler.ts +16 -16
- package/src/runtime/projections/materialized-view.ts +28 -10
- package/src/runtime/recovery/maintenance-executor.ts +426 -25
- package/src/runtime/root/mcp-transport.ts +180 -10
- package/src/runtime/root/package-connector-service.ts +230 -10
- package/src/runtime/root/package-runtime-release.ts +143 -17
- package/src/runtime/root/package-runtime-service.ts +461 -22
- package/src/runtime/root/release-manifest.ts +144 -18
- package/src/runtime/root/release-materialize.ts +125 -32
- package/src/runtime/root/release-store.ts +48 -11
- package/src/runtime/root/runtime.ts +23 -5
- package/src/runtime/root/service.ts +100 -29
- package/src/runtime/root/types.ts +12 -0
- package/src/runtime/safe-tooling/ios-development.ts +321 -63
- package/src/runtime/shared/trusted-executable.ts +62 -0
- package/src/runtime/shared/trusted-node-executable.ts +48 -0
- package/src/runtime/standalone-recovery/core.ts +1079 -240
- package/src/runtime/standalone-recovery/entry.ts +100 -24
- package/src/runtime/standalone-recovery/installer.ts +20 -4
- package/src/runtime/standalone-recovery/service-labels.ts +2 -0
- package/src/runtime/standalone-recovery/watchdog-heartbeat.ts +160 -0
- package/src/runtime/workflow/schedules/browser-probe.ts +81 -3
- package/src/runtime/workflow/schedules/engine.ts +364 -202
- package/src/runtime/workflow/schedules/github-issue-watch.ts +158 -0
- package/src/runtime/workflow/schedules/settlement.ts +64 -17
- package/src/runtime/workflow/schedules/store.ts +209 -19
- package/src/runtime/workflow/schedules/types.ts +2 -4
- package/src/runtime/workflow/schedules/work-continuation.ts +26 -13
- package/src/testing/test-governance.ts +8 -40
- package/assets/reference-configs/heartbeat-triage.md +0 -57
- package/assets/templates/helpers/harness-trace-grade.sh +0 -150
- package/assets/templates/helpers/heartbeat-triage.sh +0 -361
- package/assets/templates/helpers/maintenance-triage.sh +0 -167
- package/docs/architecture/current/approved-target-architecture.zh-CN.md +0 -1501
- package/docs/architecture/current/architecture-invariants.md +0 -331
- package/docs/architecture/current/automation-and-schedule-engine.md +0 -459
- package/docs/architecture/current/codegraph-context-provider.md +0 -238
- package/docs/architecture/current/control-plane-authority-inventory.md +0 -94
- package/docs/architecture/current/controller-repository-decoupling.md +0 -148
- package/docs/architecture/current/dispatch-and-agent-strategy.md +0 -373
- package/docs/architecture/current/entity-model.md +0 -612
- package/docs/architecture/current/external-plugin-governance.md +0 -396
- package/docs/architecture/current/failure-recovery.md +0 -447
- package/docs/architecture/current/governance.md +0 -213
- package/docs/architecture/current/human-interaction-plane.md +0 -87
- package/docs/architecture/current/implementation-status.md +0 -128
- package/docs/architecture/current/job-and-run-lifecycle.md +0 -413
- package/docs/architecture/current/legacy-governance.md +0 -58
- package/docs/architecture/current/local-workspace-target-grants.md +0 -70
- package/docs/architecture/current/migration-roadmap.md +0 -105
- package/docs/architecture/current/multi-repository-execution.md +0 -371
- package/docs/architecture/current/operations-runbook.md +0 -45
- package/docs/architecture/current/peer-supercontroller-runtime.md +0 -32
- package/docs/architecture/current/personal-assistant-plugin-baseline.md +0 -206
- package/docs/architecture/current/repository-access-modes.md +0 -102
- package/docs/architecture/current/route-policy.md +0 -15
- package/docs/architecture/current/route-session-concurrency.md +0 -85
- package/docs/architecture/current/runtime-architecture-simplification.md +0 -214
- package/docs/architecture/current/runtime-directory-map.md +0 -49
- package/docs/architecture/current/runtime-health-and-resource-lifecycle.md +0 -85
- package/docs/architecture/current/scheduler-and-resource-claims.md +0 -422
- package/docs/architecture/current/session-aware-execution-and-authorization.md +0 -77
- package/docs/architecture/current/system-overview.md +0 -169
- package/docs/architecture/current/target-requirements-traceability.md +0 -202
- package/docs/architecture/current/thin-harness-governance-policy.md +0 -32
- package/docs/architecture/current/thin-harness-v1.md +0 -321
- package/docs/architecture/current/verification-and-release-gates.md +0 -391
- package/docs/forge-autonomous-goal-loop.md +0 -9
- package/docs/forge-provider-configuration.md +0 -7
- package/docs/operations/chatgpt-personal-assistant-runtime.md +0 -93
- package/docs/operations/gmail-assistant-routines.md +0 -186
- package/docs/operations/personal-assistant-operator-guide.md +0 -61
- package/docs/operations/provider-configuration.md +0 -31
- package/scripts/harness-trace-grade.sh +0 -150
- package/scripts/heartbeat-triage.sh +0 -361
- package/scripts/maintenance-triage.sh +0 -167
- package/src/cli/local-bridge/frontend/src/features/settings/SettingsView.tsx +0 -2
- package/src/cli/repositories/goal-registry.ts +0 -239
- package/src/runtime/assistant/action-proposals.ts +0 -279
- package/src/runtime/assistant/intent.ts +0 -395
- package/src/runtime/assistant/model-provider.ts +0 -460
- package/src/runtime/assistant/openapi.ts +0 -223
- package/src/runtime/assistant/policy.ts +0 -89
- package/src/runtime/assistant/readiness.ts +0 -258
- package/src/runtime/assistant/routine-runtime.ts +0 -477
- package/src/runtime/assistant/schedule-binding.ts +0 -171
- package/src/runtime/assistant/standing-grants.ts +0 -375
- package/src/runtime/assistant/store.ts +0 -243
- package/src/runtime/assistant/types.ts +0 -135
- package/src/runtime/control-plane/goal-loop/config-facade.ts +0 -700
- package/src/runtime/control-plane/goal-loop/config-store.ts +0 -570
- package/src/runtime/control-plane/goal-loop/config-types.ts +0 -213
- package/src/runtime/control-plane/goal-loop/executor-router.ts +0 -109
- package/src/runtime/control-plane/goal-loop/failure-taxonomy.ts +0 -149
- package/src/runtime/control-plane/goal-loop/goal-contract-store.ts +0 -304
- package/src/runtime/control-plane/goal-loop/goal-loop-engine.ts +0 -1001
- package/src/runtime/control-plane/goal-loop/handoff-packet.ts +0 -160
- package/src/runtime/control-plane/goal-loop/index.ts +0 -13
- package/src/runtime/control-plane/goal-loop/local-tools.ts +0 -290
- package/src/runtime/control-plane/goal-loop/policy-gates.ts +0 -203
- package/src/runtime/control-plane/goal-loop/provider-dispatch.ts +0 -93
- package/src/runtime/control-plane/goal-loop/provider-registry.ts +0 -545
- package/src/runtime/control-plane/goal-loop/types.ts +0 -378
- package/src/runtime/control-plane/startup-recovery.ts +0 -198
- package/src/runtime/gateway/mcp/resource-policy.ts +0 -186
- package/src/runtime/personal-assistant/gmail-triage-manager.ts +0 -231
- package/src/runtime/personal-assistant/reporting-runtime.ts +0 -344
- package/src/runtime/personal-assistant/triage-runtime.ts +0 -748
- package/src/runtime/workflow/findings/store.ts +0 -204
- package/src/runtime/workflow/findings/types.ts +0 -58
- package/src/runtime/workflow/portfolio/engine.ts +0 -20
- package/src/runtime/workflow/portfolio/store.ts +0 -185
- package/src/runtime/workflow/portfolio/types.ts +0 -29
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'crypto';
|
|
2
2
|
import { createServer, type IncomingMessage, type ServerResponse } from 'http';
|
|
3
|
-
import { basename, resolve } from 'path';
|
|
3
|
+
import { basename, join, resolve } from 'path';
|
|
4
4
|
import { pathToFileURL } from 'url';
|
|
5
5
|
import { readMcpServiceOAuthPassphrase } from '../../cli/mcp/auth';
|
|
6
6
|
import { FORGE_VERSION } from '../../version';
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
repairPublicTunnel,
|
|
19
19
|
restartPrimaryConnector,
|
|
20
20
|
restartPrimaryRuntime,
|
|
21
|
+
restartRecoveryWatchdog,
|
|
21
22
|
stageAndActivateConfiguredRuntimeRelease,
|
|
22
23
|
rollbackPrevious,
|
|
23
24
|
secureEqual,
|
|
@@ -27,6 +28,12 @@ import {
|
|
|
27
28
|
type WatchdogState,
|
|
28
29
|
type RecoveryConfig,
|
|
29
30
|
} from './core';
|
|
31
|
+
import {
|
|
32
|
+
createRecoveryWatchdogHeartbeat,
|
|
33
|
+
observeRecoveryWatchdogHealth,
|
|
34
|
+
writeRecoveryWatchdogHeartbeat,
|
|
35
|
+
type RecoveryWatchdogHeartbeat,
|
|
36
|
+
} from './watchdog-heartbeat';
|
|
30
37
|
import {
|
|
31
38
|
RECOVERY_RELEASE_ROLE_CANARY_ARG,
|
|
32
39
|
writeRecoveryRuntimeIdentity,
|
|
@@ -139,12 +146,24 @@ export function resetWatchdogStateForRecoveryRelease(
|
|
|
139
146
|
// budget. Those counters are bound to the Runtime release and survive both
|
|
140
147
|
// watchdog process exits and immutable Recovery release activation.
|
|
141
148
|
recoveryGatewayRestartUsed: false,
|
|
149
|
+
primaryConnectorFailures: 0,
|
|
150
|
+
primaryConnectorFirstFailureAt: undefined,
|
|
151
|
+
primaryConnectorRestartAttempts: 0,
|
|
152
|
+
primaryConnectorRestartFailures: 0,
|
|
153
|
+
primaryConnectorRestartLastAttemptAt: undefined,
|
|
142
154
|
recoveryReleaseRevision: releaseRevision,
|
|
143
155
|
};
|
|
144
156
|
}
|
|
145
157
|
|
|
146
158
|
async function startWatchdog(config: RecoveryConfig): Promise<void> {
|
|
147
159
|
const runtimeIdentity = writeRecoveryRuntimeIdentity(config.controllerHome, 'watchdog');
|
|
160
|
+
let heartbeat: RecoveryWatchdogHeartbeat = createRecoveryWatchdogHeartbeat(runtimeIdentity);
|
|
161
|
+
const persistHeartbeat = (patch: Partial<RecoveryWatchdogHeartbeat> = {}) => {
|
|
162
|
+
heartbeat = writeRecoveryWatchdogHeartbeat(config.controllerHome, { ...heartbeat, ...patch });
|
|
163
|
+
};
|
|
164
|
+
persistHeartbeat();
|
|
165
|
+
const pulse = setInterval(() => persistHeartbeat({ lastPulseAt: new Date().toISOString() }), 5_000);
|
|
166
|
+
pulse.unref?.();
|
|
148
167
|
process.stdout.write(JSON.stringify({ status: 'ready', role: 'watchdog', runtimeIdentity }) + '\n');
|
|
149
168
|
const loadedState = loadWatchdogState(config);
|
|
150
169
|
let state = runtimeIdentity?.releaseRevision
|
|
@@ -152,9 +171,13 @@ async function startWatchdog(config: RecoveryConfig): Promise<void> {
|
|
|
152
171
|
: loadedState;
|
|
153
172
|
if (state !== loadedState) state = saveWatchdogState(config, state);
|
|
154
173
|
for (;;) {
|
|
174
|
+
const tickStartedAt = new Date().toISOString();
|
|
175
|
+
persistHeartbeat({ lastPulseAt: tickStartedAt, lastTickStartedAt: tickStartedAt, lastError: undefined });
|
|
155
176
|
try {
|
|
156
177
|
const result = await watchdogTick(config, state);
|
|
157
178
|
state = saveWatchdogState(config, result.state);
|
|
179
|
+
const tickCompletedAt = new Date().toISOString();
|
|
180
|
+
persistHeartbeat({ lastPulseAt: tickCompletedAt, lastTickCompletedAt: tickCompletedAt, lastError: undefined });
|
|
158
181
|
process.stdout.write(JSON.stringify({
|
|
159
182
|
at: new Date().toISOString(),
|
|
160
183
|
action: result.decision.action,
|
|
@@ -170,10 +193,14 @@ async function startWatchdog(config: RecoveryConfig): Promise<void> {
|
|
|
170
193
|
primaryRuntimeRecoveryDetail: result.primaryRuntimeRecovery?.detail,
|
|
171
194
|
rollbackDetail: result.rollback?.detail,
|
|
172
195
|
publicTunnelDetail: result.publicTunnelRepair?.detail,
|
|
196
|
+
primaryConnectorRestartDetail: result.primaryConnectorRestart?.detail,
|
|
173
197
|
}) + '\n');
|
|
174
198
|
} catch (error) {
|
|
175
199
|
state = saveWatchdogState(config, { ...state, failures: state.failures + 1, firstFailureAt: state.firstFailureAt ?? Date.now() });
|
|
176
|
-
|
|
200
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
201
|
+
const failedAt = new Date().toISOString();
|
|
202
|
+
persistHeartbeat({ lastPulseAt: failedAt, lastTickFailedAt: failedAt, lastError: detail.slice(0, 500) });
|
|
203
|
+
process.stderr.write(`watchdog probe failed: ${detail}\n`);
|
|
177
204
|
}
|
|
178
205
|
await new Promise((resolveSleep) => setTimeout(resolveSleep, 5_000));
|
|
179
206
|
}
|
|
@@ -228,7 +255,7 @@ export const RECOVERY_TOOLS = [
|
|
|
228
255
|
{ name: 'restart_primary_runtime', description: 'Restart the installed canonical Forge Runtime service and require whole-Runtime verification.', inputSchema: { type: 'object', properties: { request_id: { type: 'string', minLength: 8, maxLength: 120 } }, required: ['request_id'], additionalProperties: false } },
|
|
229
256
|
{ name: 'restart_primary_connector', description: 'Restart the explicitly configured primary OAuth/Connector launchd service only after local Canonical Runtime verification succeeds.', inputSchema: { type: 'object', properties: { request_id: { type: 'string', minLength: 8, maxLength: 120 } }, required: ['request_id'], additionalProperties: false } },
|
|
230
257
|
{ name: 'recover_primary_runtime', description: 'Stop the canonical Runtime, restore the attested previous whole release and SQLite backup, restart it, and require verification.', inputSchema: { type: 'object', properties: { request_id: { type: 'string', minLength: 8, maxLength: 120 } }, required: ['request_id'], additionalProperties: false } },
|
|
231
|
-
{ name: 'activate_runtime_release', description: 'Activate an already staged immutable Runtime release only if the caller-observed active release/authority revision are still current. Reverse activation of current.previous is rejected; use rollback_previous/recover_primary_runtime instead.', inputSchema: { type: 'object', properties: { request_id: { type: 'string', minLength: 8, maxLength: 120 }, release_path: { type: 'string', minLength: 8, maxLength: 1024 }, expected_active_release_id: { type: 'string', minLength: 1, maxLength: 256 }, expected_authority_revision: { type: 'integer', minimum: 1 } }, required: ['request_id', 'release_path', 'expected_active_release_id', 'expected_authority_revision'], additionalProperties: false } },
|
|
258
|
+
{ name: 'activate_runtime_release', description: 'Activate an already staged immutable Runtime release only if the caller-observed active release/authority revision are still current. Reverse activation of current.previous is rejected; use rollback_previous/recover_primary_runtime instead.', inputSchema: { type: 'object', properties: { request_id: { type: 'string', minLength: 8, maxLength: 120 }, release_path: { type: 'string', minLength: 8, maxLength: 1024, description: 'Absolute path to the staged immutable Runtime release directory.' }, expected_active_release_id: { type: 'string', minLength: 1, maxLength: 256 }, expected_authority_revision: { type: 'integer', minimum: 1 } }, required: ['request_id', 'release_path', 'expected_active_release_id', 'expected_authority_revision'], additionalProperties: false } },
|
|
232
259
|
{ name: 'stage_and_activate_runtime_release', description: 'Build one immutable Runtime release from the fixed Recovery-configured source root, then activate it transactionally with rollback protection. No arbitrary source path is accepted.', inputSchema: { type: 'object', properties: { request_id: { type: 'string', minLength: 8, maxLength: 120 } }, required: ['request_id'], additionalProperties: false } },
|
|
233
260
|
{ name: 'restart_public_tunnel', description: 'Restart the explicitly configured public tunnel only after local runtime verification succeeds and the external endpoint is unavailable.', inputSchema: { type: 'object', properties: { request_id: { type: 'string', minLength: 8, maxLength: 120 } }, required: ['request_id'], additionalProperties: false } },
|
|
234
261
|
{ name: 'reconnect_primary_connector', description: 'Check canonical Runtime Gateway and primary MCP reconnection readiness without publishing a release.', inputSchema: { type: 'object', additionalProperties: false } },
|
|
@@ -277,7 +304,8 @@ function requestUrl(request: IncomingMessage): URL {
|
|
|
277
304
|
return new URL(request.url ?? '/', `${proto}://${host}`);
|
|
278
305
|
}
|
|
279
306
|
|
|
280
|
-
function publicOrigin(request: IncomingMessage): string {
|
|
307
|
+
function publicOrigin(request: IncomingMessage, config?: Pick<RecoveryConfig, 'recoveryPublicUrl'>): string {
|
|
308
|
+
if (config?.recoveryPublicUrl) return new URL(config.recoveryPublicUrl).origin;
|
|
281
309
|
const url = requestUrl(request);
|
|
282
310
|
return `${url.protocol}//${url.host}`;
|
|
283
311
|
}
|
|
@@ -289,12 +317,12 @@ function resourcePathFromRequest(_request: IncomingMessage): '/recovery/mcp' {
|
|
|
289
317
|
return '/recovery/mcp';
|
|
290
318
|
}
|
|
291
319
|
|
|
292
|
-
function recoveryResource(request: IncomingMessage): string {
|
|
293
|
-
return `${publicOrigin(request)}${resourcePathFromRequest(request)}`;
|
|
320
|
+
function recoveryResource(request: IncomingMessage, config: Pick<RecoveryConfig, 'recoveryPublicUrl'>): string {
|
|
321
|
+
return `${publicOrigin(request, config)}${resourcePathFromRequest(request)}`;
|
|
294
322
|
}
|
|
295
323
|
|
|
296
|
-
function recoveryAuthorizationServerMetadata(request: IncomingMessage): Record<string, unknown> {
|
|
297
|
-
const origin = publicOrigin(request);
|
|
324
|
+
function recoveryAuthorizationServerMetadata(request: IncomingMessage, config: Pick<RecoveryConfig, 'recoveryPublicUrl'>): Record<string, unknown> {
|
|
325
|
+
const origin = publicOrigin(request, config);
|
|
298
326
|
return {
|
|
299
327
|
issuer: origin,
|
|
300
328
|
authorization_endpoint: `${origin}/recovery/oauth/authorize`,
|
|
@@ -308,19 +336,37 @@ function recoveryAuthorizationServerMetadata(request: IncomingMessage): Record<s
|
|
|
308
336
|
};
|
|
309
337
|
}
|
|
310
338
|
|
|
311
|
-
function recoveryProtectedResourceMetadata(request: IncomingMessage): Record<string, unknown> {
|
|
339
|
+
function recoveryProtectedResourceMetadata(request: IncomingMessage, config: Pick<RecoveryConfig, 'recoveryPublicUrl'>): Record<string, unknown> {
|
|
340
|
+
const origin = publicOrigin(request, config);
|
|
312
341
|
return {
|
|
313
|
-
resource: recoveryResource(request),
|
|
314
|
-
authorization_servers: [
|
|
342
|
+
resource: recoveryResource(request, config),
|
|
343
|
+
authorization_servers: [origin],
|
|
315
344
|
scopes_supported: [RECOVERY_OAUTH_SCOPE],
|
|
316
345
|
bearer_methods_supported: ['header'],
|
|
317
|
-
resource_documentation: `${
|
|
346
|
+
resource_documentation: `${origin}/recovery/health`,
|
|
318
347
|
};
|
|
319
348
|
}
|
|
320
349
|
|
|
321
|
-
function recoveryWwwAuthenticate(request: IncomingMessage): string {
|
|
322
|
-
const metadata = `${publicOrigin(request)}/.well-known/oauth-protected-resource${resourcePathFromRequest(request)}`;
|
|
323
|
-
return `Bearer
|
|
350
|
+
export function recoveryWwwAuthenticate(request: IncomingMessage, config: Pick<RecoveryConfig, 'recoveryPublicUrl'>): string {
|
|
351
|
+
const metadata = `${publicOrigin(request, config)}/.well-known/oauth-protected-resource${resourcePathFromRequest(request)}`;
|
|
352
|
+
return `Bearer error="invalid_token", error_description="Missing Authorization header", resource_metadata="${metadata}"`;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function recoveryUnauthorizedBody(): { error: string; message: string } {
|
|
356
|
+
return { error: 'invalid_token', message: 'Missing Authorization header' };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export type RecoveryMcpRequestClassification = 'not_mcp' | 'auth_required' | 'method_not_supported' | 'mcp';
|
|
360
|
+
|
|
361
|
+
export function classifyRecoveryMcpRequest(
|
|
362
|
+
request: Pick<IncomingMessage, 'method' | 'url' | 'headers'>,
|
|
363
|
+
expectedToken: string | undefined,
|
|
364
|
+
): RecoveryMcpRequestClassification {
|
|
365
|
+
if (!matchesAnyPath(request.url, ['/mcp', '/recovery/mcp'])) return 'not_mcp';
|
|
366
|
+
const supplied = request.headers.authorization?.replace(/^Bearer\s+/i, '').trim();
|
|
367
|
+
if (!expectedToken || !supplied || !secureEqual(supplied, expectedToken)) return 'auth_required';
|
|
368
|
+
if (request.method !== 'POST') return 'method_not_supported';
|
|
369
|
+
return 'mcp';
|
|
324
370
|
}
|
|
325
371
|
|
|
326
372
|
function parseUrlEncoded(input: string): URLSearchParams {
|
|
@@ -462,7 +508,7 @@ export async function dispatchRecoveryTool(config: RecoveryConfig, name: string,
|
|
|
462
508
|
}
|
|
463
509
|
case 'restart_primary_connector': {
|
|
464
510
|
if (!requestId(args.request_id)) throw new Error('RECOVERY_REQUEST_ID_REQUIRED');
|
|
465
|
-
return restartPrimaryConnector(config);
|
|
511
|
+
return restartPrimaryConnector(config, { requestId: `recovery-gateway:${args.request_id}` });
|
|
466
512
|
}
|
|
467
513
|
case 'recover_primary_runtime': {
|
|
468
514
|
if (!requestId(args.request_id)) throw new Error('RECOVERY_REQUEST_ID_REQUIRED');
|
|
@@ -473,7 +519,9 @@ export async function dispatchRecoveryTool(config: RecoveryConfig, name: string,
|
|
|
473
519
|
if (typeof args.release_path !== 'string' || !args.release_path.trim()) throw new Error('RECOVERY_RELEASE_PATH_REQUIRED');
|
|
474
520
|
if (typeof args.expected_active_release_id !== 'string' || !args.expected_active_release_id.trim()) throw new Error('RECOVERY_EXPECTED_ACTIVE_RELEASE_REQUIRED');
|
|
475
521
|
if (!Number.isInteger(args.expected_authority_revision) || Number(args.expected_authority_revision) < 1) throw new Error('RECOVERY_EXPECTED_AUTHORITY_REVISION_REQUIRED');
|
|
476
|
-
|
|
522
|
+
const releasePath = args.release_path.trim();
|
|
523
|
+
const manifestPath = basename(releasePath) === 'manifest.json' ? releasePath : join(releasePath, 'manifest.json');
|
|
524
|
+
return activateRuntimeRelease(config, manifestPath, {}, {
|
|
477
525
|
requestId: `recovery-gateway:${args.request_id}`,
|
|
478
526
|
expectedActiveReleaseId: args.expected_active_release_id.trim(),
|
|
479
527
|
expectedAuthorityRevision: Number(args.expected_authority_revision),
|
|
@@ -498,6 +546,7 @@ async function startGateway(config: RecoveryConfig): Promise<void> {
|
|
|
498
546
|
throw new Error('RECOVERY_GATEWAY_CONFIG_INVALID');
|
|
499
547
|
}
|
|
500
548
|
let runtimeIdentity: RecoveryRuntimeIdentity | undefined;
|
|
549
|
+
let watchdogRestartInFlight = false;
|
|
501
550
|
const recentMutations = new Map<string, number[]>();
|
|
502
551
|
const oauthCodes = new Map<string, PendingOAuthCode>();
|
|
503
552
|
const oauthClients = new Map<string, OAuthClient>();
|
|
@@ -509,9 +558,18 @@ async function startGateway(config: RecoveryConfig): Promise<void> {
|
|
|
509
558
|
return;
|
|
510
559
|
}
|
|
511
560
|
if (request.method === 'GET' && matchesAnyPath(request.url, ['/health', '/recovery/health'])) {
|
|
561
|
+
const watchdog = observeRecoveryWatchdogHealth(config.controllerHome);
|
|
512
562
|
json(response, 200, {
|
|
513
|
-
status: 'ok',
|
|
563
|
+
status: watchdog.ok ? 'ok' : 'degraded',
|
|
514
564
|
service: 'forge-standalone-recovery',
|
|
565
|
+
watchdog: {
|
|
566
|
+
ok: watchdog.ok,
|
|
567
|
+
detail: watchdog.detail,
|
|
568
|
+
pulseAgeMs: watchdog.pulseAgeMs,
|
|
569
|
+
tickAgeMs: watchdog.tickAgeMs,
|
|
570
|
+
releaseRevision: watchdog.runtimeIdentity?.releaseRevision,
|
|
571
|
+
pid: watchdog.runtimeIdentity?.pid,
|
|
572
|
+
},
|
|
515
573
|
version: FORGE_VERSION,
|
|
516
574
|
...(runtimeIdentity ? {
|
|
517
575
|
releasePath: runtimeIdentity.releasePath,
|
|
@@ -522,8 +580,8 @@ async function startGateway(config: RecoveryConfig): Promise<void> {
|
|
|
522
580
|
});
|
|
523
581
|
return;
|
|
524
582
|
}
|
|
525
|
-
if (request.method === 'GET' && isProtectedResourceMetadataPath(request)) { json(response, 200, recoveryProtectedResourceMetadata(request)); return; }
|
|
526
|
-
if (request.method === 'GET' && isOAuthMetadataPath(request)) { json(response, 200, recoveryAuthorizationServerMetadata(request)); return; }
|
|
583
|
+
if (request.method === 'GET' && isProtectedResourceMetadataPath(request)) { json(response, 200, recoveryProtectedResourceMetadata(request, config)); return; }
|
|
584
|
+
if (request.method === 'GET' && isOAuthMetadataPath(request)) { json(response, 200, recoveryAuthorizationServerMetadata(request, config)); return; }
|
|
527
585
|
if ((request.method === 'GET' || request.method === 'POST') && isAuthorizePath(request)) {
|
|
528
586
|
const params = request.method === 'GET'
|
|
529
587
|
? requestUrl(request).searchParams
|
|
@@ -640,10 +698,9 @@ async function startGateway(config: RecoveryConfig): Promise<void> {
|
|
|
640
698
|
});
|
|
641
699
|
return;
|
|
642
700
|
}
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
if (!expected || !supplied || !secureEqual(supplied, expected)) { response.setHeader('www-authenticate', recoveryWwwAuthenticate(request)); json(response, 401, { error: 'RECOVERY_AUTH_REQUIRED' }); return; }
|
|
701
|
+
const mcpRequest = classifyRecoveryMcpRequest(request, gatewayToken(config));
|
|
702
|
+
if (mcpRequest === 'not_mcp' || mcpRequest === 'method_not_supported') { json(response, 404, { error: 'NOT_FOUND' }); return; }
|
|
703
|
+
if (mcpRequest === 'auth_required') { response.setHeader('www-authenticate', recoveryWwwAuthenticate(request, config)); json(response, 401, recoveryUnauthorizedBody()); return; }
|
|
647
704
|
if (!/^application\/json(?:\s*;|$)/i.test(String(request.headers['content-type'] ?? ''))) { json(response, 415, { error: 'RECOVERY_CONTENT_TYPE_REQUIRED' }); return; }
|
|
648
705
|
let message: { id?: unknown; method?: unknown; params?: { name?: unknown; arguments?: unknown } };
|
|
649
706
|
try { message = JSON.parse(await readBody(request)) as typeof message; } catch { json(response, 400, rpcError(null, -32700, 'Invalid JSON.')); return; }
|
|
@@ -675,6 +732,25 @@ async function startGateway(config: RecoveryConfig): Promise<void> {
|
|
|
675
732
|
});
|
|
676
733
|
await new Promise<void>((resolveListen, reject) => { server.once('error', reject); server.listen(gateway.port, gateway.host, () => resolveListen()); });
|
|
677
734
|
runtimeIdentity = writeRecoveryRuntimeIdentity(config.controllerHome, 'gateway');
|
|
735
|
+
const superviseWatchdog = async () => {
|
|
736
|
+
if (!runtimeIdentity || watchdogRestartInFlight) return;
|
|
737
|
+
const watchdog = observeRecoveryWatchdogHealth(config.controllerHome);
|
|
738
|
+
if (watchdog.ok) return;
|
|
739
|
+
watchdogRestartInFlight = true;
|
|
740
|
+
try {
|
|
741
|
+
const recovery = await restartRecoveryWatchdog(config);
|
|
742
|
+
auditGateway({
|
|
743
|
+
watchdog_supervision: recovery.ok ? 'recovered' : 'failed',
|
|
744
|
+
detail: recovery.detail,
|
|
745
|
+
attempted: recovery.attempted,
|
|
746
|
+
serviceTarget: recovery.serviceTarget,
|
|
747
|
+
});
|
|
748
|
+
} finally {
|
|
749
|
+
watchdogRestartInFlight = false;
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
const watchdogSupervisor = setInterval(() => { void superviseWatchdog(); }, 15_000);
|
|
753
|
+
watchdogSupervisor.unref?.();
|
|
678
754
|
process.stdout.write(JSON.stringify({ status: 'ready', host: gateway.host, port: gateway.port, runtimeIdentity }) + '\n');
|
|
679
755
|
}
|
|
680
756
|
|
|
@@ -25,6 +25,8 @@ import {
|
|
|
25
25
|
import { isProcessAlive } from '../shared/process-tree';
|
|
26
26
|
import { FORGE_VERSION } from '../../version';
|
|
27
27
|
import { initializeStandaloneRecovery, loadRecoveryConfig, type PrimaryConnectorServiceConfig, type PrimaryRuntimeServiceConfig, type PublicTunnelServiceConfig, type RecoveryConfig } from './core';
|
|
28
|
+
import { RECOVERY_GATEWAY_LABEL, RECOVERY_WATCHDOG_LABEL } from './service-labels';
|
|
29
|
+
export { RECOVERY_GATEWAY_LABEL, RECOVERY_WATCHDOG_LABEL } from './service-labels';
|
|
28
30
|
import {
|
|
29
31
|
RECOVERY_RELEASE_BINARIES,
|
|
30
32
|
RECOVERY_RELEASE_ROLE_CANARY_ARG,
|
|
@@ -44,9 +46,6 @@ import {
|
|
|
44
46
|
type RecoveryRuntimeRole,
|
|
45
47
|
} from './release';
|
|
46
48
|
|
|
47
|
-
export const RECOVERY_GATEWAY_LABEL = 'com.moretea.forge-recovery-gateway';
|
|
48
|
-
export const RECOVERY_WATCHDOG_LABEL = 'com.moretea.forge-recovery-watchdog';
|
|
49
|
-
|
|
50
49
|
function defaultRecoveryLaunchctl(args: string[]): ReturnType<LaunchctlCommandRunner> {
|
|
51
50
|
const result = runProcess('launchctl', args, { timeoutMs: 10_000, maxOutputBytes: 64 * 1024 });
|
|
52
51
|
return { ok: result.ok, stdout: result.stdout, stderr: result.stderr, exitCode: result.status ?? 0 };
|
|
@@ -341,7 +340,7 @@ function recoveryPlist(input: {
|
|
|
341
340
|
}): string {
|
|
342
341
|
const xml = (value: string) => value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
343
342
|
const argumentsList = [
|
|
344
|
-
'/usr/bin/env', '-i', 'PATH=/usr/bin:/bin:/usr/sbin:/sbin', `FORGE_BUILD_VERSION=${FORGE_VERSION}`,
|
|
343
|
+
'/usr/bin/env', '-i', 'PATH=/usr/bin:/bin:/usr/sbin:/sbin', `FORGE_BUILD_VERSION=${FORGE_VERSION}`, `FORGE_CONNECTOR_EXECUTABLE=${process.execPath}`,
|
|
345
344
|
input.executable, input.command, '--controller-home', input.controllerHome,
|
|
346
345
|
];
|
|
347
346
|
return `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0"><dict><key>Label</key><string>${xml(input.label)}</string><key>ProgramArguments</key><array>${argumentsList.map((argument) => `<string>${xml(argument)}</string>`).join('')}</array><key>RunAtLoad</key><true/><key>KeepAlive</key><true/><key>ThrottleInterval</key><integer>5</integer><key>StandardOutPath</key><string>${xml(input.logPath)}</string><key>StandardErrorPath</key><string>${xml(input.logPath)}</string></dict></plist>\n`;
|
|
@@ -682,6 +681,12 @@ export function inspectPrimaryConnectorLaunchdContract(
|
|
|
682
681
|
return inspectLaunchdRestartContract(service, true);
|
|
683
682
|
}
|
|
684
683
|
|
|
684
|
+
export function inspectPrimaryPublicTunnelLaunchdContract(
|
|
685
|
+
service: Pick<PublicTunnelServiceConfig, 'platform' | 'label' | 'plistPath'>,
|
|
686
|
+
): RecoveryTunnelLaunchdContract {
|
|
687
|
+
return inspectLaunchdRestartContract(service, true);
|
|
688
|
+
}
|
|
689
|
+
|
|
685
690
|
export async function installStandaloneRecovery(input: {
|
|
686
691
|
controllerHome: string;
|
|
687
692
|
repoRoot: string;
|
|
@@ -690,6 +695,7 @@ export async function installStandaloneRecovery(input: {
|
|
|
690
695
|
publicMcpUrl?: string;
|
|
691
696
|
recoveryPublicUrl?: string;
|
|
692
697
|
recoveryTunnelService?: PublicTunnelServiceConfig;
|
|
698
|
+
primaryPublicTunnelService?: PublicTunnelServiceConfig;
|
|
693
699
|
primaryRuntimeService?: PrimaryRuntimeServiceConfig;
|
|
694
700
|
primaryConnectorService?: PrimaryConnectorServiceConfig;
|
|
695
701
|
stageOnly?: boolean;
|
|
@@ -705,6 +711,15 @@ export async function installStandaloneRecovery(input: {
|
|
|
705
711
|
throw new Error('RECOVERY_TUNNEL_LAUNCHD_RESTART_CONTRACT_REQUIRED: RunAtLoad=true and KeepAlive=true');
|
|
706
712
|
}
|
|
707
713
|
}
|
|
714
|
+
if (input.primaryPublicTunnelService?.platform === 'launchd') {
|
|
715
|
+
const tunnelContract = inspectPrimaryPublicTunnelLaunchdContract(input.primaryPublicTunnelService);
|
|
716
|
+
if (!tunnelContract.plistInstalled) {
|
|
717
|
+
throw new Error(`RECOVERY_PRIMARY_TUNNEL_LAUNCHD_PLIST_MISSING: ${tunnelContract.plistPath}`);
|
|
718
|
+
}
|
|
719
|
+
if (!tunnelContract.restartSafe) {
|
|
720
|
+
throw new Error('RECOVERY_PRIMARY_TUNNEL_LAUNCHD_RESTART_CONTRACT_REQUIRED: RunAtLoad=true and KeepAlive=true or KeepAlive.SuccessfulExit=false');
|
|
721
|
+
}
|
|
722
|
+
}
|
|
708
723
|
if (input.primaryConnectorService?.platform === 'launchd') {
|
|
709
724
|
const connectorContract = inspectPrimaryConnectorLaunchdContract(input.primaryConnectorService);
|
|
710
725
|
if (!connectorContract.plistInstalled) {
|
|
@@ -718,6 +733,7 @@ export async function installStandaloneRecovery(input: {
|
|
|
718
733
|
...(input.publicMcpUrl ? { publicMcpUrl: input.publicMcpUrl } : {}),
|
|
719
734
|
...(input.recoveryPublicUrl ? { recoveryPublicUrl: input.recoveryPublicUrl } : {}),
|
|
720
735
|
...(input.recoveryTunnelService ? { recoveryTunnelService: input.recoveryTunnelService } : {}),
|
|
736
|
+
...(input.primaryPublicTunnelService ? { primaryPublicTunnelService: input.primaryPublicTunnelService } : {}),
|
|
721
737
|
...(input.primaryRuntimeService ? { primaryRuntimeService: input.primaryRuntimeService } : {}),
|
|
722
738
|
primaryRuntimeSourceRoot: resolve(input.repoRoot),
|
|
723
739
|
...(input.primaryConnectorService ? { primaryConnectorService: input.primaryConnectorService } : {}),
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { writeJsonAtomic } from '../shared/json-files';
|
|
4
|
+
import {
|
|
5
|
+
readCurrentRecoveryRelease,
|
|
6
|
+
readRecoveryRuntimeIdentity,
|
|
7
|
+
recoveryRoot,
|
|
8
|
+
type RecoveryReleaseDescriptor,
|
|
9
|
+
type RecoveryRuntimeIdentity,
|
|
10
|
+
} from './release';
|
|
11
|
+
|
|
12
|
+
export const RECOVERY_WATCHDOG_MAX_PULSE_AGE_MS = 20_000;
|
|
13
|
+
export const RECOVERY_WATCHDOG_MAX_TICK_AGE_MS = 180_000;
|
|
14
|
+
|
|
15
|
+
export interface RecoveryWatchdogHeartbeat {
|
|
16
|
+
schemaVersion: 1;
|
|
17
|
+
pid: number;
|
|
18
|
+
startedAt: string;
|
|
19
|
+
releasePath: string;
|
|
20
|
+
releaseRevision: string;
|
|
21
|
+
sourceCommit: string;
|
|
22
|
+
manifestSha256: string;
|
|
23
|
+
lastPulseAt: string;
|
|
24
|
+
lastTickStartedAt?: string;
|
|
25
|
+
lastTickCompletedAt?: string;
|
|
26
|
+
lastTickFailedAt?: string;
|
|
27
|
+
lastError?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface RecoveryWatchdogHealth {
|
|
31
|
+
ok: boolean;
|
|
32
|
+
detail: string;
|
|
33
|
+
pulseAgeMs?: number;
|
|
34
|
+
tickAgeMs?: number;
|
|
35
|
+
heartbeat?: RecoveryWatchdogHeartbeat;
|
|
36
|
+
runtimeIdentity?: RecoveryRuntimeIdentity;
|
|
37
|
+
currentReleaseRevision?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function recoveryWatchdogHeartbeatPath(controllerHome: string): string {
|
|
41
|
+
return join(recoveryRoot(controllerHome), 'state', 'watchdog-heartbeat.json');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createRecoveryWatchdogHeartbeat(
|
|
45
|
+
identity: RecoveryRuntimeIdentity | undefined,
|
|
46
|
+
nowMs = Date.now(),
|
|
47
|
+
): RecoveryWatchdogHeartbeat {
|
|
48
|
+
const now = new Date(nowMs).toISOString();
|
|
49
|
+
return {
|
|
50
|
+
schemaVersion: 1,
|
|
51
|
+
pid: identity?.pid ?? process.pid,
|
|
52
|
+
startedAt: identity?.startedAt ?? now,
|
|
53
|
+
releasePath: identity?.releasePath ?? 'unmanaged-source',
|
|
54
|
+
releaseRevision: identity?.releaseRevision ?? 'unmanaged-source',
|
|
55
|
+
sourceCommit: identity?.sourceCommit ?? 'unmanaged-source',
|
|
56
|
+
manifestSha256: identity?.manifestSha256 ?? 'unmanaged-source',
|
|
57
|
+
lastPulseAt: now,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function writeRecoveryWatchdogHeartbeat(
|
|
62
|
+
controllerHome: string,
|
|
63
|
+
heartbeat: RecoveryWatchdogHeartbeat,
|
|
64
|
+
): RecoveryWatchdogHeartbeat {
|
|
65
|
+
writeJsonAtomic(recoveryWatchdogHeartbeatPath(controllerHome), heartbeat);
|
|
66
|
+
return heartbeat;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function readRecoveryWatchdogHeartbeat(controllerHome: string): RecoveryWatchdogHeartbeat | undefined {
|
|
70
|
+
try {
|
|
71
|
+
const parsed = JSON.parse(readFileSync(recoveryWatchdogHeartbeatPath(controllerHome), 'utf8')) as RecoveryWatchdogHeartbeat;
|
|
72
|
+
if (
|
|
73
|
+
parsed.schemaVersion !== 1
|
|
74
|
+
|| !Number.isInteger(parsed.pid)
|
|
75
|
+
|| typeof parsed.lastPulseAt !== 'string'
|
|
76
|
+
|| typeof parsed.releaseRevision !== 'string'
|
|
77
|
+
|| typeof parsed.manifestSha256 !== 'string'
|
|
78
|
+
) return undefined;
|
|
79
|
+
return parsed;
|
|
80
|
+
} catch {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function pidExists(pid: number): boolean {
|
|
86
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
87
|
+
try {
|
|
88
|
+
process.kill(pid, 0);
|
|
89
|
+
return true;
|
|
90
|
+
} catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function latestTerminalTickAt(heartbeat: RecoveryWatchdogHeartbeat): number {
|
|
96
|
+
return Math.max(
|
|
97
|
+
heartbeat.lastTickCompletedAt ? Date.parse(heartbeat.lastTickCompletedAt) : 0,
|
|
98
|
+
heartbeat.lastTickFailedAt ? Date.parse(heartbeat.lastTickFailedAt) : 0,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function evaluateRecoveryWatchdogHealth(input: {
|
|
103
|
+
heartbeat?: RecoveryWatchdogHeartbeat;
|
|
104
|
+
runtimeIdentity?: RecoveryRuntimeIdentity;
|
|
105
|
+
currentRelease?: Pick<RecoveryReleaseDescriptor, 'releasePath' | 'releaseRevision' | 'sourceCommit' | 'manifestSha256'>;
|
|
106
|
+
nowMs?: number;
|
|
107
|
+
maxPulseAgeMs?: number;
|
|
108
|
+
maxTickAgeMs?: number;
|
|
109
|
+
pidAlive?: (pid: number) => boolean;
|
|
110
|
+
}): RecoveryWatchdogHealth {
|
|
111
|
+
const heartbeat = input.heartbeat;
|
|
112
|
+
const runtimeIdentity = input.runtimeIdentity;
|
|
113
|
+
const current = input.currentRelease;
|
|
114
|
+
if (!current) return { ok: false, detail: 'current immutable Recovery release is unavailable' };
|
|
115
|
+
if (!runtimeIdentity) return { ok: false, detail: 'Recovery Watchdog runtime identity is unavailable', currentReleaseRevision: current.releaseRevision };
|
|
116
|
+
if (!heartbeat) return { ok: false, detail: 'Recovery Watchdog heartbeat is unavailable', runtimeIdentity, currentReleaseRevision: current.releaseRevision };
|
|
117
|
+
if (heartbeat.pid !== runtimeIdentity.pid) {
|
|
118
|
+
return { ok: false, detail: 'Recovery Watchdog heartbeat PID does not match runtime identity', heartbeat, runtimeIdentity, currentReleaseRevision: current.releaseRevision };
|
|
119
|
+
}
|
|
120
|
+
if (
|
|
121
|
+
runtimeIdentity.releasePath !== current.releasePath
|
|
122
|
+
|| runtimeIdentity.releaseRevision !== current.releaseRevision
|
|
123
|
+
|| runtimeIdentity.sourceCommit !== current.sourceCommit
|
|
124
|
+
|| runtimeIdentity.manifestSha256 !== current.manifestSha256
|
|
125
|
+
|| heartbeat.releasePath !== current.releasePath
|
|
126
|
+
|| heartbeat.releaseRevision !== current.releaseRevision
|
|
127
|
+
|| heartbeat.sourceCommit !== current.sourceCommit
|
|
128
|
+
|| heartbeat.manifestSha256 !== current.manifestSha256
|
|
129
|
+
) {
|
|
130
|
+
return { ok: false, detail: 'Recovery Watchdog is not running the current immutable Recovery release', heartbeat, runtimeIdentity, currentReleaseRevision: current.releaseRevision };
|
|
131
|
+
}
|
|
132
|
+
if (!(input.pidAlive ?? pidExists)(runtimeIdentity.pid)) {
|
|
133
|
+
return { ok: false, detail: 'Recovery Watchdog process is not alive', heartbeat, runtimeIdentity, currentReleaseRevision: current.releaseRevision };
|
|
134
|
+
}
|
|
135
|
+
const now = input.nowMs ?? Date.now();
|
|
136
|
+
const pulseAt = Date.parse(heartbeat.lastPulseAt);
|
|
137
|
+
const pulseAgeMs = Number.isFinite(pulseAt) ? Math.max(0, now - pulseAt) : Number.POSITIVE_INFINITY;
|
|
138
|
+
const maxPulseAgeMs = Math.max(1_000, input.maxPulseAgeMs ?? RECOVERY_WATCHDOG_MAX_PULSE_AGE_MS);
|
|
139
|
+
if (pulseAgeMs > maxPulseAgeMs) {
|
|
140
|
+
return { ok: false, detail: 'Recovery Watchdog heartbeat is stale', pulseAgeMs, heartbeat, runtimeIdentity, currentReleaseRevision: current.releaseRevision };
|
|
141
|
+
}
|
|
142
|
+
const tickStartedAt = heartbeat.lastTickStartedAt ? Date.parse(heartbeat.lastTickStartedAt) : 0;
|
|
143
|
+
const tickTerminalAt = latestTerminalTickAt(heartbeat);
|
|
144
|
+
if (tickStartedAt > tickTerminalAt) {
|
|
145
|
+
const tickAgeMs = Math.max(0, now - tickStartedAt);
|
|
146
|
+
const maxTickAgeMs = Math.max(maxPulseAgeMs, input.maxTickAgeMs ?? RECOVERY_WATCHDOG_MAX_TICK_AGE_MS);
|
|
147
|
+
if (tickAgeMs > maxTickAgeMs) {
|
|
148
|
+
return { ok: false, detail: 'Recovery Watchdog tick is stuck beyond its bounded recovery window', pulseAgeMs, tickAgeMs, heartbeat, runtimeIdentity, currentReleaseRevision: current.releaseRevision };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return { ok: true, detail: 'Recovery Watchdog heartbeat, PID, and immutable release identity are current', pulseAgeMs, heartbeat, runtimeIdentity, currentReleaseRevision: current.releaseRevision };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function observeRecoveryWatchdogHealth(controllerHome: string): RecoveryWatchdogHealth {
|
|
155
|
+
return evaluateRecoveryWatchdogHealth({
|
|
156
|
+
heartbeat: readRecoveryWatchdogHeartbeat(controllerHome),
|
|
157
|
+
runtimeIdentity: readRecoveryRuntimeIdentity(controllerHome, 'watchdog'),
|
|
158
|
+
currentRelease: readCurrentRecoveryRelease(controllerHome),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
@@ -95,10 +95,70 @@ async function browserAction(
|
|
|
95
95
|
});
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
interface ScheduledBrowserSessionMetadata {
|
|
99
|
+
url?: string;
|
|
100
|
+
ownership?: string;
|
|
101
|
+
provider?: string;
|
|
102
|
+
activeMode?: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export type ScheduledBrowserProbeNavigationAction = 'navigate' | 'reload' | 'wait_for_load_state';
|
|
106
|
+
|
|
107
|
+
export function scheduledBrowserProbeNavigationAction(
|
|
108
|
+
ownership: string | undefined,
|
|
109
|
+
hasUrl: boolean,
|
|
110
|
+
): ScheduledBrowserProbeNavigationAction {
|
|
111
|
+
if (ownership === 'user_owned') return 'wait_for_load_state';
|
|
112
|
+
return hasUrl ? 'navigate' : 'reload';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function scheduledBrowserProbeManagedRehydrateIntent(
|
|
116
|
+
savedSession: Pick<ScheduledBrowserSessionMetadata, 'ownership' | 'provider' | 'activeMode'> | undefined,
|
|
117
|
+
): Record<string, true> {
|
|
118
|
+
return savedSession?.ownership === 'plugin_owned'
|
|
119
|
+
&& savedSession.provider === 'playwright-persistent-context'
|
|
120
|
+
&& savedSession.activeMode === 'managed_persistent'
|
|
121
|
+
? { __forge_allow_managed_session_rehydrate: true }
|
|
122
|
+
: {};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function comparableProbeUrl(value: string): string {
|
|
126
|
+
try {
|
|
127
|
+
const parsed = new URL(value);
|
|
128
|
+
parsed.hash = '';
|
|
129
|
+
return parsed.toString();
|
|
130
|
+
} catch {
|
|
131
|
+
return value;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function scheduledBrowserSessionMetadata(
|
|
136
|
+
controllerHome: string,
|
|
137
|
+
repository: RepositoryRecord,
|
|
138
|
+
occurrenceId: string,
|
|
139
|
+
sessionId: string,
|
|
140
|
+
): Promise<ScheduledBrowserSessionMetadata | undefined> {
|
|
141
|
+
const inventory = await browserAction(controllerHome, repository, occurrenceId, 'list_sessions', {});
|
|
142
|
+
const sessions = Array.isArray(inventory.sessions) ? inventory.sessions : [];
|
|
143
|
+
const saved = sessions
|
|
144
|
+
.map((entry) => recordValue(entry))
|
|
145
|
+
.find((entry) => stringValue(entry.sessionId) === sessionId);
|
|
146
|
+
if (!saved) return undefined;
|
|
147
|
+
const browser = recordValue(saved.browser);
|
|
148
|
+
const tab = recordValue(browser.tab);
|
|
149
|
+
return {
|
|
150
|
+
url: stringValue(saved.url),
|
|
151
|
+
ownership: stringValue(tab.ownership),
|
|
152
|
+
provider: stringValue(browser.provider),
|
|
153
|
+
activeMode: stringValue(browser.activeMode),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
98
157
|
/**
|
|
99
158
|
* Bounded read-only browser observation for Schedule. It never interprets page
|
|
100
|
-
* meaning: it only refreshes/navigates
|
|
101
|
-
*
|
|
159
|
+
* meaning: it only refreshes/navigates plugin-owned targets, observes user-owned
|
|
160
|
+
* targets without mutating them, extracts bounded text, projects matching lines,
|
|
161
|
+
* detects configured login markers, and returns a stable fingerprint.
|
|
102
162
|
*/
|
|
103
163
|
export async function executeScheduledBrowserProbe(input: {
|
|
104
164
|
controllerHome: string;
|
|
@@ -112,20 +172,38 @@ export async function executeScheduledBrowserProbe(input: {
|
|
|
112
172
|
|
|
113
173
|
const waitUntil = stringValue(input.args.wait_until) ?? 'domcontentloaded';
|
|
114
174
|
const timeoutMs = boundedNumber(input.args.timeout_ms, 60_000, 1_000, 120_000);
|
|
175
|
+
const savedSession = requestedSessionId
|
|
176
|
+
? await scheduledBrowserSessionMetadata(input.controllerHome, input.repository, input.occurrenceId, requestedSessionId)
|
|
177
|
+
: undefined;
|
|
178
|
+
const navigationAction = scheduledBrowserProbeNavigationAction(savedSession?.ownership, Boolean(url));
|
|
179
|
+
const managedRehydrateIntent = scheduledBrowserProbeManagedRehydrateIntent(savedSession);
|
|
115
180
|
let navigation: Record<string, unknown>;
|
|
116
|
-
if (
|
|
181
|
+
if (navigationAction === 'wait_for_load_state') {
|
|
182
|
+
if (!savedSession) throw new Error('SCHEDULE_BROWSER_PROBE_USER_SESSION_STATE_UNAVAILABLE');
|
|
183
|
+
if (url && savedSession.url && comparableProbeUrl(url) !== comparableProbeUrl(savedSession.url)) {
|
|
184
|
+
throw new Error('SCHEDULE_BROWSER_PROBE_USER_SESSION_NAVIGATION_REFUSED');
|
|
185
|
+
}
|
|
186
|
+
navigation = await browserAction(input.controllerHome, input.repository, input.occurrenceId, 'wait_for_load_state', {
|
|
187
|
+
session_id: requestedSessionId!,
|
|
188
|
+
state: waitUntil,
|
|
189
|
+
timeout_ms: timeoutMs,
|
|
190
|
+
...managedRehydrateIntent,
|
|
191
|
+
});
|
|
192
|
+
} else if (navigationAction === 'navigate') {
|
|
117
193
|
navigation = await browserAction(input.controllerHome, input.repository, input.occurrenceId, 'navigate', {
|
|
118
194
|
...(requestedSessionId ? { session_id: requestedSessionId } : {}),
|
|
119
195
|
url,
|
|
120
196
|
wait_until: waitUntil,
|
|
121
197
|
timeout_ms: timeoutMs,
|
|
122
198
|
retries: 1,
|
|
199
|
+
...managedRehydrateIntent,
|
|
123
200
|
});
|
|
124
201
|
} else {
|
|
125
202
|
navigation = await browserAction(input.controllerHome, input.repository, input.occurrenceId, 'reload', {
|
|
126
203
|
session_id: requestedSessionId!,
|
|
127
204
|
wait_until: waitUntil,
|
|
128
205
|
timeout_ms: timeoutMs,
|
|
206
|
+
...managedRehydrateIntent,
|
|
129
207
|
});
|
|
130
208
|
}
|
|
131
209
|
|