Tool Backend 2e8e70e765
Repository Validate / repository-checks (pull_request) Successful in 19s
change: Implement 7: Document the tool-calling contract, statuses, exit codes and native-process usage
Rewrote README.md as the delivered tool-calling contract for the calling AI agent and added tests/test_readme.py to keep it synchronized with the implementation.

README.md now documents:
- Native-process usage only: `python -m tools.example` with no Docker, container image or packaging stage, Python >= 3.11, no runtime third-party dependencies.
- Every parameter exactly as produced by `--help`: positional `REQUEST.json`, `-t/--topic`, `--width`, `--height`, `-f/--format` (`png|ico`), `-s/--source` (default `wikimedia_commons`), `-c/--count` (1-50, default 10), `--download`, `-o/--output-dir`, `--resize`, `--dry-run`; width/height documented as minimums and the "document XOR flags" rule.
- The JSON request document form used by `examples/input.json`: field table, types, defaults, optional `version`, and the document-level invalid_request rules.
- The single-document JSON response schema: fixed keys `status`, `source`, `request`, `items`, `error`; item fields (actual format/width/height, `url`/`file_url`, `size_bytes`), download record, resize fragment and the verified-save failure record.
- The fixed status vocabulary with its exit codes: `ok`=0, `dry_run`=0, `invalid_request`=2, `no_format`=3, `no_size`=4, `access_error`=5, `partial_download`=6, plus why no_match/outage are distinguishable.
- Approved free sources (`wikimedia_commons`, `openverse`) with no API keys, tokens or registration, the approved HTTPS host allowlist, and the registry extension point (`SourceAdapter`, `SourceRegistry.register/unregister`, `build_registry()`/`default_registry()`, host allowlist extension).
- Both decided behaviours: `--download` without `--output-dir` is an invalid request (invalid_request / exit 2), and partial saves are reported as `partial_download` (exit 6) with `download_incomplete` details while saved paths are still reported.
- The real test entrypoint `pytest` plus the compileall/help/smoke commands.

tests/test_readme.py asserts the README documents every long option the real parser accepts, the accepted formats, every status with its exit code from STATUS_EXIT_CODES, the response keys, every registered source id, the registry extension names, both product decisions, native-process wording, and the real pytest/compileall/help/smoke commands.

Tests:
Ran the facade check set with .venv/bin on PATH after `prepare_environment(component=tools, mode=restore)`:
- `pytest -q` -> 163 passed, 1 skipped (pre-existing tests/test_live_source.py live-network skip: Wikimedia upload host returns HTTP 429 from this sandbox).
- `python -m compileall -q tools tests` -> ok.
- `python -m tools.example --help` -> exit 0.
- `python -m tools.example examples/input.json` -> exit 0, one JSON dry_run envelope.
- `pytest tests/test_readme.py -q` -> 5 passed.
Also confirmed `git merge-base --is-ancestor origin/task/1-task HEAD` (target branch already integrated, zero diff), so no integration step was required.

Progress:
Delivered the documentation slice for issue 9. README.md was a two-line placeholder; it is now the full tool-calling contract: CLI parameters matching `--help` output, the JSON request-document form for examples/input.json, the single-document response schema, the fixed status vocabulary with exit codes (ok/dry_run=0, invalid_request=2, no_format=3, no_size=4, access_error=5, partial_download=6), credential-free sources and the registry extension point, the decided --download-without---output-dir invalid-request behaviour, partial_download reporting for incomplete saves, and plain native-process execution with the real pytest entrypoint. Added tests/test_readme.py (5 tests) so CI fails if documentation drifts from build_parser(), STATUS_EXIT_CODES, ACCEPTED_FORMATS or the default registry. No protected facade paths or source behaviour changed; existing obligations preserved (full suite green apart from the pre-existing live-source skip).

Published by developer-agent through Tool Backend.

Closes #9
2026-11-03 02:40:29 +00:00
2026-11-02 22:37:32 +00:00

product-54354138-1 image toolkit

A plain native-process command-line tool for a calling AI agent. One invocation accepts a request, searches an approved free image source, verifies every candidate against its downloaded bytes and writes exactly one machine-readable JSON document to stdout. The process exit code mirrors the document's status, so success, "nothing matched", a broken request and an unusable source are distinguishable without parsing prose.

There is no Docker, no container image and no packaging step: the tool is run directly as an executable process with the interpreter that is already present.

Contents

Requirements

  • Python >= 3.11 (declared in pyproject.toml).
  • No runtime third-party dependencies: the HTTPS client, PNG/ICO container parsing, decoding, resampling and re-encoding all live in the standard library and in tools/.
  • No API keys, tokens or registration for any source (see Image sources).

There is nothing to install to use the tool from a checkout. Installing the project (pip install ., uv sync, ...) only matters for the optional test tooling.

Running the tool

The tool is always invoked as a module of the working tree:

python -m tools.example --topic "red fox" --width 640 --height 480 --format png

A JSON request document can be passed instead:

