# Shellstream Protocol Specification — Upper Layers **Identifier**: SS-SPEC-0001 (Sections 5-9) **Status**: Draft **Version**: 0.1.0 | | | |---|---| | **Author** | Tyler King, Lead Consultant — BXNet LLC | | **Date** | February 2026 | | **Organization** | BXNet LLC / Guildhouse Project | | **Normative Implementation** | Guildhouse SDK (`guildhouse-tower`, `guildhouse-mq`, `registry-protocol`, `bascule-core`) | | **Precursor Implementation** | Bascule Governed Shell (`bascule-shell`) | > *This document specifies Layers 2-4 of the Shellstream Protocol Stack. It is designed for merger with the Substrate instance's Sections 1-4, which define Layer 0 (Substrate Attestation Protocol) and Layer 1 (Shellstream Transport Protocol).* --- ## Table of Contents 5. [Layer 2 — Shellstream Session Protocol (SSP)](#5-layer-2--shellstream-session-protocol-ssp) 6. [Layer 3 — Shellstream Governance Protocol (SGP)](#6-layer-3--shellstream-governance-protocol-sgp) 7. [Layer 4 — Shellstream Federation Protocol (SFP)](#7-layer-4--shellstream-federation-protocol-sfp) 8. [Cross-Layer Interactions](#8-cross-layer-interactions) 9. [Security Considerations (L2-L4)](#9-security-considerations-l2-l4) --- ## 5. Layer 2 — Shellstream Session Protocol (SSP) ### 5.1. Purpose The Shellstream Session Protocol (SSP) establishes authenticated, tenant-scoped sessions on top of the transport layer (L1). SSP transforms an external identity claim — whether an OIDC token, an X.509 SVID, or an SSH certificate — into a Substrate Attestation Token (SAT) that binds the operator to a specific scope, tier, and tenant context for the duration of a session. SSP does not define how bits reach the session endpoint. L1 handles transport multiplexing. SSP defines how an identity is verified, what authorization scope is granted, and how that scope evolves during the session's lifetime through elevation and de-escalation. ### 5.2. Identity Model #### 5.2.1. External Identity Claims An SSP session begins with an external identity claim presented during transport-layer authentication. Implementations MUST support at least one of the following claim types: | Claim Type | Verification Method | Normative Reference | |---|---|---| | `oidc` | RS256 JWT verification against JWKS endpoint | RFC 7519, RFC 7517 | | `svid` | X.509 SVID chain validation within trust domain | SPIFFE Trust Domain and Bundle (spiffe.io) | | `workload` | SPIFFE Workload API attestation | SPIFFE Workload API (spiffe.io) | Each claim type MUST resolve to the following canonical fields: ``` ExternalIdentityClaim ::= { claim_type: STRING, -- "oidc" | "svid" | "workload" subject: STRING, -- unique identifier within issuer issuer: STRING, -- issuer URI roles: [STRING], -- authorization roles tenant_id: STRING | NULL, -- multi-tenant scope (if known) raw_token: OPAQUE | NULL -- original credential for re-verification } ``` Implementations MAY support additional claim types. Any additional claim type MUST produce the same canonical fields. #### 5.2.2. Identity Resolution The SSP endpoint MUST resolve an external claim into an `Identity` record through the following process: 1. **Verification**: Validate the cryptographic integrity of the claim (JWT signature, X.509 chain, etc.). 2. **Claim Extraction**: Extract `subject`, `issuer`, and authorization attributes. 3. **Role Mapping**: Transform issuer-specific role representations into canonical role names. For OIDC with Keycloak, this MUST extract roles from the `realm_access.roles` claim. 4. **Tenant Resolution**: Determine the tenant context via one of three strategies: - **Header**: Explicit tenant identifier in transport headers (`X-Guildhouse-Tenant`). - **FromIdentity**: Derive tenant from the identity claim (e.g., `tenant_id` in OIDC token). - **Fixed**: Static tenant assignment from endpoint configuration. The resolved identity MUST take one of three forms: - **Oidc**: Verified OIDC token with `subject`, `issuer`, `email`, `roles`, `tenant_id`, and `raw_claims`. - **Svid**: Verified SPIFFE SVID with `uri` and `trust_domain`. - **Anonymous**: No verifiable identity presented. Implementations MUST restrict Anonymous sessions to read-only operations with no governance authority. ### 5.3. Substrate Attestation Token (SAT) #### 5.3.1. SAT Structure The SAT is the authorization primitive for all governance operations. A SAT binds: who (bearer SVID), where (trust domain), for whom (tenant), what (authorized scopes), and when (validity window). ``` SatRef ::= { sat_hash: BYTES[32], -- SHA-256 of the SAT content bearer_svid: STRING, -- SPIFFE ID of the bearer scopes: [SatScope], -- authorized operation scopes issued_at: TIMESTAMP, -- UTC issuance time expires_at: TIMESTAMP -- UTC expiration time } SatScope ::= { registry_type: STRING, -- registry type (e.g., "credential", "invoice") or "*" verbs: [STRING], -- authorized verbs (e.g., ["create", "update"]) or ["*"] resource_pattern: STRING -- glob pattern for resources (e.g., "tenant-a/*") or "*" } ``` #### 5.3.2. SAT Issuance An SSP endpoint MUST issue a base SAT upon successful identity resolution. The base SAT MUST adhere to the **principle of least privilege**: - For interactive SSH sessions (Bascule precursor), the base SAT MUST be scoped to read-only operations: `verbs: ["get", "list"]`. - For service-to-service sessions (SVID-based), the base SAT MUST be scoped to the service's declared registry types and verbs. The SAT MUST be signed by the session endpoint. The current signing mechanism is HMAC-SHA256 with a server-held secret key. Implementations SHOULD transition to Ed25519 or TPM-backed signing in production deployments. SAT TTL MUST NOT exceed 3600 seconds (1 hour) for interactive sessions. Service-to-service SATs MAY have longer TTLs but MUST NOT exceed 86400 seconds (24 hours). #### 5.3.3. SAT Authorization A SAT authorizes an operation if and only if at least one scope entry satisfies all of: 1. `scope.registry_type == "*"` OR `scope.registry_type == requested_registry_type` 2. `scope.verbs` contains `"*"` OR contains `requested_verb` 3. `scope.resource_pattern` matches the target resource via glob semantics: - `"*"` matches any resource - `"tenant-a/*"` matches resources under tenant `tenant-a` - Exact string matches the literal resource identifier If no scope satisfies all three conditions, the operation MUST be denied. #### 5.3.4. SAT Expiration Implementations MUST check SAT expiration before every governed operation. An expired SAT MUST NOT authorize any operation. The session endpoint MUST return an `SessionExpired` result and prompt re-authentication. ### 5.4. Session Establishment #### 5.4.1. Session State An SSP session maintains the following mutable state: ``` SessionState ::= { identity: Identity, base_sat: SatRef, tenant_context: STRING | NULL, tier: CommandTier, elevation: ElevatedSat | NULL, started_at: TIMESTAMP, last_activity: TIMESTAMP } ``` #### 5.4.2. Command Tiers SSP defines three authorization tiers, each a strict superset of the previous: | Tier | Verbs | Requires Elevation | Description | |---|---|---|---| | **Analyst** | `get`, `list`, `query`, `history`, `verify` | No | Read-only introspection | | **Administrator** | Analyst + `approve`, `deny`, `deploy`, `pipeline`, `schematic` | Yes | Governance mutations | | **Engineer** | Administrator + `elevate`, `deescalate`, `void` | Yes | Infrastructure + break-glass | The command manifest MUST declare, for each command: - `name`: Command identifier - `tier`: Minimum required tier - `scope_check`: One of `ReadOnly` (no SAT scope check), `TenantRequired` (SAT must authorize tenant), or `RegistryScoped(registry_type, verb)` (SAT must authorize specific registry+verb) #### 5.4.3. Elevation and De-escalation An operator in the Analyst tier MAY request elevation to Administrator or Engineer. Elevation MUST: 1. Verify the operator's roles include the target tier's required role. 2. Issue a new SAT (`ElevatedSat`) with expanded scopes. 3. Set a TTL on the elevated SAT. The TTL MUST NOT exceed 3600 seconds. 4. Record the elevation event for audit. ``` ElevatedSat ::= { sat: SatRef, -- elevated SAT with broader scopes tier: CommandTier, -- Administrator | Engineer granted_at: TIMESTAMP, expires_at: TIMESTAMP, granted_by: STRING -- identity that approved elevation } ``` De-escalation drops the elevated SAT and reverts to the base SAT. De-escalation MUST be immediate and unconditional. If an elevated SAT expires during a session, subsequent commands requiring that tier MUST return `ElevationExpired`. The session remains valid at the base tier. ### 5.5. Tenant Binding #### 5.5.1. Tenant Context A session MAY be bound to a specific tenant via the `use ` command. When a tenant context is set: - All governed operations MUST validate that the SAT's resource pattern authorizes the active tenant (via `SatScope::tenant_matches()`). - All registry queries MUST be scoped to the active tenant. - Changing tenant context does NOT require re-authentication but MUST re-validate SAT scope. #### 5.5.2. Tenant Isolation SSP enforces tenant isolation at the session level as the first line of defense. This is complemented by L3's `TenantScopedRegistry` (Section 6.8), which enforces isolation at the registry protocol level. An operation targeting tenant `T` MUST be rejected if: - No SAT scope has a `resource_pattern` matching `T`. - The session's identity was resolved with a different `tenant_id` and no cross-tenant scope exists. --- ## 6. Layer 3 — Shellstream Governance Protocol (SGP) ### 6.1. Purpose The Shellstream Governance Protocol (SGP) defines how mutations to governed registries are authorized, executed, attested, and anchored. SGP is the core value proposition of the Shellstream stack: it transforms every state change — across any registry, any transport, any tool — into a governed, auditable, merkle-anchored operation. The Guildhouse SDK (`guildhouse-tower`, `guildhouse-mq`, `registry-protocol`) is the normative implementation of SGP. ### 6.2. Operation Model #### 6.2.1. Governed Operations Every mutation to a governed registry is an **operation**. An operation is characterized by: ``` GovernanceMetadata ::= { registry_type: STRING, -- target registry (e.g., "invoice", "credential") verb: STRING, -- operation verb (e.g., "create", "revoke") artifact_scope: STRING, -- resource pattern for authorization ceremony_override: CeremonyBehavior? -- optional override (Auto | Required | Skip) } ``` An operation proceeds through a fixed sequence of phases: ``` Identity Resolution → Tenant Resolution → Intent Creation → Ceremony Gate → Intent Redemption → SAT Issuance → Handler Execution → Notarization ``` Implementations MUST execute these phases in order. No phase MAY be skipped except where explicitly permitted by this specification. #### 6.2.2. Transport Agnosticism SGP is transport-agnostic. The governance workflow is defined as a pure function operating on governance types: ``` execute_governance_workflow( client: GovernanceClient, identity: Identity, tenant: Tenant, metadata: GovernanceMetadata ) → GovernanceOutcome GovernanceOutcome ::= Approved(GovernanceContext) | Denied { reason: STRING } | CeremonyRequired { ceremony_id: STRING, intent_id: STRING } ``` Transport adapters — HTTP middleware (`GovernanceLayer`), message brokers (`GovernedMessaging`), SSH sessions (`CommandExecutor`) — MUST delegate to this function. No transport adapter MAY implement its own governance logic. ### 6.3. Intent Lifecycle #### 6.3.1. MutationIntent The MutationIntent is a pre-authorization record that bridges the gap between user request time and worker execution time. This two-phase design is essential for asynchronous workflows where a short-lived SAT cannot survive across message queues, webhook-triggered cascades, or ceremony approval delays. ``` MutationIntent ::= { intent_id: UUID, registry_type: STRING, verb: STRING, artifact_scope: STRING, tenant_id: STRING, authorized_by: IdentityClaim, mediated_by: STRING, -- SVID of the mediating service authorized_at: TIMESTAMP, expires_at: TIMESTAMP, max_redemptions: UINT32, redeemed_count: UINT32, -- mutable, excluded from canonical form status: IntentStatus -- mutable, excluded from canonical form } IntentStatus ::= Active | Redeemed | Expired | Revoked ``` #### 6.3.2. Intent Creation Intent creation is the first governance gate. The process: 1. The transport adapter presents `Identity`, `Tenant`, and `GovernanceMetadata` to the governance service. 2. The governance service evaluates whether the identity is authorized to create an intent for the requested `registry_type` and `verb` combination. 3. If authorized, the service creates a `MutationIntent` with status `Active`. 4. If the verb's `default_ceremony_required()` returns `true`, the service creates a `GovernanceCeremonyRequest` and returns `ceremony_id` alongside the `intent_id`. 5. If the verb does not require a ceremony, the response includes `intent_id` only (no `ceremony_id`). The governance service response: ``` CreateIntentResponse ::= { intent_id: STRING, expires_at: TIMESTAMP, ceremony_id: STRING | NULL, denied: BOOLEAN, denial_reason: STRING | NULL } ``` #### 6.3.3. Intent Redemption Intent redemption is the second governance gate. The process: 1. The transport adapter presents the `intent_id` to the governance service. 2. The service atomically increments `redeemed_count` and transitions status from `Active` to `Redeemed` (if `max_redemptions` is reached). 3. On success, the service issues a SAT scoped to the intent's `registry_type`, `verb`, and `tenant_id`. 4. The SAT is returned as a `SatRef`. Redemption MUST be atomic. If two concurrent redemptions race, at most `max_redemptions` total redemptions MUST succeed. The normative implementation uses a single SQL `UPDATE ... WHERE redeemed_count < max_redemptions` for atomicity. ``` RedeemIntentResponse ::= { success: BOOLEAN, sat_ref: SatRef | NULL, error: STRING | NULL } ``` #### 6.3.4. Intent Canonical Form The canonical form of a MutationIntent for hashing and anchoring MUST exclude mutable fields (`redeemed_count`, `status`). The canonical form includes only immutable authorization grant fields, serialized as RFC 8785 JCS (JSON Canonicalization Scheme): ``` CanonicalIntent ::= JCS({ artifact_scope, authorized_at, authorized_by, expires_at, intent_id, max_redemptions, mediated_by, registry_type, tenant_id, verb }) ``` The intent hash is computed as: `SHA-256(0x00 || "mutation-intent" || canonical_json)`. #### 6.3.5. Intent Expiration An intent expiration loop MUST run in the governance service. Expired intents (where `now >= expires_at` and `status == Active`) MUST be transitioned to `Expired`. The normative implementation runs this loop every 60 seconds. ### 6.4. Ceremony Protocol #### 6.4.1. Ceremony Types SGP defines six ceremony types, ordered by restrictiveness: | Type | Required Approvals | Description | |---|---|---| | `SelfGrant` | 0 | Auto-approved at creation. No human intervention. | | `Autonomous` | 0 | Auto-approved. Triggered by system events (webhooks, pipelines). | | `BreakGlass` | 1 | Emergency access. One approval + external evidence. | | `SingleApproval` | 1 | One approval from a permitted role. | | `QuorumApproval` | N (configurable) | N approvals from permitted roles. Default N=2. | | `Inherit` | From parent | Uses parent classification's ceremony type. | Implementations MUST treat `SelfGrant` and `Autonomous` as equivalent for resolution purposes — both produce an immediately-approved ceremony request. #### 6.4.2. Ceremony Request A governance ceremony request is created when Accord policy determines that a mutation requires human approval: ``` GovernanceCeremonyRequest ::= { ceremony_id: UUID, ceremony_type: CeremonyType, subject: CeremonySubject, required_approvals: UINT32, approver_roles: [STRING], approvals: [CeremonyApproval], status: CeremonyStatus, created_at: TIMESTAMP, expires_at: TIMESTAMP, intent_id: STRING | NULL, run_id: STRING | NULL, pr_number: UINT64 | NULL, remote_name: STRING | NULL } CeremonyStatus ::= Pending | Approved | Denied | Expired | Cancelled ``` `Pending` is the only non-terminal status. All other statuses are terminal — once entered, MUST NOT transition to any other status. #### 6.4.3. Ceremony Subjects The `CeremonySubject` identifies what the ceremony governs. SGP defines the following subject types: | Subject Type | Fields | Use Case | |---|---|---| | `MutationIntent` | intent_id, registry_type, verb, artifact_scope, tenant_id | Registry mutation requiring approval | | `PipelineMerge` | run_id, pipeline_name, branch, commit_hash, remote_name | Pipeline result merge to canonical | | `SchematicPublish` | schematic_name, version, tree_hash | Schematic publication | | `GitOpsSync` | tool, resource_name, resource_namespace, target_revision, environment, tenant_id | ArgoCD/Flux sync governance | | `Custom` | subject_type, reference_id, description | Extension point | Implementations MAY add additional subject types. New subject types MUST be backward-compatible via tagged serialization. #### 6.4.4. Approval Decisions A stakeholder records an approval or denial: ``` CeremonyApproval ::= { approver_identity: STRING, -- who approver_role: STRING, -- in what capacity decision: Approve | Deny, comment: STRING | NULL, decided_at: TIMESTAMP } ``` Recording a decision MUST fail if: - The ceremony is in a terminal status (`AlreadyResolved`). - The ceremony has expired (`Expired`). - The approver's role is not in `approver_roles` (unless `approver_roles` is empty, which permits any role) (`InvalidRole`). - The approver has already voted in the same role (`DuplicateApproval`). #### 6.4.5. Ceremony Evaluation The ceremony engine evaluates ceremony state after each decision. Evaluation is stateless — it examines the current approvals and produces a status transition: 1. If the ceremony is already terminal, return `false` (no change). 2. If `now >= expires_at`, transition to `Expired`. 3. If `denial_count > 0`, transition to `Denied`. Any single denial immediately terminates the ceremony. 4. If `approval_count >= required_approvals`, transition to `Approved`. Evaluation MUST be called after every `record_decision` and by the expiration sweep loop. #### 6.4.6. Ceremony Resolution When a ceremony exits the `Pending` state, a `CeremonyResolution` is produced as an immutable audit record: ``` CeremonyResolution ::= { ceremony_id: STRING, status: CeremonyStatus, -- terminal status subject: CeremonySubject, approvals: [CeremonyApproval], resolved_at: TIMESTAMP, proof_hash: HEX_STRING -- SHA-256 of JCS-canonical form } ``` The proof hash is computed over the JCS-canonicalized resolution (alphabetical field order). This proof hash serves as a tamper-evident seal — any modification to the ceremony ID, status, subject, approvals, or resolution timestamp invalidates the proof. Implementations MUST provide a `verify_proof()` operation that recomputes the hash from the resolution's fields and compares it to the stored `proof_hash`. ### 6.5. Accord Policy Engine #### 6.5.1. Classification Accord is the policy engine that maps resource mutations to ceremony requirements. Classification operates on file paths or resource identifiers using glob patterns: ``` Accord ::= { classifications: [{ name: STRING, paths: [GLOB_PATTERN], -- e.g., "k8s/production/**" ceremony: CeremonyType, requirements: CeremonyReqs }] } ``` Classification proceeds as follows: 1. `classify_changeset(changes)` matches each changed path against all classification rules. 2. Multiple rules MAY match a single path — all matching classifications are collected. 3. `classify_resource(resource_type, resource_name)` provides programmatic classification for non-file-based resources. #### 6.5.2. Resolution Resolution takes all matching classifications and produces a single ceremony requirement using **most-restrictive-wins** semantics: ``` resolve_ceremony(matches) → ResolvedRequirement Restrictiveness order (least → most): SelfGrant < Autonomous < BreakGlass < SingleApproval < QuorumApproval ``` When multiple classifications match: - The most restrictive `CeremonyType` is selected. - Approver roles are merged (union). - Quorum requirements use the maximum across all matches. - Additional conditions (schedules, environments) are merged. `Inherit` classifications resolve by looking up the parent directory's classification, recursing until a concrete type is found. ### 6.6. Registry Protocol #### 6.6.1. Core Traits SGP defines three traits that every governed registry MUST implement: **RegistryArtifact** — identity and canonical serialization: ``` trait RegistryArtifact { fn artifact_id(&self) -> &str; fn registry_type(&self) -> &str; fn canonical_bytes(&self) -> Vec; } ``` `canonical_bytes()` MUST produce deterministic output for identical artifacts. The normative serialization is RFC 8785 JCS. **MutationVerb** — operation semantics: ``` trait MutationVerb { fn as_scope_verb(&self) -> &str; fn default_ceremony_required(&self) -> bool; } ``` **GovernedRegistry** — validation and execution: ``` trait GovernedRegistry { fn validate_mutation(&self, artifact: &A, verb: &V, sat: &SatRef) -> Result<()>; async fn execute_mutation(&self, artifact: A, verb: V, sat: &SatRef) -> Result; } ``` #### 6.6.2. MutationEnvelope Every executed mutation produces a `MutationEnvelope` — the universal wrapper for merkle anchoring: ``` MutationEnvelope ::= { envelope_version: UINT32, -- currently 1 registry_type: STRING, artifact_id: STRING, verb: STRING, actor_svid: STRING, sat_hash: BYTES[32], before_hash: BYTES[32]?, -- NULL for creates after_hash: BYTES[32], ceremony_id: STRING?, payload_hash: BYTES[32], -- SHA-256 of canonical payload timestamp: TIMESTAMP, intent_id: STRING?, -- Phase C+: MutationIntent ID caused_by: CausalRef? -- Phase C+: causal chain } ``` The `after_hash` is computed using domain-separated hashing: ``` after_hash = SHA-256(0x00 || registry_type || canonical_bytes) ``` The `0x00` prefix is the leaf domain separator, ensuring envelope hashes are compatible with the merkle tree infrastructure. The `payload_hash` is a bare SHA-256 of the canonical bytes (without domain separation), enabling independent verification of the payload content. #### 6.6.3. Causal References Each envelope MAY carry a causal reference linking it to the trigger of the mutation: ``` CausalRef ::= PriorMutation { envelope_id: STRING } | ExternalEvent { source, event_id, event_type, verification: STRING } | UserIntent { intent_id: STRING } | Autonomous { trigger: STRING } ``` Causal references form a directed acyclic graph (DAG) of governance events. This DAG enables auditors to trace any mutation back to its originating user action, external event, or system trigger. #### 6.6.4. Envelope Canonicalization The envelope's canonical hash for merkle anchoring is computed as: ``` canonical_hash(domain_separator) = SHA-256(0x00 || domain_separator || JCS(envelope)) ``` Where `JCS(envelope)` is the RFC 8785 canonical JSON serialization of the full envelope (including `intent_id` and `caused_by` when present). Backward compatibility: envelopes without `intent_id` and `caused_by` MUST omit these fields from JSON serialization (via `skip_serializing_if`), ensuring that pre-Phase-C hashes remain stable. ### 6.7. Execution Attestation #### 6.7.1. Notarization After handler execution, the transport adapter MUST submit the `MutationEnvelope` to the notary service for merkle anchoring. The notary: 1. Receives the envelope with its opaque `payload_hash` and computed `after_hash`. 2. Buffers the envelope in the anchor buffer. 3. Periodically (or on threshold) flushes the buffer into a merkle tree. 4. Records the leaf index and tree root for proof generation. Notarization is fire-and-forget from the transport adapter's perspective. A notarization failure MUST NOT prevent the operation from completing, but MUST be logged as a warning. The `notarized` field in the operation result indicates whether notarization succeeded. #### 6.7.2. Unified Anchor Buffer The anchor buffer accepts envelopes from all registry types and anchors them in a single merkle tree per flush cycle. This provides: - **Global ordering**: all mutations across all registries share a single timeline. - **Cross-registry proofs**: a single merkle root attests to all mutations in a batch. - **Per-type assign-back**: after anchoring, each leaf index is dispatched to its registry-specific storage via the `AnchorAssigner` trait. #### 6.7.3. Proof Generation On demand, the notary generates a merkle inclusion proof for any anchored envelope: ``` MerkleProof ::= { leaf_hash: BYTES[32], leaf_index: UINT32, siblings: [BYTES[32]], root: BYTES[32], tree_height: UINT32 } ``` A verifier recomputes the root from `leaf_hash` and `siblings` and confirms it matches `root`. ### 6.8. Tenant Isolation SGP enforces tenant isolation at two levels: 1. **Session level** (L2/SSP): SAT scope authorization with `resource_pattern` matching. 2. **Registry level** (L3/SGP): `TenantScopedRegistry` wrapper that verifies `SatScope::tenant_matches()` before delegating to the inner registry. The `TenantScopedRegistry` is a defense-in-depth measure. Even if a bug in L2 allows a session to reference the wrong tenant, L3 MUST independently verify tenant authorization. ### 6.9. Guild Integration Pattern SGP defines a standard pattern for integrating external tools (ArgoCD, Flux, Terraform, etc.) into governance: ``` External Tool Notification (webhook) → Operation Mapper (parse tool-specific payload → SyncRequest) → Governance Bridge (Accord classify → resolve ceremony) → Decision: - Allow: log audit event, proceed - Suspend: create ceremony, suspend tool resource, track pending state → On CeremonyResolved: resume tool resource → On CeremonyDenied: keep suspended, log → Attester: notarize the operation outcome ``` Tool adapters implement the `GitOpsAdapter` trait (or equivalent per-tool trait): ``` trait GitOpsAdapter { async fn suspend(&self, resource: &SyncResource) -> Result<()>; async fn resume(&self, resource: &SyncResource) -> Result<()>; async fn is_suspended(&self, resource: &SyncResource) -> Result; fn tool(&self) -> GitOpsTool; } ``` This pattern ensures governance is additive — existing tools continue to operate, with governance layered on top via webhook interception and suspend/resume control. ### 6.10. Schematic Evaluation Schematics are composite meta-artifacts that describe complete business systems under `.guildhouse/` directory trees. SGP evaluates schematic mutations with the following additional governance constraints: - `SchematicVerb::Publish` and `SchematicVerb::Withdraw` MUST require a ceremony (via `default_ceremony_required() → true`). - Approvals are bound to the schematic's `tree_hash` — if the tree changes after approval, prior approvals MUST be invalidated (rebind invalidation). - Schematic manifests MAY include per-environment GitOps policy overrides: ``` GitOpsPolicy ::= { environment: STRING, -- "production", "staging" ceremony_type: STRING, -- ceremony type override required_roles: [STRING], -- approver roles for this environment resource_patterns: [GLOB_PATTERN] -- resource names this policy applies to } ``` --- ## 7. Layer 4 — Shellstream Federation Protocol (SFP) ### 7.1. Purpose The Shellstream Federation Protocol (SFP) extends SSP and SGP across trust domain boundaries. SFP enables multiple Substrate instances — each with their own SPIFFE trust domain, governance policies, and registry state — to cooperate on shared governance workflows. SFP is designed for the MSP (Managed Service Provider) model where multiple organizations (MSPs, customers, insurers, cloud service providers, ISVs) must collaborate on governed operations that span organizational boundaries. ### 7.2. Trust Domain Model #### 7.2.1. Trust Domains Each participating organization operates a SPIFFE trust domain: ``` TrustDomain ::= { domain: STRING, -- e.g., "guildhouse.example.com" bundle: TrustBundle, -- SPIFFE trust bundle (public keys) governance_url: STRING, -- endpoint for federated governance RPCs capabilities: [Capability] -- declared protocol capabilities } ``` Trust domains are the unit of administrative control. Each domain: - Issues its own SVIDs and SATs. - Maintains its own Accord policy. - Operates its own ceremony engine and notary. - MAY delegate specific governance decisions to other domains via federation. #### 7.2.2. Federation Registry Each Substrate instance MUST maintain a federation registry — a governed record of all peer trust domains with which it has established federation: ``` FederationEntry ::= { peer_domain: STRING, consent_ceremony_id: STRING, -- ceremony that authorized federation established_at: TIMESTAMP, status: Active | Suspended | Revoked, allowed_scopes: [FederatedScope] } FederatedScope ::= { registry_type: STRING, verbs: [STRING], direction: Inbound | Outbound | Bidirectional } ``` Adding or modifying a federation entry MUST require a ceremony. The minimum ceremony type for federation establishment SHOULD be `QuorumApproval`. ### 7.3. Consent Ceremony #### 7.3.1. Federation Establishment Federation between two trust domains is established through a bilateral consent ceremony: 1. **Domain A** initiates federation by creating a `FederationProposal` containing: - The proposing domain's trust bundle. - Requested federated scopes (registry types, verbs, direction). - Proposed federation TTL. 2. **Domain B** receives the proposal and creates a local ceremony (subject type: `FederationConsent`). 3. Authorized stakeholders in Domain B approve or deny the proposal via standard ceremony mechanics (Section 6.4). 4. If approved, Domain B creates its own `FederationEntry` and returns an acceptance containing Domain B's trust bundle. 5. Domain A receives the acceptance and creates its own `FederationEntry` (subject to its own ceremony approval). 6. Both domains exchange trust bundles and establish mutual TLS connectivity. Both consent ceremonies produce `CeremonyResolution` records that are independently notarized in each domain's merkle tree. #### 7.3.2. Consent Verification At any point, either domain MAY request a proof that the other domain authorized federation. This proof consists of: - The `CeremonyResolution` from the peer domain's consent ceremony. - A merkle inclusion proof anchoring that resolution. - The peer domain's trust bundle for signature verification. ### 7.4. Mutual Attestation #### 7.4.1. Cross-Domain SVID Verification When Domain A's workload communicates with Domain B's service, mutual attestation proceeds: 1. Domain A's workload presents its X.509 SVID. 2. Domain B verifies the SVID against Domain A's trust bundle (obtained during federation establishment). 3. Domain B verifies that Domain A is in its federation registry with `Active` status. 4. Domain B verifies that the requested operation falls within the allowed federated scopes. If any check fails, the request MUST be rejected with an `Unauthorized` error that does NOT reveal which check failed (to prevent information leakage about federation configuration). #### 7.4.2. Attestation State Cross-domain requests carry an `AttestationState` header that accumulates governance metadata as the request traverses layers and domains: ``` AttestationState ::= { originating_domain: STRING, originating_svid: STRING, intent_chain: [IntentRef], -- intents from each domain traversed ceremony_chain: [CeremonyRef], -- ceremonies from each domain sat_chain: [SatRef], -- SATs issued at each domain route: [DomainHop] -- ordered list of domains traversed } DomainHop ::= { domain: STRING, entered_at: TIMESTAMP, svid: STRING, -- local SVID used in this domain sat_hash: BYTES[32] -- SAT hash issued in this domain } ``` Each domain in the chain MUST append its own hop before forwarding. The `AttestationState` provides end-to-end auditability of cross-domain governance flows. ### 7.5. Federated SAT Exchange #### 7.5.1. SAT Downscoping When Domain A presents a request to Domain B, Domain B MUST NOT accept Domain A's SAT directly. Instead, Domain B: 1. Verifies Domain A's SVID and federation status. 2. Creates a local intent scoped to the federated operation. 3. Issues a local SAT, downscoped to the intersection of: - Domain A's requested operation. - The federated scopes allowed for Domain A. - Domain B's local Accord policy. This ensures that federation never grants more access than what either domain independently authorizes. #### 7.5.2. SAT Chain The `sat_chain` in `AttestationState` records every SAT issued during a cross-domain operation. This chain enables auditors to verify that each domain independently authorized its portion of the operation. ### 7.6. Federated Ceremonies #### 7.6.1. Cross-Domain Ceremony Some operations require approval from stakeholders in multiple domains. A federated ceremony: 1. The originating domain creates a ceremony with `approver_roles` that include roles from peer domains. 2. The ceremony is published to each peer domain that has stakeholders with matching roles. 3. Stakeholders in each domain approve or deny via their local ceremony interface. 4. Approvals are collected at the originating domain. 5. Resolution follows standard ceremony evaluation (Section 6.4.5). Cross-domain approvals MUST include the approver's domain in the `approver_identity` field (e.g., `alice@ops.domain-b.example.com`) to prevent cross-domain identity collisions. #### 7.6.2. Ceremony Proof Aggregation A federated ceremony's resolution includes approvals from multiple domains. The proof hash MUST cover all approvals regardless of origin domain. Verifiers can independently check each approval against its origin domain's trust bundle. ### 7.7. Cross-Domain Routing #### 7.7.1. Routing Rules SFP defines routing rules for cross-domain operations: - **Inbound**: Peer domain's request arrives at local governance endpoint. Local policy applies. - **Outbound**: Local operation triggers a side-effect in a peer domain. The local domain creates a federated intent. - **Passthrough**: A request transits through a domain without local execution. The transiting domain MUST still record a hop in `AttestationState`. #### 7.7.2. Loop Prevention Cross-domain routing MUST detect and prevent routing loops. A request MUST be rejected if the `route` array in `AttestationState` already contains the current domain. Implementations SHOULD also enforce a maximum hop count (default: 5). ### 7.8. Consent Revocation Either domain MAY revoke federation consent at any time by: 1. Creating a ceremony (subject: `FederationRevocation`) in the revoking domain. 2. Upon ceremony approval, transitioning the `FederationEntry` to `Revoked` status. 3. Notifying the peer domain of the revocation (best-effort; the peer SHOULD also monitor federation health). Revocation MUST be immediate and unilateral — it does NOT require approval from the peer domain. All in-flight operations from the revoked domain MUST be rejected after revocation takes effect. --- ## 8. Cross-Layer Interactions ### 8.1. SAT Lifecycle Across Layers The SAT flows through all layers: | Layer | SAT Role | |---|---| | L1 (STP) | Transport carries SAT hash in headers/metadata | | L2 (SSP) | Session issues base SAT, manages elevation | | L3 (SGP) | Intent lifecycle creates operation-scoped SATs; `sat_hash` recorded in MutationEnvelope | | L4 (SFP) | Federated SAT exchange; SAT chain in AttestationState | **Invariant**: At every layer, operations MUST be authorized by a valid, non-expired SAT. No layer MAY bypass SAT authorization. **Scope narrowing**: SATs become more specific as they flow up the stack. A session SAT (L2) authorizes a broad scope. An intent-derived SAT (L3) is scoped to a specific `registry_type` and `verb`. A federated SAT (L4) is further restricted to the intersection of federation scopes. ### 8.2. AttestationState Propagation `AttestationState` is constructed and enriched at each layer: 1. **L2**: Session establishment populates `originating_domain` and `originating_svid`. 2. **L3**: Intent creation adds to `intent_chain`. Ceremony resolution adds to `ceremony_chain`. SAT issuance adds to `sat_chain`. 3. **L4**: Each domain hop appends to `route`. Federated SAT exchange appends to `sat_chain`. For HTTP transports, `AttestationState` is propagated via: - `X-Guildhouse-Intent-Id`: Current intent identifier. - `X-Guildhouse-Sat-Hash`: Hex-encoded SAT hash. - `X-Guildhouse-Ceremony-Id`: Ceremony identifier (when ceremony was involved). - `X-Guildhouse-Notarized`: Boolean indicating notarization success. - `X-Guildhouse-Correlation-Id`: End-to-end correlation identifier. For message transports, `AttestationState` fields are carried in `MessageHeaders`: ``` MessageHeaders ::= { message_id: STRING, timestamp: TIMESTAMP, registry_type: STRING, verb: STRING, tenant_id: STRING, intent_id: STRING?, sat_hash: STRING?, -- hex-encoded ceremony_id: STRING?, correlation_id: STRING? } ``` ### 8.3. Error Escalation Errors propagate upward through the stack with increasing abstraction: | Layer | Error | HTTP Status | Behavior | |---|---|---|---| | L2 | Identity verification failure | 401 Unauthorized | Connection rejected | | L2 | Tenant mismatch | 403 Forbidden | Operation rejected | | L2 | SAT expired | N/A (session) | `SessionExpired` result; re-auth required | | L3 | Intent denied by policy | 403 Forbidden | Reason provided to caller | | L3 | Ceremony required | 202 Accepted | `ceremony_id` returned; handler NOT executed | | L3 | Governance service unavailable | 503 Service Unavailable | Opaque message; details logged server-side | | L3 | Intent redemption failure | 503 Service Unavailable | Opaque message | | L4 | Federation not established | 403 Forbidden | Rejected; no federation details leaked | | L4 | Federated scope exceeded | 403 Forbidden | Rejected | | L4 | Routing loop detected | 508 Loop Detected | Request terminated | **Security invariant**: Error responses for L3 infrastructure errors (`GovernanceUnavailable`, `Internal`) MUST NOT leak internal details (IP addresses, service names, error specifics). The canonical error message is returned to the caller; the full error is logged server-side for debugging. ### 8.4. Ceremony-Intent Coupling When an operation requires a ceremony: 1. **L3** creates an intent (status: `Active`) and a ceremony (status: `Pending`). The intent's `intent_id` is linked to the ceremony. 2. The transport adapter returns `CeremonyRequired { ceremony_id, intent_id }` to the caller. 3. The caller (or their delegate) submits approvals via the ceremony interface. 4. When the ceremony resolves to `Approved`, the intent becomes redeemable. 5. The caller re-submits the operation with the `intent_id`. L3 redeems the intent and proceeds. If the ceremony resolves to `Denied` or `Expired`, the linked intent MUST also be transitioned to a terminal state. ### 8.5. Transport-Specific Behavior #### 8.5.1. HTTP (Tower Middleware Stack) The HTTP governance stack is composed as ordered Tower layers: ``` IdentityLayer → TenantLayer → GovernanceLayer → [Handler] → NotarizeLayer ``` Each layer extracts or inserts values into the request's extensions map. Order is critical: - `IdentityLayer` MUST run before `TenantLayer` (tenant resolution may depend on identity). - `TenantLayer` MUST run before `GovernanceLayer` (intent creation requires tenant context). - `GovernanceLayer` short-circuits on denial (403) or ceremony requirement (202). - `NotarizeLayer` runs after the handler, reading the `MutationOutcome` from response extensions. Routes are classified into three tiers: - **Public**: No middleware (`/health`, `/metrics`). - **Read**: `IdentityLayer` + `TenantLayer` only (queries, listings). - **Governed**: Full stack (mutations). #### 8.5.2. Messaging (GovernedMessaging) Governed messaging follows the same governance workflow as HTTP but wraps a `MessageDriver` rather than an HTTP handler: - **Publish**: Full governance lifecycle (intent → ceremony check → SAT → publish → notarize). - **Subscribe**: Tenant isolation check only (`topic.tenant_id == subscriber.tenant_id`). Message headers carry the same governance metadata as HTTP headers (Section 8.2). #### 8.5.3. SSH (Bascule Shell) SSH sessions use SSP (L2) directly: - Identity is established during SSH key authentication. - Base SAT is issued with read-only scope. - Commands check SAT scope before execution. - Mutations require elevation (Section 5.4.3). - Elevation grants are time-bounded and audited. ### 8.6. Host Function Layer as Embedded ABI The Host Function Layer (HFL, [Substrate HFL Spec](../../substrate/docs/hfl-spec.md)) is the in-process capability boundary within Shellstream sessions. While Shellstream transports governance across network boundaries (L1–L4), the HFL enforces governance *within a single host*. **Relationship to Shellstream layers:** | Boundary | Protocol | Scope | |----------|----------|-------| | Shellstream L1–L4 | Network protocol (SSH, HTTP, messaging) | Cross-machine governance transport | | HFL | Component Model ABI (WIT interfaces) | In-process host function dispatch | | eBPF shell | Kernel enforcement | Syscall and network gating | Shellstream sessions originating from HFL modules carry the module's attenuated `RequestToken` in the session's SAT header. The receiving service (Quartermaster, Bascule) validates this token using the same SAT verification logic used for any Shellstream session. From the receiver's perspective, an HFL-originated session is indistinguishable from any other Shellstream session — the attenuation chain is transparent. **Consent channel negotiation at session establishment.** When an SSP session (L2) is established, the server MAY advertise available consent channels via the `consent-channels@guildhouse.dev` SSH certificate extension ([Shellstream Extensions](../../guildhouse-spire-plugins/specs/shellstream-extensions.md)). Client modules use this information to determine which consent transport tier is available (see [GH-DESIGN-0005 §5](../../guildhouse-platform/docs/GH-DESIGN-0005-wasm-component-model.md)). The formal WIT interface for consent is defined in `substrate:consent@0.1.0`. **WIT as the formal contract.** The HFL's host function signatures are defined in WIT (WebAssembly Interface Types). Four packages — `substrate:governance`, `substrate:identity`, `substrate:consent`, `substrate:network` — capture the governance-layer abstractions. Standard Component Model toolchains can target these interfaces, making HFL modules portable across hosts that implement the same WIT packages. ### 8.7. Network Governance Lifecycle Events The `governance-notifier` SPIRE plugin emits network lifecycle events alongside credential lifecycle events. These events use the same tiered transport model described in [GH-DESIGN-0005 §5](../../guildhouse-platform/docs/GH-DESIGN-0005-wasm-component-model.md) (Tier 0–3 consent channels): | Event | Trigger | Notarized | |-------|---------|-----------| | Workload attached to network | Kedge `CNI ADD` → `substrate:network/workload-network.attach-workload` | Yes | | Workload detached from network | Kedge `CNI DEL` → `substrate:network/workload-network.detach-workload` | Yes | | Flow policy installed/updated | `substrate:network/firewall.install-flow-policy` | Yes | | Route announced | `substrate:network/routing.announce-route` | Yes | | Route withdrawn | `substrate:network/routing.withdraw-route` | Yes | On vanilla Linux without Pulsar (Tier 0–1), network events go to local store-and-forward queues. On full deployments with message queue fabric (Tier 2), they publish to governed Pulsar topics. The transport tier is determined by the host's `HostCapabilities.consent_channels` in the SAT (SAT-SPEC-0001 §3.9). Network lifecycle events follow the same `MutationEnvelope` format as credential events (see [Credential Governance](../specs/credential-governance.md)): each event is wrapped in an envelope, canonicalized via RFC 8785 JCS, and anchored in the Quartermaster merkle tree via `NotaryService.CreateAnchor`. --- ## 9. Security Considerations (L2-L4) ### 9.1. Session Security (L2) **S-L2-1: Credential Replay Prevention**. SSP implementations MUST bind session establishment to a transport-layer nonce or challenge to prevent credential replay across sessions. For SSH transport, the SSH session ID serves this purpose. For HTTP transport, implementations SHOULD use short-lived JWTs with `jti` (JWT ID) claims. **S-L2-2: SAT Scope Minimization**. Base SATs MUST follow the principle of least privilege. Interactive sessions MUST start at Analyst tier with read-only scope. Implementations MUST NOT issue SATs with wildcard scopes (`"*"`) to human operators; wildcard scopes are reserved for infrastructure services authenticated via SVID. **S-L2-3: Elevation Auditing**. Every elevation MUST be logged with the operator identity, target tier, granted scopes, and TTL. Elevation grants MUST have a maximum TTL of 3600 seconds. Implementations SHOULD alert on repeated elevation failures. **S-L2-4: Session Timeout**. Idle sessions MUST be terminated after a configurable timeout (default: 1800 seconds). SAT expiration provides a hard upper bound on session lifetime regardless of activity. **S-L2-5: Tenant Boundary Enforcement**. Tenant context changes within a session MUST re-validate SAT scope. An operator with a SAT scoped to `tenant-a/*` MUST NOT be able to switch to `tenant-b` context. ### 9.2. Governance Security (L3) **S-L3-1: Intent Atomicity**. Intent redemption MUST be atomic. Concurrent redemptions MUST NOT exceed `max_redemptions`. The normative implementation uses a single SQL `UPDATE ... WHERE redeemed_count < max_redemptions` to guarantee this invariant under concurrent access. **S-L3-2: Ceremony Tamper Evidence**. Every ceremony resolution MUST produce a proof hash. The proof hash MUST cover all fields that affect the ceremony outcome (ceremony ID, status, subject, approvals, resolved timestamp). Implementations MUST provide `verify_proof()` for independent verification. **S-L3-3: Denial Finality**. A single denial MUST immediately and irrevocably terminate a ceremony. This prevents social engineering attacks where an attacker pressures a denier to change their vote. **S-L3-4: Information Leakage Prevention**. Error responses for governance infrastructure failures (`GovernanceUnavailable`, `Internal`) MUST NOT contain internal details. The full error is logged server-side; the client receives a generic message. This prevents attackers from probing the governance infrastructure topology. **S-L3-5: Canonical Hash Stability**. Envelope canonical hashes MUST be deterministic across implementations. All implementations MUST use RFC 8785 JCS for canonicalization. Backward-compatible field additions (e.g., `intent_id`, `caused_by`) MUST use `skip_serializing_if` to ensure hashes of pre-addition envelopes remain stable. **S-L3-6: Domain Separation**. All hashes in SGP use domain-separated hashing: `SHA-256(0x00 || domain || data)`. The `0x00` leaf prefix ensures separation from internal merkle tree nodes. The domain string ensures separation between registry types. Implementations MUST NOT use bare `SHA-256(data)` for any governance hash. **S-L3-7: Notarization Independence**. Notarization failure MUST NOT block the governed operation. However, a persistent notarization failure SHOULD trigger an alert. Implementations SHOULD track the notarization success rate and degrade gracefully (e.g., buffering envelopes for retry) rather than silently losing attestation records. ### 9.3. Federation Security (L4) **S-L4-1: Consent Ceremony Threshold**. Federation establishment SHOULD require `QuorumApproval` or stronger. `SelfGrant` and `SingleApproval` SHOULD NOT be used for federation consent, as federation grants cross-organizational access. **S-L4-2: SAT Downscoping**. A federated SAT MUST be scoped to the intersection of: (a) the requested operation, (b) the federated scopes for the peer domain, and (c) local Accord policy. Federated SATs MUST NOT grant broader access than what the local domain would grant to a local workload performing the same operation. **S-L4-3: Loop Prevention**. Cross-domain routing MUST reject requests where the current domain already appears in the `AttestationState.route`. Implementations MUST enforce a maximum hop count (RECOMMENDED: 5) to prevent amplification attacks via long routing chains. **S-L4-4: Unilateral Revocation**. Federation revocation MUST be unilateral and immediate. The revoking domain MUST NOT wait for peer acknowledgment. All in-flight operations from the revoked domain MUST be rejected. Revocation ceremonies MUST be notarized for audit. **S-L4-5: Trust Bundle Rotation**. Trust bundle updates during active federation MUST NOT invalidate in-flight operations. Implementations SHOULD support a grace period where both the old and new trust bundles are accepted. The grace period SHOULD NOT exceed the maximum SAT TTL (24 hours for service-to-service SATs). **S-L4-6: Federation Information Leakage**. Rejected cross-domain requests MUST NOT reveal: - Whether federation exists with the requesting domain. - Which scopes are configured for federation. - Why the request was rejected (scope mismatch vs. federation not established). All rejection reasons MUST be logged locally but MUST return a generic `Forbidden` to the requesting domain. **S-L4-7: Cross-Domain Identity Namespacing**. Approver identities in federated ceremonies MUST include the originating domain (e.g., `alice@ops.domain-b.example.com`) to prevent identity collisions between domains. Two approvers named `alice@ops` in different domains MUST be treated as distinct identities. --- ## Appendix A: Normative References | Reference | Description | |---|---| | RFC 7519 | JSON Web Token (JWT) | | RFC 7517 | JSON Web Key (JWK) | | RFC 8785 | JSON Canonicalization Scheme (JCS) | | SPIFFE | Secure Production Identity Framework for Everyone (spiffe.io) | | Guildhouse SDK | `guildhouse-tower`, `guildhouse-mq`, `registry-protocol` crates | | Bascule Shell | `bascule-shell`, `bascule-core` crates | | Accord | `accord-core` crate — policy classification and ceremony resolution | ## Appendix B: Glossary | Term | Definition | |---|---| | **SAT** | Substrate Attestation Token — authorization primitive binding identity, scope, tenant, and time | | **SVID** | SPIFFE Verifiable Identity Document — workload identity certificate | | **Intent** | MutationIntent — pre-authorization for a governed operation | | **Ceremony** | Multi-stakeholder approval flow for operations requiring human sign-off | | **Accord** | Policy engine mapping resource mutations to ceremony requirements | | **Envelope** | MutationEnvelope — universal wrapper for merkle anchoring of governed mutations | | **Registry** | A governed data store implementing RegistryArtifact + MutationVerb + GovernedRegistry | | **Schematic** | Composite meta-artifact describing a complete business system | | **JCS** | JSON Canonicalization Scheme (RFC 8785) — deterministic JSON serialization | | **Trust Domain** | SPIFFE trust domain — unit of administrative control in federation | | **Guild** | An integrated external tool (ArgoCD, Flux, Terraform) operating under governance |