Skip to content

fix: close the v0.22.1 re-audit medium-severity findings (ME-03..ME-22)#88

Merged
CodeWithJuber merged 6 commits into
masterfrom
claude/parallel-solving-3dc0ji
Jul 19, 2026
Merged

fix: close the v0.22.1 re-audit medium-severity findings (ME-03..ME-22)#88
CodeWithJuber merged 6 commits into
masterfrom
claude/parallel-solving-3dc0ji

Conversation

@CodeWithJuber

Copy link
Copy Markdown
Owner

What & why

Completes the v0.22.1 edge-case re-audit remediation, following the merged criticals (#85) and high-severity fixes (#87). This PR closes the medium-severity findings — robustness and trust-boundary polish — as five independently-green workstream commits plus a consolidated changelog. Only ME-23 (hook shell→exec-form migration) is deferred: it would churn the just-landed HI-05 ownership manifest and RA-12 quoting for a "safer design" that isn't a correctness bug.

Ledger (ME-05, ME-06, ME-07)src/ledger.js, src/ledger_store.js

  • Evidence now separates resolved proof from merely well-formed refs: ci:/human:/file: are format- and existence-checked, and unresolved pointers like test:<id> or a missing file: path can't lift confidence into the trusted/serving band (they count at reduced weight, capped below the serve floor). A resolvable git-ref confirmation is unchanged.
  • Secret-shaped evidence refs, authors, tombstone reasons, and provenance metadata are refused before disk; quarantine stores redacted records and keys them by a trusted content hash (not the record's own attacker-controllable hash), so distinct forgeries sharing a fake hash are both retained.

MCP (ME-08, ME-09, ME-10, ME-11)src/emit/mcp.js, src/integrations.js

  • Adoption is tracked per {server, target} (legacy adopted: [name] still honored), so adopting a same-name server for one tool never authorizes overwriting another's. A divergent user entry sharing a built-in name (forge-cortex) is preserved, not overwritten. Add/remove are atomic (record nothing on partial emit; keep the entry and report incomplete if cleanup fails). Names must match ^[a-z0-9][a-z0-9_-]{0,63}$, foo/forge-foo filename collisions are rejected, and commands are serialized safely into YAML/TOML.

Doctor (ME-15, ME-16, ME-17, ME-18)src/doctor.js

  • Functional probes replace existence checks: validates real hook wiring against the template, verifies ~/.forge is a symlink/dir whose guard assets resolve (catching plain-file shadows and dangling links), runs a live redactor self-test, and records a partial --fix sync (nested action:"error" rows) as a failed repair.

Config / init (ME-12, ME-13, ME-14, ME-21, ME-22)src/repo_config.js, src/init.js, src/cli.js

  • Non-object JSON (null/[]/"x"/42) is treated as corrupt (bytes preserved) for both repo config and settings; forge.config.json writes are backed-up + temp-file+rename atomic; roo is a selectable tool and aider/continue/windsurf/roo auto-detect; the ledger .gitattributes rule is matched by exact line (a comment no longer suppresses it); the global-settings merge is disclosed before it mutates the file.

Stack / sync (ME-03, ME-19, ME-20)src/stack.js, src/sync.js

  • detectStack() surfaces workspaces + packageRoots (bounded scan) so a root command can't silently claim to cover a monorepo; forge sync returns an explicit PARTIAL status (and exits non-zero) when any target fails; a corrupt legacy .forge/rules.json warns and falls back instead of aborting sync.

Integrated branch: 1056 tests / 0 failures, Biome + tsc + forge docs check green, forge verify PASS.

Checklist

  • npm test passes (1056 tests, 0 fail; ~40 new regression tests)
  • npm run check passes (Biome)
  • New public functions have a test
  • Conventional commit messages (fix:/docs:)
  • CHANGELOG.md updated under ## [Unreleased]
  • No new runtime dependency (zero-dep preserved; YAML/TOML handled by hand)
  • Substrate/docs updated (forge tools list, GUIDE; forge docs check green)

Risk & rollback

  • Risk level: low-medium — the ledger confidence change (ME-05) intentionally lowers the weight of unresolvable typed refs, and sync now exits non-zero on partial failure. Legitimate git/untyped-ref evidence is unchanged; each workstream is self-contained.
  • Rollback plan: revert the branch, or any single workstream commit.

Extra checks (tick if applicable)

  • npm run typecheck passes
  • Input validated at boundaries; errors handled (the theme of this batch)
  • Authorization/ownership checked (per-target MCP ownership; config-shape validation)
  • Logs contain no secrets/PII (ME-06 refuses secret metadata; test literals via test/_fixtures.js)
  • If AI-assisted: I understand it, verified the package APIs, and it has tests

🤖 Generated with Claude Code

https://claude.ai/code/session_01KfCEnSJzSG914yyVesjjD6


Generated by Claude Code

claude added 6 commits July 19, 2026 09:23
…ction; catch nested repair errors (ME-15..ME-18)

Replace existence-only probes with functional ones so doctor can no longer
report a broken subsystem as healthy. All checks stay fail-open and keep the
ok/warn/fail/na result shape.

- ME-15 settings: verify the ACTUAL Forge wiring — every REQUIRED guard identity
  from the settings template (compared via the exported guardKey) plus permissions
  — instead of trusting the `_forge` marker + any hook. A marker with one unrelated
  hook is now DEGRADED, ACTIVE only when the expected guard set is installed.
- ME-16 install: `~/.forge` must be a symlink/dir whose required guard assets
  (protect-paths.sh, secret-redact.sh/.mjs) resolve, are readable, and (for .sh)
  executable. A plain file, a dangling symlink, or a partial install is now
  UNAVAILABLE/FAILED; ACTIVE only when the assets resolve.
- ME-17 redaction: run a safe, side-effect-free self-test — a runtime-assembled
  fake secret fed through the real secret-redact.mjs — and require it to redact
  and preserve output shape. ACTIVE only on a passing round-trip; DEGRADED when
  node/redactor is missing or the output is wrong. Health maps secret-redaction
  to this self-test, not the generic node check.
- ME-18 repairs: repairFailure inspects NESTED report rows — a partial sync whose
  top-level result is fine but whose emitter rows carry action:"error" (or a
  writeForgeConfig-style {ok:false}) is now recorded ok:false with the reason.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KfCEnSJzSG914yyVesjjD6
…ata; trusted quarantine identity (ME-05, ME-06, ME-07)

ME-05: separate record-integrity validity from evidence RESOLUTION. validateRef now
carries real format grammars — `ci:` must be a CI locator (URL / owner/repo@run / run id),
`human:` must be an explicit ratification (author@ref), and `file:` resolves to an existing
path when a repo root is available (pure/no-throw, mirroring the `git:` resolver). A new
pure refStrength() classifies each ref as RESOLVED (untyped/legacy, git, ci, human) or
FORMAT-ONLY (file, test). In val(), format-only evidence counts at UNRESOLVED_WEIGHT and a
claim with no resolved confirmation is capped at UNRESOLVED_VAL_CAP (0.55) — below the reuse
SERVE_FLOOR (0.6) and the trusted band (0.65). So `test:made-up-run` / `file:/does/not/exist`
can never lift confidence into the serving range, while a resolvable git-ref confirmation
raises it exactly as before. Strength is re-derived purely from the ref, never trusted from
the stored record (same discipline as the ORACLES weights).

ME-06: run the secret detector over evidence refs/authors, tombstone reasons, and provenance
metadata BEFORE append. outcomeRecord refuses secret-shaped ref/author at mint; appendRecord
is the single persist choke point that refuses any secret-bearing metadata record, so no
channel (evidence, provenance, tombstone, import) can write a credential to disk.

ME-07: quarantine dedups by a TRUSTED contentHash over the (redacted) rejected record plus
its rejection reason — never the record's attacker-chosen `h`. Two distinct forged records
sharing one fake `h` now get distinct identities and both survive; malformed no-`h` lines are
captured instead of silently dropped; and the stored record is redacted so quarantine never
persists raw secret material. mergeDirs now scans the source logs RAW line-by-line (instead
of through the hash-deduping read path) so nothing is collapsed or lost before quarantine.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KfCEnSJzSG914yyVesjjD6
…; explicit PARTIAL sync status (ME-03, ME-19, ME-20)

ME-03: detectStack() read manifests only at the repo root, so npm/pnpm/yarn
workspaces and Turborepo/lerna/Maven/Gradle subprojects (and nested Python
packages) were invisible — a single root test command silently "covered" a
whole monorepo. detectStack now surfaces two additive fields: `workspaces`
(declared globs from package.json workspaces, pnpm-workspace.yaml, lerna.json)
and `packageRoots` (nested package/suite roots found on disk via a bounded,
budgeted, depth-capped BFS — never a deep walk). Both are [] for a plain
single-root repo, so testCommands/testRunners and the rest of the shape are
back-compat.

ME-20: legacy `.forge/rules.json` was parsed with a bare JSON.parse, so a
single typo threw and ABORTED `forge sync`. It now reads through a guarded
parser (like loadConfig): invalid JSON warns once to stderr, falls back to the
default rules, surfaces a sync warning row, and never rewrites the file — the
user's bytes are preserved to fix.

ME-19: sync writes AGENTS.md, then per-tool files, then MCP files and is not
transactional; a mid-way failure was only reported per-target, so init/CLI
could still imply every tool is ready. sync() now returns an explicit
aggregate: `partial` / `status` ("PARTIAL" when any target row is
action:"error", else "OK"). The `forge sync` and `forge init` summaries reflect
PARTIAL and exit non-zero instead of unconditional success. (Chosen the
explicit-status approach over full temp+rename transactionality across every
target: the emit helpers already writeIfChanged, and per-target atomicity would
be a large, invasive change for little gain over an honest aggregate flag.)

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KfCEnSJzSG914yyVesjjD6
…ies; atomic add/remove; validate names (ME-08..ME-11)

MCP ownership was global-by-name, so `--adopt context7` could authorise
overwriting every target's same-name entry, built-in registry names were
implicitly owned (letting a divergent user `forge-cortex` be clobbered),
add/remove left disk and the managed-set record inconsistent on partial
failure, and the exported emitter trusted arbitrary future server names.

- ME-08: record adoption PER TARGET as `{server, target}` pairs in
  `mcp.adopted`; `owns(target, name)` consults only that target. Legacy bare
  names are still honoured as a wildcard and migrated to pairs on next write.
- ME-09: registry names carry no implicit ownership — a divergent same-name
  entry is preserved and reported with the adopt hint; forge refreshes only
  what it can prove it wrote (byte-identical spec, forge marker, or adoption).
- ME-10: add emits all targets first and records only on full success; remove
  cleans all targets first and drops the record only when every cleanup
  succeeded — per-target write/cleanup failures surface as `error` rows and
  are reported as an incomplete transaction instead of throwing.
- ME-11: validate every managed name against `^[a-z0-9][a-z0-9_-]{0,63}$`
  before any write, reject `foo`/`forge-foo` Continue-filename collisions, and
  serialise command strings safely into YAML (quoted) and TOML.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KfCEnSJzSG914yyVesjjD6
…tes; complete tool list; exact gitattributes rule; disclose before merge (ME-12,13,14,21,22)

ME-12: readConfigFile (repo_config) and readExistingSettings (init) now treat
valid-but-non-object top-level JSON (null / [] / "x" / 42) as CORRUPT, not as
empty {}. Reads report corrupt and writes refuse, so a later write can never
silently replace bytes a human may still want to fix.

ME-13: writeForgeConfig now backs up an existing valid config (timestamped) and
writes via a temp file + atomic rename — a crash mid-write can no longer truncate
the unified config. The corrupt-refusal path (ME-12) is preserved.

ME-14: reconciled KNOWN_TOOLS and primary-tool auto-detection with the emit
targets — added roo to KNOWN_TOOLS and added on-disk detection markers for
aider (.aider.conf.yml), continue (.continue), windsurf (.windsurf), and roo
(.roo). `forge tools <name>` now accepts all of them; docs updated.

ME-21: ensureLedgerGitattributes now parses the exact active attribute LINE
(non-comment pattern + attr) instead of a substring check, so a comment merely
mentioning .forge/ledger/ no longer suppresses the real union-merge rule; an
existing real rule is still not duplicated.

ME-22: the GLOBAL-settings disclosure is now emitted BEFORE the merge mutates
~/.claude/settings.json — mergeSettings fires an onNotice(target) hook prior to
any write, threaded through init as onSettingsNotice, and the CLI prints the
consent line via that hook instead of after the fact.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KfCEnSJzSG914yyVesjjD6
Consolidated [Unreleased] entries for the ledger, MCP, doctor, config/init,
and stack/sync workstreams.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KfCEnSJzSG914yyVesjjD6
@CodeWithJuber
CodeWithJuber marked this pull request as ready for review July 19, 2026 12:45
@CodeWithJuber
CodeWithJuber merged commit b54e2c1 into master Jul 19, 2026
8 checks passed
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.

2 participants