Task 02 (the whole runner) ended at 21:08 with no file and no commit: Ornith wrote the
implementation out in its reply (~700 lines), OpenCode compacted the session, and the model
then emitted a malformed tool call. It had also looked for the given test at a wrong path
("_files/…" relative to the plan: my ambiguity).
- 02-runner → 02-runner-core (types, Submit, Status, Touch, Cancel; no goroutines) and
03-runner-workers; the given test split to match (runner_core_test.go gains three tests:
queued storage, cancel while queued, Status never moves the lease); later tasks renumbered
04-09 and every cross-reference checked.
- Copy paths spelled from the repo root.
- guard.PreCheck (+ its test) is committed here by the owner: the old task 05 told the
implementer to edit internal/guard/, which AGENTS.md forbids. My contradiction.
- AGENTS.md rule 6: write code into files early, in small steps.
Both runner test files compile against a panic-only skeleton (the core file also alone) and
fail there on panic: todo.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
5.7 KiB
5.7 KiB
P6 task 07: the /v1/batch endpoints
Branch: p6 (git switch p6; git status --short must be empty, otherwise stop)
Commit subject: api: /v1/batch — submit, status summary, one result, cancel
Goal
The HTTP surface over s.Batch (task 06): submit, a status summary, one target's result, and
cancel, with the submit-time guard pre-check, the caps as HTTP statuses, and ownership.
Files
- Copy:
docs/plans/p6/_files/api/batch_test.go→internal/api/batch_test.go - Modify:
internal/api/batch.go(the four handlers),internal/api/api.go(only the routes inNewandServeHTTP, rule 2),docs/implementer-log.md
What already exists
// internal/batch (task 02)
type Target struct{ URL string; Options Options }
type Options struct{ Mode, Question, Pages, Grep, JQ string; Context *bool; Recover bool }
type Result struct{ Content string; Meta map[string]any }
type Job struct{ ID, Caller, Status, Error string; TargetsTotal, TargetsDone int; CreatedAt int64 }
type TargetStatus struct{ Idx int; URL, Status string; Result *Result; Error, Hint string }
var ErrNoTargets, ErrTooManyTargets, ErrCallerBusy, ErrBusy error
func (r *Runner) Submit(ctx context.Context, c *auth.Caller, callerClass string, targets []Target) (string, bool, error)
func (r *Runner) Cancel(id string) bool
func (r *Runner) Status(id string) (Job, []TargetStatus, bool) // a pure read
func (r *Runner) Touch(id string) // bumps the lease to now
// internal/guard (on master)
func (g *Guard) PreCheck(raw string) error // "blocked: …" errors
// internal/api
Batch *batch.Runner // in Server (task 06); nil → 503 "batch disabled"
func (s *Server) caller(w http.ResponseWriter, r *http.Request) *auth.Caller // nil after writing 401/403
func (s *Server) callerClass(c *auth.Caller) string
// maxReqBody (64 KiB), writeErr(w, status, msg, hint), writeJSON(w, status, v), s.Prom.Observe(path, d, code)
Add const batchMaxBody = 256 << 10.
Rules
- Routes (in
New):POST /v1/batch,GET /v1/batch/{id},GET /v1/batch/{id}/{idx},DELETE /v1/batch/{id}. Each starts withs.caller(w, r). ServeHTTP:- The body cap is
batchMaxBodyfor/v1/batchandmaxReqBodyfor every other path. The cap is applied once, here: a handler cannot raise a cap already set. - Batch paths are observed with a fixed label:
/v1/batchor/v1/batch/{id}. A job id must never become a label value.
- The body cap is
- Submit (
POST /v1/batch), checked in this order:- The body is
{"targets": [{"url", "mode", "question", "pages", "grep", "jq", "context", "recover"}, …]}, each target flat with/v1/fetch's field names. Bad JSON, a missing or empty list, an emptyurl, or amodeother than empty,clean,rawordigest→400 invalid batch: …. s.Guard.PreCheck(url)for every target. The first failure →422with the messageblocked: target <idx>: <reason>, and no job is created.s.Batch.Submit(r.Context(), c, s.callerClass(c), targets):ErrTooManyTargets→422 too many targets (max 32)(the configured cap);ErrCallerBusy→429 busy: a batch of yours is still running,Retry-After: 30;ErrBusy→429 batch jobs busy,Retry-After: 60;ErrNoTargets→400.
202 {"success": true, "job_id", "status", "targets": n, "reused"}.statusis"queued"for a new job, and the job's current status for a reused one.
- The body is
- Ownership: a job whose
Calleris notc.Name, or an unknown id, is404 not foundfor every GET and DELETE. It is never403: a caller cannot probe for another caller's jobs. - Status (
GET /v1/batch/{id}): after the ownership check,s.Batch.Touch(id), then200 {"job_id", "status", "error" (only when set), "targets_total", "targets_done", "created_at" (RFC 3339, UTC), "targets": [{"idx", "url", "status", "error"?, "hint"?}]}.- This is a summary: no results, because 32 inline results can be 6 MB, too much for one agent tool call.
- With
?results=1, eachdonetarget also carries"result": the envelope, i.e.Metaplus"content".
- One result (
GET /v1/batch/{id}/{idx}): after ownership andTouch,200 {"job_id", "idx", "url", "status", "result"?, "error"?, "hint"?}. Anidxthat is not a number in0..targets_total-1→404. Cancel (DELETE): after ownership, a terminal job →409 job already finished; otherwiseCancel→200 {"success": true, "status": "cancelled"}. - Logging: each batch handler logs one line:
batchwithcaller,job_id,targetsandstatus(orreused). Never a target URL or hostname, for any caller, and never theidem_key. The per-targetfetchline insidefetchTargetis unchanged: it is redacted for restricted callers exactly as for/v1/fetch.
Facts
newBatchServer,pageServerandfakeDNScome frombatchrun_test.go(task 06). Targets are localhttptestservers, some held on a channel, orinternal.test(resolves to10.0.0.5).TestBatchBodyCapposts 32 targets with questions, about 66 KiB: overmaxReqBody, underbatchMaxBody.
Steps
- 1.
git switch p6; copy the given test. 2. See it fail. - 3. The handlers. 4. Routes and
ServeHTTP. - 5.
gofmt -w;go test -race -count=3 ./internal/api/→ok. 6.make gate. - 7. Row
p6/07-api-endpoints; commit.
git add internal/api docs/implementer-log.md
git commit -F .state/commit-msg.txt
Done when
- The given test and every existing api test pass under
-race -count=3;make gateok; the given file is byte-identical.
Stop and report if
- Any given test needs a URL that is not local, or a change to an existing test.