Skip to content
adelic-aiPublic

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

41 Commits

Folders and files

Repository files navigation

SC

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.

Quick start

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 end

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

Using it

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.

Ending an Authority: current validity

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

Two sources of authority, and one that is easy to miss

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

What SC does not guarantee

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

Layout

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

Documents

  • docs/CORE_CONTRACT.md — the compact invariant contract
  • DESIGN.md — normative architecture
  • docs/IMPLEMENTATION_PLAN.md — build sequence and gates
  • docs/adr/ — material architecture decisions
  • docs/AUTHORITY_CURRENT_VALIDITY.md — current-validity research, guarantee, non-guarantees
  • docs/SC_V0_NUCLEUS_HANDOFF.md — what this tranche built and proved
  • docs/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 proved
  • research/source/ — the validated extraction findings SC was built from

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages