Skip to content

feat: pluggable capability config with unified configDef#29

Merged
TimBeyer merged 11 commits intomainfrom
feat/pluggable-capability-config
Mar 17, 2026
Merged

feat: pluggable capability config with unified configDef#29
TimBeyer merged 11 commits intomainfrom
feat/pluggable-capability-config

Conversation

@TimBeyer
Copy link
Owner

@TimBeyer TimBeyer commented Mar 17, 2026

Summary

Makes capabilities fully pluggable, self-describing modules. Adding a new capability now requires only the module file + one export line — no central files to edit for config, validation, TUI, or sidebar help.

Unified configDef:

  • Capabilities declare a single CapabilityConfigDef<T> that drives config validation (Zod derived from fields), TUI form rendering, sidebar help, and secret sanitization
  • defineCapabilityConfig<T>() validates field paths against the config type at compile time
  • ConfigPath<T> supports plain keys and JSON Pointer for future nested config

Dynamic TUI:

  • New CapabilitySection component renders any capability's configDef generically
  • ConfigBuilder renders capability sections dynamically between Network and Agent Identity
  • Sidebar help and config review derive content from capability definitions

Host-side hooks:

  • Registry pattern in capability-hooks.ts replaces hardcoded 1Password/Tailscale blocks in headless.ts
  • ProvisionMonitor derives stages dynamically from enabled hooks

Legacy removal:

  • Removed services.onePassword and network.tailscale config paths entirely
  • Removed ProvisionFeatures, normalizeConfig, servicesSchema, backwards-compat flags
  • capabilities map is the single source of truth for optional feature config
  • Updated all docs, tests, and examples

Bug fixes:

  • Ctrl-C before provisioning no longer leaves the process hanging
  • TUI focus navigation order matches visual render order

Test plan

  • bun test — 256 pass, 0 fail
  • bun run lint — clean
  • bun run format:check — clean
  • Manual: run clawctl-dev create, verify Tailscale and 1Password render as dynamic capability sections
  • Manual: verify Ctrl-C exits cleanly at each wizard phase
  • Manual: test config file with capabilities: { "tailscale": { "authKey": "...", "mode": "serve" } }

🤖 Generated with Claude Code

TimBeyer and others added 11 commits March 17, 2026 12:56
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tion

Capabilities can now declare a `configDef` that unifies config schema,
TUI form fields, sidebar help, and secret marking in one definition.
A `defineCapabilityConfig<T>()` helper ensures field paths type-check
against the config interface. Zod schemas are derived from field
definitions — no hand-written Zod per capability.

- Add CapabilityConfigField, CapabilityConfigDef, ConfigPath, JsonPointer types
- Add defineCapabilityConfig<T>() helper for type-safe definitions
- Add schema derivation utilities (deriveConfigSchema, buildCapabilitiesSchema)
- Add path resolution utilities (getByPath, setByPath) for JSON Pointer support
- Migrate tailscale and one-password capabilities to declare configDef
- Add ALL_CAPABILITIES list export from @clawctl/capabilities

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- validateConfig accepts optional capabilitySchema for strict capability
  config validation (derived from configDef field definitions)
- Add normalizeConfig to bridge legacy paths (services.onePassword,
  network.tailscale) with the capabilities map bidirectionally
- provisionVM accepts full capabilities map, passes config objects
  (not just true) through to provision.json
- headless.ts normalizes config on entry and passes capabilities map
- sanitizeConfig strips secrets from capability configs using configDef
  field.secret markers
- Update op:// cross-validation to also check capabilities["one-password"]

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ConfigBuilder now renders capability sections dynamically from
configDef declarations — no hardcoded Services or Tailscale sections.

- Add CapabilitySection component for generic capability form rendering
- Refactor ConfigBuilder: replace hardcoded Services/Tailscale with
  dynamic sections from ALL_CAPABILITIES.filter(c => !c.core && c.configDef)
- Use capValues state (Record<string, Record<string, string>>) for
  capability config, replacing individual useState hooks
- Dynamic focus list with cap:<name> and cap:<name>:<field> patterns
- buildConfig assembles capabilities map from capValues + normalizeConfig
- Sidebar help falls through to configDef.fields[].help and sectionHelp
- ConfigReview renders dynamic capability rows from configDef
- defineCapabilityConfig returns type-erased CapabilityConfigDef for storage
  (generic validates at definition site, erased for CapabilityDef[])

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace hardcoded 1Password and Tailscale setup blocks in headless.ts
with a hook registry pattern. The headless pipeline iterates over
enabled capabilities and runs their registered host hooks.

- Add HostCapabilityHook interface and registry in capability-hooks.ts
- Wrap setupOnePassword and setupTailscale as registered hooks
- getHostHooksForConfig derives active hooks from config
- headless.ts loops over hooks instead of hardcoded if-blocks
- ProvisionMonitor derives stage labels dynamically from host hooks
- HeadlessStage usage relaxed to string for dynamic stage names

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Capability sections are rendered between Network and Agent Identity
in the JSX, but the focus list had them appended after all core
sections. This caused arrow-down navigation to jump out of sequence.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the user presses Ctrl-C during the config form (before
provisioning), provisionConfig is null so neither the success nor
cleanup path ran — the process hung because Ink's tty stream kept
the event loop alive. Now explicitly process.exit(130) in that case.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
No backwards compatibility needed — capabilities map is the single
source of truth for optional feature config.

Removed:
- services.onePassword from InstanceConfig type and servicesSchema
- network.tailscale from InstanceConfig type and networkSchema
- ProvisionConfig.onePassword and .tailscale deprecated fields
- ProvisionFeatures interface
- normalizeConfig function and all call sites
- Legacy fallbacks in capability enabled() functions
- Legacy path handling in getHostHooksForConfig/getCapabilityConfig
- Backwards-compat boolean translation in VM provision-config reader

Updated:
- bootstrap.ts reads tailscale mode from capabilities.tailscale
- All tests updated to use capabilities map format
- Docs (config-reference, 1password-setup, headless-mode) updated
  with new capabilities-based examples

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the old configSchema example with the new unified configDef
approach: defineCapabilityConfig<T>(), typed field paths, TUI form
definition, and secret marking — all in one declaration.

Update registration instructions: capabilities only need an export
in index.ts + optional host hook entry.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@TimBeyer TimBeyer merged commit 16936f0 into main Mar 17, 2026
4 checks passed
@TimBeyer TimBeyer deleted the feat/pluggable-capability-config branch March 17, 2026 15:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant