Skip to content

feat(custom-engine): download artifacts.files[].url artifacts into the report#103

Open
zpzjzj wants to merge 2 commits into
mainfrom
feat/custom-artifact-url-download
Open

feat(custom-engine): download artifacts.files[].url artifacts into the report#103
zpzjzj wants to merge 2 commits into
mainfrom
feat/custom-artifact-url-download

Conversation

@zpzjzj

@zpzjzj zpzjzj commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

What

This is PR-C of the Custom Engine HTTP rollout (after #99 JSON core and #102 multipart upload). It implements downloading SessionResult.artifacts.files[].url artifacts into the report — the last unimplemented piece of the custom-engine artifact contract.

Previously the case f.URL != "" branch in collectArtifacts only logged a debug stub ("... url is not downloaded in the local transport"). It now performs a real, best-effort HTTP download.

How

  • downloadURLArtifact (new, in custom.go): GETs the declared URL and writes the body into the case artifact directory (opts.ArtifactDir) under f.Name via the existing writeLocalArtifact, then registers the local path in artifacts.GeneratedFiles. Wired into collectArtifacts.
  • Bounded: 256 MiB cap via io.LimitReader (maxArtifactDownloadBytes) and a per-download timeout via context (customArtifactDownloadTimeout). Mirrors the maxHTTPResponseBytes / io.LimitReader / maskAPIKey style introduced in feat(custom-engine): HTTP transport JSON request/response core #99's custom_http.go.
  • Best-effort: a non-2xx status, transport error, non-http(s) scheme, or over-cap body is logged as a warning and skipped — the run still succeeds, consistent with the rest of artifact handling.
  • Security: only the exact declared URL is fetched (SSRF stays the engine/operator's responsibility, same posture as the http transport). Non-http(s) schemes are rejected. A URL embedding the configured API key is refused up front (mirrors errSecretInURL), and error strings are scrubbed via maskAPIKey. The download writes to the artifact output dir, never the workspace, so it bypasses f.Path workspace confinement by construction (writeLocalArtifact confines the name to the dir basename).

It is handled transport-agnostically in collectArtifacts (per the design contract, any engine may declare a url), not in the transport layer.

Independent of #102

This PR touches a different code region (collectArtifacts in custom.go) than the in-flight multipart PR #102 (custom_http.go). The only overlap is the trivial CHANGELOG [Unreleased] entry; rebase that line if #102 lands first.

Docs

Refs the custom-engine HTTP transport design (docs/design/custom-engine.md).

Tests

New internal/agent/custom_artifact_url_test.go (httptest-driven, reuses newCustomTestRuntime etc.):

  • url declared → downloaded and registered in GeneratedFiles with the right content
  • end-to-end Run where the artifact url returns 500 → run still succeeds, artifact skipped (with warning)
  • unreachable url → skipped with warning
  • non-http(s) scheme (file://) → skipped
  • size cap enforced (cap lowered via the test-overridable var)
  • url embedding the configured API key → refused, no leak
  • engine.custom.env secret embedded in the url → masked in the failure warning

make test (race), make verify (0 issues), and the make e2e custom-engine subset all pass.

🤖 Generated with Claude Code

…e report

Replace the local-transport URL stub in collectArtifacts with a real,
best-effort HTTP download: a SessionResult that declares a downloadable
`url` is GET'd (http/https only) and written into the case artifact
directory under `name`, then registered in GeneratedFiles. This completes
the custom-engine artifact contract (PR-C, after the #99 JSON core).

The download is bounded (256 MiB cap via io.LimitReader, per-download
timeout via context) and best-effort: a non-2xx status, transport error,
non-http(s) scheme, or over-cap body is logged and skipped without
failing the run, consistent with the rest of artifact handling. A URL
embedding the configured API key is refused up front (mirrors
errSecretInURL on the http transport) and error strings are scrubbed via
maskAPIKey. SSRF stays the engine/operator's responsibility, the same
trust posture as the http transport.

Handled transport-agnostically in collectArtifacts (any engine may
declare a url per docs/design/custom-engine.md), so it is independent of
the in-flight multipart PR which only touches custom_http.go.
@zpzjzj zpzjzj requested a review from hittyt as a code owner June 10, 2026 12:54
@zpzjzj zpzjzj requested review from jwx0925, lbfsc and lijunfeng722 June 11, 2026 03:02
@zpzjzj

zpzjzj commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ac643023c0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/agent/custom.go Outdated
…facts

The default http.Client follows 3xx redirects, so the archived bytes could
come from a Location URL rather than the engine-declared f.URL — breaking the
documented "exact declared URL" boundary and the up-front api-key/scheme
checks, and letting an artifact host point the fetch at a different (possibly
internal) endpoint. Set CheckRedirect to http.ErrUseLastResponse so a 3xx is
returned as-is and falls through to the existing non-2xx skip path.

Addresses Codex review on #103.
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