From 9044743931b5dfb43b88c6940d7c42d4e0d3673b Mon Sep 17 00:00:00 2001 From: rldyourmnd Date: Sat, 12 Sep 2026 22:37:31 +0500 Subject: [PATCH 1/3] fix(bootstrap): bind device setup and source review to their owning roots Signed-off-by: rldyourmnd --- .gitignore | 6 +- .serena/.auto_sync_head | 1 - core/app/github_readonly_test.go | 6 +- core/app/services.go | 4 +- core/app/source_operations.go | 20 +- core/app/source_owner_test.go | 30 +++ core/estate/compiler.go | 4 +- core/estate/compiler_test.go | 2 +- core/reconciler/reconciler_test.go | 7 +- docs/contracts/estate-v1.md | 16 +- docs/contracts/seed-bootstrap-v1.md | 21 +- docs/runbooks/bootstrap-device.md | 63 +++-- docs/runbooks/seed-clean-device.md | 398 +++++++--------------------- docs/source-register/README.md | 50 ++-- scripts/bootstrap-device.sh | 52 +++- tests/test_bootstrap_device.py | 74 ++++++ 16 files changed, 361 insertions(+), 393 deletions(-) delete mode 100644 .serena/.auto_sync_head create mode 100644 core/app/source_owner_test.go diff --git a/.gitignore b/.gitignore index f476fb2..ceb9908 100644 --- a/.gitignore +++ b/.gitignore @@ -20,7 +20,11 @@ __pycache__/ # make the worktree dirty for the assurance gate's RequireCleanWorktree check. /gds -# Serena policy lives in the tracked .serena/.gitignore file. +# Serena runtime state is device-local; authored project/memory files stay visible. +/.serena/.* +/.serena/cache/ +/.serena/logs/ +/.serena/project.local.yml .zcode/ # Device-local harness runtime output: adapter lock files and the installed diff --git a/.serena/.auto_sync_head b/.serena/.auto_sync_head deleted file mode 100644 index efbd4de..0000000 --- a/.serena/.auto_sync_head +++ /dev/null @@ -1 +0,0 @@ -f8930197661c66eb037fb04b1d31ac043096a14f diff --git a/core/app/github_readonly_test.go b/core/app/github_readonly_test.go index 8c1aef4..342d5c3 100644 --- a/core/app/github_readonly_test.go +++ b/core/app/github_readonly_test.go @@ -67,7 +67,7 @@ func TestGitHubInventoryAndReconciliationUseLiveReadOnlyRuntime(t *testing.T) { } planData, ok := plan.Data.(ReconciliationPlanData) if !ok || len(planData.Result.Inventory.Repositories) != 5 || - len(planData.Result.Drift) != 5 || len(planData.ExternalMutations) != 0 { + len(planData.Result.Drift) != 0 || len(planData.ExternalMutations) != 0 { t.Fatalf("plan data=%#v", plan.Data) } summary := services.ReportEstateSummary(context.Background(), root, GitHubReadOptions{ @@ -77,8 +77,8 @@ func TestGitHubInventoryAndReconciliationUseLiveReadOnlyRuntime(t *testing.T) { if summary.ExitClass != domain.ExitSuccess || !ok || summaryData.Repositories != 5 || summaryData.ManagementModes["observe-only"] != 3 || summaryData.ManagementModes["managed"] != 2 || - summaryData.IdentityStates["unassigned"] != 5 || - summaryData.DriftByClass["identity"] != 5 { + summaryData.IdentityStates["not-observed"] != 5 || + summaryData.DriftByClass["identity"] != 0 { t.Fatalf("summary=%#v", summary) } coverage := services.GitHubCoverage(context.Background(), root, GitHubCoverageOptions{ diff --git a/core/app/services.go b/core/app/services.go index 4fd09d3..1c25eb4 100644 --- a/core/app/services.go +++ b/core/app/services.go @@ -414,8 +414,8 @@ func (services *Services) policyInputsWithEstateRoot( return root, anchor, nil } -// projectionPolicyInputs permits a public module to render only its own local -// projections from policy sources shipped in that same public tree. It does +// projectionPolicyInputs resolves repository-owned projections and source +// verification against policy sources shipped in that same public tree. It does // not make the module an estate authority: every provider, workspace and // cross-repository operation continues to use policyInputs and therefore // requires a verified external control-plane. The compiler independently diff --git a/core/app/source_operations.go b/core/app/source_operations.go index 3c15927..8c96626 100644 --- a/core/app/source_operations.go +++ b/core/app/source_operations.go @@ -218,13 +218,13 @@ func (services *Services) VerifySourceVerification( if err != nil { return operationFailureEnvelope("gds source mark-verified verify", err) } - root, anchor, findings := services.policyInputs(ctx, path) + root, anchor, findings := services.projectionPolicyInputs(ctx, path) if len(findings) != 0 { return domain.NewEnvelope( "gds source mark-verified verify", classifyFindings(findings), nil, findings..., ) } - if finding := requireControlPlaneRole(anchor); finding != nil { + if finding := requireSourceOwnerRole(anchor); finding != nil { return domain.NewEnvelope( "gds source mark-verified verify", domain.ExitPolicy, nil, *finding, ) @@ -265,11 +265,14 @@ func (services *Services) sourceVerificationContext( approved *source.VerificationSpec, requireReproducible bool, ) (sourceVerificationContext, []domain.Finding) { - root, anchor, findings := services.policyInputs(ctx, path) + // Source review, like self-projection, writes only the repository-owned + // register. A registered private estate must not replace a public module's + // source boundary; provider and cross-repository operations stay separate. + root, anchor, findings := services.projectionPolicyInputs(ctx, path) if len(findings) != 0 { return sourceVerificationContext{}, findings } - if finding := requireControlPlaneRole(anchor); finding != nil { + if finding := requireSourceOwnerRole(anchor); finding != nil { return sourceVerificationContext{}, []domain.Finding{*finding} } compiled := services.Compiler.CompileDirectory(root, anchor, compiler.DevelopmentBundleVersion) @@ -375,15 +378,16 @@ func (services *Services) sourceVerificationContext( }, nil } -func requireControlPlaneRole(anchor domain.RepositoryAnchor) *domain.Finding { +func requireSourceOwnerRole(anchor domain.RepositoryAnchor) *domain.Finding { for _, role := range anchor.Repository.Roles { - if role == "control-plane" { + if role == "control-plane" || + (role == "module" && anchor.Classification.VisibilityContract == "public") { return nil } } return &domain.Finding{ - Code: "GDS_CONTROL_PLANE_ROLE_REQUIRED", Severity: domain.SeverityHigh, - Message: "Source verification mutations are restricted to the GDS control-plane repository.", + Code: "GDS_SOURCE_OWNER_ROLE_REQUIRED", Severity: domain.SeverityHigh, + Message: "Source verification requires the control plane or public module that owns the register.", Evidence: map[string]any{"repository_id": anchor.Repository.ID}, } } diff --git a/core/app/source_owner_test.go b/core/app/source_owner_test.go new file mode 100644 index 0000000..250ec1f --- /dev/null +++ b/core/app/source_owner_test.go @@ -0,0 +1,30 @@ +package app + +import ( + "github.com/NDDev-OpenNetwork/github-device-sync/core/domain" + "testing" +) + +func TestSourceVerificationUsesTheCanonicalRegisterOwner(t *testing.T) { + for _, tc := range []struct { + name string + roles []string + visibility string + allowed bool + }{ + {"private control plane", []string{"control-plane"}, "private", true}, + {"public engine module", []string{"project", "module"}, "public", true}, + {"ordinary public project", []string{"project"}, "public", false}, + {"private consumer module", []string{"module"}, "private", false}, + } { + t.Run(tc.name, func(t *testing.T) { + var anchor domain.RepositoryAnchor + anchor.Repository.Roles = tc.roles + anchor.Classification.VisibilityContract = tc.visibility + finding := requireSourceOwnerRole(anchor) + if (finding == nil) != tc.allowed { + t.Fatalf("allowed=%v finding=%#v", tc.allowed, finding) + } + }) + } +} diff --git a/core/estate/compiler.go b/core/estate/compiler.go index 4432f54..3364b34 100644 --- a/core/estate/compiler.go +++ b/core/estate/compiler.go @@ -46,7 +46,9 @@ func Compile( seenProviderIDs[repository.ProviderID] = struct{}{} assignment := Assignment{ ProviderID: repository.ProviderID, Owner: repository.Owner, Name: repository.Name, - Archived: repository.Archived, IdentityState: "unassigned", + // Provider listing does not observe a GDS anchor. Unknown identity + // is not proof that an already-onboarded repository lacks one. + Archived: repository.Archived, IdentityState: "not-observed", ManagementMode: config.Root.Discovery.DefaultManagementMode, RolloutRing: config.Root.Rollout.DefaultRing, } diff --git a/core/estate/compiler_test.go b/core/estate/compiler_test.go index b20150c..8aed085 100644 --- a/core/estate/compiler_test.go +++ b/core/estate/compiler_test.go @@ -43,7 +43,7 @@ func TestCompileTwoThousandRepositoriesAndForksDeterministically(t *testing.T) { managed := 0 for index, assignment := range compiled.Repositories { if assignment.ProviderID != int64(index+1000) || - assignment.MatchedSelector == "" || assignment.IdentityState != "unassigned" { + assignment.MatchedSelector == "" || assignment.IdentityState != "not-observed" { t.Fatalf("assignment[%d] = %#v", index, assignment) } if assignment.MatchedSelector == "organization-sources" { diff --git a/core/reconciler/reconciler_test.go b/core/reconciler/reconciler_test.go index 4e60bbf..515ad64 100644 --- a/core/reconciler/reconciler_test.go +++ b/core/reconciler/reconciler_test.go @@ -56,12 +56,17 @@ func TestReconcileAllCompilesFiveInstallationsAndTwoThousandRepositories(t *test }, }).ReconcileAll(context.Background()) if len(result.Findings) != 0 || len(result.Inventory.Repositories) != 2000 || - len(result.Installations) != 5 || len(result.Drift) != 2000 { + len(result.Installations) != 5 || len(result.Drift) != 0 { t.Fatalf( "repositories=%d installations=%#v drift=%d findings=%#v", len(result.Inventory.Repositories), result.Installations, len(result.Drift), result.Findings, ) } + for _, repository := range result.Inventory.Repositories { + if repository.IdentityState != "not-observed" { + t.Fatalf("listing invented anchor evidence: %#v", repository) + } + } } func TestReconcileAllIsolatesInstallationFailure(t *testing.T) { diff --git a/docs/contracts/estate-v1.md b/docs/contracts/estate-v1.md index c7beb9c..f6796b1 100644 --- a/docs/contracts/estate-v1.md +++ b/docs/contracts/estate-v1.md @@ -62,11 +62,17 @@ The compiler: - applies the unique highest-priority selector; - rejects equal-priority selector ambiguity; - emits deterministic provider-ID order; -- keeps GDS identity `unassigned` until repository onboarding proves an anchor. - -Fork lifecycle identity outranks name categories. The current `server-*` -selectors therefore explicitly require `fork: false`; a server-named fork is -classified by the fork selector. Organization and personal server portfolios +- reports GDS identity `not-observed` when only provider metadata was read. + Listing a repository cannot prove whether its GDS anchor exists. Inventory + reconciliation therefore does not manufacture identity drift or an onboarding + request for every listed repository; use an actual anchor/workspace audit to + establish that fact. Historical `unassigned` assignments remain readable and + retain their original JSON representation for signed audit verification. + +Repositories are classified by their owning account. The legacy `match.fork` +and `classification.fork_portfolio` fields remain readable for compatibility +but do not select a separate portfolio. Archive and name-specific selectors +retain their own priority. Organization and personal server portfolios use distinct device workspace roots so their filesystem placement remains injective even when owners contain repositories with the same name. diff --git a/docs/contracts/seed-bootstrap-v1.md b/docs/contracts/seed-bootstrap-v1.md index 6435ea0..260e3e9 100644 --- a/docs/contracts/seed-bootstrap-v1.md +++ b/docs/contracts/seed-bootstrap-v1.md @@ -1,9 +1,8 @@ # GDS clean-device seed bootstrap v1 contract -Status: seed contract defined; the trusted external-release acquisition and the -Ubuntu consumer leg remain `NOT_PROVEN` (completion plan residual #8, stage -`C9`). The interim owner-operated reproducible-source seed is locally -rehearsable on macOS `arm64`. +Status: implemented seed and installation contracts; device acceptance is +reported by each consumer from its actual OS/architecture and install evidence. +This public contract does not carry one private device's current rollout state. This contract types the zero-to-one handoff described operationally in `docs/runbooks/seed-clean-device.md`. It defines the boundary between a bare @@ -15,8 +14,8 @@ write authority. The seed spans exactly two adjacent mutation boundaries and stops at a third: -1. **OS bootstrap boundary** — `modules/macos-ubuntu-bootstrap`. Installs dev - tools and the CloakBrowser service; installs no `gds`. +1. **OS bootstrap boundary** — `modules/macos-ubuntu-bootstrap`. Installs the selected + profile and dev tools; installs no `gds`. 2. **Seed boundary (this contract)** — acquire, verify, and trust the first `gds` artifact; initialize local state; pass authority on. 3. **Control-plane boundary** — `gds-bootstrap-device` skill and the release, @@ -33,7 +32,7 @@ authentication stays an explicit owner handoff. | Input | Source | Constraint | |---|---|---| | device identity/profile | owner | canonical device ID, OS/arch, selected harnesses | -| OS bootstrap receipt | OS bootstrap boundary | dev-tool + CloakBrowser provisioning complete | +| OS bootstrap receipt | OS bootstrap boundary | selected OS profile verified | | bootstrap implementation | pinned `macos-ubuntu-bootstrap` release/commit | selected commit and `VERSION` verified before any apply; absolute `BOOTSTRAP_ROOT` | | seed verifier | owner-operated reproducible build, or previously trusted transfer | independently authenticated digest; compatibility floor checked; never taken from the release it verifies | | GDS artifact | trusted external release, or owner-operated reproducible source build | byte-identical reproducible build; six-file release dir when hosted | @@ -47,7 +46,7 @@ authentication stays an explicit owner handoff. 0. The bootstrap implementation is acquired at its selected immutable commit and its identity is verified before it is executed. No repository-relative path is used before an absolute root is established. -1. OS bootstrap receipt proves the base runtime and browser service exist. The +1. OS bootstrap receipt proves the selected base profile and tools exist. The bootstrap boundary never installs `gds` or the seed verifier. 1a. The seed verifier is acquired under a trust mechanism independent of the target release, authenticated against an out-of-band digest, and retired @@ -81,6 +80,6 @@ Before a seed target may be declared accepted (not merely rehearsed): - explicit credential handoff with no silent token collection; - durable install/upgrade/rollback receipts. -Ubuntu `24.04`/`26.04` acceptance remains `NOT_PROVEN` until produced on a real -VM. macOS lifecycle rehearsal is recorded; the external Linux rehearsal is the -open item (stage `C9`). +An OS/architecture is accepted only with the real-device evidence above. +Consumers keep their receipts and explicit gaps; neither a historical Mac +rehearsal nor one successful Linux installation certifies the whole matrix. diff --git a/docs/runbooks/bootstrap-device.md b/docs/runbooks/bootstrap-device.md index 98a7f9c..247bbbe 100644 --- a/docs/runbooks/bootstrap-device.md +++ b/docs/runbooks/bootstrap-device.md @@ -6,7 +6,7 @@ This runbook documents the single entry point that brings a new device through the three GDS mutation boundaries in order: ```text -OS bootstrap -> seed (Go toolchain + gds) -> control-plane staged commands +preflight -> source seed -> optional OS bootstrap -> control-plane plans ``` The entry point is `scripts/bootstrap-device.sh`, a phased orchestrator that @@ -15,6 +15,21 @@ reads a device descriptor (`estate/devices/.yaml`) and derives the `class:` block, so the device intent and the OS installer it drives cannot disagree. +The public engine and private consumer are separate Git roots. When the engine +is consumed as a gitlink, run it from the estate with an explicit root: + +```bash +modules/github-device-sync/scripts/bootstrap-device.sh \ + --estate-root . --device estate/devices/.yaml --phase 0 --plan +``` + +The script proves that its engine checkout and the sibling OS bootstrap checkout +match declared estate gitlinks and have no uncommitted changes. Device paths, +registration and runtime configuration resolve against the selected estate; +source version and Go builds resolve against the engine. The default source-root +mode remains available for standalone development layouts. Do not create a +second standalone copy of an already-consumed module. + It is the wrapper over the canonical, lower-level runbooks: - `seed-clean-device.md` — the zero-to-one seam for a stock device with no @@ -78,8 +93,12 @@ it, and use the verified binary. Source-build is the development/canary path. ### Phase 2 — OS bootstrap -**This phase requires interactive sudo.** The agent cannot enter the password. -Present the exact command to the owner and wait for confirmation. +Privileged operations use the OS installer's existing sudo/PolicyKit route. +If it requests an interactive password, the owner enters it directly; never +collect or pipe that password. Existing authorized passwordless sudo does not +require a second confirmation. Review the plan before applying an installer to +an already-provisioned desktop so its existing session and managed tools are +preserved. Invokes `bash modules/macos-ubuntu-bootstrap/scripts/bootstrap.sh --platform

