Skip to content

[Proposal] Make CapabilityRegistry the canonical executable runtime path #18

Description

@artiphishle

Goal

Make Capability the canonical executable concept in Ankhorage Runtime, and make an Action a concrete serializable invocation of a capability.

Target model:

CapabilityDefinition = what can be executed
Action               = { id: CapabilityId, args }
EventBinding          = Event -> Action
CapabilityRegistry   = resolves/validates/executes capability handlers
CLI                  = another trigger for the same Capability

Runtime must not create a second executable vocabulary alongside the existing ankh capability architecture.

Depends on ankhorage/contracts#134.


Current API baseline

The canonical API abstraction is already established by the [api] roadmap:

AppManifest
  -> infra.apis[]
  -> ApiDefinition
  -> endpoint / operation metadata

Released Runtime v2 resolves API bindings by canonical API identity and delegates execution to @ankhorage/data-sources:

BindingOperationRef
  apiId
  endpointId
  operationId
-> manifest.infra.apis[]
-> Runtime API selection
-> @ankhorage/data-sources
-> HTTP

The old API -> DataSource -> database shortcut is gone. Explicit database capabilities such as db.persist remain a separate concern.

CapabilityRegistry must build on this path, not recreate API ownership or move APIs back under DataSource metadata.


Core execution model

ZORA event
   |
   v
EventBinding
   |
   v
Action { id, args }
   |
   v
resolve bindable args
   |
   v
CapabilityRegistry.invoke(id, resolvedArgs)
   |
   v
registered capability handler
   |
   v
structured result / diagnostics

A state value or API result used as data remains a ValueBinding source. Executing state mutation/API/navigation/infra/etc. is a capability invocation.


Reuse existing capability IDs

Capability IDs follow the package/domain that owns the executable behavior.

Examples:

navigation.push
navigation.replace
navigation.back
console.log
console.warn
state.set
state.toggle
infra.generate
infra.status
infra.up
infra.down
api.crm.customers.create

Do not create UI-specific aliases for existing capabilities.

Explicitly remove/forbid duplicate IDs such as:

studio.project.infraUp
studio.project.infraDown
studio.project.infraStatus
studio.project.infraGenerate

when @ankhorage/infra already owns:

infra.up
infra.down
infra.status
infra.generate

Studio may own studio.* capabilities only for genuinely Studio-owned workspace operations that have no canonical owner elsewhere.

Capability identity follows capability ownership, not the invoking screen/app.


Action is an invocation, not a definition

Runtime consumes the canonical Contracts Action shape:

interface Action {
  readonly id: CapabilityId;
  readonly args?: BindingInputMap;
}

Runtime should not own a competing ActionDefinition concept that duplicates CapabilityDefinition.

The registry receives package-owned CapabilityDefinitions + handlers and exposes one composed capability set for the current app/runtime environment.


Dynamic args

All Action args must resolve through the canonical recursive binding expression model.

Required sources include:

  • literal;
  • event;
  • context;
  • state;
  • canonical API operation result where retained for read semantics;
  • prior capability result.

Example:

DomainList.itemPress
  -> navigation.push({
       pathname: '/ankh/domain/[name]',
       params: { name: event.item.name }
     })

Same machinery:

Input.changeText -> state.set({ path: 'form.domain', value: event.value })
Button.press     -> infra.up({ projectId: context.route.params.projectId })

No action-specific interpolation syntax.


CapabilityRegistry replaces current parallel execution paths

Runtime already contains RuntimeActionRegistry / createRuntimeActionRegistry() and handler-map based execution in RuntimeRenderer.

Converge these into one canonical CapabilityRegistry path rather than adding another registry.

Target responsibilities:

register definition + handler
lookup by CapabilityId
resolve invocation
validate args
execute handler
validate/record result
produce diagnostics
unregister/compose providers

Required cleanup:

  • remove direct RuntimeRenderer handler-map dispatch that bypasses the registry;
  • remove/rename RuntimeActionRegistry if it would coexist redundantly with CapabilityRegistry;
  • remove parallel RuntimeActionHandlers maps once provider registration replaces them;
  • remove old { type, payload } runtime Action fallback;
  • remove action objects/strings directly serialized into on* props once explicit EventBindings own execution;
  • remove obsolete RuntimeActionDescriptor / RuntimeBindingDescriptor / RuntimeManifest.actions bootstrap model if final consumer audit confirms it has no real production owner;
  • move db.persist out of Renderer special-case injection and into normal capability registration if it remains a supported capability.

Do not retain compatibility aliases after first-party migration.


Capability providers / app composition

Each running app has one composed set of available capabilities.

Conceptually:

App CapabilityRegistry
├─ Runtime/core capabilities
├─ Expo/platform capabilities
├─ State capabilities
├─ API-derived capabilities from infra.apis[]
├─ Installed module/package capabilities
└─ App-specific capabilities

Studio authoring must inspect this same composed capability set. It must not maintain its own generic ACTION_REGISTRY catalog.

A capability present elsewhere in the npm ecosystem is not automatically available to every app. Availability comes from the app/runtime composition and installed/configured providers.

Future authorization is separate:

available?
  -> authorized?
  -> execute

CLI is a peer trigger, not Runtime's transport model

The existing ankh architecture already maps CLI command descriptors to capability IDs.

Runtime should therefore not spawn shell commands such as:

ankh infra up foo

for capability execution.

Instead CLI and Runtime invoke the same structured handler:

CLI parser
  -> invokeCapability('infra.up', { projectId: 'foo' })

