Skip to content

Consume blindfold as a dependency; remove in-tree security code#274

Open
mraduldubey wants to merge 34 commits into
mainfrom
md/project-vault
Open

Consume blindfold as a dependency; remove in-tree security code#274
mraduldubey wants to merge 34 commits into
mainfrom
md/project-vault

Conversation

@mraduldubey
Copy link
Copy Markdown
Contributor

@mraduldubey mraduldubey commented May 20, 2026

Summary

  • Extracts the credential-security system (auth-socket, credential-store, crypto, shell-escape, OOB collection, token resolver) into the standalone blindfold package and consumes it as a dependency. Fleet no longer maintains the security layer in-tree.
  • Moves the egress-confirmation subcommand from apra-fleet secret --confirm to apra-fleet auth --confirm (matching what blindfold's OOB launcher spawns). The old path is removed entirely.
  • Hardens test isolation: vitest.config.ts + tests/setup.ts now make it impossible for npm test to write to the real ~/.apra-fleet/data registry (root-caused a load-order bug that had been silently polluting the live registry during test runs).
  • Pins blindfold to v0.0.2, which removes the resolve_secure MCP tool. blindfold's MCP wire surface is now vault-only (set/list/update/delete).

What changed under the hood

  • blindfold/ added as a git submodule pinned to v0.0.2; package.json pulls it in as "blindfold": "file:./blindfold". Switching to the npm-published version later is a one-line bump.
  • New src/services/blindfold-init.ts calls initBlindfold({ dataDir: FLEET_DIR, productName: 'apra-fleet', pipeName: 'apra-fleet-auth' }) at every entrypoint after --version/--help short-circuit. This preserves existing users' on-disk state (~/.apra-fleet/data/credentials.json, the auth socket path, and the Windows named pipe name) - no migration required.
  • All ~26 fleet source files + ~11 test files now import security primitives via from 'blindfold'. Fleet-local re-implementations of resolveSecureTokens / redactOutput / resolveSecureField / SECURE_TOKEN_RE are removed in favor of blindfold's canonical exports.
  • 9 src files and 7 test files deleted (covered by blindfold/tests/).
  • postinstall script in package.json builds the submodule on fresh clones so node_modules/blindfold/dist/ is always present.

Blindfold security hardening (v0.0.2)

The resolve_secure MCP tool that v0.0.1 shipped is a fundamental hole in blindfold's threat model: it took a string containing {{secure.NAME}} tokens and returned the same string with plaintext credentials substituted in - directly to the LLM caller. Even with redaction markers in the response, the plaintext bytes were already in the LLM's context window, transcript, and any upstream logging.

v0.0.2 removes that tool. The blindfold MCP wire surface is now exactly four tools, all vault-management only:

  • credential_store_set - OOB collection, returns a handle
  • credential_store_list - metadata only
  • credential_store_update - metadata only
  • credential_store_delete - returns status

Library exports are unchanged (resolveSecureTokens, resolveSecureField, redactOutput, containsSecureTokens, SECURE_TOKEN_RE, SEC_HANDLE_RE). apra-fleet uses those server-side inside execute_command to substitute tokens just before shell exec and redact plaintext from output before returning to the LLM. Plaintext therefore continues to never cross the LLM boundary in apra-fleet workflows.

Implication for standalone blindfold users: blindfold-without-a-host now stores credentials but cannot resolve them in agentic workflows. That is the intended design - vault-only. Hosts (apra-fleet, or anyone consuming the library) own the execution path and the secret-aware redaction. Documented in blindfold's README.md under "Standalone vs host-integrated usage".

Compatibility

  • MCP tool surface (credential_store_*, execute_command, etc.): unchanged names, schemas, and response shapes.
  • CLI surface: secret --set/--list/--update/--delete unchanged. Only secret --confirm moved to auth --confirm.
  • Existing users' credentials, sockets, and pipes: preserved bit-for-bit through initBlindfold configuration.
  • blindfold's removed resolve_secure MCP tool: not consumed by apra-fleet (it always used the library function), so zero behavioral impact on apra-fleet.

Test plan

  • npm run build (tsc) - clean
  • npm test - 1169 passing, 3 failing (all pre-existing baseline: 1 platform.test.ts login-shell env probe, 2 time-utils.test.ts IST timezone). Independently re-verified by reviewer in a separate clone.
  • npm run smoke - 13/13 pass with isolated data dir
  • npm run build:binary - SEA produces a binary that boots --version in under 400 ms across linux/macos/windows matrix
  • CLI round-trip in an isolated APRA_FLEET_DATA_DIR - secret --set / --list / --update / --delete all green
  • Test-isolation hardening verified empirically - snapshot-diff of ~/.apra-fleet/data/registry.json across multiple npm test runs returns 0 lines
  • Postinstall hook verified - rm node_modules + dist, run npm install, both blindfold/dist and node_modules/blindfold repopulate
  • apra-fleet auth --confirm <bad name> rejects with exit 1
  • blindfold MCP wire surface inspected via live tools/list JSON-RPC probe - exactly 4 vault tools, no resolve_secure
  • blindfold's own test suite: 139 passing, 0 failing
  • ASCII-only sprint-wide, no AI / Claude / Anthropic attribution traces

Direct-use testing (NOT done in this PR)

These were deliberately skipped to keep the PR isolated from the reviewer's live MCP setup. Recommended before merge:

  1. Point a separate Claude Code session at node /path/to/dist/index.js with a custom APRA_FLEET_DATA_DIR; drive register_member + credential_store_set + execute_command echo {{secure.FOO}} (verify [REDACTED:FOO] in output).
  2. Set a credential with network_policy=confirm, run a curl-style execute_command, verify an OOB terminal pops running apra-fleet auth --confirm.
  3. provision_vcs_auth for a test repo to exercise the collectOobApiKey path.

Migration to npm (follow-up)

When blindfold publishes to npm:

  • Change package.json: "blindfold": "file:./blindfold" -> "blindfold": "^X.Y.Z"
  • Optionally drop the submodule

…ackage

Phases 1-4 of the blindfold extraction: config/types, crypto, dual-tier
credential store, token resolver, OOB auth socket, CLI (secret/auth/install),
and standalone MCP server with 5 tools. 140 tests, all passing.

none
Critical: prevent plaintext leaking via redact_markers (opaque handles
instead of values), fix empty-string persistence bug in secret CLI,
validate ciphertext format in decryptPassword.

High: purge session-only expired credentials, remove fleet-specific
claudeAiOauth from credential-validation, fix require() in ESM (async
import for node:sea), remove misleading password zeroing.

Medium: throw instead of process.exit in collect-secret, remove
unnecessary credentialResolve in update handler, add memberName
validation in launchAuthTerminal, export SEC_HANDLE_RE,
add clearTaskCredentials.

Low: rename BlindfolConfig → BlindfoldConfig, server.registerTool
with full schema, fileURLToPath for Windows path safety.
Phase 5: esbuild-based SEA binary (dist/sea-bundle.cjs → blindfold-linux-x64),
build:binary script with CJS wrapper for ESM entry, postject as devDep,
prepack script, npmignore to exclude SEA artifacts from tarball.

Phase 7: README with API usage, MCP tool reference, CLI reference, security
model; Apache-2.0 LICENSE; GitHub Actions CI (ubuntu/macos/windows matrix).

Review fixes: correct Claude Code MCP config path (~/.claude.json), fix
resolveSecureTokens and collectOobApiKey README examples, expand CLI flag
docs, add engines.node >=20, keywords, BlindfoldConfig typo.
- collectOobConfirm gains command/memberName opts, passes --context/--on
  args to the spawned terminal so users see what triggered the prompt
- buildHeadlessFallback is now mode-aware: confirm -> auth --confirm,
  collect -> secret --set, with optional context lines injected
- launchAuthTerminal extracts --context/--on from extraArgs and passes
  fallbackContext to all buildHeadlessFallback call sites
- getAuthCommand confirm branch forwards --context/--on to cmdArgs
- cli/auth.ts confirm branch parses --context/--on, displays network
  egress context, uses readline with "yes" prompt, sanitizes inputs,
  re-validates memberName on the CLI side
- tests: 5 new cases covering mode-aware fallback wording and
  collectOobConfirm additionalArgs construction + 200-char slice
- fix: read version from package.json dynamically so npm version bumps
  are reflected in CLI --version output without a manual edit
- ci: build + test matrix on ubuntu/macos/windows for every push/PR
- release: tag-triggered workflow — test gate, cross-platform SEA
  binary builds with smoke tests, GitHub Release with all 3 binaries;
  npm publish deferred to after fleet integration testing
- export OobLaunchFn type from auth-socket
- credentialSetHandler accepts optional _launchFn for test injection
- mcp-tools tests pass the mock launchFn so OOB flow is controlled
  instead of trying to spawn a real terminal in CI
PM-side sprint scaffolding for the migration of apra-fleet to depend on
the blindfold package: PLAN, progress tracker, status, requirements,
backlog, doer/reviewer role overlays, and the empty permissions ledger.

Doer/reviewer will commit phase artifacts onto this scaffold; see
blindfold-migration/PLAN.md for the 7-phase plan.
Two findings from the Phase 0 review (commit 3918add) recorded for
later action:
- BL-1 (MEDIUM): fresh-clone build needs a postinstall hook so
  blindfold's prepack runs automatically. Addressed in Phase 6.
- BL-2 (LOW): cosmetic progress.json SHA mismatch from the amend.
Adds src/services/blindfold-init.ts with an idempotent initFleetBlindfold()
that calls initBlindfold with dataDir=FLEET_DIR, productName='apra-fleet',
pipeName='apra-fleet-auth' so existing users' credentials, sockets, and
Windows pipe paths remain unchanged.

Wired in at every entrypoint AFTER the --version/--help short-circuits
(to keep them fast) and BEFORE any subcommand dispatch:
- src/index.ts        (MCP server + CLI subcommands)
- src/smoke-test.ts   (top of file)
- tests/setup.ts      (after APRA_FLEET_DATA_DIR is set)

The logger writes directly to process.stderr instead of going through
log-helpers/pino. log-helpers pulls in side-effects (registry +
statusline writes) at import time that were observably breaking the
statusline test suite. Direct stderr keeps init free of cross-module
coupling.

Tests: 1280 passing, 3 pre-existing failures (1 platform login-shell
env probe, 2 time-utils IST timezone arithmetic), same as Phase 0
baseline.
Tests were leaking writes into the real fleet registry, wiping live
members and replacing them with the suite's fake test agents. Root
cause: paths.ts captures FLEET_DIR at module-load time, but
tests/setup.ts set APRA_FLEET_DATA_DIR after its hoisted imports had
already pulled paths.ts (transitively) into the graph, so FLEET_DIR
resolved to ~/.apra-fleet/data despite setup.ts's intent.

Two-layer fix:

1. vitest.config.ts sets APRA_FLEET_DATA_DIR at the very top of the
   config module, before any test code is loaded, AND declares
   test.env so vitest itself propagates it into the test process.
   This is the earliest reachable point in the test lifecycle.

2. tests/setup.ts now refuses to start (process.exit(2)) if
   APRA_FLEET_DATA_DIR is missing or differs from the expected tmp
   dir. Belt-and-suspenders: even if vitest config drifts, a
   subsequent run cannot silently write to the real data dir.

Verified: npm test runs clean, real registry.json untouched, test
registry.json empty after cleanup.
Update progress.json with the actual Phase 1 commit SHA (6dbe017,
content-identical to the prior 7cf1aab snapshot but reauthored after
the doer's background session re-amended). Record INC-1 (registry
wipe + recovery + test isolation hardening commit eb65946) in
backlog.md.
Without submodules: recursive, GitHub Actions clones the apra-fleet repo with an empty blindfold/ directory (just the gitlink), and the package.json postinstall hook then fails with 'cd blindfold && npm install' because the dir has no package.json. Pass submodules: recursive to actions/checkout in ci.yml (all 5 jobs), fleet-e2e.yml, and blindfold-ci.yml. Also fix a pre-existing non-ASCII em-dash in ci.yml line 271 that the project's ASCII pre-commit hook surfaced.
mraduldubey and others added 4 commits May 20, 2026 15:26
v0.0.2 removes the resolve_secure MCP tool. apra-fleet consumes
blindfold via library exports (resolveSecureTokens etc.) inside
execute_command, so this is a no-op on apra-fleet's behavior and
MCP surface. Plaintext continues to never cross the LLM boundary.
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