Establishes the tasks/architecture/ blocker-task half of the Safe Exit protocol. Each deferred OQ now has an external-trigger tracker task representing its unblocking condition, and the OQ's Blocked on field names the tracker task ID — closing the gap where the deferral field existed but the task-graph half was unenforced. Changes: - Add 6 external-trigger tracker tasks for the deferred OQs (OQ-09, 10, 32, 41, 44, 46) under tasks/architecture/, tagged [external-trigger, deferred-oq] - Add structured Blocked on field to OQ-09 and OQ-10 (previously used legacy 'deferred' status with the reason in the Resolution prose); index now surfaces all 6 deferred OQs with concrete conditions, no placeholders - Document the architecture-task level mapping (level: implementation for ADRs/specs, decomposition for spec-to-ADR breakdown, planning for backlog seeding, research/review direct fits) and the deferred-OQ blocker-task pattern in docs/sdd_process.md - Mark architecture/safe-exit-blocker-task-mechanism and architecture/oq-09-10-blocking-conditions completed (implemented by this pass) - Fix 4 pre-existing ADR-050 implementation tasks: status: done → completed (taskgraph enum is 'completed', not 'done') — taskgraph validate now passes for all 100 tasks
6.9 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level | |
|---|---|---|---|---|---|---|---|---|
| call/registry/access-control-ownership-check | Update AccessControl::check to consult OwnershipProvider for dynamic resource ownership (ADR-050 §2) | completed |
|
moderate | medium | component | implementation |
Description
Update AccessControl::check to accept resource_id and an optional
OwnershipProvider reference, consulting the provider for runtime-spawned
resource ownership checks. Per ADR-050 §2.
The signature change
Current (in crates/alknet-call/src/registry/spec.rs):
pub fn check(&self, identity: Option<&Identity>) -> AccessResult
Updated:
pub fn check(
&self,
identity: Option<&Identity>,
resource_id: Option<&str>,
ownership: Option<&dyn OwnershipProvider>,
) -> AccessResult
Check logic
1. Scope check (unchanged): identity.scopes ⊇ required_scopes.
If identity is None and scopes are required, deny here.
(Same as current — no change to the scope-check path.)
2. Resource check (only if self.resource_type is Some):
a. resource_id Some + ownership Some:
→ identity must be Some (owns takes &Identity, not Option);
if identity is None, deny.
→ p.owns(identity, resource_type, resource_id, resource_action)
→ if false, Forbidden("not owner of resource")
b. resource_id None + ownership Some (the `list` case, ADR-050 §4a):
→ if identity is None, deny.
→ p.owns_any(identity, resource_type) [scope-gate]
→ if false, Forbidden("no owned resources of type")
c. ownership None → fall back to static
→ identity.resources[resource_type] ∋ resource_action
(backward compat for non-runtime resources — the current logic)
Backward compatibility
When ownership is None, check falls back to the static
Identity.resources path — the existing resource-check logic (lines 83-105
of spec.rs) runs unchanged. This means operations with static resource
sets work without wiring an ownership provider. The ownership provider is
an additional check, not a replacement.
When resource_id is None and ownership is None, the existing logic
also runs unchanged (the resource_action without a specific resource ID
path, lines 97-105). This covers operations that have resource_action but
no resource_type — those stay on the static path.
Call sites to update
There are 6 call sites for check() that need the signature update:
registry/registration.rs:142—acl.check(identity.as_ref())ininvoke()registry/registration.rs:194—acl.check(identity.as_ref())ininvoke_streaming()protocol/connection.rs:351—access_control.check(caller_identity.as_ref())ininvoke_with_policy()registry/discovery.rs:224—spec.access_control.check(calling_identity)inservices/listregistry/discovery.rs:247—spec.access_control.check(calling_identity)inservices/schemaregistry/discovery.rs:269—reg.spec.access_control.check(calling_identity)inservices/list-peers
For sites 1-3 (dispatch path): pass resource_id: None and
ownership: None for now — the dispatch-path wiring (extracting
resource_id from input via spec.resource_id_path and threading the
ownership provider) is call/registry/dispatch-resource-id-extraction,
which depends on this task. This task changes the signature and the check
logic; the dispatch task wires the new parameters.
For sites 4-6 (service discovery): these are services/list /
services/schema / services/list-peers filtering — they check whether a
calling peer can see an operation at all. Pass resource_id: None and
ownership: None — service discovery doesn't do per-resource ownership
checks (it's scope-gating only). The list result-filter (ADR-050 §4a) is
a handler-level concern, not a service-discovery concern.
What this task does NOT do
- Does NOT extract
resource_idfrom the input or thread the ownership provider into the dispatch path — that'scall/registry/dispatch-resource-id-extraction, which depends on this task. This task changes the signature and the check logic; all call sites passNone, Noneand behave exactly as before. - Does NOT add
resource_id_pathtoOperationSpec— that'scall/registry/operation-spec-resource-id-path, which is independent (no dependency).
Tests
The existing tests in spec.rs (lines 157-327) all call check(identity)
with one argument. They need to be updated to check(identity, None, None)
— the behavior should be identical (backward compat). Add new tests for
the ownership-provider path:
resource_id Some + ownership Some + provider says owns → Allowedresource_id Some + ownership Some + provider says not owns → Forbiddenresource_id Some + ownership Some + identity None → Forbiddenresource_id None + ownership Some + provider says owns_any → Allowed(thelistscope-gate)resource_id None + ownership Some + provider says not owns_any → Forbiddenownership None + resource_type Some → static fallback (existing behavior)- A mock
OwnershipProviderimpl for testing
Acceptance Criteria
AccessControl::checksignature updated:(identity, resource_id, ownership) -> AccessResult- Scope check path unchanged (required_scopes, required_scopes_any)
- Resource check path: when
ownershipis Some, consults the provider - Resource check path: when
ownershipis None, falls back to staticIdentity.resources(backward compat) resource_id None + ownership Some→ callsowns_any(thelistscope-gate)resource_id Some + ownership Some→ callsownsidentity None + ownership Some + resource_type Some→ Forbidden (owns needs &Identity)- All 6 existing call sites updated to pass
(None, None)— behavior unchanged - All existing tests updated to
check(identity, None, None)— behavior unchanged - New tests with a mock
OwnershipProviderfor the dynamic path cargo test -p alknet-callsucceedscargo clippy -p alknet-callsucceeds with no warnings
References
- docs/architecture/crates/call/operation-registry.md — AccessControl (updated with the new check signature)
- docs/architecture/decisions/050-dynamic-resource-ownership-for-runtime-spawned-resources.md — ADR-050 §2, §4a
- crates/alknet-call/src/registry/spec.rs — current AccessControl::check implementation
- tasks/core/ownership-store-trait.md — the OwnershipProvider trait (dependency)
Notes
This is the one-way door — the
checksignature change touches every call site and test. The key design property: backward compatibility whenownershipisNone. All existing call sites pass(None, None)in this task and behave exactly as before. The dispatch-path wiring (extractingresource_idfrom input and threading a real ownership provider) is the next task. This task is the signature + logic change; the next task is the wiring.