SC is a small, host-neutral authority component for agent runtimes.
It mediates exactly one transition: from "a host says some principal wants to do X" to "X is permitted or denied under bounded Authority, and any Effect the host later reports can be correlated to the Decision that authorized it."
SC authorizes; it does not execute. This package contains no executor, no execution backend, no process launcher, no tool invoker, and no shell or container runner. A permitted Decision is permission, not an occurrence.
trusted bootstrap ──▶ Root Authority ──▶ Request ──▶ Gate.authorize ──▶ Decision
│
HOST performs the operation │
(outside SC, on its own path) │
▼
host reports Effect ──▶ realizes_decision(effect, decision)
SC is derived from validated Siphonophore research but is a separate project. It is not a fork, not Siphonophore v2, and imports nothing from it.
python3 -m venv .venv && .venv/bin/pip install -e '.[dev]'
.venv/bin/python -m pytest # the whole suite, ~0.3s
.venv/bin/python examples/reference_host.py # the vertical slice, end to endThe test suite needs no API key, no network, no root, and nothing Linux-specific. If any of those are ever required, a test has gone wrong.
import sc
class MyPolicy(sc.Policy):
def evaluate(self, request: sc.Request) -> sc.PolicyOutcome:
return sc.PolicyOutcome(permitted=request.operation_name == "document.read")
gate = sc.Gate(MyPolicy(), secret=...) # 32+ bytes, yours to manage
# 1. The deployment establishes a trust anchor. SC cannot check this grant.
root = gate.bootstrap_authority(
"agent://root", {"document.read"},
issuer_reference="deployment-control-plane", max_delegations=2,
)
# 2. The root agent delegates a narrower authority to a child. A_child <=
# A_parent, enforced. The delegator is who YOUR host says is acting, and
# it must be the parent's principal: holding an Authority is not enough.
child = gate.delegate(root, "agent://child", delegator_reference="agent://root",
allowed_operations={"document.read"})
# 3. Before the host does anything, it asks.
request = sc.Request("document.read", "agent://child", {"name": "notes.txt"})
decision = gate.authorize(request, child) # raises on any refusal
# 4. The HOST performs the operation. SC is not involved and cannot be.
result = my_host_actually_reads_the_document(request)
# 5. The HOST reports what it did, citing the Decision it was given.
effect = sc.Effect(
request_id=request.request_id,
decision_reference=decision.reference,
detail={"bytes": len(result)},
)
assert sc.realizes_decision(effect, decision)Every refusal is a GateViolation, which is a PermissionError — a host that
catches nothing specific still fails closed.
An authentic Authority is not necessarily one the deployment still wants used. Give the Gate a status source, which is whatever your control plane uses to withdraw authority, and SC will require the Authority and every ancestor to be current before it delegates from it, issues a Decision under it, or lets a Decision under it be relied on (ADR 0003):
class MyControlPlane(sc.AuthorityStatusSource):
source_reference = "my-control-plane"
def statuses(self, authority_ids): # one call, one snapshot
withdrawn = my_store.withdrawn_among(authority_ids)
return sc.StatusReport(
{i: sc.AuthorityStatus.NOT_CURRENT if i in withdrawn
else sc.AuthorityStatus.CURRENT for i in authority_ids},
snapshot_reference=my_store.version(),
)
gate = sc.Gate(MyPolicy(), secret=..., status_source=MyControlPlane())Withdrawing a root disables every descendant, including ones your control
plane never heard of, because each Authority carries its authenticated
lineage. A Decision issued before the withdrawal is refused by
check_decision afterwards. The source can say "unknown", or fail. Either
way SC refuses, and it never treats that as current.
examples/withdrawal_registry.py is a minimal in-memory reference.
Without a status source the Gate checks no currency and claims none: every
Decision it mints has current_at_issuance=False.
| Refusal | Means |
|---|---|
AuthorityVerificationError |
The Authority was not minted here, or was altered |
PrincipalMismatchError |
Genuine Authority, but bound to a different principal than the requester, or than the delegator. A subclass of AuthorityVerificationError |
DelegationError |
The requested delegation would exceed the parent |
DecisionVerificationError |
The Decision was forged, altered, or belongs to a different Request |
PolicyDeniedError |
The Decision is genuine and says no. Carries the signed refusal |
ContentMismatchError |
Pinned content differs from what was authorized |
AuthorityNotCurrentError |
Genuine Authority, but it or an ancestor is withdrawn. Carries the StatusAssessment |
AuthorityStatusUndeterminedError |
Genuine Authority, but currency could not be established (unknown, or source failed). Never a permit |
gate.authorize(request, authority) checks the Authority and then the
policy. The Authority can narrow what the policy would allow; it can never
widen it.
gate.authorize(request) — with no Authority — consults the policy alone.
That is a deliberate, supported path: a deployment's policy is itself an
authority source. It is not a permit, and it is not an oversight to be relied
on. The resulting Decision records authority_id=None. A Policy cannot see
whether an Authority was presented, so a deployment that relies on Authority
must refuse such a Decision at its execution binding. Otherwise every
refusal an Authority gives is avoided by presenting none. The reference binding
does this (docs/adr/0009).
These are the load-bearing limits. Stating them precisely is part of the design, not a caveat appended to it.
No state of its own. SC has no expiry, no leases, no usage counters, no
one-shot capabilities, and no child-count tracking. An Authority is a standing
capability: it is used, and delegated from, only for the subject it is bound
to, but nothing limits how often. A permitted Decision can be presented with
its own Request any number of times. Ending an Authority is possible only through a
configured status source (above). The source holds that state, and SC only
asks. Without a source, there is still no way to end an agent's authority
short of rotating the secret. With one, the guarantee holds exactly as far as
the source is truthful and current. SC does not authenticate the source.
There is also a window between check_decision returning and the host acting,
and only the host can close it. See docs/AUTHORITY_CURRENT_VALIDITY.md
§15–16 for the precise guarantee and its limits.
This paragraph replaced the original "Stateless V0" statement when ADR 0003
landed. That history is in DESIGN.md §13 / OD-1 and the ADR.
Symmetric authentication only. A holder of the Gate's secret can verify a token and can equally mint one. There is no public verifiability and no non-repudiation, and a verifier is not cryptographically prevented from minting. SC claims nothing stronger.
Identity is host-asserted. principal_reference and issuer_reference are
opaque strings the host supplies. SC binds authority to them and never parses,
validates, or authenticates them. If the host verified an identity, that
happened before SC saw it; SC's possession of the string is not evidence of it.
Correlation is not verification, and not proof of execution.
realizes_decision says an Effect cites a Decision, and names that Decision's
request_id. It does not verify that Decision — call gate.check_decision
separately — and it cannot establish that the host performed anything, or that
what it performed matches what was authorized. An Effect is the host's
self-report. It is not authenticated, carries no outcome, and does not
distinguish one execution from another. See docs/CORE_CONTRACT.md
"Execution contract".
A conforming execution binding performs exactly what was authorized. It
takes a Decision and its Request and nothing else, runs check_decision
before anything happens, and performs exactly the Request's bound material or
refuses. It never coerces, defaults or ignores that material. If the
deployment relies on Authority, it also refuses a policy-only Decision.
ReferenceHost.carry_out in examples/reference_host.py is the reference. That
is fidelity, not mediation: SC cannot see whether the host has another route
to the same effect.
The same Request means the same bound material, and nothing more. A
Decision holds for any Request with equal request_id, operation_name,
requester_reference, canonical arguments and bound content, however many
times it is built. SC normalizes no values: "/a//b" is not "/a/b", and NFC
is not NFD. If your host treats such values as equal, normalize them before
you build the Request. A Request stores exactly what SC bound, frozen, so
nested arguments come back as tuples and read-only mappings. See
docs/CORE_CONTRACT.md "Request identity".
SC cannot stop a host that does not ask. SC being called is not the same as the effect being unable to bypass SC. Enforcing that ordering is the host adapter's discipline, and no host examined in the source research offers a stronger guarantee.
One value serves two roles. Decision.reference is concretely the same
string as Decision.token in V0. They are named separately because they are
conceptually distinct — a stable correlation identifier versus an
authenticator — and may be separated later without changing any Effect
construction site. A consequence worth knowing: holding a reference is enough
to construct an Effect that correlates, though not to mint or verify anything.
Nothing depends on the token being secret. Knowing a whole Decision lets a
party present that exact Decision again, which stateless V0 already allows,
and nothing more.
A verified Decision shows what was authenticated. Decision, Authority
and Scope store plain builtin values, and none of them, nor Request, can
be subclassed. A str subclass or str-Enum you pass in comes back as the
equal plain str. See docs/CORE_CONTRACT.md "Decision binding".
src/sc/
__init__.py the whole public API, 22 names
errors.py the typed refusals
status.py the current-validity boundary: AuthorityStatusSource & co.
authority.py Scope, Authority
request.py Request
decision.py Decision, DecisionProjection, Policy, PolicyOutcome
effect.py Effect, realizes_decision
gate.py Gate — mint, verify, delegate, authorize
_canonical.py private: canonicalization, digests, HMAC
examples/
reference_host.py a host, outside the package, that owns the action
withdrawal_registry.py a control plane, outside the package, that owns withdrawal
Zero third-party runtime dependencies, enforced by a test. Python 3.11+.
docs/CORE_CONTRACT.md— the compact invariant contractDESIGN.md— normative architecturedocs/IMPLEMENTATION_PLAN.md— build sequence and gatesdocs/adr/— material architecture decisionsdocs/AUTHORITY_CURRENT_VALIDITY.md— current-validity research, guarantee, non-guaranteesdocs/SC_V0_NUCLEUS_HANDOFF.md— what this tranche built and proveddocs/AUTHORITY_CURRENT_VALIDITY_HANDOFF.md,docs/ACCEPTED_PRIORS_HANDOFF.md,docs/DELEGATION_HARDENING_HANDOFF.md,docs/EXACT_REQUEST_SEMANTICS_HANDOFF.md— what each later tranche built and provedresearch/source/— the validated extraction findings SC was built from