Skip to content

feat(ruflo-components): manage and surface ruflo components by installed version (ADR-0058) - #236

Open
pacphi wants to merge 35 commits into
mainfrom
feat/managed-ruflo-components
Open

pacphi wants to merge 35 commits into
mainfrom
feat/managed-ruflo-components

Conversation

@pacphi

@pacphi pacphi commented Sep 23, 2026 •

Copy link
Copy Markdown
Owner

Summary

Implements ADR-0058. ak now manages a set of ruflo components for the user, based on the installed ruflo version. It shows each one, with what its state means, in ak setup, ak status and the dashboard.

Component Managed value Min ruflo
Typesafe agent picker (@ruvector/typesafe) on (package installed + CLAUDE_FLOW_ROUTER_TYPESAFE=1) 3.43.0
MiniLM agent picker on (CLAUDE_FLOW_ROUTER_EMBEDDER=minilm) 3.44.0
MCP tool governance on: ak-written .harness/mcp-policy.json (audit log, 120 calls/60 s); env only in ruflo projects with a valid ak policy 3.42.0
Learning profile balanced; all five profiles reported 3.42.1
MetaHarness turn-credit on (verify only) 3.36.0
Memory fix (#2887) on (verify only) 3.36.0
Funnel off (ruflo funnel disable, released on uninstall/opt-out) —
Encryption at rest reported as not yet managed; see the planned ADR-0059 —
  • Opting out: set a component to false in kit.json. For the funnel, set funnel: true to hand it back.
  • Receipts: every write is receipt-owned, using the owned-environment engine generalized from ADR-0055. The engine uses a pending guard, a preimage check and atomic replace. A user value is never adopted or overwritten.
  • Deciding state: status is decided by what ak actually wrote to the host configuration, then confirmed by ruflo's own evidence: doctor, funnel status --json, the hooks route embedder marker, and the MCP audit log.
  • States:
    • drifted: a value ak set was removed. The next ak sync restores it.
    • user-managed: the user changed or set the value. ak leaves it alone.
  • Hosts: the settings are projected to Claude Code, OpenCode and Codex. The Codex hooks environment is reported as partial.

Important finding: governance is written but not enforced by ruflo ≤ 3.44.0

In ruflo 3.44.0, RUFLO_MCP_ENFORCE_POLICY / .harness/mcp-policy.json enforcement lives only in dist/src/mcp-server.js (MCPServerManager). Neither real stdio entry point reaches it:

  • bin/mcp-server.js, which ak registers, imports only dist/src/mcp-client.js.
  • The ruflo mcp start non-TTY fast path in bin/cli.js has its own tools/call handler.

Tool calls succeed even with the policy file removed, and nothing reaches the audit log. ak still writes the policy, so it is ready when ruflo wires it in. Status, docs and the ADR say plainly that it is not enforced yet. Filed upstream as ruflo#3415.

Verification

  • pnpm run check: 4489 tests, 4483 pass, 0 fail, 6 skipped (win32). This ran with HOME and every XDG_* pointed at temp dirs.
  • lint:links:internal: 0 errors.
  • Real ruflo 3.44.0 captures are under tests/fixtures/ruflo-components/. They cover typesafe doctor, the MiniLM route marker, funnel status, and governance with and without a policy file.
  • Not yet verified on a real machine:
    • The full ak status → ak uninstall round trip. An earlier sandbox attempt was invalid; see below.
    • The dashboard browser visual check.
  • Reviews: a per-task spec and quality review for each task, a whole-branch review, one fix wave, and a scoped re-review. The final verdict was APPROVED.
  • New tripwire test: it fails if any test resolves a path outside its sandbox. This was added after a sandbox that overrode only HOME leaked to real XDG_* paths while this branch was being verified.

Follow-ups (non-blocking)

  • Funnel re-enabled: if the user runs ruflo funnel enable while ak manages the funnel as off, status shows "applied, not verified" with no fix. This predates the branch.
  • Stale wording: the drifted action text still says "keep your value".
  • Uninstall stuck at exit 1: when a user has edited an ak-set value, uninstall exits 1 on every run. TROUBLESHOOTING should document removing the receipt.
  • Launcher latency: the MCP launcher's version check costs 0.2–0.5 s per launch. It stays uncached because a cached global root may disagree with npm root -g.
  • Upstream ruflo requests (filed 2026-09-24):
    • ruflo#3415: governance wiring into the stdio entry points (follow-up to ruflo#3138/#3151)
    • ruflo#3416: doctor --json
    • ruflo#3417: a per-project, rotated audit log path
    • ruflo#3418: doctor components for MiniLM and governance
    • ruflo#3419: Codex hooks environment (question)
  • ADR-0059: encryption at rest as a separate spec. It will use OS storage-level encryption, in three tiers.

ADR-0058 stays Accepted, with dated notes on what remains. ADR-0016 has an update note: the Claude memory pin is now receipt-owned.

🤖 Generated with Claude Code

Accept the decision to have agentic-kit turn on, verify and explain opt-in
ruflo components, with the task plan that implements it.
…n (ADR-0016 drift)

Adds src/lib/claude-env-projection.mjs: reconcileClaudeComponentEnv projects
machine-wide ruflo component keys into the user Claude settings and the
governance key into a project's settings.local.json (only when a valid MCP
policy exists), and reconcileMemoryPin gives the CLAUDE_FLOW_DB_PATH pin a
receipt, adopting a legacy unreceipted pin once when it matches ak's computed
value. pinProjectMemoryDbPath in setup.mjs now delegates to reconcileMemoryPin.

