SARC Conformance — what yupana × quipu still needs
Status: Phases 1–6 landed. Constraint metadata
and placement; the Σ-derived trace record and signed verdict emission; a real
Post-Action Auditor with throttle; an Escalation Router with a bounded
reversibility window; the T ⊨ Σ checker, the dispatch-graph inventory and the
replay harness; authority intersection over named graphs and the attribution
tuple.
Read the "as built" sections, not this line. Each ends with what its phase
did not close, and four of those matter: there is no Action-Time Monitor at
all (G6, untouched); the escalation queue has no server, so §5.3's
W_q < τ_rev is unmeasured; θ is calibratable but not calibrated, because replay
counts blocks and cannot label false positives; and no trust predicate evaluates
imported content — the boundary is declared and reported, not closed. See Build
order for the phases as originally scoped.
Why this document
Besanson [SARC] proposes that constraints become a first-class specification object alongside state, action space and reward (§3.1):
c = ⟨src, class, pred, verif, resp⟩ + a declared operating point θ
compiled into four named enforcement points in the agent loop (§4.1) — a
Pre-Action Gate (PAG), an Action-Time Monitor (ATM), a Post-Action
Auditor (PAA), and an Escalation Router (ER) — under eight runtime
invariants I1–I8 (§3.5) whose joint effect is specification-trace
correspondence (Definition 2, §3.6): given a specification Σ and a trace T, an
auditor can mechanically decide T ⊨ Σ in O(|T|·|C|) without access to the
model, its prompts, or its developers.
The stack is already most of the way there, and on the specification side it
is ahead of the paper's own prototype: quipu's aegis: governance ontology is
SHACL-validated and bitemporal, and verdicts are ed25519-signed against a
human-owned VerifierRegistration root of trust — SARC's reference artifact is
a JSON spec file and a Python checker (§3.6, §13.4).
What is missing is not the substrate. It is a specific, enumerable set of holes: the constraint objects are under-declared, three of the four enforcement points are absent or advisory-only, the trace is not derived from the specification, and nothing checks correspondence. This document names them and orders the work.
Where the stack stands
Already in place
| SARC concept | Stack today | Where |
|---|---|---|
| Constraint specification object | aegis:Policy + aegis:Selector / aegis:Predicate atoms | quipu/shapes/governance.ttl |
pred | aegis:claim (SPARQL ASK), or selector .scm + predicate regex | quipu/shapes/policies/treesitter.ttl |
| Pre-Action Gate | yupana hook pre-edit, Mode::{Off,Advise,Enforce} | src/hook/pre_edit.rs, src/policy.rs |
| Policy-layer reference monitor | quipu pre-commit write gate | quipu/src/governance/guard.rs |
| Verdict as attestation, not claim | ed25519-signed, evidence-hash-bound aegis:Verdict | src/verdict.rs, quipu/src/signing.rs |
| Root of trust | aegis:VerifierRegistration, human-authored | quipu/shapes/governance.ttl |
| Latency budget ([SARC] §5.1) | policy.deadline_ms, fail-open on expiry | src/policy.rs |
| One-directional policy projection | quipu canonical → yupana read cache | src/project.rs, src/hook/rule_planes.rs |
| Confidence inputs | tier ∈ {live,lsp,tree-sitter,committed,attested} + freshness | shapes, FR-3 |
| Layer discipline (SARC I6) | no rule lives in the prompt; declared, not yet verified against reality | Governance Plane |
| Post-Action Auditor | yupana hook post-edit, soft class + throttle | src/hook/paa.rs, src/throttle.rs |
| Constraint class + placement | aegis:constraintClass / verificationPoint, checked at write | quipu/src/governance/placement.rs |
| Σ-derived trace record | constraints[] with outcome, response and placement | src/trace.rs |
| Signed verdicts, both sides | yupana spools at the gate and the PAA; quipu persists the write gate's | src/verdict_spool.rs, quipu/src/governance/verdict_facts.rs |
Governance Plane independently anticipates much of SARC —
risk × confidence adaptive effect, verdict integrity, the out-of-band verifier,
the prevented/observed enforcement gradient. SARC's marginal contribution on
top of it is placement discipline ([SARC] §4.2, Table 3: which constraint
class belongs at which enforcement point) and checkable correspondence
([SARC] §3.6: a decidable audit). SARC is explicit that this is a specification
discipline layered over a policy-as-code substrate rather than a replacement
for one (§2.1) — which is exactly the relationship quipu's write gate already
has to yupana's projection.
The gaps
G1 — Constraint objects are incomplete. Violates I2 ([SARC] §3.5: "a constraint missing any field is not a constraint; it is a comment").
aegis:Policy carries targets, claim, boundary ∈ {action,transition} and
effect ∈ {allow,warn,require-approval,deny,escalate,record}. It does not carry:
class ∈ {hard, soft, escalation}.effectconflates class with response, so "what kind of constraint is this" is not declarable and the class→placement rules ([SARC] Table 3) cannot be checked.- an operating point θ — no false-positive / false-negative tolerance.
- a reversibility window τ_rev and on-timeout behaviour (needed by I4).
- a per-constraint latency budget at its verification point.
- a hosting layer (
orchestration|tool|policy), needed to check I6. src.type—aegis:Directivesupplies optionalauthority/issuedBy, and the shipped catalog sets neither.
G2 — The verdict path is built but not wired. Violates I3 and I8 ([SARC] §3.5). Closed in Phase 2, both halves.
src/verdict.rs implemented signing and promote_verdict correctly — mirroring
quipu's scheme so a yupana-signed verdict verifies under quipu's root of trust —
and had no caller outside yupana verdict-key. A pre-edit guard decision, the
exact moment a constraint fires, never became a governed fact. Symmetrically,
quipu's own write-gate decision was not persisted (Q-VERDICT-PERSIST).
Both now record. yupana signs at the gate and at the PAA and spools locally,
drained by yupana verdicts; quipu stages its write-gate verdicts and flushes
them after the savepoint resolves, so a denial's verdict survives the rollback
that denial caused.
G3 — The trace is not derived from Σ. Violates I3 ([SARC] §3.5: "the trace is generated; it is not reconstructed"), and I8 by consequence. Closed for the constraint set in Phase 2; the attribution half stays open under G7. See Structured constraint records.
src/metrics.rs emitted {kind, ts, agent, tenant, item, …} per event. There
was no pre/post state, no constraints_evaluated set with outcomes, no
attribution tuple. docs/work-scoped-governance.md names this precisely — "records and
rules share one vocabulary" — and phases it. Its phase 1 is SARC's I3. It is
designed, not built.
G4 — The Post-Action Auditor is advisory context, not a constraint site. Closed in Phase 3.
src/hook/post_edit.rs injects blast-radius context after an edit. It evaluates
no constraint, emits no verdict, and cannot prevent the next action — which is
what a PAA is for ([SARC] §4.1). SARC's soft class C_s has nowhere to live,
and throttle — the declared PAA response responsible for the paper's entire
89.5% soft-overage reduction ([SARC] §10.3, Table 6) — is not in quipu's
effect enum.
G5 — There is no Escalation Router. Violates I4 ([SARC] §3.5, §5.3).
require-approval and escalate currently fail closed at the quipu write gate
with no channel to grant approval (guard.rs::effect_blocks, and the design
doc says so plainly). aegis:Decision and aegis:assignsWorkflow shapes exist;
there is no runtime router, no operator group, no queue, no τ_rev, no
default-deny-on-timeout, no capacity model. [SARC] I4: "escalation without a
bound is not human oversight; it is deferred autonomy." The queueing model that
makes W_q < τ_rev measurable rather than asserted is §5.3.
G6 — There is no Action-Time Monitor.
Nothing observes an action mid-flight. Long-running Bash, MCP tool calls and
sub-agent runs have no cumulative-budget monitor and no interrupt.
src/hook/pre_bash.rs is deliberately record-only and prints nothing.
G7 — No attribution tuple, no authority intersection. Violates I5 ([SARC] §9.3, §9.6).
SARC's α = ⟨P, planner, executor, tool, auth, C_eval⟩ has no counterpart. The
spool carries one flat agent + tenant + item. There is no
principal-and-agent chain, no authority composition (all-of / any-of), no
monotonic narrowing under delegation — and the trace is a sequence, not a
tree, so orchestrator/worker runs are exposed to exactly the
constraint-laundering and attribution-dilution failure modes of [SARC] §9.5.
quipu's group_id is documented as provenance-only, and
Governance Plane scopes v1 to a single trust domain. This is the deepest gap and the one with a
real prerequisite.
Correction. The "real prerequisite" was described as quipu multi-tenancy, and that was wrong: named graphs are already a storage-enforced isolation substrate, and the actual gap was authorization over them. Mostly closed — see Phase 6, as built for what landed and what is still open (the trace is still a sequence, not a tree).
G8 — Enforcement completeness is unmeasured. Violates I7 ([SARC] §3.5).
docs/work-scoped-governance.md §"What this cannot reach" is an honest,
explicit list of bypass surfaces: CI pipelines, cron, the far side of a remote
shell, a sibling session's VCS index, a hostile agent. I7 is a property of the
dispatch graph, verified by inspection against Σ — and there is no inventory
of governed vs ungoverned tool-call classes, so "which actions traverse an
enforcement point" is not answerable mechanically.
G9 — The aegis: vocabulary is not described, only constrained.
Found while building Phase 1, not in the original analysis. shapes/*.ttl
contained zero rdfs:domain, rdfs:range, rdf:Property or owl:*Property
declarations: every aegis: term was defined implicitly, by the SHACL shapes
that constrain it. A shape says what is valid; it does not say what a term
means or what it relates. So a reader could learn that constraintClass is
one of three strings and could not learn how it differs from effect.
That matters for two consumers the stack already plans on. The authoring surface
in Governance Plane §Authoring is composition over a
catalog — an agent drafting a policy has nothing to read. And aligning aegis:
against an external governance vocabulary (the ontology-matching problem
[Agent-OM] addresses) needs property descriptions, which is precisely what was
missing.
It also starved machinery already built: src/owl.rs parses rdfs:domain /
rdfs:range / rdfs:subPropertyOf, and the reasoner materialises domain/range
inference — fed nothing. Partly closed in Phase 1 by
shapes/aegis-properties.ttl for the SARC fields; the rest of the vocabulary is
Q-SARC-VOCAB.
G10 — No audit checker, no replay, no calibration.
Nothing computes T ⊨ Σ. There is no spool reader, no replay harness, no
measured false-positive rate — so the advise→enforce promotion ladder in
work-scoped-governance.md §Evals cannot be walked, and θ (G1) would be
undeclarable-in-practice even once the field exists.
Position on the adoption ladder ([SARC] §13.3)
As first written, and where each rung now stands:
- Level 1 (PAG, hard constraints at the tool/policy layer, structured trace emission) — was "substantially met; I3 and I8 outstanding". I3 met by the Σ-derived trace record; I8 waits on the checker (Phase 5).
- Level 2 (PAA, soft constraints with calibrated operating points) — was "not
started". PAA and
throttlebuilt, and the replay harness now measures the promotion gates. But θ is calibratable rather than calibrated: replay counts blocks and cannot label false positives, and bounds no false negatives at all. A declared θ that no measurement backs is a number, not a calibration, and the rung should not be claimed on one. - Level 3 (ATM, ER with declared τ_rev) — ER built with a declared window
and default-deny past it. No ATM: nothing observes an action mid-flight
(G6), and the escalation queue has no server, so §5.3's
W_q < τ_revis unmeasured. This rung is half met. - Level 4 (multi-agent) — was "blocked on quipu multi-tenancy", which misdescribed the blocker. Authority intersection, the attribution tuple, the reconstructed dispatch tree, constraint inheritance with a decidability rescue, and trust-boundary declarations are all built. Two things keep this rung from being claimed outright: the trace is emitted as a sequence, so sibling dispatches of the same principal are indistinguishable, and nothing evaluates imported content.
Decisions
- Vocabulary: extend
aegis:in place inquipu/shapes/governance.ttlrather than layering a separatesarc:overlay. One vocabulary; the existing SHACL validation and projection decode carry the new fields for free. Cost: the shippedtreesitter.ttlcatalog needs backfilling in the same change. - Escalation Router owner: quipu. The engine of record already models
Decision,assignsWorkflowand the bitemporal audit trail, andrequire-approvalalready fails closed there. Yupana gets a thin client. This matches the settled "the engine lives in Quipu; yupana never originates policy" rule. - MVP scope: Phases 1–3 — SARC-conformant for a single agent in a single trust domain, which is exactly the v1 boundary Governance Plane already declared.
Build order
Phase 1 — Complete the constraint object
Closes G1. Prerequisite for everything else: you cannot place a constraint by class before class exists.
quipu — shapes/governance.ttl:
aegis:constraintClass,sh:in ("hard" "soft" "escalation"), required onboundary "action"policies.aegis:verificationPoint,sh:in ("PAG" "ATM" "PAA" "tool_layer" "policy_layer"). This replaces nothing —boundarystays as the coarse action/transition split;verificationPointis the fine placement SARC needs.tool_layer/policy_layeralready appear in yupana's projected policies.aegis:hostedAtLayer,sh:in ("orchestration" "tool" "policy"). Deliberately no"prompt"value. See Phase 1, as built for why the omission is defence in depth rather than the guarantee it first looked like.aegis:OperatingPointnode shape +aegis:operatingPointonPolicy:falsePositiveTolerance,falseNegativeTolerance,threshold,calibrationBasis.aegis:reversibilityWindowSecondsandaegis:onTimeout— the lattersh:in ("deny"), one value only, and re-checked by value inplacement.rsbecause the shape alone does not bind on every write path. Required on escalation-class policies.aegis:latencyBudgetMsonPolicy."throttle"added to theaegis:effectenum, plusaegis:backoffFormula.aegis:sourceType,sh:in ("regulatory" "contractual" "ethical" "operational"), andaegis:authorityrequired on action-boundary policies.
quipu — src/governance/placement.rs (new): a class↔placement conformance
pass, run at definition time alongside SHACL. hard ⇒ verificationPoint ∈ {PAG, ATM, tool_layer, policy_layer}; soft ⇒ {ATM, PAA}; escalation ⇒ {PAG, PAA}
and must declare τ_rev. This is [SARC] Table 3 made mechanical, and it is what
"placement discipline" means in practice. Backfill shapes/policies/treesitter.ttl
(no-ticket-in-comment → hard/PAG, todo-needs-ticket → soft/PAA).
yupana:
project_queries::POLICY_QUERYgains?constraintClass ?verificationPoint ?latencyBudgetMs ?fpTolerance ?fnTolerance ?reversibilityWindowSecondsas OPTIONALs. Not required — a projection that hard-required a field quipu had not yet backfilled would return zero rows, which is the exact both-sides-shipped-and-the-seam-returned-nothing failureproject_queries.rsalready documents in its own comments.project::ProjectedPolicygains those fields alongsideeffect;decode_policiesreads them through the existingrequired/optionalclosures. An unrecognisedconstraintClassis anError::Projection, matching how an unknownmatchTypeis handled — never a silent drop.rules::Rulegainsclassandverification_point, so a locally-configured[[yupana.policy.rules]]and a projected policy stay one type.hook/rule_planes.rs::governed_checkselects its response by declared class (hard ⇒ block underEnforce; soft ⇒ never block; escalation ⇒ route) rather than byproject::effect_blocksalone.Mode::Advisekeeps its ceiling: it never blocks, whatever the class.
Phase 1, as built
Shipped in quipu (shapes/governance.ttl, src/governance/placement.rs) and
yupana (src/constraint.rs, src/project_decode.rs,
src/hook/rule_planes.rs). Three things came out differently from the plan
above, each because building it surfaced something the analysis had not:
Two fields are refused on the write path, and omitted from the vocabulary as
defence in depth. aegis:hostedAtLayer has no "prompt" value and
aegis:onTimeout has only "deny".
This was first described here as making the unsafe settings unrepresentable,
which was an overstatement worth correcting rather than quietly fixing. A
sh:in enum only binds when SHACL runs, and SHACL runs under
shacl.validate_on_write — which defaults to false and validates episode
ingest rather than Store::transact generally. A policy written through /knot
or a direct transact could carry onTimeout "allow" and nothing would object.
"The shape rejects it" and "the store cannot hold it" are different claims, and
shipping the second while only the first was true is the exact failure this
document catalogues elsewhere.
Both values are therefore re-checked in placement.rs, on the path that is
actually on — before the action-boundary exemption, since a bad onTimeout
fails just as silently on a transition-boundary policy. The vocabulary omission
sits behind that as a second layer, not above it as a guarantee. Even so, the
honest ceiling is "refused on quipu's write path": a raw SQL write, or a
process that opens the store directly, bypasses every check quipu has.
hostedAtLayer was declared but otherwise unconsumed at this point — I6 was
checked for well-formedness and not for truth, so a policy could claim tool
while being enforced only in yupana's orchestration-layer hook. Spec
B is now built; see
H-SARC-I6, as built.
Multi-valued fields are refused, not resolved. Asserting
constraintClass "hard" over an existing "soft" leaves both facts active —
assertion is not replacement. The first implementation read the last SPARQL row
and silently picked one, so a re-class would have landed while the old placement
still validated. This was caught by the write-path test, not by the unit tests
over the rule table, which is the argument for having both. A policy with two
classes is now refused as ambiguous, with the retract-in-the-same-transaction
remedy in the message, and a_clean_re_placement_retracting_the_old_value_lands
is the recoverability half — refusing ambiguity is only safe if there is a way
to legitimately move a policy.
The projection decoder got the same collapse the text decoder already had.
POLICY_QUERY gained three OPTIONALs, and SPARQL returns the cross product of
them: a policy carrying two rdfs:labels comes back as two rows and became two
identical rules. decode_text_rules already carried a comment recording this
exact failure on the live catalogue — 7 entities projecting as 11 rules, 4
duplicates, each reported twice to the model with conflicting rationales.
decode_policies was exposed to it the whole time and the new fields made it
likelier, so it now collapses on the policy IRI and refuses rows that disagree
on a required field. Identity is the IRI, not the label: an unlabelled policy
falls back to a row-indexed name, so keying on the name would give every row a
distinct identity and collapse nothing.
The vocabulary had no property declarations at all. Not a SARC gap — a gap
SARC's fields made visible. shapes/aegis-properties.ttl now declares the SARC
properties with rdfs:domain / rdfs:range / rdfs:comment, and a test checks
the two files against each other so a field added to the shape without a
description fails rather than drifting. Domains are declared one property at a
time, not swept in: rdfs:domain is an inference rule the reasoner
materialises, so declaring it on a generically-named field like aegis:kind
would silently type the first unrelated thing in the estate that used the name.
The OperatingPoint fields deliberately carry range and comment only.
Two behaviour changes worth knowing about when reading a verdict:
- The declared class outranks the governed effect. A
softpolicy never blocks, even witheffect "deny"— that combination is contradictory, quipu's placement check now refuses to define it, and honouring what the author declared it to be is the only reading that is not a guess. A policy with no class (projected from a catalog predating the field) still behaves exactly as before: the effect decides. - A policy declared at the PAA does not fire at the pre-edit gate. It is skipped, not evaluated-and-ignored. Evaluating it there would tell the model to fix something its author scoped to after the fact — and until Phase 3 lands the post-edit auditor, such a policy is not evaluated at all. That is the honest state, and it is visible in the projection rather than hidden.
Mode::Advise remains a ceiling over all of it: an advise-mode deployment never
blocks, whatever class a projected constraint declares. That is what makes
staging a new hard constraint safe before anyone has measured its
false-positive rate.
Two gaps Phase 1 exposed — and the spec to close them
Both were found by building, not by analysis, and neither is a SARC gap as such: SARC assumes a described vocabulary and takes I6 as a design rule rather than a checkable property. They are gaps in this stack's ability to back the claims it makes.
Spec A — Q-SARC-VOCAB: describe the rest of the aegis: vocabulary
The gap. G9. Outside shapes/aegis-properties.ttl (SARC fields only), no
aegis: term carries rdfs:domain, rdfs:range or rdfs:comment. Terms are
defined by the shapes that constrain them, which states validity and not meaning.
What to produce. One declaration per property in the aegis: namespace,
in shapes/aegis-properties.ttl, each carrying:
a owl:DatatypePropertyora owl:ObjectProperty— literal-valued vs IRI-valued. The distinction is load-bearing for the reasoner, not cosmetic.rdfs:label— a human-readable name.rdfs:range— the datatype or class. Inert for the reasoner on datatype properties (it types only IRI-valued objects), so this is for readers and matchers.rdfs:comment— what the term means and how it differs from its nearest neighbour. A comment restating the label is worse than none: it passes the presence check while telling a reader nothing.rdfs:domain— only where the subject class is unambiguous.
The domain rule, stated as a rule because it is a trap. rdfs:domain is an
inference the reasoner materialises: declaring it asserts rdf:type on every
subject carrying the property. A generically-named term (aegis:kind,
aegis:threshold, aegis:name) will eventually be used by something else in
the estate, and a domain would silently retype it. Declare a domain when the
property's name is specific enough that no other class could reasonably carry
it; otherwise range and comment only, and say so in a comment on the omission.
Sequencing. Property-at-a-time, in shape-file order, not one sweep. Each batch runs the reasoner over a store holding the shipped catalog and asserts the inferred types are the intended ones — the materialisation is the risk, so it is what the test has to exercise.
Acceptance. every_sarc_property_the_shape_constrains_is_also_described
generalises from the SARC field list to every aegis: property the shape graph
mentions; adding a constrained property without a declaration fails the build.
A second test asserts no declaration carries a rdfs:comment that is merely its
rdfs:label restated.
As built. governance_plane_properties_are_all_described in
quipu/src/governance_tests.rs does exactly that, over every sh:path in
governance.ttl, with a non-vacuity floor — an extractor that silently found
nothing would otherwise pass over an empty set.
every_description_says_more_than_its_own_label is the second test; seven
declarations were rewritten to pass it honestly rather than by lowering the bar.
Two things came out differently. aegis:gate carries two meanings in this
vocabulary — a Predicate's applicability condition, and the gate that produced an
aegis:Decision — and the declaration records that rather than picking one, with
a test asserting it still does. A reader who meets only one of the two will write
code assuming it is the only one.
And the scope is the governance plane only. The ~100 estate properties in
aegis-ontology.shapes.ttl (hostname, rig, park, plexId) are excluded by
name: their intended subjects are not all obvious from their shapes, and
asserting domains by guess materialises wrong rdf:types rather than merely
documenting badly. materialising_the_declarations_types_the_shipped_catalog_correctly
runs the reasoner over the shipped catalog and asserts no Selector or Predicate
became a Policy — verified by mutation, since the materialisation is the risk and
so is what the test has to exercise.
Why it earns its place. Two consumers this stack has already committed to.
The authoring surface (Governance Plane §Authoring) is
composition over a catalog, and an agent drafting a policy currently has nothing
to read about the terms it is composing. And aligning aegis: against an
external governance vocabulary — the ontology-matching problem
[Agent-OM] addresses, with property descriptions as its primary signal — is not
attemptable against a vocabulary that has none.
Spec B — H-SARC-I6: check the hosting layer against reality
The gap. aegis:hostedAtLayer is declared and unconsumed. Nothing compares
it against where a constraint is actually evaluated, so a policy may claim
"tool" — the layer an agent cannot route around — while being enforced solely
by yupana's orchestration-layer pre-edit hook, which an agent bypasses by writing
the file another way. I6 is currently checked for well-formedness and not for
truth, and a false tool claim is worse than an honest orchestration one
because it stops people looking.
What to produce. A layer-truth check at the projection seam, where both halves are known at once:
- Yupana knows what it is. A rule evaluated by
yupana hook pre-editis hosted at the orchestration layer, always — that is what the hook is. This is a constant inrule_planes.rs, not a configurable. - On projection, compare each policy's declared
hostedAtLayerto the layer that will actually evaluate it. A policy declaringtoolorpolicywhile yupana is its only evaluator is a mismatch. - The response is a loud fail-open, not a block: yupana refusing to project a policy because its metadata overclaims would disable a rule that does still work, trading a documentation error for an enforcement gap. Report the mismatch, project the rule, evaluate it at the layer that is real.
- Record the layer actually used on the verdict and the trace record
(
hosted_atonConstraintEvaluation), so the audit checker of Phase 5 can verify claim against record rather than taking the claim.
The asymmetry that makes it worth doing. Declaring a weaker layer than the
truth is harmless — a tool-enforced constraint described as orchestration
understates its own robustness. Declaring a stronger one is the failure. The
check is therefore one-directional: flag when declared is more robust than
actual, stay silent otherwise.
Acceptance. A projected policy declaring hostedAtLayer "tool" and
evaluated by yupana produces a mismatch notice naming the policy, the claimed
layer and the real one; the rule still evaluates and still blocks if it is hard.
A policy declaring "orchestration" produces silence. The negative case — no
declaration at all — also produces silence, since an absent claim overclaims
nothing.
H-SARC-I6, as built
yupana/src/hosting.rs, quipu/src/governance/audit/passes.rs::hosting. The spec
above held up; three things are worth recording about how it landed.
The check runs twice, on purpose, and they are different checks. Yupana checks
at the projection seam — once per refresh, not per edit, because a metadata
defect repeated on every guard line is a notice people learn to scroll past. That
one compares the catalog's claim against YUPANA_HOSTS_AT, a constant rather than
a configurable: a hook in the agent's loop is the orchestration layer, and
making it settable would let a deployment declare itself into a robustness it
does not have. Quipu checks at audit time, comparing the claim in Σ against
the layer the trace says actually ran the constraint. The second is the one that
cannot be fooled by yupana being wrong about itself.
The record must not echo the claim. ConstraintEvaluation::hosted_at is
stamped from the evaluating code's own constant, never copied from the policy's
aegis:hostedAtLayer. A field that repeated the declaration would let an
overclaim survive the audit by being asserted twice, which is the failure mode
the whole check exists to close.
An unknown layer is a projection error, not a None. The one value quipu's
vocabulary deliberately omits is "prompt", and silently decoding it to "no
claim" would turn the single claim I6 forbids into no claim at all. It fails the
projection the way an unknown constraintClass does. On the audit side the same
value is an incompleteness rather than a violation, because there the checker
may simply be older than the runtime.
Phase 2 — Wire the verdict, derive the trace
Closes G2 and G3 — the two gaps that make I8 impossible.
-
Call
verdict::promote_verdictfrom the pre-edit decision path. Every fired constraint emits one signed, evidence-hash-boundaegis:Verdict. -
Carry the projection's real freshness into
aegis:freshness.rule_verdict_messagealready threads it into the model-facing text;verdict_turtlecurrently hardcodes"fresh", which is precisely the silent-fresh-tag the tier discipline exists to prevent. -
Batch and buffer. Promotion must not sit on the edit's critical path —
deadline_msis 100 ms and a/knotround-trip is not. Spool locally with the existing fail-silent discipline; drain from the resident daemon. -
Persist quipu's write-gate verdict too (
Q-VERDICT-PERSIST), so the policy-layer monitor is as auditable as the orchestration-layer one. -
Restructure the spool record to be derived from Σ:
{ pre_state_ref, action, post_state_ref, constraints_evaluated: [{ id, class, verification_point, outcome, response_taken }], attribution, reward_components }This is
work-scoped-governance.mdphase 1 with [SARC] §3.6'sE_iand §9.6'sα_inamed explicitly. Records and policies then share one vocabulary — the precondition for derive/test/explain in that document and for the checker in Phase 5.
Structured constraint records
src/trace.rs defines ConstraintEvaluation — SARC's E_i element — carrying
the four things the audit checker's passes need per constraint: which one,
where it was evaluated, what it concluded, and what was done about
it. outcome and response are separate fields on purpose: pass (iii) is "does
the recorded response match the one the policy declared", and a single collapsed
field makes it unwritable. Outcome::Unknown stays distinct from Unsatisfied
for the same reason it does in SARC — collapsing them makes an unevaluated check
indistinguishable from a passing one.
Response::NoAction is representable and distinct from Logged. A constraint
that fired and drew no response is a real state — a soft rule under a runtime
with nowhere to put it, which is exactly where the stack sits before Phase 3 —
and rounding it to Logged would read as a deliberate choice.
ProjectedViolation carries each rule's id, class and verification point, and
the spool record names every evaluated rule. The rule field is derived from
the constraint set for dashboards that group on it.
Record composition is tested through guard_recorded, which decides and
composes the record; guard emits it and returns. The spool emitter is covered
separately by the metrics tests. This keeps test setup within the crate's
unsafe_code = "deny" policy.
Verdict emission (G2) closed. src/verdict_spool.rs signs one verdict per
evaluated constraint at the moment the constraint fires, and appends it locally;
yupana verdicts promotes the spool to quipu. It is a spool and not a direct
call because the guard runs inside PreToolUse under a deadline_ms that
defaults to 100 ms, and a /knot round-trip is not that — the projection path
already records what an unbounded quipu call does here (a wedged quipu held the
guard for two minutes). Promotion on the edit path would make every agent's
edit latency a function of quipu's availability, to record a fact nobody needs
until an audit. Signing is microseconds; the verdict is durable at the moment
of decision and stays bound to the evidence hash it was signed over, so the
delay costs nothing.
Four judgement calls in it worth naming:
- The guard never mints a signing key.
verdict::load_or_generatecreates one when absent, and a keypair materialising as a side effect of an agent's edit should not happen quietly. On the hook path only an existing key signs;yupana verifieris the deliberate act that creates one. - The verdict records what the PREDICATE concluded, not what the guard did. A constraint can be unsatisfied while the mode declined to block. Conflating them would make an advise-mode fleet indistinguishable from a compliant one in the governed record; the response lives in the trace record beside it.
unknownspools nothing. An unknown verdict asserts "there was no evidence", and a constraint yupana evaluated had evidence by construction. Minting satisfied or unsatisfied for it would be a signed claim about something that concluded neither.- A rejected verdict is retained, never dropped. The drain truncates only when every line was accepted. A rejection is a fact about the verdict — a shape violation, an unregistered verifier — and truncating past it would erase exactly the record worth investigating.
Freshness stopped being a lie in two places. verdict_turtle hardcoded
aegis:freshness "fresh", so every verdict yupana could have promoted would have
claimed currency it never checked. It now takes the real value, and Decision
carries the currency of the policy set that produced it — a parameter rather
than a defaulted field, because a default would be Fresh and every caller who
forgot it would rebuild the same defect. The trace record carries it as
policy_freshness, which is what stops a soak window counting verdicts computed
against a stale projection as evidence about current policy.
Recomputing maps to stale on the verdict and stays itself on the trace
record: aegis:freshness admits only fresh/stale and the conservative reading is
the only one that cannot overstate, while a trace record is diagnostic and the
distinction is real. Two audiences, two mappings, each stated where it applies.
The attribution tuple, since closed. Phase 2 shipped α partial: yupana
supplied tool, executor and C_eval, and left the principal chain P and
the intersected authority auth absent rather than filled with the single agent
id — a one-element chain asserted where a real chain belongs reads as "this
action had one principal" to exactly the auditor the field exists for. The
stated blocker was multi-tenancy quipu did not have. That reading was wrong
about quipu and is now wrong about yupana too; see Phase 6
for what named graphs already provided, what authority intersection added, and
src/attribution.rs for the five elements yupana now records. auth is still not
one of them, and that is deliberate rather than pending: it is the intersection
of grants quipu owns, so yupana records P and the checker derives auth from the
authoritative source.
Phase 3 — Make the PAA a real enforcement point
Closes G4.
Give hook/post_edit.rs a constraint-evaluation path alongside its advisory
context: evaluate verificationPoint "PAA" policies against
(pre, post, action, obs), emit verdicts, and implement the throttle
response — a declared backoff applied to subsequent actions once a soft window
is crossed. This is the single highest-leverage mechanism in the [SARC]
evaluation
and the stack has no equivalent.
Soft constraints stay non-blocking by construction. The PAA's "prevents the
next, not the just-completed" semantics must be explicit in the module doc,
because presenting it otherwise is the false-prevented claim the enforcement
gradient exists to stop.
Phase 3, as built
src/hook/paa.rs evaluates the constraints that DECLARE
verificationPoint "PAA" against the completed file, records them in the same
trace vocabulary the gate uses, and spools their verdicts.
The two points partition the rule set; they do not overlap. A rule declaring
PAA is skipped at the gate and judged here; one declaring PAG, or declaring
nothing at all, is judged at the gate exactly as before the field existed. So a
constraint is evaluated once and its verdict says where — auditing at both would
record two verdicts for one action and make the coverage pass ambiguous.
A satisfied constraint is recorded, not skipped. "The rule ran and held" and "the rule never ran" are different facts, and an absent evaluation reads as a constraint nobody applied.
The auditor never blocks, under any mode. Enforce and Advise behave
identically here; only Off disarms it. That is not an oversight — it is what
makes the soft class mean something, and quipu's placement check already refuses
to let a hard constraint declare itself at this point.
throttle, and what it honestly is. A declared backoffFormula records an
expiring, repo-scoped backoff (src/throttle.rs) that the next edit's advisory
surfaces. It is observed, not prevented: an agent that ignores it proceeds.
There is no timer and no sleeping — a throttle is state with an expiry, read by
whatever hook runs next — so it is deliberately not described as rate limiting,
which it cannot enforce. What it does buy is that a crossing is bounded,
recorded, and visible to the next action instead of dissolving into a log line.
Three judgement calls worth naming:
- An unparsed backoff formula applies no throttle and says so, recording
Response::NoAction. A default would be a backoff nobody declared, under a constraint whose entire point is that its cost was declared — and silently warning instead would report the crossing as handled. - Throttles are scoped to the repo root. State is one file per user, not per checkout. The first implementation was unscoped, and the test suite caught it the way it deserved to be caught: tests passed alone and failed together, because a throttle written by one leaked into another. In production the same bug means a window crossed in one repo advises an agent editing an unrelated one — an advisory that is true about something and false about the work in front of it, which is how agents learn to ignore advisories.
- An unscoped record matches nothing rather than everything. The permissive reading of a legacy line would advise every repo on the host.
The audit runs independently of the blast-radius advisory. advisory_for
bails early on a non-Rust file, a file with no symbols, or an edit landing
outside every symbol body. The auditor must not inherit those exits, or a
constraint's coverage would depend on a property of the file with nothing to do
with the rule.
Phase 4 — Escalation Router (quipu, yupana client)
Closes G5. Turns require-approval from fail-closed-with-no-channel into
bounded human oversight.
aegis:OperatorGroupshape: capacity model (M/M/c—c,mean_service_s), hours, after-hours mode,fallback_if_unavailable "deny".- Router service in quipu: accept a suspended action, dispatch a
DecisionRequestto the group's queue, hold until τ_rev, default-deny on timeout. A ruling that modifies the action must re-enter the gate (SARC Algorithm 1'sgoto PagCheck) — re-validation, not trust. - Decisions stay content-bound to the evidence hash (already the shape's contract), so approve-then-change goes stale automatically.
- Yupana gets a thin ER client at the pre-edit seam. When the router is unreachable the escalation-class response is deny, and the fail-open notice says so loudly.
- Emit queue-depth / wait / utilisation metrics, because the operative claim of
[SARC] §5.3 is that
W_q < τ_revis a measurable property of an M/M/c queue, not an assertion.
Phase 4, as built
quipu/src/governance/router.rs, plus aegis:OperatorGroupShape and
aegis:DecisionRequestShape. Three things came out differently from the sketch
above, each because the sketch described something that could not be true.
The router does not hold the transaction open. A write gate is synchronous;
"hold until τ_rev" would convert an approval gate into a lock on the store, and
a store you cannot write to while a human is at lunch is an outage wearing
governance's clothes. What actually happens: the refused attempt mints a
DecisionRequest naming the policy, the target, the group that can rule, an
evidence hash and an expiresAt derived from the constraint's reversibility
window; a human signs an aegis:Decision bound to the same hash; the next
attempt finds it and proceeds. The hold is the agent retrying, not the engine
waiting. §5.3's window still governs — past expiresAt an unserviced request is
Expired, which is a denial, and Ruling::permits returns true for
Approved alone so neither Pending nor Expired can read as a pass.
A rejection outranks an approval. When both are bound to the same evidence, two humans have disagreed, and resolving that by row order would make the outcome depend on storage layout. The safe reading of a disagreement about whether to permit something is no.
The request is staged, not written in place. The gate runs inside the
savepoint the refusal is about to roll back, so a request written there would
vanish with it — leaving an operator a refusal and no request to act on, exactly
the state the router exists to end. Requests stage on the Store and flush after
the savepoint resolves either way, the same mechanism verdicts use rather than
a second one invented for decisions.
What it is not: there is no scheduler and no notification. routedTo records
which group should rule; delivering the request to them is a consumer of the
record. The queue-depth and utilisation metrics of §5.3 need a queue with a
server attached, and claiming W_q < τ_rev from a store that only records
requests would be the dashboard anti-pattern with extra steps. That measurement
stays open, and it is named here rather than quietly dropped.
Phase 5 — Audit checker and enforcement inventory
Closes G8 and G10 — the "auditable by construction" claim itself.
quipu_audit_check(Σ, T): four passes per [SARC] Definition 2 (§3.6) — coverage, class-placement compatibility, outcome consistency, attribution completeness — returning a structured discrepancy report. Deterministic, predicate-language-agnostic, and explicitly not an LLM call: [SARC] §5.1's design rule, which is the sameO(ℓ_tool)budget discipline yupana already applies to its own guard.- A dispatch-graph inventory for I7: enumerate every tool-call class the harness
exposes, mark governed/ungoverned, fail the check when an executable class
traverses no compatible enforcement point. Seed it from
work-scoped-governance.md§"What this cannot reach", so the known-unreachable surfaces become data rather than prose. - The replay/eval harness from
work-scoped-governance.md§Evals — liveness, both-outcomes, non-vacuity, recoverability, replay. This is what makes the Phase-1 operating point θ an honest number rather than a declared one, and it gates advise→enforce promotion per rule.
All three built; see Phase 5, as built, including what replay measures and what it provably cannot.
Phase 5, as built
quipu/src/governance/audit.rs (+ audit/passes.rs, audit_spec.rs),
inventory.rs, replay.rs, and quipu audit <trace>|inventory|replay <trace>.
All three pieces landed. What is worth recording is where each one stops.
The checker
Four passes, each a comparison between two declared values and never a model call. Coverage, class-placement compatibility, outcome consistency, attribution completeness — plus the I6 claim-versus-record check described above.
Two severities, kept apart deliberately. A violation is the trace
contradicting Σ: a soft constraint that blocked, a declared deny that only
warned under enforce, a record whose declared chain disagrees with the process
that ran. An incompleteness is the trace not saying enough to decide: no
principal chain, no declared class, a constraint Σ declares that this window
never exercised. Collapsing them would break the checker in the direction that
matters — report everything as a violation and an operator learns to ignore the
output; report everything as an incompleteness and a soft constraint blocking an
edit reads as a formatting note. Only violations change the exit code.
The outcome pass is mode-aware, and has to be. advise has a declared
ceiling, so a hard deny constraint that only warned is correct under advise
and a violation under enforce. A check that ignored the mode would have to pick
one of those two records to be wrong about.
The placement pass reuses placement::points_for rather than carrying its
own copy of Table 3. Two copies would eventually disagree, and the disagreement
would be between the definition-time check and the audit-time one — the two
places that must not.
What it cannot check, stated in the module doc rather than papered over. SARC's coverage pass asks whether every constraint that applied to an action was evaluated. Deciding applicability means re-running the selector against the file as it stood, and quipu has neither the file nor the parser. What is checkable is the converse — nothing is cited that Σ does not define — plus vacuity. A checker reporting "coverage: pass" while testing something weaker would be the more dangerous artifact.
Unreadable lines are counted, never skipped, and the count is in every summary even at zero. Conformance over a window that was only partly read is not conformance.
The dispatch inventory (I7)
shapes/dispatch-inventory.ttl turns
docs/work-scoped-governance.md §"What this cannot reach" from prose into
aegis:ToolClass facts. Prose goes stale the first time a harness adds a tool;
nothing recomputes, and the list quietly becomes a description of last year's
deployment.
The distinction the whole thing exists for: an executable class traversing
nothing with no stated reason is an unknown hole (violation); the same class
with an aegis:ungovernedReason is an acknowledged bypass surface
(incompleteness). Neither is "governed", and the checker never reports one as the
other — without that split an operator cannot tell a decision from an oversight.
Every acknowledged surface is reported on every run, because one an operator has
stopped seeing is one they have stopped weighing.
Plus the cross-check the other direction: a constraint placed at a point no executable class traverses can never fire. It reads as governance in the catalog and is inert in the deployment — the failure hardest to see from either side alone. An empty inventory is an incompleteness, never a pass: an unwritten dispatch graph is not an empty one.
The seed file says in its own comment that nothing derives it from the harness's tool registry, so it can drift the way the prose did. The difference is that a drifted declaration is a wrong answer to a question something asks, rather than a paragraph nobody re-reads.
The replay harness
Five gates per rule — liveness, both-outcomes, in-spec, recoverability, and new-blocks (not a gate; the number an operator is actually deciding about). Recoverability walks the trace in order: a target cleared before its refusal proves nothing about whether anyone got past the rule, and counting it would make every rule that ever allowed anything look recoverable.
Nothing is re-evaluated. The predicate needed the file as it stood and that file is gone, so replay is deterministic arithmetic over records rather than a simulation.
Three limits ride in the summary rather than in a footnote, because a promotion number read without them is read as a safety claim:
- It measures only traffic that happened. A rule that would block a kind of edit nobody attempted shows zero new blocks and is not therefore safe.
- It counts false-positive candidates, never false positives. A block is
wrong only if the action was legitimate, and no record carries that judgement
— it needs a human. Reporting
new_blocksas an FP rate would be the number-shaped fiction the operating point exists to replace. - It bounds no false negatives at all. Actions a rule let through without firing look exactly like actions it correctly approved.
So θ is now measurable in one direction and still undeclared in the other, and the doc should not be read as saying the operating points are calibrated. They are calibratable.
Still open in Phase 5
There is no scheduler behind the Escalation Router, so §5.3's W_q < τ_rev
remains unmeasured: the queue-depth and utilisation metrics need a queue with a
server attached, and claiming the inequality from a store that only records
requests would be the dashboard anti-pattern with extra steps.
Phase 6 — Multi-agent
Closes G7. Originally written as "gated on quipu enforceable multi-tenancy — do not start before it". That gate was misdescribed; see Phase 6, as built.
- Attribution tuple α on every trace record; the trace stored as a tree, worker subtrees attached to their dispatch node, never summarised.
- Authority intersection along the call chain, monotonically non-increasing; an empty intersection fails safe.
- Constraint inheritance with decidability rescue: evaluate an inherited constraint at the deepest layer where it remains decidable, or escalate — never silently drop it, which is the constraint-laundering path.
- Trust-boundary tagging on imported state (Bobbin retrieval results, MCP tool output, sub-agent responses) with a PAA trust predicate. The zero-trust agent gateway of [SARC] §9.5, expressed in the existing constraint vocabulary rather than as a separate perimeter layer.
Phase 6, as built
The gate was described wrongly
"Blocked on quipu multi-tenancy" was the framing above, and in G7, and in the Phase 2 note about the attribution tuple. It was wrong, and it was wrong in a way that cost real work: it made a whole phase look unreachable when the substrate was already there.
Quipu's named graphs are not a provenance label. graphs is a registry with an
enforced committed|overlay class and a bind-once parent_branch, and writes,
retractions and idempotency are all graph-scoped in SQL
(store/overlays.rs, store/ops.rs). Partitioning was never the missing piece.
What was missing is authorization: http_auth::authorize is one global
bearer token, all-or-nothing, and nothing checked whether a principal may write
to graph N. "Multi-tenancy is unbuilt" and "multi-tenancy has no access-control
layer" are different claims, and only the second was true. quipu's
docs/design/group-isolation.md carried the older framing and has been corrected
in place, because a doc that is being read as a blocker is not a neutral
inaccuracy.
Authority intersection — quipu/src/governance/authority.rs
§9.3's rule, auth_k = ⋂ authority(p_i) for i in 0..=k, as code:
Authority::intersectis monotonically non-increasing, so adding a delegate can only narrow. That is the defence against authority escalation via tool capability (§9.5) — a sub-agent whose own credentials are broader cannot use them, because the effective authority is the intersection and not the executor's own.- The wildcard is the identity, not a widening. A principal may hold
*, which is how a single-tenant deployment keeps working unchanged;*intersected with a narrow authority is the narrow one, so a wildcard-holding orchestrator delegating to a scoped worker yields the worker's scope. - An empty intersection fails safe. A chain narrowed to nothing cannot act, and that is a refusal rather than a fallback to the principal's authority — the fallback would be precisely the escalation the rule exists to stop.
- An empty chain is
none(), notany(). "Nobody said who is acting" must not mean "anybody may act". - An undeclared principal holds nothing. Reading an absent grant as permission is how an access-control layer becomes decorative.
Enforcement runs in Store::enforce_graph_authority, called before the
savepoint in transact_to_graph — the real write path, not a helper a caller
can route around. It is gated by enforce_authority (default off) and is inert
without a principal chain, so every existing caller is untouched: the flag makes
a supplied chain binding rather than making attribution a hard requirement
beneath a running deployment. The refusal names the chain, the graph, and what
the chain actually holds, because a refusal that says only "denied" leaves an
operator guessing which link narrowed it.
The attribution tuple — yupana/src/attribution.rs
α = ⟨P, planner, executor, tool, auth, C_eval⟩, on the gate record and the PAA
record alike. Five of the six:
PfromYUPANA_PRINCIPAL_CHAIN, comma-separated and caller-first. A dispatcher that spawns a sub-agent appends itself and exports the extended chain. Absent when undeclared — an undeclared chain is not a one-link chain, and this is the same distinction that kept the field out of the record in Phase 2.plannerfromYUPANA_PLANNER, declared and never derived from the chain's head. Which link deliberated and which executed is a fact about the dispatch; reading it off list position would be an inference wearing a record's clothes.executorfrom$SHANTY_AGENT— the identity of the process that actually ran, which is ground truth rather than a claim.toolfrom the hook payload.C_evalis theconstraintsarray Phase 2 already emits.
auth is deliberately not recorded by yupana, and this is a settled decision
rather than a pending one: the effective authority is the intersection of grants
that live in quipu, yupana cannot read them inside a 100 ms pre-edit budget, and a
locally-guessed value would put a number in the field the grant store never
agreed to. Recording P is what lets the checker derive auth from the
authoritative source. The tuple is completed by the audit, not faked by the hook.
The conflict flag. YUPANA_PRINCIPAL_CHAIN is a declaration; $SHANTY_AGENT
is what is running. When a chain is declared and its tail disagrees with the
executor, the record says attribution_conflict rather than silently preferring
one. That disagreement is the observable signature of a laundered chain — an
agent acting under a dispatch record naming somebody else — and a record that
resolved it by precedence would delete the only evidence of it. It is emitted
only when true: a false on every line trains a reader to skip the field, which
is the opposite of what it is for.
The attribution tree — quipu/src/governance/tree.rs
quipu audit tree <trace.jsonl> reassembles the dispatch forest from the
principal chains. The trace is still emitted as a sequence, so this is
reconstruction rather than structure, and the output says so in three places
rather than presenting a tree as a guarantee:
- Unattributed records are not placed. Attaching one to whichever root happened to be first would invent an answer to the question the tree exists to answer.
- Implied dispatch nodes are flagged. A chain
[orchestrator, worker]proves an orchestrator exists; it does not prove the orchestrator's own actions are in the window. "Did nothing" and "was not recorded" are different facts. - Collapsed nodes get a note. Two dispatches of the same worker by the same caller produce the same chain and land on one node — attribution dilution reappearing at the reader instead of at the record.
Constraint inheritance — quipu/src/governance/inheritance.rs
aegis:inheritedByDelegates declares that a constraint binds the whole subtree
under a dispatch. aegis:onUndecidable admits only "escalate" — the same shape
as onTimeout admitting only "deny" — which is the decidability rescue:
evaluate at the deepest layer where the constraint still decides, or hand it to a
human. A constraint that silently stops applying where it cannot be checked is
one an agent escapes by dispatching into a context where the evidence is absent.
Two findings, at deliberately different severities because one has evidence and the other has a question. A constraint that decided on a target and is absent from a deeper action on the same target is a violation — it proved it could decide there. A constraint evaluated at a dispatch node and never in its subtree is an incompleteness: that might be laundering and might be a selector that matched nothing deeper, and deciding needs the files as they stood. Reporting both as violations would make the strong one unfindable.
is_below is a strict prefix test. A sibling branch is not a delegation of this
one, and a shallower action after a deeper one is returning, not delegating —
either read as laundering would flag every parallel run.
Trust boundaries — declared on the ToolClass
aegis:importsUntrustedState and aegis:untrustedOrigin, on the dispatch
inventory rather than on a policy, because the tool class is the thing that
actually imports. Reported by quipu audit inventory whether or not the class is
governed: governedAt says a class's own actions traverse a point and says
nothing about what it returned. A class that imports and declares no origin is
a violation — an import channel nobody can describe is one nobody can weigh.
shapes/dispatch-inventory.ttl declares this stack's three real channels: a
sub-agent's response text, MCP tool output, and retrieved documents.
Still open in Phase 6
There is no trust predicate. The vocabulary now names the channels and the inventory reports them on every run, but nothing evaluates imported content. The PAA judges the file after an edit, not the material the edit was based on, and building a predicate over sub-agent responses needs a producer that records them — which no part of this stack does today. So the boundary is declared and open, not closed; the honest claim is that it is now visible on every run rather than absent from the model.
The trace is emitted as a sequence. Making it structurally a tree means the
harness emitting a dispatch id per spawn, which is a change in the harness, not
in yupana. Until then, sibling dispatches of the same principal are
indistinguishable — and quipu audit tree says so per node instead of leaving a
reader to assume otherwise.
Files this touches
quipu
shapes/governance.ttl— the constraint-object extensions (Phase 1)shapes/policies/treesitter.ttl— backfill the shipped catalog (Phase 1)src/governance/guard.rs— class-aware effects, verdict persistencesrc/governance/placement.rs(new) — class↔placement conformance passsrc/governance/router.rs(new) — escalation router (Phase 4)src/governance/audit.rs(new) — theT ⊨ Σchecker (Phase 5)src/signing.rs— reused unchanged; it is already the root of trustdocs/design/policy-edit-hooks.md— the SARC gaps land in its backlog table
yupana
src/project_queries.rs,src/project.rs— projection of the new fieldssrc/rules.rs—Rulegains class + verification pointsrc/hook/rule_planes.rs— response selection by declared classsrc/hook/pre_edit.rs— verdict emission on decisionsrc/hook/post_edit.rs— PAA constraint evaluation + throttle (Phase 3)src/metrics.rs,src/audit.rs— Σ-derived trace record (Phase 2)src/verdict.rs— real freshness and batching; the signing scheme is correctsrc/daemon/— verdict drain, ER client (Phase 4)docs/work-scoped-governance.md— reconcile its phasing with this one
Verification
Beyond each repo's normal gate:
- quipu —
cargo test governance: the extended catalog must still conform; new tests that a class↔placement mismatch is rejected at write (the definition-time half of the discipline), and that an escalation-class policy without τ_rev fails validation. - yupana —
just check && just test, plus two-sided fixtures through the real hook binary perwork-scoped-governance.md§Evals: a RED case and a GREEN case per new constraint class, and a non-vacuity mutation check. - End to end — a policy authored in quipu, projected into yupana, fired at
pre-edit, promoted back as a signed verdict, and accepted by
quipu_audit_checkagainst the same Σ. That round trip is the decidable-audit property of [SARC] Property 1, and it is the acceptance test for Phases 1–2.
One caveat travels with any number this produces: replay measures false positives only on traffic that actually happened, and measures no false negatives at all. A seeded adversarial corpus gives coverage against known attacks and none against novel ones.
Out of scope — and why
Five of the six sources this analysis was drawn from bear on how Σ gets authored and kept aligned with its sources, not on how it is enforced at runtime. That distinction matters: [SARC] §6 is explicit that the arrow from obligation to predicate is an institutional process, not a technical step, and that the framework "presupposes rather than resolves" it. So the authoring work is real, and it is a different piece of work.
Where each one lands, should we pick that thread up:
- [Raji & Bashir] surveys the governance requirements Σ has to encode, and is the clearest statement of the principal-agent problem behind G7 — who is the principal, and what is the agent authorized to do on their behalf. Its Singapore MGF summary ("assess and bound the risks upfront", "make humans meaningfully accountable") maps onto the risk map and the ER respectively.
- [Informatica/Deloitte] argues the semantic-layer case Quipu already embodies. Useful as external corroboration for why the governed graph is the right home for policy, not as a source of requirements.
- [Agent-OM] is directly applicable to keeping
aegis:aligned with external vocabularies as they drift — the maintenance half of the translation layer, and the thing that stops predicates silently decaying away from their source obligations ([SARC] §6, "the translation layer as a governed control surface"). - [Peshevski et al.] is the agent-driven ontology-construction pattern behind the "mined" authoring modality in Governance Plane §Authoring.
- [Olivares-Alarcos et al.] grounds explanation generation in an ontology
while keeping the reasoning sound — relevant to making a refusal legible, which
is a stated requirement of the recoverability eval in
docs/work-scoped-governance.md§Evals ("every refusal names the command that satisfies it").
None of them changes the runtime gap list above, which is why they are named here rather than folded into it.
Sources
- [SARC] — Besanson, G. (2026). SARC: A Governance-by-Architecture Framework for Agentic AI Systems: Compiling Regulatory Obligations into Runtime Constraints. Working paper, Universidad Torcuato Di Tella. arXiv:2605.07728v1 [cs.SE]. Reference artifacts: https://github.com/besanson/sarc-governance. All section, table, definition and invariant references in this document are to this paper.
- [Raji & Bashir] — Raji, M. & Bashir, M. (2026). Towards Agentic AI Governance: A Preliminary Assessment. AIR-RES 2026, Springer Nature. arXiv:2607.07612v1.
- [Informatica/Deloitte] — Beierschoder, M., Andrensek, J. & Rebele, T. Building the Semantic Data Layer for Agentic AI. Informatica / Deloitte whitepaper (5340en).
- [Agent-OM] — Qiang, Z., Wang, W. & Taylor, K. (2024). Agent-OM: Leveraging LLM Agents for Ontology Matching. PVLDB 18(3), 516–529. doi:10.14778/3712221.3712222.
- [Peshevski et al.] — Peshevski, D., Stojanov, R. & Trajanov, D. (2025). AI Agent-Driven Framework for Automated Product Knowledge Graph Construction in E-Commerce. arXiv:2511.11017v1 [cs.AI].
- [Olivares-Alarcos et al.] — Olivares-Alarcos, A., Ahsan, M., Sanjaya, S., Lin, H.-I. & Alenyà, G. Ontological grounding for sound and natural robot explanations via large language models. arXiv:2602.13800v1.
Internal design documents this analysis builds on, all in-tree:
- Governance Plane — the verification spine, verdict integrity, risk × confidence, the Yupana↔Quipu integration contract.
- Policy edit hooks — evidence locality, the quipu
pre-commit gate, the yupana projection, and the
Q-*/H-*backlog theQ-SARC-*beads extend. - Tiers and Freshness — the confidence inputs SARC's operating point composes over.
docs/work-scoped-governance.md— the trace taxonomy, the five eval properties, and the per-rule promotion ladder. Out of the book by design; cited by path.