Policy and rulesets
The policy kernel evaluates a frozen resolved request against immutable configuration. Its results are DENY, REQUIRE_APPROVAL and ELIGIBLE. It has no HTTP/MCP route, consumes no human approval and grants no dispatch ownership.
Bundles and lifecycle
BundleContent contains tenant/bundle identity, Cedar schema, separate authorization and approval text, exact mapping references, typed limits, optional context_settings (see Context contracts), optional quorum rules and optional signal_mappings (Security signals). A quorum rule names an approval policy id (policy:<id>) or a REQUIRE_APPROVAL limit rule (limit:<rule_id>) of the same bundle and requires two to five distinct human approvers; rule ids and obligations are unique, a rule naming an unknown obligation fails validation with its field location, and the effective requirement of a decision is the largest over its obligations, or one when no rule matches. The quorum never adds or removes an obligation. A mapping reference may set observe_only: requests for that operation are resolved and evaluated but only observed, never dispatched, while the rest of the bundle stays enforced; the flag serializes only when set, so existing content hashes are unchanged. It may instead set external_executor (External execution checkpoint): requests for that operation are resolved, evaluated, reserved and claimed exactly as an enforced one, and then answered with a grant instead of a dispatch, because the caller's own platform holds the vendor connector. That flag serializes only when set too. The two are mutually exclusive — the model refuses the pair at construction and the store refuses it again with its field location — and a mapping carrying external_executor is refused unless its provider, action and provider operation are a released binding of the action catalog and the tenant has at least one unrevoked executor key, since without one an unreported grant would have no exit. Both checks run where every other component check runs, at validation and at activation; admission does not repeat them, because it reads the active bundle through the execution store without revalidating components, so two later guards carry the key requirement instead: revoke_executor_key refuses to retire a tenant's last live key while its active bundle declares an external executor, and admission itself refuses to issue a grant the tenant's key ring could not attest. Reporting a key compromised deliberately bypasses the first, which is why the second exists. Placement is bundle content and never a request field: no header, credential attribute or client capability moves an operation into or out of a checkpoint. A mapping reference may also carry action, the catalog action its operation ref serves, set only when the ref is another name for that action (mapping-create --ref); ruleset-compile sets it from the tenant's mapping row, it serializes only when set, and the model refuses one equal to the ref, so one meaning has one serialization. Validation and activation refuse an action that is not the one the tenant's mapping row serves, and a bundle carrying rules whose ref names another action than its row serves, both at mappings.<i>.action; evaluation denies INVALID_BUNDLE for a plan of another action. Rules, schemas and limits name the catalog action, never the ref, and one bundle serves an action under one contract version: ruleset-compile names two mappings of one action under different versions, or two contracts describing one Cedar entity differently, at mappings.<i>, and validation and activation refuse such a bundle as INVALID_COMPONENT without a location. Every component except context_settings, quorum, ruleset and signal_mappings must be present; an explicitly empty approval set is allowed. Schema and policy text have byte bounds, limit identities are unique, and unsupported rule fields/types fail validation. Content hashes include the entire canonical bundle; plan hashes also bind component versions and the resolved evidence.
A current ADMIN, or a Policy manager in the Spaces they manage, manages the lifecycle from the policy pages (/policy: list current versions with retired ones folded behind a count, create a draft from a JSON document or a rules file; /policy/{bundle_id}: validate, preview, then activate with an explicit confirmation and a reason, compare with the policy in use, download the rules, retire) or the bundle-* commands; both go through the same audited store functions. An activation records the reason it was given as the reason key of its policy.bundle_activated event, never in the bundle, so content hashes do not move and an activation given no reason (bundle-activate without --reason) writes the payload it always did. A Policy manager's preview, and an administrator's narrowed to some Spaces, counts only the decisions recorded in those Spaces. Content is stored separately from DRAFT, VALIDATED, ACTIVE and RETIRED lifecycle state. The runtime has no content update/delete/truncate privilege. Administrative state transitions must validate the current credential and role, verify referenced mapping/account versions, and append an audit event in the same transaction. A per-tenant control row serializes activation, including simultaneous first activation. Since migration 0041_spaces the selection is per space: space_policy_selection holds at most one bundle per space as its primary key, ACTIVE means "selected by at least one space of this tenant", activation returns the previously selected bundle to VALIDATED only when no other space still selects it (audited as policy.bundle_deselected), and retiring a bundle any space selects is refused. The fence does not move — activation still locks tenant_policy_control first and then writes the space's selection, so two activations in two spaces serialize on the row they always serialized on and admission reads the space's selection under the lock it already takes. A space with no selection fails closed exactly as a tenant with none did.
The synthetic endpoint_bundle() pack authorizes workstation isolation only with a linked open, critical incident. It forbids domain controllers and criticality 4 resources, requires approval for production, sets a principal autonomous threshold of ten targets per rolling hour, and a tenant total ceiling of twenty per rolling fifteen minutes. These are mock demonstration rules.
execbound_harness.soc_demo.soc_bundle() is a second synthetic pack covering all six catalog actions on both mocks. It permits workstation isolation and standard-identity control with an open, critical linked incident, permits a reset only for a standard identity with an open high or critical incident, and permits lifting isolation when the incident is CLOSED or the caller states context.arguments.reason == "false_positive". It forbids domain controllers, privileged and tier-0 identities and criticality 4. Approval is always required to enable an identity, for any production target, for a lift whose reason is remediated and for a reset that does not force a change at next sign-in. Each action gets a principal autonomous threshold of ten targets per rolling hour and a tenant total ceiling of twenty per rolling fifteen minutes. seed-validation installs it as VALIDATED beside the active demo bundle; Action catalog records the rules and the ADMIN activation step. These are mock demonstration rules.
Bundles may include context_settings: per contract and source kind, a max age (up to the contract ceiling) and a permitted writer allowlist. Unset settings use the contract defaults and are omitted from the stored bytes, so earlier bundle hashes are unchanged. Validation and activation require every mapping's contract to exist in the catalog and serve that mapping, the Cedar entity for each contract to declare exactly tenant, account and the contract's facts with their types, and every setting to name a used contract and source kind within its ceiling. They also check each mapping against the action catalog: the Cedar schema must declare the mapped action, its appliesTo.context record must declare exactly that action's arguments with their Cedar types (an empty record for an action with none), and every action a limit rule names must be a catalog action, because a rule for an unknown action would neither charge nor cap anything. Contract validation reads the declared entity attributes, so a bundle's Cedar schema must use Cedar's JSON schema format. A bundle whose schema uses Cedar's human-readable schema format is rejected at validation and activation.
Bundles may include signal_mappings, the operator's reading of security signal producers (Security signals): per contract version and capability, the sources admitted by id (one to four) and the table that maps each producer verdict, with an optional confidence floor, to a value of the capability's closed domain. Like context_settings it serializes only when set, so every earlier bundle keeps its content hash, and it is part of the content hash when set, so a changed threshold is a new bundle that makes plans and approvals frozen under the previous one stale. A bundle that maps a contract with a signal fact must carry an entry for it. Validation and activation check each entry against the tenant's source registrations and the bundle's space, and refuse at the field location an entry for an unmapped contract or an absent capability, a source that is unknown, revoked, registered for something else, agent_forwarded or unusable by the bundle's space, an offered bundle or the activating space, and a row reading clean over a source that does not attest liveness. Admission never repeats those checks: a source revoked after activation makes its facts fail at the next decision rather than taking the bundle down. The kernel reads the mapping at every resolution and again at evaluation, which re-applies it to the records the plan froze. ruleset-compile --signal-mappings carries a file of entries into the compiled bundle, and editing a draft's rules carries its signal_mappings into the new draft unchanged.
A limit rule names an action, a scope, a rolling window_seconds, a max_targets ceiling, a budget (autonomous or total) and an on_exceed effect (DENY or REQUIRE_APPROVAL). scope is one of principal, tenant, group, account, selector and space: a group rule carries group_id, an account rule account_id, a space rule space_id, and a selector rule selector, which is {"fact": <name>, "equals": <string | integer | boolean>} over one scalar trusted fact of the frozen plan. A scope without its qualifier, a qualifier without its scope or two qualifiers at once fail validation, so a rule can never silently widen to the tenant. An optional actions list of 1 to 8 catalog actions, sorted, unique and containing action, makes the rule count impact of every listed action and charge a candidate whose action is in the list; without it the rule counts action alone. A space rule is the per-customer ceiling an MSSP wants inside one tenant: it counts over the retained space_id of the impact row, never over where the agent is placed now, so moving an agent later never rewrites what past impact counted for. A tenant rule still counts across every space, which is deliberate and, named plainly, a cross-space denial channel. The five qualifiers serialize only when set, so the content hash, security_versions.limits and every plan hash of a bundle written before they existed are byte-identical. The store validates each rule with a field location: every name in the action list must be a catalog action (limits.<i>.action), group_id an existing principal group of the tenant (limits.<i>.group_id), account_id an active connector account of the tenant (limits.<i>.account_id), space_id an existing space of the tenant (limits.<i>.space_id), and a selector must name a fact that every context contract of the bundle's mappings serving one of the rule's actions declares, with a value inside each such contract's domain, and at least one contract must serve those actions (limits.<i>.selector). These checks run at validation and at activation, and load_active_bundle repeats them for a caller that wants a revalidated snapshot; admission does not, because it reads the active bundle through the execution store without re-running the component checks, so a group deleted after activation is not refused there. A group can only be deleted while it has no members, since the membership table's foreign key has no cascade. The shipped demo, identity and SOC packs carry only principal and tenant rules; group administration is described in Operations.
Rules
A ruleset is a bundle's source: allow, deny and review rules over the facts and arguments the catalog declares, each with typed conditions and values the declared domain admits, plus budgets and a quorum, compiled deterministically into the Cedar sets, limits and quorum the kernel already executes. The kernel never reads a ruleset; it reads the compiled bundle, and validation and activation recompile the ruleset and refuse a bundle whose components disagree with it, naming the component. A ruleset cannot express anything a bundle cannot, so hard-denial precedence and default deny are untouched by construction.
The pages show a rule as one sentence, labelled as how the rules read and never as the decision, above the compiled policy it produced; the preview names the rule behind each decision that would change, and marks a candidate that would owe a review as unresolved, because a replay cannot know a person's answer. A note on a rule is inert: shown as text, compiled nowhere.
The console's rules editor states budgets as well as rules. A budget written there counts one action for each agent, the whole tenant, or one agent group, connector account or space. The editor's read offers the qualifiers a budget may name, all read inside the tenant fence: the tenant's principal groups; the active connector accounts the draft's own mappings name, since every candidate admitted under the draft uses one of them; and the spaces the draft may serve (the space that owns it, or every space once it is offered tenant-wide) that the person may see. The write refuses any other group, account or space at the budget's field (ruleset.budgets.<i>.group_id) without echoing the identifier, and validation and activation check every qualifier again as above. A selector budget, or one over several actions, is written as JSON; a draft carrying one is read in the editor but not changed there, so no edit made in the form can drop it. A budget counted for a group, account or space the read no longer offers (a group the owner removed, or a space the draft does not serve or the person may not see) locks the draft the same way and is named, because the write would refuse every save that restates it.
A rule can also be drafted from a sentence. The console's assistant reads the draft an administrator is editing, with that administrator's own authority, and answers with one Rule in the rule model's own JSON schema, validated by the same validator the add-a-rule form uses and compiled with the draft's other rules against the draft's mappings before it is offered. What comes back is the rule, its sentence, and its conditions in the words the editor parses, which the page uses to pre-fill the form beside what the person said. The assistant writes nothing: it creates no draft, adds no rule and activates nothing. The administrator submits the form, and the draft then goes through validation and preview like any other, whose changed decisions are the check that the rule does what was meant. A proposal the compiler will not take is refused with the field that caused it rather than offered and failing later.
The welcome page and an agent's page read the rules the same way. project() in policy/rulesets.py says what the rules about one action say over one set of facts, following the compiled bundle's own precedence (a denial wins, an allow is needed, an applicable review means approval), names the rules the reading rests on, and names any fact it was not given instead of guessing. It is a reading for a person, never a decision: limits, quorum and people are outside it, every page labels it so, and tests/test_pack_rulesets.py holds it equal to the kernel's policy decision over every input of every pack. The packs write only is and in, so tests/test_rule_reading_operators.py holds it equal for the other three operators too: every is_not, at_least and at_most condition on every field at every value of its domain, the bounds included, each operator in every effect and in conjunction, and the Defender scenario's rules (compromise is_not compromised) over every input and every compromise value.
Cedar boundary
The pinned cedarpy==4.12.1 community binding provides the Cedar engine. It is not an AWS-supported Python SDK. Both policy sets are parsed and validated against the supplied schema. Authorization supports static permits and forbids; approval supports static permits only. Templates and unsupported effects are rejected through structured parsing.
The adapter checks the native JSON response rather than the convenience result accessors because those accessors default missing diagnostics to empty values. Missing or malformed decisions/diagnostics, unknown policy IDs, inconsistent effects and dependency exceptions deny. Runtime authorization and approval sets are both evaluated even if the first denies or errors. Native API use is deliberately narrow and must be reviewed when changing the pinned binding. The adapter calls cedarpy._internal.is_authorized and cedarpy._internal.validate_policies directly. As of 4.12.1 (checked October 9, 2026, and first on 4.12.0 on September 15, 2026) the public is_authorized wraps the same native call, but its Diagnostics accessors substitute empty lists for missing errors and reason keys, which would turn an incomplete engine response into an apparent clean allow; no public entry point exposes the raw response, so the private call stays. tests/test_cedar.py asserts that the private entry points exist and return the expected shapes, so a binding upgrade that changes them fails with an explicit review message instead of silently altering decisions. The move from 4.12.0 to 4.12.1 was reviewed on October 9, 2026, as R6 requires. It changes no Python source and keeps the Cedar 4.12.0 crates; its one engine change is the text of the error a request Cedar cannot build carries, which now states the cause, sometimes quoting the request's context, in place of a fixed phrase. The response keeps its shape, a NoDecision with one error and no reason, so the adapter denies it as before, and no diagnostic text leaves the adapter, whatever it quotes. Binding documentation, Cedar error semantics
Only authenticated identity and validated resolved/derived facts enter Cedar entities. Principal and resource identifiers include tenant/account scope; caller claims never become entity attributes. The request context is empty for an action that declares no arguments; for an action that declares them, evaluation rechecks the frozen arguments against the action spec and sends them as the single record context.arguments, denying with INVALID_ARGUMENTS when they no longer match. Arguments are caller intent, so they stay in the context and never become resource attributes. Missing or inconsistent provenance, writers outside the bundle's effective allowlist, invalid derivations, stale mappings and expired evidence deny. Evidence expiry is checked again after policy evaluation. Writer allowlists should name only trusted writer labels. Admitting a writer label that an agent can write (the tests use agent) removes the scenario T protection against agent-written context.
Decision composition
Any policy error, missing base authorization, applicable forbid, invalid input/dependency or exceeded DENY limit yields DENY. Policy and limit obligations are considered only when no denial exists. Engine policy IDs are namespaced with policy: and stable rule IDs with limit:; an obligation is meaningful only within its tenant, principal and bundle/plan binding.
UsageSnapshot is an immutable internal input scoped to tenant, principal, bundle, the frozen intent's account (account_id) and the memberships the coordinator read inside the fence for this decision (groups, at most 64, sorted and unique). It must contain exactly one count for every rule of the bundle, and a snapshot whose tenant, principal, bundle or account differs from the frozen plan is INVALID_USAGE. A rule applies to a candidate through LimitRule.applies_to: the frozen action must be in the rule's action list, and a group rule needs the group among the snapshot's memberships, an account rule the frozen intent's account, and a selector rule the selected fact in the frozen plan with exactly the selected value and type (1, true and "1" are three different values); a rule that does not apply neither charges nor waives anything, exactly like a rule for another action. Each projection adds confirmed targets, unresolved reservations and the proposed target's applicable contribution. A scoped internal VerifiedApproval exempts the candidate from autonomous impact while preserving total impact. Exceeded REQUIRE_APPROVAL rules add obligations before satisfaction; exceeded applicable DENY limits still deny. Rule order does not alter the effect.
Required execution integration
The pure evaluator does not query active configuration, refresh credentials, reserve capacity or establish that supplied usage is current. The execution coordinator authenticates, resolves trusted evidence, loads the active bundle and derives usage from retained impact, in each rule's scope, while holding the tenant fence. It rechecks authority before commitment.
The coordinator validates and consumes scoped server-side approval, acquires the resource/idempotency claims and persists dispatch intent. There is no caller-supplied approval boolean. Activating a bundle changes the plan binding; old approval cannot authorize a new active bundle. History and uncertainty survive rule changes.
The decision is the same for an externally executed operation: the same resolution freezes the same plan, the same evaluation composes the same Cedar sets and limit rules, DENY still wins, and an unsatisfied obligation returns the same approval flow with its window, quorum and continuation. A denied checkpoint grants nothing, reserves nothing and claims nothing. What differs is only what follows an ELIGIBLE decision, which External execution checkpoint describes; an issued grant is not recalled by a later bundle edit, and the report route reads the bundle the execution was frozen under rather than the active one.
Policy tests cover the decision portions of acceptance N/R; coordinator tests add approval consumption, live concurrent budgets and precommit configuration rechecks. Separate provider, surface, identity and replay tests now cover the controlled mock system. The acceptance inventory maps each behavior to the tests that show it holds.
Verification
Policy behavior is covered by the Cedar adapter, model, decision and store tests, and by the acceptance scenarios for decisions, approvals and limits in the acceptance inventory. Migrations are tested in both directions. Destructive regression tests run only against a local test database.