Policy Engine Overview
The policy engine evaluates owner-signed Policies (schema xyz.tinycloud.policy/policy/v0) and, when a Policy's when conditions are satisfied, issues a grant: a portable-delegation whose capabilities are bounded by the Policy's permissions_ceiling through strict containment. It is the declarative, evidence-aware layer above raw delegation — a Policy says "given this subject and this verifiable evidence, grant this subset of my authority for at most this TTL" — and it is the concrete mechanism behind "credentials feed the policy engine".
Role
The policy engine is the permissioning layer of Layer 1, a peer of capabilities and OpenKey within the protocol. Where a bare capability is imperative (the owner signs a specific grant to a specific key), a Policy is declarative: the owner signs a rule once, and the engine mints grants on demand to any holder who satisfies the rule. This is what lets authority be conditioned on facts the owner cannot know in advance — an OpenCredentials credential the holder presents at request time, an enrolled agent acting for an eligible subject — without the owner being online to sign each grant.
Design-intent boundary (validated, stated honestly): the policy engine is a standalone Rust workspace (policy-core + crates/policy-runtime + crates/policy-evidence-vc). It is not yet consumed by tinycloud-node — the node's authorization today is the in-tree capabilities/delegation path, and tinycloud-node has no dependency on policy-core or policy-runtime (verified: no such Cargo dependency). The locked layer model says the node will consume the engine at runtime; the v0 contract is "the policy engine emits native authority" (spec/mvp-doc-reconciliation.md), i.e. it produces a portable-delegation the node would honor — but the embedding is design-intent, not shipped wiring. Author and read this concept as in-progress.
Mechanics
A Policy is resolved in two HTTP-shaped round trips (crates/policy-runtime/src/lib.rs, PolicyRuntime):
- Challenge.
issue_challenge(policy_id, now)loads the active Policy and returns a nonce-bearingGrantChallenge(xyz.tinycloud.policy/challenge/v0) — a fresh random nonce, the engine'saudience, accepted signature suites, and a TTL. The nonce is the replay-protection anchor. - Resolve. The holder returns a
GrantPresentation(xyz.tinycloud.policy/presentation/v0) — its requested capabilities, aholder_bindingproof, the nonce, and anyevidence.resolve(presentation, now)then, in order: re-loads the active Policy; consumes the nonce (a second use is rejectedchallenge-nonce-consumed); runsvalidate_grant_presentation(src/evaluator.rs); validates the holder enrollment binding (validate_enrolled_agent_binding); verifies eachevidenceitem via the VC evidence verifier; re-evaluateswhenwith the satisfied evidence IDs (evaluate_expression); and only then callsGrantIssuer::issueto mint thePortableDelegation, recording anIssuanceRecord.
The grant's expiry is the minimum of policy.grant.max_ttl_seconds, the presentation's own expires_at, and every satisfied credential's valid_until (grant_expires_at) — so a grant never outlives its evidence.
Shape
A Policy (src/types.rs:49) is a signed object with fields:
{ schema, policy_id, owner_did, signing_key_did, created_at, expires_at?,
resource: PolicyResource, // resource_type, resource_id, permissions_ceiling: [PolicyCapability]
when: Expression, // allOf | anyOf | subject{did} | evidence{EvidenceRequirement}
grant: GrantTemplate, // output: portable-delegation, max_ttl_seconds, delegation_mode, revocation
disclosure?, audit?, signature }
whenis the condition grammar — a recursiveExpressionofallOf/anyOfoversubject{did}(the eligible subject must match) andevidence{…}(a named, verifier-bound credential requirement must be satisfied).grantcan only outputportable-delegation;delegation_modeisterminal(the grant cannot be re-delegated) orattenuable;revocationisrefresh-onlyoractive-cutoff.permissions_ceilingis a list ofPolicyCapabilityover servicestinycloud.kv | tinycloud.sql | tinycloud.vfs; every requested capability must be contained by some ceiling entry or the request is rejectedrequested-capabilities-exceeded.
Control-plane objects (Policy, challenge, enrollment, …) are signed under the TinyCloud Signed Object Profile: a domain-separated SHA-256 over RFC 8785 JCS of the body, with two suites (eddsa-ed25519-sha256-jcs-v1, eip191-secp256k1-sha256-jcs-v1). The GrantPresentation is deliberately nonce-bound, not content-addressed, in keeping with strict nonce-based replay protection.
Relationships
Evaluates an owner-signed Policy whose when may demand credential evidence verified against OpenCredentials; emits a portable-delegation that is a capability grant bounded by permissions_ceiling containment; binds the holder via holder enrollment; sits in Layer 1 as the permissioning peer of capabilities and OpenKey; the satisfied-evidence pipeline is detailed in feeds-policy-engine.
Example
An owner publishes one Policy pol_email_domain: when = evidence{ requirement_id: "email-domain", verifier: "w3c.vc/credential/v1", requirements: { type: "opencredentials.email/v1", emailDomains: ["tinycloud.xyz"] } }; permissions_ceiling = a single tinycloud.sql/read capability over a Listen transcript table; grant = portable-delegation, max_ttl_seconds: 3600, terminal, active-cutoff. Any enrolled agent that presents a valid SD-JWT proving a @tinycloud.xyz email — without revealing the address itself — receives a one-hour, non-re-delegatable read grant over exactly that table. No new owner signature is needed per holder. (This is the runtime's own end-to-end test, challenge_resolve_native_read_then_active_cutoff_denies.)
Status & drift
in-progress. The v0 contracts are frozen in spec/ (schemas, test vectors, the Signed Object Profile) and policy-core types + canonicalization + containment + grant-presentation validation are shipped; policy-runtime and policy-evidence-vc exist and pass an end-to-end vector test (despite the spec README still calling the runtime crates "not yet implemented"). Not implemented here: any HTTP service, persistence, or node embedding; node-side revocation / invocation enforcement (specified in spec/revocation.md, not built); and issue_challenge currently emits chal_{nonce} while the schema expects a gchal_… id. The biggest honest caveat is above: the node does not yet consume the engine — treat the L1-internal boundary as design-intent. See contradictions.
Sources
policy-engine:src/lib.rs(public surface),src/types.rs:49(Policy/Expression/GrantTemplate),crates/policy-runtime/src/lib.rs(PolicyRuntime::issue_challenge/resolve, end-to-end test),spec/README.md+spec/mvp-doc-reconciliation.md(frozen v0 contract, "policy-engine emits native authority")- Verified absence:
tinycloud-nodehas nopolicy-core/policy-runtimeCargo dependency (node does not yet consume the engine)