Audit the /api/v3 to /api/v1 rename (#211)

5c745a1d944a · AtlantisPleb · · parent add522b94349

Audit the /api/v3 to /api/v1 rename (#211)

Inventory every surface the rename touches in the Phoenix repo and the
monorepo CLI, pin the counts to 28aece5 and 022ebbd933, verify that gh
hardcodes /api/v3 for custom hosts while octokit needs only a base URL,
and recommend serving both paths for one deploy before deleting v3.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GoYpb8FEmdxVErsv7ABCYi
Co-Authored-By
Claude Fable 5 <noreply@anthropic.com>

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

pushed
by user · WAL seq 305 · 2026-08-24T20:24:27.022878Z

Changed files

  • added docs/2026-08-24-api-v1-rename-audit.md

Diff

1 file changed, +340 -0

docs/2026-08-24-api-v1-rename-audit.md added +340

@@ -0,0 +1,340 @@

1
# API rename audit: /api/v3 to /api/v1
2
3
Audit for issue #211. The API lives at `/api/v3` because it began by aiming at
4
parity with GitHub's v3 REST API, but the version in the path reads as this
5
API's own version, and this API is at its first version. This document records
6
every surface the rename touches, what breaks between the server rename and
7
the CLI release, and the recommended sequencing. Backward compatibility is not
8
required — there are no external users — but internal consumers that would
9
break mid-deploy are inventoried.
10
11
All counts are pinned to specific commits, because both repositories are under
12
concurrent edit:
13
14
- `openagents.com` at `28aece5` ("Refuse a backdated issuer key retirement
15
  instead of unverifying history").
16
- `openagents` monorepo at `022ebbd933`.
17
18
## Headline numbers
19
20
| Surface | Files | Hits |
21
| --- | --- | --- |
22
| Phoenix repo, total tracked | 134 | 1,534 |
23
| Phoenix `lib/` | 30 | 366 |
24
| Phoenix `test/` | 52 | 768 |
25
| Phoenix `docs/` | 34 | 210 |
26
| Phoenix `priv/` (site docs and contract) | 14 | 150 |
27
| Phoenix root (`AGENTS.md` 11, `INVARIANTS.md` 25, `README.md` 1) and `.agents/` (3) | 4 | 40 |
28
| Monorepo, total tracked | 26 | 118 |
29
| Monorepo `packages/openagents-cli` | 25 | ~115 |
30
| Monorepo elsewhere (`docs/teardowns/`) | 1 | 3 |
31
32
Every hit is the literal string `api/v3`. There is no other spelling of the
33
version in code: the only non-path occurrences in `lib/` are doc comments in
34
`ApiRouteAuthority` and the `"api_version" => "v3"` field in
35
`lib/openagents_web/controllers/api_extension_controller.ex:383`.
36
37
## Phoenix repo inventory
38
39
### Router
40
41
`lib/openagents_web/router.ex` mounts 23 `scope` blocks that begin with
42
`/api/v3` (21 hits; two scopes carry longer prefixes such as
43
`/api/v3/conversations/:conversation_id/boxes`). The unversioned scopes
44
(`/api`, `/api/operator`, `/api/contracts/...`, `/controller/...`) are outside
45
the rename.
46
47
### Route authority inventories
48
49
- `lib/openagents_web/api_route_authority.ex` (185 hits) is the single
50
  authority inventory for every `/api/v3` route: each entry is a literal
51
  `"METHOD /api/v3/..."` key carrying principal, family, and error contract.
52
  `test/openagents_web/api_route_authority_test.exs` proves the inventory and
53
  the router agree, so the rename must change the router and this table in the
54
  same commit or CI fails.
55
- `lib/openagents_web/route_authority.ex` (58 hits) classifies every Phoenix
56
  route including the API surface, with the same CI-enforced agreement
57
  (`test/openagents_web/route_authority_test.exs`, 49 hits).
58
59
### The GET /api/v3 root document and error envelope
60
61
- `lib/openagents_web/controllers/api_extension_controller.ex` (22 hits)
62
  publishes the root document: endpoint strings per family, extension
63
  descriptors, and the `"api_version" => "v3"` field at line 383, which
64
  becomes `"v1"`.
65
- `lib/openagents_web/api_error.ex` builds `documentation_url` from
66
  `Endpoint.url() <> "/api/v3"` at line 194, so every refusal envelope links
67
  to the root document. One line, but it appears in every error response.
68
69
### Links generated into responses
70
71
JSON views interpolate the path into `url` fields the API returns:
72
73
- `lib/openagents_web/controllers/issue_json.ex` (issue and pull URLs)
74
- `lib/openagents_web/controllers/stack_json.ex` (stack, pull, and
75
  merge-async operation URLs)
76
- `lib/openagents_web/controllers/pull_request_json.ex`
77
- `lib/openagents_web/controllers/label_json.ex`
78
- `lib/openagents_web/controllers/milestone_json.ex`
79
- `lib/openagents_web/controllers/project_json.ex`
80
- `lib/openagents_web/controllers/fleet_target_controller.ex`
81
  (`~p"/api/v3/admin/forge/targets/..."`)
82
83
Because these are interpolated at render time, they emit the new path the
84
moment the code deploys. Nothing stores an `/api/v3` URL in the database.
85
86
### Push receipts
87
88
`lib/openagents/forge/git_http.ex:477` appends the WAL receipt to the
89
side-band output that `git push` prints:
90
91
```
92
openagents wal-receipt seq=N link=HASH (GET /api/v3/repos/OWNER/REPO/pushes/N)
93
```
94
95
The `link` value is a WAL chain hash, not a URL, so only the parenthesized
96
`GET` path changes — one line. Receipts already printed into old terminal
97
scrollback and transcripts will name a path that stops existing after the
98
alias is removed; that is acceptable, since the durable identity of a receipt
99
is `seq` plus `link`, not the hint path.
100
101
### Contracts and manifests
102
103
- `priv/api-contracts/repositories-v1.json` (15 hits) names routes as
104
  `"METHOD /api/v3/..."` strings. It is served at
105
  `GET /api/contracts/repositories-v1.json` — the serving route itself is
106
  unversioned and does not change.
107
- The same file is vendored in the CLI at
108
  `packages/openagents-cli/contracts/repositories-v1.json`, and
109
  `packages/openagents-cli/src/api-contract.ts` pins its SHA-256
110
  (`REPOSITORY_CONTRACT_SHA256`), enforced by
111
  `test/api-contract.test.ts`. Renaming the paths inside the contract
112
  changes the bytes, so both copies and the pinned hash must move together.
113
- There is no OpenAPI document; the extension document at the API root and
114
  the contribution contract are the machine-readable manifests.
115
116
### The agent front door
117
118
`lib/openagents_web/contribution_contract.ex` (28 hits) generates the
119
standing instructions served at `openagents.com/agents.md` and its JSON
120
twin, listing `/api/v3` entry points. The rename flows through automatically
121
once this module changes; agents that cached the old page re-read it on
122
their next visit.
123
124
### UI and LiveView
125
126
Two user-facing occurrences:
127
128
- `lib/openagents_web/live/home_live.ex:544` — FAQ copy: "served under
129
  `/api/v3`. An existing client usually needs only a base URL change."
130
- `lib/openagents_web/live/thread_index_live.ex:51` — empty-state hint
131
  naming `POST /api/v3/threads`.
132
133
Both are copy changes and need explicit sign-off under the repo's
134
copy-change rule.
135
136
### Data-rights export inventory
137
138
`lib/openagents/data_rights/export_inventory.ex` (13 hits) names the
139
`/api/v3` route for each export mechanism, pinned by
140
`test/openagents/data_rights/export_inventory_test.exs` (11 hits).
141
142
### Site docs, repo docs, and skills
143
144
- `priv/docs/` (150 hits, 14 files) is the documentation served at `/docs`;
145
  `rest-api.md` alone carries 88 hits, `stacks-api.md` 20.
146
- `docs/` (210 hits, 34 files) — design docs, audits, runbooks. Historical
147
  audits can keep their `/api/v3` text with a dated note; live references
148
  (for example `docs/api-authentication.md`,
149
  `docs/github-api-issues-projects-assessment.md`) should be updated.
150
- `.agents/skills/openagents-work-management/SKILL.md` (3 hits) instructs
151
  agents that relative `openagents api` paths resolve under `/api/v3/`.
152
- `AGENTS.md` (11 hits, `CLAUDE.md` is a symlink to it), `INVARIANTS.md`
153
  (25 hits), `README.md` (1 hit).
154
155
### Tests
156
157
52 test files, 768 hits, all literal request paths
158
(`~p"/api/v3/..."` and string paths). Largest:
159
`project_controller_test.exs` (114), `issue_controller_test.exs` (74),
160
`thread_controller_test.exs` (73), `forum_api_controller_test.exs` (55),
161
`route_authority_test.exs` (49). All mechanical.
162
163
## Monorepo inventory
164
165
All 26 files with hits sit in `packages/openagents-cli` except one teardown
166
doc (`docs/teardowns/2026-08-23-openagents-coder-tui-agent-fleet-port-plan.md`,
167
historical). No other package or app in the monorepo calls the `/api/v3`
168
surface.
169
170
### The path is not one constant
171
172
`src/endpoint.ts` resolves only the **origin** (profile or `--api-url`); it
173
rejects any URL that carries a path. The version segment is scattered:
174
175
- `src/api-passthrough.ts:10` — `API_BASE_PATH = "/api/v3/"`, the resolver
176
  for `openagents api` relative paths.
177
- `src/coder-thread.ts:69`, `src/coder-resume.ts:42`,
178
  `src/coder-transcript.ts:22` — three separate
179
  `THREADS_PATH = "/api/v3/threads"` constants for the coder lane, resume
180
  picker, and transcript writer.
181
- `src/tracker-request.ts:103` — the `/api/v3/repos/{owner}/{repo}` base
182
  that `issue-client.ts` and `project-client.ts` build on.
183
- `src/repository-client.ts` (12 literals), `src/forum-client.ts` (7),
184
  `src/device-client.ts` (2, the device-authorization pairing flow).
185
- `src/coder-backends.ts`, `src/coder-skills.ts`, `src/cli.ts` — comments
186
  and one skill-text line.
187
188
### Shipped artifacts
189
190
The published npm package (`@openagentsinc/cli`, `files: ["dist",
191
"contracts", "README.md", "skills"]`) ships:
192
193
- `contracts/repositories-v1.json` with the `/api/v3` route strings and the
194
  SHA-256 pin in `src/api-contract.ts` (see the contract section above).
195
- `skills/openagents-cli/SKILL.MD` telling agents that bare `openagents api`
196
  paths resolve under `/api/v3/`.
197
198
The published version on npm is `0.3.5`, identical to the repo `HEAD`
199
version, so **every installed CLI in existence hardcodes `/api/v3`**.
200
201
### Surfaces that do not change
202
203
- The git credential helper (`src/git-credential-helper.ts`) matches on
204
  origin only, and the helper line written into git config is
205
  `!openagents --api-url ORIGIN auth git-credential` — no path. It keeps
206
  working across the rename as long as the installed binary is current;
207
  the helper itself makes no `/api/v3` request.
208
- Machine pairing (`src/computer-client.ts`) calls `/controller/pairings`
209
  and `/controller/status`, which are outside the versioned scope.
210
- The Phoenix contract route `GET /api/contracts/repositories-v1.json` is
211
  unversioned; only the strings inside the body change.
212
213
## The GitHub-compatibility question
214
215
**Verdict: octokit depends only on response shape plus an explicit base URL;
216
`gh` depends on the literal `/api/v3` path segment for any non-github.com
217
host.**
218
219
Evidence:
220
221
- The installed `gh` binary (v2.89.0, built on `go-gh` v2.13.0) contains the
222
  hardcoded format string `https://%s/api/v3/` alongside
223
  `https://%s/api/graphql`. This is `go-gh`'s REST prefix: when `GH_HOST`
224
  names any host other than github.com, `gh` constructs REST URLs as
225
  `https://HOST/api/v3/...` with no way to override the path segment. A
226
  forge that wants `GH_HOST=openagents.com gh ...` to work must serve
227
  `/api/v3` at that literal path.
228
- Octokit takes a full `baseUrl` (for GitHub Enterprise Server the
229
  documented value is `https://HOST/api/v3`); the version segment is part
230
  of caller configuration, not the client. Only the response shape and
231
  headers matter.
232
- The repo's own stated posture already stops short of drop-in tooling:
233
  `docs/github-api-issues-projects-assessment.md:390` and
234
  `priv/docs/rest-api.md:346` say pagination and link headers "do not
235
  provide complete Octokit or `gh` parity", and the homepage FAQ promises
236
  only "a base URL change" — the octokit story, not the `gh` story.
237
- One doc overstates it: `docs/episode-triage.md:1207` says the shape means
238
  "`gh` and octokit work unchanged". After the rename that stays true for
239
  octokit and becomes false for `gh` against a custom host, unless a
240
  permanent `/api/v3` alias is kept.
241
242
The rename therefore forfeits hypothetical `GH_HOST`-style `gh` drop-in
243
compatibility. Nothing in either repo currently exercises `gh` against
244
openagents.com, so this is a posture decision to record, not a breakage.
245
246
## Mid-deploy breakage analysis
247
248
Ordered by severity, for a hard cutover with no alias:
249
250
1. **Every installed CLI breaks.** Published `0.3.5` equals repo `HEAD`;
251
   all issue, project, repo, forum, `api`, coder, resume, and device-pairing
252
   commands 404 until the user updates to a release that targets `/api/v1`.
253
   The server cannot be renamed before that release exists, and the release
254
   cannot ship before the server serves `/api/v1` — a deadlock unless one
255
   deploy serves both.
256
2. **In-flight coder sessions die.** A running `openagents coder` session
257
   holds a thread grant and appends events to
258
   `POST /api/v3/threads/{id}/events`; long sessions span deploys. A hard
259
   cutover mid-session breaks transcript writes and the exit-time
260
   `DELETE /api/v3/threads/{id}` revocation.
261
3. **Agent instructions go stale.** The served `agents.md`, the skill inside
262
   the npm package, and `.agents/skills/openagents-work-management` all name
263
   `/api/v3`. Agents following cached instructions 404 until they re-read.
264
4. **Push receipt hint paths.** New pushes print the new path immediately
265
   (rendered, not stored). Old receipts in scrollback and transcripts point
266
   at a dead path once v3 stops answering; the WAL `seq`/`link` identity is
267
   unaffected.
268
5. **Not affected:** the git credential helper (origin-only), machine
269
   pairing (`/controller/*`), the contract-serving route
270
   (`/api/contracts/...`), and the database (no stored `/api/v3` URLs).
271
272
## Recommended sequencing
273
274
Serve both paths for one deploy, as a pure alias, then delete it. The alias
275
costs roughly 20 lines and one deploy of patience; a hard cutover saves those
276
lines but breaks every installed CLI and every in-flight coder thread during
277
the window between the server deploy and the npm publish. Given the deadlock
278
in breakage item 1, the alias is the cheapest ordering that never leaves a
279
consumer without a working path.
280
281
1. **Phoenix, one commit: rename to `/api/v1` and alias `/api/v3`.**
282
   Mechanical rename across router, `ApiRouteAuthority`, `RouteAuthority`,
283
   controllers, JSON views, `ApiError`, `ContributionContract`, the
284
   extension document (`api_version: "v1"`), the push-receipt line, the
285
   export inventory, `priv/api-contracts/repositories-v1.json`, `priv/docs`,
286
   tests, `AGENTS.md`, `INVARIANTS.md`, and `.agents/skills`. Add one plug
287
   ahead of the router that rewrites `path_info` `["api", "v3" | rest]` to
288
   `["api", "v1" | rest]` — a transparent rewrite, not a redirect, because
289
   old clients POST and do not follow redirects reliably. The router stays
290
   single-sourced at `/api/v1`, so the authority tests keep proving the real
291
   surface; one test pins the alias. All generated links and receipts emit
292
   `/api/v1` from this deploy on.
293
2. **Monorepo: point the CLI at `/api/v1`, publish `0.4.0`.** Update the
294
   scattered literals (or, better, route them through one exported
295
   constant), regenerate `contracts/repositories-v1.json` and
296
   `REPOSITORY_CONTRACT_SHA256`, update the shipped skill and README, bump,
297
   publish. Old `0.3.5` installs keep working through the alias; new
298
   installs use v1 natively.
299
3. **Update instructions and docs in both repos** (served docs are part of
300
   step 1's commit; monorepo docs ride step 2). Decide the `gh` posture
301
   explicitly: correct `docs/episode-triage.md`'s "gh works unchanged"
302
   claim, or commit to a permanent `/api/v3` alias for `GH_HOST` drop-in.
303
4. **Delete the alias** once the fleet's installed CLIs are `>= 0.4.0` —
304
   remove the plug and its test. If step 3 chose to keep `gh` drop-in,
305
   skip this step and record the alias as permanent in `INVARIANTS.md`.
306
307
## Estimated diff size
308
309
- **Phoenix**: about 1,530 single-line textual changes across ~134 files —
310
  a mechanical `sed` plus `mix precommit`, with three files needing thought:
311
  the router (23 scopes plus the alias plug), `api_extension_controller.ex`
312
  (`api_version` field), and the two LiveView copy lines (copy sign-off).
313
  Net new code: the rewrite plug (~20 lines) and one alias test.
314
- **Monorepo**: about 115 single-line changes across 25 files in
315
  `packages/openagents-cli`, plus the contract SHA recompute, a version
316
  bump, and an npm publish. Optional consolidation of the path constants
317
  adds a small refactor.
318
319
## Follow-up issues
320
321
1. **Rename the API surface to `/api/v1` and alias `/api/v3`** — Phoenix
322
   serves the whole surface at `/api/v1`, emits v1 links and receipts, and
323
   transparently rewrites `/api/v3` requests, all in one deploy.
324
2. **Ship CLI `0.4.0` targeting `/api/v1`** — every hardcoded path, the
325
   vendored contract and its SHA pin, and the shipped skill name v1;
326
   published to npm.
327
3. **Consolidate the CLI's API base path into one constant** — the coder
328
   thread, resume, transcript, tracker, repository, forum, and device
329
   clients all import a single versioned base, so the next rename is one
330
   line.
331
4. **Decide and record the `gh` compatibility posture** — either correct
332
   the "gh works unchanged" claim in `docs/episode-triage.md` and the
333
   compat framing, or make the `/api/v3` alias permanent for `GH_HOST`
334
   drop-in; record the choice in `INVARIANTS.md`.
335
5. **Delete the `/api/v3` alias** — after confirming installed CLIs are
336
   `>= 0.4.0`, remove the rewrite plug and its test (skipped if issue 4
337
   keeps the alias).
338
6. **Sweep remaining `/api/v3` references in docs and skills** — update
339
   live docs in both repos and annotate historical audits, so no standing
340
   instruction names the dead path.

This page updates live while a promote is in flight · changelog