Skip to content

fix(kernel): name Cedar policies in denials, close policy/actor-directory races - #418

Open
rita-aga wants to merge 1 commit into
mainfrom
claude/temper-error-taxonomy
Open

fix(kernel): name Cedar policies in denials, close policy/actor-directory races#418
rita-aga wants to merge 1 commit into
mainfrom
claude/temper-error-taxonomy

Conversation

@rita-aga

@rita-aga rita-aga commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Found while diagnosing why a contributor agent could not learn why it had been denied — it burned ~76 minutes and ~$8 hunting introspection endpoints that do not exist (ARN-286).

Denials name their source file

A denial read denied by policy: policy1874, policy1875 — positional ids that map to nothing, so neither an agent nor a human can tell which rule fired.

Policies loaded from an app bundle now carry the label {app}/{path under policies/}, so the same denial reads katagami-commons/art_style.cedar#2. The durable policies row id is that same string, so a denial recovered after a restart still names the file rather than reverting to a position.

The pre-ARN-286 row id is deleted once the policy has been re-saved under its label — fatally, not warn-only. A surviving legacy row is loaded again on the next recovery, bringing the OLD generation of those statements back alongside the new one; with Cedar's forbid-overrides-permit that silently reactivates a forbid the operator believes they deleted. Stale authorization restored by a restart, with no signal at install time, is not something to log and continue past.

Policy reload was read-then-write

Reading a tenant's sources, compiling them, and swapping them in were three separate steps. Two concurrent rebuilds both read the same pre-state, both wrote, and the first caller's policies were gone — with both calls returning Ok. update_tenant_policy_sources now performs the whole read-modify-write while holding the tenant write lock, and the os-app installer mirrors what the engine actually installed rather than what it computed before the merge.

Policy load failures no longer fail open

A failed reload after an os-app install was logged and swallowed, leaving the tenant running on whatever policy set happened to be live. It is now fatal to the install.

Three maps had to agree about every live actor

The actor registry, the entity index, and the last-accessed stamps were mutated independently, so a failure on a cleanup path could remove an actor from one and leave it in the others. Rather than patch the four sites that leaked, the invariant moved into one guarded directory that owns all three and takes the registry write lock first — the lock everything else already takes first.

Also in here

  • Restored a committed // TEMP probe: lock acquisition removed that had disabled per-tenant install serialization on this branch.
  • Extracted the inline #[cfg(test)] modules from cell.rs, recovery.rs, and dispatch/mod.rs into *_test.rs siblings, and renamed engine/tests.rs to engine_test.rs. This is what the readability ratchet is asking for — production files shrink, tests are excluded by design. The ratchet is now better than baseline: PROD_FILES_GT500 83 vs 84.

Verification

  • cargo build --workspace clean.
  • cargo clippy --workspace --all-targets -- -D warnings clean (the CI gate — an earlier run without -D warnings hid a type_complexity error).
  • Full suite green through the pre-push hook, including the Docker-backed testcontainers integration tests.
  • Live e2e: booted temper serve against katagami-commons/specs, created an ArtStyle as an agent_type=contributor principal, attempted SubmitForReview — correctly denied.

One honest gap. That live denial read primary#5, primary#4, not katagami-commons/art_style.cedar#2. The labelling is on the os-app install path, which is what production uses and which is covered by tests asserting the exact string (including after a reload). temper serve --app takes a different path that flattens the policies into one blob before they reach the engine. So this is fixed and tested for the production path, but not demonstrated end-to-end against a running server — that needs a Genesis-backed install. Filed as ARN-314 with the code path and a verification recipe. I am not claiming the live run proves the headline fix; it does not.

Unrelated flakes that gate this repo's pre-push hook are filed as ARN-323.

🤖 Generated with Claude Code

Greptile Summary

The PR gives Cedar statements stable source-file labels, serializes tenant policy rebuilds, makes policy activation failures fatal, and centralizes actor registry/index/access bookkeeping. It also improves authorization error classification, entity-initialization failure propagation, OData key parsing, metrics, recovery, and associated tests.

Confidence Score: 3/5

The PR should not merge until legacy policy cleanup verifies ownership or otherwise avoids deleting colliding policy IDs.

A valid app installation can derive the same lossy legacy key as another durable policy and delete that row, potentially removing an authorization forbid or permit during recovery.

Files Needing Attention: crates/temper-platform/src/os_apps/policy_rows.rs

Security Review

App-policy migration cleanup derives a lossy legacy ID and deletes that row without validating ownership, allowing an installation to remove an unrelated durable Cedar policy after an ID collision.

Important Files Changed

