Files
alknet/tasks/call/registry/operation-spec-resource-id-path.md
glm-5.2 73724b8a1d feat(core,call): implement ADR-050 dynamic resource ownership — ownership store, resource_id_path, check signature, dispatch wiring
Implements the 4-task DAG for runtime-spawned resource ownership so
AccessControl::check can answer "does this identity own this specific
container/TTY/process" instead of relying only on static
Identity.resources grants.

1. core/ownership-store-trait: OwnershipProvider (sync read) +
   OwnershipStore (async write) traits + InMemoryOwnershipStore +
   OwnershipError in alknet-core. Fourth instance of the repo/adapter
   pattern (ADR-033), mirroring CredentialStore/store.rs.

2. call/registry/operation-spec-resource-id-path: resource_id_path
   field on OperationSpec — JSON pointer into input for resource ID
   extraction. Single field addition, all ~40 construction sites across
   alknet-call + alknet-http updated to pass None (no semantic change).

3. call/registry/access-control-ownership-check: check() signature
   gains resource_id + Option<&dyn OwnershipProvider>. 3-case decision
   tree: ownership Some + resource_id Some -> owns(); ownership Some +
   resource_id None -> owns_any() (list scope-gate); ownership None ->
   static Identity.resources fallback (backward compat). 7 call sites
   updated to (None, None) — including a 7th in alknet-http/gateway_routes
   not listed in the original spec.

4. call/registry/dispatch-resource-id-extraction: wire the dispatch
   path. OperationContext gains ownership field; Dispatcher/CallAdapter
   gain with_ownership_provider builders; invoke/invoke_streaming/
   invoke_with_policy extract resource_id via spec.resource_id_path
   and thread context.ownership to check(). extract_json_pointer helper
   handles $.field syntax (graceful None on missing/non-string). 20
   OperationContext literals updated across both crates.

Backward compatibility is load-bearing throughout: ownership=None
falls back to the existing static resource-check path. Deployments
without runtime-spawned resources wire nothing and behave identically.

821 tests pass workspace-wide (was ~770); clippy clean; fmt clean.
Task specs marked done.
2026-07-05 12:57:33 +00:00

94 lines
3.8 KiB
Markdown

---
id: call/registry/operation-spec-resource-id-path
name: Add resource_id_path field to OperationSpec (ADR-050 §2a)
status: done
depends_on: []
scope: single
risk: low
impact: component
level: implementation
---
## Description
Add a `resource_id_path: Option<String>` field to `OperationSpec`. This is
a JSON pointer into the operation input that tells the dispatcher where to
find the resource ID for runtime-spawned resource authorization. Per ADR-050
§2a.
### The field
```rust
pub struct OperationSpec {
pub name: String,
pub namespace: String,
pub op_type: OperationType,
pub visibility: Visibility,
pub input_schema: Value,
pub output_schema: Value,
pub error_schemas: Vec<ErrorDefinition>,
pub access_control: AccessControl,
/// JSON pointer into the input for the resource ID, when
/// `access_control.resource_type` is set and the operation targets a
/// specific runtime-spawned resource (ADR-050). e.g., `"$.containerId"`
/// for `docker/container/exec`. Absent for no-specific-resource
/// operations (the `list` case). `None` for operations with no
/// `resource_type` or with static resource sets.
pub resource_id_path: Option<String>,
}
```
### Construction
`OperationSpec::new(...)` currently takes 7 arguments (name, op_type,
visibility, input_schema, output_schema, error_schemas, access_control).
This task adds `resource_id_path` as an 8th argument. Since all
construction sites need to update, consider whether a builder pattern is
worth introducing — but for now, add it as the last positional argument
(defaulting to `None` at call sites that don't need it).
### What this task does NOT do
- Does NOT change `AccessControl::check` — that's
`call/registry/access-control-ownership-check`, which depends on
`core/ownership-store-trait`.
- Does NOT extract the resource ID from input or pass it to `check`
that's `call/registry/dispatch-resource-id-extraction`, which depends on
this task.
- This task only adds the field to the struct, updates `new()`, and updates
all existing construction sites + tests.
### Existing construction sites
Search for `OperationSpec::new(` across the codebase — every call site
needs the new argument added. Most will pass `None` (no runtime-spawned
resources). The existing tests in `spec.rs` construct `OperationSpec`
without `resource_id_path` — they all need the `None` argument added.
## Acceptance Criteria
- [ ] `OperationSpec` struct has `resource_id_path: Option<String>` field
- [ ] `OperationSpec::new(...)` takes `resource_id_path` as the 8th argument
- [ ] All existing `OperationSpec::new(...)` call sites updated (most pass `None`)
- [ ] All existing tests that construct `OperationSpec` updated
- [ ] Existing tests still pass (no semantic change — `None` means "no resource ID extraction")
- [ ] Unit test: `resource_id_path` is `None` by default when not specified
- [ ] `cargo test -p alknet-call` succeeds
- [ ] `cargo clippy -p alknet-call` succeeds with no warnings
## References
- docs/architecture/crates/call/operation-registry.md — OperationSpec (updated with `resource_id_path`)
- docs/architecture/decisions/050-dynamic-resource-ownership-for-runtime-spawned-resources.md — ADR-050 §2a
- crates/alknet-call/src/registry/spec.rs — current OperationSpec struct
## Notes
> The fit with JSON Schema is load-bearing: `input_schema` is already a
> JSON Schema, so `resource_id_path` is a pointer *within* an existing
> schema on the same spec. The `OperationSpec` becomes fully
> self-describing for authorization — what resource type, what action,
> and *which input field* drives the resource lookup. This is a single
> field addition with no semantic change — existing call sites pass
> `None` and behave exactly as before. The field is consumed by the
> dispatch path in `call/registry/dispatch-resource-id-extraction`.