guildhouse-spire-plugins/docs/shellstream-spec-0001-upper-layers.md
Tyler King 6321037ac1 Add network-policy extension and network governance lifecycle events
New shellstream extension §10.6 network-policy@guildhouse.dev carrying
GovernedNetworkPolicy hash in SSH certificates. New §8.7 in upper layers
spec documenting network governance lifecycle events (attach, detach,
flow policy, route announce/withdraw) emitted by governance-notifier
using the tiered consent transport model.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 19:38:13 -05:00

52 KiB
Raw Blame History

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

  1. Layer 2 — Shellstream Session Protocol (SSP)
  2. Layer 3 — Shellstream Governance Protocol (SGP)
  3. Layer 4 — Shellstream Federation Protocol (SFP)
  4. Cross-Layer Interactions
  5. 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 <tenant> 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<u8>;
}

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<A: RegistryArtifact, V: MutationVerb> {
    fn validate_mutation(&self, artifact: &A, verb: &V, sat: &SatRef) -> Result<()>;
    async fn execute_mutation(&self, artifact: A, verb: V, sat: &SatRef) -> Result<MutationOutcome>;
}

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<bool>;
    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.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.

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).

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) is the in-process capability boundary within Shellstream sessions. While Shellstream transports governance across network boundaries (L1L4), the HFL enforces governance within a single host.

Relationship to Shellstream layers:

Boundary Protocol Scope
Shellstream L1L4 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). Client modules use this information to determine which consent transport tier is available (see GH-DESIGN-0005 §5). 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 (Tier 03 consent channels):

Event Trigger Notarized
Workload attached to network Kedge CNI ADDsubstrate:network/workload-network.attach-workload Yes
Workload detached from network Kedge CNI DELsubstrate: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 01), 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): 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