Skip to content

fix: data-integrity and security hygiene follow-up (post-GF4) - #4

Merged
OneBigHen merged 6 commits into
mainfrom
fix/data-integrity-security-hygiene
Aug 25, 2026
Merged

OneBigHen merged 6 commits into
mainfrom
fix/data-integrity-security-hygiene

Conversation

@OneBigHen

Copy link
Copy Markdown
Owner

Summary

Scoped follow-up pass fixing four residual risks called out honestly in docs/progress/GF4_RELEASE_REPORT.md after the 0.14.0-rc.1 merge — all four verified against current code (not assumed from prior docs) before fixing.

  • Sources provenance-drop seam (packages/collaboration/src/index.ts): setCanonicalSources did a full clear-and-rewrite of the Yjs sources map on every command, from a snapshot that had already silently filtered out any unparseable source. The next command — even one unrelated to sources — permanently destroyed that entry. Now preserved instead of deleted when it can't round-trip through the typed snapshot.
  • Silent runtime-session reset + fake-complete guard (apps/web/src/services/guideStore.ts, run.$guideId.tsx): loadRuntimeSession silently replaced an in-progress session with a fresh one whenever the guide's steps changed, discarding real recorded progress with zero signal. Now returns { runtime, supersededSession } and the run page shows a dismissible banner when that happens. Also wires in isRuntimeSession — an already-exported semantic consistency guard that existed but was never called on the load path — alongside the Ajv shape check, so a schema-valid-but-inconsistent stored record can no longer render as falsely complete.
  • apps/api organization-owner default (apps/api/src/bind-guard.ts, server.ts, infra/docker/docker-compose.yml): the server minted the organization-owner role for any caller whenever GUIDEFORGE_OWNER_ID was unset, documented as "loopback/dev mode" but never actually enforced as loopback-only. Now refuses to boot on a non-loopback bind without an explicit owner. Also documented + wired up why the compose file's published 8080 port was previously dead (bound to 127.0.0.1 inside the container with no way to configure otherwise).
  • Signature pinning (packages/package-gforge/src/release.ts): verifyReleasePackage verified a release's signature against the public key embedded in the same package — proves internal self-consistency, not authenticity. TrustedKeyStore already existed fully built and unit-tested but was never wired into verification anywhere. Added an optional { trustedKeys } parameter; omitting it preserves prior behavior exactly.

One item from PLAN_TONIGHT.md (Phase 0.3, "remove committed planning-pack zips") turned out not to apply — those files are already .gitignored, deliberately, across three separate prior commits with explicit rationale comments. Documented as a corrected assumption rather than silently dropped. Full details and one newly-discovered-but-not-yet-fixed issue (the same provenance-drop pattern in claimsJson/citationsJson/generationRunsJson) are in docs/progress/CLAUDE_SESSION_WORKLOG.md.

Test plan

  • pnpm check (format/lint/typecheck/test/build across all 25 packages): 98/99 tasks pass. The only failure is @guideforge/api#test's Postgres-dependent cases (6 of 22) — confirmed via git stash to fail identically on unmodified main (no local Postgres in this sandbox, ECONNREFUSED :15432), not a regression from this branch.
  • New/updated unit tests for every fix (Yjs source preservation, superseded-session surfacing, inconsistent-session rejection, bind-guard loopback enforcement, trusted-key pinning + rejection + backward compatibility) — all passing.
  • GitHub CI on this PR (pending)

Security / privacy impact

Two of the four items are security-relevant: the apps/api owner-role default now fails closed instead of open, and release verification now supports authenticity pinning instead of only self-consistency. Neither changes default behavior for existing local-only/loopback usage.

Known limitations

  • apps/xr-web (the one consumer that opens .gforge files from outside the local session) still doesn't pass a trust store to verification — documented in a code comment rather than inventing a key-distribution mechanism unprompted.
  • The parallel provenance-drop pattern in claimsJson/citationsJson/generationRunsJson (same root cause as the sources fix, different write shape) is flagged but not fixed this pass — needs its own dedicated treatment.

Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com

