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:
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
- Adopt canonical
CapabilityId, CapabilityDefinition, Action and result contracts.
- Remove runtime-local competing Action identity/definition shapes.
- Freeze provider composition semantics.
Phase 2 — central CapabilityRegistry
- Evolve/replace existing RuntimeActionRegistry into the canonical registry.
- Route all EventBinding execution through it.
- Add structured invocation args/results/diagnostics.
- Compose capability providers deterministically.
- Remove direct handler-map bypasses.
Phase 3 — migrate capability owners
- State/core runtime capabilities.
- Expo/navigation/platform capabilities.
- API-derived capabilities from canonical
infra.apis[], delegating to the Runtime v2 API executor.
- Infra/host capabilities through trusted transport where required.
- Localization/module capabilities.
Phase 4 — remove obsolete Runtime execution models
- Remove old
{ type, payload } Action model.
- Remove serialized
on* action/string callback path.
- Remove obsolete RuntimeManifest action/binding descriptors if unused.
- Remove
db.persist Renderer special-case.
- Remove duplicate registries/maps/adapters after consumer migration.
Phase 5 — Studio dogfood
- Studio Action picker reads composed CapabilityDefinitions.
- Project detail uses
infra.up, infra.down, etc.
- No duplicate
studio.project.infra* capability IDs remain.
- Replace imperative screen callbacks with manifest EventBindings.
Acceptance criteria
- Runtime has one canonical CapabilityRegistry execution path.
- Action means a concrete
{ id: CapabilityId, args } capability invocation.
- Runtime does not own a second generic ActionDefinition vocabulary.
- Existing package-owned IDs such as
infra.up are reused directly.
studio.project.infraUp and analogous aliases are absent from the final architecture/code/docs/tests.
- Dynamic nested args resolve from literal/event/context/state/API-result/prior-result sources.
- All EventBindings execute through CapabilityRegistry.
- API operations derive from canonical
infra.apis[] metadata and delegate to the existing Runtime API executor without duplicating endpoint schemas or request semantics.
- API operation execution never routes through a database adapter.
- CLI and UI can invoke the same structured capability implementation without Runtime shelling out to CLI.
- Host/HTTP transport remains behind provider execution and is not serialized into Actions.
- Structured results and sequencing work, including repeated invocation of the same capability ID.
- Old handler maps, old Action shapes and obsolete parallel runtime execution surfaces are deleted rather than deprecated.
- Studio can dogfood
infra.up/infra.down via normal ZORA bindings.
- Tests/docs/Changeset and normal repository validation pass.
Goal
Make Capability the canonical executable concept in Ankhorage Runtime, and make an Action a concrete serializable invocation of a capability.
Target model:
Runtime must not create a second executable vocabulary alongside the existing
ankhcapability architecture.Depends on
ankhorage/contracts#134.Current API baseline
The canonical API abstraction is already established by the
[api]roadmap:Released Runtime v2 resolves API bindings by canonical API identity and delegates execution to
@ankhorage/data-sources:The old API -> DataSource -> database shortcut is gone. Explicit database capabilities such as
db.persistremain a separate concern.CapabilityRegistry must build on this path, not recreate API ownership or move APIs back under DataSource metadata.
Core execution model
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:
Do not create UI-specific aliases for existing capabilities.
Explicitly remove/forbid duplicate IDs such as:
when
@ankhorage/infraalready owns:Studio may own
studio.*capabilities only for genuinely Studio-owned workspace operations that have no canonical owner elsewhere.Action is an invocation, not a definition
Runtime consumes the canonical Contracts Action shape:
Runtime should not own a competing
ActionDefinitionconcept that duplicatesCapabilityDefinition.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:
Example:
Same machinery:
No action-specific interpolation syntax.
CapabilityRegistry replaces current parallel execution paths
Runtime already contains
RuntimeActionRegistry/createRuntimeActionRegistry()and handler-map based execution inRuntimeRenderer.Converge these into one canonical
CapabilityRegistrypath rather than adding another registry.Target responsibilities:
Required cleanup:
RuntimeActionRegistryif it would coexist redundantly withCapabilityRegistry;RuntimeActionHandlersmaps once provider registration replaces them;{ type, payload }runtime Action fallback;on*props once explicit EventBindings own execution;RuntimeActionDescriptor/RuntimeBindingDescriptor/RuntimeManifest.actionsbootstrap model if final consumer audit confirms it has no real production owner;db.persistout 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:
Studio authoring must inspect this same composed capability set. It must not maintain its own generic
ACTION_REGISTRYcatalog.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:
CLI is a peer trigger, not Runtime's transport model
The existing
ankharchitecture already maps CLI command descriptors to capability IDs.Runtime should therefore not spawn shell commands such as:
for capability execution.
Instead CLI and Runtime invoke the same structured handler:
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:
Others require a trusted host/server boundary:
The serialized Action remains the same:
Runtime/provider registration decides how the capability is executed.
Do not serialize transport such as:
into each Action.
For Studio, the browser/runtime may forward
infra.upto 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:
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:
Requirements:
Studio dogfood target
apps/studiomust become a normal Ankhorage app using normal EventBindings.Correct examples:
Not:
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:
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
CapabilityId,CapabilityDefinition, Action and result contracts.Phase 2 — central CapabilityRegistry
Phase 3 — migrate capability owners
infra.apis[], delegating to the Runtime v2 API executor.Phase 4 — remove obsolete Runtime execution models
{ type, payload }Action model.on*action/string callback path.db.persistRenderer special-case.Phase 5 — Studio dogfood
infra.up,infra.down, etc.studio.project.infra*capability IDs remain.Acceptance criteria
{ id: CapabilityId, args }capability invocation.infra.upare reused directly.studio.project.infraUpand analogous aliases are absent from the final architecture/code/docs/tests.infra.apis[]metadata and delegate to the existing Runtime API executor without duplicating endpoint schemas or request semantics.infra.up/infra.downvia normal ZORA bindings.