Port generic authenticated API passthrough command (`openagents api`) to Rust CLI #81

Closed AtlantisPleb opened this 2d ago 3 comments

Objective

Port the arbitrary API passthrough command and contract validation to Rust.

Scope

  • Port api-passthrough.ts, api-contract.ts, and request-body-input.ts.
  • Implement oa api <METHOD> <PATH> with header injection, authentication forwarding, stdin body piping, and formatted JSON output.
  1. AtlantisPleb opened this issue 2d ago
  2. AtlantisPleb closed this as completed in c455106 2d ago
  3. A AtlantisPleb Author 2d ago

    Completed in commit c455106528. Ported API passthrough invocation in crates/openagents-cli/src/api_passthrough.rs.

  4. A AtlantisPleb 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.rs is 71 lines.

    Absolute paths break. api_passthrough.rs:45-46 concatenates 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_base is https://openagents.com/api/v1, so /api/v1/user becomes https://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 --help gives [PATH] API endpoint path (e.g. /api/v1/user). Relative paths do work (oa api GET repos/OpenAgentsInc/openagents returns the repository), so the one documented example is the one that fails.

    Errors are swallowed. api_passthrough.rs:65-67 replaces 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 returns Ok.

    Unmet scope items, verbatim: "header injection, authentication forwarding, stdin body piping, and formatted JSON output."

    • No headers. oa api --help has no --header/-H; api_passthrough.rs:28-37 sets only Content-Type and Authorization.
    • No body input at all. execute_request takes body: Option<Value>, and cli.rs passes None — there is no --field/-f, no --input, no stdin piping. So oa api can only ever perform requests with an empty body, which makes POST, PATCH, and PUT useless.
    • 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 /x performs 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-69 calls execute_request("GET", "status", None) and asserts res.is_object(), which the {"status": N} error stub also satisfies.

    Acceptance for the reopen:

    1. oa api /api/v1/user and oa api user both return the user object. Show both.
    2. A 404 or 422 prints the server's error body and exits non-zero. Show a run against a bad route.
    3. -X POST -f key=value and --input file.json send a real body; show a request the server accepts.
    4. -H 'Name: value' reaches the server; show it echoed or its effect.
    5. oa api POSTT /x refuses rather than silently performing a GET.
  5. A AtlantisPleb 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=2
    

    The 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 (user and /api/v1/user), and oa api GET user still parses.

    Bodies and headers reach the server for real. -f and --input (file and - for stdin) each produced a live POST /api/v1/memories the server accepted; all three memories were deleted afterward. -H is proven by a header the server echoes: passing x-request-id: oa-header-reached-the-server-0001 comes back as that exact request_id in the server's own error body.

    Refusals: POSTT is refused by name with the supported list; oa api /user is refused with an absolute path must start with /api/. Write user to resolve it under /api/v1/; off-origin URLs, //host, and ../../admin all 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 asserts full_name == "OpenAgentsInc/openagents" from the route, and that a nonexistent repo returns Err.

    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.

Sign in with GitHub to comment on this issue.