https://claude.ai/code/session_0194R12WjWHUbh7EQbjB3i1h

GuideForge Build Agent and others added 5 commits August 24, 2026 22:48
setCanonicalSources did a full clear-and-rewrite of the Yjs sources map
on every command, from a snapshot that materializeSources had already
silently filtered (any source whose sourceJson failed to parse was
dropped). The next command, even one unrelated to sources, would
permanently destroy that entry. Now preserved instead of deleted when
it can't be round-tripped through the typed snapshot.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194R12WjWHUbh7EQbjB3i1h
loadRuntimeSession silently replaced an in-progress runtime session
with a fresh one whenever the guide's stepIds changed, discarding real
recorded progress with no signal to the caller. It now returns
{ runtime, supersededSession }, and run.$guideId.tsx shows a
dismissible banner when real progress was set aside. The old session
was never deleted from storage (a new sessionId is created) — this
just makes the fact visible instead of silent.

Also wires in isRuntimeSession, an already-exported semantic guard
(completions/currentStepIndex/status cross-field consistency) that the
"resume an existing session" path never actually called, alongside the
Ajv shape check. Without it, a schema-valid but internally inconsistent
stored record (e.g. status: 'completed' with no matching completions)
would be trusted and rendered as complete by the UI.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194R12WjWHUbh7EQbjB3i1h
POST /api/session minted the organization-owner role for any caller
whenever GUIDEFORGE_OWNER_ID was unset, documented as "loopback/dev
mode" but never actually enforced as loopback-only in code. Added
assertSafeBindConfig (apps/api/src/bind-guard.ts), called at startup
in server.ts: refuses to bind to a non-loopback host unless
GUIDEFORGE_OWNER_ID is also set, turning that comment into an
enforced invariant.

Also: infra/docker/docker-compose.yml published 8080:8080 for the api
service without ever setting GUIDEFORGE_HOST, so the app bound to
127.0.0.1 inside the container and the published port was actually
unreachable. Documented this and wired GUIDEFORGE_HOST/
GUIDEFORGE_OWNER_ID through as configurable env passthroughs, with the
new guard as the backstop if network mode is enabled without an owner.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194R12WjWHUbh7EQbjB3i1h
verifyReleasePackage verified the embedded signature against the
public key embedded in the same package — proves internal
self-consistency only, not authenticity. Anyone can sign a forged
package with their own key and embed it alongside; keyId was a
free-form label with no cryptographic binding to the key that signed
it. TrustedKeyStore (signing.ts) already existed, fully unit-tested,
but was never wired into verification anywhere.

Added an optional { trustedKeys: TrustedKeyStore } option: when
provided, the embedded keyId must resolve to a currently-active pinned
entry AND its public key must match what's embedded, or verification
fails. Omitting the option preserves prior behavior exactly.

apps/xr-web/src/main.tsx (the one consumer that opens .gforge files
from outside the local session) still doesn't pass a trust store —
documented why in a code comment rather than inventing a
key-distribution mechanism unprompted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194R12WjWHUbh7EQbjB3i1h
Tracks the four GF4 residual-risk fixes in this branch, the newly
discovered (not yet fixed) claims/citations/generationRuns
provenance-drop pattern, and a corrected assumption: PLAN_TONIGHT.md's
Phase 0.3 (remove committed planning-pack zips) doesn't apply — those
files are already .gitignored, deliberately, across three prior
commits.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194R12WjWHUbh7EQbjB3i1h
@coderabbitai

coderabbitai Bot commented Aug 25, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a9e235d3-4e68-4c14-b128-c17518a47b4f


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Root-level 'pnpm format:check' (prettier --check .) covers the whole
repo including docs/, unlike the per-package turbo check tasks I'd
verified locally, which only run prettier scoped to each package's
src/. CI caught this; verified pnpm format:check, lint, typecheck,
build, boundary, dep-check, and security:policy-test all pass at the
root level before repushing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194R12WjWHUbh7EQbjB3i1h
@OneBigHen
OneBigHen merged commit 58f6790 into main Aug 25, 2026
4 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.

1 participant