Skip to content

fix(rank-tracking): finalize stuck runs after DFS snapshots - #4

Open
Kroma86 wants to merge 78 commits into
mainfrom
fix/rank-check-finalize-hang
Open

Kroma86 wants to merge 78 commits into
mainfrom
fix/rank-check-finalize-hang

Conversation

@Kroma86

@Kroma86 Kroma86 commented Sep 12, 2026

Copy link
Copy Markdown
Owner

Summary

  • Fixes rank-check finalize hang: DB status flip first + fire-and-forget PostHog so runs stop wedging at status=running after DFS snapshots are written.
  • Adds stale reclaim / reconciler path (reconcileStuckRankCheckRuns) so stuck finals can complete from snapshots.
  • Tip commit: 7b0eb18. Do not deploy without Jon yes.

Important

  • Branch tip is the finalize fix, but this branch currently sits ~78 commits ahead of origin/main (peer WIP / agency platform ancestry). Review scope carefully before any merge/deploy to production.
  • Dirty local working tree was not included in this push.

Test plan

  • After Jon-approved deploy: one daily wave with 0 stuck running finals when snapshots exist
  • Spot-check completed runs show positions in UI/API (completed-run filter)
  • Confirm PostHog capture still fires without blocking finalize

Made with Cursor

Kroma86 and others added 30 commits August 30, 2026 08:56
…, SAM homegrown-otto skill, Access selfhost wiring
Selfhost can disable OpenRouter ZDR for first-party Anthropic, and SAM can report HomeGrown OTTO queue plus agency-board pixel status without claiming live applies.

Co-authored-by: Cursor <cursoragent@cursor.com>
Port analysis and care recipes into OpenSEO. Refuse Ads, Cloud Stacks,
paid PR, and auto-publish. Fix tsc so selfhost deploy can finish.
get_agency_score_inputs now reports GSC/GA4 connection state, GBP
native-gap status, and last-28-day Search Console site totals (per-day
rows summed, impression-weighted position — the default ["query"]
dimension's first row is the top query, not totals). Null means not
measured, never an invented zero: unmapped, empty, errored, and
non-finite paths all return null; a genuinely measured zero stays zero.
Sam's system prompt carries the niceseo-pillars law verbatim.

Reviewed: Grok 4.6 (cross-family one-off, Jon-authorized 2026-08-31 —
both Kimi reviewer lanes quota-blocked), round 2 APPROVE.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Set via API 2026-08-31; pinned here so a redeploy doesn't silently
reset the app to the 24h default.

Reviewed: Grok 4.6 (cross-family one-off, Jon-authorized), APPROVE.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Kimi-reviewed (1 repair round, APPROVE). Two tools wrapping the existing
ai-search services: get_ai_brand_visibility (LLM Mentions, SoV, cited sources)
and explore_ai_prompt (max 2 models). Cache-first, credits noted in
descriptions, absent metrics say 'not measured', registered for MCP and Sam.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Kimi-reviewed (APPROVE, round 1). wrapLanguageModel middleware adds cache
breakpoints (last tool, last system, last message; <=4) for anthropic/ model
ids only; OPENROUTER_PROMPT_CACHE=false or any other model = byte-identical
identity. Per-step cache counters logged as [sam] cache. Expected 75-90%
input-cost cut on multi-step turns.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…re inputs

Lead-authored (Do-Not-Route: schema). sam_loops + sam_loop_runs (one in-flight
run per loop enforced at the DB), ai_visibility_configs/prompts/runs (prompt-set
versioned baselines), and gscTopQueries (top-25 real GSC queries by clicks) in
the agency score inputs. Migration generation happens in the loops build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Kimi-reviewed (1 repair round, APPROVE — zero findings above LOW). A loop =
skill|custom prompt + cadence + project. Cron claims due loops (DB-enforced
single in-flight run), SamLoopWorkflow runs Sam headlessly (step cap 24) with a
FAIL-CLOSED tool allowlist: free readers + propose_homegrown_otto_fixes only —
new tools are blocked by default. Plain-English run reports, honest
proposalsQueued (successful proposals only), six seeded default loops per
project, loops UI page, read-only MCP tools, D1+PG migrations with parity test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…posal

From the mined SA playbook catalog. Double-reviewed (Kimi + second seat), one
repair round, APPROVE: only striking-distance auto-seeds as a loop template —
the three parameterized skills are on-demand (a headless loop cannot supply a
prospect domain / city+service / competitor set, and must never improvise one).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ft handoff, starter-loop seeding

Four Session-5 papercuts: manual trigger no longer skips the scheduled run
(future nextRunAt untouched; due/missing/corrupt re-anchors from now);
mission card click scrolls the report into view once per selection; Ask-Sam
draft is read+cleared by the chat composer on mount (prefill, no auto-send);
empty loops list gets a one-click idempotent starter-loops seed (real
unique-index test). Built by Cursor auto in a detached worktree off 153a51f;
Kimi review round 1 FIX-THEN-COMMIT (mock-only idempotence test, scroll
hijack, draft-key linger, unparsable-anchor strand), round 2 APPROVE.
tsc clean; vitest 1200/1200 in this tree.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QruuTUXfQXZYnL5igfw9fH
GET /api/internal/agency-loop-reports — read-only, bearer-gated with the
existing AGENCY_SCORE_EXPORT_TOKEN (deliberate reuse, commented): completed
and failed sam_loop_runs joined to loop + project, inclusive lexical
finishedAt cursor (since required, ms-ISO validated), limit clamped 1-200,
no-store, no write path. 9 route auth tests + 5 service tests on in-memory
SQLite. Built by Cursor auto in a detached worktree off 153a51f; Kimi
review r1 FIX-THEN-COMMIT (missing route auth tests, since validation),
r2 verified clean, r3 APPROVE alongside the Hermes poller consumer
(mirrored in the agency-seo repo). tsc clean; vitest 1214/1214 here.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QruuTUXfQXZYnL5igfw9fH
Replaces the auto-redirect home with an agency home: Sam prompt bar
(sessionStorage prefill), workflow chips, missions rail (org-wide recent
loop runs, deep-link selects the run), portfolio table (GSC last-28d when
connected, tracked keywords, loops active, setup pills). Org-scoped
server functions behind auth middleware; honest not-measured states.
Built by Cursor auto; Kimi review APPROVE (tenant isolation pinned in
SQL tests). 1224 tests green, typecheck clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QruuTUXfQXZYnL5igfw9fH
Jon: the login page should never ask. With one IdP and 730h sessions,
Access now redirects straight through; when the Cloudflare session is
alive the flow is fully silent. Kimi micro-review APPROVE (no change to
the allow-policy, session model, or /api/internal bypass scoping).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QruuTUXfQXZYnL5igfw9fH
…t (P10)

Grok build in a worktree, Kimi adversarial review: round-1 FIX (5 loophole
findings: self-supplied targets, SERP-fabrication path, undefined spend gate,
read-aloud ceremony, unlabeled business-fit factor) — all repaired, round-2
APPROVE. On-demand skills only: no auto-seeded loop template (monthly content
loop token cost is Jon's call), no publishing path, no new storage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QruuTUXfQXZYnL5igfw9fH
Both Grok-built, Kimi K3 adversarial review APPROVE round 1.
page-pruning: prune/noindex/merge candidates, recommends only, no GSC -> keep-watch cap.
brand-facts: canonical brand record with provenance tags; llms.txt + JSON-LD drafts from confirmed facts only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QruuTUXfQXZYnL5igfw9fH
Map -> brief -> draft chain as a seeded monthly custom loop; seeder now
honors template sourceType/customPrompt; honest skillName typing surfaced
and fixed three null-unsafe UI consumption sites in SamLoopsPage.
Grok-built; Kimi r1 FIX (as-never typing lie) -> repair exposed latent UI
nulls -> r2 APPROVE. Tests + full tsc clean. Jon approved the monthly
token cost 2026-08-31.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QruuTUXfQXZYnL5igfw9fH
Box-pushed artifacts (alert cycles, monthly reports, digests) now land in
OpenSEO: POST /api/internal/agency-ops-artifacts (bearer, dedup by
kind+sourceKey), agency home alerts card, Operations page with sandboxed
HTML/markdown rendering. SamLoopsPage reports now render as markdown.
… severity, honest error states

Post-review fixes: parse the box's real snake_case alert JSON (with
camelCase fallback), treat high/critical case-insensitively, allow null
domains on site-wide alerts, reject array roots as parse errors, ungate
the home alerts query from projects, add real error/not-found states on
the Operations page, count high alerts beyond the display cap, and test
ingest validation against the real service.
…error state, count summing, conflict-lookup guard

Non-validation ingest failures now return 500 ingest_failed (never the
internal message, never a 400 the pusher would treat as permanent); the
home alerts card gets an honest error state; lowercased severity counts
sum instead of overwriting; insertIfNew guards the empty conflict lookup.
…hecks, MCP tools, project page

Builder: cursor composer. Lead fixes: regenerated routeTree for the new
ai-visibility route, narrowed prompt-result union before brandMentioned,
widened the prompt-explorer model set lookup to accept platform strings.
Rotating suggested asks in the prompt bar, favicon/letter-tile avatars,
status + setup pills from real fields only, merged honest Traffic column
(no time series fetched, so no sparkline — never fabricate), missions
rail edge fades + scroll buttons, shared pill tones, drill-in rows.
…letter tiles, muted contrast, quiet-state dashes

Post-review fixes: pills use solid daisyUI color + -content text (light
theme AA), invisible scroll buttons get pointer-events-none until
revealed (hover or keyboard focus), letter tiles at L32% for white text,
muted tone at /70, setup pills keep the em-dash quiet marker, scroll
state rebinds via MutationObserver instead of every parent render.
…d-operable rows, solid info pill, focused placeholder

The useMemo sat after five early returns (conditional hook call — crash
on error-then-recover renders). Rows get tabIndex/role/Enter+Space and a
visible focus state. info tone goes solid like the others. The prompt
input keeps a static placeholder while focused so the field is never
blank; the rotating overlay still owns the unfocused state.
…p role=link so the nested connect link keeps its semantics
…isolation, deterministic cap self-repair, ISO cutoffs, 1h failure backoff, measured-only denominators, honest cost labels
…ess entirely, propagate reclaimed outcome, CAS the failure backoff
Kroma86 and others added 22 commits September 1, 2026 14:27
…rom the deployment's users in cloudflare_access mode

Proven live: POST /api/internal/audits answered 409 no_actor_available and
POST /api/internal/gsc|ga4 answered property_not_visible reason no_grant,
because the routes looked for member rows of "shared-workspace" — the
workspace merge moved every project there but cascaded the legacy orgs'
member rows away. In cloudflare_access mode the deployment is one tenant:
the actor is the earliest user with a non-empty email; grant holders are all
users (email not required), same ordering. local_noauth keeps the member
path; hosted stays refused; project ownership checks unchanged.

Builder: Grok 4.6 (worktree off 5032b04). Review: Cursor auto = Composer 2.5,
r1 FINDINGS (null-safe email, zero-users 409, blank-email tests) -> repair ->
r2 APPROVE. Gates: tsc 0, targeted 162+.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JK3iKtUGinxgMnUrxpxjbr
…loudflare_access mode

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JK3iKtUGinxgMnUrxpxjbr
… requested property

Explicit propertyId + name mismatch + flag → attach with a warn line and displayNameMismatchAccepted in the response; without the flag the 409 is unchanged; auto-pick ignores the flag. 4 tests. Built by Grok 4.6, reviewed by Cursor auto (Composer): APPROVE, no HIGH.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JK3iKtUGinxgMnUrxpxjbr
…ernal GA4 door (Grok build, Composer review APPROVE)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JK3iKtUGinxgMnUrxpxjbr
…RKER_CPU_LIMIT=off)

Wix audits failed with cpu_limit on the 30 s plan default in cloudflare_access mode. Composer review APPROVE (one HIGH about free-plan self-hosts in the wild, not this deploy).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JK3iKtUGinxgMnUrxpxjbr
…t-out WORKER_CPU_LIMIT=off)"

This reverts commit dc6ccbe.
…pt-out WORKER_CPU_LIMIT=off)"

This reverts commit a5347be.
… GET /api/internal/agency-monthly-export reader, rankSummary + GSC window fields on score-inputs

Composer 2.5 build + 2 repair rounds; native Kimi K3 binding review r4 APPROVE (r3 gate evidence corrected). Gates: vitest 1575, tsc 0, vite build 0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCD7JKhqdBcUHVj3gNnZDa
….1000, default 40) — scheduler, service, trigger 429 path; env.d.ts + alchemy binding; tests

(cherry-picked from build-loop-cap-20260902 commit 9d7e300; previously deployed 2026-09-02 and reverted accidentally by the 2026-09-03 fixkeys deploy — restoring)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…'already applied by the pixel', per path) — loops stop re-proposing applied fixes

served_fix_keys/served_fix_paths flow from the agency board metrics pixel slice into the tool text + structuredContent; fail-closed when per-path detail is unavailable. Hermes side (seo_dashboard_export.py + agency_dash_push.py) shipped separately. Reviewed: Cursor auto r3 APPROVE 2026-09-03; combo review with 9d7e300 APPROVE.
…ities (monthly) templates; deterministic per-project schedule spread

Three new client-safe loops (prompts enforce: two read-only, one title+description pending-only). One unified spread rule shared by app and migration: weekly -> assigned weekday (fnv1a(projectId:loop)%7), monthly -> day 1+hash%28, on seed and every advance; daily untouched. Kills the end-of-month cliff (~250 runs/day -> <=18/day). Includes one-off D1 stagger script (idempotent, --dry-run/--verify; ran 2026-09-03: 117 inserts + 335 updates, VERIFY OK).

Reviewed: Cursor auto r3 APPROVE 2026-09-03.
…-run call caps

get_business_profile/get_business_reviews join LOOP_ALLOWED_TOOLS so the GBP loops can fetch; LOOP_TOOL_CALL_CAPS + capLoopToolCalls hard-cap each at 5 calls/run (6th throws) so prompt text is never the cost control.

Reviewed: Cursor auto r2 APPROVE 2026-09-03.
- Per-loop tool scoping keyed by template identity (prompt byte-match,
  fail-closed): write + paid GBP tools only for the templates that own
  them; drift-tolerant capabilities map; warn when a template-named loop
  misses the map; dead skillName param dropped
- capLoopToolCalls fails closed when a capped tool has no callable
  execute; caps pinned in tests (12/12)
- Reserved template prompts rejected on create AND on prompt change,
  with template-family carve-out and repair-path message
- computeNextSamLoopRunAt: unparseable anchor throws labeled error;
  monthly(36)/weekly(520) bounded roll guards with shared needsRoll and
  loud throws on exhaustion/Date-overflow
- updateSamLoop seeds use existing.projectId + final name on cadence
  re-anchor and enable paths
- Tests: guard exhaustion (30y anchors), routine monthly advance,
  update-path suite, cap pins, cross-check (non-empty prompts, collision
  pin, full key-set equality), DOGFOOD pin+coupling

Review: Opus 5 via OpenRouter r7 APPROVE (r1/r3-r6 FIX rounds resolved).
Gates: tsc --noEmit clean; vitest 176 files / 1631 tests.
…loops beyond house domains

Preserved uncommitted work from the main checkout before the branch
fast-forwards to 5872164. Not reviewed yet — do not deploy from this.
…d house domains

Peer work preserved from the main checkout's uncommitted state (15b5a13).
Clean 3-way merge (verified via merge-tree). Reviewed by Opus before push.
…es TS2322 from the peer WIP; matches alchemy.run.ts idiom)
…dening

- alchemy.access: service-token policy attaches ONLY to the path-scoped
  /mcp app, never the hostname-wide app (there it minted user-audience
  JWTs for service tokens — the C1 hole); annotate mcpPolicyAud
- cloudflareAccess: kind no longer follows audience alone — service_token
  requires common_name; common_name at the user audience is rejected
- transport: accessContext explicitly nullable (omitting it is now a type
  error); server + test call sites updated
- runHeadlessSamLoop: report instruction no longer names the domains it
  suppresses
- tests: cloudflareAccess cross-audience suite (both directions, jose
  claim-shape pin, user-door guard, config-missing paths)
- scripts/sam-loop-prompt-refresh-20260904.py: idempotent D1 migration
  to byte-match stored seeded prompts to the updated templates (operator
  runs at deploy time; dry-run/--write/--verify like the stagger script)
