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:a826f0d40fa915e13c75cb7bfae1dc225d556a08316a86338dacb35421505c85"
digest: "sha256:a1c9ab806577705f57cc98ca78600d2d9b5085321f144ce39255e4994e22af02"
source_tree_digest: "sha256:50743872ba177cd8a1b293d497e4c2f03a21a46b9b6040594e4fb8c3fff755d5"
digest: "sha256:215167d571f0a4be0d1af3567bd49396b6069d604905dac742a6d2f633e51858"

projection:
input_digest: "sha256:bd2c29dc422bb1153b6d87d7d3bba6051cfc8f5aa61d7dcf6d6abbc7981fd0ad"
output_digest: "sha256:5b9ae7bd9ea74902e1fc22dd35dadc7271ee350b3fe688051313260d3864bd54"
input_digest: "sha256:dc9842379e787b40c7e71830122e78ef4181c00d5d8aea46a6e5f861be631cef"
output_digest: "sha256:b05e1cdccfa2f91c474e16d065df790645d48f6d35854b2939b31a8b07864e72"
files:
- path: ".gds/compiled-policy.json"
digest: "sha256:37ece9020fd9eeed4ee90582600c5e50cb3a7e32ee773719d1ccfad2d9cd1b6d"
- path: ".github/workflows/gds-ci.yml"
digest: "sha256:bba7c8fab8091083982cbb8cbe70c7802cb6b75ea5fb6038dfda137295324c0f"
digest: "sha256:6aac2541813495a8e06db7c5ba36ab666b1d4cf89c207abd9e7394e9031f3ec9"
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:a826f0d40fa915e13c75cb7bfae1dc225d556a08316a86338dacb35421505c85
# input-digest: sha256:bd2c29dc422bb1153b6d87d7d3bba6051cfc8f5aa61d7dcf6d6abbc7981fd0ad
# source-tree-digest: sha256:50743872ba177cd8a1b293d497e4c2f03a21a46b9b6040594e4fb8c3fff755d5
# input-digest: sha256:dc9842379e787b40c7e71830122e78ef4181c00d5d8aea46a6e5f861be631cef
# output-digest: sha256:b9bf3d0c64c0fb371596e7d090e82e62aebbfde91929115fc15fb28644e4fd38
# edit-source:
# - .gds/repository.yaml
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ Versioning.

## [Unreleased]

- Surface the registered device and its `class` (`profile`/`gui`/`docker_mode`/
`execution_policy`) on `gds context` from the device-local locator bound to
exactly one `estate/devices/*.yaml`. `gds-orient` reads `context.device`
instead of inferring the host.
- Add the `desktop-server` device class: Linux x86_64, GUI required, Docker
none|rootful|rootless (default none). The bootstrap orchestrator forwards
`docker_mode` and server-baseline hardening flags. Name it in `gds-orient`
Expand Down
144 changes: 144 additions & 0 deletions core/context/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
gitprovider "github.com/NDDev-OpenNetwork/github-device-sync/core/providers/git"
"github.com/NDDev-OpenNetwork/github-device-sync/core/serialization"
"github.com/NDDev-OpenNetwork/github-device-sync/core/validation"
"github.com/NDDev-OpenNetwork/github-device-sync/core/workspace"
)

type Resolver struct {
Expand All @@ -46,6 +47,7 @@ type Context struct {
Repository RepositoryContext `json:"repository"`
Mode ModeContext `json:"mode"`
Estate EstateContext `json:"estate"`
Device *DeviceContext `json:"device,omitempty"`
Policy PolicyContext `json:"policy"`
Agent AgentContext `json:"agent"`
Boundaries []BoundaryContext `json:"boundaries"`
Expand Down Expand Up @@ -77,6 +79,19 @@ type EstateContext struct {
Root string `json:"root,omitempty"`
}

// DeviceContext is the device descriptor bound by the device-local estate
// locator. It is omitted when no locator exists or the locator does not
// name a descriptor under the proven estate root. Context never infers a
// device from hostname, cwd, or the first yaml in estate/devices.
type DeviceContext struct {
ID string `json:"id"`
Name string `json:"name,omitempty"`
OS string `json:"os,omitempty"`
Architecture string `json:"architecture,omitempty"`
Path string `json:"path"`
Class *workspace.DeviceClass `json:"class,omitempty"`
}

type PolicyContext struct {
BundleLockPresent bool `json:"bundle_lock_present"`
Provenance string `json:"provenance"`
Expand Down Expand Up @@ -248,6 +263,7 @@ func (resolver *Resolver) Resolve(ctx context.Context, path string) Outcome {
}

resolveEstate(resolver, &result, &findings)
resolveDevice(resolver, &result, &findings)
policyFindingCount := len(findings)
document := resolveAppliedPolicy(resolver, &result, &findings, info.WorktreeRoot)
if document != nil && repositoryAnchor != nil && result.Policy.Digest != "" &&
Expand Down Expand Up @@ -575,6 +591,134 @@ func resolveEstate(resolver *Resolver, result *Context, findings *[]domain.Findi
result.Estate = EstateContext{Registered: true, Root: root}
}

func resolveDevice(resolver *Resolver, result *Context, findings *[]domain.Finding) {
if !result.Estate.Registered || result.Estate.Root == "" {
return
}
deviceID := lookupRegisteredDeviceID(resolver, result, findings)
if deviceID == "" {
return
}
matches := []string{}
pattern := filepath.Join(result.Estate.Root, "estate", "devices", "*.yaml")
paths, err := filepath.Glob(pattern)
if err != nil {
*findings = append(*findings, domain.Finding{
Code: "GDS_CONTEXT_DEVICE_DESCRIPTOR_UNREADABLE",
Severity: domain.SeverityHigh,
Message: "Cannot enumerate device descriptors for the registered device.",
Evidence: map[string]any{"pattern": pattern, "error": err.Error()},
})
return
}
for _, path := range paths {
if peekDeviceID(path) != deviceID {
continue
}
matches = append(matches, path)
}
if len(matches) == 0 {
*findings = append(*findings, domain.Finding{
Code: "GDS_CONTEXT_DEVICE_DESCRIPTOR_MISSING",
Severity: domain.SeverityHigh,
Message: "Registered device_id has no matching estate/devices descriptor.",
Evidence: map[string]any{"device_id": deviceID, "estate_root": result.Estate.Root},
})
return
}
if len(matches) != 1 {
*findings = append(*findings, domain.Finding{
Code: "GDS_CONTEXT_DEVICE_DESCRIPTOR_AMBIGUOUS",
Severity: domain.SeverityHigh,
Message: "Registered device_id matches more than one estate/devices descriptor.",
Evidence: map[string]any{"device_id": deviceID, "paths": matches},
})
return
}
descriptor, deviceFindings := workspace.LoadDevice(matches[0], resolver.schemas)
if len(deviceFindings) != 0 {
*findings = append(*findings, deviceFindings...)
return
}
result.Device = &DeviceContext{
ID: descriptor.Device.ID,
Name: descriptor.Device.Name,
OS: descriptor.Device.OS,
Architecture: descriptor.Device.Architecture,
Path: matches[0],
Class: descriptor.Device.Class,
}
}

func lookupRegisteredDeviceID(resolver *Resolver, result *Context, findings *[]domain.Finding) string {
registrationPath, err := estateregistry.DefaultPath(resolver.getenv, resolver.userHome)
if err != nil {
return ""
}
if _, err := os.Lstat(registrationPath); errors.Is(err, os.ErrNotExist) {
return ""
} else if err != nil {
*findings = append(*findings, domain.Finding{
Code: "GDS_CONTEXT_DEVICE_NOT_PROVEN",
Severity: domain.SeverityMedium,
Message: fmt.Sprintf("Device-local estate registration cannot be inspected: %v", err),
Evidence: map[string]any{"registration_path": registrationPath},
})
return ""
}
registration, registrationFindings := estateregistry.Load(registrationPath, resolver.schemas)
if len(registrationFindings) != 0 {
*findings = append(*findings, domain.Finding{
Code: "GDS_CONTEXT_DEVICE_NOT_PROVEN",
Severity: domain.SeverityMedium,
Message: "Device-local estate registration cannot bind a device descriptor.",
Evidence: map[string]any{"registration_path": registrationPath},
})
return ""
}
if !samePhysicalPath(registration.Document.Estate.Root, result.Estate.Root) {
*findings = append(*findings, domain.Finding{
Code: "GDS_CONTEXT_DEVICE_LOCATOR_ROOT_MISMATCH",
Severity: domain.SeverityMedium,
Message: "Device-local estate registration does not name the proven estate root.",
Evidence: map[string]any{
"registration_path": registrationPath,
"registered_root": registration.Document.Estate.Root,
"estate_root": result.Estate.Root,
},
})
return ""
}
return registration.Document.DeviceID
}

func peekDeviceID(path string) string {
value, err := serialization.DecodeFile(path)
if err != nil {
return ""
}
object, _ := value.(map[string]any)
device, _ := object["device"].(map[string]any)
id, _ := device["id"].(string)
return id
}

func samePhysicalPath(left string, right string) bool {
resolve := func(path string) (string, bool) {
if path == "" {
return "", false
}
physical, err := filepath.EvalSymlinks(path)
if err != nil {
return filepath.Clean(path), true
}
return filepath.Clean(physical), true
}
a, aOK := resolve(left)
b, bOK := resolve(right)
return aOK && bOK && a == b
}

func resolveDirectory(path string) (string, error) {
absolute, err := filepath.Abs(path)
if err != nil {
Expand Down
Loading