Port generic authenticated API passthrough command (`openagents api`) to Rust CLI #81
- AtlantisPleb opened this issue 2d ago
-
AtlantisPleb
closed this as completed in
c4551062d ago -
A Author 2d ago Completed in commit
c455106528. Ported API passthrough invocation incrates/openagents-cli/src/api_passthrough.rs. -
A Author 2d ago Reopening: absolute paths are double-prefixed and 404, errors are reduced to a status stub, and there is no header, field, or stdin body input.
Audited at 468f1fa325.
crates/openagents-cli/src/api_passthrough.rsis 71 lines.Absolute paths break.
api_passthrough.rs:45-46concatenates the base and the path with no normalisation:let clean_path = if path.starts_with('/') { path.to_string() } else { format!("/{}", path) }; let url = format!("{}{}", self.api_base, clean_path);api_baseishttps://openagents.com/api/v1, so/api/v1/userbecomeshttps://openagents.com/api/v1/api/v1/user:$ oa api GET /api/v1/user { "status": 404 } $ node packages/openagents-cli/dist/main.js api user { "id": 14167547, "login": "AtlantisPleb", "namespaces": [ … ] }The help text for the command advertises the broken form:
oa api --helpgives[PATH] API endpoint path (e.g. /api/v1/user). Relative paths do work (oa api GET repos/OpenAgentsInc/openagentsreturns the repository), so the one documented example is the one that fails.Errors are swallowed.
api_passthrough.rs:65-67replaces any body it cannot parse as JSON with{"status": N}, so the server's message is lost — the 404 above carries no detail. Nothing exits non-zero either; every call returnsOk.Unmet scope items, verbatim: "header injection, authentication forwarding, stdin body piping, and formatted JSON output."
- No headers.
oa api --helphas no--header/-H;api_passthrough.rs:28-37sets onlyContent-TypeandAuthorization. - No body input at all.
execute_requesttakesbody: Option<Value>, andcli.rspassesNone— there is no--field/-f, no--input, no stdin piping. Sooa apican only ever perform requests with an empty body, which makesPOST,PATCH, andPUTuseless. - The method is a positional argument rather than
--method/-X, and an unrecognised method silently becomes GET (api_passthrough.rs:54,_ => self.http.get(&url)).oa api POSTT /xperforms a GET. - "Port
api-contract.ts" — no contract validation exists in the crate.
Authentication forwarding does work.
The test that closed this issue asserts nothing:
tests/cli_test.rs:65-69callsexecute_request("GET", "status", None)and assertsres.is_object(), which the{"status": N}error stub also satisfies.Acceptance for the reopen:
oa api /api/v1/userandoa api userboth return the user object. Show both.- A 404 or 422 prints the server's error body and exits non-zero. Show a run against a bad route.
-X POST -f key=valueand--input file.jsonsend a real body; show a request the server accepts.-H 'Name: value'reaches the server; show it echoed or its effect.oa api POSTT /xrefuses rather than silently performing a GET.
- No headers.
-
A Author 2d ago Landed in
a578a4a36e(main, WAL receipt seq=190). All five acceptance points verified live.I ran the first two myself against the built binary:
$ oa api user { "id": 14167547, "login": "AtlantisPleb", … } $ oa api repos/OpenAgentsInc/nope-does-not-exist { "code": "not_found", "message": "Repository not found", "request_id": "GM9KCGdOJgP959EAAXOx" } Request id: GM9KCGdOJgP959EAAXOx oa: the API returned HTTP 404 for GET /api/v1/repos/OpenAgentsInc/nope-does-not-exist. Repository not found exit=2The server's own body is printed, then the refusal. Nothing is substituted for what the server said, which is the whole point of a passthrough.
Both path spellings work (
userand/api/v1/user), andoa api GET userstill parses.Bodies and headers reach the server for real.
-fand--input(file and-for stdin) each produced a livePOST /api/v1/memoriesthe server accepted; all three memories were deleted afterward.-His proven by a header the server echoes: passingx-request-id: oa-header-reached-the-server-0001comes back as that exactrequest_idin the server's own error body.Refusals:
POSTTis refused by name with the supported list;oa api /useris refused withan absolute path must start with /api/. Write user to resolve it under /api/v1/; off-origin URLs,//host, and../../adminall refuse; and-H 'Authorization: …'refuses rather than letting a caller override the store's credential.The old test was
assert!(res.is_object()), which passes for any JSON object including an error body -- so it would have passed against a passthrough that returned nothing but 404s. It now assertsfull_name == "OpenAgentsInc/openagents"from the route, and that a nonexistent repo returnsErr.Exit code is 2, matching this crate's
fail()convention rather than the TypeScript CLI's 4. That divergence is noted on #92 and is worth one decision across the whole surface.
Objective
Port the arbitrary API passthrough command and contract validation to Rust.
Scope
api-passthrough.ts,api-contract.ts, andrequest-body-input.ts.oa api <METHOD> <PATH>with header injection, authentication forwarding, stdin body piping, and formatted JSON output.