ZORA binding
  -> invokeCapability('infra.up', { projectId: resolvedProjectId })

CLI argv/stdout/prompts belong to the CLI adapter. Capability handlers receive structured args and return structured results.


Host/remote execution is transport behind a capability

Some capabilities execute in-process:

navigation.push
state.set
console.log

Others require a trusted host/server boundary:

infra.up
workspace filesystem/process operations

The serialized Action remains the same:

{ id: 'infra.up', args: ... }

Runtime/provider registration decides how the capability is executed.

Do not serialize transport such as:

executor: http
executor: cli
executor: host

into each Action.

For Studio, the browser/runtime may forward infra.up to the trusted local Studio control plane, which then invokes the same structured Infra capability implementation. The HTTP route is transport, not a new canonical operation/capability ID.

Do not automatically expose every registered capability over HTTP.


Canonical API operations

infra.apis[] is the source of truth for API identity, schemas, endpoints and operations.

At the executable event boundary, API operations may be exposed as derived capabilities without copying API metadata into a second registry:

infra.apis[] / ApiDefinition
   -> endpoint + operation metadata
   -> derived CapabilityDefinition
   -> Action
   -> CapabilityRegistry
   -> existing Runtime API executor
   -> @ankhorage/data-sources
   -> HTTP

The CapabilityRegistry layer must delegate to the existing canonical Runtime API execution path. It must not duplicate method/path/schema/request semantics and must never route API execution through a database adapter.

Property/value bindings may continue to consume API results through the canonical binding/result contracts.


Results and sequencing

Capability handlers may return structured serializable results.

Required pattern:

form.submit
  -> api.customer.create(...)
  -> navigation.push({ id: previousResult.id })

Requirements:

  • definitions may declare optional result schema;
  • runtime stores result/loading/error state outside desired manifest state;
  • result addressing distinguishes two invocations of the same capability in one sequence;
  • void capabilities remain valid;
  • existing canonical API-operation result semantics are preserved while execution is unified.

Studio dogfood target

apps/studio must become a normal Ankhorage app using normal EventBindings.

Correct examples:

Infrastructure Up button.press
  -> infra.up({ projectId: context.route.params.projectId })

Infrastructure Down button.press
  -> infra.down({ projectId: context.route.params.projectId })

Not:

studio.project.infraUp
studio.project.infraDown

For Studio-owned workspace operations, use studio.* only when Studio genuinely owns the capability.

This is part of ankhorage/studio#113.


Security/interception point

CapabilityRegistry becomes the future centralized interception point:

resolve args
-> validate
-> authorize
-> execute
-> validate result
-> audit
-> telemetry

Initial Studio dogfooding may defer RBAC/ABAC, but the registry design must support it.

Capability availability does not imply remote exposure.


Implementation phases

Phase 1 — consume Contracts #134

  1. Adopt canonical CapabilityId, CapabilityDefinition, Action and result contracts.
  2. Remove runtime-local competing Action identity/definition shapes.
  3. Freeze provider composition semantics.

Phase 2 — central CapabilityRegistry

  1. Evolve/replace existing RuntimeActionRegistry into the canonical registry.
  2. Route all EventBinding execution through it.
  3. Add structured invocation args/results/diagnostics.
  4. Compose capability providers deterministically.
  5. Remove direct handler-map bypasses.

Phase 3 — migrate capability owners

  1. State/core runtime capabilities.
  2. Expo/navigation/platform capabilities.
  3. API-derived capabilities from canonical infra.apis[], delegating to the Runtime v2 API executor.
  4. Infra/host capabilities through trusted transport where required.
  5. Localization/module capabilities.

Phase 4 — remove obsolete Runtime execution models

  1. Remove old { type, payload } Action model.
  2. Remove serialized on* action/string callback path.
  3. Remove obsolete RuntimeManifest action/binding descriptors if unused.
  4. Remove db.persist Renderer special-case.
  5. Remove duplicate registries/maps/adapters after consumer migration.

Phase 5 — Studio dogfood

  1. Studio Action picker reads composed CapabilityDefinitions.
  2. Project detail uses infra.up, infra.down, etc.
  3. No duplicate studio.project.infra* capability IDs remain.
  4. Replace imperative screen callbacks with manifest EventBindings.

Acceptance criteria

  1. Runtime has one canonical CapabilityRegistry execution path.
  2. Action means a concrete { id: CapabilityId, args } capability invocation.
  3. Runtime does not own a second generic ActionDefinition vocabulary.
  4. Existing package-owned IDs such as infra.up are reused directly.
  5. studio.project.infraUp and analogous aliases are absent from the final architecture/code/docs/tests.
  6. Dynamic nested args resolve from literal/event/context/state/API-result/prior-result sources.
  7. All EventBindings execute through CapabilityRegistry.
  8. API operations derive from canonical infra.apis[] metadata and delegate to the existing Runtime API executor without duplicating endpoint schemas or request semantics.
  9. API operation execution never routes through a database adapter.
  10. CLI and UI can invoke the same structured capability implementation without Runtime shelling out to CLI.
  11. Host/HTTP transport remains behind provider execution and is not serialized into Actions.
  12. Structured results and sequencing work, including repeated invocation of the same capability ID.
  13. Old handler maps, old Action shapes and obsolete parallel runtime execution surfaces are deleted rather than deprecated.
  14. Studio can dogfood infra.up/infra.down via normal ZORA bindings.
  15. Tests/docs/Changeset and normal repository validation pass.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions