Add consent-channels@guildhouse.dev SSH certificate extension for advertising available consent transport channels. Add §8.6 to upper layers spec describing HFL as the in-process capability boundary within Shellstream sessions, with WIT as the formal contract. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
51 KiB
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
- Layer 2 — Shellstream Session Protocol (SSP)
- Layer 3 — Shellstream Governance Protocol (SGP)
- Layer 4 — Shellstream Federation Protocol (SFP)
- Cross-Layer Interactions
- 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:
- Verification: Validate the cryptographic integrity of the claim (JWT signature, X.509 chain, etc.).
- Claim Extraction: Extract
subject,issuer, and authorization attributes. - Role Mapping: Transform issuer-specific role representations into canonical role names. For OIDC with Keycloak, this MUST extract roles from the
realm_access.rolesclaim. - 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_idin OIDC token). - Fixed: Static tenant assignment from endpoint configuration.
- Header: Explicit tenant identifier in transport headers (
The resolved identity MUST take one of three forms:
- Oidc: Verified OIDC token with
subject,issuer,email,roles,tenant_id, andraw_claims. - Svid: Verified SPIFFE SVID with
uriandtrust_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:
scope.registry_type == "*"ORscope.registry_type == requested_registry_typescope.verbscontains"*"OR containsrequested_verbscope.resource_patternmatches the target resource via glob semantics:"*"matches any resource"tenant-a/*"matches resources under tenanttenant-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 identifiertier: Minimum required tierscope_check: One ofReadOnly(no SAT scope check),TenantRequired(SAT must authorize tenant), orRegistryScoped(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:
- Verify the operator's roles include the target tier's required role.
- Issue a new SAT (
ElevatedSat) with expanded scopes. - Set a TTL on the elevated SAT. The TTL MUST NOT exceed 3600 seconds.
- 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_patternmatchingT. - The session's identity was resolved with a different
tenant_idand 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:
- The transport adapter presents
Identity,Tenant, andGovernanceMetadatato the governance service. - The governance service evaluates whether the identity is authorized to create an intent for the requested
registry_typeandverbcombination. - If authorized, the service creates a
MutationIntentwith statusActive. - If the verb's
default_ceremony_required()returnstrue, the service creates aGovernanceCeremonyRequestand returnsceremony_idalongside theintent_id. - If the verb does not require a ceremony, the response includes
intent_idonly (noceremony_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:
- The transport adapter presents the
intent_idto the governance service. - The service atomically increments
redeemed_countand transitions status fromActivetoRedeemed(ifmax_redemptionsis reached). - On success, the service issues a SAT scoped to the intent's
registry_type,verb, andtenant_id. - 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(unlessapprover_rolesis 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:
- If the ceremony is already terminal, return
false(no change). - If
now >= expires_at, transition toExpired. - If
denial_count > 0, transition toDenied. Any single denial immediately terminates the ceremony. - If
approval_count >= required_approvals, transition toApproved.
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:
classify_changeset(changes)matches each changed path against all classification rules.- Multiple rules MAY match a single path — all matching classifications are collected.
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
CeremonyTypeis 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:
- Receives the envelope with its opaque
payload_hashand computedafter_hash. - Buffers the envelope in the anchor buffer.
- Periodically (or on threshold) flushes the buffer into a merkle tree.
- 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
AnchorAssignertrait.
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:
- Session level (L2/SSP): SAT scope authorization with
resource_patternmatching. - Registry level (L3/SGP):
TenantScopedRegistrywrapper that verifiesSatScope::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::PublishandSchematicVerb::WithdrawMUST require a ceremony (viadefault_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:
-
Domain A initiates federation by creating a
FederationProposalcontaining:- The proposing domain's trust bundle.
- Requested federated scopes (registry types, verbs, direction).
- Proposed federation TTL.
-
Domain B receives the proposal and creates a local ceremony (subject type:
FederationConsent). -
Authorized stakeholders in Domain B approve or deny the proposal via standard ceremony mechanics (Section 6.4).
-
If approved, Domain B creates its own
FederationEntryand returns an acceptance containing Domain B's trust bundle. -
Domain A receives the acceptance and creates its own
FederationEntry(subject to its own ceremony approval). -
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
CeremonyResolutionfrom 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:
- Domain A's workload presents its X.509 SVID.
- Domain B verifies the SVID against Domain A's trust bundle (obtained during federation establishment).
- Domain B verifies that Domain A is in its federation registry with
Activestatus. - 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:
- Verifies Domain A's SVID and federation status.
- Creates a local intent scoped to the federated operation.
- 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:
- The originating domain creates a ceremony with
approver_rolesthat include roles from peer domains. - The ceremony is published to each peer domain that has stakeholders with matching roles.
- Stakeholders in each domain approve or deny via their local ceremony interface.
- Approvals are collected at the originating domain.
- 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:
- Creating a ceremony (subject:
FederationRevocation) in the revoking domain. - Upon ceremony approval, transitioning the
FederationEntrytoRevokedstatus. - 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:
- L2: Session establishment populates
originating_domainandoriginating_svid. - L3: Intent creation adds to
intent_chain. Ceremony resolution adds toceremony_chain. SAT issuance adds tosat_chain. - L4: Each domain hop appends to
route. Federated SAT exchange appends tosat_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:
- L3 creates an intent (status:
Active) and a ceremony (status:Pending). The intent'sintent_idis linked to the ceremony. - The transport adapter returns
CeremonyRequired { ceremony_id, intent_id }to the caller. - The caller (or their delegate) submits approvals via the ceremony interface.
- When the ceremony resolves to
Approved, the intent becomes redeemable. - 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:
IdentityLayerMUST run beforeTenantLayer(tenant resolution may depend on identity).TenantLayerMUST run beforeGovernanceLayer(intent creation requires tenant context).GovernanceLayershort-circuits on denial (403) or ceremony requirement (202).NotarizeLayerruns after the handler, reading theMutationOutcomefrom response extensions.
Routes are classified into three tiers:
- Public: No middleware (
/health,/metrics). - Read:
IdentityLayer+TenantLayeronly (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 (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). 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). Three packages — substrate:governance, substrate:identity, substrate:consent — 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.
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 |