--profile

[--gui|--no-gui] [--docker-mode ] [--apply|--plan]` @@ -87,28 +106,23 @@ with flags derived from the descriptor's `class:` block. This installs dev tools, language hosts (Node/uv/Bun), selected harness CLIs, and the browser layer. It never installs `gds`. Use `--plan` (default) for a dry-run first. -On Ubuntu desktop (`profile: desktop`, `gui: enabled`), the OS bootstrap also -calls `scripts/ubuntu/desktop.sh`, which: -- moves the GNOME dock to the bottom (macOS-style); -- adds a Russian keyboard layout with Alt+Shift toggle; -- installs BrowserOS (open-source agentic browser, `.deb`); -- removes the stock snap + apt Firefox completely. +Desktop contents and exact artifact versions belong to the selected +`macos-ubuntu-bootstrap` contract, including its Google Chrome GUI choice. +There is no BrowserOS/CloakBrowser provisioning prerequisite in this GDS path. +Each OS operation remains plan-aware and independently verifiable. -Each desktop step is independent and idempotent; sudo is refreshed per-step. +From the selected estate root, apply only the separately reviewed OS phase: -**Exact command for the owner (Ubuntu desktop example):** ```bash -cd ~/Developer/control-plane/github-device-sync -scripts/bootstrap-device.sh --device estate/devices/example-user-ubuntu-1.yaml --apply --from-phase 2 +modules/github-device-sync/scripts/bootstrap-device.sh \ + --estate-root . --device estate/devices/.yaml --phase 2 --apply ``` -The script prompts for sudo and runs to completion. If a step fails on expired -sudo, re-running resumes from the failed phase. ### Phase 3 — control-plane staged -Each step keeps its own plan/approval/apply/verify. The orchestrator extracts -plan and operation ids from the JSON envelopes and threads them through. These -steps do **not** require sudo. +Each step keeps its own plan/approval/apply/verify. The orchestrator reports +plans and diagnostics; it does not combine their writes or approvals. These +steps do not require sudo. - **3a release install** (release mode only) — skipped when bootstrapping from source, since there is no release directory. In release mode, set @@ -138,10 +152,12 @@ steps do **not** require sudo. ```bash # Plan the whole bootstrap (read-only) -scripts/bootstrap-device.sh --device estate/devices/.yaml --plan +modules/github-device-sync/scripts/bootstrap-device.sh --estate-root . \ + --device estate/devices/.yaml --plan # Apply only installer phases 0-2 -scripts/bootstrap-device.sh --device estate/devices/.yaml --phase 1 --apply +modules/github-device-sync/scripts/bootstrap-device.sh --estate-root . \ + --device estate/devices/.yaml --phase 1 --apply # Phase 3: run each printed plan command, sign its exact digest, then use # scripts/gds-exact-apply.sh for the separate enable/apply/verify sequence. @@ -154,9 +170,10 @@ toolchain. It does **not** edit `~/.bashrc` silently. ## Device integrity receipt -After a successful apply, phase 3d rebuilds and verifies a device integrity -receipt — a canonical-JSON snapshot that binds the device to the contract it -was bootstrapped against. The receipt lives at +The OS installer owns the device integrity receipt after its verification +passes. Combined phase-3 apply is disabled, so the read-only phase 3d does not +create that receipt. A receipt is a canonical-JSON snapshot binding the device +to the OS contract actually verified. The receipt lives at `~/.local/share/rldyour/device-receipt.json` (mode `0600`), mirroring the architecture of the browser runtime receipt. diff --git a/docs/runbooks/seed-clean-device.md b/docs/runbooks/seed-clean-device.md index a06a60e..2b12486 100644 --- a/docs/runbooks/seed-clean-device.md +++ b/docs/runbooks/seed-clean-device.md @@ -1,282 +1,70 @@ # GDS clean-device seed runbook -## Scope - -Use this runbook for the zero-to-one seam on a **stock supported device that has -no `gds` binary, no cloned control plane, and no initialized GDS state**: how an -owner brings that device from bare OS to a point where the -`gds-bootstrap-device` skill can take over. It is the documented entrypoint that -currently precedes `gds release verify`; nothing earlier existed before this -file. - -This runbook does not authorize a hosted release, artifact publication, tag, -GitHub Release, repository rollout, or any external mutation. It does not -weaken the release trust policy or the `release-lifecycle.md` stop conditions. - -Supported seed targets match the OS bootstrap contract: macOS `arm64`, and -Ubuntu `24.04`/`26.04` (`amd64`/`arm64`). - -## Status - -- macOS `arm64`: seed steps 0–4 are locally rehearsable; step 2b can use the - published release, and the seed verifier of step 2a is owner-operated. A full - stock-device rehearsal of the whole graph is **`NOT_PROVEN`**. -- Ubuntu `24.04`/`26.04`: **`NOT_PROVEN`.** Linux consumer execution remains - `NOT_PROVEN` (completion plan residual #8; stage `C9`). Do not claim a - clean-Ubuntu acceptance that was not produced on a real VM. -- The first external immutable release exists: `gds-v0.1.0` (source commit - `bace996`) was published on 2026-07-24T10:11:01Z with the six-file release - directory attached and keyless Sigstore SLSA provenance plus an SBOM - attestation. The hosted-workflow preconditions in `release-lifecycle.md` are - met (private example-org repository; harness runtime proof is delegated out of - the release gate). Its published assets are the six release files only: the - offline evidence attachment landed in the workflow after that tag, so for - `gds-v0.1.0` the evidence directory must still be reconstructed out-of-band. - Publication does not accept the bundle on this device — step 3 verification - still governs. - -Authority: `docs/contracts/authority-and-change-protocol-v1.md`. Typed handoff contract: -`docs/contracts/seed-bootstrap-v1.md`. Higher-level sequencing: -`docs/runbooks/bootstrap-device.md` (the `scripts/bootstrap-device.sh` orchestrator -that acquires the Go toolchain and drives this seam on a canary/source-build -device). - -## Prerequisites (non-secret) - -- Device identity and profile (device ID, OS/architecture, selected harnesses). -- Approved out-of-band value of the expected trusted-root digest, and of the - seed-verifier digest (step 2a). Never take the trust policy or either digest - from the same location that serves the release being verified. -- Canonical install root and a path for the local GDS state database. -- Owner-controlled GitHub authentication handled through the bootstrap auth - handoff, which is non-secret and never reads, prints, stores, or uploads - credentials (`modules/macos-ubuntu-bootstrap/scripts/auth-handoff.sh`). - -## Step 0 — Acquire the bootstrap implementation - -Every later command uses an absolute path defined by an earlier step. On a stock -device nothing in this repository is present yet, so the first act is to obtain -the pinned OS bootstrap implementation and prove which commit it is. - -The selected implementation is `example-org/macos-ubuntu-bootstrap` release -`2.6.1`, commit `6c4e953a0c3699103f3bcac233f9b0c87eea00ec`. The seed deliberately -follows the immutable release tag rather than the control plane's current gitlink: -the gitlink may sit ahead of the tag on documentation-only commits, and a -zero-to-one path must depend on something that cannot move. `docs/version-ledger.md` -records the current gitlink and labels any skew. Do not clone a mutable default -branch and do not pipe a remote script into a shell. - -```bash -BOOTSTRAP_COMMIT=6c4e953a0c3699103f3bcac233f9b0c87eea00ec -BOOTSTRAP_ROOT="$HOME/.local/share/gds-seed/macos-ubuntu-bootstrap" - -mkdir -p -- "$(dirname -- "$BOOTSTRAP_ROOT")" -git clone --no-checkout \ - https://github.com/example-org/macos-ubuntu-bootstrap.git "$BOOTSTRAP_ROOT" -git -C "$BOOTSTRAP_ROOT" fetch --depth 1 origin "$BOOTSTRAP_COMMIT" -git -C "$BOOTSTRAP_ROOT" checkout --detach "$BOOTSTRAP_COMMIT" -``` - -`git` is present on a stock Mac through the Command Line Tools; if it is not, -macOS prompts to install them on first invocation. Verify the selected identity -before running anything from the checkout, and stop if either check disagrees: - -```bash -test "$(git -C "$BOOTSTRAP_ROOT" rev-parse HEAD)" = "$BOOTSTRAP_COMMIT" -test "$(cat -- "$BOOTSTRAP_ROOT/VERSION")" = "2.0.0" -``` - -An owner who already has a verified control-plane checkout on this device may -instead set `BOOTSTRAP_ROOT` to its `modules/macos-ubuntu-bootstrap` path, after -confirming the same two checks. Every following step uses `$BOOTSTRAP_ROOT`. - -## Step 1 — OS bootstrap (dev tools, not GDS) - -Compose the base runtime with the pinned OS bootstrap adapter. This installs -Homebrew/LSPs, the immutable Node/uv/Bun runtime, the AI CLIs through their -owner modules, and the required CloakBrowser service. It does **not** install -`gds` and does not touch the control-plane release flow. +This procedure brings a supported device with no GDS installation to a verified +release and the estate/harness registration boundary. It describes a procedure, +not a claim that every OS/architecture has been accepted. Record actual device +acceptance and rollback evidence in the consuming estate. + +The current boundaries are the public GDS engine, the consuming private estate, +and the pinned OS-bootstrap module. A release does not contain private estate +intent or user credentials. The OS bootstrap does not install GDS. + +## Select immutable inputs + +The owner supplies the estate repository and exact commit, device descriptor, +release version/sequence/channel, independent consumer trust policy, canonical +installation/state paths and a trusted seed verifier. Use the device's declared +profile: Ubuntu GUI build work uses desktop-builds; headless server and macOS +desktop profiles have different Docker and execution contracts. + +Acquire the estate at its selected commit, verify its Git identity and then +initialize its exact submodules. The engine is at modules/github-device-sync +and the OS installer at modules/macos-ubuntu-bootstrap in the consuming layout. +Verify their gitlinks before executing their scripts. Do not clone a second +standalone copy of a consumed module or choose mutable latest/default-branch +contents as installation authority. + +GitHub authentication belongs to the device owner and vendor login flow. Do +not copy another device's credentials into the repository, read them into logs, +or place tokens in clone URLs. Local runtime configuration references that +authentication without embedding the secret. Preserve an existing desktop's +session and tools; an OS bootstrap plan is not authority to overwrite them. + +## Establish the seed verifier + +The first verifier must be trusted independently of the release being checked. +Use a previously trusted binary with an independently authenticated digest, or +an owner-operated reproducible build from a verified source commit and the +registered Go toolchain. Verify the exact seed bytes on the new device before +executing them. Its version must meet the target manifest's minimum CLI version; +a development prerelease can sort below a stable minimum even when its numeric +version looks equal. + +Source building is the development/canary seam documented in +[bootstrap-device.md](bootstrap-device.md). With a pinned consuming estate: ```bash -# plan first -bash "$BOOTSTRAP_ROOT/scripts/bootstrap.sh" --platform macos -bash "$BOOTSTRAP_ROOT/scripts/bootstrap.sh" --platform ubuntu --profile desktop -# apply only after reviewing the plan, on the target device -bash "$BOOTSTRAP_ROOT/scripts/bootstrap.sh" --platform macos --apply +modules/github-device-sync/scripts/bootstrap-device.sh \ + --estate-root . --device estate/devices/.yaml --phase 0 --plan +modules/github-device-sync/scripts/bootstrap-device.sh \ + --estate-root . --device estate/devices/.yaml --phase 1 --apply ``` -At release 2.0.0 the bootstrap installs the selected harnesses itself: when -`RLDYOUR_CODEX_MODULE` / `RLDYOUR_ZCODE_MODULE` are unset it clones each owner -module at its exact contract commit and runs that module's install lifecycle. -Set those variables only to override with an existing local checkout. Bootstrap -does **not** install `gds` and does **not** install the seed verifier of step 2. - -The OS bootstrap and the GDS control plane are two mutation boundaries. This -step ends with dev tools present but no `gds` on the device. - -## Step 2 — Establish a seed verifier and obtain the release - -Step 3 runs `gds release verify`. That `gds` cannot come from the release it is -about to verify — the trust chain would be circular — and the initial state of -this runbook says no `gds` is installed. So the seed verifier is acquired first, -under its own trust mechanism, and is retired once a verified release binary is -installed. - -### 2a — Seed verifier (`$SEED_GDS`) - -Use exactly one of the two mechanisms, in trust order. Both end with an absolute -`SEED_GDS` path and a recorded digest. +Source version/builds use the engine root; device intent and registration use +the estate root. A source seed does not by itself become a signed stable +release or authorize installation of an unverified target artifact. -1. **Owner-operated reproducible build on an already-trusted machine.** From a - fully tracked clean worktree of this control plane, at the commit whose - release is being installed: +## Acquire and verify the release - The version must be injected, not left at its development default. A plain - `go build` leaves `cli.Version` at `0.1.0-dev`, and the release verifier - compares that value to the manifest's `minimum_cli_version` with SemVer - precedence — where a prerelease sorts *below* the release of the same - number, so `0.1.0-dev` fails a `0.1.0` floor with - `GDS_RELEASE_CLI_VERSION_BLOCKED`. Build the seed with the same identity the - release builder injects, at or above the floor of the release being - installed: +Download the exact selected release's six-file release directory and offline +evidence using its immutable identity. Split evidence into the directory shape +specified by [release-lifecycle.md](release-lifecycle.md); failure envelopes and +auxiliary result JSON are not installable release contents. A release page with +only failure evidence is not a usable release. - ```bash - SEED_VERSION=0.1.0 # >= the target release manifest's minimum_cli_version - GOTOOLCHAIN=go1.26.7 go build -trimpath \ - -ldflags "-X github.com/NDDev-OpenNetwork/github-device-sync/core/cli.Version=$SEED_VERSION" \ - -o gds ./core/cmd/gds - shasum -a 256 gds # record; this is the seed digest - ``` - - The seed's trust comes from this out-of-band build and its recorded digest, - not from matching the released binary byte for byte: the release builder adds - `-s -w -buildid=` and the two artifacts are deliberately not the same bytes. - - Transfer the binary to the seed device out-of-band, place it at an absolute - path, and re-check the digest there: - - ```bash - SEED_GDS="$HOME/.local/share/gds-seed/bin/gds" - shasum -a 256 -- "$SEED_GDS" # must equal the recorded digest - chmod 0755 -- "$SEED_GDS" - ``` - - The reproducible builder requirement (byte-identical rebuild) is the same one - the release gate enforces (`docs/contracts/bundle-release-v1.md`), so an - independent rebuild of the same commit reproduces this digest. - -2. **Previously trusted transfer.** A `gds` binary carried from a device where - it was already verified, authenticated on arrival against a digest delivered - through the same approved out-of-band channel as the trusted-root digest — - never through the location serving the release. - -Both mechanisms must satisfy the compatibility floor before use. `--version` is -the supported query; there is no `version` subcommand: - -```bash -"$SEED_GDS" --version # prints: gds version -``` - -Read the SemVer it prints and stop unless it is greater than or equal to the -`minimum_cli_version` in the target release's `manifest.json` (step 2b downloads -that manifest; the floor can be read before verification because step 3 checks -it again against the signed manifest). - -Never obtain the seed by extracting it from the unverified target release, by a -mutable `latest` download, or by piping a remote installer into a shell. - -### 2b — Release and evidence assets - -The release workflow attaches the six release-directory files **and** the -offline evidence files to one GitHub Release, while the consumer requires two -separate directories and rejects a release directory that does not hold exactly -six entries. Download into a staging directory and split explicitly: - -```bash -RELEASE_TAG=gds-v0.1.0 -STAGING="$(mktemp -d)" -RELEASE_DIRECTORY="$HOME/.local/share/gds-seed/release" -EVIDENCE_DIRECTORY="$HOME/.local/share/gds-seed/evidence" - -gh release download "$RELEASE_TAG" \ - -R example-org/github-device-sync --dir "$STAGING" - -mkdir -p -- "$RELEASE_DIRECTORY" "$EVIDENCE_DIRECTORY" -for name in \ - manifest.json release-envelope.json bundle-trust.yaml SHA256SUMS \ - sbom.spdx.json "gds-bundle-${RELEASE_TAG#gds-}.tar.gz" -do - cp -- "$STAGING/$name" "$RELEASE_DIRECTORY/$name" -done -for name in provenance.sigstore.json sbom.sigstore.json trusted-root.jsonl; do - cp -- "$STAGING/$name" "$EVIDENCE_DIRECTORY/$name" -done -``` - -Auxiliary result JSON published alongside the evidence (`build-result.json`, -`verification-result.json`, `trusted-root-verification.json`) is diagnostic -only. Leave it in `$STAGING`; copying it into either consumer input breaks the -contract. Preflight before step 3, and stop on any mismatch: - -```bash -test "$(find "$RELEASE_DIRECTORY" -mindepth 1 -maxdepth 1 | wc -l | tr -d ' ')" = 6 -test ! "$(find "$RELEASE_DIRECTORY" "$EVIDENCE_DIRECTORY" -mindepth 1 ! -type f)" -for name in provenance.sigstore.json sbom.sigstore.json trusted-root.jsonl; do - test -s "$EVIDENCE_DIRECTORY/$name" -done -``` - -Releases published before the evidence assets were attached carry only the six -release files. **`gds-v0.1.0`, the only tag published so far, is one of them**: -its assets are `manifest.json`, `release-envelope.json`, `bundle-trust.yaml`, -`SHA256SUMS`, `sbom.spdx.json`, and `gds-bundle-v0.1.0.tar.gz`, and nothing else. -The evidence copy loop above therefore has nothing to copy for that tag and the -preflight stops the seed. That is the intended outcome, not a silent skip — but -it also means the flow is not closed by `gh release download` alone. - -Retrieve the evidence from the immutable artifact of the workflow run that -produced the tag: - -```bash -RELEASE_RUN=$(gh run list -R example-org/github-device-sync \ - --workflow release-bundle.yml --json databaseId,headSha,conclusion \ - --jq 'map(select(.conclusion=="success"))[0].databaseId') -gh run download "$RELEASE_RUN" -R example-org/github-device-sync --dir "$STAGING/run" -find "$STAGING/run" -type f \ - \( -name provenance.sigstore.json -o -name sbom.sigstore.json \ - -o -name trusted-root.jsonl \) -exec cp -- {} "$EVIDENCE_DIRECTORY/" \; -``` - -If the run's retention window has expired the evidence is unrecoverable for that -tag, and the correct action is to cut a new release that attaches it rather than -to seed a device without verification. - -### 2c — Materialize the trust inputs - -Step 3 needs two absolute paths that no earlier download provides. Establish -them here, because the trust policy must not come from the location that served -the release: - -```bash -LOCAL_TRUST_POLICY="$HOME/.local/share/gds-seed/consumer-trust.yaml" -GDS_STATE_PATH="$HOME/.local/state/gds/seed.db" -mkdir -p -- "$(dirname -- "$LOCAL_TRUST_POLICY")" "$(dirname -- "$GDS_STATE_PATH")" -``` - -Place the consumer trust policy at `$LOCAL_TRUST_POLICY` from the same approved -out-of-band channel that delivered the seed digest — the owner's trusted machine -or an internal distribution path — and confirm its digest there. The -`bundle-trust.yaml` inside `$RELEASE_DIRECTORY` is the release's own claim about -itself and is not a substitute. `$GDS_STATE_PATH` is created by the first -command that writes it; only its parent directory must exist. - -Never take the trust policy or the trusted root from the same location that -served the release. - -## Step 3 — Verify before install - -Run the read-only verification exactly as in `release-lifecycle.md`. `NOT_PROVEN` -is a stop condition, not a warning: +Obtain the consumer trust policy and trusted-root digest through an independent +approved channel. A policy downloaded alongside the artifact cannot establish +its own trust. With absolute paths to those prepared inputs: ```bash "$SEED_GDS" --json release verify \ @@ -286,40 +74,36 @@ is a stop condition, not a warning: --state-path "$GDS_STATE_PATH" ``` -Continue only on `success`. - -After step 4 installs the verified release, prove the installed binary's -identity (`gds --version` plus its recorded digest) and only then quarantine or -delete `$SEED_GDS`. Do not keep an unverified seed on the device as a fallback -verifier. - -## Step 4 — Hand off to the device bootstrap skill - -Authority now passes to `skills/canonical/gds-bootstrap-device/SKILL.md`, which -runs the staged, plan-first install with approval at each apply: pinned bundle -install (`release install --plan/--apply/--verify`), estate registration, -selected harness render/install, and `gds doctor`. Follow that skill; this -runbook does not duplicate its verbs. - -## Resumability and recovery - -- Every device mutation is plan → apply `` `--approval-ref` → verify, - and is idempotent on rerun. A partial operation is inspected with - `gds operation inspect `; do not repair installed records or the - acceptance database by hand. -- An interrupted seed re-enters at the first step whose result is not durably - recorded. Reboot/login between steps is safe. -- Rollback of an installed release follows the authorized rollback section of - `release-lifecycle.md`; it is an explicit exception to the monotonic sequence - floor and needs a schema-valid authorization plus an exact approval reference. - -## Stop conditions - -Stop without mutation on any of: a bootstrap checkout whose HEAD or `VERSION` -disagrees with step 0; a seed verifier whose digest was not independently -authenticated, or that was taken from the release it verifies; a release -directory that does not hold exactly six regular files, or an evidence directory -missing one of the three required inputs; an unpinned or changed trusted root; a release -obtained from an untrusted location whose trust policy came from that same -location; a missing approval reference; a `NOT_PROVEN` verification result; or -an attempt to install on Ubuntu and claim acceptance without real VM evidence. +Proceed only on success. Bad digests, missing attestations, unsupported minimum +versions and unproven trust stop installation. Preserve the previous accepted +release and monotonic acceptance floor; a failed release/tag is superseded by +a new immutable identity rather than overwritten. + +## Install and register + +Use the exact release install/upgrade plan, approval, one-shot enablement, apply +and verify commands in [release-lifecycle.md](release-lifecycle.md). Then bind +the device-local estate registration to its device ID, estate repository ID, +canonical root and anchor digest. Derive the private gh-CLI runtime configuration +from the declared installations and prove each account through read-only +inventory. Install only the selected harness adapters through their individual +plans; `gds harness sync` first classifies drift and does not itself apply it. + +The phased script prints phase-3 plans and diagnostics. It intentionally rejects +combined phase-3 apply; no single approval silently covers release installation, +estate registration and every harness. The owning task can authorize these +operations, but each journal still binds its own exact identity. + +## Acceptance and recovery + +Verify the installed executable/version/digest, estate context, doctor, selected +harness discovery, workspace placement and read-only provider reconciliation on +the actual device. Repeat verification and planning to establish idempotence, +and check a fresh login can resolve the installed commands. Record unsupported +or untested surfaces explicitly. + +Keep interrupted-download, wrong-digest, unavailable-source, exact installation, +upgrade/rollback and restart/login behavior as separate evidence. Tests on one +prepared desktop do not establish clean-system acceptance for the complete OS +matrix. Resume through the operation journal after failure; do not edit accepted +release state or erase history to make a retry pass. diff --git a/docs/source-register/README.md b/docs/source-register/README.md index 81b16b0..4f44fda 100644 --- a/docs/source-register/README.md +++ b/docs/source-register/README.md @@ -1,29 +1,33 @@ # GDS source register -This directory records volatile external facts that affect GDS implementation -or compatibility. Official documentation, source repositories, release pages, -and local runtime evidence are evidence; they do not authorize mutations. +`docs/source-register/sources.yaml` records volatile official facts used by the +engine, schemas and release verifier. Documentation and runtime observations +are evidence; they do not authorize a mutation or establish device acceptance. -`sources.yaml` is the current bootstrap register. A dedicated source-register -schema, freshness command, content-change detector, and release gate belong to -the source-maintenance phase. Until then, missing content digests are -`NOT_PROVEN`, not implied verification. +The source schema, freshness classifier, content-change detector, exact review +transaction and release freshness gate are implemented: -The currently installed `go1.26.4` toolchain is explicitly development-only. -The register pins `go1.26.7` as the current release builder because official -Go advisories identify security fixes in that release. The full Go validation -gate fails closed until the exact registered builder is available. +```bash +gds source status --json +gds source check --id --json +gds source mark-verified --help +``` -Phase 05 adds current official Codex instruction, skill, plugin, and hook pages -plus the Agent Skills specification and quality guidance. Those sources prove -documented contracts only. The Codex profile remains provisional until an exact -runtime version passes isolated discovery, invocation, hook, and visibility -tests. +Run review transactions in the repository that owns this register. A public +module uses its own source/policy root even when a private estate is registered; +it does not acquire control-plane authority over that estate. A private control +plane can review its own register. Other repository roles fail with +`GDS_SOURCE_OWNER_ROLE_REQUIRED`. Plans still bind the exact source, committed +Git identity, semantic review evidence and approval; apply and verify retain +those same boundaries. -Phase 10 adds official capability sources for the exact owner-selected harness -set. `antigravity-cli` is the single Google CLI identity. Its workspace-native -instruction and skill surfaces are `AGENTS.md` and `.agents/skills`; the vendor -global configuration directory is only a product locator. Current Cursor docs -and changelog cover CLI Agent Skills, while the installed MiMo Code runtime -provides bounded skill-discovery inspection. No profile becomes `supported` -from documentation or binary presence alone. +Review the actual governed claims before changing `verified_at`, `next_review`, +status or content digest. Changed bytes alone neither invalidate a claim nor +prove it remains true. Missing digests and unavailable official representations +remain explicit. The release builder and toolchain security floor are pinned +in this register and validated by `scripts/validate_go_core.sh`; a workstation's +installed version is not a public product fact. + +Harness documentation establishes documented interfaces only. Runtime support +and stable-release active-seven evidence require their own exact, fresh tests +and signed producer records, as described in the release lifecycle runbook. diff --git a/scripts/bootstrap-device.sh b/scripts/bootstrap-device.sh index 76bd237..2f9d56c 100755 --- a/scripts/bootstrap-device.sh +++ b/scripts/bootstrap-device.sh @@ -44,6 +44,7 @@ set -euo pipefail # ----------------------------- paths ----------------------------- SCRIPT_DIR=$(CDPATH='' cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) ROOT=$(CDPATH='' cd -- "$SCRIPT_DIR/.." && pwd) +SOURCE_ROOT="$ROOT" # Pinned toolchain (the security floor enforced by validate_go_core.sh). GO_VERSION="1.26.7" @@ -73,7 +74,7 @@ yaml_get() { local file="$1" path="$2" awk -v path="$path" ' function strip(s){ sub(/^[ \t]+/,"",s); sub(/[ \t]+$/,"",s); return s } - BEGIN { depth=split(path,p,"."); for(i=1;i<=depth;i++) want[i]=p[i] } + BEGIN { ctr=0; depth=split(path,p,"."); for(i=1;i<=depth;i++) want[i]=p[i] } { line=$0; sub(/#.*/,"",line) if (line ~ /^[ \t]*$/) next @@ -130,21 +131,22 @@ sha256_file() { } source_build_dirty() { - [ -n "$(git -C "$ROOT" status --porcelain --untracked-files=all -- core go.mod go.sum)" ] + [ -n "$(git -C "$SOURCE_ROOT" status --porcelain --untracked-files=all -- core go.mod go.sum)" ] } source_build_version() { local tag base revision dirty_suffix="" - tag=$(git -C "$ROOT" describe --tags --match 'gds-v[0-9]*' --abbrev=0 2>/dev/null || true) + tag=$(git -C "$SOURCE_ROOT" describe --tags --match 'gds-v[0-9]*' --abbrev=0 2>/dev/null || true) base=${tag#gds-v} [ -n "$base" ] || base="0.1.0-dev" - revision=$(git -C "$ROOT" rev-parse --short=12 HEAD) + revision=$(git -C "$SOURCE_ROOT" rev-parse --short=12 HEAD) source_build_dirty && dirty_suffix=".dirty" printf '%s+source.%s%s\n' "$base" "$revision" "$dirty_suffix" } # ----------------------------- args ----------------------------- DEVICE_PATH="" +ESTATE_ROOT="" APPLY=0 PHASE_ONLY="" FROM_PHASE="" @@ -171,6 +173,8 @@ Phase control (optional): --from-phase resume starting at phase N Other: + --estate-root consuming control plane; engine and OS bootstrap must + match its declared gitlinks (default: source root) --source-build-version print the deterministic source-build version and exit -h, --help show this help EOF @@ -179,6 +183,7 @@ EOF while [ "$#" -gt 0 ]; do case "$1" in --device) DEVICE_PATH="${2:?--device requires a path}"; shift 2;; + --estate-root) ESTATE_ROOT="${2:?--estate-root requires a path}"; shift 2;; --apply) APPLY=1; shift;; --plan) APPLY=0; shift;; --approval-ref) die "--approval-ref is removed: one reference cannot authorize multiple exact plans";; @@ -203,7 +208,42 @@ if [ "$APPLY" -eq 1 ]; then esac fi -# Resolve a relative device path against the repo root. +# Keep the source/build boundary separate from the consuming estate. A copied +# script or an unpinned module must not select a different engine or installer. +require_pinned_module() { + local module_root="$1" relative mode expected stage ignored actual declared + case "$module_root" in + "$ROOT"/*) relative=${module_root#"$ROOT"/} ;; + *) die "module is outside the selected estate: $module_root" ;; + esac + declared=0 + while read -r ignored actual; do + [ "$actual" != "$relative" ] || declared=1 + done < <(git -C "$ROOT" config -f .gitmodules --get-regexp '^submodule\..*\.path$' || true) + [ "$declared" -eq 1 ] || die "module is not declared in estate .gitmodules: $relative" + read -r mode expected stage ignored < <(git -C "$ROOT" ls-files --stage -- "$relative") || + die "module has no estate gitlink: $relative" + [ "$mode" = 160000 ] && [ "$stage" = 0 ] || die "module has no unambiguous estate gitlink: $relative" + actual=$(git -C "$module_root" rev-parse HEAD) + [ "$actual" = "$expected" ] || die "module checkout differs from estate gitlink: $relative" + [ -z "$(git -C "$module_root" status --porcelain --untracked-files=all)" ] || + die "module checkout has uncommitted changes: $relative" +} + +if [ -n "$ESTATE_ROOT" ]; then + ROOT=$(CDPATH='' cd -- "$ESTATE_ROOT" && pwd -P) + [ "$(git -C "$ROOT" rev-parse --show-toplevel)" = "$ROOT" ] || die "estate root must be a Git repository root" + [ -f "$ROOT/.gds/repository.yaml" ] || die "estate repository anchor is missing" + if [ "$ROOT" != "$SOURCE_ROOT" ]; then + require_pinned_module "$SOURCE_ROOT" + bootstrap_root=$(CDPATH='' cd -- "$ROOT/modules/macos-ubuntu-bootstrap" && pwd -P) + require_pinned_module "$bootstrap_root" + fi + BOOTSTRAP_SCRIPT="${ROOT}/modules/macos-ubuntu-bootstrap/scripts/bootstrap.sh" + DEVICE_INTEGRITY="${ROOT}/modules/macos-ubuntu-bootstrap/scripts/device_integrity.py" +fi + +# Resolve a relative device path against the selected control-plane root. [[ "$DEVICE_PATH" = /* ]] || DEVICE_PATH="$ROOT/$DEVICE_PATH" [ -f "$DEVICE_PATH" ] || die "device descriptor not found: $DEVICE_PATH" @@ -389,7 +429,7 @@ phase_1() { mkdir -p "$(dirname "$GDS_BIN_TARGET")" local candidate candidate=$(mktemp "${GDS_BIN_TARGET}.tmp.XXXXXX") - if ! (cd "$ROOT" && go build -trimpath \ + if ! (cd "$SOURCE_ROOT" && go build -trimpath \ -ldflags "-X github.com/NDDev-OpenNetwork/github-device-sync/core/cli.Version=${expected_version}" \ -o "$candidate" ./core/cmd/gds); then rm -f -- "$candidate" diff --git a/tests/test_bootstrap_device.py b/tests/test_bootstrap_device.py index a3b703e..e7a4759 100644 --- a/tests/test_bootstrap_device.py +++ b/tests/test_bootstrap_device.py @@ -191,3 +191,77 @@ def test_phase_three_combined_apply_is_removed() -> None: ) assert result.returncode != 0 assert "exact per-plan approve, enable, apply, and verify" in result.stderr + + +def test_embedded_bootstrap_binds_both_modules_to_the_selected_estate(tmp_path: Path) -> None: + """The real Git graph must bind the source and sibling installer before use.""" + import shutil + + def git(path: Path, *args: str) -> str: + return subprocess.check_output( + ["git", "-c", "protocol.file.allow=always", "-c", "commit.gpgsign=false", + "-c", "user.name=Example", "-c", "user.email=example@example.invalid", + "-C", str(path), *args], text=True, stderr=subprocess.DEVNULL, + ).strip() + + sources = tmp_path / "sources" + engine = sources / "engine" + installer = sources / "installer" + estate = tmp_path / "estate with spaces" + for repository in (engine, installer, estate): + repository.mkdir(parents=True) + git(repository, "init", "-b", "main") + (engine / "scripts").mkdir() + shutil.copy2(BOOTSTRAP, engine / "scripts/bootstrap-device.sh") + git(engine, "add", "scripts") + git(engine, "commit", "-m", "test: seed engine") + (installer / "scripts").mkdir() + (installer / "scripts/bootstrap.sh").write_text("#!/bin/sh\nexit 0\n") + git(installer, "add", "scripts") + git(installer, "commit", "-m", "test: seed installer") + (estate / ".gds").mkdir() + (estate / ".gds/repository.yaml").write_text("repository:\n id: example-estate\n") + (estate / "estate/devices").mkdir(parents=True) + device = estate / "estate/devices/example.yaml" + device.write_text( + "device:\n id: example-device\n name: example\n os: linux\n" + " architecture: x86_64\n class:\n profile: desktop-builds\n" + " gui: enabled\n docker_mode: rootful\n" + ) + git(estate, "submodule", "add", str(engine), "modules/github-device-sync") + git(estate, "submodule", "add", str(installer), "modules/macos-ubuntu-bootstrap") + git(estate, "add", ".gds", "estate", ".gitmodules", "modules") + git(estate, "commit", "-m", "test: pin estate") + fake_bin = tmp_path / "bin" + fake_bin.mkdir() + fake_gh = fake_bin / "gh" + fake_gh.write_text("#!/bin/sh\nexit 1\n") + fake_gh.chmod(0o755) + env = {**os.environ, "PATH": str(fake_bin) + os.pathsep + os.environ["PATH"]} + embedded = estate / "modules/github-device-sync/scripts/bootstrap-device.sh" + command = [str(embedded), "--estate-root", str(estate), "--device", + "estate/devices/example.yaml", "--phase", "0", "--plan"] + before = git(estate, "status", "--porcelain") + result = subprocess.run(command, env=env, capture_output=True, text=True) + assert result.returncode == 0, result.stdout + result.stderr + assert f"control-plane root: {estate}" in result.stdout + assert "OS installer present" in result.stdout + assert git(estate, "status", "--porcelain") == before == "" + + # Source identity comes from the engine, not the consuming estate commit. + version = subprocess.check_output( + [str(embedded), "--estate-root", str(estate), "--source-build-version"], + env=env, text=True, + ).strip() + assert "+source." + git(estate / "modules/github-device-sync", "rev-parse", "--short=12", "HEAD") in version + + installed_source = estate / "modules/macos-ubuntu-bootstrap" + (installed_source / "scripts/bootstrap.sh").write_text("#!/bin/sh\nexit 7\n") + result = subprocess.run(command, env=env, capture_output=True, text=True) + assert result.returncode != 0 + assert "uncommitted changes" in result.stdout + result.stderr + git(installed_source, "add", "scripts/bootstrap.sh") + git(installed_source, "commit", "-m", "test: unpublished installer revision") + result = subprocess.run(command, env=env, capture_output=True, text=True) + assert result.returncode != 0 + assert "differs from estate gitlink" in result.stdout + result.stderr From 5eec9040ac4c5c2a61bebac908be02dacd6c33bf Mon Sep 17 00:00:00 2001 From: rldyourmnd Date: Sat, 12 Sep 2026 22:42:04 +0500 Subject: [PATCH 2/3] chore(projections): bind current source ownership and bootstrap contracts Signed-off-by: rldyourmnd --- .gds/bundle.lock.yaml | 10 +++++----- .github/workflows/gds-ci.yml | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.gds/bundle.lock.yaml b/.gds/bundle.lock.yaml index def97b1..6c22c56 100644 --- a/.gds/bundle.lock.yaml +++ b/.gds/bundle.lock.yaml @@ -5,14 +5,14 @@ bundle: version: "0.9.1-dev" release_sequence: 0 channel: "development" - source_tree_digest: "sha256:498e81afa9eb5da38751ce33334d3df18c02197b15084f08749380145c15f885" - digest: "sha256:be355c26fc2447e8d56c6fb255aa322cdcad1e142e3130c66f6c8744e70352bc" + source_tree_digest: "sha256:3cacaa23205120c46abf2b2e8c71c7063f95124688f2c2284af2b601545b7751" + digest: "sha256:d7150724f79195ecffdc35a8c722d72f003632e7fdd1c826144d8a3cf11d7a8b" projection: - input_digest: "sha256:cc6d3b26ef3bfd1f03d14dcf3647156bc72aa3fc2f3ad3ecf7833ce97c738432" - output_digest: "sha256:a8a615008f33ae1297c85b4d0fec632ce70260f3520b5da6be5f6c5869429753" + input_digest: "sha256:a69fc0431080b0849b303ab1fec4313cb556449aa3946dddbbf9d2b18de645b2" + output_digest: "sha256:19302391ff426b6c2317bb281be0554045803580a83b868ded9340cc3d33a808" files: - path: ".gds/compiled-policy.json" digest: "sha256:f8b613f78ef25fb46e44ea482044932c1b7bd1780f189a72bed172a242bbec52" - path: ".github/workflows/gds-ci.yml" - digest: "sha256:fab957abdce4c6e8109d23f29bfab499b9418b5b08ed6846c19bae31adda45b5" + digest: "sha256:64ca46a0a1d295b2e6fb5eeaac4818d02cc6ee4867d7822eb45f0dec26f4d90f" diff --git a/.github/workflows/gds-ci.yml b/.github/workflows/gds-ci.yml index f6019cf..ae9e910 100644 --- a/.github/workflows/gds-ci.yml +++ b/.github/workflows/gds-ci.yml @@ -1,8 +1,8 @@ # GENERATED FILE - DO NOT EDIT DIRECTLY # generator: gds # bundle: 0.9.1-dev -# source-tree-digest: sha256:498e81afa9eb5da38751ce33334d3df18c02197b15084f08749380145c15f885 -# input-digest: sha256:cc6d3b26ef3bfd1f03d14dcf3647156bc72aa3fc2f3ad3ecf7833ce97c738432 +# source-tree-digest: sha256:3cacaa23205120c46abf2b2e8c71c7063f95124688f2c2284af2b601545b7751 +# input-digest: sha256:a69fc0431080b0849b303ab1fec4313cb556449aa3946dddbbf9d2b18de645b2 # output-digest: sha256:8c045e745cc69b731bc695a4a9d58a48c10f1ab7dd85b7354db7bfd0e072711c # edit-source: # - .gds/repository.yaml From fca1b90456afe5f68eb843bcd7798517d4690e7f Mon Sep 17 00:00:00 2001 From: rldyourmnd Date: Sat, 12 Sep 2026 22:47:52 +0500 Subject: [PATCH 3/3] fix(bootstrap): verify the exact path returned for the pinned module Signed-off-by: rldyourmnd --- scripts/bootstrap-device.sh | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/scripts/bootstrap-device.sh b/scripts/bootstrap-device.sh index 2f9d56c..0a8be6a 100755 --- a/scripts/bootstrap-device.sh +++ b/scripts/bootstrap-device.sh @@ -211,19 +211,20 @@ fi # Keep the source/build boundary separate from the consuming estate. A copied # script or an unpinned module must not select a different engine or installer. require_pinned_module() { - local module_root="$1" relative mode expected stage ignored actual declared + local module_root="$1" relative mode expected stage recorded actual declared case "$module_root" in "$ROOT"/*) relative=${module_root#"$ROOT"/} ;; *) die "module is outside the selected estate: $module_root" ;; esac declared=0 - while read -r ignored actual; do + while read -r recorded actual; do [ "$actual" != "$relative" ] || declared=1 done < <(git -C "$ROOT" config -f .gitmodules --get-regexp '^submodule\..*\.path$' || true) [ "$declared" -eq 1 ] || die "module is not declared in estate .gitmodules: $relative" - read -r mode expected stage ignored < <(git -C "$ROOT" ls-files --stage -- "$relative") || + read -r mode expected stage recorded < <(git -C "$ROOT" ls-files --stage -- "$relative") || die "module has no estate gitlink: $relative" - [ "$mode" = 160000 ] && [ "$stage" = 0 ] || die "module has no unambiguous estate gitlink: $relative" + [ "$mode" = 160000 ] && [ "$stage" = 0 ] && [ "$recorded" = "$relative" ] || + die "module has no unambiguous estate gitlink: $relative" actual=$(git -C "$module_root" rev-parse HEAD) [ "$actual" = "$expected" ] || die "module checkout differs from estate gitlink: $relative" [ -z "$(git -C "$module_root" status --porcelain --untracked-files=all)" ] ||