Skip to content

Rules, RKM and the policy engine

Failures from authoring a rule, from moving a proposal through the review queue, and from the engine that evaluates the result.

curl examples assume the scratch broker on 127.0.0.1:8001 and export NAUTILUS=http://127.0.0.1:8001.

Authoring a rule

ERROR {serr.file}:{serr.line}: {serr.message}{hint_suffix}

nautilus/cli/rules.py:171-177 (and :281-287 on the rule path). Printed once per static error to stderr; nautilus rules validate exits 1. {hint_suffix} is " Hint: {serr.hint}" when the compiler supplied one, and empty otherwise. Rendered example:

ERROR /tmp/bad_rule.yaml:1: Rule file does not compile: [fathom.compiler] parse rules failed:
YAML file must contain a top-level 'module' key Hint: Each 'when' condition takes slot + one
of expression/bind/test. 'operator:'/'value:' are not condition keys.

The text after [fathom.compiler] comes from the rule compiler, so it changes with the rule. The file and line locate it. With --json, the same errors are emitted as {"file": …, "ok": false, "errors": [{"file": …, "line": …, "message": …}]}.

ERROR: file not found: {file_path}

nautilus/cli/rules.py:148-150. Exit 1. The path passed to nautilus rules validate does not exist.

printf 'rules:\n  - name: r\n' > /tmp/nautilus-bad-rule.yaml
nautilus rules validate /tmp/nautilus-bad-rule.yaml; echo "exit=$?"
nautilus rules validate /nonexistent/rule.yaml; echo "exit=$?"

Fact-file errors

nautilus/rules/facts.py, all ValueError, all prefixed with {path}:{line}::

Message Line
{yaml_file}:1: YAML parse error: {exc} facts.py:46
{path}:{approx_line}: invalid relationship_type={rel_type!r}; must be one of [{valid}] facts.py:62-65
{path}:{approx_line}: {float_slot} must be a float, got {value!r} facts.py:74-77
{path}:{approx_line}: {float_slot}={fval} out of range [0.0, 1.0] facts.py:80

The sandbox validator

Every proposal is replayed against recorded traffic before it can be promoted.

proposed rule {rule_name!r} does not compile: {exc}

SandboxRuleError (nautilus/rkm/validator/sandbox.py:49), raised at :227. {exc} is the compiler error. Same fix as nautilus rules validate.

Regression detected: rule {rule_name!r} denies access the current rules grant, in {regression_count} of {replayed} replayed request(s). First: {first_regression}

SandboxRegressionError (nautilus/rkm/validator/sandbox.py:37), raised at :328-333.

Means. The proposed rule was replayed against real recorded requests and took away access that today's rules give. {replayed} is how many requests were replayed — if it is small, the audit log is thin and the result is weak evidence either way. {first_regression} identifies the first request that changed.

Fix. Narrow the rule's when conditions, or accept the tightening deliberately — that is a policy decision, so it goes through review with the regression named in the proposal record.

The review queue

proposal not found: {proposal_id!r}

KeyError from nautilus/rkm/review.py:122,241; nautilus/rkm/queue.py:268 uses the unquoted form, proposal not found: {proposal_id}. The queue file has no such proposal. GET /v1/rkm/queue lists what is there. Over HTTP the transport does not pass this string through: GET, approve and reject all answer 404 with proposal {proposal_id!r} not found.

{"error": "already_decided", "current_status": …}

HTTP 409, a JSON object rather than a string. nautilus/transport/fastapi_app.py:1560-1564 and :1561-1565, from AlreadyDecidedError (nautilus/rkm/review.py:39). current_status is the status the proposal already holds — approved, rejected or retracted. Decisions are not idempotent replays: a second one is refused, not silently ignored.

invalid proposal id: {proposal_id!r}

InvalidProposalIdError (nautilus/rkm/queue.py:46), raised from _safe_id at :52. A proposal id is interpolated into a filename, so it must be a bare name: [A-Za-z0-9._-]+, and neither . nor ... GET /v1/rkm/proposals/{proposal_id} takes the id straight off the URL path, so an id containing / or .. used to reach open() outside the queue directory. Reads answer 404 rather than raising — an id that cannot name a file is a proposal the queue does not hold — while submit raises, because a write that cannot name its own file has nowhere to go.

