Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .gds/bundle.lock.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
4 changes: 2 additions & 2 deletions .github/workflows/gds-ci.yml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion .serena/.auto_sync_head

This file was deleted.

6 changes: 3 additions & 3 deletions core/app/github_readonly_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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{
Expand Down
4 changes: 2 additions & 2 deletions core/app/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 12 additions & 8 deletions core/app/source_operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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},
}
}
Expand Down
30 changes: 30 additions & 0 deletions core/app/source_owner_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
4 changes: 3 additions & 1 deletion core/estate/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down
2 changes: 1 addition & 1 deletion core/estate/compiler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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" {
Expand Down
7 changes: 6 additions & 1 deletion core/reconciler/reconciler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
16 changes: 11 additions & 5 deletions docs/contracts/estate-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
21 changes: 10 additions & 11 deletions docs/contracts/seed-bootstrap-v1.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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,
Expand All @@ -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 |
Expand All @@ -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
Expand Down Expand Up @@ -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.
63 changes: 40 additions & 23 deletions docs/runbooks/bootstrap-device.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -15,6 +15,21 @@ reads a device descriptor (`estate/devices/<device>.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/<device>.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
Expand Down Expand Up @@ -78,37 +93,36 @@ 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 <p> --profile <p> [--gui|--no-gui] [--docker-mode <m>] [--apply|--plan]`
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/<device>.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
Expand Down Expand Up @@ -138,10 +152,12 @@ steps do **not** require sudo.

```bash
# Plan the whole bootstrap (read-only)
scripts/bootstrap-device.sh --device estate/devices/<device>.yaml --plan
modules/github-device-sync/scripts/bootstrap-device.sh --estate-root . \
--device estate/devices/<device>.yaml --plan

# Apply only installer phases 0-2
scripts/bootstrap-device.sh --device estate/devices/<device>.yaml --phase 1 --apply
modules/github-device-sync/scripts/bootstrap-device.sh --estate-root . \
--device estate/devices/<device>.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.
Expand All @@ -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.

Expand Down
Loading