Files
hub/docs/architecture/operations.md
glm-5.1 2b63cda1c7 Setup repo: migrate architecture specs, code stubs, and tasks from alkhub_ts
Copy architecture docs, ADRs, storage domain specs, research, reviews,
and 56 storage architecture tasks from the alkhub_ts monorepo. Adapt for
standalone @alkdev/hub repo structure (src/ not packages/hub/).

Sanitize all sensitive information:
- Replace private IPs (10.0.0.1) with localhost defaults
- Remove internal server hostnames (dev1, ns528096)
- Replace /workspace/ private paths with npm package references
- Remove hardcoded credentials from examples
- Rewrite infrastructure.md without private network details

Add Deno project scaffolding: deno.json (pinned deps), .gitignore,
AGENTS.md, entry point. Migrate existing code stubs (crypto, config
types, logger) with updated import paths.
2026-05-25 10:56:32 +00:00

9.2 KiB

status, last_updated
status last_updated
draft 2026-05-18

Operations System

Overview

The operations system is the universal abstraction for all work in the alk.dev platform. Every API endpoint, agent action, coordination tool, and MCP tool is an operation with typed input/output schemas, access control metadata, and a handler function.

Package: @alkdev/operations (npm)

Core Components

Core Types (operations/types.ts)

  • OperationType — QUERY (read-only), MUTATION (write), SUBSCRIPTION (async generator)
  • OperationSpec — serializable, hashable subset (name, namespace, version, type, description, tags, inputSchema, outputSchema, errorSchemas, accessControl, _meta)
  • IOperationDefinition — extends OperationSpec with runtime handler
  • OperationContext — metadata, requestId, parentRequestId, identity, env
  • AccessControl — requiredScopes (all match), requiredScopesAny (any match), resourceType, resourceAction. See below.
  • ResponseEnvelope<T> — universal result wrapper with source tracking (local/http/mcp). All execute() and env functions return ResponseEnvelope<T>.
  • CallError / InfrastructureErrorCode — structured error codes: OPERATION_NOT_FOUND, ACCESS_DENIED, VALIDATION_ERROR, TIMEOUT, ABORTED, EXECUTION_ERROR, UNKNOWN_ERROR.

