feat: pluggable capability config with unified configDef#29
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:CapabilityConfigDef<T>that drives config validation (Zod derived from fields), TUI form rendering, sidebar help, and secret sanitizationdefineCapabilityConfig<T>()validates field paths against the config type at compile timeConfigPath<T>supports plain keys and JSON Pointer for future nested configDynamic TUI:
CapabilitySectioncomponent renders any capability'sconfigDefgenericallyConfigBuilderrenders capability sections dynamically between Network and Agent IdentityHost-side hooks:
capability-hooks.tsreplaces hardcoded 1Password/Tailscale blocks inheadless.tsProvisionMonitorderives stages dynamically from enabled hooksLegacy removal:
services.onePasswordandnetwork.tailscaleconfig paths entirelyProvisionFeatures,normalizeConfig,servicesSchema, backwards-compat flagscapabilitiesmap is the single source of truth for optional feature configBug fixes:
Test plan
bun test— 256 pass, 0 failbun run lint— cleanbun run format:check— cleanclawctl-dev create, verify Tailscale and 1Password render as dynamic capability sectionscapabilities: { "tailscale": { "authKey": "...", "mode": "serve" } }🤖 Generated with Claude Code