Unify issue-family error envelopes and publish the route inventory #82

Closed AtlantisPleb opened this 23h ago 1 comment

Outcome

Give every issue-family API route a stable error envelope and publish a complete route inventory derived from the Phoenix router.

Current behavior

The API routes are reachable through openagents api, but validation and not-found shapes vary by controller, and the published contract can omit a route without failing CI.

Source: docs/2026-08-21-issues-projects-work-system-assessment.md, tracks A4 and A6.

Contract

  • Define stable error codes, messages, field details, request IDs, and status mappings.
  • Preserve deliberate 404 non-disclosure for private resources.
  • Migrate issue, comment, label, assignee, milestone, project, field, and item controllers.
  • Derive the method and path inventory from the router.
  • Fail CI when the published inventory omits or misclassifies a route.
  • Keep response schemas explicit and versioned; do not infer them from runtime examples.
  • Give CLI clients enough structured detail for distinct exit results.

Acceptance criteria

  • Equivalent errors have the same envelope across the issue family.
  • Private missing and unauthorized resources remain indistinguishable where policy requires it.
  • The route inventory matches the router on every test run.
  • Adding a route without contract classification fails CI.
  • Existing measured clients remain compatible or receive a documented migration.

Verification

Add golden contract tests, controller matrix tests, privacy tests, route-diff tests, and packaged CLI error checks. Run mix precommit.

Dependencies

Build on #7 and #9. Coordinate the CLI issue in OpenAgentsInc/openagents#13.

  1. AtlantisPleb opened this issue 23h ago
  2. A AtlantisPleb Author 6h ago

    Shipped in 2cd0ed1 on main.

    The envelope

    Every refusal from an issue-family /api/v3 route now carries the same six keys, built by the new OpenAgentsWeb.ApiError:

    {
      "message": "Validation Failed",
      "code": "validation_failed",
      "status": 422,
      "documentation_url": "https://openagents.com/api/v3",
      "request_id": "GM5_fLaSSJDluDMAACUh",
      "errors": { "state": ["must be one of: open, closed, all"] }
    }
    

    errors is always present and is {} when the failure is not field-level, so a client parses one shape. A code determines its status inside ApiError, so a controller never picks a status for a failure the API has already named:

    Code Status
    unauthenticated 401
    forbidden 403
    agent_participation_forbidden 403
    not_found 404
    label_not_on_issue 404
    dependency_not_found 404
    validation_failed 422
    delete_failed 422

    What was inconsistent

    Six mutually incompatible bodies for the same class of failure: {"message": "Not Found"} for a missing resource, {"errors": {...}} for a rejected field, {"error": "forbidden"} for a refused person, {"error": {"code": ...}} for a refused agent, {"error": "invalid_api_token"} for a refused token, and the repository family's code/message/request_id triple. 403 alone had three renderings inside IssueController, CommentController, and ProjectController. Reading one refusal taught a client nothing about the next.

    Backward compatibility

    Nothing was renamed or removed. message keeps its GitHub meaning and a missing resource still reads exactly Not Found. The errors map keeps the field-to-messages shape local clients read. The two participation refusals and the three bearer-token pipelines keep their error key beside the envelope. priv/api-contracts/repositories-v1.json is untouched — the envelope is a superset of the three keys it describes, so its claim stays true and the CLI's vendored hash stays valid.

    Non-disclosure

    Preserved by construction, not by care: a private resource and an absent one both refuse with not_found, and no code distinguishes them. ApiErrorContractTest asserts the two bodies are byte-identical apart from request_id.

    Route inventory

    GET /api/v3 now publishes the inventory beside the extension index from #100, rather than adding a second discovery surface. Each entry is derived from Router.__routes__/0 through ApiRouteAuthority:

    {
      "method": "GET",
      "path": "/api/v3/repos/{owner}/{repo}/issues/{issue_number}",
      "authority": "optional_bearer",
      "family": "issue",
      "errors": "envelope",
      "mutation": false
    }
    

    The document also carries version, families, and an errors section publishing the envelope keys and the stable code table. Inventory entries went from one classification to three — principal, resource family, error contract — all mandatory.

    What fails CI

    • ApiRouteAuthorityTest — the router and the inventory disagree in either direction, or a classification does not match what the pipeline does to an anonymous request.
    • ApiExtensionControllerTest — the published document omits a live route, or a route is missing an authority, family, or error classification.
    • ApiErrorContractTest — dispatches every route classified envelope and fails on any body that is not the envelope. Verified by reverting one controller to %{message: "Not Found"}: GET /api/v3/repos/:owner/:repo/labels did not answer with the API error envelope. Keys: ["message"].

    ConnCase.assert_api_error/4 and api_error_code/2 replace the exact-equality assertions that previously pinned the old bespoke shapes, so a regression fails at the contract rather than at whichever test happened to name the shape it broke.

    Adoption

    #80's project lifecycle and field validation routes are already converged — they route through ProjectController's unprocessable/2, unprocessable_changeset/2, forbidden/1, and not_found/1, all of which now delegate to ApiError. #81 adopts it by calling ApiError.validation_failed(conn, %{field => [message]}) or ApiError.changeset(conn, changeset); a new code goes in the @codes table in lib/openagents_web/api_error.ex with the one status it always carries.

    Verification

    POOL_SIZE=8 MIX_TEST_PARTITION=lane82 mix precommit passes: 3175 tests, 0 failures, no warnings.

    Contract recorded as FORGEAPI-001 in INVARIANTS.md, with the envelope and route inventory documented in docs/github-api-issues-projects-assessment.md and docs/openagents-cli/api.md.

  3. closed this as completed 6h ago
Sign in with GitHub to comment on this issue.