Widens replaceableRufloRegistration in mcp.mjs to recognize the ak-owned
ruflo-components env keys (Task 0's 2026-09-23 spike confirmed Claude Code
passes the settings env block through to stdio MCP servers and hooks, so
register()'s own desired.env is otherwise unchanged). Records the spike's
findings in ADR-0058 §3 and the implementation status table.
Removes pruneIfEmptyEnvelope from claude-env-projection.mjs. It deleted
.claude/settings.local.json (or a machine ~/.claude/settings.json) whenever
the last owned key was retracted and the file happened to render as {} —
including a file that pre-existed before ak ever wrote to it, since per-key
receipts carry no record of "was this file absent before ak's first write".
Reproduced: pre-create settings.local.json as {}, let ak add
RUFLO_MCP_ENFORCE_POLICY, remove the policy file, reconcile again — the
pre-existing file was deleted. Violates ADR-0016 §4.

An emptied file now stays behind as {}, matching the Task 3 engine's and
AQE's existing behavior (owned-env-projection.mjs never deletes a file it
emptied). Updated the "deleting the policy file..." test to assert the key
is absent from env rather than that the file is gone, and added a
regression test that a pre-existing {} file survives an add-then-retract
cycle. Added a negative test in mcp-scopes.test.mjs: a canonical
user-scoped registration carrying a foreign env key alongside an ak key is
preserved (register() returns false, no remove call).
…hooks env baking

Task 5 fix round 1 (review + controller-widened scope):

- ak now OWNS RUFLO_MCP_ENFORCE_POLICY whenever governance is managed: it
  sets the key on a valid policy and actively clears any inherited/stale
  value otherwise, in the Codex launcher (ruflo-memory.mjs) and both
  OpenCode templates (gateway configure(), hooks projectHookEnv()). Leaves
  an inherited value untouched when governance is not managed.
- The templates' policy check now mirrors ruflo-components/policy.mjs's
  readPolicy exactly (regular file, not a symlink, <=1MiB, plain JSON
  object), plus a provenance check: only a policy file ak itself wrote
  (renderPolicy's `_about: 'Managed by agentic-kit...'` stamp) counts, so a
  foreign .harness/mcp-policy.json (e.g. from a MetaHarness-generated
  project) can never silently cap ruflo's MCP server.
- Renamed the composed helper from applyManagedEnforcement to
  resolveEnforcedEnvironment in both templates: verified empirically against
  stock OpenCode 1.18.32 that a plugin file exporting a name starting with
  "apply" is invoked by OpenCode's plugin loader as an additional plugin
  factory (single context arg), which crashed and silently broke the whole
  gateway's tool projection (opencode-stock-ruflo-gateway.test.mjs's compact
  lazy-call acceptance test went from pass to fail without this rename).
- Closed the reported OpenCode-hooks gap: the deployed hooks artifact now
  bakes the machine component env + AK_RUFLO_GOVERNANCE marker via a new
  RUFLO_COMPONENT_ENV constant substitution (hooksDesiredText in
  opencode-artifacts.mjs, mirroring gatewayDesiredText), wired through
  deployPlugin/pluginStatus's new componentEnv option and
  opencode-lifecycle.mjs's two call sites via opencode-core.mjs's newly
  exported rufloComponentEnvFor(cfg). The governance decision inside
  projectHookEnv is made against the merged (baked + caller) environment,
  not raw process.env alone. The committed template's unsubstituted
  RUFLO_COMPONENT_ENV stays {}, so opencode-hooks.test.mjs's existing
  assertion is unchanged.
Surfaces every ruflo component in `ak status` (with --refresh to re-probe),
adds an `ak sync` step that repairs them right after ruflo upgrades, adds
the trust-manifest disclosure and setup results table, and releases every
ak-owned setting on `ak uninstall`.

Deviates from the task brief in two hermetic-safety fixes: encryptionAtRest
(permanently "not-applied", ADR-0059 not yet implemented) is excluded from
the fixable set so it never enters ak sync's real plan, and
offlineKitConfig() now defaults rufloComponents off (matching its existing
agentBrowser/ruvnetBrain precedent) so pre-existing real (non-dry) setup/
sync/uninstall tests stay hermetic instead of spawning real npm installs or
mutating the developer's actual ruflo funnel state.
…ag, sync convergence

