@ashlr/hub 3.12.0 → 3.14.0
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/CHANGELOG.md +157 -0
- package/README.md +29 -16
- package/dist/authority-surface.json +274 -148
- package/dist/build-identity.json +2 -2
- package/dist/cli/authority.d.ts +145 -33
- package/dist/cli/authority.js +1292 -315
- package/dist/cli/authority.js.map +1 -1
- package/dist/cli/cloud.js +12 -3
- package/dist/cli/cloud.js.map +1 -1
- package/dist/cli/comms.d.ts +33 -19
- package/dist/cli/comms.js +128 -151
- package/dist/cli/comms.js.map +1 -1
- package/dist/cli/completions.js +1 -1
- package/dist/cli/completions.js.map +1 -1
- package/dist/cli/daemon.js +29 -0
- package/dist/cli/daemon.js.map +1 -1
- package/dist/cli/director.d.ts +8 -12
- package/dist/cli/director.js +17 -52
- package/dist/cli/director.js.map +1 -1
- package/dist/cli/fleet.js +4 -1
- package/dist/cli/fleet.js.map +1 -1
- package/dist/cli/help.js +1 -0
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/leader.d.ts +27 -3
- package/dist/cli/leader.js +253 -4
- package/dist/cli/leader.js.map +1 -1
- package/dist/core/authority/protected-paths.js +11 -0
- package/dist/core/authority/protected-paths.js.map +1 -1
- package/dist/core/authority/resident.d.ts +101 -0
- package/dist/core/authority/resident.js +246 -0
- package/dist/core/authority/resident.js.map +1 -0
- package/dist/core/authority/server-enforcement.d.ts +110 -0
- package/dist/core/authority/server-enforcement.js +260 -0
- package/dist/core/authority/server-enforcement.js.map +1 -0
- package/dist/core/authority/standing-grant.d.ts +24 -4
- package/dist/core/authority/standing-grant.js +19 -3
- package/dist/core/authority/standing-grant.js.map +1 -1
- package/dist/core/authority/types.d.ts +43 -0
- package/dist/core/authority/types.js +6 -0
- package/dist/core/authority/types.js.map +1 -1
- package/dist/core/cloud/budget.js +11 -1
- package/dist/core/cloud/budget.js.map +1 -1
- package/dist/core/cloud/cloud-api.d.ts +25 -2
- package/dist/core/cloud/cloud-api.js +139 -5
- package/dist/core/cloud/cloud-api.js.map +1 -1
- package/dist/core/cloud/pr-actions.d.ts +93 -0
- package/dist/core/cloud/pr-actions.js +436 -0
- package/dist/core/cloud/pr-actions.js.map +1 -0
- package/dist/core/cloud/pr-preview.d.ts +128 -0
- package/dist/core/cloud/pr-preview.js +228 -0
- package/dist/core/cloud/pr-preview.js.map +1 -0
- package/dist/core/cloud/store.d.ts +2 -0
- package/dist/core/cloud/store.js +28 -0
- package/dist/core/cloud/store.js.map +1 -1
- package/dist/core/cloud/timeline-api.d.ts +24 -0
- package/dist/core/cloud/timeline-api.js +46 -0
- package/dist/core/cloud/timeline-api.js.map +1 -0
- package/dist/core/cloud/timeline-types.d.ts +62 -0
- package/dist/core/cloud/timeline-types.js +36 -0
- package/dist/core/cloud/timeline-types.js.map +1 -0
- package/dist/core/cloud/timeline.d.ts +102 -0
- package/dist/core/cloud/timeline.js +925 -0
- package/dist/core/cloud/timeline.js.map +1 -0
- package/dist/core/cloud/tracker.js +67 -0
- package/dist/core/cloud/tracker.js.map +1 -1
- package/dist/core/cloud/types.d.ts +38 -0
- package/dist/core/cloud/types.js +1 -1
- package/dist/core/cloud/types.js.map +1 -1
- package/dist/core/comms/change-digest.d.ts +130 -0
- package/dist/core/comms/change-digest.js +451 -0
- package/dist/core/comms/change-digest.js.map +1 -0
- package/dist/core/comms/director-prompt.d.ts +4 -5
- package/dist/core/comms/director-prompt.js +4 -71
- package/dist/core/comms/director-prompt.js.map +1 -1
- package/dist/core/comms/director.d.ts +24 -61
- package/dist/core/comms/director.js +29 -382
- package/dist/core/comms/director.js.map +1 -1
- package/dist/core/comms/dispatch.d.ts +37 -12
- package/dist/core/comms/dispatch.js +244 -177
- package/dist/core/comms/dispatch.js.map +1 -1
- package/dist/core/comms/elon-dialogue.d.ts +23 -15
- package/dist/core/comms/elon-dialogue.js +36 -307
- package/dist/core/comms/elon-dialogue.js.map +1 -1
- package/dist/core/comms/fleet-pulse.js +4 -7
- package/dist/core/comms/fleet-pulse.js.map +1 -1
- package/dist/core/comms/handlers.d.ts +10 -6
- package/dist/core/comms/handlers.js +14 -48
- package/dist/core/comms/handlers.js.map +1 -1
- package/dist/core/comms/migrations.d.ts +47 -0
- package/dist/core/comms/migrations.js +125 -0
- package/dist/core/comms/migrations.js.map +1 -0
- package/dist/core/comms/requests.d.ts +32 -5
- package/dist/core/comms/requests.js +101 -5
- package/dist/core/comms/requests.js.map +1 -1
- package/dist/core/comms/telegram-channel.d.ts +104 -0
- package/dist/core/comms/telegram-channel.js +622 -0
- package/dist/core/comms/telegram-channel.js.map +1 -0
- package/dist/core/comms/telegram-thread-map.d.ts +56 -0
- package/dist/core/comms/telegram-thread-map.js +144 -0
- package/dist/core/comms/telegram-thread-map.js.map +1 -0
- package/dist/core/daemon/capacity-publisher.d.ts +8 -1
- package/dist/core/daemon/capacity-publisher.js +40 -2
- package/dist/core/daemon/capacity-publisher.js.map +1 -1
- package/dist/core/daemon/loop.js +18 -4
- package/dist/core/daemon/loop.js.map +1 -1
- package/dist/core/daemon/service-install-authority.d.ts +10 -2
- package/dist/core/daemon/service-install-authority.js +14 -5
- package/dist/core/daemon/service-install-authority.js.map +1 -1
- package/dist/core/daemon/service.d.ts +15 -0
- package/dist/core/daemon/service.js +26 -0
- package/dist/core/daemon/service.js.map +1 -1
- package/dist/core/daemon/tick-deadline.d.ts +44 -0
- package/dist/core/daemon/tick-deadline.js +83 -0
- package/dist/core/daemon/tick-deadline.js.map +1 -0
- package/dist/core/daemon/tick-hooks.d.ts +7 -0
- package/dist/core/daemon/tick-hooks.js.map +1 -1
- package/dist/core/daemon/tick-progress.d.ts +39 -0
- package/dist/core/daemon/tick-progress.js +194 -0
- package/dist/core/daemon/tick-progress.js.map +1 -0
- package/dist/core/dashboard.js +3 -1
- package/dist/core/dashboard.js.map +1 -1
- package/dist/core/fleet/cloud-intake.d.ts +95 -0
- package/dist/core/fleet/cloud-intake.js +557 -0
- package/dist/core/fleet/cloud-intake.js.map +1 -0
- package/dist/core/fleet/dispatch-router.d.ts +13 -0
- package/dist/core/fleet/dispatch-router.js +32 -8
- package/dist/core/fleet/dispatch-router.js.map +1 -1
- package/dist/core/fleet/fleet-merge-state.d.ts +16 -0
- package/dist/core/fleet/fleet-merge-state.js.map +1 -1
- package/dist/core/fleet/host-merge.d.ts +14 -0
- package/dist/core/fleet/host-merge.js +24 -2
- package/dist/core/fleet/host-merge.js.map +1 -1
- package/dist/core/fleet/merge-gates.d.ts +21 -0
- package/dist/core/fleet/merge-gates.js +39 -5
- package/dist/core/fleet/merge-gates.js.map +1 -1
- package/dist/core/fleet/mirrors.d.ts +162 -6
- package/dist/core/fleet/mirrors.js +1051 -49
- package/dist/core/fleet/mirrors.js.map +1 -1
- package/dist/core/fleet/standing-merge-pass.d.ts +8 -0
- package/dist/core/fleet/standing-merge-pass.js +102 -1
- package/dist/core/fleet/standing-merge-pass.js.map +1 -1
- package/dist/core/fleet/status.d.ts +17 -0
- package/dist/core/fleet/status.js +22 -0
- package/dist/core/fleet/status.js.map +1 -1
- package/dist/core/fleet/tick-hooks-live.d.ts +10 -1
- package/dist/core/fleet/tick-hooks-live.js +57 -8
- package/dist/core/fleet/tick-hooks-live.js.map +1 -1
- package/dist/core/fleet/verify-check-run.d.ts +90 -0
- package/dist/core/fleet/verify-check-run.js +274 -0
- package/dist/core/fleet/verify-check-run.js.map +1 -0
- package/dist/core/git.d.ts +17 -0
- package/dist/core/git.js +124 -25
- package/dist/core/git.js.map +1 -1
- package/dist/core/goals/open-goals.d.ts +47 -0
- package/dist/core/goals/open-goals.js +33 -0
- package/dist/core/goals/open-goals.js.map +1 -0
- package/dist/core/inbox/merge.d.ts +5 -0
- package/dist/core/inbox/merge.js +58 -2
- package/dist/core/inbox/merge.js.map +1 -1
- package/dist/core/integrations/locus.d.ts +28 -0
- package/dist/core/integrations/locus.js +155 -30
- package/dist/core/integrations/locus.js.map +1 -1
- package/dist/core/integrations/telegram-format.d.ts +40 -0
- package/dist/core/integrations/telegram-format.js +123 -0
- package/dist/core/integrations/telegram-format.js.map +1 -0
- package/dist/core/integrations/telegram.d.ts +60 -4
- package/dist/core/integrations/telegram.js +161 -28
- package/dist/core/integrations/telegram.js.map +1 -1
- package/dist/core/mcp-registry.d.ts +12 -0
- package/dist/core/mcp-registry.js +70 -0
- package/dist/core/mcp-registry.js.map +1 -1
- package/dist/core/observability/rollup.d.ts +12 -10
- package/dist/core/observability/rollup.js +44 -17
- package/dist/core/observability/rollup.js.map +1 -1
- package/dist/core/resources/connection-monitor.d.ts +9 -0
- package/dist/core/resources/connection-monitor.js +19 -9
- package/dist/core/resources/connection-monitor.js.map +1 -1
- package/dist/core/resources/quota-refresh-lease.d.ts +17 -0
- package/dist/core/resources/quota-refresh-lease.js +34 -1
- package/dist/core/resources/quota-refresh-lease.js.map +1 -1
- package/dist/core/routing/budget-api.d.ts +18 -1
- package/dist/core/routing/budget-api.js +120 -10
- package/dist/core/routing/budget-api.js.map +1 -1
- package/dist/core/routing/budget-store.d.ts +22 -1
- package/dist/core/routing/budget-store.js +22 -8
- package/dist/core/routing/budget-store.js.map +1 -1
- package/dist/core/routing/readiness-types.d.ts +82 -0
- package/dist/core/routing/readiness-types.js +23 -0
- package/dist/core/routing/readiness-types.js.map +1 -0
- package/dist/core/routing/readiness.d.ts +103 -0
- package/dist/core/routing/readiness.js +480 -0
- package/dist/core/routing/readiness.js.map +1 -0
- package/dist/core/sandbox/confine.js +22 -1
- package/dist/core/sandbox/confine.js.map +1 -1
- package/dist/core/sandbox/policy.d.ts +23 -0
- package/dist/core/sandbox/policy.js +100 -0
- package/dist/core/sandbox/policy.js.map +1 -1
- package/dist/core/types.d.ts +13 -0
- package/dist/core/types.js.map +1 -1
- package/dist/core/universe/builtins/preparation/manifest.json +1 -1
- package/dist/core/universe/builtins/preparation/preparation-bridge.mjs +1204 -939
- package/dist/core/universe/builtins/preparation/preparation-verification-fixtures.mjs +1319 -1054
- package/dist/core/usage/frontier-usage.d.ts +20 -2
- package/dist/core/usage/frontier-usage.js +75 -17
- package/dist/core/usage/frontier-usage.js.map +1 -1
- package/dist/core/verse/account-health.js +9 -3
- package/dist/core/verse/account-health.js.map +1 -1
- package/dist/core/verse/accounts.d.ts +45 -0
- package/dist/core/verse/accounts.js +246 -17
- package/dist/core/verse/accounts.js.map +1 -1
- package/dist/core/verse/activity-api.d.ts +5 -1
- package/dist/core/verse/activity-api.js +62 -14
- package/dist/core/verse/activity-api.js.map +1 -1
- package/dist/core/verse/apps-api.js +23 -11
- package/dist/core/verse/apps-api.js.map +1 -1
- package/dist/core/verse/apps.d.ts +11 -9
- package/dist/core/verse/apps.js +2 -2
- package/dist/core/verse/apps.js.map +1 -1
- package/dist/core/verse/authority-api.d.ts +17 -3
- package/dist/core/verse/authority-api.js +122 -7
- package/dist/core/verse/authority-api.js.map +1 -1
- package/dist/core/verse/autonomy-ladder.d.ts +172 -0
- package/dist/core/verse/autonomy-ladder.js +251 -0
- package/dist/core/verse/autonomy-ladder.js.map +1 -0
- package/dist/core/verse/context-fit.js +7 -5
- package/dist/core/verse/context-fit.js.map +1 -1
- package/dist/core/verse/control-api.d.ts +23 -1
- package/dist/core/verse/control-api.js +117 -10
- package/dist/core/verse/control-api.js.map +1 -1
- package/dist/core/verse/fleet-live-api.js +13 -5
- package/dist/core/verse/fleet-live-api.js.map +1 -1
- package/dist/core/verse/folder-io.d.ts +50 -0
- package/dist/core/verse/folder-io.js +142 -0
- package/dist/core/verse/folder-io.js.map +1 -0
- package/dist/core/verse/git-api.js +5 -4
- package/dist/core/verse/git-api.js.map +1 -1
- package/dist/core/verse/git-ops.d.ts +10 -1
- package/dist/core/verse/git-ops.js +18 -2
- package/dist/core/verse/git-ops.js.map +1 -1
- package/dist/core/verse/github-api.d.ts +1 -1
- package/dist/core/verse/github-api.js +5 -4
- package/dist/core/verse/github-api.js.map +1 -1
- package/dist/core/verse/github-repo.js +118 -14
- package/dist/core/verse/github-repo.js.map +1 -1
- package/dist/core/verse/leader-api.d.ts +5 -4
- package/dist/core/verse/leader-api.js +237 -10
- package/dist/core/verse/leader-api.js.map +1 -1
- package/dist/core/verse/local-models.d.ts +10 -0
- package/dist/core/verse/local-models.js +10 -1
- package/dist/core/verse/local-models.js.map +1 -1
- package/dist/core/verse/login-path.d.ts +73 -0
- package/dist/core/verse/login-path.js +123 -14
- package/dist/core/verse/login-path.js.map +1 -1
- package/dist/core/verse/mcp-control-api.js +4 -4
- package/dist/core/verse/mcp-control-api.js.map +1 -1
- package/dist/core/verse/mcp-scope.d.ts +22 -0
- package/dist/core/verse/mcp-scope.js +41 -1
- package/dist/core/verse/mcp-scope.js.map +1 -1
- package/dist/core/verse/mcp-seat-view.d.ts +10 -0
- package/dist/core/verse/mcp-seat-view.js +30 -4
- package/dist/core/verse/mcp-seat-view.js.map +1 -1
- package/dist/core/verse/overnight-api.js +20 -2
- package/dist/core/verse/overnight-api.js.map +1 -1
- package/dist/core/verse/path-guard.d.ts +24 -0
- package/dist/core/verse/path-guard.js +91 -22
- package/dist/core/verse/path-guard.js.map +1 -1
- package/dist/core/verse/preview-api.js +22 -0
- package/dist/core/verse/preview-api.js.map +1 -1
- package/dist/core/verse/projects.d.ts +21 -0
- package/dist/core/verse/projects.js +82 -0
- package/dist/core/verse/projects.js.map +1 -1
- package/dist/core/verse/seat-readiness.d.ts +25 -0
- package/dist/core/verse/seat-readiness.js +43 -6
- package/dist/core/verse/seat-readiness.js.map +1 -1
- package/dist/core/verse/session-controls-api.js +5 -3
- package/dist/core/verse/session-controls-api.js.map +1 -1
- package/dist/core/verse/session-handoff.d.ts +26 -0
- package/dist/core/verse/session-handoff.js +90 -12
- package/dist/core/verse/session-handoff.js.map +1 -1
- package/dist/core/verse/terminal-api.js +7 -4
- package/dist/core/verse/terminal-api.js.map +1 -1
- package/dist/core/verse/terminal.js +9 -5
- package/dist/core/verse/terminal.js.map +1 -1
- package/dist/core/verse/verse-api.d.ts +3 -1
- package/dist/core/verse/verse-api.js +87 -27
- package/dist/core/verse/verse-api.js.map +1 -1
- package/dist/core/verse/workspaces.d.ts +51 -0
- package/dist/core/verse/workspaces.js +177 -47
- package/dist/core/verse/workspaces.js.map +1 -1
- package/dist/core/vision/leader-apply.d.ts +40 -0
- package/dist/core/vision/leader-apply.js +101 -30
- package/dist/core/vision/leader-apply.js.map +1 -1
- package/dist/core/vision/leader-cadence.d.ts +114 -0
- package/dist/core/vision/leader-cadence.js +127 -0
- package/dist/core/vision/leader-cadence.js.map +1 -0
- package/dist/core/vision/leader-goal-evidence.d.ts +25 -0
- package/dist/core/vision/leader-goal-evidence.js +36 -0
- package/dist/core/vision/leader-goal-evidence.js.map +1 -0
- package/dist/core/vision/leader-health.d.ts +53 -0
- package/dist/core/vision/leader-health.js +92 -0
- package/dist/core/vision/leader-health.js.map +1 -0
- package/dist/core/vision/leader-operator.d.ts +176 -0
- package/dist/core/vision/leader-operator.js +355 -0
- package/dist/core/vision/leader-operator.js.map +1 -0
- package/dist/core/vision/leader-run-chain.d.ts +41 -0
- package/dist/core/vision/leader-run-chain.js +54 -0
- package/dist/core/vision/leader-run-chain.js.map +1 -0
- package/dist/core/vision/leader-seat-plan.d.ts +68 -0
- package/dist/core/vision/leader-seat-plan.js +93 -0
- package/dist/core/vision/leader-seat-plan.js.map +1 -0
- package/dist/core/vision/leader-seat.d.ts +71 -5
- package/dist/core/vision/leader-seat.js +235 -33
- package/dist/core/vision/leader-seat.js.map +1 -1
- package/dist/core/vision/leader-thread-types.d.ts +104 -0
- package/dist/core/vision/leader-thread-types.js +9 -0
- package/dist/core/vision/leader-thread-types.js.map +1 -0
- package/dist/core/vision/leader-thread.d.ts +138 -0
- package/dist/core/vision/leader-thread.js +926 -0
- package/dist/core/vision/leader-thread.js.map +1 -0
- package/dist/core/vision/leader-types.d.ts +75 -1
- package/dist/core/vision/leader.d.ts +88 -18
- package/dist/core/vision/leader.js +269 -69
- package/dist/core/vision/leader.js.map +1 -1
- package/dist/core/web/public/next/assets/{ActionMenu-B4-39f2S.js → ActionMenu-CIiX2X8w.js} +2 -2
- package/dist/core/web/public/next/assets/{ActionMenu-B4-39f2S.js.map → ActionMenu-CIiX2X8w.js.map} +1 -1
- package/dist/core/web/public/next/assets/Advanced-DHsysoIL.js +2 -0
- package/dist/core/web/public/next/assets/{Advanced-B-LqYXvd.js.map → Advanced-DHsysoIL.js.map} +1 -1
- package/dist/core/web/public/next/assets/App-CMeAocmW.js +7 -0
- package/dist/core/web/public/next/assets/{App-E2zCDRxm.js.map → App-CMeAocmW.js.map} +1 -1
- package/dist/core/web/public/next/assets/AppsSection-DISYnMnc.js +2 -0
- package/dist/core/web/public/next/assets/{AppsSection-8wXPPhZb.js.map → AppsSection-DISYnMnc.js.map} +1 -1
- package/dist/core/web/public/next/assets/AreaTrend-DK2Y2TbZ.js +2 -0
- package/dist/core/web/public/next/assets/AreaTrend-DK2Y2TbZ.js.map +1 -0
- package/dist/core/web/public/next/assets/AutonomyOffState-CEPSZToP.js +3 -0
- package/dist/core/web/public/next/assets/AutonomyOffState-CEPSZToP.js.map +1 -0
- package/dist/core/web/public/next/assets/AutonomyOffState-CmSSTHVe.css +1 -0
- package/dist/core/web/public/next/assets/AutonomySection-DWc3ciAD.js +2 -0
- package/dist/core/web/public/next/assets/{AutonomySection-B46igJKR.js.map → AutonomySection-DWc3ciAD.js.map} +1 -1
- package/dist/core/web/public/next/assets/AutonomyStatus-CYaHjJ9U.js +2 -0
- package/dist/core/web/public/next/assets/AutonomyStatus-CYaHjJ9U.js.map +1 -0
- package/dist/core/web/public/next/assets/{BranchBar-BRkrEQGs.js → BranchBar-BxhdiIpx.js} +2 -2
- package/dist/core/web/public/next/assets/{BranchBar-BRkrEQGs.js.map → BranchBar-BxhdiIpx.js.map} +1 -1
- package/dist/core/web/public/next/assets/BudgetControl-C-0KhjRS.js +2 -0
- package/dist/core/web/public/next/assets/BudgetControl-C-0KhjRS.js.map +1 -0
- package/dist/core/web/public/next/assets/{Button-BhO-WKPp.js → Button-D90dzI_o.js} +2 -2
- package/dist/core/web/public/next/assets/{Button-BhO-WKPp.js.map → Button-D90dzI_o.js.map} +1 -1
- package/dist/core/web/public/next/assets/{CapacityStrip-CkFz5vq2.js → CapacityStrip-qKDBC65C.js} +2 -2
- package/dist/core/web/public/next/assets/{CapacityStrip-CkFz5vq2.js.map → CapacityStrip-qKDBC65C.js.map} +1 -1
- package/dist/core/web/public/next/assets/ChartFrame-DY-GVUb3.js +2 -0
- package/dist/core/web/public/next/assets/ChartFrame-DY-GVUb3.js.map +1 -0
- package/dist/core/web/public/next/assets/ChartFrame-DyjmyV8N.css +1 -0
- package/dist/core/web/public/next/assets/ChatSection-DlT4QU6a.js +3 -0
- package/dist/core/web/public/next/assets/ChatSection-DlT4QU6a.js.map +1 -0
- package/dist/core/web/public/next/assets/{CloudBudgetForm-Bd91iStN.js → CloudBudgetForm-BrNun_Yu.js} +2 -2
- package/dist/core/web/public/next/assets/{CloudBudgetForm-Bd91iStN.js.map → CloudBudgetForm-BrNun_Yu.js.map} +1 -1
- package/dist/core/web/public/next/assets/CommandPalette-C5lDDKN0.js +2 -0
- package/dist/core/web/public/next/assets/CommandPalette-C5lDDKN0.js.map +1 -0
- package/dist/core/web/public/next/assets/CommandSection-B0xuU-yZ.css +1 -0
- package/dist/core/web/public/next/assets/CommandSection-DXFvPgKp.js +3 -0
- package/dist/core/web/public/next/assets/CommandSection-DXFvPgKp.js.map +1 -0
- package/dist/core/web/public/next/assets/{ConfirmDialog-Bv4hdK2T.js → ConfirmDialog-BR4nZQ2D.js} +2 -2
- package/dist/core/web/public/next/assets/{ConfirmDialog-Bv4hdK2T.js.map → ConfirmDialog-BR4nZQ2D.js.map} +1 -1
- package/dist/core/web/public/next/assets/{DeleteChatDialog-CH1Ix8Eh.js → DeleteChatDialog-BCfP12GH.js} +2 -2
- package/dist/core/web/public/next/assets/{DeleteChatDialog-CH1Ix8Eh.js.map → DeleteChatDialog-BCfP12GH.js.map} +1 -1
- package/dist/core/web/public/next/assets/{Dialog-BSsIFNqD.js → Dialog-D3msQlCj.js} +2 -2
- package/dist/core/web/public/next/assets/{Dialog-BSsIFNqD.js.map → Dialog-D3msQlCj.js.map} +1 -1
- package/dist/core/web/public/next/assets/{DiffPane-BpNuJmth.js → DiffPane-Ix6hvXlY.js} +2 -2
- package/dist/core/web/public/next/assets/{DiffPane-BpNuJmth.js.map → DiffPane-Ix6hvXlY.js.map} +1 -1
- package/dist/core/web/public/next/assets/{DiffViewer-CJarUvs0.js → DiffViewer-C4WKsCIe.js} +2 -2
- package/dist/core/web/public/next/assets/{DiffViewer-CJarUvs0.js.map → DiffViewer-C4WKsCIe.js.map} +1 -1
- package/dist/core/web/public/next/assets/DockHost-aWP7liri.js +2 -0
- package/dist/core/web/public/next/assets/{DockHost-DW9q0NdR.js.map → DockHost-aWP7liri.js.map} +1 -1
- package/dist/core/web/public/next/assets/{EmptyState-DPrd2aak.js → EmptyState-D2IRy3Cg.js} +2 -2
- package/dist/core/web/public/next/assets/{EmptyState-DPrd2aak.js.map → EmptyState-D2IRy3Cg.js.map} +1 -1
- package/dist/core/web/public/next/assets/{Epistemic-D7MxSmED.js → Epistemic-DMr18muX.js} +2 -2
- package/dist/core/web/public/next/assets/{Epistemic-D7MxSmED.js.map → Epistemic-DMr18muX.js.map} +1 -1
- package/dist/core/web/public/next/assets/EvidenceTimeline-DXmmz6Fi.js +2 -0
- package/dist/core/web/public/next/assets/EvidenceTimeline-DXmmz6Fi.js.map +1 -0
- package/dist/core/web/public/next/assets/EvidenceTimeline-PBDAiSuG.css +1 -0
- package/dist/core/web/public/next/assets/FleetSection-BfplcwFU.js +3 -0
- package/dist/core/web/public/next/assets/FleetSection-BfplcwFU.js.map +1 -0
- package/dist/core/web/public/next/assets/Funnel-Dh2yVHws.js +2 -0
- package/dist/core/web/public/next/assets/Funnel-Dh2yVHws.js.map +1 -0
- package/dist/core/web/public/next/assets/{GearTray-CIPtzeIx.js → GearTray-Dt-ee32-.js} +2 -2
- package/dist/core/web/public/next/assets/{GearTray-CIPtzeIx.js.map → GearTray-Dt-ee32-.js.map} +1 -1
- package/dist/core/web/public/next/assets/GrowthSection-P9EUfGeA.js +2 -0
- package/dist/core/web/public/next/assets/{GrowthSection-iiQg6wvK.js.map → GrowthSection-P9EUfGeA.js.map} +1 -1
- package/dist/core/web/public/next/assets/{HandoffDialog-DrQ6ofuy.js → HandoffDialog-CLJQLe7M.js} +2 -2
- package/dist/core/web/public/next/assets/{HandoffDialog-DrQ6ofuy.js.map → HandoffDialog-CLJQLe7M.js.map} +1 -1
- package/dist/core/web/public/next/assets/{Input-C0MQxEM6.js → Input-D_KRJA9V.js} +2 -2
- package/dist/core/web/public/next/assets/{Input-C0MQxEM6.js.map → Input-D_KRJA9V.js.map} +1 -1
- package/dist/core/web/public/next/assets/LeaderCard-D-7DlJuR.js +2 -0
- package/dist/core/web/public/next/assets/LeaderCard-D-7DlJuR.js.map +1 -0
- package/dist/core/web/public/next/assets/LeaderCard-DNsbLwY3.css +1 -0
- package/dist/core/web/public/next/assets/LeaderConversation-DGZjWI-E.js +2 -0
- package/dist/core/web/public/next/assets/LeaderConversation-DGZjWI-E.js.map +1 -0
- package/dist/core/web/public/next/assets/LeaderConversation-DPHUTq6W.css +1 -0
- package/dist/core/web/public/next/assets/MatrixHeatmap-C7BCG2d7.js +2 -0
- package/dist/core/web/public/next/assets/MatrixHeatmap-C7BCG2d7.js.map +1 -0
- package/dist/core/web/public/next/assets/McpSection-DGbRcyv8.js +2 -0
- package/dist/core/web/public/next/assets/{McpSection-BRuCcTjT.js.map → McpSection-DGbRcyv8.js.map} +1 -1
- package/dist/core/web/public/next/assets/{MessageMarkdown-bRvD5nSc.js → MessageMarkdown-DFvk06qG.js} +3 -3
- package/dist/core/web/public/next/assets/{MessageMarkdown-bRvD5nSc.js.map → MessageMarkdown-DFvk06qG.js.map} +1 -1
- package/dist/core/web/public/next/assets/{MessageMarkdownRenderer-FjjkPKA2.js → MessageMarkdownRenderer-BAv4oEUf.js} +2 -2
- package/dist/core/web/public/next/assets/{MessageMarkdownRenderer-FjjkPKA2.js.map → MessageMarkdownRenderer-BAv4oEUf.js.map} +1 -1
- package/dist/core/web/public/next/assets/{Meter-CFNLpwA-.js → Meter-CMj-VafN.js} +2 -2
- package/dist/core/web/public/next/assets/{Meter-CFNLpwA-.js.map → Meter-CMj-VafN.js.map} +1 -1
- package/dist/core/web/public/next/assets/MindSection-C6kLPIjt.js +3 -0
- package/dist/core/web/public/next/assets/MindSection-C6kLPIjt.js.map +1 -0
- package/dist/core/web/public/next/assets/{MutationTokenDialog-T7gv3a9r.js → MutationTokenDialog-DbnO8aYi.js} +2 -2
- package/dist/core/web/public/next/assets/{MutationTokenDialog-T7gv3a9r.js.map → MutationTokenDialog-DbnO8aYi.js.map} +1 -1
- package/dist/core/web/public/next/assets/NeedsYouDrawer-crsiX-Nc.js +3 -0
- package/dist/core/web/public/next/assets/NeedsYouDrawer-crsiX-Nc.js.map +1 -0
- package/dist/core/web/public/next/assets/NeedsYouList-CCsijdWt.js +2 -0
- package/dist/core/web/public/next/assets/{NeedsYouList-BgH8ezqf.js.map → NeedsYouList-CCsijdWt.js.map} +1 -1
- package/dist/core/web/public/next/assets/NeedsYouList-CH6Mq-E_.css +1 -0
- package/dist/core/web/public/next/assets/NewChatDialog-BlCHtOIr.js +3 -0
- package/dist/core/web/public/next/assets/{NewChatDialog-Db5Xjrk8.js.map → NewChatDialog-BlCHtOIr.js.map} +1 -1
- package/dist/core/web/public/next/assets/OnboardingFlow-Cy9BA9eR.js +3 -0
- package/dist/core/web/public/next/assets/OnboardingFlow-Cy9BA9eR.js.map +1 -0
- package/dist/core/web/public/next/assets/OnboardingGate-BXpNFoLt.js +3 -0
- package/dist/core/web/public/next/assets/{OnboardingGate-C4VXn7BJ.js.map → OnboardingGate-BXpNFoLt.js.map} +1 -1
- package/dist/core/web/public/next/assets/PreviewPane-COid5A0f.js +2 -0
- package/dist/core/web/public/next/assets/{PreviewPane-BPBbcROb.js.map → PreviewPane-COid5A0f.js.map} +1 -1
- package/dist/core/web/public/next/assets/{ProviderLogo-CT2cSfkg.js → ProviderLogo-Cq-CYIwT.js} +2 -2
- package/dist/core/web/public/next/assets/{ProviderLogo-CT2cSfkg.js.map → ProviderLogo-Cq-CYIwT.js.map} +1 -1
- package/dist/core/web/public/next/assets/{RailStatus-Vgl0GYtd.js → RailStatus-Co5UB6We.js} +2 -2
- package/dist/core/web/public/next/assets/{RailStatus-Vgl0GYtd.js.map → RailStatus-Co5UB6We.js.map} +1 -1
- package/dist/core/web/public/next/assets/{RefreshIndicator-CSyqpY_q.js → RefreshIndicator-DO9ibnSq.js} +2 -2
- package/dist/core/web/public/next/assets/{RefreshIndicator-CSyqpY_q.js.map → RefreshIndicator-DO9ibnSq.js.map} +1 -1
- package/dist/core/web/public/next/assets/ResourcePoolConsoleApp-Dr2WRgkq.js +7 -0
- package/dist/core/web/public/next/assets/{ResourcePoolConsoleApp-BX2nkdmZ.js.map → ResourcePoolConsoleApp-Dr2WRgkq.js.map} +1 -1
- package/dist/core/web/public/next/assets/ResourcesChrome-CwMnbqI1.js +3 -0
- package/dist/core/web/public/next/assets/ResourcesChrome-CwMnbqI1.js.map +1 -0
- package/dist/core/web/public/next/assets/ResourcesDrawer-B_Xp_JNc.js +2 -0
- package/dist/core/web/public/next/assets/ResourcesDrawer-B_Xp_JNc.js.map +1 -0
- package/dist/core/web/public/next/assets/ResourcesDrawer-DrGoO8KG.css +1 -0
- package/dist/core/web/public/next/assets/{RouteErrorBoundary-BaC7YVzR.js → RouteErrorBoundary-SkNhhUbt.js} +2 -2
- package/dist/core/web/public/next/assets/{RouteErrorBoundary-BaC7YVzR.js.map → RouteErrorBoundary-SkNhhUbt.js.map} +1 -1
- package/dist/core/web/public/next/assets/{RunInCloudAction-D5PDHJPE.js → RunInCloudAction-BXPlZwJQ.js} +2 -2
- package/dist/core/web/public/next/assets/{RunInCloudAction-D5PDHJPE.js.map → RunInCloudAction-BXPlZwJQ.js.map} +1 -1
- package/dist/core/web/public/next/assets/{SeatSelector-Cg75LInq.js → SeatSelector-J5R3zDkB.js} +3 -3
- package/dist/core/web/public/next/assets/{SeatSelector-Cg75LInq.js.map → SeatSelector-J5R3zDkB.js.map} +1 -1
- package/dist/core/web/public/next/assets/{Segmented-CametjLt.js → Segmented-CGh_-tBb.js} +2 -2
- package/dist/core/web/public/next/assets/{Segmented-CametjLt.js.map → Segmented-CGh_-tBb.js.map} +1 -1
- package/dist/core/web/public/next/assets/{Select-De52fghU.js → Select-CWnmUzs3.js} +2 -2
- package/dist/core/web/public/next/assets/{Select-De52fghU.js.map → Select-CWnmUzs3.js.map} +1 -1
- package/dist/core/web/public/next/assets/{SessionGate-CzlcNq3O.js → SessionGate-sp4KsnUo.js} +2 -2
- package/dist/core/web/public/next/assets/{SessionGate-CzlcNq3O.js.map → SessionGate-sp4KsnUo.js.map} +1 -1
- package/dist/core/web/public/next/assets/{SessionInsightChip-Bk_Owa7u.js → SessionInsightChip-DLFAzdbi.js} +2 -2
- package/dist/core/web/public/next/assets/{SessionInsightChip-Bk_Owa7u.js.map → SessionInsightChip-DLFAzdbi.js.map} +1 -1
- package/dist/core/web/public/next/assets/{SessionSearch-C6C8DHN8.js → SessionSearch-DbFjwR1h.js} +2 -2
- package/dist/core/web/public/next/assets/{SessionSearch-C6C8DHN8.js.map → SessionSearch-DbFjwR1h.js.map} +1 -1
- package/dist/core/web/public/next/assets/{SettingsSection-mI9wQYZg.js → SettingsSection-Ch23msJ5.js} +2 -2
- package/dist/core/web/public/next/assets/SettingsSection-Ch23msJ5.js.map +1 -0
- package/dist/core/web/public/next/assets/SetupChecklist-Bd76MBAR.js +2 -0
- package/dist/core/web/public/next/assets/SetupChecklist-Bd76MBAR.js.map +1 -0
- package/dist/core/web/public/next/assets/SetupChecklist-BhZpFLsj.css +1 -0
- package/dist/core/web/public/next/assets/ShadowDecisions-C5RAY91f.js +3 -0
- package/dist/core/web/public/next/assets/ShadowDecisions-C5RAY91f.js.map +1 -0
- package/dist/core/web/public/next/assets/{Sheet-CJhK9Dqv.js → Sheet-BZ2AG4Eo.js} +2 -2
- package/dist/core/web/public/next/assets/{Sheet-CJhK9Dqv.js.map → Sheet-BZ2AG4Eo.js.map} +1 -1
- package/dist/core/web/public/next/assets/{ShortcutsOverlay-DDW-i9Fc.js → ShortcutsOverlay-C8n8y3zf.js} +2 -2
- package/dist/core/web/public/next/assets/{ShortcutsOverlay-DDW-i9Fc.js.map → ShortcutsOverlay-C8n8y3zf.js.map} +1 -1
- package/dist/core/web/public/next/assets/Sidebar-DN--cPYN.js +3 -0
- package/dist/core/web/public/next/assets/Sidebar-DN--cPYN.js.map +1 -0
- package/dist/core/web/public/next/assets/{Slider-Bwd9HfXl.js → Slider-KVoJLDD6.js} +2 -2
- package/dist/core/web/public/next/assets/{Slider-Bwd9HfXl.js.map → Slider-KVoJLDD6.js.map} +1 -1
- package/dist/core/web/public/next/assets/StatTile-C4KgjltA.js +2 -0
- package/dist/core/web/public/next/assets/StatTile-C4KgjltA.js.map +1 -0
- package/dist/core/web/public/next/assets/StatTile-CNCqDoPO.css +1 -0
- package/dist/core/web/public/next/assets/{StatusBadge-DHKRtttH.js → StatusBadge-CXsYqmdU.js} +2 -2
- package/dist/core/web/public/next/assets/{StatusBadge-DHKRtttH.js.map → StatusBadge-CXsYqmdU.js.map} +1 -1
- package/dist/core/web/public/next/assets/StepBand-BMtVOFpK.js +2 -0
- package/dist/core/web/public/next/assets/StepBand-BMtVOFpK.js.map +1 -0
- package/dist/core/web/public/next/assets/{Surface-D--LXgQm.js → Surface-BfDmQcKe.js} +2 -2
- package/dist/core/web/public/next/assets/{Surface-D--LXgQm.js.map → Surface-BfDmQcKe.js.map} +1 -1
- package/dist/core/web/public/next/assets/Swimlane-B9Exxekk.js +2 -0
- package/dist/core/web/public/next/assets/Swimlane-B9Exxekk.js.map +1 -0
- package/dist/core/web/public/next/assets/Swimlane-Bf4Rjoy9.css +1 -0
- package/dist/core/web/public/next/assets/{Switch-PMixU6b0.js → Switch-9NtO_6Lj.js} +2 -2
- package/dist/core/web/public/next/assets/{Switch-PMixU6b0.js.map → Switch-9NtO_6Lj.js.map} +1 -1
- package/dist/core/web/public/next/assets/{Tag-fordBsrJ.js → Tag-DscCRFcN.js} +2 -2
- package/dist/core/web/public/next/assets/{Tag-fordBsrJ.js.map → Tag-DscCRFcN.js.map} +1 -1
- package/dist/core/web/public/next/assets/{TerminalPane-BSnGdUuF.js → TerminalPane-B668-DJW.js} +5 -5
- package/dist/core/web/public/next/assets/{TerminalPane-BSnGdUuF.js.map → TerminalPane-B668-DJW.js.map} +1 -1
- package/dist/core/web/public/next/assets/UniverseConsoleApp-BUV_brY7.js +2 -0
- package/dist/core/web/public/next/assets/{UniverseConsoleApp-DtXFhAhw.js.map → UniverseConsoleApp-BUV_brY7.js.map} +1 -1
- package/dist/core/web/public/next/assets/{UniverseView-sWm3pnTW.js → UniverseView-DG70zH0W.js} +4 -4
- package/dist/core/web/public/next/assets/{UniverseView-sWm3pnTW.js.map → UniverseView-DG70zH0W.js.map} +1 -1
- package/dist/core/web/public/next/assets/UsageSection-DY7uOTm1.js +3 -0
- package/dist/core/web/public/next/assets/UsageSection-DY7uOTm1.js.map +1 -0
- package/dist/core/web/public/next/assets/VerseConsoleApp-CWU6R3By.js +3 -0
- package/dist/core/web/public/next/assets/VerseConsoleApp-CWU6R3By.js.map +1 -0
- package/dist/core/web/public/next/assets/Workspace-D1nP9BhD.js +10 -0
- package/dist/core/web/public/next/assets/{Workspace-BQSZFSeO.js.map → Workspace-D1nP9BhD.js.map} +1 -1
- package/dist/core/web/public/next/assets/actions-CY-PjpGc.js +2 -0
- package/dist/core/web/public/next/assets/{actions-Ci6I4C0l.js.map → actions-CY-PjpGc.js.map} +1 -1
- package/dist/core/web/public/next/assets/actions-DkWhhWIg.css +1 -0
- package/dist/core/web/public/next/assets/{apps-model-DlwnfqwJ.js → apps-model-Du02KhAf.js} +2 -2
- package/dist/core/web/public/next/assets/{apps-model-DlwnfqwJ.js.map → apps-model-Du02KhAf.js.map} +1 -1
- package/dist/core/web/public/next/assets/{authority-model-BFuPezSo.js → authority-model-79SeuKlJ.js} +2 -2
- package/dist/core/web/public/next/assets/{authority-model-BFuPezSo.js.map → authority-model-79SeuKlJ.js.map} +1 -1
- package/dist/core/web/public/next/assets/autonomy-ladder-BGWmZzP7.css +1 -0
- package/dist/core/web/public/next/assets/autonomy-ladder.module-ArDv4bkr.js +2 -0
- package/dist/core/web/public/next/assets/autonomy-ladder.module-ArDv4bkr.js.map +1 -0
- package/dist/core/web/public/next/assets/budget-queries-D8QV5XtH.js +2 -0
- package/dist/core/web/public/next/assets/{budget-queries-DsAjH8ac.js.map → budget-queries-D8QV5XtH.js.map} +1 -1
- package/dist/core/web/public/next/assets/{capacity-strip-model-Dn1cNOhz.js → capacity-strip-model-BwZ1ZbkU.js} +2 -2
- package/dist/core/web/public/next/assets/{capacity-strip-model-Dn1cNOhz.js.map → capacity-strip-model-BwZ1ZbkU.js.map} +1 -1
- package/dist/core/web/public/next/assets/charts-BZ50mZD_.js +2 -0
- package/dist/core/web/public/next/assets/charts-BZ50mZD_.js.map +1 -0
- package/dist/core/web/public/next/assets/charts-ClCB18la.css +1 -0
- package/dist/core/web/public/next/assets/cloud-model-BDivRMqv.js +2 -0
- package/dist/core/web/public/next/assets/{cloud-model-CX407gPY.js.map → cloud-model-BDivRMqv.js.map} +1 -1
- package/dist/core/web/public/next/assets/{cloud-queries-QAhpExlz.js → cloud-queries-BPQQ5_pJ.js} +2 -2
- package/dist/core/web/public/next/assets/{cloud-queries-QAhpExlz.js.map → cloud-queries-BPQQ5_pJ.js.map} +1 -1
- package/dist/core/web/public/next/assets/{colors-BZ3a9wFf.js → colors-DvK0wMHH.js} +2 -2
- package/dist/core/web/public/next/assets/{colors-BZ3a9wFf.js.map → colors-DvK0wMHH.js.map} +1 -1
- package/dist/core/web/public/next/assets/{command-catalog-CF0cQQac.js → command-catalog-BIntSa9l.js} +2 -2
- package/dist/core/web/public/next/assets/command-catalog-BIntSa9l.js.map +1 -0
- package/dist/core/web/public/next/assets/{context-model-Co4wh3SM.js → context-model-Br8t9jgG.js} +2 -2
- package/dist/core/web/public/next/assets/{context-model-Co4wh3SM.js.map → context-model-Br8t9jgG.js.map} +1 -1
- package/dist/core/web/public/next/assets/{context-model-DV1zCfAm.js → context-model-CokrgEUq.js} +3 -3
- package/dist/core/web/public/next/assets/{context-model-DV1zCfAm.js.map → context-model-CokrgEUq.js.map} +1 -1
- package/dist/core/web/public/next/assets/context-queries-eL3caKVG.js +2 -0
- package/dist/core/web/public/next/assets/{context-queries-CktFQqSS.js.map → context-queries-eL3caKVG.js.map} +1 -1
- package/dist/core/web/public/next/assets/{control-queries-DAvlh_YM.js → control-queries-B9K-wmcB.js} +2 -2
- package/dist/core/web/public/next/assets/{control-queries-DAvlh_YM.js.map → control-queries-B9K-wmcB.js.map} +1 -1
- package/dist/core/web/public/next/assets/copy-setup-XMgCC5f8.js +2 -0
- package/dist/core/web/public/next/assets/copy-setup-XMgCC5f8.js.map +1 -0
- package/dist/core/web/public/next/assets/{dock-icons-BFuBbqJl.js → dock-icons-BJ6ZqB2t.js} +2 -2
- package/dist/core/web/public/next/assets/{dock-icons-BFuBbqJl.js.map → dock-icons-BJ6ZqB2t.js.map} +1 -1
- package/dist/core/web/public/next/assets/{fleet-model-Dfd5JyLR.js → fleet-model-BvDR3gjp.js} +2 -2
- package/dist/core/web/public/next/assets/{fleet-model-Dfd5JyLR.js.map → fleet-model-BvDR3gjp.js.map} +1 -1
- package/dist/core/web/public/next/assets/{fleet-queries-RtMTd5rR.js → fleet-queries-W2UFVMZJ.js} +2 -2
- package/dist/core/web/public/next/assets/{fleet-queries-RtMTd5rR.js.map → fleet-queries-W2UFVMZJ.js.map} +1 -1
- package/dist/core/web/public/next/assets/{focus-trap-BdrbWfC4.js → focus-trap-CtI1SlMX.js} +2 -2
- package/dist/core/web/public/next/assets/{focus-trap-BdrbWfC4.js.map → focus-trap-CtI1SlMX.js.map} +1 -1
- package/dist/core/web/public/next/assets/format-CH4g_sl1.js +2 -0
- package/dist/core/web/public/next/assets/format-CH4g_sl1.js.map +1 -0
- package/dist/core/web/public/next/assets/{format-DKrNjdwD.js → format-D7nBg2bn.js} +2 -2
- package/dist/core/web/public/next/assets/{format-DKrNjdwD.js.map → format-D7nBg2bn.js.map} +1 -1
- package/dist/core/web/public/next/assets/{git-model-_BnI_Ogh.js → git-model-Dnkd1TnK.js} +2 -2
- package/dist/core/web/public/next/assets/{git-model-_BnI_Ogh.js.map → git-model-Dnkd1TnK.js.map} +1 -1
- package/dist/core/web/public/next/assets/global-CMNW_dqn.js +2 -0
- package/dist/core/web/public/next/assets/{global-CNNGMril.js.map → global-CMNW_dqn.js.map} +1 -1
- package/dist/core/web/public/next/assets/guarded-runners-YeAc9wLQ.js +2 -0
- package/dist/core/web/public/next/assets/{guarded-runners-DPh2d32e.js.map → guarded-runners-YeAc9wLQ.js.map} +1 -1
- package/dist/core/web/public/next/assets/{icons-0haWy6nh.js → icons-D1sYbCTQ.js} +2 -2
- package/dist/core/web/public/next/assets/{icons-0haWy6nh.js.map → icons-D1sYbCTQ.js.map} +1 -1
- package/dist/core/web/public/next/assets/{index-QesfXBsb.js → index-DZ-UQ7v8.js} +3 -3
- package/dist/core/web/public/next/assets/{index-QesfXBsb.js.map → index-DZ-UQ7v8.js.map} +1 -1
- package/dist/core/web/public/next/assets/leader-focus-BjeJv7CW.js +2 -0
- package/dist/core/web/public/next/assets/leader-focus-BjeJv7CW.js.map +1 -0
- package/dist/core/web/public/next/assets/leader-thread-types-CDb-VPPO.js +2 -0
- package/dist/core/web/public/next/assets/leader-thread-types-CDb-VPPO.js.map +1 -0
- package/dist/core/web/public/next/assets/{live-model-De-q0ZFK.js → live-model-D_32RAOR.js} +2 -2
- package/dist/core/web/public/next/assets/live-model-D_32RAOR.js.map +1 -0
- package/dist/core/web/public/next/assets/{mind.module-BrK2nFOL.js → mind.module-7hA92hRg.js} +2 -2
- package/dist/core/web/public/next/assets/{mind.module-BrK2nFOL.js.map → mind.module-7hA92hRg.js.map} +1 -1
- package/dist/core/web/public/next/assets/mutations-DQPbFdRJ.js +2 -0
- package/dist/core/web/public/next/assets/{mutations-DTCtKCn_.js.map → mutations-DQPbFdRJ.js.map} +1 -1
- package/dist/core/web/public/next/assets/{nav-KmGX0CVz.js → nav-0dOYe2rJ.js} +2 -2
- package/dist/core/web/public/next/assets/{nav-KmGX0CVz.js.map → nav-0dOYe2rJ.js.map} +1 -1
- package/dist/core/web/public/next/assets/{overnight-queries-mlO0xzsr.js → overnight-queries-C4wOnFtE.js} +2 -2
- package/dist/core/web/public/next/assets/{overnight-queries-mlO0xzsr.js.map → overnight-queries-C4wOnFtE.js.map} +1 -1
- package/dist/core/web/public/next/assets/primitives-2A5Eq8FQ.js +1 -0
- package/dist/core/web/public/next/assets/{queries-CyBE8nsW.js → queries-BTXWdZzU.js} +2 -2
- package/dist/core/web/public/next/assets/{queries-CyBE8nsW.js.map → queries-BTXWdZzU.js.map} +1 -1
- package/dist/core/web/public/next/assets/{reasoning-pref-CaoA4_Vl.js → reasoning-pref-Ca6lN6x_.js} +2 -2
- package/dist/core/web/public/next/assets/{reasoning-pref-CaoA4_Vl.js.map → reasoning-pref-Ca6lN6x_.js.map} +1 -1
- package/dist/core/web/public/next/assets/{reveal-anchor-BP1VqnPU.js → reveal-anchor-DfNLgK_I.js} +2 -2
- package/dist/core/web/public/next/assets/{reveal-anchor-BP1VqnPU.js.map → reveal-anchor-DfNLgK_I.js.map} +1 -1
- package/dist/core/web/public/next/assets/seat-subscription-C5uPHBsN.js +2 -0
- package/dist/core/web/public/next/assets/{seat-subscription-C5Tvr0WQ.js.map → seat-subscription-C5uPHBsN.js.map} +1 -1
- package/dist/core/web/public/next/assets/{sidebar-model-B5Qk95k_.js → sidebar-model-CLCdqwkR.js} +2 -2
- package/dist/core/web/public/next/assets/{sidebar-model-B5Qk95k_.js.map → sidebar-model-CLCdqwkR.js.map} +1 -1
- package/dist/core/web/public/next/assets/{slots-DHKQDoE6.js → slots-C-qBeu-8.js} +3 -3
- package/dist/core/web/public/next/assets/{slots-DHKQDoE6.js.map → slots-C-qBeu-8.js.map} +1 -1
- package/dist/core/web/public/next/assets/{surface-data-CchClvHo.js → surface-data-Dj71tdD-.js} +2 -2
- package/dist/core/web/public/next/assets/{surface-data-CchClvHo.js.map → surface-data-Dj71tdD-.js.map} +1 -1
- package/dist/core/web/public/next/assets/surface-prefetch-B0WdJOJG.js +2 -0
- package/dist/core/web/public/next/assets/surface-prefetch-B0WdJOJG.js.map +1 -0
- package/dist/core/web/public/next/assets/{tasks-model-D6WFVULI.js → tasks-model-O8yR5YXp.js} +2 -2
- package/dist/core/web/public/next/assets/{tasks-model-D6WFVULI.js.map → tasks-model-O8yR5YXp.js.map} +1 -1
- package/dist/core/web/public/next/assets/{terminal-icons-BvCvuXk2.js → terminal-icons-CoRZq9iO.js} +2 -2
- package/dist/core/web/public/next/assets/{terminal-icons-BvCvuXk2.js.map → terminal-icons-CoRZq9iO.js.map} +1 -1
- package/dist/core/web/public/next/assets/thread-data-DQB_g66d.js +2 -0
- package/dist/core/web/public/next/assets/thread-data-DQB_g66d.js.map +1 -0
- package/dist/core/web/public/next/assets/types-IYKrYLku.js +2 -0
- package/dist/core/web/public/next/assets/types-IYKrYLku.js.map +1 -0
- package/dist/core/web/public/next/assets/{usage-queries-D1SU1t7r.js → usage-queries-AdjEyEtn.js} +2 -2
- package/dist/core/web/public/next/assets/{usage-queries-D1SU1t7r.js.map → usage-queries-AdjEyEtn.js.map} +1 -1
- package/dist/core/web/public/next/assets/{use-guarded-action-BZmBv7CJ.js → use-guarded-action-CuuMHeDX.js} +2 -2
- package/dist/core/web/public/next/assets/{use-guarded-action-BZmBv7CJ.js.map → use-guarded-action-CuuMHeDX.js.map} +1 -1
- package/dist/core/web/public/next/assets/{use-ticker-CkJ4-0wU.js → use-ticker-BBcp02SZ.js} +2 -2
- package/dist/core/web/public/next/assets/{use-ticker-CkJ4-0wU.js.map → use-ticker-BBcp02SZ.js.map} +1 -1
- package/dist/core/web/public/next/assets/{use-token-gate-CYcfk_GS.js → use-token-gate-CbXVYQYI.js} +2 -2
- package/dist/core/web/public/next/assets/{use-token-gate-CYcfk_GS.js.map → use-token-gate-CbXVYQYI.js.map} +1 -1
- package/dist/core/web/public/next/assets/{useOnboarding-CujRS6PK.js → useOnboarding-C_Fpe_sH.js} +2 -2
- package/dist/core/web/public/next/assets/{useOnboarding-CujRS6PK.js.map → useOnboarding-C_Fpe_sH.js.map} +1 -1
- package/dist/core/web/public/next/assets/{useSeatsRefresh-BEaQdkZs.js → useSeatsRefresh-Co1CnxLc.js} +2 -2
- package/dist/core/web/public/next/assets/{useSeatsRefresh-BEaQdkZs.js.map → useSeatsRefresh-Co1CnxLc.js.map} +1 -1
- package/dist/core/web/public/next/assets/{useVerseTranscript-BQ0J6Xhx.js → useVerseTranscript-DgbigUlV.js} +2 -2
- package/dist/core/web/public/next/assets/{useVerseTranscript-BQ0J6Xhx.js.map → useVerseTranscript-DgbigUlV.js.map} +1 -1
- package/dist/core/web/public/next/assets/{verse-icons-DndgEgKS.js → verse-icons-S56hIiXS.js} +2 -2
- package/dist/core/web/public/next/assets/{verse-icons-DndgEgKS.js.map → verse-icons-S56hIiXS.js.map} +1 -1
- package/dist/core/web/public/next/assets/verse-model-DAVevkNe.js +2 -0
- package/dist/core/web/public/next/assets/{verse-model-MMySehjj.js.map → verse-model-DAVevkNe.js.map} +1 -1
- package/dist/core/web/public/next/assets/{warmup-CNEH_-be.js → warmup-B_RAtNZ_.js} +3 -3
- package/dist/core/web/public/next/assets/{warmup-CNEH_-be.js.map → warmup-B_RAtNZ_.js.map} +1 -1
- package/dist/core/web/public/next/assets/why-seat-model-M_r0wX3b.js +2 -0
- package/dist/core/web/public/next/assets/why-seat-model-M_r0wX3b.js.map +1 -0
- package/dist/core/web/public/next/assets/{workspace-model-Dukb2wwv.js → workspace-model-dji5-Al3.js} +2 -2
- package/dist/core/web/public/next/assets/{workspace-model-Dukb2wwv.js.map → workspace-model-dji5-Al3.js.map} +1 -1
- package/dist/core/web/public/next/index.html +1 -1
- package/dist/core/web/read-projection-worker.js +2 -1
- package/dist/core/web/read-projection-worker.js.map +1 -1
- package/dist/core/web/read-projections.d.ts +3 -0
- package/dist/core/web/read-projections.js +2 -2
- package/dist/core/web/read-projections.js.map +1 -1
- package/dist/core/web/visibility.js +3 -4
- package/dist/core/web/visibility.js.map +1 -1
- package/dist/release-dependency-inventory.json +1 -1
- package/docs/AUTONOMY-GAP.md +1 -1
- package/package.json +3 -2
- package/dist/core/web/public/next/assets/Advanced-B-LqYXvd.js +0 -2
- package/dist/core/web/public/next/assets/App-E2zCDRxm.js +0 -7
- package/dist/core/web/public/next/assets/AppsSection-8wXPPhZb.js +0 -2
- package/dist/core/web/public/next/assets/AreaTrend-BFimExYn.js +0 -2
- package/dist/core/web/public/next/assets/AreaTrend-BFimExYn.js.map +0 -1
- package/dist/core/web/public/next/assets/AutonomyOffState-B5eEkmiz.js +0 -2
- package/dist/core/web/public/next/assets/AutonomyOffState-B5eEkmiz.js.map +0 -1
- package/dist/core/web/public/next/assets/AutonomyOffState-DmbE71vI.css +0 -1
- package/dist/core/web/public/next/assets/AutonomySection-B46igJKR.js +0 -2
- package/dist/core/web/public/next/assets/BudgetControl-T_RjPrw9.js +0 -2
- package/dist/core/web/public/next/assets/BudgetControl-T_RjPrw9.js.map +0 -1
- package/dist/core/web/public/next/assets/ChartFrame-BVuBe6RP.css +0 -1
- package/dist/core/web/public/next/assets/ChartFrame-SygZ7B3x.js +0 -2
- package/dist/core/web/public/next/assets/ChartFrame-SygZ7B3x.js.map +0 -1
- package/dist/core/web/public/next/assets/ChatSection-D6WybBpB.js +0 -3
- package/dist/core/web/public/next/assets/ChatSection-D6WybBpB.js.map +0 -1
- package/dist/core/web/public/next/assets/CommandPalette-B6SgDJH-.js +0 -2
- package/dist/core/web/public/next/assets/CommandPalette-B6SgDJH-.js.map +0 -1
- package/dist/core/web/public/next/assets/CommandSection-CvAVd1w4.css +0 -1
- package/dist/core/web/public/next/assets/CommandSection-DgMGoepd.js +0 -2
- package/dist/core/web/public/next/assets/CommandSection-DgMGoepd.js.map +0 -1
- package/dist/core/web/public/next/assets/DockHost-DW9q0NdR.js +0 -2
- package/dist/core/web/public/next/assets/FleetSection-BdZN6rGS.js +0 -3
- package/dist/core/web/public/next/assets/FleetSection-BdZN6rGS.js.map +0 -1
- package/dist/core/web/public/next/assets/Funnel-BeNQZWv2.js +0 -2
- package/dist/core/web/public/next/assets/Funnel-BeNQZWv2.js.map +0 -1
- package/dist/core/web/public/next/assets/GrowthSection-iiQg6wvK.js +0 -2
- package/dist/core/web/public/next/assets/LeaderCard-BRr3_yE-.js +0 -2
- package/dist/core/web/public/next/assets/LeaderCard-BRr3_yE-.js.map +0 -1
- package/dist/core/web/public/next/assets/MatrixHeatmap-DZvZeWXL.js +0 -2
- package/dist/core/web/public/next/assets/MatrixHeatmap-DZvZeWXL.js.map +0 -1
- package/dist/core/web/public/next/assets/McpSection-BRuCcTjT.js +0 -2
- package/dist/core/web/public/next/assets/MindSection-BTIiQS8P.js +0 -2
- package/dist/core/web/public/next/assets/MindSection-BTIiQS8P.js.map +0 -1
- package/dist/core/web/public/next/assets/NeedsYouDrawer-lYN4G_aM.js +0 -2
- package/dist/core/web/public/next/assets/NeedsYouDrawer-lYN4G_aM.js.map +0 -1
- package/dist/core/web/public/next/assets/NeedsYouList-BgH8ezqf.js +0 -2
- package/dist/core/web/public/next/assets/NeedsYouList-CruOt4JT.css +0 -1
- package/dist/core/web/public/next/assets/NewChatDialog-Db5Xjrk8.js +0 -3
- package/dist/core/web/public/next/assets/OnboardingFlow-LoY-zuSe.js +0 -2
- package/dist/core/web/public/next/assets/OnboardingFlow-LoY-zuSe.js.map +0 -1
- package/dist/core/web/public/next/assets/OnboardingGate-C4VXn7BJ.js +0 -3
- package/dist/core/web/public/next/assets/PreviewPane-BPBbcROb.js +0 -2
- package/dist/core/web/public/next/assets/ResourcePoolConsoleApp-BX2nkdmZ.js +0 -7
- package/dist/core/web/public/next/assets/ResourcesChrome-B2U2lO1H.js +0 -3
- package/dist/core/web/public/next/assets/ResourcesChrome-B2U2lO1H.js.map +0 -1
- package/dist/core/web/public/next/assets/ResourcesDrawer-BCFJ2Uq8.css +0 -1
- package/dist/core/web/public/next/assets/ResourcesDrawer-DlUKOjl9.js +0 -2
- package/dist/core/web/public/next/assets/ResourcesDrawer-DlUKOjl9.js.map +0 -1
- package/dist/core/web/public/next/assets/SettingsSection-mI9wQYZg.js.map +0 -1
- package/dist/core/web/public/next/assets/Sidebar-DT7ykaqS.js +0 -3
- package/dist/core/web/public/next/assets/Sidebar-DT7ykaqS.js.map +0 -1
- package/dist/core/web/public/next/assets/StatTile-BuLMsJMx.css +0 -1
- package/dist/core/web/public/next/assets/StatTile-DPZ5yyaU.js +0 -2
- package/dist/core/web/public/next/assets/StatTile-DPZ5yyaU.js.map +0 -1
- package/dist/core/web/public/next/assets/StepBand-DQykLkk_.js +0 -2
- package/dist/core/web/public/next/assets/StepBand-DQykLkk_.js.map +0 -1
- package/dist/core/web/public/next/assets/Swimlane-BzH6OCee.js +0 -2
- package/dist/core/web/public/next/assets/Swimlane-BzH6OCee.js.map +0 -1
- package/dist/core/web/public/next/assets/Swimlane-o6_Zsrnr.css +0 -1
- package/dist/core/web/public/next/assets/UniverseConsoleApp-DtXFhAhw.js +0 -2
- package/dist/core/web/public/next/assets/UsageSection-CiD1DbuF.js +0 -3
- package/dist/core/web/public/next/assets/UsageSection-CiD1DbuF.js.map +0 -1
- package/dist/core/web/public/next/assets/VerseConsoleApp-BCY5uSok.js +0 -3
- package/dist/core/web/public/next/assets/VerseConsoleApp-BCY5uSok.js.map +0 -1
- package/dist/core/web/public/next/assets/Workspace-BQSZFSeO.js +0 -10
- package/dist/core/web/public/next/assets/actions-BmIf9avl.css +0 -1
- package/dist/core/web/public/next/assets/actions-Ci6I4C0l.js +0 -2
- package/dist/core/web/public/next/assets/budget-queries-DsAjH8ac.js +0 -2
- package/dist/core/web/public/next/assets/charts-C_xJbJiw.js +0 -2
- package/dist/core/web/public/next/assets/charts-C_xJbJiw.js.map +0 -1
- package/dist/core/web/public/next/assets/charts-DhKTrQLw.css +0 -1
- package/dist/core/web/public/next/assets/cloud-model-CX407gPY.js +0 -2
- package/dist/core/web/public/next/assets/command-catalog-CF0cQQac.js.map +0 -1
- package/dist/core/web/public/next/assets/context-queries-CktFQqSS.js +0 -2
- package/dist/core/web/public/next/assets/copy-setup-CqRSfELB.js +0 -2
- package/dist/core/web/public/next/assets/copy-setup-CqRSfELB.js.map +0 -1
- package/dist/core/web/public/next/assets/format-Dk4Fmxax.js +0 -2
- package/dist/core/web/public/next/assets/format-Dk4Fmxax.js.map +0 -1
- package/dist/core/web/public/next/assets/global-CNNGMril.js +0 -2
- package/dist/core/web/public/next/assets/guarded-runners-DPh2d32e.js +0 -2
- package/dist/core/web/public/next/assets/live-model-De-q0ZFK.js.map +0 -1
- package/dist/core/web/public/next/assets/mutations-DTCtKCn_.js +0 -2
- package/dist/core/web/public/next/assets/primitives-D0Fb3TtS.js +0 -1
- package/dist/core/web/public/next/assets/seat-subscription-C5Tvr0WQ.js +0 -2
- package/dist/core/web/public/next/assets/surface-prefetch-BWr0LBQn.js +0 -2
- package/dist/core/web/public/next/assets/surface-prefetch-BWr0LBQn.js.map +0 -1
- package/dist/core/web/public/next/assets/types-D5XOdyw8.js +0 -2
- package/dist/core/web/public/next/assets/types-D5XOdyw8.js.map +0 -1
- package/dist/core/web/public/next/assets/verse-model-MMySehjj.js +0 -2
- package/dist/core/web/public/next/assets/why-seat-model-jlDTwYSc.js +0 -2
- package/dist/core/web/public/next/assets/why-seat-model-jlDTwYSc.js.map +0 -1
package/dist/cli/authority.js
CHANGED
|
@@ -15,13 +15,16 @@
|
|
|
15
15
|
* github-app [--org <login>] [--yes] GitHub App manifest flow; the key goes straight to custody
|
|
16
16
|
* rotate-provenance [--yes] replace the provenance HMAC key agents could read
|
|
17
17
|
* setup [--dry-run [--json]] [--yes] [--source <ashlr-hub checkout>] the guided Phase-0 command
|
|
18
|
+
* resident start|stop|status [--json] the resident daemon service under the grant (docs/RESIDENT-RUNTIME.md)
|
|
18
19
|
*
|
|
19
20
|
* LOWERING never asks anything. Everything that raises authority or touches
|
|
20
21
|
* GitHub asks first (or needs --yes) and prints exactly what it did. Nothing
|
|
21
|
-
* here ever runs `sudo
|
|
22
|
+
* here ever runs `sudo` or prints a secret. Only `resident start` / `resident
|
|
23
|
+
* stop` change launchd, only for ai.ashlr.daemon, and `start` only after the
|
|
24
|
+
* operator confirms at a terminal under an active standing grant (no --yes).
|
|
22
25
|
*/
|
|
23
|
-
import { spawnSync } from 'node:child_process';
|
|
24
|
-
import { createPublicKey, randomBytes } from 'node:crypto';
|
|
26
|
+
import { execFile, spawnSync } from 'node:child_process';
|
|
27
|
+
import { createHash, createPublicKey, randomBytes } from 'node:crypto';
|
|
25
28
|
import { existsSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
26
29
|
import { createServer } from 'node:http';
|
|
27
30
|
import { homedir, tmpdir } from 'node:os';
|
|
@@ -30,9 +33,10 @@ import { createInterface } from 'node:readline';
|
|
|
30
33
|
import { CUSTODY_HELPER_PATH } from '../core/authority/custody-client.js';
|
|
31
34
|
// Loaded lazily: `authority stop` and friends must never depend on the daemon
|
|
32
35
|
// service module being importable.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
+
// `cached` (the Verse probe) reuses a launchd read up to 15 s old.
|
|
37
|
+
async function realDaemonService(cached = false) {
|
|
38
|
+
const { serviceStatus, serviceStatusCached } = await import('../core/daemon/service.js');
|
|
39
|
+
const status = cached ? serviceStatusCached() : serviceStatus();
|
|
36
40
|
if (status.running)
|
|
37
41
|
return 'running';
|
|
38
42
|
if (status.registrationState === 'absent')
|
|
@@ -117,8 +121,118 @@ async function defaultDeps() {
|
|
|
117
121
|
fetch: globalThis.fetch,
|
|
118
122
|
custody: await import('../core/authority/custody-client.js'),
|
|
119
123
|
daemonService: realDaemonService,
|
|
124
|
+
resident: realResidentDeps,
|
|
120
125
|
};
|
|
121
126
|
}
|
|
127
|
+
async function residentServiceOptions() {
|
|
128
|
+
const { loadConfigReadOnlyStrict } = await import('../core/config.js');
|
|
129
|
+
const { daemonServiceInstallOptions } = await import('../core/daemon/service-config.js');
|
|
130
|
+
let cfg;
|
|
131
|
+
try {
|
|
132
|
+
cfg = loadConfigReadOnlyStrict();
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
return { ok: false, problem: `config.json could not be read (${error.message.slice(0, 160)})` };
|
|
136
|
+
}
|
|
137
|
+
// ALWAYS regenerated from config (daemon.dailyBudgetUsd / intervalMs /
|
|
138
|
+
// parallel) — never carried over from an earlier install, so a budget
|
|
139
|
+
// changed in config reaches the service on the next `resident start`.
|
|
140
|
+
const opts = daemonServiceInstallOptions(cfg, { autostart: true });
|
|
141
|
+
return { ok: true, opts, budgetUsd: opts.budget ?? null };
|
|
142
|
+
}
|
|
143
|
+
/** The installed service file: its text, null when absent, undefined when unreadable. */
|
|
144
|
+
function readServiceFile(path) {
|
|
145
|
+
try {
|
|
146
|
+
return readFileSync(path, 'utf8');
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
return error.code === 'ENOENT' ? null : undefined;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const realResidentDeps = {
|
|
153
|
+
admission: async () => (await import('../core/authority/resident.js')).observeResidentAdmission().admission,
|
|
154
|
+
observe: async () => {
|
|
155
|
+
const { residentPlistState, plistBudgetUsd } = await import('../core/authority/resident.js');
|
|
156
|
+
const { generateServiceDefinition } = await import('../core/daemon/service.js');
|
|
157
|
+
let state;
|
|
158
|
+
try {
|
|
159
|
+
state = await realDaemonService();
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
state = 'unknown';
|
|
163
|
+
}
|
|
164
|
+
const prepared = await residentServiceOptions();
|
|
165
|
+
const def = generateServiceDefinition(prepared.ok ? prepared.opts : {});
|
|
166
|
+
const installed = readServiceFile(def.filePath);
|
|
167
|
+
return {
|
|
168
|
+
state,
|
|
169
|
+
plist: residentPlistState(installed, prepared.ok ? def.content : null),
|
|
170
|
+
plistPath: def.filePath,
|
|
171
|
+
installedBudgetUsd: plistBudgetUsd(installed ?? null),
|
|
172
|
+
expectedBudgetUsd: prepared.ok ? prepared.budgetUsd : null,
|
|
173
|
+
problem: prepared.ok ? null : prepared.problem,
|
|
174
|
+
};
|
|
175
|
+
},
|
|
176
|
+
operatorRefusal: async () => {
|
|
177
|
+
const { operatorContextRefusal, currentOperatorContext } = await import('../core/authority/resident.js');
|
|
178
|
+
return operatorContextRefusal(currentOperatorContext());
|
|
179
|
+
},
|
|
180
|
+
start: async () => {
|
|
181
|
+
const resident = await import('../core/authority/resident.js');
|
|
182
|
+
const service = await import('../core/daemon/service.js');
|
|
183
|
+
const { appendLedger } = await import('../core/authority/ledger.js');
|
|
184
|
+
const prepared = await residentServiceOptions();
|
|
185
|
+
if (!prepared.ok)
|
|
186
|
+
return { ok: false, reason: prepared.problem };
|
|
187
|
+
// The mint re-observes the operator context and re-verifies the grant,
|
|
188
|
+
// the build identity and the authority surface itself: nothing this
|
|
189
|
+
// command computed or printed earlier counts toward the decision.
|
|
190
|
+
const minted = resident.mintResidentServiceCapability();
|
|
191
|
+
if (!minted.ok)
|
|
192
|
+
return { ok: false, reason: minted.reason };
|
|
193
|
+
const def = service.generateServiceDefinition(prepared.opts);
|
|
194
|
+
const digest = createHash('sha256').update(def.content, 'utf8').digest('hex');
|
|
195
|
+
const grantId = minted.capability.grantId;
|
|
196
|
+
const detail = `release ${minted.admission.revision?.slice(0, 12) ?? '?'} · plist sha256 ${digest.slice(0, 16)} · `
|
|
197
|
+
+ `budget $${prepared.budgetUsd ?? '?'}/day · interval ${prepared.opts.intervalMs ?? '?'} ms · parallel ${prepared.opts.parallel ?? '?'}`;
|
|
198
|
+
// Fail closed: a start the authority ledger cannot record does not happen.
|
|
199
|
+
const recorded = appendLedger({ kind: 'note', actor: 'mason', grantId, repo: null, data: { topic: 'resident-service:start', detail } });
|
|
200
|
+
if (!recorded.ok)
|
|
201
|
+
return { ok: false, reason: `the ledger refused the start record: ${recorded.reason}` };
|
|
202
|
+
try {
|
|
203
|
+
await service.installResidentService(prepared.opts, minted.capability);
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
appendLedger({ kind: 'note', actor: 'mason', grantId, repo: null, data: { topic: 'resident-service:start-failed', detail: error.message.slice(0, 300) } });
|
|
207
|
+
return { ok: false, reason: error.message };
|
|
208
|
+
}
|
|
209
|
+
const status = service.serviceStatus(prepared.opts);
|
|
210
|
+
const grant = `grant #${minted.admission.grantSeq ?? '?'}`;
|
|
211
|
+
return {
|
|
212
|
+
ok: true,
|
|
213
|
+
detail: status.running
|
|
214
|
+
? `ai.ashlr.daemon is running under ${grant} (${detail})`
|
|
215
|
+
: `ai.ashlr.daemon is installed and loaded under ${grant}, but launchd has not reported it running yet — check \`${resident.RESIDENT_STATUS_COMMAND}\` (${detail})`,
|
|
216
|
+
};
|
|
217
|
+
},
|
|
218
|
+
stop: async () => {
|
|
219
|
+
const service = await import('../core/daemon/service.js');
|
|
220
|
+
try {
|
|
221
|
+
await service.uninstall({});
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
return { ok: false, reason: error.message };
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
const { appendLedger } = await import('../core/authority/ledger.js');
|
|
228
|
+
appendLedger({ kind: 'note', actor: 'mason', grantId: null, repo: null, data: { topic: 'resident-service:stop', detail: 'ai.ashlr.daemon booted out and its plist removed' } });
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
// Lowering never depends on the ledger.
|
|
232
|
+
}
|
|
233
|
+
return { ok: true, detail: 'ai.ashlr.daemon is stopped and its plist removed' };
|
|
234
|
+
},
|
|
235
|
+
};
|
|
122
236
|
const VALUE_FLAGS = new Set(['--reason', '--payload', '--switch', '--limit', '--kind', '--repo', '--org', '--source']);
|
|
123
237
|
function parseArgs(args) {
|
|
124
238
|
const out = { positional: [], flags: new Set(), values: new Map() };
|
|
@@ -163,6 +277,9 @@ const USAGE = `Usage: ashlr authority <command>
|
|
|
163
277
|
rotate-provenance [--yes] Replace the provenance HMAC key
|
|
164
278
|
setup [--dry-run [--json]] [--yes] [--source <checkout>]
|
|
165
279
|
Guided Phase 0: custody, trust root, App, token, canary, rulesets, first grant, daemon
|
|
280
|
+
resident start Install / restart the resident daemon under the active grant (you confirm here)
|
|
281
|
+
resident stop Boot the resident daemon out and remove its plist (Stop is separate)
|
|
282
|
+
resident status [--json] Resident admission, service state and plist drift
|
|
166
283
|
|
|
167
284
|
Lowering authority never asks. Anything that raises it asks first (or takes --yes).`;
|
|
168
285
|
// ---------------------------------------------------------------------------
|
|
@@ -211,6 +328,8 @@ export async function runAuthorityCli(args, injected) {
|
|
|
211
328
|
return await cmdRotateProvenance(parsed, deps);
|
|
212
329
|
case 'setup':
|
|
213
330
|
return await cmdSetup(parsed, deps);
|
|
331
|
+
case 'resident':
|
|
332
|
+
return await cmdResident(rest[0], parsed, deps);
|
|
214
333
|
default:
|
|
215
334
|
deps.err(`Unknown authority command: ${command}`);
|
|
216
335
|
deps.out(USAGE);
|
|
@@ -249,12 +368,48 @@ async function cmdStatus(parsed, deps) {
|
|
|
249
368
|
const merging = status.policy.repos.filter((repo) => repo.stage === 'merge').map((repo) => repo.nameWithOwner);
|
|
250
369
|
deps.out(`Policy: ${status.policy.repos.length} repos (${merging.length} merging), engines ${status.policy.engines.join(', ')}, caps ${status.policy.merge.maxFiles}/${status.policy.merge.maxLines}`);
|
|
251
370
|
}
|
|
371
|
+
for (const line of await enforcementStatusLines(deps))
|
|
372
|
+
deps.out(line);
|
|
252
373
|
deps.out(`Ledger: ${status.ledger.state}${status.ledger.head ? `, ${status.ledger.head.seq + 1} entries, head ${status.ledger.head.hash.slice(0, 12)}` : ''}${status.ledger.reason ? ` — ${status.ledger.reason}` : ''}`);
|
|
253
374
|
const c = status.custody;
|
|
254
375
|
const yn = (v) => (v === null ? 'unknown' : v ? 'yes' : 'no');
|
|
255
376
|
deps.out(`Custody: installed ${yn(c.installed)}, key ${yn(c.keyInitialized)}, GitHub App ${yn(c.githubApp)}, Claude token ${yn(c.claudeToken)}`);
|
|
256
377
|
return 0;
|
|
257
378
|
}
|
|
379
|
+
/**
|
|
380
|
+
* `status`: does GitHub still enforce what the installed grant says? Only the
|
|
381
|
+
* grant's `server` repos are read (local ones need nothing from GitHub). A
|
|
382
|
+
* mismatch is REPORTED, never fixed here — the signed grant is only ever
|
|
383
|
+
* changed by a re-approval, which switches such a repo to local enforcement.
|
|
384
|
+
*/
|
|
385
|
+
async function enforcementStatusLines(deps) {
|
|
386
|
+
const { readInstalledGrant } = await import('../core/authority/standing-grant.js');
|
|
387
|
+
const installed = readInstalledGrant();
|
|
388
|
+
if (installed.state !== 'ok')
|
|
389
|
+
return [];
|
|
390
|
+
const grant = installed.envelope.payload;
|
|
391
|
+
const server = grant.repos.filter((repo) => repo.enforcement === 'server');
|
|
392
|
+
if (server.length === 0)
|
|
393
|
+
return [];
|
|
394
|
+
const { enforcementMismatch, probeServerEnforcementAll } = await import('../core/authority/server-enforcement.js');
|
|
395
|
+
const probes = await probeServerEnforcementAll(server.map((repo) => repo.nameWithOwner), ghGet(deps));
|
|
396
|
+
const lines = [];
|
|
397
|
+
const unread = [];
|
|
398
|
+
let enforced = 0;
|
|
399
|
+
for (const repo of server) {
|
|
400
|
+
const probe = probes.get(repo.nameWithOwner.toLowerCase());
|
|
401
|
+
const mismatch = enforcementMismatch(repo, probe, grant.grantSeq);
|
|
402
|
+
if (mismatch)
|
|
403
|
+
lines.push(` ${mismatch}`);
|
|
404
|
+
else if (probe?.state === 'enforced')
|
|
405
|
+
enforced += 1;
|
|
406
|
+
else
|
|
407
|
+
unread.push(repo.nameWithOwner);
|
|
408
|
+
}
|
|
409
|
+
const head = `Enforcement: ${enforced}/${server.length} server-enforced repo(s) protected by GitHub` +
|
|
410
|
+
`${unread.length > 0 ? `; could not check ${shortList(unread)}` : ''}${lines.length > 0 ? ` — ${lines.length} mismatch${lines.length === 1 ? '' : 'es'}:` : ''}`;
|
|
411
|
+
return [head, ...lines];
|
|
412
|
+
}
|
|
258
413
|
const SWITCHES = ['off', 'propose', 'autonomous'];
|
|
259
414
|
async function cmdSwitch(to, deps) {
|
|
260
415
|
if (!to || !SWITCHES.includes(to)) {
|
|
@@ -334,7 +489,7 @@ async function cmdRevoke(parsed, deps) {
|
|
|
334
489
|
async function cmdDraft(parsed, deps) {
|
|
335
490
|
const { buildStandingGrantDraft } = await import('../core/verse/authority-api.js');
|
|
336
491
|
const kind = parsed.flags.has('--new') ? 'new' : parsed.flags.has('--reapprove') ? 'reapprove' : 'auto';
|
|
337
|
-
const draft = await buildStandingGrantDraft(kind);
|
|
492
|
+
const draft = await buildStandingGrantDraft(kind, Date.now(), { githubGet: ghGet(deps) });
|
|
338
493
|
if (parsed.flags.has('--json')) {
|
|
339
494
|
deps.out(JSON.stringify({ kind: draft.kind, digest: draft.digest, payload: draft.payload }, null, 2));
|
|
340
495
|
return 0;
|
|
@@ -382,7 +537,7 @@ async function cmdGrant(parsed, deps, kind) {
|
|
|
382
537
|
}
|
|
383
538
|
else {
|
|
384
539
|
const { buildStandingGrantDraft } = await import('../core/verse/authority-api.js');
|
|
385
|
-
payload = (await buildStandingGrantDraft(kind)).payload;
|
|
540
|
+
payload = (await buildStandingGrantDraft(kind, Date.now(), { githubGet: ghGet(deps) })).payload;
|
|
386
541
|
}
|
|
387
542
|
deps.out(kind === 'new' ? 'You are about to sign this standing grant:' : 'You are about to re-approve (continue) this standing grant:');
|
|
388
543
|
for (const line of describeGrantScope(payload))
|
|
@@ -520,68 +675,266 @@ export function buildFleetRuleset(requiredChecks) {
|
|
|
520
675
|
],
|
|
521
676
|
};
|
|
522
677
|
}
|
|
523
|
-
|
|
524
|
-
|
|
678
|
+
/** A `gh api <path>` GET through the CLI's runner (tests inject it; the Verse probe's runner is read-only). */
|
|
679
|
+
function ghGet(deps) {
|
|
680
|
+
return async (path) => deps.run('gh', ['api', path]);
|
|
681
|
+
}
|
|
682
|
+
async function ghJson(deps, args) {
|
|
683
|
+
const result = await deps.run('gh', args);
|
|
525
684
|
if (result.status !== 0)
|
|
526
685
|
throw new Error(`gh ${args.slice(0, 3).join(' ')} failed: ${result.stderr.trim().slice(0, 200)}`);
|
|
527
686
|
return JSON.parse(result.stdout || 'null');
|
|
528
687
|
}
|
|
529
688
|
/** Check-run names on the default branch head: what CI actually reports today. */
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
689
|
+
/**
|
|
690
|
+
* 3.13: with `verifyAppId` (the repo is in the grant, the fleet can verify it
|
|
691
|
+
* and the ashlr-fleet App's id is known) the host-verified `ashlr/verify` is
|
|
692
|
+
* always required, pinned to the App — even when the default branch has no
|
|
693
|
+
* runs at all (Actions off), which is exactly the repo it exists for. A
|
|
694
|
+
* same-named run from another App never replaces the pin.
|
|
695
|
+
*/
|
|
696
|
+
async function discoverChecks(deps, repo, branch, verifyAppId = null) {
|
|
697
|
+
const runs = await ghJson(deps, ['api', `repos/${repo}/commits/${encodeURIComponent(branch)}/check-runs?per_page=100`]);
|
|
534
698
|
const found = [];
|
|
535
699
|
for (const run of runs.check_runs ?? []) {
|
|
536
700
|
if (typeof run.name !== 'string' || run.name.length === 0)
|
|
537
701
|
continue;
|
|
702
|
+
if (verifyAppId !== null && run.name === ASHLR_VERIFY_CHECK)
|
|
703
|
+
continue;
|
|
538
704
|
const appId = run.app?.id;
|
|
539
705
|
found.push({ context: run.name, integrationId: typeof appId === 'number' && Number.isSafeInteger(appId) && appId > 0 ? appId : null });
|
|
540
706
|
}
|
|
707
|
+
if (verifyAppId !== null)
|
|
708
|
+
found.push({ context: ASHLR_VERIFY_CHECK, integrationId: verifyAppId });
|
|
541
709
|
return normalizeRequiredChecks(found);
|
|
542
710
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
711
|
+
/** The check the ashlr-fleet App posts on fleet PR heads (core/fleet/verify-check-run.ts ASHLR_VERIFY_CHECK_NAME). */
|
|
712
|
+
export const ASHLR_VERIFY_CHECK = 'ashlr/verify';
|
|
713
|
+
/** The ashlr-fleet App as GitHub describes it to your own gh auth (a `gh api` read); null = unreadable. */
|
|
714
|
+
export async function readFleetApp(deps) {
|
|
715
|
+
try {
|
|
716
|
+
const app = await ghJson(deps, ['api', `apps/${FLEET_APP_NAME}`]);
|
|
717
|
+
if (!app || typeof app.id !== 'number' || !Number.isSafeInteger(app.id) || app.id <= 0)
|
|
718
|
+
return null;
|
|
719
|
+
const slug = typeof app.slug === 'string' && /^[a-z0-9-]{1,100}$/.test(app.slug) ? app.slug : FLEET_APP_NAME;
|
|
720
|
+
const ownerLogin = typeof app.owner?.login === 'string' && /^[A-Za-z0-9-]{1,39}$/.test(app.owner.login) ? app.owner.login : '';
|
|
721
|
+
const checks = typeof app.permissions?.['checks'] === 'string' ? app.permissions['checks'] : null;
|
|
722
|
+
return { id: app.id, slug, ownerLogin, ownerIsOrg: app.owner?.type === 'Organization', checks };
|
|
723
|
+
}
|
|
724
|
+
catch {
|
|
725
|
+
return null;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
/** PURE: where the App's permissions (and then each installation's acceptance) are changed. */
|
|
729
|
+
export function fleetAppPermissionUrls(app) {
|
|
730
|
+
const base = app.ownerIsOrg && app.ownerLogin
|
|
731
|
+
? `https://github.com/organizations/${app.ownerLogin}/settings`
|
|
732
|
+
: 'https://github.com/settings';
|
|
733
|
+
return { permissions: `${base}/apps/${app.slug}/permissions`, installations: `${base}/installations` };
|
|
734
|
+
}
|
|
735
|
+
/** True when the fleet mirror of `repo` exists and the fleet detects at least one required verify command there. */
|
|
736
|
+
async function fleetCanVerify(repo) {
|
|
737
|
+
try {
|
|
738
|
+
const { mirrorPathFor } = await import('../core/fleet/mirrors.js');
|
|
739
|
+
const { detectVerifyCommands } = await import('../core/run/verify-commands.js');
|
|
740
|
+
const mirror = mirrorPathFor(repo);
|
|
741
|
+
if (!existsSync(mirror))
|
|
742
|
+
return false;
|
|
743
|
+
return detectVerifyCommands(mirror, 'merge').some((command) => command.required !== false);
|
|
744
|
+
}
|
|
745
|
+
catch {
|
|
746
|
+
return false;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* PURE: `actual` (a ruleset as GitHub returns it) already says everything
|
|
751
|
+
* `desired` says. GitHub adds ids, links, timestamps and server-side rule
|
|
752
|
+
* defaults to what it returns, so extra object keys are ignored — but every
|
|
753
|
+
* field we set must match, and arrays must match one-to-one in any order, so
|
|
754
|
+
* an extra rule, an extra bypass actor or an extra required check is drift
|
|
755
|
+
* (re-applying would remove it). Anything unreadable is "not up to date".
|
|
756
|
+
*/
|
|
757
|
+
export function rulesetMatches(desired, actual) {
|
|
758
|
+
if (Array.isArray(desired)) {
|
|
759
|
+
if (!Array.isArray(actual) || actual.length !== desired.length)
|
|
760
|
+
return false;
|
|
761
|
+
const unused = [...actual];
|
|
762
|
+
for (const wanted of desired) {
|
|
763
|
+
const at = unused.findIndex((candidate) => rulesetMatches(wanted, candidate));
|
|
764
|
+
if (at < 0)
|
|
765
|
+
return false;
|
|
766
|
+
unused.splice(at, 1);
|
|
767
|
+
}
|
|
768
|
+
return true;
|
|
769
|
+
}
|
|
770
|
+
if (desired !== null && typeof desired === 'object') {
|
|
771
|
+
if (actual === null || typeof actual !== 'object' || Array.isArray(actual))
|
|
772
|
+
return false;
|
|
773
|
+
const have = actual;
|
|
774
|
+
return Object.entries(desired).every(([key, value]) => rulesetMatches(value, have[key]));
|
|
775
|
+
}
|
|
776
|
+
return Object.is(desired, actual);
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* The repos the fleet works in: the installed grant's, or — before the first
|
|
780
|
+
* grant — the ones it would name (every enrolled checkout's GitHub repo plus
|
|
781
|
+
* the canary; authority-api draftRepos), whose enforcement is not known yet
|
|
782
|
+
* (null): the draft names `server` only where GitHub then enforces required
|
|
783
|
+
* checks (authority/server-enforcement.ts). Read-only: no draft is built and the ledger is not
|
|
784
|
+
* touched, so a dry run (and Verse's checklist) can ask it freely.
|
|
785
|
+
*/
|
|
786
|
+
async function setupTargetRepos() {
|
|
787
|
+
const { readInstalledGrant, FLEET_CANARY_REPO } = await import('../core/authority/standing-grant.js');
|
|
788
|
+
const installed = readInstalledGrant();
|
|
789
|
+
if (installed.state === 'ok') {
|
|
790
|
+
return installed.envelope.payload.repos.map((repo) => ({ nameWithOwner: repo.nameWithOwner, enforcement: repo.enforcement }));
|
|
791
|
+
}
|
|
792
|
+
const byKey = new Map();
|
|
793
|
+
try {
|
|
794
|
+
const { readEnrollmentRegistry } = await import('../core/sandbox/policy.js');
|
|
795
|
+
const { repoIdentityOfPath } = await import('../core/fleet/repo-identity.js');
|
|
796
|
+
const registry = readEnrollmentRegistry();
|
|
797
|
+
if (registry.state === 'ready') {
|
|
798
|
+
for (const path of registry.repos) {
|
|
799
|
+
const nameWithOwner = repoIdentityOfPath(path);
|
|
800
|
+
if (nameWithOwner && !byKey.has(nameWithOwner.toLowerCase()))
|
|
801
|
+
byKey.set(nameWithOwner.toLowerCase(), { nameWithOwner, enforcement: null });
|
|
802
|
+
}
|
|
555
803
|
}
|
|
556
|
-
repos = grantRepos.map((r) => r.nameWithOwner);
|
|
557
|
-
enforcement = new Map(grantRepos.map((r) => [r.nameWithOwner, r.enforcement]));
|
|
558
804
|
}
|
|
805
|
+
catch {
|
|
806
|
+
// An unreadable registry leaves the canary alone on the list.
|
|
807
|
+
}
|
|
808
|
+
if (!byKey.has(FLEET_CANARY_REPO))
|
|
809
|
+
byKey.set(FLEET_CANARY_REPO, { nameWithOwner: FLEET_CANARY_REPO, enforcement: null });
|
|
810
|
+
return [...byKey.values()];
|
|
811
|
+
}
|
|
812
|
+
/** Read-only: what `protect --apply` would do for each repo (every gh call here is a GET). */
|
|
813
|
+
async function protectPlans(parsed, deps, given) {
|
|
814
|
+
const explicit = parsed.values.get('--repo') ?? [];
|
|
815
|
+
const byGrant = given !== undefined || explicit.length === 0;
|
|
816
|
+
const targets = given
|
|
817
|
+
?? (explicit.length > 0 ? explicit.map((nameWithOwner) => ({ nameWithOwner, enforcement: null })) : await setupTargetRepos());
|
|
818
|
+
// Repos the grant covers (installed grant, or the would-be grant's own list):
|
|
819
|
+
// only those get the fleet's ashlr/verify requirement.
|
|
820
|
+
const grantMembers = new Set();
|
|
821
|
+
const { readInstalledGrant } = await import('../core/authority/standing-grant.js');
|
|
822
|
+
const installed = readInstalledGrant();
|
|
823
|
+
if (installed.state === 'ok')
|
|
824
|
+
for (const r of installed.envelope.payload.repos)
|
|
825
|
+
grantMembers.add(r.nameWithOwner.toLowerCase());
|
|
826
|
+
if (byGrant)
|
|
827
|
+
for (const r of targets)
|
|
828
|
+
grantMembers.add(r.nameWithOwner.toLowerCase());
|
|
829
|
+
const grantEnforcement = new Map();
|
|
830
|
+
if (installed.state === 'ok')
|
|
831
|
+
for (const r of installed.envelope.payload.repos)
|
|
832
|
+
grantEnforcement.set(r.nameWithOwner.toLowerCase(), r.enforcement);
|
|
833
|
+
const grantSeq = installed.state === 'ok' ? installed.envelope.payload.grantSeq : null;
|
|
834
|
+
const { classifyGithubReadFailure, describeGithubReadFailure, enforcementMismatch } = await import('../core/authority/server-enforcement.js');
|
|
835
|
+
const unreadable = (repo, error) => {
|
|
836
|
+
const message = error.message;
|
|
837
|
+
return { repo, skipped: `could not read it from GitHub (${describeGithubReadFailure(classifyGithubReadFailure({ message }))}): ${message}`, checks: [], ruleset: null, existingId: null, upToDate: false };
|
|
838
|
+
};
|
|
839
|
+
let fleetApp;
|
|
559
840
|
const plans = [];
|
|
560
|
-
for (const repo of
|
|
561
|
-
if (enforcement
|
|
562
|
-
plans.push({ repo, skipped:
|
|
841
|
+
for (const { nameWithOwner: repo, enforcement } of targets) {
|
|
842
|
+
if (enforcement === 'local') {
|
|
843
|
+
plans.push({ repo, skipped: `local enforcement in the grant — GitHub protects nothing here; the daemon enforces its gates and G7 requires the App's host-verified ${ASHLR_VERIFY_CHECK}`, checks: [], ruleset: null, existingId: null, upToDate: false });
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
let info;
|
|
847
|
+
try {
|
|
848
|
+
info = await ghJson(deps, ['api', `repos/${repo}`]);
|
|
849
|
+
}
|
|
850
|
+
catch (error) {
|
|
851
|
+
plans.push(unreadable(repo, error));
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
// Rulesets before anything else: on GitHub Free a PRIVATE repo has neither
|
|
855
|
+
// rulesets nor classic branch protection (both answer 403 "Upgrade to
|
|
856
|
+
// GitHub Pro or make this repository public…", verified live), which is a
|
|
857
|
+
// plan limit — not a permission problem and not a network error — so it is
|
|
858
|
+
// reported as such, and only `local` enforcement can work there.
|
|
859
|
+
const listed = await deps.run('gh', ['api', `repos/${repo}/rulesets`]);
|
|
860
|
+
if (listed.status !== 0 && classifyGithubReadFailure({ message: listed.stderr }) === 'plan-unavailable') {
|
|
861
|
+
const kind = info?.private === false ? 'repo' : 'private repo';
|
|
862
|
+
const mismatch = grantEnforcement.get(repo.toLowerCase()) === 'server'
|
|
863
|
+
? enforcementMismatch({ nameWithOwner: repo, enforcement: 'server' }, { nameWithOwner: repo, state: 'unavailable', private: info?.private ?? null, failure: 'plan-unavailable', detail: '' }, grantSeq)
|
|
864
|
+
: null;
|
|
865
|
+
plans.push({
|
|
866
|
+
repo,
|
|
867
|
+
skipped: mismatch ?? `rulesets unavailable on this plan for ${kind} ${repo} — using local enforcement with ${ASHLR_VERIFY_CHECK}`,
|
|
868
|
+
checks: [],
|
|
869
|
+
ruleset: null,
|
|
870
|
+
existingId: null,
|
|
871
|
+
upToDate: false,
|
|
872
|
+
unavailable: true,
|
|
873
|
+
mismatch,
|
|
874
|
+
});
|
|
563
875
|
continue;
|
|
564
876
|
}
|
|
565
877
|
try {
|
|
566
|
-
|
|
567
|
-
|
|
878
|
+
if (listed.status !== 0)
|
|
879
|
+
throw new Error(`gh api repos/${repo}/rulesets failed: ${listed.stderr.trim().slice(0, 200)}`);
|
|
880
|
+
let verifyAppId = null;
|
|
881
|
+
let verifyNote = null;
|
|
882
|
+
if (grantMembers.has(repo.toLowerCase())) {
|
|
883
|
+
if (!(await fleetCanVerify(repo))) {
|
|
884
|
+
verifyNote = `${ASHLR_VERIFY_CHECK} not required: the fleet mirror has no verify command yet (rerun protect once it has synced)`;
|
|
885
|
+
}
|
|
886
|
+
else {
|
|
887
|
+
if (fleetApp === undefined)
|
|
888
|
+
fleetApp = await readFleetApp(deps);
|
|
889
|
+
if (fleetApp)
|
|
890
|
+
verifyAppId = fleetApp.id;
|
|
891
|
+
else
|
|
892
|
+
verifyNote = `${ASHLR_VERIFY_CHECK} not required: the ${FLEET_APP_NAME} App could not be read (\`gh api apps/${FLEET_APP_NAME}\`)`;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
const required = await discoverChecks(deps, repo, info?.default_branch ?? 'main', verifyAppId);
|
|
568
896
|
const checks = required.map((check) => check.context);
|
|
569
|
-
const existing =
|
|
897
|
+
const existing = JSON.parse(listed.stdout || 'null');
|
|
570
898
|
const match = Array.isArray(existing) ? existing.find((r) => r.name === FLEET_RULESET_NAME) : undefined;
|
|
899
|
+
const existingId = typeof match?.id === 'number' && Number.isSafeInteger(match.id) ? match.id : null;
|
|
900
|
+
const ruleset = buildFleetRuleset(required);
|
|
901
|
+
// Rerun-safe: a ruleset already on GitHub with this exact content is
|
|
902
|
+
// reported as in place instead of being PUT again.
|
|
903
|
+
const upToDate = existingId !== null && rulesetMatches(ruleset, await ghJson(deps, ['api', `repos/${repo}/rulesets/${existingId}`]));
|
|
571
904
|
plans.push({
|
|
572
905
|
repo,
|
|
573
|
-
skipped: info
|
|
906
|
+
skipped: info?.private === true && checks.length === 0 ? 'private repo with no CI checks — protect it by hand or keep it propose-only' : null,
|
|
574
907
|
checks,
|
|
575
|
-
ruleset
|
|
576
|
-
existingId
|
|
908
|
+
ruleset,
|
|
909
|
+
existingId,
|
|
910
|
+
upToDate,
|
|
911
|
+
verifyNote,
|
|
577
912
|
});
|
|
578
913
|
}
|
|
579
914
|
catch (error) {
|
|
580
|
-
plans.push(
|
|
915
|
+
plans.push(unreadable(repo, error));
|
|
581
916
|
}
|
|
582
917
|
}
|
|
583
918
|
return plans;
|
|
584
919
|
}
|
|
920
|
+
/** Create or update each plan's ruleset; the number that failed. */
|
|
921
|
+
async function applyProtectPlans(plans, deps) {
|
|
922
|
+
let failures = 0;
|
|
923
|
+
for (const plan of plans) {
|
|
924
|
+
const result = await deps.run('gh', [
|
|
925
|
+
'api', '--method', plan.existingId === null ? 'POST' : 'PUT',
|
|
926
|
+
`repos/${plan.repo}/rulesets${plan.existingId === null ? '' : `/${plan.existingId}`}`,
|
|
927
|
+
'--input', '-',
|
|
928
|
+
], { input: JSON.stringify(plan.ruleset) });
|
|
929
|
+
if (result.status === 0)
|
|
930
|
+
deps.out(`${plan.repo}: ruleset ${plan.existingId === null ? 'created' : 'updated'}.`);
|
|
931
|
+
else {
|
|
932
|
+
failures += 1;
|
|
933
|
+
deps.err(`${plan.repo}: failed — ${result.stderr.trim().slice(0, 300)}`);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
return failures;
|
|
937
|
+
}
|
|
585
938
|
async function cmdProtect(parsed, deps) {
|
|
586
939
|
const apply = parsed.flags.has('--apply');
|
|
587
940
|
if (!apply && !parsed.flags.has('--print')) {
|
|
@@ -594,7 +947,17 @@ async function cmdProtect(parsed, deps) {
|
|
|
594
947
|
deps.out(`${plan.repo}: skipped — ${plan.skipped}`);
|
|
595
948
|
continue;
|
|
596
949
|
}
|
|
597
|
-
|
|
950
|
+
const checks = plan.checks.length > 0 ? plan.checks.join(', ') : 'none found (the repo goes to the owner lane until it has CI)';
|
|
951
|
+
if (plan.upToDate) {
|
|
952
|
+
deps.out(`${plan.repo}: ruleset ${plan.existingId} already up to date; required checks: ${checks}`);
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
955
|
+
deps.out(`${plan.repo}: ${plan.existingId === null ? 'create' : `update ruleset ${plan.existingId}`}; required checks: ${checks}`);
|
|
956
|
+
if (plan.verifyNote)
|
|
957
|
+
deps.out(` note: ${plan.verifyNote}`);
|
|
958
|
+
else if (plan.checks.includes(ASHLR_VERIFY_CHECK)) {
|
|
959
|
+
deps.out(` note: ${ASHLR_VERIFY_CHECK} is posted only on fleet PRs; your own PRs need the admin bypass unless another required check covers them`);
|
|
960
|
+
}
|
|
598
961
|
if (!apply) {
|
|
599
962
|
deps.out(` gh api --method ${plan.existingId === null ? 'POST' : 'PUT'} repos/${plan.repo}/rulesets${plan.existingId === null ? '' : `/${plan.existingId}`} --input - <<'JSON'`);
|
|
600
963
|
deps.out(JSON.stringify(plan.ruleset, null, 2));
|
|
@@ -603,37 +966,27 @@ async function cmdProtect(parsed, deps) {
|
|
|
603
966
|
}
|
|
604
967
|
if (!apply)
|
|
605
968
|
return 0;
|
|
606
|
-
const actionable = plans.filter((plan) => !plan.skipped);
|
|
969
|
+
const actionable = plans.filter((plan) => !plan.skipped && !plan.upToDate);
|
|
607
970
|
if (actionable.length === 0)
|
|
608
971
|
return 0;
|
|
609
972
|
if (!parsed.flags.has('--yes') && !(await deps.confirm(`Apply the ruleset to ${actionable.length} repo(s)?`))) {
|
|
610
973
|
deps.out('Nothing applied.');
|
|
611
974
|
return 1;
|
|
612
975
|
}
|
|
613
|
-
|
|
614
|
-
for (const plan of actionable) {
|
|
615
|
-
const result = deps.run('gh', [
|
|
616
|
-
'api', '--method', plan.existingId === null ? 'POST' : 'PUT',
|
|
617
|
-
`repos/${plan.repo}/rulesets${plan.existingId === null ? '' : `/${plan.existingId}`}`,
|
|
618
|
-
'--input', '-',
|
|
619
|
-
], { input: JSON.stringify(plan.ruleset) });
|
|
620
|
-
if (result.status === 0)
|
|
621
|
-
deps.out(`${plan.repo}: ruleset ${plan.existingId === null ? 'created' : 'updated'}.`);
|
|
622
|
-
else {
|
|
623
|
-
failures += 1;
|
|
624
|
-
deps.err(`${plan.repo}: failed — ${result.stderr.trim().slice(0, 300)}`);
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
return failures === 0 ? 0 : 1;
|
|
976
|
+
return (await applyProtectPlans(actionable, deps)) === 0 ? 0 : 1;
|
|
628
977
|
}
|
|
629
978
|
// ---------------------------------------------------------------------------
|
|
630
979
|
// github-app — App Manifest flow (addendum §3)
|
|
631
980
|
// ---------------------------------------------------------------------------
|
|
632
981
|
export const FLEET_APP_NAME = 'ashlr-fleet';
|
|
633
982
|
/**
|
|
634
|
-
* PURE: the App manifest. Contents + pull requests read/write, checks
|
|
635
|
-
*
|
|
636
|
-
*
|
|
983
|
+
* PURE: the App manifest. Contents + pull requests read/write, checks
|
|
984
|
+
* read/write (3.13: the App posts the host-verified `ashlr/verify` check run
|
|
985
|
+
* on fleet PR heads — core/fleet/verify-check-run.ts), statuses / metadata
|
|
986
|
+
* read. No workflows, no administration — GitHub itself then refuses
|
|
987
|
+
* CI-config and protection changes from the fleet. An App created before 3.13
|
|
988
|
+
* has `checks: read`; `ashlr authority setup` detects that and prints where
|
|
989
|
+
* to raise it (fleetAppPermissionUrls).
|
|
637
990
|
*/
|
|
638
991
|
export function buildGithubAppManifest(redirectUrl) {
|
|
639
992
|
return {
|
|
@@ -642,7 +995,7 @@ export function buildGithubAppManifest(redirectUrl) {
|
|
|
642
995
|
hook_attributes: { url: 'https://github.com/ashlrai/ashlr-hub', active: false },
|
|
643
996
|
redirect_url: redirectUrl,
|
|
644
997
|
public: false,
|
|
645
|
-
default_permissions: { contents: 'write', pull_requests: 'write', checks: '
|
|
998
|
+
default_permissions: { contents: 'write', pull_requests: 'write', checks: 'write', statuses: 'read', metadata: 'read' },
|
|
646
999
|
default_events: [],
|
|
647
1000
|
};
|
|
648
1001
|
}
|
|
@@ -729,26 +1082,165 @@ async function cmdGithubApp(parsed, deps) {
|
|
|
729
1082
|
const result = await runGithubAppFlow(deps, { org });
|
|
730
1083
|
deps.out(`Created ${result.slug} (App id ${result.appId}); its private key is in custody and was never written to disk.`);
|
|
731
1084
|
deps.out(`Install it on the enrolled repos: ${result.installUrl}`);
|
|
1085
|
+
deps.out(`It can post the host-verified ${ASHLR_VERIFY_CHECK} check (checks: write). An ${FLEET_APP_NAME} App created before 3.13 cannot: \`ashlr authority setup\` prints the exact settings link to raise it.`);
|
|
732
1086
|
return 0;
|
|
733
1087
|
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
1088
|
+
/** Where Mason installs the App (and picks its repos). */
|
|
1089
|
+
export const FLEET_APP_INSTALL_URL = `https://github.com/apps/${FLEET_APP_NAME}/installations/new`;
|
|
1090
|
+
/** Bounded wait: a probe that does not answer in time is "unknown", never a hang. */
|
|
1091
|
+
async function withTimeout(work, ms, what) {
|
|
1092
|
+
let timer;
|
|
1093
|
+
try {
|
|
1094
|
+
return await Promise.race([
|
|
1095
|
+
work,
|
|
1096
|
+
new Promise((_, reject) => {
|
|
1097
|
+
timer = setTimeout(() => reject(new Error(`${what} did not answer within ${Math.round(ms / 1000)} s`)), ms);
|
|
1098
|
+
}),
|
|
1099
|
+
]);
|
|
1100
|
+
}
|
|
1101
|
+
finally {
|
|
1102
|
+
if (timer)
|
|
1103
|
+
clearTimeout(timer);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
const APP_PROBE_TIMEOUT_MS = 20_000;
|
|
1107
|
+
/**
|
|
1108
|
+
* Where the ashlr-fleet App is installed — read-only, in two passes:
|
|
1109
|
+
*
|
|
1110
|
+
* 1. per owner, `gh api orgs/<owner>/installations` with Mason's own gh auth
|
|
1111
|
+
* (a GET; needs admin:org). An installation on "All repositories" settles
|
|
1112
|
+
* every repo of that owner; no installation at all settles them as missing.
|
|
1113
|
+
* 2. what that cannot settle (a user account, "Only select repositories", a
|
|
1114
|
+
* gh token without admin:org) is asked of the App itself: custody's
|
|
1115
|
+
* gh-token looks the repo up AS the App (GET /repos/<r>/installation under
|
|
1116
|
+
* the App's JWT), and a 404 there means "not installed". The one-hour token
|
|
1117
|
+
* the helper mints on success stays in this process's memory, is never
|
|
1118
|
+
* printed, and changes nothing on GitHub.
|
|
1119
|
+
*/
|
|
1120
|
+
export async function probeAppInstallation(deps, repos) {
|
|
1121
|
+
const result = { installed: [], missing: [], unknown: [] };
|
|
1122
|
+
const byOwner = new Map();
|
|
1123
|
+
for (const repo of repos) {
|
|
1124
|
+
const owner = repo.split('/')[0];
|
|
1125
|
+
byOwner.set(owner, [...(byOwner.get(owner) ?? []), repo]);
|
|
1126
|
+
}
|
|
1127
|
+
const unsettled = [];
|
|
1128
|
+
for (const [owner, ownerRepos] of byOwner) {
|
|
1129
|
+
let listed = null;
|
|
1130
|
+
try {
|
|
1131
|
+
const read = await ghJson(deps, ['api', `orgs/${owner}/installations?per_page=100`]);
|
|
1132
|
+
if (read && Array.isArray(read.installations))
|
|
1133
|
+
listed = read.installations;
|
|
1134
|
+
}
|
|
1135
|
+
catch {
|
|
1136
|
+
listed = null;
|
|
1137
|
+
}
|
|
1138
|
+
const app = listed?.find((entry) => entry.app_slug === FLEET_APP_NAME);
|
|
1139
|
+
if (listed && !app)
|
|
1140
|
+
result.missing.push(...ownerRepos);
|
|
1141
|
+
else if (app?.repository_selection === 'all')
|
|
1142
|
+
result.installed.push(...ownerRepos);
|
|
1143
|
+
else
|
|
1144
|
+
unsettled.push(...ownerRepos);
|
|
1145
|
+
}
|
|
1146
|
+
// A few at a time: each lookup is two HTTPS calls inside the helper.
|
|
1147
|
+
const queue = [...unsettled];
|
|
1148
|
+
const worker = async () => {
|
|
1149
|
+
for (let repo = queue.shift(); repo !== undefined; repo = queue.shift()) {
|
|
1150
|
+
try {
|
|
1151
|
+
await withTimeout(deps.custody.githubToken(repo), APP_PROBE_TIMEOUT_MS, 'ashlr-custody gh-token');
|
|
1152
|
+
result.installed.push(repo);
|
|
1153
|
+
}
|
|
1154
|
+
catch (error) {
|
|
1155
|
+
const code = error.code;
|
|
1156
|
+
const message = error.message ?? String(error);
|
|
1157
|
+
if (code === 'github' && /not installed/u.test(message))
|
|
1158
|
+
result.missing.push(repo);
|
|
1159
|
+
else
|
|
1160
|
+
result.unknown.push({ repo, reason: message.slice(0, 160) });
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
};
|
|
1164
|
+
await Promise.all(Array.from({ length: Math.min(4, queue.length) }, worker));
|
|
1165
|
+
// Stable order for the checklist line, whatever order the lookups finished in.
|
|
1166
|
+
const order = new Map(repos.map((repo, i) => [repo, i]));
|
|
1167
|
+
const byInput = (a, b) => (order.get(a) ?? 0) - (order.get(b) ?? 0);
|
|
1168
|
+
result.installed.sort(byInput);
|
|
1169
|
+
result.missing.sort(byInput);
|
|
1170
|
+
result.unknown.sort((a, b) => byInput(a.repo, b.repo));
|
|
1171
|
+
return result;
|
|
1172
|
+
}
|
|
1173
|
+
/** "a, b and 3 more" — a list short enough for one checklist line. */
|
|
1174
|
+
function shortList(items, max = 3) {
|
|
1175
|
+
if (items.length <= max)
|
|
1176
|
+
return items.join(', ');
|
|
1177
|
+
return `${items.slice(0, max).join(', ')} and ${items.length - max} more`;
|
|
1178
|
+
}
|
|
1179
|
+
function provenanceRotationRecordPath(keyPath) {
|
|
1180
|
+
return `${keyPath}.rotation.json`;
|
|
1181
|
+
}
|
|
1182
|
+
function sha256Hex(bytes) {
|
|
1183
|
+
return createHash('sha256').update(bytes).digest('hex');
|
|
1184
|
+
}
|
|
1185
|
+
function readRotationRecord(keyPath) {
|
|
1186
|
+
try {
|
|
1187
|
+
const raw = JSON.parse(readFileSync(provenanceRotationRecordPath(keyPath), 'utf8'));
|
|
1188
|
+
if (raw.v !== 1 || typeof raw.rotatedAt !== 'string' || typeof raw.keySha256 !== 'string' || !/^[a-f0-9]{64}$/u.test(raw.keySha256))
|
|
1189
|
+
return null;
|
|
1190
|
+
return { v: 1, rotatedAt: raw.rotatedAt, keySha256: raw.keySha256 };
|
|
1191
|
+
}
|
|
1192
|
+
catch {
|
|
1193
|
+
return null;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
/** Read-only: never creates, repairs or moves the key. */
|
|
1197
|
+
export async function provenanceKeyState() {
|
|
1198
|
+
const { provenanceKeyPath, loadExistingProvenanceKeyReadOnly } = await import('../core/foundry/provenance.js');
|
|
1199
|
+
let path;
|
|
1200
|
+
let key;
|
|
1201
|
+
try {
|
|
1202
|
+
path = provenanceKeyPath();
|
|
1203
|
+
key = loadExistingProvenanceKeyReadOnly();
|
|
1204
|
+
}
|
|
1205
|
+
catch (error) {
|
|
1206
|
+
return { state: 'unhealthy', reason: error.message.replace(homedir(), '~') };
|
|
1207
|
+
}
|
|
1208
|
+
if (!key)
|
|
1209
|
+
return { state: 'missing' };
|
|
1210
|
+
const record = readRotationRecord(path);
|
|
1211
|
+
// The record names the exact key it made: a key swapped in afterwards is not "already rotated".
|
|
1212
|
+
if (record && record.keySha256 === sha256Hex(key))
|
|
1213
|
+
return { state: 'rotated', rotatedAt: record.rotatedAt };
|
|
1214
|
+
return { state: 'unrotated' };
|
|
1215
|
+
}
|
|
737
1216
|
/**
|
|
738
1217
|
* Move the provenance HMAC key aside and create a new one. Every pending
|
|
739
1218
|
* provenance signature stops verifying (by design: agents could read the old
|
|
740
1219
|
* key while confinement was off), so pending proposals are re-verified.
|
|
1220
|
+
* Records which key it made (provenanceKeyState reads it) so setup can tell
|
|
1221
|
+
* a key it already rotated from the burned one.
|
|
741
1222
|
*/
|
|
742
1223
|
export async function rotateProvenanceKey() {
|
|
743
|
-
const { provenanceKeyPath, loadOrCreateKey } = await import('../core/foundry/provenance.js');
|
|
1224
|
+
const { provenanceKeyPath, loadOrCreateKey, loadExistingProvenanceKeyReadOnly } = await import('../core/foundry/provenance.js');
|
|
744
1225
|
const path = provenanceKeyPath();
|
|
745
1226
|
let retiredAs = null;
|
|
746
1227
|
if (existsSync(path)) {
|
|
747
1228
|
retiredAs = `${path}.retired-${new Date().toISOString().replace(/[:.]/g, '-')}`;
|
|
748
1229
|
renameSync(path, retiredAs);
|
|
749
1230
|
}
|
|
750
|
-
loadOrCreateKey();
|
|
751
|
-
|
|
1231
|
+
const created = loadOrCreateKey();
|
|
1232
|
+
// loadOrCreateKey falls back to a process-lifetime key when it cannot write
|
|
1233
|
+
// one; that is not a rotation — say so instead of recording it.
|
|
1234
|
+
const stored = loadExistingProvenanceKeyReadOnly();
|
|
1235
|
+
if (!stored || !stored.equals(created))
|
|
1236
|
+
throw new Error('the new provenance key could not be stored in ~/.ashlr/foundry');
|
|
1237
|
+
const rotatedAt = new Date().toISOString();
|
|
1238
|
+
const record = { v: 1, rotatedAt, keySha256: sha256Hex(stored) };
|
|
1239
|
+
const target = provenanceRotationRecordPath(path);
|
|
1240
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
1241
|
+
writeFileSync(tmp, `${JSON.stringify(record)}\n`, { mode: 0o600 });
|
|
1242
|
+
renameSync(tmp, target);
|
|
1243
|
+
return { retiredAs, rotatedAt };
|
|
752
1244
|
}
|
|
753
1245
|
async function cmdRotateProvenance(parsed, deps) {
|
|
754
1246
|
if (!parsed.flags.has('--yes') && !(await deps.confirm('Replace the provenance HMAC key? Pending proposals will need re-verification.'))) {
|
|
@@ -840,12 +1332,41 @@ const SETUP_STEP_NEEDS = Object.freeze({
|
|
|
840
1332
|
'standing-grant': ['touch-id'],
|
|
841
1333
|
'autonomy-switch': [],
|
|
842
1334
|
'daemon-service': ['terminal'],
|
|
843
|
-
'resident-runtime': [],
|
|
1335
|
+
'resident-runtime': ['terminal'],
|
|
844
1336
|
});
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
const
|
|
1337
|
+
/** The one command that walks every step and resumes where the last run stopped. */
|
|
1338
|
+
export const AUTHORITY_SETUP_COMMAND = 'ashlr authority setup';
|
|
1339
|
+
/** The only command that installs or restarts the resident service (you, at a terminal, under the grant). */
|
|
1340
|
+
const RESIDENT_START_COMMAND = 'ashlr authority resident start';
|
|
1341
|
+
/**
|
|
1342
|
+
* The command that moves each step on while it is not in place. Setup is
|
|
1343
|
+
* rerun-safe, so for everything it performs itself the answer is setup; the
|
|
1344
|
+
* rest is what only Mason runs (sudo, the build, the grant, resident start).
|
|
1345
|
+
* A row may name its own (the resident step names what its admission asks
|
|
1346
|
+
* for, or none when a prerequisite outside this Mac's commands is missing).
|
|
1347
|
+
*/
|
|
1348
|
+
const SETUP_STEP_COMMAND = Object.freeze({
|
|
1349
|
+
'custody-helper': 'sudo scripts/install-custody.sh',
|
|
1350
|
+
'host-binding': 'sudo scripts/install-custody.sh',
|
|
1351
|
+
'signing-key': AUTHORITY_SETUP_COMMAND,
|
|
1352
|
+
'trust-root': AUTHORITY_SETUP_COMMAND,
|
|
1353
|
+
deploy: 'npm run build',
|
|
1354
|
+
'github-app': AUTHORITY_SETUP_COMMAND,
|
|
1355
|
+
'claude-token': AUTHORITY_SETUP_COMMAND,
|
|
1356
|
+
'canary-repo': AUTHORITY_SETUP_COMMAND,
|
|
1357
|
+
rulesets: AUTHORITY_SETUP_COMMAND,
|
|
1358
|
+
'old-activation-state': AUTHORITY_SETUP_COMMAND,
|
|
1359
|
+
'provenance-key': AUTHORITY_SETUP_COMMAND,
|
|
1360
|
+
'standing-grant': 'ashlr authority grant',
|
|
1361
|
+
'autonomy-switch': 'ashlr authority switch autonomous',
|
|
1362
|
+
'daemon-service': RESIDENT_START_COMMAND,
|
|
1363
|
+
'resident-runtime': RESIDENT_START_COMMAND,
|
|
1364
|
+
});
|
|
1365
|
+
// Every exit before the grant is known to be active (the first unmet live
|
|
1366
|
+
// prerequisite, or a dry run past one) reports the resident step with this:
|
|
1367
|
+
// a resident service is admitted only under an active standing grant
|
|
1368
|
+
// (docs/RESIDENT-RUNTIME.md), so without one it is genuinely blocked.
|
|
1369
|
+
const RESIDENT_NEEDS_GRANT = 'blocked until a grant is active (`ashlr authority grant`, Touch ID); then `ashlr authority resident start` in your own terminal installs the resident daemon under it';
|
|
849
1370
|
function setupStepId(step) {
|
|
850
1371
|
return step.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
851
1372
|
}
|
|
@@ -854,7 +1375,17 @@ export function authoritySetupReport(report, dryRun) {
|
|
|
854
1375
|
const count = (status) => report.filter((r) => r.status === status).length;
|
|
855
1376
|
const steps = report.map((r) => {
|
|
856
1377
|
const id = setupStepId(r.step);
|
|
857
|
-
|
|
1378
|
+
// A step in place needs nothing; one that is not names what moves it on.
|
|
1379
|
+
const open = r.status !== 'done' && r.status !== 'already';
|
|
1380
|
+
return {
|
|
1381
|
+
id,
|
|
1382
|
+
step: r.step,
|
|
1383
|
+
status: r.status,
|
|
1384
|
+
detail: r.detail,
|
|
1385
|
+
needs: SETUP_STEP_NEEDS[id] ?? [],
|
|
1386
|
+
command: open ? (r.command !== undefined ? r.command : SETUP_STEP_COMMAND[id] ?? null) : null,
|
|
1387
|
+
link: open ? r.link ?? null : null,
|
|
1388
|
+
};
|
|
858
1389
|
});
|
|
859
1390
|
const open = steps.find((s) => s.status !== 'done' && s.status !== 'already');
|
|
860
1391
|
return {
|
|
@@ -868,7 +1399,6 @@ export function authoritySetupReport(report, dryRun) {
|
|
|
868
1399
|
}
|
|
869
1400
|
async function cmdSetup(parsed, deps) {
|
|
870
1401
|
const dryRun = parsed.flags.has('--dry-run');
|
|
871
|
-
const yes = parsed.flags.has('--yes');
|
|
872
1402
|
const json = parsed.flags.has('--json');
|
|
873
1403
|
if (json && !dryRun) {
|
|
874
1404
|
// A real run asks questions and prints what gh / signing did; only the
|
|
@@ -876,24 +1406,119 @@ async function cmdSetup(parsed, deps) {
|
|
|
876
1406
|
deps.err('setup --json needs --dry-run');
|
|
877
1407
|
return 2;
|
|
878
1408
|
}
|
|
1409
|
+
const report = await runSetup(parsed, deps, { dryRun, yes: parsed.flags.has('--yes'), print: !json });
|
|
1410
|
+
return finish(report, deps, dryRun, json);
|
|
1411
|
+
}
|
|
1412
|
+
// ---------------------------------------------------------------------------
|
|
1413
|
+
// The read-only checklist (Verse: GET /api/verse/authority/setup)
|
|
1414
|
+
// ---------------------------------------------------------------------------
|
|
1415
|
+
/** Per `gh api` GET in the read-only probe. */
|
|
1416
|
+
const READ_ONLY_GH_TIMEOUT_MS = 10_000;
|
|
1417
|
+
/** All `gh api` reads of one read-only probe together. */
|
|
1418
|
+
const READ_ONLY_PROBE_BUDGET_MS = 30_000;
|
|
1419
|
+
const GH_API_PATH_RE = /^[A-Za-z0-9][A-Za-z0-9._~/?=&%-]{0,400}$/u;
|
|
1420
|
+
/**
|
|
1421
|
+
* The read-only probe's runner: `gh api <path>` — a GET, since no field,
|
|
1422
|
+
* method or input flag is ever passed — run asynchronously (the Verse server
|
|
1423
|
+
* must not block its event loop) and killed after READ_ONLY_GH_TIMEOUT_MS.
|
|
1424
|
+
* Anything else (git, `gh pr`, `gh repo create`, `--method`) is refused
|
|
1425
|
+
* without running, so a dry-run path that ever grew a write could not reach it.
|
|
1426
|
+
*/
|
|
1427
|
+
/** PURE: `gh api <path>` with nothing else — the only call the read-only probe runs. */
|
|
1428
|
+
export function isReadOnlyGhApiCall(bin, args) {
|
|
1429
|
+
return bin === 'gh' && args.length === 2 && args[0] === 'api' && GH_API_PATH_RE.test(args[1] ?? '');
|
|
1430
|
+
}
|
|
1431
|
+
export function readOnlyGhRun(bin, args) {
|
|
1432
|
+
if (!isReadOnlyGhApiCall(bin, args)) {
|
|
1433
|
+
return Promise.resolve({ status: 1, stdout: '', stderr: 'refused: the read-only setup probe only runs `gh api <path>` reads' });
|
|
1434
|
+
}
|
|
1435
|
+
return new Promise((done) => {
|
|
1436
|
+
execFile('gh', ['api', args[1]], {
|
|
1437
|
+
encoding: 'utf8',
|
|
1438
|
+
timeout: READ_ONLY_GH_TIMEOUT_MS,
|
|
1439
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
1440
|
+
env: { ...process.env, GH_PROMPT_DISABLED: '1', GH_NO_UPDATE_NOTIFIER: '1' },
|
|
1441
|
+
}, (error, stdout, stderr) => {
|
|
1442
|
+
const code = error?.code;
|
|
1443
|
+
done({
|
|
1444
|
+
status: error ? (typeof code === 'number' ? code : 1) : 0,
|
|
1445
|
+
stdout: stdout ?? '',
|
|
1446
|
+
stderr: stderr || (error ? error.message : ''),
|
|
1447
|
+
});
|
|
1448
|
+
});
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
function refusedInProbe(what) {
|
|
1452
|
+
throw new Error(`${what} is not available to the read-only setup probe`);
|
|
1453
|
+
}
|
|
1454
|
+
/**
|
|
1455
|
+
* `ashlr authority setup --dry-run --json` as a function: the same planning
|
|
1456
|
+
* path (cmdSetup's steps with --dry-run), with dependencies that cannot ask,
|
|
1457
|
+
* prompt, open a browser, reach the network directly or run anything but a
|
|
1458
|
+
* bounded `gh api` GET. Custody is only asked read-only questions (status,
|
|
1459
|
+
* host binding, public key, and whether the App can see a repo).
|
|
1460
|
+
*/
|
|
1461
|
+
export async function planAuthoritySetup(injected) {
|
|
1462
|
+
// The whole probe gets READ_ONLY_PROBE_BUDGET_MS of gh time: past it, a read
|
|
1463
|
+
// is answered "not read" at once (the step then says what it could not
|
|
1464
|
+
// check) instead of queueing more ten-second waits behind a slow network.
|
|
1465
|
+
const deadline = Date.now() + READ_ONLY_PROBE_BUDGET_MS;
|
|
1466
|
+
const budgeted = (bin, args) => (Date.now() > deadline
|
|
1467
|
+
? Promise.resolve({ status: 1, stdout: '', stderr: 'not read: the setup probe ran out of time' })
|
|
1468
|
+
: readOnlyGhRun(bin, args));
|
|
1469
|
+
const deps = {
|
|
1470
|
+
out: () => undefined,
|
|
1471
|
+
err: () => undefined,
|
|
1472
|
+
confirm: async () => false,
|
|
1473
|
+
readSecret: async () => refusedInProbe('a secret prompt'),
|
|
1474
|
+
run: budgeted,
|
|
1475
|
+
openBrowser: () => refusedInProbe('a browser'),
|
|
1476
|
+
fetch: (async () => refusedInProbe('the network')),
|
|
1477
|
+
custody: injected?.custody ?? await import('../core/authority/custody-client.js'),
|
|
1478
|
+
daemonService: () => realDaemonService(true),
|
|
1479
|
+
// The resident step only reads (admission verdict, service + plist
|
|
1480
|
+
// observation); the effects are refused here, never reached by setup.
|
|
1481
|
+
resident: {
|
|
1482
|
+
admission: realResidentDeps.admission,
|
|
1483
|
+
observe: realResidentDeps.observe,
|
|
1484
|
+
operatorRefusal: async () => 'the read-only setup probe is not the operator at a terminal',
|
|
1485
|
+
start: async () => refusedInProbe('resident start'),
|
|
1486
|
+
stop: async () => refusedInProbe('resident stop'),
|
|
1487
|
+
},
|
|
1488
|
+
...(injected ?? {}),
|
|
1489
|
+
};
|
|
1490
|
+
const parsed = { positional: ['setup'], flags: new Set(['--dry-run', '--json']), values: new Map() };
|
|
1491
|
+
return authoritySetupReport(await runSetup(parsed, deps, { dryRun: true, yes: false, print: false }), true);
|
|
1492
|
+
}
|
|
1493
|
+
/**
|
|
1494
|
+
* Every step of setup, in order. RERUN-SAFE: Mason runs it several times, so
|
|
1495
|
+
* each step first checks whether it is already in place and says so
|
|
1496
|
+
* ('already') instead of doing it again — the provenance key is rotated once,
|
|
1497
|
+
* a ruleset GitHub already holds is not PUT again, the App counts only when it
|
|
1498
|
+
* is installed, an open trust-root PR is found rather than reopened. A step
|
|
1499
|
+
* that throws is recorded as failed and the run still ends with its summary.
|
|
1500
|
+
*/
|
|
1501
|
+
async function runSetup(parsed, deps, opts) {
|
|
1502
|
+
const { dryRun, yes } = opts;
|
|
879
1503
|
const report = [];
|
|
880
|
-
const note = (step, status, detail) => {
|
|
881
|
-
report.push({ step, status, detail });
|
|
882
|
-
if (
|
|
1504
|
+
const note = (step, status, detail, extra = {}) => {
|
|
1505
|
+
report.push({ step, status, detail, ...extra });
|
|
1506
|
+
if (!opts.print)
|
|
883
1507
|
return;
|
|
884
1508
|
// 'skipped' gets its own glyph: a ✓ on a step a dry run did not perform read as done.
|
|
885
1509
|
const glyph = status === 'failed' ? '✗' : status === 'blocked' ? '×' : status === 'waiting-on-you' ? '…' : status === 'skipped' ? '·' : '✓';
|
|
886
1510
|
deps.out(`${glyph} ${step}: ${detail}`);
|
|
887
1511
|
};
|
|
888
1512
|
const done = () => {
|
|
889
|
-
// Every exit, including the first unmet live prerequisite,
|
|
890
|
-
//
|
|
1513
|
+
// Every exit, including the first unmet live prerequisite, reports the
|
|
1514
|
+
// resident step exactly once — it is never silently complete.
|
|
891
1515
|
if (!report.some((row) => row.step === 'resident runtime')) {
|
|
892
|
-
note('resident runtime', 'blocked',
|
|
1516
|
+
note('resident runtime', 'blocked', RESIDENT_NEEDS_GRANT, { command: 'ashlr authority grant' });
|
|
893
1517
|
}
|
|
894
|
-
return
|
|
1518
|
+
return report;
|
|
895
1519
|
};
|
|
896
1520
|
const ask = async (question) => !dryRun && (yes || deps.confirm(question));
|
|
1521
|
+
const failure = (error) => (error?.message ?? String(error)).replace(homedir(), '~').slice(0, 300);
|
|
897
1522
|
// --dry-run PLANS: the CHANGELOG and docs/AUTHORITY.md promise it "prints
|
|
898
1523
|
// every step first" (3.10 review c20). A real run must stop at an unmet
|
|
899
1524
|
// prerequisite; a dry run notes it and keeps describing the remaining
|
|
@@ -907,256 +1532,561 @@ async function cmdSetup(parsed, deps) {
|
|
|
907
1532
|
planning = true;
|
|
908
1533
|
return false;
|
|
909
1534
|
};
|
|
910
|
-
//
|
|
911
|
-
|
|
1535
|
+
// Which step is running: an exception nobody anticipated becomes its
|
|
1536
|
+
// failure (with the summary still printed), never a bare abort.
|
|
1537
|
+
let current = 'custody helper';
|
|
912
1538
|
try {
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
catch (error) {
|
|
916
|
-
note('custody helper', 'failed', `status unavailable (${error.message})`);
|
|
917
|
-
}
|
|
918
|
-
if (!custody || !custody.installed) {
|
|
919
|
-
note('custody helper', 'waiting-on-you', 'run `sudo scripts/install-custody.sh` from the ashlr-hub checkout, then run setup again');
|
|
920
|
-
if (stopHere())
|
|
921
|
-
return done();
|
|
922
|
-
}
|
|
923
|
-
else {
|
|
924
|
-
note('custody helper', 'already', `installed${custody.version ? ` (${custody.version})` : ''}`);
|
|
925
|
-
}
|
|
926
|
-
// 1b. The helper and this CLI must agree on which Mac this is: the helper
|
|
927
|
-
// derives StandingGrantV1.hostBinding as sha256(uppercase IOPlatformUUID)
|
|
928
|
-
// and so does surface.ts. A disagreement would make every grant fail
|
|
929
|
-
// "signed for a different Mac" only AFTER Mason's Touch ID — catch it first.
|
|
930
|
-
if (planning) {
|
|
931
|
-
note('host binding', 'skipped', 'would check that ashlr-custody and this release agree on which Mac this is');
|
|
932
|
-
}
|
|
933
|
-
else {
|
|
1539
|
+
// 1. Custody helper (installing it needs sudo — Mason runs that himself).
|
|
1540
|
+
let custody = null;
|
|
934
1541
|
try {
|
|
935
|
-
|
|
936
|
-
const ours = currentHostBinding();
|
|
937
|
-
const helpers = await deps.custody.custodyHostBinding();
|
|
938
|
-
if (ours === null)
|
|
939
|
-
note('host binding', 'failed', "this Mac's IOPlatformUUID could not be read, so no grant can be bound to it");
|
|
940
|
-
else if (ours !== helpers)
|
|
941
|
-
note('host binding', 'failed', 'ashlr-custody and this release disagree on the host binding — reinstall the helper from this checkout');
|
|
942
|
-
else
|
|
943
|
-
note('host binding', 'already', `this Mac is ${ours.slice(0, 12)}…`);
|
|
1542
|
+
custody = await deps.custody.custodyStatus();
|
|
944
1543
|
}
|
|
945
1544
|
catch (error) {
|
|
946
|
-
note('
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
return done();
|
|
951
|
-
// 2. Secure Enclave key (Touch ID). The helper is not on PATH, so every
|
|
952
|
-
// message names it by its installed path.
|
|
953
|
-
let keyId = custody?.keyId ?? null;
|
|
954
|
-
let publicKeyPem = null;
|
|
955
|
-
if (planning || !custody) {
|
|
956
|
-
note('signing key', 'skipped', 'would create the Secure Enclave signing key (Touch ID) unless it already exists');
|
|
957
|
-
}
|
|
958
|
-
else if (!custody.keyInitialized) {
|
|
959
|
-
if (!(await ask('Create the Secure Enclave signing key now (Touch ID)?'))) {
|
|
960
|
-
note('signing key', dryRun ? 'skipped' : 'waiting-on-you', dryRun
|
|
961
|
-
? `would create it (Touch ID) — or run \`${CUSTODY_HELPER_PATH} init\``
|
|
962
|
-
: `create it with \`${CUSTODY_HELPER_PATH} init\` or rerun setup`);
|
|
1545
|
+
note('custody helper', 'failed', `status unavailable (${failure(error)})`);
|
|
1546
|
+
}
|
|
1547
|
+
if (!custody || !custody.installed) {
|
|
1548
|
+
note('custody helper', 'waiting-on-you', 'run `sudo scripts/install-custody.sh` from the ashlr-hub checkout, then run setup again');
|
|
963
1549
|
if (stopHere())
|
|
964
1550
|
return done();
|
|
965
1551
|
}
|
|
966
1552
|
else {
|
|
967
|
-
|
|
968
|
-
keyId = info.keyId;
|
|
969
|
-
publicKeyPem = info.publicKeyPem;
|
|
970
|
-
note('signing key', 'done', `created ${info.keyId}`);
|
|
1553
|
+
note('custody helper', 'already', `installed${custody.version ? ` (${custody.version})` : ''}`);
|
|
971
1554
|
}
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
const isTrusted = (id) => id !== null && STANDING_GRANT_TRUST_ROOTS.some((root) => root.keyId === id);
|
|
982
|
-
// No kickstart here: a daemon that is disabled or not loaded (the usual
|
|
983
|
-
// state before the first grant) has nothing to kickstart, and the last step
|
|
984
|
-
// (daemon service) says how to start it once the grant is in force.
|
|
985
|
-
const DEPLOY_DETAIL = 'after you merge that PR: `npm run build`, install the release as you normally do (docs/RELEASING-LOCALLY.md), then rerun setup (restart a running daemon with `launchctl kickstart -k gui/$(id -u)/ai.ashlr.daemon`)';
|
|
986
|
-
if (planning) {
|
|
987
|
-
note('trust root', 'skipped', 'would check that your signing key is compiled into this release, and if not open a PR adding it to trust-roots.ts for you to review and merge');
|
|
988
|
-
note('deploy', 'skipped', `would wait for you: ${DEPLOY_DETAIL}`);
|
|
989
|
-
}
|
|
990
|
-
else {
|
|
991
|
-
let trustRootFailed = false;
|
|
992
|
-
if (!isTrusted(keyId) && !publicKeyPem) {
|
|
1555
|
+
// 1b. The helper and this CLI must agree on which Mac this is: the helper
|
|
1556
|
+
// derives StandingGrantV1.hostBinding as sha256(uppercase IOPlatformUUID)
|
|
1557
|
+
// and so does surface.ts. A disagreement would make every grant fail
|
|
1558
|
+
// "signed for a different Mac" only AFTER Mason's Touch ID — catch it first.
|
|
1559
|
+
current = 'host binding';
|
|
1560
|
+
if (planning) {
|
|
1561
|
+
note('host binding', 'skipped', 'would check that ashlr-custody and this release agree on which Mac this is');
|
|
1562
|
+
}
|
|
1563
|
+
else {
|
|
993
1564
|
try {
|
|
994
|
-
const
|
|
995
|
-
|
|
996
|
-
|
|
1565
|
+
const { currentHostBinding } = await import('../core/authority/surface.js');
|
|
1566
|
+
const ours = currentHostBinding();
|
|
1567
|
+
const helpers = await deps.custody.custodyHostBinding();
|
|
1568
|
+
if (ours === null)
|
|
1569
|
+
note('host binding', 'failed', "this Mac's IOPlatformUUID could not be read, so no grant can be bound to it");
|
|
1570
|
+
else if (ours !== helpers)
|
|
1571
|
+
note('host binding', 'failed', 'ashlr-custody and this release disagree on the host binding — reinstall the helper from this checkout');
|
|
1572
|
+
else
|
|
1573
|
+
note('host binding', 'already', `this Mac is ${ours.slice(0, 12)}…`);
|
|
997
1574
|
}
|
|
998
1575
|
catch (error) {
|
|
999
|
-
note('
|
|
1000
|
-
trustRootFailed = true;
|
|
1576
|
+
note('host binding', 'failed', `could not compare with ashlr-custody (${failure(error)})`);
|
|
1001
1577
|
}
|
|
1002
1578
|
}
|
|
1003
|
-
if (
|
|
1004
|
-
|
|
1005
|
-
|
|
1579
|
+
if (report.some((r) => r.step === 'host binding' && r.status === 'failed') && stopHere())
|
|
1580
|
+
return done();
|
|
1581
|
+
// 2. Secure Enclave key (Touch ID). The helper is not on PATH, so every
|
|
1582
|
+
// message names it by its installed path.
|
|
1583
|
+
current = 'signing key';
|
|
1584
|
+
let keyId = custody?.keyId ?? null;
|
|
1585
|
+
let publicKeyPem = null;
|
|
1586
|
+
if (planning || !custody) {
|
|
1587
|
+
note('signing key', 'skipped', 'would create the Secure Enclave signing key (Touch ID) unless it already exists');
|
|
1588
|
+
}
|
|
1589
|
+
else if (!custody.keyInitialized) {
|
|
1590
|
+
if (!(await ask('Create the Secure Enclave signing key now (Touch ID)?'))) {
|
|
1591
|
+
note('signing key', dryRun ? 'skipped' : 'waiting-on-you', dryRun
|
|
1592
|
+
? `would create it (Touch ID) — or run \`${CUSTODY_HELPER_PATH} init\``
|
|
1593
|
+
: `create it with \`${CUSTODY_HELPER_PATH} init\` or rerun setup`);
|
|
1594
|
+
if (stopHere())
|
|
1595
|
+
return done();
|
|
1596
|
+
}
|
|
1597
|
+
else {
|
|
1598
|
+
try {
|
|
1599
|
+
const info = await deps.custody.custodyInit();
|
|
1600
|
+
keyId = info.keyId;
|
|
1601
|
+
publicKeyPem = info.publicKeyPem;
|
|
1602
|
+
note('signing key', 'done', `created ${info.keyId}`);
|
|
1603
|
+
}
|
|
1604
|
+
catch (error) {
|
|
1605
|
+
note('signing key', 'failed', `not created (${failure(error)}) — rerun setup, or run \`${CUSTODY_HELPER_PATH} init\``);
|
|
1606
|
+
if (stopHere())
|
|
1607
|
+
return done();
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
else {
|
|
1612
|
+
note('signing key', 'already', `key ${keyId ?? '(id unknown)'}`);
|
|
1006
1613
|
}
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1614
|
+
// 3. Trust root compiled in (Mason's own PR).
|
|
1615
|
+
current = 'trust root';
|
|
1616
|
+
const { STANDING_GRANT_TRUST_ROOTS } = await import('../core/authority/trust-roots.js');
|
|
1617
|
+
// A key made on an earlier run whose id status did not report: read its
|
|
1618
|
+
// public half back (`ashlr-custody pubkey`, no Touch ID) instead of asking
|
|
1619
|
+
// Mason to copy it by hand.
|
|
1620
|
+
const isTrusted = (id) => id !== null && STANDING_GRANT_TRUST_ROOTS.some((root) => root.keyId === id);
|
|
1621
|
+
// No kickstart here: a daemon that is disabled or not loaded (the usual
|
|
1622
|
+
// state before the first grant) has nothing to kickstart, and the last step
|
|
1623
|
+
// (daemon service) says how to start it once the grant is in force.
|
|
1624
|
+
const DEPLOY_DETAIL = 'after you merge that PR: `npm run build`, install the release as you normally do (docs/RELEASING-LOCALLY.md), then rerun setup (restart a running daemon with `launchctl kickstart -k gui/$(id -u)/ai.ashlr.daemon`)';
|
|
1625
|
+
if (planning) {
|
|
1626
|
+
note('trust root', 'skipped', 'would check that your signing key is compiled into this release, and if not open a PR adding it to trust-roots.ts for you to review and merge');
|
|
1627
|
+
note('deploy', 'skipped', `would wait for you: ${DEPLOY_DETAIL}`);
|
|
1010
1628
|
}
|
|
1011
1629
|
else {
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1630
|
+
let trustRootFailed = false;
|
|
1631
|
+
if (!isTrusted(keyId) && !publicKeyPem) {
|
|
1632
|
+
try {
|
|
1633
|
+
const info = await deps.custody.custodyPublicKey();
|
|
1634
|
+
keyId = info.keyId;
|
|
1635
|
+
publicKeyPem = info.publicKeyPem;
|
|
1636
|
+
}
|
|
1637
|
+
catch (error) {
|
|
1638
|
+
note('trust root', 'failed', `could not read the custody public key (${failure(error)})`);
|
|
1639
|
+
trustRootFailed = true;
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
if (trustRootFailed) {
|
|
1643
|
+
if (stopHere())
|
|
1644
|
+
return done();
|
|
1645
|
+
}
|
|
1646
|
+
else if (isTrusted(keyId)) {
|
|
1647
|
+
note('trust root', 'already', `${keyId} is compiled into this release`);
|
|
1648
|
+
note('deploy', 'already', 'this release carries your trust root');
|
|
1016
1649
|
}
|
|
1017
1650
|
else {
|
|
1018
|
-
|
|
1651
|
+
const root = { keyId: keyId, alg: 'ES256', publicKeyPem: publicKeyPem };
|
|
1652
|
+
const invalid = validateCustodyRoot(root, deps.custody.keyIdForPublicKeyPem);
|
|
1653
|
+
if (invalid) {
|
|
1654
|
+
note('trust root', 'failed', invalid);
|
|
1655
|
+
}
|
|
1656
|
+
else {
|
|
1657
|
+
let pr;
|
|
1658
|
+
try {
|
|
1659
|
+
// A dry run looks too, through `gh api` reads only (the
|
|
1660
|
+
// Verse checklist is this path): an open PR, a merged key or a
|
|
1661
|
+
// pushed branch is reported instead of "add this root yourself".
|
|
1662
|
+
pr = (dryRun ? await probeTrustRootReadOnly(deps, root) : null) ?? await openTrustRootPr(parsed, deps, root, ask, !dryRun);
|
|
1663
|
+
}
|
|
1664
|
+
catch (error) {
|
|
1665
|
+
pr = { status: 'failed', detail: failure(error) };
|
|
1666
|
+
}
|
|
1667
|
+
note('trust root', pr.status, pr.detail, {
|
|
1668
|
+
...(pr.link ? { link: pr.link } : {}),
|
|
1669
|
+
...(pr.command !== undefined ? { command: pr.command } : {}),
|
|
1670
|
+
});
|
|
1671
|
+
note('deploy', 'waiting-on-you', DEPLOY_DETAIL);
|
|
1672
|
+
}
|
|
1673
|
+
if (stopHere())
|
|
1674
|
+
return done();
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
// The canary's existence decides whether the App and ruleset checks
|
|
1678
|
+
// include it yet (setup creates it at step 6). One read-only GET.
|
|
1679
|
+
const canaryRepo = 'ashlrai/fleet-canary';
|
|
1680
|
+
let canaryExists = planning ? false : (await deps.run('gh', ['api', `repos/${canaryRepo}`])).status === 0;
|
|
1681
|
+
// 4. GitHub App: its key in custody is half of it — it also has to be
|
|
1682
|
+
// INSTALLED on the repos the fleet works in, or every mint fails.
|
|
1683
|
+
current = 'GitHub App';
|
|
1684
|
+
if (!custody?.githubApp) {
|
|
1685
|
+
if (await ask(`Create the ${FLEET_APP_NAME} GitHub App now (one browser page)?`)) {
|
|
1019
1686
|
try {
|
|
1020
|
-
|
|
1687
|
+
const app = await runGithubAppFlow(deps, { org: one(parsed, '--org') ?? 'ashlrai' });
|
|
1688
|
+
// A new App is installed nowhere yet.
|
|
1689
|
+
note('GitHub App', 'waiting-on-you', `created ${app.slug} (its key is in custody); now install it on the enrolled repos: ${app.installUrl} — then rerun setup`, { link: app.installUrl });
|
|
1021
1690
|
}
|
|
1022
1691
|
catch (error) {
|
|
1023
|
-
|
|
1692
|
+
note('GitHub App', 'failed', `not created (${failure(error)}) — rerun setup, or run \`ashlr authority github-app\``);
|
|
1024
1693
|
}
|
|
1025
|
-
note('trust root', pr.status, pr.detail);
|
|
1026
|
-
note('deploy', 'waiting-on-you', DEPLOY_DETAIL);
|
|
1027
1694
|
}
|
|
1028
|
-
|
|
1029
|
-
|
|
1695
|
+
else
|
|
1696
|
+
note('GitHub App', dryRun ? 'skipped' : 'waiting-on-you', 'run `ashlr authority github-app`');
|
|
1697
|
+
}
|
|
1698
|
+
else if (planning) {
|
|
1699
|
+
note('GitHub App', 'skipped', `the ${FLEET_APP_NAME} key is in custody; would check the App is installed on the enrolled repos`);
|
|
1700
|
+
}
|
|
1701
|
+
else {
|
|
1702
|
+
const targets = (await setupTargetRepos()).map((repo) => repo.nameWithOwner)
|
|
1703
|
+
.filter((repo) => canaryExists || repo.toLowerCase() !== canaryRepo);
|
|
1704
|
+
const probe = await probeAppInstallation(deps, targets);
|
|
1705
|
+
const later = canaryExists ? '' : ` (${canaryRepo} needs it too once it exists)`;
|
|
1706
|
+
if (probe.missing.length > 0) {
|
|
1707
|
+
note('GitHub App', 'waiting-on-you', `the ${FLEET_APP_NAME} key is in custody, but the App is not installed on ${shortList(probe.missing)} — install it: ${FLEET_APP_INSTALL_URL}, then rerun setup${later}`, { link: FLEET_APP_INSTALL_URL });
|
|
1708
|
+
}
|
|
1709
|
+
else if (probe.unknown.length > 0) {
|
|
1710
|
+
note('GitHub App', 'waiting-on-you', `the ${FLEET_APP_NAME} key is in custody, but its installation on ${shortList(probe.unknown.map((u) => u.repo))} could not be confirmed (${probe.unknown[0].reason}) — check ${FLEET_APP_INSTALL_URL}, then rerun setup`, { link: FLEET_APP_INSTALL_URL });
|
|
1711
|
+
}
|
|
1712
|
+
else {
|
|
1713
|
+
const where = probe.installed.length === 0 ? 'no enrolled repo needs it yet' : probe.installed.length === 1 ? `installed on ${probe.installed[0]}` : `installed on all ${probe.installed.length} enrolled repos`;
|
|
1714
|
+
// 3.13: it must also be able to post ashlr/verify (checks: write); an
|
|
1715
|
+
// App created before 3.13 has checks: read and must be raised by hand.
|
|
1716
|
+
const app = await readFleetApp(deps);
|
|
1717
|
+
if (app && app.checks !== 'write') {
|
|
1718
|
+
const urls = fleetAppPermissionUrls(app);
|
|
1719
|
+
note('GitHub App', 'waiting-on-you', `the ${FLEET_APP_NAME} key is in custody and the App is ${where}, but it cannot post the ${ASHLR_VERIFY_CHECK} check (Checks: ${app.checks ?? 'no access'}): `
|
|
1720
|
+
+ `set "Checks: Read and write" at ${urls.permissions}, accept the new permission on each installation at ${urls.installations}, then rerun setup`, { link: urls.permissions });
|
|
1721
|
+
}
|
|
1722
|
+
else {
|
|
1723
|
+
note('GitHub App', 'already', app
|
|
1724
|
+
? `the ${FLEET_APP_NAME} key is in custody; ${where}${later}; the App can post ${ASHLR_VERIFY_CHECK} (checks: write)`
|
|
1725
|
+
: `the ${FLEET_APP_NAME} key is in custody; ${where}${later} (its checks permission was not read; it needs "Checks: Read and write" for ${ASHLR_VERIFY_CHECK})`);
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
// 5. Claude token for restricted judge / Leader calls.
|
|
1730
|
+
current = 'Claude token';
|
|
1731
|
+
if (custody?.claudeToken)
|
|
1732
|
+
note('Claude token', 'already', 'stored in custody');
|
|
1733
|
+
else if (await ask('Store a Claude token now (run `claude setup-token` in another terminal and paste it)?')) {
|
|
1734
|
+
try {
|
|
1735
|
+
const token = (await deps.readSecret('Paste the token (hidden): ')).trim();
|
|
1736
|
+
if (!token)
|
|
1737
|
+
note('Claude token', 'failed', 'nothing was pasted');
|
|
1738
|
+
else {
|
|
1739
|
+
await deps.custody.storeClaudeToken(token);
|
|
1740
|
+
note('Claude token', 'done', 'stored in custody (never written to disk here)');
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
catch (error) {
|
|
1744
|
+
note('Claude token', 'failed', `not stored (${failure(error)}) — run \`claude setup-token\`, then rerun setup`);
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
else
|
|
1748
|
+
note('Claude token', dryRun ? 'skipped' : 'waiting-on-you', 'run `claude setup-token`, then rerun setup');
|
|
1749
|
+
// 6. Canary repo (the App cannot write workflows, so this uses your own gh
|
|
1750
|
+
// auth). Before the rulesets: the grant's server-enforced repos include the
|
|
1751
|
+
// canary, and a ruleset cannot be applied to a repo that does not exist yet.
|
|
1752
|
+
current = 'canary repo';
|
|
1753
|
+
if (canaryExists)
|
|
1754
|
+
note('canary repo', 'already', `${canaryRepo} exists`);
|
|
1755
|
+
else if (await ask(`Create the public repo ${canaryRepo} with its CI workflow (your gh auth)?`)) {
|
|
1756
|
+
const created = await deps.run('gh', ['repo', 'create', canaryRepo, '--public', '--add-readme', '--description', 'ashlr fleet canary (revert drills)']);
|
|
1757
|
+
const workflow = created.status === 0
|
|
1758
|
+
? await deps.run('gh', ['api', '--method', 'PUT', `repos/${canaryRepo}/contents/.github/workflows/ci.yml`, '--input', '-'], {
|
|
1759
|
+
input: JSON.stringify({ message: 'ci: canary workflow', content: Buffer.from(CANARY_WORKFLOW, 'utf8').toString('base64') }),
|
|
1760
|
+
})
|
|
1761
|
+
: null;
|
|
1762
|
+
canaryExists = created.status === 0;
|
|
1763
|
+
note('canary repo', created.status === 0 && workflow?.status === 0 ? 'done' : 'failed', created.status === 0 ? (workflow?.status === 0 ? 'created with its CI workflow' : 'created, but the workflow push failed') : created.stderr.trim().slice(0, 200));
|
|
1764
|
+
}
|
|
1765
|
+
else
|
|
1766
|
+
note('canary repo', dryRun ? 'skipped' : 'waiting-on-you', `create ${canaryRepo} with a CI workflow`);
|
|
1767
|
+
// 7. Rulesets — only where GitHub does not already hold exactly this one.
|
|
1768
|
+
current = 'rulesets';
|
|
1769
|
+
if (planning) {
|
|
1770
|
+
note('rulesets', 'skipped', 'would check the fleet ruleset on the server-enforced repos and apply it where it is missing or different');
|
|
1771
|
+
}
|
|
1772
|
+
else {
|
|
1773
|
+
const targets = (await setupTargetRepos()).filter((repo) => canaryExists || repo.nameWithOwner.toLowerCase() !== canaryRepo);
|
|
1774
|
+
const plans = await protectPlans(parsed, deps, targets);
|
|
1775
|
+
const stale = plans.filter((plan) => !plan.skipped && !plan.upToDate);
|
|
1776
|
+
const inPlace = plans.filter((plan) => !plan.skipped && plan.upToDate).length;
|
|
1777
|
+
const unreadable = plans.filter((plan) => plan.skipped?.startsWith('could not read')).map((plan) => plan.repo);
|
|
1778
|
+
const unavailable = plans.filter((plan) => plan.unavailable === true);
|
|
1779
|
+
const mismatches = unavailable.map((plan) => plan.mismatch).filter((m) => typeof m === 'string');
|
|
1780
|
+
const byHand = plans.filter((plan) => plan.skipped && !plan.unavailable && !plan.skipped.startsWith('could not read')).length;
|
|
1781
|
+
const unread = unreadable.length > 0 ? `; could not read ${shortList(unreadable)} from GitHub` : '';
|
|
1782
|
+
// A plan limit is not a read failure: say which repos only local enforcement can protect.
|
|
1783
|
+
const noRulesets = unavailable.length > 0
|
|
1784
|
+
? `; rulesets unavailable on this plan for private ${shortList(unavailable.map((plan) => plan.repo))} — ${mismatches.length > 0 ? mismatches.join('; ') : `using local enforcement with ${ASHLR_VERIFY_CHECK}`}`
|
|
1785
|
+
: '';
|
|
1786
|
+
const left = `${byHand > 0 ? `; ${byHand} left to you (local enforcement or no CI)` : ''}${noRulesets}`;
|
|
1787
|
+
const inPlaceText = inPlace > 0 ? `the fleet ruleset is in place on ${inPlace === 1 ? '1 repo' : `${inPlace} repos`}` : 'no repo has the fleet ruleset';
|
|
1788
|
+
if (plans.length === 0) {
|
|
1789
|
+
// Nothing to look at yet — never "in place on 0 repos".
|
|
1790
|
+
if (canaryExists)
|
|
1791
|
+
note('rulesets', 'already', 'no server-enforced repo needs one');
|
|
1792
|
+
else
|
|
1793
|
+
note('rulesets', dryRun ? 'skipped' : 'waiting-on-you', `${canaryRepo} gets the fleet ruleset once it exists`);
|
|
1794
|
+
}
|
|
1795
|
+
else if (stale.length > 0) {
|
|
1796
|
+
const where = shortList(stale.map((plan) => plan.repo));
|
|
1797
|
+
if (await ask(`Apply the fleet ruleset to ${stale.length} repo(s) where it is missing or different (${where})?`)) {
|
|
1798
|
+
const failures = await applyProtectPlans(stale, deps);
|
|
1799
|
+
note('rulesets', failures > 0 ? 'failed' : unreadable.length > 0 || mismatches.length > 0 ? 'waiting-on-you' : 'done', failures === 0
|
|
1800
|
+
? `applied to ${where}${inPlace > 0 ? `; ${inPlace} already in place` : ''}${unread}${left}`
|
|
1801
|
+
: `${failures} of ${stale.length} failed (see above)${unread}${noRulesets}`);
|
|
1802
|
+
}
|
|
1803
|
+
else {
|
|
1804
|
+
note('rulesets', dryRun ? 'skipped' : 'waiting-on-you', `${stale.length} repo(s) need the fleet ruleset (${where})${unread}${noRulesets} — run \`ashlr authority protect --print\`, then \`--apply\``);
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
else if (unreadable.length > 0) {
|
|
1808
|
+
note('rulesets', 'waiting-on-you', `could not read ${shortList(unreadable)} from GitHub — check \`gh auth status\`, then rerun setup${inPlace > 0 ? ` (${inPlace} already in place)` : ''}${noRulesets}`);
|
|
1809
|
+
}
|
|
1810
|
+
else if (mismatches.length > 0) {
|
|
1811
|
+
// The signed grant is never rewritten here: only a re-approval (Touch ID) switches the repo.
|
|
1812
|
+
note('rulesets', 'waiting-on-you', `${inPlaceText}${left}`, { command: 'ashlr authority re-approve' });
|
|
1813
|
+
}
|
|
1814
|
+
else {
|
|
1815
|
+
note('rulesets', 'already', `${inPlaceText}${left}`);
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
// 8. Retire the burned activation directory.
|
|
1819
|
+
current = 'old activation state';
|
|
1820
|
+
const activation = join(homedir(), '.ashlr', 'activation');
|
|
1821
|
+
if (!existsSync(activation))
|
|
1822
|
+
note('old activation state', 'already', 'nothing to retire');
|
|
1823
|
+
else if (await ask('Move ~/.ashlr/activation (the burned key) out of ~/.ashlr so no code path can reach it?')) {
|
|
1824
|
+
try {
|
|
1825
|
+
const target = join(homedir(), `ashlr-activation-retired-${new Date().toISOString().replace(/[:.]/g, '-')}`);
|
|
1826
|
+
renameSync(activation, target);
|
|
1827
|
+
note('old activation state', 'done', `moved to ${target.replace(homedir(), '~')} — move it offline, then delete it`);
|
|
1828
|
+
}
|
|
1829
|
+
catch (error) {
|
|
1830
|
+
note('old activation state', 'failed', `not moved (${failure(error)})`);
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
else
|
|
1834
|
+
note('old activation state', dryRun ? 'skipped' : 'waiting-on-you', 'archive ~/.ashlr/activation offline, then delete it');
|
|
1835
|
+
// 9. Provenance key — rotated ONCE. The burned key is whichever one no
|
|
1836
|
+
// rotation recorded; a key a rotation made (and recorded) is left alone,
|
|
1837
|
+
// because rotating again would invalidate every pending proposal for nothing.
|
|
1838
|
+
current = 'provenance key';
|
|
1839
|
+
const provenance = await provenanceKeyState();
|
|
1840
|
+
if (provenance.state === 'rotated') {
|
|
1841
|
+
note('provenance key', 'already', `rotated ${provenance.rotatedAt.slice(0, 10)}; not rotated again, so pending proposals keep verifying`);
|
|
1842
|
+
}
|
|
1843
|
+
else if (await ask(provenance.state === 'missing'
|
|
1844
|
+
? 'Create the provenance HMAC key now?'
|
|
1845
|
+
: 'Rotate the provenance HMAC key (agents could read the old one)?')) {
|
|
1846
|
+
try {
|
|
1847
|
+
const rotated = await rotateProvenanceKey();
|
|
1848
|
+
note('provenance key', 'done', rotated.retiredAs ? 'rotated; the old key was moved aside' : 'created');
|
|
1849
|
+
}
|
|
1850
|
+
catch (error) {
|
|
1851
|
+
note('provenance key', 'failed', `not rotated (${failure(error)})`);
|
|
1852
|
+
}
|
|
1030
1853
|
}
|
|
1031
|
-
}
|
|
1032
|
-
// 4. GitHub App.
|
|
1033
|
-
if (custody?.githubApp)
|
|
1034
|
-
note('GitHub App', 'already', 'the ashlr-fleet key is in custody');
|
|
1035
|
-
else if (await ask(`Create the ${FLEET_APP_NAME} GitHub App now (one browser page)?`)) {
|
|
1036
|
-
const app = await runGithubAppFlow(deps, { org: one(parsed, '--org') ?? 'ashlrai' });
|
|
1037
|
-
note('GitHub App', 'done', `created ${app.slug}; install it on the repos: ${app.installUrl}`);
|
|
1038
|
-
}
|
|
1039
|
-
else
|
|
1040
|
-
note('GitHub App', dryRun ? 'skipped' : 'waiting-on-you', 'run `ashlr authority github-app`');
|
|
1041
|
-
// 5. Claude token for restricted judge / Leader calls.
|
|
1042
|
-
if (custody?.claudeToken)
|
|
1043
|
-
note('Claude token', 'already', 'stored in custody');
|
|
1044
|
-
else if (await ask('Store a Claude token now (run `claude setup-token` in another terminal and paste it)?')) {
|
|
1045
|
-
const token = (await deps.readSecret('Paste the token (hidden): ')).trim();
|
|
1046
|
-
if (!token)
|
|
1047
|
-
note('Claude token', 'failed', 'nothing was pasted');
|
|
1048
1854
|
else {
|
|
1049
|
-
|
|
1050
|
-
note('
|
|
1855
|
+
const why = provenance.state === 'unhealthy' ? `the current key is unusable (${provenance.reason}); ` : '';
|
|
1856
|
+
note('provenance key', dryRun ? 'skipped' : 'waiting-on-you', `${why}run \`ashlr authority rotate-provenance\``);
|
|
1857
|
+
}
|
|
1858
|
+
// 10. First grant (Touch ID), 11. the switch, 12. the daemon service,
|
|
1859
|
+
// 13. resident admission (docs/RESIDENT-RUNTIME.md). Setup itself never
|
|
1860
|
+
// touches launchd: it names the one command that does.
|
|
1861
|
+
const DAEMON_DETAIL = 'would check that the ai.ashlr.daemon service is loaded and running (each tick re-verifies the grant)';
|
|
1862
|
+
if (planning) {
|
|
1863
|
+
note('standing grant', 'skipped', 'would sign the first standing grant (Touch ID)');
|
|
1864
|
+
note('autonomy switch', 'skipped', 'would offer to set the autonomy switch to Autonomous (the ladder starts in shadow)');
|
|
1865
|
+
note('daemon service', 'skipped', DAEMON_DETAIL);
|
|
1866
|
+
return done();
|
|
1867
|
+
}
|
|
1868
|
+
current = 'standing grant';
|
|
1869
|
+
const { evaluateStandingAuthority, displaySurfaceTarget } = await import('../core/authority/effective-config.js');
|
|
1870
|
+
const evaluation = evaluateStandingAuthority({ mode: 'cached', surface: displaySurfaceTarget() });
|
|
1871
|
+
let grantActive = evaluation.grantState === 'active';
|
|
1872
|
+
if (grantActive) {
|
|
1873
|
+
note('standing grant', 'already', `grant #${evaluation.grant?.grantSeq ?? '?'} is active`);
|
|
1874
|
+
}
|
|
1875
|
+
else if (await ask('Sign the first standing grant now (Touch ID)?')) {
|
|
1876
|
+
try {
|
|
1877
|
+
const { buildStandingGrantDraft } = await import('../core/verse/authority-api.js');
|
|
1878
|
+
const draft = await buildStandingGrantDraft('auto', Date.now(), { githubGet: ghGet(deps) });
|
|
1879
|
+
for (const line of draft.summary)
|
|
1880
|
+
deps.out(` ${line}`);
|
|
1881
|
+
const signed = await signAndInstall(draft.payload, deps);
|
|
1882
|
+
grantActive = signed.ok;
|
|
1883
|
+
note('standing grant', signed.ok ? 'done' : 'failed', signed.ok ? `grant #${signed.grant.grantSeq} installed, starting at ${signed.grant.rollout.stages[0].id}` : signed.reason);
|
|
1884
|
+
}
|
|
1885
|
+
catch (error) {
|
|
1886
|
+
note('standing grant', 'failed', `no grant was drafted (${failure(error)}) — rerun setup, or run \`ashlr authority grant\``);
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
else
|
|
1890
|
+
note('standing grant', dryRun ? 'skipped' : 'waiting-on-you', 'run `ashlr authority grant`');
|
|
1891
|
+
// Resumable: a grant signed on an earlier run (or with `authority grant`)
|
|
1892
|
+
// still gets the switch offered — it used to be offered only in the run
|
|
1893
|
+
// that signed. Raising stays capped by the grant (requestAutonomySwitch).
|
|
1894
|
+
current = 'autonomy switch';
|
|
1895
|
+
if (!grantActive) {
|
|
1896
|
+
note('autonomy switch', dryRun ? 'skipped' : 'waiting-on-you', 'needs an active standing grant first');
|
|
1897
|
+
}
|
|
1898
|
+
else if (evaluation.switch === 'autonomous') {
|
|
1899
|
+
note('autonomy switch', 'already', 'Autonomous');
|
|
1900
|
+
}
|
|
1901
|
+
else if (await ask('Set the autonomy switch to Autonomous (the ladder starts in shadow)?')) {
|
|
1902
|
+
const { requestAutonomySwitch } = await import('../core/authority/effective-config.js');
|
|
1903
|
+
const switched = requestAutonomySwitch('autonomous', 'mason', 'ashlr authority setup');
|
|
1904
|
+
note('autonomy switch', switched.ok ? 'done' : 'failed', switched.ok ? 'Autonomous' : switched.reason);
|
|
1905
|
+
}
|
|
1906
|
+
else
|
|
1907
|
+
note('autonomy switch', dryRun ? 'skipped' : 'waiting-on-you', `it is ${evaluation.switch} — run \`ashlr authority switch autonomous\``);
|
|
1908
|
+
// 12. Read-only service observation. Only `resident start` (Mason, in his
|
|
1909
|
+
// own terminal, under the active grant) installs or restarts the service.
|
|
1910
|
+
current = 'daemon service';
|
|
1911
|
+
let service;
|
|
1912
|
+
try {
|
|
1913
|
+
service = await deps.daemonService();
|
|
1914
|
+
}
|
|
1915
|
+
catch {
|
|
1916
|
+
service = 'unknown';
|
|
1917
|
+
}
|
|
1918
|
+
const START = `\`${RESIDENT_START_COMMAND}\``;
|
|
1919
|
+
const after = grantActive ? `run ${START}` : `after the standing grant, run ${START}`;
|
|
1920
|
+
// Before the grant, the command that moves this step on is the grant.
|
|
1921
|
+
const serviceCommand = grantActive ? {} : { command: 'ashlr authority grant' };
|
|
1922
|
+
if (service === 'running')
|
|
1923
|
+
note('daemon service', 'already', 'ai.ashlr.daemon is running (each tick re-verifies the grant)');
|
|
1924
|
+
else if (service === 'absent')
|
|
1925
|
+
note('daemon service', 'waiting-on-you', `no ai.ashlr.daemon service is installed — ${after} to install it`, serviceCommand);
|
|
1926
|
+
else if (service === 'loaded')
|
|
1927
|
+
note('daemon service', 'waiting-on-you', `ai.ashlr.daemon is loaded but stopped — ${after} to restart it`, serviceCommand);
|
|
1928
|
+
else if (service === 'not-loaded')
|
|
1929
|
+
note('daemon service', 'waiting-on-you', `ai.ashlr.daemon is not loaded — ${after} to load it`, serviceCommand);
|
|
1930
|
+
else
|
|
1931
|
+
note('daemon service', 'waiting-on-you', `ai.ashlr.daemon state is unknown — \`ashlr authority resident status\` shows why; ${after} to repair it`, serviceCommand);
|
|
1932
|
+
// 13. Resident admission: the grant, this release and the service together.
|
|
1933
|
+
current = 'resident runtime';
|
|
1934
|
+
if (!grantActive) {
|
|
1935
|
+
note('resident runtime', 'blocked', RESIDENT_NEEDS_GRANT, { command: 'ashlr authority grant' });
|
|
1936
|
+
}
|
|
1937
|
+
else {
|
|
1938
|
+
const resident = await residentSetupRow(deps, service);
|
|
1939
|
+
note('resident runtime', resident.status, resident.detail, { command: resident.command });
|
|
1051
1940
|
}
|
|
1052
1941
|
}
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
// canary, and a ruleset cannot be applied to a repo that does not exist yet.
|
|
1058
|
-
const canary = planning ? null : deps.run('gh', ['api', 'repos/ashlrai/fleet-canary']);
|
|
1059
|
-
if (canary?.status === 0)
|
|
1060
|
-
note('canary repo', 'already', 'ashlrai/fleet-canary exists');
|
|
1061
|
-
else if (await ask('Create the public repo ashlrai/fleet-canary with its CI workflow (your gh auth)?')) {
|
|
1062
|
-
const created = deps.run('gh', ['repo', 'create', 'ashlrai/fleet-canary', '--public', '--add-readme', '--description', 'ashlr fleet canary (revert drills)']);
|
|
1063
|
-
const workflow = created.status === 0
|
|
1064
|
-
? deps.run('gh', ['api', '--method', 'PUT', 'repos/ashlrai/fleet-canary/contents/.github/workflows/ci.yml', '--input', '-'], {
|
|
1065
|
-
input: JSON.stringify({ message: 'ci: canary workflow', content: Buffer.from(CANARY_WORKFLOW, 'utf8').toString('base64') }),
|
|
1066
|
-
})
|
|
1067
|
-
: null;
|
|
1068
|
-
note('canary repo', created.status === 0 && workflow?.status === 0 ? 'done' : 'failed', created.status === 0 ? (workflow?.status === 0 ? 'created with its CI workflow' : 'created, but the workflow push failed') : created.stderr.trim().slice(0, 200));
|
|
1942
|
+
catch (error) {
|
|
1943
|
+
// Nothing above is expected to throw; if something does, it is this
|
|
1944
|
+
// step's failure and the run still ends with the summary.
|
|
1945
|
+
note(report.some((row) => row.step === current) ? 'setup' : current, 'failed', `stopped: ${failure(error)}`);
|
|
1069
1946
|
}
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1947
|
+
return done();
|
|
1948
|
+
}
|
|
1949
|
+
/**
|
|
1950
|
+
* Setup's resident step (docs/RESIDENT-RUNTIME.md §d):
|
|
1951
|
+
* already admitted AND ai.ashlr.daemon running AND its plist is what
|
|
1952
|
+
* `resident start` would write from config now;
|
|
1953
|
+
* waiting-on-you the exact command that gets there (start / clear-stop / switch);
|
|
1954
|
+
* blocked a prerequisite is missing (no active grant, not a compiled
|
|
1955
|
+
* release, dirty build, no confinement, not macOS).
|
|
1956
|
+
* Read-only: never mints, never touches launchd.
|
|
1957
|
+
*/
|
|
1958
|
+
async function residentSetupRow(deps, service) {
|
|
1959
|
+
let admission;
|
|
1960
|
+
try {
|
|
1961
|
+
admission = await deps.resident.admission();
|
|
1076
1962
|
}
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
// 8. Retire the burned activation directory.
|
|
1080
|
-
const activation = join(homedir(), '.ashlr', 'activation');
|
|
1081
|
-
if (!existsSync(activation))
|
|
1082
|
-
note('old activation state', 'already', 'nothing to retire');
|
|
1083
|
-
else if (await ask('Move ~/.ashlr/activation (the burned key) out of ~/.ashlr so no code path can reach it?')) {
|
|
1084
|
-
const target = join(homedir(), `ashlr-activation-retired-${new Date().toISOString().replace(/[:.]/g, '-')}`);
|
|
1085
|
-
renameSync(activation, target);
|
|
1086
|
-
note('old activation state', 'done', `moved to ${target.replace(homedir(), '~')} — move it offline, then delete it`);
|
|
1963
|
+
catch (error) {
|
|
1964
|
+
return { status: 'failed', detail: `resident admission could not be evaluated (${error.message})` };
|
|
1087
1965
|
}
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
const rotated = await rotateProvenanceKey();
|
|
1093
|
-
note('provenance key', 'done', rotated.retiredAs ? 'rotated; the old key was moved aside' : 'created');
|
|
1966
|
+
if (!admission.ok) {
|
|
1967
|
+
const fix = admission.command ? ` — run \`${admission.command}\`` : '';
|
|
1968
|
+
// The checklist's command is exactly what the admission asks for (none when it names none).
|
|
1969
|
+
return { status: admission.status === 'waiting-on-you' ? 'waiting-on-you' : 'blocked', detail: `${admission.reason}${fix}`, command: admission.command ?? null };
|
|
1094
1970
|
}
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
// 13. production resident admission. A running legacy service is not proof
|
|
1099
|
-
// that its execution gate or native release boundary is available.
|
|
1100
|
-
const DAEMON_DETAIL = 'would check that the ai.ashlr.daemon service is loaded and running (each tick re-verifies the grant)';
|
|
1101
|
-
if (planning) {
|
|
1102
|
-
note('standing grant', 'skipped', 'would sign the first standing grant (Touch ID)');
|
|
1103
|
-
note('autonomy switch', 'skipped', 'would offer to set the autonomy switch to Autonomous (the ladder starts in shadow)');
|
|
1104
|
-
note('daemon service', 'skipped', DAEMON_DETAIL);
|
|
1105
|
-
return done();
|
|
1106
|
-
}
|
|
1107
|
-
const { evaluateStandingAuthority, displaySurfaceTarget } = await import('../core/authority/effective-config.js');
|
|
1108
|
-
const evaluation = evaluateStandingAuthority({ mode: 'cached', surface: displaySurfaceTarget() });
|
|
1109
|
-
let grantActive = evaluation.grantState === 'active';
|
|
1110
|
-
if (grantActive) {
|
|
1111
|
-
note('standing grant', 'already', `grant #${evaluation.grant?.grantSeq ?? '?'} is active`);
|
|
1112
|
-
}
|
|
1113
|
-
else if (await ask('Sign the first standing grant now (Touch ID)?')) {
|
|
1114
|
-
const { buildStandingGrantDraft } = await import('../core/verse/authority-api.js');
|
|
1115
|
-
const draft = await buildStandingGrantDraft('auto');
|
|
1116
|
-
for (const line of draft.summary)
|
|
1117
|
-
deps.out(` ${line}`);
|
|
1118
|
-
const signed = await signAndInstall(draft.payload, deps);
|
|
1119
|
-
grantActive = signed.ok;
|
|
1120
|
-
note('standing grant', signed.ok ? 'done' : 'failed', signed.ok ? `grant #${signed.grant.grantSeq} installed, starting at ${signed.grant.rollout.stages[0].id}` : signed.reason);
|
|
1971
|
+
let observed;
|
|
1972
|
+
try {
|
|
1973
|
+
observed = await deps.resident.observe();
|
|
1121
1974
|
}
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
// Resumable: a grant signed on an earlier run (or with `authority grant`)
|
|
1125
|
-
// still gets the switch offered — it used to be offered only in the run
|
|
1126
|
-
// that signed. Raising stays capped by the grant (requestAutonomySwitch).
|
|
1127
|
-
if (!grantActive) {
|
|
1128
|
-
note('autonomy switch', dryRun ? 'skipped' : 'waiting-on-you', 'needs an active standing grant first');
|
|
1975
|
+
catch (error) {
|
|
1976
|
+
return { status: 'failed', detail: `the resident service could not be observed (${error.message})` };
|
|
1129
1977
|
}
|
|
1130
|
-
|
|
1131
|
-
|
|
1978
|
+
if (observed.problem)
|
|
1979
|
+
return { status: 'blocked', detail: observed.problem, command: null };
|
|
1980
|
+
const start = '`ashlr authority resident start`';
|
|
1981
|
+
if (service !== 'running') {
|
|
1982
|
+
return { status: 'waiting-on-you', detail: `admitted under grant #${admission.grantSeq ?? '?'} but the resident daemon is not running — run ${start} in your own terminal` };
|
|
1132
1983
|
}
|
|
1133
|
-
|
|
1134
|
-
const
|
|
1135
|
-
|
|
1136
|
-
|
|
1984
|
+
if (observed.plist === 'drifted') {
|
|
1985
|
+
const budget = observed.installedBudgetUsd !== observed.expectedBudgetUsd
|
|
1986
|
+
? ` (installed budget $${observed.installedBudgetUsd ?? '?'}/day, config says $${observed.expectedBudgetUsd ?? '?'}/day)`
|
|
1987
|
+
: '';
|
|
1988
|
+
return { status: 'waiting-on-you', detail: `the running service's plist differs from config${budget} — run ${start} to regenerate and restart it` };
|
|
1137
1989
|
}
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
// 12. Read-only service observation. Manual launchctl advice would imply
|
|
1141
|
-
// that this build can admit a resident start, which its production gate denies.
|
|
1142
|
-
let service;
|
|
1143
|
-
try {
|
|
1144
|
-
service = await deps.daemonService();
|
|
1990
|
+
if (observed.plist !== 'current') {
|
|
1991
|
+
return { status: 'waiting-on-you', detail: `the running service's plist could not be matched (${observed.plist}) — run ${start} to regenerate it` };
|
|
1145
1992
|
}
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1993
|
+
return { status: 'already', detail: `the resident daemon is running under grant #${admission.grantSeq ?? '?'} (until ${admission.expiresAt ?? '?'}), from clean release ${admission.revision?.slice(0, 12) ?? '?'}` };
|
|
1994
|
+
}
|
|
1995
|
+
// ---------------------------------------------------------------------------
|
|
1996
|
+
// resident start | stop | status
|
|
1997
|
+
// ---------------------------------------------------------------------------
|
|
1998
|
+
function describeAdmission(admission) {
|
|
1999
|
+
if (admission.ok)
|
|
2000
|
+
return `admitted — ${admission.reason}`;
|
|
2001
|
+
return `${admission.status} — ${admission.reason}${admission.command ? ` (run \`${admission.command}\`)` : ''}`;
|
|
2002
|
+
}
|
|
2003
|
+
async function cmdResident(sub, parsed, deps) {
|
|
2004
|
+
switch (sub) {
|
|
2005
|
+
case 'status':
|
|
2006
|
+
return cmdResidentStatus(parsed, deps);
|
|
2007
|
+
case 'start':
|
|
2008
|
+
return cmdResidentStart(parsed, deps);
|
|
2009
|
+
case 'stop':
|
|
2010
|
+
return cmdResidentStop(deps);
|
|
2011
|
+
default:
|
|
2012
|
+
deps.err('Usage: ashlr authority resident <start|stop|status [--json]>');
|
|
2013
|
+
return 2;
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
async function cmdResidentStatus(parsed, deps) {
|
|
2017
|
+
const admission = await deps.resident.admission();
|
|
2018
|
+
const observed = await deps.resident.observe();
|
|
2019
|
+
if (parsed.flags.has('--json')) {
|
|
2020
|
+
deps.out(JSON.stringify({ schema: 'ashlr.resident-status.v1', admission, service: observed }, null, 2));
|
|
2021
|
+
return 0;
|
|
2022
|
+
}
|
|
2023
|
+
deps.out(`Resident admission: ${describeAdmission(admission)}`);
|
|
2024
|
+
deps.out(`Service: ai.ashlr.daemon ${observed.state}${observed.plistPath ? ` (${observed.plistPath.replace(homedir(), '~')})` : ''}`);
|
|
2025
|
+
const budget = `installed $${observed.installedBudgetUsd ?? '—'}/day, config $${observed.expectedBudgetUsd ?? '?'}/day`;
|
|
2026
|
+
deps.out(`Plist: ${observed.plist}${observed.plist === 'drifted' ? ' — `ashlr authority resident start` regenerates it' : ''} (budget: ${budget})`);
|
|
2027
|
+
if (observed.problem)
|
|
2028
|
+
deps.out(` ${observed.problem}`);
|
|
2029
|
+
return 0;
|
|
2030
|
+
}
|
|
2031
|
+
/**
|
|
2032
|
+
* `resident start` — the only command that installs or restarts the resident
|
|
2033
|
+
* service. Order: refuse a non-operator context; show the verdict; stop if
|
|
2034
|
+
* not admitted; no-op when already running from the current plist; show
|
|
2035
|
+
* exactly what will happen and ask (no --yes: the operator confirms here);
|
|
2036
|
+
* then the real start, which re-verifies everything itself (mint).
|
|
2037
|
+
*/
|
|
2038
|
+
async function cmdResidentStart(parsed, deps) {
|
|
2039
|
+
if (parsed.flags.has('--yes')) {
|
|
2040
|
+
deps.err('resident start takes no --yes: confirm it yourself at the prompt.');
|
|
2041
|
+
return 2;
|
|
2042
|
+
}
|
|
2043
|
+
const refusal = await deps.resident.operatorRefusal();
|
|
2044
|
+
if (refusal) {
|
|
2045
|
+
deps.err(`resident start refused: ${refusal}`);
|
|
2046
|
+
return 1;
|
|
2047
|
+
}
|
|
2048
|
+
const admission = await deps.resident.admission();
|
|
2049
|
+
if (!admission.ok) {
|
|
2050
|
+
deps.err(`resident start refused: ${describeAdmission(admission)}`);
|
|
2051
|
+
return 1;
|
|
2052
|
+
}
|
|
2053
|
+
const observed = await deps.resident.observe();
|
|
2054
|
+
if (observed.problem) {
|
|
2055
|
+
deps.err(`resident start refused: ${observed.problem}`);
|
|
2056
|
+
return 1;
|
|
2057
|
+
}
|
|
2058
|
+
if (observed.state === 'running' && observed.plist === 'current') {
|
|
2059
|
+
deps.out(`ai.ashlr.daemon is already running under grant #${admission.grantSeq ?? '?'} from the current plist; nothing to do.`);
|
|
2060
|
+
return 0;
|
|
2061
|
+
}
|
|
2062
|
+
const action = observed.state === 'running' ? 'regenerate the plist from config and restart' : observed.plist === 'absent' ? 'install and start' : 'regenerate, load and start';
|
|
2063
|
+
deps.out(`Resident daemon under grant #${admission.grantSeq ?? '?'} (expires ${admission.expiresAt ?? '?'}):`);
|
|
2064
|
+
deps.out(` release ${admission.revision ?? '?'} (${admission.packageRoot?.replace(homedir(), '~') ?? '?'})`);
|
|
2065
|
+
deps.out(` service ai.ashlr.daemon → ${observed.plistPath?.replace(homedir(), '~') ?? '?'} (${observed.state}, plist ${observed.plist})`);
|
|
2066
|
+
deps.out(` budget $${observed.expectedBudgetUsd ?? '?'}/day from config daemon.dailyBudgetUsd${observed.installedBudgetUsd !== null && observed.installedBudgetUsd !== observed.expectedBudgetUsd ? ` (installed: $${observed.installedBudgetUsd})` : ''}`);
|
|
2067
|
+
deps.out(' launchd enable + bootstrap (RunAtLoad); every tick re-verifies the grant, Stop and the switch');
|
|
2068
|
+
if (!(await deps.confirm(`${action[0].toUpperCase()}${action.slice(1)} ai.ashlr.daemon now?`))) {
|
|
2069
|
+
deps.out('Nothing was changed.');
|
|
2070
|
+
return 1;
|
|
2071
|
+
}
|
|
2072
|
+
const result = await deps.resident.start();
|
|
2073
|
+
if (!result.ok) {
|
|
2074
|
+
deps.err(`resident start failed: ${result.reason}`);
|
|
2075
|
+
return 1;
|
|
2076
|
+
}
|
|
2077
|
+
deps.out(`✓ ${result.detail}`);
|
|
2078
|
+
deps.out('Stop anytime: `ashlr authority stop` halts agents (the service stays up, idle); `ashlr authority resident stop` removes the service.');
|
|
2079
|
+
return 0;
|
|
2080
|
+
}
|
|
2081
|
+
/** Lowering: never asks, never needs the grant. */
|
|
2082
|
+
async function cmdResidentStop(deps) {
|
|
2083
|
+
const result = await deps.resident.stop();
|
|
2084
|
+
if (!result.ok) {
|
|
2085
|
+
deps.err(`resident stop failed: ${result.reason}`);
|
|
2086
|
+
return 1;
|
|
2087
|
+
}
|
|
2088
|
+
deps.out(`${result.detail}. Stop (~/.ashlr/KILL) and the grant are unchanged.`);
|
|
2089
|
+
return 0;
|
|
1160
2090
|
}
|
|
1161
2091
|
function finish(report, deps, dryRun = false, json = false) {
|
|
1162
2092
|
const result = authoritySetupReport(report, dryRun);
|
|
@@ -1166,10 +2096,54 @@ function finish(report, deps, dryRun = false, json = false) {
|
|
|
1166
2096
|
}
|
|
1167
2097
|
else {
|
|
1168
2098
|
const planned = dryRun ? `, ${summary.planned} planned (dry run: nothing was asked or changed)` : '';
|
|
1169
|
-
deps.out(`\nSetup: ${summary.done} done, ${summary.already} already in place, ${summary.waitingOnYou} waiting on you, ${summary.blocked} blocked
|
|
2099
|
+
deps.out(`\nSetup: ${summary.done} done, ${summary.already} already in place, ${summary.waitingOnYou} waiting on you, ${summary.blocked} blocked on a prerequisite, ${summary.failed} failed${planned}.`);
|
|
1170
2100
|
}
|
|
1171
2101
|
return summary.failed > 0 ? 1 : 0;
|
|
1172
2102
|
}
|
|
2103
|
+
const HUB_REPO = 'ashlrai/ashlr-hub';
|
|
2104
|
+
/**
|
|
2105
|
+
* The dry run's look at the trust-root PR, through `gh api <path>` GETs only
|
|
2106
|
+
* (each passes isReadOnlyGhApiCall, so the Verse probe can run it): the PR an
|
|
2107
|
+
* earlier run opened, the key already merged on the default branch, or a
|
|
2108
|
+
* branch an earlier run pushed without a PR. Same order as the live run's
|
|
2109
|
+
* probes. null = none of these (or GitHub could not be read): the step then
|
|
2110
|
+
* reads as it always has.
|
|
2111
|
+
*/
|
|
2112
|
+
async function probeTrustRootReadOnly(deps, root) {
|
|
2113
|
+
const branch = `authority/trust-root-${root.keyId}`;
|
|
2114
|
+
const read = async (path) => {
|
|
2115
|
+
const result = await deps.run('gh', ['api', path]);
|
|
2116
|
+
if (result.status !== 0)
|
|
2117
|
+
return null;
|
|
2118
|
+
try {
|
|
2119
|
+
return JSON.parse(result.stdout || 'null');
|
|
2120
|
+
}
|
|
2121
|
+
catch {
|
|
2122
|
+
return null;
|
|
2123
|
+
}
|
|
2124
|
+
};
|
|
2125
|
+
const open = await read(`repos/${HUB_REPO}/pulls?head=${HUB_REPO.split('/')[0]}%3A${encodeURIComponent(branch)}&state=open&per_page=1`);
|
|
2126
|
+
const url = Array.isArray(open) ? open[0]?.html_url : undefined;
|
|
2127
|
+
if (typeof url === 'string' && /^https:\/\/github\.com\/[^\s]+\/pull\/\d+$/u.test(url)) {
|
|
2128
|
+
return { status: 'waiting-on-you', detail: `${url} is already open — review and merge it yourself`, link: url, command: null };
|
|
2129
|
+
}
|
|
2130
|
+
const repo = await read(`repos/${HUB_REPO}`);
|
|
2131
|
+
const base = typeof repo?.default_branch === 'string' && /^[A-Za-z0-9._/-]{1,100}$/u.test(repo.default_branch) ? repo.default_branch : 'master';
|
|
2132
|
+
const file = await read(`repos/${HUB_REPO}/contents/src/core/authority/trust-roots.ts?ref=${encodeURIComponent(base)}`);
|
|
2133
|
+
if (file && file.encoding === 'base64' && typeof file.content === 'string'
|
|
2134
|
+
&& Buffer.from(file.content, 'base64').toString('utf8').includes(`keyId: '${root.keyId}'`)) {
|
|
2135
|
+
return {
|
|
2136
|
+
status: 'waiting-on-you',
|
|
2137
|
+
detail: `${root.keyId} is merged on ${base}, but this release was built before it — install a release built after it`,
|
|
2138
|
+
command: 'npm run build',
|
|
2139
|
+
};
|
|
2140
|
+
}
|
|
2141
|
+
const pushed = await read(`repos/${HUB_REPO}/git/ref/heads/${branch.split('/').map(encodeURIComponent).join('/')}`);
|
|
2142
|
+
if (pushed && pushed.ref === `refs/heads/${branch}`) {
|
|
2143
|
+
return { status: 'waiting-on-you', detail: `${branch} was pushed by an earlier run but has no PR yet — rerun setup to open it` };
|
|
2144
|
+
}
|
|
2145
|
+
return null;
|
|
2146
|
+
}
|
|
1173
2147
|
/**
|
|
1174
2148
|
* Open Mason's trust-root PR from a throwaway worktree (his checkout is never
|
|
1175
2149
|
* touched). Resumable: a rerun finds the PR it already opened, or the key
|
|
@@ -1180,73 +2154,76 @@ async function openTrustRootPr(parsed, deps, root, ask, probe) {
|
|
|
1180
2154
|
if (!source)
|
|
1181
2155
|
return { status: 'waiting-on-you', detail: 'pass --source <your ashlr-hub checkout> so setup can open the trust-root PR' };
|
|
1182
2156
|
const branch = `authority/trust-root-${root.keyId}`;
|
|
1183
|
-
const base = () => {
|
|
1184
|
-
const result = deps.run('gh', ['repo', 'view', 'ashlrai/ashlr-hub', '--json', 'defaultBranchRef', '--jq', '.defaultBranchRef.name']);
|
|
2157
|
+
const base = async () => {
|
|
2158
|
+
const result = await deps.run('gh', ['repo', 'view', 'ashlrai/ashlr-hub', '--json', 'defaultBranchRef', '--jq', '.defaultBranchRef.name']);
|
|
1185
2159
|
return result.status === 0 && result.stdout.trim() ? result.stdout.trim() : 'master';
|
|
1186
2160
|
};
|
|
2161
|
+
const prLink = (text) => /https:\/\/github\.com\/\S+\/pull\/\d+/u.exec(text)?.[0];
|
|
1187
2162
|
let baseBranch = null;
|
|
1188
2163
|
let pushedAlready = false;
|
|
1189
2164
|
if (probe) {
|
|
1190
2165
|
// Read-only probes (a dry run never makes them).
|
|
1191
|
-
const open = deps.run('gh', ['pr', 'list', '--repo', 'ashlrai/ashlr-hub', '--head', branch, '--state', 'open', '--json', 'url', '--jq', '.[0].url // ""']);
|
|
2166
|
+
const open = await deps.run('gh', ['pr', 'list', '--repo', 'ashlrai/ashlr-hub', '--head', branch, '--state', 'open', '--json', 'url', '--jq', '.[0].url // ""']);
|
|
1192
2167
|
if (open.status === 0 && open.stdout.trim()) {
|
|
1193
|
-
|
|
2168
|
+
const url = open.stdout.trim();
|
|
2169
|
+
return { status: 'waiting-on-you', detail: `${url} is already open — review and merge it yourself`, ...(prLink(url) ? { link: prLink(url) } : {}) };
|
|
1194
2170
|
}
|
|
1195
|
-
baseBranch = base();
|
|
1196
|
-
const fetched = deps.run('git', ['-C', source, 'fetch', 'origin', baseBranch]);
|
|
2171
|
+
baseBranch = await base();
|
|
2172
|
+
const fetched = await deps.run('git', ['-C', source, 'fetch', 'origin', baseBranch]);
|
|
1197
2173
|
const onBase = fetched.status === 0
|
|
1198
|
-
? deps.run('git', ['-C', source, 'show', `origin/${baseBranch}:src/core/authority/trust-roots.ts`])
|
|
2174
|
+
? await deps.run('git', ['-C', source, 'show', `origin/${baseBranch}:src/core/authority/trust-roots.ts`])
|
|
1199
2175
|
: null;
|
|
1200
2176
|
if (onBase?.status === 0 && onBase.stdout.includes(`keyId: '${root.keyId}'`)) {
|
|
1201
2177
|
return { status: 'waiting-on-you', detail: `${root.keyId} is already on ${baseBranch}, but this release was built before it` };
|
|
1202
2178
|
}
|
|
1203
2179
|
// An earlier run pushed the branch but `gh pr create` failed: open the PR from it.
|
|
1204
|
-
const remote = deps.run('git', ['-C', source, 'ls-remote', '--exit-code', '--heads', 'origin', branch]);
|
|
2180
|
+
const remote = await deps.run('git', ['-C', source, 'ls-remote', '--exit-code', '--heads', 'origin', branch]);
|
|
1205
2181
|
pushedAlready = remote.status === 0 && remote.stdout.trim() !== '';
|
|
1206
2182
|
}
|
|
1207
2183
|
if (!(await ask(`Open a PR in ${source.replace(homedir(), '~')} adding key ${root.keyId} to trust-roots.ts?`))) {
|
|
1208
2184
|
return { status: 'waiting-on-you', detail: `add this root yourself: ${JSON.stringify({ keyId: root.keyId, alg: 'ES256' })}` };
|
|
1209
2185
|
}
|
|
1210
|
-
baseBranch ??= base();
|
|
1211
|
-
const createPr = (cwd) => {
|
|
1212
|
-
const pr = deps.run('gh', ['pr', 'create', '--repo', 'ashlrai/ashlr-hub', '--base', baseBranch, '--head', branch,
|
|
2186
|
+
baseBranch ??= await base();
|
|
2187
|
+
const createPr = async (cwd) => {
|
|
2188
|
+
const pr = await deps.run('gh', ['pr', 'create', '--repo', 'ashlrai/ashlr-hub', '--base', baseBranch, '--head', branch,
|
|
1213
2189
|
'--title', `authority: trust custody key ${root.keyId}`,
|
|
1214
2190
|
'--body', `Adds the Secure Enclave custody key \`${root.keyId}\` to STANDING_GRANT_TRUST_ROOTS (opened by \`ashlr authority setup\`). Tier-1 path: review it yourself.`], cwd ? { cwd } : {});
|
|
1215
2191
|
if (pr.status !== 0)
|
|
1216
2192
|
return { status: 'failed', detail: `pushed ${branch}, but the PR could not be opened: ${pr.stderr.trim().slice(0, 200)}` };
|
|
1217
|
-
|
|
2193
|
+
const url = pr.stdout.trim();
|
|
2194
|
+
return { status: 'waiting-on-you', detail: `opened ${url} — review and merge it yourself`, ...(prLink(url) ? { link: prLink(url) } : {}) };
|
|
1218
2195
|
};
|
|
1219
2196
|
if (pushedAlready)
|
|
1220
2197
|
return createPr();
|
|
1221
2198
|
const worktree = mkdtempSync(join(tmpdir(), 'ashlr-trust-root-'));
|
|
1222
2199
|
rmSync(worktree, { recursive: true, force: true });
|
|
1223
|
-
const step = (bin, args, cwd) => {
|
|
1224
|
-
const result = deps.run(bin, args, cwd ? { cwd } : {});
|
|
2200
|
+
const step = async (bin, args, cwd) => {
|
|
2201
|
+
const result = await deps.run(bin, args, cwd ? { cwd } : {});
|
|
1225
2202
|
if (result.status !== 0)
|
|
1226
2203
|
throw new Error(`${bin} ${args[0] === '-C' ? args[2] : args[0]} failed: ${result.stderr.trim().slice(0, 200)}`);
|
|
1227
2204
|
};
|
|
1228
2205
|
let worktreeAdded = false;
|
|
1229
2206
|
try {
|
|
1230
|
-
step('git', ['-C', source, 'fetch', 'origin', baseBranch]);
|
|
2207
|
+
await step('git', ['-C', source, 'fetch', 'origin', baseBranch]);
|
|
1231
2208
|
// -B: a local branch left by an earlier run that stopped before the PR
|
|
1232
2209
|
// (push or `gh pr create` failed) is reset instead of blocking every rerun.
|
|
1233
|
-
step('git', ['-C', source, 'worktree', 'add', '-B', branch, worktree, `origin/${baseBranch}`]);
|
|
2210
|
+
await step('git', ['-C', source, 'worktree', 'add', '-B', branch, worktree, `origin/${baseBranch}`]);
|
|
1234
2211
|
worktreeAdded = true;
|
|
1235
2212
|
const file = join(worktree, 'src', 'core', 'authority', 'trust-roots.ts');
|
|
1236
2213
|
const updated = renderTrustRootsWithKey(readFileSync(file, 'utf8'), root);
|
|
1237
2214
|
if (!updated)
|
|
1238
2215
|
return { status: 'failed', detail: `trust-roots.ts on ${baseBranch} already lists a different root — add the key by hand` };
|
|
1239
2216
|
writeFileSync(file, updated);
|
|
1240
|
-
step('git', ['add', 'src/core/authority/trust-roots.ts'], worktree);
|
|
1241
|
-
step('git', ['commit', '-m', `authority: trust custody key ${root.keyId}`], worktree);
|
|
1242
|
-
step('git', ['push', '-u', 'origin', branch], worktree);
|
|
1243
|
-
return createPr(worktree);
|
|
2217
|
+
await step('git', ['add', 'src/core/authority/trust-roots.ts'], worktree);
|
|
2218
|
+
await step('git', ['commit', '-m', `authority: trust custody key ${root.keyId}`], worktree);
|
|
2219
|
+
await step('git', ['push', '-u', 'origin', branch], worktree);
|
|
2220
|
+
return await createPr(worktree);
|
|
1244
2221
|
}
|
|
1245
2222
|
finally {
|
|
1246
|
-
deps.run('git', ['-C', source, 'worktree', 'remove', '--force', worktree]);
|
|
2223
|
+
await deps.run('git', ['-C', source, 'worktree', 'remove', '--force', worktree]);
|
|
1247
2224
|
// The pushed branch lives on origin; the local copy only blocks a rerun.
|
|
1248
2225
|
if (worktreeAdded)
|
|
1249
|
-
deps.run('git', ['-C', source, 'branch', '-D', branch]);
|
|
2226
|
+
await deps.run('git', ['-C', source, 'branch', '-D', branch]);
|
|
1250
2227
|
}
|
|
1251
2228
|
}
|
|
1252
2229
|
/** The enrolled checkout of ashlr-hub itself, if any. */
|