cannot transition {proposal_id} from {current!r} to {to!r}

InvalidTransition (nautilus/rkm/queue.py:75), raised at :234. Maps to HTTP 409. The state machine has no edge between those two states.

lock contention timeout on proposal queue

ProposalQueueLocked (nautilus/rkm/queue.py:71), raised at :103 and :114 after a 5-second lockf backoff. Maps to HTTP 503. Another live process holds the queue file lock — a concurrent nautilus rkm command, or a server serving a governance route. There is no such thing as a stale one: lockf locks are held by the process, and the kernel drops them when it exits however it exits, so a .lock file left behind by a kill -9 blocks nothing and must not be deleted to "clear" it. Retry; if it persists, find the process still running (fuser <queue_dir>/.lock).

Governance request-body validation

HTTP 400 unless noted, from nautilus/transport/fastapi_app.py:

Message Line Route
body must carry 'rule_yaml': the contents of the rule file :1244 POST /v1/rkm/queue — send the rule file's text, not a path
reason is required for rejection :1412 POST /v1/rkm/queue/{id}/reject
reason is required for retraction :1528 POST /v1/rules/{name}/retract
to_version is required for rollback :1621 POST /v1/rules/{name}/rollback
yes=true required for destructive operation (412) :1522,1615 retract and rollback

X-Nautilus-Reviewer header required is in auth.md.

curl -s -X POST "$NAUTILUS/v1/rkm/queue" \
  -H 'X-API-Key: govern-key' -H 'Content-Type: application/json' -d '{}'
curl -s -X POST "$NAUTILUS/v1/rules/some_rule/retract" \
  -H 'X-API-Key: govern-key' -H 'Content-Type: application/json' -d '{}'

Promotion

PromotionFailedError (nautilus/rkm/review.py:51) and PolicyEngineError (nautilus/core/__init__.py:14) — an approval that could not be made real.

HTTP 422 from POST /v1/rkm/queue/{proposal_id}/approve (nautilus/transport/fastapi_app.py), as a JSON object carrying error, message, current_status and recovery. It was an uncaught 500 until the route learned to catch it, which meant the commonest cause — no rules.user_rules_dirs on a default install — reached the reviewer as nothing at all. The proposal is left in approved: re-approving retries the promotion, rejecting is the other way out.

cannot promote rule {rule_name!r}: no rules.user_rules_dirs is configured, so the rule would live only in this process and be gone at the next restart while the proposal reads 'promoted'. Configure a writable rules directory and retry the approval.

nautilus/core/fathom_router.py:803-810. Set rules.user_rules_dirs to a writable directory and approve again.

cannot promote rule {rule_name!r}: writing {target} failed: {exc}

nautilus/core/fathom_router.py:817-820. The directory is configured but the write failed — {exc} names the OS error. Read-only mount and missing directory are the usual causes.

proposal {proposal.proposal_id!r} carries no rule YAML: artifact has neither 'yaml' nor 'yaml_path' (keys: {sorted(artifact)})

nautilus/rkm/review.py:418-421. The proposal record is incomplete; {sorted(artifact)} shows what it does carry. Re-propose the rule.

proposal {proposal.proposal_id!r} references unreadable rule file {path!r}: {exc}

nautilus/rkm/review.py:415-418. The proposal stored a yaml_path and the file has since moved or become unreadable. Re-propose with the YAML inline.

FathomRouter.reload_rule failed for proposal {proposal_id!r}: {exc}

nautilus/rkm/review.py:157-160. The rule was written but the engine refused to load it. {exc} carries the compiler or engine error.

cannot retract rule {rule_name!r}: it is still in force after a rebuild, so it comes from a loaded rule pack. Remove the pack from rules.packs instead.

nautilus/core/fathom_router.py:856-860. Pack-supplied rules are not retractable one at a time; edit rules.packs.