python -m tools.example examples/input.json

--help is the authoritative parameter list; the table below is a summary:

python -m tools.example --help

Rules for the two invocation styles:

  • Either a request document or flags must be given. With neither, the run fails with invalid_request (exit code 2).
  • A request document cannot be combined with any flag; doing so is invalid_request (exit code 2).

Request parameters

Parameter Kind Meaning
REQUEST.json positional Path to a JSON request document holding the same fields as the flags. It cannot be combined with any of the flags.
-t, --topic TEXT text Search topic (required unless a request document is given).
--width PX positive integer Target width in pixels; only images at least this wide are accepted.
--height PX positive integer Target height in pixels; only images at least this tall are accepted.
-f, --format FMT png or ico Image format; required. Any other value is invalid_request.
-s, --source ID source id Image source id (default: wikimedia_commons); unknown ids are rejected with the list of available sources.
-c, --count N integer 1-50 How many candidates to request (default: 10).
--download flag Download every matching image into the output directory.
-o, --output-dir DIR path Caller-specified download directory; required with --download.
--resize flag Downscale every downloaded image that is larger than the request in either dimension to exactly the requested width and height, re-encoded in the requested format; images are never scaled up. Without it the original bytes and dimensions are written.
--dry-run flag Validate the request and emit the envelope without any network access.

--width and --height are a minimum, not an exact match: an image that is at least that large is usable because it can be downscaled, while a smaller image would have to be scaled up and is therefore rejected. --resize decides whether the stored file is downscaled; it never changes this filter.

--count bounds how many matching images are reported. Because nothing a source reports about a hit is trusted, the tool inspects a bounded window of up to count * 4 candidates (capped at 50) and stops as soon as count verified images have been found.

Short flags: -t/--topic, -f/--format, -s/--source, -c/--count, -o/--output-dir, -h/--help. Every other parameter is long-form only.

JSON request document

The document accepted as the positional argument (see examples/input.json) is a single JSON object using the same names as the flags, in snake_case:

{
  "version": 1,
  "topic": "red fox in the snow",
  "width": 640,
  "height": 480,
  "format": "png",
  "source": "wikimedia_commons",
  "count": 5,
  "download": false,
  "output_dir": "downloads",
  "resize": false,
  "dry_run": true
}
Field Type Required Notes
topic string, non-empty yes Same as --topic.
width integer > 0 or null no Minimum width; omitted/null means unconstrained.
height integer > 0 or null no Minimum height; omitted/null means unconstrained.
format "png" or "ico" yes Same as --format.
source string no Defaults to wikimedia_commons.
count integer 1-50 no Defaults to 10.
download boolean no Defaults to false.
output_dir string, non-empty required when download is true Download directory.
resize boolean no Defaults to false.
dry_run boolean no Defaults to false.
version integer no Optional document metadata; accepted and otherwise ignored.

Document rules that produce invalid_request (exit code 2):

  • the file cannot be read, is not valid JSON, or is not a JSON object;
  • an unknown field is present (error.code = "request_field", with details.unknown_fields and details.accepted_fields);
  • a field has the wrong type, is empty, or is out of range;
  • format is outside png|ico;
  • download is true while output_dir is absent or empty.

JSON response document

Every run - success or failure - prints exactly one JSON document (one line) to stdout, with this fixed set of top-level keys:

{
  "status": "ok",
  "source": "wikimedia_commons",
  "request": {"topic": "red fox", "width": 640, "height": 480, "format": "png", "source": "wikimedia_commons", "count": 1, "download": false, "output_dir": null, "resize": false, "dry_run": false},
  "items": [
    {
      "source": "wikimedia_commons",
      "url": "https://upload.wikimedia.org/wikipedia/commons/.../Fox.png",
      "file_url": "https://upload.wikimedia.org/wikipedia/commons/.../Fox.png",
      "format": "png",
      "width": 1280,
      "height": 853,
      "size_bytes": 1489231,
      "title": "File:Fox.png",
      "page_url": "https://commons.wikimedia.org/wiki/File:Fox.png",
      "mime_type": "image/png"
    }
  ],
  "error": null
}
Key Type Meaning
status string One value of the fixed vocabulary below.
source string or null Source id the request selected, when known.
request object or null The validated request (every field, defaults filled in), null when the request could not be validated.
items array Verified images (empty unless status is ok or partial_download).
error object or null {"code", "message", "details"}; details is optional.

An item is only reported after its bytes were downloaded and parsed, so its format, width and height are the actual values read from the payload container, never what the source claimed. url and file_url are the same direct HTTPS location; file_url is kept as an alias. title, page_url and mime_type are present only when the source provides them. size_bytes is the number of stored/fetched bytes.

With download enabled every item additionally carries a download record:

{"ok": true, "path": "downloads/001-file-fox.png", "bytes": 1489231, "format": "png", "width": 1280, "height": 853}

With resize enabled the record also carries a resize fragment: {"requested": {...}, "from": {...}, "to": {...}, "format": "png", "applied": true} ("applied": false, "reason": "already_fits" when nothing had to change).