Task 9 fix round 1 per review of 6d2e88e:

1. Add and export rufloProjectRoot(cwd) in apply.mjs: a ruflo project is a git
   root that ALSO has .claude-flow/ (ADR-0058), not merely any ancestor .git.
   reconcileRufloComponents now accepts an explicit projectRoot option
   (undefined -> rufloProjectRoot(cwd); null -> machine-scope only). setup.mjs's
   run_machine passes projectRoot: null explicitly so a dotfiles repo at/above
   $HOME can never receive a .harness/mcp-policy.json. The status section and
   the uninstall step now compute projectRoot via rufloProjectRoot too.

2. uninstall's stepRufloComponents now sets ctx.state.ownershipTeardownOk =
   false when reconcileClaudeComponentEnv or reconcileMemoryPin report
   ok === false, or when a policy removal throws -- matching every other
   uninstall step, so a purge can no longer delete kit.json while a
   ruflo-components receipt could not actually be released.

3. sync.mjs drops a ruflo-components row whose state is needs-ruflo from the
   plan under --no-upgrade (same treatment as 'versions'), keyed off the
   row's own message text rather than a new row field. The status section's
   LEVEL mapping now reports 'blocked' components as 'fail' (not 'warn'), so
   ak sync's post-heal convergence check counts them instead of silently
   reporting "converged" while a component stays broken.

Retrofitted 3 existing ruflo-components-apply.test.mjs fixtures to also
create .claude-flow/ (they were exercising project-scope behavior via a
bare .git, which the stricter project definition no longer treats as a
project). status-golden.json is unchanged (verified) -- the offline fixture
has no managed/blocked component, so neither change alters its rows.
Document ADR-0058's managed ruflo components: the component/managed-value
table and state-meanings table in MANAGED-TOOLS.md, the setup trust
disclosure group and restart reminder in SETUP.md, the Overview > Runtime
panel and About summary link in DASHBOARD.md, two Troubleshooting rows
(governance lockout, stuck applied-not-verified), and one README feature
line. Current-behaviour only, grounded in the merged catalogue, env,
policy, snapshot, apply, status-section, and dashboard-client source on
this branch.
Refresh ADR-0058's Implementation status table against the merged branch
commits (catalogue through the dashboard panel); Status stays Accepted
pending Task 11A's real-machine verification against ruflo 3.44.0 and
filing the four upstream requests. Correct §7 and the Consequences line
per Ruling 14: picker evidence is the `hooks route` probe's `embedder=`
marker and `doctor -c typesafe`, not a per-picker routed-count stat —
`routedByCounts` belongs to the model router (ADR-148), not the agent
pickers this ADR manages. Note that the status section's FIXABLE set
excludes encryptionAtRest (ADR-0059, not yet implemented) and that the
hermetic test suite's default kit.json leaves every ruflo component
unmanaged. Add ADR-0016's Updated note: the Claude memory pin is now
receipt-owned and removed by `ak uninstall` (ADR-0058).
handleRufloComponents resolved project scope with paths.repoRoot (any git
root) instead of rufloProjectRoot (git root AND .claude-flow/), unlike
status/sections/ruflo-components.mjs's equivalent call. A git repo above
cwd with no .claude-flow/ (e.g. a dotfiles repo at $HOME) could therefore
receive project-scope policy reads it should never see. Adds a regression
guard asserting the handler's source uses rufloProjectRoot, not repoRoot.
…ence

ruflo's funnel precedence is env > enterprise-policy > user-config >
project-config > package-default (v3/@claude-flow/cli/__tests__/funnel.test.ts).
ak's own `ruflo funnel disable` only ever lands as the user-tier
('user-config') source — apply.mjs's releaseFunnel already gates its undo
on the same /user/i test. classifyComponent's funnel state previously
called any enabled:false "active" regardless of decidedBy, claiming credit
for a disable something else (an enterprise policy, project config, env
var, or even the package default) produced. Now a funnel disabled by any
source other than ak's own reads as user-managed, with the deciding source
named in the meaning — off, but not an error and not ak's doing.

