Files
alknet/tasks/call/registry/operation-spec.md
glm-5.2 098fd8b9b9 tasks: decompose vault, core, call crates into 28 atomic implementation tasks
Break down the three initial crates (alknet-vault, alknet-core, alknet-call)
into dependency-ordered task files for implementation agents.

Structure:
- tasks/vault/ (10 tasks) — drift fixes from ADR-025/026 refactor, review,
  spec sync. Vault is independent and can run fully in parallel with core/call.
- tasks/core/ (6 tasks) — crate init, core types, config, auth, endpoint,
  review. Core is foundational; call depends on it.
- tasks/call/ (12 tasks) — split into registry/ and protocol/ topic subdirs
  reflecting the two subsystems. CallAdapter is the merge point.

Key decisions:
- Drifts 3+9+10 grouped as one task (key-versioning-rotation) — the complete
  ADR-021 rotation feature that doesn't compile in pieces
- Reviews injected at end of each crate phase (vault, core, call)
- Vault spec-sync task removes the drift table and bumps doc status to stable
- ACME deferred in core/endpoint (noted as TODO; X509 manual certs for now)
- OperationEnv kept as a trait (load-bearing for ADR-024 layering)

Validated: 28 tasks, no cycles, 11 generations of parallel work.
Critical path runs through call (11 tasks). Vault completes by generation 4.
6 high-risk tasks identified (21%): irpc-removal, endpoint, operation-context,
operation-env, call-adapter, abort-cascade.
2026-06-23 12:41:47 +00:00

6.4 KiB

id, name, status, depends_on, scope, risk, impact, level
id name status depends_on scope risk impact level
call/registry/operation-spec Implement OperationSpec, OperationType, Visibility, ErrorDefinition, and AccessControl pending
call/crate-init
moderate medium component implementation

Description

Implement the operation specification types in src/registry/spec.rs. These types declare what an operation is, its schemas, and its access control policy.

OperationSpec

pub struct OperationSpec {
    pub name: String,              // e.g., "fs/readFile", "agent/chat" (no leading slash)
    pub namespace: String,         // e.g., "fs", "agent"
    pub op_type: OperationType,    // Query, Mutation, Subscription
    pub visibility: Visibility,    // External (wire-callable) or Internal (composition-only)
    pub input_schema: Value,       // JSON Schema for input
    pub output_schema: Value,      // JSON Schema for output
    pub error_schemas: Vec<ErrorDefinition>,  // Declared domain errors (ADR-023)
    pub access_control: AccessControl,
}

Operation names use slash-based paths without a leading slash, aligned with URL path conventions: fs/readFile, agent/chat, services/list. The leading slash is added for display (spec.path() returns /fs/readFile) and wire format. The registry stores names without the leading slash.

The namespace field is derived from the name: for fs/readFile it's fs, for agent/chat it's agent. It's a convenience accessor for ACL matching and service grouping.

Implement OperationSpec::path(&self) -> String that returns /{name} (the wire/display form with leading slash).

OperationType

pub enum OperationType {
    Query,         // Read-only, idempotent (e.g., "fs/readFile", "services/list")
    Mutation,      // Side effects (e.g., "bash/exec", "github/authenticate")
    Subscription,  // Streaming (e.g., "agent/chat", "events/subscribe")
}

Visibility

pub enum Visibility {
    External,  // Callable from the wire (call.requested from a client)
    Internal,  // Composition-only (env.invoke from a handler)
}

External operations appear in services/list and accept call.requested. Internal operations return NOT_FOUND when called from the wire and do not appear in services/list. The assembly layer declares visibility at registration. All import adapters register operations as Internal by default (they're composition material); the handler that composes them is External.

ErrorDefinition

pub struct ErrorDefinition {
    pub code: String,           // e.g., "FILE_NOT_FOUND", "RATE_LIMITED"
    pub description: String,    // Human-readable description
    pub schema: Value,           // JSON Schema for the error detail payload
    pub http_status: Option<u16>,  // HTTP status for adapter projection (from_openapi/to_openapi)
}

A declared operation-level error (ADR-023). When a handler returns a CallError whose code matches a declared ErrorDefinition, the call.error event carries that code and the error's detail payload. If it doesn't match, the call.error carries INTERNAL.

AccessControl

pub struct AccessControl {
    pub required_scopes: Vec<String>,          // AND-checked: caller must have ALL
    pub required_scopes_any: Option<Vec<String>>, // OR-checked: caller must have at LEAST ONE
    pub resource_type: Option<String>,          // e.g., "service"
    pub resource_action: Option<String>,        // e.g., "read"
}

ACL check flow

When a call.requested event arrives:

  1. Registry checks visibility — if Internal, returns NOT_FOUND (does not leak existence)
  2. Registry checks access_control.check(identity):
    • For external calls (internal: false): ACL against the caller's identity
    • For internal calls (internal: true): ACL against the handler's composition authority (ADR-015)
  3. If denied: FORBIDDEN
  4. If identity is None and operation has restrictions: FORBIDDEN with message "authentication required"

Operations with empty AccessControl (no required scopes, no resource checks) are accessible to all callers, including unauthenticated ones.

Implement AccessControl::check

impl AccessControl {
    pub fn check(&self, identity: Option<&Identity>) -> AccessResult;
}

pub enum AccessResult {
    Allowed,
    Forbidden(String),  // reason
}

The check logic:

  • required_scopes: caller must have ALL (subset check)
  • required_scopes_any: caller must have at LEAST ONE (if present)
  • resource_type / resource_action: check against identity.resources
  • If identity is None and any scope/resource is required: Forbidden("authentication required")

Acceptance Criteria

  • OperationSpec struct with all 8 fields
  • OperationSpec::path() returns /{name} (leading slash for wire/display)
  • OperationSpec::namespace derived from name (split on /)
  • OperationType enum with Query, Mutation, Subscription
  • Visibility enum with External, Internal
  • ErrorDefinition struct with all 4 fields
  • AccessControl struct with all 4 fields
  • AccessControl::check(identity) returns AccessResult
  • required_scopes is AND-checked (caller must have all)
  • required_scopes_any is OR-checked (caller must have at least one)
  • None identity with restrictions → Forbidden("authentication required")
  • Empty AccessControl → Allowed for all callers
  • Unit tests for AccessControl::check (all combinations)
  • Unit test: OperationSpec::path() produces leading slash
  • Unit test: namespace derived correctly from name
  • cargo test -p alknet-call succeeds
  • cargo clippy -p alknet-call succeeds with no warnings

References

  • docs/architecture/crates/call/operation-registry.md — OperationSpec, AccessControl, Visibility
  • docs/architecture/decisions/015-privilege-model-and-authority-context.md — ADR-015 (visibility, ACL)
  • docs/architecture/decisions/023-operation-error-schemas.md — ADR-023 (ErrorDefinition)

Notes

Operation names have NO leading slash in the registry (fs/readFile). The leading slash is added for wire format and display (/fs/readFile). This is a single rule applied consistently — do not mix the two forms. Visibility controls wire-callability: Internal ops return NOT_FOUND from the wire (don't leak existence). AccessControl.check is the ACL gate — read it carefully against ADR-015 for the internal vs external authority distinction.

Summary

To be filled on completion