Files
waggle-os/docs/superpowers/specs/2026-06-30-tool-adapter-registry-design.md
Oleg Maslov 0c3e2ead3b
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled
moving
2026-09-02 10:10:29 +02:00

8.2 KiB
Raw Blame History

Pluggable Tool-Adapter Registry — Design

Date: 2026-06-30 · Author: Claude Opus 4.8 (1M) · Owner: Marko (founder) Arc: AI-OS external-agent recon · STEAL NOW item #5 from docs/analysis/external-agent-launching-and-memory-comparison-2026-06-29.md Effort: M · Branch: feat/tool-adapter-registry · v1 scope (founder-approved): full registry + JSON loader.


1. Goal & Non-Goals

Goal. Turn the 7 hardcoded external tools — currently the same IDs duplicated across 8 structures in shared + agent — into one derived adapter registry, and let a self-hosted / KVARK operator add a PATH-based CLI runtime by dropping a JSON file (~/.waggle/adapters/*.json), with no core edit, no recompile, and no executable code load (honors CLAUDE.md §7: no eval / no dynamic require).

Non-goals (YAGNI):

  • No third-party GUI-desktop adapters (candidate-path detection stays built-in code — see §3 escape hatch). CLI agents are the launch target.
  • No require()/dynamic code load — third-party adapters are declarative data only.
  • No change to the launch/observe behavior shipped in #1#4. No UI redesign (the dock derives its cohort lists from the same source).
  • No signed-plugin trust store (that was the rejected "M-plus" option).

2. The 8 hardcoded structures collapsing into 1 source of truth

Today (authored separately) After
shared: SUPPORTED_TOOLS, LAUNCH_COHORT, TOOL_DISPLAY_NAMES derive from BUILTIN_TOOL_MANIFESTS
agent: HOOK_POINTER_BY_TOOL, detectorsById + per-tool detect* fns one manifest-driven detect loop
agent tool-launcher: HOOKS_COHORT, hookPackageFor derive from the registry (hookCapable)
agent launcher-prompt-args: promptArgsForTool built-ins unchanged; third-party via declarative promptArgTemplate

3. Architecture — data (shared) vs behavior (agent)

Unit A — ToolManifest + built-in manifests (packages/shared/src/tool-detection.ts)

export type ToolDetectSpec =
  | { kind: 'path'; binaryName: string }   // PATH lookup (CLI tools; the only third-party-allowed kind)
  | { kind: 'candidates' };                // GUI/desktop — paths resolved by an agent-side resolver (built-in only)

export interface ToolManifest {
  id: string;
  displayName: string;
  launchable: boolean;
  hookCapable: boolean;             // ⟺ ships a real @waggle/hive-mind-hooks-<id> bin
  hookPointer: string;             // relative pointer path for hook-status probe
  detect: ToolDetectSpec;
  /** Declarative inline-prompt arg template for THIRD-PARTY path adapters, e.g.
   *  ['--print', '{prompt}']. Built-ins keep their logic in launcher-prompt-args.ts. */
  promptArgTemplate?: string[];
  /** true = first-party (the 7); false/absent = loaded third-party. */
  builtin?: boolean;
}

SUPPORTED_TOOLS stays the as const literal type anchor (preserves the ToolId union → zero blast radius on the ~7 places typed Record<ToolId, …>). BUILTIN_TOOL_MANIFESTS: ToolManifest[] holds the per-tool data (the 7). The other consts derive from it: TOOL_DISPLAY_NAMES, LAUNCH_COHORT, HOOK_POINTER_BY_TOOL (and a new HOOKS_COHORT = manifests where hookCapable). Pure data → consumed by both the web bundle and the sidecar.

Unit B — registry + detection loop (packages/agent)

  • tool-registry.ts (new). getToolRegistry(deps?) returns the merged adapter list: the 7 built-in manifests (each candidates manifest paired with its existing agent-side resolver (deps) => string[], keyed by id — the escape hatch) plus validated third-party manifests from the loader (Unit C). Built-in ids win on collision.
  • tool-detection.ts. detectAll iterates getToolRegistry() instead of the detectorsById map: detect.kind:'path'detectByPath(id, binaryName); detect.kind:'candidates' → the adapter's resolver → detectByCandidates. probeHooks reads manifest.hookPointer. The bespoke detectClaudeCode/detectCursor/… wrappers and detectorsById are deleted; detectByPath / detectByCandidates / the 3 candidate-path helpers are kept (they're the reusable engine).
  • tool-launcher.ts. HOOKS_COHORT and the launch-cohort guard derive from the registry (hookCapable / launchable); hookPackageFor(id) = manifest.hookPackage ?? @waggle/hive-mind-hooks-<id>.

