diff --git a/docs/contract-additions/immutable-node-profiles.md b/docs/contract-additions/immutable-node-profiles.md new file mode 100644 index 0000000..5f32e09 --- /dev/null +++ b/docs/contract-additions/immutable-node-profiles.md @@ -0,0 +1,61 @@ +# Immutable Node Profile Contracts + +This contract addition defines the first SourceOS machine-readable surface for immutable Linux node profiles. + +## Why this exists + +The platform standards layer already defines the general immutable host capability placement grammar. SourceOS needs a contract-native form that downstream implementation repos can validate, render, and emit evidence for. + +The key boundary is: + +- SourceOS immutable nodes are base substrate profiles. +- Socios capability packs are optional automation commons and must not be required for a base SourceOS node. + +## Added schemas + +| Schema | Purpose | URN prefix | +|---|---|---| +| `ImmutableNodeProfile.json` | Top-level immutable node profile tying together substrate posture, release refs, host placements, state schemas, validation commands, and optional Socios pack refs. | `urn:srcos:immutable-node-profile:` | +| `HostCapabilityPlacement.json` | Placement declaration for one host capability, service workload, extension, or state root. | `urn:srcos:host-capability-placement:` | +| `NodeStateSchema.json` | Durable state root contract with rollback compatibility and mutability posture. | `urn:srcos:node-state-schema:` | + +## Added examples + +| Example | Schema | +|---|---| +| `examples/immutablenodeprofile.json` | `schemas/ImmutableNodeProfile.json` | +| `examples/hostcapabilityplacement.json` | `schemas/HostCapabilityPlacement.json` | +| `examples/nodestateschema.json` | `schemas/NodeStateSchema.json` | + +## Validation + +The focused validator is: + +```bash +python3 tools/validate_immutable_node_examples.py +``` + +A future hygiene pass should wire this into the repository `Makefile` and `schemas/README.md` once the contract slice is accepted. + +## Downstream implementation map + +| Downstream repo | Expected role | +|---|---| +| `SourceOS-Linux/sourceos-boot` | Consumes boot/release references and coordinates boot/recovery/install/rollback handoff. | +| `SourceOS-Linux/agent-machine` | Renders and validates systemd, Quadlet, state roots, activation decisions, storage receipts, and runtime evidence. | +| `SourceOS-Linux/sourceos-devtools` | Exposes `sourceosctl immutable-node plan|validate|inspect` operator flows. | +| `SocioProphet/agentplane` | Consumes immutable-node activation/runtime evidence and replayable receipts. | +| `SocioProphet/prophet-platform` | Consumes profiles as deployment/FogStack substrate evidence. | +| `SocioProphet/sociosphere` | Registers topology and cross-repo governance edges. | +| `SociOS-Linux/socios` | Optional automation/personalization commons only after explicit enrollment. | + +## Non-goals + +This tranche does not: + +- implement boot execution; +- mutate host state; +- make Socios mandatory; +- replace BootReleaseSet; +- add a production node renderer; +- add CI or Makefile wiring yet. diff --git a/examples/hostcapabilityplacement.json b/examples/hostcapabilityplacement.json new file mode 100644 index 0000000..3fb0208 --- /dev/null +++ b/examples/hostcapabilityplacement.json @@ -0,0 +1,20 @@ +{ + "id": "urn:srcos:host-capability-placement:sourceos-supervisor", + "type": "HostCapabilityPlacement", + "specVersion": "2.2.0", + "name": "SourceOS host supervisor", + "placementClass": "image-baked-host-capability", + "authority": "sourceos-base", + "mandatoryForBaseNode": true, + "requiresEnrollment": false, + "lifecycleCoupling": "host-release", + "pathHints": [ + "/usr/libexec/sourceos/supervisor", + "/usr/lib/systemd/system/sourceos-supervisor.service" + ], + "evidenceRefs": [ + "urn:srcos:release-receipt:sourceos-supervisor-bootstrap" + ], + "policyRef": "urn:srcos:policy:immutable-node-host-supervisor", + "notes": "Base SourceOS capability. This is not a Socios enrollment dependency." +} diff --git a/examples/immutablenodeprofile.json b/examples/immutablenodeprofile.json new file mode 100644 index 0000000..7776739 --- /dev/null +++ b/examples/immutablenodeprofile.json @@ -0,0 +1,39 @@ +{ + "id": "urn:srcos:immutable-node-profile:m2-asahi-dev", + "type": "ImmutableNodeProfile", + "specVersion": "2.2.0", + "name": "M2 Asahi SourceOS dev immutable node", + "channel": "m2-asahi-dev", + "substrate": { + "strategy": "bootc-first", + "hostMutationPosture": "staged-updates-only", + "sociosRequired": false + }, + "bootReleaseSetRef": "urn:srcos:boot-release-set:m2-asahi-dev", + "releaseSetRef": "urn:srcos:release-set:m2-asahi-dev", + "workstationProfileRef": "urn:srcos:workstation-profile:workstation-v0", + "agentMachineProfileRef": "urn:srcos:agent-machine-profile:m2-asahi-dev", + "hostCapabilityPlacementRefs": [ + "urn:srcos:host-capability-placement:sourceos-supervisor" + ], + "nodeStateSchemaRefs": [ + "urn:srcos:node-state-schema:sourceos-evidence-root" + ], + "optionalSociosCapabilityPackRefs": [ + "urn:socios:capability-pack:automation-commons-opt-in" + ], + "validation": { + "planCommand": "sourceosctl immutable-node plan --profile m2-asahi-dev --dry-run", + "validateCommand": "sourceosctl immutable-node validate --profile m2-asahi-dev", + "inspectCommand": "sourceosctl immutable-node inspect --profile m2-asahi-dev", + "evidenceCommand": "agent-machine release evidence inspect --profile m2-asahi-dev" + }, + "policyRefs": [ + "urn:srcos:policy:immutable-node-base", + "urn:srcos:policy:socios-opt-in-required" + ], + "evidenceRefs": [ + "urn:srcos:release-receipt:m2-asahi-dev" + ], + "notes": "SourceOS base node profile. Socios pack reference is optional and does not imply enrollment." +} diff --git a/examples/nodestateschema.json b/examples/nodestateschema.json new file mode 100644 index 0000000..367b2a8 --- /dev/null +++ b/examples/nodestateschema.json @@ -0,0 +1,14 @@ +{ + "id": "urn:srcos:node-state-schema:sourceos-evidence-root", + "type": "NodeStateSchema", + "specVersion": "2.2.0", + "name": "SourceOS evidence root", + "rootPath": "/var/lib/sourceos/evidence", + "stateClass": "append-only-evidence", + "schemaRef": "urn:srcos:release-receipt:sourceos-evidence", + "rollbackCompatibility": "required-n-and-n-minus-1", + "mutability": "append-only", + "owner": "agent-machine", + "evidenceRequired": true, + "notes": "Durable evidence root survives host rollback and must preserve reader compatibility." +} diff --git a/schemas/HostCapabilityPlacement.json b/schemas/HostCapabilityPlacement.json new file mode 100644 index 0000000..d9cb79c --- /dev/null +++ b/schemas/HostCapabilityPlacement.json @@ -0,0 +1,100 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.srcos.ai/v2/HostCapabilityPlacement.json", + "title": "HostCapabilityPlacement", + "description": "A SourceOS immutable-node placement declaration for one host capability, service workload, extension, or state root.", + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "type", + "specVersion", + "name", + "placementClass", + "authority" + ], + "properties": { + "id": { + "type": "string", + "pattern": "^urn:srcos:host-capability-placement:", + "description": "Stable URN identifier. Pattern: urn:srcos:host-capability-placement:." + }, + "type": { + "const": "HostCapabilityPlacement", + "description": "Discriminator constant — always HostCapabilityPlacement." + }, + "specVersion": { + "type": "string", + "description": "Spec version of this document, e.g. 2.2.0." + }, + "name": { + "type": "string", + "minLength": 1, + "description": "Human-readable capability name." + }, + "placementClass": { + "type": "string", + "enum": [ + "image-baked-host-capability", + "sysext-host-capability", + "confext-policy-config-capability", + "quadlet-bound-service-workload", + "quadlet-floating-service-workload", + "var-state-object" + ], + "description": "Canonical placement class inherited from the immutable-node host capability model." + }, + "authority": { + "type": "string", + "enum": [ + "sourceos-base", + "sourceos-boot", + "agent-machine", + "sourceos-devtools", + "agentplane", + "prophet-platform", + "socios-optional-pack" + ], + "description": "Repo or plane class that owns the implementation boundary for this capability." + }, + "mandatoryForBaseNode": { + "type": "boolean", + "default": false, + "description": "Whether the capability is required for a base SourceOS immutable node without Socios enrollment." + }, + "requiresEnrollment": { + "type": "boolean", + "default": false, + "description": "Whether activation requires opt-in enrollment, Proof-of-Life, or signed intent." + }, + "lifecycleCoupling": { + "type": "string", + "enum": [ + "host-release", + "extension-release", + "service-release", + "state-schema", + "operator-local" + ], + "description": "Primary lifecycle to which this capability is coupled." + }, + "pathHints": { + "type": "array", + "items": { "type": "string" }, + "description": "Expected implementation paths such as /usr/libexec/sourceos, /usr/share/containers/systemd, /var/lib/sourceos, or /var/lib/socios." + }, + "evidenceRefs": { + "type": "array", + "items": { "type": "string" }, + "description": "Optional references to evidence records emitted by the capability." + }, + "policyRef": { + "type": ["string", "null"], + "description": "Optional policy identifier or URN governing activation or mutation." + }, + "notes": { + "type": ["string", "null"], + "description": "Human-readable review notes." + } + } +} diff --git a/schemas/ImmutableNodeProfile.json b/schemas/ImmutableNodeProfile.json new file mode 100644 index 0000000..fa28d35 --- /dev/null +++ b/schemas/ImmutableNodeProfile.json @@ -0,0 +1,118 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.srcos.ai/v2/ImmutableNodeProfile.json", + "title": "ImmutableNodeProfile", + "description": "A SourceOS immutable-node profile tying together boot/release references, host capability placements, durable state schemas, validation entrypoints, and optional Socios capability-pack posture.", + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "type", + "specVersion", + "name", + "channel", + "substrate" + ], + "properties": { + "id": { + "type": "string", + "pattern": "^urn:srcos:immutable-node-profile:", + "description": "Stable URN identifier. Pattern: urn:srcos:immutable-node-profile:." + }, + "type": { + "const": "ImmutableNodeProfile", + "description": "Discriminator constant — always ImmutableNodeProfile." + }, + "specVersion": { + "type": "string", + "description": "Spec version of this document, e.g. 2.2.0." + }, + "name": { + "type": "string", + "minLength": 1, + "description": "Human-readable immutable node profile name." + }, + "channel": { + "type": "string", + "minLength": 1, + "description": "Realization channel such as linux-dev, linux-stable, m2-asahi-dev, or fog-node." + }, + "substrate": { + "type": "object", + "additionalProperties": false, + "required": ["strategy", "hostMutationPosture", "sociosRequired"], + "properties": { + "strategy": { + "type": "string", + "enum": ["bootc-first", "ostree-compatible", "rpm-ostree", "coreos-assembler", "nixos-adapter", "other"], + "description": "Primary immutable-host realization strategy." + }, + "hostMutationPosture": { + "type": "string", + "enum": ["immutable-runtime", "staged-updates-only", "lab-live-mutation-allowed", "unknown"], + "description": "Allowed host mutation posture for this profile." + }, + "sociosRequired": { + "const": false, + "description": "Base SourceOS immutable nodes must not require Socios enrollment. Optional Socios packs are modeled separately." + } + } + }, + "bootReleaseSetRef": { + "type": ["string", "null"], + "description": "Optional BootReleaseSet or boot/recovery handoff reference." + }, + "releaseSetRef": { + "type": ["string", "null"], + "description": "Optional ReleaseSet assignment reference." + }, + "workstationProfileRef": { + "type": ["string", "null"], + "description": "Optional WorkstationProfile reference for desktop/workstation realization." + }, + "agentMachineProfileRef": { + "type": ["string", "null"], + "description": "Optional Agent Machine or AgentPod profile reference for local runtime realization." + }, + "hostCapabilityPlacementRefs": { + "type": "array", + "items": { "type": "string", "pattern": "^urn:srcos:host-capability-placement:" }, + "description": "References to HostCapabilityPlacement objects used by this profile." + }, + "nodeStateSchemaRefs": { + "type": "array", + "items": { "type": "string", "pattern": "^urn:srcos:node-state-schema:" }, + "description": "References to NodeStateSchema objects used by this profile." + }, + "optionalSociosCapabilityPackRefs": { + "type": "array", + "items": { "type": "string" }, + "description": "Optional Socios capability pack references. Presence does not imply base node dependency or enrollment." + }, + "validation": { + "type": "object", + "additionalProperties": false, + "properties": { + "planCommand": { "type": ["string", "null"] }, + "validateCommand": { "type": ["string", "null"] }, + "inspectCommand": { "type": ["string", "null"] }, + "evidenceCommand": { "type": ["string", "null"] } + }, + "description": "Operator-facing dry-run, validation, inspection, and evidence entrypoints." + }, + "policyRefs": { + "type": "array", + "items": { "type": "string" }, + "description": "Policy references that govern activation, mutation, enrollment, or rollback." + }, + "evidenceRefs": { + "type": "array", + "items": { "type": "string" }, + "description": "Evidence or receipt references expected from this node profile." + }, + "notes": { + "type": ["string", "null"], + "description": "Human-readable review notes." + } + } +} diff --git a/schemas/NodeStateSchema.json b/schemas/NodeStateSchema.json new file mode 100644 index 0000000..aca0ab2 --- /dev/null +++ b/schemas/NodeStateSchema.json @@ -0,0 +1,91 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schemas.srcos.ai/v2/NodeStateSchema.json", + "title": "NodeStateSchema", + "description": "A SourceOS immutable-node durable state schema declaration for rollback-aware host and service state roots.", + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "type", + "specVersion", + "name", + "rootPath", + "stateClass", + "rollbackCompatibility" + ], + "properties": { + "id": { + "type": "string", + "pattern": "^urn:srcos:node-state-schema:", + "description": "Stable URN identifier. Pattern: urn:srcos:node-state-schema:." + }, + "type": { + "const": "NodeStateSchema", + "description": "Discriminator constant — always NodeStateSchema." + }, + "specVersion": { + "type": "string", + "description": "Spec version of this document, e.g. 2.2.0." + }, + "name": { + "type": "string", + "minLength": 1, + "description": "Human-readable state root name." + }, + "rootPath": { + "type": "string", + "pattern": "^/var/(lib|cache)/", + "description": "Durable or rebuildable state root. Authoritative mutable truth must not live under /usr or /etc." + }, + "stateClass": { + "type": "string", + "enum": [ + "durable-truth", + "append-only-evidence", + "replay-checkpoint", + "model-cache", + "rebuildable-cache", + "operator-local" + ], + "description": "Semantic class of the state root." + }, + "schemaRef": { + "type": ["string", "null"], + "description": "Optional schema, contract, or URN reference for records stored in this root." + }, + "rollbackCompatibility": { + "type": "string", + "enum": [ + "required-n-and-n-minus-1", + "best-effort", + "not-required-rebuildable", + "operator-managed" + ], + "description": "Rollback compatibility expectation for readers and writers of this state root." + }, + "mutability": { + "type": "string", + "enum": [ + "append-only", + "controlled-write", + "rebuildable", + "ephemeral-cache" + ], + "description": "Expected write posture for this root." + }, + "owner": { + "type": ["string", "null"], + "description": "Owning plane or repository class for this state root." + }, + "evidenceRequired": { + "type": "boolean", + "default": false, + "description": "Whether writes to this state root require evidence emission." + }, + "notes": { + "type": ["string", "null"], + "description": "Human-readable review notes." + } + } +} diff --git a/tools/validate_immutable_node_examples.py b/tools/validate_immutable_node_examples.py new file mode 100644 index 0000000..ea37d87 --- /dev/null +++ b/tools/validate_immutable_node_examples.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +"""Validate immutable-node contract examples against their schemas.""" + +from __future__ import annotations + +import json +from pathlib import Path + +import jsonschema + +ROOT = Path(__file__).resolve().parents[1] + +PAIRS = [ + ("schemas/HostCapabilityPlacement.json", "examples/hostcapabilityplacement.json"), + ("schemas/NodeStateSchema.json", "examples/nodestateschema.json"), + ("schemas/ImmutableNodeProfile.json", "examples/immutablenodeprofile.json"), +] + + +def load_json(path: str) -> object: + with (ROOT / path).open("r", encoding="utf-8") as fh: + return json.load(fh) + + +def main() -> None: + for schema_path, example_path in PAIRS: + schema = load_json(schema_path) + example = load_json(example_path) + jsonschema.Draft202012Validator.check_schema(schema) + jsonschema.validate(instance=example, schema=schema) + print(f"OK: {example_path} validates against {schema_path}") + + +if __name__ == "__main__": + main()