Skip to content

feat(safari): artifact-download; bound byte transfers by progress, not total time - #246

Open
ysyneu wants to merge 3 commits into
feat/artifact-downloadfrom
artifact-download
Open

ysyneu wants to merge 3 commits into
feat/artifact-downloadfrom
artifact-download

Conversation

@ysyneu

@ysyneu ysyneu commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Problem

  • Reading an artifact took three commands plus hand-parsing a token. The steps were artifact-gallery-get → artifact-sign → cutting t out of download_url with sed → artifact-stream --t. An agent has to get all of them right.
  • The generated artifact-stream lost data. It went through the typed JSON path:
    • a JSON file printed OK: GET /safari/artifact/stream and its bytes were dropped;
    • files over 10MB were truncated;
    • any transfer longer than the 30s client timeout failed.
  • Other byte transfers had the same 30s cap. This applied to session export and to file, skill and CSV uploads, however healthy the transfer.

Change

  • New safari artifact-download <art_…|pf_…|link>.
    • Takes an artifact ID, a presented-file ID, or a console link that contains either.
    • Resolves the ID, signs, and streams the file to disk under the server's file name (base name only, so the file cannot land outside the current directory), or to --output (- means stdout).
    • Prints the saved path, byte count and content type; --json prints the same as an object.
    • Removes a partial file on failure.
  • safari artifact-stream --t stays at its path-name (the path-is-king invariant). It is now curated on the SDK's streaming Artifacts.Download and saves files the same way.
  • Timeouts by kind of call.
    • JSON calls keep the SDK's 30s overall timeout.
    • Byte transfers have no total limit and are cancelled after 60s with no bytes moved. They report transfer stalled: no bytes moved for 1m0s… instead of context canceled. The idle clock also covers waiting for the response to start. Covered: artifact download/stream, session export, safari send --file, skill upload, mapping CSV upload.
    • In broker mode, the SDK copies the broker client and drops its overall timeout for transfers, so the same rules apply there.
  • go.mod points at the go-flashduty PR commit that adds Artifacts.Download (feat(artifacts): stream file downloads; no overall timeout on byte transfers go-flashduty#115). Bump it to the release tag before release.

Verification

  • go test ./... passes, including TestEveryOperationHasPathCommand and TestGeneratorTargetsFullSpec.
  • make check-cards passes.
  • golangci-lint reports nothing in the changed files. The 3 govet reflect.Ptr notes are in untouched files.
  • New tests:
    • a console link resolves art → pf → token, and a JSON file whose server name is ../../etc/data.json is saved verbatim as data.json;
    • -o with --json;
    • -o - writes to stdout;
    • an argument with no ID is rejected;
    • a stalled body is cancelled with errTransferStalled and leaves no partial file;
    • a 200ms trickle survives an 80ms idle timeout.
  • Ran the built binary against a dev deployment:
    • art_ ID → 18996-byte HTML saved under its server name;
    • console link with -o --json;
    • pf_ ID to stdout;
    • a missing ID returns ResourceNotFound, exit 1.

Follow-up commits

  • A failed download goes through a temp file that is renamed into place. The earlier file is kept and no partial file is left behind. Devices and FIFOs such as -o /dev/null are written directly. The temp file is created 0666, so the umask decides the saved file's mode.
  • Without --output, a server-chosen file name that already exists is refused. An explicit --output path is overwritten.
  • A response body's idle clock runs only while a read waits on the network. Piping into a slow consumer such as | less is therefore not reported as a stall. Uploads keep reset-on-read, because the SDK reads the source only as fast as the network drains it.
  • An artifact that resolves to an empty file_id gets a clear error.

Release order

  1. Merge and tag go-flashduty#115.
  2. Repin go.mod to that tag. The current pin is a branch pseudo-version.
  3. Release the CLI.
  4. Bundle the new fduty into the runner and sandbox images.

…t total time

Reading an artifact took three commands and hand-parsing a token
(gallery-get → sign → cut t out of download_url → artifact-stream), and the
generated artifact-stream printed "OK" for a JSON file and truncated files
over 10MB.

- `safari artifact-download <art_…|pf_…|link>` resolves the ID (also from a
  console link), signs, streams to the server's file name (base name only) or
  --output (- for stdout), and removes a partial file on failure.
- `safari artifact-stream --t` stays at its path-name for callers holding a
  download_url, now curated on the SDK's streaming Artifacts.Download.
- Timeouts by kind of call: JSON calls keep the SDK's 30s overall timeout;
  byte transfers (artifact download, session export, file/skill/CSV uploads)
  have no total limit and are cancelled after 60s with no bytes moved,
  with an error that says so.

Requires go-flashduty with Artifacts.Download.
…fails

Write through a temp file beside the destination and rename it into place, so a failed or stalled download leaves neither a partial file nor a truncated earlier one; devices and FIFOs (-o /dev/null) are written directly. Reject an artifact that resolves to no file_id. Bumps go-flashduty for download-token redaction in logged URLs.
…place local ones

A response body's idle clock now runs only while a read waits on the network, so piping a download or export into a slow consumer is not reported as a stalled transfer. Without --output, a server-chosen name that already exists is refused instead of replaced. The temp file is created with 0666 so the umask decides the saved file's mode. go mod tidy drops stale go.sum lines.
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