Headless runs already pass isSamLoopProjectAllowed. Skill bodies were
still telling the model to stop unless the domain was house-only, so
enabled clients spent money and quit. Drop that stop. Composer APPROVE r3.
…ve the /mcp boundary

- alchemy.access: new mcpDiscoveryBypass — Bypass(everyone) path-scoped
  Access app for /.well-known/oauth-authorization-server +
  oauth-protected-resource (public RFC 8414 metadata; machine clients and
  user agents both reach it; the /mcp service-token gate is untouched and
  never attaches hostname-wide). Docblock now describes the real design
- alchemy.run: wire mcpDiscoveryBypass; mcpPolicyAud stays undefined when
  unprovisioned (binding omitted, visibly off — no empty-string AUD)
- server.test.ts (new): the /mcp security boundary — service token goes
  to the OAuth provider (its 401 is the client's answer), user gate goes
  to the user handler with context, OPTIONS passes with explicit null,
  discovery paths route to the provider with the /mcp-suffix rewrite;
  compile-time assertion that Env carries OAUTH_KV
- runHeadlessSamLoop gate early-return coverage already existed; the
  samSkills dead assertion removed
- migration script: ASCII guard on the SUBSTR offset, cross-reference
  note for the customized-heuristic literals
- remove committed .pyc, gitignore __pycache__
…pg client across JWKS

- shared SELFHOST_OAUTH_DISCOVERY_PATH_PREFIXES (src/shared) drives BOTH
  the Access bypass destinations (alchemy.access) and the Worker
  allowlist/deny (oauth-resource/server) — the two lists cannot drift
- Worker 404s anything under the discovery prefixes that is not an exact
  discovery path (edge bypass is prefix-matched; the Worker allowlist is
  exact — the edge must never admit more than the Worker serves) + test
- resolveCloudflareAccessMcpGate is now network-only (verified identity,
  no DB); the workspace context is resolved by callers in their own
  short withPgClient scope AFTER the network wait; fetch() bypasses the
  request-wide pg wrapper for the MCP surface — a pooled client is never
  held across a JWKS round-trip
- alchemy.access.test.ts: topology test pinning the C1 invariant (the
  service-token policy attaches ONLY to the /mcp app, never the
  hostname-wide user gate)
- joseContract.test.ts: pins the REAL jose error shape (mock-free)
- loop instruction drops the affirmative gate claim (silence is correct)
- migration script: --verify fails on empty target match, CF_ACCOUNT_ID /
  D1_DATABASE_ID overrides + target printed, clearer empty-report wording
- Env OAUTH_KV compile-time assertion moved beside the casts it protects
- alchemy.run: note that the manual TEAM_DOMAIN+POLICY_AUD path never
  provisions MCP service auth
Awaiting PostHog shutdown in the finalize step could wedge the workflow
after snapshots were already written, leaving status=running so the API
hid positions. Flip DB status first without awaiting telemetry, reclaim
snapshot-complete blockers as completed, and add a cron watchdog.

Co-authored-by: Cursor <cursoragent@cursor.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 7b0eb18. Configure here.

console.log(
`[rank-check] watchdog failed stale incomplete run ${run.id}`,
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Watchdog fails in-progress rank checks

High Severity

reconcileStuckRankCheckRuns marks a run failed after 25 minutes without checking whether the Cloudflare workflow is still queued or collecting. Scheduled checks sleep ~15 minutes, then run collect and live-fallback steps that often push past that window. Finalize then no-ops on a terminal run, so paid snapshots stay hidden behind the completed-run filter.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7b0eb18. Configure here.

@@ -212,7 +213,15 @@ export async function beginRankCheckRun(input: {
ageMs: Date.now() - new Date(blocker.startedAt).getTime(),
});
if (staleReason) {
await failRunIfActive(blocker.id, staleReason, blocker);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Watchdog can clobber completed runs

High Severity

The watchdog passes its stale in-memory run into failRunIfActive, which trusts that snapshot and issues an unconditional updateRun. If the workflow completes between the coverage check and the fail write, a completed run is overwritten to failed and positions disappear again.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7b0eb18. Configure here.

if (input.requireFullCoverage) {
if (keywordsChecked === 0 || keywordsChecked < keywordsTotal) {
return null;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Full coverage ignores missing devices

High Severity

Watchdog requireFullCoverage treats distinct trackingKeywordId as done, not keyword×device tasks. With default devices=both, one device back for every keyword looks complete while the other device is still polling. The run is closed after the 3-minute grace, the inflight slot is freed, and a second paid check can start.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7b0eb18. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not approved: Cursor Bugbot skipped as a check and left three unresolved high-severity findings that need human review. Reviewers were not assigned because no eligible collaborators besides the author could be requested.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver (1)

@Kroma86

Kroma86 commented Sep 12, 2026

Copy link
Copy Markdown
Owner Author

PR hygiene (Pass 4): this branch is ~78 commits ahead of `main` (agency platform WIP ancestry). For a trust-merge of only the rank finalize hang fix, use the slim PR instead:

#5

Same tip commit intent (7b0eb18 cherry-picked onto current `origin/main`), 7 files only. Do not merge/deploy without Jon yes.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agentic security review of this PR found three high-severity authorization issues: unscoped MCP OTTO/ops tools, org-wide Operations reads for any logged-in user, and new Hermes export routes that skip the hosted-mode refuse used by sibling internal APIs.

Open in Web View Automation 

Sent by Cursor Security Agent: Security Reviewer (1)

rationale?: string;
human_review?: string[];
},
_context: ToolContext,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Agentic Security Review
Severity: HIGH

propose_homegrown_otto_fixes and list_homegrown_otto_proposals discard ToolContext and read/write the global KV index (homegrown-otto:proposal-index) with no organization or project check. Sibling MCP tools (get_agency_score_inputs, withMcpProjectAuth writers) bind to the caller’s org. get_niceseo_ops_status has the same _context discard and, when configured, uses the deployment AGENCY_DASH_TOKEN for a caller-chosen hostname. These tools are registered on the shared hosted OAuth MCP server.

Impact: Any authenticated MCP or SAM session can list other tenants’ queued OTTO proposals or enqueue proposals for arbitrary domains, and can pull agency pixel/ops data using a service credential.

Fix in Cursor Fix in Web

Reviewed by Cursor Security Reviewer for commit 7b0eb18. Configure here.


export const getLatestAlertCycle = createServerFn({ method: "POST" })
.middleware(requireAuthenticatedContext)
.handler(async () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Agentic Security Review
Severity: HIGH

getLatestAlertCycle, listOpsArtifacts, and getOpsArtifact only require a logged-in user. They never filter by organizationId. The handlers comment that artifacts are box-wide for a single-agency self-host, but that is not enforced (AUTH_MODE=hosted is not refused). /operations is in the default Agency sidebar for every install.

Impact: On a hosted multi-tenant deployment, any authenticated user can list and read Hermes-ingested client reports, alert cycles, and HTML artifacts for every organization.

Fix in Cursor Fix in Web

Reviewed by Cursor Security Reviewer for commit 7b0eb18. Configure here.

);
}

const data = await getAgencyScoreInputsGlobal(domain);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Agentic Security Review
Severity: HIGH

This route authenticates only with the deployment-wide AGENCY_SCORE_EXPORT_TOKEN, then calls getAgencyScoreInputsGlobal, which scans every non-archived project (organizationId: null). Neighboring internal routes (/api/internal/projects, gsc, ga4, audits) explicitly refuse AUTH_MODE=hosted because a deployment-wide token cannot map to one SaaS org. The new Hermes routes (agency-score-inputs, agency-otto-page-inputs, agency-loop-reports, and related mutate/list endpoints) omit that refuse.

Impact: If the export token is set on hosted OpenSEO, that single secret can read (and on sibling new routes, mutate) other customers’ rank, audit, GSC, GA4, and loop data.

Fix in Cursor Fix in Web

Reviewed by Cursor Security Reviewer for commit 7b0eb18. Configure here.

@Kroma86

Kroma86 commented Sep 12, 2026

Copy link
Copy Markdown
Owner Author

Pass 4 update: true slim PR is now against upstream main (1 commit / 7 files / +322 −68):

every-app#326

Fork PR #5 was closed (fork `main` was stale so GitHub showed a fat diff). This fat PR #4 remains agency WIP ancestry — do not trust-merge for the finalize-only fix. No merge/deploy without Jon yes.

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