@ashlr/hub 3.11.5 → 3.13.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 +153 -0
- package/README.md +29 -16
- package/dist/authority-surface.json +91 -45
- package/dist/build-identity.json +2 -2
- package/dist/cli/authority.d.ts +145 -33
- package/dist/cli/authority.js +1196 -313
- 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/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/trust-roots.js +7 -1
- package/dist/core/authority/trust-roots.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 +919 -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/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/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 +10 -0
- package/dist/core/fleet/fleet-merge-state.js.map +1 -1
- package/dist/core/fleet/host-merge.d.ts +5 -0
- package/dist/core/fleet/host-merge.js +11 -0
- package/dist/core/fleet/host-merge.js.map +1 -1
- package/dist/core/fleet/merge-gates.d.ts +8 -0
- package/dist/core/fleet/merge-gates.js +25 -5
- package/dist/core/fleet/merge-gates.js.map +1 -1
- package/dist/core/fleet/standing-merge-pass.js +30 -0
- package/dist/core/fleet/standing-merge-pass.js.map +1 -1
- package/dist/core/fleet/tick-hooks-live.d.ts +9 -0
- package/dist/core/fleet/tick-hooks-live.js +44 -5
- 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/routing/budget-api.d.ts +11 -0
- package/dist/core/routing/budget-api.js +64 -8
- 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/universe/builtins/preparation/manifest.json +1 -1
- package/dist/core/universe/builtins/preparation/preparation-bridge.mjs +8 -2
- package/dist/core/universe/builtins/preparation/preparation-verification-fixtures.mjs +7 -1
- package/dist/core/verse/account-health.js +9 -3
- package/dist/core/verse/account-health.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/authority-api.d.ts +9 -1
- package/dist/core/verse/authority-api.js +82 -2
- package/dist/core/verse/authority-api.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/verse-api.d.ts +3 -1
- package/dist/core/verse/verse-api.js +6 -0
- package/dist/core/verse/verse-api.js.map +1 -1
- package/dist/core/web/public/next/assets/{ActionMenu-DffwPklw.js → ActionMenu-Dwjk6rUQ.js} +2 -2
- package/dist/core/web/public/next/assets/{ActionMenu-DffwPklw.js.map → ActionMenu-Dwjk6rUQ.js.map} +1 -1
- package/dist/core/web/public/next/assets/Advanced-cVYTAsfB.js +2 -0
- package/dist/core/web/public/next/assets/{Advanced-CxzE6ekM.js.map → Advanced-cVYTAsfB.js.map} +1 -1
- package/dist/core/web/public/next/assets/App-IsaWDsdF.js +7 -0
- package/dist/core/web/public/next/assets/{App-DFlp-vdv.js.map → App-IsaWDsdF.js.map} +1 -1
- package/dist/core/web/public/next/assets/AppsSection-DDqeSrty.js +2 -0
- package/dist/core/web/public/next/assets/{AppsSection-Ct2O605-.js.map → AppsSection-DDqeSrty.js.map} +1 -1
- package/dist/core/web/public/next/assets/{AreaTrend-o-GLl2Is.js → AreaTrend-C32rUaxm.js} +2 -2
- package/dist/core/web/public/next/assets/{AreaTrend-o-GLl2Is.js.map → AreaTrend-C32rUaxm.js.map} +1 -1
- package/dist/core/web/public/next/assets/AutonomyOffState-CmSSTHVe.css +1 -0
- package/dist/core/web/public/next/assets/AutonomyOffState-CpFYS3tQ.js +3 -0
- package/dist/core/web/public/next/assets/AutonomyOffState-CpFYS3tQ.js.map +1 -0
- package/dist/core/web/public/next/assets/AutonomySection-Dwn8UtOy.js +2 -0
- package/dist/core/web/public/next/assets/{AutonomySection-BTg7yVa1.js.map → AutonomySection-Dwn8UtOy.js.map} +1 -1
- package/dist/core/web/public/next/assets/{BranchBar-BjPFtGsj.js → BranchBar-ISZ7xUCj.js} +2 -2
- package/dist/core/web/public/next/assets/{BranchBar-BjPFtGsj.js.map → BranchBar-ISZ7xUCj.js.map} +1 -1
- package/dist/core/web/public/next/assets/BudgetControl-BARVp-En.js +2 -0
- package/dist/core/web/public/next/assets/BudgetControl-BARVp-En.js.map +1 -0
- package/dist/core/web/public/next/assets/{Button-OjVJSrFq.js → Button-DtE0rlWD.js} +2 -2
- package/dist/core/web/public/next/assets/{Button-OjVJSrFq.js.map → Button-DtE0rlWD.js.map} +1 -1
- package/dist/core/web/public/next/assets/{CapacityStrip-Bx4c3dxD.js → CapacityStrip-DKbKA_Aa.js} +2 -2
- package/dist/core/web/public/next/assets/{CapacityStrip-Bx4c3dxD.js.map → CapacityStrip-DKbKA_Aa.js.map} +1 -1
- package/dist/core/web/public/next/assets/{ChartFrame-B_w2Siqg.js → ChartFrame-DK_BDXCt.js} +2 -2
- package/dist/core/web/public/next/assets/{ChartFrame-B_w2Siqg.js.map → ChartFrame-DK_BDXCt.js.map} +1 -1
- package/dist/core/web/public/next/assets/ChatSection-C9myY4ei.js +3 -0
- package/dist/core/web/public/next/assets/ChatSection-C9myY4ei.js.map +1 -0
- package/dist/core/web/public/next/assets/{CloudBudgetForm-5Gf4iAok.js → CloudBudgetForm-CvjK4Oh6.js} +2 -2
- package/dist/core/web/public/next/assets/{CloudBudgetForm-5Gf4iAok.js.map → CloudBudgetForm-CvjK4Oh6.js.map} +1 -1
- package/dist/core/web/public/next/assets/{CommandPalette-iTpk6I8r.js → CommandPalette-DzPsth0u.js} +2 -2
- package/dist/core/web/public/next/assets/{CommandPalette-iTpk6I8r.js.map → CommandPalette-DzPsth0u.js.map} +1 -1
- package/dist/core/web/public/next/assets/CommandSection-Brp73iox.js +3 -0
- package/dist/core/web/public/next/assets/CommandSection-Brp73iox.js.map +1 -0
- package/dist/core/web/public/next/assets/{ConfirmDialog-_S9iE-cJ.js → ConfirmDialog-CLUhzaBj.js} +2 -2
- package/dist/core/web/public/next/assets/{ConfirmDialog-_S9iE-cJ.js.map → ConfirmDialog-CLUhzaBj.js.map} +1 -1
- package/dist/core/web/public/next/assets/{DeleteChatDialog-KUYUygiA.js → DeleteChatDialog-C_eDhV8b.js} +2 -2
- package/dist/core/web/public/next/assets/{DeleteChatDialog-KUYUygiA.js.map → DeleteChatDialog-C_eDhV8b.js.map} +1 -1
- package/dist/core/web/public/next/assets/{Dialog-BuIaG6HF.js → Dialog-BRd_B9OE.js} +2 -2
- package/dist/core/web/public/next/assets/{Dialog-BuIaG6HF.js.map → Dialog-BRd_B9OE.js.map} +1 -1
- package/dist/core/web/public/next/assets/{DiffPane-De4IQt82.js → DiffPane-Cpr9soB2.js} +2 -2
- package/dist/core/web/public/next/assets/{DiffPane-De4IQt82.js.map → DiffPane-Cpr9soB2.js.map} +1 -1
- package/dist/core/web/public/next/assets/{DiffViewer-B75t6Ile.js → DiffViewer-CNwd0W3Q.js} +2 -2
- package/dist/core/web/public/next/assets/{DiffViewer-B75t6Ile.js.map → DiffViewer-CNwd0W3Q.js.map} +1 -1
- package/dist/core/web/public/next/assets/DockHost-BcEq2qaS.js +2 -0
- package/dist/core/web/public/next/assets/{DockHost-BPqyhtCP.js.map → DockHost-BcEq2qaS.js.map} +1 -1
- package/dist/core/web/public/next/assets/{EmptyState-DXkT4vhW.js → EmptyState-DsN1H6MC.js} +2 -2
- package/dist/core/web/public/next/assets/{EmptyState-DXkT4vhW.js.map → EmptyState-DsN1H6MC.js.map} +1 -1
- package/dist/core/web/public/next/assets/{Epistemic-CfqFe_aP.js → Epistemic-fycQTBRO.js} +2 -2
- package/dist/core/web/public/next/assets/{Epistemic-CfqFe_aP.js.map → Epistemic-fycQTBRO.js.map} +1 -1
- package/dist/core/web/public/next/assets/EvidenceTimeline-PBDAiSuG.css +1 -0
- package/dist/core/web/public/next/assets/EvidenceTimeline-sCCAdnBK.js +2 -0
- package/dist/core/web/public/next/assets/EvidenceTimeline-sCCAdnBK.js.map +1 -0
- package/dist/core/web/public/next/assets/{FleetSection-B7f-XBiX.js → FleetSection-BG8iGraG.js} +3 -3
- package/dist/core/web/public/next/assets/{FleetSection-B7f-XBiX.js.map → FleetSection-BG8iGraG.js.map} +1 -1
- package/dist/core/web/public/next/assets/{Funnel-pXYySwXQ.js → Funnel-B88_wpA4.js} +2 -2
- package/dist/core/web/public/next/assets/{Funnel-pXYySwXQ.js.map → Funnel-B88_wpA4.js.map} +1 -1
- package/dist/core/web/public/next/assets/{GearTray-gj6lT7ba.js → GearTray-CpDt0xwj.js} +2 -2
- package/dist/core/web/public/next/assets/{GearTray-gj6lT7ba.js.map → GearTray-CpDt0xwj.js.map} +1 -1
- package/dist/core/web/public/next/assets/{GrowthSection-D8e9m9Wy.js → GrowthSection-BvZezOsX.js} +2 -2
- package/dist/core/web/public/next/assets/{GrowthSection-D8e9m9Wy.js.map → GrowthSection-BvZezOsX.js.map} +1 -1
- package/dist/core/web/public/next/assets/{HandoffDialog-VfKxKhxF.js → HandoffDialog-B1G6gQyk.js} +2 -2
- package/dist/core/web/public/next/assets/{HandoffDialog-VfKxKhxF.js.map → HandoffDialog-B1G6gQyk.js.map} +1 -1
- package/dist/core/web/public/next/assets/{Input-u1YR_Ogl.js → Input-CI0yBHmn.js} +2 -2
- package/dist/core/web/public/next/assets/{Input-u1YR_Ogl.js.map → Input-CI0yBHmn.js.map} +1 -1
- package/dist/core/web/public/next/assets/{LeaderCard-BDELBrnx.js → LeaderCard-BxxiEg4e.js} +2 -2
- package/dist/core/web/public/next/assets/{LeaderCard-BDELBrnx.js.map → LeaderCard-BxxiEg4e.js.map} +1 -1
- package/dist/core/web/public/next/assets/{MatrixHeatmap-NKLF2srR.js → MatrixHeatmap-iPFEU2Tv.js} +2 -2
- package/dist/core/web/public/next/assets/{MatrixHeatmap-NKLF2srR.js.map → MatrixHeatmap-iPFEU2Tv.js.map} +1 -1
- package/dist/core/web/public/next/assets/McpSection-nuXer4K-.js +2 -0
- package/dist/core/web/public/next/assets/{McpSection-D1tDvlRa.js.map → McpSection-nuXer4K-.js.map} +1 -1
- package/dist/core/web/public/next/assets/{MessageMarkdown-DlfLEJxu.js → MessageMarkdown-BCe4TsXk.js} +3 -3
- package/dist/core/web/public/next/assets/{MessageMarkdown-DlfLEJxu.js.map → MessageMarkdown-BCe4TsXk.js.map} +1 -1
- package/dist/core/web/public/next/assets/{MessageMarkdownRenderer-BYZzI115.js → MessageMarkdownRenderer-DL3LU5kL.js} +2 -2
- package/dist/core/web/public/next/assets/{MessageMarkdownRenderer-BYZzI115.js.map → MessageMarkdownRenderer-DL3LU5kL.js.map} +1 -1
- package/dist/core/web/public/next/assets/{Meter-BSVhU43Q.js → Meter-b69LM0mU.js} +2 -2
- package/dist/core/web/public/next/assets/{Meter-BSVhU43Q.js.map → Meter-b69LM0mU.js.map} +1 -1
- package/dist/core/web/public/next/assets/{MindSection-lqV2e_Sq.js → MindSection-Dc5v5rH3.js} +2 -2
- package/dist/core/web/public/next/assets/{MindSection-lqV2e_Sq.js.map → MindSection-Dc5v5rH3.js.map} +1 -1
- package/dist/core/web/public/next/assets/{MutationTokenDialog-CLGP72g6.js → MutationTokenDialog-Bm34BNVV.js} +2 -2
- package/dist/core/web/public/next/assets/{MutationTokenDialog-CLGP72g6.js.map → MutationTokenDialog-Bm34BNVV.js.map} +1 -1
- package/dist/core/web/public/next/assets/NeedsYouDrawer-N1cWJjpz.js +3 -0
- package/dist/core/web/public/next/assets/NeedsYouDrawer-N1cWJjpz.js.map +1 -0
- package/dist/core/web/public/next/assets/NeedsYouList-CH6Mq-E_.css +1 -0
- package/dist/core/web/public/next/assets/NeedsYouList-D5Kk549g.js +2 -0
- package/dist/core/web/public/next/assets/{NeedsYouList-eBOlJr0b.js.map → NeedsYouList-D5Kk549g.js.map} +1 -1
- package/dist/core/web/public/next/assets/{NewChatDialog-DoYTH26M.js → NewChatDialog-BqFTWgHU.js} +2 -2
- package/dist/core/web/public/next/assets/{NewChatDialog-DoYTH26M.js.map → NewChatDialog-BqFTWgHU.js.map} +1 -1
- package/dist/core/web/public/next/assets/OnboardingFlow-DcDsguAM.js +3 -0
- package/dist/core/web/public/next/assets/OnboardingFlow-DcDsguAM.js.map +1 -0
- package/dist/core/web/public/next/assets/OnboardingGate-BZafM1Vf.js +3 -0
- package/dist/core/web/public/next/assets/{OnboardingGate-DrDEVBqw.js.map → OnboardingGate-BZafM1Vf.js.map} +1 -1
- package/dist/core/web/public/next/assets/{PreviewPane-ULN673zm.js → PreviewPane-zT3gK9TD.js} +2 -2
- package/dist/core/web/public/next/assets/{PreviewPane-ULN673zm.js.map → PreviewPane-zT3gK9TD.js.map} +1 -1
- package/dist/core/web/public/next/assets/{ProviderLogo-8WaUjTlN.js → ProviderLogo-AwHn2sKm.js} +2 -2
- package/dist/core/web/public/next/assets/{ProviderLogo-8WaUjTlN.js.map → ProviderLogo-AwHn2sKm.js.map} +1 -1
- package/dist/core/web/public/next/assets/{RailStatus-Ckw9nvYU.js → RailStatus-xHrovWvI.js} +2 -2
- package/dist/core/web/public/next/assets/{RailStatus-Ckw9nvYU.js.map → RailStatus-xHrovWvI.js.map} +1 -1
- package/dist/core/web/public/next/assets/{RefreshIndicator-DGubF4vR.js → RefreshIndicator-SEFZlMB2.js} +2 -2
- package/dist/core/web/public/next/assets/{RefreshIndicator-DGubF4vR.js.map → RefreshIndicator-SEFZlMB2.js.map} +1 -1
- package/dist/core/web/public/next/assets/{ResourcePoolConsoleApp-BahEaY4d.js → ResourcePoolConsoleApp-CAxPQ4sZ.js} +3 -3
- package/dist/core/web/public/next/assets/{ResourcePoolConsoleApp-BahEaY4d.js.map → ResourcePoolConsoleApp-CAxPQ4sZ.js.map} +1 -1
- package/dist/core/web/public/next/assets/ResourcesChrome-BnhnEdJe.js +3 -0
- package/dist/core/web/public/next/assets/{ResourcesChrome-C2ZrA7la.js.map → ResourcesChrome-BnhnEdJe.js.map} +1 -1
- package/dist/core/web/public/next/assets/{ResourcesDrawer-BC93fzwQ.js → ResourcesDrawer-BAFGodny.js} +2 -2
- package/dist/core/web/public/next/assets/{ResourcesDrawer-BC93fzwQ.js.map → ResourcesDrawer-BAFGodny.js.map} +1 -1
- package/dist/core/web/public/next/assets/{RouteErrorBoundary-BvhHNpE_.js → RouteErrorBoundary-C8WGiixc.js} +2 -2
- package/dist/core/web/public/next/assets/{RouteErrorBoundary-BvhHNpE_.js.map → RouteErrorBoundary-C8WGiixc.js.map} +1 -1
- package/dist/core/web/public/next/assets/{RunInCloudAction-Dywfnw0C.js → RunInCloudAction-1-IPLYhf.js} +2 -2
- package/dist/core/web/public/next/assets/{RunInCloudAction-Dywfnw0C.js.map → RunInCloudAction-1-IPLYhf.js.map} +1 -1
- package/dist/core/web/public/next/assets/{SeatSelector-CH7hTgwy.js → SeatSelector-DpwNiX8F.js} +2 -2
- package/dist/core/web/public/next/assets/{SeatSelector-CH7hTgwy.js.map → SeatSelector-DpwNiX8F.js.map} +1 -1
- package/dist/core/web/public/next/assets/{Segmented-Wynbg0F5.js → Segmented-Wt1ATod9.js} +2 -2
- package/dist/core/web/public/next/assets/{Segmented-Wynbg0F5.js.map → Segmented-Wt1ATod9.js.map} +1 -1
- package/dist/core/web/public/next/assets/{Select-DMyLCkId.js → Select-By-V9lMc.js} +2 -2
- package/dist/core/web/public/next/assets/{Select-DMyLCkId.js.map → Select-By-V9lMc.js.map} +1 -1
- package/dist/core/web/public/next/assets/{SessionGate-8RtQFWeZ.js → SessionGate-WROS02tK.js} +2 -2
- package/dist/core/web/public/next/assets/{SessionGate-8RtQFWeZ.js.map → SessionGate-WROS02tK.js.map} +1 -1
- package/dist/core/web/public/next/assets/{SessionInsightChip-zBJbKv2L.js → SessionInsightChip-DNUcruwp.js} +2 -2
- package/dist/core/web/public/next/assets/{SessionInsightChip-zBJbKv2L.js.map → SessionInsightChip-DNUcruwp.js.map} +1 -1
- package/dist/core/web/public/next/assets/{SessionSearch-DfJIcxxM.js → SessionSearch-UHtt7a8f.js} +2 -2
- package/dist/core/web/public/next/assets/{SessionSearch-DfJIcxxM.js.map → SessionSearch-UHtt7a8f.js.map} +1 -1
- package/dist/core/web/public/next/assets/{SettingsSection-Da9RYyH5.js → SettingsSection-Bf74l0Gj.js} +2 -2
- package/dist/core/web/public/next/assets/{SettingsSection-Da9RYyH5.js.map → SettingsSection-Bf74l0Gj.js.map} +1 -1
- package/dist/core/web/public/next/assets/SetupChecklist-BhZpFLsj.css +1 -0
- package/dist/core/web/public/next/assets/SetupChecklist-CzpEcfOl.js +2 -0
- package/dist/core/web/public/next/assets/SetupChecklist-CzpEcfOl.js.map +1 -0
- package/dist/core/web/public/next/assets/{Sheet-DhXyIDmq.js → Sheet-HkBOBMTT.js} +2 -2
- package/dist/core/web/public/next/assets/{Sheet-DhXyIDmq.js.map → Sheet-HkBOBMTT.js.map} +1 -1
- package/dist/core/web/public/next/assets/{ShortcutsOverlay-Bi5f-wUw.js → ShortcutsOverlay-DiigiT8H.js} +2 -2
- package/dist/core/web/public/next/assets/{ShortcutsOverlay-Bi5f-wUw.js.map → ShortcutsOverlay-DiigiT8H.js.map} +1 -1
- package/dist/core/web/public/next/assets/Sidebar-C9YtBYVZ.js +3 -0
- package/dist/core/web/public/next/assets/Sidebar-C9YtBYVZ.js.map +1 -0
- package/dist/core/web/public/next/assets/{Slider-Bd9YFWEV.js → Slider-DaEDws-b.js} +2 -2
- package/dist/core/web/public/next/assets/{Slider-Bd9YFWEV.js.map → Slider-DaEDws-b.js.map} +1 -1
- package/dist/core/web/public/next/assets/{StatTile-DScTd3c-.js → StatTile-CTX41O6y.js} +2 -2
- package/dist/core/web/public/next/assets/{StatTile-DScTd3c-.js.map → StatTile-CTX41O6y.js.map} +1 -1
- package/dist/core/web/public/next/assets/{StatusBadge-DQ1UtQAJ.js → StatusBadge-CXG3iNL4.js} +2 -2
- package/dist/core/web/public/next/assets/{StatusBadge-DQ1UtQAJ.js.map → StatusBadge-CXG3iNL4.js.map} +1 -1
- package/dist/core/web/public/next/assets/{StepBand-BB3IJxhr.js → StepBand-jJTqYIdX.js} +2 -2
- package/dist/core/web/public/next/assets/{StepBand-BB3IJxhr.js.map → StepBand-jJTqYIdX.js.map} +1 -1
- package/dist/core/web/public/next/assets/{Surface-DWMtQe3Q.js → Surface-5bNA08tK.js} +2 -2
- package/dist/core/web/public/next/assets/{Surface-DWMtQe3Q.js.map → Surface-5bNA08tK.js.map} +1 -1
- package/dist/core/web/public/next/assets/{Swimlane-DcENqtdp.js → Swimlane-LrslsGxw.js} +2 -2
- package/dist/core/web/public/next/assets/{Swimlane-DcENqtdp.js.map → Swimlane-LrslsGxw.js.map} +1 -1
- package/dist/core/web/public/next/assets/{Switch-CenChEYf.js → Switch-BmCzfFnu.js} +2 -2
- package/dist/core/web/public/next/assets/{Switch-CenChEYf.js.map → Switch-BmCzfFnu.js.map} +1 -1
- package/dist/core/web/public/next/assets/{Tag-D4k5E3u0.js → Tag-WSUIaMVy.js} +2 -2
- package/dist/core/web/public/next/assets/{Tag-D4k5E3u0.js.map → Tag-WSUIaMVy.js.map} +1 -1
- package/dist/core/web/public/next/assets/{TerminalPane-DCQTukcE.js → TerminalPane-BVFgYX6K.js} +5 -5
- package/dist/core/web/public/next/assets/{TerminalPane-DCQTukcE.js.map → TerminalPane-BVFgYX6K.js.map} +1 -1
- package/dist/core/web/public/next/assets/{UniverseConsoleApp-DsuWT0pi.js → UniverseConsoleApp-BpphLqoz.js} +2 -2
- package/dist/core/web/public/next/assets/{UniverseConsoleApp-DsuWT0pi.js.map → UniverseConsoleApp-BpphLqoz.js.map} +1 -1
- package/dist/core/web/public/next/assets/{UniverseView-DyI5ApsA.js → UniverseView-DbpbSMwO.js} +4 -4
- package/dist/core/web/public/next/assets/{UniverseView-DyI5ApsA.js.map → UniverseView-DbpbSMwO.js.map} +1 -1
- package/dist/core/web/public/next/assets/{UsageSection-Imr28nwK.js → UsageSection-CUlaKtws.js} +3 -3
- package/dist/core/web/public/next/assets/{UsageSection-Imr28nwK.js.map → UsageSection-CUlaKtws.js.map} +1 -1
- package/dist/core/web/public/next/assets/VerseConsoleApp-DXDv1aOi.js +3 -0
- package/dist/core/web/public/next/assets/VerseConsoleApp-DXDv1aOi.js.map +1 -0
- package/dist/core/web/public/next/assets/{Workspace-CpxBO98E.js → Workspace-RXy0pqp6.js} +5 -5
- package/dist/core/web/public/next/assets/{Workspace-CpxBO98E.js.map → Workspace-RXy0pqp6.js.map} +1 -1
- package/dist/core/web/public/next/assets/{actions-DE8NoWV_.js → actions-BZTidCXZ.js} +2 -2
- package/dist/core/web/public/next/assets/{actions-DE8NoWV_.js.map → actions-BZTidCXZ.js.map} +1 -1
- package/dist/core/web/public/next/assets/{apps-model-CFGCUu9p.js → apps-model-CHO133qt.js} +2 -2
- package/dist/core/web/public/next/assets/{apps-model-CFGCUu9p.js.map → apps-model-CHO133qt.js.map} +1 -1
- package/dist/core/web/public/next/assets/{authority-model-CmnfJzmM.js → authority-model-8NeAEwAF.js} +2 -2
- package/dist/core/web/public/next/assets/{authority-model-CmnfJzmM.js.map → authority-model-8NeAEwAF.js.map} +1 -1
- package/dist/core/web/public/next/assets/{budget-queries-DjrPRFwl.js → budget-queries-DFF-3ICg.js} +2 -2
- package/dist/core/web/public/next/assets/{budget-queries-DjrPRFwl.js.map → budget-queries-DFF-3ICg.js.map} +1 -1
- package/dist/core/web/public/next/assets/{capacity-strip-model-C7cdQnuK.js → capacity-strip-model-5ViUgUlP.js} +2 -2
- package/dist/core/web/public/next/assets/{capacity-strip-model-C7cdQnuK.js.map → capacity-strip-model-5ViUgUlP.js.map} +1 -1
- package/dist/core/web/public/next/assets/{charts-DUD_jprE.js → charts-CzxWrZn7.js} +2 -2
- package/dist/core/web/public/next/assets/{charts-DUD_jprE.js.map → charts-CzxWrZn7.js.map} +1 -1
- package/dist/core/web/public/next/assets/cloud-model-CfRydq8N.js +2 -0
- package/dist/core/web/public/next/assets/{cloud-model-DoxBMDvx.js.map → cloud-model-CfRydq8N.js.map} +1 -1
- package/dist/core/web/public/next/assets/{cloud-queries-CTGvO-k-.js → cloud-queries-DmptbjS8.js} +2 -2
- package/dist/core/web/public/next/assets/{cloud-queries-CTGvO-k-.js.map → cloud-queries-DmptbjS8.js.map} +1 -1
- package/dist/core/web/public/next/assets/{command-catalog-CmrHNFzh.js → command-catalog-ffCAijVY.js} +2 -2
- package/dist/core/web/public/next/assets/{command-catalog-CmrHNFzh.js.map → command-catalog-ffCAijVY.js.map} +1 -1
- package/dist/core/web/public/next/assets/{context-model-2Owp2jYF.js → context-model-Bb15chFt.js} +2 -2
- package/dist/core/web/public/next/assets/{context-model-2Owp2jYF.js.map → context-model-Bb15chFt.js.map} +1 -1
- package/dist/core/web/public/next/assets/{context-model-9xywbXxX.js → context-model-CDeyP0cA.js} +2 -2
- package/dist/core/web/public/next/assets/{context-model-9xywbXxX.js.map → context-model-CDeyP0cA.js.map} +1 -1
- package/dist/core/web/public/next/assets/{context-queries-wvcRnV-I.js → context-queries-_9bFWb9v.js} +2 -2
- package/dist/core/web/public/next/assets/{context-queries-wvcRnV-I.js.map → context-queries-_9bFWb9v.js.map} +1 -1
- package/dist/core/web/public/next/assets/{control-queries-DB6Kh7UN.js → control-queries-CXtmZZH6.js} +2 -2
- package/dist/core/web/public/next/assets/{control-queries-DB6Kh7UN.js.map → control-queries-CXtmZZH6.js.map} +1 -1
- package/dist/core/web/public/next/assets/copy-setup-BIp0iaiZ.js +2 -0
- package/dist/core/web/public/next/assets/copy-setup-BIp0iaiZ.js.map +1 -0
- package/dist/core/web/public/next/assets/{dock-icons-BBUGl4tE.js → dock-icons-Cc43W7r4.js} +2 -2
- package/dist/core/web/public/next/assets/{dock-icons-BBUGl4tE.js.map → dock-icons-Cc43W7r4.js.map} +1 -1
- package/dist/core/web/public/next/assets/{fleet-model-BRfRWkZl.js → fleet-model-DOK8ZtEt.js} +2 -2
- package/dist/core/web/public/next/assets/{fleet-model-BRfRWkZl.js.map → fleet-model-DOK8ZtEt.js.map} +1 -1
- package/dist/core/web/public/next/assets/{fleet-queries-tU6UEXN2.js → fleet-queries-BeEAqD6y.js} +2 -2
- package/dist/core/web/public/next/assets/{fleet-queries-tU6UEXN2.js.map → fleet-queries-BeEAqD6y.js.map} +1 -1
- package/dist/core/web/public/next/assets/{focus-trap-BGOceFvj.js → focus-trap-ChZ5EFfM.js} +2 -2
- package/dist/core/web/public/next/assets/{focus-trap-BGOceFvj.js.map → focus-trap-ChZ5EFfM.js.map} +1 -1
- package/dist/core/web/public/next/assets/format-BmH2yWL6.js +2 -0
- package/dist/core/web/public/next/assets/format-BmH2yWL6.js.map +1 -0
- package/dist/core/web/public/next/assets/{git-model-53iRLInY.js → git-model-BFwAVbxF.js} +2 -2
- package/dist/core/web/public/next/assets/{git-model-53iRLInY.js.map → git-model-BFwAVbxF.js.map} +1 -1
- package/dist/core/web/public/next/assets/global-y_exIyAP.js +2 -0
- package/dist/core/web/public/next/assets/{global-CXHHMz9o.js.map → global-y_exIyAP.js.map} +1 -1
- package/dist/core/web/public/next/assets/{guarded-runners-B8zFVBVZ.js → guarded-runners-Cd4POBtI.js} +2 -2
- package/dist/core/web/public/next/assets/{guarded-runners-B8zFVBVZ.js.map → guarded-runners-Cd4POBtI.js.map} +1 -1
- package/dist/core/web/public/next/assets/{icons-D4s-jlxX.js → icons-CygCEFWs.js} +2 -2
- package/dist/core/web/public/next/assets/{icons-D4s-jlxX.js.map → icons-CygCEFWs.js.map} +1 -1
- package/dist/core/web/public/next/assets/{index-BG177cPN.js → index-CyK31YRm.js} +3 -3
- package/dist/core/web/public/next/assets/{index-BG177cPN.js.map → index-CyK31YRm.js.map} +1 -1
- package/dist/core/web/public/next/assets/{live-model-CEf01wrW.js → live-model-D7hilYFk.js} +2 -2
- package/dist/core/web/public/next/assets/live-model-D7hilYFk.js.map +1 -0
- package/dist/core/web/public/next/assets/{mind.module-D_xk6HIQ.js → mind.module-B15k0Y9q.js} +2 -2
- package/dist/core/web/public/next/assets/{mind.module-D_xk6HIQ.js.map → mind.module-B15k0Y9q.js.map} +1 -1
- package/dist/core/web/public/next/assets/mutations-CWlxEMEm.js +2 -0
- package/dist/core/web/public/next/assets/{mutations-CbTSofVH.js.map → mutations-CWlxEMEm.js.map} +1 -1
- package/dist/core/web/public/next/assets/{nav-CmZcbPFJ.js → nav-mimleI0R.js} +2 -2
- package/dist/core/web/public/next/assets/{nav-CmZcbPFJ.js.map → nav-mimleI0R.js.map} +1 -1
- package/dist/core/web/public/next/assets/{overnight-queries-CR0b_j-V.js → overnight-queries-DJ4mZKSj.js} +2 -2
- package/dist/core/web/public/next/assets/{overnight-queries-CR0b_j-V.js.map → overnight-queries-DJ4mZKSj.js.map} +1 -1
- package/dist/core/web/public/next/assets/primitives-DMijqcu0.js +1 -0
- package/dist/core/web/public/next/assets/{queries-rvRsAGW2.js → queries-Bw1McHTj.js} +2 -2
- package/dist/core/web/public/next/assets/{queries-rvRsAGW2.js.map → queries-Bw1McHTj.js.map} +1 -1
- package/dist/core/web/public/next/assets/{reasoning-pref-DMPQkujg.js → reasoning-pref-KkhI_5-j.js} +2 -2
- package/dist/core/web/public/next/assets/{reasoning-pref-DMPQkujg.js.map → reasoning-pref-KkhI_5-j.js.map} +1 -1
- package/dist/core/web/public/next/assets/{reveal-anchor-BKk2wZZZ.js → reveal-anchor-CCCx_kfT.js} +2 -2
- package/dist/core/web/public/next/assets/{reveal-anchor-BKk2wZZZ.js.map → reveal-anchor-CCCx_kfT.js.map} +1 -1
- package/dist/core/web/public/next/assets/{seat-subscription-CLn2ReSZ.js → seat-subscription-BhUUz7VL.js} +2 -2
- package/dist/core/web/public/next/assets/{seat-subscription-CLn2ReSZ.js.map → seat-subscription-BhUUz7VL.js.map} +1 -1
- package/dist/core/web/public/next/assets/{sidebar-model-CjQLvdag.js → sidebar-model-Dcd437cf.js} +2 -2
- package/dist/core/web/public/next/assets/{sidebar-model-CjQLvdag.js.map → sidebar-model-Dcd437cf.js.map} +1 -1
- package/dist/core/web/public/next/assets/{slots-DRxNosZg.js → slots-BL_J5mmV.js} +3 -3
- package/dist/core/web/public/next/assets/{slots-DRxNosZg.js.map → slots-BL_J5mmV.js.map} +1 -1
- package/dist/core/web/public/next/assets/{surface-data-DshUC_TU.js → surface-data-C3UAQ4QX.js} +2 -2
- package/dist/core/web/public/next/assets/{surface-data-DshUC_TU.js.map → surface-data-C3UAQ4QX.js.map} +1 -1
- package/dist/core/web/public/next/assets/surface-prefetch-BXKjPsV5.js +2 -0
- package/dist/core/web/public/next/assets/{surface-prefetch-DO8r4ttQ.js.map → surface-prefetch-BXKjPsV5.js.map} +1 -1
- package/dist/core/web/public/next/assets/{tasks-model-BKPFZAmZ.js → tasks-model-CzHvclVm.js} +2 -2
- package/dist/core/web/public/next/assets/{tasks-model-BKPFZAmZ.js.map → tasks-model-CzHvclVm.js.map} +1 -1
- package/dist/core/web/public/next/assets/{terminal-icons-DoeQkP3y.js → terminal-icons-BwgQTQP0.js} +2 -2
- package/dist/core/web/public/next/assets/{terminal-icons-DoeQkP3y.js.map → terminal-icons-BwgQTQP0.js.map} +1 -1
- 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-MnUeYzwp.js → usage-queries-pXxvy54x.js} +2 -2
- package/dist/core/web/public/next/assets/{usage-queries-MnUeYzwp.js.map → usage-queries-pXxvy54x.js.map} +1 -1
- package/dist/core/web/public/next/assets/{use-guarded-action-P12MfNwU.js → use-guarded-action-EgVWjM5c.js} +2 -2
- package/dist/core/web/public/next/assets/{use-guarded-action-P12MfNwU.js.map → use-guarded-action-EgVWjM5c.js.map} +1 -1
- package/dist/core/web/public/next/assets/{use-ticker-DIvAZ4Ey.js → use-ticker-BoqFxRFG.js} +2 -2
- package/dist/core/web/public/next/assets/{use-ticker-DIvAZ4Ey.js.map → use-ticker-BoqFxRFG.js.map} +1 -1
- package/dist/core/web/public/next/assets/{use-token-gate-DfEja7Wr.js → use-token-gate-DSNIIpI6.js} +2 -2
- package/dist/core/web/public/next/assets/{use-token-gate-DfEja7Wr.js.map → use-token-gate-DSNIIpI6.js.map} +1 -1
- package/dist/core/web/public/next/assets/{useOnboarding-CkOHadqC.js → useOnboarding-Bk0VnkxQ.js} +2 -2
- package/dist/core/web/public/next/assets/{useOnboarding-CkOHadqC.js.map → useOnboarding-Bk0VnkxQ.js.map} +1 -1
- package/dist/core/web/public/next/assets/{useSeatsRefresh-BDlbDlIU.js → useSeatsRefresh-q7Id9RcW.js} +2 -2
- package/dist/core/web/public/next/assets/{useSeatsRefresh-BDlbDlIU.js.map → useSeatsRefresh-q7Id9RcW.js.map} +1 -1
- package/dist/core/web/public/next/assets/{useVerseTranscript-BGtA55k2.js → useVerseTranscript-CqH51rt9.js} +2 -2
- package/dist/core/web/public/next/assets/{useVerseTranscript-BGtA55k2.js.map → useVerseTranscript-CqH51rt9.js.map} +1 -1
- package/dist/core/web/public/next/assets/{verse-icons-wOet9JZy.js → verse-icons-DsG1dOQx.js} +2 -2
- package/dist/core/web/public/next/assets/{verse-icons-wOet9JZy.js.map → verse-icons-DsG1dOQx.js.map} +1 -1
- package/dist/core/web/public/next/assets/{verse-model-CSzzdyu2.js → verse-model-JTuvRBVK.js} +2 -2
- package/dist/core/web/public/next/assets/{verse-model-CSzzdyu2.js.map → verse-model-JTuvRBVK.js.map} +1 -1
- package/dist/core/web/public/next/assets/{warmup-CecO_Nk0.js → warmup-Bv1RumVq.js} +3 -3
- package/dist/core/web/public/next/assets/{warmup-CecO_Nk0.js.map → warmup-Bv1RumVq.js.map} +1 -1
- package/dist/core/web/public/next/assets/why-seat-model-Cau8qG6O.js +2 -0
- package/dist/core/web/public/next/assets/why-seat-model-Cau8qG6O.js.map +1 -0
- package/dist/core/web/public/next/assets/{workspace-model-BdIVybKr.js → workspace-model-D-SLfads.js} +2 -2
- package/dist/core/web/public/next/assets/{workspace-model-BdIVybKr.js.map → workspace-model-D-SLfads.js.map} +1 -1
- package/dist/core/web/public/next/index.html +1 -1
- package/dist/release-dependency-inventory.json +1 -1
- package/docs/AUTONOMY-GAP.md +1 -1
- package/package.json +1 -1
- package/dist/core/web/public/next/assets/Advanced-CxzE6ekM.js +0 -2
- package/dist/core/web/public/next/assets/App-DFlp-vdv.js +0 -7
- package/dist/core/web/public/next/assets/AppsSection-Ct2O605-.js +0 -2
- package/dist/core/web/public/next/assets/AutonomyOffState-CXhXpsim.js +0 -2
- package/dist/core/web/public/next/assets/AutonomyOffState-CXhXpsim.js.map +0 -1
- package/dist/core/web/public/next/assets/AutonomyOffState-DmbE71vI.css +0 -1
- package/dist/core/web/public/next/assets/AutonomySection-BTg7yVa1.js +0 -2
- package/dist/core/web/public/next/assets/BudgetControl-ng6swoZp.js +0 -2
- package/dist/core/web/public/next/assets/BudgetControl-ng6swoZp.js.map +0 -1
- package/dist/core/web/public/next/assets/ChatSection-Bisj9R6E.js +0 -3
- package/dist/core/web/public/next/assets/ChatSection-Bisj9R6E.js.map +0 -1
- package/dist/core/web/public/next/assets/CommandSection-D8ZihzvE.js +0 -2
- package/dist/core/web/public/next/assets/CommandSection-D8ZihzvE.js.map +0 -1
- package/dist/core/web/public/next/assets/DockHost-BPqyhtCP.js +0 -2
- package/dist/core/web/public/next/assets/McpSection-D1tDvlRa.js +0 -2
- package/dist/core/web/public/next/assets/NeedsYouDrawer-D992XTOn.js +0 -2
- package/dist/core/web/public/next/assets/NeedsYouDrawer-D992XTOn.js.map +0 -1
- package/dist/core/web/public/next/assets/NeedsYouList-CruOt4JT.css +0 -1
- package/dist/core/web/public/next/assets/NeedsYouList-eBOlJr0b.js +0 -2
- package/dist/core/web/public/next/assets/OnboardingFlow-D3yfITK1.js +0 -2
- package/dist/core/web/public/next/assets/OnboardingFlow-D3yfITK1.js.map +0 -1
- package/dist/core/web/public/next/assets/OnboardingGate-DrDEVBqw.js +0 -3
- package/dist/core/web/public/next/assets/ResourcesChrome-C2ZrA7la.js +0 -3
- package/dist/core/web/public/next/assets/Sidebar-BuO3sViG.js +0 -3
- package/dist/core/web/public/next/assets/Sidebar-BuO3sViG.js.map +0 -1
- package/dist/core/web/public/next/assets/VerseConsoleApp-DeuBEvAO.js +0 -3
- package/dist/core/web/public/next/assets/VerseConsoleApp-DeuBEvAO.js.map +0 -1
- package/dist/core/web/public/next/assets/cloud-model-DoxBMDvx.js +0 -2
- package/dist/core/web/public/next/assets/copy-setup-CJQhFl-4.js +0 -2
- package/dist/core/web/public/next/assets/copy-setup-CJQhFl-4.js.map +0 -1
- package/dist/core/web/public/next/assets/format-DEAaVF6X.js +0 -2
- package/dist/core/web/public/next/assets/format-DEAaVF6X.js.map +0 -1
- package/dist/core/web/public/next/assets/global-CXHHMz9o.js +0 -2
- package/dist/core/web/public/next/assets/live-model-CEf01wrW.js.map +0 -1
- package/dist/core/web/public/next/assets/mutations-CbTSofVH.js +0 -2
- package/dist/core/web/public/next/assets/primitives-DNI-uJBd.js +0 -1
- package/dist/core/web/public/next/assets/surface-prefetch-DO8r4ttQ.js +0 -2
- 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/why-seat-model-D68ZA7G8.js +0 -2
- package/dist/core/web/public/next/assets/why-seat-model-D68ZA7G8.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);
|
|
@@ -520,68 +639,219 @@ export function buildFleetRuleset(requiredChecks) {
|
|
|
520
639
|
],
|
|
521
640
|
};
|
|
522
641
|
}
|
|
523
|
-
function ghJson(deps, args) {
|
|
524
|
-
const result = deps.run('gh', args);
|
|
642
|
+
async function ghJson(deps, args) {
|
|
643
|
+
const result = await deps.run('gh', args);
|
|
525
644
|
if (result.status !== 0)
|
|
526
645
|
throw new Error(`gh ${args.slice(0, 3).join(' ')} failed: ${result.stderr.trim().slice(0, 200)}`);
|
|
527
646
|
return JSON.parse(result.stdout || 'null');
|
|
528
647
|
}
|
|
529
648
|
/** Check-run names on the default branch head: what CI actually reports today. */
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
649
|
+
/**
|
|
650
|
+
* 3.13: with `verifyAppId` (the repo is in the grant, the fleet can verify it
|
|
651
|
+
* and the ashlr-fleet App's id is known) the host-verified `ashlr/verify` is
|
|
652
|
+
* always required, pinned to the App — even when the default branch has no
|
|
653
|
+
* runs at all (Actions off), which is exactly the repo it exists for. A
|
|
654
|
+
* same-named run from another App never replaces the pin.
|
|
655
|
+
*/
|
|
656
|
+
async function discoverChecks(deps, repo, branch, verifyAppId = null) {
|
|
657
|
+
const runs = await ghJson(deps, ['api', `repos/${repo}/commits/${encodeURIComponent(branch)}/check-runs?per_page=100`]);
|
|
534
658
|
const found = [];
|
|
535
659
|
for (const run of runs.check_runs ?? []) {
|
|
536
660
|
if (typeof run.name !== 'string' || run.name.length === 0)
|
|
537
661
|
continue;
|
|
662
|
+
if (verifyAppId !== null && run.name === ASHLR_VERIFY_CHECK)
|
|
663
|
+
continue;
|
|
538
664
|
const appId = run.app?.id;
|
|
539
665
|
found.push({ context: run.name, integrationId: typeof appId === 'number' && Number.isSafeInteger(appId) && appId > 0 ? appId : null });
|
|
540
666
|
}
|
|
667
|
+
if (verifyAppId !== null)
|
|
668
|
+
found.push({ context: ASHLR_VERIFY_CHECK, integrationId: verifyAppId });
|
|
541
669
|
return normalizeRequiredChecks(found);
|
|
542
670
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
671
|
+
/** The check the ashlr-fleet App posts on fleet PR heads (core/fleet/verify-check-run.ts ASHLR_VERIFY_CHECK_NAME). */
|
|
672
|
+
export const ASHLR_VERIFY_CHECK = 'ashlr/verify';
|
|
673
|
+
/** The ashlr-fleet App as GitHub describes it to your own gh auth (a `gh api` read); null = unreadable. */
|
|
674
|
+
export async function readFleetApp(deps) {
|
|
675
|
+
try {
|
|
676
|
+
const app = await ghJson(deps, ['api', `apps/${FLEET_APP_NAME}`]);
|
|
677
|
+
if (!app || typeof app.id !== 'number' || !Number.isSafeInteger(app.id) || app.id <= 0)
|
|
678
|
+
return null;
|
|
679
|
+
const slug = typeof app.slug === 'string' && /^[a-z0-9-]{1,100}$/.test(app.slug) ? app.slug : FLEET_APP_NAME;
|
|
680
|
+
const ownerLogin = typeof app.owner?.login === 'string' && /^[A-Za-z0-9-]{1,39}$/.test(app.owner.login) ? app.owner.login : '';
|
|
681
|
+
const checks = typeof app.permissions?.['checks'] === 'string' ? app.permissions['checks'] : null;
|
|
682
|
+
return { id: app.id, slug, ownerLogin, ownerIsOrg: app.owner?.type === 'Organization', checks };
|
|
683
|
+
}
|
|
684
|
+
catch {
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
/** PURE: where the App's permissions (and then each installation's acceptance) are changed. */
|
|
689
|
+
export function fleetAppPermissionUrls(app) {
|
|
690
|
+
const base = app.ownerIsOrg && app.ownerLogin
|
|
691
|
+
? `https://github.com/organizations/${app.ownerLogin}/settings`
|
|
692
|
+
: 'https://github.com/settings';
|
|
693
|
+
return { permissions: `${base}/apps/${app.slug}/permissions`, installations: `${base}/installations` };
|
|
694
|
+
}
|
|
695
|
+
/** True when the fleet mirror of `repo` exists and the fleet detects at least one required verify command there. */
|
|
696
|
+
async function fleetCanVerify(repo) {
|
|
697
|
+
try {
|
|
698
|
+
const { mirrorPathFor } = await import('../core/fleet/mirrors.js');
|
|
699
|
+
const { detectVerifyCommands } = await import('../core/run/verify-commands.js');
|
|
700
|
+
const mirror = mirrorPathFor(repo);
|
|
701
|
+
if (!existsSync(mirror))
|
|
702
|
+
return false;
|
|
703
|
+
return detectVerifyCommands(mirror, 'merge').some((command) => command.required !== false);
|
|
704
|
+
}
|
|
705
|
+
catch {
|
|
706
|
+
return false;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* PURE: `actual` (a ruleset as GitHub returns it) already says everything
|
|
711
|
+
* `desired` says. GitHub adds ids, links, timestamps and server-side rule
|
|
712
|
+
* defaults to what it returns, so extra object keys are ignored — but every
|
|
713
|
+
* field we set must match, and arrays must match one-to-one in any order, so
|
|
714
|
+
* an extra rule, an extra bypass actor or an extra required check is drift
|
|
715
|
+
* (re-applying would remove it). Anything unreadable is "not up to date".
|
|
716
|
+
*/
|
|
717
|
+
export function rulesetMatches(desired, actual) {
|
|
718
|
+
if (Array.isArray(desired)) {
|
|
719
|
+
if (!Array.isArray(actual) || actual.length !== desired.length)
|
|
720
|
+
return false;
|
|
721
|
+
const unused = [...actual];
|
|
722
|
+
for (const wanted of desired) {
|
|
723
|
+
const at = unused.findIndex((candidate) => rulesetMatches(wanted, candidate));
|
|
724
|
+
if (at < 0)
|
|
725
|
+
return false;
|
|
726
|
+
unused.splice(at, 1);
|
|
555
727
|
}
|
|
556
|
-
|
|
557
|
-
|
|
728
|
+
return true;
|
|
729
|
+
}
|
|
730
|
+
if (desired !== null && typeof desired === 'object') {
|
|
731
|
+
if (actual === null || typeof actual !== 'object' || Array.isArray(actual))
|
|
732
|
+
return false;
|
|
733
|
+
const have = actual;
|
|
734
|
+
return Object.entries(desired).every(([key, value]) => rulesetMatches(value, have[key]));
|
|
558
735
|
}
|
|
736
|
+
return Object.is(desired, actual);
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* The repos the fleet works in: the installed grant's, or — before the first
|
|
740
|
+
* grant — the ones it would name (every enrolled checkout's GitHub repo plus
|
|
741
|
+
* the canary; authority-api draftRepos), which are server-enforced until a
|
|
742
|
+
* grant says otherwise. Read-only: no draft is built and the ledger is not
|
|
743
|
+
* touched, so a dry run (and Verse's checklist) can ask it freely.
|
|
744
|
+
*/
|
|
745
|
+
async function setupTargetRepos() {
|
|
746
|
+
const { readInstalledGrant, FLEET_CANARY_REPO } = await import('../core/authority/standing-grant.js');
|
|
747
|
+
const installed = readInstalledGrant();
|
|
748
|
+
if (installed.state === 'ok') {
|
|
749
|
+
return installed.envelope.payload.repos.map((repo) => ({ nameWithOwner: repo.nameWithOwner, enforcement: repo.enforcement }));
|
|
750
|
+
}
|
|
751
|
+
const byKey = new Map();
|
|
752
|
+
try {
|
|
753
|
+
const { readEnrollmentRegistry } = await import('../core/sandbox/policy.js');
|
|
754
|
+
const { repoIdentityOfPath } = await import('../core/fleet/repo-identity.js');
|
|
755
|
+
const registry = readEnrollmentRegistry();
|
|
756
|
+
if (registry.state === 'ready') {
|
|
757
|
+
for (const path of registry.repos) {
|
|
758
|
+
const nameWithOwner = repoIdentityOfPath(path);
|
|
759
|
+
if (nameWithOwner && !byKey.has(nameWithOwner.toLowerCase()))
|
|
760
|
+
byKey.set(nameWithOwner.toLowerCase(), { nameWithOwner, enforcement: 'server' });
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
catch {
|
|
765
|
+
// An unreadable registry leaves the canary alone on the list.
|
|
766
|
+
}
|
|
767
|
+
if (!byKey.has(FLEET_CANARY_REPO))
|
|
768
|
+
byKey.set(FLEET_CANARY_REPO, { nameWithOwner: FLEET_CANARY_REPO, enforcement: 'server' });
|
|
769
|
+
return [...byKey.values()];
|
|
770
|
+
}
|
|
771
|
+
/** Read-only: what `protect --apply` would do for each repo (every gh call here is a GET). */
|
|
772
|
+
async function protectPlans(parsed, deps, given) {
|
|
773
|
+
const explicit = parsed.values.get('--repo') ?? [];
|
|
774
|
+
const byGrant = given !== undefined || explicit.length === 0;
|
|
775
|
+
const targets = given
|
|
776
|
+
?? (explicit.length > 0 ? explicit.map((nameWithOwner) => ({ nameWithOwner, enforcement: null })) : await setupTargetRepos());
|
|
777
|
+
// Repos the grant covers (installed grant, or the would-be grant's own list):
|
|
778
|
+
// only those get the fleet's ashlr/verify requirement.
|
|
779
|
+
const grantMembers = new Set();
|
|
780
|
+
const { readInstalledGrant } = await import('../core/authority/standing-grant.js');
|
|
781
|
+
const installed = readInstalledGrant();
|
|
782
|
+
if (installed.state === 'ok')
|
|
783
|
+
for (const r of installed.envelope.payload.repos)
|
|
784
|
+
grantMembers.add(r.nameWithOwner.toLowerCase());
|
|
785
|
+
if (byGrant)
|
|
786
|
+
for (const r of targets)
|
|
787
|
+
grantMembers.add(r.nameWithOwner.toLowerCase());
|
|
788
|
+
let fleetApp;
|
|
559
789
|
const plans = [];
|
|
560
|
-
for (const repo of
|
|
561
|
-
if (enforcement
|
|
562
|
-
plans.push({ repo, skipped: 'local enforcement (private free-plan repo: GitHub rulesets unavailable) — the daemon enforces its gates', checks: [], ruleset: null, existingId: null });
|
|
790
|
+
for (const { nameWithOwner: repo, enforcement } of targets) {
|
|
791
|
+
if (enforcement === 'local') {
|
|
792
|
+
plans.push({ repo, skipped: 'local enforcement (private free-plan repo: GitHub rulesets unavailable) — the daemon enforces its gates', checks: [], ruleset: null, existingId: null, upToDate: false });
|
|
563
793
|
continue;
|
|
564
794
|
}
|
|
565
795
|
try {
|
|
566
|
-
const info = ghJson(deps, ['api', `repos/${repo}`]);
|
|
567
|
-
|
|
796
|
+
const info = await ghJson(deps, ['api', `repos/${repo}`]);
|
|
797
|
+
let verifyAppId = null;
|
|
798
|
+
let verifyNote = null;
|
|
799
|
+
if (grantMembers.has(repo.toLowerCase())) {
|
|
800
|
+
if (!(await fleetCanVerify(repo))) {
|
|
801
|
+
verifyNote = `${ASHLR_VERIFY_CHECK} not required: the fleet mirror has no verify command yet (rerun protect once it has synced)`;
|
|
802
|
+
}
|
|
803
|
+
else {
|
|
804
|
+
if (fleetApp === undefined)
|
|
805
|
+
fleetApp = await readFleetApp(deps);
|
|
806
|
+
if (fleetApp)
|
|
807
|
+
verifyAppId = fleetApp.id;
|
|
808
|
+
else
|
|
809
|
+
verifyNote = `${ASHLR_VERIFY_CHECK} not required: the ${FLEET_APP_NAME} App could not be read (\`gh api apps/${FLEET_APP_NAME}\`)`;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
const required = await discoverChecks(deps, repo, info?.default_branch ?? 'main', verifyAppId);
|
|
568
813
|
const checks = required.map((check) => check.context);
|
|
569
|
-
const existing = ghJson(deps, ['api', `repos/${repo}/rulesets`]);
|
|
814
|
+
const existing = await ghJson(deps, ['api', `repos/${repo}/rulesets`]);
|
|
570
815
|
const match = Array.isArray(existing) ? existing.find((r) => r.name === FLEET_RULESET_NAME) : undefined;
|
|
816
|
+
const existingId = typeof match?.id === 'number' && Number.isSafeInteger(match.id) ? match.id : null;
|
|
817
|
+
const ruleset = buildFleetRuleset(required);
|
|
818
|
+
// Rerun-safe: a ruleset already on GitHub with this exact content is
|
|
819
|
+
// reported as in place instead of being PUT again.
|
|
820
|
+
const upToDate = existingId !== null && rulesetMatches(ruleset, await ghJson(deps, ['api', `repos/${repo}/rulesets/${existingId}`]));
|
|
571
821
|
plans.push({
|
|
572
822
|
repo,
|
|
573
|
-
skipped: info
|
|
823
|
+
skipped: info?.private === true && checks.length === 0 ? 'private repo with no CI checks — protect it by hand or keep it propose-only' : null,
|
|
574
824
|
checks,
|
|
575
|
-
ruleset
|
|
576
|
-
existingId
|
|
825
|
+
ruleset,
|
|
826
|
+
existingId,
|
|
827
|
+
upToDate,
|
|
828
|
+
verifyNote,
|
|
577
829
|
});
|
|
578
830
|
}
|
|
579
831
|
catch (error) {
|
|
580
|
-
plans.push({ repo, skipped: `could not read it from GitHub: ${error.message}`, checks: [], ruleset: null, existingId: null });
|
|
832
|
+
plans.push({ repo, skipped: `could not read it from GitHub: ${error.message}`, checks: [], ruleset: null, existingId: null, upToDate: false });
|
|
581
833
|
}
|
|
582
834
|
}
|
|
583
835
|
return plans;
|
|
584
836
|
}
|
|
837
|
+
/** Create or update each plan's ruleset; the number that failed. */
|
|
838
|
+
async function applyProtectPlans(plans, deps) {
|
|
839
|
+
let failures = 0;
|
|
840
|
+
for (const plan of plans) {
|
|
841
|
+
const result = await deps.run('gh', [
|
|
842
|
+
'api', '--method', plan.existingId === null ? 'POST' : 'PUT',
|
|
843
|
+
`repos/${plan.repo}/rulesets${plan.existingId === null ? '' : `/${plan.existingId}`}`,
|
|
844
|
+
'--input', '-',
|
|
845
|
+
], { input: JSON.stringify(plan.ruleset) });
|
|
846
|
+
if (result.status === 0)
|
|
847
|
+
deps.out(`${plan.repo}: ruleset ${plan.existingId === null ? 'created' : 'updated'}.`);
|
|
848
|
+
else {
|
|
849
|
+
failures += 1;
|
|
850
|
+
deps.err(`${plan.repo}: failed — ${result.stderr.trim().slice(0, 300)}`);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
return failures;
|
|
854
|
+
}
|
|
585
855
|
async function cmdProtect(parsed, deps) {
|
|
586
856
|
const apply = parsed.flags.has('--apply');
|
|
587
857
|
if (!apply && !parsed.flags.has('--print')) {
|
|
@@ -594,7 +864,17 @@ async function cmdProtect(parsed, deps) {
|
|
|
594
864
|
deps.out(`${plan.repo}: skipped — ${plan.skipped}`);
|
|
595
865
|
continue;
|
|
596
866
|
}
|
|
597
|
-
|
|
867
|
+
const checks = plan.checks.length > 0 ? plan.checks.join(', ') : 'none found (the repo goes to the owner lane until it has CI)';
|
|
868
|
+
if (plan.upToDate) {
|
|
869
|
+
deps.out(`${plan.repo}: ruleset ${plan.existingId} already up to date; required checks: ${checks}`);
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
deps.out(`${plan.repo}: ${plan.existingId === null ? 'create' : `update ruleset ${plan.existingId}`}; required checks: ${checks}`);
|
|
873
|
+
if (plan.verifyNote)
|
|
874
|
+
deps.out(` note: ${plan.verifyNote}`);
|
|
875
|
+
else if (plan.checks.includes(ASHLR_VERIFY_CHECK)) {
|
|
876
|
+
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`);
|
|
877
|
+
}
|
|
598
878
|
if (!apply) {
|
|
599
879
|
deps.out(` gh api --method ${plan.existingId === null ? 'POST' : 'PUT'} repos/${plan.repo}/rulesets${plan.existingId === null ? '' : `/${plan.existingId}`} --input - <<'JSON'`);
|
|
600
880
|
deps.out(JSON.stringify(plan.ruleset, null, 2));
|
|
@@ -603,37 +883,27 @@ async function cmdProtect(parsed, deps) {
|
|
|
603
883
|
}
|
|
604
884
|
if (!apply)
|
|
605
885
|
return 0;
|
|
606
|
-
const actionable = plans.filter((plan) => !plan.skipped);
|
|
886
|
+
const actionable = plans.filter((plan) => !plan.skipped && !plan.upToDate);
|
|
607
887
|
if (actionable.length === 0)
|
|
608
888
|
return 0;
|
|
609
889
|
if (!parsed.flags.has('--yes') && !(await deps.confirm(`Apply the ruleset to ${actionable.length} repo(s)?`))) {
|
|
610
890
|
deps.out('Nothing applied.');
|
|
611
891
|
return 1;
|
|
612
892
|
}
|
|
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;
|
|
893
|
+
return (await applyProtectPlans(actionable, deps)) === 0 ? 0 : 1;
|
|
628
894
|
}
|
|
629
895
|
// ---------------------------------------------------------------------------
|
|
630
896
|
// github-app — App Manifest flow (addendum §3)
|
|
631
897
|
// ---------------------------------------------------------------------------
|
|
632
898
|
export const FLEET_APP_NAME = 'ashlr-fleet';
|
|
633
899
|
/**
|
|
634
|
-
* PURE: the App manifest. Contents + pull requests read/write, checks
|
|
635
|
-
*
|
|
636
|
-
*
|
|
900
|
+
* PURE: the App manifest. Contents + pull requests read/write, checks
|
|
901
|
+
* read/write (3.13: the App posts the host-verified `ashlr/verify` check run
|
|
902
|
+
* on fleet PR heads — core/fleet/verify-check-run.ts), statuses / metadata
|
|
903
|
+
* read. No workflows, no administration — GitHub itself then refuses
|
|
904
|
+
* CI-config and protection changes from the fleet. An App created before 3.13
|
|
905
|
+
* has `checks: read`; `ashlr authority setup` detects that and prints where
|
|
906
|
+
* to raise it (fleetAppPermissionUrls).
|
|
637
907
|
*/
|
|
638
908
|
export function buildGithubAppManifest(redirectUrl) {
|
|
639
909
|
return {
|
|
@@ -642,7 +912,7 @@ export function buildGithubAppManifest(redirectUrl) {
|
|
|
642
912
|
hook_attributes: { url: 'https://github.com/ashlrai/ashlr-hub', active: false },
|
|
643
913
|
redirect_url: redirectUrl,
|
|
644
914
|
public: false,
|
|
645
|
-
default_permissions: { contents: 'write', pull_requests: 'write', checks: '
|
|
915
|
+
default_permissions: { contents: 'write', pull_requests: 'write', checks: 'write', statuses: 'read', metadata: 'read' },
|
|
646
916
|
default_events: [],
|
|
647
917
|
};
|
|
648
918
|
}
|
|
@@ -729,26 +999,165 @@ async function cmdGithubApp(parsed, deps) {
|
|
|
729
999
|
const result = await runGithubAppFlow(deps, { org });
|
|
730
1000
|
deps.out(`Created ${result.slug} (App id ${result.appId}); its private key is in custody and was never written to disk.`);
|
|
731
1001
|
deps.out(`Install it on the enrolled repos: ${result.installUrl}`);
|
|
1002
|
+
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
1003
|
return 0;
|
|
733
1004
|
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
1005
|
+
/** Where Mason installs the App (and picks its repos). */
|
|
1006
|
+
export const FLEET_APP_INSTALL_URL = `https://github.com/apps/${FLEET_APP_NAME}/installations/new`;
|
|
1007
|
+
/** Bounded wait: a probe that does not answer in time is "unknown", never a hang. */
|
|
1008
|
+
async function withTimeout(work, ms, what) {
|
|
1009
|
+
let timer;
|
|
1010
|
+
try {
|
|
1011
|
+
return await Promise.race([
|
|
1012
|
+
work,
|
|
1013
|
+
new Promise((_, reject) => {
|
|
1014
|
+
timer = setTimeout(() => reject(new Error(`${what} did not answer within ${Math.round(ms / 1000)} s`)), ms);
|
|
1015
|
+
}),
|
|
1016
|
+
]);
|
|
1017
|
+
}
|
|
1018
|
+
finally {
|
|
1019
|
+
if (timer)
|
|
1020
|
+
clearTimeout(timer);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
const APP_PROBE_TIMEOUT_MS = 20_000;
|
|
1024
|
+
/**
|
|
1025
|
+
* Where the ashlr-fleet App is installed — read-only, in two passes:
|
|
1026
|
+
*
|
|
1027
|
+
* 1. per owner, `gh api orgs/<owner>/installations` with Mason's own gh auth
|
|
1028
|
+
* (a GET; needs admin:org). An installation on "All repositories" settles
|
|
1029
|
+
* every repo of that owner; no installation at all settles them as missing.
|
|
1030
|
+
* 2. what that cannot settle (a user account, "Only select repositories", a
|
|
1031
|
+
* gh token without admin:org) is asked of the App itself: custody's
|
|
1032
|
+
* gh-token looks the repo up AS the App (GET /repos/<r>/installation under
|
|
1033
|
+
* the App's JWT), and a 404 there means "not installed". The one-hour token
|
|
1034
|
+
* the helper mints on success stays in this process's memory, is never
|
|
1035
|
+
* printed, and changes nothing on GitHub.
|
|
1036
|
+
*/
|
|
1037
|
+
export async function probeAppInstallation(deps, repos) {
|
|
1038
|
+
const result = { installed: [], missing: [], unknown: [] };
|
|
1039
|
+
const byOwner = new Map();
|
|
1040
|
+
for (const repo of repos) {
|
|
1041
|
+
const owner = repo.split('/')[0];
|
|
1042
|
+
byOwner.set(owner, [...(byOwner.get(owner) ?? []), repo]);
|
|
1043
|
+
}
|
|
1044
|
+
const unsettled = [];
|
|
1045
|
+
for (const [owner, ownerRepos] of byOwner) {
|
|
1046
|
+
let listed = null;
|
|
1047
|
+
try {
|
|
1048
|
+
const read = await ghJson(deps, ['api', `orgs/${owner}/installations?per_page=100`]);
|
|
1049
|
+
if (read && Array.isArray(read.installations))
|
|
1050
|
+
listed = read.installations;
|
|
1051
|
+
}
|
|
1052
|
+
catch {
|
|
1053
|
+
listed = null;
|
|
1054
|
+
}
|
|
1055
|
+
const app = listed?.find((entry) => entry.app_slug === FLEET_APP_NAME);
|
|
1056
|
+
if (listed && !app)
|
|
1057
|
+
result.missing.push(...ownerRepos);
|
|
1058
|
+
else if (app?.repository_selection === 'all')
|
|
1059
|
+
result.installed.push(...ownerRepos);
|
|
1060
|
+
else
|
|
1061
|
+
unsettled.push(...ownerRepos);
|
|
1062
|
+
}
|
|
1063
|
+
// A few at a time: each lookup is two HTTPS calls inside the helper.
|
|
1064
|
+
const queue = [...unsettled];
|
|
1065
|
+
const worker = async () => {
|
|
1066
|
+
for (let repo = queue.shift(); repo !== undefined; repo = queue.shift()) {
|
|
1067
|
+
try {
|
|
1068
|
+
await withTimeout(deps.custody.githubToken(repo), APP_PROBE_TIMEOUT_MS, 'ashlr-custody gh-token');
|
|
1069
|
+
result.installed.push(repo);
|
|
1070
|
+
}
|
|
1071
|
+
catch (error) {
|
|
1072
|
+
const code = error.code;
|
|
1073
|
+
const message = error.message ?? String(error);
|
|
1074
|
+
if (code === 'github' && /not installed/u.test(message))
|
|
1075
|
+
result.missing.push(repo);
|
|
1076
|
+
else
|
|
1077
|
+
result.unknown.push({ repo, reason: message.slice(0, 160) });
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
};
|
|
1081
|
+
await Promise.all(Array.from({ length: Math.min(4, queue.length) }, worker));
|
|
1082
|
+
// Stable order for the checklist line, whatever order the lookups finished in.
|
|
1083
|
+
const order = new Map(repos.map((repo, i) => [repo, i]));
|
|
1084
|
+
const byInput = (a, b) => (order.get(a) ?? 0) - (order.get(b) ?? 0);
|
|
1085
|
+
result.installed.sort(byInput);
|
|
1086
|
+
result.missing.sort(byInput);
|
|
1087
|
+
result.unknown.sort((a, b) => byInput(a.repo, b.repo));
|
|
1088
|
+
return result;
|
|
1089
|
+
}
|
|
1090
|
+
/** "a, b and 3 more" — a list short enough for one checklist line. */
|
|
1091
|
+
function shortList(items, max = 3) {
|
|
1092
|
+
if (items.length <= max)
|
|
1093
|
+
return items.join(', ');
|
|
1094
|
+
return `${items.slice(0, max).join(', ')} and ${items.length - max} more`;
|
|
1095
|
+
}
|
|
1096
|
+
function provenanceRotationRecordPath(keyPath) {
|
|
1097
|
+
return `${keyPath}.rotation.json`;
|
|
1098
|
+
}
|
|
1099
|
+
function sha256Hex(bytes) {
|
|
1100
|
+
return createHash('sha256').update(bytes).digest('hex');
|
|
1101
|
+
}
|
|
1102
|
+
function readRotationRecord(keyPath) {
|
|
1103
|
+
try {
|
|
1104
|
+
const raw = JSON.parse(readFileSync(provenanceRotationRecordPath(keyPath), 'utf8'));
|
|
1105
|
+
if (raw.v !== 1 || typeof raw.rotatedAt !== 'string' || typeof raw.keySha256 !== 'string' || !/^[a-f0-9]{64}$/u.test(raw.keySha256))
|
|
1106
|
+
return null;
|
|
1107
|
+
return { v: 1, rotatedAt: raw.rotatedAt, keySha256: raw.keySha256 };
|
|
1108
|
+
}
|
|
1109
|
+
catch {
|
|
1110
|
+
return null;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
/** Read-only: never creates, repairs or moves the key. */
|
|
1114
|
+
export async function provenanceKeyState() {
|
|
1115
|
+
const { provenanceKeyPath, loadExistingProvenanceKeyReadOnly } = await import('../core/foundry/provenance.js');
|
|
1116
|
+
let path;
|
|
1117
|
+
let key;
|
|
1118
|
+
try {
|
|
1119
|
+
path = provenanceKeyPath();
|
|
1120
|
+
key = loadExistingProvenanceKeyReadOnly();
|
|
1121
|
+
}
|
|
1122
|
+
catch (error) {
|
|
1123
|
+
return { state: 'unhealthy', reason: error.message.replace(homedir(), '~') };
|
|
1124
|
+
}
|
|
1125
|
+
if (!key)
|
|
1126
|
+
return { state: 'missing' };
|
|
1127
|
+
const record = readRotationRecord(path);
|
|
1128
|
+
// The record names the exact key it made: a key swapped in afterwards is not "already rotated".
|
|
1129
|
+
if (record && record.keySha256 === sha256Hex(key))
|
|
1130
|
+
return { state: 'rotated', rotatedAt: record.rotatedAt };
|
|
1131
|
+
return { state: 'unrotated' };
|
|
1132
|
+
}
|
|
737
1133
|
/**
|
|
738
1134
|
* Move the provenance HMAC key aside and create a new one. Every pending
|
|
739
1135
|
* provenance signature stops verifying (by design: agents could read the old
|
|
740
1136
|
* key while confinement was off), so pending proposals are re-verified.
|
|
1137
|
+
* Records which key it made (provenanceKeyState reads it) so setup can tell
|
|
1138
|
+
* a key it already rotated from the burned one.
|
|
741
1139
|
*/
|
|
742
1140
|
export async function rotateProvenanceKey() {
|
|
743
|
-
const { provenanceKeyPath, loadOrCreateKey } = await import('../core/foundry/provenance.js');
|
|
1141
|
+
const { provenanceKeyPath, loadOrCreateKey, loadExistingProvenanceKeyReadOnly } = await import('../core/foundry/provenance.js');
|
|
744
1142
|
const path = provenanceKeyPath();
|
|
745
1143
|
let retiredAs = null;
|
|
746
1144
|
if (existsSync(path)) {
|
|
747
1145
|
retiredAs = `${path}.retired-${new Date().toISOString().replace(/[:.]/g, '-')}`;
|
|
748
1146
|
renameSync(path, retiredAs);
|
|
749
1147
|
}
|
|
750
|
-
loadOrCreateKey();
|
|
751
|
-
|
|
1148
|
+
const created = loadOrCreateKey();
|
|
1149
|
+
// loadOrCreateKey falls back to a process-lifetime key when it cannot write
|
|
1150
|
+
// one; that is not a rotation — say so instead of recording it.
|
|
1151
|
+
const stored = loadExistingProvenanceKeyReadOnly();
|
|
1152
|
+
if (!stored || !stored.equals(created))
|
|
1153
|
+
throw new Error('the new provenance key could not be stored in ~/.ashlr/foundry');
|
|
1154
|
+
const rotatedAt = new Date().toISOString();
|
|
1155
|
+
const record = { v: 1, rotatedAt, keySha256: sha256Hex(stored) };
|
|
1156
|
+
const target = provenanceRotationRecordPath(path);
|
|
1157
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
1158
|
+
writeFileSync(tmp, `${JSON.stringify(record)}\n`, { mode: 0o600 });
|
|
1159
|
+
renameSync(tmp, target);
|
|
1160
|
+
return { retiredAs, rotatedAt };
|
|
752
1161
|
}
|
|
753
1162
|
async function cmdRotateProvenance(parsed, deps) {
|
|
754
1163
|
if (!parsed.flags.has('--yes') && !(await deps.confirm('Replace the provenance HMAC key? Pending proposals will need re-verification.'))) {
|
|
@@ -840,12 +1249,41 @@ const SETUP_STEP_NEEDS = Object.freeze({
|
|
|
840
1249
|
'standing-grant': ['touch-id'],
|
|
841
1250
|
'autonomy-switch': [],
|
|
842
1251
|
'daemon-service': ['terminal'],
|
|
843
|
-
'resident-runtime': [],
|
|
1252
|
+
'resident-runtime': ['terminal'],
|
|
844
1253
|
});
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
const
|
|
1254
|
+
/** The one command that walks every step and resumes where the last run stopped. */
|
|
1255
|
+
export const AUTHORITY_SETUP_COMMAND = 'ashlr authority setup';
|
|
1256
|
+
/** The only command that installs or restarts the resident service (you, at a terminal, under the grant). */
|
|
1257
|
+
const RESIDENT_START_COMMAND = 'ashlr authority resident start';
|
|
1258
|
+
/**
|
|
1259
|
+
* The command that moves each step on while it is not in place. Setup is
|
|
1260
|
+
* rerun-safe, so for everything it performs itself the answer is setup; the
|
|
1261
|
+
* rest is what only Mason runs (sudo, the build, the grant, resident start).
|
|
1262
|
+
* A row may name its own (the resident step names what its admission asks
|
|
1263
|
+
* for, or none when a prerequisite outside this Mac's commands is missing).
|
|
1264
|
+
*/
|
|
1265
|
+
const SETUP_STEP_COMMAND = Object.freeze({
|
|
1266
|
+
'custody-helper': 'sudo scripts/install-custody.sh',
|
|
1267
|
+
'host-binding': 'sudo scripts/install-custody.sh',
|
|
1268
|
+
'signing-key': AUTHORITY_SETUP_COMMAND,
|
|
1269
|
+
'trust-root': AUTHORITY_SETUP_COMMAND,
|
|
1270
|
+
deploy: 'npm run build',
|
|
1271
|
+
'github-app': AUTHORITY_SETUP_COMMAND,
|
|
1272
|
+
'claude-token': AUTHORITY_SETUP_COMMAND,
|
|
1273
|
+
'canary-repo': AUTHORITY_SETUP_COMMAND,
|
|
1274
|
+
rulesets: AUTHORITY_SETUP_COMMAND,
|
|
1275
|
+
'old-activation-state': AUTHORITY_SETUP_COMMAND,
|
|
1276
|
+
'provenance-key': AUTHORITY_SETUP_COMMAND,
|
|
1277
|
+
'standing-grant': 'ashlr authority grant',
|
|
1278
|
+
'autonomy-switch': 'ashlr authority switch autonomous',
|
|
1279
|
+
'daemon-service': RESIDENT_START_COMMAND,
|
|
1280
|
+
'resident-runtime': RESIDENT_START_COMMAND,
|
|
1281
|
+
});
|
|
1282
|
+
// Every exit before the grant is known to be active (the first unmet live
|
|
1283
|
+
// prerequisite, or a dry run past one) reports the resident step with this:
|
|
1284
|
+
// a resident service is admitted only under an active standing grant
|
|
1285
|
+
// (docs/RESIDENT-RUNTIME.md), so without one it is genuinely blocked.
|
|
1286
|
+
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
1287
|
function setupStepId(step) {
|
|
850
1288
|
return step.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
851
1289
|
}
|
|
@@ -854,7 +1292,17 @@ export function authoritySetupReport(report, dryRun) {
|
|
|
854
1292
|
const count = (status) => report.filter((r) => r.status === status).length;
|
|
855
1293
|
const steps = report.map((r) => {
|
|
856
1294
|
const id = setupStepId(r.step);
|
|
857
|
-
|
|
1295
|
+
// A step in place needs nothing; one that is not names what moves it on.
|
|
1296
|
+
const open = r.status !== 'done' && r.status !== 'already';
|
|
1297
|
+
return {
|
|
1298
|
+
id,
|
|
1299
|
+
step: r.step,
|
|
1300
|
+
status: r.status,
|
|
1301
|
+
detail: r.detail,
|
|
1302
|
+
needs: SETUP_STEP_NEEDS[id] ?? [],
|
|
1303
|
+
command: open ? (r.command !== undefined ? r.command : SETUP_STEP_COMMAND[id] ?? null) : null,
|
|
1304
|
+
link: open ? r.link ?? null : null,
|
|
1305
|
+
};
|
|
858
1306
|
});
|
|
859
1307
|
const open = steps.find((s) => s.status !== 'done' && s.status !== 'already');
|
|
860
1308
|
return {
|
|
@@ -868,7 +1316,6 @@ export function authoritySetupReport(report, dryRun) {
|
|
|
868
1316
|
}
|
|
869
1317
|
async function cmdSetup(parsed, deps) {
|
|
870
1318
|
const dryRun = parsed.flags.has('--dry-run');
|
|
871
|
-
const yes = parsed.flags.has('--yes');
|
|
872
1319
|
const json = parsed.flags.has('--json');
|
|
873
1320
|
if (json && !dryRun) {
|
|
874
1321
|
// A real run asks questions and prints what gh / signing did; only the
|
|
@@ -876,24 +1323,119 @@ async function cmdSetup(parsed, deps) {
|
|
|
876
1323
|
deps.err('setup --json needs --dry-run');
|
|
877
1324
|
return 2;
|
|
878
1325
|
}
|
|
1326
|
+
const report = await runSetup(parsed, deps, { dryRun, yes: parsed.flags.has('--yes'), print: !json });
|
|
1327
|
+
return finish(report, deps, dryRun, json);
|
|
1328
|
+
}
|
|
1329
|
+
// ---------------------------------------------------------------------------
|
|
1330
|
+
// The read-only checklist (Verse: GET /api/verse/authority/setup)
|
|
1331
|
+
// ---------------------------------------------------------------------------
|
|
1332
|
+
/** Per `gh api` GET in the read-only probe. */
|
|
1333
|
+
const READ_ONLY_GH_TIMEOUT_MS = 10_000;
|
|
1334
|
+
/** All `gh api` reads of one read-only probe together. */
|
|
1335
|
+
const READ_ONLY_PROBE_BUDGET_MS = 30_000;
|
|
1336
|
+
const GH_API_PATH_RE = /^[A-Za-z0-9][A-Za-z0-9._~/?=&%-]{0,400}$/u;
|
|
1337
|
+
/**
|
|
1338
|
+
* The read-only probe's runner: `gh api <path>` — a GET, since no field,
|
|
1339
|
+
* method or input flag is ever passed — run asynchronously (the Verse server
|
|
1340
|
+
* must not block its event loop) and killed after READ_ONLY_GH_TIMEOUT_MS.
|
|
1341
|
+
* Anything else (git, `gh pr`, `gh repo create`, `--method`) is refused
|
|
1342
|
+
* without running, so a dry-run path that ever grew a write could not reach it.
|
|
1343
|
+
*/
|
|
1344
|
+
/** PURE: `gh api <path>` with nothing else — the only call the read-only probe runs. */
|
|
1345
|
+
export function isReadOnlyGhApiCall(bin, args) {
|
|
1346
|
+
return bin === 'gh' && args.length === 2 && args[0] === 'api' && GH_API_PATH_RE.test(args[1] ?? '');
|
|
1347
|
+
}
|
|
1348
|
+
export function readOnlyGhRun(bin, args) {
|
|
1349
|
+
if (!isReadOnlyGhApiCall(bin, args)) {
|
|
1350
|
+
return Promise.resolve({ status: 1, stdout: '', stderr: 'refused: the read-only setup probe only runs `gh api <path>` reads' });
|
|
1351
|
+
}
|
|
1352
|
+
return new Promise((done) => {
|
|
1353
|
+
execFile('gh', ['api', args[1]], {
|
|
1354
|
+
encoding: 'utf8',
|
|
1355
|
+
timeout: READ_ONLY_GH_TIMEOUT_MS,
|
|
1356
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
1357
|
+
env: { ...process.env, GH_PROMPT_DISABLED: '1', GH_NO_UPDATE_NOTIFIER: '1' },
|
|
1358
|
+
}, (error, stdout, stderr) => {
|
|
1359
|
+
const code = error?.code;
|
|
1360
|
+
done({
|
|
1361
|
+
status: error ? (typeof code === 'number' ? code : 1) : 0,
|
|
1362
|
+
stdout: stdout ?? '',
|
|
1363
|
+
stderr: stderr || (error ? error.message : ''),
|
|
1364
|
+
});
|
|
1365
|
+
});
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
function refusedInProbe(what) {
|
|
1369
|
+
throw new Error(`${what} is not available to the read-only setup probe`);
|
|
1370
|
+
}
|
|
1371
|
+
/**
|
|
1372
|
+
* `ashlr authority setup --dry-run --json` as a function: the same planning
|
|
1373
|
+
* path (cmdSetup's steps with --dry-run), with dependencies that cannot ask,
|
|
1374
|
+
* prompt, open a browser, reach the network directly or run anything but a
|
|
1375
|
+
* bounded `gh api` GET. Custody is only asked read-only questions (status,
|
|
1376
|
+
* host binding, public key, and whether the App can see a repo).
|
|
1377
|
+
*/
|
|
1378
|
+
export async function planAuthoritySetup(injected) {
|
|
1379
|
+
// The whole probe gets READ_ONLY_PROBE_BUDGET_MS of gh time: past it, a read
|
|
1380
|
+
// is answered "not read" at once (the step then says what it could not
|
|
1381
|
+
// check) instead of queueing more ten-second waits behind a slow network.
|
|
1382
|
+
const deadline = Date.now() + READ_ONLY_PROBE_BUDGET_MS;
|
|
1383
|
+
const budgeted = (bin, args) => (Date.now() > deadline
|
|
1384
|
+
? Promise.resolve({ status: 1, stdout: '', stderr: 'not read: the setup probe ran out of time' })
|
|
1385
|
+
: readOnlyGhRun(bin, args));
|
|
1386
|
+
const deps = {
|
|
1387
|
+
out: () => undefined,
|
|
1388
|
+
err: () => undefined,
|
|
1389
|
+
confirm: async () => false,
|
|
1390
|
+
readSecret: async () => refusedInProbe('a secret prompt'),
|
|
1391
|
+
run: budgeted,
|
|
1392
|
+
openBrowser: () => refusedInProbe('a browser'),
|
|
1393
|
+
fetch: (async () => refusedInProbe('the network')),
|
|
1394
|
+
custody: injected?.custody ?? await import('../core/authority/custody-client.js'),
|
|
1395
|
+
daemonService: () => realDaemonService(true),
|
|
1396
|
+
// The resident step only reads (admission verdict, service + plist
|
|
1397
|
+
// observation); the effects are refused here, never reached by setup.
|
|
1398
|
+
resident: {
|
|
1399
|
+
admission: realResidentDeps.admission,
|
|
1400
|
+
observe: realResidentDeps.observe,
|
|
1401
|
+
operatorRefusal: async () => 'the read-only setup probe is not the operator at a terminal',
|
|
1402
|
+
start: async () => refusedInProbe('resident start'),
|
|
1403
|
+
stop: async () => refusedInProbe('resident stop'),
|
|
1404
|
+
},
|
|
1405
|
+
...(injected ?? {}),
|
|
1406
|
+
};
|
|
1407
|
+
const parsed = { positional: ['setup'], flags: new Set(['--dry-run', '--json']), values: new Map() };
|
|
1408
|
+
return authoritySetupReport(await runSetup(parsed, deps, { dryRun: true, yes: false, print: false }), true);
|
|
1409
|
+
}
|
|
1410
|
+
/**
|
|
1411
|
+
* Every step of setup, in order. RERUN-SAFE: Mason runs it several times, so
|
|
1412
|
+
* each step first checks whether it is already in place and says so
|
|
1413
|
+
* ('already') instead of doing it again — the provenance key is rotated once,
|
|
1414
|
+
* a ruleset GitHub already holds is not PUT again, the App counts only when it
|
|
1415
|
+
* is installed, an open trust-root PR is found rather than reopened. A step
|
|
1416
|
+
* that throws is recorded as failed and the run still ends with its summary.
|
|
1417
|
+
*/
|
|
1418
|
+
async function runSetup(parsed, deps, opts) {
|
|
1419
|
+
const { dryRun, yes } = opts;
|
|
879
1420
|
const report = [];
|
|
880
|
-
const note = (step, status, detail) => {
|
|
881
|
-
report.push({ step, status, detail });
|
|
882
|
-
if (
|
|
1421
|
+
const note = (step, status, detail, extra = {}) => {
|
|
1422
|
+
report.push({ step, status, detail, ...extra });
|
|
1423
|
+
if (!opts.print)
|
|
883
1424
|
return;
|
|
884
1425
|
// 'skipped' gets its own glyph: a ✓ on a step a dry run did not perform read as done.
|
|
885
1426
|
const glyph = status === 'failed' ? '✗' : status === 'blocked' ? '×' : status === 'waiting-on-you' ? '…' : status === 'skipped' ? '·' : '✓';
|
|
886
1427
|
deps.out(`${glyph} ${step}: ${detail}`);
|
|
887
1428
|
};
|
|
888
1429
|
const done = () => {
|
|
889
|
-
// Every exit, including the first unmet live prerequisite,
|
|
890
|
-
//
|
|
1430
|
+
// Every exit, including the first unmet live prerequisite, reports the
|
|
1431
|
+
// resident step exactly once — it is never silently complete.
|
|
891
1432
|
if (!report.some((row) => row.step === 'resident runtime')) {
|
|
892
|
-
note('resident runtime', 'blocked',
|
|
1433
|
+
note('resident runtime', 'blocked', RESIDENT_NEEDS_GRANT, { command: 'ashlr authority grant' });
|
|
893
1434
|
}
|
|
894
|
-
return
|
|
1435
|
+
return report;
|
|
895
1436
|
};
|
|
896
1437
|
const ask = async (question) => !dryRun && (yes || deps.confirm(question));
|
|
1438
|
+
const failure = (error) => (error?.message ?? String(error)).replace(homedir(), '~').slice(0, 300);
|
|
897
1439
|
// --dry-run PLANS: the CHANGELOG and docs/AUTHORITY.md promise it "prints
|
|
898
1440
|
// every step first" (3.10 review c20). A real run must stop at an unmet
|
|
899
1441
|
// prerequisite; a dry run notes it and keeps describing the remaining
|
|
@@ -907,256 +1449,550 @@ async function cmdSetup(parsed, deps) {
|
|
|
907
1449
|
planning = true;
|
|
908
1450
|
return false;
|
|
909
1451
|
};
|
|
910
|
-
//
|
|
911
|
-
|
|
1452
|
+
// Which step is running: an exception nobody anticipated becomes its
|
|
1453
|
+
// failure (with the summary still printed), never a bare abort.
|
|
1454
|
+
let current = 'custody helper';
|
|
912
1455
|
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 {
|
|
1456
|
+
// 1. Custody helper (installing it needs sudo — Mason runs that himself).
|
|
1457
|
+
let custody = null;
|
|
934
1458
|
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)}…`);
|
|
1459
|
+
custody = await deps.custody.custodyStatus();
|
|
944
1460
|
}
|
|
945
1461
|
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`);
|
|
1462
|
+
note('custody helper', 'failed', `status unavailable (${failure(error)})`);
|
|
1463
|
+
}
|
|
1464
|
+
if (!custody || !custody.installed) {
|
|
1465
|
+
note('custody helper', 'waiting-on-you', 'run `sudo scripts/install-custody.sh` from the ashlr-hub checkout, then run setup again');
|
|
963
1466
|
if (stopHere())
|
|
964
1467
|
return done();
|
|
965
1468
|
}
|
|
966
1469
|
else {
|
|
967
|
-
|
|
968
|
-
keyId = info.keyId;
|
|
969
|
-
publicKeyPem = info.publicKeyPem;
|
|
970
|
-
note('signing key', 'done', `created ${info.keyId}`);
|
|
1470
|
+
note('custody helper', 'already', `installed${custody.version ? ` (${custody.version})` : ''}`);
|
|
971
1471
|
}
|
|
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) {
|
|
1472
|
+
// 1b. The helper and this CLI must agree on which Mac this is: the helper
|
|
1473
|
+
// derives StandingGrantV1.hostBinding as sha256(uppercase IOPlatformUUID)
|
|
1474
|
+
// and so does surface.ts. A disagreement would make every grant fail
|
|
1475
|
+
// "signed for a different Mac" only AFTER Mason's Touch ID — catch it first.
|
|
1476
|
+
current = 'host binding';
|
|
1477
|
+
if (planning) {
|
|
1478
|
+
note('host binding', 'skipped', 'would check that ashlr-custody and this release agree on which Mac this is');
|
|
1479
|
+
}
|
|
1480
|
+
else {
|
|
993
1481
|
try {
|
|
994
|
-
const
|
|
995
|
-
|
|
996
|
-
|
|
1482
|
+
const { currentHostBinding } = await import('../core/authority/surface.js');
|
|
1483
|
+
const ours = currentHostBinding();
|
|
1484
|
+
const helpers = await deps.custody.custodyHostBinding();
|
|
1485
|
+
if (ours === null)
|
|
1486
|
+
note('host binding', 'failed', "this Mac's IOPlatformUUID could not be read, so no grant can be bound to it");
|
|
1487
|
+
else if (ours !== helpers)
|
|
1488
|
+
note('host binding', 'failed', 'ashlr-custody and this release disagree on the host binding — reinstall the helper from this checkout');
|
|
1489
|
+
else
|
|
1490
|
+
note('host binding', 'already', `this Mac is ${ours.slice(0, 12)}…`);
|
|
997
1491
|
}
|
|
998
1492
|
catch (error) {
|
|
999
|
-
note('
|
|
1000
|
-
trustRootFailed = true;
|
|
1493
|
+
note('host binding', 'failed', `could not compare with ashlr-custody (${failure(error)})`);
|
|
1001
1494
|
}
|
|
1002
1495
|
}
|
|
1003
|
-
if (
|
|
1004
|
-
|
|
1005
|
-
|
|
1496
|
+
if (report.some((r) => r.step === 'host binding' && r.status === 'failed') && stopHere())
|
|
1497
|
+
return done();
|
|
1498
|
+
// 2. Secure Enclave key (Touch ID). The helper is not on PATH, so every
|
|
1499
|
+
// message names it by its installed path.
|
|
1500
|
+
current = 'signing key';
|
|
1501
|
+
let keyId = custody?.keyId ?? null;
|
|
1502
|
+
let publicKeyPem = null;
|
|
1503
|
+
if (planning || !custody) {
|
|
1504
|
+
note('signing key', 'skipped', 'would create the Secure Enclave signing key (Touch ID) unless it already exists');
|
|
1006
1505
|
}
|
|
1007
|
-
else if (
|
|
1008
|
-
|
|
1009
|
-
|
|
1506
|
+
else if (!custody.keyInitialized) {
|
|
1507
|
+
if (!(await ask('Create the Secure Enclave signing key now (Touch ID)?'))) {
|
|
1508
|
+
note('signing key', dryRun ? 'skipped' : 'waiting-on-you', dryRun
|
|
1509
|
+
? `would create it (Touch ID) — or run \`${CUSTODY_HELPER_PATH} init\``
|
|
1510
|
+
: `create it with \`${CUSTODY_HELPER_PATH} init\` or rerun setup`);
|
|
1511
|
+
if (stopHere())
|
|
1512
|
+
return done();
|
|
1513
|
+
}
|
|
1514
|
+
else {
|
|
1515
|
+
try {
|
|
1516
|
+
const info = await deps.custody.custodyInit();
|
|
1517
|
+
keyId = info.keyId;
|
|
1518
|
+
publicKeyPem = info.publicKeyPem;
|
|
1519
|
+
note('signing key', 'done', `created ${info.keyId}`);
|
|
1520
|
+
}
|
|
1521
|
+
catch (error) {
|
|
1522
|
+
note('signing key', 'failed', `not created (${failure(error)}) — rerun setup, or run \`${CUSTODY_HELPER_PATH} init\``);
|
|
1523
|
+
if (stopHere())
|
|
1524
|
+
return done();
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
else {
|
|
1529
|
+
note('signing key', 'already', `key ${keyId ?? '(id unknown)'}`);
|
|
1530
|
+
}
|
|
1531
|
+
// 3. Trust root compiled in (Mason's own PR).
|
|
1532
|
+
current = 'trust root';
|
|
1533
|
+
const { STANDING_GRANT_TRUST_ROOTS } = await import('../core/authority/trust-roots.js');
|
|
1534
|
+
// A key made on an earlier run whose id status did not report: read its
|
|
1535
|
+
// public half back (`ashlr-custody pubkey`, no Touch ID) instead of asking
|
|
1536
|
+
// Mason to copy it by hand.
|
|
1537
|
+
const isTrusted = (id) => id !== null && STANDING_GRANT_TRUST_ROOTS.some((root) => root.keyId === id);
|
|
1538
|
+
// No kickstart here: a daemon that is disabled or not loaded (the usual
|
|
1539
|
+
// state before the first grant) has nothing to kickstart, and the last step
|
|
1540
|
+
// (daemon service) says how to start it once the grant is in force.
|
|
1541
|
+
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`)';
|
|
1542
|
+
if (planning) {
|
|
1543
|
+
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');
|
|
1544
|
+
note('deploy', 'skipped', `would wait for you: ${DEPLOY_DETAIL}`);
|
|
1010
1545
|
}
|
|
1011
1546
|
else {
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1547
|
+
let trustRootFailed = false;
|
|
1548
|
+
if (!isTrusted(keyId) && !publicKeyPem) {
|
|
1549
|
+
try {
|
|
1550
|
+
const info = await deps.custody.custodyPublicKey();
|
|
1551
|
+
keyId = info.keyId;
|
|
1552
|
+
publicKeyPem = info.publicKeyPem;
|
|
1553
|
+
}
|
|
1554
|
+
catch (error) {
|
|
1555
|
+
note('trust root', 'failed', `could not read the custody public key (${failure(error)})`);
|
|
1556
|
+
trustRootFailed = true;
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
if (trustRootFailed) {
|
|
1560
|
+
if (stopHere())
|
|
1561
|
+
return done();
|
|
1562
|
+
}
|
|
1563
|
+
else if (isTrusted(keyId)) {
|
|
1564
|
+
note('trust root', 'already', `${keyId} is compiled into this release`);
|
|
1565
|
+
note('deploy', 'already', 'this release carries your trust root');
|
|
1016
1566
|
}
|
|
1017
1567
|
else {
|
|
1018
|
-
|
|
1568
|
+
const root = { keyId: keyId, alg: 'ES256', publicKeyPem: publicKeyPem };
|
|
1569
|
+
const invalid = validateCustodyRoot(root, deps.custody.keyIdForPublicKeyPem);
|
|
1570
|
+
if (invalid) {
|
|
1571
|
+
note('trust root', 'failed', invalid);
|
|
1572
|
+
}
|
|
1573
|
+
else {
|
|
1574
|
+
let pr;
|
|
1575
|
+
try {
|
|
1576
|
+
// A dry run looks too, through `gh api` reads only (the
|
|
1577
|
+
// Verse checklist is this path): an open PR, a merged key or a
|
|
1578
|
+
// pushed branch is reported instead of "add this root yourself".
|
|
1579
|
+
pr = (dryRun ? await probeTrustRootReadOnly(deps, root) : null) ?? await openTrustRootPr(parsed, deps, root, ask, !dryRun);
|
|
1580
|
+
}
|
|
1581
|
+
catch (error) {
|
|
1582
|
+
pr = { status: 'failed', detail: failure(error) };
|
|
1583
|
+
}
|
|
1584
|
+
note('trust root', pr.status, pr.detail, {
|
|
1585
|
+
...(pr.link ? { link: pr.link } : {}),
|
|
1586
|
+
...(pr.command !== undefined ? { command: pr.command } : {}),
|
|
1587
|
+
});
|
|
1588
|
+
note('deploy', 'waiting-on-you', DEPLOY_DETAIL);
|
|
1589
|
+
}
|
|
1590
|
+
if (stopHere())
|
|
1591
|
+
return done();
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
// The canary's existence decides whether the App and ruleset checks
|
|
1595
|
+
// include it yet (setup creates it at step 6). One read-only GET.
|
|
1596
|
+
const canaryRepo = 'ashlrai/fleet-canary';
|
|
1597
|
+
let canaryExists = planning ? false : (await deps.run('gh', ['api', `repos/${canaryRepo}`])).status === 0;
|
|
1598
|
+
// 4. GitHub App: its key in custody is half of it — it also has to be
|
|
1599
|
+
// INSTALLED on the repos the fleet works in, or every mint fails.
|
|
1600
|
+
current = 'GitHub App';
|
|
1601
|
+
if (!custody?.githubApp) {
|
|
1602
|
+
if (await ask(`Create the ${FLEET_APP_NAME} GitHub App now (one browser page)?`)) {
|
|
1019
1603
|
try {
|
|
1020
|
-
|
|
1604
|
+
const app = await runGithubAppFlow(deps, { org: one(parsed, '--org') ?? 'ashlrai' });
|
|
1605
|
+
// A new App is installed nowhere yet.
|
|
1606
|
+
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
1607
|
}
|
|
1022
1608
|
catch (error) {
|
|
1023
|
-
|
|
1609
|
+
note('GitHub App', 'failed', `not created (${failure(error)}) — rerun setup, or run \`ashlr authority github-app\``);
|
|
1024
1610
|
}
|
|
1025
|
-
note('trust root', pr.status, pr.detail);
|
|
1026
|
-
note('deploy', 'waiting-on-you', DEPLOY_DETAIL);
|
|
1027
1611
|
}
|
|
1028
|
-
|
|
1029
|
-
|
|
1612
|
+
else
|
|
1613
|
+
note('GitHub App', dryRun ? 'skipped' : 'waiting-on-you', 'run `ashlr authority github-app`');
|
|
1614
|
+
}
|
|
1615
|
+
else if (planning) {
|
|
1616
|
+
note('GitHub App', 'skipped', `the ${FLEET_APP_NAME} key is in custody; would check the App is installed on the enrolled repos`);
|
|
1030
1617
|
}
|
|
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
1618
|
else {
|
|
1049
|
-
await
|
|
1050
|
-
|
|
1619
|
+
const targets = (await setupTargetRepos()).map((repo) => repo.nameWithOwner)
|
|
1620
|
+
.filter((repo) => canaryExists || repo.toLowerCase() !== canaryRepo);
|
|
1621
|
+
const probe = await probeAppInstallation(deps, targets);
|
|
1622
|
+
const later = canaryExists ? '' : ` (${canaryRepo} needs it too once it exists)`;
|
|
1623
|
+
if (probe.missing.length > 0) {
|
|
1624
|
+
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 });
|
|
1625
|
+
}
|
|
1626
|
+
else if (probe.unknown.length > 0) {
|
|
1627
|
+
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 });
|
|
1628
|
+
}
|
|
1629
|
+
else {
|
|
1630
|
+
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`;
|
|
1631
|
+
// 3.13: it must also be able to post ashlr/verify (checks: write); an
|
|
1632
|
+
// App created before 3.13 has checks: read and must be raised by hand.
|
|
1633
|
+
const app = await readFleetApp(deps);
|
|
1634
|
+
if (app && app.checks !== 'write') {
|
|
1635
|
+
const urls = fleetAppPermissionUrls(app);
|
|
1636
|
+
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'}): `
|
|
1637
|
+
+ `set "Checks: Read and write" at ${urls.permissions}, accept the new permission on each installation at ${urls.installations}, then rerun setup`, { link: urls.permissions });
|
|
1638
|
+
}
|
|
1639
|
+
else {
|
|
1640
|
+
note('GitHub App', 'already', app
|
|
1641
|
+
? `the ${FLEET_APP_NAME} key is in custody; ${where}${later}; the App can post ${ASHLR_VERIFY_CHECK} (checks: write)`
|
|
1642
|
+
: `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})`);
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
// 5. Claude token for restricted judge / Leader calls.
|
|
1647
|
+
current = 'Claude token';
|
|
1648
|
+
if (custody?.claudeToken)
|
|
1649
|
+
note('Claude token', 'already', 'stored in custody');
|
|
1650
|
+
else if (await ask('Store a Claude token now (run `claude setup-token` in another terminal and paste it)?')) {
|
|
1651
|
+
try {
|
|
1652
|
+
const token = (await deps.readSecret('Paste the token (hidden): ')).trim();
|
|
1653
|
+
if (!token)
|
|
1654
|
+
note('Claude token', 'failed', 'nothing was pasted');
|
|
1655
|
+
else {
|
|
1656
|
+
await deps.custody.storeClaudeToken(token);
|
|
1657
|
+
note('Claude token', 'done', 'stored in custody (never written to disk here)');
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
catch (error) {
|
|
1661
|
+
note('Claude token', 'failed', `not stored (${failure(error)}) — run \`claude setup-token\`, then rerun setup`);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
else
|
|
1665
|
+
note('Claude token', dryRun ? 'skipped' : 'waiting-on-you', 'run `claude setup-token`, then rerun setup');
|
|
1666
|
+
// 6. Canary repo (the App cannot write workflows, so this uses your own gh
|
|
1667
|
+
// auth). Before the rulesets: the grant's server-enforced repos include the
|
|
1668
|
+
// canary, and a ruleset cannot be applied to a repo that does not exist yet.
|
|
1669
|
+
current = 'canary repo';
|
|
1670
|
+
if (canaryExists)
|
|
1671
|
+
note('canary repo', 'already', `${canaryRepo} exists`);
|
|
1672
|
+
else if (await ask(`Create the public repo ${canaryRepo} with its CI workflow (your gh auth)?`)) {
|
|
1673
|
+
const created = await deps.run('gh', ['repo', 'create', canaryRepo, '--public', '--add-readme', '--description', 'ashlr fleet canary (revert drills)']);
|
|
1674
|
+
const workflow = created.status === 0
|
|
1675
|
+
? await deps.run('gh', ['api', '--method', 'PUT', `repos/${canaryRepo}/contents/.github/workflows/ci.yml`, '--input', '-'], {
|
|
1676
|
+
input: JSON.stringify({ message: 'ci: canary workflow', content: Buffer.from(CANARY_WORKFLOW, 'utf8').toString('base64') }),
|
|
1677
|
+
})
|
|
1678
|
+
: null;
|
|
1679
|
+
canaryExists = created.status === 0;
|
|
1680
|
+
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));
|
|
1681
|
+
}
|
|
1682
|
+
else
|
|
1683
|
+
note('canary repo', dryRun ? 'skipped' : 'waiting-on-you', `create ${canaryRepo} with a CI workflow`);
|
|
1684
|
+
// 7. Rulesets — only where GitHub does not already hold exactly this one.
|
|
1685
|
+
current = 'rulesets';
|
|
1686
|
+
if (planning) {
|
|
1687
|
+
note('rulesets', 'skipped', 'would check the fleet ruleset on the server-enforced repos and apply it where it is missing or different');
|
|
1688
|
+
}
|
|
1689
|
+
else {
|
|
1690
|
+
const targets = (await setupTargetRepos()).filter((repo) => canaryExists || repo.nameWithOwner.toLowerCase() !== canaryRepo);
|
|
1691
|
+
const plans = await protectPlans(parsed, deps, targets);
|
|
1692
|
+
const stale = plans.filter((plan) => !plan.skipped && !plan.upToDate);
|
|
1693
|
+
const inPlace = plans.filter((plan) => !plan.skipped && plan.upToDate).length;
|
|
1694
|
+
const unreadable = plans.filter((plan) => plan.skipped?.startsWith('could not read')).map((plan) => plan.repo);
|
|
1695
|
+
const byHand = plans.filter((plan) => plan.skipped && !plan.skipped.startsWith('could not read')).length;
|
|
1696
|
+
const unread = unreadable.length > 0 ? `; could not read ${shortList(unreadable)} from GitHub` : '';
|
|
1697
|
+
const left = byHand > 0 ? `; ${byHand} left to you (local enforcement or no CI)` : '';
|
|
1698
|
+
if (plans.length === 0) {
|
|
1699
|
+
// Nothing to look at yet — never "in place on 0 repos".
|
|
1700
|
+
if (canaryExists)
|
|
1701
|
+
note('rulesets', 'already', 'no server-enforced repo needs one');
|
|
1702
|
+
else
|
|
1703
|
+
note('rulesets', dryRun ? 'skipped' : 'waiting-on-you', `${canaryRepo} gets the fleet ruleset once it exists`);
|
|
1704
|
+
}
|
|
1705
|
+
else if (stale.length > 0) {
|
|
1706
|
+
const where = shortList(stale.map((plan) => plan.repo));
|
|
1707
|
+
if (await ask(`Apply the fleet ruleset to ${stale.length} repo(s) where it is missing or different (${where})?`)) {
|
|
1708
|
+
const failures = await applyProtectPlans(stale, deps);
|
|
1709
|
+
note('rulesets', failures > 0 ? 'failed' : unreadable.length > 0 ? 'waiting-on-you' : 'done', failures === 0
|
|
1710
|
+
? `applied to ${where}${inPlace > 0 ? `; ${inPlace} already in place` : ''}${unread}${left}`
|
|
1711
|
+
: `${failures} of ${stale.length} failed (see above)${unread}`);
|
|
1712
|
+
}
|
|
1713
|
+
else {
|
|
1714
|
+
note('rulesets', dryRun ? 'skipped' : 'waiting-on-you', `${stale.length} repo(s) need the fleet ruleset (${where})${unread} — run \`ashlr authority protect --print\`, then \`--apply\``);
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
else if (unreadable.length > 0) {
|
|
1718
|
+
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)` : ''}`);
|
|
1719
|
+
}
|
|
1720
|
+
else {
|
|
1721
|
+
note('rulesets', 'already', `the fleet ruleset is in place on ${inPlace === 1 ? '1 repo' : `${inPlace} repos`}${left}`);
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
// 8. Retire the burned activation directory.
|
|
1725
|
+
current = 'old activation state';
|
|
1726
|
+
const activation = join(homedir(), '.ashlr', 'activation');
|
|
1727
|
+
if (!existsSync(activation))
|
|
1728
|
+
note('old activation state', 'already', 'nothing to retire');
|
|
1729
|
+
else if (await ask('Move ~/.ashlr/activation (the burned key) out of ~/.ashlr so no code path can reach it?')) {
|
|
1730
|
+
try {
|
|
1731
|
+
const target = join(homedir(), `ashlr-activation-retired-${new Date().toISOString().replace(/[:.]/g, '-')}`);
|
|
1732
|
+
renameSync(activation, target);
|
|
1733
|
+
note('old activation state', 'done', `moved to ${target.replace(homedir(), '~')} — move it offline, then delete it`);
|
|
1734
|
+
}
|
|
1735
|
+
catch (error) {
|
|
1736
|
+
note('old activation state', 'failed', `not moved (${failure(error)})`);
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
else
|
|
1740
|
+
note('old activation state', dryRun ? 'skipped' : 'waiting-on-you', 'archive ~/.ashlr/activation offline, then delete it');
|
|
1741
|
+
// 9. Provenance key — rotated ONCE. The burned key is whichever one no
|
|
1742
|
+
// rotation recorded; a key a rotation made (and recorded) is left alone,
|
|
1743
|
+
// because rotating again would invalidate every pending proposal for nothing.
|
|
1744
|
+
current = 'provenance key';
|
|
1745
|
+
const provenance = await provenanceKeyState();
|
|
1746
|
+
if (provenance.state === 'rotated') {
|
|
1747
|
+
note('provenance key', 'already', `rotated ${provenance.rotatedAt.slice(0, 10)}; not rotated again, so pending proposals keep verifying`);
|
|
1748
|
+
}
|
|
1749
|
+
else if (await ask(provenance.state === 'missing'
|
|
1750
|
+
? 'Create the provenance HMAC key now?'
|
|
1751
|
+
: 'Rotate the provenance HMAC key (agents could read the old one)?')) {
|
|
1752
|
+
try {
|
|
1753
|
+
const rotated = await rotateProvenanceKey();
|
|
1754
|
+
note('provenance key', 'done', rotated.retiredAs ? 'rotated; the old key was moved aside' : 'created');
|
|
1755
|
+
}
|
|
1756
|
+
catch (error) {
|
|
1757
|
+
note('provenance key', 'failed', `not rotated (${failure(error)})`);
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
else {
|
|
1761
|
+
const why = provenance.state === 'unhealthy' ? `the current key is unusable (${provenance.reason}); ` : '';
|
|
1762
|
+
note('provenance key', dryRun ? 'skipped' : 'waiting-on-you', `${why}run \`ashlr authority rotate-provenance\``);
|
|
1763
|
+
}
|
|
1764
|
+
// 10. First grant (Touch ID), 11. the switch, 12. the daemon service,
|
|
1765
|
+
// 13. resident admission (docs/RESIDENT-RUNTIME.md). Setup itself never
|
|
1766
|
+
// touches launchd: it names the one command that does.
|
|
1767
|
+
const DAEMON_DETAIL = 'would check that the ai.ashlr.daemon service is loaded and running (each tick re-verifies the grant)';
|
|
1768
|
+
if (planning) {
|
|
1769
|
+
note('standing grant', 'skipped', 'would sign the first standing grant (Touch ID)');
|
|
1770
|
+
note('autonomy switch', 'skipped', 'would offer to set the autonomy switch to Autonomous (the ladder starts in shadow)');
|
|
1771
|
+
note('daemon service', 'skipped', DAEMON_DETAIL);
|
|
1772
|
+
return done();
|
|
1773
|
+
}
|
|
1774
|
+
current = 'standing grant';
|
|
1775
|
+
const { evaluateStandingAuthority, displaySurfaceTarget } = await import('../core/authority/effective-config.js');
|
|
1776
|
+
const evaluation = evaluateStandingAuthority({ mode: 'cached', surface: displaySurfaceTarget() });
|
|
1777
|
+
let grantActive = evaluation.grantState === 'active';
|
|
1778
|
+
if (grantActive) {
|
|
1779
|
+
note('standing grant', 'already', `grant #${evaluation.grant?.grantSeq ?? '?'} is active`);
|
|
1780
|
+
}
|
|
1781
|
+
else if (await ask('Sign the first standing grant now (Touch ID)?')) {
|
|
1782
|
+
try {
|
|
1783
|
+
const { buildStandingGrantDraft } = await import('../core/verse/authority-api.js');
|
|
1784
|
+
const draft = await buildStandingGrantDraft('auto');
|
|
1785
|
+
for (const line of draft.summary)
|
|
1786
|
+
deps.out(` ${line}`);
|
|
1787
|
+
const signed = await signAndInstall(draft.payload, deps);
|
|
1788
|
+
grantActive = signed.ok;
|
|
1789
|
+
note('standing grant', signed.ok ? 'done' : 'failed', signed.ok ? `grant #${signed.grant.grantSeq} installed, starting at ${signed.grant.rollout.stages[0].id}` : signed.reason);
|
|
1790
|
+
}
|
|
1791
|
+
catch (error) {
|
|
1792
|
+
note('standing grant', 'failed', `no grant was drafted (${failure(error)}) — rerun setup, or run \`ashlr authority grant\``);
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
else
|
|
1796
|
+
note('standing grant', dryRun ? 'skipped' : 'waiting-on-you', 'run `ashlr authority grant`');
|
|
1797
|
+
// Resumable: a grant signed on an earlier run (or with `authority grant`)
|
|
1798
|
+
// still gets the switch offered — it used to be offered only in the run
|
|
1799
|
+
// that signed. Raising stays capped by the grant (requestAutonomySwitch).
|
|
1800
|
+
current = 'autonomy switch';
|
|
1801
|
+
if (!grantActive) {
|
|
1802
|
+
note('autonomy switch', dryRun ? 'skipped' : 'waiting-on-you', 'needs an active standing grant first');
|
|
1803
|
+
}
|
|
1804
|
+
else if (evaluation.switch === 'autonomous') {
|
|
1805
|
+
note('autonomy switch', 'already', 'Autonomous');
|
|
1806
|
+
}
|
|
1807
|
+
else if (await ask('Set the autonomy switch to Autonomous (the ladder starts in shadow)?')) {
|
|
1808
|
+
const { requestAutonomySwitch } = await import('../core/authority/effective-config.js');
|
|
1809
|
+
const switched = requestAutonomySwitch('autonomous', 'mason', 'ashlr authority setup');
|
|
1810
|
+
note('autonomy switch', switched.ok ? 'done' : 'failed', switched.ok ? 'Autonomous' : switched.reason);
|
|
1811
|
+
}
|
|
1812
|
+
else
|
|
1813
|
+
note('autonomy switch', dryRun ? 'skipped' : 'waiting-on-you', `it is ${evaluation.switch} — run \`ashlr authority switch autonomous\``);
|
|
1814
|
+
// 12. Read-only service observation. Only `resident start` (Mason, in his
|
|
1815
|
+
// own terminal, under the active grant) installs or restarts the service.
|
|
1816
|
+
current = 'daemon service';
|
|
1817
|
+
let service;
|
|
1818
|
+
try {
|
|
1819
|
+
service = await deps.daemonService();
|
|
1820
|
+
}
|
|
1821
|
+
catch {
|
|
1822
|
+
service = 'unknown';
|
|
1823
|
+
}
|
|
1824
|
+
const START = `\`${RESIDENT_START_COMMAND}\``;
|
|
1825
|
+
const after = grantActive ? `run ${START}` : `after the standing grant, run ${START}`;
|
|
1826
|
+
// Before the grant, the command that moves this step on is the grant.
|
|
1827
|
+
const serviceCommand = grantActive ? {} : { command: 'ashlr authority grant' };
|
|
1828
|
+
if (service === 'running')
|
|
1829
|
+
note('daemon service', 'already', 'ai.ashlr.daemon is running (each tick re-verifies the grant)');
|
|
1830
|
+
else if (service === 'absent')
|
|
1831
|
+
note('daemon service', 'waiting-on-you', `no ai.ashlr.daemon service is installed — ${after} to install it`, serviceCommand);
|
|
1832
|
+
else if (service === 'loaded')
|
|
1833
|
+
note('daemon service', 'waiting-on-you', `ai.ashlr.daemon is loaded but stopped — ${after} to restart it`, serviceCommand);
|
|
1834
|
+
else if (service === 'not-loaded')
|
|
1835
|
+
note('daemon service', 'waiting-on-you', `ai.ashlr.daemon is not loaded — ${after} to load it`, serviceCommand);
|
|
1836
|
+
else
|
|
1837
|
+
note('daemon service', 'waiting-on-you', `ai.ashlr.daemon state is unknown — \`ashlr authority resident status\` shows why; ${after} to repair it`, serviceCommand);
|
|
1838
|
+
// 13. Resident admission: the grant, this release and the service together.
|
|
1839
|
+
current = 'resident runtime';
|
|
1840
|
+
if (!grantActive) {
|
|
1841
|
+
note('resident runtime', 'blocked', RESIDENT_NEEDS_GRANT, { command: 'ashlr authority grant' });
|
|
1842
|
+
}
|
|
1843
|
+
else {
|
|
1844
|
+
const resident = await residentSetupRow(deps, service);
|
|
1845
|
+
note('resident runtime', resident.status, resident.detail, { command: resident.command });
|
|
1051
1846
|
}
|
|
1052
1847
|
}
|
|
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));
|
|
1848
|
+
catch (error) {
|
|
1849
|
+
// Nothing above is expected to throw; if something does, it is this
|
|
1850
|
+
// step's failure and the run still ends with the summary.
|
|
1851
|
+
note(report.some((row) => row.step === current) ? 'setup' : current, 'failed', `stopped: ${failure(error)}`);
|
|
1069
1852
|
}
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1853
|
+
return done();
|
|
1854
|
+
}
|
|
1855
|
+
/**
|
|
1856
|
+
* Setup's resident step (docs/RESIDENT-RUNTIME.md §d):
|
|
1857
|
+
* already admitted AND ai.ashlr.daemon running AND its plist is what
|
|
1858
|
+
* `resident start` would write from config now;
|
|
1859
|
+
* waiting-on-you the exact command that gets there (start / clear-stop / switch);
|
|
1860
|
+
* blocked a prerequisite is missing (no active grant, not a compiled
|
|
1861
|
+
* release, dirty build, no confinement, not macOS).
|
|
1862
|
+
* Read-only: never mints, never touches launchd.
|
|
1863
|
+
*/
|
|
1864
|
+
async function residentSetupRow(deps, service) {
|
|
1865
|
+
let admission;
|
|
1866
|
+
try {
|
|
1867
|
+
admission = await deps.resident.admission();
|
|
1076
1868
|
}
|
|
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`);
|
|
1869
|
+
catch (error) {
|
|
1870
|
+
return { status: 'failed', detail: `resident admission could not be evaluated (${error.message})` };
|
|
1087
1871
|
}
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
const rotated = await rotateProvenanceKey();
|
|
1093
|
-
note('provenance key', 'done', rotated.retiredAs ? 'rotated; the old key was moved aside' : 'created');
|
|
1872
|
+
if (!admission.ok) {
|
|
1873
|
+
const fix = admission.command ? ` — run \`${admission.command}\`` : '';
|
|
1874
|
+
// The checklist's command is exactly what the admission asks for (none when it names none).
|
|
1875
|
+
return { status: admission.status === 'waiting-on-you' ? 'waiting-on-you' : 'blocked', detail: `${admission.reason}${fix}`, command: admission.command ?? null };
|
|
1094
1876
|
}
|
|
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);
|
|
1877
|
+
let observed;
|
|
1878
|
+
try {
|
|
1879
|
+
observed = await deps.resident.observe();
|
|
1121
1880
|
}
|
|
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');
|
|
1881
|
+
catch (error) {
|
|
1882
|
+
return { status: 'failed', detail: `the resident service could not be observed (${error.message})` };
|
|
1129
1883
|
}
|
|
1130
|
-
|
|
1131
|
-
|
|
1884
|
+
if (observed.problem)
|
|
1885
|
+
return { status: 'blocked', detail: observed.problem, command: null };
|
|
1886
|
+
const start = '`ashlr authority resident start`';
|
|
1887
|
+
if (service !== 'running') {
|
|
1888
|
+
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
1889
|
}
|
|
1133
|
-
|
|
1134
|
-
const
|
|
1135
|
-
|
|
1136
|
-
|
|
1890
|
+
if (observed.plist === 'drifted') {
|
|
1891
|
+
const budget = observed.installedBudgetUsd !== observed.expectedBudgetUsd
|
|
1892
|
+
? ` (installed budget $${observed.installedBudgetUsd ?? '?'}/day, config says $${observed.expectedBudgetUsd ?? '?'}/day)`
|
|
1893
|
+
: '';
|
|
1894
|
+
return { status: 'waiting-on-you', detail: `the running service's plist differs from config${budget} — run ${start} to regenerate and restart it` };
|
|
1137
1895
|
}
|
|
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();
|
|
1896
|
+
if (observed.plist !== 'current') {
|
|
1897
|
+
return { status: 'waiting-on-you', detail: `the running service's plist could not be matched (${observed.plist}) — run ${start} to regenerate it` };
|
|
1145
1898
|
}
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1899
|
+
return { status: 'already', detail: `the resident daemon is running under grant #${admission.grantSeq ?? '?'} (until ${admission.expiresAt ?? '?'}), from clean release ${admission.revision?.slice(0, 12) ?? '?'}` };
|
|
1900
|
+
}
|
|
1901
|
+
// ---------------------------------------------------------------------------
|
|
1902
|
+
// resident start | stop | status
|
|
1903
|
+
// ---------------------------------------------------------------------------
|
|
1904
|
+
function describeAdmission(admission) {
|
|
1905
|
+
if (admission.ok)
|
|
1906
|
+
return `admitted — ${admission.reason}`;
|
|
1907
|
+
return `${admission.status} — ${admission.reason}${admission.command ? ` (run \`${admission.command}\`)` : ''}`;
|
|
1908
|
+
}
|
|
1909
|
+
async function cmdResident(sub, parsed, deps) {
|
|
1910
|
+
switch (sub) {
|
|
1911
|
+
case 'status':
|
|
1912
|
+
return cmdResidentStatus(parsed, deps);
|
|
1913
|
+
case 'start':
|
|
1914
|
+
return cmdResidentStart(parsed, deps);
|
|
1915
|
+
case 'stop':
|
|
1916
|
+
return cmdResidentStop(deps);
|
|
1917
|
+
default:
|
|
1918
|
+
deps.err('Usage: ashlr authority resident <start|stop|status [--json]>');
|
|
1919
|
+
return 2;
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
async function cmdResidentStatus(parsed, deps) {
|
|
1923
|
+
const admission = await deps.resident.admission();
|
|
1924
|
+
const observed = await deps.resident.observe();
|
|
1925
|
+
if (parsed.flags.has('--json')) {
|
|
1926
|
+
deps.out(JSON.stringify({ schema: 'ashlr.resident-status.v1', admission, service: observed }, null, 2));
|
|
1927
|
+
return 0;
|
|
1928
|
+
}
|
|
1929
|
+
deps.out(`Resident admission: ${describeAdmission(admission)}`);
|
|
1930
|
+
deps.out(`Service: ai.ashlr.daemon ${observed.state}${observed.plistPath ? ` (${observed.plistPath.replace(homedir(), '~')})` : ''}`);
|
|
1931
|
+
const budget = `installed $${observed.installedBudgetUsd ?? '—'}/day, config $${observed.expectedBudgetUsd ?? '?'}/day`;
|
|
1932
|
+
deps.out(`Plist: ${observed.plist}${observed.plist === 'drifted' ? ' — `ashlr authority resident start` regenerates it' : ''} (budget: ${budget})`);
|
|
1933
|
+
if (observed.problem)
|
|
1934
|
+
deps.out(` ${observed.problem}`);
|
|
1935
|
+
return 0;
|
|
1936
|
+
}
|
|
1937
|
+
/**
|
|
1938
|
+
* `resident start` — the only command that installs or restarts the resident
|
|
1939
|
+
* service. Order: refuse a non-operator context; show the verdict; stop if
|
|
1940
|
+
* not admitted; no-op when already running from the current plist; show
|
|
1941
|
+
* exactly what will happen and ask (no --yes: the operator confirms here);
|
|
1942
|
+
* then the real start, which re-verifies everything itself (mint).
|
|
1943
|
+
*/
|
|
1944
|
+
async function cmdResidentStart(parsed, deps) {
|
|
1945
|
+
if (parsed.flags.has('--yes')) {
|
|
1946
|
+
deps.err('resident start takes no --yes: confirm it yourself at the prompt.');
|
|
1947
|
+
return 2;
|
|
1948
|
+
}
|
|
1949
|
+
const refusal = await deps.resident.operatorRefusal();
|
|
1950
|
+
if (refusal) {
|
|
1951
|
+
deps.err(`resident start refused: ${refusal}`);
|
|
1952
|
+
return 1;
|
|
1953
|
+
}
|
|
1954
|
+
const admission = await deps.resident.admission();
|
|
1955
|
+
if (!admission.ok) {
|
|
1956
|
+
deps.err(`resident start refused: ${describeAdmission(admission)}`);
|
|
1957
|
+
return 1;
|
|
1958
|
+
}
|
|
1959
|
+
const observed = await deps.resident.observe();
|
|
1960
|
+
if (observed.problem) {
|
|
1961
|
+
deps.err(`resident start refused: ${observed.problem}`);
|
|
1962
|
+
return 1;
|
|
1963
|
+
}
|
|
1964
|
+
if (observed.state === 'running' && observed.plist === 'current') {
|
|
1965
|
+
deps.out(`ai.ashlr.daemon is already running under grant #${admission.grantSeq ?? '?'} from the current plist; nothing to do.`);
|
|
1966
|
+
return 0;
|
|
1967
|
+
}
|
|
1968
|
+
const action = observed.state === 'running' ? 'regenerate the plist from config and restart' : observed.plist === 'absent' ? 'install and start' : 'regenerate, load and start';
|
|
1969
|
+
deps.out(`Resident daemon under grant #${admission.grantSeq ?? '?'} (expires ${admission.expiresAt ?? '?'}):`);
|
|
1970
|
+
deps.out(` release ${admission.revision ?? '?'} (${admission.packageRoot?.replace(homedir(), '~') ?? '?'})`);
|
|
1971
|
+
deps.out(` service ai.ashlr.daemon → ${observed.plistPath?.replace(homedir(), '~') ?? '?'} (${observed.state}, plist ${observed.plist})`);
|
|
1972
|
+
deps.out(` budget $${observed.expectedBudgetUsd ?? '?'}/day from config daemon.dailyBudgetUsd${observed.installedBudgetUsd !== null && observed.installedBudgetUsd !== observed.expectedBudgetUsd ? ` (installed: $${observed.installedBudgetUsd})` : ''}`);
|
|
1973
|
+
deps.out(' launchd enable + bootstrap (RunAtLoad); every tick re-verifies the grant, Stop and the switch');
|
|
1974
|
+
if (!(await deps.confirm(`${action[0].toUpperCase()}${action.slice(1)} ai.ashlr.daemon now?`))) {
|
|
1975
|
+
deps.out('Nothing was changed.');
|
|
1976
|
+
return 1;
|
|
1977
|
+
}
|
|
1978
|
+
const result = await deps.resident.start();
|
|
1979
|
+
if (!result.ok) {
|
|
1980
|
+
deps.err(`resident start failed: ${result.reason}`);
|
|
1981
|
+
return 1;
|
|
1982
|
+
}
|
|
1983
|
+
deps.out(`✓ ${result.detail}`);
|
|
1984
|
+
deps.out('Stop anytime: `ashlr authority stop` halts agents (the service stays up, idle); `ashlr authority resident stop` removes the service.');
|
|
1985
|
+
return 0;
|
|
1986
|
+
}
|
|
1987
|
+
/** Lowering: never asks, never needs the grant. */
|
|
1988
|
+
async function cmdResidentStop(deps) {
|
|
1989
|
+
const result = await deps.resident.stop();
|
|
1990
|
+
if (!result.ok) {
|
|
1991
|
+
deps.err(`resident stop failed: ${result.reason}`);
|
|
1992
|
+
return 1;
|
|
1993
|
+
}
|
|
1994
|
+
deps.out(`${result.detail}. Stop (~/.ashlr/KILL) and the grant are unchanged.`);
|
|
1995
|
+
return 0;
|
|
1160
1996
|
}
|
|
1161
1997
|
function finish(report, deps, dryRun = false, json = false) {
|
|
1162
1998
|
const result = authoritySetupReport(report, dryRun);
|
|
@@ -1166,10 +2002,54 @@ function finish(report, deps, dryRun = false, json = false) {
|
|
|
1166
2002
|
}
|
|
1167
2003
|
else {
|
|
1168
2004
|
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
|
|
2005
|
+
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
2006
|
}
|
|
1171
2007
|
return summary.failed > 0 ? 1 : 0;
|
|
1172
2008
|
}
|
|
2009
|
+
const HUB_REPO = 'ashlrai/ashlr-hub';
|
|
2010
|
+
/**
|
|
2011
|
+
* The dry run's look at the trust-root PR, through `gh api <path>` GETs only
|
|
2012
|
+
* (each passes isReadOnlyGhApiCall, so the Verse probe can run it): the PR an
|
|
2013
|
+
* earlier run opened, the key already merged on the default branch, or a
|
|
2014
|
+
* branch an earlier run pushed without a PR. Same order as the live run's
|
|
2015
|
+
* probes. null = none of these (or GitHub could not be read): the step then
|
|
2016
|
+
* reads as it always has.
|
|
2017
|
+
*/
|
|
2018
|
+
async function probeTrustRootReadOnly(deps, root) {
|
|
2019
|
+
const branch = `authority/trust-root-${root.keyId}`;
|
|
2020
|
+
const read = async (path) => {
|
|
2021
|
+
const result = await deps.run('gh', ['api', path]);
|
|
2022
|
+
if (result.status !== 0)
|
|
2023
|
+
return null;
|
|
2024
|
+
try {
|
|
2025
|
+
return JSON.parse(result.stdout || 'null');
|
|
2026
|
+
}
|
|
2027
|
+
catch {
|
|
2028
|
+
return null;
|
|
2029
|
+
}
|
|
2030
|
+
};
|
|
2031
|
+
const open = await read(`repos/${HUB_REPO}/pulls?head=${HUB_REPO.split('/')[0]}%3A${encodeURIComponent(branch)}&state=open&per_page=1`);
|
|
2032
|
+
const url = Array.isArray(open) ? open[0]?.html_url : undefined;
|
|
2033
|
+
if (typeof url === 'string' && /^https:\/\/github\.com\/[^\s]+\/pull\/\d+$/u.test(url)) {
|
|
2034
|
+
return { status: 'waiting-on-you', detail: `${url} is already open — review and merge it yourself`, link: url, command: null };
|
|
2035
|
+
}
|
|
2036
|
+
const repo = await read(`repos/${HUB_REPO}`);
|
|
2037
|
+
const base = typeof repo?.default_branch === 'string' && /^[A-Za-z0-9._/-]{1,100}$/u.test(repo.default_branch) ? repo.default_branch : 'master';
|
|
2038
|
+
const file = await read(`repos/${HUB_REPO}/contents/src/core/authority/trust-roots.ts?ref=${encodeURIComponent(base)}`);
|
|
2039
|
+
if (file && file.encoding === 'base64' && typeof file.content === 'string'
|
|
2040
|
+
&& Buffer.from(file.content, 'base64').toString('utf8').includes(`keyId: '${root.keyId}'`)) {
|
|
2041
|
+
return {
|
|
2042
|
+
status: 'waiting-on-you',
|
|
2043
|
+
detail: `${root.keyId} is merged on ${base}, but this release was built before it — install a release built after it`,
|
|
2044
|
+
command: 'npm run build',
|
|
2045
|
+
};
|
|
2046
|
+
}
|
|
2047
|
+
const pushed = await read(`repos/${HUB_REPO}/git/ref/heads/${branch.split('/').map(encodeURIComponent).join('/')}`);
|
|
2048
|
+
if (pushed && pushed.ref === `refs/heads/${branch}`) {
|
|
2049
|
+
return { status: 'waiting-on-you', detail: `${branch} was pushed by an earlier run but has no PR yet — rerun setup to open it` };
|
|
2050
|
+
}
|
|
2051
|
+
return null;
|
|
2052
|
+
}
|
|
1173
2053
|
/**
|
|
1174
2054
|
* Open Mason's trust-root PR from a throwaway worktree (his checkout is never
|
|
1175
2055
|
* touched). Resumable: a rerun finds the PR it already opened, or the key
|
|
@@ -1180,73 +2060,76 @@ async function openTrustRootPr(parsed, deps, root, ask, probe) {
|
|
|
1180
2060
|
if (!source)
|
|
1181
2061
|
return { status: 'waiting-on-you', detail: 'pass --source <your ashlr-hub checkout> so setup can open the trust-root PR' };
|
|
1182
2062
|
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']);
|
|
2063
|
+
const base = async () => {
|
|
2064
|
+
const result = await deps.run('gh', ['repo', 'view', 'ashlrai/ashlr-hub', '--json', 'defaultBranchRef', '--jq', '.defaultBranchRef.name']);
|
|
1185
2065
|
return result.status === 0 && result.stdout.trim() ? result.stdout.trim() : 'master';
|
|
1186
2066
|
};
|
|
2067
|
+
const prLink = (text) => /https:\/\/github\.com\/\S+\/pull\/\d+/u.exec(text)?.[0];
|
|
1187
2068
|
let baseBranch = null;
|
|
1188
2069
|
let pushedAlready = false;
|
|
1189
2070
|
if (probe) {
|
|
1190
2071
|
// 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 // ""']);
|
|
2072
|
+
const open = await deps.run('gh', ['pr', 'list', '--repo', 'ashlrai/ashlr-hub', '--head', branch, '--state', 'open', '--json', 'url', '--jq', '.[0].url // ""']);
|
|
1192
2073
|
if (open.status === 0 && open.stdout.trim()) {
|
|
1193
|
-
|
|
2074
|
+
const url = open.stdout.trim();
|
|
2075
|
+
return { status: 'waiting-on-you', detail: `${url} is already open — review and merge it yourself`, ...(prLink(url) ? { link: prLink(url) } : {}) };
|
|
1194
2076
|
}
|
|
1195
|
-
baseBranch = base();
|
|
1196
|
-
const fetched = deps.run('git', ['-C', source, 'fetch', 'origin', baseBranch]);
|
|
2077
|
+
baseBranch = await base();
|
|
2078
|
+
const fetched = await deps.run('git', ['-C', source, 'fetch', 'origin', baseBranch]);
|
|
1197
2079
|
const onBase = fetched.status === 0
|
|
1198
|
-
? deps.run('git', ['-C', source, 'show', `origin/${baseBranch}:src/core/authority/trust-roots.ts`])
|
|
2080
|
+
? await deps.run('git', ['-C', source, 'show', `origin/${baseBranch}:src/core/authority/trust-roots.ts`])
|
|
1199
2081
|
: null;
|
|
1200
2082
|
if (onBase?.status === 0 && onBase.stdout.includes(`keyId: '${root.keyId}'`)) {
|
|
1201
2083
|
return { status: 'waiting-on-you', detail: `${root.keyId} is already on ${baseBranch}, but this release was built before it` };
|
|
1202
2084
|
}
|
|
1203
2085
|
// 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]);
|
|
2086
|
+
const remote = await deps.run('git', ['-C', source, 'ls-remote', '--exit-code', '--heads', 'origin', branch]);
|
|
1205
2087
|
pushedAlready = remote.status === 0 && remote.stdout.trim() !== '';
|
|
1206
2088
|
}
|
|
1207
2089
|
if (!(await ask(`Open a PR in ${source.replace(homedir(), '~')} adding key ${root.keyId} to trust-roots.ts?`))) {
|
|
1208
2090
|
return { status: 'waiting-on-you', detail: `add this root yourself: ${JSON.stringify({ keyId: root.keyId, alg: 'ES256' })}` };
|
|
1209
2091
|
}
|
|
1210
|
-
baseBranch ??= base();
|
|
1211
|
-
const createPr = (cwd) => {
|
|
1212
|
-
const pr = deps.run('gh', ['pr', 'create', '--repo', 'ashlrai/ashlr-hub', '--base', baseBranch, '--head', branch,
|
|
2092
|
+
baseBranch ??= await base();
|
|
2093
|
+
const createPr = async (cwd) => {
|
|
2094
|
+
const pr = await deps.run('gh', ['pr', 'create', '--repo', 'ashlrai/ashlr-hub', '--base', baseBranch, '--head', branch,
|
|
1213
2095
|
'--title', `authority: trust custody key ${root.keyId}`,
|
|
1214
2096
|
'--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
2097
|
if (pr.status !== 0)
|
|
1216
2098
|
return { status: 'failed', detail: `pushed ${branch}, but the PR could not be opened: ${pr.stderr.trim().slice(0, 200)}` };
|
|
1217
|
-
|
|
2099
|
+
const url = pr.stdout.trim();
|
|
2100
|
+
return { status: 'waiting-on-you', detail: `opened ${url} — review and merge it yourself`, ...(prLink(url) ? { link: prLink(url) } : {}) };
|
|
1218
2101
|
};
|
|
1219
2102
|
if (pushedAlready)
|
|
1220
2103
|
return createPr();
|
|
1221
2104
|
const worktree = mkdtempSync(join(tmpdir(), 'ashlr-trust-root-'));
|
|
1222
2105
|
rmSync(worktree, { recursive: true, force: true });
|
|
1223
|
-
const step = (bin, args, cwd) => {
|
|
1224
|
-
const result = deps.run(bin, args, cwd ? { cwd } : {});
|
|
2106
|
+
const step = async (bin, args, cwd) => {
|
|
2107
|
+
const result = await deps.run(bin, args, cwd ? { cwd } : {});
|
|
1225
2108
|
if (result.status !== 0)
|
|
1226
2109
|
throw new Error(`${bin} ${args[0] === '-C' ? args[2] : args[0]} failed: ${result.stderr.trim().slice(0, 200)}`);
|
|
1227
2110
|
};
|
|
1228
2111
|
let worktreeAdded = false;
|
|
1229
2112
|
try {
|
|
1230
|
-
step('git', ['-C', source, 'fetch', 'origin', baseBranch]);
|
|
2113
|
+
await step('git', ['-C', source, 'fetch', 'origin', baseBranch]);
|
|
1231
2114
|
// -B: a local branch left by an earlier run that stopped before the PR
|
|
1232
2115
|
// (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}`]);
|
|
2116
|
+
await step('git', ['-C', source, 'worktree', 'add', '-B', branch, worktree, `origin/${baseBranch}`]);
|
|
1234
2117
|
worktreeAdded = true;
|
|
1235
2118
|
const file = join(worktree, 'src', 'core', 'authority', 'trust-roots.ts');
|
|
1236
2119
|
const updated = renderTrustRootsWithKey(readFileSync(file, 'utf8'), root);
|
|
1237
2120
|
if (!updated)
|
|
1238
2121
|
return { status: 'failed', detail: `trust-roots.ts on ${baseBranch} already lists a different root — add the key by hand` };
|
|
1239
2122
|
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);
|
|
2123
|
+
await step('git', ['add', 'src/core/authority/trust-roots.ts'], worktree);
|
|
2124
|
+
await step('git', ['commit', '-m', `authority: trust custody key ${root.keyId}`], worktree);
|
|
2125
|
+
await step('git', ['push', '-u', 'origin', branch], worktree);
|
|
2126
|
+
return await createPr(worktree);
|
|
1244
2127
|
}
|
|
1245
2128
|
finally {
|
|
1246
|
-
deps.run('git', ['-C', source, 'worktree', 'remove', '--force', worktree]);
|
|
2129
|
+
await deps.run('git', ['-C', source, 'worktree', 'remove', '--force', worktree]);
|
|
1247
2130
|
// The pushed branch lives on origin; the local copy only blocks a rerun.
|
|
1248
2131
|
if (worktreeAdded)
|
|
1249
|
-
deps.run('git', ['-C', source, 'branch', '-D', branch]);
|
|
2132
|
+
await deps.run('git', ['-C', source, 'branch', '-D', branch]);
|
|
1250
2133
|
}
|
|
1251
2134
|
}
|
|
1252
2135
|
/** The enrolled checkout of ashlr-hub itself, if any. */
|