Filename Overview
crates/temper-platform/src/os_apps/policy_rows.rs Persists source-labelled policy rows and removes legacy rows, but the lossy legacy-ID deletion can remove an unrelated policy.
crates/temper-authz/src/engine/mod.rs Adds stable statement labels and serializes tenant policy source read-modify-write operations under the tenant-policy lock.
crates/temper-platform/src/os_apps/mod.rs Serializes tenant installations, activates merged policy sources, and mirrors the engine’s actual installed text.
crates/temper-server/src/state/entity_ops.rs Centralizes actor registry, entity-index, and access-stamp mutations while retracting failed actor initialization safely.
crates/temper-runtime/src/actor/cell.rs Propagates classified initialization failures to pending requests and observers after restart supervision is exhausted.
crates/temper-odata/src/path.rs Expands key and parameter literal parsing, including double-quoted forms and escaped quote handling.
crates/temper-platform/src/recovery.rs Recovers granular labelled Cedar sources alongside compatible legacy policy state.
crates/temper-server/src/authz/helpers.rs Separates policy decisions from malformed-request and engine faults for API and approval-queue handling.

Sequence Diagram

sequenceDiagram
    participant Installer
    participant PolicyStore
    participant AuthzEngine
    participant Recovery
    Installer->>PolicyStore: Save app/path.cedar
    Installer->>PolicyStore: Delete derived legacy slug
    Note over PolicyStore: Lossy slug may identify another policy
    Installer->>AuthzEngine: Atomically merge labelled sources
    Recovery->>PolicyStore: Load durable policy rows
    PolicyStore-->>Recovery: Remaining rows
    Recovery->>AuthzEngine: Activate recovered labelled policies
Loading

Fix All in Claude Code Fix All in Codex Fix All in Cursor

Prompt To Fix All With AI
### Issue 1
crates/temper-platform/src/os_apps/policy_rows.rs:80-83
**Legacy cleanup deletes colliding policies**

When two policy sources share the same lossy legacy slug, this cleanup deletes the row by tenant and derived ID without validating its source or owner, causing an unrelated durable permit or forbid to disappear after recovery.

**How this was verified:** Both dots and path separators collapse to `-`, while deletion uses only the resulting tenant-scoped policy ID.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "fix(kernel): name Cedar policies in deni..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

Context used (4)

…tory races

Found while diagnosing why a contributor agent could not learn why it was
denied, and burned ~76 minutes and ~$8 hunting introspection endpoints that do
not exist (ARN-286).

Denials name their source file

A denial read `denied by policy: policy1874, policy1875` — positional ids that
map to nothing, so neither an agent nor a human can tell WHICH rule fired.
Policies loaded from an app bundle now carry the label
`{app}/{path under policies/}#{n}`, so the same denial reads
`katagami-commons/art_style.cedar#2`. The durable `policies` row id is the same
string, so a denial recovered after a restart still names the file rather than
reverting to a position. The pre-ARN-286 row id is deleted once the policy has
been re-saved under its label — fatally, not warn-only: a surviving legacy row
is loaded again on the next recovery, bringing the OLD generation of those
statements back alongside the new one, and with forbid-overrides-permit that
silently reactivates a forbid the operator believes they deleted.

Policy reload was read-then-write

Reading a tenant's sources, compiling, and swapping them in were three separate
steps, so two concurrent rebuilds both read the same pre-state, both wrote, and
the first caller's policies were gone — with both calls returning Ok.
`update_tenant_policy_sources` now does the whole read-modify-write under the
tenant write lock, and the os-app installer mirrors what the engine actually
installed rather than what it computed before the merge.

Policy load failures no longer fail open

A failed reload after an os-app install was logged and swallowed, leaving the
tenant running on whatever policy set happened to be live. It is now fatal to
the install.

Three maps had to agree about every live actor

The actor registry, the entity index and the last-accessed stamps were mutated
independently, so a failure on a cleanup path could remove an actor from one
and leave it in the others. Rather than patch the four sites that leaked, the
invariant moved into one guarded directory that owns all three and takes the
registry write lock first — the lock everything else already takes first.

Verification: 279 tests pass; build and clippy clean. The only failures in the
workspace are testcontainers tests that need a Docker daemon (not running
here), in temper-actor-runtime, which this branch does not touch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@rita-aga

Copy link
Copy Markdown
Collaborator Author

@greptile review

Comment on lines +80 to +83
let legacy_id = legacy_os_app_policy_row_id(app_name, &source.relative_path);
if legacy_id != policy_id {
policy_store
.delete_policy(tenant, &legacy_id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Legacy cleanup deletes colliding policies

When two policy sources share the same lossy legacy slug, this cleanup deletes the row by tenant and derived ID without validating its source or owner, causing an unrelated durable permit or forbid to disappear after recovery.

How this was verified: Both dots and path separators collapse to -, while deletion uses only the resulting tenant-scoped policy ID.

Knowledge Base Used: temper-authz: Cedar-Based Authorization for Agents

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/temper-platform/src/os_apps/policy_rows.rs
Line: 80-83

Comment:
**Legacy cleanup deletes colliding policies**

When two policy sources share the same lossy legacy slug, this cleanup deletes the row by tenant and derived ID without validating its source or owner, causing an unrelated durable permit or forbid to disappear after recovery.

**How this was verified:** Both dots and path separators collapse to `-`, while deletion uses only the resulting tenant-scoped policy ID.

**Knowledge Base Used:** [temper-authz: Cedar-Based Authorization for Agents](https://app.greptile.com/arni-labs/-/custom-context/knowledge-base/nerdsane/temper/-/docs/temper-authz.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code Fix in Codex Fix in Cursor

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant