Skip to content

feat: Plan 8 PR A — Go edge SSE client + atomic snapshot - #18

Merged
messagesgoel-blip merged 7 commits into
mainfrom
feat/go-edge-hardening-pr-a
Jul 30, 2026
Merged

feat: Plan 8 PR A — Go edge SSE client + atomic snapshot#18
messagesgoel-blip merged 7 commits into
mainfrom
feat/go-edge-hardening-pr-a

Conversation

@messagesgoel-blip

@messagesgoel-blip messagesgoel-blip commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add internal/edgeverifier snapshot store (atomic swap), event apply (idempotent), disk persist (tmp → fsync → rename → dir fsync)
  • Control-plane client + SSE sync loop: retry ms, shutdown reconnect, 429/410 snapshot recovery
  • Wire proxy to synced keys/principal scores/policy + stale mode 503; keep demo MockTrustStore path when sync off
  • Flags/env: VERILINK_CONTROL_PLANE_URL, VERILINK_API_KEY, VERILINK_SNAPSHOT_PATH

Test plan

  • go test ./internal/edgeverifier/... ./cmd/edge-verifier/...
  • go vet ./internal/edgeverifier/... ./cmd/edge-verifier/...
  • CI Gate / Go integration
  • Manual smoke optional: edge against local control-plane with API key

@coderabbitai review

Summary by CodeRabbit

  • New Features
    • Added optional control-plane synchronization with SSE, including snapshot bootstrapping and automatic recovery after rate limiting.
    • Added local snapshot persistence for faster startup and improved resilience during outages.
    • Added policy-based verification modes (stale/expired), signature requirements, and score-based allow/deny decisions.
    • Added graceful shutdown handling for the verifier and synchronization flow.
  • Tests
    • Added unit and end-to-end SSE sync tests covering cursor advancement, snapshot recovery, and snapshot/disk round-trips.
  • Documentation
    • Updated handover documentation to reflect the latest synchronization and snapshot scope.

Plan 8 PR A: immutable snapshot + apply, disk persist with dir fsync,
control-plane client, SSE loop (shutdown/429 recovery), and proxy
wiring to synced keys/scores/policy.

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

cursor Bot commented Jul 30, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 58 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0efbad39-c0d4-476e-af31-bb4b983c04bf

📥 Commits

Reviewing files that changed from the base of the PR and between bd0155b and 22da199.

📒 Files selected for processing (5)
  • internal/edgeverifier/apply.go
  • internal/edgeverifier/cpclient.go
  • internal/edgeverifier/snapshot.go
  • internal/edgeverifier/sse.go
  • internal/edgeverifier/sse_test.go

Walkthrough

The edge verifier now supports control-plane snapshot synchronization through in-memory state, disk persistence, HTTP snapshot fetching, reconnecting SSE events, policy-aware proxy decisions, and graceful signal-triggered shutdown.

Changes

Edge-verifier synchronization

Layer / File(s) Summary
Snapshot model and ingestion
internal/edgeverifier/snapshot.go, internal/edgeverifier/validate.go, internal/edgeverifier/apply.go, internal/edgeverifier/cpclient.go, internal/edgeverifier/disk.go, internal/edgeverifier/snapshot_test.go
Adds atomic snapshots, validation, event and cursor application, control-plane parsing, size limits, and atomic durable persistence with tests.
SSE synchronization runner
internal/edgeverifier/sse.go, internal/edgeverifier/sse_test.go
Adds snapshot bootstrap, authenticated SSE streaming, cursor handling, reconnect backoff, recovery, periodic persistence, and end-to-end SSE tests.
Policy-aware proxy enforcement
internal/edgeverifier/policy.go, internal/edgeverifier/proxy.go, internal/edgeverifier/snapshot_test.go
Adds freshness modes, policy-controlled signature requirements, snapshot key lookup, score decisions, denial headers, and stale-snapshot responses.
Runtime synchronization and shutdown
cmd/edge-verifier/main.go, docs/superpowers/plans/HANDOVER.md
Adds sync configuration and runner wiring, connects reachability to the proxy, handles process signals, coordinates server shutdown, and updates handover metadata.

Estimated code review effort: 5 (Critical) | ~90 minutes

Sequence Diagram(s)

sequenceDiagram
  participant EdgeVerifierProxy
  participant SyncRunner
  participant ControlPlaneClient
  participant SnapshotStore
  SyncRunner->>ControlPlaneClient: Fetch initial snapshot
  ControlPlaneClient-->>SyncRunner: Return snapshot data
  SyncRunner->>SnapshotStore: Apply snapshot
  SyncRunner->>ControlPlaneClient: Open SSE session with cursor
  ControlPlaneClient-->>SyncRunner: Deliver sync event
  SyncRunner->>SnapshotStore: Apply event and update cursor
  EdgeVerifierProxy->>SnapshotStore: Read policy, score, or key
  SnapshotStore-->>EdgeVerifierProxy: Return snapshot-backed decision data
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is specific and matches the main change: an edge SSE client with atomic snapshot support.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/go-edge-hardening-pr-a

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

Co-authored-by: Cursor <cursoragent@cursor.com>
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Go edge: control-plane SSE sync with atomic snapshot + stale-mode gating

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Add atomic snapshot store with idempotent event apply and on-disk persistence.
• Implement control-plane client and SSE reconnect loop with 429/410 snapshot recovery.
• Wire proxy to synced keys/scores/policy with stale-mode 503 and new sync flags/env.
Diagram

graph TD
  main["edge-verifier main"] --> runner["SyncRunner"] --> client["ControlPlaneClient"] --> cp{{"Control Plane"}}
  runner --> store["Snapshot Store"]
  runner --> disk[("Snapshot file")]
  main --> proxy["EdgeVerifierProxy"] --> backend{{"Backend API"}}
  proxy --> store

  subgraph Legend
    direction LR
    _svc["Service/Module"] ~~~ _ext{{"External"}} ~~~ _disk[("Disk")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use an EventSource/SSE library
  • ➕ Reduces risk of subtle SSE parsing/edge-case bugs
  • ➕ Often includes reconnection and retry handling out of the box
  • ➖ Adds dependency surface area
  • ➖ May be harder to enforce exact semantics (shutdown event, cursor handling, custom backoff caps)
2. RWMutex-protected mutable state instead of atomic immutable snapshots
  • ➕ Less per-event allocation/copying for large snapshots
  • ➕ Simpler to update in-place for high event rates
  • ➖ Increases risk of readers observing partially-applied state
  • ➖ Harder to reason about correctness under concurrent reads
3. Persist in an embedded DB (BoltDB/Badger) instead of JSON snapshot file
  • ➕ Better for incremental updates and large datasets
  • ➕ Natural path to WAL/compaction later
  • ➖ More operational complexity and schema/versioning work
  • ➖ Overkill for an initial “single file snapshot” milestone

Recommendation: The PR’s current approach (atomic swap of immutable snapshots + periodic durable JSON persist + SSE reconnect with snapshot recovery) is a strong fit for correctness-first edge hardening. It keeps the read path lock-free and makes replay/idempotency explicit. If SSE edge cases become painful, consider swapping in a small SSE library later, but the current minimal parser is reasonable for controlled server semantics.

Files changed (11) +1630 / -32

Enhancement (8) +1316 / -26
main.goAdd control-plane sync bootstrap, flags/env, and graceful shutdown +79/-4

Add control-plane sync bootstrap, flags/env, and graceful shutdown

• Introduces flags/env wiring for control-plane URL/API key/snapshot path and enables sync when configured. Bootstraps a SyncRunner and runs it in the background, then installs SIGINT/SIGTERM handling to cancel sync and gracefully shut down servers.

cmd/edge-verifier/main.go

apply.goImplement snapshot swap and idempotent event application +213/-0

Implement snapshot swap and idempotent event application

• Adds ApplySnapshot and ApplyEvent with copy-on-write cloning and high-water gating for idempotency. Implements parsing and application for score/key/policy events plus a non-durable cursor advancement path.

internal/edgeverifier/apply.go

cpclient.goAdd control-plane HTTP client for snapshot retrieval +177/-0

Add control-plane HTTP client for snapshot retrieval

• Implements FetchSnapshot with gzip support and response-shape tolerance (wrapped or bare). Maps snapshot JSON into internal Score/Key/Policy structures with key decoding and time parsing.

internal/edgeverifier/cpclient.go

disk.goAdd atomic disk persistence for snapshots with fsync guarantees +175/-0

Add atomic disk persistence for snapshots with fsync guarantees

• Implements SaveSnapshot using tmp write + file sync + rename + directory sync to ensure durability. Adds LoadSnapshot with schema versioning and decoding back into the in-memory snapshot types.

internal/edgeverifier/disk.go

policy.goAdd freshness mode evaluation and policy helpers +84/-0

Add freshness mode evaluation and policy helpers

• Introduces sync degradation modes (degraded/stale/expired) based on last authenticated SSE bytes and policy max age/fail-open behavior. Adds helpers for signature requirements and score/blacklist-based allow/deny decisions.

internal/edgeverifier/policy.go

proxy.goRead verification keys/scores/policy from synced snapshot and enforce stale-mode +79/-22

Read verification keys/scores/policy from synced snapshot and enforce stale-mode

• Extends the proxy to optionally consult a snapshot store for public key lookup and principal trust evaluation (headers include principal, score, reason). Adds freshness mode headering and returns 503 when the snapshot is stale, while keeping the existing demo trust-store path when sync is disabled.

internal/edgeverifier/proxy.go

snapshot.goIntroduce immutable snapshot types and atomic store +201/-0

Introduce immutable snapshot types and atomic store

• Defines Snapshot/Policy/ScoreEntry/KeyEntry types and a Store backed by an atomic pointer for lock-free reads. Adds key validity window checks, policy accessors, snapshot cloning, and robust base64 public key decoding.

internal/edgeverifier/snapshot.go

sse.goAdd SSE sync runner with reconnect/backoff and snapshot recovery +308/-0

Add SSE sync runner with reconnect/backoff and snapshot recovery

• Implements SyncRunner bootstrap (disk-first then control-plane snapshot) and an SSE loop with retry/backoff handling. Supports shutdown-triggered reconnect, cursor-only advancement, durable event application, and 429/410 recovery by refetching a full snapshot.

internal/edgeverifier/sse.go

Tests (2) +307 / -0
snapshot_test.goAdd unit tests for snapshot store, apply idempotency, disk round-trip, and policy mode +164/-0

Add unit tests for snapshot store, apply idempotency, disk round-trip, and policy mode

• Covers atomic swap/miss behavior, ApplyEvent idempotency and version gating, cursor advance, disk save/load round-trip, retry parsing, mode evaluation (stale/expired), and key validity windows.

internal/edgeverifier/snapshot_test.go

sse_test.goAdd tests for SSE reconnect/shutdown and 429 recovery behavior +143/-0

Add tests for SSE reconnect/shutdown and 429 recovery behavior

• Uses httptest servers to validate that shutdown events force reconnection and cursor events advance high-water versions. Verifies 429 responses trigger snapshot recovery and that subsequent sessions use the recovered cursor.

internal/edgeverifier/sse_test.go

Documentation (1) +7 / -6
HANDOVER.mdUpdate Plan 8 status to track PR A progress +7/-6

Update Plan 8 status to track PR A progress

• Updates the handover note to reflect Plan 8 docs merged and PR A in progress, and clarifies next steps (PR A then WAL PR B).

docs/superpowers/plans/HANDOVER.md

@messagesgoel-blip messagesgoel-blip left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Review: PR #18 — Plan 8 PR A: Go edge SSE client + atomic snapshot

This is a substantial, well-structured PR. 1630 additions across 11 files implementing the Go-side edge sync client.

Architecture

Component Purpose
snapshot.go Store (atomic.Pointer[Snapshot]), ScoreEntry, KeyEntry, Policy, lookup helpers with key validity windows
apply.go ApplySnapshot, ApplyEvent (idempotent), AdvanceCursor, CloneSnapshot
cpclient.go ControlPlaneClient.FetchSnapshot with gzip support
disk.go Crash-safe save (tmp→fsync→rename→dir fsync) and load with schema versioning
sse.go SyncRunner: bootstrap, reconnect loop with backoff, SSE protocol parser, 429/410 recovery
policy.go EvaluateMode (stale→503, expired→X-Verilink-Mode), AllowByScore, RequireSignaturesFromPolicy
proxy.go Modified to use snapshot store for key/score/policy lookups
main.go Wiring: flags, bootstrap, sync runner goroutine, signal handling

What is done well

  • Atomic snapshot swapStore uses atomic.Pointer for lock-free concurrent reads from the proxy
  • Crash-safe disk persistence — tmp → fsync → rename → dir fsync pattern
  • Idempotent event applysyncVersion <= hw skips already-applied events
  • SSE protocol parser — correctly handles event/id/data/retry/comment lines and blank-line dispatch
  • 429/410 recoveryrunSession fetches a fresh snapshot on those status codes and reconnects with the new cursor
  • event: shutdown handling — returns a sentinel error triggering reconnect
  • Exponential backoff — 5s default, 30s max
  • Key validity windowsLookupKey checks ValidFrom/ValidUntil
  • Testssnapshot_test.go (164 lines) and sse_test.go (143 lines) covering atomic swap, idempotent apply, disk round-trip, evaluate mode, key validity, shutdown reconnect, and 429 recovery with snapshot fetch

Issues

1. AdvanceCursor clones the entire snapshot on every cursor event (Performance — high impact)

func AdvanceCursor(store *Store, highWater int64) {
    next := CloneSnapshot(cur)  // deep-copies ALL scores + keys
    next.HighWaterVersion = highWater
    store.Swap(next)
}

Cursor events fire every poll cycle (every 5s). On a tenant with 100K scores, this means cloning 100K map entries every 5 seconds — even when no data changed. This is O(n) per poll cycle for the entire lifetime of the SSE connection.

Fix: Store HighWaterVersion as a separate atomic.Int64 on Store instead of inside Snapshot. Then AdvanceCursor just needs atomic.Store instead of clone+swap. The Snapshot HW field becomes a bootstrap-only value.

2. No SetSyncReachable(false) when SSE disconnects (Correctness — medium impact)

The proxys EvaluateMode checks syncReachable via p.syncReachable.Load(), but nothing ever sets it to false. The SyncRunner.Run loop has no reference to the proxy. So syncReachable stays true forever, and stale detection is entirely reactive (waits for BytesAge to exceed policy threshold).

Fix: Pass a SetSyncReachable func(bool) into SyncRunner (or ControlPlaneClient), called when an SSE session starts (true) and when it fails (false). The proxy calls SetSyncReachable to trigger proactive stale detection.

3. parseSnapshotJSON fallback swallows original error context (Minor)

When both the wrapped and bare unmarshal fail, the function returns the first error, not the second. If the bare format is close but has a typo, the error message may mislead about what went wrong.

Minor observations

  • main.go uses syscall for signal handling — fine, but os.Signal and os/signal are sufficient for SIGINT/SIGTERM
  • http.Client{Timeout: 0} is used for the SSE client — correct (no timeout for long-lived streams)
  • The 30s bootstrap timeout is a good default for the initial snapshot fetch
  • envOr helper is defined at the bottom of main.go — a small utility, fine to keep local

Verdict

Changes requested — issue #1 (AdvanceCursor O(n) clone on every poll cycle) is a significant performance concern at scale. Issue #2 (SetSyncReachable never called) is a correctness gap for proactive stale detection. Everything else is high quality and well-tested.

@qodo-code-review

qodo-code-review Bot commented Jul 30, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (1) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 18 rules

Grey Divider


Action required

1. Nil store policy panic ✓ Resolved 🐞 Bug ≡ Correctness
Description
RequireSignaturesFromPolicy dereferences store.ActivePolicy() without a nil check. When sync is
disabled, the proxy passes a nil snapshot store and unsigned requests will panic the server.
Code

internal/edgeverifier/policy.go[R44-52]

+func RequireSignaturesFromPolicy(store *Store, flagRequire bool) bool {
+	if flagRequire {
+		return true
+	}
+	pol := store.ActivePolicy()
+	if pol == nil {
+		return false
+	}
+	return pol.UnsignedAction == "deny"
Relevance

●●● Strong

Nil-pointer/panic avoidance is typically accepted; no contrary precedent found in edge-verifier
history.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
snapStore is nil when sync is disabled, but the proxy still calls RequireSignaturesFromPolicy on
unsigned requests; that function dereferences the nil store via store.ActivePolicy() causing a
panic.

cmd/edge-verifier/main.go[72-103]
cmd/edge-verifier/main.go[122-124]
internal/edgeverifier/proxy.go[142-148]
internal/edgeverifier/policy.go[43-53]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Unsigned requests can crash the edge-verifier when sync is disabled because `RequireSignaturesFromPolicy` calls `store.ActivePolicy()` even when `store` is `nil`.

### Issue Context
- `cmd/edge-verifier/main.go` passes `snapStore` into the proxy; `snapStore` remains `nil` unless sync is enabled.
- `proxy.ServeHTTP` always evaluates unsigned-request policy via `RequireSignaturesFromPolicy(p.snapshot, ...)`.

### Fix
- Make `RequireSignaturesFromPolicy` safe for `store == nil` (treat as “no policy loaded”).
- Consider also guarding `AllowByScore` similarly for defensive robustness.
- Add a unit/integration test that sends an unsigned request with sync disabled and asserts no panic + passthrough behavior when `-require-signatures=false`.

### Fix Focus Areas
- internal/edgeverifier/policy.go[43-53]
- internal/edgeverifier/proxy.go[66-153]
- cmd/edge-verifier/main.go[72-125]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Unbounded snapshot ReadAll ✓ Resolved 🐞 Bug ☼ Reliability
Description
FetchSnapshot reads the entire (possibly gzipped) snapshot response into memory via io.ReadAll.
Large or highly-compressible responses can cause excessive memory usage/OOM during bootstrap or
recovery.
Code

internal/edgeverifier/cpclient.go[R59-65]

+	data, err := io.ReadAll(body)
+	if err != nil {
+		return nil, err
+	}
+	if resp.StatusCode != http.StatusOK {
+		return nil, fmt.Errorf("snapshot: status %d: %s", resp.StatusCode, truncate(string(data), 200))
+	}
Relevance

●●● Strong

They’ve accepted bounding/avoiding unbounded ReadAll patterns for reliability (PR #5 hardening
suggestions accepted/partially accepted).

PR-#5

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
FetchSnapshot uses io.ReadAll on the response body (possibly a gzip reader) and is called from
both bootstrap and snapshot recovery paths.

internal/edgeverifier/cpclient.go[33-68]
internal/edgeverifier/sse.go[61-80]
internal/edgeverifier/sse.go[155-162]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`FetchSnapshot` buffers the full snapshot response in memory with no size cap. With gzip, the decompressed size can be much larger than the wire payload.

### Issue Context
This runs on edge startup (`Bootstrap`) and on recovery (`recoverSnapshot`), so OOM here can prevent the edge from coming up or recovering.

### Fix
- Introduce a max snapshot size constant (post-decompression) and read via `io.LimitReader`.
- Prefer decoding with `json.Decoder` from a limited reader (streaming) instead of `io.ReadAll`.
- For non-200 responses, read only a small capped body (e.g., 512 bytes) for error messages.

### Fix Focus Areas
- internal/edgeverifier/cpclient.go[33-68]
- internal/edgeverifier/sse.go[155-162]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Malformed event IDs applied ✓ Resolved 🐞 Bug ≡ Correctness
Description
SSE event IDs are parsed with ParseInt and parse errors are ignored for normal events, turning
malformed IDs into syncVersion=0. ApplyEvent only enforces idempotency for syncVersion>0, so
missing/invalid/non-positive IDs can mutate the snapshot without valid ordering metadata.
Code

internal/edgeverifier/sse.go[R190-193]

+		default:
+			hw, _ := parseID(idStr)
+			if err := ApplyEvent(r.Store, hw, eventName, json.RawMessage(data)); err != nil {
+				r.logf("apply %s: %v", eventName, err)
Relevance

●●● Strong

Team accepted strict non-negative SSE cursor/event-id parsing requirements in Plan 7 docs (PR #14).

PR-#14

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
For ordinary events, readStream ignores parseID errors and passes the parsed value into
ApplyEvent; ApplyEvent only performs the high-water no-op check when syncVersion > 0, so
invalid/missing IDs aren’t protected by idempotency/ordering logic.

internal/edgeverifier/sse.go[190-199]
internal/edgeverifier/sse.go[295-301]
internal/edgeverifier/apply.go[24-36]
PR-#14

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Normal SSE events ignore `parseID` errors (`hw, _ := parseID(...)`) and then call `ApplyEvent` with `syncVersion=0` on malformed IDs. `ApplyEvent` skips the stale-event guard for `syncVersion <= 0`, allowing unversioned mutations.

### Issue Context
This undermines the “idempotent by high-water version” contract and can lead to repeated reapplication after reconnects.

### Fix
- In `readStream` default event handling: require `id:` to parse successfully and be strictly positive (or at least non-negative + explicitly defined semantics).
 - If invalid: either (a) drop the event with a log and continue, or (b) treat as session error and reconnect.
- Consider tightening `ApplyEvent` to reject `syncVersion <= 0` for durable event types (upsert/delete/replace).

### Fix Focus Areas
- internal/edgeverifier/sse.go[172-245]
- internal/edgeverifier/sse.go[295-301]
- internal/edgeverifier/apply.go[24-36]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. High-water parse ignored ✓ Resolved 🐞 Bug ≡ Correctness
Description
parseSnapshotJSON silently sets HighWaterVersion to 0 when parsing fails. A malformed/oversized
highWaterVersion will reset the SSE cursor and can cause unnecessary replays or recovery churn.
Code

internal/edgeverifier/cpclient.go[R115-118]

+	hw, err := wire.Data.HighWaterVersion.Int64()
+	if err != nil {
+		hw = 0
+	}
Relevance

●●● Strong

Repo strongly favors strict cursor/version parsing over silent coercion (Plan 7 doc fixes accepted
in PR #14).

PR-#14

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The snapshot parser coerces parse errors to HW=0, and the SSE client always uses the store HW as the
Last-Event-ID cursor for /v1/sync/events, so a bad HW value changes reconnection behavior.

internal/edgeverifier/cpclient.go[111-123]
internal/edgeverifier/cpclient.go[115-118]
internal/edgeverifier/sse.go[118-129]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`parseSnapshotJSON` currently treats an invalid `highWaterVersion` as `0` instead of failing. This can reset the edge cursor and break ordering/idempotency assumptions.

### Issue Context
- `SyncRunner.runSession` sets `Last-Event-ID` from `Store.HighWater()`; if the snapshot parser forces HW to 0, the client reconnects from the beginning.

### Fix
- If `highWaterVersion` exists but `Int64()` fails, return an error (and optionally treat negative HW as invalid).
- Consider validating `wire.OK` when present, and failing if `ok=false` (unless explicitly supporting an error shape).
- Add a test fixture with an out-of-range or non-integer `highWaterVersion` and assert bootstrap fails.

### Fix Focus Areas
- internal/edgeverifier/cpclient.go[92-170]
- internal/edgeverifier/sse.go[118-153]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (2)
5. Proxy outbound requests unsigned 📘 Rule violation ⛨ Security
Description
EdgeVerifierProxy forwards requests to the upstream backend without attaching RFC 9421
Signature-Input/Signature headers. This violates the requirement to sign reverse-proxy outbound
requests and can break upstream verification/security guarantees.
Code

internal/edgeverifier/proxy.go[R142-146]

+	requireSig := RequireSignaturesFromPolicy(p.snapshot, p.requireSignatures)
+	if requireSig {
		log.Printf("UNSIGNED_REJECTED: method=%s uri=%s", r.Method, r.URL)
		w.Header().Set("X-Verilink-Auth-Status", "unsigned-rejected")
		http.Error(w, "Unauthorized: Request must be signed", http.StatusUnauthorized)
Relevance

●● Moderate

No historical evidence of signing outbound reverse-proxy requests; prior edge-verifier security
hardening suggestions were sometimes rejected (PR #7).

PR-#7

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2383208 requires RFC 9421 signature headers on outbound reverse-proxy requests. In
internal/edgeverifier/proxy.go, the proxy forwards requests with p.proxy.ServeHTTP(w, r) and
there is no logic that constructs/signs an RFC 9421 signature base or sets
Signature-Input/Signature headers on the upstream request.

Rule 2383208: Require RFC 9421 HTTP Message Signatures in edge-verifier reverse proxy requests
internal/edgeverifier/proxy.go[48-59]
internal/edgeverifier/proxy.go[142-153]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Outbound requests from `EdgeVerifierProxy` to the upstream are not being signed with RFC 9421 HTTP Message Signatures. The proxy currently verifies inbound signatures but then forwards the request upstream via `httputil.ReverseProxy` without adding `Signature-Input` and `Signature` headers.

## Issue Context
Compliance requires RFC 9421 message signatures on reverse proxy outbound requests. Evidence shows no code that sets `Signature-Input`/`Signature` on outbound requests in the proxy.

## Fix Focus Areas
- internal/edgeverifier/proxy.go[48-59]
- internal/edgeverifier/proxy.go[142-153]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Retry exceeds max backoff ✓ Resolved 🐞 Bug ☼ Reliability
Description
The SSE retry: value is accepted as any positive integer and used directly as the reconnect delay.
A large retry value can stall reconnects even though maxBackoffMs exists.
Code

internal/edgeverifier/sse.go[R95-115]

+		retryMs, err := r.runSession(ctx)
+		if retryMs > 0 {
+			backoffMs = retryMs
+		}
+		if ctx.Err() != nil {
+			return ctx.Err()
+		}
+		if err != nil {
+			r.logf("sse session ended: %v; reconnect in %dms", err, backoffMs)
+		} else {
+			r.logf("sse session ended; reconnect in %dms", backoffMs)
+		}
+		timer := time.NewTimer(time.Duration(backoffMs) * time.Millisecond)
+		select {
+		case <-ctx.Done():
+			timer.Stop()
+			return ctx.Err()
+		case <-timer.C:
+		}
+		backoffMs = minInt(backoffMs*2, maxBackoffMs)
+	}
Relevance

●● Moderate

Docs accept honoring SSE retry/backoff, but no precedent on capping server-provided retry to
maxBackoff (PR #14/17).

PR-#14
PR-#17

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
readStream accepts any positive retry and Run uses it directly for the reconnect timer;
maxBackoffMs only applies to the later doubling step, not the server-provided value.

internal/edgeverifier/sse.go[90-115]
internal/edgeverifier/sse.go[226-230]
internal/edgeverifier/sse.go[283-293]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`retry:` from the SSE stream can set `backoffMs` to an arbitrarily large value because it is not clamped to `maxBackoffMs`.

### Issue Context
- `readStream` updates `retryMs` from `retry:`.
- `Run` assigns `backoffMs = retryMs` before applying exponential backoff clamping.

### Fix
- Clamp server-provided retry to `[1, maxBackoffMs]` (or a separate max) when parsing or when applying it.
- Consider also guarding against integer overflow when converting to `time.Duration`.
- Add a test where the stream emits `retry: 999999` and assert the returned retry is capped.

### Fix Focus Areas
- internal/edgeverifier/sse.go[90-115]
- internal/edgeverifier/sse.go[226-230]
- internal/edgeverifier/sse.go[283-293]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

7. Missing key.upsert validation ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
ApplyEvent handles key.upsert without validating principal_id or key_id. Empty IDs can insert
malformed entries (e.g., under the empty-string key) and cause confusing verification/attribution
behavior for those IDs.
Code

internal/edgeverifier/apply.go[R72-105]

+	case "key.upsert":
+		var p struct {
+			PrincipalID  string  `json:"principal_id"`
+			KeyID        string  `json:"key_id"`
+			PublicKeyRaw string  `json:"public_key_raw"`
+			ValidFrom    string  `json:"valid_from"`
+			ValidUntil   *string `json:"valid_until"`
+		}
+		if err := json.Unmarshal(payload, &p); err != nil {
+			return fmt.Errorf("key.upsert: %w", err)
+		}
+		raw, err := DecodePublicKeyRaw(p.PublicKeyRaw)
+		if err != nil {
+			return err
+		}
+		vf, err := parseTime(p.ValidFrom)
+		if err != nil {
+			return fmt.Errorf("key.upsert valid_from: %w", err)
+		}
+		var vu *time.Time
+		if p.ValidUntil != nil && *p.ValidUntil != "" {
+			t, err := parseTime(*p.ValidUntil)
+			if err != nil {
+				return fmt.Errorf("key.upsert valid_until: %w", err)
+			}
+			vu = &t
+		}
+		next.Keys[p.KeyID] = KeyEntry{
+			PrincipalID:  p.PrincipalID,
+			KeyID:        p.KeyID,
+			PublicKeyRaw: raw,
+			ValidFrom:    vf,
+			ValidUntil:   vu,
+		}
Relevance

●●● Strong

Team previously accepted adding input validation/hardening for IDs/fields (PR #4 partially accepted,
PR #5 accepted).

PR-#4
PR-#5

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Unlike score.upsert, key.upsert does not reject missing identifiers before writing into
next.Keys[p.KeyID]. The proxy’s snapshot path relies on key lookups by key ID and principal
attribution, so malformed IDs create unusable or misleading state for those IDs.

internal/edgeverifier/apply.go[41-61]
internal/edgeverifier/apply.go[72-106]
internal/edgeverifier/proxy.go[155-178]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`key.upsert` decodes the key and parses times but does not validate `principal_id`/`key_id` before inserting into the snapshot maps.

### Issue Context
While the control-plane is typically trusted, rejecting malformed sync data early prevents silent state corruption and makes failures diagnosable.

### Fix
- In the `key.upsert` branch, enforce:
 - `p.KeyID != ""`
 - `p.PrincipalID != ""`
 - (optional) `p.PublicKeyRaw != ""`
- Return a typed/annotated error like `fmt.Errorf("key.upsert: missing key_id")`.

### Fix Focus Areas
- internal/edgeverifier/apply.go[72-106]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread internal/edgeverifier/proxy.go
Comment thread internal/edgeverifier/policy.go
Comment thread internal/edgeverifier/cpclient.go
Comment thread internal/edgeverifier/cpclient.go Outdated
Comment thread internal/edgeverifier/sse.go
Comment thread internal/edgeverifier/sse.go Outdated
Comment thread internal/edgeverifier/apply.go Outdated
Store high-water as atomic.Int64 (no cursor clone), wire OnReachable,
nil-safe policy helpers, clamp retry, reject bad event IDs / HW, cap
snapshot reads, validate key.upsert IDs, and clear golangci-lint.

Co-authored-by: Cursor <cursoragent@cursor.com>
@messagesgoel-blip

Copy link
Copy Markdown
Collaborator Author

Addressed CI lint + review findings:

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

@messagesgoel-blip Thanks for the detailed update. I’ll review the latest changes, including the atomic cursor update, sync reachability wiring, and the added SSE/snapshot validation safeguards.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai 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.

Actionable comments posted: 12

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/edge-verifier/main.go`:
- Line 35: Remove the api-key flag registration from the command-line
configuration, ensuring the control-plane API key is sourced only through the
existing VERILINK_API_KEY environment variable path in the edge verifier startup
flow.
- Around line 86-88: Bound control-plane operations without adding an overall
SSE timeout: in cmd/edge-verifier/main.go lines 86-88, clone the default HTTP
transport and configure a finite ResponseHeaderTimeout for connection
establishment; in internal/edgeverifier/sse.go lines 138-148, apply that header
deadline while keeping the established response body long-lived; in
internal/edgeverifier/sse.go lines 171-177, derive and use a finite timeout
context for each recovery snapshot fetch.

In `@internal/edgeverifier/apply.go`:
- Around line 176-190: Validate p.BelowThresholdAction and p.UnsignedAction
after applying their defaults and before returning the policy, allowing only
allow or deny for the former and passthrough or deny for the latter. Return an
appropriate error for unsupported values so they cannot be published into the
live enforcement policy.
- Around line 62-69: Validate p.PrincipalID in the score.delete handling before
deleting from next.Scores or acknowledging the event; return an error for an
empty identifier so the high-water mark is not advanced. Keep valid deletions
unchanged.

In `@internal/edgeverifier/cpclient.go`:
- Around line 154-190: Add one shared full-snapshot validator and invoke it
before publishing or inserting entries: internal/edgeverifier/cpclient.go lines
154-190 must reject empty or duplicate principal/key IDs and invalid key
validity windows, while internal/edgeverifier/disk.go lines 113-153 must use the
same validation, reject negative high-water values, and normalize policy
defaults consistently before returning. Ensure both ingestion paths use the
validator rather than maintaining separate checks.

In `@internal/edgeverifier/disk.go`:
- Around line 103-110: Update LoadSnapshot to read through an io.LimitedReader
capped by MaxSnapshotBytes plus one byte, rather than using os.ReadFile
directly; reject input containing the extra byte before calling json.Unmarshal,
while preserving the existing error propagation and snapshot parsing behavior.

In `@internal/edgeverifier/policy.go`:
- Around line 17-41: Update EvaluateMode and Store.BytesAge so the supplied now
timestamp is used for staleness calculation, changing BytesAge to accept now and
passing it from EvaluateMode; remove the unused _ = now statement while
preserving existing policy and mode behavior.

In `@internal/edgeverifier/sse_test.go`:
- Around line 15-18: Update the httptest server handler in the SSE test to count
sessions only after confirming the request path is /v1/sync/events, using an
atomic counter to avoid races with the polling goroutine. Use the per-request
atomic count for the first-session shutdown branch, and keep the assertion
validating at least two SSE sessions; verify with go test ./....

In `@internal/edgeverifier/sse.go`:
- Around line 224-235: Add an idle watchdog around the SSE read loop in the
relevant runner, using the heartbeat freshness deadline to close the response
body when no data arrives; ensure the blocked ReadString unblocks and returns
through the existing reconnect/error path, and stop the watchdog when the stream
exits.
- Around line 228-264: Update the SSE parsing loop around ReadString and
dataLines to enforce explicit maximum line and assembled-event sizes. Reject or
return an appropriate error when a line exceeds the configured maximum, and
check the aggregate size before appending each data: value so oversized events
cannot grow memory unbounded; preserve normal flush, retry, and event parsing
behavior.
- Around line 104-130: Preserve exponential backoff in the reconnect loop around
runSession by only updating backoffMs when the server provides a positive retry
value, rather than treating the session functions’ default retryMs of 5000 as an
override on every failure. Ensure the same behavior is applied to the
corresponding retry-handling blocks at the other referenced session paths, while
retaining the existing backoffMs doubling when no retry field is sent.
- Around line 214-220: Update the event handling around ApplyEvent so an error
applying a known event terminates the session instead of returning nil and
continuing. Preserve the existing error log, but return the appropriate non-nil
session-processing error from this branch so later events cannot advance the
high-water mark past the rejected event; keep successful events on the existing
persist-and-return path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 78b61405-5567-41ab-a090-a0e63ece6ad3

📥 Commits

Reviewing files that changed from the base of the PR and between 23c6162 and dd2d5f6.

📒 Files selected for processing (11)
  • cmd/edge-verifier/main.go
  • docs/superpowers/plans/HANDOVER.md
  • internal/edgeverifier/apply.go
  • internal/edgeverifier/cpclient.go
  • internal/edgeverifier/disk.go
  • internal/edgeverifier/policy.go
  • internal/edgeverifier/proxy.go
  • internal/edgeverifier/snapshot.go
  • internal/edgeverifier/snapshot_test.go
  • internal/edgeverifier/sse.go
  • internal/edgeverifier/sse_test.go

Comment thread cmd/edge-verifier/main.go Outdated
Comment thread cmd/edge-verifier/main.go
Comment thread internal/edgeverifier/apply.go Outdated
Comment thread internal/edgeverifier/apply.go Outdated
Comment thread internal/edgeverifier/cpclient.go Outdated
Comment thread internal/edgeverifier/sse_test.go Outdated
Comment thread internal/edgeverifier/sse.go
Comment thread internal/edgeverifier/sse.go Outdated
Comment thread internal/edgeverifier/sse.go Outdated
Comment thread internal/edgeverifier/sse.go Outdated
Env-only API key, snapshot validation, SSE idle/size bounds,
preserve backoff without retry:, and fail sessions on apply errors.

Co-authored-by: Cursor <cursoragent@cursor.com>
@messagesgoel-blip

Copy link
Copy Markdown
Collaborator Author

Addressed all 12 CodeRabbit findings in 579aa72:

  1. Removed -api-key CLI flag — VERILINK_API_KEY env-only
  2. ResponseHeaderTimeout on transport + 30s snapshot fetch context (SSE body still unbounded)
  3. Reject empty score.delete principal_id
  4. Validate policy enums (allow|deny, passthrough|deny)
  5. Shared validateSnapshot on CP + disk ingest (empty/duplicate IDs, key windows)
  6. Cap disk LoadSnapshot with LimitedReader / MaxSnapshotBytes
  7. EvaluateMode now uses injected now for staleness
  8. TestSSEShutdownAndCursor counts only /v1/sync/events sessions (atomic)
  9. Preserve exponential backoff when server omits retry:
  10. Fail SSE session on ApplyEvent error
  11. Idle heartbeat watchdog (90s → close body / reconnect)
  12. Bound SSE line (256 KiB) and assembled event (1 MiB) sizes

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

@messagesgoel-blip Thanks for the thorough remediation summary. I’ll review the latest updates in 579aa72, with particular attention to the SSE bounds/watchdog behavior, snapshot validation and read caps, and the retry/reachability paths.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

CI Gate failed with complexity 26 on both functions after the
validation/guard additions; extract event and wire helpers.

Co-authored-by: Cursor <cursoragent@cursor.com>
@messagesgoel-blip

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review — also fixed gocyclo Gate failure in follow-up commit.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

@messagesgoel-blip I’ll review the latest follow-up changes, including the gocyclo gate fix and its interaction with the SSE/snapshot synchronization paths.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
internal/edgeverifier/sse.go (2)

81-100: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Bootstrap's snapshot fetch has no timeout, unlike recoverSnapshot.

recoverSnapshot (Line 177) wraps FetchSnapshot in context.WithTimeout(ctx, snapshotFetchTO), but Bootstrap's equivalent call at Line 90 uses the raw ctx directly. If the control plane hangs during startup (no TCP reset, just silence) and ctx has no deadline, Bootstrap — and therefore Run — can block indefinitely with no recovery path, defeating the purpose of the snapshotFetchTO guard established elsewhere in this same file.

🐛 Proposed fix
-	snap, err := r.Client.FetchSnapshot(ctx)
+	fetchCtx, cancel := context.WithTimeout(ctx, snapshotFetchTO)
+	defer cancel()
+	snap, err := r.Client.FetchSnapshot(fetchCtx)
 	if err != nil {
 		return fmt.Errorf("fetch snapshot: %w", err)
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/edgeverifier/sse.go` around lines 81 - 100, Update
SyncRunner.Bootstrap to wrap the FetchSnapshot call with the existing
snapshotFetchTO timeout, matching recoverSnapshot’s behavior, and pass the timed
context to FetchSnapshot so startup cannot block indefinitely when the parent
context has no deadline.

257-268: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Process the final SSE line before returning on io.EOF
readSSELine can return a non-empty line together with io.EOF, but this loop exits before dispatching it. That drops a final unterminated data: line and can lose the last SSE event. Handle the buffered line first, then flush and return on EOF.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/edgeverifier/sse.go` around lines 257 - 268, Update the SSE read
loop around readSSELine to process any non-empty line returned alongside io.EOF
before flushing and returning. Preserve the existing EOF flush and return
behavior afterward, ensuring the final unterminated data line is dispatched.
internal/edgeverifier/sse_test.go (1)

82-144: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Use an atomic counter for the snapshot request count
n is incremented in the httptest handler and read from the test goroutine, so this test still has an unsynchronized cross-goroutine access. Switch it to atomic.Int32 or guard it with a mutex to avoid -race failures.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/edgeverifier/sse_test.go` around lines 82 - 144, The snapshot
request counter n is accessed concurrently without synchronization. Replace n
with an atomic.Int32 (or protect it with a mutex), update its increment in the
httptest handler and load in the final t.Fatalf message, and preserve the
existing request-count behavior.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/edgeverifier/apply.go`:
- Around line 108-152: Update applyKeyUpsert to validate that a non-nil
ValidUntil is strictly after ValidFrom before inserting the KeyEntry, matching
validateSnapshot’s full-snapshot behavior. Return a key.upsert validation error
and avoid mutating next.Keys when the validity window is inverted or
zero-length.

In `@internal/edgeverifier/cpclient.go`:
- Around line 201-231: Unify public-key decoding by moving the raw-base64
fallback into DecodePublicKeyRaw, then remove the one-off fallback from
wireKeyToEntry so applyKeyUpsert and LoadSnapshot share identical behavior. In
wireKeyToEntry, return an explicit error when the decoded key length is not 32
bytes instead of wrapping a potentially nil error.

---

Outside diff comments:
In `@internal/edgeverifier/sse_test.go`:
- Around line 82-144: The snapshot request counter n is accessed concurrently
without synchronization. Replace n with an atomic.Int32 (or protect it with a
mutex), update its increment in the httptest handler and load in the final
t.Fatalf message, and preserve the existing request-count behavior.

In `@internal/edgeverifier/sse.go`:
- Around line 81-100: Update SyncRunner.Bootstrap to wrap the FetchSnapshot call
with the existing snapshotFetchTO timeout, matching recoverSnapshot’s behavior,
and pass the timed context to FetchSnapshot so startup cannot block indefinitely
when the parent context has no deadline.
- Around line 257-268: Update the SSE read loop around readSSELine to process
any non-empty line returned alongside io.EOF before flushing and returning.
Preserve the existing EOF flush and return behavior afterward, ensuring the
final unterminated data line is dispatched.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 68fa75e0-b524-4fc8-ba32-b31661d40795

📥 Commits

Reviewing files that changed from the base of the PR and between dd2d5f6 and bd0155b.

📒 Files selected for processing (8)
  • cmd/edge-verifier/main.go
  • internal/edgeverifier/apply.go
  • internal/edgeverifier/cpclient.go
  • internal/edgeverifier/disk.go
  • internal/edgeverifier/policy.go
  • internal/edgeverifier/sse.go
  • internal/edgeverifier/sse_test.go
  • internal/edgeverifier/validate.go

Comment thread internal/edgeverifier/apply.go
Comment thread internal/edgeverifier/cpclient.go
Align key.upsert validity and key decoding across ingest paths,
add bootstrap snapshot timeout, handle trailing SSE EOF lines,
and make the 429 recovery counter race-safe.

Co-authored-by: Cursor <cursoragent@cursor.com>
@messagesgoel-blip

Copy link
Copy Markdown
Collaborator Author

Addressed the latest CodeRabbit CHANGES_REQUESTED findings:

  1. key.upsert now rejects valid_until <= valid_from
  2. DecodePublicKeyRaw owns std/raw-std/raw-URL decoding; wireKeyToEntry no longer has a one-off fallback
  3. Bootstrap uses snapshotFetchTO like recoverSnapshot
  4. SSE EOF path dispatches any trailing unterminated line before flush
  5. TestSSE429RecoversViaSnapshot uses atomic.Int32 for the snapshot counter

Also extracted SSE frame parsing to keep gocyclo under the Gate limit.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

@messagesgoel-blip I’ll review the latest follow-up changes, focusing on the validation, timeout, SSE EOF/frame parsing, and concurrency updates.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@messagesgoel-blip messagesgoel-blip left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Re-review: PR #18

The 5 new commits thoroughly address all concerns from my previous review.

What was fixed

Previous issue Fix
AdvanceCursor clones entire snapshot every poll cycle Store now has a separate atomic.Int64 for high-water. advanceHighWater uses CompareAndSwap — no clone.
SetSyncReachable never called SyncRunner has OnReachable callback. Wired via WithOnReachable(proxy.SetSyncReachable).
API key in argv -api-key flag removed. VERILINK_API_KEY is env-only.

Additional hardening added

  • SSE idle watchdog (90s timeout)
  • Line/event byte limits (256KiB / 1MiB)
  • ResponseHeaderTimeout on HTTP transport
  • 30s timeout constant for all snapshot fetches
  • RawStdEncoding fallback for key decoding
  • Cyclomatic complexity split for lint compliance

Verdict

All concerns resolved, plus meaningful hardening beyond what was requested. Ready to merge.

@messagesgoel-blip
messagesgoel-blip merged commit 2b88d46 into main Jul 30, 2026
4 of 5 checks passed
@messagesgoel-blip
messagesgoel-blip deleted the feat/go-edge-hardening-pr-a branch July 30, 2026 14:01
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