symfonic.capabilities.governance¶
governance ¶
Safety and governance stages (T3.4.4).
Seven safety concerns used to live in six places: a scrubber in
agent/hygiene.py, an intent filter threaded through three engine entry
points, a fabrication detector and a metacognitive critic in
agent/middleware/, a precondition gate in core/nodes/, steering
inside the tool span seam, and budgeting in services/budget/. Each one
worked. Together they had no order anyone had written down, no shared
account of what happens when one of them breaks, and no single record of
what they decided about a turn.
This package composes them:
- The rulebook — :mod:
.rulebook. The canonical order, and for each stage a declared failure mode with the argument for it. Ordering is the one property no individual stage can enforce, so it lives here and the pipeline refuses a composition that contradicts it. - The pipeline — :mod:
.pipeline. Order, declaration integrity, containment by declared failure mode, and the trace. - The seven stages — :mod:
.stages. One module each, each one drivable on its own with no graph, no engine, and no provider. - The ports — :mod:
.contracts. Classifier, detector, critic, meter, and objector, all structural. The capability implements none of them.
Failure is a declaration, not an accident. Every stage says in advance what its own breakage means. Five fail open, because a broken preference must not become an outage. Two fail closed — credential hygiene and budget — because a scrubber that did not run has not proved anything, and a spend that cannot be measured cannot be bounded.
Objecting is not failing. A stage that finds an unmet precondition, a policy violation, or an unsupported claim is a stage that worked. The legacy gates conflated the two by catching everything and returning "allowed", which made a crashed checker and a clean turn produce the same trace.
This package imports nothing outside itself; a contract test asserts it.
BudgetLimits
dataclass
¶
None measures without capping; a number caps.
Zero is refused rather than read as "unlimited": a deployment that
typed 0 meant "nothing", and silently reading that as "everything"
is the most expensive possible misreading.
BudgetStage ¶
ConfidenceOnlyMetacognitionPolicy ¶
ConfidenceOnlyMetacognitionPolicy(*, confidence_floor: float = 0.6, sensitive_terms: Sequence[str] = ())
The pre-FP-1 capability policy, retained as an explicit cost opt-out.
It intentionally does not preserve the legacy selective hard floors. A deployment choosing it accepts that numbers, tool actions, recalled tags, findings, and intent do not by themselves request reflection.
Source code in src/symfonic/capabilities/governance/stages/metacognition_policy.py
ConfidenceReporter ¶
Bases: Protocol
How sure the caller is of this draft, when nothing else knows.
Synchronous and offline, like :class:TokenMeter and unlike
:class:Reflector: the gate exists to decide whether the one expensive
port is worth calling, so the signal it decides on must not cost a model
call of its own.
Returning None withholds the claim and leaves the gate unfired, which
is the same tri-state rule GovernanceSubject.confidence follows. The
framework never computes this: a confidence synthesised from whatever
happened to be in reach is how the legacy gate came to fire on every turn.
CredentialHygieneStage ¶
Drop credential-shaped keys before any other stage observes them.
Source code in src/symfonic/capabilities/governance/stages/credentials.py
Disposition ¶
Bases: StrEnum
What a working stage decided about this turn.
ALLOW nothing to say.
ANNOTATE the subject was amended; the amendment travels onward.
STEER an objection the model (or the caller) must act on; the
turn continues, because a governance stage that halts on
every objection cannot express "fix this and carry on".
REFUSE terminal. No later stage runs.
Enforcement ¶
Bases: StrEnum
The three-position knob every legacy governance flag already had.
OFF is a zero-cost guarantee, not a quiet allow: the pipeline does
not run, so no classifier is called and no meter is read.
FabricationDetector ¶
Bases: Protocol
Scans a draft against what actually grounded it.
Synchronous by contract: the legacy detector is regex over text, and an async signature would invite someone to put a network call behind the one stage that has to be cheap enough to run on every turn.
FabricationStage ¶
FabricationStage(detector: FabricationDetector | None, *, min_confidence: float = 0.6, refuse_min_confidence: float | None = 0.9)
Scan the draft, cross-check the intent, and report what is unsupported.
Source code in src/symfonic/capabilities/governance/stages/fabrication.py
FailureMode ¶
Bases: StrEnum
What the pipeline does when a stage fails, as opposed to objects.
An objection is the stage working. A raise is the stage broken. The two are separate axes and conflating them is how a crashed scrubber turns into a clean bill of health.
Finding
dataclass
¶
One piece of evidence a stage produced.
confidence is the number thresholds are read against. A stage that
cannot estimate one states 1.0 and says so in detail rather
than inventing a hedge.
with_stage ¶
Stamp the stage, keeping everything else.
replace rather than a field-by-field rebuild: the rebuild dropped
whichever field was added last, silently, and a finding that reaches
an operator with its rule erased is worse than one that never
travelled.
Source code in src/symfonic/capabilities/governance/values.py
GovernanceCapability ¶
One object that answers: what does governance do to this turn, and why?
Source code in src/symfonic/capabilities/governance/capability.py
compose
classmethod
¶
compose(*, classifier: IntentClassifier | None = None, detector: FabricationDetector | None = None, reflector: Reflector | None = None, meter: TokenMeter | None = None, preconditions: Sequence[Objector] = (), guards: Sequence[Objector] = (), limits: BudgetLimits | None = None, patterns: Sequence[str] | None | object = USE_DEFAULT_PATTERNS, confidence_floor: float = 0.6, sensitive_terms: Sequence[str] = (), sensitive_tags: Sequence[str] = (), read_only_tools: Sequence[str] = (), trivial_ack_patterns: Sequence[str] = (), confidence: ConfidenceReporter | None = None, trigger_policy: MetacognitionTriggerPolicy | None = None, min_confidence: float = 0.6, refuse_min_confidence: float | None = 0.9, rulebook: RuleBook = CANONICAL_RULEBOOK) -> GovernanceCapability
Build the pipeline in canonical order from the available ports.
Source code in src/symfonic/capabilities/governance/capability.py
declarations ¶
The declaration table for the rulebook this capability composes.
describe
staticmethod
¶
Render the ordered stage/phase/failure-mode/rationale table.
This is the reviewable artefact: the same table a release note or a security review reads, generated from the objects the pipeline actually enforces rather than from a document beside them.
Source code in src/symfonic/capabilities/governance/capability.py
GovernanceContext
dataclass
¶
GovernanceContext(run_id: str = '', scope_path: tuple[str, ...] = (), enforcement: Enforcement = Enforcement.ENFORCE, intent: IntentReading | None = None, metadata: dict[str, Any] = dict())
Ambient facts for one governed turn.
intent starts empty and is filled by the intent stage. A stage
reading it before that stage has run reads None — which is the
withheld claim, not "no action intended".
GovernanceError ¶
Bases: Exception
Base class for every governance composition failure.
GovernanceOutcome
dataclass
¶
GovernanceOutcome(subject: GovernanceSubject, trace: tuple[StageRecord, ...] = (), refusal: StageRecord | None = None)
The governed turn: the surviving subject and the whole trail.
GovernancePhase ¶
Bases: StrEnum
When a stage runs, relative to the model and to the effects.
The phase is not decoration: it is what makes the canonical order checkable. A stage that claims INGRESS cannot be sequenced after one that claims EGRESS, because "before the model reads the turn" and "after the model has drafted" are not orderings a composition may reverse locally.
GovernancePipeline ¶
Run governance stages in the declared order and report what they decided.
Source code in src/symfonic/capabilities/governance/pipeline.py
for_phase ¶
The stages this pipeline runs at phase, or None for none.
The kernel runs a turn as a ladder of phases and governance spans three of them -- a credential scrub belongs before the model reads the query, a budget ceiling before a tool is admitted, a reflection pass after the draft exists. Running the whole pipeline at one rung would put the scrubber after the text it was meant to scrub.
None rather than an empty pipeline, because the caller's decision
differs: a phase with no stages must contribute no kernel stage at all.
A declared stage that examines nothing is exactly the "in-but-inert"
shape compose refuses one layer down -- indistinguishable in a trace
from a stage that looked and found nothing.
carry names stages that run on every rung, ahead of that rung's
own. The rulebook's first rule is why: credential hygiene is ordered
first "because every later stage observes the payload", and that is an
argument about relative order within a pass, not about which rung a
pass happens on. A scrubber that only ran before the model never sees a
tool argument or a tool result -- which is where a secret actually
travels -- so on a ladder with several passes it has to lead each one.
Carried stages keep their rulebook rank, so the pipeline still refuses an order the rulebook contradicts. A stage already selected for this phase is not added twice.
Source code in src/symfonic/capabilities/governance/pipeline.py
select ¶
Select a subsequence without changing its canonical order or rules.
Integration layers choose placement; classification and containment remain properties of the original pipeline.
Source code in src/symfonic/capabilities/governance/pipeline.py
GovernanceStage ¶
Bases: Protocol
One governed concern.
The three class-level declarations are not metadata: the pipeline
checks them against the rulebook at composition time and refuses a
stage whose claims differ. apply may raise — that is precisely
what failure_mode is a declaration about.
GovernanceSubject
dataclass
¶
GovernanceSubject(query: str = '', draft: str = '', pinned: str = '', grounding: str = '', properties: Mapping[str, Any] = dict(), tool_calls: tuple[ToolCall, ...] = (), confidence: float | None = None)
The turn, as governance sees it.
pinned is the portion of the payload that may never be dropped to
make room — boundaries, operator instructions, the safety preamble.
Budgeting refuses rather than trims it (T3.2.1's rule: a prompt
missing its boundaries is worse than a prompt that refused to build).
with_properties ¶
A subject carrying these properties, which the result cannot edit.
A proxy rather than dict(...). Several stages read one subject's
properties in a turn, and a stage handed a mutable bag could rewrite
what a later stage sees -- a side channel around the pipeline, whose
whole shape is "a stage says what it decided by returning a verdict".
Source code in src/symfonic/capabilities/governance/context.py
IntentClassifier ¶
Bases: Protocol
Reads the user's turn. Async because the real one calls a model.
IntentFilterStage ¶
IntentReading
dataclass
¶
The classifier's answer about the user's turn.
Published once by the intent stage and read by later stages; the canonical order exists so that "later" is a fact rather than a hope.
MetacognitionStage ¶
MetacognitionStage(reflector: Reflector | None, *, confidence_floor: float = 0.6, sensitive_terms: Sequence[str] = (), sensitive_tags: Sequence[str] = (), read_only_tools: Sequence[str] = (), trivial_ack_patterns: Sequence[str] = (), confidence: ConfidenceReporter | None = None, trigger_policy: MetacognitionTriggerPolicy | None = None)
Reflect on the draft when, and only when, the gate fires.
Source code in src/symfonic/capabilities/governance/stages/metacognition.py
MetacognitionTriggerPolicy ¶
Bases: Protocol
Names the evidence that requires reflection for one draft.
None means the policy deliberately skipped a trivial acknowledgment;
a non-empty string is copied into the trace as the trigger. The policy is
synchronous and uses only the subject/context evidence already present at
the governance boundary.
Objector ¶
Bases: Protocol
A precondition or a policy guard: same shape, different authority.
Returns the objection text, or None to admit. tool_name scopes
a precondition to one tool; a guard that omits it sees every call.
PolicySteeringStage ¶
Reflection
dataclass
¶
The critic's answer about the draft.
Reflector ¶
Bases: Protocol
The critic. Async, and the only port that costs a model call.
RuleBook ¶
An ordered, immutable set of stage declarations.
Source code in src/symfonic/capabilities/governance/ordering.py
extend ¶
Return a new rulebook with rule inserted after after.
after=None appends. The result is validated by the constructor,
so a phase inversion is refused here rather than at the first turn.
Source code in src/symfonic/capabilities/governance/ordering.py
validate ¶
Refuse a composition that is not a subsequence of this order.
Source code in src/symfonic/capabilities/governance/ordering.py
SelectiveMetacognitionPolicy ¶
SelectiveMetacognitionPolicy(*, confidence_floor: float = 0.6, sensitive_terms: Sequence[str] = (), sensitive_tags: Sequence[str] = (), read_only_tools: Sequence[str] = (), trivial_ack_patterns: Sequence[str] = ())
Preserve the legacy selective gate's hard floors on the capability path.
A scalar confidence is supplemental evidence. It can never stand in for claims, actions, detector findings, or recall-time sensitivity.
Source code in src/symfonic/capabilities/governance/stages/metacognition_policy.py
trigger ¶
trigger(subject: GovernanceSubject, context: GovernanceContext, confidence: float | None) -> str | None
Return a stable trigger name, or None for a trivial ack.
Source code in src/symfonic/capabilities/governance/stages/metacognition_policy.py
StageContractError ¶
Bases: GovernanceError
A stage object declares something other than what the rulebook does.
The likeliest instance is a stage quietly declaring FAIL_OPEN for a
concern the rulebook declares fail-closed — a disarmed guard that
would otherwise look identical to a working one.
StageOrderError ¶
Bases: GovernanceError
The composition contradicts the rulebook's order.
Duplicates, inversions, and phase reversals all land here. Ordering is the one property no individual stage can enforce.
StageRecord
dataclass
¶
StageRecord(stage: str, phase: GovernancePhase, failure_mode: FailureMode, disposition: Disposition, reason: str = '', objection: str = '', findings: tuple[Finding, ...] = (), evidence: Mapping[str, Any] = dict(), degraded: bool = False, downgraded: bool = False)
What one stage did, including the stages that did nothing.
degraded means the stage failed and the declared failure mode
decided the rest. downgraded means the stage worked and observe
mode suppressed its terminal effect. Two flags because they are two
different post-mortems.
StageRule
dataclass
¶
One stage's declaration: where it runs, and what a failure means.
rationale is required and is checked for being a sentence rather
than a label. A failure mode without a stated reason is a coin flip
somebody will later "optimise" in the wrong direction.
StageVerdict
dataclass
¶
StageVerdict(disposition: Disposition, reason: str = '', objection: str = '', subject: GovernanceSubject | None = None, findings: tuple[Finding, ...] = (), evidence: Mapping[str, Any] = dict())
One stage's answer. Built through the four constructors, not by hand.
allow
classmethod
¶
allow(reason: str = '', *, evidence: Mapping[str, Any] | None = None, findings: Sequence[Finding] = ()) -> StageVerdict
Admit the subject, optionally saying what was noticed on the way.
findings on an allow is not a contradiction. A rule that
repaired a call and raised no objection admitted it -- and still
decided something an operator needs attributed. Without this the
repair travelled anonymously, because only objections were recorded.
Source code in src/symfonic/capabilities/governance/records.py
TokenMeter ¶
Bases: Protocol
Counts a payload. Offline by construction — see T3.2.1's budgeting rule.
ToolCall
dataclass
¶
A reading of one call: enough to govern it, not enough to run it.
with_result ¶
Replace the result, keeping the call it belongs to.
The twin of :meth:with_args, and it exists because that method
preserves result — deliberately, since amending a call's inputs
should not silently rewrite what it returned. Scrubbing the output is a
separate decision and therefore a separate method.
Source code in src/symfonic/capabilities/governance/values.py
ToolPreconditionStage ¶
UnknownStageError ¶
Bases: GovernanceError
A stage name the rulebook does not declare.
Raised rather than defaulted: a stage nobody declared has no declared phase and no declared failure mode, so the pipeline cannot say what it would do when the stage breaks.
compile_credential_pattern ¶
None -> the default set; [] -> disabled; otherwise a replacement.
An invalid fragment raises re.error here, at construction, rather
than at scrub time: a misconfigured pattern list must fail while the
deployment is being built, not while a secret is passing through it.