Unit C — declarative loader (packages/agent/src/tool-manifest-loader.ts, new)

Reads ~/.waggle/adapters/*.json, zod-validates each, and returns ToolManifest[]:

  • Allowed detect.kind for third-party: 'path' only (candidates would require code).
  • Safe-string refinement on id / binaryName / hookPointer / template entries: no shell metacharacters (; | & $ \ ( )), no path traversal (..), no absolute path separators in binaryName`. Reject (skip + log) any manifest that fails — never throw into detection.
  • Injected readDir / readFile deps so the loader is hermetic in tests; missing dir → [].
  • builtin: false stamped on every loaded manifest.

4. Data Flow

BUILTIN_TOOL_MANIFESTS (shared, data) ──┐
                                        ├─▶ getToolRegistry() ──▶ detectAll loops adapters
~/.waggle/adapters/*.json ─ loader ─────┘     (built-in resolvers for `candidates`;
   (zod + safe-string, kind:'path' only)        detectByPath for `path`)
SUPPORTED_TOOLS/ToolId  (anchor, unchanged)
LAUNCH_COHORT / HOOKS_COHORT / TOOL_DISPLAY_NAMES / HOOK_POINTER_BY_TOOL  ── derive from manifests

5. Error Handling / Security

  • Loader never throws into detection: a malformed/unsafe manifest is skipped + logged; detection proceeds with built-ins.
  • detect.kind:'candidates' from a third-party manifest is rejected (code-only strategy).
  • Safe-string refinement blocks shell-metachar / traversal injection in adapter fields (the boundary defense for external descriptors).
  • Built-in ids always win over a third-party manifest claiming the same id (no built-in hijack).
  • No require(), no eval, no dynamic import of adapter code — data only.

6. Testing (TDD)

  • shared: BUILTIN_TOOL_MANIFESTS has 7 entries; LAUNCH_COHORT/HOOKS_COHORT/TOOL_DISPLAY_NAMES/HOOK_POINTER_BY_TOOL derive correctly and match today's values (regression-lock the current 7-tool reality).
  • loader: valid path-manifest → parsed + builtin:false; kind:'candidates' rejected; shell-metachar / .. rejected; missing dir → []; injected fake fs.
  • registry: built-ins present; a loaded third-party path adapter appears and is detectable; built-in id wins a collision.
  • detection: detectAll over the registry yields the same results as today for the 7 (injected spawns — existing tool-detection.test.ts stays green); a third-party path adapter detects via detectByPath.
  • launcher: HOOKS_COHORT derivation matches today's 6; hookPackageFor honors a manifest override.

Gates: tsc 0 (shared/agent/server/web); existing tool-detection/tool-launcher/tools-routes* suites stay green; new units RED→GREEN.

7. File Change List

File Change
packages/shared/src/tool-detection.ts ToolManifest/ToolDetectSpec types, BUILTIN_TOOL_MANIFESTS, derive the 5 consts
packages/agent/src/tool-registry.ts newgetToolRegistry() (built-ins + loaded), candidate-resolver map
packages/agent/src/tool-manifest-loader.ts new — zod + safe-string loader for ~/.waggle/adapters/*.json
packages/agent/src/tool-detection.ts drive detectAll from the registry; delete per-tool wrappers + detectorsById
packages/agent/src/tool-launcher.ts derive HOOKS_COHORT / cohort guard / hookPackageFor from the registry
packages/agent/src/index.ts export registry + loader + manifest types
apps/web/src/components/os/apps/LauncherApp.tsx derive its local LAUNCH_COHORT/HOOKS_COHORT from shared manifests (kill the local copies)
tests (56) per §6

8. Open Questions

  • None blocking. Scope (full registry + loader), the candidate-path escape hatch, and the data-only/no-require() security model are founder-approved. Third-party GUI-desktop adapters + a signed-plugin trust store are explicit future items, not gaps.