From 13acdab7fcd3af0ae7b8bf5f4d60a2c486be6131 Mon Sep 17 00:00:00 2001 From: saladday <1203511142@qq.com> Date: Wed, 23 Sep 2026 00:51:50 +0800 Subject: [PATCH 1/7] Record bounded official list-query comparison and acceptance scope --- CONTRIBUTING.md | 8 +++ contracts/agents-api/README.md | 2 + contracts/agents-api/list-query-semantics.md | 69 ++++++++++++++++++++ 3 files changed, 79 insertions(+) create mode 100644 contracts/agents-api/list-query-semantics.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8b4bf4ff4..0f521af86 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -57,6 +57,14 @@ to recover a lost creation response. Session metadata updates require a supplied metadata field, with null/empty clearing it. Validate an empty update before any resource lookup, after authentication. +List order parsing distinguishes omission from an explicit empty value. Reuse the +shared parser and error serializer, preserving the observed Beta, Files and Skills +error fields rather than applying one error code to every resource. Qualification +of one query error does not authorize changing page bounds, cursor ownership or +parent lookup order. Record uncertain range/lookup behavior separately; do not +reproduce observed upstream server failures as compatibility behavior. See +`contracts/agents-api/list-query-semantics.md` for the bounded evidence. + Keep runtime state, test artifacts and build output under `~/.parsar/`. Require absolute user-supplied working directories. Keep credentials out of source and logs. Update this guide when architecture, ownership or generated contracts change. diff --git a/contracts/agents-api/README.md b/contracts/agents-api/README.md index 7ffafabd8..c1154c3e3 100644 --- a/contracts/agents-api/README.md +++ b/contracts/agents-api/README.md @@ -1,6 +1,8 @@ # Agents API contract See the [58-operation evidence inventory](operation-evidence.md) for observed official behavior, local verification and remaining unknowns. +The [list-query comparison](list-query-semantics.md) distinguishes measured order +errors from unresolved range, cursor and lookup semantics. The external reference is [openai-python beta/agents](https://github.com/openai/openai-python/tree/d7c41efee1b0802b79f3f88a678ef2052b06e9ce/src/openai/resources/beta/agents), pinned in `upstream.json`. Its resource methods, corresponding types, pagination and streaming helpers define the compatibility target. This directory records diff --git a/contracts/agents-api/list-query-semantics.md b/contracts/agents-api/list-query-semantics.md new file mode 100644 index 000000000..878932d57 --- /dev/null +++ b/contracts/agents-api/list-query-semantics.md @@ -0,0 +1,69 @@ +# List query semantics — September 23, 2026 + +The fixed target remains openai-python 3.13.0, commit +`d7c41efee1b0802b79f3f88a678ef2052b06e9ce`, with `agents=v1` on beta resources. +This batch starts from Core main `c86b5bb` and addresses list order validation and +the error fields actually observed for those requests. It does not change page +limits, cursor lookup order, execution or native harness behavior. + +## Official observations + +Exactly 35 new read-only official GET requests were made, with no retries, resource +writes or model execution. Nested requests used random missing Session/Vault IDs; +top-level requests used missing cursors or a nonexistent Agent filter. Every +successful list was empty. Evidence, request IDs, fixed type copies and the full +matrix are retained under +`~/.parsar/remediation/20260923/error-query-survey/`. Earlier owned Session/Turn +observations are separately identified in that directory, not counted as new calls. + +The nine sampled collections rejected explicit `order=`. The fixed enum permits +only `asc` or `desc`; omission and an explicitly empty string are different inputs. + +| Measured request | Status | Error type | Code | Param | +| --- | --- | --- | --- | --- | +| Empty order on Agents, Sessions, Turns, Items, Templates, Vaults, Credentials | 400 | invalid_request_error | invalid_request_error | null | +| Empty order on Files | 400 | invalid_request_error | null | null | +| Empty order on Skills | 400 | invalid_request_error | invalid_value | order | +| Invalid status on Vaults/Credentials | 400 | invalid_request_error | invalid_request_error | null | + +These are operation-specific observations. Do not apply the Skills code to all +validation errors or make every Files error share one parameter. Other endpoints +using the same order enum may share the parser, but were not independently probed +here. Authentication and ownership checks retain their existing boundaries. + +## Deferred differences and uncertainty + +- Query limits require separate qualification. Missing cursors or parents can mask + a later numeric validation error. A successful filtered-empty request does not + establish useful zero-sized pagination or an actual maximum page capacity. +- An owned official Item list accepted 101 although its pinned type documents + 1–100; an owned Turn list rejected 101. Keep the fixed range until the conflict + is resolved explicitly. No general range change follows from this survey. +- Vault/Credential negative limits rejected on the official service, while the + pinned description broadly says values clamp to 1–100. Core's clamping policy + is unchanged in this batch. +- Files invalid purpose and missing cursor expose different `param` values; + purpose typing and cursor behavior need a separate bounded decision. The + verbose Files validation message and additional `detail` object are not a + reason to recreate an upstream validation framework. +- Two malformed Skills cursor observations returned 500, while a shaped missing + cursor returned 404. Retain the evidence; do not deliberately reproduce an + upstream failure or weaken safe missing-resource handling. +- Unknown/repeated query keys, whitespace cursors, numeric overflow, all status + combinations and concurrent-page mutation were not newly qualified. + +Current documentation was consulted alongside the pin, including the +[Session list reference](https://developers.openai.com/api/reference/go/resources/beta/subresources/agents/subresources/sessions/methods/list) +and [Vault list reference](https://developers.openai.com/api/reference/typescript/resources/beta/subresources/agents/subresources/vaults/methods/list). +New documentation and sampled tolerance do not silently replace the fixed SDK. + +## Acceptance boundary + +Acceptance must exercise fixed-SDK and raw HTTP requests against the actual Core +service and a dedicated PostgreSQL database: rejected queries, omitted/valid order, +scoped history and pagination, authentication, tenant isolation and no mutation +from rejected GETs. Controlled tests support the same contract. This read-only +change requires no new native-model capability qualification; existing real-model +evidence retains its original scope. Record completed checks and independent review +before merging; neither a deserializable response nor a route inventory proves +complete compatibility. From 27f72cf96a00ee6cb60cd09768d2ba977b6ee557 Mon Sep 17 00:00:00 2001 From: saladday <1203511142@qq.com> Date: Wed, 23 Sep 2026 00:52:09 +0800 Subject: [PATCH 2/7] Index read-only official query evidence --- contracts/agents-api/operation-evidence.md | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/agents-api/operation-evidence.md b/contracts/agents-api/operation-evidence.md index 321c6b261..f4b1126cd 100644 --- a/contracts/agents-api/operation-evidence.md +++ b/contracts/agents-api/operation-evidence.md @@ -25,6 +25,7 @@ Repository paths below are relative to the inspected worktree; private evidence | E | `~/.parsar/remediation/20260922/official-semantics-alignment/error-surface-probe.json`: missing non-beta File/Skill observations only. Not successful-resource or full error-surface coverage. | | C | `~/.parsar/remediation/20260922/official-semantics-alignment/live/acceptance-summary.json`, source `7c80d604ba47c578084ebc9fa99cff332bd5d5f2`. Seven resource/safety groups (DB), plus three real Turns per harness (Live): Codex/Claude Kimi K3; MiniMax M2.7. Successful runs: `live/resources/run-1790091139881592673`, `live/codex/run-1790091781322707360`, `live/claude_sdk/run-1790091781322554888`, `live/mcode/run-1790091781324544033`; each has `result.json` and `raw-evidence.json`, model runs also history/SSE evidence. Four network-interrupted attempts remain failures. No new native capability/OAuth refresh/E2B qualification. | | N | `contracts/agents-api/official-semantics-alignment.md`, September 23 admission section; private `~/.parsar/remediation/20260923/session-admission-alignment/{official,live}/`: conditional create/update official probes plus exact-source `7ccc636` Core/daemon real none execution (six Turns), write rejection, local retry, metadata and isolation. Qualified idle hosted admission and Codex self-hosted admission are not new execution profiles. | +| Q | [List query semantics](list-query-semantics.md); private `~/.parsar/remediation/20260923/error-query-survey/REPORT.md` and `request-index.md`: 35 read-only official GETs, nine empty-order rejections and family-specific error fields. Missing-parent/cursor cases do not qualify later numeric bounds. No model execution or resource creation. Core acceptance is recorded in the linked batch document when complete. | | A | `contracts/agents-api/official-semantics-alignment.md`: merged status/envelope/no-op/error/rejection changes and explicit remaining differences. `contracts/agents-api/README.md:63` supplies the current resource ledger; it is a coverage summary, not raw evidence. | | T | `contracts/agents-api/execution-tools.md`: per-operation input, function, required-action, structured-output, discovery and policy matrix; evidence register M1/M2/F1/F2/F3/S1/S2/D1/P1/E1/E2 gives exact private run paths. These are profile-specific real acceptances. The older blanket OAuth rejection in this document is superseded by O. | | D | `contracts/agents-api/README.md:115`, `contracts/agents-api/user-managed-runtime-v1.md`: recorded three-harness Docker MVP and separate user-managed Docker/E2B qualification. Historical `~/.parsar/remediation/20260920/three-harness-mvp/REPORT.md`, `acceptance-results.json` on zju_a100_2; prior Core-managed E2B qualification is retired-route evidence, not current enrollment qualification. | From 636e6a2f7dc6edac680942593d012ca510691953 Mon Sep 17 00:00:00 2001 From: saladday <1203511142@qq.com> Date: Wed, 23 Sep 2026 00:53:46 +0800 Subject: [PATCH 3/7] fix(agents-api): align list order and status errors --- .../internal/api/environment_files_query.go | 4 +- .../internal/api/environment_files_test.go | 3 + services/agents-api/internal/api/errors.go | 8 +- .../agents-api/internal/api/pagination.go | 23 +++-- .../internal/api/pagination_test.go | 99 ++++++++++++++++++- .../internal/api/source_files_list.go | 2 +- .../internal/api/source_files_list_test.go | 2 +- .../internal/api/vault_pagination.go | 4 +- .../internal/api/vault_pagination_test.go | 35 +++++++ 9 files changed, 165 insertions(+), 15 deletions(-) create mode 100644 services/agents-api/internal/api/vault_pagination_test.go diff --git a/services/agents-api/internal/api/environment_files_query.go b/services/agents-api/internal/api/environment_files_query.go index a4b82cd1f..d7050b4d9 100644 --- a/services/agents-api/internal/api/environment_files_query.go +++ b/services/agents-api/internal/api/environment_files_query.go @@ -29,7 +29,7 @@ func readEnvironmentFileQuery(w http.ResponseWriter, r *http.Request, environmen return options, false } for key, values := range q { - if !slices.Contains([]string{"path", "limit", "order", "page"}, key) || len(values) != 1 || values[0] == "" { + if !slices.Contains([]string{"path", "limit", "order", "page"}, key) || len(values) != 1 || (key != "order" && values[0] == "") { writeError(w, http.StatusBadRequest, "invalid_request", "Supported list parameters are path, limit, order and page, each supplied once with a nonempty value.") return options, false } @@ -40,7 +40,7 @@ func readEnvironmentFileQuery(w http.ResponseWriter, r *http.Request, environmen pageQuery[key] = values } } - page, ok := readPageQuery(w, pageQuery, true) + page, ok := readPageQuery(w, r, pageQuery, true) if !ok { return options, false } diff --git a/services/agents-api/internal/api/environment_files_test.go b/services/agents-api/internal/api/environment_files_test.go index e6a338f45..e80683aad 100644 --- a/services/agents-api/internal/api/environment_files_test.go +++ b/services/agents-api/internal/api/environment_files_test.go @@ -209,6 +209,9 @@ func TestEnvironmentFilesRejectsInvalidRequestsBeforeRead(t *testing.T) { if w.Code != 400 || f.lookups != 1 || f.reads != 0 { t.Fatal("invalid query reached runtime", w.Code, w.Body, f) } + if query == "order=" || query == "order=ASC" { + assertListQueryError(t, w, "invalid_request_error", nil, "Failed to deserialize query string: order: unknown variant `"+strings.TrimPrefix(query, "order=")+"`, expected `asc` or `desc`") + } }) } } diff --git a/services/agents-api/internal/api/errors.go b/services/agents-api/internal/api/errors.go index bcb02fcca..1934973f8 100644 --- a/services/agents-api/internal/api/errors.go +++ b/services/agents-api/internal/api/errors.go @@ -19,7 +19,7 @@ func writeJSON(w http.ResponseWriter, status int, value any) { _ = json.NewEncoder(w).Encode(value) } -func writeError(w http.ResponseWriter, status int, code, message string) { +func writeError(w http.ResponseWriter, status int, code, message string, param ...string) { kind := "invalid_request_error" if status >= 500 { kind = "server_error" @@ -32,7 +32,11 @@ func writeError(w http.ResponseWriter, status int, code, message string) { if code != "" { errorCode = &code } - writeJSON(w, status, v1.ErrorResponse{Error: v1.APIError{Message: message, Type: kind, Code: errorCode}}) + var errorParam *string + if len(param) > 0 { + errorParam = ¶m[0] + } + writeJSON(w, status, v1.ErrorResponse{Error: v1.APIError{Message: message, Type: kind, Code: errorCode, Param: errorParam}}) } func writeStoreError(w http.ResponseWriter, r *http.Request, err error) { diff --git a/services/agents-api/internal/api/pagination.go b/services/agents-api/internal/api/pagination.go index b3385e0fe..a4df0317c 100644 --- a/services/agents-api/internal/api/pagination.go +++ b/services/agents-api/internal/api/pagination.go @@ -20,14 +20,14 @@ func readPage(w http.ResponseWriter, r *http.Request, extraKeys ...string) (page } func readPageSize(w http.ResponseWriter, r *http.Request, rejectLarger bool, extraKeys ...string) (pageOptions, bool) { - return readPageQuery(w, r.URL.Query(), rejectLarger, extraKeys...) + return readPageQuery(w, r, r.URL.Query(), rejectLarger, extraKeys...) } -func readPageQuery(w http.ResponseWriter, q url.Values, rejectLarger bool, extraKeys ...string) (pageOptions, bool) { - return readPageQueryLimits(w, q, 20, 100, rejectLarger, extraKeys...) +func readPageQuery(w http.ResponseWriter, r *http.Request, q url.Values, rejectLarger bool, extraKeys ...string) (pageOptions, bool) { + return readPageQueryLimits(w, r, q, 20, 100, rejectLarger, extraKeys...) } -func readPageQueryLimits(w http.ResponseWriter, q url.Values, defaultLimit, maxLimit int, rejectLarger bool, extraKeys ...string) (pageOptions, bool) { +func readPageQueryLimits(w http.ResponseWriter, r *http.Request, q url.Values, defaultLimit, maxLimit int, rejectLarger bool, extraKeys ...string) (pageOptions, bool) { keys := append([]string{"after", "limit", "order"}, extraKeys...) for key, values := range q { if !slices.Contains(keys, key) || len(values) != 1 { @@ -50,9 +50,20 @@ func readPageQueryLimits(w http.ResponseWriter, q url.Values, defaultLimit, maxL } limit = int(min(requested, int64(maxLimit))) } - if order != "" && order != "asc" && order != "desc" { - writeError(w, http.StatusBadRequest, "invalid_request", "order must be asc or desc.") + if _, supplied := q["order"]; supplied && order != "asc" && order != "desc" { + writeListOrderError(w, r, order) return pageOptions{}, false } return pageOptions{after: strings.TrimSpace(q.Get("after")), limit: limit, ascending: order == "asc"}, true } + +func writeListOrderError(w http.ResponseWriter, r *http.Request, order string) { + switch { + case r.URL.Path == "/v1/files" || strings.HasPrefix(r.URL.Path, "/v1/files/"): + writeError(w, http.StatusBadRequest, "", "order must be asc or desc.") + case r.URL.Path == "/v1/skills" || strings.HasPrefix(r.URL.Path, "/v1/skills/"): + writeError(w, http.StatusBadRequest, "invalid_value", fmt.Sprintf("Invalid value: '%s'. Supported values are: 'asc' and 'desc'.", order), "order") + default: + writeError(w, http.StatusBadRequest, "invalid_request_error", fmt.Sprintf("Failed to deserialize query string: order: unknown variant `%s`, expected `asc` or `desc`", order)) + } +} diff --git a/services/agents-api/internal/api/pagination_test.go b/services/agents-api/internal/api/pagination_test.go index f90e9392a..e3e135ae0 100644 --- a/services/agents-api/internal/api/pagination_test.go +++ b/services/agents-api/internal/api/pagination_test.go @@ -1,7 +1,12 @@ package api import ( + "encoding/json" + "fmt" + "net/http" "net/http/httptest" + "net/url" + "reflect" "testing" ) @@ -16,7 +21,7 @@ func TestPageSizePolicy(t *testing.T) { {"limit=9223372036854775808", false, false, 0}, {"limit=0", false, false, 0}, {"limit=-1", false, false, 0}, {"limit=1.5", false, false, 0}, {"limit=", false, false, 0}, {"limit=null", false, false, 0}, {"limit=2&limit=3", false, false, 0}, {"order=invalid", false, false, 0}, - {"tenant_id=other", false, false, 0}, + {"order=", false, false, 0}, {"tenant_id=other", false, false, 0}, } { t.Run(test.query, func(t *testing.T) { for _, strict := range []bool{true, false} { @@ -34,3 +39,95 @@ func TestPageSizePolicy(t *testing.T) { }) } } + +func TestPageOrderPolicy(t *testing.T) { + for _, path := range []string{"/v1/agents", "/v1/files", "/v1/skills", "/v1/skills/skill-example/versions"} { + for _, test := range []struct { + query string + ascending bool + }{{"", false}, {"order=asc", true}, {"order=desc", false}} { + t.Run(path+"?"+test.query, func(t *testing.T) { + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodGet, path+"?"+test.query, nil) + page, ok := readPage(w, r) + if !ok || page.ascending != test.ascending || page.limit != 20 || w.Body.Len() != 0 { + t.Fatalf("page=%+v ok=%t response=%s", page, ok, w.Body.String()) + } + }) + } + } +} + +func TestListOrderErrorEnvelopes(t *testing.T) { + for _, test := range []struct { + path string + code, param any + message string + }{ + {"/v1/agents", "invalid_request_error", nil, "Failed to deserialize query string: order: unknown variant `%s`, expected `asc` or `desc`"}, + {"/v1/agents/environments/environment-example/files", "invalid_request_error", nil, "Failed to deserialize query string: order: unknown variant `%s`, expected `asc` or `desc`"}, + {"/v1/files", nil, nil, "order must be asc or desc."}, + {"/v1/skills", "invalid_value", "order", "Invalid value: '%s'. Supported values are: 'asc' and 'desc'."}, + {"/v1/skills/skill-example/versions", "invalid_value", "order", "Invalid value: '%s'. Supported values are: 'asc' and 'desc'."}, + } { + for _, order := range []string{"", "invalid", "ASC", " "} { + t.Run(test.path+"/"+order, func(t *testing.T) { + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodGet, test.path+"?order="+url.QueryEscape(order), nil) + if _, ok := readPage(w, r); ok { + t.Fatal("invalid order accepted") + } + message := test.message + if test.code != nil { + message = fmt.Sprintf(message, order) + } + assertListQueryError(t, w, test.code, test.param, message) + }) + } + } +} + +func TestListOrderValidationPreservesOtherQueryErrors(t *testing.T) { + for _, test := range []struct{ query, code string }{ + {"order=&limit=0", "invalid_request"}, + {"order=asc&order=desc", "unsupported_parameter"}, + {"order=&tenant_id=other", "unsupported_parameter"}, + } { + t.Run(test.query, func(t *testing.T) { + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodGet, "/v1/skills?"+test.query, nil) + if _, ok := readPage(w, r); ok { + t.Fatal("invalid query accepted") + } + var response struct { + Error struct { + Code string + Param *string + } + } + if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { + t.Fatal(err) + } + if w.Code != http.StatusBadRequest || response.Error.Code != test.code || response.Error.Param != nil { + t.Fatalf("other query error changed: %d %s", w.Code, w.Body.String()) + } + }) + } +} + +func assertListQueryError(t *testing.T, w *httptest.ResponseRecorder, code, param any, message string) { + t.Helper() + var body map[string]any + if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil { + t.Fatal(err) + } + want := map[string]any{"error": map[string]any{ + "type": "invalid_request_error", "code": code, "param": param, "message": message, + }} + if w.Code != http.StatusBadRequest || !reflect.DeepEqual(body, want) { + t.Fatalf("error envelope: status=%d body=%s want=%v", w.Code, w.Body.String(), want) + } + if w.Header().Get("Content-Type") != "application/json" || w.Header().Get("Cache-Control") != "no-store" { + t.Fatalf("error headers changed: %v", w.Header()) + } +} diff --git a/services/agents-api/internal/api/source_files_list.go b/services/agents-api/internal/api/source_files_list.go index d29365089..e5c97fab6 100644 --- a/services/agents-api/internal/api/source_files_list.go +++ b/services/agents-api/internal/api/source_files_list.go @@ -44,7 +44,7 @@ func (h *Handler) listSourceFiles(w http.ResponseWriter, r *http.Request) { func readSourceFilePage(w http.ResponseWriter, r *http.Request) (pageOptions, *string, bool) { q := r.URL.Query() - options, ok := readPageQueryLimits(w, q, 10000, 10000, true, "purpose") + options, ok := readPageQueryLimits(w, r, q, 10000, 10000, true, "purpose") if !ok { return pageOptions{}, nil, false } diff --git a/services/agents-api/internal/api/source_files_list_test.go b/services/agents-api/internal/api/source_files_list_test.go index e95a216fc..f665fc958 100644 --- a/services/agents-api/internal/api/source_files_list_test.go +++ b/services/agents-api/internal/api/source_files_list_test.go @@ -45,7 +45,7 @@ func TestSourceFileListParametersAndEnvelope(t *testing.T) { func TestSourceFileListRejectsInvalidQueriesBeforeStorage(t *testing.T) { for _, query := range []string{ "limit=", "limit=0", "limit=10001", "limit=1.5", "limit=1&limit=2", - "order=invalid", "after=a&after=b", "purpose=a&purpose=b", "tenant_id=foreign", + "order=", "order=invalid", "after=a&after=b", "purpose=a&purpose=b", "tenant_id=foreign", } { f := &sourceFilesFixture{} h, _ := environmentFileCreateHandler(t, WithSourceFiles(f)) diff --git a/services/agents-api/internal/api/vault_pagination.go b/services/agents-api/internal/api/vault_pagination.go index 9290e11e3..5f9b614ee 100644 --- a/services/agents-api/internal/api/vault_pagination.go +++ b/services/agents-api/internal/api/vault_pagination.go @@ -19,7 +19,7 @@ func readVaultPage(w http.ResponseWriter, r *http.Request) (pageOptions, []strin } for _, status := range statuses { if status != "active" && status != "archived" { - writeError(w, http.StatusBadRequest, "invalid_request", "status must be active or archived.") + writeError(w, http.StatusBadRequest, "invalid_request_error", "Failed to deserialize query string: status: data did not match any variant of untagged enum VaultStatusFilterParam") return pageOptions{}, nil, false } } @@ -34,6 +34,6 @@ func readVaultPage(w http.ResponseWriter, r *http.Request) (pageOptions, []strin } q.Set("limit", strconv.FormatInt(max(1, min(requested, 100)), 10)) } - options, ok := readPageQuery(w, q, false) + options, ok := readPageQuery(w, r, q, false) return options, statuses, ok } diff --git a/services/agents-api/internal/api/vault_pagination_test.go b/services/agents-api/internal/api/vault_pagination_test.go new file mode 100644 index 000000000..1fe129480 --- /dev/null +++ b/services/agents-api/internal/api/vault_pagination_test.go @@ -0,0 +1,35 @@ +package api + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func TestVaultStatusErrorEnvelopes(t *testing.T) { + for _, path := range []string{"/v1/vaults", "/v1/vaults/vault-example/credentials"} { + for _, query := range []string{"status=invalid", "status=", "status[]=active&status[]=invalid", "status[]="} { + t.Run(path+"?"+query, func(t *testing.T) { + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodGet, path+"?"+query, nil) + if _, _, ok := readVaultPage(w, r); ok { + t.Fatal("invalid status accepted") + } + assertListQueryError(t, w, "invalid_request_error", nil, "Failed to deserialize query string: status: data did not match any variant of untagged enum VaultStatusFilterParam") + }) + } + } +} + +func TestVaultStatusGrammarUnchanged(t *testing.T) { + for _, query := range []string{"status=active&status=archived", "status=active&status[]=archived"} { + t.Run(query, func(t *testing.T) { + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodGet, "/v1/vaults?"+query, nil) + if _, _, ok := readVaultPage(w, r); ok { + t.Fatal("invalid status grammar accepted") + } + assertListQueryError(t, w, "invalid_request", nil, "Supply status once or use status[] for an array.") + }) + } +} From 1c8976bbc3e543c459a9be7eb8e8ae946b99e675 Mon Sep 17 00:00:00 2001 From: saladday <1203511142@qq.com> Date: Wed, 23 Sep 2026 00:54:36 +0800 Subject: [PATCH 4/7] Document list order presence in generated API contract --- contracts/agents-api/openapi.yaml | 48 ++++++++++++------- .../agents-api/internal/api/agents_list.go | 2 +- .../internal/api/credentials_list.go | 2 +- .../internal/api/environment_files.go | 2 +- .../internal/api/environment_templates.go | 2 +- services/agents-api/internal/api/handler.go | 2 +- services/agents-api/internal/api/items.go | 2 +- .../internal/api/session_artifacts.go | 2 +- .../agents-api/internal/api/skills_list.go | 4 +- .../internal/api/source_files_list.go | 2 +- .../agents-api/internal/api/subagent_turns.go | 4 +- services/agents-api/internal/api/subagents.go | 4 +- services/agents-api/internal/api/turns.go | 2 +- .../agents-api/internal/api/vaults_list.go | 2 +- 14 files changed, 48 insertions(+), 32 deletions(-) diff --git a/contracts/agents-api/openapi.yaml b/contracts/agents-api/openapi.yaml index 40ccff928..008de4ff4 100644 --- a/contracts/agents-api/openapi.yaml +++ b/contracts/agents-api/openapi.yaml @@ -2050,7 +2050,8 @@ paths: name: limit type: integer - default: desc - description: Creation order + description: Creation order; omit for descending, explicit empty values are + invalid enum: - asc - desc @@ -2373,7 +2374,8 @@ paths: name: limit type: integer - default: desc - description: Case-sensitive path-component order + description: Case-sensitive path-component order; omit for descending, explicit + empty values are invalid enum: - asc - desc @@ -2509,7 +2511,8 @@ paths: name: limit type: integer - default: desc - description: Creation order + description: Creation order; omit for descending, explicit empty values are + invalid enum: - asc - desc @@ -2772,7 +2775,8 @@ paths: name: limit type: integer - default: desc - description: Creation order + description: Creation order; omit for descending, explicit empty values are + invalid enum: - asc - desc @@ -3133,7 +3137,8 @@ paths: name: limit type: integer - default: desc - description: Publication order + description: Publication order; omit for descending, explicit empty values + are invalid enum: - asc - desc @@ -3497,7 +3502,8 @@ paths: name: limit type: integer - default: desc - description: Creation order + description: Creation order; omit for descending, explicit empty values are + invalid enum: - asc - desc @@ -3559,7 +3565,8 @@ paths: name: limit type: integer - default: desc - description: Resource order + description: Resource order; omit for descending, explicit empty values are + invalid enum: - asc - desc @@ -3683,7 +3690,8 @@ paths: name: limit type: integer - default: desc - description: Resource order + description: Resource order; omit for descending, explicit empty values are + invalid enum: - asc - desc @@ -3754,7 +3762,8 @@ paths: name: limit type: integer - default: desc - description: Creation order + description: Creation order; omit for descending, explicit empty values are + invalid enum: - asc - desc @@ -3888,7 +3897,8 @@ paths: name: limit type: integer - default: desc - description: Resource order + description: Resource order; omit for descending, explicit empty values are + invalid enum: - asc - desc @@ -3955,7 +3965,8 @@ paths: name: limit type: integer - default: desc - description: Creation order + description: Creation order; omit for descending, explicit empty values are + invalid enum: - asc - desc @@ -4056,7 +4067,8 @@ paths: name: limit type: integer - default: desc - description: Creation order + description: Creation order; omit for descending, explicit empty values are + invalid enum: - asc - desc @@ -4296,7 +4308,8 @@ paths: in: query name: limit type: integer - - description: Creation order + - description: Creation order; omit for descending, explicit empty values are + invalid enum: - asc - desc @@ -4452,7 +4465,8 @@ paths: in: query name: limit type: integer - - description: Version order + - description: Version order; omit for descending, explicit empty values are + invalid enum: - asc - desc @@ -4601,7 +4615,8 @@ paths: name: limit type: integer - default: desc - description: Creation order + description: Creation order; omit for descending, explicit empty values are + invalid enum: - asc - desc @@ -4827,7 +4842,8 @@ paths: name: limit type: integer - default: desc - description: Creation order + description: Creation order; omit for descending, explicit empty values are + invalid enum: - asc - desc diff --git a/services/agents-api/internal/api/agents_list.go b/services/agents-api/internal/api/agents_list.go index 102df3e89..505211fcd 100644 --- a/services/agents-api/internal/api/agents_list.go +++ b/services/agents-api/internal/api/agents_list.go @@ -14,7 +14,7 @@ import ( // @Param OpenAI-Beta header string true "agents=v1" // @Param after query string false "Last Agent ID from the previous page" // @Param limit query int64 false "Maximum requested resources; pages contain at most 100" minimum(1) -// @Param order query string false "Creation order" Enums(asc,desc) default(desc) +// @Param order query string false "Creation order; omit for descending, explicit empty values are invalid" Enums(asc,desc) default(desc) // @Success 200 {object} v1.SavedAgentList // @Failure 400,401,404,500 {object} v1.ErrorResponse // @Router /agents [get] diff --git a/services/agents-api/internal/api/credentials_list.go b/services/agents-api/internal/api/credentials_list.go index 07fd78b92..a324d5be9 100644 --- a/services/agents-api/internal/api/credentials_list.go +++ b/services/agents-api/internal/api/credentials_list.go @@ -15,7 +15,7 @@ import ( // @Param vault_id path string true "Vault ID" // @Param after query string false "Last Credential ID from the previous page" // @Param limit query integer false "Requested page size, clamped to 1–100" default(20) -// @Param order query string false "Creation order" Enums(asc,desc) default(desc) +// @Param order query string false "Creation order; omit for descending, explicit empty values are invalid" Enums(asc,desc) default(desc) // @Param status query string false "Scalar status filter" Enums(active,archived) // @Param status[] query []string false "Array status filter; cannot be combined with status" collectionFormat(multi) Enums(active,archived) // @Success 200 {object} v1.CredentialList diff --git a/services/agents-api/internal/api/environment_files.go b/services/agents-api/internal/api/environment_files.go index 3be4fe99a..13f5b559a 100644 --- a/services/agents-api/internal/api/environment_files.go +++ b/services/agents-api/internal/api/environment_files.go @@ -32,7 +32,7 @@ func WithEnvironmentDirectoryReader(reader EnvironmentDirectoryReader) Option { // @Param environment_id path string true "Environment ID" // @Param path query string false "Absolute directory inside the Environment workspace" // @Param limit query int false "Maximum file count; local default 20" minimum(1) maximum(100) -// @Param order query string false "Case-sensitive path-component order" Enums(asc,desc) default(desc) +// @Param order query string false "Case-sensitive path-component order; omit for descending, explicit empty values are invalid" Enums(asc,desc) default(desc) // @Param page query string false "Opaque continuation token; keep path, order and limit unchanged" // @Success 200 {object} v1.EnvironmentFileList // @Failure 400,401,404,500,503 {object} v1.ErrorResponse diff --git a/services/agents-api/internal/api/environment_templates.go b/services/agents-api/internal/api/environment_templates.go index 32a1cce1c..832bd49be 100644 --- a/services/agents-api/internal/api/environment_templates.go +++ b/services/agents-api/internal/api/environment_templates.go @@ -194,7 +194,7 @@ func (h *Handler) deleteEnvironmentTemplate(w http.ResponseWriter, r *http.Reque // @Param OpenAI-Beta header string true "agents=v1" // @Param after query string false "Previous Template ID" // @Param limit query integer false "Page size" default(20) minimum(1) maximum(100) -// @Param order query string false "Creation order" Enums(asc,desc) default(desc) +// @Param order query string false "Creation order; omit for descending, explicit empty values are invalid" Enums(asc,desc) default(desc) // @Success 200 {object} v1.EnvironmentTemplateList // @Failure 400,401,404,500 {object} v1.ErrorResponse // @Router /agents/environments/templates [get] diff --git a/services/agents-api/internal/api/handler.go b/services/agents-api/internal/api/handler.go index e490cee5a..d61f3e349 100644 --- a/services/agents-api/internal/api/handler.go +++ b/services/agents-api/internal/api/handler.go @@ -322,7 +322,7 @@ func (h *Handler) respondSessionStatus(w http.ResponseWriter, r *http.Request, s // @Param agent_id query string false "Root Agent ID whose Sessions to return" // @Param after query string false "Last Session ID from the previous page" // @Param limit query int false "Page size" minimum(1) maximum(100) default(20) -// @Param order query string false "Creation order" Enums(asc,desc) default(desc) +// @Param order query string false "Creation order; omit for descending, explicit empty values are invalid" Enums(asc,desc) default(desc) // @Success 200 {object} v1.SessionList // @Failure 400,401,404,500 {object} v1.ErrorResponse // @Router /agents/sessions [get] diff --git a/services/agents-api/internal/api/items.go b/services/agents-api/internal/api/items.go index 43b7500fd..6b0910e9b 100644 --- a/services/agents-api/internal/api/items.go +++ b/services/agents-api/internal/api/items.go @@ -14,7 +14,7 @@ import ( // @Param session_id path string true "Session ID" // @Param after query string false "Last Item ID from the previous page" // @Param limit query int false "Page size" minimum(1) maximum(100) default(20) -// @Param order query string false "Creation order" Enums(asc,desc) default(desc) +// @Param order query string false "Creation order; omit for descending, explicit empty values are invalid" Enums(asc,desc) default(desc) // @Success 200 {object} v1.ItemList // @Failure 400,401,404,500 {object} v1.ErrorResponse // @Router /agents/sessions/{session_id}/items [get] diff --git a/services/agents-api/internal/api/session_artifacts.go b/services/agents-api/internal/api/session_artifacts.go index 32aad8332..ecadde853 100644 --- a/services/agents-api/internal/api/session_artifacts.go +++ b/services/agents-api/internal/api/session_artifacts.go @@ -44,7 +44,7 @@ func (h *Handler) artifactsReady(w http.ResponseWriter, r *http.Request, list bo // @Param environment_id query string false "Producing Environment ID" // @Param after query string false "Last immutable artifact ID" // @Param limit query int false "Page size" minimum(1) maximum(100) default(20) -// @Param order query string false "Publication order" Enums(asc,desc) default(desc) +// @Param order query string false "Publication order; omit for descending, explicit empty values are invalid" Enums(asc,desc) default(desc) // @Success 200 {object} v1.SessionArtifactList // @Failure 400,401,404,500,503 {object} v1.ErrorResponse // @Router /agents/sessions/{session_id}/artifacts [get] diff --git a/services/agents-api/internal/api/skills_list.go b/services/agents-api/internal/api/skills_list.go index abcc4d5f6..6692df0c4 100644 --- a/services/agents-api/internal/api/skills_list.go +++ b/services/agents-api/internal/api/skills_list.go @@ -14,7 +14,7 @@ import ( // @Security BearerAuth // @Param after query string false "Skill resource cursor" // @Param limit query integer false "Page size" -// @Param order query string false "Creation order" Enums(asc,desc) +// @Param order query string false "Creation order; omit for descending, explicit empty values are invalid" Enums(asc,desc) // @Success 200 {object} v1.SkillList // @Router /skills [get] func (h *Handler) listSkills(w http.ResponseWriter, r *http.Request) { @@ -49,7 +49,7 @@ func (h *Handler) listSkills(w http.ResponseWriter, r *http.Request) { // @Param skill_id path string true "Skill ID" // @Param after query string false "Version resource cursor" // @Param limit query integer false "Page size" -// @Param order query string false "Version order" Enums(asc,desc) +// @Param order query string false "Version order; omit for descending, explicit empty values are invalid" Enums(asc,desc) // @Success 200 {object} v1.SkillVersionList // @Router /skills/{skill_id}/versions [get] func (h *Handler) listSkillVersions(w http.ResponseWriter, r *http.Request) { diff --git a/services/agents-api/internal/api/source_files_list.go b/services/agents-api/internal/api/source_files_list.go index e5c97fab6..9aa057377 100644 --- a/services/agents-api/internal/api/source_files_list.go +++ b/services/agents-api/internal/api/source_files_list.go @@ -13,7 +13,7 @@ import ( // @Security BearerAuth // @Param after query string false "Last File ID from the previous page" // @Param limit query integer false "Maximum page size, 1–10000" default(10000) minimum(1) maximum(10000) -// @Param order query string false "Creation order" Enums(asc,desc) default(desc) +// @Param order query string false "Creation order; omit for descending, explicit empty values are invalid" Enums(asc,desc) default(desc) // @Param purpose query string false "Only return Files with this purpose" // @Success 200 {object} v1.SourceFileList // @Failure 400,401,404,500,503 {object} v1.ErrorResponse diff --git a/services/agents-api/internal/api/subagent_turns.go b/services/agents-api/internal/api/subagent_turns.go index 0f9afbc67..d090f1f3e 100644 --- a/services/agents-api/internal/api/subagent_turns.go +++ b/services/agents-api/internal/api/subagent_turns.go @@ -44,7 +44,7 @@ func (h *Handler) getSubagentTurn(w http.ResponseWriter, r *http.Request) { // @Param subagent_id path string true "Subagent ID" // @Param after query string false "Last Turn ID from the previous page" // @Param limit query int false "Page size" minimum(1) maximum(100) default(20) -// @Param order query string false "Creation order" Enums(asc,desc) default(desc) +// @Param order query string false "Creation order; omit for descending, explicit empty values are invalid" Enums(asc,desc) default(desc) // @Success 200 {object} v1.TurnList // @Failure 400,401,404,500,503 {object} v1.ErrorResponse // @Router /agents/sessions/{session_id}/subagents/{subagent_id}/turns [get] @@ -72,7 +72,7 @@ func (h *Handler) listSubagentTurns(w http.ResponseWriter, r *http.Request) { // @Param turn_id path string true "Turn ID" // @Param after query string false "Last Item ID from the previous page" // @Param limit query int false "Page size" minimum(1) maximum(100) default(20) -// @Param order query string false "Resource order" Enums(asc,desc) default(desc) +// @Param order query string false "Resource order; omit for descending, explicit empty values are invalid" Enums(asc,desc) default(desc) // @Success 200 {object} v1.ItemList // @Failure 400,401,404,500,503 {object} v1.ErrorResponse // @Router /agents/sessions/{session_id}/subagents/{subagent_id}/turns/{turn_id}/items [get] diff --git a/services/agents-api/internal/api/subagents.go b/services/agents-api/internal/api/subagents.go index 93e1b307b..a7a54306b 100644 --- a/services/agents-api/internal/api/subagents.go +++ b/services/agents-api/internal/api/subagents.go @@ -75,7 +75,7 @@ func (h *Handler) getSubagent(w http.ResponseWriter, r *http.Request) { // @Param session_id path string true "Session ID" // @Param after query string false "Last Subagent ID from the previous page" // @Param limit query int false "Page size" minimum(1) maximum(100) default(20) -// @Param order query string false "Resource order" Enums(asc,desc) default(desc) +// @Param order query string false "Resource order; omit for descending, explicit empty values are invalid" Enums(asc,desc) default(desc) // @Success 200 {object} v1.SubagentList // @Failure 400,401,404,500,503 {object} v1.ErrorResponse // @Router /agents/sessions/{session_id}/subagents [get] @@ -105,7 +105,7 @@ func (h *Handler) listSubagents(w http.ResponseWriter, r *http.Request) { // @Param subagent_id path string true "Subagent ID" // @Param after query string false "Last Item ID from the previous page" // @Param limit query int false "Page size" minimum(1) maximum(100) default(20) -// @Param order query string false "Resource order" Enums(asc,desc) default(desc) +// @Param order query string false "Resource order; omit for descending, explicit empty values are invalid" Enums(asc,desc) default(desc) // @Success 200 {object} v1.ItemList // @Failure 400,401,404,500,503 {object} v1.ErrorResponse // @Router /agents/sessions/{session_id}/subagents/{subagent_id}/items [get] diff --git a/services/agents-api/internal/api/turns.go b/services/agents-api/internal/api/turns.go index cd5226e23..ded7ebb91 100644 --- a/services/agents-api/internal/api/turns.go +++ b/services/agents-api/internal/api/turns.go @@ -54,7 +54,7 @@ func (h *Handler) getTurn(w http.ResponseWriter, r *http.Request) { // @Param session_id path string true "Session ID" // @Param after query string false "Last Turn ID from the previous page" // @Param limit query int false "Page size" minimum(1) maximum(100) default(20) -// @Param order query string false "Creation order" Enums(asc,desc) default(desc) +// @Param order query string false "Creation order; omit for descending, explicit empty values are invalid" Enums(asc,desc) default(desc) // @Success 200 {object} v1.TurnList // @Failure 400,401,404,500 {object} v1.ErrorResponse // @Router /agents/sessions/{session_id}/turns [get] diff --git a/services/agents-api/internal/api/vaults_list.go b/services/agents-api/internal/api/vaults_list.go index 57aa2973a..862915863 100644 --- a/services/agents-api/internal/api/vaults_list.go +++ b/services/agents-api/internal/api/vaults_list.go @@ -14,7 +14,7 @@ import ( // @Param OpenAI-Beta header string true "agents=v1" // @Param after query string false "Last Vault ID from the previous page" // @Param limit query integer false "Requested page size, clamped to 1–100" default(20) -// @Param order query string false "Creation order" Enums(asc,desc) default(desc) +// @Param order query string false "Creation order; omit for descending, explicit empty values are invalid" Enums(asc,desc) default(desc) // @Param status query string false "Scalar status filter" Enums(active,archived) // @Param status[] query []string false "Array status filter; cannot be combined with status" collectionFormat(multi) Enums(active,archived) // @Success 200 {object} v1.VaultList From d64634c927de4dec9b07c9568a60d5b6abc5a2de Mon Sep 17 00:00:00 2001 From: saladday <1203511142@qq.com> Date: Wed, 23 Sep 2026 00:56:13 +0800 Subject: [PATCH 5/7] Distinguish empty SDK query omission from raw HTTP validation --- contracts/agents-api/list-query-semantics.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/contracts/agents-api/list-query-semantics.md b/contracts/agents-api/list-query-semantics.md index 878932d57..aa6eefd83 100644 --- a/contracts/agents-api/list-query-semantics.md +++ b/contracts/agents-api/list-query-semantics.md @@ -31,6 +31,12 @@ validation errors or make every Files error share one parameter. Other endpoints using the same order enum may share the parser, but were not independently probed here. Authentication and ownership checks retain their existing boundaries. +The fixed Python SDK's query serializer drops empty string values. Consequently, +`list(order="")` does not send `order=` and follows omission/default behavior. +Explicit-empty rejection requires raw HTTP evidence; nonempty invalid SDK order +values exercise the rejected path normally. Do not alter Core or the SDK to hide +that request-serialization distinction. + ## Deferred differences and uncertainty - Query limits require separate qualification. Missing cursors or parents can mask From e4cb8a5540981ae852b5936340939dc01eb3d33d Mon Sep 17 00:00:00 2001 From: saladday <1203511142@qq.com> Date: Wed, 23 Sep 2026 00:56:38 +0800 Subject: [PATCH 6/7] Verify list query errors through the fixed official client and PostgreSQL --- .../internal/store/list_query_public_test.go | 66 +++++++ .../agents-api/tests/official_list_query.py | 164 ++++++++++++++++++ 2 files changed, 230 insertions(+) create mode 100644 services/agents-api/internal/store/list_query_public_test.go create mode 100644 services/agents-api/tests/official_list_query.py diff --git a/services/agents-api/internal/store/list_query_public_test.go b/services/agents-api/internal/store/list_query_public_test.go new file mode 100644 index 000000000..6faeaeeda --- /dev/null +++ b/services/agents-api/internal/store/list_query_public_test.go @@ -0,0 +1,66 @@ +package store_test + +import ( + "bytes" + "context" + "net/http/httptest" + "os" + "os/exec" + "testing" + "time" + + "github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/device" + "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/api" + "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/credentialcrypto" + "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/execution" + "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/store" + "github.com/google/uuid" +) + +func TestListQueryOfficialClientPostgres(t *testing.T) { + python := os.Getenv("PARSAR_OFFICIAL_SDK_PYTHON") + if python == "" { + t.Skip("pinned official Python SDK required") + } + _, pool := store.NewTestStore(t) + cipher, err := credentialcrypto.New(bytes.Repeat([]byte{72}, 32)) + if err != nil { + t.Fatal(err) + } + s := store.NewWithCredentialCipher(pool, cipher) + token, foreign := uuid.NewString(), uuid.NewString() + auth, err := api.NewAuthenticator([]api.APIKey{ + {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "query-owner", TokenSHA256: device.HashCredential(token), TenantID: uuid.NewString()}, + {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "query-foreign", TokenSHA256: device.HashCredential(foreign), TenantID: uuid.NewString()}, + }) + if err != nil { + t.Fatal(err) + } + // Use real admission while leaving dispatch paused. Public cancellation retains + // the queued history; this fixture does not perform native or model execution. + worker, err := execution.StartWorker(t.Context(), &execution.Dispatcher{Store: s}) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + stopped, cancel := context.WithCancel(context.Background()) + cancel() + if err := worker.Run(stopped); err != context.Canceled { + t.Error(err) + } + }) + handler, err := api.NewHandler(s, auth, "codex", api.WithExecution(worker), api.WithSkills(s), api.WithSourceFiles(s)) + if err != nil { + t.Fatal(err) + } + server := httptest.NewServer(handler) + defer server.Close() + ctx, cancel := context.WithTimeout(t.Context(), time.Minute) + defer cancel() + command := exec.CommandContext(ctx, python, "../../tests/official_list_query.py", server.URL, token, foreign) + output, err := command.CombinedOutput() + if err != nil { + t.Fatalf("official list query acceptance: %v %s", err, output) + } + t.Log(string(output)) +} diff --git a/services/agents-api/tests/official_list_query.py b/services/agents-api/tests/official_list_query.py new file mode 100644 index 000000000..b862cba1f --- /dev/null +++ b/services/agents-api/tests/official_list_query.py @@ -0,0 +1,164 @@ +"""List query acceptance through real HTTP/PostgreSQL and the fixed official SDK. + +Owned fixtures use real Worker admission with dispatch paused. No native executor +or model runs, and initial Turn/Item history remains visible throughout the test. +""" + +import importlib.metadata +import json +import secrets +import sys +from contextlib import ExitStack +from pathlib import Path + +import httpx2 +from openai import APIStatusError, DefaultHttpxClient, OpenAI + + +def main(): + base, token, foreign = sys.argv[1:] + pin = json.loads((Path(__file__).resolve().parents[3] / "contracts/agents-api/upstream.json").read_text()) + distribution = importlib.metadata.distribution("openai") + assert distribution.version == pin["sdk_version"] + assert json.loads(distribution.read_text("direct_url.json"))["vcs_info"]["commit_id"] == pin["commit"] + with ExitStack() as cleanup: + raw = cleanup.enter_context(httpx2.Client(trust_env=False, timeout=10)) + client = cleanup.enter_context(OpenAI( + api_key=token, base_url=base + "/v1", max_retries=0, + _strict_response_validation=True, http_client=DefaultHttpxClient(trust_env=False), + )) + agents = client.beta.agents + sessions = agents.sessions + templates = agents.environments.templates + vaults = agents.vaults + secret = "query-credential-" + secrets.token_hex(16) + owned = {name: [] for name in ("agents", "templates", "vaults", "credentials", "sessions", "files", "skills")} + for index in range(2): + agent = agents.create(model="query-fixture-model", name=f"Query Agent {index}") + owned["agents"].append(agent.id) + cleanup.callback(agents.delete, agent.id) + template = templates.create(name=f"Query Template {index}") + owned["templates"].append(template.id) + cleanup.callback(templates.delete, template.id) + vault = vaults.create(name=f"Query Vault {index}") + owned["vaults"].append(vault.id) + cleanup.callback(vaults.delete, vault.id) + file = client.files.create(file=(f"query-{index}.txt", b"Owned list query fixture"), purpose="user_data") + owned["files"].append(file.id) + cleanup.callback(client.files.delete, file.id) + manifest = f"---\nname: query-{index}\ndescription: Owned query fixture.\n---\nList metadata only.\n" + skill = client.skills.create(files=[(f"query-{index}/SKILL.md", manifest.encode(), "text/markdown")]) + owned["skills"].append(skill.id) + cleanup.callback(client.skills.delete, skill.id) + session = sessions.create( + agent_id=agent.id, environment={"type": "none"}, + input=f"Retain query fixture initial history {index}.", + ) + owned["sessions"].append(session.id) + cleanup.callback(sessions.delete, session.id) + sessions.events.create(session.id, events=[{"type": "agent.session.input.cancel"}]) + vault_id = owned["vaults"][0] + for index in range(2): + credential = vaults.credentials.create( + vault_id, name=f"Query Credential {index}", + auth={"type": "static_bearer", "mcp_server_url": "https://query.example.invalid/mcp", "token": secret}, + ) + owned["credentials"].append(credential.id) + session_id = owned["sessions"][0] + for index in range(2): + sessions.events.create(session_id, events=[{ + "type": "agent.session.input.message", + "input": [{"role": "user", "content": [{"type": "input_text", "text": f"Retain query history {index}."}]}], + }]) + sessions.events.create(session_id, events=[{"type": "agent.session.input.cancel"}]) + turns = list(sessions.turns.list(session_id, order="asc")) + items = list(sessions.items.list(session_id, order="asc")) + assert len(turns) == len(items) == 3 + assert all(turn.status == "cancelled" for turn in turns) + owned["turns"] = [turn.id for turn in turns] + owned["items"] = [item.id for item in items] + + families = [ + ("agents", "/agents", agents.list, True, False), + ("templates", "/agents/environments/templates", templates.list, True, False), + ("sessions", "/agents/sessions", sessions.list, True, False), + ("turns", f"/agents/sessions/{session_id}/turns", lambda **q: sessions.turns.list(session_id, **q), True, True), + ("items", f"/agents/sessions/{session_id}/items", lambda **q: sessions.items.list(session_id, **q), True, True), + ("vaults", "/vaults", vaults.list, True, False), + ("credentials", f"/vaults/{vault_id}/credentials", lambda **q: vaults.credentials.list(vault_id, **q), True, True), + ("files", "/files", client.files.list, False, False), + ("skills", "/skills", client.skills.list, False, False), + ] + state_before = {name: [value.to_dict() for value in listing(order="asc", limit=1)] for name, _, listing, _, _ in families} + rejected = 0 + for name, path, listing, beta, nested in families: + headers = {"Authorization": "Bearer " + token} + if beta: + headers["OpenAI-Beta"] = "agents=v1" + url = base + "/v1" + path + before = state_before[name] + assert [value["id"] for value in before] == owned[name], name + assert [value.id for value in listing(order="desc", limit=1)] == owned[name][::-1], name + assert [value.id for value in listing(limit=1)] == owned[name][::-1], name + page = raw.get(url, headers=headers, params={"order": "asc", "limit": 1}) + assert page.status_code == 200 and page.json()["has_more"] is True, name + assert page.json()["first_id"] == page.json()["last_id"] == owned[name][0], name + tail = raw.get(url, headers=headers, params={"order": "asc", "after": owned[name][-1], "limit": 1}) + assert tail.status_code == 200 and tail.json()["data"] == [] and tail.json()["has_more"] is False, name + + expected_code = "invalid_request_error" if beta else ("invalid_value" if name == "skills" else None) + expected_param = "order" if name == "skills" else None + for order in ("", "sideways"): + response = raw.get(url, headers=headers, params={"order": order}) + assert response.status_code == 400, (name, order, response.status_code) + body = response.json()["error"] + assert body["type"] == "invalid_request_error" and body["code"] == expected_code and body["param"] == expected_param, (name, body) + assert isinstance(body["message"], str) and body["message"], name + assert secret not in response.text and all(value not in response.text for value in owned[name]), name + if order: + try: + listing(order=order) + except APIStatusError as error: + assert error.status_code == 400 and error.body == body, (name, error.body) + else: + raise AssertionError(f"SDK accepted invalid {name} order") + else: + # Fixed 3.13 drops empty query values in _qs.stringify_items. + # Only the raw request above exercises an explicit order=. + assert [value.id for value in listing(order="", limit=1)] == owned[name][::-1], name + foreign_error = raw.get(url, headers={**headers, "Authorization": "Bearer " + foreign}, params={"order": order}) + assert foreign_error.status_code == 400 and foreign_error.json() == response.json(), name + unauthorized = raw.get(url, headers={k: v for k, v in headers.items() if k != "Authorization"}, params={"order": order}) + assert unauthorized.status_code == 401 and secret not in unauthorized.text, name + rejected += 2 if order else 1 + + foreign_page = raw.get(url, headers={**headers, "Authorization": "Bearer " + foreign}, params={"order": "asc"}) + if nested: + assert foreign_page.status_code == 404, name + else: + assert foreign_page.status_code == 200 and foreign_page.json()["data"] == [], name + assert secret not in foreign_page.text and all(value not in foreign_page.text for value in owned[name]), name + if name in {"vaults", "credentials"}: + for query in ({"status": "query-invalid"}, {"status[]": "query-invalid"}): + response = raw.get(url, headers=headers, params=query) + assert response.status_code == 400, name + error = response.json()["error"] + assert (error["type"], error["code"], error["param"]) == ("invalid_request_error", "invalid_request_error", None), (name, error) + try: + listing(extra_query=query) + except APIStatusError as sdk_error: + assert sdk_error.status_code == 400 and sdk_error.body == error, name + else: + raise AssertionError(f"SDK accepted invalid {name} status") + rejected += 2 + assert [value.to_dict() for value in listing(order="asc", limit=1)] == before, name + assert {name: [value.to_dict() for value in listing(order="asc", limit=1)] for name, _, listing, _, _ in families} == state_before + assert list(sessions.turns.list(session_id, order="asc")) == turns + assert list(sessions.items.list(session_id, order="asc")) == items + print(json.dumps({"result": "passed", "families": len(families), "sdk_and_raw_rejections": rejected, + "sdk_empty_order_omitted": len(families), "retained_turns": len(turns), "retained_items": len(items), "postgres": True, + "worker_admission": True, "native_model_execution": False})) + + +if __name__ == "__main__": + main() From 3a6d680a6fb8048672fa9df78e6934cdbca826d4 Mon Sep 17 00:00:00 2001 From: saladday <1203511142@qq.com> Date: Wed, 23 Sep 2026 01:05:00 +0800 Subject: [PATCH 7/7] Record list-query service acceptance and independent review --- contracts/agents-api/list-query-semantics.md | 26 ++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/contracts/agents-api/list-query-semantics.md b/contracts/agents-api/list-query-semantics.md index aa6eefd83..95d5d3622 100644 --- a/contracts/agents-api/list-query-semantics.md +++ b/contracts/agents-api/list-query-semantics.md @@ -73,3 +73,29 @@ change requires no new native-model capability qualification; existing real-mode evidence retains its original scope. Record completed checks and independent review before merging; neither a deserializable response nor a route inventory proves complete compatibility. + +## Completed acceptance + +The nine-family fixed-SDK/raw-HTTP regression passed against actual Core HTTP, +PostgreSQL and Worker admission with dispatch paused. It checked 35 primary +rejections, nine SDK empty-query omission cases, successful ascending/descending +and default pagination, authentication and foreign-tenant masking. Resource +snapshots and the original three cancelled Turns/three Items remained unchanged. +The baseline failed at raw empty-order admission; the implementation passed. +This is resource/query acceptance, not native or model execution. Logs, exact +SDK serializer source and hashes are retained in the survey directory's +`ACCEPTANCE.md`; its owned database was removed. + +Server `make -o check-web check` passed at `e4cb8a5`, including the dedicated +PostgreSQL regression, sqlc regeneration, service/adapter Go tests and builds, +Claude/MiniMax checks and Rust tests/format/Clippy. Web/client source and dependencies +are unchanged from PR #35: its 287 client tests, 583 Web tests and 76 browser cases +remain the applicable exact-source evidence; no new Web run is claimed. The +optional packaged MiniMax native-tools probe was skipped. This batch changes no +Runtime/provider/model behavior and does not requalify their combinations. + +A fresh independent GPT-6 Astra high reviewer found no grounded in-scope findings +after inspecting the full diff and evidence; API tests and `git diff --check` +passed independently. Server logs are retained under +`~/.parsar/remediation/20260923/list-query-alignment/`. The remaining limits, cursor, +lookup and Files verbose error differences above are not declared compatible.