---
id: call/registry/operation-spec
name: Implement OperationSpec, OperationType, Visibility, ErrorDefinition, and AccessControl
status: completed
depends_on: [call/crate-init]
scope: moderate
risk: medium
impact: component
level: 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

```rust
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

```rust
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

```rust
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

```rust
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

```rust
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

```rust
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

Implemented operation specification types in `registry/spec.rs`: `OperationSpec`
(with `path()` returning `/{name}`, namespace derived from name split on `/`),
`OperationType` (Query, Mutation, Subscription), `Visibility` (External, Internal),
`ErrorDefinition` (ADR-023), `AccessControl::check` returning `AccessResult`
(AND-scopes, OR-scopes, resource_type/resource_action checks, None identity →
"authentication required", empty ACL → Allowed). 9 unit tests pass; clippy clean.
Merged to develop.