Lookups

Message Where Status
rule {rule_name!r} not found fastapi_app.py (lineage, retract) 404
rule {rule_name!r} version {version} not found fastapi_app.py (retract, rollback) 404
rule {rule_name!r} v{to_version} not found in lineage rkm/review.py:351 KeyError
lineage record not found: {rule_name} v{version} rkm/lineage.py:249 KeyError

GET /v1/rules lists live rules; GET /v1/rules/{rule_name}/lineage lists the versions a rollback may target.

invalid rule name {rule_name!r}: expected a bare rule name, optionally prefixed 'module::'

LineageNameError (nautilus/rkm/lineage.py:21), raised at :34-38. Rule names are name or module::name. A path, a file name with .yaml, or extra :: separators are rejected, because the name becomes part of a lineage file name.

LineageCycleError

nautilus/rkm/lineage.py:43, raised at :186 with the offending path as its only argument — so the message is the list of rule versions forming the cycle. A rule's supersedes chain loops back on itself. Break the loop by correcting the supersedes of the newest record.

The engine

rule pack {pack_name!r} is claimed by more than one installed distribution ({', '.join(claimants)}). Which policy runs would be decided by distribution name order. Uninstall one, or rename the pack in the distribution you do not want.

PolicyEngineError, nautilus/core/fathom_router.py:135-141. Two installed packages both register the same entry-point pack name. Nautilus refuses to pick.

Fathom engine construction failed: {exc}

nautilus/core/fathom_router.py:196 and :229. The rule set did not compile into an engine at startup or after a reload. {exc} is the compiler error — validate the rule files with nautilus rules validate.

FathomRouter.route() failed for agent_id={agent_id!r}: {exc} / FathomRouter.replay() failed: {exc} / FathomRouter.reload_rule() failed for rule_name={rule_name!r}: {exc}

nautilus/core/fathom_router.py:481, :525, :831. Evaluation raised. {exc} is the underlying failure; a rule referencing a slot that does not exist is the common one.

ConsistencyError

nautilus/core/__init__.py:73, raised by FathomRouter at :625-671 with a code and a message. Each means the evaluation produced a fact set that contradicts itself — a routing decision Nautilus will not act on.

Code Message Meaning
routing_unknown_source routing_decision references undeclared source(s) {sorted(unknown_routed)!r} A rule routed to a source id that is not in sources:. Usually a typo in the rule.
scope_without_routing scope_constraint without a routing_decision for source(s) {sorted(unscoped)!r} A rule constrained a source it never routed to; the constraint would never be applied.
denial_unknown_source denial_record references undeclared source {denial.source_id!r} A denial names a source that does not exist.
denial_missing_linkage denial_record for source {denial.source_id!r} is missing reason/rule_name linkage (reason={denial.reason!r}, rule_name={denial.rule_name!r}) A denial without a reason or an owning rule is unexplainable in the audit record.
agent_fact_integrity expected exactly 1 agent fact after evaluation, found {len(agent_facts)} Rules asserted or retracted the agent fact.
agent_fact_integrity agent fact slot {slot!r} mutated during evaluation: asserted {asserted!r}, found {actual!r} A rule rewrote the caller's identity mid-evaluation.
session_exposure_count expected {expected_exposure_count} session_exposure fact(s) after evaluation, found {len(exposure_facts)} (unexpected retraction cascade or injection) The exposure ledger was changed by rule evaluation.

Fix, all seven. These name a rule bug, not an operational one. Find the rule that asserts the offending fact and correct it; nautilus rules validate --sandbox replays it before it ships.

unknown agent id={agent_id!r}

Not an exception — a denial reason on the response (nautilus/core/fathom_router.py:363). The request named an agent with no entry under agents:.

rule {rule_name!r} targets routing-owned template {template!r}

CuratorIsolationViolation (nautilus/rkm/curator/isolation.py:32), raised at :85-88 with a file:line location. A curator meta-rule tried to write a template the routing module owns. Meta-rules may propose changes to rules; they may not assert routing facts directly.