Registry (operations/registry.ts)

  • Register by {namespace}.{name} key
  • register() now accepts OperationSpec & { handler? } (handler can be registered separately)
  • registerSpec() / registerHandler() — separate spec and handler registration
  • execute() returns Promise<ResponseEnvelope<TOutput>> (not Promise<TOutput>)
  • Constructor accepts optional SchemaAdapter for Zod/Valibot conversion
  • Access control is enforced in the registry (via enforceAccess)
  • Validate input before handler execution
  • Warn on output schema mismatch (don't throw)
  • getSpec() / getAllSpecs() for serializable specs

Scanner (operations/scanner.ts)

  • Recursive filesystem scan for .ts operation definitions
  • scanOperations(dirPath, fs) — takes an abstracted ScannerFS interface, not Deno.readDir directly
  • ScannerFS { readdir(path): AsyncIterable, cwd(): string } — inject Deno or Node adapter
  • Auto-discovery and registration
  • Validates against OperationSpecSchema, not OperationDefinition

Env Builder (operations/env.ts)

  • buildEnv() creates namespace-keyed OperationEnv for nested calls
  • Direct mode: buildEnv({ registry, context }) → env functions call registry.execute() directly
  • buildEnv no longer takes a callMap parameter
  • Sets trusted: true on nested context (bypasses access control for internal calls)
  • Env functions return Promise<ResponseEnvelope>, callers use unwrap(envelope) or envelope.data
  • Filters SUBSCRIPTION operations out of env

FromSchema (operations/from_schema.ts)

  • JSON Schema → TypeBox TSchema converter
  • Handles allOf, anyOf, oneOf, enum, object, tuple, array, const, $ref, primitives

Schema Adapters (@alkdev/operations/from-typemap)

The SchemaAdapter pattern converts non-TypeBox schemas to TypeBox at registration time:

import { zodAdapter, valibotAdapter } from "@alkdev/operations/from-typemap"

const registry = new OperationRegistry({ schemaAdapter: zodAdapter() })
// or: { schemaAdapter: valibotAdapter() }
// or: { schemaAdapter: defaultAdapter } // TypeBox only (default)

The SchemaAdapter interface has toTypeBox(schema) and optional init(). Zod and Valibot adapters use dynamic import of @alkdev/typemap and check for ~standard vendor property for auto-detection.

@alkdev/typemap is an optional peer dependency — it's only loaded when a Zod or Valibot schema is actually encountered. Spoke authors using TypeBox directly have no extra dependencies. Non-TypeScript spokes send JSON Schema over the wire, which the hub converts via FromSchema().

See ADR-013 for the full decision and trade-offs.

FromOpenAPI (operations/from_openapi.ts)

  • Key piece: generates IOperationDefinition[] from OpenAPI specs
  • Detects text/event-stream responses as SUBSCRIPTION type
  • Auto-generates HTTP fetch handlers with path/query/body param routing
  • Supports bearer, apiKey, basic auth
  • Use case: import opencode's OpenAPI spec → instant typed client operations

MCP Wrapper (mcp/wrapper.ts, mcp/loader.ts)

  • createMCPClient connects to MCP servers (stdio or HTTP)
  • MCP tools → IOperationDefinition[] with auto-generated handlers
  • MCPClientLoader manages multiple MCP client connections
  • Use case: connect to external MCP servers (websearch, etc.) and wrap as operations

ResponseEnvelope

All execute() calls and env functions return ResponseEnvelope<T>:

interface ResponseEnvelope<T> {
  data: T
  meta: ResponseMeta  // source: "local" | "http" | "mcp", timestamps, status codes
}

Factory functions: localEnvelope(data, operationId), httpEnvelope(data, meta), mcpEnvelope(data, meta). Use unwrap(envelope) to extract .data or isResponseEnvelope(value) to type-guard.

Access Control

checkAccess(accessControl, identity) — boolean check. enforceAccess(accessControl, identity, operationId, trusted?) — throws CallError on denial. The trusted: true flag bypasses all access checks (set by buildEnv on nested calls).

CallError

CallError extends Error with code and details. InfrastructureErrorCode enum provides standard error codes. mapError(error, errorSchemas?) matches thrown errors against declared errorSchemas.

Open Issues

Call Protocol Integration

Operations use buildEnv() which supports direct mode (see call-graph.md):

  • Direct mode: buildEnv({ registry, context }) → env functions call registry.execute()

The call protocol (PendingRequestMap, CallHandler) is part of @alkdev/operations. It provides call graph tracking, abort cascading, and structured error handling across all transports. See call-graph.md for the full spec.

How It Connects to Everything Else

Hub HTTP API routes ──→ registry.execute("namespace.operation", input, ctx)
                              │
MCP server tools    ──→ registry.execute(...)
                              │
FromOpenAPI ops     ──→ fetch(opencode container REST API)
                              │
MCP client tools    ──→ MCPClientLoader → registry.execute(...)
                              │
Agent session LLM   ──→ tool calls with JSON Schema → registry.execute(...)

All paths funnel into the same registry. Access control, validation, and error handling are consistent regardless of entry point.

Access Control Model

Authentication uses keypal for API key management. keypal verifies bearer tokens and provides a two-tier scope model:

  1. Global scopes: flat string array (e.g., ["read", "write", "admin"])
  2. Resource-scoped permissions: Record<string, string[]> keyed by "type:id" (e.g., { "project:abc": ["read", "write"] })

Identity

The Identity type derives from keypal's ApiKeyMetadata:

interface Identity {
  id: string                           // keypal ownerId
  scopes: string[]                     // global scopes from keypal
  resources?: Record<string, string[]>  // resource-scoped permissions, key format: "type:id"
}

"Roles" are scope bundles — a convention on top of scopes, not a separate type. For example, a scope of "implement" might grant access to ["dev.fs.read", "dev.fs.write", "dev.bash.exec"]. Defining which scopes a "role" maps to is a configuration concern, not a type-system concern.

AccessControl

The AccessControl definition on each operation declares what permissions are required:

Field Semantics Example
requiredScopes AND — caller must have ALL of these scopes ["call"] — caller can invoke operations
requiredScopesAny OR — caller must have at least ONE of these scopes ["admin", "coord.spawn"] — admin OR can spawn
resourceType Resource category for resource-scoped checks "project"
resourceAction Required action on the resource "write"

Enforcement: The CallHandler (see call-graph.md) checks AccessControl against Identity before dispatching to registry.execute(). The registry itself is a pure execution engine — access control lives at the call handler layer.

Resource checks: When resourceType + resourceAction are set, the check is: does identity.resources["{resourceType}:{resourceId}"] include resourceAction? This maps directly to keypal's checkResourceScope(record, resourceType, resourceId, scope).

Access Control Flow

Request → CallHandler receives call.requested with Identity
  → Look up operation's AccessControl
  → Check requiredScopes (caller has ALL?)
  → Check requiredScopesAny (caller has ANY?)
  → Check resourceType/resourceAction against identity.resources
  → All pass → registry.execute()
  → Any fail → call.error with ACCESS_DENIED

Known Gaps

  • Logger config: core/logger/mod.ts is a stub that only logs the ["logtape", "meta"] category. Needs proper config for app-level loggers.
  • Config: core/config/types.ts has spoke-only config. Needs hub-specific config (postgres, redis, auth).