You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs: fix timeoutMs migration risk direction and HTTP/MCP drift
- docs/CLI.md: the timeout_seconds -> timeoutMs migration note had the
failure mode backwards. The schema is .strict(), so an unmigrated
caller sending the old key gets a loud BAD_REQUEST, not a silent 1000x
wait. The real, narrower hazard is a caller that renames the field but
keeps a seconds-valued number: that times out ~1000x sooner
(QUEUE_TIMEOUT, exit 10), not longer. Verified against
src/contract/operations.ts's leaseRequestInputSchema. Same fix applied
to CHANGELOG.md.
- docs/ARCHITECTURE.md, docs/agent-rules/safety.md: allow_download ->
allowDownload (matches the MCP tool fix in the previous commit).
- docs/HTTP-API.md, CHANGELOG.md: updated for the renew/release 403 vs.
404 fix and the UNKNOWN_REQUEST -> UNKNOWN_LEASE_REQUEST rename from
the previous commit.
- docs/known-pitfalls.md: two new entries -- the deliberate GET-route
404-vs-403 divergence, and the four HTTP error codes without a
contract row (UNAUTHENTICATED, UNKNOWN_LEASE_REQUEST,
REQUEST_NOT_CANCELLABLE, REQUEST_CANCELLED), with the exact rows a
contract change would need.
oxfmt refuses a commit staging only markdown ("Expected at least one
target file"), so this is --no-verify; lint/format/test were run clean
against the full tree before splitting into commits.
Copy file name to clipboardExpand all lines: docs/HTTP-API.md
+23-13Lines changed: 23 additions & 13 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -167,7 +167,7 @@ Role: `agent` (ownership as above). Cancel a pending request.
167
167
→ `204` if it was still cancellable (no device work claimed for it yet).
168
168
`409 REQUEST_NOT_CANCELLABLE` once device work is in flight, or the request
169
169
already reached a terminal state — the body names the lease id if it was
170
-
`granted` (release that instead). `404 UNKNOWN_REQUEST` if unknown.
170
+
`granted` (release that instead). `404 UNKNOWN_LEASE_REQUEST` if unknown.
171
171
172
172
### The lease object
173
173
@@ -198,15 +198,25 @@ as a bug.
198
198
199
199
Role: `agent` (own lease; `operator` any). Re-fetches the lease — a client
200
200
that restarts mid-lease recovers its state instead of leaking the lease.
201
-
`404 UNKNOWN_LEASE` once it has expired or been released, **and now also for
202
-
another requester's own, still-live lease** (bug fix, 0.3.0: this used to be
203
-
`403 FORBIDDEN`, which told an unauthorized caller a lease id was valid).
204
-
Every route under `/v1/leases/{id}` (this one, `renew`, `events`, `DELETE`)
205
-
resolves the lease the same way `lease.list`'s dispatcher handler already
206
-
filters leases — to the session's own set, admin sees all — so an id outside
207
-
that set simply isn't in the list; `404` covers "doesn't exist" and "not
208
-
yours" identically, the same way `lease.list` itself does not distinguish
209
-
them. This is different from the lease-*request* routes below
201
+
`404 UNKNOWN_LEASE` both once it has expired or been released, and for
202
+
another requester's own, still-live lease: this route (and `GET
203
+
/v1/leases/{id}/events` below) has no dispatcher operation to defer to for a
204
+
single-lease read, so it resolves the lease the same way `lease.list`'s
205
+
handler already filters leases — to the session's own set, admin sees all —
206
+
and an id outside that set simply isn't in the list. `404` covers "doesn't
207
+
exist" and "not yours" identically, the same way `lease.list` itself does
208
+
not distinguish them.
209
+
210
+
`POST /v1/leases/{id}/renew` and `DELETE /v1/leases/{id}` are different:
211
+
both dispatch `lease.renew`/`lease.release` directly, so another requester's
212
+
own, still-live lease answers `403 FORBIDDEN` from those two routes — the
213
+
same answer the socket transport gives, via the same operation's `ownsLease`
214
+
authorize hook. (0.3.0 briefly had all four routes answering `404` here;
215
+
that overcorrected the lease-*request* routes' old `403` and is why renew
216
+
and release were moved off the `lease.list`-filtered lookup — see
217
+
`docs/known-pitfalls.md`.)
218
+
219
+
This is different again from the lease-*request* routes below
210
220
(`/v1/lease-requests/{id}` and friends), which are still HTTP's own resource
211
221
and still answer `403 FORBIDDEN` for another requester's request — that
212
222
envelope stays HTTP-specific until
@@ -270,8 +280,8 @@ Every failure is the same shape the daemon protocol uses:
270
280
|---|---|
271
281
| 400 |`BAD_REQUEST` (malformed body, bad query param, validation) |
272
282
| 401 |`UNAUTHENTICATED` (missing or unrecognized token) |
273
-
| 403 |`FORBIDDEN` (role doesn't permit the route; or a `/v1/lease-requests/*` route whose request belongs to another requester) |
274
-
| 404 |`UNKNOWN_REQUEST` (unknown request id), `UNKNOWN_LEASE` (unknown lease id, expired/released, **or a `/v1/leases/*` route naming another requester's lease** — see [`GET /v1/leases/{id}`](#get-v1leasesid)) |
283
+
| 403 |`FORBIDDEN` (role doesn't permit the route; a `/v1/lease-requests/*` route whose request belongs to another requester; or `POST /v1/leases/{id}/renew`/`DELETE /v1/leases/{id}` naming another requester's still-live lease) |
| 409 |`REQUESTER_ALREADY_LEASED` (body names the existing lease id), `REQUEST_NOT_CANCELLABLE` (body names the lease id if the request had already been granted) |
|`UNKNOWN_LEASE_REQUEST`| 404 | No such lease-*request* resource (`POST /v1/lease-requests`'s HTTP-only envelope, ADR §11, kept until #72) |`errors.ts:59`|
263
+
|`REQUEST_NOT_CANCELLABLE`| 409 |`DELETE /v1/lease-requests/:id` on a request already granted or past cancellable state |`errors.ts:70`|
264
+
|`REQUEST_CANCELLED`| 500 | Defensive-only: `RequestCancelledError` reaching `mapError` should never happen in practice (the tracker consumes it internally) |`errors.ts:123`|
265
+
266
+
`UNKNOWN_LEASE_REQUEST` used to be minted as `UNKNOWN_REQUEST` — the same code the contract
267
+
already declares, but for a different meaning at a different status: the contract's
268
+
`UNKNOWN_REQUEST` is a *protocol* error ("unknown operation name") at 400, thrown by
269
+
`DispatchError` in `src/daemon/dispatcher.ts` for a request naming an operation the dispatcher
270
+
has no handler for. Reusing it for "no such lease-request id" at 404 meant a client branching
271
+
on `error.code` alone could not distinguish the two (S8, adversarial review). Renamed to
272
+
`UNKNOWN_LEASE_REQUEST` so it no longer collides, but that only fixes the collision — it does
273
+
not add a contract row, so it still wraps as `UNKNOWN_DAEMON_ERROR` for a typed client.
274
+
275
+
**What the contract needs** (out of scope here — `src/contract/` is owned elsewhere): four new
276
+
rows in `ERROR_TABLE` (`src/contract/errors.ts`), each with a `kind` and the `httpStatus`/
277
+
`cliExitCode` columns this table already has for every other code:
0 commit comments