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.4-dev"
release_sequence: 0
channel: "development"
source_tree_digest: "sha256:6e0da387f1ed9a162e18f93470f9712783b4a7501a2ec9979d4ce2e15fc73f46"
digest: "sha256:030d8ccfe93fc4ded1b5fabe8e8df2eee4195c7b2eec33f6b1880dec851116bd"
source_tree_digest: "sha256:2b1a4353cef1f4e5ca83504a7f9cffb21ac04fcaf3e31d32068b108cba735d84"
digest: "sha256:4fced9b3b3ba51cbf1390489310ce01ce21d180a95d601771db066bfb9b0a8f5"

projection:
input_digest: "sha256:6bae4a92f7cdb2890369fc557f3e82f64172dae27ce782d7f10455a6a0f47949"
output_digest: "sha256:92322690bf490037b4ec99b33584004ed63a1dc7f2b7368e6362510292b81d3c"
input_digest: "sha256:e0ebdc1dfa4345e685358212879ba7e2e39d8c666d60e2f1dba468c415626393"
output_digest: "sha256:e33b2ddf57c2990f9f9521eb011edcac98073683e5b476fdcc59e6e34329cabe"
files:
- path: ".gds/compiled-policy.json"
digest: "sha256:37ece9020fd9eeed4ee90582600c5e50cb3a7e32ee773719d1ccfad2d9cd1b6d"
- path: ".github/workflows/gds-ci.yml"
digest: "sha256:21bbe5aad38befa4f10438841bcfd06f42dd9981047e057ec8142fa9326d0bd0"
digest: "sha256:d77abe7402b55f8e8315b0f2077126713836dd179b8cdb108c024bd39d5f3b16"
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.4-dev
# source-tree-digest: sha256:6e0da387f1ed9a162e18f93470f9712783b4a7501a2ec9979d4ce2e15fc73f46
# input-digest: sha256:6bae4a92f7cdb2890369fc557f3e82f64172dae27ce782d7f10455a6a0f47949
# source-tree-digest: sha256:2b1a4353cef1f4e5ca83504a7f9cffb21ac04fcaf3e31d32068b108cba735d84
# input-digest: sha256:e0ebdc1dfa4345e685358212879ba7e2e39d8c666d60e2f1dba468c415626393
# output-digest: sha256:15b87303cd0692fd8e5d88281771dded9fdc2fc027d600681155cb4e71f42386
# edit-source:
# - .gds/repository.yaml
Expand Down
42 changes: 42 additions & 0 deletions core/validation/device_class_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,24 @@ func TestDeviceClassAcceptsDesktopBuilds(t *testing.T) {
}
}

func TestDeviceClassAcceptsDesktopServer(t *testing.T) {
t.Parallel()
set, err := NewSchemaSet()
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)
}
}

func TestDeviceClassAcceptsServerHeadless(t *testing.T) {
t.Parallel()
set, err := NewSchemaSet()
Expand Down Expand Up @@ -170,6 +188,30 @@ func TestDeviceClassRules(t *testing.T) {
class: map[string]any{"profile": "desktop-builds", "docker_mode": "rootless"},
wantCode: "GDS_DEVICE_CLASS_DESKTOP_BUILDS_DOCKER",
},
{
name: "desktop-server without gui", osName: "linux",
class: map[string]any{
"profile": "desktop-server", "gui": "disabled", "docker_mode": "none",
"execution_policy": "interactive-desktop-server",
},
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{
"profile": "desktop-server", "gui": "enabled", "docker_mode": "none",
"execution_policy": "interactive-desktop-server",
},
wantCode: "GDS_DEVICE_CLASS_DESKTOP_SERVER_PLATFORM",
},
{
name: "desktop with hardening", osName: "linux",
class: map[string]any{
Expand Down
24 changes: 21 additions & 3 deletions core/validation/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,7 @@ func deviceFindings(source string, object map[string]any) []domain.Finding {
var deviceClassExecutionPolicies = map[string]string{
"desktop": "source-lsp-only",
"desktop-builds": "local-dev-with-builds",
"desktop-server": "interactive-desktop-server",
"server": "container-execution-only",
}

Expand Down Expand Up @@ -531,6 +532,23 @@ func deviceClassFindings(source string, object map[string]any) []domain.Finding
"The desktop-builds profile requires docker_mode rootful.",
map[string]any{"profile": profile, "docker_mode": dockerMode})
}
if profile == "desktop-server" {
if osName != "linux" || (device["architecture"] != "x86_64" && device["architecture"] != "") {
rule("GDS_DEVICE_CLASS_DESKTOP_SERVER_PLATFORM",
"The desktop-server profile requires Linux x86_64.",
map[string]any{"profile": profile, "os": osName, "architecture": device["architecture"]})
}
if gui != "" && gui != "enabled" {
rule("GDS_DEVICE_CLASS_DESKTOP_SERVER_GUI",
"The desktop-server profile requires gui enabled.",
map[string]any{"profile": profile, "gui": gui})
}
if dockerMode != "" && dockerMode != "none" {
rule("GDS_DEVICE_CLASS_DESKTOP_SERVER_DOCKER",
"The desktop-server profile defaults to no local Docker; docker_mode must be none.",
map[string]any{"profile": profile, "docker_mode": dockerMode})
}
}
// execution_policy, when declared, must match the profile. The mapping is the
// macos-ubuntu-bootstrap targets block, mirrored here so a device descriptor
// and the OS installer it drives cannot disagree.
Expand All @@ -542,11 +560,11 @@ func deviceClassFindings(source string, object map[string]any) []domain.Finding
map[string]any{"profile": profile, "execution_policy": executionPolicy, "expected": expected})
}
}
// Hardening toggles are server-only.
if hasHardening && profile != "" && profile != "server" {
// Hardening toggles belong to profiles that compose the server baseline.
if hasHardening && profile != "" && profile != "server" && profile != "desktop-server" {
if hardeningMap, ok := hardening.(map[string]any); ok && len(hardeningMap) > 0 {
rule("GDS_DEVICE_CLASS_HARDENING_PROFILE",
"Device class hardening is only permitted with the server profile.",
"Device class hardening is only permitted with a server-baseline profile.",
map[string]any{"profile": profile, "hardening": hardening})
}
}
Expand Down
10 changes: 5 additions & 5 deletions docs/runbooks/bootstrap-device.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ mirrored from `modules/macos-ubuntu-bootstrap/config/rldyour-contract.json`:

| Field | Values | Notes |
|---|---|---|
| `profile` | `desktop` \| `desktop-builds` \| `server` | LSP-only workstation vs build workstation with Docker vs headless container host |
| `gui` | `enabled` \| `disabled` | server is always `disabled` |
| `docker_mode` | `none` \| `rootful` \| `rootless` | desktop requires `none`; desktop-builds requires `rootful`; macOS never installs Docker |
| `execution_policy` | `source-lsp-only` \| `local-dev-with-builds` \| `container-execution-only` | must match profile |
| `hardening.{ssh,ufw,fail2ban}` | `true` \| `false` | server only |
| `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 |
| `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 |

Cross-field rules are enforced by the schema validator
(`GDS_DEVICE_CLASS_*` findings), mirroring the rules in
Expand Down
12 changes: 7 additions & 5 deletions schemas/v1/device.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,38 +51,40 @@
"additionalProperties": false,
"properties": {
"profile": {
"description": "desktop = a workstation (LSP/agents only); desktop-builds = a workstation with local Docker (local-dev-with-builds); server = a headless container-execution host.",
"description": "desktop = a workstation (LSP/agents only); desktop-builds = a workstation with local Docker; desktop-server = an amd64 Linux remote GUI workstation reached through a private tunnel; server = a headless container-execution host.",
"enum": [
"desktop",
"desktop-builds",
"desktop-server",
"server"
]
},
"gui": {
"description": "Whether the device runs a GUI session. The server profile is always disabled.",
"description": "Whether the device runs a GUI session. desktop-server is always enabled and server is always disabled.",
"enum": [
"enabled",
"disabled"
]
},
"docker_mode": {
"description": "Local Docker posture. desktop permits only none; desktop-builds requires rootful; server permits none/rootful/rootless. macOS never installs local Docker.",
"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.",
"enum": [
"none",
"rootful",
"rootless"
]
},
"execution_policy": {
"description": "source-lsp-only for desktop; local-dev-with-builds for desktop-builds; container-execution-only for server.",
"description": "source-lsp-only for desktop; local-dev-with-builds for desktop-builds; interactive-desktop-server for desktop-server; container-execution-only for server.",
"enum": [
"source-lsp-only",
"local-dev-with-builds",
"interactive-desktop-server",
"container-execution-only"
]
},
"hardening": {
"description": "Server-only hardening toggles. Permitted only when profile is server.",
"description": "Server-baseline hardening toggles. Permitted when profile is server or desktop-server.",
"type": "object",
"additionalProperties": false,
"properties": {
Expand Down
18 changes: 17 additions & 1 deletion scripts/gds-exact-apply.sh
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ command -v jq >/dev/null 2>&1 || die "jq is required"
# the install/upgrade/rollback inputs is explicitly rejected by the native CLI.
# Other lifecycle commands still need their selectors (for example --harness).
verify_command=("${command[@]}")
apply_command=("${command[@]}")
for ((command_index = 1; command_index + 1 < ${#command[@]}; command_index++)); do
if [ "${command[command_index]}" = release ]; then
case "${command[command_index + 1]}" in
Expand All @@ -53,14 +54,29 @@ for ((command_index = 1; command_index + 1 < ${#command[@]}; command_index++));
break ;;
esac
fi
if [ "${command[command_index]}" = workspace ] &&
[ "${command[command_index + 1]}" = register-estate ]; then
apply_command=()
for ((index = 0; index < ${#command[@]}; index++)); do
case "${command[index]}" in
--estate-root|--registration-path)
((index + 1 < ${#command[@]})) || die "missing value for ${command[index]}"
index=$((index + 1)) ;;
--estate-root=*|--registration-path=*) ;;
*) apply_command+=("${command[index]}") ;;
esac
done
verify_command=("${apply_command[@]}")
break
fi
done

"$gds_bin" --json operation enable "$plan_id" \
--state-path "$state_path" --approval-file "$approval_file" \
--device-id "$device_id" --session-id "$session_id" |
jq -e '.result == "succeeded" and .data.status == "active"' >/dev/null

apply_output=$("${command[@]}" --apply "$plan_id" --approval-ref "$approval_file" \
apply_output=$("${apply_command[@]}" --apply "$plan_id" --approval-ref "$approval_file" \
--state-path "$state_path" --device-id "$device_id" --session-id "$session_id")
operation_id=$(printf '%s' "$apply_output" | jq -er 'select(.result == "succeeded") | .operation_id')

Expand Down
18 changes: 18 additions & 0 deletions tests/test_exact_apply.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,24 @@ def test_harness_selectors_and_literal_arguments_survive_verification(tmp_path:
assert "--approval-ref" not in calls[2]


@pytest.mark.parametrize("equals", [False, True])
def test_workspace_registration_drops_planning_only_inputs_for_apply_and_verify(
tmp_path: Path, equals: bool,
) -> None:
estate = str(tmp_path / "estate with spaces")
registration = str(tmp_path / "config with spaces" / "registration.json")
inputs = {"estate-root": estate, "registration-path": registration}
flags = [arg for name, value in inputs.items()
for arg in ([f"--{name}={value}"] if equals else [f"--{name}", value])]
base = ["workspace", "register-estate", *flags]
result, calls = run_helper(tmp_path, base)
assert result.returncode == 0, result.stderr
assert calls[1][1:3] == calls[2][1:3] == ["workspace", "register-estate"]
for call in calls[1:]:
assert all(arg.split("=", 1)[0] not in {"--estate-root", "--registration-path"}
for arg in call)


@pytest.mark.parametrize("operation", ["install", "upgrade", "rollback", "remove"])
@pytest.mark.parametrize("equals", [False, True])
def test_release_verification_uses_stored_identity_with_native_cli(
Expand Down
Loading