SITUSDemoDashboardMailboxesPolicyMandatesGrantsReceiptsVerifyPasskeysDocs

Docs

What Situs Pilot 1 is, and how to build against it.

From docs/ARCHITECTURE.md in the repository.

Architecture — Situs Pilot 1

Situs puts a deterministic authority between an AI agent and the systems it can move money in. The agent proposes. A signed Intent Contract and a versioned, signed Trust Profile decide. A human signs the exact action when the amount requires it. One enforcement point holds the only bank credential, and every step leaves a signed fact that ends up in a portable Proof Receipt anyone can verify offline.

Services and ports

ServicePortHoldsTalks to
gateway8000JWT dev secret. No bank credential.authority, pep, the model (or the scripted stub)
authority8001authority and situs_assembly keys. Contracts, grants, evidence chain, receipts, transparency log; the Approval API (docs/APPROVAL_API.md) and the approver keys it enrolls (keys/enrolled/, authority-signed).webhooks to registered approver apps (optional Rekor)
pep8002pep key and the broker secret, the only way to mint a bank tokenauthority, bank
bank8003bank key and the broker secret, to verify tokens. A mock bank.nothing
console8080reads the people's key files only to sign: approve (carried as a registered approver app), revoke, a mandate, a profile versionauthority

CLIs: situs-demo runs the storyline, situs-approve signs pending grants, situs-verify checks a receipt offline. Keys live in ./keys as ed25519 PEM files, created by make keys. Each service writes its own SQLite file under ./data. The authority writes receipts to ./out.

flowchart LR
    agent([agent]) -- JWT + contract id --> gateway[gateway :8000]
    gateway -- propose / present --> authority[authority :8001]
    gateway -- execute / fetch --> pep[pep :8002]
    pep -- redeem, evidence --> authority
    pep -- broker token --> bank[(bank :8003)]
    console[console :8080] --> authority
    approver([approver]) -- signs action hash --> authority
    treasurer([treasurer]) -- signs contract, revocation --> authority

The seven steps

The storyline from SPEC §1 as it runs through the services. make demo plays it with the scripted agent. Steps 1 to 5 are the live flow. Step 6 is offline. Step 7 is the bypass, shown for contrast.

sequenceDiagram
    autonumber
    actor T as Treasurer (principal)
    actor A as Agent
    participant G as gateway :8000
    participant AU as authority :8001
    actor AP as Approver
    participant P as pep :8002
    participant B as bank :8003
    actor V as Verifier (offline)

    T->>AU: POST /contracts (Intent Contract signed by principal_jdoe)

    rect rgba(200, 80, 80, 0.12)
    Note over A,AU: Step 1. Injected invoice
    A->>G: propose_transfer to 9988776655 (from the injected invoice)
    G->>AU: POST /grants (canonical action)
    AU-->>AU: deterministic policy: beneficiary_not_approved
    AU-->>G: grant status denied
    G->>AU: request event, receipt assembled (deny, no bank block)
    G-->>A: denied. No grant, no transfer
    end

    rect rgba(80, 140, 200, 0.12)
    Note over A,AP: Step 2. Legitimate transfer over the approval threshold
    A->>G: propose_transfer 75000.00 to ven_northwind
    G->>AU: POST /grants
    AU-->>G: grant pending with action_hash
    AP->>AU: GET /grants/{id}, recompute action_hash, sign it (situs-approve)
    AU-->>AU: grant active, single use, 10 min, idempotency key
    end

    rect rgba(80, 180, 120, 0.12)
    Note over A,B: Step 3. Execution through the enforcement point
    A->>G: propose_transfer with grant_id
    G->>P: POST /execute (grant and canonical action verbatim)
    P-->>P: verify grant signature, expiry, action hash, approver signature
    P->>AU: POST /grants/{id}/redeem
    P->>AU: enforcement_decision event signed by pep
    P->>B: POST /transfers via connector.json: 5 min broker token, idempotency key, action hash
    B-->>P: acknowledgment signed by bank, naming the action hash and idempotency key
    P->>AU: counterparty_ack event
    G->>AU: request event, receipt with five signatures, chained, logged
    G-->>A: executed
    end

    rect rgba(200, 160, 60, 0.12)
    Note over A,P: Step 4. Retry storm
    loop three times
        A->>G: propose_transfer with the same grant_id
        G->>P: POST /execute
        P-->>G: grant_already_redeemed with the original bank acknowledgment
        G-->>A: duplicate. One transfer exists
    end
    end

    rect rgba(150, 100, 200, 0.12)
    Note over T,AU: Step 5. Revocation
    T->>AU: POST /contracts/{id}/revoke (signed by principal_jdoe)
    A->>G: propose_transfer 18400.00 to ven_contoso
    G->>AU: POST /grants
    AU-->>G: denied, contract_revoked, with a receipt
    end

    Note over V: Step 6. situs-verify out/receipt.json runs nine checks with no network
    Note over A,B: Step 7. make demo-bypass: the agent holds a raw bank key and calls the bank directly. The wire executes. No Situs in the path.

Where each fact is signed

FactSigned byReceipt slotVerifier check
Intent Contract (scope, limits, window)principal principal_jdoesignatures.principal over contract_hash3
The exact actionapprover approver_jdoesignatures.approver over action_hash4
Allow or deny at executionenforcement point pep (the authority key for a policy denial that never reached the pep)signatures.pep5
Money moved (the counterparty's acknowledgment, bound to the action hash and idempotency key)the counterparty, bank for treasury (kid from connector.json)signatures.counterparty over counterparty_ack_hash6
The assembly of the abovesitus_assemblysignatures.situs over the receipt body2
Order and inclusionauthority over the signed tree headstatus.log, bundle.log_root_signature7, 8

Situs signs only the assembly. Editing a fact after the fact breaks the signature of the party that owns it, even if the assembly is re-signed. The demo shows this in step 6.

The enforcement point and its connector (receipt 0.3, Task 29)

The pep names no plugin's domain. It loads each plugin's declarations (façades, profile, connector.json) and never imports its rules.py. One generic HTTP connector (services/pep/connector.py) reads the connector file:

The counterparty_ack event carries the signed body with the field names and ack paths, so the offline verifier re-reads the acknowledgment without the plugin.

Developer docs, schemas and the venue bundle (Task 35)

situs_core/devdocs.py is the developer guide as data; the console's Docs tab renders it with the stack's live values and make docs writes docs/DEVELOPERS.md from it. situs_core/schemas.py generates the JSON Schemas (/docs/schema/<name>) from the pydantic models the services validate with. The authority's GET /plugins lists each loaded plugin's declarations (name, version, tier, façades, covered façade, counterparty kind), never its rules. situs_core/trust_bundle.json is the venue bundle the verifier ships with; situs_core/venues.py turns it into the "known venue" / "unknown key" label.

Canonical bytes

Every hash is SHA-256 over RFC 8785 JSON Canonicalization Scheme bytes, in situs_core/canon.py. Amounts are decimal strings, never floats. Timestamps are RFC 3339 UTC with a trailing Z. The approver, the enforcement point, and the verifier each recompute action_hash from the canonical action themselves. None of them trusts a hash it was handed.

What this build does not prove

Every receipt carries a proven and an unproven array. In this build the gateway's measurement is software only. what.level_achieved is L1-sim, and the hardware quote fields what.quotes.cpu_tdx and what.quotes.gpu are present and empty. The model's identity is claimed. docs/DEPLOY.md describes what changes for a real pilot.