From ff63ac07f15e049379612225c4fa277013b8e7bd Mon Sep 17 00:00:00 2001 From: Danil Silantyev Date: Tue, 15 Sep 2026 05:39:34 +0500 Subject: [PATCH 1/2] fix(device): allow desktop-server Docker none|rootful|rootless Match macos-ubuntu-bootstrap: desktop-server may take an explicit Docker mode. Forward that mode and server-baseline hardening from the orchestrator. Co-authored-by: Cursor --- CHANGELOG.md | 4 ++ README.md | 6 +-- core/validation/device_class_test.go | 31 +++++++-------- core/validation/schema.go | 7 +++- docs/contracts/estate-v1.md | 2 +- docs/runbooks/bootstrap-device.md | 2 +- estate/devices/example-user-ubuntu-1.yaml | 8 ++-- schemas/v1/device.schema.json | 2 +- scripts/bootstrap-device.sh | 31 +++++++++++---- scripts/validate_gds_schemas.py | 39 ++++++++++++++++++- skills/canonical/gds-orient/SKILL.md | 11 +++--- tests/fixtures/schemas/v1/cases.json | 6 +++ .../v1/valid-device-class-desktop-server.yaml | 30 ++++++++++++++ tests/test_bootstrap_device.py | 8 ++++ 14 files changed, 143 insertions(+), 44 deletions(-) create mode 100644 tests/fixtures/schemas/v1/valid-device-class-desktop-server.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index 95226cd..30f0a4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ Versioning. ## [Unreleased] +- Align the desktop-server device class with macos-ubuntu-bootstrap: Docker + may be none, rootful, or rootless. The bootstrap orchestrator forwards the + declared docker_mode and server-baseline hardening flags. + ## [0.9.2] - 2026-09-12 - Add a public, exact-attempt harness evidence producer that rejects private diff --git a/README.md b/README.md index fe27312..3257ee2 100644 --- a/README.md +++ b/README.md @@ -65,9 +65,9 @@ validated against their Git-reported superproject and typed top-level placement drift. A device descriptor may declare an optional `class:` block -(`profile: desktop|server`, `gui`, `docker_mode`, `execution_policy`, and -server-only `hardening`) so the device intent and the OS installer it drives -cannot disagree. The phased bootstrap orchestrator +(`profile: desktop|desktop-builds|desktop-server|server`, `gui`, `docker_mode`, +`execution_policy`, and server-baseline `hardening`) so the device intent and +the OS installer it drives cannot disagree. The phased bootstrap orchestrator `scripts/bootstrap-device.sh` reads the class and drives the OS bootstrap, the seed Go toolchain + `gds` build, and the control-plane staged commands in order, with plan/apply gates at each boundary. See diff --git a/core/validation/device_class_test.go b/core/validation/device_class_test.go index 506dbfc..3128143 100644 --- a/core/validation/device_class_test.go +++ b/core/validation/device_class_test.go @@ -99,15 +99,20 @@ func TestDeviceClassAcceptsDesktopServer(t *testing.T) { if err != nil { t.Fatal(err) } - value := deviceClassFixture(t, map[string]any{ - "profile": "desktop-server", - "gui": "enabled", - "docker_mode": "none", - "execution_policy": "interactive-desktop-server", - "hardening": map[string]any{"ssh": true, "ufw": true, "fail2ban": true}, - }) - if findings := set.Validate("device", value, "test"); len(findings) != 0 { - t.Fatalf("expected no findings, got %#v", findings) + for _, dockerMode := range []string{"none", "rootful", "rootless"} { + t.Run(dockerMode, func(t *testing.T) { + t.Parallel() + value := deviceClassFixture(t, map[string]any{ + "profile": "desktop-server", + "gui": "enabled", + "docker_mode": dockerMode, + "execution_policy": "interactive-desktop-server", + "hardening": map[string]any{"ssh": true, "ufw": true, "fail2ban": true}, + }) + if findings := set.Validate("device", value, "test"); len(findings) != 0 { + t.Fatalf("expected no findings, got %#v", findings) + } + }) } } @@ -196,14 +201,6 @@ func TestDeviceClassRules(t *testing.T) { }, wantCode: "GDS_DEVICE_CLASS_DESKTOP_SERVER_GUI", }, - { - name: "desktop-server with docker", osName: "linux", - class: map[string]any{ - "profile": "desktop-server", "gui": "enabled", "docker_mode": "rootful", - "execution_policy": "interactive-desktop-server", - }, - wantCode: "GDS_DEVICE_CLASS_DESKTOP_SERVER_DOCKER", - }, { name: "desktop-server on macos", osName: "macos", class: map[string]any{ diff --git a/core/validation/schema.go b/core/validation/schema.go index f37d69b..d111d95 100644 --- a/core/validation/schema.go +++ b/core/validation/schema.go @@ -543,9 +543,12 @@ func deviceClassFindings(source string, object map[string]any) []domain.Finding "The desktop-server profile requires gui enabled.", map[string]any{"profile": profile, "gui": gui}) } - if dockerMode != "" && dockerMode != "none" { + // desktop-server defaults to none in the bootstrap contract, but + // rootful and rootless are explicit installer choices — the same + // set server accepts. Do not collapse that into desktop-builds. + if dockerMode != "" && dockerMode != "none" && dockerMode != "rootful" && dockerMode != "rootless" { rule("GDS_DEVICE_CLASS_DESKTOP_SERVER_DOCKER", - "The desktop-server profile defaults to no local Docker; docker_mode must be none.", + "The desktop-server profile permits docker_mode none, rootful, or rootless.", map[string]any{"profile": profile, "docker_mode": dockerMode}) } } diff --git a/docs/contracts/estate-v1.md b/docs/contracts/estate-v1.md index f6796b1..aaad437 100644 --- a/docs/contracts/estate-v1.md +++ b/docs/contracts/estate-v1.md @@ -179,7 +179,7 @@ submodule-consistency rule above applies only to devices that carry an inventory; there is nothing for an absent one to contradict. A device descriptor may declare an optional `class:` block -(`profile`/`gui`/`docker_mode`/`execution_policy`, plus server-only `hardening`) +(`profile`/`gui`/`docker_mode`/`execution_policy`, plus server-baseline `hardening`) whose vocabulary mirrors the `modules/macos-ubuntu-bootstrap` targets block, so the descriptor's intent and the OS installer it drives cannot disagree. The `GDS_DEVICE_CLASS_*` rules (for example `GDS_DEVICE_CLASS_MACOS_CONFLICT`, diff --git a/docs/runbooks/bootstrap-device.md b/docs/runbooks/bootstrap-device.md index 2a82c15..4f3d670 100644 --- a/docs/runbooks/bootstrap-device.md +++ b/docs/runbooks/bootstrap-device.md @@ -51,7 +51,7 @@ mirrored from `modules/macos-ubuntu-bootstrap/config/rldyour-contract.json`: |---|---|---| | `profile` | `desktop` \| `desktop-builds` \| `desktop-server` \| `server` | LSP-only, local-build, tunneled remote-GUI, or headless-server host | | `gui` | `enabled` \| `disabled` | desktop-server is always `enabled`; server is always `disabled` | -| `docker_mode` | `none` \| `rootful` \| `rootless` | desktop and desktop-server require `none`; desktop-builds requires `rootful`; macOS never installs Docker | +| `docker_mode` | `none` \| `rootful` \| `rootless` | desktop requires `none`; desktop-builds requires `rootful`; desktop-server and server permit all three (desktop-server defaults to `none`); macOS never installs Docker | | `execution_policy` | `source-lsp-only` \| `local-dev-with-builds` \| `interactive-desktop-server` \| `container-execution-only` | must match profile | | `hardening.{ssh,ufw,fail2ban}` | `true` \| `false` | server-baseline profiles only | diff --git a/estate/devices/example-user-ubuntu-1.yaml b/estate/devices/example-user-ubuntu-1.yaml index f2d2f57..e7285d4 100644 --- a/estate/devices/example-user-ubuntu-1.yaml +++ b/estate/devices/example-user-ubuntu-1.yaml @@ -1,9 +1,9 @@ schema_version: 1 -# A third device, and the only one that may run containers. Device class is not -# decoration: it decides what an agent harness is allowed to do here, so a -# build host and a laptop are described separately even when the estate they -# see is identical. +# A Linux workstation that may run local containers. desktop-builds is the +# GUI-optional build host; desktop-server is the tunneled remote GUI host that +# can also take an explicit Docker mode. Device class decides what an agent +# harness is allowed to do here. device: id: "device_01JEXAMPZ00000000000000002" name: "example-user-ubuntu-1" diff --git a/schemas/v1/device.schema.json b/schemas/v1/device.schema.json index c121769..ac64617 100644 --- a/schemas/v1/device.schema.json +++ b/schemas/v1/device.schema.json @@ -67,7 +67,7 @@ ] }, "docker_mode": { - "description": "Local Docker posture. desktop and desktop-server permit only none; desktop-builds requires rootful; server permits none/rootful/rootless. macOS never installs local Docker.", + "description": "Local Docker posture. desktop permits only none; desktop-builds requires rootful; desktop-server and server permit none/rootful/rootless (desktop-server defaults to none). macOS never installs local Docker.", "enum": [ "none", "rootful", diff --git a/scripts/bootstrap-device.sh b/scripts/bootstrap-device.sh index 2424439..18764ca 100755 --- a/scripts/bootstrap-device.sh +++ b/scripts/bootstrap-device.sh @@ -273,23 +273,38 @@ esac # declared, default to desktop so the orchestrator never infers a server. PROFILE="${CLASS_PROFILE:-desktop}" OS_ARGS=("--platform" "$PLATFORM" "--profile" "$PROFILE") -if [ "$PROFILE" = "desktop" ]; then - case "${CLASS_GUI:-enabled}" in +append_gui_flag() { + case "${CLASS_GUI:-$1}" in enabled) OS_ARGS+=("--gui");; disabled) OS_ARGS+=("--no-gui");; esac -fi -if [ "$PROFILE" = "server" ]; then - # server is always headless; docker_mode defaults to rootful per contract. - OS_ARGS+=("--no-gui" "--docker-mode" "${CLASS_DOCKER:-rootful}") - # Server-only hardening toggles, if declared. +} +append_hardening_flags() { HARDEN_SSH=$(yaml_get "$DEVICE_PATH" "device.class.hardening.ssh" || true) HARDEN_UFW=$(yaml_get "$DEVICE_PATH" "device.class.hardening.ufw" || true) HARDEN_F2B=$(yaml_get "$DEVICE_PATH" "device.class.hardening.fail2ban" || true) [ "${HARDEN_SSH:-}" = "true" ] && OS_ARGS+=("--harden-ssh") [ "${HARDEN_UFW:-}" = "true" ] && OS_ARGS+=("--enable-ufw") [ "${HARDEN_F2B:-}" = "true" ] && OS_ARGS+=("--with-fail2ban") -fi +} +case "$PROFILE" in + desktop) + append_gui_flag enabled + ;; + desktop-builds) + append_gui_flag enabled + OS_ARGS+=("--docker-mode" "${CLASS_DOCKER:-rootful}") + ;; + desktop-server) + append_gui_flag enabled + OS_ARGS+=("--docker-mode" "${CLASS_DOCKER:-none}") + append_hardening_flags + ;; + server) + OS_ARGS+=("--no-gui" "--docker-mode" "${CLASS_DOCKER:-rootful}") + append_hardening_flags + ;; +esac run_phase() { local n="$1" diff --git a/scripts/validate_gds_schemas.py b/scripts/validate_gds_schemas.py index 54f530d..7485211 100755 --- a/scripts/validate_gds_schemas.py +++ b/scripts/validate_gds_schemas.py @@ -572,11 +572,46 @@ def _semantic_findings(schema_name: str, instance: Any, path: Path) -> list[Find {"path": str(path), "profile": profile, "docker_mode": docker_mode}, ) ) + if profile == "desktop-server": + architecture = device.get("architecture") + if os_name != "linux" or (architecture not in (None, "", "x86_64")): + findings.append( + Finding( + "GDS_DEVICE_CLASS_DESKTOP_SERVER_PLATFORM", + "high", + "The desktop-server profile requires Linux x86_64.", + { + "path": str(path), + "profile": profile, + "os": os_name, + "architecture": architecture, + }, + ) + ) + if gui and gui != "enabled": + findings.append( + Finding( + "GDS_DEVICE_CLASS_DESKTOP_SERVER_GUI", + "high", + "The desktop-server profile requires gui enabled.", + {"path": str(path), "profile": profile, "gui": gui}, + ) + ) + if docker_mode and docker_mode not in {"none", "rootful", "rootless"}: + findings.append( + Finding( + "GDS_DEVICE_CLASS_DESKTOP_SERVER_DOCKER", + "high", + "The desktop-server profile permits docker_mode none, rootful, or rootless.", + {"path": str(path), "profile": profile, "docker_mode": docker_mode}, + ) + ) # execution_policy, when declared, must match the profile. if execution_policy and profile: _POLICY_MAP = { "desktop": "source-lsp-only", "desktop-builds": "local-dev-with-builds", + "desktop-server": "interactive-desktop-server", "server": "container-execution-only", } expected = _POLICY_MAP.get(profile) @@ -599,13 +634,13 @@ def _semantic_findings(schema_name: str, instance: Any, path: Path) -> list[Find isinstance(hardening, Mapping) and hardening and profile - and profile != "server" + and profile not in {"server", "desktop-server"} ): findings.append( Finding( "GDS_DEVICE_CLASS_HARDENING_PROFILE", "high", - "Device class hardening is only permitted with the server profile.", + "Device class hardening is only permitted with a server-baseline profile.", {"path": str(path), "profile": profile, "hardening": hardening}, ) ) diff --git a/skills/canonical/gds-orient/SKILL.md b/skills/canonical/gds-orient/SKILL.md index 798d67a..f591f33 100644 --- a/skills/canonical/gds-orient/SKILL.md +++ b/skills/canonical/gds-orient/SKILL.md @@ -72,14 +72,15 @@ anchoring, materializing, or reclassifying it. The optional `class:` block on a device descriptor (`estate/devices/.yaml`) expresses device-class intent — -`profile: desktop|desktop-builds|server`, `gui: enabled|disabled`, +`profile: desktop|desktop-builds|desktop-server|server`, `gui: enabled|disabled`, `docker_mode: none|rootful|rootless`, and -`execution_policy: source-lsp-only|local-dev-with-builds|container-execution-only`. +`execution_policy: source-lsp-only|local-dev-with-builds|interactive-desktop-server|container-execution-only`. It mirrors the `macos-ubuntu-bootstrap` targets block so the device descriptor and the OS installer it drives cannot disagree. Cross-field rules are enforced by the schema validator (`GDS_DEVICE_CLASS_*`): `desktop` permits only -`docker_mode: none`, `desktop-builds` requires `rootful`, and `server` is -always `gui: disabled`. The phased bootstrap orchestrator that consumes the -class is `scripts/bootstrap-device.sh`; see +`docker_mode: none`, `desktop-builds` requires `rootful`, `desktop-server` +permits `none|rootful|rootless` (default `none`) with `gui: enabled`, and +`server` is always `gui: disabled`. The phased bootstrap orchestrator that +consumes the class is `scripts/bootstrap-device.sh`; see `docs/runbooks/bootstrap-device.md` for the seam. Otherwise no additional runtime reference is required; use current structured `gds` output. diff --git a/tests/fixtures/schemas/v1/cases.json b/tests/fixtures/schemas/v1/cases.json index d72a310..e7b0d56 100644 --- a/tests/fixtures/schemas/v1/cases.json +++ b/tests/fixtures/schemas/v1/cases.json @@ -421,6 +421,12 @@ "path": "valid-device-class-desktop.yaml", "valid": true }, + { + "id": "valid-device-class-desktop-server", + "schema": "device", + "path": "valid-device-class-desktop-server.yaml", + "valid": true + }, { "id": "invalid-device-class-server-gui", "schema": "device", diff --git a/tests/fixtures/schemas/v1/valid-device-class-desktop-server.yaml b/tests/fixtures/schemas/v1/valid-device-class-desktop-server.yaml new file mode 100644 index 0000000..bf5ccec --- /dev/null +++ b/tests/fixtures/schemas/v1/valid-device-class-desktop-server.yaml @@ -0,0 +1,30 @@ +schema_version: 1 + +device: + id: "device_0Q0MPJ4Z2ENZ97XWETRESKZGTH" + name: "example-ubuntu-desktop-server" + os: "linux" + architecture: "x86_64" + class: + profile: "desktop-server" + gui: "enabled" + docker_mode: "rootful" + execution_policy: "interactive-desktop-server" + +workspace_roots: + personal: "${HOME}/Developer/personal" + organization: "${HOME}/Developer/organization" + +materialization: + default_mode: "absent" + include: + - selector: "portfolio:active-projects" + workspace_root: "personal" + mode: "active" + +harnesses: + - "codex" + - "claude-code" + +state: + path: "${XDG_STATE_HOME}/github-device-sync" diff --git a/tests/test_bootstrap_device.py b/tests/test_bootstrap_device.py index e7a4759..e2b424a 100644 --- a/tests/test_bootstrap_device.py +++ b/tests/test_bootstrap_device.py @@ -47,6 +47,14 @@ def test_source_build_never_accepts_a_merely_runnable_binary() -> None: assert "if ! source_build_dirty" in script +def test_orchestrator_forwards_class_docker_mode() -> None: + script = BOOTSTRAP.read_text(encoding="utf-8") + assert 'desktop-server)' in script + assert 'OS_ARGS+=("--docker-mode" "${CLASS_DOCKER:-none}")' in script + assert 'OS_ARGS+=("--docker-mode" "${CLASS_DOCKER:-rootful}")' in script + assert "append_hardening_flags" in script + + def test_seed_go_verifies_the_physical_toolchain_without_auto_selection() -> None: script = BOOTSTRAP.read_text(encoding="utf-8") From 608ea172bc5f9f8653e0d9f6fda0d4a93b8273bb Mon Sep 17 00:00:00 2001 From: Danil Silantyev Date: Tue, 15 Sep 2026 05:55:43 +0500 Subject: [PATCH 2/2] chore(ci): restamp development lock after desktop-server class docs Canonical skill and schema edits moved the source-tree digest; regenerate the lock and gds-ci header so context provenance matches. Co-authored-by: Cursor --- .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 e1e50e7..4a705e5 100644 --- a/.gds/bundle.lock.yaml +++ b/.gds/bundle.lock.yaml @@ -5,14 +5,14 @@ bundle: version: "0.9.4-dev" release_sequence: 0 channel: "development" - source_tree_digest: "sha256:90b0640fff2f50074048eac7f73a79f385546758cc7d1d7c23424dc218eadcd6" - digest: "sha256:6c7e560ee6299259acd2fd2e1edd6eb083f9cbc8d707a36913fb69d39ffd59f8" + source_tree_digest: "sha256:a826f0d40fa915e13c75cb7bfae1dc225d556a08316a86338dacb35421505c85" + digest: "sha256:a1c9ab806577705f57cc98ca78600d2d9b5085321f144ce39255e4994e22af02" projection: - input_digest: "sha256:09499feb56c896a377b4e041584d4db2a32537f744a91f323a0ed3efd20d73ed" - output_digest: "sha256:1412c2e0568bf067755755577cd1bf1c735b8760486e831b151b54009de75b15" + input_digest: "sha256:bd2c29dc422bb1153b6d87d7d3bba6051cfc8f5aa61d7dcf6d6abbc7981fd0ad" + output_digest: "sha256:5b9ae7bd9ea74902e1fc22dd35dadc7271ee350b3fe688051313260d3864bd54" files: - path: ".gds/compiled-policy.json" digest: "sha256:37ece9020fd9eeed4ee90582600c5e50cb3a7e32ee773719d1ccfad2d9cd1b6d" - path: ".github/workflows/gds-ci.yml" - digest: "sha256:0ab163157aef7e1ff5e76dfe240fda39f293596e252d4307c6e1700745af168f" + digest: "sha256:bba7c8fab8091083982cbb8cbe70c7802cb6b75ea5fb6038dfda137295324c0f" diff --git a/.github/workflows/gds-ci.yml b/.github/workflows/gds-ci.yml index 80b48da..c972b06 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.4-dev -# source-tree-digest: sha256:90b0640fff2f50074048eac7f73a79f385546758cc7d1d7c23424dc218eadcd6 -# input-digest: sha256:09499feb56c896a377b4e041584d4db2a32537f744a91f323a0ed3efd20d73ed +# source-tree-digest: sha256:a826f0d40fa915e13c75cb7bfae1dc225d556a08316a86338dacb35421505c85 +# input-digest: sha256:bd2c29dc422bb1153b6d87d7d3bba6051cfc8f5aa61d7dcf6d6abbc7981fd0ad # output-digest: sha256:b9bf3d0c64c0fb371596e7d090e82e62aebbfde91929115fc15fb28644e4fd38 # edit-source: # - .gds/repository.yaml