FarmAgent2 · Code & Security Review
v3 · post-session · 2026-06-28
Re-evaluation against the Jun-27 baseline

The security floor rose. The authorization ceiling and the hygiene didn't.

The first review called this "a capable, fast-moving product on shaky operational and security foundations" — 3 criticals, ~18 highs, decorative CI, committed credentials. A session of focused work closed the two most dangerous criticals and turned CI into a real gate. What remains is narrower and more structural: authorization breadth, a god-file, and a repo that still treats prod as a scratchpad.

Criticals (was 3)
2 closed · 1 open
Tenant isolation
Impossible-match bug fixed + guarded
CI/CD
Now gates (partly)
Authorization breadth
Still UI-only outside Labor
Repo hygiene
Dead tree gone · cruft remains
Net posture
Materially safer, not done
What changed this session

The delta

Shipped to production & verified

  • customer_id is a name, never a UUID — purged 31 casts + the resolver across 78 call-sites; 6 onboarding tables + billing migrated uuid→text; RLS de-cast.
  • A guard test (test_no_uuid_customer_id.py) now fails the build if the pattern returns — the rule is enforced, not just documented.
  • Feature 038 shipped (cross-artifact session merge) through the full spec lifecycle — the strongest-tested subsystem in the codebase now.
  • 038 atomicity fix — merge records + entity write now commit in one transaction (was three), closing a reversibility-bookkeeping gap.
  • DB topology confirmed by live introspectiondigital_twin is a separate instance (postgres-digital-twin), not a schema in postgres-prod; recipe documented.
  • Context system versionedFruitScout/FarmAgent-Context created; the rule codified in AGENTS.md (RULE #2), CLAUDE.md, context.md, habits.md.
The five "exploitable today" issues — then vs now

Status of the original launch-blockers

Each maps to a finding from the first review. Statuses are evidence-based from a fresh scan of main; two are marked verify where the scan saw the surface but didn't confirm the fix end-to-end.

Closed
1 · Committed production JWT signing secret
Was: FA2_JWT_SECRET hardcoded in tracked scratch scripts — anyone could forge tokens. Now: zero hardcoded secrets in services/api; loaded from Secret Manager; a blocking gitleaks scan in CI; historical secrets rotated/neutralized.
Closed
Tenant-isolation: the UUID-vs-name mismatch (the RLS "could never match")
Was: JWT customer_id was a UUID while dt_* stores a text name — the policy comparison could never match, so isolation rested entirely on per-query WHERE. Now: customer_id is text everywhere, RLS compares text=text, and a guard test prevents regression.
Addressed · verify
2 · Cross-tenant IDOR in campo_plan
Was: took customer_id from the client form/body, never compared to the JWT. Now: the resolver sweep moved routers (incl. campo_plan) to a JWT-derived customer_id — a client can no longer pass customer_id=Sauza. Verify the form parameter is fully removed from every campo_plan path.
Open
3 · Unauthenticated privileged endpoints
Was: /api/internal/* exempt from auth — could mint API keys / list users with no credentials. Now: /api/internal is still an open middleware prefix, and all GET requests bypass APIKeyMiddleware entirely — read-endpoint protection depends on per-route deps being applied consistently (un-audited). Still a launch-blocker.
Verify
4 · Fail-open canopy callback (auth bypass + SSRF-to-GCS)
Was: if CANOPY_CALLBACK_SECRET unset, the HMAC check is skipped; endpoint fetches a client-supplied gcs_uri. Now: /api/review/canopy/callback remains an open prefix; fail-closed behavior not confirmed. Make it fail-closed.
Pre-launch gate
5 · Server-side authorization bypass (tier & tenant)
Was: upgrade-labor-tier self-serve; switch-customer issued tokens for any customer. Now: entitlement is enforced server-side only in the Labor router; POST /api/auth/upgrade still mints a management token. Billing is now real (Stripe), but gating breadth is the pre-launch item.
By dimension — then → now

Dimension scorecard

DimensionJun 27NowWhat moved
Security & tenancyCritical gapsImproved · gaps remainSecrets remediated, tenancy bug closed; unauth-internal & authz-breadth open.
Data model & ETLIntegrity risksClarifiedHard Wall confirmed a separate instance; 038 writes now atomic. Staging-delete invariant still unenforced.
Backend architectureWorkable, tangledWorkable, tangledDead apps/api/ deleted; review.py god-file (~2,400 LOC, no tests) untouched.
Frontend & productRich, needs refactorRich, needs refactorUnchanged — 3 parallel OpenLayers maps still un-abstracted; 3 e2e specs.
Platform & CI/CDFragile / decorativeNow gates (partly)CI gates ruff + unit tests + blocking secret-scan + gated prod deploy. Type/tsc/eslint still || true.
What still gates a commercially-safe codebase

The remaining work, ranked

1 · Authorization breadth

Server-side entitlement only in Labor; everything behind FeatureGate is UI-only and bypassable by direct API call. /api/internal + GET-bypass need an audit. The #1 launch-blocker.

2 · The review.py god-file

~2,400 lines conflating chat, reconciliation, promotion, geometry, jobs — zero tests. The riskiest file to change. Split & cover.

3 · Make CI fully block

Drop the || true on mypy / tsc / eslint so type and lint failures actually fail the build. The gate exists; it's half-open.

4 · Evict prod-as-scratchpad

33 tracked scratch_*/debug_*/.dump files (+~1 MB stray data), none gitignored. Move investigation scripts out; gitignore the rest.

5 · Test the high-risk surface

gis, review, vlm_agent, campo_plan have no tests; auth has no unit test. Onboarding is well-covered — extend that discipline.

6 · Refresh the docs

Now the inverse problem: CLAUDE.md understates reality (claims no CI & that apps/api exists — both false). Re-sync it to the improved state.

Bottom line

Where the code sits on the path to release

The dangerous, exploitable-today items that dominated the first review are mostly handled — committed credentials, the tenancy-comparison bug, and the dead-tree/CI-theatre problems are gone or much improved. The codebase is meaningfully safer than it was. What's left is narrower but real: authorization is enforced inconsistently outside Labor, the review.py monolith is an untested liability, and the repo still ships investigation scripts beside product code. None of these is a "stop everything" fire the way a committed signing secret was — they're a focused, finishable punch-list standing between "safe internally" and "safe to sell."