diff --git a/plan/policy.md b/plan/policy.md index 0c23b4b2..5afe120d 100644 --- a/plan/policy.md +++ b/plan/policy.md @@ -175,6 +175,19 @@ of an evaluator failure. ## The evaluator contract +- [ ] **`compile`** — given a predicate document, return one of three + outcomes: compiled (with a `CompiledId` the evaluator itself minted), + compiled with warnings (same, plus operator-facing advice that does + not block the policy), or rejected with a reason. Validation *is* + compilation; there is no separate `validate`. This is what lets + `didbot-pds`'s loader decide "this document is syntactically invalid + for this language" at load, from the evaluator that actually speaks + the language, rather than inferring it from a later evaluation + failure. The `CompiledId` `compile` hands back — not `PolicyId` — is + what `evaluate`, `observe` and `policy_lifecycle` are addressed by + afterward: the compiled artifact belongs to the evaluator, which may + run out of process, so a caller-owned handle could only ever be an + identifier for state the caller does not hold. - [ ] **`evaluate`** — given a subject and a matched policy, return an outcome. - [ ] **`observe`** — every attempt and every outcome on the declared surface, whether or not evaluation short-circuited. **The decision channel @@ -184,6 +197,31 @@ of an evaluator failure. - [ ] **A lifecycle channel** — froze, unfroze, deleted. An evaluator holds per-agent state and will accumulate it for accounts that no longer exist unless deletion reaches it. Unfreeze matters especially: see below. +- [ ] **A parallel, policy-scoped lifecycle channel** — loaded, superseded, + removed — so an evaluator that accumulates state per compiled artifact + (not per account) does not keep it for a policy that no longer exists. + Delivery is not reliable and does not need to be: the tree decides what + is routed anywhere, so a missed removal is a memory leak, not a + correctness problem. No evaluator in this workspace accumulates + per-artifact state yet (`didbot-policy-regex` holds exactly one, replaced + wholesale on every recompile), so nothing here calls this channel today + — it exists on `Evaluator` (defaulted to a no-op) for the evaluator that + will need it. + +### Last-good-revision + +A policy that compiled at revision N and fails to compile at N+1 keeps +enforcing N — dropping it would widen what an agent may do, and denying +everything it covers would turn a typo into an outage. `didbot-policy-source`'s +`LastGoodPolicies` holds the last successfully-compiled record per +operator-sourced id, across every `merge::build` call; a revision that fails +to compile is looked up there and, if found, recompiled and enforced in the +new revision's place, reported as a `LastGoodFallback` — distinct from a +`ScopedDenial`, which is what a policy that has *never* compiled still gets, +loudly, because there is nothing to fall back to and a first-publish typo +must not silently deny its own coverage. `startup` (source 2, immutable for +the run) has no last-good-revision fallback: there is never a second +revision to have fallen back from. - [ ] **Evaluators own their state**, built from what they have seen. Not a re-index of historical log state. State that survives nothing is acceptable; state that must be reconstructed from the whole log is not. @@ -398,4 +436,7 @@ first: ## Done -Nothing closed yet. +- The `compile` step on `Evaluator` (three outcomes, a `CompiledId` the + evaluator owns, a parallel policy-lifecycle channel) and + last-good-revision (`crates/didbot-policy`, `crates/didbot-policy-source`, + `crates/didbot-policy-regex`, `crates/didbot-pds/src/policy_tree.rs`).