Skip to content

Control Policy Reference

CONTROL_POLICY_PATH names one YAML or JSON file with up to six sections (rules, approvals, principals, grounding, effects, scopes). All are optional. To adopt it step by step, see Turning On Control; for what each part guarantees, Control and Evidence.

Fail closed. A file that cannot be read refuses every provider call and every effect, and requires a verified approver for every action. A rule that cannot be read refuses what it matches (or everything, when its match cannot be read). jarviscore policy check names each such problem before you deploy.

The file is read again when it changes on disk; no restart is needed.


rules: provider calls

Refuse more provider calls, or lift mutation-without-atom for your own code.

rules:
  - name: no-deletes
    decision: deny
    match: {method: DELETE}
  - name: our-crm-client
    decision: allow
    match: {provider: hubspot, code_origin: direct}
Key Values
name Recorded with every decision the rule makes
decision deny, or allow (lifts only mutation-without-atom); anything else is treated as deny
match.provider / system Provider name (system is the same)
match.atom Integration name
match.effect read, propose, write, notify, destructive
match.method HTTP method
match.code_origin runtime, shipped, registry, model, direct
match.verified Whether the call is one of the operations the integration's code verifiably sends

A list matches any of its values. All keys must match.

approvals and principals: who decides

approvals:
  - name: payments-need-finance
    match: {provider: erp, effect: [write, destructive]}
    roles: [finance]
    distinct_from: [requester]
    assurance: verified
  - name: drafts-need-nobody
    match: {atom: gmail_create_draft}
    require: none
principals:
  carol@example.com: [finance]
Key Values
match provider/system, atom, effect. The first matching rule decides
require approval (default) or none. none never removes approval from a destructive action
roles The approver must hold one (from their assertion, or principals)
distinct_from [requester]: the person the run acts for may not approve it
assurance Least identity strength: unauthenticated, asserted (an Approver from your code), verified (an assertion signed with APPROVAL_ASSERTION_KEY)
principals Roles held per approver, when their sign-in does not carry them

Without a matching rule, the integration's own ATOM_POLICY decides. See HITL: Who may approve.

grounding: values from the system of record

Request fields that must come from records the runtime read itself.

grounding:
  - name: pay-the-supplier-on-record
    match: {provider: erp, method: POST, path: /payments}
    bind:
      - name: invoice
        read: {provider: erp, path: "/invoices/{id}"}
        where: {id: request.json.invoice}
      - name: supplier
        read: {provider: erp, path: "/suppliers/{id}"}
        where: {id: invoice.supplier_id}
    require:
      - {field: request.json.iban, equals: supplier.iban, as: iban}
      - {field: request.json.amount, at_most: invoice.amount_due, as: money}
    selectors_from: [input, request]
    other_fields: deny
    fresh: run
    max_age_seconds: 900
