feat(kernel): bound dark-window exceptions so silence cannot amplify authority (#135) - #164
Conversation
…authority (#135) A dark window is a timer-boxed conditional grant: if the owner does not answer, apply the pre-agreed default. The kernel persisted one pending exception and one timer per unique `(rule_id, rule_version, request_fingerprint)`, and that fingerprint hashes action, grant, chat and payload — all caller-varying. A worker whose quota was exhausted could change a subject line and receive a fresh pending exception, with its own timer and its own eventual silence-based Allow, per variation. Each request was individually idempotent and globally amplifying: the reviewed budget bounded approved admissions while silence was unbounded. The obvious fix is wrong. Coarsening the fingerprint to the reviewed scope would collapse the variations into one row, but the fingerprint is also the token binding — `consume_standing_rule_fired_pending` recomputes it and refuses a mismatch, which is what stops a waiver minted for one draft admitting another. Coarsening trades an unbounded queue of narrow waivers for one unbounded waiver. So the concerns get separate keys: the cap on `(rule_id, rule_version)`, the token binding on the exact request, and a new consume-time revalidation against the reviewed scope and compatibility epoch. The cap is a reviewed `max_pending_exceptions` (default 1, validated to a small ceiling) counted inside the existing `BEGIN IMMEDIATE` scheduling transaction, BEFORE anything is inserted, so a refusal cannot leave an orphan row or timer and the refusal path's safety does not depend on rollback. Deduplication runs first, so an idempotent repeat never burns a slot. Suppression stays an ordinary `ApprovalRequired`, consumes no budget, and is reported identically to having no dark window — a distinguishable signal would tell a worker exactly when the slot frees. Every lifecycle transition now stales its open exceptions in the same transaction: revoke, expiry, drift to `needs_review`, version bump, and the new startup sweep. `claim_standing_rule_dark_window` always treated `stale` as terminal; nothing had ever written it. A fired exception is accounted as an exception, not as quota: distinct audit class, counted per rule version and never pooled, and it does not refresh the lapse-after-unused clock — owner silence must not keep alive the rule that clock exists to retire. ## The classifier that read as strict and was not The `responsibility-contract` spec has always required that reusable delegation reject a dark-window Allow for communication and connector-write effects. Nothing enforced it. The first implementation here added enforcement as a *classifier*: eligible unless the action could be shown to be counterparty-facing, egress-classed, or descriptor-backed. That refused 5 actions out of roughly 50. `coolify.deploy`, `filesystem.host_write`, `secret.rotate`, `network.raw_egress` and `policy.modify_direct` stayed eligible, and all three limbs are false for an uncatalogued id, so unknown actions were fail-open. This diff's own earlier revision contained a passing test that activated an `Allow` dark window on `coolify.deploy`. A predicate that permits whatever it cannot classify is not fail-closed however its prose reads. Eligibility is now an explicit allowlist, empty today: `BTreeSet::new().contains(x)` is false for all x, so every catalogued connector write and every uncatalogued id is refused by construction rather than by classification. A startup sweep converges stored state, because activation is not retroactive: any active rule whose stored `Allow` is now ineligible moves to `needs_review` with its exceptions staled, in one transaction. D-162 was rewritten — title, index row, body and Change Log — to record why the classifier shape was wrong, so the next reader inherits the lesson. Decisions: D-159 (bounded outstanding exceptions), D-160 (cap and token binding answer different questions), D-161 (exception accounting is not quota accounting), D-162 (explicit empty allowlist, enforced and swept). One part lands ahead of its wiring: scoped consultation contains no dark-window scheduling code, so the pending row's reviewed-context digests are always NULL today and that binding is proven at store level only. See design.md §"Currently unreachable in production" for why it lands now. Filed #163 for a pre-existing timing flake found on the way, in `rate_limited_write_admission_is_refused_without_a_fence_row` — the only deterministic pin on #127's permit-before-fence ordering. Reviewed independently: APPROVE_WITH_NITS after a REQUEST_CHANGES round that caught the classifier.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe pull request bounds dark-window exceptions by rule version, binds them to request and reviewed-context digests, adds lifecycle staleness and separate fired-exception accounting, enforces fail-closed ChangesBounded dark-window exception flow
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant ActionAPI
participant StandingRuleGate
participant Store
ActionAPI->>StandingRuleGate: consult standing rule
StandingRuleGate->>Store: schedule exception with request and context digests
Store-->>StandingRuleGate: return schedule outcome
StandingRuleGate-->>ActionAPI: return covered or ordinary approval-required result
Possibly related issues
Possibly related PRs
Poem
Caution Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional.
❌ Failed checks (1 error)
✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 16
🧹 Nitpick comments (2)
crates/openspine-kernel/src/store/standing_rules_exceptions_tests.rs (1)
320-396: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winTest the cross-connection race.
This test shares one
Store, so itsMutex<Connection>serializes every caller before SQLite transaction behavior matters. It cannot detect a regression fromTransactionBehavior::Immediateto a deferred transaction.Open two independent
Storeinstances for the same database path and synchronize scheduling with a barrier.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/openspine-kernel/src/store/standing_rules_exceptions_tests.rs` around lines 320 - 396, Update concurrent_requests_cannot_cross_the_final_slot to open two independent Store instances against the same database path and distribute callers between them, avoiding a shared Store mutex. Synchronize the scheduling attempts with a barrier so SQLite transaction contention is exercised, while preserving the assertion that exactly one caller schedules the final slot and only one pending record and timer exist.crates/openspine-kernel/src/store/standing_rules_allow_eligibility_tests.rs (1)
120-170: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider reusing the shared scheduling helper.
Lines 128-170 duplicate the setup in
scheduled_exception_with_defaultinstanding_rules_staleness_tests.rs. The only difference is the two digest arguments passed toschedule_standing_rule_dark_window. Extend the shared helper to accept the reviewed-scope and compatibility digests. The duplicated block then collapses to one call.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/openspine-kernel/src/store/standing_rules_allow_eligibility_tests.rs` around lines 120 - 170, Extend the shared scheduled_exception_with_default helper to accept and forward the reviewed-scope and compatibility digest arguments to schedule_standing_rule_dark_window. Replace the duplicated setup in a_drifted_context_cannot_spend_a_pre_drift_waiver with a call to that helper, preserving the test-specific values and returned pending identifier behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/openspine-kernel/src/api/actions.rs`:
- Around line 1078-1080: Update the fired-exception handling around
finalize_standing_rule_exception_reservation so reservation finalization and
recording the fired effect occur in one transaction. Only mark the pending row
as dispatched after finalization succeeds; on accounting failure, preserve its
recoverable state and propagate the error for redelivery.
In `@crates/openspine-kernel/src/api/dark_window_cap_tests.rs`:
- Around line 235-246: Update the loop in the dark-window cap test to retain
each mediate_and_dispatch_action result, expect successful mediation, and assert
the expected GateDecision for every request. Add an assertion for the expected
suppression audit count before the existing quota and pending-row assertions.
In `@crates/openspine-kernel/src/store/standing_rules_recovery.rs`:
- Around line 295-317: Update outstanding_dark_window_exceptions to use the
scheduling transaction’s nonterminal predicate: count rows with resolved_at IS
NULL, plus owner-allowed rows whose terminal dispatch has not occurred. Keep the
rule_id and rule_version filters unchanged and reuse the existing
dispatch-state/allowed-row symbols rather than counting only unresolved rows.
In `@crates/openspine-kernel/src/store/standing_rules_scoped.rs`:
- Around line 130-136: Update the unscoped expiry flow in
consult_and_reserve_standing_rule to call stale_pending_exceptions_in_tx within
the same transaction when a rule lapses, matching the scoped path’s lifecycle
handling. Add coverage for an unscoped rule with a pending exception and verify
the exception reaches the required stale state.
In `@crates/openspine-kernel/src/store/standing_rules_staleness_tests.rs`:
- Around line 1-6: Update the module-level documentation in
standing_rules_staleness_tests.rs to remove the stale references to
“Allow-eligibility tests” and the activation refusal now covered by
standing_rules_allow_eligibility_tests.rs. Keep the remaining description
focused on lifecycle staleness tests and the writers that update stale.
In `@crates/openspine-kernel/src/store/standing_rules.rs`:
- Around line 185-197: Update the replacement flow around
stale_pending_exceptions_in_tx and the overlap update so it loads every active
rule revoked by the manifest replacement, not only the prior version of
manifest.id. Before marking each overlapping rule revoked, stale its pending
exceptions within the same transaction, preserving the existing timestamp and
version handling for each rule.
In `@graphify-out/2026-08-07/GRAPH_REPORT.md`:
- Line 4709: Update the Graphify report generator’s inline-code escaping so
nested backtick spans such as gate() render as valid Markdown, then regenerate
the generated reports with graphify update .. Apply the fix for
graphify-out/2026-08-07/GRAPH_REPORT.md lines 4709-4709 and
graphify-out/GRAPH_REPORT.md lines 4773-4773; do not edit either generated file
manually.
In
`@graphify-out/cache/ast/v0.9.10/1248127d290d2e537c9d11d905a18b370a3f340dec1e085aefcf161c3cef30d3.json`:
- Line 1: Configure the graph generator to normalize generated origin_file
values to repository-relative paths instead of absolute local paths, then
regenerate graphify-out with graphify update .. Apply the refreshed output to
graphify-out/cache/ast/v0.9.10/1248127d290d2e537c9d11d905a18b370a3f340dec1e085aefcf161c3cef30d3.json:1-1,
278aeb989433822f524983d2cd027a10ce3cecc6f138bae8b0928d390b7733ca.json:1-1,
27acf36b0dae4c908929f0a5a201dfbb0dab17092fb5b012c3535dfd7cbcf209.json:1-1,
45e9acf242f489fbbcb952a5b8e4ec26fa85cee13696dac837d7fa9fcebf440c.json:1-1, and
56f8f172a08c3f825a5a518d20235b6fda98978a28341d297be6af49967936c3.json:1-1; do
not edit generated files manually.
In
`@graphify-out/cache/ast/v0.9.10/688dceec0a5c474e5292fdc49065f8436b5e536d51874a1dfa5a76cb9e579f39.json`:
- Line 1: Configure Graphify to generate repository-relative identifiers and
omit workstation-specific origin_file paths and account segments, then run
graphify update . to regenerate the affected artifacts. Update
graphify-out/cache/ast/v0.9.10/688dceec0a5c474e5292fdc49065f8436b5e536d51874a1dfa5a76cb9e579f39.json:1-1,
6cf3cdd91930424f8c7f9719dff300a923d3b809360447071a37fa3f02907f66.json:1-1,
6dae3bf3fe40966ce23a371c87524aa4f0bdbabffb5bc81efd4792f93fcf9650.json:1-1,
7101978a59a177d5853df7dc9d0ba73163416a753980ae7884de290b62b051f8.json:1-1,
756265d4d24ff8f39cebfcb27a59049a69c4a1c1acf8be0ee7a7d2fd62feeba4.json:1-1, and
7acdf07f902e6faa8dc58ee5a6eb4b112d93060f4c850d6f9a33aaa48eb0dbfb.json:1-1 to
remove absolute origin_file values; also remove workstation-derived account
segments from
71287964f2954f021642941499a0f18874d98425f9d2c50662fc68b9cf0538c1.json:1-1. Do
not manually edit generated output.
In
`@graphify-out/cache/ast/v0.9.10/9e8ac984a39ba3c3b98fcb0a001b9ac1938884ae125472e29d07faa317b92d04.json`:
- Line 1: Configure Graphify’s path serialization to emit repository-relative
values for origin_file instead of absolute workstation paths, then run graphify
update . to regenerate the cache. Refresh
graphify-out/cache/ast/v0.9.10/9e8ac984a39ba3c3b98fcb0a001b9ac1938884ae125472e29d07faa317b92d04.json:1-1,
9f9fe9f21f17d87d16e746913e60a8c76be3a9d3c4c3a350c7cedf150d624e06.json:1-1,
b2159e49135b74419afff01c8abbf4b4a54e4902824453e4c7bded9e690a2428.json:1-1,
b830828cf0540726d10c41de8ccf2427814bab551215dcd2787a220814b80dfd.json:1-1,
bc97c434838ad6c1cc455908303330d35836b9d3acbcb6d5fd3f07b94b7375ec.json:1-1,
bfb795fa7a0de2b268424891b8920c5ebd80e98bd15c7087c71928cdbb302e7e.json:1-1, and
d16af9507d92186e56cfc97c46714ce55ea630948d5e359a2d59f84dd291bac8.json:1-1; do
not edit generated files manually.
In
`@graphify-out/cache/ast/v0.9.10/d366cceda4c28fdf82a364ecaf8b57b6b775ce2793223360c2c57cd3fc10fb5f.json`:
- Line 1: Absolute workstation paths are being embedded in Graphify cache
identifiers, origin_file values, and raw-call locations. Configure Graphify to
emit repository-relative paths, then regenerate with graphify update . rather
than editing generated JSON. Apply this to
graphify-out/cache/ast/v0.9.10/d366cceda4c28fdf82a364ecaf8b57b6b775ce2793223360c2c57cd3fc10fb5f.json:1-1,
d4bd48cae27eef334c5e0a577db8a20ecf9e16dea1c315aff5fe8ce084d8ef4e.json:1-1,
d66e400daba72cd489dc47b676c94af31b4d234b9253bfb5e3a7caa8fa16a7f0.json:1-1,
d6f257c68ceb74a21975f05363131983728e0debc08a62f906f56dd3b33d7d29.json:1-1,
dff45101825cafe000a2c118e8501418f65188a062405412df7cd3493e331c4e.json:1-1, and
e7df18d9e1cbf14bf708a5d3d49b8e3a249f9a1afedd084b0dde9eccde22b422.json:1-1.
In
`@graphify-out/cache/ast/v0.9.10/ebd7764d618601a72fa2dd5e3506de886b04f87150267cef271c29ad4c9cbb96.json`:
- Line 1: Configure the graphify path generation used by canonical_catalog and
related AST graph extraction to emit repository-relative paths instead of
machine-local absolute paths. Regenerate the cache outputs so
graphify-out/cache/ast/v0.9.10/ebd7764d618601a72fa2dd5e3506de886b04f87150267cef271c29ad4c9cbb96.json:1-1,
f27d172120c60710cd66d1a1f97758ca7f483e220c9cfe3be7527e10185b3dde.json:1-1, and
f3d0433a16bbe18268724c8223eaac2aa640f46276a8e6937756497b5639f80c.json:1-1,
replacing all absolute source_file and origin_file values with
repository-relative paths.
In `@openspec/changes/bound-dark-window-exceptions/design.md`:
- Around line 23-34: Specify text as the language for the fenced blocks at
openspec/changes/bound-dark-window-exceptions/design.md lines 23-34,
openspec/changes/bound-dark-window-exceptions/proposal.md lines 15-17, and
openspec/changes/bound-dark-window-exceptions/proposal.md lines 23-29 by
changing each opening fence to ```text.
In `@openspec/changes/bound-dark-window-exceptions/proposal.md`:
- Line 59: Update the fired-exception behavior statement in the proposal so it
says the exception occupies one max_pending_exceptions slot only while pending,
rather than consuming an allowance after firing. Preserve the existing auditing,
non-replay, and lapse-clock behavior wording.
In `@openspec/changes/bound-dark-window-exceptions/specs/standing-rules/spec.md`:
- Around line 159-169: Update the “Fired missing executor cancels and re-arms
the token” scenario to remove the ineligible email.create_draft action. Use an
abstract future eligible action or frame the scenario as a legacy-fixture
lifecycle test, while preserving the required NoExecutor failure, reservation
cancellation, unchanged budgets, and cleared token_consumed_at behavior.
In `@openspec/changes/bound-dark-window-exceptions/tasks.md`:
- Line 6: Correct the task text for StandingRuleManifest::validate by removing
trailing whitespace and stating the lower-bound requirement and small hard
maximum as separate, complete clauses.
---
Nitpick comments:
In `@crates/openspine-kernel/src/store/standing_rules_allow_eligibility_tests.rs`:
- Around line 120-170: Extend the shared scheduled_exception_with_default helper
to accept and forward the reviewed-scope and compatibility digest arguments to
schedule_standing_rule_dark_window. Replace the duplicated setup in
a_drifted_context_cannot_spend_a_pre_drift_waiver with a call to that helper,
preserving the test-specific values and returned pending identifier behavior.
In `@crates/openspine-kernel/src/store/standing_rules_exceptions_tests.rs`:
- Around line 320-396: Update concurrent_requests_cannot_cross_the_final_slot to
open two independent Store instances against the same database path and
distribute callers between them, avoiding a shared Store mutex. Synchronize the
scheduling attempts with a barrier so SQLite transaction contention is
exercised, while preserving the assertion that exactly one caller schedules the
final slot and only one pending record and timer exist.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 3d99d942-da6a-481d-8d68-b75d7bf62bb4
📒 Files selected for processing (91)
.raw/openspine-decision-log.mdcrates/openspine-kernel/src/action_catalog.rscrates/openspine-kernel/src/action_catalog_contracts.rscrates/openspine-kernel/src/action_catalog_data.rscrates/openspine-kernel/src/api/actions.rscrates/openspine-kernel/src/api/dark_window_cap_tests.rscrates/openspine-kernel/src/api/effect_executor_tests.rscrates/openspine-kernel/src/api/effect_readiness_tests.rscrates/openspine-kernel/src/api/mod.rscrates/openspine-kernel/src/api/scoped_admission_tests.rscrates/openspine-kernel/src/api/standing_rule_mediation_tests.rscrates/openspine-kernel/src/pipeline/standing_rule_timer.rscrates/openspine-kernel/src/standing_rules_gate.rscrates/openspine-kernel/src/store/migration_scoped_tests.rscrates/openspine-kernel/src/store/migration_tests.rscrates/openspine-kernel/src/store/migrations.rscrates/openspine-kernel/src/store/migrations_versioned.rscrates/openspine-kernel/src/store/mod.rscrates/openspine-kernel/src/store/standing_rule_scheduling_tests.rscrates/openspine-kernel/src/store/standing_rules.rscrates/openspine-kernel/src/store/standing_rules_activation_guard.rscrates/openspine-kernel/src/store/standing_rules_activation_tests.rscrates/openspine-kernel/src/store/standing_rules_allow_eligibility_tests.rscrates/openspine-kernel/src/store/standing_rules_budget.rscrates/openspine-kernel/src/store/standing_rules_exceptions.rscrates/openspine-kernel/src/store/standing_rules_exceptions_tests.rscrates/openspine-kernel/src/store/standing_rules_fired_token.rscrates/openspine-kernel/src/store/standing_rules_owner_resolution.rscrates/openspine-kernel/src/store/standing_rules_pending.rscrates/openspine-kernel/src/store/standing_rules_recovery.rscrates/openspine-kernel/src/store/standing_rules_row.rscrates/openspine-kernel/src/store/standing_rules_schema.rscrates/openspine-kernel/src/store/standing_rules_scoped.rscrates/openspine-kernel/src/store/standing_rules_staleness_tests.rscrates/openspine-kernel/src/store/standing_rules_tests.rscrates/openspine-schemas/src/standing_rule.rsgraphify-out/.graphify_labels.jsongraphify-out/2026-08-07/.graphify_labels.jsongraphify-out/2026-08-07/GRAPH_REPORT.mdgraphify-out/2026-08-07/graph.jsongraphify-out/2026-08-07/manifest.jsongraphify-out/GRAPH_REPORT.mdgraphify-out/cache/ast/v0.9.10/02e021567dc5dfe7007eff78694af73ce8d248bf9dbcadd4b616338d63180740.jsongraphify-out/cache/ast/v0.9.10/0be72e308b43f84e046ca9b1dff0ce067f1d835077f13587e11f7a79b458800c.jsongraphify-out/cache/ast/v0.9.10/10d870a4b4b3297228a26003e6ec43265de0eb5940b96c3dd890aaa877c95c44.jsongraphify-out/cache/ast/v0.9.10/11f12b2e111438aa8211bac72098cb012d66f8911f6015cf5d02e451cf473fe4.jsongraphify-out/cache/ast/v0.9.10/1248127d290d2e537c9d11d905a18b370a3f340dec1e085aefcf161c3cef30d3.jsongraphify-out/cache/ast/v0.9.10/278aeb989433822f524983d2cd027a10ce3cecc6f138bae8b0928d390b7733ca.jsongraphify-out/cache/ast/v0.9.10/27acf36b0dae4c908929f0a5a201dfbb0dab17092fb5b012c3535dfd7cbcf209.jsongraphify-out/cache/ast/v0.9.10/45e9acf242f489fbbcb952a5b8e4ec26fa85cee13696dac837d7fa9fcebf440c.jsongraphify-out/cache/ast/v0.9.10/56f8f172a08c3f825a5a518d20235b6fda98978a28341d297be6af49967936c3.jsongraphify-out/cache/ast/v0.9.10/5d4bf4241ec42792a86313f35a2c47a3c36644787ce2b5f15d173f41228eb4fc.jsongraphify-out/cache/ast/v0.9.10/688dceec0a5c474e5292fdc49065f8436b5e536d51874a1dfa5a76cb9e579f39.jsongraphify-out/cache/ast/v0.9.10/6cf3cdd91930424f8c7f9719dff300a923d3b809360447071a37fa3f02907f66.jsongraphify-out/cache/ast/v0.9.10/6dae3bf3fe40966ce23a371c87524aa4f0bdbabffb5bc81efd4792f93fcf9650.jsongraphify-out/cache/ast/v0.9.10/7101978a59a177d5853df7dc9d0ba73163416a753980ae7884de290b62b051f8.jsongraphify-out/cache/ast/v0.9.10/71287964f2954f021642941499a0f18874d98425f9d2c50662fc68b9cf0538c1.jsongraphify-out/cache/ast/v0.9.10/756265d4d24ff8f39cebfcb27a59049a69c4a1c1acf8be0ee7a7d2fd62feeba4.jsongraphify-out/cache/ast/v0.9.10/7acdf07f902e6faa8dc58ee5a6eb4b112d93060f4c850d6f9a33aaa48eb0dbfb.jsongraphify-out/cache/ast/v0.9.10/7bbf851bc255d7c2fb6ee97657df32dde8cdbade4a096bb4fef212f468850f51.jsongraphify-out/cache/ast/v0.9.10/80cf65bb2c64dfa103aa5c07052ae53792f564b2c83ed462d45fd458860c055b.jsongraphify-out/cache/ast/v0.9.10/82077a8bfeba42e9bc0a0e2ce44f2863c5e124c7e57695653c72776be4e57d8a.jsongraphify-out/cache/ast/v0.9.10/879995db8efbdf3b1fe3f28a8bed1ec3a50f073b1684a9f581cd59af4e1d139e.jsongraphify-out/cache/ast/v0.9.10/8c652ab32cb2875ac8093e27139091e2123a60ac04f7ad9d4dc854a30733424e.jsongraphify-out/cache/ast/v0.9.10/926974e9b7e5ac7857cc01ca7bda61380afb514920f823928a243e9854a1ae1a.jsongraphify-out/cache/ast/v0.9.10/9bf3b0ef55cc068abdca76371ece3cdc07b8c2926692277a319dd656fba7ac52.jsongraphify-out/cache/ast/v0.9.10/9e8ac984a39ba3c3b98fcb0a001b9ac1938884ae125472e29d07faa317b92d04.jsongraphify-out/cache/ast/v0.9.10/9f9fe9f21f17d87d16e746913e60a8c76be3a9d3c4c3a350c7cedf150d624e06.jsongraphify-out/cache/ast/v0.9.10/b2159e49135b74419afff01c8abbf4b4a54e4902824453e4c7bded9e690a2428.jsongraphify-out/cache/ast/v0.9.10/b830828cf0540726d10c41de8ccf2427814bab551215dcd2787a220814b80dfd.jsongraphify-out/cache/ast/v0.9.10/bc97c434838ad6c1cc455908303330d35836b9d3acbcb6d5fd3f07b94b7375ec.jsongraphify-out/cache/ast/v0.9.10/bfb795fa7a0de2b268424891b8920c5ebd80e98bd15c7087c71928cdbb302e7e.jsongraphify-out/cache/ast/v0.9.10/d16af9507d92186e56cfc97c46714ce55ea630948d5e359a2d59f84dd291bac8.jsongraphify-out/cache/ast/v0.9.10/d366cceda4c28fdf82a364ecaf8b57b6b775ce2793223360c2c57cd3fc10fb5f.jsongraphify-out/cache/ast/v0.9.10/d4bd48cae27eef334c5e0a577db8a20ecf9e16dea1c315aff5fe8ce084d8ef4e.jsongraphify-out/cache/ast/v0.9.10/d66e400daba72cd489dc47b676c94af31b4d234b9253bfb5e3a7caa8fa16a7f0.jsongraphify-out/cache/ast/v0.9.10/d6f257c68ceb74a21975f05363131983728e0debc08a62f906f56dd3b33d7d29.jsongraphify-out/cache/ast/v0.9.10/dff45101825cafe000a2c118e8501418f65188a062405412df7cd3493e331c4e.jsongraphify-out/cache/ast/v0.9.10/e7df18d9e1cbf14bf708a5d3d49b8e3a249f9a1afedd084b0dde9eccde22b422.jsongraphify-out/cache/ast/v0.9.10/ebd7764d618601a72fa2dd5e3506de886b04f87150267cef271c29ad4c9cbb96.jsongraphify-out/cache/ast/v0.9.10/f27d172120c60710cd66d1a1f97758ca7f483e220c9cfe3be7527e10185b3dde.jsongraphify-out/cache/ast/v0.9.10/f3d0433a16bbe18268724c8223eaac2aa640f46276a8e6937756497b5639f80c.jsongraphify-out/cache/ast/v0.9.10/f3fe1bbb61a380efe9fadb47d7c7a378d8271c4247fe743b9479729d0dc1a5a0.jsongraphify-out/cache/stat-index.jsongraphify-out/graph.jsongraphify-out/manifest.jsonopenspec/changes/bound-dark-window-exceptions/design.mdopenspec/changes/bound-dark-window-exceptions/proposal.mdopenspec/changes/bound-dark-window-exceptions/specs/responsibility-contract/spec.mdopenspec/changes/bound-dark-window-exceptions/specs/standing-rules/spec.mdopenspec/changes/bound-dark-window-exceptions/tasks.md
| // D-161: a fired exception is accounted as an exception, so it | ||
| // commits its usage without refreshing the lapse clock. | ||
| if let Err(err) = state.store.finalize_standing_rule_exception_reservation( |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Keep the fired exception recoverable when accounting fails.
finalize_standing_rule_exception_reservation can fail, but Line 1091 still marks the pending row as dispatched. This leaves the exception reservation reserved while recovery no longer sees the row as claimed and unredriven.
Finalize the reservation and record the fired effect in one transaction. If finalization fails, do not mark the effect as dispatched. Surface the accounting failure for recovery.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/openspine-kernel/src/api/actions.rs` around lines 1078 - 1080, Update
the fired-exception handling around finalize_standing_rule_exception_reservation
so reservation finalization and recording the fired effect occur in one
transaction. Only mark the pending row as dispatched after finalization
succeeds; on accounting failure, preserve its recoverable state and propagate
the error for redelivery.
| for i in 0..8 { | ||
| let _ = mediate_and_dispatch_action( | ||
| &state, | ||
| &grant, | ||
| ActionId::new(ACTION), | ||
| OWNER_CHAT_ID, | ||
| Some(&json!({ "text": format!("request {i}") })), | ||
| FailureSurface::DirectResponse, | ||
| None, | ||
| ) | ||
| .await; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Check every mediation result.
This loop discards every Result. A failure on a suppressed request can pass while the final quota and pending-row assertions still hold.
Use .expect(...) and assert the expected GateDecision for each iteration. Also assert the expected suppression audit count.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/openspine-kernel/src/api/dark_window_cap_tests.rs` around lines 235 -
246, Update the loop in the dark-window cap test to retain each
mediate_and_dispatch_action result, expect successful mediation, and assert the
expected GateDecision for every request. Add an assertion for the expected
suppression audit count before the existing quota and pending-row assertions.
| /// Outstanding (nonterminal) pending dark-window exceptions for one rule | ||
| /// version — the same predicate the scheduling transaction enforces the | ||
| /// reviewed `max_pending_exceptions` cap with (#135). | ||
| /// | ||
| /// Version-aware, because the cap is: exceptions reviewed against a prior | ||
| /// version are staled when a new version activates and must not be counted | ||
| /// against the new one. The pre-#135 shape of this helper counted across | ||
| /// every version and had no caller; a second, looser counting path beside | ||
| /// the enforced one is exactly how a cap drifts out of agreement with | ||
| /// itself, so it is replaced rather than kept alongside. | ||
| pub fn outstanding_dark_window_exceptions( | ||
| &self, | ||
| rule_id: &str, | ||
| rule_version: u32, | ||
| ) -> Result<usize, StoreError> { | ||
| let conn = self.conn.lock(); | ||
| let count: i64 = conn.query_row( | ||
| "SELECT COUNT(*) FROM standing_rule_pending_actions \ | ||
| WHERE rule_id = ?1 AND resolved_at IS NULL", | ||
| params![rule_id], | ||
| WHERE rule_id = ?1 AND rule_version = ?2 AND resolved_at IS NULL", | ||
| params![rule_id, rule_version as i64], | ||
| |row| row.get(0), | ||
| )?; | ||
| Ok(count as usize) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Count all nonterminal exceptions.
Line 313 excludes every owner-allowed exception because owner resolution sets resolved_at. An allowed row remains outstanding until it reaches its terminal dispatch state. This helper can therefore underreport the version-scoped exception count.
Use the same nonterminal predicate as the scheduling transaction. Count unresolved rows and allowed rows that are not yet dispatched.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/openspine-kernel/src/store/standing_rules_recovery.rs` around lines
295 - 317, Update outstanding_dark_window_exceptions to use the scheduling
transaction’s nonterminal predicate: count rows with resolved_at IS NULL, plus
owner-allowed rows whose terminal dispatch has not occurred. Keep the rule_id
and rule_version filters unchanged and reuse the existing
dispatch-state/allowed-row symbols rather than counting only unresolved rows.
| // #135: a lapsed rule leaves no fireable exception behind. | ||
| super::standing_rules_exceptions::stale_pending_exceptions_in_tx( | ||
| &tx, | ||
| &rule.rule_id, | ||
| Some(rule.version), | ||
| now_nanos, | ||
| )?; |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Stale exceptions in the unscoped expiry path.
These lines handle only scoped consultation. crates/openspine-kernel/src/standing_rules_gate.rs still uses consult_and_reserve_standing_rule for action-keyed admission. Its separate expiry path must also call stale_pending_exceptions_in_tx in the same transaction.
Otherwise, expiry through that path leaves an unresolved exception outside the required stale lifecycle state. Add coverage for an unscoped rule with a pending exception.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/openspine-kernel/src/store/standing_rules_scoped.rs` around lines 130
- 136, Update the unscoped expiry flow in consult_and_reserve_standing_rule to
call stale_pending_exceptions_in_tx within the same transaction when a rule
lapses, matching the scoped path’s lifecycle handling. Add coverage for an
unscoped rule with a pending exception and verify the exception reaches the
required stale state.
| //! Lifecycle staleness and Allow-eligibility tests (#135). | ||
| //! | ||
| //! `claim_standing_rule_dark_window` has always treated `stale` as terminal; | ||
| //! nothing ever wrote it. These pin the writers that now do, and the | ||
| //! activation refusal that gives the `responsibility-contract` prohibition on | ||
| //! communication dark-window Allow its first enforcing code. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Update the module doc after the split.
The header states that this file holds "Allow-eligibility tests" and "the activation refusal". Those tests now live in standing_rules_allow_eligibility_tests.rs, whose own header records the split. Remove the stale sentence so the doc matches the file contents.
📝 Proposed doc fix
-//! Lifecycle staleness and Allow-eligibility tests (`#135`).
+//! Lifecycle staleness tests (`#135`).
//!
//! `claim_standing_rule_dark_window` has always treated `stale` as terminal;
-//! nothing ever wrote it. These pin the writers that now do, and the
-//! activation refusal that gives the `responsibility-contract` prohibition on
-//! communication dark-window Allow its first enforcing code.
+//! nothing ever wrote it. These pin the writers that now do. The
+//! Allow-eligibility and activation-refusal tests live in
+//! `standing_rules_allow_eligibility_tests.rs`.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| //! Lifecycle staleness and Allow-eligibility tests (#135). | |
| //! | |
| //! `claim_standing_rule_dark_window` has always treated `stale` as terminal; | |
| //! nothing ever wrote it. These pin the writers that now do, and the | |
| //! activation refusal that gives the `responsibility-contract` prohibition on | |
| //! communication dark-window Allow its first enforcing code. | |
| //! Lifecycle staleness tests (`#135`). | |
| //! | |
| //! `claim_standing_rule_dark_window` has always treated `stale` as terminal; | |
| //! nothing ever wrote it. These pin the writers that now do. The | |
| //! Allow-eligibility and activation-refusal tests live in | |
| //! `standing_rules_allow_eligibility_tests.rs`. |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/openspine-kernel/src/store/standing_rules_staleness_tests.rs` around
lines 1 - 6, Update the module-level documentation in
standing_rules_staleness_tests.rs to remove the stale references to
“Allow-eligibility tests” and the activation refusal now covered by
standing_rules_allow_eligibility_tests.rs. Keep the remaining description
focused on lifecycle staleness tests and the writers that update stale.
| @@ -0,0 +1 @@ | |||
| {"nodes": [{"id": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_rs", "label": "action_catalog.rs", "file_type": "code", "source_file": "crates/openspine-kernel/src/action_catalog.rs", "source_location": "L1"}, {"id": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_id", "label": "id()", "file_type": "code", "source_file": "crates/openspine-kernel/src/action_catalog.rs", "source_location": "L22"}, {"id": "actionid", "label": "ActionId", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/george/repos/openspine/crates/openspine-kernel/src/action_catalog.rs"}, {"id": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_canonical_catalog", "label": "canonical_catalog()", "file_type": "code", "source_file": "crates/openspine-kernel/src/action_catalog.rs", "source_location": "L35"}, {"id": "actioncatalog", "label": "ActionCatalog", "file_type": "code", "source_file": "", "source_location": "", "origin_file": "/Users/george/repos/openspine/crates/openspine-kernel/src/action_catalog.rs"}, {"id": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_test_catalog_effect_paths_are_fully_enumerated_and_classified", "label": "test_catalog_effect_paths_are_fully_enumerated_and_classified()", "file_type": "code", "source_file": "crates/openspine-kernel/src/action_catalog.rs", "source_location": "L220"}, {"id": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_counterparty_classification_is_kernel_owned_and_fails_closed", "label": "counterparty_classification_is_kernel_owned_and_fails_closed()", "file_type": "code", "source_file": "crates/openspine-kernel/src/action_catalog.rs", "source_location": "L260"}, {"id": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_handler_registry_requires_explicit_classification", "label": "handler_registry_requires_explicit_classification()", "file_type": "code", "source_file": "crates/openspine-kernel/src/action_catalog.rs", "source_location": "L268"}, {"id": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_worker_actions_declare_no_egress_and_no_output_channel", "label": "worker_actions_declare_no_egress_and_no_output_channel()", "file_type": "code", "source_file": "crates/openspine-kernel/src/action_catalog.rs", "source_location": "L310"}, {"id": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_every_delegation_descriptor_names_a_catalogued_action", "label": "every_delegation_descriptor_names_a_catalogued_action()", "file_type": "code", "source_file": "crates/openspine-kernel/src/action_catalog.rs", "source_location": "L336"}, {"id": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_email_draft_has_a_reviewed_descriptor_and_a_registered_implementation", "label": "email_draft_has_a_reviewed_descriptor_and_a_registered_implementation()", "file_type": "code", "source_file": "crates/openspine-kernel/src/action_catalog.rs", "source_location": "L348"}, {"id": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_dark_window_allow_eligibility_allowlist_is_empty_and_fails_closed", "label": "dark_window_allow_eligibility_allowlist_is_empty_and_fails_closed()", "file_type": "code", "source_file": "crates/openspine-kernel/src/action_catalog.rs", "source_location": "L394"}, {"id": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_non_effect_stub_allowlist_is_explicit_and_fails_closed", "label": "non_effect_stub_allowlist_is_explicit_and_fails_closed()", "file_type": "code", "source_file": "crates/openspine-kernel/src/action_catalog.rs", "source_location": "L422"}, {"id": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_overlay_export_restore_are_non_delegable_with_no_egress", "label": "overlay_export_restore_are_non_delegable_with_no_egress()", "file_type": "code", "source_file": "crates/openspine-kernel/src/action_catalog.rs", "source_location": "L473"}], "edges": [{"source": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_rs", "target": "action", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "crates/openspine-kernel/src/action_catalog.rs", "source_location": "L9", "weight": 1.0, "context": "import"}, {"source": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_rs", "target": "selectiontokentype", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "crates/openspine-kernel/src/action_catalog.rs", "source_location": "L10", "weight": 1.0, "context": "import"}, {"source": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_rs", "target": "action_catalog_contracts", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "crates/openspine-kernel/src/action_catalog.rs", "source_location": "L19", "weight": 1.0, "context": "import"}, {"source": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_rs", "target": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_id", "relation": "contains", "confidence": "EXTRACTED", "source_file": "crates/openspine-kernel/src/action_catalog.rs", "source_location": "L22", "weight": 1.0}, {"source": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_id", "target": "actionid", "relation": "references", "confidence": "EXTRACTED", "source_file": "crates/openspine-kernel/src/action_catalog.rs", "source_location": "L22", "weight": 1.0, "context": "return_type"}, {"source": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_rs", "target": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_canonical_catalog", "relation": "contains", "confidence": "EXTRACTED", "source_file": "crates/openspine-kernel/src/action_catalog.rs", "source_location": "L35", "weight": 1.0}, {"source": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_canonical_catalog", "target": "actioncatalog", "relation": "references", "confidence": "EXTRACTED", "source_file": "crates/openspine-kernel/src/action_catalog.rs", "source_location": "L35", "weight": 1.0, "context": "return_type"}, {"source": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_rs", "target": "super", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "crates/openspine-kernel/src/action_catalog.rs", "source_location": "L214", "weight": 1.0, "context": "import"}, {"source": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_rs", "target": "action", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "crates/openspine-kernel/src/action_catalog.rs", "source_location": "L215", "weight": 1.0, "context": "import"}, {"source": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_rs", "target": "egressclass", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "crates/openspine-kernel/src/action_catalog.rs", "source_location": "L218", "weight": 1.0, "context": "import"}, {"source": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_rs", "target": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_test_catalog_effect_paths_are_fully_enumerated_and_classified", "relation": "contains", "confidence": "EXTRACTED", "source_file": "crates/openspine-kernel/src/action_catalog.rs", "source_location": "L220", "weight": 1.0}, {"source": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_rs", "target": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_counterparty_classification_is_kernel_owned_and_fails_closed", "relation": "contains", "confidence": "EXTRACTED", "source_file": "crates/openspine-kernel/src/action_catalog.rs", "source_location": "L260", "weight": 1.0}, {"source": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_rs", "target": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_handler_registry_requires_explicit_classification", "relation": "contains", "confidence": "EXTRACTED", "source_file": "crates/openspine-kernel/src/action_catalog.rs", "source_location": "L268", "weight": 1.0}, {"source": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_rs", "target": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_worker_actions_declare_no_egress_and_no_output_channel", "relation": "contains", "confidence": "EXTRACTED", "source_file": "crates/openspine-kernel/src/action_catalog.rs", "source_location": "L310", "weight": 1.0}, {"source": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_rs", "target": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_every_delegation_descriptor_names_a_catalogued_action", "relation": "contains", "confidence": "EXTRACTED", "source_file": "crates/openspine-kernel/src/action_catalog.rs", "source_location": "L336", "weight": 1.0}, {"source": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_rs", "target": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_email_draft_has_a_reviewed_descriptor_and_a_registered_implementation", "relation": "contains", "confidence": "EXTRACTED", "source_file": "crates/openspine-kernel/src/action_catalog.rs", "source_location": "L348", "weight": 1.0}, {"source": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_rs", "target": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_dark_window_allow_eligibility_allowlist_is_empty_and_fails_closed", "relation": "contains", "confidence": "EXTRACTED", "source_file": "crates/openspine-kernel/src/action_catalog.rs", "source_location": "L394", "weight": 1.0}, {"source": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_rs", "target": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_non_effect_stub_allowlist_is_explicit_and_fails_closed", "relation": "contains", "confidence": "EXTRACTED", "source_file": "crates/openspine-kernel/src/action_catalog.rs", "source_location": "L422", "weight": 1.0}, {"source": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_rs", "target": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_overlay_export_restore_are_non_delegable_with_no_egress", "relation": "contains", "confidence": "EXTRACTED", "source_file": "crates/openspine-kernel/src/action_catalog.rs", "source_location": "L473", "weight": 1.0}, {"source": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_test_catalog_effect_paths_are_fully_enumerated_and_classified", "target": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_canonical_catalog", "relation": "calls", "context": "call", "confidence": "EXTRACTED", "source_file": "crates/openspine-kernel/src/action_catalog.rs", "source_location": "L221", "weight": 1.0}, {"source": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_counterparty_classification_is_kernel_owned_and_fails_closed", "target": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_canonical_catalog", "relation": "calls", "context": "call", "confidence": "EXTRACTED", "source_file": "crates/openspine-kernel/src/action_catalog.rs", "source_location": "L261", "weight": 1.0}, {"source": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_handler_registry_requires_explicit_classification", "target": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_canonical_catalog", "relation": "calls", "context": "call", "confidence": "EXTRACTED", "source_file": "crates/openspine-kernel/src/action_catalog.rs", "source_location": "L273", "weight": 1.0}, {"source": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_worker_actions_declare_no_egress_and_no_output_channel", "target": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_canonical_catalog", "relation": "calls", "context": "call", "confidence": "EXTRACTED", "source_file": "crates/openspine-kernel/src/action_catalog.rs", "source_location": "L315", "weight": 1.0}, {"source": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_every_delegation_descriptor_names_a_catalogued_action", "target": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_canonical_catalog", "relation": "calls", "context": "call", "confidence": "EXTRACTED", "source_file": "crates/openspine-kernel/src/action_catalog.rs", "source_location": "L337", "weight": 1.0}, {"source": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_email_draft_has_a_reviewed_descriptor_and_a_registered_implementation", "target": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_canonical_catalog", "relation": "calls", "context": "call", "confidence": "EXTRACTED", "source_file": "crates/openspine-kernel/src/action_catalog.rs", "source_location": "L349", "weight": 1.0}, {"source": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_non_effect_stub_allowlist_is_explicit_and_fails_closed", "target": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_canonical_catalog", "relation": "calls", "context": "call", "confidence": "EXTRACTED", "source_file": "crates/openspine-kernel/src/action_catalog.rs", "source_location": "L423", "weight": 1.0}, {"source": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_overlay_export_restore_are_non_delegable_with_no_egress", "target": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_canonical_catalog", "relation": "calls", "context": "call", "confidence": "EXTRACTED", "source_file": "crates/openspine-kernel/src/action_catalog.rs", "source_location": "L474", "weight": 1.0}], "raw_calls": [{"caller_nid": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_canonical_catalog", "callee": "with_effect_paths", "is_member_call": true, "source_file": "/Users/george/repos/openspine/crates/openspine-kernel/src/action_catalog.rs", "source_location": "L93"}, {"caller_nid": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_canonical_catalog", "callee": "with_implementation_descriptors", "is_member_call": true, "source_file": "/Users/george/repos/openspine/crates/openspine-kernel/src/action_catalog.rs", "source_location": "L93"}, {"caller_nid": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_canonical_catalog", "callee": "with_delegation_descriptors", "is_member_call": true, "source_file": "/Users/george/repos/openspine/crates/openspine-kernel/src/action_catalog.rs", "source_location": "L93"}, {"caller_nid": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_canonical_catalog", "callee": "with_egress_declarations", "is_member_call": true, "source_file": "/Users/george/repos/openspine/crates/openspine-kernel/src/action_catalog.rs", "source_location": "L93"}, {"caller_nid": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_canonical_catalog", "callee": "with_token_requiring", "is_member_call": true, "source_file": "/Users/george/repos/openspine/crates/openspine-kernel/src/action_catalog.rs", "source_location": "L93"}, {"caller_nid": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_canonical_catalog", "callee": "with_counterparty_facing", "is_member_call": true, "source_file": "/Users/george/repos/openspine/crates/openspine-kernel/src/action_catalog.rs", "source_location": "L93"}, {"caller_nid": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_canonical_catalog", "callee": "with_non_effect_stub", "is_member_call": true, "source_file": "/Users/george/repos/openspine/crates/openspine-kernel/src/action_catalog.rs", "source_location": "L93"}, {"caller_nid": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_canonical_catalog", "callee": "with_non_delegable", "is_member_call": true, "source_file": "/Users/george/repos/openspine/crates/openspine-kernel/src/action_catalog.rs", "source_location": "L93"}, {"caller_nid": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_canonical_catalog", "callee": "with_kernel_origin", "is_member_call": true, "source_file": "/Users/george/repos/openspine/crates/openspine-kernel/src/action_catalog.rs", "source_location": "L93"}, {"caller_nid": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_test_catalog_effect_paths_are_fully_enumerated_and_classified", "callee": "effect_paths", "is_member_call": true, "source_file": "/Users/george/repos/openspine/crates/openspine-kernel/src/action_catalog.rs", "source_location": "L222"}, {"caller_nid": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_test_catalog_effect_paths_are_fully_enumerated_and_classified", "callee": "as_str", "is_member_call": true, "source_file": "/Users/george/repos/openspine/crates/openspine-kernel/src/action_catalog.rs", "source_location": "L229"}, {"caller_nid": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_test_catalog_effect_paths_are_fully_enumerated_and_classified", "callee": "find", "is_member_call": true, "source_file": "/Users/george/repos/openspine/crates/openspine-kernel/src/action_catalog.rs", "source_location": "L234"}, {"caller_nid": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_handler_registry_requires_explicit_classification", "callee": "registered_action_ids", "is_member_call": true, "source_file": "/Users/george/repos/openspine/crates/openspine-kernel/src/action_catalog.rs", "source_location": "L275"}, {"caller_nid": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_handler_registry_requires_explicit_classification", "callee": "unwrap_or_else", "is_member_call": true, "source_file": "/Users/george/repos/openspine/crates/openspine-kernel/src/action_catalog.rs", "source_location": "L276"}, {"caller_nid": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_handler_registry_requires_explicit_classification", "callee": "egress_decl_for", "is_member_call": true, "source_file": "/Users/george/repos/openspine/crates/openspine-kernel/src/action_catalog.rs", "source_location": "L276"}, {"caller_nid": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_handler_registry_requires_explicit_classification", "callee": "as_str", "is_member_call": true, "source_file": "/Users/george/repos/openspine/crates/openspine-kernel/src/action_catalog.rs", "source_location": "L279"}, {"caller_nid": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_handler_registry_requires_explicit_classification", "callee": "as_ref", "is_member_call": true, "source_file": "/Users/george/repos/openspine/crates/openspine-kernel/src/action_catalog.rs", "source_location": "L288"}, {"caller_nid": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_handler_registry_requires_explicit_classification", "callee": "as_str", "is_member_call": true, "source_file": "/Users/george/repos/openspine/crates/openspine-kernel/src/action_catalog.rs", "source_location": "L296"}, {"caller_nid": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_worker_actions_declare_no_egress_and_no_output_channel", "callee": "egress_decl_for", "is_member_call": true, "source_file": "/Users/george/repos/openspine/crates/openspine-kernel/src/action_catalog.rs", "source_location": "L321"}, {"caller_nid": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_email_draft_has_a_reviewed_descriptor_and_a_registered_implementation", "callee": "delegation_descriptor_for", "is_member_call": true, "source_file": "/Users/george/repos/openspine/crates/openspine-kernel/src/action_catalog.rs", "source_location": "L350"}, {"caller_nid": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_email_draft_has_a_reviewed_descriptor_and_a_registered_implementation", "callee": "validated_delegation_contract", "is_member_call": true, "source_file": "/Users/george/repos/openspine/crates/openspine-kernel/src/action_catalog.rs", "source_location": "L364"}, {"caller_nid": "users_george_repos_openspine_crates_openspine_kernel_src_action_catalog_overlay_export_restore_are_non_delegable_with_no_egress", "callee": "egress_decl_for", "is_member_call": true, "source_file": "/Users/george/repos/openspine/crates/openspine-kernel/src/action_catalog.rs", "source_location": "L484"}]} | |||
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift
Remove machine-local paths from generated graph data.
The generator emits /Users/george/repos/openspine into committed cache data. This exposes a local username and makes generated output differ by checkout location. Configure graphify to emit repository-relative paths, then regenerate the output.
graphify-out/cache/ast/v0.9.10/ebd7764d618601a72fa2dd5e3506de886b04f87150267cef271c29ad4c9cbb96.json#L1-L1: replace absolute source and origin paths with repository-relative paths.graphify-out/cache/ast/v0.9.10/f27d172120c60710cd66d1a1f97758ca7f483e220c9cfe3be7527e10185b3dde.json#L1-L1: replace absolute call-source paths with repository-relative paths.graphify-out/cache/ast/v0.9.10/f3d0433a16bbe18268724c8223eaac2aa640f46276a8e6937756497b5639f80c.json#L1-L1: replace absolute call-source paths with repository-relative paths.
📍 Affects 3 files
graphify-out/cache/ast/v0.9.10/ebd7764d618601a72fa2dd5e3506de886b04f87150267cef271c29ad4c9cbb96.json#L1-L1(this comment)graphify-out/cache/ast/v0.9.10/f27d172120c60710cd66d1a1f97758ca7f483e220c9cfe3be7527e10185b3dde.json#L1-L1graphify-out/cache/ast/v0.9.10/f3d0433a16bbe18268724c8223eaac2aa640f46276a8e6937756497b5639f80c.json#L1-L1
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@graphify-out/cache/ast/v0.9.10/ebd7764d618601a72fa2dd5e3506de886b04f87150267cef271c29ad4c9cbb96.json`
at line 1, Configure the graphify path generation used by canonical_catalog and
related AST graph extraction to emit repository-relative paths instead of
machine-local absolute paths. Regenerate the cache outputs so
graphify-out/cache/ast/v0.9.10/ebd7764d618601a72fa2dd5e3506de886b04f87150267cef271c29ad4c9cbb96.json:1-1,
f27d172120c60710cd66d1a1f97758ca7f483e220c9cfe3be7527e10185b3dde.json:1-1, and
f3d0433a16bbe18268724c8223eaac2aa640f46276a8e6937756497b5639f80c.json:1-1,
replacing all absolute source_file and origin_file values with
repository-relative paths.
| ``` | ||
| BEGIN IMMEDIATE | ||
| 1. dedup: does a row already exist for (rule_id, rule_version, fingerprint)? | ||
| yes -> existing idempotent behaviour, consume no slot, return it | ||
| 2. count nonterminal rows for (rule_id, rule_version) | ||
| resolved_at IS NULL -- open | ||
| (a resolved row, allowed/denied/stale, is not outstanding) | ||
| 3. count >= max_pending_exceptions -> SUPPRESS: no row, no timer, | ||
| no *scheduled* evidence (a refusal audit IS written) | ||
| 4. otherwise insert the row, the timer, and the scheduled audit | ||
| COMMIT | ||
| ``` |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Specify a language for each fenced block.
markdownlint reports MD040 for these blocks. Use text for the digest and pseudocode examples.
openspec/changes/bound-dark-window-exceptions/design.md#L23-L34: change the opening fence to```text.openspec/changes/bound-dark-window-exceptions/proposal.md#L15-L17: change the opening fence to```text.openspec/changes/bound-dark-window-exceptions/proposal.md#L23-L29: change the opening fence to```text.
🧰 Tools
🪛 markdownlint-cli2 (0.23.2)
[warning] 23-23: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
📍 Affects 2 files
openspec/changes/bound-dark-window-exceptions/design.md#L23-L34(this comment)openspec/changes/bound-dark-window-exceptions/proposal.md#L15-L17openspec/changes/bound-dark-window-exceptions/proposal.md#L23-L29
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@openspec/changes/bound-dark-window-exceptions/design.md` around lines 23 -
34, Specify text as the language for the fenced blocks at
openspec/changes/bound-dark-window-exceptions/design.md lines 23-34,
openspec/changes/bound-dark-window-exceptions/proposal.md lines 15-17, and
openspec/changes/bound-dark-window-exceptions/proposal.md lines 23-29 by
changing each opening fence to ```text.
Source: Linters/SAST tools
| - Same-request scheduling stays idempotent and does not consume a second slot. | ||
| - A distinct request at the cap stays `ApprovalRequired`, creates no pending row and no timer, consumes no quota or rate, and reports no pending default. | ||
| - A fired token whose reviewed scope or compatibility epoch no longer matches the freshly resolved context grants no authority and leaves the pending row terminal. | ||
| - A fired exception is audited in its own class, consumes exactly one exception allowance, cannot be replayed, and does not refresh the rule's lapse-after-unused clock. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Correct the exception-slot wording.
A fired exception has resolved, so it no longer occupies max_pending_exceptions. Replace “consumes exactly one exception allowance” with wording that says it occupies one slot while pending.
The current wording conflicts with the design and standing-rules specification, which define resolved exceptions as no longer outstanding.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@openspec/changes/bound-dark-window-exceptions/proposal.md` at line 59, Update
the fired-exception behavior statement in the proposal so it says the exception
occupies one max_pending_exceptions slot only while pending, rather than
consuming an allowance after firing. Preserve the existing auditing, non-replay,
and lapse-clock behavior wording.
| #### Scenario: Fired missing executor cancels and re-arms the token | ||
|
|
||
| - **WHEN** a fired one-use Allow token admits `email.create_draft` during the dark window | ||
| - **AND** generic dispatch reaches the action with no runnable executor | ||
| - **THEN** dispatch MUST return the typed `DispatchError::NoExecutor` failure | ||
| - **AND** the fired reservation MUST be cancelled rather than finalized | ||
| - **AND** the database MUST show zero reserved rows and zero committed rows | ||
| - **AND** the full remaining quota and rate budget MUST be unchanged | ||
| - **AND** `token_consumed_at` MUST be NULL again | ||
|
|
||
| Test: `fired_token_no_executor_cancels_reservation_and_rearms_once` |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Do not specify email.create_draft for this Allow-default scenario.
The current design prohibits a dark-window Allow for email.create_draft, and the corresponding readiness test now uses coolify.deploy through the legacy test installer.
Use an abstract future eligible action, or describe this as a legacy-fixture lifecycle test. The specification must not require an impossible production flow.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@openspec/changes/bound-dark-window-exceptions/specs/standing-rules/spec.md`
around lines 159 - 169, Update the “Fired missing executor cancels and re-arms
the token” scenario to remove the ineligible email.create_draft action. Use an
abstract future eligible action or frame the scenario as a legacy-fixture
lifecycle test, while preserving the required NoExecutor failure, reservation
cancellation, unchanged budgets, and cleared token_consumed_at behavior.
| ## 1. Make the exception allowance reviewable data | ||
|
|
||
| - [x] Add `max_pending_exceptions` to `DarkWindowConfig`, defaulting to 1, serialized so the owner review object shows it. | ||
| - [x] Extend `StandingRuleManifest::validate` to require `1 <= max_pending_exceptions <= ` a small hard maximum. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Correct the validation task text.
The code span contains trailing whitespace and ends with an incomplete inequality. State the lower-bound validation and the hard maximum as separate clauses.
🧰 Tools
🪛 markdownlint-cli2 (0.23.2)
[warning] 6-6: Spaces inside code span elements
(MD038, no-space-in-code)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@openspec/changes/bound-dark-window-exceptions/tasks.md` at line 6, Correct
the task text for StandingRuleManifest::validate by removing trailing whitespace
and stating the lower-bound requirement and small hard maximum as separate,
complete clauses.
Source: Linters/SAST tools
A dark window is a timer-boxed conditional grant: if the owner does not
answer, apply the pre-agreed default. The kernel persisted one pending
exception and one timer per unique
(rule_id, rule_version, request_fingerprint), and that fingerprint hashes action, grant, chat andpayload — all caller-varying. A worker whose quota was exhausted could
change a subject line and receive a fresh pending exception, with its own
timer and its own eventual silence-based Allow, per variation. Each request
was individually idempotent and globally amplifying: the reviewed budget
bounded approved admissions while silence was unbounded.
The obvious fix is wrong. Coarsening the fingerprint to the reviewed scope
would collapse the variations into one row, but the fingerprint is also the
token binding —
consume_standing_rule_fired_pendingrecomputes it andrefuses a mismatch, which is what stops a waiver minted for one draft
admitting another. Coarsening trades an unbounded queue of narrow waivers
for one unbounded waiver. So the concerns get separate keys: the cap on
(rule_id, rule_version), the token binding on the exact request, and a newconsume-time revalidation against the reviewed scope and compatibility
epoch.
The cap is a reviewed
max_pending_exceptions(default 1, validated to asmall ceiling) counted inside the existing
BEGIN IMMEDIATEschedulingtransaction, BEFORE anything is inserted, so a refusal cannot leave an
orphan row or timer and the refusal path's safety does not depend on
rollback. Deduplication runs first, so an idempotent repeat never burns a
slot. Suppression stays an ordinary
ApprovalRequired, consumes no budget,and is reported identically to having no dark window — a distinguishable
signal would tell a worker exactly when the slot frees.
Every lifecycle transition now stales its open exceptions in the same
transaction: revoke, expiry, drift to
needs_review, version bump, and thenew startup sweep.
claim_standing_rule_dark_windowalways treatedstaleas terminal; nothing had ever written it.
A fired exception is accounted as an exception, not as quota: distinct audit
class, counted per rule version and never pooled, and it does not refresh
the lapse-after-unused clock — owner silence must not keep alive the rule
that clock exists to retire.
The classifier that read as strict and was not
The
responsibility-contractspec has always required that reusabledelegation reject a dark-window Allow for communication and connector-write
effects. Nothing enforced it. The first implementation here added
enforcement as a classifier: eligible unless the action could be shown to
be counterparty-facing, egress-classed, or descriptor-backed.
That refused 5 actions out of roughly 50.
coolify.deploy,filesystem.host_write,secret.rotate,network.raw_egressandpolicy.modify_directstayed eligible, and all three limbs are false for anuncatalogued id, so unknown actions were fail-open. This diff's own earlier
revision contained a passing test that activated an
Allowdark window oncoolify.deploy. A predicate that permits whatever it cannot classify isnot fail-closed however its prose reads.
Eligibility is now an explicit allowlist, empty today:
BTreeSet::new().contains(x)is false for all x, so every cataloguedconnector write and every uncatalogued id is refused by construction rather
than by classification. A startup sweep converges stored state, because
activation is not retroactive: any active rule whose stored
Allowis nowineligible moves to
needs_reviewwith its exceptions staled, in onetransaction.
D-162 was rewritten — title, index row, body and Change Log — to record why
the classifier shape was wrong, so the next reader inherits the lesson.
Decisions: D-159 (bounded outstanding exceptions), D-160 (cap and token
binding answer different questions), D-161 (exception accounting is not
quota accounting), D-162 (explicit empty allowlist, enforced and swept).
One part lands ahead of its wiring: scoped consultation contains no
dark-window scheduling code, so the pending row's reviewed-context digests
are always NULL today and that binding is proven at store level only. See
design.md §"Currently unreachable in production" for why it lands now.
Filed #163 for a pre-existing timing flake found on the way, in
rate_limited_write_admission_is_refused_without_a_fence_row— the onlydeterministic pin on #127's permit-before-fence ordering.
Reviewed independently: APPROVE_WITH_NITS after a REQUEST_CHANGES round that
caught the classifier.
Summary by cubic
Bounds dark-window exceptions so owner silence cannot amplify authority. Adds a per-rule-version cap (default 1), stales exceptions on lifecycle changes, and enforces an explicit (currently empty) allowlist for
Allowdefaults.New Features
max_pending_exceptions(default 1). Enforced inside the scheduling transaction before insert; dedup happens first; at-cap returns ordinary approval with no timer.needs_review, version bump, and at startup (sweep).Allow(empty today). Fired exceptions are audited as exceptions, not quota, and do not refresh last-used.Migration
dark_window_max_pending; schema helpers split for clarity. Applied automatically on open.Allowrules toneeds_reviewand stale their exceptions. No manual steps.Written for commit 01f08e4. Summary will update on new commits.