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.
6.6 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level | |
|---|---|---|---|---|---|---|---|---|
| call/registry/service-discovery | Implement services/list and services/schema built-in operations | pending |
|
narrow | low | isolated | implementation |
Description
Implement the two built-in service discovery operations in
src/registry/discovery.rs. These are read-only operations that expose what
the node offers.
Operations
| Operation name | Display path | Type | Description |
|---|---|---|---|
services/list |
/services/list |
Query | List registered operation names and metadata |
services/schema |
/services/schema |
Query | Get the OperationSpec for a specific operation |
services/list
Returns External operations only. Internal operations are not part of the
wire-facing API surface — they're implementation details of composition. A
remote client cannot enumerate the internal call tree (ADR-015).
{
"operations": [
{ "name": "fs/readFile", "namespace": "fs", "op_type": "query" },
{ "name": "agent/chat", "namespace": "agent", "op_type": "subscription" },
{ "name": "events/subscribe", "namespace": "events", "op_type": "subscription" }
]
}
The handler queries the registry's list_operations() (which returns External
specs only) and serializes to the above format.
services/schema
Accepts { "name": "fs/readFile" } (no leading slash — registry form, same as
OperationSpec.name) and returns the full OperationSpec including
input/output JSON Schemas and declared error_schemas (ADR-023).
The CallAdapter normalizes the leading slash from wire operationIds before
lookup, so services/schema accepts both fs/readFile and /fs/readFile.
This enables client code generation: a client reading the schema can produce typed error enums instead of generic error handling.
Registration
These are registered as Local provenance with empty composition authority,
empty scoped env, and empty capabilities (they don't compose, don't need
credentials):
.with_local(services_list_spec(), Arc::new(services_list_handler),
CompositionAuthority::none(), ScopedOperationEnv::empty(), Capabilities::new())
.with_local(services_schema_spec(), Arc::new(schema_handler),
CompositionAuthority::none(), ScopedOperationEnv::empty(), Capabilities::new())
Specs
fn services_list_spec() -> OperationSpec {
OperationSpec {
name: "services/list".into(),
namespace: "services".into(),
op_type: OperationType::Query,
visibility: Visibility::External,
input_schema: json!({}), // no input
output_schema: json!({
"type": "object",
"properties": {
"operations": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": { "type": "string" },
"namespace": { "type": "string" },
"op_type": { "type": "string", "enum": ["query", "mutation", "subscription"] }
}
}
}
}
}),
error_schemas: vec![],
access_control: AccessControl::default(), // no restrictions — callable by all
}
}
fn services_schema_spec() -> OperationSpec {
OperationSpec {
name: "services/schema".into(),
namespace: "services".into(),
op_type: OperationType::Query,
visibility: Visibility::External,
input_schema: json!({
"type": "object",
"properties": { "name": { "type": "string" } },
"required": ["name"]
}),
output_schema: json!({ /* full OperationSpec schema */ }),
error_schemas: vec![],
access_control: AccessControl::default(),
}
}
Handlers
The handlers need access to the registry. Since handlers are Arc<dyn Fn>,
the registry reference is captured in the closure. Use Arc<OperationRegistry>
cloned into the closure.
fn services_list_handler(registry: Arc<OperationRegistry>) -> Handler {
Arc::new(move |input: Value, ctx: OperationContext| {
let registry = registry.clone();
Box::pin(async move {
let ops: Vec<_> = registry.list_operations()
.into_iter()
.filter(|s| s.visibility == Visibility::External)
.map(|s| json!({
"name": s.name,
"namespace": s.namespace,
"op_type": match s.op_type {
OperationType::Query => "query",
OperationType::Mutation => "mutation",
OperationType::Subscription => "subscription",
}
}))
.collect();
ResponseEnvelope::ok(ctx.request_id, json!({ "operations": ops }))
})
})
}
Acceptance Criteria
services/listspec with correct fields (Query, External, no input, output schema)services/schemaspec with correct fields (Query, External, name input, full spec output)services/listhandler returns External operations only (Internal excluded)services/listoutput format matches spec (operations array with name, namespace, op_type)services/schemahandler accepts name with or without leading slashservices/schemareturns full OperationSpec (input_schema, output_schema, error_schemas)services/schemareturns NOT_FOUND for unknown operation name- Both registered as Local provenance, empty authority/env/caps
- Both have empty AccessControl (callable by all, including unauthenticated)
- Unit test: services/list returns only External ops
- Unit test: services/schema returns spec for known op
- Unit test: services/schema returns NOT_FOUND for unknown op
- Unit test: services/schema accepts both "fs/readFile" and "/fs/readFile"
cargo test -p alknet-callsucceedscargo clippy -p alknet-callsucceeds with no warnings
References
- docs/architecture/crates/call/operation-registry.md — Service Discovery section
- docs/architecture/decisions/015-privilege-model-and-authority-context.md — ADR-015 (Internal not in services/list)
Notes
services/list returns External ops only — Internal ops are implementation details of composition and must not be enumerable from the wire. The CallAdapter normalizes leading slashes, so services/schema accepts both forms. These are the only built-in operations; no admin operations are exposed through the call protocol itself.
Summary
To be filled on completion