Corrects the shared test fixture's funnel decidedBy from the placeholder
'user' to the real 'user-config' value (confirmed against a live ruflo
3.44.0 capture), and adds coverage for all five precedence sources plus
parseFunnel's round-trip of each.
The citation for lookbackDays pointed at dashboard-server.mjs:1779; the
line has since moved to 1824 (handleUsage's readIndex call). Caught by
tests/kit/doc-citations.test.mjs.
Real captures against a disposable ruflo@3.44.0 install (Task 11A Steps 2
and 5): doctor-typesafe-installed-3.44.0.txt (the confirmed/enabled row,
CLAUDE_FLOW_ROUTER_TYPESAFE=1, after `ak sync` installed the package —
"v0.1.0 installed; enabled (hash embedder, uncalibrated)"), and
funnel-status-disabled-3.44.0.txt/.json (the disabled state after `ak
sync` ran `ruflo funnel disable`, decidedBy: user-config). Machine-local
paths redacted to /Users/example/... per the existing fixture convention.
ak never writes CLAUDE_FLOW_ROUTER_* or RUFLO_INTELLIGENCE_MODE into the
claude-flow registration (Claude inherits the settings env), so a
registration carrying one is user-authored and must not be replaced
(ADR-0016 §4). Revert the replaceable key set to AGENT_BROWSER_CONFIG only.
…andbox

sandboxHome left XDG_STATE_HOME/XDG_DATA_HOME/XDG_CACHE_HOME/LOCALAPPDATA
and CLAUDE_CONFIG_DIR/CODEX_HOME/HERMES_HOME inherited, so a shell that
exports them to real paths let setup.run_project write the real evidence
cache. Pin or delete each one, make assertSandboxed check the state base,
turn ruflo components off in the routing-retirement fixture, and add a
child-process tripwire that runs under a hostile environment.
The multi-key engine threw on the first user-set key, so one
CLAUDE_FLOW_ROUTER_TYPESAFE=0 left MiniLM and the learning profile
unwritten. Per-key conflicts are now recorded (plan.keys, plan.conflicts)
and skipped; file-level errors still refuse the file, and the single-key
AQE format keeps its refuse-on-conflict contract. A managed value the
user deleted is restored rather than wedging the receipt, and the pending
marker of a mixed add+remove plan keeps the released key's before-state.
Status and sync classified components from probe evidence alone, so an
unwritten Claude settings file read as active, opt-outs never took
effect and sync never planned the reconcile. The Claude projection is
now authoritative first: per-key write -> not applied, a deleted ak
value -> drifted (restored), a foreign or user-edited value ->
user-managed, a file ak cannot touch -> blocked for each of its keys.
Evidence decides only once the projection has converged.

Also: a dropped typesafe package is not applied (sync reinstalls);
opt-outs that still hold ak's env or funnel receipt are fixable and
sync releases them (funnel: true re-enables ak's disable); governance
turned off releases every receipted project; encryption at rest is an
info 'not yet managed' row outside the active count; rows carry their
state id and sync filters needs-ruflo on it; ensureFunnel treats an
unreadable status as nothing to do and leaves an env/enterprise-policy
decision alone; the dashboard route mirrors status when ruflo is
absent; turn-credit evidence without a probe result has no line; the
governance unknown reason says ruflo <= 3.44.0 does not enforce the
policy on stdio launches.
Uninstall removed policy files in every receipted project but released
the project env key, memory pin and their receipt sidecars only for the
cwd project. Projects that receive a project env or pin receipt are now
recorded, and teardown (moved to ruflo-components/teardown.mjs) walks
policies and projects together. A retained user-edited receipt or a
failed typesafe removal under --purge now keeps kit.json and fails the
run.
…catalogue

opencode-core and the Codex launcher hard-coded '3.42.0'; use
componentById('mcpGovernance').minRuflo like apply.mjs does.
…olicy on stdio

The policy enforcer lives only in MCPServerManager, which neither stdio
entry point (bin/mcp-server.js, 'ruflo mcp start') reaches. The
catalogue, trust disclosure and code comments claimed audit and caps
were in force; they now say ak writes the policy ready for when ruflo
wires enforcement (upstream request 6). The funnel and encryption
change text no longer promise what sync does not do.
run_machine printed raw result lines with no table and no restart
reminder (ADR-0058 §3/§7). Both setup paths and sync now share one
report: results table, failed steps, and the reminder after a change.
Also pin the unreferenced 3.44.0 installed-typesafe and disabled-funnel
fixtures in parser and confirmer tests.
…aviour

Governance: ruflo <= 3.44.0 does not enforce the policy on stdio MCP
launches, so the component stays unknown (upstream request 6, with the
evidence table, added to ADR-0058 §8). TROUBLESHOOTING's lockout row
becomes 'governance stays unknown' and no longer says status removes
the variable. ADR-0058 records the real-machine outcomes, marks the
status/uninstall round trip and dashboard browser check as not
verified, and describes projection-first classification, per-key
conflict preservation, opt-out release and multi-project uninstall.
Encryption at rest is 'not yet managed' and outside the active count.
…-applied, so sync re-disables it

Found in the Docker first-use round trip against ruflo 3.45.0: after funnel:true
(release) then funnel:false, status said applied-unverified (restart), which carries
no fix, so ak sync never ran ruflo funnel disable again.

This branch has not been deployed

No deployments
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