Key Values
match provider/system, method, atom, host, path (a template such as /payments/{id})
bind[] Records to follow, in order. name; read.provider, read.path (template); read.record (dot path to the record in the response), or read.items + read.key for a list; where: path variables or record fields, each selected by an expression
Expressions request.json.*, request.form.*, request.query.*, request.path.*, input.* (the caller's execute_goal(context=...)), or an earlier binding (invoice.supplier_id). [*] expands a list
require[] field (a request.* path), one of equals, one_of, at_most, and as
as exact (default), id, text (NFC, exact), iban (ISO 13616 with check digits), money and number (decimal), email (IDNA domain)
selectors_from Where where may take selectors from: input, request. [input] pins which record to the caller
other_fields deny: the request may send no top-level field no requirement or selector names
fresh run (default): any read in this workflow; reread: read each record again just before the call
max_age_seconds Ignore reads older than this

effects: everything else

effects:
  - name: shop-api
    decision: allow
    match: {sink: browser, target: "api.shop.example/*"}
  - name: no-broadcast
    decision: deny
    match: {sink: peer, action: broadcast}
Key Values
decision deny, or allow (lifts only unconfined-execution and browser-send-off-site)
match.sink code, process, artifact, workspace, memory, peer, output, browser
match.action run; publish; export, write, append; send, delegate, broadcast; deliver; navigate
match.origin runtime, shipped, registry, model, page
match.agent Agent id
match.target Glob over the target: a host and path (browser), artifact key, recipient, command, memory kind, step id
match.confined true or false: whether code ran in a confined child

scopes: what each person may do in your organisation

scopes:
  require:
    - name: claim-approval
      match: {provider: guidewire, method: POST, path: "/claims/{id}/approve"}
      requires: claims.approve
      limits:
        - {field: json.amount, kind: money, at_most: amount}
    - name: claim-payment
      match: {provider: guidewire, method: POST, path: "/claims/{id}/payments"}
      requires: claims.pay
      approved_by: claims.approve
      limits:
        - {field: json.amount, kind: money, at_most: amount}
    - name: finance-portal
      match: {sink: browser, target: "finance.acme.com/*"}
      requires: finance.reports.read
  grants:
    acme/carol: {claims.approve: {amount: "10000"}, claims.vet: {}}
  unattended: {}
Key Meaning
require[].match A provider call (provider/system, method, host, path with {variables}, atom, effect) or another effect (sink, action, target glob, origin, agent), not both. Match the operation (method and path) to hold any code reaching it; atom alone holds only that integration
require[].requires The scope the person the work is for must hold
require[].approved_by The scope the approver must hold (provider calls only). The action then always needs an approval from an asserted or verified approver, whose scopes come from their assertion, Approver(scopes=...) or grants. Only an integration's action can wait for an approval: other code reaching the operation carries none and is refused
require[].limits Each {field, kind, at_most\|one_of\|equals: <term>}: the request field (json.*, query.*, form.*, path.<variable>; effects: attributes.*, target) compared as kind (money, number, email, iban, id, text, exact) with the holder's grant term. at_most takes money or number. email compares the domain case-insensitively and the local part exactly
grants Scopes per person key, for people whose sign-in does not carry them. Your application's OnBehalfOf(..., scopes=...) comes first for every scope it names
unattended Scopes work done for no one holds

A grant is {scope: {term: value}} or a list of scope names. A term the requirement checks and the grant does not state is refused; "*" states no limit. Every requirement a call matches must hold. A value the request does not carry, or carries in a body that cannot be read, is refused; every item of a list is checked. A requirement that cannot be read refuses what it matches (everything, when its match cannot be read); a scopes section that cannot be read refuses everything. jarviscore policy check names each problem; jarviscore policy scopes prints who may do what.

Browser requests are intercepted, in report mode too, wherever a sink: browser requirement exists, so a refusal stops them. A page's own reads (fetch/XHR GET) are not decided on; its navigations and sends are.

Mandate (workflow context, not this file)

What a person delegated travels with the work, registered by your application: execute_goal(..., on_behalf_of=OnBehalfOf(person, mandate={...}, scopes={...})) (or the same on_behalf_of on run_task, workflow, submit_goal, create_chat_router).

Key Values
systems Providers the work may call
effects Any of read, propose, write, notify, destructive
scopes Of the person's own scopes, the ones this work may use (narrows, never widens)
expires_at ISO 8601 time with a zone, or epoch seconds

A mandate holds provider calls (for browser pages and other effects, use scopes). Unknown keys or malformed values make the mandate unreadable, which refuses every provider call. See Acting for a person.

Built-in refusals

Made in report and strict mode alike; enforced only in strict, except what a person may do (outside-mandate, scope-missing, over-scope-limit), which is enforced in report mode too: your application said what the person may do.

Rule Refuses Liftable
outside-mandate A provider call outside what the person delegated (system, effect, expiry, scopes) No
scope-missing A call or effect needing a scope the person (or, for approved_by, the approver) does not hold, or one a scopes section that cannot be read may cover No
over-scope-limit A request beyond the limit of the holder's scope, or one whose limit cannot be checked No
approval-missing A gated action reaching the broker without its approval No
ungrounded-argument A call a grounding rule matches whose values are not grounded No
mutation-without-atom A provider mutation not from an integration rules: allow
beyond-declared-effect A method beyond the integration's declared effect. For code not shipped with JarvisCore (registry, model, direct), a call is never taken as less than its method: a "read" that POSTs is a write No
outside-contract Model-written code sending an operation its code does not contain No
unverified-model-code Model-written code mutating where its operations cannot be read No
unapproved-model-effect Model-written code mutating without an approval of that exact action No
unconfined-execution Model or registry code, or a model-chosen host command, outside a confined sandbox effects: allow
browser-send-off-site A page sending data to a host other than its own effects: allow

The journal

Each workflow step's run is a file of hash-linked events under JOURNAL_DIR/runs/, with every run's latest event registered in JOURNAL_DIR/heads.jsonl.

Kind Holds
policy.decision One decision: the call or effect, outcome, rule, enforced, acting_for, the reason for a refusal, for grounded calls the checks and the reads cited, and for scoped calls the requirements, each holder, whether they held the scope and each limit's outcome (scope; never the values)
policy.preview A decision on a previewed action, before anyone was asked
effect.attempt / effect.outcome That it was attempted, and what became of it
approval.request / decision / refused / claim / settle / reopened / reconciled An approval's life

Records hold methods, hosts, paths, sizes and digests, never bodies, query strings or credentials.

Python API

Name Use
jarviscore.control.policy.check_policy(path) (problems, summary) for a policy file, as jarviscore policy check
jarviscore.control.policy.review_journal(root) Refusals grouped by rule and target, as jarviscore policy review
jarviscore.control.effects.mediated(Effect(...)) Decide on and record your own effect (guide)
jarviscore.evidence.journal.verify(root) The checks jarviscore verify runs
jarviscore.control.approvals.sign_assertion(..., scopes=...) Sign an approval in your backend, for verified assurance, with the approver's scopes
jarviscore.OnBehalfOf(person, mandate=..., scopes=...) Whom work is for, what they delegated, what they may do
jarviscore.control.scopes.parse_section(raw) The scopes section as the deployment reads it
jarviscore.act_for(person, delegated=None, held=None) Run a block of your own code for a person, within their mandate and scopes
jarviscore.control.approvals.Approver(subject, roles, assurance, authenticated_by, scopes) An approver your code established (asserted), for resolve_hitl(approver=...)
Mesh.connect(provider, on_behalf_of=..., return_url=...) / Mesh.connected(...) Start, or check, a person's consent to a provider
jarviscore.evidence.people.account(root, who) / export(root, who, dest, content=False) / erase(root, who, dry_run=False) What was done for a person or tenant; an export for their auditor; erase their replay content

Settings: CONTROL_MODE, CONTROL_POLICY_PATH, JOURNAL_DIR, JOURNAL_CAPTURE, JOURNAL_CONTENT_DAYS, APPROVAL_ASSERTION_KEY(S) (Configuration).