Skip to content

1.AF P3 + P4/D13 — media egress, failover re-materialization, SSRF mechanism + byte-delivery gate#34

Merged
cemililik merged 10 commits into
mainfrom
development
Jun 19, 2026
Merged

1.AF P3 + P4/D13 — media egress, failover re-materialization, SSRF mechanism + byte-delivery gate#34
cemililik merged 10 commits into
mainfrom
development

Conversation

@cemililik

@cemililik cemililik commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

What

Follow-on to #33 (1.AF P1+P2, merged). This lands 1.AF P3 (media egress + failover re-materialization + the SSRF mechanism half) and P4/D13 (the byte-delivery Range gate) — the security-critical media-plumbing layer behind the @relavium/llm seam and the engine.

Design is pinned in the three accepted 1.AF ADRs (no new ADR): ADR-0042 (substrate), ADR-0043 (egress/SSRF), ADR-0044 (access/cost).

Landed

P3 — egress / failover / SSRF (D7/D8/D9)

  • D9 — media egress + the SSRF mechanism half. deInlineMedia re-hosts a url media source to a content-addressed handle via an injected MediaUrlFetch hook (the seam stays a pure url → bytes function; with no hook a url hard-fails — I3). fetchMediaBytes is the Node host reference (@relavium/db): HTTPS-only + no-creds on the one shared SSRF primitive, resolve DNS + validate every IP + connect pinned to the validated IP (DNS-rebind/TOCTOU defense), per-hop redirect re-validation, streamed + size-bounded body, TLS never disabled, secret-free typed errors. Wired at the one #emitDurable choke point via the new ExecutionHost.fetchMedia port (the engine supplies the size-bound policy + abort signal).
  • D8 + D7 — resolve-before-egress + the re-materialization sidecar. The FallbackChain resolves every handle media source to the provider's in-flight source before egress (the injected resolveForEgress hook — adapters stay pure string → string, never hold a MediaStore), with a byte-free (provider, handle) sidecar: a non-base64 provider ref is cached + reused on a later same-provider call, a base64 source is never cached (the sidecar stays byte-free), a cross-provider advance re-materializes (a foreign ref never crosses the boundary), and a re-materialization failure is retryable-advance (turn-fatal only when there is no next provider).

P4 — byte delivery (D13)

  • D13 — the byte-delivery Range gate. MediaStore.readRange(handle, range, signal?) (host mechanism: the path-jail + sha256 integrity check + a defensive re-bound against the actual stored size) + the engine-pure validateByteRange(range, byteLength) policy (fail-closed on a non-integer / negative / reversed / out-of-bounds range — no raw parseInt, never trusting a client size). Shared by the read_media tool (D12, next PR) and the 1.AH desktop command — the gate is written once.

Security review (P3). A dedicated, independent adversarial egress/SSRF pass — 0 blockers, 0 highs. DNS-rebind/TOCTOU, per-hop redirect re-validation, IPv4-mapped-IPv6 / numeric-IPv4 / trailing-dot, multi-record fail-closed, mid-stream size bound, TLS-never-disabled, I3 url hard-fail, secret-free errors, the byte-free sidecar + cross-provider isolation, and engine/seam purity were all verified clean. The 4 lower follow-ups (abort→typed-error normalization, AbortSignalLike host-wiring type, {once:true} redundancy) are fixed (346b79a).

Validation

  • pnpm turbo run lint typecheck test build — 16/16 green (shared 369, db 58, llm 341, core 742).
  • pnpm run format:check clean; Leakwatch 0 findings.

Out of scope (remaining P4, next PR)

D12 read_media (the 13th tool + the scope-set authz + ToolPolicyDenyReason.media_scope_denied + the ToolDispatchContext byteLength/session-scope fields + the media_references row-writing lifecycle), D16 save_to write port, D15 output_modalities load-check, D17 the per-modality media cost governor, D11 the terminal-state sweep + GC, the keychain no-raw-key IPC test, the P4 byte-delivery security-review, and the still-pending canonical-home docs (built-in-tools, config-spec, security-review). The 1.AF roadmap row stays ◇ until all of P4 merges (Done-after-merge).

Conformance

  • Strict TS — no any, no unsafe as; typed/discriminated errors (MediaEgressError); unknown + guards at boundaries.
  • @relavium/llm seam holds — no vendor SDK type crosses it; the chain holds only a (handle, provider) => MediaSource function hook, never a MediaStore.
  • Engine puritypackages/core stays free of node:*/DOM/Tauri; the socket I/O is confined to @relavium/db's fetchMediaBytes, injected through the ExecutionHost.fetchMedia port.
  • I3 — no media bytes / un-re-hosted url crosses a durable boundary; the deInlineMedia hard-fail stays total with the fetch hook.
  • No new runtime dependency. Secrets / handles / IPs / bytes never in an error message or event (rule 6).
  • No new ADR needed — design pinned in ADR-0042/0043/0044; canonical-home doc updates land with their P4 D-items.

Refs: ADR-0042, ADR-0043, ADR-0044

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added secure URL-based media egress with SSRF protections, redirect validation, IP connection pinning, and strict size limits.
    • Durable event media can now be re-hosted from URL parts into safe stored handles when media egress is available.
    • Added inclusive byte-range reads with strict range validation.
  • Bug Fixes

    • URL-based media now fails safely when media egress is unavailable, preventing unsafe/inline URL persistence.
  • Tests

    • Expanded coverage for media egress, redirect/size/error handling, byte-range validation, and URL re-hosting behavior.

cemililik and others added 7 commits June 19, 2026 04:57
…iaUrlFetch hook

The shared half of the media-egress mechanism (ADR-0043 §3). deInlineMedia gains an
optional, injected `fetchUrl: MediaUrlFetch` (a narrow `url => Promise<Uint8Array>`):
- WITH the hook, a canonical url media part is re-hosted — the host fetch performs the
  SSRF-validated, streamed, size-bounded connect and the bytes are content-addressed via
  MediaStore.put exactly like a base64 source (handle-only durable, byteLength set, I3).
- WITHOUT the hook (or a malformed url / a url part with no mimeType to content-address),
  the url source still HARD-FAILS — an un-re-hosted url may never persist.

The seam stays pure `url => bytes` (no socket in @relavium/shared); the engine binds the
per-fetch size bound + AbortSignal and supplies the host mechanism (db reference + the
surfaces). rewriteMediaPart now resolves bytes via a mediaPartBytes helper (base64 decode
or url re-host) with the modality fail-closed check hoisted ahead of byte resolution.
Backward-compatible: the param is optional; every existing (value, store) caller is
unchanged. +2 tests (re-host happy path; mimeType-less url still fails even with a hook).

Refs: ADR-0043

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ference (host mechanism)

The host mechanism half of media egress (ADR-0043 §2/§3) — the Node reference the engine
binds into a MediaUrlFetch hook. Security contract, enforced in one place (never an adapter):
- HTTPS-only + no embedded credentials, via the shared SSRF policy primitives
  (extractHttpsHost / urlHasCredentials) — never a second hand-rolled parser.
- DNS-rebind/TOCTOU defense: resolve the host, validate EVERY resolved IP against the shared
  isPrivateOrLocalHost range-block, then connect pinned to the validated IP (a pinned lookup),
  so the address checked is the address connected to.
- Per-hop redirect re-validation: every 3xx Location re-runs the whole https + no-creds +
  resolve + range-block + pin cycle (a redirect-to-private / -to-http is blocked mid-fetch),
  bounded by maxRedirects; the redirect body is never read.
- Streamed + size-bounded: the body is aborted the moment it exceeds maxBytes (never fully
  buffered).
- TLS verification never disabled: connect to the pinned IP but keep the hostname as SNI
  servername, so the cert is validated against the hostname.

Errors are a typed MediaEgressError naming a reason only (never url/IP/host-stack/bytes,
rule 6). The DNS resolver + connection opener are injectable (MediaEgressDeps) so the SSRF
policy/redirect/size-bound orchestration is deterministically unit-tested without real
network — 14 tests (private-resolve block, every-IP fail-closed, redirect re-validation,
redirect-to-private/-to-http block, too_many_redirects, too_large, allowPrivate opt-in).
The Node defaults use node:dns + node:https + node:net.

Refs: ADR-0043

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ineMedia choke point

Completes D9 end-to-end (ADR-0043 §2/§3): the engine now re-hosts a url media source at the
one #emitDurable choke point.
- ExecutionHost gains an optional HostMediaFetch port (fetchMedia: (url, maxBytes, signal))
  — the host mechanism (db's fetchMediaBytes); absent-tolerant (no port ⇒ a url hard-fails).
- #deInlineDraft builds the deInlineMedia url-rehost hook from that port, bound to the run's
  size-bound POLICY (DEFAULT_MAX_MEDIA_DOWNLOAD_BYTES = 25 MiB, shared) + abort signal, so the
  engine supplies the bound and the host performs the validated connect.
- shared: DEFAULT_MAX_MEDIA_DOWNLOAD_BYTES default size bound.
- createInMemoryHost accepts a fetchMedia stub for tests.

The engine-supplied bound + the SSRF-validated host connect mean a provider/input url is
re-hosted to a content-addressed handle before it can reach a durable position; with no port
wired the url still hard-fails (I3). +2 engine tests: url node output re-hosted to a handle
(no url in the delivered or persisted stream); url output with a store but no egress port
hard-fails to run:failed with nothing stored.

Refs: ADR-0043

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…efore egress + re-materialization sidecar

D8 (engine resolves handle->source before egress) + D7 (FallbackChain sidecar +
cross-provider re-materialization), ADR-0043 §1/§4. The chain owns it (it advances
providers), staying byte-free + platform-free via an injected hook — never a MediaStore.

- FallbackChainOptions.resolveForEgress?(handle, provider) => Promise<MediaSource>: the
  host hook (backed by MediaStore.resolveForEgress). Before each attempted entry the chain
  re-materializes every handle media source in the request for that entry's provider
  (#materializeMedia, non-mutating, cheap scan + allocate-only-if-handle-present), so the
  adapter only ever sees an already-resolved source and never holds a MediaStore (I1/I2).
- The byte-free egress sidecar (#egressSidecar): caches ONLY a non-base64 provider ref per
  (provider, handle), reused on a later same-provider call (no re-upload); a base64 source
  is never cached (re-resolved — the sidecar stays byte-free). A cross-provider advance
  misses the cache (different key) + re-materializes, so a foreign provider's ref never
  crosses a boundary (matches the ADR-0030 strip-on-failover precedent).
- A re-materialization failure is retryable-advance: a visible failed attempt (secret-free
  'media re-materialization failed before egress' — never the handle/path/bytes) that
  advances to the next provider, or is turn-fatal when there is no next provider.
- Threaded through ChainCapabilities + AgentRunnerDeps.resolveForEgress (forwarded into the
  per-node chain); absent on a text-only host (a handle is then sent unchanged).

+7 chain tests: handle->base64 before the provider; no-op without a hook; non-base64 ref
cached + reused; base64 never cached (byte-free); per-provider re-materialize across a
failover; retryable-advance + single-provider-fatal on a resolution failure.

Refs: ADR-0043

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ighs found)

The dedicated egress/SSRF security pass over D7/D8/D9 found NO bypass: DNS-rebind/TOCTOU
(connect pinned to the validated IP), per-hop redirect re-validation, multi-record
fail-closed, IPv4-mapped-IPv6 / numeric-IPv4 / trailing-dot handling, mid-stream size
bound, TLS-never-disabled, I3 url hard-fail, secret-free errors, byte-free sidecar +
cross-provider isolation, and engine/seam purity were all verified clean. Four lower
follow-ups, fixed:

- MEDIUM: a socket abort/destroy mid-body-read (after headers resolved) escaped readBounded
  as a raw Node AbortError, breaking the 'only MediaEgressError' contract. Now normalized to
  MediaEgressError('network') (the too_large/typed errors are preserved). Not a leak (the
  message stays fixed + secret-free), but the typed discriminant is the contract. +1 test.
- LOW: FetchMediaBytesOptions.signal AbortSignal -> AbortSignalLike, so the engine's run
  AbortSignalLike wires into the HostMediaFetch port without a cast (a real AbortSignal still
  satisfies it structurally; the internal AbortController passed to node:https stays real).
- LOW: dropped the redundant addEventListener {once:true} (AbortSignalLike's 2-arg
  addEventListener has no options arg) — the finally-block removeEventListener is the cleanup.

Validation: pnpm turbo typecheck/test/build 12/12 (db 55, +1) + format clean + Leakwatch 0.

Refs: ADR-0043, security-review.md

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…eadRange + validateByteRange)

The byte-delivery mechanism + the engine-pure Range policy (ADR-0044 §1, security-review.md
Media byte delivery). The read_media tool (D12) and the 1.AH desktop command share this one gate.

- shared: ByteRange (inclusive start/end, HTTP Range semantics) + MediaStore.readRange(handle,
  range, signal?) on the host port. validateByteRange(range, byteLength) is the engine-pure
  policy: fail-closed on a non-integer, negative, reversed (end < start), or out-of-bounds
  (end >= byteLength) range — no raw parseInt, never trusts a client size; returns the validated
  range or a secret-free reason (a bound, never bytes).
- db: FilesystemMediaStore.readRange reuses get()'s path-jail (realpath/commonpath via #pathFor)
  + sha256 integrity check, then defensively re-bounds the inclusive range against the actual
  stored size (sliceRange). InMemoryMediaStore.readRange mirrors it. The signal param is omitted
  (a fewer-param method satisfies the interface) — the Node reference reads the bounded blob
  whole; the real desktop Rust CAS streams the range with an abort (1.AH).

The ToolDispatchContext byteLength/session-scope fields + the read_media tool that consumes this
gate land with D12. +8 tests (validateByteRange edge cases; readRange valid/out-of-bounds/reversed/
negative/malformed-handle on both stores).

Refs: ADR-0044

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…erge)

Status snapshot for the follow-on 1.AF PR (P1+P2 already merged via PR #33):
- phase-1-engine-and-llm.md: split the 1.AF status into Merged (P1+P2, PR #33), Landed on
  development (P3 D7/D8/D9 + the P3 egress/SSRF security-review + P4/D13 the byte-delivery
  Range gate, pending merge), and Remaining (P4: D12 read_media + authz, D16 save_to, D15
  load-check, D17 cost governor, D11 terminal sweep + GC, the keychain IPC test, the P4
  byte-delivery security-review, + the still-pending built-in-tools/config-spec/security-review
  doc homes).
- current.md + CLAUDE.md (x2): same Merged / landed-pending-merge / remaining split.

Per Roadmap-Done-After-Merge, nothing is marked Done — P3/D13 are 'landed on development,
pending merge'; the 1.AF matrix row stays ◇ until the PR(s) merge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@sourcery-ai sourcery-ai 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.

Sorry @cemililik, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@cemililik, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 3 hours, 6 minutes, and 22 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more credits in the billing tab to continue.

⌛ How to resolve this issue?

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 credits.

🚦 How do rate limits work?

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

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, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8eaff3d6-3695-448e-91c6-749603e59d55

📥 Commits

Reviewing files that changed from the base of the PR and between 7723dba and be22954.

📒 Files selected for processing (6)
  • packages/core/src/engine/engine.test.ts
  • packages/db/src/media-egress.test.ts
  • packages/shared/src/content.test.ts
  • packages/shared/src/content.ts
  • packages/shared/src/media-deinline.test.ts
  • packages/shared/src/media-deinline.ts
📝 Walkthrough

Walkthrough

Adds SSRF-safe media URL egress (fetchMediaBytes with DNS rebind defense and IP pinning), extends deInlineMedia to re-host url-sourced media parts to durable handles via an injected fetchUrl hook, implements MediaStore.readRange with inclusive ByteRange validation in both store backends, and threads the HostMediaFetch/resolveForEgress capabilities through WorkflowEngine, ExecutionHost, ChainCapabilities, and AgentRunner.

Changes

Media Egress P3+P4

Layer / File(s) Summary
Shared media contracts: ByteRange, validateByteRange, MediaUrlFetch, MediaStore.readRange
packages/shared/src/content.ts, packages/shared/src/content.test.ts
content.ts adds DEFAULT_MAX_MEDIA_DOWNLOAD_BYTES, ByteRange interface, validateByteRange helper, MediaStore.readRange method signature, and MediaUrlFetch type; updates DeInlineMedia overloads to accept optional fetchUrl. Tests cover inclusive semantics, boundary acceptance, and all fail-closed rejection cases.
MediaStore readRange implementation
packages/db/src/media-store.ts, packages/db/src/media-store.test.ts
Reformats shared imports, adds internal sliceRange helper using validateByteRange, then implements readRange on both FilesystemMediaStore and InMemoryMediaStore by reusing get() for path/integrity checks before applying the bounded slice. Tests cover valid inclusive reads, out-of-bounds, reversed, negative, and malformed-handle rejections.
deInlineMedia URL re-hosting via fetchUrl hook
packages/shared/src/media-deinline.ts, packages/shared/src/media-deinline.test.ts
Extends all deInlineMedia overloads and implementation with optional fetchUrl?: MediaUrlFetch; threads it through rewriteMediaPart, rewriteContainer, rewrite, and all recursive traversal helpers. Introduces mediaPartBytes to decode base64 or re-host url sources (fail-closed without hook). Tests cover URL-to-handle conversion and mimeType-less URL hard-fail.
SSRF-safe fetchMediaBytes module
packages/db/src/media-egress.ts, packages/db/src/media-egress.test.ts, packages/db/src/index.ts
New media-egress.ts module with MediaEgressError/MediaEgressErrorCode, FetchMediaBytesOptions, injectable MediaEgressDeps, SSRF validators (validateEgressHost, resolveValidatedIps), bounded body reader (readBounded), and main fetchMediaBytes orchestration with IP pinning, redirect re-validation, and abort wiring. nodeMediaEgressDeps uses node:dns + node:https with SNI preservation. db/index.ts re-exports the module. Tests validate HTTPS enforcement, credential rejection, private-IP blocking, DNS rebind defense, redirect chains, size limits, and network error normalization.
Engine and ExecutionHost wiring for HostMediaFetch
packages/core/src/engine/execution-host.ts, packages/core/src/engine/engine.ts, packages/core/src/engine/engine.test.ts
execution-host.ts adds HostMediaFetch type and optional fetchMedia on ExecutionHost and createInMemoryHost. engine.ts adds private #mediaUrlFetch() helper that builds an abort-aware bounded hook from host.fetchMedia, and passes it into deInlineMedia via #deInlineDraft. Engine tests cover URL re-hosting success (no URL leaks) and fail-closed path (no store write).
resolveForEgress through ChainCapabilities, AgentRunner, and FallbackChain
packages/core/src/engine/agent-turn.ts, packages/core/src/engine/agent-runner.ts, packages/llm/src/fallback-chain.test.ts
agent-turn.ts adds 'resolveForEgress' to the ChainCapabilities Pick. agent-runner.ts adds optional resolveForEgress to AgentRunnerDeps and wires it into chainCapabilities. FallbackChain tests extend makeProvider with capabilities? override and add a full re-materialization suite (handle resolution, caching rules, per-provider isolation, failover retry, fatal with no next provider).
Roadmap and CLAUDE.md status updates
CLAUDE.md, docs/roadmap/current.md, docs/roadmap/phases/phase-1-engine-and-llm.md
Updates milestone tracking across all three roadmap files to reflect P1+P2 merged (PR #33), P3+P4/D13 landed on development (pending merge), and remaining P4 items.

Sequence Diagram

sequenceDiagram
  participant WorkflowEngine
  participant deInlineMedia
  participant HostFetchMedia as host.fetchMedia
  participant fetchMediaBytes
  participant NodeDeps as nodeMediaEgressDeps

  rect rgba(70, 130, 180, 0.5)
    Note over WorkflowEngine: `#deInlineDraft`()
    WorkflowEngine->>WorkflowEngine: `#mediaUrlFetch`() → bounded hook or undefined
  end
  WorkflowEngine->>deInlineMedia: draft, store, fetchUrlHook
  deInlineMedia->>deInlineMedia: mediaPartBytes(url source, fetchUrlHook)
  deInlineMedia->>HostFetchMedia: fetchUrlHook(url)
  HostFetchMedia->>fetchMediaBytes: url, {maxBytes, signal}
  rect rgba(180, 60, 60, 0.5)
    Note over fetchMediaBytes,NodeDeps: SSRF validation + IP pinning
    fetchMediaBytes->>NodeDeps: resolveHost(hostname) → IPs (fail-closed if private)
    fetchMediaBytes->>NodeDeps: openConnection(pinnedIp, SNI=hostname)
    NodeDeps-->>fetchMediaBytes: HopResponse (status, body, dispose)
  end
  fetchMediaBytes-->>HostFetchMedia: Uint8Array
  HostFetchMedia-->>deInlineMedia: bytes
  deInlineMedia->>deInlineMedia: store.put(bytes, mimeType) → handle
  deInlineMedia-->>WorkflowEngine: DurableContentPart (handle, no URL)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • HodeTech/Relavium#11: Builds the engine/db "url media de-inline + egress" plumbing on top of the ADR-0031 multimodal seam types/contracts (notably the shared DeInlineMedia/media model in packages/shared/src/content.ts).
  • HodeTech/Relavium#33: Direct predecessor for 1.AF media plumbing; this PR lands the P3+P4 work (URL re-hosting, SSRF egress, readRange) by wiring deInlineMedia/the engine #deInlineDraft choke point to host-provided URL media fetch (fetchMedia/egress) and extending the same media-store/byte-range infrastructure.

Poem

🐇 A URL hopped in, suspicious and wide,
I pinned its IP and checked every side.
No private address slipped into my store—
The bytes came in clean, durable once more.
With ranges now guarded and egress secure,
Phase One draws to close… (almost, I'm sure)! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 72.73% 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 clearly summarizes the main Phase 3+4 deliverables: media egress with SSRF protection, failover re-materialization, and byte-range delivery, matching the substantial changes across the codebase.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch development

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

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request implements key parts of the 1.AF engine media plumbing, specifically the host-side SSRF-validated, size-bounded media egress mechanism (fetchMediaBytes), the byte-delivery range gate (readRange and validateByteRange), and the integration of URL re-hosting within deInlineMedia. Feedback on the changes suggests wrapping the entire execution loop in fetchMediaBytes in a try...catch block to ensure all unexpected raw errors (such as DNS resolution failures or malformed redirect URLs) are properly normalized to a typed MediaEgressError, preventing raw system errors from escaping.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +204 to +245
try {
let target = url;
for (let redirects = 0; ; redirects += 1) {
if (redirects > maxRedirects) {
throw new MediaEgressError(
'too_many_redirects',
'media egress exceeded the redirect limit',
);
}
const host = validateEgressHost(target);
const ips = await resolveValidatedIps(host, deps, allowPrivate);
// Connect by the FIRST validated IP — every IP was range-checked above, so any is safe; pinning the
// address means the address validated is the address connected to (no re-resolve TOCTOU window).
const response = await deps.openConnection(
{ url: target, hostname: host, pinnedIp: ips[0] ?? host },
controller.signal,
);
if (isRedirectStatus(response.status)) {
response.dispose(); // never read a redirect body
const location = response.location;
if (location === undefined || location.length === 0) {
throw new MediaEgressError('bad_status', 'media egress redirect had no Location');
}
target = new URL(location, target).toString(); // resolve a relative Location against the current url
continue; // re-validate the new target on the next iteration (per-hop re-validation)
}
if (response.status !== 200) {
response.dispose();
throw new MediaEgressError('bad_status', 'media egress received a non-200 status');
}
try {
return await readBounded(response.body, options.maxBytes, response.dispose);
} catch (error) {
if (error instanceof MediaEgressError) {
throw error; // a too_large (or other typed) failure — preserve the discriminant
}
// A socket abort/destroy mid-body-read (timeout / caller abort) surfaces a raw Node error; normalize
// it to the typed, secret-free network failure so the function's only thrown type is MediaEgressError.
throw new MediaEgressError('network', 'media egress request failed');
}
}
} finally {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security-high high

To strictly adhere to the contract that fetchMediaBytes only throws MediaEgressError and never leaks raw system/network errors (such as DNS resolution errors from resolveValidatedIps or connection errors from deps.openConnection), we should wrap the entire execution loop in a try...catch block to normalize any unexpected raw errors to MediaEgressError('network', ...). Additionally, we should wrap the new URL(location, target) call in a try...catch to prevent a raw TypeError from escaping if the server returns a malformed Location header.

  try {
    try {
      let target = url;
      for (let redirects = 0; ; redirects += 1) {
        if (redirects > maxRedirects) {
          throw new MediaEgressError(
            'too_many_redirects',
            'media egress exceeded the redirect limit',
          );
        }
        const host = validateEgressHost(target);
        const ips = await resolveValidatedIps(host, deps, allowPrivate);
        // Connect by the FIRST validated IP — every IP was range-checked above, so any is safe; pinning the
        // address means the address validated is the address connected to (no re-resolve TOCTOU window).
        const response = await deps.openConnection(
          { url: target, hostname: host, pinnedIp: ips[0] ?? host },
          controller.signal,
        );
        if (isRedirectStatus(response.status)) {
          response.dispose(); // never read a redirect body
          const location = response.location;
          if (location === undefined || location.length === 0) {
            throw new MediaEgressError('bad_status', 'media egress redirect had no Location');
          }
          try {
            target = new URL(location, target).toString(); // resolve a relative Location against the current url
          } catch {
            throw new MediaEgressError('insecure_url', 'media egress redirect had a malformed Location URL');
          }
          continue; // re-validate the new target on the next iteration (per-hop re-validation)
        }
        if (response.status !== 200) {
          response.dispose();
          throw new MediaEgressError('bad_status', 'media egress received a non-200 status');
        }
        try {
          return await readBounded(response.body, options.maxBytes, response.dispose);
        } catch (error) {
          if (error instanceof MediaEgressError) {
            throw error; // a too_large (or other typed) failure — preserve the discriminant
          }
          // A socket abort/destroy mid-body-read (timeout / caller abort) surfaces a raw Node error; normalize
          // it to the typed, secret-free network failure so the function's only thrown type is MediaEgressError.
          throw new MediaEgressError('network', 'media egress request failed');
        }
      }
    } catch (error) {
      if (error instanceof MediaEgressError) {
        throw error;
      }
      throw new MediaEgressError('network', 'media egress request failed');
    }
  } finally {

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{}

@cemililik

Copy link
Copy Markdown
Contributor Author

@coderabbitai review all

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown

@cemililik Sure, I'll perform a full review of all the changes in this PR!

✅ Action performed

Full review finished.

@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: 3

🧹 Nitpick comments (1)
packages/db/src/media-egress.test.ts (1)

76-237: ⚡ Quick win

Add regression tests for raw-error normalization and non-IP DNS answers.

Please add cases ensuring Line 214/217/227 paths stay fail-closed and typed: (1) resolveHost rejects, (2) resolveHost returns a non-IP token, and (3) redirect Location is unparsable.

🤖 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 `@packages/db/src/media-egress.test.ts` around lines 76 - 237, Add three
regression test cases to the fetchMediaBytes test suite to ensure error paths
remain fail-closed and return typed MediaEgressError responses: first, add a
test that verifies when resolveHost rejects, the function rejects with an
appropriate error code; second, add a test for when resolveHost returns a non-IP
token (invalid DNS response), ensuring it rejects safely; third, add a test for
when a redirect Location header is unparsable (malformed URL), confirming it
rejects with a typed error. Each test should follow the existing pattern using
fakeDeps to configure the mock resolver behavior and validate that the error
code is set correctly.
🤖 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 `@packages/db/src/media-egress.ts`:
- Around line 142-154: The resolveValidatedIps function lacks a runtime check to
ensure that each resolved value from deps.resolveHost is actually an IP literal
rather than a hostname. Add validation for each resolved address using a
standard IP validation check (such as Node's net.isIPv4 or net.isIPv6) either
when resolveHost is called or within the existing for loop that iterates through
the ips array. If an address fails the IP literal validation, throw a
MediaEgressError with an appropriate error code and message to prevent hostnames
from being used as pinnedIp values, which would bypass the validated-IP pinning
mechanism when passed to Node's HTTPS lookup callback.
- Around line 214-227: The function contains three calls that can throw raw
errors without normalizing them to MediaEgressError: the resolveValidatedIps
call, the deps.openConnection call, and the new URL constructor call. Wrap each
of these three calls in try-catch blocks that catch any thrown errors and
normalize them by rethrowing as MediaEgressError instances with appropriate
error codes and messages, following the same error normalization pattern already
implemented elsewhere in the code for handling body-read failures.

In `@packages/db/src/media-store.ts`:
- Line 58: The slicing operation on line 58 uses the original range object
properties range.start and range.end instead of the validated values, which
could be exploited if the range object is mutable or accessor-backed. Replace
range.start and range.end in the bytes.slice call with the corresponding
properties from the checked.range object (which contains the validated bounds)
to ensure the validated values are actually used for slicing.

---

Nitpick comments:
In `@packages/db/src/media-egress.test.ts`:
- Around line 76-237: Add three regression test cases to the fetchMediaBytes
test suite to ensure error paths remain fail-closed and return typed
MediaEgressError responses: first, add a test that verifies when resolveHost
rejects, the function rejects with an appropriate error code; second, add a test
for when resolveHost returns a non-IP token (invalid DNS response), ensuring it
rejects safely; third, add a test for when a redirect Location header is
unparsable (malformed URL), confirming it rejects with a typed error. Each test
should follow the existing pattern using fakeDeps to configure the mock resolver
behavior and validate that the error code is set correctly.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: a61af4c3-91cb-4941-87e9-7f37fc78bb18

📥 Commits

Reviewing files that changed from the base of the PR and between 0538c8d and 1c05d6e.

📒 Files selected for processing (19)
  • CLAUDE.md
  • docs/roadmap/current.md
  • docs/roadmap/phases/phase-1-engine-and-llm.md
  • packages/core/src/engine/agent-runner.ts
  • packages/core/src/engine/agent-turn.ts
  • packages/core/src/engine/engine.test.ts
  • packages/core/src/engine/engine.ts
  • packages/core/src/engine/execution-host.ts
  • packages/db/src/index.ts
  • packages/db/src/media-egress.test.ts
  • packages/db/src/media-egress.ts
  • packages/db/src/media-store.test.ts
  • packages/db/src/media-store.ts
  • packages/llm/src/fallback-chain.test.ts
  • packages/llm/src/fallback-chain.ts
  • packages/shared/src/content.test.ts
  • packages/shared/src/content.ts
  • packages/shared/src/media-deinline.test.ts
  • packages/shared/src/media-deinline.ts

Comment thread packages/db/src/media-egress.ts
Comment thread packages/db/src/media-egress.ts Outdated
Comment thread packages/db/src/media-store.ts Outdated
cemililik and others added 3 commits June 19, 2026 12:56
…s errors, validated-range slice

Still-valid review findings on the P3/D13 media surface, fixed:
- db/media-egress: resolveValidatedIps now rejects a resolver value that is NOT an IP literal
  (isIP === 0 -> blocked_host) — a resolver returning a hostname would pass the range-block (a
  hostname is not a private IP) and become the pinned lookup target, defeating the
  connect-by-validated-IP guarantee. Fail-closed.
- db/media-egress: fetchMediaBytes now has ONE outer catch normalizing every RAW throw (a resolver
  DNS error, an openConnection socket error, a malformed-Location new URL TypeError, an aborted
  body read) to a typed secret-free MediaEgressError('network') — so its only thrown type is
  MediaEgressError (the contract). Replaces the narrower inner readBounded catch.
- db/media-egress: extracted performHop (one validated hop -> redirect | bytes) so fetchMediaBytes's
  cognitive complexity drops back under the threshold (sonar S3776, was 16).
- db/media-store: sliceRange now slices with the VALIDATED snapshot (checked.range), not the input
  range object — an accessor-backed / mutated range cannot TOCTOU past the validated bounds.
- shared/media-deinline: the base64 source.data typeof guard throws TypeError (a type check), the
  sibling domain/value throws stay Error.

+3 regression tests: resolver rejection -> network; resolver non-IP token -> blocked_host;
malformed redirect Location -> network (no raw URL TypeError escapes).

Skipped: the sonar hardcoded-IP hotspots (10.0.0.1 / 169.254.169.254 / 192.168.1.5 in the SSRF
tests) — those are the exact private literals the block tests assert against; safe by design,
dispositionable as safe (a placeholder would defeat the test).

Validation: pnpm turbo lint/typecheck/test/build 16/16 (db 61, +3) + format clean + Leakwatch 0.

Refs: ADR-0043, ADR-0044, security-review.md

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… egress test

The malformed-redirect-Location test needs an UNPARSABLE url (new URL throws on the unclosed
IPv6 bracket); the scheme is incidental, so http:// -> https:// clears the sonar 'http insecure'
hotspot while keeping the same assertion. (The http:// literals in the http-rejection +
redirect-to-http-block tests stay — those must test that http is rejected.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…th guard + message clarity + coverage

Multi-dimensional adversarial review (9 dimensions, 2-lens verify; 35 raw -> 12 confirmed,
verdict ship-with-fixes, 0 merge-blocking — no SSRF/I3 break, no leak, no seam/purity violation).
Still-valid items fixed:
- shared/content: validateByteRange now fail-closes on its OWN byteLength (NaN/Infinity/negative)
  as the first check — an exported policy primitive (reused by the deferred read_media + 1.AH) must
  not trust its caller; a NaN byteLength would otherwise make end>=byteLength silently pass. +3 tests.
- shared/media-deinline: the non-string base64 data throw now names the real fault ('base64 media
  source.data must be a string') instead of a misleading 'unsupported kind base64'; the no-mimeType
  url throw gets a distinct suffix (vs the no-hook url throw); the unknown-kind interpolation is
  length-bounded (slice 64) to match the <=255 mimeType bound on the opaque-payload walk. Secret-free,
  fail-closed unchanged.
- coverage gaps closed: engine test for the fetchMedia hook THROWING on a url output (run:failed,
  zero puts, url never delivered/persisted) — the third D9 branch; db test for an empty DNS resolution
  (blocked_host, no openConnection); base64-non-string-data + mimeType-less-url (asserts the fetch hook
  is never invoked) regressions.

Skipped (with reason): the AbortSignal-threading test (low value — signal is plain cancellation, not
the SSRF defense; the IP-pin + abort-normalization are already tested); the #materializeMedia
per-message realloc (negligible — single-flight chain, 1-3 entries, the per-request guard already
gates the common no-media case).

Validation: pnpm turbo lint/typecheck/test/build 16/16 (shared 371, db 62, llm 334, core 743) +
format clean + Leakwatch 0.

Refs: ADR-0043, ADR-0044

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@cemililik
cemililik merged commit d0faf38 into main Jun 19, 2026
9 checks passed
@sonarqubecloud

Copy link
Copy Markdown

cemililik added a commit that referenced this pull request Jun 19, 2026
Bring the roadmap status current: PR #34 (P3 + P4/D13) merged; the P4 remainder
(D12, D11, D15, D16, D17 + the byte-delivery review + canonical-home docs) is
landed on `development`, pending merge. The keychain no-raw-key IPC test is
recorded as deferred to 1.AH (no Phase-1 desktop surface; ADR-0044 §4). The
matrix row stays ◇ until the PR merges (Roadmap-Done-After-Merge).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
cemililik added a commit that referenced this pull request Jun 20, 2026
…surfaces

All 1.AF (engine media plumbing) PRs are merged to main (2026-06-20): P1+P2 (#33),
P3 + P4/D13 (#34), P4 remainder (#35), and the multi-agent + external review
follow-ups (#36). Flip the status markers from in-progress / pending-merge to Done
across CLAUDE.md, README.md, current.md, the phase-1 task entry + matrix row + the
1.m6 milestone row, and the deferred-tasks note.

The deferred host-wiring half (D12 MediaReadAccess + session-scope population, the
D15 loader call, the D17/resolveForEgress config) and the keychain no-raw-key IPC
test remain owned by 1.AH (already recorded in deferred-tasks.md). Remaining
Phase-1 work: 1.AG/1.AH.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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