feat(media): treg host reference-file hosting + catalog-id-with-path error

Two fixes from generating an 18 s Seedance 2.5 take through treg on 2026-09-14.

`treg host <file>` / `POST /media` / `GET /m/{token}`: AIGC endpoints take reference
images, voice clips and videos as public URLs the vendor fetches, and every paste host
tried failed a vendor probe (catbox unreachable from reAPI's fal-backed probe, tmpfiles
serving HTML, uguu timing out) - five submissions for one accepted task. Bytes live in a
new `media` table (migration 0037), served by a 192-bit opaque token, 30 MB per file,
300 MB per org per 24 h, 7-day TTL, image/audio/video only, refused in the sandbox, not
metered. Expiry is swept inline on upload.

`treg call reapi.tasks.get tasks/<id>` used to answer "no tool 'reapi.tasks.get' in this
org": the own-tool shape applied to a catalog id sent the caller to the wrong half of
treg. It now answers 400 naming the endpoint's parameter slots and the --query form.

Fragments: new architecture/media.md; interface/cli.md, interface/api.md. Front door:
llms.txt, skill.md (+ generated plugin copies), USAGE.md. Snapshots regenerated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WYmLQ8AcvGcDM95MRNjodb
This commit is contained in:
Jason Zhou
2026-09-14 23:01:38 +10:00
co-authored by Claude Fable 5.1
parent b116e2f706
commit c8d51c5e21
31 changed files with 515 additions and 7 deletions
+7 -2
View File
@@ -78,6 +78,7 @@ Regenerate via `scripts/build-map.py`.
| `src/treg/alembic/versions/0034_managed_api_keys.py` | architecture/data-model.md, ops/deploy.md |
| `src/treg/alembic/versions/0035_default_key_generation.py` | architecture/data-model.md, ops/deploy.md |
| `src/treg/alembic/versions/0036_activity_key_indexes.py` | architecture/data-model.md, ops/deploy.md |
| `src/treg/alembic/versions/0037_media_hosting.py` | architecture/media.md |
| `src/treg/analytics.py` | architecture/data-model.md |
| `src/treg/api.py` | architecture/archive.md, architecture/money.md, architecture/multi-tenancy.md, architecture/proxy-model.md, architecture/super-admin.md, interface/api.md, interface/dashboard.md, interface/landing-sandbox.md, interface/seo.md |
| `src/treg/application/__init__.py` | architecture/import-boundaries.md |
@@ -105,6 +106,7 @@ Regenerate via `scripts/build-map.py`.
| `src/treg/application/call/types.py` | architecture/import-boundaries.md, architecture/proxy-model.md, interface/api.md |
| `src/treg/application/connect.py` | architecture/auth-secrets.md, architecture/composition.md, guides/expanding-a-category.md, interface/api.md |
| `src/treg/application/feedback.py` | architecture/feedback.md |
| `src/treg/application/media.py` | architecture/media.md |
| `src/treg/application/onboard.py` | interface/api.md |
| `src/treg/application/onboard/__init__.py` | interface/landing-sandbox.md, interface/onboarding.md |
| `src/treg/application/onboard/demo.py` | interface/onboarding.md |
@@ -345,7 +347,7 @@ Regenerate via `scripts/build-map.py`.
| `src/treg/maintenance.py` | architecture/data-model.md, ops/deploy.md |
| `src/treg/mcp.py` | architecture/catalog.md, architecture/instagram-oauth.md, architecture/mcp-oauth.md |
| `src/treg/mcp_install.py` | interface/skill.md |
| `src/treg/models.py` | architecture/data-model.md, architecture/money.md, architecture/multi-tenancy.md, interface/enrich-arena.md |
| `src/treg/models.py` | architecture/data-model.md, architecture/media.md, architecture/money.md, architecture/multi-tenancy.md, interface/enrich-arena.md |
| `src/treg/oauth.py` | architecture/auth-secrets.md |
| `src/treg/oauth_providers.py` | architecture/auth-secrets.md, architecture/harvestapi.md, guides/expanding-a-category.md |
| `src/treg/providers.py` | interface/env-import.md |
@@ -362,6 +364,7 @@ Regenerate via `scripts/build-map.py`.
| `src/treg/routers/catalog.py` | architecture/catalog.md, interface/api.md |
| `src/treg/routers/connections.py` | architecture/auth-secrets.md, architecture/composition.md, guides/expanding-a-category.md, interface/api.md |
| `src/treg/routers/feedback.py` | architecture/feedback.md |
| `src/treg/routers/media.py` | architecture/media.md, interface/api.md |
| `src/treg/routers/onboard.py` | architecture/composition.md, interface/api.md, interface/landing-sandbox.md, interface/onboarding.md |
| `src/treg/routers/orgs.py` | architecture/composition.md, architecture/money.md, architecture/multi-tenancy.md, interface/api.md |
| `src/treg/routers/referrals.py` | architecture/composition.md, architecture/money.md, interface/api.md |
@@ -475,6 +478,7 @@ Regenerate via `scripts/build-map.py`.
| `tests/test_mcp.py` | architecture/mcp-oauth.md |
| `tests/test_mcp_directory.py` | architecture/mcp-oauth.md |
| `tests/test_mcp_oauth.py` | architecture/mcp-oauth.md |
| `tests/test_media.py` | architecture/media.md |
| `tests/test_oauth_billed.py` | architecture/proxy-model.md |
| `tests/test_oauth_refresh.py` | architecture/auth-secrets.md |
| `tests/test_passthrough.py` | architecture/proxy-model.md |
@@ -509,6 +513,7 @@ Regenerate via `scripts/build-map.py`.
| `architecture/local-proxy.md` | `localproxy.py`, `server.js` |
| `architecture/local-run.md` | `localrun.py`, `egress.py`, `fsjail.py` |
| `architecture/mcp-oauth.md` | `auth.py`, `mcp.py`, `health.py`, `mcp_oauth.py`, `session.py`, `access.py`, `api_keys.py`, `api_keys.py`, `auth.py`, `claude-connector.html`, `connect-demo.html`, `CLAUDE-CONNECTOR-SUBMISSION.md`, `test_mcp.py`, `test_mcp_oauth.py`, `test_mcp_directory.py`, `test_marketplace_call.py` |
| `architecture/media.md` | `media.py`, `media.py`, `models.py`, `0037_media_hosting.py`, `test_media.py` |
| `architecture/money.md` | `__init__.py`, `settlement.py`, `__init__.py`, `models.py`, `billing.py`, `idempotency.py`, `intake.py`, `resolve.py`, `service.py`, `reserve.py`, `settle.py`, `tomba.yaml`, `asynctasks.py`, `0017_async_task_record.py`, `0018_async_resource_ownership.py`, `0019_async_poll_failures.py`, `referrals.py`, `budgets.py`, `__init__.py`, `stripe.py`, `reconcile.py`, `referrals.py`, `api.py`, `signup.py`, `promotions.py`, `0033_signup_promo_eligibility.py`, `admin.py`, `billing.py`, `call.py`, `orgs.py`, `referrals.py`, `test_call_architecture.py`, `test_asynctasks.py` |
| `architecture/multi-tenancy.md` | `models.py`, `api.py`, `caller_metadata.py`, `auth.py`, `asynctasks.py`, `resolve.py`, `signup.py`, `access.py`, `budgets.py`, `publicdemo.py`, `teams.py`, `usage.py`, `access.py`, `api_keys.py`, `session.py`, `promotions.py`, `test_team_limit.py`, `test_auth.py`, `test_token_revocation.py`, `auth.py`, `orgs.py`, `resources.py`, `bundles.py`, `db.py`, `0017_async_task_record.py`, `0018_async_resource_ownership.py`, `test_router_dependencies.py`, `test_asynctasks.py` |
| `architecture/proxy-model.md` | `relay.py`, `ssrf.py`, `api.py`, `authorize.py`, `idempotency.py`, `intake.py`, `resolve.py`, `reserve.py`, `settle.py`, `evidence.py`, `service.py`, `types.py`, `asynctasks.py`, `client_identity.py`, `call_surface.py`, `sandbox_identity.py`, `access.py`, `publicdemo.py`, `usage.py`, `call.py`, `test_ssrf_public_addresses.py`, `test_call_application_contract.py`, `test_call_cancellation.py`, `test_call_response_limits.py`, `test_error_capture.py`, `test_marketplace_call.py`, `test_oauth_billed.py`, `test_passthrough.py`, `test_tag_billing.py`, `test_tag_billing_adversarial.py`, `test_call_architecture.py`, `test_asynctasks.py` |
@@ -516,7 +521,7 @@ Regenerate via `scripts/build-map.py`.
| `architecture/super-admin.md` | `api.py`, `admin.py`, `access.py`, `config.py` |
| `foundation/charter.md` | `2026-06-30-jason-tools-registry.md`, `README.md` |
| `guides/expanding-a-category.md` | `oauth_providers.py`, `authorization.py`, `oauth_flow.py`, `oauth_exchange.py`, `connect.py`, `connections.py`, `config.py` |
| `interface/api.md` | `sitetrack.js`, `api.py`, `bootstrap_handlers.py`, `bootstrap_http.py`, `call_surface.py`, `caller_metadata.py`, `client_identity.py`, `auth.py`, `access.py`, `authorize.py`, `idempotency.py`, `intake.py`, `resolve.py`, `reserve.py`, `settle.py`, `evidence.py`, `service.py`, `types.py`, `relay.py`, `connect.py`, `onboard.py`, `referrals.py`, `signup.py`, `__init__.py`, `admin.py`, `auth.py`, `auth_helpers.py`, `billing.py`, `call.py`, `catalog.py`, `connections.py`, `onboard.py`, `orgs.py`, `api_keys.py`, `resources.py`, `referrals.py`, `signup_cookies.py`, `web.py`, `access.py`, `api_keys.py`, `teams.py`, `access.py`, `budgets.py`, `publicdemo.py`, `usage.py`, `mcp_oauth.py`, `session.py`, `timeutil.py`, `store.py`, `email.py`, `runner.py`, `ratestore.py` |
| `interface/api.md` | `media.py`, `sitetrack.js`, `api.py`, `bootstrap_handlers.py`, `bootstrap_http.py`, `call_surface.py`, `caller_metadata.py`, `client_identity.py`, `auth.py`, `access.py`, `authorize.py`, `idempotency.py`, `intake.py`, `resolve.py`, `reserve.py`, `settle.py`, `evidence.py`, `service.py`, `types.py`, `relay.py`, `connect.py`, `onboard.py`, `referrals.py`, `signup.py`, `__init__.py`, `admin.py`, `auth.py`, `auth_helpers.py`, `billing.py`, `call.py`, `catalog.py`, `connections.py`, `onboard.py`, `orgs.py`, `api_keys.py`, `resources.py`, `referrals.py`, `signup_cookies.py`, `web.py`, `access.py`, `api_keys.py`, `teams.py`, `access.py`, `budgets.py`, `publicdemo.py`, `usage.py`, `mcp_oauth.py`, `session.py`, `timeutil.py`, `store.py`, `email.py`, `runner.py`, `ratestore.py` |
| `interface/catalog-review-proposal.md` | `store.py`, `capabilities.yaml` |
| `interface/cli.md` | `cli.py`, `test_released_cli_compat.py`, `test_cli_key_compatibility.py`, `auth_helpers.py`, `cli_analytics.py`, `convert.py`, `agents.py`, `api_keys.py`, `test_api_keys.py` |
| `interface/dashboard.md` | `sitetrack.js`, `index.html`, `agent-setup.js`, `README.md`, `vue-3.5.41.global.prod.js`, `tutorial.js`, `tutorial.html`, `tour.js`, `index.html`, `api.py`, `web.py`, `session.py`, `api_keys.py`, `test_api_keys.py` |
+1
View File
@@ -137,6 +137,7 @@ treg tool add google-ads --base-url https://googleads.googleapis.com \
| `treg catalog get` | `ENDPOINT_ID` | docs, parameters, **the price**, and how you would be served |
| `treg call ENDPOINT_ID` | `--query K=V`, `--data STR` | call it |
| `treg call ENDPOINT_ID --await` | `--timeout N` (default 900) | a generation call (video/image): submit, poll the provider, print the final response |
| `treg host FILE` | `--content-type T`, `--json` | host a reference image/audio/video at a public URL a vendor can fetch (30 MB, 7-day TTL, free); prints the URL for `image_urls` / `audio_urls` |
| `treg catalog request` | `"what's missing"` | searched, not there? file it — requests steer what gets added next |
```bash
+2 -1
View File
@@ -30,6 +30,7 @@ covers (frontmatter `sources:`). Regenerate this index with
| [Local proxy — catch a program's own outgoing calls (`treg <command>`)](architecture/local-proxy.md) | shipped | localproxy.py, server.js |
| [Local CLI runs — run a vendor CLI as a dedicated user with a server-held credential (`treg run`)](architecture/local-run.md) | shipped | localrun.py, egress.py, fsjail.py |
| [MCP — the front door for assistants, and treg as an OAuth authorization server](architecture/mcp-oauth.md) | shipped | auth.py, mcp.py, health.py, mcp_oauth.py, … |
| [Media hosting - reference files a vendor can fetch (`treg host`)](architecture/media.md) | shipped | media.py, media.py, models.py, 0037_media_hosting.py, … |
| [Money — prepaid balance, the ledger, Stripe, and the reports that check it](architecture/money.md) | shipped | __init__.py, settlement.py, __init__.py, models.py, … |
| [Multi-tenancy — orgs, memberships, invites, per-org scoping](architecture/multi-tenancy.md) | shipped | models.py, api.py, caller_metadata.py, auth.py, … |
| [The proxy — faithful credential-injecting relay + tool resolution](architecture/proxy-model.md) | shipped | relay.py, ssrf.py, api.py, authorize.py, … |
@@ -40,7 +41,7 @@ covers (frontmatter `sources:`). Regenerate this index with
| Fragment | Status | Covers |
|---|---|---|
| [The API — the only brain (FastAPI)](interface/api.md) | shipped | sitetrack.js, api.py, bootstrap_handlers.py, bootstrap_http.py, … |
| [The API — the only brain (FastAPI)](interface/api.md) | shipped | media.py, sitetrack.js, api.py, bootstrap_handlers.py, … |
| [Catalog browse review — categories, platform placement, and domain sections](interface/catalog-review-proposal.md) | reference | store.py, capabilities.yaml |
| [The CLI (treg) + skill scaffolding](interface/cli.md) | shipped | cli.py, test_released_cli_compat.py, test_cli_key_compatibility.py, auth_helpers.py, … |
| [The web dashboard (Ledger, served from FastAPI)](interface/dashboard.md) | shipped | sitetrack.js, index.html, agent-setup.js, README.md, … |
+2 -1
View File
@@ -154,7 +154,8 @@ Arena statistics. Dataplane processes do not run this collector; `/arena/insight
Shutdown cancels and awaits every started background worker before draining Arena, audit and
analytics or closing the shared client, so database rollback/close finishes before event-loop teardown.
`POST /reviews` and `GET /admin/reviews` belong to control, alongside feedback intake and reads.
`POST /reviews` and `GET /admin/reviews` belong to control, alongside feedback intake and reads,
as do `POST /media` and the public `GET /m/{token}` that serves a hosted reference file.
The archive object-store lifespan normalizes configuration once, chooses an R2 factory or
injected in-memory context, and resets the store on exit. R2 validation runs before DB startup
+4
View File
@@ -98,6 +98,10 @@ See [signup eligibility](money.md#signup-credit-eligibility).
category/message/references, authenticated org and user attribution, and the references verified
against that team's call records or ledger. Revision `0025`; `domain.feedback` owns inserts;
`application.feedback` commits. Team deletion removes these rows. See [feedback](feedback.md).
- **`Media`** - a reference file a member hosted for a vendor to fetch (`treg host`): opaque
token, org, media type, size, the bytes, created and expiry. Revision `0037`;
`application.media` is the only writer and sweeps expired rows on each upload. Team deletion
removes these rows. See [media](media.md).
- **`FeedbackHandling` / `FeedbackHandlingEvent`** - internal current processing state and
versioned history (revision 0030), owned by this schema and written only by the private admin
service. Both cascade from the original report. See [feedback](feedback.md).
+56
View File
@@ -0,0 +1,56 @@
---
title: Media hosting - reference files a vendor can fetch (`treg host`)
status: shipped
sources:
- src/treg/application/media.py
- src/treg/routers/media.py
- src/treg/models.py
- src/treg/alembic/versions/0037_media_hosting.py
- tests/test_media.py
related:
- architecture/proxy-model.md
- interface/cli.md
- interface/api.md
---
# Media hosting
AIGC endpoints (reAPI, PiAPI, OpenRouter video) take reference images, voice clips and videos as
public URLs that the vendor's fetcher downloads. An agent on a laptop has no host, and every paste
host tried failed a vendor's pre-flight probe at least once: catbox unreachable from reAPI's
fal-backed probe, tmpfiles answering HTML on its download link, uguu timing out. Five submissions
to land one 18 s Seedance task was the cost on 2026-09-14. `treg host <file>` removes that class.
## Shape
- `POST /media` (member+, raw body, `Content-Type` names the media type) stores the bytes in the
`media` table and answers `{url, token, content_type, size, expires_at}`. The URL is
`<public_url>/m/<token>`.
- `GET /m/{token}` is unauthenticated on purpose: the vendor's fetcher holds no treg token. The
token is 192 random bits and the row names nothing about the org, so the URL is the only
capability. Served inline with the stored type, `Cache-Control: public, max-age=3600` and
`X-Robots-Tag: noindex`. No listing endpoint exists.
- Bounds, all in `application/media.py`: 30 MB per file (the vendors' own reference limit),
300 MB per org per rolling 24 h, 7-day TTL (reAPI's output URLs live about as long), and only
`image/*`, `audio/*`, `video/*`, with SVG denied (an image type that carries script). Sandbox
orgs are refused. The router refuses on `Content-Length` and again mid-stream at the cap, so an
oversized body never reaches worker RAM in full.
- Expiry is swept inline on every upload (`DELETE WHERE expires_at < now`) rather than by a cron;
an expired row also 404s on read before the sweep reaches it.
- Not metered. Hosting is a courtesy like polling, so money's five entries stay untouched and the
call runtime never touches this table.
## Why bytes in the database
The archive's object store is private and configured per deployment (`archive_body_write` is
`db` in production today). A row in Postgres serves a 30 MB reference correctly, keeps the feature
one table and one router, and needs no public bucket. Move bodies to the object store when the
media table's size, not its correctness, becomes the problem.
## Not built
- A treg-side pre-flight fetch of the URLs an agent passes to a vendor. It would model the
vendor's probe, which non-negotiable 4 forbids, and it cannot see host blocks that apply only to
the vendor's fetcher (catbox served us 200 while fal got nothing).
- An MCP `host` tool. MCP clients that need it can `POST /media` directly; add the tool when an
agent asks for it, and pin it in the MCP tool-set tests like the others.
+2 -1
View File
@@ -291,7 +291,8 @@ bearer path refuses it once expired rather than reviving an expired cookie.
transfer = promote another to owner, then step down), `leave_org` (`POST /orgs/{id}/leave`, self-removal,
same last-owner guard), `delete_org` (`DELETE /orgs/{id}`, owner-only, cascades every org-scoped row
through `cascade_delete_org` / `ORG_SCOPED_MODELS` in `domain/governance/teams.py` - including any
pending `AdConversion`: a queued conversion belongs to the team it would be attributed to).
pending `AdConversion`: a queued conversion belongs to the team it would be attributed to, and
`Media`: hosted reference files would otherwise outlive the team until their TTL).
**That list is the only one.** Owner delete, admin force-delete, the landing-sandbox reaper and the
demo reset all go through it; `test_org_delete_clears_EVERY_org_scoped_table` walks the models module
for anything carrying `org_id` and also refuses a reaper that keeps a private copy. The sandbox reaper
+3 -1
View File
@@ -179,7 +179,9 @@ cancellation cleanup, metering, audit, idempotency, and faithful relay.
`base_url + path`. **No path → the base URL itself, without a trailing slash** - a tool pinned to a
full resource (`.../v1/charges`) must relay as-is, since Stripe `404`s `/v1/charges/`.
Named misses also inspect the org's caller-usable own tools on the error path. When a dotted operation
A named miss whose `<tool>` is an exact catalog id with a path behind it (`reapi.tasks.get/tasks/1`)
is the own-tool shape applied to the catalog half: it answers `400` naming the endpoint's parameter
slots and the `--query` form, before any hint below runs. Named misses also inspect the org's caller-usable own tools on the error path. When a dotted operation
name shares its provider/first segment with one (for example `google-analytics.report` beside the
connected `google-analytics` tool), the 404 carries `hint` plus `did_you_mean` and points at
`/call/google-analytics/<path>`. If that dotted name is a real catalog endpoint, the hint follows the
+9
View File
@@ -2,6 +2,7 @@
title: The API — the only brain (FastAPI)
status: shipped
sources:
- src/treg/routers/media.py
- src/treg/web/sitetrack.js
- src/treg/api.py
- src/treg/bootstrap_handlers.py
@@ -86,6 +87,14 @@ and super-admin `GET /admin/feedback`. The intake's transaction belongs to `appl
the routes are in the control role. `routers.web.feedback_md` serves the compact instructions.
See [feedback](../architecture/feedback.md) for the contract and provenance boundaries.
## Media hosting
`POST /media` (member+, raw body, `Content-Type` = the media type) stores a reference file and
answers `{url, token, content_type, size, expires_at}`; `GET /m/{token}` serves it publicly, no
token, because the vendor's fetcher has none. 30 MB per file, 300 MB per org per 24 h, 7-day TTL,
`image/*` / `audio/*` / `video/*` only, refused in the sandbox. Refusals: 415 type, 413 size, 429
quota, 403 sandbox. Not metered. See [media](../architecture/media.md).
## Composition
`api.router` preserves public registration order while concern routers contribute ordered route blocks.
+9
View File
@@ -584,3 +584,12 @@ Existing unscoped tokens retain their old team-create behavior. Fresh email logi
with typed credentials require the updated CLI on the affected paths. This is a controlled upgrade
requirement, not full support for all fresh-login flows in old clients. The released-wheel test in
`test_released_cli_compat` checks that refusal preserves config bytes and the prior usable team.
## `treg host` - reference files for AIGC endpoints
`cmd_host` implements `treg host <file> [--content-type TYPE] [--json]`: one `POST /media` with the
file's bytes and a type guessed from the extension, printing the public URL alone on stdout (size
and expiry go to stderr) so `$(treg host face.jpg)` drops straight into a `--data` body. 30 MB per
file, 7-day TTL, image / audio / video only, free. See
[media](../architecture/media.md). `treg call <catalog-id> <path>` (the own-tool shape applied to a
catalog id) now answers 400 naming the endpoint's parameter slots instead of "no tool in this org".
+4
View File
@@ -200,6 +200,10 @@ How it works:
task id, a resumable `treg call …` command (Ctrl-C loses the wait, never the task or the money),
progress, and the result URL. Exit 0 = done, 2 = the provider failed the task, 3 = timed out
(resume with the printed command).
- **Reference media (a face image, a voice clip, a first frame) must be a public URL the vendor
can fetch.** Do not reach for a paste host: they fail vendor probes at random (catbox, tmpfiles,
uguu all did). `treg host face.jpg` prints a public URL (30 MB, 7 days, free) that drops straight
into `image_urls` / `audio_urls`: `--data "{\"image_urls\":[\"$(treg host face.jpg)\"], …}"`.
- **CLI agents: raise your shell tool's timeout or run the call in the background.** A video takes
1-5 minutes; a runtime's default 2-minute command limit cuts it off mid-wait.
- **MCP and raw-HTTP agents:** the response header `X-Treg-Async` is the descriptor - where to poll,
+4
View File
@@ -203,6 +203,10 @@ How it works:
task id, a resumable `treg call …` command (Ctrl-C loses the wait, never the task or the money),
progress, and the result URL. Exit 0 = done, 2 = the provider failed the task, 3 = timed out
(resume with the printed command).
- **Reference media (a face image, a voice clip, a first frame) must be a public URL the vendor
can fetch.** Do not reach for a paste host: they fail vendor probes at random (catbox, tmpfiles,
uguu all did). `treg host face.jpg` prints a public URL (30 MB, 7 days, free) that drops straight
into `image_urls` / `audio_urls`: `--data "{\"image_urls\":[\"$(treg host face.jpg)\"], …}"`.
- **CLI agents: raise your shell tool's timeout or run the call in the background.** A video takes
1-5 minutes; a runtime's default 2-minute command limit cuts it off mid-wait.
- **MCP and raw-HTTP agents:** the response header `X-Treg-Async` is the descriptor - where to poll,
+4
View File
@@ -184,6 +184,10 @@ How it works:
task id, a resumable `treg call …` command (Ctrl-C loses the wait, never the task or the money),
progress, and the result URL. Exit 0 = done, 2 = the provider failed the task, 3 = timed out
(resume with the printed command).
- **Reference media (a face image, a voice clip, a first frame) must be a public URL the vendor
can fetch.** Do not reach for a paste host: they fail vendor probes at random (catbox, tmpfiles,
uguu all did). `treg host face.jpg` prints a public URL (30 MB, 7 days, free) that drops straight
into `image_urls` / `audio_urls`: `--data "{\"image_urls\":[\"$(treg host face.jpg)\"], …}"`.
- **CLI agents: raise your shell tool's timeout or run the call in the background.** A video takes
1-5 minutes; a runtime's default 2-minute command limit cuts it off mid-wait.
- **MCP and raw-HTTP agents:** the response header `X-Treg-Async` is the descriptor - where to poll,
+4
View File
@@ -198,6 +198,10 @@ How it works:
task id, a resumable `treg call …` command (Ctrl-C loses the wait, never the task or the money),
progress, and the result URL. Exit 0 = done, 2 = the provider failed the task, 3 = timed out
(resume with the printed command).
- **Reference media (a face image, a voice clip, a first frame) must be a public URL the vendor
can fetch.** Do not reach for a paste host: they fail vendor probes at random (catbox, tmpfiles,
uguu all did). `treg host face.jpg` prints a public URL (30 MB, 7 days, free) that drops straight
into `image_urls` / `audio_urls`: `--data "{\"image_urls\":[\"$(treg host face.jpg)\"], …}"`.
- **CLI agents: raise your shell tool's timeout or run the call in the background.** A video takes
1-5 minutes; a runtime's default 2-minute command limit cuts it off mid-wait.
- **MCP and raw-HTTP agents:** the response header `X-Treg-Async` is the descriptor - where to poll,
+4
View File
@@ -182,6 +182,10 @@ How it works:
task id, a resumable `treg call …` command (Ctrl-C loses the wait, never the task or the money),
progress, and the result URL. Exit 0 = done, 2 = the provider failed the task, 3 = timed out
(resume with the printed command).
- **Reference media (a face image, a voice clip, a first frame) must be a public URL the vendor
can fetch.** Do not reach for a paste host: they fail vendor probes at random (catbox, tmpfiles,
uguu all did). `treg host face.jpg` prints a public URL (30 MB, 7 days, free) that drops straight
into `image_urls` / `audio_urls`: `--data "{\"image_urls\":[\"$(treg host face.jpg)\"], …}"`.
- **CLI agents: raise your shell tool's timeout or run the call in the background.** A video takes
1-5 minutes; a runtime's default 2-minute command limit cuts it off mid-wait.
- **MCP and raw-HTTP agents:** the response header `X-Treg-Async` is the descriptor - where to poll,
@@ -0,0 +1,42 @@
"""media: hosted reference files for vendors to fetch (`treg host`)
Revision ID: 0037
Revises: 0036
Create Date: 2026-09-14
Additive: one new table, nothing else touched.
"""
from collections.abc import Sequence
from alembic import op
import sqlalchemy as sa
import sqlmodel
revision: str = "0037"
down_revision: str | Sequence[str] | None = "0036"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
op.create_table(
"media",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("token", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("org_id", sa.Integer(), nullable=False),
sa.Column("content_type", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("size", sa.Integer(), nullable=False),
sa.Column("body", sa.LargeBinary(), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.Column("expires_at", sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(["org_id"], ["org.id"]),
sa.PrimaryKeyConstraint("id"),
)
op.create_index("ix_media_token", "media", ["token"], unique=True)
op.create_index("ix_media_expires_at", "media", ["expires_at"])
op.create_index("ix_media_org_created", "media", ["org_id", "created_at"])
def downgrade() -> None:
op.drop_table("media")
+2
View File
@@ -57,6 +57,7 @@ from .routers import call as call_routes
from .routers import catalog as catalog_routes
from .routers import connections as connection_routes
from .routers import feedback as feedback_routes
from .routers import media as media_routes
from .routers import onboard as onboard_routes
from .routers import orgs as org_routes
from .routers import referrals as referral_routes
@@ -301,6 +302,7 @@ async def create_tool_request(
router.routes.extend(feedback_routes.app.routes)
router.routes.extend(media_routes.app.routes)
router.routes.extend(auth_routes.social_router.routes)
router.routes.extend(auth_routes.cli_router.routes) # CLI pairing
router.routes.extend(auth_routes.session_router.routes)
+15
View File
@@ -135,6 +135,21 @@ async def _resolve_call(rest: str, caller: Caller, db: AsyncSession) -> Resolved
).scalar_one_or_none()
if tool is None:
cat = catalog_store.load()
# An EXACT catalog id followed by a URL path (`reapi.tasks.get/tasks/<id>`) is the own-tool
# shape applied to the catalog half: the caller had the right id and wrote the vendor's path
# by hand. "no tool in this org" sends them hunting in the wrong half of treg; name the
# endpoint's real parameter slots instead so the next call is the right one.
if path and (ep := cat.by_id.get(name)) is not None:
inp = ep.get("input") or {}
slots = sorted({k for sec in ("pathParams", "queryParams", "query", "body")
for k in (inp.get(sec) or {})})
raise ResolutionFailed(
"invalid_target", status_code=400, detail={
"error": f"{name!r} is a catalog endpoint and takes no URL path",
"hint": (f"pass parameters as --query K=V (path and query params) or --data "
f"'{{…}}' (body): treg call {name} --query <k>=<v>"
+ (f"; parameters: {', '.join(slots)}" if slots else "")),
"parameters": slots})
# A DOTTED name that reached here was meant to be a catalog endpoint id and missed — a
# near-miss id, most often one segment off. Answering "no tool 'lusha.companies-signals' in
# this org" describes the wrong half of treg and leaves the caller nothing to try; naming
+65
View File
@@ -0,0 +1,65 @@
"""Host a reference file so a vendor can fetch it: the one use case behind `treg host`.
AIGC endpoints (reAPI, PiAPI, OpenRouter video) take reference images, voice clips and videos as
public URLs their fetcher downloads. An agent on a laptop has no host, and every paste host we tried
failed a vendor probe at least once (catbox unreachable from fal, tmpfiles answering HTML, uguu
timing out). This keeps the bytes in treg and serves them from an opaque token.
Deliberately not metered: hosting is a courtesy like polling, so money's five entries stay untouched.
Bounded instead: a size cap per file, a daily byte quota per org, a TTL, and a media-only type list.
"""
from datetime import timedelta
import secrets
from sqlalchemy import delete, func, select
from ..infra.db import session_maker
from ..models import Media, _now
MAX_BYTES = 30 * 1024 * 1024 # matches the vendors' per-reference limits
DAILY_ORG_BYTES = 300 * 1024 * 1024 # ten maximal files a day; a runaway agent stops there
TTL = timedelta(days=7) # reAPI's own output URLs live about this long
ALLOWED_PREFIXES = ("image/", "audio/", "video/")
# SVG is an image type that carries script. Served inline on the treg.to origin it would run with a
# logged-in viewer's session, and no vendor takes an SVG reference anyway.
DENIED_TYPES = frozenset({"image/svg+xml", "image/svg"})
class MediaRejected(Exception):
def __init__(self, status_code: int, detail: str):
self.status_code, self.detail = status_code, detail
super().__init__(detail)
async def put(*, org_id: int, body: bytes, content_type: str) -> Media:
ctype = (content_type or "").split(";", 1)[0].strip().lower()
if not ctype.startswith(ALLOWED_PREFIXES) or ctype in DENIED_TYPES:
raise MediaRejected(415, f"only image/*, audio/* and video/* (not SVG) can be hosted, not {ctype or 'an untyped body'!r}")
if not body:
raise MediaRejected(400, "empty body")
if len(body) > MAX_BYTES:
raise MediaRejected(413, f"file is {len(body)} bytes; the cap is {MAX_BYTES}")
now = _now()
async with session_maker() as db:
# ponytail: expiry is swept inline on every upload rather than by a cron; a dedicated
# sweep only matters once uploads are rare and the table is not.
await db.execute(delete(Media).where(Media.expires_at < now))
# ponytail: read-then-insert, so two concurrent uploads can overshoot the quota by one file;
# a row lock per org is the upgrade if the courtesy ever gets abused.
used = (await db.execute(select(func.coalesce(func.sum(Media.size), 0)).where(
Media.org_id == org_id, Media.created_at >= now - timedelta(days=1)))).scalar_one()
if used + len(body) > DAILY_ORG_BYTES:
raise MediaRejected(429, f"this team has hosted {used} bytes in the last 24 h; the daily quota is {DAILY_ORG_BYTES}")
row = Media(token=secrets.token_urlsafe(24), org_id=org_id, content_type=ctype,
size=len(body), body=body, created_at=now, expires_at=now + TTL)
db.add(row)
await db.commit()
return row # every field the router answers with was set above; no re-read of the body
async def get(token: str) -> Media | None:
async with session_maker() as db:
row = (await db.execute(select(Media).where(Media.token == token))).scalar_one_or_none()
if row is None or row.expires_at < _now():
return None
return row
+2
View File
@@ -93,6 +93,8 @@ _CONTROL_ROUTE_KEYS: frozenset[RouteKey] = frozenset({
('/reviews', ('POST',), 'submit_review'),
('/admin/reviews', ('GET',), 'admin_reviews'),
('/feedback/{feedback_id}', ('GET',), 'get_feedback'),
('/media', ('POST',), 'host_media'),
('/m/{token}', ('GET',), 'serve_media'),
('/admin/feedback', ('GET',), 'admin_feedback'),
('/auth/github', ('GET',), 'auth_github'),
('/auth/github/callback', ('GET',), 'auth_github_callback'),
+36 -1
View File
@@ -5105,6 +5105,30 @@ def cmd_review(args, cfg) -> None:
print(json.dumps(receipt, indent=2))
def cmd_host(args, cfg) -> None:
"""`treg host <file>`: host a reference image / audio / video so a vendor can fetch it by URL.
AIGC endpoints take references as public URLs; paste hosts fail vendor probes at random, and an
agent on a laptop has nothing better. Prints the URL alone so it drops straight into --data."""
import mimetypes
p = Path(args.file).expanduser()
if not p.is_file():
sys.exit(f"treg host: file not found: {p}")
ctype = args.content_type or mimetypes.guess_type(p.name)[0] or ""
if not ctype:
sys.exit(f"treg host: cannot guess the media type of {p.name}; pass --content-type image/png (or audio/*, video/*)")
with _client(cfg) as c:
r = c.post("/media", content=p.read_bytes(), headers={"content-type": ctype})
if r.status_code >= 400:
_show(r)
sys.exit(1)
body = r.json()
if getattr(args, "json", False):
print(json.dumps(body, indent=2))
else:
print(body["url"])
print(f" {body['content_type']}, {body['size']} bytes, expires {body['expires_at']}", file=sys.stderr)
def cmd_feedback(args, cfg) -> None:
if args.message == "-" and sys.stdin.isatty():
_feedback_error("stdin_required", "Pipe sanitized text or redirect a file into stdin when using '-'.")
@@ -5508,6 +5532,7 @@ HELP_GROUPS: list[tuple[str, list[tuple[str, str]]]] = [
("THE CATALOG — tools you don't have a key for", [
("catalog", "Find a tool by what you want to DO. ~2,600 endpoints, each with its price."),
("call", "Call a tool: a catalog endpoint by id, or one of your own by URL."),
("host", "Host a reference image / audio / video at a public URL for a vendor to fetch."),
("balance", "Prepaid balance: credit left, calls in flight, recent spend."),
("topup", "Add funds, or set up automatic top-ups."),
("feedback", "Share a problem or suggestion about treg."),
@@ -5879,7 +5904,8 @@ def build_parser() -> argparse.ArgumentParser:
# ---- calling ----
cl = mk(sub, "call", "Call a tool through the proxy: `call <tool> <path>` or `call <full-url>`. Key injected server-side.",
"treg call stripe v1/charges", "treg call https://api.stripe.com/v1/charges",
"treg call posthog api/events --query limit=5", "treg call slack chat.postMessage --method POST --data '{\"channel\":\"C1\"}'")
"treg call posthog api/events --query limit=5", "treg call slack chat.postMessage --method POST --data '{\"channel\":\"C1\"}'",
"treg call reapi.tasks.get --query id=task_01a09ddf # a catalog id: path/query params go in --query, never in a path")
cl.add_argument("target", help="a tool name, or a full upstream URL")
cl.add_argument("path", nargs="?", default="", help="the path when using a tool name")
cl.add_argument("--method", default=None,
@@ -6144,6 +6170,15 @@ def build_parser() -> argparse.ArgumentParser:
review.add_argument("--reason", help="optional sanitized reason, 1-200 characters")
review.set_defaults(fn=cmd_review)
ho = mk(sub, "host", "Host a reference file (image / audio / video) at a public URL that a vendor can fetch: "
"the image_urls / audio_urls an AIGC endpoint takes. 30 MB per file, 7-day TTL, free.",
"treg host face.jpg", "treg host voice.mp3 --content-type audio/mpeg",
"treg call reapi.video-gen.seedance-2-5 --data \"{\\\"image_urls\\\":[\\\"$(treg host face.jpg)\\\"], …}\"")
ho.add_argument("file", help="the local file to host")
ho.add_argument("--content-type", dest="content_type", metavar="TYPE", help="override the type guessed from the extension")
ho.add_argument("--json", action="store_true", help="print the full response (url, token, size, expires_at)")
ho.set_defaults(fn=cmd_host)
fb = mk(sub, "feedback", "Submit or retrieve private team feedback.",
'treg feedback submit friction "The pagination example is unclear."',
'treg feedback submit quality "The result is outdated." --call-id CALL_ID --endpoint-id PROVIDER.ENDPOINT',
+2
View File
@@ -21,6 +21,7 @@ from ...models import (
DenyRule,
Feedback,
CallReview,
Media,
Hold,
IdempotentCall,
Invite,
@@ -174,6 +175,7 @@ ORG_SCOPED_MODELS = (
ToolRequest, # attribution rows go with the team; anonymous filings carry no org_id and stay
Feedback,
CallReview,
Media, # hosted reference files expire on their own; a deleted team's go now
AdConversion, # pending Google Ads conversions belong to the team they'd be attributed to
Membership, # last: it is what makes the caller a member of the org being deleted
)
+17
View File
@@ -1282,6 +1282,23 @@ class CallReview(SQLModel, table=True):
created_at: datetime = Field(default_factory=_now)
class Media(SQLModel, table=True):
"""A reference file a member hosted for a vendor to fetch (`treg host`): the image, voice clip
or video an AIGC endpoint takes as a public URL. Bytes live in the row, expire by TTL, and are
served by an opaque token that names no org or file. See docs/context/architecture/media.md.
"""
__table_args__ = (Index("ix_media_org_created", "org_id", "created_at"),)
id: int | None = Field(default=None, primary_key=True)
token: str = Field(unique=True, index=True)
org_id: int = Field(foreign_key="org.id")
content_type: str
size: int = Field(default=0)
body: bytes
created_at: datetime = Field(default_factory=_now)
expires_at: datetime = Field(index=True)
class ToolRequest(SQLModel, table=True):
"""A "the catalog doesn't have X" report — filed from the catalog page, the CLI, or by an
agent mid-search over MCP. Demand signal for which provider to key next; reviewed by querying
+51
View File
@@ -0,0 +1,51 @@
"""`POST /media` hosts a reference file for a vendor to fetch; `GET /m/{token}` serves it."""
from fastapi import APIRouter, Depends, HTTPException, Request, Response
from .. import sandbox as demo_sandbox
from ..application import media as media_app
from ..config import get_settings
from ..domain.identity.access import Caller, require_member
app = APIRouter()
@app.post("/media", status_code=201)
async def host_media(request: Request, caller: Caller = Depends(require_member)) -> dict:
"""Raw body in, public URL out. The Content-Type header names the media type; there is no
multipart wrapper because the CLI and an agent's HTTP client both send bytes more simply."""
if demo_sandbox.is_sandbox(caller.org):
raise HTTPException(status_code=403, detail="hosting is disabled in the sandbox")
# Refuse before buffering: no body-size middleware exists, so a declared or streamed body past
# the cap must never reach worker RAM in full.
declared = request.headers.get("content-length")
if declared and declared.isdigit() and int(declared) > media_app.MAX_BYTES:
raise HTTPException(status_code=413, detail=f"file is {declared} bytes; the cap is {media_app.MAX_BYTES}")
chunks, total = [], 0
async for chunk in request.stream():
total += len(chunk)
if total > media_app.MAX_BYTES:
raise HTTPException(status_code=413, detail=f"file exceeds the {media_app.MAX_BYTES}-byte cap")
chunks.append(chunk)
try:
row = await media_app.put(org_id=caller.org_id, body=b"".join(chunks),
content_type=request.headers.get("content-type", ""))
except media_app.MediaRejected as e:
raise HTTPException(status_code=e.status_code, detail=e.detail) from None
url = f"{get_settings().public_url.rstrip('/')}/m/{row.token}"
return {"url": url, "token": row.token, "content_type": row.content_type, "size": row.size,
"expires_at": row.expires_at.isoformat() + "Z"}
@app.get("/m/{token}", include_in_schema=False)
async def serve_media(token: str) -> Response:
"""Public by design: the vendor's fetcher holds no treg token. The token is 192 random bits
and the row names nothing about the org, so the URL is the only capability."""
row = await media_app.get(token)
if row is None:
raise HTTPException(status_code=404, detail="no such media, or it expired")
# `sandbox` CSP + nosniff: user bytes served on the treg.to origin must be inert even if a
# scriptable type ever slips past the allow-list. Vendors' fetchers ignore both headers.
return Response(content=row.body, media_type=row.content_type, headers={
"Content-Length": str(row.size), "Cache-Control": "public, max-age=3600",
"X-Robots-Tag": "noindex", "Content-Disposition": "inline",
"Content-Security-Policy": "sandbox", "X-Content-Type-Options": "nosniff"})
+2
View File
@@ -480,6 +480,8 @@ are both supported; you do not need to pre-arrange anything with us.
treg call <tool> <path> | treg call <full-url> proxy an HTTP call (named or agent-native)
treg call <endpoint-id> --await [--timeout N] a catalog call that is an async task (generation):
submit, poll, print the final response (default 900 s)
treg host <file> [--content-type T] host a reference image/audio/video at a public URL a
vendor can fetch (30 MB, 7 days, free) — for image_urls / audio_urls
treg calls [--limit N] the proxied-call audit log
treg audit [--limit N] [--calls | --runs] calls + CLI runs in one log; generation tasks carry
their state and result link
+4
View File
@@ -163,6 +163,10 @@ How it works:
task id, a resumable `treg call …` command (Ctrl-C loses the wait, never the task or the money),
progress, and the result URL. Exit 0 = done, 2 = the provider failed the task, 3 = timed out
(resume with the printed command).
- **Reference media (a face image, a voice clip, a first frame) must be a public URL the vendor
can fetch.** Do not reach for a paste host: they fail vendor probes at random (catbox, tmpfiles,
uguu all did). `treg host face.jpg` prints a public URL (30 MB, 7 days, free) that drops straight
into `image_urls` / `audio_urls`: `--data "{\"image_urls\":[\"$(treg host face.jpg)\"], …}"`.
- **CLI agents: raise your shell tool's timeout or run the call in the background.** A video takes
1-5 minutes; a runtime's default 2-minute command limit cuts it off mid-wait.
- **MCP and raw-HTTP agents:** the response header `X-Treg-Async` is the descriptor - where to poll,
+63
View File
@@ -6463,6 +6463,69 @@
"summary": "Accept My Invite"
}
},
"/media": {
"post": {
"description": "Raw body in, public URL out. The Content-Type header names the media type; there is no\nmultipart wrapper because the CLI and an agent's HTTP client both send bytes more simply.",
"operationId": "host_media_media_post",
"parameters": [
{
"in": "header",
"name": "x-treg-token",
"required": false,
"schema": {
"default": "",
"title": "X-Treg-Token",
"type": "string"
}
},
{
"in": "header",
"name": "x-treg-org",
"required": false,
"schema": {
"default": "",
"title": "X-Treg-Org",
"type": "string"
}
},
{
"in": "cookie",
"name": "treg_session",
"required": false,
"schema": {
"default": "",
"title": "Treg Session",
"type": "string"
}
}
],
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"additionalProperties": true,
"title": "Response Host Media Media Post",
"type": "object"
}
}
},
"description": "Successful Response"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
"description": "Validation Error"
}
},
"summary": "Host Media"
}
},
"/meta": {
"get": {
"description": "Open: what the dashboard needs to render correct, shareable snippets — the public proxy URL\n(so copy/paste snippets use the real domain, not whatever origin the browser happens to be on)\n— plus the bundle version, so an open tab can detect a new deploy and offer a refresh.\n\n`treg_version` and `app_version` answer DIFFERENT questions and both are worth having.\n`app_version` is a hash of index.html: it changes whenever the dashboard bundle does, which is\nwhat an open tab compares to offer a refresh. `treg_version` is the released package version,\nwhich is what a release check needs — after publishing 0.9.0 there was no way to confirm from the\nlive path which version was actually serving, only the commit id.",
+17
View File
@@ -302,6 +302,23 @@
"name": "admin_reviews",
"path": "/admin/reviews"
},
{
"kind": "APIRoute",
"methods": [
"POST"
],
"name": "host_media",
"path": "/media"
},
{
"kind": "APIRoute",
"methods": [
"GET",
"HEAD"
],
"name": "serve_media",
"path": "/m/{token}"
},
{
"kind": "APIRoute",
"methods": [
+1
View File
@@ -40,6 +40,7 @@ EXPECTED_MAKERS: dict[str, set[str]] = {
"application/arena_verification_insights.py": {API}, # explicit aggregate publication, no worker
"application/feedback.py": {API}, # synchronous intake; admin reads use get_admin_session
"application/media.py": {API}, # `treg host`: one short write, one short read, no upstream wait
"application/referrals.py": {API}, "application/signup.py": {API},
"application/onboard/__init__.py": {API},
+69
View File
@@ -0,0 +1,69 @@
"""`treg host`: reference-file hosting for AIGC endpoints (docs/context/architecture/media.md)."""
from datetime import timedelta
from httpx import AsyncClient
from sqlalchemy import update
from treg.application import media as media_app
from treg.infra.db import session_maker
from treg.models import Media, _now
PNG = b"\x89PNG\r\n\x1a\n" + b"\x00" * 64
async def test_host_then_fetch_roundtrip_is_byte_exact_and_public(clients: AsyncClient):
r = await clients.post("/media", content=PNG, headers={"content-type": "image/png"})
assert r.status_code == 201, r.text
body = r.json()
assert body["url"].endswith("/m/" + body["token"]) and body["size"] == len(PNG)
# The vendor's fetcher holds no treg token: the URL alone must serve the bytes.
anon = AsyncClient(transport=clients._transport, base_url="http://registry")
g = await anon.get(f"/m/{body['token']}")
assert g.status_code == 200 and g.content == PNG and g.headers["content-type"] == "image/png"
assert g.headers["content-security-policy"] == "sandbox" and g.headers["x-content-type-options"] == "nosniff"
async def test_hosting_needs_a_member_token(clients: AsyncClient):
anon = AsyncClient(transport=clients._transport, base_url="http://registry")
r = await anon.post("/media", content=PNG, headers={"content-type": "image/png"})
assert r.status_code == 401
async def test_only_media_types_are_hosted(clients: AsyncClient):
r = await clients.post("/media", content=b"<html>", headers={"content-type": "text/html"})
assert r.status_code == 415
r = await clients.post("/media", content=b"{}", headers={"content-type": "application/json"})
assert r.status_code == 415
# An SVG is an image that carries script; inline on our origin it would be stored XSS.
r = await clients.post("/media", content=b"<svg onload=alert(1)/>", headers={"content-type": "image/svg+xml"})
assert r.status_code == 415
async def test_the_per_file_cap_and_daily_quota_refuse_before_storing(clients: AsyncClient, monkeypatch):
monkeypatch.setattr(media_app, "MAX_BYTES", 100)
r = await clients.post("/media", content=b"x" * 101, headers={"content-type": "audio/mpeg"})
assert r.status_code == 413 # refused on Content-Length, before the body is read
async def chunked():
yield b"x" * 60; yield b"x" * 60
r = await clients.post("/media", content=chunked(), headers={"content-type": "audio/mpeg"})
assert r.status_code == 413 # chunked, no Content-Length: refused mid-stream at the cap
monkeypatch.setattr(media_app, "DAILY_ORG_BYTES", 150)
assert (await clients.post("/media", content=b"x" * 100, headers={"content-type": "audio/mpeg"})).status_code == 201
r = await clients.post("/media", content=b"x" * 100, headers={"content-type": "audio/mpeg"})
assert r.status_code == 429
async def test_expired_media_is_gone_and_swept_by_the_next_upload(clients: AsyncClient):
tok = (await clients.post("/media", content=PNG, headers={"content-type": "image/png"})).json()["token"]
async with session_maker() as db:
await db.execute(update(Media).where(Media.token == tok).values(expires_at=_now() - timedelta(seconds=1)))
await db.commit()
assert (await clients.get(f"/m/{tok}")).status_code == 404
await clients.post("/media", content=PNG, headers={"content-type": "image/png"})
assert await media_app.get(tok) is None
async with session_maker() as db:
assert (await db.execute(Media.__table__.select().where(Media.token == tok))).first() is None
async def test_unknown_token_is_404(clients: AsyncClient):
assert (await clients.get("/m/nope")).status_code == 404
+12
View File
@@ -199,3 +199,15 @@ async def test_a_credential_dead_end_names_a_sibling_treg_can_already_serve(
assert "callable now on treg's key" not in unkeyed
assert "needs your own scrapecreators credential" in unkeyed
A.get_settings.cache_clear()
async def test_a_catalog_id_with_a_url_path_names_the_parameter_slots(clients: AsyncClient):
"""`treg call reapi.tasks.get tasks/<id>` is the own-tool shape applied to a catalog id. The old
answer, "no tool 'reapi.tasks.get' in this org", described the wrong half of treg; the caller
had the right id and only needed to know where the parameter goes."""
r = await clients.get("/call/reapi.tasks.get/tasks/task_01")
assert r.status_code == 400
detail = r.json()["detail"]
assert "catalog endpoint" in detail["error"]
assert detail["parameters"] == ["id"]
assert "--query" in detail["hint"]