A file whose stored bytes do not read back as the requested format at the expected size is deleted and reported as {"ok": false, "target": "...", "error": {...}} instead of being named as saved.

Status vocabulary and exit codes

The status vocabulary is fixed and every branch reuses it; the exit code is derived from the status.

status Exit code Meaning
ok 0 At least one image really is the requested format and at least the requested width and height (all downloads completed when requested).
dry_run 0 The request was validated and echoed without contacting a source.
invalid_request 2 The request itself is unusable: bad or missing flags, unreadable/invalid document, unknown field, unsupported format, unknown source, or download without output_dir.
no_format 3 The source answered but offers nothing in the requested format for the topic.
no_size 4 The source has images of the requested format, but all of them are smaller than requested.
access_error 5 The source could not be searched, or not a single candidate could be downloaded; never reported as "no match".
partial_download 6 A download run where some images were saved and some could not be (see below).

The distinction is intentional and stable:

  • no_format / no_size describe the topic, not an outage, so they are not errors of the source.
  • A candidate whose bytes turn out to be a different format (JPEG, SVG, TIFF, an HTML error page, ...) is a format rejection that feeds no_format, not an access error.
  • access_error means the source could not be used at all; its error.code comes from the typed error (source_unreachable, source_http_error, source_invalid_response, source_host_not_allowed, candidate_unreadable, access_error).
  • invalid_request always carries a machine-readable error.code (request_invalid, request_document, request_field, request_format_unsupported, source_unknown) plus error.details.

Image sources

Two working free sources ship with the tool. Neither needs an API key, a token, registration or a personal account; both are queried anonymously over HTTPS with a stable identifying User-Agent.

Source id Service Approved hosts
wikimedia_commons (default) Wikimedia Commons commons.wikimedia.org, upload.wikimedia.org
openverse Openverse api.openverse.org

Only those hosts on https/port 443 are contacted; any other URL is refused with source_host_not_allowed rather than fetched. HTTP 429 and 5xx responses are retried a bounded number of times with capped exponential backoff and then reported as access_error; a failed or unknown source is never silently degraded into an empty result.

An unknown --source value fails with invalid_request and lists the available ids:

{"error": {"code": "source_unknown", "message": "unknown image source 'nope'; available sources: openverse, wikimedia_commons", "details": {"source_id": "nope", "available_sources": ["openverse", "wikimedia_commons"]}}}

Adding another source

The registry in tools/sources.py is the extension point:

  • Implement the SourceAdapter protocol: a non-empty source_id string and search(topic, limit) -> list[SourceCandidate].
  • Build SourceCandidate values whose file_url is an approved HTTPS URL and whose fetch_bytes() returns the image bytes through HTTPSClient.
  • Register the adapter with SourceRegistry.register(adapter) (a duplicate id raises DuplicateSourceError; unregister(source_id) removes one) and wire it into build_registry() / default_registry(), which is what the CLI uses. HTTPSClient(allowed_hosts=...) and validate_source_url() extend the approved-host allowlist for a new provider.
  • The new id becomes selectable through --source / "source" immediately; SourceRegistry.get() raises UnknownSourceError listing all registered ids for anything else.

Download and resize behaviour

Two product decisions are fixed and documented here:

  1. --download without --output-dir is an invalid request. There is no hidden default directory; a download run without a caller-supplied directory could only guess where files should go. Such a run fails with status: "invalid_request", exit code 2, and error.code: "request_field", error.details.field: "output_dir". The same rule applies to a request document with "download": true and no output_dir.
  2. Partial saves are reported as partial_download. When a download run saves some images but fails to fetch or verify others, the status is partial_download (exit code 6) - never ok. The document still lists every successfully saved item, and error.code: "download_incomplete" carries details.saved, details.failed, details.total, details.saved_paths and details.failures. A run in which every image was saved is ok (exit code 0); a run in which nothing could be fetched at all is access_error (exit code 5).

Files are written into --output-dir as NNN-<slug>.<format>, created with mkdir -p semantics. Without --resize the verified original bytes are stored unchanged. With --resize each stored image is downscaled to exactly the requested width and height (a dimension left open keeps its source value) and re-encoded in the requested container format; an image already at or below the requested size is written unchanged and the resize fragment reports "applied": false. --resize never enlarges an image. A search without --download never touches the filesystem.

Tests and checks

The test suite entry point is pytest from the repository root:

pytest

Other repository checks used by CI:

python -m compileall tools tests
python -m tools.example --help
python -m tools.example examples/input.json

examples/input.json is a --dry-run request, so the smoke command validates the contract and prints the envelope without any network access.

Running the tests may require the declared dev dependency (pytest>=8.0) to be installed, for example pip install -e '.[dev]' or uv sync. That install is only for development; the tool itself runs as a plain process with no Docker or container packaging at any stage.

S
Description
agent-intake-source: platform/product-requests#1
Readme
219 KiB
Languages
Python 100%