diff --git a/.task/os/gate-internal-dashboard-routing-on-complete-access-config/current.json b/.task/os/gate-internal-dashboard-routing-on-complete-access-config/current.json new file mode 100644 index 0000000000..f7531be59a --- /dev/null +++ b/.task/os/gate-internal-dashboard-routing-on-complete-access-config/current.json @@ -0,0 +1,19 @@ +{ + "area": "os", + "stream": "stream/os", + "taskBranch": "task/os/gate-internal-dashboard-routing-on-complete-access-config", + "baseBranch": "stream/os", + "sourceBranch": "stream/os", + "startFrom": "stream", + "prNumber": 2149, + "prUrl": "https://github.com/consuelohq/opensaas/pull/2149", + "githubPrUrl": "https://github.com/consuelohq/opensaas/pull/2149", + "graphitePrUrl": "https://app.graphite.com/github/pr/consuelohq/opensaas/2149/gate-internal-dashboard-routing-on-complete-access-config", + "taskPrUrl": "https://github.com/consuelohq/opensaas/pull/2149", + "taskGraphitePrUrl": "https://app.graphite.com/github/pr/consuelohq/opensaas/2149/gate-internal-dashboard-routing-on-complete-access-config", + "worktreePath": "/private/var/folders/vl/1zvhm0bj28d1dbvbcb12b39r0000gn/T/opensaas-worktrees/task-os-gate-internal-dashboard-routing-on-complete-access-config", + "taskSession": "tsk_be4cc7cc41f5", + "tmuxSession": "opensaas-os-gate-internal-dashboard-routing-on-complete-acce-be4cc7cc", + "sessionPath": "/private/var/folders/vl/1zvhm0bj28d1dbvbcb12b39r0000gn/T/opensaas-worktrees/task-os-gate-internal-dashboard-routing-on-complete-access-config/.task/os/gate-internal-dashboard-routing-on-complete-access-config/session.json", + "createdAt": "2026-08-16T22:29:43.195Z" +} diff --git a/.task/os/gate-internal-dashboard-routing-on-complete-access-config/session.json b/.task/os/gate-internal-dashboard-routing-on-complete-access-config/session.json new file mode 100644 index 0000000000..5edd87f8ee --- /dev/null +++ b/.task/os/gate-internal-dashboard-routing-on-complete-access-config/session.json @@ -0,0 +1,15 @@ +{ + "taskSession": "tsk_be4cc7cc41f5", + "tmuxSession": "opensaas-os-gate-internal-dashboard-routing-on-complete-acce-be4cc7cc", + "area": "os", + "stream": "stream/os", + "taskBranch": "task/os/gate-internal-dashboard-routing-on-complete-access-config", + "branch": "task/os/gate-internal-dashboard-routing-on-complete-access-config", + "worktreePath": "/private/var/folders/vl/1zvhm0bj28d1dbvbcb12b39r0000gn/T/opensaas-worktrees/task-os-gate-internal-dashboard-routing-on-complete-access-config", + "worktree": "/private/var/folders/vl/1zvhm0bj28d1dbvbcb12b39r0000gn/T/opensaas-worktrees/task-os-gate-internal-dashboard-routing-on-complete-access-config", + "prNumber": 2149, + "prUrl": "https://github.com/consuelohq/opensaas/pull/2149", + "createdAt": "2026-08-16T22:29:43.194Z", + "tmuxCreated": true, + "sessionPath": "/private/var/folders/vl/1zvhm0bj28d1dbvbcb12b39r0000gn/T/opensaas-worktrees/task-os-gate-internal-dashboard-routing-on-complete-access-config/.task/os/gate-internal-dashboard-routing-on-complete-access-config/session.json" +} diff --git a/.task/os/gate-internal-dashboard-routing-on-complete-access-config/verify.json b/.task/os/gate-internal-dashboard-routing-on-complete-access-config/verify.json new file mode 100644 index 0000000000..78be752bec --- /dev/null +++ b/.task/os/gate-internal-dashboard-routing-on-complete-access-config/verify.json @@ -0,0 +1,232 @@ +{ + "result": "pass", + "publishValid": true, + "mode": "full", + "branch": "task/os/gate-internal-dashboard-routing-on-complete-access-config", + "base": "origin/task/os/gate-internal-dashboard-routing-on-complete-access-config", + "headSha": "657ff0432310ddd0cb1dbfb81dfe8a27df01a5a5", + "changeHash": "9c412968b404a93e45fe9ae8c6b200f41c097a7fc031b0c255785cb9b4f027f5", + "changedFiles": [ + "packages/os/cloudflare/workspace-edge/src/index.ts", + "packages/os/scripts/lib/cloudflare-worker-release-readiness.ts", + "packages/os/tests/cloudflare-worker-release-readiness.test.ts", + "packages/os/tests/internal-dashboard-integration.test.ts" + ], + "verifiedAt": "2026-08-16T22:32:26.816Z", + "review": { + "skipped": false, + "passed": true, + "status": 0 + }, + "testSelection": { + "skipped": false, + "passed": true, + "status": 0, + "data": { + "kind": "selection", + "passed": true, + "changedFiles": [ + ".task/os/gate-internal-dashboard-routing-on-complete-access-config/current.json", + ".task/os/gate-internal-dashboard-routing-on-complete-access-config/session.json", + ".task/os/gate-internal-dashboard-routing-on-complete-access-config/workpad.md", + ".task/tasks/os/gate-internal-dashboard-routing-on-complete-access-config.json", + "packages/os/cloudflare/workspace-edge/src/index.ts", + "packages/os/scripts/lib/cloudflare-worker-release-readiness.ts", + "packages/os/tests/cloudflare-worker-release-readiness.test.ts", + "packages/os/tests/internal-dashboard-integration.test.ts" + ], + "matchedRules": [ + { + "id": "os-device-approval-canonical-identity", + "critical": true, + "exclusive": true, + "reason": "Native OS device-approval and canonical-identity changes must prove the exact auth, tenant, and control-plane contracts without selecting unsafe unrelated package-wide OS tests.", + "matchedFiles": [ + "packages/os/tests/internal-dashboard-integration.test.ts" + ], + "origin": "explicit" + }, + { + "id": "os-managed-cloud-one-click-provisioning", + "critical": true, + "exclusive": true, + "reason": "One-click managed-cloud control-plane, launcher, provisioning-runner, enrollment, and Worker changes must prove their focused security and lifecycle contracts without selecting the historically red unrelated OS package suite.", + "matchedFiles": [ + "packages/os/cloudflare/workspace-edge/src/index.ts", + "packages/os/scripts/lib/cloudflare-worker-release-readiness.ts", + "packages/os/tests/cloudflare-worker-release-readiness.test.ts" + ], + "origin": "explicit" + } + ], + "selectedSuites": [ + { + "name": "OS canonical device approval contracts", + "command": [ + "bun", + "--cwd", + "packages/os", + "test", + "tests/canonical-device-identity.test.ts", + "tests/install-control-plane.test.ts", + "tests/install-control-plane-d1.test.ts", + "tests/oauth-device-page-contract.test.ts", + "tests/internal-dashboard-integration.test.ts", + "tests/native-google-device-approval.test.ts", + "tests/os-device-authority-worker.test.ts" + ], + "ruleId": "os-device-approval-canonical-identity", + "critical": true + }, + { + "name": "OS canonical device approval syntax contracts", + "command": [ + "node", + "packages/os/scripts/check-syntax.js" + ], + "ruleId": "os-device-approval-canonical-identity", + "critical": true + }, + { + "name": "OS one-click managed cloud contracts", + "command": [ + "bun", + "--cwd", + "packages/os", + "test", + "tests/managed-cloud-one-click-provisioning.test.ts", + "tests/managed-cloud-one-click-runner.test.ts", + "tests/launcher-nodes-control-plane.test.ts", + "tests/settings-site.test.ts", + "tests/cloudflare-worker-release-readiness.test.ts", + "tests/os-device-authority-architecture.test.ts", + "tests/os-device-authority-release-contract.test.ts", + "tests/managed-cloud-node-enrollment.test.ts", + "tests/managed-cloud-node-enrollment-cli.test.ts", + "tests/platform-managed-cloud-node.test.ts", + "tests/managed-cloud-pricing.test.ts", + "tests/managed-cloud-public-pricing.test.ts", + "tests/managed-cloud-node-instance-contract.test.ts", + "tests/managed-cloud-node-contract.test.ts" + ], + "ruleId": "os-managed-cloud-one-click-provisioning", + "critical": true + }, + { + "name": "OS device authority Worker contract", + "command": [ + "bun", + "x", + "vitest", + "run", + "packages/os/tests/os-device-authority-worker.test.ts" + ], + "ruleId": "os-managed-cloud-one-click-provisioning", + "critical": true + } + ], + "level": "pass", + "zeroSuiteReason": null, + "run": true, + "runResults": [ + { + "name": "OS canonical device approval contracts", + "command": [ + "bun", + "--cwd", + "packages/os", + "test", + "tests/canonical-device-identity.test.ts", + "tests/install-control-plane.test.ts", + "tests/install-control-plane-d1.test.ts", + "tests/oauth-device-page-contract.test.ts", + "tests/internal-dashboard-integration.test.ts", + "tests/native-google-device-approval.test.ts", + "tests/os-device-authority-worker.test.ts" + ], + "ruleId": "os-device-approval-canonical-identity", + "critical": true, + "status": "passed", + "exitCode": 0, + "signal": null, + "error": null, + "durationMs": 576, + "outputTail": "\n\u001b[1m\u001b[30m\u001b[46m RUN \u001b[49m\u001b[39m\u001b[22m \u001b[36mv4.1.5 \u001b[39m\u001b[90m/private/var/folders/vl/1zvhm0bj28d1dbvbcb12b39r0000gn/T/opensaas-worktrees/task-os-gate-internal-dashboard-routing-on-complete-access-config/packages/os\u001b[39m\n\n \u001b[32m✓\u001b[39m tests/oauth-device-page-contract.test.ts \u001b[2m(\u001b[22m\u001b[2m5 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 2\u001b[2mms\u001b[22m\u001b[39m\n \u001b[32m✓\u001b[39m tests/install-control-plane-d1.test.ts \u001b[2m(\u001b[22m\u001b[2m6 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 11\u001b[2mms\u001b[22m\u001b[39m\n \u001b[32m✓\u001b[39m tests/install-control-plane.test.ts \u001b[2m(\u001b[22m\u001b[2m9 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 14\u001b[2mms\u001b[22m\u001b[39m\n \u001b[32m✓\u001b[39m tests/canonical-device-identity.test.ts \u001b[2m(\u001b[22m\u001b[2m8 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 9\u001b[2mms\u001b[22m\u001b[39m\n \u001b[32m✓\u001b[39m tests/native-google-device-approval.test.ts \u001b[2m(\u001b[22m\u001b[2m2 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 23\u001b[2mms\u001b[22m\u001b[39m\n \u001b[32m✓\u001b[39m tests/internal-dashboard-integration.test.ts \u001b[2m(\u001b[22m\u001b[2m8 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 39\u001b[2mms\u001b[22m\u001b[39m\n \u001b[32m✓\u001b[39m tests/os-device-authority-worker.test.ts \u001b[2m(\u001b[22m\u001b[2m31 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 76\u001b[2mms\u001b[22m\u001b[39m\n\n\u001b[2m Test Files \u001b[22m \u001b[1m\u001b[32m7 passed\u001b[39m\u001b[22m\u001b[90m (7)\u001b[39m\n\u001b[2m Tests \u001b[22m \u001b[1m\u001b[32m69 passed\u001b[39m\u001b[22m\u001b[90m (69)\u001b[39m\n\u001b[2m Start at \u001b[22m 18:32:22\n\u001b[2m Duration \u001b[22m 411ms\u001b[2m (transform 791ms, setup 0ms, import 1.05s, tests 175ms, environment 0ms)\u001b[22m\n\n$ vitest run tests/canonical-device-identity.test.ts tests/install-control-plane.test.ts \"tests/install-control-plane-d1.test.ts\" tests/oauth-device-page-contract.test.ts tests/internal-dashboard-integration.test.ts tests/native-google-device-approval.test.ts tests/os-device-authority-worker.test.ts\n" + }, + { + "name": "OS canonical device approval syntax contracts", + "command": [ + "node", + "packages/os/scripts/check-syntax.js" + ], + "ruleId": "os-device-approval-canonical-identity", + "critical": true, + "status": "passed", + "exitCode": 0, + "signal": null, + "error": null, + "durationMs": 1687, + "outputTail": "workspace script syntax checks passed\n" + }, + { + "name": "OS one-click managed cloud contracts", + "command": [ + "bun", + "--cwd", + "packages/os", + "test", + "tests/managed-cloud-one-click-provisioning.test.ts", + "tests/managed-cloud-one-click-runner.test.ts", + "tests/launcher-nodes-control-plane.test.ts", + "tests/settings-site.test.ts", + "tests/cloudflare-worker-release-readiness.test.ts", + "tests/os-device-authority-architecture.test.ts", + "tests/os-device-authority-release-contract.test.ts", + "tests/managed-cloud-node-enrollment.test.ts", + "tests/managed-cloud-node-enrollment-cli.test.ts", + "tests/platform-managed-cloud-node.test.ts", + "tests/managed-cloud-pricing.test.ts", + "tests/managed-cloud-public-pricing.test.ts", + "tests/managed-cloud-node-instance-contract.test.ts", + "tests/managed-cloud-node-contract.test.ts" + ], + "ruleId": "os-managed-cloud-one-click-provisioning", + "critical": true, + "status": "passed", + "exitCode": 0, + "signal": null, + "error": null, + "durationMs": 1199, + "outputTail": "\n\u001b[1m\u001b[30m\u001b[46m RUN \u001b[49m\u001b[39m\u001b[22m \u001b[36mv4.1.5 \u001b[39m\u001b[90m/private/var/folders/vl/1zvhm0bj28d1dbvbcb12b39r0000gn/T/opensaas-worktrees/task-os-gate-internal-dashboard-routing-on-complete-access-config/packages/os\u001b[39m\n\n \u001b[32m✓\u001b[39m tests/managed-cloud-node-instance-contract.test.ts \u001b[2m(\u001b[22m\u001b[2m5 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 46\u001b[2mms\u001b[22m\u001b[39m\n \u001b[32m✓\u001b[39m tests/platform-managed-cloud-node.test.ts \u001b[2m(\u001b[22m\u001b[2m2 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 53\u001b[2mms\u001b[22m\u001b[39m\n \u001b[32m✓\u001b[39m tests/managed-cloud-node-contract.test.ts \u001b[2m(\u001b[22m\u001b[2m4 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 13\u001b[2mms\u001b[22m\u001b[39m\n \u001b[32m✓\u001b[39m tests/managed-cloud-public-pricing.test.ts \u001b[2m(\u001b[22m\u001b[2m6 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 22\u001b[2mms\u001b[22m\u001b[39m\n \u001b[32m✓\u001b[39m tests/managed-cloud-pricing.test.ts \u001b[2m(\u001b[22m\u001b[2m8 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 7\u001b[2mms\u001b[22m\u001b[39m\n \u001b[32m✓\u001b[39m tests/cloudflare-worker-release-readiness.test.ts \u001b[2m(\u001b[22m\u001b[2m3 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 2\u001b[2mms\u001b[22m\u001b[39m\n \u001b[32m✓\u001b[39m tests/os-device-authority-architecture.test.ts \u001b[2m(\u001b[22m\u001b[2m25 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 55\u001b[2mms\u001b[22m\u001b[39m\n \u001b[32m✓\u001b[39m tests/managed-cloud-one-click-provisioning.test.ts \u001b[2m(\u001b[22m\u001b[2m4 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 54\u001b[2mms\u001b[22m\u001b[39m\n \u001b[32m✓\u001b[39m tests/launcher-nodes-control-plane.test.ts \u001b[2m(\u001b[22m\u001b[2m7 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 39\u001b[2mms\u001b[22m\u001b[39m\n \u001b[32m✓\u001b[39m tests/managed-cloud-node-enrollment-cli.test.ts \u001b[2m(\u001b[22m\u001b[2m4 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[33m 572\u001b[2mms\u001b[22m\u001b[39m\n \u001b[33m\u001b[2m✓\u001b[22m\u001b[39m parses explicit home, onboarding, and status paths \u001b[33m 570\u001b[2mms\u001b[22m\u001b[39m\n \u001b[32m✓\u001b[39m tests/managed-cloud-node-enrollment.test.ts \u001b[2m(\u001b[22m\u001b[2m9 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[33m 600\u001b[2mms\u001b[22m\u001b[39m\n \u001b[33m\u001b[2m✓\u001b[22m\u001b[39m persists one owner-only node keypair and reuses it across enrollment retries \u001b[33m 590\u001b[2mms\u001b[22m\u001b[39m\n \u001b[32m✓\u001b[39m tests/managed-cloud-one-click-runner.test.ts \u001b[2m(\u001b[22m\u001b[2m2 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 3\u001b[2mms\u001b[22m\u001b[39m\n \u001b[32m✓\u001b[39m tests/settings-site.test.ts \u001b[2m(\u001b[22m\u001b[2m8 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 146\u001b[2mms\u001b[22m\u001b[39m\n \u001b[32m✓\u001b[39m tests/os-device-authority-release-contract.test.ts \u001b[2m(\u001b[22m\u001b[2m18 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[33m 486\u001b[2mms\u001b[22m\u001b[39m\n\n\u001b[2m Test Files \u001b[22m \u001b[1m\u001b[32m14 passed\u001b[39m\u001b[22m\u001b[90m (14)\u001b[39m\n\u001b[2m Tests \u001b[22m \u001b[1m\u001b[32m105 passed\u001b[39m\u001b[22m\u001b[90m (105)\u001b[39m\n\u001b[2m Start at \u001b[22m 18:32:24\n\u001b[2m Duration \u001b[22m 1.03s\u001b[2m (transform 2.15s, setup 0ms, import 2.90s, tests 2.10s, environment 1ms)\u001b[22m\n\n$ vitest run tests/managed-cloud-one-click-provisioning.test.ts tests/managed-cloud-one-click-runner.test.ts tests/launcher-nodes-control-plane.test.ts tests/settings-site.test.ts tests/cloudflare-worker-release-readiness.test.ts tests/os-device-authority-architecture.test.ts tests/os-device-authority-release-contract.test.ts tests/managed-cloud-node-enrollment.test.ts tests/managed-cloud-node-enrollment-cli.test.ts tests/platform-managed-cloud-node.test.ts tests/managed-cloud-pricing.test.ts tests/managed-cloud-public-pricing.test.ts tests/managed-cloud-node-instance-contract.test.ts tests/managed-cloud-node-contract.test.ts\n" + }, + { + "name": "OS device authority Worker contract", + "command": [ + "bun", + "x", + "vitest", + "run", + "packages/os/tests/os-device-authority-worker.test.ts" + ], + "ruleId": "os-managed-cloud-one-click-provisioning", + "critical": true, + "status": "passed", + "exitCode": 0, + "signal": null, + "error": null, + "durationMs": 718, + "outputTail": "\n\u001b[1m\u001b[46m RUN \u001b[49m\u001b[22m \u001b[36mv4.0.18 \u001b[39m\u001b[90m/private/var/folders/vl/1zvhm0bj28d1dbvbcb12b39r0000gn/T/opensaas-worktrees/task-os-gate-internal-dashboard-routing-on-complete-access-config\u001b[39m\n\n \u001b[32m✓\u001b[39m packages/os/tests/os-device-authority-worker.test.ts \u001b[2m(\u001b[22m\u001b[2m31 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 75\u001b[2mms\u001b[22m\u001b[39m\n\n\u001b[2m Test Files \u001b[22m \u001b[1m\u001b[32m1 passed\u001b[39m\u001b[22m\u001b[90m (1)\u001b[39m\n\u001b[2m Tests \u001b[22m \u001b[1m\u001b[32m31 passed\u001b[39m\u001b[22m\u001b[90m (31)\u001b[39m\n\u001b[2m Start at \u001b[22m 18:32:26\n\u001b[2m Duration \u001b[22m 425ms\u001b[2m (transform 238ms, setup 0ms, import 283ms, tests 75ms, environment 0ms)\u001b[22m\n\n" + } + ], + "failedSuites": [] + } + }, + "db": { + "skipped": false, + "passed": true, + "warnOnly": false, + "risks": [], + "findings": [] + }, + "commandVersion": 2 +} diff --git a/.task/os/gate-internal-dashboard-routing-on-complete-access-config/workpad.md b/.task/os/gate-internal-dashboard-routing-on-complete-access-config/workpad.md new file mode 100644 index 0000000000..2023d7aeaa --- /dev/null +++ b/.task/os/gate-internal-dashboard-routing-on-complete-access-config/workpad.md @@ -0,0 +1,86 @@ +# gate internal dashboard routing on complete access config + +branch: `task/os/gate-internal-dashboard-routing-on-complete-access-config` +stream: `stream/os` +pr: https://app.graphite.com/github/pr/consuelohq/opensaas/2149/gate-internal-dashboard-routing-on-complete-access-config +github pr: https://github.com/consuelohq/opensaas/pull/2149 +started: 2026-08-16 + +## acceptance criteria + +- [ ] Define explicit task acceptance criteria before coding. + +## plan + +1. Read the relevant code and update this plan before editing. + +## current status + +- Task started. Update this before publish. + +## files changed + +- none yet + +## workspace-owned: files changed + +- none yet + +## workspace-owned: activity log + +- 2026-08-16 22:29:59 fs.write: `.task/os/gate-internal-dashboard-routing-on-complete-access-config/workpad.md` +- 2026-08-16 22:32:10 fs.write: `.task/os/gate-internal-dashboard-routing-on-complete-access-config/workpad.md` + +## workspace-owned: validation evidence + +- 2026-08-16 22:32:26 `verify`: passed — OK + +## key decisions + +- none yet + +## notes for ko + +- none yet + +## improvements noticed + +- none yet + +## issues and recovery + +- none yet + +--- + +## publish checklist + +```bash +bun run task:push -- --message "type(os): description" --changed +bun run task:pr +bun run task:finish +``` + +## Test-first contract + +behavior under test: when the internal dashboard has no Access configuration, its shared-host paths must fall through to normal workspace routing instead of returning dashboard 401/403; complete configuration enables dashboard interception; partial configuration fails closed without falling through. +existing local pattern: install-control-plane docs state Access prerequisites must be satisfied before enabling the internal dashboard; the edge Worker already receives the three optional authorization bindings. +new or changed tests: add edge integration coverage for absent, complete, and partial dashboard Access configuration; remove unconditional deployment-secret requirements so a disabled dashboard does not block restoring the shared workspace Worker. +focused red command: cd packages/os && bun vitest run tests/internal-dashboard-integration.test.ts tests/cloudflare-worker-release-readiness.test.ts +expected red failure: absent dashboard bindings still intercept `/` and `/users`; release readiness currently treats an optional disabled dashboard as a hard deployment failure. +no-test waiver: not applicable + +- 2026-08-16 22:29:59 append: `.task/os/gate-internal-dashboard-routing-on-complete-access-config/workpad.md` + +## Implementation and verification + +- Added explicit dashboard Access states: disabled when all three bindings are absent, configured only when all three are present (or a test authorizer is injected), and partial otherwise. +- Disabled state falls through to canonical workspace routing, so an incomplete dashboard rollout cannot seize `/`, `/users`, or other shared-host routes. +- Partial state returns `workspace_auth_unavailable` 503 before dashboard data or handlers run. +- Configured state preserves workspace-session validation, HTML login redirect, and Cloudflare Access operator authorization. +- Removed the unconditional dashboard secret requirement from Worker deployment readiness because absence now means safely disabled; the existing core edge secrets remain mandatory. +- Red: focused suites failed 3 assertions (disabled route received 403 instead of fallthrough 404, partial config received 403 instead of 503, optional-disabled deploy was rejected). +- Green: `internal-dashboard-integration`, `install-control-plane-cloudflare`, and `cloudflare-worker-release-readiness` passed 14/14. +- Green: `yarn nx run consuelo-os:typecheck` passed. + +- 2026-08-16 22:32:10 append: `.task/os/gate-internal-dashboard-routing-on-complete-access-config/workpad.md` diff --git a/.task/os/repair-runtime-retention-and-watchdog-recovery/current.json b/.task/os/repair-runtime-retention-and-watchdog-recovery/current.json new file mode 100644 index 0000000000..06452f8252 --- /dev/null +++ b/.task/os/repair-runtime-retention-and-watchdog-recovery/current.json @@ -0,0 +1,19 @@ +{ + "area": "os", + "stream": "stream/os", + "taskBranch": "task/os/repair-runtime-retention-and-watchdog-recovery", + "baseBranch": "stream/os", + "sourceBranch": "stream/os", + "startFrom": "stream", + "prNumber": 2150, + "prUrl": "https://github.com/consuelohq/opensaas/pull/2150", + "githubPrUrl": "https://github.com/consuelohq/opensaas/pull/2150", + "graphitePrUrl": "https://app.graphite.com/github/pr/consuelohq/opensaas/2150/repair-runtime-retention-and-watchdog-recovery", + "taskPrUrl": "https://github.com/consuelohq/opensaas/pull/2150", + "taskGraphitePrUrl": "https://app.graphite.com/github/pr/consuelohq/opensaas/2150/repair-runtime-retention-and-watchdog-recovery", + "worktreePath": "/private/var/folders/vl/1zvhm0bj28d1dbvbcb12b39r0000gn/T/opensaas-worktrees/task-os-repair-runtime-retention-and-watchdog-recovery", + "taskSession": "tsk_af28ff74214b", + "tmuxSession": "opensaas-os-repair-runtime-retention-and-watchdog-recovery-af28ff74", + "sessionPath": "/private/var/folders/vl/1zvhm0bj28d1dbvbcb12b39r0000gn/T/opensaas-worktrees/task-os-repair-runtime-retention-and-watchdog-recovery/.task/os/repair-runtime-retention-and-watchdog-recovery/session.json", + "createdAt": "2026-08-16T22:46:56.311Z" +} diff --git a/.task/os/repair-runtime-retention-and-watchdog-recovery/session.json b/.task/os/repair-runtime-retention-and-watchdog-recovery/session.json new file mode 100644 index 0000000000..3cfc2b9616 --- /dev/null +++ b/.task/os/repair-runtime-retention-and-watchdog-recovery/session.json @@ -0,0 +1,15 @@ +{ + "taskSession": "tsk_af28ff74214b", + "tmuxSession": "opensaas-os-repair-runtime-retention-and-watchdog-recovery-af28ff74", + "area": "os", + "stream": "stream/os", + "taskBranch": "task/os/repair-runtime-retention-and-watchdog-recovery", + "branch": "task/os/repair-runtime-retention-and-watchdog-recovery", + "worktreePath": "/private/var/folders/vl/1zvhm0bj28d1dbvbcb12b39r0000gn/T/opensaas-worktrees/task-os-repair-runtime-retention-and-watchdog-recovery", + "worktree": "/private/var/folders/vl/1zvhm0bj28d1dbvbcb12b39r0000gn/T/opensaas-worktrees/task-os-repair-runtime-retention-and-watchdog-recovery", + "prNumber": 2150, + "prUrl": "https://github.com/consuelohq/opensaas/pull/2150", + "createdAt": "2026-08-16T22:46:56.310Z", + "tmuxCreated": true, + "sessionPath": "/private/var/folders/vl/1zvhm0bj28d1dbvbcb12b39r0000gn/T/opensaas-worktrees/task-os-repair-runtime-retention-and-watchdog-recovery/.task/os/repair-runtime-retention-and-watchdog-recovery/session.json" +} diff --git a/.task/os/repair-runtime-retention-and-watchdog-recovery/workpad.md b/.task/os/repair-runtime-retention-and-watchdog-recovery/workpad.md new file mode 100644 index 0000000000..4ba35edd71 --- /dev/null +++ b/.task/os/repair-runtime-retention-and-watchdog-recovery/workpad.md @@ -0,0 +1,154 @@ +# Repair runtime retention and watchdog recovery + +branch: `task/os/repair-runtime-retention-and-watchdog-recovery` +stream: `stream/os` +pr: https://app.graphite.com/github/pr/consuelohq/opensaas/2150/repair-runtime-retention-and-watchdog-recovery +github pr: https://github.com/consuelohq/opensaas/pull/2150 +started: 2026-08-16 + +## acceptance criteria + +- [x] Remove corrupt, obsolete runtime releases without weakening verification for current, previous, pinned, or content-base releases. +- [x] Exclude worktrees and vendor trees from both semantic index implementations while retaining existing generated-output exclusions. +- [x] Install an integrity-checked `consuelo-os` Bun clone before persisting daemon runtime paths. +- [x] Smoke-test one isolated worker without contending with the already-running supervisor. +- [x] Let the watchdog fall back to bounded launchd recovery only when canonical rolling recovery rejects an unhealthy pool. +- [x] Complete the focused suite and typecheck/verification gates. +- [ ] Publish the task. + +## plan + +1. Reproduce each retention, index, installer, and watchdog regression with a focused red test. +2. Implement the smallest bounded fixes and validate a live named-executable cutover. +3. Run the focused and package-level gates, inspect the final diff, and publish to `stream/os`. + +## current status + +- Implementation and verification are complete. Publish remains. + +## files changed + +- `packages/os/scripts/bootstrap.sh` +- `packages/os/scripts/install-system-daemons.sh` +- `packages/os/scripts/lib/index/indexer.js` +- `packages/os/scripts/lib/lifecycle/retention.ts` +- `packages/os/scripts/start-consuelo-daemon.sh` +- `packages/os/scripts/workspace-watchdog.sh` +- `packages/os/tests/index-path-exclusions.test.ts` +- `packages/os/tests/installer-runtime-dependencies.test.ts` +- `packages/os/tests/lifecycle-retention-uninstall.test.ts` +- `packages/os/tests/system-daemon-reliability.test.ts` +- `packages/workspace/scripts/lib/index/indexer.js` +- `packages/workspace/tests/index-path-exclusions.test.js` + +## workspace-owned: files changed + +- none yet + +## workspace-owned: activity log + +- 2026-08-16 22:47:14 fs.write: `.task/os/repair-runtime-retention-and-watchdog-recovery/workpad.md` +- 2026-08-16 23:09:59 fs.write: `.task/os/repair-runtime-retention-and-watchdog-recovery/workpad.md` +- 2026-08-16 23:11:47 fs.write: `.task/os/repair-runtime-retention-and-watchdog-recovery/workpad.md` + +## workspace-owned: validation evidence + +- Retention regression: red on corrupt obsolete bundle digest; green after classifying only unprotected obsolete releases for deletion. +- Index path regressions: red on `vendor` and nested worktree paths; green for both mirrored indexers. +- Named executable regression: red on missing `ensure_named_bun_runtime`; green after APFS clone/copy, byte comparison, and atomic replacement. +- Installer stage regression: red on missing single-worker mode; green plus live smoke worker `/ready` on port 10851 while the production supervisor remained active. +- Watchdog fallback regression: red with no launchd recovery after canonical CLI failure; green with a bounded `kickstart` fallback. +- Live node: ports 46321 and 46322 run as `consuelo-os`; Caddy 46320, both worker readiness endpoints, and pooled health are green. +- Live watchdog: `StartInterval=30`, last exit code 0, and idle `not running` state between successful probes. +- Full lifecycle retention/uninstall suite: 21/21 passing after bringing its signed-bundle fixture up to the current recovery-capability contract. +- Full daemon reliability and index slices: 16/16 passing; installer named-runtime and isolated-stage regressions passing. +- `yarn nx run consuelo-os:typecheck`: passing. +- `git diff --check` and `bash -n` for every changed shell entrypoint: passing. +- `yarn nx run consuelo-os:test` cannot enter the package because Yarn does not recognize `packages/os` in this temporary worktree. Direct Vitest execution is used for the affected suites. +- The installer suite's 10 unrelated dry-run fixture failures reproduce unchanged from `HEAD` in an isolated archive; this repair adds two passing tests and does not add installer failures. + +## key decisions + +- Retention continues to fail closed for every protected release; only canonical, unprotected obsolete release directories can bypass strict verification on their way to deletion. +- The installer stages `server/main.ts` as one supervised smoke worker so it does not open the singleton lifecycle endpoint or reuse the production pool snapshot. +- Normal CLI restart remains rolling and non-destructive. Only the already-thresholded watchdog recovery path may use launchd kickstart after rolling recovery returns non-zero. +- The process-name change reuses Bun's existing embedded signature through an APFS clone/copy; no application bundle or paid Apple signing membership is required. + +## notes for ko + +- The historical watchdog failures followed SQLite `unable to open database file` errors under critical disk pressure. The pool was already non-ready, so rolling replacement correctly refused it; the watchdog lacked the final launchd recovery step. + +## improvements noticed + +- none yet + +## issues and recovery + +- The canonical daemon installer smoke test initially failed because it started a second supervisor against the live worker-pool snapshot. The new single-worker stage mode removes that conflict. +- Codex could boot out but not bootstrap the GUI LaunchAgent from its app sandbox. Ko reloaded the validated plist once from Terminal; the connector and both named workers recovered. + +--- + +## publish checklist + +```bash +bun run task:push -- --message "type(os): description" --changed +bun run task:pr +bun run task:finish +``` + +## Test-first contract + +behavior under test: runtime retention accepts a verified installed release whose directory name uses the canonical sha256- form, then removes all releases except current and previous; watchdog restart paths converge without a permanent lock or stopped LaunchAgent. +existing local pattern: inspect lifecycle engine retention validation and adjacent focused tests before editing. +new or changed tests: add a focused regression reproducing the installed-directory digest mismatch and the expected two-release keep set; add watchdog coverage only if the bug is in repository logic rather than current launchd state. +focused red command: to be selected from the nearest lifecycle test target after inspection. +expected red failure: retention rejects the canonical installed release or preserves obsolete releases because identity comparison uses incompatible digest forms. +no-test waiver: not applicable. + +- 2026-08-16 22:47:14 append: `.task/os/repair-runtime-retention-and-watchdog-recovery/workpad.md` + +- 2026-08-16 22:49:59 apply-patch: `packages/os/tests/lifecycle-retention-uninstall.test.ts` +- 2026-08-16 22:51:31 apply-patch: `packages/os/tests/lifecycle-retention-uninstall.test.ts` +- 2026-08-16 22:52:10 apply-patch: `packages/os/scripts/lib/lifecycle/retention.ts` +- 2026-08-16 22:52:19 apply-patch: `packages/os/tests/lifecycle-retention-uninstall.test.ts` +## Semantic index path policy contract + +behavior under test: the mirrored workspace and OS semantic indexers reject vendor trees and nested worktree roots, while continuing to index ordinary source files; existing exclusions already cover node_modules, dist/build/out, caches, generated output, coverage, and task metadata. +existing local pattern: both indexers export isIndexablePath and keep a mirrored EXCLUDE_DIRS set. +new or changed tests: add focused path-policy tests for both package copies. +focused red command: yarn vitest run packages/workspace/tests/index-path-exclusions.test.js packages/os/tests/index-path-exclusions.test.ts +expected red failure: vendor and worktrees paths are currently accepted. +no-test waiver: not applicable. + +- 2026-08-16 23:09:59 append: `.task/os/repair-runtime-retention-and-watchdog-recovery/workpad.md` + +- 2026-08-16 23:10:18 apply-patch: `packages/workspace/tests/index-path-exclusions.test.js` +- 2026-08-16 23:10:18 apply-patch: `packages/os/tests/index-path-exclusions.test.ts` +- 2026-08-16 23:10:34 apply-patch: `packages/workspace/scripts/lib/index/indexer.js` +- 2026-08-16 23:10:34 apply-patch: `packages/os/scripts/lib/index/indexer.js` +## Named service executable contract + +behavior under test: macOS bootstrap atomically materializes an integrity-checked Bun clone at $CONSUELO_HOME/bin/consuelo-os and persists it as BUN_BIN before daemon generation, so supervisor and worker process names are Consuelo-owned without requiring app signing. +existing local pattern: Windows already copies Bun into the Consuelo bin directory and verifies source/destination SHA-256 before service registration. +new or changed tests: extend installer runtime dependency contract with named executable, APFS clone fallback, byte comparison, atomic replacement, and ordering assertions. +focused red command: yarn vitest run packages/os/tests/installer-runtime-dependencies.test.ts -t 'named Consuelo service executable' +expected red failure: bootstrap does not yet contain ensure_named_bun_runtime or the consuelo-os target. +no-test waiver: not applicable. + +- 2026-08-16 23:11:47 append: `.task/os/repair-runtime-retention-and-watchdog-recovery/workpad.md` + +- 2026-08-16 23:14:04 apply-patch: `packages/os/tests/installer-runtime-dependencies.test.ts` +- 2026-08-16 23:15:06 apply-patch: `packages/os/scripts/bootstrap.sh` +- 2026-08-16 23:28:22 apply-patch: `packages/os/tests/installer-runtime-dependencies.test.ts` +- 2026-08-16 23:28:42 apply-patch: `packages/os/scripts/start-consuelo-daemon.sh` +- 2026-08-16 23:28:42 apply-patch: `packages/os/scripts/install-system-daemons.sh` +- 2026-08-16 23:31:55 apply-patch: `packages/os/tests/system-daemon-reliability.test.ts` +- 2026-08-16 23:32:15 apply-patch: `packages/os/scripts/workspace-watchdog.sh` + +- 2026-08-16 23:33:04 apply-patch: `.task/os/repair-runtime-retention-and-watchdog-recovery/workpad.md` +- 2026-08-16 23:36:21 apply-patch: `packages/os/tests/lifecycle-retention-uninstall.test.ts` +- 2026-08-16 23:36:55 apply-patch: `packages/os/tests/lifecycle-retention-uninstall.test.ts` +- 2026-08-16 23:38:29 apply-patch: `packages/os/tests/facade/__snapshots__/facade.test.ts.snap` + +- 2026-08-16 23:38:48 apply-patch: `.task/os/repair-runtime-retention-and-watchdog-recovery/workpad.md` \ No newline at end of file diff --git a/.task/os/restore-internal-dashboard-browser-auth-handoff/current.json b/.task/os/restore-internal-dashboard-browser-auth-handoff/current.json new file mode 100644 index 0000000000..7d0c0dcaee --- /dev/null +++ b/.task/os/restore-internal-dashboard-browser-auth-handoff/current.json @@ -0,0 +1,19 @@ +{ + "area": "os", + "stream": "stream/os", + "taskBranch": "task/os/restore-internal-dashboard-browser-auth-handoff", + "baseBranch": "stream/os", + "sourceBranch": "stream/os", + "startFrom": "stream", + "prNumber": 2147, + "prUrl": "https://github.com/consuelohq/opensaas/pull/2147", + "githubPrUrl": "https://github.com/consuelohq/opensaas/pull/2147", + "graphitePrUrl": "https://app.graphite.com/github/pr/consuelohq/opensaas/2147/restore-internal-dashboard-browser-auth-handoff", + "taskPrUrl": "https://github.com/consuelohq/opensaas/pull/2147", + "taskGraphitePrUrl": "https://app.graphite.com/github/pr/consuelohq/opensaas/2147/restore-internal-dashboard-browser-auth-handoff", + "worktreePath": "/private/var/folders/vl/1zvhm0bj28d1dbvbcb12b39r0000gn/T/opensaas-worktrees/task-os-restore-internal-dashboard-browser-auth-handoff", + "taskSession": "tsk_fdaa1f3e0fdd", + "tmuxSession": "opensaas-os-restore-internal-dashboard-browser-auth-handoff-fdaa1f3e", + "sessionPath": "/private/var/folders/vl/1zvhm0bj28d1dbvbcb12b39r0000gn/T/opensaas-worktrees/task-os-restore-internal-dashboard-browser-auth-handoff/.task/os/restore-internal-dashboard-browser-auth-handoff/session.json", + "createdAt": "2026-08-16T22:16:27.043Z" +} diff --git a/.task/os/restore-internal-dashboard-browser-auth-handoff/session.json b/.task/os/restore-internal-dashboard-browser-auth-handoff/session.json new file mode 100644 index 0000000000..de30256873 --- /dev/null +++ b/.task/os/restore-internal-dashboard-browser-auth-handoff/session.json @@ -0,0 +1,15 @@ +{ + "taskSession": "tsk_fdaa1f3e0fdd", + "tmuxSession": "opensaas-os-restore-internal-dashboard-browser-auth-handoff-fdaa1f3e", + "area": "os", + "stream": "stream/os", + "taskBranch": "task/os/restore-internal-dashboard-browser-auth-handoff", + "branch": "task/os/restore-internal-dashboard-browser-auth-handoff", + "worktreePath": "/private/var/folders/vl/1zvhm0bj28d1dbvbcb12b39r0000gn/T/opensaas-worktrees/task-os-restore-internal-dashboard-browser-auth-handoff", + "worktree": "/private/var/folders/vl/1zvhm0bj28d1dbvbcb12b39r0000gn/T/opensaas-worktrees/task-os-restore-internal-dashboard-browser-auth-handoff", + "prNumber": 2147, + "prUrl": "https://github.com/consuelohq/opensaas/pull/2147", + "createdAt": "2026-08-16T22:16:27.043Z", + "tmuxCreated": true, + "sessionPath": "/private/var/folders/vl/1zvhm0bj28d1dbvbcb12b39r0000gn/T/opensaas-worktrees/task-os-restore-internal-dashboard-browser-auth-handoff/.task/os/restore-internal-dashboard-browser-auth-handoff/session.json" +} diff --git a/.task/os/restore-internal-dashboard-browser-auth-handoff/verify.json b/.task/os/restore-internal-dashboard-browser-auth-handoff/verify.json new file mode 100644 index 0000000000..881f1a45a0 --- /dev/null +++ b/.task/os/restore-internal-dashboard-browser-auth-handoff/verify.json @@ -0,0 +1,232 @@ +{ + "result": "pass", + "publishValid": true, + "mode": "full", + "branch": "task/os/restore-internal-dashboard-browser-auth-handoff", + "base": "origin/task/os/restore-internal-dashboard-browser-auth-handoff", + "headSha": "c3a7b2bade5e62fc83f7c8a7bdf595786121f5bb", + "changeHash": "fb326067cf0da51621caa195e33ced1c3ee184354dd1025439ef1972ac290720", + "changedFiles": [ + "packages/os/cloudflare/workspace-edge/src/index.ts", + "packages/os/scripts/lib/cloudflare-worker-release-readiness.ts", + "packages/os/tests/cloudflare-worker-release-readiness.test.ts", + "packages/os/tests/internal-dashboard-integration.test.ts" + ], + "verifiedAt": "2026-08-16T22:27:05.042Z", + "review": { + "skipped": false, + "passed": true, + "status": 0 + }, + "testSelection": { + "skipped": false, + "passed": true, + "status": 0, + "data": { + "kind": "selection", + "passed": true, + "changedFiles": [ + ".task/os/restore-internal-dashboard-browser-auth-handoff/current.json", + ".task/os/restore-internal-dashboard-browser-auth-handoff/session.json", + ".task/os/restore-internal-dashboard-browser-auth-handoff/workpad.md", + ".task/tasks/os/restore-internal-dashboard-browser-auth-handoff.json", + "packages/os/cloudflare/workspace-edge/src/index.ts", + "packages/os/scripts/lib/cloudflare-worker-release-readiness.ts", + "packages/os/tests/cloudflare-worker-release-readiness.test.ts", + "packages/os/tests/internal-dashboard-integration.test.ts" + ], + "matchedRules": [ + { + "id": "os-device-approval-canonical-identity", + "critical": true, + "exclusive": true, + "reason": "Native OS device-approval and canonical-identity changes must prove the exact auth, tenant, and control-plane contracts without selecting unsafe unrelated package-wide OS tests.", + "matchedFiles": [ + "packages/os/tests/internal-dashboard-integration.test.ts" + ], + "origin": "explicit" + }, + { + "id": "os-managed-cloud-one-click-provisioning", + "critical": true, + "exclusive": true, + "reason": "One-click managed-cloud control-plane, launcher, provisioning-runner, enrollment, and Worker changes must prove their focused security and lifecycle contracts without selecting the historically red unrelated OS package suite.", + "matchedFiles": [ + "packages/os/cloudflare/workspace-edge/src/index.ts", + "packages/os/scripts/lib/cloudflare-worker-release-readiness.ts", + "packages/os/tests/cloudflare-worker-release-readiness.test.ts" + ], + "origin": "explicit" + } + ], + "selectedSuites": [ + { + "name": "OS canonical device approval contracts", + "command": [ + "bun", + "--cwd", + "packages/os", + "test", + "tests/canonical-device-identity.test.ts", + "tests/install-control-plane.test.ts", + "tests/install-control-plane-d1.test.ts", + "tests/oauth-device-page-contract.test.ts", + "tests/internal-dashboard-integration.test.ts", + "tests/native-google-device-approval.test.ts", + "tests/os-device-authority-worker.test.ts" + ], + "ruleId": "os-device-approval-canonical-identity", + "critical": true + }, + { + "name": "OS canonical device approval syntax contracts", + "command": [ + "node", + "packages/os/scripts/check-syntax.js" + ], + "ruleId": "os-device-approval-canonical-identity", + "critical": true + }, + { + "name": "OS one-click managed cloud contracts", + "command": [ + "bun", + "--cwd", + "packages/os", + "test", + "tests/managed-cloud-one-click-provisioning.test.ts", + "tests/managed-cloud-one-click-runner.test.ts", + "tests/launcher-nodes-control-plane.test.ts", + "tests/settings-site.test.ts", + "tests/cloudflare-worker-release-readiness.test.ts", + "tests/os-device-authority-architecture.test.ts", + "tests/os-device-authority-release-contract.test.ts", + "tests/managed-cloud-node-enrollment.test.ts", + "tests/managed-cloud-node-enrollment-cli.test.ts", + "tests/platform-managed-cloud-node.test.ts", + "tests/managed-cloud-pricing.test.ts", + "tests/managed-cloud-public-pricing.test.ts", + "tests/managed-cloud-node-instance-contract.test.ts", + "tests/managed-cloud-node-contract.test.ts" + ], + "ruleId": "os-managed-cloud-one-click-provisioning", + "critical": true + }, + { + "name": "OS device authority Worker contract", + "command": [ + "bun", + "x", + "vitest", + "run", + "packages/os/tests/os-device-authority-worker.test.ts" + ], + "ruleId": "os-managed-cloud-one-click-provisioning", + "critical": true + } + ], + "level": "pass", + "zeroSuiteReason": null, + "run": true, + "runResults": [ + { + "name": "OS canonical device approval contracts", + "command": [ + "bun", + "--cwd", + "packages/os", + "test", + "tests/canonical-device-identity.test.ts", + "tests/install-control-plane.test.ts", + "tests/install-control-plane-d1.test.ts", + "tests/oauth-device-page-contract.test.ts", + "tests/internal-dashboard-integration.test.ts", + "tests/native-google-device-approval.test.ts", + "tests/os-device-authority-worker.test.ts" + ], + "ruleId": "os-device-approval-canonical-identity", + "critical": true, + "status": "passed", + "exitCode": 0, + "signal": null, + "error": null, + "durationMs": 571, + "outputTail": "\n\u001b[1m\u001b[30m\u001b[46m RUN \u001b[49m\u001b[39m\u001b[22m \u001b[36mv4.1.5 \u001b[39m\u001b[90m/private/var/folders/vl/1zvhm0bj28d1dbvbcb12b39r0000gn/T/opensaas-worktrees/task-os-restore-internal-dashboard-browser-auth-handoff/packages/os\u001b[39m\n\n \u001b[32m✓\u001b[39m tests/oauth-device-page-contract.test.ts \u001b[2m(\u001b[22m\u001b[2m5 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 2\u001b[2mms\u001b[22m\u001b[39m\n \u001b[32m✓\u001b[39m tests/install-control-plane-d1.test.ts \u001b[2m(\u001b[22m\u001b[2m6 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 11\u001b[2mms\u001b[22m\u001b[39m\n \u001b[32m✓\u001b[39m tests/install-control-plane.test.ts \u001b[2m(\u001b[22m\u001b[2m9 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 14\u001b[2mms\u001b[22m\u001b[39m\n \u001b[32m✓\u001b[39m tests/canonical-device-identity.test.ts \u001b[2m(\u001b[22m\u001b[2m8 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 10\u001b[2mms\u001b[22m\u001b[39m\n \u001b[32m✓\u001b[39m tests/native-google-device-approval.test.ts \u001b[2m(\u001b[22m\u001b[2m2 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 20\u001b[2mms\u001b[22m\u001b[39m\n \u001b[32m✓\u001b[39m tests/internal-dashboard-integration.test.ts \u001b[2m(\u001b[22m\u001b[2m6 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 36\u001b[2mms\u001b[22m\u001b[39m\n \u001b[32m✓\u001b[39m tests/os-device-authority-worker.test.ts \u001b[2m(\u001b[22m\u001b[2m31 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 71\u001b[2mms\u001b[22m\u001b[39m\n\n\u001b[2m Test Files \u001b[22m \u001b[1m\u001b[32m7 passed\u001b[39m\u001b[22m\u001b[90m (7)\u001b[39m\n\u001b[2m Tests \u001b[22m \u001b[1m\u001b[32m67 passed\u001b[39m\u001b[22m\u001b[90m (67)\u001b[39m\n\u001b[2m Start at \u001b[22m 18:27:00\n\u001b[2m Duration \u001b[22m 399ms\u001b[2m (transform 701ms, setup 0ms, import 979ms, tests 164ms, environment 0ms)\u001b[22m\n\n$ vitest run tests/canonical-device-identity.test.ts tests/install-control-plane.test.ts \"tests/install-control-plane-d1.test.ts\" tests/oauth-device-page-contract.test.ts tests/internal-dashboard-integration.test.ts tests/native-google-device-approval.test.ts tests/os-device-authority-worker.test.ts\n" + }, + { + "name": "OS canonical device approval syntax contracts", + "command": [ + "node", + "packages/os/scripts/check-syntax.js" + ], + "ruleId": "os-device-approval-canonical-identity", + "critical": true, + "status": "passed", + "exitCode": 0, + "signal": null, + "error": null, + "durationMs": 1656, + "outputTail": "workspace script syntax checks passed\n" + }, + { + "name": "OS one-click managed cloud contracts", + "command": [ + "bun", + "--cwd", + "packages/os", + "test", + "tests/managed-cloud-one-click-provisioning.test.ts", + "tests/managed-cloud-one-click-runner.test.ts", + "tests/launcher-nodes-control-plane.test.ts", + "tests/settings-site.test.ts", + "tests/cloudflare-worker-release-readiness.test.ts", + "tests/os-device-authority-architecture.test.ts", + "tests/os-device-authority-release-contract.test.ts", + "tests/managed-cloud-node-enrollment.test.ts", + "tests/managed-cloud-node-enrollment-cli.test.ts", + "tests/platform-managed-cloud-node.test.ts", + "tests/managed-cloud-pricing.test.ts", + "tests/managed-cloud-public-pricing.test.ts", + "tests/managed-cloud-node-instance-contract.test.ts", + "tests/managed-cloud-node-contract.test.ts" + ], + "ruleId": "os-managed-cloud-one-click-provisioning", + "critical": true, + "status": "passed", + "exitCode": 0, + "signal": null, + "error": null, + "durationMs": 1152, + "outputTail": "\n\u001b[1m\u001b[30m\u001b[46m RUN \u001b[49m\u001b[39m\u001b[22m \u001b[36mv4.1.5 \u001b[39m\u001b[90m/private/var/folders/vl/1zvhm0bj28d1dbvbcb12b39r0000gn/T/opensaas-worktrees/task-os-restore-internal-dashboard-browser-auth-handoff/packages/os\u001b[39m\n\n \u001b[32m✓\u001b[39m tests/managed-cloud-node-instance-contract.test.ts \u001b[2m(\u001b[22m\u001b[2m5 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 48\u001b[2mms\u001b[22m\u001b[39m\n \u001b[32m✓\u001b[39m tests/platform-managed-cloud-node.test.ts \u001b[2m(\u001b[22m\u001b[2m2 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 45\u001b[2mms\u001b[22m\u001b[39m\n \u001b[32m✓\u001b[39m tests/managed-cloud-node-contract.test.ts \u001b[2m(\u001b[22m\u001b[2m4 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 11\u001b[2mms\u001b[22m\u001b[39m\n \u001b[32m✓\u001b[39m tests/managed-cloud-public-pricing.test.ts \u001b[2m(\u001b[22m\u001b[2m6 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 20\u001b[2mms\u001b[22m\u001b[39m\n \u001b[32m✓\u001b[39m tests/managed-cloud-pricing.test.ts \u001b[2m(\u001b[22m\u001b[2m8 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 7\u001b[2mms\u001b[22m\u001b[39m\n \u001b[32m✓\u001b[39m tests/cloudflare-worker-release-readiness.test.ts \u001b[2m(\u001b[22m\u001b[2m3 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 3\u001b[2mms\u001b[22m\u001b[39m\n \u001b[32m✓\u001b[39m tests/managed-cloud-one-click-provisioning.test.ts \u001b[2m(\u001b[22m\u001b[2m4 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 43\u001b[2mms\u001b[22m\u001b[39m\n \u001b[32m✓\u001b[39m tests/os-device-authority-architecture.test.ts \u001b[2m(\u001b[22m\u001b[2m25 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 47\u001b[2mms\u001b[22m\u001b[39m\n \u001b[32m✓\u001b[39m tests/launcher-nodes-control-plane.test.ts \u001b[2m(\u001b[22m\u001b[2m7 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 34\u001b[2mms\u001b[22m\u001b[39m\n \u001b[32m✓\u001b[39m tests/managed-cloud-node-enrollment-cli.test.ts \u001b[2m(\u001b[22m\u001b[2m4 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[33m 563\u001b[2mms\u001b[22m\u001b[39m\n \u001b[33m\u001b[2m✓\u001b[22m\u001b[39m parses explicit home, onboarding, and status paths \u001b[33m 561\u001b[2mms\u001b[22m\u001b[39m\n \u001b[32m✓\u001b[39m tests/managed-cloud-node-enrollment.test.ts \u001b[2m(\u001b[22m\u001b[2m9 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[33m 576\u001b[2mms\u001b[22m\u001b[39m\n \u001b[33m\u001b[2m✓\u001b[22m\u001b[39m persists one owner-only node keypair and reuses it across enrollment retries \u001b[33m 567\u001b[2mms\u001b[22m\u001b[39m\n \u001b[32m✓\u001b[39m tests/managed-cloud-one-click-runner.test.ts \u001b[2m(\u001b[22m\u001b[2m2 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 2\u001b[2mms\u001b[22m\u001b[39m\n \u001b[32m✓\u001b[39m tests/settings-site.test.ts \u001b[2m(\u001b[22m\u001b[2m8 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 138\u001b[2mms\u001b[22m\u001b[39m\n \u001b[32m✓\u001b[39m tests/os-device-authority-release-contract.test.ts \u001b[2m(\u001b[22m\u001b[2m18 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[33m 459\u001b[2mms\u001b[22m\u001b[39m\n\n\u001b[2m Test Files \u001b[22m \u001b[1m\u001b[32m14 passed\u001b[39m\u001b[22m\u001b[90m (14)\u001b[39m\n\u001b[2m Tests \u001b[22m \u001b[1m\u001b[32m105 passed\u001b[39m\u001b[22m\u001b[90m (105)\u001b[39m\n\u001b[2m Start at \u001b[22m 18:27:03\n\u001b[2m Duration \u001b[22m 984ms\u001b[2m (transform 2.18s, setup 0ms, import 2.86s, tests 2.00s, environment 1ms)\u001b[22m\n\n$ vitest run tests/managed-cloud-one-click-provisioning.test.ts tests/managed-cloud-one-click-runner.test.ts tests/launcher-nodes-control-plane.test.ts tests/settings-site.test.ts tests/cloudflare-worker-release-readiness.test.ts tests/os-device-authority-architecture.test.ts tests/os-device-authority-release-contract.test.ts tests/managed-cloud-node-enrollment.test.ts tests/managed-cloud-node-enrollment-cli.test.ts tests/platform-managed-cloud-node.test.ts tests/managed-cloud-pricing.test.ts tests/managed-cloud-public-pricing.test.ts tests/managed-cloud-node-instance-contract.test.ts tests/managed-cloud-node-contract.test.ts\n" + }, + { + "name": "OS device authority Worker contract", + "command": [ + "bun", + "x", + "vitest", + "run", + "packages/os/tests/os-device-authority-worker.test.ts" + ], + "ruleId": "os-managed-cloud-one-click-provisioning", + "critical": true, + "status": "passed", + "exitCode": 0, + "signal": null, + "error": null, + "durationMs": 724, + "outputTail": "\n\u001b[1m\u001b[46m RUN \u001b[49m\u001b[22m \u001b[36mv4.0.18 \u001b[39m\u001b[90m/private/var/folders/vl/1zvhm0bj28d1dbvbcb12b39r0000gn/T/opensaas-worktrees/task-os-restore-internal-dashboard-browser-auth-handoff\u001b[39m\n\n \u001b[32m✓\u001b[39m packages/os/tests/os-device-authority-worker.test.ts \u001b[2m(\u001b[22m\u001b[2m31 tests\u001b[22m\u001b[2m)\u001b[22m\u001b[32m 62\u001b[2mms\u001b[22m\u001b[39m\n\n\u001b[2m Test Files \u001b[22m \u001b[1m\u001b[32m1 passed\u001b[39m\u001b[22m\u001b[90m (1)\u001b[39m\n\u001b[2m Tests \u001b[22m \u001b[1m\u001b[32m31 passed\u001b[39m\u001b[22m\u001b[90m (31)\u001b[39m\n\u001b[2m Start at \u001b[22m 18:27:04\n\u001b[2m Duration \u001b[22m 412ms\u001b[2m (transform 236ms, setup 0ms, import 279ms, tests 62ms, environment 0ms)\u001b[22m\n\n" + } + ], + "failedSuites": [] + } + }, + "db": { + "skipped": false, + "passed": true, + "warnOnly": false, + "risks": [], + "findings": [] + }, + "commandVersion": 2 +} diff --git a/.task/os/restore-internal-dashboard-browser-auth-handoff/workpad.md b/.task/os/restore-internal-dashboard-browser-auth-handoff/workpad.md new file mode 100644 index 0000000000..56b0a16550 --- /dev/null +++ b/.task/os/restore-internal-dashboard-browser-auth-handoff/workpad.md @@ -0,0 +1,93 @@ +# restore internal dashboard browser auth handoff + +branch: `task/os/restore-internal-dashboard-browser-auth-handoff` +stream: `stream/os` +pr: https://app.graphite.com/github/pr/consuelohq/opensaas/2147/restore-internal-dashboard-browser-auth-handoff +github pr: https://github.com/consuelohq/opensaas/pull/2147 +started: 2026-08-16 + +## acceptance criteria + +- [ ] Define explicit task acceptance criteria before coding. + +## plan + +1. Read the relevant code and update this plan before editing. + +## current status + +- Task started. Update this before publish. + +## files changed + +- none yet + +## workspace-owned: files changed + +- none yet + +## workspace-owned: activity log + +- 2026-08-16 22:16:49 fs.write: `.task/os/restore-internal-dashboard-browser-auth-handoff/workpad.md` +- 2026-08-16 22:26:36 fs.write: `.task/os/restore-internal-dashboard-browser-auth-handoff/workpad.md` + +## workspace-owned: validation evidence + +- 2026-08-16 22:27:05 `verify`: passed — OK + +## key decisions + +- none yet + +## notes for ko + +- none yet + +## improvements noticed + +- none yet + +## issues and recovery + +- none yet + +--- + +## publish checklist + +```bash +bun run task:push -- --message "type(os): description" --changed +bun run task:pr +bun run task:finish +``` + +## Test-first contract + +behavior under test: an unauthenticated GET browser navigation to the internal dashboard (Accept includes text/html) starts the canonical Google web-login handoff and preserves pathname/query in return_to; non-HTML and API requests remain fail-closed with JSON 401; the owner-only dashboard authorization gate still runs after a valid workspace session. +existing local pattern: packages/os/scripts/lib/workspace-cloudflare-edge-router.ts already redirects unauthenticated HTML navigations to https://os.consuelohq.com/login/google/start with purpose=web and return_to. +new or changed tests: change packages/os/tests/internal-dashboard-integration.test.ts to require 302 + canonical Location for anonymous HTML / and /users requests, and retain an explicit JSON 401 assertion. +focused red command: yarn nx run os:test -- --run packages/os/tests/internal-dashboard-integration.test.ts +expected red failure: current dashboard edge short-circuit returns 401 for HTML / and /users instead of 302. +no-test waiver: not applicable + +- 2026-08-16 22:16:49 append: `.task/os/restore-internal-dashboard-browser-auth-handoff/workpad.md` + +## Diagnosis and implementation + +- Live anonymous root returned 401 `{error: workspace_session_required}` while `/traces` correctly returned a 302 Google web-login redirect. +- Current upstream dashboard dispatch short-circuited generic workspace routing and explicitly asserted 401 for HTML `/` and `/users` requests. +- Fixed dashboard workspace-session rejection to redirect only GET requests accepting `text/html`, preserving pathname and query in a relative `return_to`; JSON/non-browser traffic remains 401. +- Live deployed Worker version `2d69b4b3-7170-46d3-a3d3-7cbd6d14dccf` has no `OS_INTERNAL_DASHBOARD_ACCESS_TEAM_DOMAIN`, `OS_INTERNAL_DASHBOARD_ACCESS_AUD`, or `OS_INTERNAL_DASHBOARD_ALLOWED_EMAILS` bindings. The Cloudflare account currently reports zero Access applications, so authenticated dashboard traffic necessarily fails closed as 403. +- Added all three operator authorization bindings to workspace-edge release readiness so future deploys cannot silently omit them. + +## Verification + +- Red: `cd packages/os && bun vitest run tests/internal-dashboard-integration.test.ts` failed `expected 401 to be 302` at the new browser-navigation assertion. +- Green: the same focused suite passed 6/6. +- Red: `cd packages/os && bun vitest run tests/cloudflare-worker-release-readiness.test.ts` failed because deployment incorrectly proceeded without dashboard auth bindings. +- Green: `internal-dashboard-integration`, `install-control-plane-cloudflare`, and `cloudflare-worker-release-readiness` passed 12/12. +- Green: `yarn nx run consuelo-os:typecheck` passed workspace script syntax checks. +- Note: Nx target discovery identified the project as `consuelo-os`; the inferred `consuelo-os:test` target is currently unusable because `packages/os` is not declared in the root Yarn workspace, so focused Vitest ran with Bun from `packages/os`. +- Cloudflare Access provisioning remains an external deployment gate: the cached Wrangler OAuth token can list Access apps but lacks `Access: Apps and Policies Write`; no Access application or audience currently exists to bind. + +- 2026-08-16 22:26:36 append: `.task/os/restore-internal-dashboard-browser-auth-handoff/workpad.md` diff --git a/.task/tasks/os/gate-internal-dashboard-routing-on-complete-access-config.json b/.task/tasks/os/gate-internal-dashboard-routing-on-complete-access-config.json new file mode 100644 index 0000000000..f7531be59a --- /dev/null +++ b/.task/tasks/os/gate-internal-dashboard-routing-on-complete-access-config.json @@ -0,0 +1,19 @@ +{ + "area": "os", + "stream": "stream/os", + "taskBranch": "task/os/gate-internal-dashboard-routing-on-complete-access-config", + "baseBranch": "stream/os", + "sourceBranch": "stream/os", + "startFrom": "stream", + "prNumber": 2149, + "prUrl": "https://github.com/consuelohq/opensaas/pull/2149", + "githubPrUrl": "https://github.com/consuelohq/opensaas/pull/2149", + "graphitePrUrl": "https://app.graphite.com/github/pr/consuelohq/opensaas/2149/gate-internal-dashboard-routing-on-complete-access-config", + "taskPrUrl": "https://github.com/consuelohq/opensaas/pull/2149", + "taskGraphitePrUrl": "https://app.graphite.com/github/pr/consuelohq/opensaas/2149/gate-internal-dashboard-routing-on-complete-access-config", + "worktreePath": "/private/var/folders/vl/1zvhm0bj28d1dbvbcb12b39r0000gn/T/opensaas-worktrees/task-os-gate-internal-dashboard-routing-on-complete-access-config", + "taskSession": "tsk_be4cc7cc41f5", + "tmuxSession": "opensaas-os-gate-internal-dashboard-routing-on-complete-acce-be4cc7cc", + "sessionPath": "/private/var/folders/vl/1zvhm0bj28d1dbvbcb12b39r0000gn/T/opensaas-worktrees/task-os-gate-internal-dashboard-routing-on-complete-access-config/.task/os/gate-internal-dashboard-routing-on-complete-access-config/session.json", + "createdAt": "2026-08-16T22:29:43.195Z" +} diff --git a/.task/tasks/os/repair-runtime-retention-and-watchdog-recovery.json b/.task/tasks/os/repair-runtime-retention-and-watchdog-recovery.json new file mode 100644 index 0000000000..06452f8252 --- /dev/null +++ b/.task/tasks/os/repair-runtime-retention-and-watchdog-recovery.json @@ -0,0 +1,19 @@ +{ + "area": "os", + "stream": "stream/os", + "taskBranch": "task/os/repair-runtime-retention-and-watchdog-recovery", + "baseBranch": "stream/os", + "sourceBranch": "stream/os", + "startFrom": "stream", + "prNumber": 2150, + "prUrl": "https://github.com/consuelohq/opensaas/pull/2150", + "githubPrUrl": "https://github.com/consuelohq/opensaas/pull/2150", + "graphitePrUrl": "https://app.graphite.com/github/pr/consuelohq/opensaas/2150/repair-runtime-retention-and-watchdog-recovery", + "taskPrUrl": "https://github.com/consuelohq/opensaas/pull/2150", + "taskGraphitePrUrl": "https://app.graphite.com/github/pr/consuelohq/opensaas/2150/repair-runtime-retention-and-watchdog-recovery", + "worktreePath": "/private/var/folders/vl/1zvhm0bj28d1dbvbcb12b39r0000gn/T/opensaas-worktrees/task-os-repair-runtime-retention-and-watchdog-recovery", + "taskSession": "tsk_af28ff74214b", + "tmuxSession": "opensaas-os-repair-runtime-retention-and-watchdog-recovery-af28ff74", + "sessionPath": "/private/var/folders/vl/1zvhm0bj28d1dbvbcb12b39r0000gn/T/opensaas-worktrees/task-os-repair-runtime-retention-and-watchdog-recovery/.task/os/repair-runtime-retention-and-watchdog-recovery/session.json", + "createdAt": "2026-08-16T22:46:56.311Z" +} diff --git a/.task/tasks/os/restore-internal-dashboard-browser-auth-handoff.json b/.task/tasks/os/restore-internal-dashboard-browser-auth-handoff.json new file mode 100644 index 0000000000..7d0c0dcaee --- /dev/null +++ b/.task/tasks/os/restore-internal-dashboard-browser-auth-handoff.json @@ -0,0 +1,19 @@ +{ + "area": "os", + "stream": "stream/os", + "taskBranch": "task/os/restore-internal-dashboard-browser-auth-handoff", + "baseBranch": "stream/os", + "sourceBranch": "stream/os", + "startFrom": "stream", + "prNumber": 2147, + "prUrl": "https://github.com/consuelohq/opensaas/pull/2147", + "githubPrUrl": "https://github.com/consuelohq/opensaas/pull/2147", + "graphitePrUrl": "https://app.graphite.com/github/pr/consuelohq/opensaas/2147/restore-internal-dashboard-browser-auth-handoff", + "taskPrUrl": "https://github.com/consuelohq/opensaas/pull/2147", + "taskGraphitePrUrl": "https://app.graphite.com/github/pr/consuelohq/opensaas/2147/restore-internal-dashboard-browser-auth-handoff", + "worktreePath": "/private/var/folders/vl/1zvhm0bj28d1dbvbcb12b39r0000gn/T/opensaas-worktrees/task-os-restore-internal-dashboard-browser-auth-handoff", + "taskSession": "tsk_fdaa1f3e0fdd", + "tmuxSession": "opensaas-os-restore-internal-dashboard-browser-auth-handoff-fdaa1f3e", + "sessionPath": "/private/var/folders/vl/1zvhm0bj28d1dbvbcb12b39r0000gn/T/opensaas-worktrees/task-os-restore-internal-dashboard-browser-auth-handoff/.task/os/restore-internal-dashboard-browser-auth-handoff/session.json", + "createdAt": "2026-08-16T22:16:27.043Z" +} diff --git a/packages/os/cloudflare/workspace-edge/src/index.ts b/packages/os/cloudflare/workspace-edge/src/index.ts index e3b3182c89..b4103f2b7e 100644 --- a/packages/os/cloudflare/workspace-edge/src/index.ts +++ b/packages/os/cloudflare/workspace-edge/src/index.ts @@ -161,7 +161,27 @@ function forbiddenInternalDashboardResponse(): Response { }); } -function workspaceSessionRequiredResponse(): Response { +function workspaceSessionRequiredResponse(request: Request): Response { + const url = new URL(request.url); + const acceptsHtml = + request.method === 'GET' && + (request.headers.get('accept') ?? '').includes('text/html'); + if (acceptsHtml) { + const login = new URL( + '/login/google/start', + 'https://os.consuelohq.com', + ); + login.searchParams.set('purpose', 'web'); + login.searchParams.set('return_to', `${url.pathname}${url.search}`); + return new Response(null, { + status: 302, + headers: { + location: login.toString(), + 'cache-control': 'no-store', + 'x-content-type-options': 'nosniff', + }, + }); + } return new Response(JSON.stringify({ error: 'workspace_session_required' }), { status: 401, headers: { @@ -419,6 +439,23 @@ export function createWorkspaceEdgeHandler( }), }) : undefined); + const internalDashboardAccessValues = [ + env.OS_INTERNAL_DASHBOARD_ACCESS_TEAM_DOMAIN?.trim() ?? '', + env.OS_INTERNAL_DASHBOARD_ACCESS_AUD?.trim() ?? '', + internalDashboardAllowedEmails( + env.OS_INTERNAL_DASHBOARD_ALLOWED_EMAILS, + ).join(','), + ]; + const configuredInternalDashboardAccessValues = + internalDashboardAccessValues.filter(Boolean).length; + const internalDashboardAccessState = options.authorizeInternalDashboard + ? 'configured' + : configuredInternalDashboardAccessValues === 0 + ? 'disabled' + : configuredInternalDashboardAccessValues === + internalDashboardAccessValues.length + ? 'configured' + : 'partial'; const authorizeInternalDashboard = options.authorizeInternalDashboard ?? createCloudflareAccessDashboardAuthorizer({ @@ -496,7 +533,16 @@ export function createWorkspaceEdgeHandler( url.pathname === INSTALL_DASHBOARD_API_PREFIX || url.pathname.startsWith(`${INSTALL_DASHBOARD_API_PREFIX}/`) || isInternalDashboardPagePath(url.pathname); - if (internalDashboardRequest) { + if ( + internalDashboardRequest && + internalDashboardAccessState === 'partial' + ) { + return closedAuthResponse(); + } + if ( + internalDashboardRequest && + internalDashboardAccessState === 'configured' + ) { const sessionValidation = await validateWorkspaceBrowserSession({ request, stub, @@ -506,27 +552,27 @@ export function createWorkspaceEdgeHandler( if (!sessionValidation) return closedAuthResponse(); if (sessionValidation.status !== 204) { return sessionValidation.status === 401 - ? workspaceSessionRequiredResponse() + ? workspaceSessionRequiredResponse(request) : closedAuthResponse(); } - } - if ( - url.pathname === INSTALL_DASHBOARD_API_PREFIX || - url.pathname.startsWith(`${INSTALL_DASHBOARD_API_PREFIX}/`) - ) { - const diagnostic = await proxyInstallDiagnosticRequest({ - request, - stub, - internalAuthSecret: env.WORKSPACE_EDGE_INTERNAL_SIGNING_SECRET, - authorize: authorizeInternalDashboard, - }); - if (diagnostic) return diagnostic; - if (!internalDashboardHandler) return closedAuthResponse(); - return await internalDashboardHandler(request); - } - if (isInternalDashboardPagePath(url.pathname)) { - if (!internalDashboardPageHandler) return closedAuthResponse(); - return await internalDashboardPageHandler(request); + if ( + url.pathname === INSTALL_DASHBOARD_API_PREFIX || + url.pathname.startsWith(`${INSTALL_DASHBOARD_API_PREFIX}/`) + ) { + const diagnostic = await proxyInstallDiagnosticRequest({ + request, + stub, + internalAuthSecret: env.WORKSPACE_EDGE_INTERNAL_SIGNING_SECRET, + authorize: authorizeInternalDashboard, + }); + if (diagnostic) return diagnostic; + if (!internalDashboardHandler) return closedAuthResponse(); + return await internalDashboardHandler(request); + } + if (isInternalDashboardPagePath(url.pathname)) { + if (!internalDashboardPageHandler) return closedAuthResponse(); + return await internalDashboardPageHandler(request); + } } } if (url.pathname.startsWith('/gateway/nodes/')) { diff --git a/packages/os/scripts/bootstrap.sh b/packages/os/scripts/bootstrap.sh index 99355292de..8e16130eb6 100644 --- a/packages/os/scripts/bootstrap.sh +++ b/packages/os/scripts/bootstrap.sh @@ -684,6 +684,39 @@ ensure_bun() { log "Bun installed: $BUN_BIN" } +ensure_named_bun_runtime() { + local source="$BUN_BIN" + local target="$RUNTIME_BIN_DIR/consuelo-os" + local temporary="$target.$$.tmp" + + if [ "$DRY_RUN" -eq 1 ]; then + log "dry-run: would install the named Consuelo service executable at $target" + return 0 + fi + [ -x "$source" ] || fail "Consuelo OS cannot create its named service executable because Bun is unavailable: ${source:-unset}" + if [ "$source" = "$target" ]; then + return 0 + fi + + mkdir -p "$RUNTIME_BIN_DIR" + if [ -x "$target" ] && /usr/bin/cmp -s "$source" "$target"; then + BUN_BIN="$target" + return 0 + fi + + /bin/rm -f -- "$temporary" + if ! /bin/cp -c "$source" "$temporary" 2>/dev/null; then + /bin/cp -p "$source" "$temporary" || fail "Consuelo OS could not copy Bun to its named service executable." + fi + /bin/chmod 0755 "$temporary" + if ! /usr/bin/cmp -s "$source" "$temporary"; then + /bin/rm -f -- "$temporary" + fail "The Consuelo Bun service clone failed integrity verification." + fi + /bin/mv -f "$temporary" "$target" + BUN_BIN="$target" +} + runtime_arch() { local machine machine="$(uname -m 2>/dev/null || true)" @@ -1891,6 +1924,7 @@ main() { render_dependency_progress prompt_dependency_setup ensure_bun + ensure_named_bun_runtime ensure_install_id ensure_portless ensure_caddy diff --git a/packages/os/scripts/install-system-daemons.sh b/packages/os/scripts/install-system-daemons.sh index ccc499488a..0e05757193 100644 --- a/packages/os/scripts/install-system-daemons.sh +++ b/packages/os/scripts/install-system-daemons.sh @@ -683,7 +683,9 @@ fi [ "$quiet" = "1" ] || log "running Consuelo OS smoke test on port $stage_port" background_service_failure_code="BACKGROUND_SERVICE_START_FAILED" -WORKSPACE_DAEMON_PORT="$stage_port" bash "$script_dir/start-consuelo-daemon.sh" > /tmp/consuelo-os-stage.log 2>&1 & +CONSUELO_OS_SINGLE_WORKER_SMOKE_TEST=1 \ + WORKSPACE_DAEMON_PORT="$stage_port" \ + bash "$script_dir/start-consuelo-daemon.sh" > /tmp/consuelo-os-stage.log 2>&1 & stage_pid=$! background_service_failure_code="BACKGROUND_SERVICE_HEALTHCHECK_FAILED" if ! wait_for_health "http://127.0.0.1:${stage_port}/health" 20 1; then diff --git a/packages/os/scripts/lib/index/indexer.js b/packages/os/scripts/lib/index/indexer.js index 508b7b64cd..7322abea15 100644 --- a/packages/os/scripts/lib/index/indexer.js +++ b/packages/os/scripts/lib/index/indexer.js @@ -25,6 +25,9 @@ const EXCLUDE_DIRS = new Set([ 'generated', 'generated-metadata', 'coverage', + 'vendor', + 'worktrees', + '.worktrees', '__pycache__', '.task', ]); diff --git a/packages/os/scripts/lib/lifecycle/retention.ts b/packages/os/scripts/lib/lifecycle/retention.ts index cc163fb608..c6faa4c02c 100644 --- a/packages/os/scripts/lib/lifecycle/retention.ts +++ b/packages/os/scripts/lib/lifecycle/retention.ts @@ -356,12 +356,18 @@ function readRetentionState(home?: string): RetentionState { }; } -function listStrictVerifiedReleases( - home?: string, -): LifecycleReleaseReference[] { +type RetentionReleaseCandidate = { + path: string; + bundleId: string; +}; + +function listRetentionReleaseCandidates( + home: string | undefined, + protectedBundleIds: ReadonlySet, +): RetentionReleaseCandidate[] { const paths = resolveLifecyclePaths(home); if (!existsSync(paths.releasesDir)) return []; - const releases: LifecycleReleaseReference[] = []; + const releases: RetentionReleaseCandidate[] = []; for (const entry of readdirSync(paths.releasesDir, { withFileTypes: true })) { const releasePath = join(paths.releasesDir, entry.name); const stat = lstatSync(releasePath); @@ -377,14 +383,31 @@ function listStrictVerifiedReleases( `runtime release entry is not a directory: ${entry.name}`, ); } - const manifest = verifyInstalledRuntimeRelease(releasePath); - if (!releaseDirectoryMatchesBundleId(entry.name, manifest.bundleId)) { + let bundleId: string; + try { + bundleId = runtimeBundleIdFromDirectoryName(entry.name); + } catch (error: unknown) { throw lifecycleError( 'RETENTION_FAILED', - `runtime release identity mismatch: ${entry.name}`, + `runtime release entry has an invalid identity: ${entry.name}`, + { cause: error }, ); } - releases.push({ path: releasePath, manifest }); + try { + const manifest = verifyInstalledRuntimeRelease(releasePath); + if (manifest.bundleId !== bundleId) { + throw new Error(`verified bundle identity does not match ${entry.name}`); + } + } catch (error: unknown) { + if (protectedBundleIds.has(bundleId)) { + throw lifecycleError( + 'RETENTION_FAILED', + `protected runtime release failed verification: ${entry.name}`, + { cause: error }, + ); + } + } + releases.push({ path: releasePath, bundleId }); } return releases; } @@ -476,16 +499,19 @@ export function pruneLifecycleReleases(input: { const current = readLifecycleReleaseReference(paths.home, 'current'); const previous = readLifecycleReleaseReference(paths.home, 'previous'); const state = readRetentionState(paths.home); - const releases = listStrictVerifiedReleases(paths.home); - const byId = new Map( - releases.map((release) => [release.manifest.bundleId, release]), - ); const protectedBundleIds = new Set([ ...(current ? [current.manifest.bundleId] : []), ...(previous ? [previous.manifest.bundleId] : []), ...state.pinnedBundleIds, ...state.unresolvedContentBaseBundleIds, ]); + const releases = listRetentionReleaseCandidates( + paths.home, + protectedBundleIds, + ); + const byId = new Map( + releases.map((release) => [release.bundleId, release]), + ); for (const bundleId of protectedBundleIds) { if (!byId.has(bundleId)) { throw lifecycleError( @@ -496,7 +522,7 @@ export function pruneLifecycleReleases(input: { } const removedBundleIds: string[] = []; for (const release of releases) { - if (protectedBundleIds.has(release.manifest.bundleId)) continue; + if (protectedBundleIds.has(release.bundleId)) continue; if ( !isPathWithin(paths.releasesDir, release.path) || release.path === resolve(paths.releasesDir) @@ -508,7 +534,7 @@ export function pruneLifecycleReleases(input: { } if (!(input.dryRun ?? false)) rmSync(release.path, { recursive: true, force: true }); - removedBundleIds.push(release.manifest.bundleId); + removedBundleIds.push(release.bundleId); } const removedEphemeralPaths = pruneLifecycleEphemeralDirectories({ home: paths.home, diff --git a/packages/os/scripts/start-consuelo-daemon.sh b/packages/os/scripts/start-consuelo-daemon.sh index 566935ac72..c484080a77 100644 --- a/packages/os/scripts/start-consuelo-daemon.sh +++ b/packages/os/scripts/start-consuelo-daemon.sh @@ -63,6 +63,15 @@ case ":$PATH:" in *) export PATH="$bun_dir:$PATH" ;; esac +if [ "${CONSUELO_OS_SINGLE_WORKER_SMOKE_TEST:-0}" = "1" ]; then + export CONSUELO_OS_WORKER_PROCESS="1" + export CONSUELO_OS_WORKER_ID="smoke-worker" + export CONSUELO_OS_WORKER_INSTANCE_ID="smoke-$$" + export CONSUELO_OS_WORKER_RELEASE_PATH="$root_dir" + unset CONSUELO_OS_SUPERVISOR_PID + exec "$bun_bin" "$root_dir/scripts/server/main.ts" +fi + run_with_timeout() { local timeout_seconds="$1" shift diff --git a/packages/os/scripts/workspace-watchdog.sh b/packages/os/scripts/workspace-watchdog.sh index 377413245c..a93c3bb227 100644 --- a/packages/os/scripts/workspace-watchdog.sh +++ b/packages/os/scripts/workspace-watchdog.sh @@ -228,10 +228,12 @@ restart_workspace() { log "restart command failed for $workspace_label; canonical Consuelo CLI is missing or not executable at $consuelo_cli" return 1 fi - if ! CONSUELO_HOME="$consuelo_home" "$consuelo_cli" restart --quiet; then - log "restart command failed for $workspace_label; canonical Consuelo restart returned non-zero" - return 1 + if CONSUELO_HOME="$consuelo_home" "$consuelo_cli" restart --quiet; then + return 0 fi + log "restart command failed for $workspace_label; canonical Consuelo restart returned non-zero" + log "falling back to launchd recovery for $workspace_label" + restart_launchd_label "$workspace_label" } reconcile_public_route() { diff --git a/packages/os/tests/cloudflare-worker-release-readiness.test.ts b/packages/os/tests/cloudflare-worker-release-readiness.test.ts index 5b97fe43b0..5a80f9a259 100644 --- a/packages/os/tests/cloudflare-worker-release-readiness.test.ts +++ b/packages/os/tests/cloudflare-worker-release-readiness.test.ts @@ -6,28 +6,30 @@ import { } from '../scripts/lib/cloudflare-worker-release-readiness'; describe('Cloudflare Worker release readiness', () => { - it('rejects incomplete secret metadata before deployment', async () => { + it('allows deployment with the optional internal dashboard disabled', async () => { const commands: string[][] = []; - await expect(deployCloudflareWorker({ + await deployCloudflareWorker({ target: 'workspace-edge', runner: async ({ argv }) => { commands.push(argv); - return { - exitCode: 0, - stdout: JSON.stringify([{ name: 'CONSUELO_EDGE_SIGNING_SECRET' }]), - stderr: '', - }; + return argv[1] === 'secret' + ? { + exitCode: 0, + stdout: JSON.stringify([ + { name: 'CONSUELO_EDGE_SIGNING_SECRET' }, + { name: 'WORKSPACE_EDGE_INTERNAL_SIGNING_SECRET' }, + ]), + stderr: '', + } + : { exitCode: 0, stdout: '', stderr: '' }; }, - })).rejects.toThrow( - 'Workspace edge secret WORKSPACE_EDGE_INTERNAL_SIGNING_SECRET is not configured', - ); - expect(commands).toEqual([[ + }); + expect(commands[1]).toEqual([ 'wrangler', - 'secret', - 'list', + 'deploy', '--config', 'cloudflare/workspace-edge/wrangler.toml', - ]]); + ]); }); it('validates required secrets before issuing deployment', async () => { diff --git a/packages/os/tests/index-path-exclusions.test.ts b/packages/os/tests/index-path-exclusions.test.ts new file mode 100644 index 0000000000..a1dd0c0efe --- /dev/null +++ b/packages/os/tests/index-path-exclusions.test.ts @@ -0,0 +1,45 @@ +import { readFileSync } from 'node:fs'; +import { createRequire } from 'node:module'; +import { join } from 'node:path'; +import vm from 'node:vm'; + +import { describe, expect, it } from 'vitest'; + +const require = createRequire(import.meta.url); + +type IndexPathPolicy = { + isIndexablePath: (filePath: string) => boolean; +}; + +function loadIndexPathPolicy(): IndexPathPolicy { + const source = readFileSync( + join(import.meta.dirname, '../scripts/lib/index/indexer.js'), + 'utf8', + ); + const module = { exports: {} as IndexPathPolicy }; + vm.runInNewContext(source, { + module, + process, + require: (specifier: string) => + specifier.startsWith('.') ? {} : require(specifier), + }); + return module.exports; +} + +describe('OS semantic index path exclusions', () => { + it('excludes dependency, generated, vendor, and nested worktree trees', () => { + const { isIndexablePath } = loadIndexPathPolicy(); + + for (const filePath of [ + 'node_modules/package/index.js', + 'packages/app/dist/index.js', + 'packages/app/generated/schema.ts', + 'packages/app/vendor/library/index.js', + 'worktrees/task/packages/app/src/index.ts', + '.worktrees/task/packages/app/src/index.ts', + ]) { + expect(isIndexablePath(filePath), filePath).toBe(false); + } + expect(isIndexablePath('packages/app/src/index.ts')).toBe(true); + }); +}); diff --git a/packages/os/tests/installer-runtime-dependencies.test.ts b/packages/os/tests/installer-runtime-dependencies.test.ts index 14142fdaf5..21a88ce9a6 100644 --- a/packages/os/tests/installer-runtime-dependencies.test.ts +++ b/packages/os/tests/installer-runtime-dependencies.test.ts @@ -333,6 +333,47 @@ describe('public installer runtime dependencies', () => { ); }); + it('should install a named Consuelo service executable before daemon generation', () => { + const bootstrap = readBootstrap(); + const namedRuntime = extractShellFunction( + bootstrap, + 'ensure_named_bun_runtime', + ); + + expect(namedRuntime).toContain('consuelo-os'); + expect(namedRuntime).toContain('/bin/cp -c'); + expect(namedRuntime).toContain('/bin/cp -p'); + expect(namedRuntime).toContain('/usr/bin/cmp -s'); + expect(namedRuntime).toContain('/bin/mv -f'); + expect(namedRuntime).toContain('BUN_BIN="$target"'); + + const main = extractShellFunction(bootstrap, 'main'); + expect(main.indexOf('ensure_bun')).toBeLessThan( + main.indexOf('ensure_named_bun_runtime'), + ); + expect(main.indexOf('ensure_named_bun_runtime')).toBeLessThan( + main.indexOf('persist_runtime_paths'), + ); + }); + + it('should stage one smoke-test worker without contending with the live supervisor', () => { + const installer = readDaemonInstaller(); + const daemon = readFileSync( + join(PACKAGE_ROOT, 'scripts', 'start-consuelo-daemon.sh'), + 'utf8', + ); + + expect(installer).toContain('CONSUELO_OS_SINGLE_WORKER_SMOKE_TEST=1'); + expect(daemon).toContain( + 'if [ "${CONSUELO_OS_SINGLE_WORKER_SMOKE_TEST:-0}" = "1" ]; then', + ); + expect(daemon).toContain('CONSUELO_OS_WORKER_ID="smoke-worker"'); + expect(daemon).toContain('scripts/server/main.ts'); + expect(daemon.indexOf('scripts/server/main.ts')).toBeLessThan( + daemon.lastIndexOf('scripts/server/supervisor.ts'), + ); + }); + it('should use the regular local port when Portless is disabled by default', () => { const home = createTempHome('consuelo-os-installer-runtime-bootstrap-'); const result = runBootstrapDryRun(home); diff --git a/packages/os/tests/internal-dashboard-integration.test.ts b/packages/os/tests/internal-dashboard-integration.test.ts index 17b7e93330..bee5555ce4 100644 --- a/packages/os/tests/internal-dashboard-integration.test.ts +++ b/packages/os/tests/internal-dashboard-integration.test.ts @@ -206,6 +206,75 @@ describe('Branch 6 internal dashboard integration', () => { expect(users.status).toBe(403); }); + it('leaves shared-host paths on normal workspace routing when dashboard Access is disabled', async () => { + const routeRegistry = createInMemoryWorkspaceRouteD1(); + await migrateWorkspaceRouteD1(routeRegistry); + let sessionValidationCalls = 0; + const edge = createWorkspaceEdgeHandler( + { + WORKSPACE_ROUTE_REGISTRY: routeRegistry, + CONSUELO_EDGE_SIGNING_SECRET: 'edge-secret', + WORKSPACE_EDGE_INTERNAL_SIGNING_SECRET: 'internal-secret', + OS_DEVICE_AUTHORITY: { + idFromName: (name: string) => name, + get: () => ({ + fetch: async () => { + sessionValidationCalls += 1; + return new Response(null, { status: 204 }); + }, + }), + }, + }, + { + internalDashboardService: createInstallControlPlaneService({ + repository: createMemoryInstallControlPlaneRepository(), + }), + now: () => NOW, + }, + ); + + const response = await edge( + new Request('https://internal.consuelohq.com/', { + headers: { accept: 'text/html' }, + }), + ); + expect(response.status).toBe(404); + expect(sessionValidationCalls).toBe(0); + }); + + it('fails closed instead of intercepting with a partially configured dashboard', async () => { + const routeRegistry = createInMemoryWorkspaceRouteD1(); + await migrateWorkspaceRouteD1(routeRegistry); + const edge = createWorkspaceEdgeHandler( + { + WORKSPACE_ROUTE_REGISTRY: routeRegistry, + CONSUELO_EDGE_SIGNING_SECRET: 'edge-secret', + WORKSPACE_EDGE_INTERNAL_SIGNING_SECRET: 'internal-secret', + OS_INTERNAL_DASHBOARD_ACCESS_TEAM_DOMAIN: 'consuelo.cloudflareaccess.com', + OS_DEVICE_AUTHORITY: { + idFromName: (name: string) => name, + get: () => ({ + fetch: async () => new Response(null, { status: 204 }), + }), + }, + }, + { + internalDashboardService: createInstallControlPlaneService({ + repository: createMemoryInstallControlPlaneRepository(), + }), + now: () => NOW, + }, + ); + + const response = await edge( + new Request('https://internal.consuelohq.com/users'), + ); + expect(response.status).toBe(503); + await expect(response.json()).resolves.toEqual({ + error: 'workspace_auth_unavailable', + }); + }); + it('requires a valid internal-host workspace session before applying the operator dashboard gate', async () => { const routeRegistry = createInMemoryWorkspaceRouteD1(); await migrateWorkspaceRouteD1(routeRegistry); @@ -240,14 +309,28 @@ describe('Branch 6 internal dashboard integration', () => { authorizeInternalDashboard: async () => true, now: () => NOW, }); - const anonymous = await allowedOperator(new Request('https://internal.consuelohq.com/users', { - headers: { accept: 'text/html' }, - })); - expect(anonymous.status).toBe(401); + const anonymous = await allowedOperator(new Request( + 'https://internal.consuelohq.com/users?state=active', + { headers: { accept: 'text/html' } }, + )); + expect(anonymous.status).toBe(302); + expect(anonymous.headers.get('location')).toBe( + 'https://os.consuelohq.com/login/google/start?purpose=web&return_to=%2Fusers%3Fstate%3Dactive', + ); const anonymousRoot = await allowedOperator(new Request('https://internal.consuelohq.com/', { headers: { accept: 'text/html' }, })); - expect(anonymousRoot.status).toBe(401); + expect(anonymousRoot.status).toBe(302); + expect(anonymousRoot.headers.get('location')).toBe( + 'https://os.consuelohq.com/login/google/start?purpose=web&return_to=%2F', + ); + const anonymousJson = await allowedOperator(new Request('https://internal.consuelohq.com/users', { + headers: { accept: 'application/json' }, + })); + expect(anonymousJson.status).toBe(401); + await expect(anonymousJson.json()).resolves.toEqual({ + error: 'workspace_session_required', + }); const authenticated = await allowedOperator(new Request('https://internal.consuelohq.com/users', { headers: { cookie: '__Host-consuelo_os_session=target-session' }, diff --git a/packages/os/tests/lifecycle-retention-uninstall.test.ts b/packages/os/tests/lifecycle-retention-uninstall.test.ts index 307d8dcd5c..00e260a192 100644 --- a/packages/os/tests/lifecycle-retention-uninstall.test.ts +++ b/packages/os/tests/lifecycle-retention-uninstall.test.ts @@ -51,6 +51,17 @@ const requiredRuntimePaths = [ 'scripts/server/main.ts', 'scripts/server/supervisor.ts', 'scripts/lib/install-state.ts', + 'scripts/lib/mcp-protocol.ts', + 'scripts/lib/mcp-gateway.ts', + 'scripts/server/routes/mcp.ts', + 'scripts/lib/worker-pool.ts', + 'scripts/lib/security-gateway.ts', + 'scripts/consuelo-reload.js', + 'scripts/workspace-watchdog.sh', + 'scripts/lib/lifecycle/connector-readiness.ts', + 'scripts/workspace-node-heartbeat.ts', + 'scripts/lib/workspace-node-heartbeat-client.ts', + 'scripts/lib/subagent/runner.ts', 'scripts/managed-components.ts', 'scripts/lib/managed-components.ts', 'scripts/lib/managed-component-install.ts', @@ -135,6 +146,7 @@ function signedManifest(bundle: BuiltBundle): SignedReleaseManifest { architecture: bundle.manifest.architecture, archiveDigest: resolved.bundleDigest, bundleId: resolved.bundleId, + capabilities: bundle.manifest.capabilities, cloudflareObjectKey: resolved.bundleUrl, githubAssetName: `consuelo-os-runtime-${resolved.version}.tar.gz`, platform: bundle.manifest.platform, @@ -451,6 +463,36 @@ describe('lifecycle rollback and retention', () => { ].sort()); }); + it('removes a corrupt obsolete release without weakening protected release verification', () => { + writeInstalledIdentity(); + stageBundle(bundle110, 'stage-obsolete'); + const previousPath = stageBundle(bundle120, 'stage-previous'); + stageBundle(bundle130, 'stage-current'); + mkdirSync(join(tempHome, 'runtime'), { recursive: true }); + symlinkSync(runtimeReleaseTargetFor(bundle130), join(tempHome, 'runtime', 'current')); + symlinkSync(runtimeReleaseTargetFor(bundle120), join(tempHome, 'runtime', 'previous')); + const obsoletePath = join( + tempHome, + 'runtime', + runtimeReleaseTargetFor(bundle110), + ); + writeFileSync(join(obsoletePath, 'package.json'), '{"corrupt":true}\n'); + + expect(pruneLifecycleReleases({ home: tempHome })).toMatchObject({ + removedBundleIds: [bundle110.manifest.bundleId], + retainedBundleIds: expect.arrayContaining([ + bundle120.manifest.bundleId, + bundle130.manifest.bundleId, + ]), + }); + expect(existsSync(obsoletePath)).toBe(false); + + writeFileSync(join(previousPath, 'package.json'), '{"corrupt":true}\n'); + expect(() => pruneLifecycleReleases({ home: tempHome })).toThrow( + /previous.*digest mismatch|protected runtime release failed verification/i, + ); + }); + it('refuses pruning when a runtime reference is inconsistent', () => { writeInstalledIdentity(); stageBundle(bundle100, 'stage-current'); diff --git a/packages/os/tests/system-daemon-reliability.test.ts b/packages/os/tests/system-daemon-reliability.test.ts index acefc10269..a9cf0afe03 100644 --- a/packages/os/tests/system-daemon-reliability.test.ts +++ b/packages/os/tests/system-daemon-reliability.test.ts @@ -316,6 +316,49 @@ describe('macOS runtime service reliability', () => { ); }); + it('should kickstart launchd when canonical rolling recovery rejects an unhealthy pool', () => { + const fixtureRoot = temporaryDirectory('consuelo-watchdog-recovery-fallback-'); + const fakeBin = join(fixtureRoot, 'bin'); + const home = join(fixtureRoot, 'home'); + const consueloHome = join(home, '.consuelo'); + const launchLog = join(fixtureRoot, 'launchctl.log'); + const consueloLog = join(fixtureRoot, 'consuelo.log'); + mkdirSync(fakeBin, { recursive: true }); + mkdirSync(home, { recursive: true }); + installFakeConsuelo(consueloHome); + writeExecutable( + join(consueloHome, 'bin', 'consuelo'), + '#!/bin/bash\nprintf "%s\\n" "$*" >> "$WATCHDOG_CONSUELO_LOG"\nexit 1\n', + ); + writeExecutable(join(fakeBin, 'lsof'), '#!/bin/bash\nexit 0\n'); + writeExecutable(join(fakeBin, 'curl'), '#!/bin/bash\nexit 1\n'); + writeExecutable( + join(fakeBin, 'launchctl'), + '#!/bin/bash\nprintf "%s\\n" "$*" >> "$WATCHDOG_LAUNCH_LOG"\nexit 0\n', + ); + + const result = run('bash', [resolve(osRoot, 'scripts/workspace-watchdog.sh')], { + ...process.env, + HOME: home, + CONSUELO_HOME: consueloHome, + WORKSPACE_WATCHDOG_PATH: `${fakeBin}:/usr/bin:/bin:/usr/sbin:/sbin`, + WORKSPACE_WATCHDOG_DISABLE_EXTERNAL: '1', + WORKSPACE_WATCHDOG_LOCAL_HTTP_FAILURE_THRESHOLD: '1', + WORKSPACE_WATCHDOG_MIN_RESTART_GAP_SECONDS: '0', + WATCHDOG_LAUNCH_LOG: launchLog, + WATCHDOG_CONSUELO_LOG: consueloLog, + }); + + expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0); + expect(readFileSync(consueloLog, 'utf8')).toContain('restart --quiet'); + expect(readFileSync(launchLog, 'utf8')).toContain( + 'kickstart -k gui/' + String(process.getuid?.()) + '/com.consuelo.system', + ); + expect(result.stdout).toContain( + 'falling back to launchd recovery for com.consuelo.system', + ); + }); + it('should bootstrap a missing Caddy label when the HA ingress is unavailable', () => { const fixtureRoot = temporaryDirectory('consuelo-watchdog-bootstrap-'); const fakeBin = join(fixtureRoot, 'bin'); diff --git a/packages/workspace/scripts/lib/index/indexer.js b/packages/workspace/scripts/lib/index/indexer.js index 78be1023fc..93710131a7 100644 --- a/packages/workspace/scripts/lib/index/indexer.js +++ b/packages/workspace/scripts/lib/index/indexer.js @@ -25,6 +25,9 @@ const EXCLUDE_DIRS = new Set([ 'generated', 'generated-metadata', 'coverage', + 'vendor', + 'worktrees', + '.worktrees', '__pycache__', '.task', ]); diff --git a/packages/workspace/tests/index-path-exclusions.test.js b/packages/workspace/tests/index-path-exclusions.test.js new file mode 100644 index 0000000000..285ffb9f72 --- /dev/null +++ b/packages/workspace/tests/index-path-exclusions.test.js @@ -0,0 +1,41 @@ +import { readFileSync } from 'node:fs'; +import { createRequire } from 'node:module'; +import { join } from 'node:path'; +import vm from 'node:vm'; + +import { describe, expect, it } from 'vitest'; + +const require = createRequire(import.meta.url); + +function loadIndexPathPolicy() { + const source = readFileSync( + join(import.meta.dirname, '../scripts/lib/index/indexer.js'), + 'utf8', + ); + const module = { exports: {} }; + vm.runInNewContext(source, { + module, + process, + require: (specifier) => + specifier.startsWith('.') ? {} : require(specifier), + }); + return module.exports; +} + +describe('semantic index path exclusions', () => { + it('excludes dependency, generated, vendor, and nested worktree trees', () => { + const { isIndexablePath } = loadIndexPathPolicy(); + + for (const filePath of [ + 'node_modules/package/index.js', + 'packages/app/dist/index.js', + 'packages/app/generated/schema.ts', + 'packages/app/vendor/library/index.js', + 'worktrees/task/packages/app/src/index.ts', + '.worktrees/task/packages/app/src/index.ts', + ]) { + expect(isIndexablePath(filePath), filePath).toBe(false); + } + expect(isIndexablePath('packages/app/src/index.ts')).toBe(true); + }); +});