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
{{ message }}
Repository navigation
api: problem-details later tiers (specific codes, instance, result envelope, git/LFS writers, problem+json) — held on SDK trigger #352
Tier 1 (#349, PR #351, merge 1f392ac3) adds type, title, status, a generic per-status code, and detail to the plain {"error"} body. This issue tracks the remaining tiers, held until a trigger fires:
Specific codes such as repository_not_found, branch_not_found, ref_not_found, insufficient_scope and repo_name_mismatch. Some can come from central helpers (resolveRepo, auth middleware); covering them fully means going through each writeErr call site.
The instance field.
Problem fields on the write routes' result envelope ({"result":{"success":false,…}}, ADR-26), where upstream uses problem bodies.
The git smart-HTTP and LFS error writers in internal/githttp.
Switching to application/problem+json. The 1.17.1 Go and TypeScript SDKs parse JSON errors only for application/json. 1.18.0-beta.0 also accepts problem+json, so this becomes possible once the stable SDKs do.
Revisit triggers
These match the RFC 9457 row in docs/explanation/wire-compatibility.md:
A stable SDK release that reads code, or that parses application/problem+json (item 5).
A user report of depending on a specific code.
The daily upstream compatibility audit checks the SDK release channels, so the desk can re-triage when a trigger fires.
New tier: field-level validation (invalid_params), explicitly deferred
Upstream's 2026-10-08 spec (#396) adds ValidationProblemDetails: ProblemDetails plus an optional invalid_params: [{name, reason}] on about 30 write 400s. name is the dotted wire path of one invalid request-body field. Upstream leaves the member out when it cannot name a field.
Decision: deferred. open.storage omits invalid_params. error, code and the rest of the envelope do not change.
Why:
The member is optional, so omitting it is wire-compatible.
No SDK reads it. I searched the Go, TypeScript and Python packages at 1.17.1 (stable) and 1.18.0-beta.1 (beta) and found 0 references.
The 400s come from about 186 call sites and five writers (writeErr, aliasFail, and the result-envelope writers that tier 3 above covers). A partial rollout would cover an arbitrary subset.
Docs: PR #398 records the omission in docs/reference/http-api.md (Errors) and in the RFC 9457 row of docs/explanation/wire-compatibility.md.
Revisit triggers for this tier, in addition to the ones above:
Plan: later problem-details tiers (code, instance, invalid_params)
This plan covers four parts of upstream's error contract: specific code values, instance, invalid_params, and the content type. Tier 3 (result-envelope bodies) and tier 4 (git and LFS writers) stay held. The error field and its text do not change: the 1.17.1 SDKs read it.
The desk owner reopened the invalid_params deferral above (issuecomment-6052209313). This plan replaces that deferral.
1. One central producer
internal/api/errors.go gets one problem writer. writeErr becomes a thin wrapper around it with the generic code. Call sites never build a problem body.
// problem is one RFC 9457 error body. Only writeProblem writes it.typeproblemstruct {
statusintcodestring// generic code unless a specific code appliesmsgstring// goes to `error` and `detail`, unchanged textinvalidParams []invalidParam// 400 request-body failures only
}
typeinvalidParamstruct {
Namestring`json:"name"`// dotted wire path, e.g. base_repo.auth.tokenReasonstring`json:"reason"`// the broken rule, e.g. "is required"
}
Validation failures use one error type. A validator or decode helper returns a *fieldError. One writer, writeInvalid(w, err), writes the 400. It reads invalid_params from the error with errors.As. Error() returns the current message, so the error text stays the same.
A specific code has the status that upstream lists for it.
invalid_params appears only on 400, and each item has a non-empty name and reason.
instance is the request path, escaped, without the query. Upstream's examples show the same thing: instance: /api/repos for a bad ?cursor. A wrapper on the outermost handler (outside recoverPanics) records the path. writeProblem finds it through the Unwrap() chain that http.ResponseController already uses. The path is captured at the outermost handler because the /api/repos/{repo_name}/... aliases rewrite r.URL.Path to the v1 path before they dispatch (alias.go:212). With this capture, instance is the path that the client sent, and the 228 writeErr call sites do not change.
2. Validators that can name a field
Upstream sends invalid_params only for request-body fields. It leaves the member out when it cannot name a field, for example when the body is not valid JSON. This plan does the same.
Validator
Field name source
Example {name, reason}
Strict decode (strictJSON / checkStrictObject)
the member path the checker already builds
{base_repo.auth, "is not a known field"}, duplicate, must not be null
These failures get no invalid_params: unreadable or empty bodies, JSON syntax errors, rules across two fields (base_repo and upstream cannot both be set, the same as upstream's example), "one of A, B or C is required", query-parameter 400s, and result-envelope 400s (tier 3). The scope is the plain-envelope 400s on the about 15 operations where upstream's spec uses ValidationProblemDetails.
3. code mapping
The spec's "Specific codes:" list is the source. A test parses it, the same way as TestGenericProblemCodeMatchesUpstream. Every code that open.storage emits must be in the list with the same status.
Code
Status
open.storage source
PR
insufficient_scope
403
requireAuthWith "missing required scope" on plain-body routes
2
repository_not_found
404
repo lookups that answer "repository not found" (handlers*.go, repourls.go, the alias read flavor)
2
repo_name_mismatch
400
the 3 repoNameMismatchMsg sites (create, fork, sync create)
2
branch_not_found
404
"branch not found", and merge's plain "source/target branch X not found"
3
ref_not_found
404
"ref not found", "tag not found", "commit not found", "base ref not found", "ref does not resolve to a commit"
3
credential_not_found
404
git-credentials plain bodies
3
credential_exists
409
git-credentials create
3
repository_deleted
409
DELETE on a deleted repo
3
These codes stay generic. Each has a reason:
repo_claim_missing: upstream answers 400. open.storage answers 403 ("token is not repo-scoped"). A status change breaks clients, so it is out of scope.
authentication_unavailable: open.storage answers 401 on every verifier failure. It has no separate "cannot verify now" path.
route_not_found: unmatched /api/ paths reach the mux's plain-text 404 and 405 answers or the git fallback. A catch-all route changes 405 answers. This belongs with tier 4.
merge_conflict: merge conflicts use the result envelope (tier 3).
sync_in_progress, upstream_already_configured: no handler answers these 409s.
repository_thawing, github_*, hosting_*, deployment_*, idempotency_key_reused: open.storage has no such feature.
"Not found" answers for paths, files, notes, webhooks and organizations keep not_found. Upstream has no specific code for them.
4. Content type: stays application/json
The stable SDKs still parse error JSON only for application/json. npm latest is 1.17.1 and beta is 1.18.0-beta.1 (checked 2026-10-13).
Go code-storage-go@v1.17.1fetch.go:91: if strings.Contains(contentType, "application/json"). application/problem+json does not contain that substring, so the SDK falls back to the raw body text as the message.
TypeScript @pierre/storage@1.17.1dist/index.js:1266: if (contentType.includes("application/json")). Otherwise it reads the body as text.
Both betas also accept problem+json: Go v1.18.0-beta.1fetch.go:96, TS 1.18.0-beta.1dist/index.js:1318.
Python pierre_storage 1.17.1 calls response.json() without a content-type check, so it does not decide this.
Upstream's spec lists application/json next to application/problem+json for every error response, with the same schema. So application/json stays a faithful choice. The switch stays held on the existing trigger: a stable SDK release that accepts problem+json.
5. Tests on the PG and SQLite lanes
Unit tests: the producer's assertions, instance lookup through statusRecorder and gitReadWriter, the panic path, and one test for each decode-failure class in the table in section 2.
Spec pins: the specific-code list (both directions for the codes we emit), the InvalidParam fields, and upstream's default_branch / must be a string example. A schema-shape test checks that emitted bodies have no members outside ProblemDetails / ValidationProblemDetails.
Full server, both lanes (runBackends: pg and sqlite):
requireEnvelope asserts instance equals the client's escaped request path. It already runs on about 85 error assertions, so all of them check instance.
One request for each mapped code and each invalid_params class.
Negative controls: query 400s and syntax errors carry no invalid_params, and result-envelope bodies do not change.
SDK compatibility: sdkcompat_test.go with OPENSTORAGE_SDK_DIR at Go v1.17.1. Dogfood with a real server, curl, and the Go and TS 1.17.1 SDKs reading the error message.
6. PRs (each at most 500 changed lines)
feat(api): the central writeProblem and instance on every plain problem body. Docs: http-api.md Errors, and the RFC 9457 row in wire-compatibility.md.
feat(api): the specific-code registry pinned to the spec, plus the central-helper codes (insufficient_scope, repository_not_found, repo_name_mismatch).
feat(api): the call-site codes (branch_not_found, ref_not_found, credential_*, repository_deleted).
feat(api): fieldError, writeInvalid and invalid_params for decode failures in the shared decode helpers.
feat(api): invalid_params for required, enum and range validators on upstream's ValidationProblemDetails routes. If this PR goes over 500 lines, it splits by route family (repo create/patch/fork/sync, then refs/credentials/grep/merge).
problem+json gets no PR. It stays held as described in section 4.
Acceptance
Every plain problem body has instance equal to the request path that the client sent.
Every code that open.storage emits is in upstream's table with the same status.
invalid_params appears only on 400 bodies with a nameable body field.
error and detail text do not change, and the content type stays application/json.
The SDK 1.17.1 Go and TS clients still surface the same error message.
I am starting PR 1 now. This plan needs no product decision: it changes no status codes and no message text.
Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high
4 (round 1: 1 P2 fixed, 1 P2 rejected because it changes a status code; round 2 clean)
ready
A re-delete through the DELETE /api/repos/{repo_name} alias stays 404 repository_not_found, because its name lookup skips deleted repositories. docs/reference/http-api.md records this.
Next: PR 4 (invalid_params on decode failures), then PR 5 (single-field validators).
Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high
PR 4 of the plan landed: #436, merge 66f5c329. A 400 for a request body that fails to decode now names the field in invalid_params. Examples: wrong JSON type, including nested base_repo.* and upstream.* members and array elements such as paths.0, and unknown, duplicate, null or non-object members of strict bodies.
Merge-group runs: ci 38431551836 and pr-title 38431551616 passed. Main push CI 38433702162 passed.
Codex: 4 assessments. Round 1 raised one security P3 and three normal P2s. I fixed the P3 and two P2s. One P2 was a duplicate of the P3. I rejected the element-path claim with probe evidence and pinned element paths in tests. Round 2 was clean in both lanes.
Clean-context advisory: ready. The PR is 528 changed lines, over the 500-line plan limit. The extra lines came from the fix that round 1 asked for.
Next: PR 5 (single-field validators). It also carries the advisory follow-ups.
Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high
All five PRs from the plan (issuecomment-6143529650) have landed. error and detail text, status codes and the application/json content type did not change. Every code change goes from the generic code to upstream's specific code.
invalid_params on body decode failures: wrong JSON type, including nested members and array elements, and strict unknown, duplicate, null or non-object members
invalid_params from single-field validators: required fields, allowed values, ranges, name formats on the ValidationProblemDetails routes
e1beeeb8
A test pins every specific code that open.storage emits to the vendored spec's "Specific codes:" list. A test also pins the problem member set to ProblemDetails plus invalid_params. I ran dogfood A/B checks with the stable Go and TS SDKs (1.17.1) on each PR. The SDK output did not change.
Still held: application/problem+json
The content type stays application/json. The stable SDKs (1.17.1, npm latest as of 2026-10-13) parse error JSON only for application/json. Only the 1.18.0-beta clients also accept application/problem+json. Revisit trigger: a stable SDK release that accepts application/problem+json. This issue stays open for that item.
Recorded limits (no follow-up issues)
These upstream codes are not emitted, because each would need a status change, a new body shape or a feature open.storage lacks: repo_claim_missing (open.storage answers 403), authentication_unavailable, route_not_found, merge_conflict (result envelope), sync_in_progress, upstream_already_configured, and the hosting, deployment, GitHub App and idempotency codes.
A re-delete through DELETE /api/repos/{repo_name} answers 404 repository_not_found, not 409 repository_deleted. The alias's name lookup skips deleted repositories.
invalid_params is left out for mutually exclusive members (base_repo with upstream), query parameters, delete-branch's name (query or body), and grep's "cursor past the matches" 400.
Result-envelope bodies (tier 3) and the git and LFS writers (tier 4) carry no problem fields.
Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high
Item 1, partly. The remaining specific codes each need a status change, a new body shape or a feature open.storage lacks: repo_claim_missing, authentication_unavailable, route_not_found, merge_conflict (result envelope), sync_in_progress, upstream_already_configured, and the hosting, deployment, GitHub App and idempotency codes.
Item 3. Problem fields on the write routes' result envelope ({"result":{"success":false,…}}, ADR-26).
Item 4. The git smart-HTTP and LFS error writers in internal/githttp.
Item 5.application/problem+json. The stable SDKs (1.17.1) still parse error JSON only for application/json.
The revisit triggers in the issue body do not change.
Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high
Upstream audit 2026-10-17: the docs spec now gives the merge 409 two bodies. An application/jsonMergeConflict body reports file conflicts and stale-target retries. A ProblemDetails body (code conflict) reports every other conflict. Our merge failures all use the result envelope as application/json (handlers_write_merge.go:128-143; recorded deviation wire-compatibility.md:203). This belongs to items 3 and 5 here and stays gated on G1 (no stable SDK accepts problem+json yet; stable is 1.17.1).
Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: medium
Follow-up from #568 (PRs #571 and #573), item 3 of this issue:
A note write whose expected_notes_ref_sha guard passed can still lose the notes-ref CAS at publish time. That 409 (" moved concurrently", internal/api/handlers_write_refs.go:1114 at fix(api): add ref_guard_failed problem members to the notes 409s #573's head) keeps the plain note envelope, with no problem members.
Trigger: upstream documents problem members (or ref_guard_failed) for a lost notes CAS, or this issue's G1 gate fires. Owner: the desk, at the next upstream audit.
The four 412 deviation rows (commit-pack, diff-commit, restore-commit, notes body forms) keep their #533 reasons. Their wire-compatibility rows now name ref_guard_failed.
Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high
Upstream's unmerged SDK branch necolas/code-storage-pointer says the @pierre/storage / pierre-storage / code-storage-go line gets no new release, and names @pierre/sdk, pierre-sdk and github.com/pierrecomputer/sdk/go as replacements. @pierre/sdk0.1.0-canary.3 (npm, Oct 27) already parses every error body as JSON, so it accepts application/problem+json.
G1 now fires on the first stable (non-canary, non-beta) release of @pierre/sdk, pierre-sdk or github.com/pierrecomputer/sdk/go, or on a stable @pierre/storage release that accepts problem+json, whichever comes first. A README change or a canary does not fire it. The daily upstream audit watches the npm dist-tags, PyPI and the Go proxy for these packages.
Note for the rest of this issue: the canary picks RefGuardError by code === "ref_guard_failed", so our #533 412 deviations will matter once it ships.
Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: medium
Gate G1 fired on 2026-11-04. Stable releases: npm @pierre/sdk 0.1.0 (latest), PyPI pierre-sdk 0.1.0 and Go github.com/pierrecomputer/sdk/go v0.1.0 (@latest, tag go/v0.1.0 = 5f1a4bc). The upstream changelog entry pierre-sdk-and-cli (2026-11-03) says the old SDKs get no new releases, and the pointer branch merged as SDK #66.
SDK 0.1.0 parses every error body as JSON whatever the content type (Go go/errors.go:186-210 at go/v0.1.0), so it accepts application/problem+json, and it branches on ref_guard_failed.
The desk is assigning owners now. The canonical routes SDK 0.1.0 calls are tracked separately in #656.
Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: medium
Item 5: upstream documents application/problem+json for problem bodies. I am switching.
Answer to the open question (checked 2026-11-04 against the vendored spec 412b90c3 on main 63f81faa):
The spec's Errors section (docs/upstream/openapi.yaml:3027) says: "Most endpoints return RFC 9457-compatible problem details under application/problem+json".
661 error responses list both application/json and application/problem+json. 646 of them give both the same ProblemDetails schema.
In the other 15 (the write 409s of commit-pack, diff-commit, merge, notes, notes/append, reset-commits and restore-commit, both route forms), the media type separates the two body shapes. application/problem+json carries the problem body (CommitWriteRefGuardProblemDetails, NotesWriteRefGuardProblemDetails, RestoreCommitRefGuardProblemDetails, ProblemDetails). application/json carries the result envelope (CommitWriteResponse, MergeConflict) or plain ProblemDetails.
25 error responses list only application/json. None is a problem body: CommitWriteResponse (14), ReposCommitsRestoreResponse (8), ReposUpstreamDeleteResponse (3).
Live check: an unauthenticated GET https://api.pierre.code.storage/api/repos answers 401 with Content-Type: application/problem+json for no Accept, Accept: application/json, Accept: application/problem+json and Accept: */*. Upstream does not negotiate.
What each SDK line does with a application/problem+json body:
SDK
Behavior
0.1.0 Go (go/runtime.go:386-395, go/errors.go:187-210 at go/v0.1.0)
Reads every error body as JSON, whatever the content type. No change.
0.1.0 Python (pierre/_runtime.py:393-400)
Same: json.loads of the body, no content-type check. No change.
0.1.0 TypeScript (dist/runtime/http.js:467)
Same: JSON.parse of the body text. No change.
1.18.0-beta
Accepts both types (plan comment above).
1.17.1 Go (fetch.go:91, notes util.go:154) and TypeScript (dist/index.js:1266, notes :2131)
Parse JSON only when Content-Type contains application/json. On a problem+json body, APIError.Message / ApiError.message becomes the raw body text instead of error, and Body becomes that string. A note write's 409 guard body becomes a generic API error, not the note result.
1.17.1 Python
Calls response.json() with no content-type check. No change.
All three 0.1.0 SDKs send Accept: application/json on JSON operations. Upstream answers problem+json to that request (point 5), so open.storage will too.
Decision (desk rule: follow what the spec documents): every body that newProblem builds switches to application/problem+json. That covers writeErr, writeProblem, writeInvalid, the ref_guard_failed bodies (merge, notes append, the notes DELETE query form) and the git-surface 429. These stay application/json: success bodies, result envelopes (commit-pack, diff-commit, restore, reset, merge results and their failures), and the internal/githttp{"error"} writers (item 4). The PR is marked breaking, with a BREAKING CHANGE: line that names the 1.17.1 Go and TS effect.
Evidence (spec counts script, probe transcript) is in my scratch dir; the PR links back here.
Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high
I checked every write route of the spec. Upstream documents a plain ProblemDetails body (no result) for 401, 403, 404, 429, 500 and 503 on every write route. It documents a result-envelope body only here:
Route
Statuses with the envelope or another non-problem body
commit-pack
400, 409, 412, 413, 502 (CommitWriteResponse)
diff-commit
400, 409, 412, 502 (CommitWriteResponse)
restore-commit
412, 502 (ReposCommitsRestoreResponse). 400 and 409 are ProblemDetails.
merge
409 file conflicts and stale-target retries (MergeConflict: error, conflict_type, conflict_paths, merge_base_sha, no result). Every other status is ProblemDetails.
notes, notes/append, DELETE notes
none. Every failure is ProblemDetails, except the 409 guard body (NotesWriteRefGuardProblemDetails), which open.storage already sends.
branches, tags, git-credentials (path-id)
none
open.storage sends the ADR-26 result envelope on all of these: auth 401/403, the write-route 429, the alias and git-credentials failures, and every wcFail, wmFail and wrefNoteFail status.
Why this matters now: SDK 0.1.0 Go (go/errors.go:209-275 at go/v0.1.0) turns any error body that has a result member into a RefUpdateError, before it looks at the status. So a 401 on merge gives *pierre.RefUpdateError with the message "401: request failed" and no code, where upstream gives AuthenticationError with code and detail. My item 5 dogfood shows this on branches/create, merge and notes/append.
Decision (#234 rule: implement only what upstream documents): where upstream documents ProblemDetails, open.storage sends the plain problem body and no result. Where upstream documents the envelope, the envelope stays. Each PR is marked breaking for SDK 1.17.1, which builds a RefUpdateError from result.status on some of these statuses. Three PRs:
Shared writers: write-route auth 401/403, the write-route 429, and the alias and git-credentials failures.
Commit writes (commit-pack, diff-commit, restore-commit): the statuses outside the table above.
Merge and notes: plain problem bodies outside the table, and the MergeConflict shape for merge conflicts.
Not in item 3: the stale-guard 412s of the #533 deviations. #647 part 2 moves them to 409 ref_guard_failed, and another owner works on it.
Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high
Item 4 (git smart-HTTP and LFS error writers): no change needed
Upstream documents no problem bodies for the git surface, so open.storage changes nothing in internal/githttp for this issue (#234 rule: implement only what upstream documents). Evidence, checked 2026-11-04:
The vendored spec (docs/upstream/openapi.yaml, ee8092bf on main) has only /api/... paths. It documents no smart-HTTP or LFS route and no error body for them.
The vendored guides (docs/upstream/guides/git-operations.md, git-lfs.md) and the live pages docs/repos/git-operations.md and docs/repos/git-lfs.md describe no error body. The live API overview, which defines the RFC 9457 error contract, covers the HTTP API and does not mention the git surface.
Upstream's live git surface sends no problem body:
GET https://pierre.code.storage/nope.git/info/refs?service=git-upload-pack without credentials: 401, Content-Type: text/plain; charset=utf-8, body Authentication required.
GET https://pierre.code.storage/nope.git/info/lfs/objects/batch without credentials: 401, Content-Type: application/vnd.git-lfs+json, body {"message":"basic auth credentials are required"}.
open.storage's LFS writer (lfsError, internal/githttp/lfs.go) already sends the git-lfs {"message"} body with application/vnd.git-lfs+json. That is the shape the git-lfs client reads and the shape upstream sends.
Two related facts, outside this item:
The git-surface 429 comes from the shared REST limiter (api.NewGitRateLimit, writeErr), so it already carries the problem members. Since feat(api)!: send application/problem+json on problem bodies #660 it is application/problem+json. Upstream documents no git-surface 429 body.
open.storage's smart-HTTP errors use {"error"} JSON (httpError, internal/githttp/server.go), where upstream sends text/plain. Git clients print the body as text either way. That is not a problem-details question, so I did not change it here.
Item 4 is closed by this evidence.
Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high
commit-pack, diff-commit and restore-commit send the result envelope only on the statuses upstream documents for it, and the plain problem body otherwise, with repository_not_found, branch_not_found and ref_not_found on the 404s.
Merge file conflicts answer upstream's MergeConflict. Every other merge and note-write failure is the plain problem body. A non-fast-forward ff_only merge and unrelated histories answer 412 precondition_failed. A target that moved during a merge answers 409 conflict, because open.storage has no automatic retry. Single-field 400s name the field in invalid_params.
404 route_not_found and 405 method_not_allowed (with Allow) problem bodies for unmatched /api/ paths, and 403 authentication_unavailable when the key store fails (REST and fork source tokens).
4
none
none
No change: upstream documents no problem bodies for the git surface (comment).
merge_conflict: the merge file-conflict 409 is MergeConflict, which has no code member.
target_moved and repo_changed: upstream sends them only after an automatic retry, which open.storage does not do.
sync_in_progress, upstream_already_configured: no upstream operation documents them.
Hosting, deployment, GitHub App, cold-storage and idempotency codes: open.storage lacks those features.
Related: #664 (upstream answers an invalid or expired token with 403, not 401) is open for desk triage. #673 is a comment-only follow-up from the #672 review.
Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high
Activity
ThomasK33 commentedon Oct 8, 2026
New tier: field-level validation (
invalid_params), explicitly deferredUpstream's 2026-10-08 spec (#396) adds
ValidationProblemDetails:ProblemDetailsplus an optionalinvalid_params: [{name, reason}]on about 30 write 400s.nameis the dotted wire path of one invalid request-body field. Upstream leaves the member out when it cannot name a field.Decision: deferred. open.storage omits
invalid_params.error,codeand the rest of the envelope do not change.Why:
1.17.1(stable) and1.18.0-beta.1(beta) and found 0 references.writeErr,aliasFail, and the result-envelope writers that tier 3 above covers). A partial rollout would cover an arbitrary subset.Docs: PR #398 records the omission in
docs/reference/http-api.md(Errors) and in the RFC 9457 row ofdocs/explanation/wire-compatibility.md.Revisit triggers for this tier, in addition to the ones above:
invalid_params, orRefs #396.
Generated with
xum• Model:anthropic:claude-opus-5-5• Thinking:highfix(sync): send x-token-auth for Bitbucket credentials without a user…
ThomasK33 commentedon Oct 13, 2026
Plan: later problem-details tiers (
code,instance,invalid_params)This plan covers four parts of upstream's error contract: specific
codevalues,instance,invalid_params, and the content type. Tier 3 (result-envelope bodies) and tier 4 (git and LFS writers) stay held. Theerrorfield and its text do not change: the 1.17.1 SDKs read it.The desk owner reopened the
invalid_paramsdeferral above (issuecomment-6052209313). This plan replaces that deferral.1. One central producer
internal/api/errors.gogets one problem writer.writeErrbecomes a thin wrapper around it with the generic code. Call sites never build a problem body.Validation failures use one error type. A validator or decode helper returns a
*fieldError. One writer,writeInvalid(w, err), writes the 400. It readsinvalid_paramsfrom the error witherrors.As.Error()returns the current message, so theerrortext stays the same.writeProblemasserts these rules:invalid_paramsappears only on 400, and each item has a non-emptynameandreason.instanceis the request path, escaped, without the query. Upstream's examples show the same thing:instance: /api/reposfor a bad?cursor. A wrapper on the outermost handler (outsiderecoverPanics) records the path.writeProblemfinds it through theUnwrap()chain thathttp.ResponseControlleralready uses. The path is captured at the outermost handler because the/api/repos/{repo_name}/...aliases rewriter.URL.Pathto the v1 path before they dispatch (alias.go:212). With this capture,instanceis the path that the client sent, and the 228writeErrcall sites do not change.2. Validators that can name a field
Upstream sends
invalid_paramsonly for request-body fields. It leaves the member out when it cannot name a field, for example when the body is not valid JSON. This plan does the same.{name, reason}strictJSON/checkStrictObject){base_repo.auth, "is not a known field"}, duplicate,must not be nullDisallowUnknownFieldsdecodes (forkbase_repo, createupstream, create-branch)json: unknown field "x", prefixed with the member (upstream.x). A test pins this format.{upstream.branch, "is not a known field"}*json.UnmarshalTypeError.Field)Field{default_branch, "must be a string"}. This is the example in upstream's spec.default_branch,password,id,ref,base_repo.name,base_repo.auth.tokendefault_branch,target_branch, the branch and tag names,password(reserved value)limits.max_lines,limits.max_matches_per_file,pagination.limitThese failures get no
invalid_params: unreadable or empty bodies, JSON syntax errors, rules across two fields (base_repo and upstream cannot both be set, the same as upstream's example), "one of A, B or C is required", query-parameter 400s, and result-envelope 400s (tier 3). The scope is the plain-envelope 400s on the about 15 operations where upstream's spec usesValidationProblemDetails.3.
codemappingThe spec's "Specific codes:" list is the source. A test parses it, the same way as
TestGenericProblemCodeMatchesUpstream. Every code that open.storage emits must be in the list with the same status.insufficient_scoperequireAuthWith"missing required scope" on plain-body routesrepository_not_foundhandlers*.go,repourls.go, the alias read flavor)repo_name_mismatchrepoNameMismatchMsgsites (create, fork, sync create)branch_not_foundref_not_foundcredential_not_foundcredential_existsrepository_deletedThese codes stay generic. Each has a reason:
repo_claim_missing: upstream answers 400. open.storage answers 403 ("token is not repo-scoped"). A status change breaks clients, so it is out of scope.authentication_unavailable: open.storage answers 401 on every verifier failure. It has no separate "cannot verify now" path.route_not_found: unmatched/api/paths reach the mux's plain-text 404 and 405 answers or the git fallback. A catch-all route changes 405 answers. This belongs with tier 4.merge_conflict: merge conflicts use the result envelope (tier 3).sync_in_progress,upstream_already_configured: no handler answers these 409s.repository_thawing,github_*,hosting_*,deployment_*,idempotency_key_reused: open.storage has no such feature."Not found" answers for paths, files, notes, webhooks and organizations keep
not_found. Upstream has no specific code for them.4. Content type: stays
application/jsonThe stable SDKs still parse error JSON only for
application/json. npmlatestis1.17.1andbetais1.18.0-beta.1(checked 2026-10-13).code-storage-go@v1.17.1fetch.go:91:if strings.Contains(contentType, "application/json").application/problem+jsondoes not contain that substring, so the SDK falls back to the raw body text as the message.@pierre/storage@1.17.1dist/index.js:1266:if (contentType.includes("application/json")). Otherwise it reads the body as text.v1.18.0-beta.1fetch.go:96, TS1.18.0-beta.1dist/index.js:1318.pierre_storage 1.17.1callsresponse.json()without a content-type check, so it does not decide this.Upstream's spec lists
application/jsonnext toapplication/problem+jsonfor every error response, with the same schema. Soapplication/jsonstays a faithful choice. The switch stays held on the existing trigger: a stable SDK release that accepts problem+json.5. Tests on the PG and SQLite lanes
instancelookup throughstatusRecorderandgitReadWriter, the panic path, and one test for each decode-failure class in the table in section 2.InvalidParamfields, and upstream'sdefault_branch/must be a stringexample. A schema-shape test checks that emitted bodies have no members outsideProblemDetails/ValidationProblemDetails.runBackends:pgandsqlite):requireEnvelopeassertsinstanceequals the client's escaped request path. It already runs on about 85 error assertions, so all of them checkinstance.invalid_paramsclass.invalid_params, and result-envelope bodies do not change.sdkcompat_test.gowithOPENSTORAGE_SDK_DIRat Gov1.17.1. Dogfood with a real server,curl, and the Go and TS 1.17.1 SDKs reading theerrormessage.6. PRs (each at most 500 changed lines)
feat(api): the centralwriteProblemandinstanceon every plain problem body. Docs:http-api.mdErrors, and the RFC 9457 row inwire-compatibility.md.feat(api): the specific-code registry pinned to the spec, plus the central-helper codes (insufficient_scope,repository_not_found,repo_name_mismatch).feat(api): the call-site codes (branch_not_found,ref_not_found,credential_*,repository_deleted).feat(api):fieldError,writeInvalidandinvalid_paramsfor decode failures in the shared decode helpers.feat(api):invalid_paramsfor required, enum and range validators on upstream'sValidationProblemDetailsroutes. If this PR goes over 500 lines, it splits by route family (repo create/patch/fork/sync, then refs/credentials/grep/merge).problem+json gets no PR. It stays held as described in section 4.
Acceptance
instanceequal to the request path that the client sent.invalid_paramsappears only on 400 bodies with a nameable body field.erroranddetailtext do not change, and the content type staysapplication/json.I am starting PR 1 now. This plan needs no product decision: it changes no status codes and no message text.
Generated with
xum• Model:anthropic:claude-opus-5-5• Thinking:highThomasK33 commentedon Oct 13, 2026
PR 1 of the plan above landed: #420, merge
6b23ae9f. Plain problem bodies now carryinstance.c4b79752, both clean. A clean-context advisory recommended "ready".6b23ae9fpassed (run 38390906991).PRs 2 to 5 (specific codes, then
invalid_params) wait for a go-ahead from the desk. This issue stays open.Generated with
xum• Model:anthropic:claude-opus-5-5• Thinking:highThomasK33 commentedon Oct 13, 2026
Progress on the #352 plan: PR 2 and PR 3 landed.
insufficient_scope,repository_not_found,repo_name_mismatch)b14fa588branch_not_found,ref_not_found,credential_not_found,credential_exists,repository_deleted)5cadc73dA re-delete through the
DELETE /api/repos/{repo_name}alias stays 404repository_not_found, because its name lookup skips deleted repositories.docs/reference/http-api.mdrecords this.Next: PR 4 (
invalid_paramson decode failures), then PR 5 (single-field validators).Generated with
xum• Model:anthropic:claude-opus-5-5• Thinking:highThomasK33 commentedon Oct 13, 2026
PR 4 of the plan landed: #436, merge
66f5c329. A 400 for a request body that fails to decode now names the field ininvalid_params. Examples: wrong JSON type, including nestedbase_repo.*andupstream.*members and array elements such aspaths.0, and unknown, duplicate,nullor non-object members of strict bodies.Next: PR 5 (single-field validators). It also carries the advisory follow-ups.
Generated with
xum• Model:anthropic:claude-opus-5-5• Thinking:highThomasK33 commentedon Oct 13, 2026
The #352 plan has shipped. One item stays held.
All five PRs from the plan (issuecomment-6143529650) have landed.
erroranddetailtext, status codes and theapplication/jsoncontent type did not change. Every code change goes from the generic code to upstream's specific code.instanceon every plain problem body: the client's escaped request path, with the alias path for/api/repos/{repo_name}/...6b23ae9fwriteProblemproducer, plusinsufficient_scope,repository_not_found,repo_name_mismatchb14fa588branch_not_found,ref_not_found,credential_not_found,credential_exists,repository_deleted5cadc73dinvalid_paramson body decode failures: wrong JSON type, including nested members and array elements, and strict unknown, duplicate,nullor non-object members66f5c329invalid_paramsfrom single-field validators: required fields, allowed values, ranges, name formats on theValidationProblemDetailsroutese1beeeb8A test pins every specific code that open.storage emits to the vendored spec's "Specific codes:" list. A test also pins the problem member set to
ProblemDetailsplusinvalid_params. I ran dogfood A/B checks with the stable Go and TS SDKs (1.17.1) on each PR. The SDK output did not change.Still held:
application/problem+jsonThe content type stays
application/json. The stable SDKs (1.17.1, npmlatestas of 2026-10-13) parse error JSON only forapplication/json. Only the1.18.0-betaclients also acceptapplication/problem+json. Revisit trigger: a stable SDK release that acceptsapplication/problem+json. This issue stays open for that item.Recorded limits (no follow-up issues)
repo_claim_missing(open.storage answers 403),authentication_unavailable,route_not_found,merge_conflict(result envelope),sync_in_progress,upstream_already_configured, and the hosting, deployment, GitHub App and idempotency codes.DELETE /api/repos/{repo_name}answers 404repository_not_found, not 409repository_deleted. The alias's name lookup skips deleted repositories.invalid_paramsis left out for mutually exclusive members (base_repowithupstream), query parameters, delete-branch's name (query or body), and grep's "cursor past the matches" 400.Generated with
xum• Model:anthropic:claude-opus-5-5• Thinking:highThomasK33 commentedon Oct 13, 2026
Summary: PRs 1–5 shipped. Items 3, 4 and 5 of this issue stay held.
This comment corrects my previous one (issuecomment-6152364859), which said that only one item stays held. Against the tier list in this issue's body:
Shipped
b14fa588) and feat(api): emit upstream's specific not-found and conflict problem codes #435 (5cadc73d). Eight upstream codes on plain problem bodies.instance: feat(api): add RFC 9457 instance to problem-details error bodies #420 (6b23ae9f).invalid_paramson request-body 400s (the tier added in issuecomment-6052209313): feat(api): name the invalid field in invalid_params on body decode failures #436 (66f5c329) and feat(api): name invalid fields from the single-field body validators #443 (e1beeeb8).Still held. This issue stays open for them:
repo_claim_missing,authentication_unavailable,route_not_found,merge_conflict(result envelope),sync_in_progress,upstream_already_configured, and the hosting, deployment, GitHub App and idempotency codes.{"result":{"success":false,…}}, ADR-26).internal/githttp.application/problem+json. The stable SDKs (1.17.1) still parse error JSON only forapplication/json.The revisit triggers in the issue body do not change.
Generated with
xum• Model:anthropic:claude-opus-5-5• Thinking:highThomasK33 commentedon Oct 18, 2026
Upstream audit 2026-10-17: the docs spec now gives the merge 409 two bodies. An
application/jsonMergeConflictbody reports file conflicts and stale-target retries. AProblemDetailsbody (codeconflict) reports every other conflict. Our merge failures all use the result envelope asapplication/json(handlers_write_merge.go:128-143; recorded deviationwire-compatibility.md:203). This belongs to items 3 and 5 here and stays gated on G1 (no stable SDK accepts problem+json yet; stable is 1.17.1).Generated with
xum• Model:anthropic:claude-opus-5-5• Thinking:mediumThomasK33 commentedon Oct 27, 2026
Follow-up from #568 (PRs #571 and #573), item 3 of this issue:
expected_notes_ref_shaguard passed can still lose the notes-ref CAS at publish time. That 409 (" moved concurrently",internal/api/handlers_write_refs.go:1114at fix(api): add ref_guard_failed problem members to the notes 409s #573's head) keeps the plain note envelope, with no problem members.ref_guard_failedproblem members only to the stale-guard 409 ofPOST .../notes/appendand theDELETE .../notesquery form. Upstream's notes 409 text (specc494c01b) describes only the guard mismatch, so the lost-CAS 409 stays out of Upstream drift (spec 2026-10-27): failed ref guards answer 409 ref_guard_failed with guard and current_sha; vendor c494c01b #568.ref_guard_failed) for a lost notes CAS, or this issue's G1 gate fires. Owner: the desk, at the next upstream audit.The four 412 deviation rows (commit-pack, diff-commit, restore-commit, notes body forms) keep their #533 reasons. Their wire-compatibility rows now name
ref_guard_failed.Generated with
xum• Model:anthropic:claude-opus-5-5• Thinking:highThomasK33 commentedon Oct 29, 2026
Desk decision: redefine gate G1 (2026-10-29 audit)
Upstream's unmerged SDK branch
necolas/code-storage-pointersays the@pierre/storage/pierre-storage/code-storage-goline gets no new release, and names@pierre/sdk,pierre-sdkandgithub.com/pierrecomputer/sdk/goas replacements.@pierre/sdk0.1.0-canary.3(npm, Oct 27) already parses every error body as JSON, so it acceptsapplication/problem+json.G1 now fires on the first stable (non-canary, non-beta) release of
@pierre/sdk,pierre-sdkorgithub.com/pierrecomputer/sdk/go, or on a stable@pierre/storagerelease that accepts problem+json, whichever comes first. A README change or a canary does not fire it. The daily upstream audit watches the npm dist-tags, PyPI and the Go proxy for these packages.Note for the rest of this issue: the canary picks
RefGuardErrorbycode === "ref_guard_failed", so our #533 412 deviations will matter once it ships.Generated with
xum• Model:anthropic:claude-opus-5-5• Thinking:mediumThomasK33 commentedon Nov 4, 2026
Gate G1 fired on 2026-11-04. Stable releases: npm
@pierre/sdk0.1.0 (latest), PyPIpierre-sdk0.1.0 and Gogithub.com/pierrecomputer/sdk/gov0.1.0 (@latest, taggo/v0.1.0=5f1a4bc). The upstream changelog entrypierre-sdk-and-cli(2026-11-03) says the old SDKs get no new releases, and the pointer branch merged as SDK #66.SDK 0.1.0 parses every error body as JSON whatever the content type (Go
go/errors.go:186-210atgo/v0.1.0), so it acceptsapplication/problem+json, and it branches onref_guard_failed.The desk is assigning owners now. The canonical routes SDK 0.1.0 calls are tracked separately in #656.
Generated with
xum• Model:anthropic:claude-opus-5-5• Thinking:mediumThomasK33 commentedon Nov 4, 2026
Item 5: upstream documents
application/problem+jsonfor problem bodies. I am switching.Answer to the open question (checked 2026-11-04 against the vendored spec
412b90c3on main63f81faa):docs/upstream/openapi.yaml:3027) says: "Most endpoints return RFC 9457-compatible problem details underapplication/problem+json".application/jsonandapplication/problem+json. 646 of them give both the sameProblemDetailsschema.application/problem+jsoncarries the problem body (CommitWriteRefGuardProblemDetails,NotesWriteRefGuardProblemDetails,RestoreCommitRefGuardProblemDetails,ProblemDetails).application/jsoncarries the result envelope (CommitWriteResponse,MergeConflict) or plainProblemDetails.application/json. None is a problem body:CommitWriteResponse(14),ReposCommitsRestoreResponse(8),ReposUpstreamDeleteResponse(3).GET https://api.pierre.code.storage/api/reposanswers 401 withContent-Type: application/problem+jsonfor noAccept,Accept: application/json,Accept: application/problem+jsonandAccept: */*. Upstream does not negotiate.What each SDK line does with a
application/problem+jsonbody:go/runtime.go:386-395,go/errors.go:187-210atgo/v0.1.0)pierre/_runtime.py:393-400)json.loadsof the body, no content-type check. No change.dist/runtime/http.js:467)JSON.parseof the body text. No change.fetch.go:91, notesutil.go:154) and TypeScript (dist/index.js:1266, notes:2131)Content-Typecontainsapplication/json. On a problem+json body,APIError.Message/ApiError.messagebecomes the raw body text instead oferror, andBodybecomes that string. A note write's 409 guard body becomes a generic API error, not the note result.response.json()with no content-type check. No change.All three 0.1.0 SDKs send
Accept: application/jsonon JSON operations. Upstream answers problem+json to that request (point 5), so open.storage will too.Decision (desk rule: follow what the spec documents): every body that
newProblembuilds switches toapplication/problem+json. That coverswriteErr,writeProblem,writeInvalid, theref_guard_failedbodies (merge, notes append, the notes DELETE query form) and the git-surface 429. These stayapplication/json: success bodies, result envelopes (commit-pack, diff-commit, restore, reset, merge results and their failures), and theinternal/githttp{"error"}writers (item 4). The PR is marked breaking, with aBREAKING CHANGE:line that names the 1.17.1 Go and TS effect.Evidence (spec counts script, probe transcript) is in my scratch dir; the PR links back here.
Generated with
xum• Model:anthropic:claude-opus-5-5• Thinking:highThomasK33 commentedon Nov 4, 2026
Item 3: scope, from the vendored spec
412b90c3I checked every write route of the spec. Upstream documents a plain
ProblemDetailsbody (noresult) for 401, 403, 404, 429, 500 and 503 on every write route. It documents a result-envelope body only here:CommitWriteResponse)CommitWriteResponse)ReposCommitsRestoreResponse). 400 and 409 areProblemDetails.MergeConflict:error,conflict_type,conflict_paths,merge_base_sha, noresult). Every other status isProblemDetails.ProblemDetails, except the 409 guard body (NotesWriteRefGuardProblemDetails), which open.storage already sends.open.storage sends the ADR-26 result envelope on all of these: auth 401/403, the write-route 429, the alias and git-credentials failures, and every
wcFail,wmFailandwrefNoteFailstatus.Why this matters now: SDK 0.1.0 Go (
go/errors.go:209-275atgo/v0.1.0) turns any error body that has aresultmember into aRefUpdateError, before it looks at the status. So a 401 on merge gives*pierre.RefUpdateErrorwith the message "401: request failed" and nocode, where upstream givesAuthenticationErrorwithcodeanddetail. My item 5 dogfood shows this on branches/create, merge and notes/append.Decision (#234 rule: implement only what upstream documents): where upstream documents
ProblemDetails, open.storage sends the plain problem body and noresult. Where upstream documents the envelope, the envelope stays. Each PR is marked breaking for SDK 1.17.1, which builds aRefUpdateErrorfromresult.statuson some of these statuses. Three PRs:MergeConflictshape for merge conflicts.Not in item 3: the stale-guard 412s of the #533 deviations. #647 part 2 moves them to 409
ref_guard_failed, and another owner works on it.Generated with
xum• Model:anthropic:claude-opus-5-5• Thinking:highThomasK33 commentedon Nov 4, 2026
Item 4 (git smart-HTTP and LFS error writers): no change needed
Upstream documents no problem bodies for the git surface, so open.storage changes nothing in
internal/githttpfor this issue (#234 rule: implement only what upstream documents). Evidence, checked 2026-11-04:docs/upstream/openapi.yaml,ee8092bfon main) has only/api/...paths. It documents no smart-HTTP or LFS route and no error body for them.docs/upstream/guides/git-operations.md,git-lfs.md) and the live pagesdocs/repos/git-operations.mdanddocs/repos/git-lfs.mddescribe no error body. The live API overview, which defines the RFC 9457 error contract, covers the HTTP API and does not mention the git surface.GET https://pierre.code.storage/nope.git/info/refs?service=git-upload-packwithout credentials: 401,Content-Type: text/plain; charset=utf-8, bodyAuthentication required.GET https://pierre.code.storage/nope.git/info/lfs/objects/batchwithout credentials: 401,Content-Type: application/vnd.git-lfs+json, body{"message":"basic auth credentials are required"}.lfsError,internal/githttp/lfs.go) already sends the git-lfs{"message"}body withapplication/vnd.git-lfs+json. That is the shape the git-lfs client reads and the shape upstream sends.Two related facts, outside this item:
api.NewGitRateLimit,writeErr), so it already carries the problem members. Since feat(api)!: send application/problem+json on problem bodies #660 it isapplication/problem+json. Upstream documents no git-surface 429 body.{"error"}JSON (httpError,internal/githttp/server.go), where upstream sendstext/plain. Git clients print the body as text either way. That is not a problem-details question, so I did not change it here.Item 4 is closed by this evidence.
Generated with
xum• Model:anthropic:claude-opus-5-5• Thinking:highThomasK33 commentedon Nov 4, 2026
Done: items 5, 3, 1 and 4
All remaining items of this issue have landed. #672 closed the issue.
3c6bfde3application/problem+json.e0a48fbe74c96847resultenvelope only on the statuses upstream documents for it, and the plain problem body otherwise, withrepository_not_found,branch_not_foundandref_not_foundon the 404s.3c4b2b57MergeConflict. Every other merge and note-write failure is the plain problem body. A non-fast-forwardff_onlymerge and unrelated histories answer 412precondition_failed. A target that moved during a merge answers 409conflict, because open.storage has no automatic retry. Single-field 400s name the field ininvalid_params.8638fa95route_not_foundand 405method_not_allowed(withAllow) problem bodies for unmatched/api/paths, and 403authentication_unavailablewhen the key store fails (REST and fork source tokens).Codes that stay unsent, with the reason:
repo_claim_missing: since fix(auth): accept any non-empty sub and treat a token without repo as org-wide #666, a token without arepoclaim is org-wide, and a per-repo route refuses it with 403forbidden.merge_conflict: the merge file-conflict 409 isMergeConflict, which has nocodemember.target_movedandrepo_changed: upstream sends them only after an automatic retry, which open.storage does not do.sync_in_progress,upstream_already_configured: no upstream operation documents them.Related: #664 (upstream answers an invalid or expired token with 403, not 401) is open for desk triage. #673 is a comment-only follow-up from the #672 review.
Generated with
xum• Model:anthropic:claude-opus-5-5• Thinking:highdocs(api): give the preview caveat as examples, and name issue #352 i…