From a2390f78cb4ecef38db1fb8a9a04908b50d5f34f Mon Sep 17 00:00:00 2001 From: saladday <1203511142@qq.com> Date: Wed, 23 Sep 2026 01:28:35 +0800 Subject: [PATCH 1/7] fix(agents-api): accept nullable Skill reference selectors --- .../api/environment_skill_selectors_test.go | 87 +++++++++++++++++++ .../internal/api/environment_skills.go | 14 ++- .../internal/api/environment_skills_test.go | 2 +- .../environment_skill_references_test.go | 8 ++ 4 files changed, 106 insertions(+), 5 deletions(-) create mode 100644 services/agents-api/internal/api/environment_skill_selectors_test.go diff --git a/services/agents-api/internal/api/environment_skill_selectors_test.go b/services/agents-api/internal/api/environment_skill_selectors_test.go new file mode 100644 index 000000000..eed2d05d0 --- /dev/null +++ b/services/agents-api/internal/api/environment_skill_selectors_test.go @@ -0,0 +1,87 @@ +package api + +import ( + "encoding/json" + "reflect" + "testing" + + "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/store" +) + +func TestSkillReferenceNullableSelectorAdmissionAndTemplateProjection(t *testing.T) { + for _, test := range []struct { + name, field, selector string + publicVersion any + }{ + {name: "omitted"}, + {name: "null", field: `,"version":null`}, + {name: "null whitespace", field: `,"version": null `}, + {name: "latest", field: `,"version":"latest"`, selector: "latest", publicVersion: "latest"}, + {name: "exact", field: `,"version":"2"`, selector: "2", publicVersion: "2"}, + } { + t.Run(test.name, func(t *testing.T) { + skills := `[{"type":"skill_reference","skill_id":"skill-owned"` + test.field + `}]` + template, err := decodeTemplateInput([]byte(`{"skills":` + skills + `}`)) + if err != nil || !template.SetSkills || len(template.Initialization.Skills) != 1 { + t.Fatalf("template admission: %+v %v", template, err) + } + want := store.EnvironmentSkill{Metadata: store.EnvironmentSkillMetadata{Type: "skill_reference", SkillID: "skill-owned", Version: test.selector}} + if !reflect.DeepEqual(template.Initialization.Skills[0], want) { + t.Fatalf("unresolved selector changed: %+v", template.Initialization.Skills[0]) + } + public := templateResponse(store.EnvironmentTemplate{Skills: template.Initialization.SkillMetadata()}) + var reference map[string]any + if len(public.Skills) != 1 || json.Unmarshal(public.Skills[0], &reference) != nil { + t.Fatalf("template projection: %+v", public.Skills) + } + if !reflect.DeepEqual(reference, map[string]any{"type": "skill_reference", "skill_id": "skill-owned", "version": test.publicVersion}) { + t.Fatalf("template reference fields: %v", reference) + } + for _, templateField := range []string{"", `,"environment_template_id":"template-owned"`} { + var request decodedSessionRequest + body := `{"agent":{"model":"test"},"environment":{"type":"openai_hosted"` + templateField + `,"skills":` + skills + `}}` + if err := json.Unmarshal([]byte(body), &request); err != nil { + t.Fatal(err) + } + input, err := request.validated() + if err != nil || !reflect.DeepEqual(input.initialization.Skills, template.Initialization.Skills) { + t.Fatalf("Session admission differs from Template: %+v %v", input.initialization.Skills, err) + } + } + }) + } +} + +func TestSkillReferenceNullDoesNotWidenOtherSelectors(t *testing.T) { + for _, version := range []string{`""`, `"default"`, `"LATEST"`, `"01"`, `"0"`, `"-1"`, `1`, `true`, `{}`, `[]`} { + if _, err := decodeEnvironmentSkills([]byte(`[{"type":"skill_reference","skill_id":"skill-owned","version":` + version + `}]`)); err == nil { + t.Fatalf("invalid selector accepted: %s", version) + } + } +} + +func TestInstalledSkillReferenceRequiresConcreteVersion(t *testing.T) { + metadata := store.EnvironmentSkillMetadata{Type: "skill_reference", SkillID: "skill-owned", Version: "2", Name: "proof", Description: "A proof."} + public := skillResponse([]store.EnvironmentSkillMetadata{metadata}) + var reference map[string]any + if len(public) != 1 || json.Unmarshal(public[0], &reference) != nil { + t.Fatalf("installed projection: %s", public) + } + want := map[string]any{"type": "skill_reference", "skill_id": "skill-owned", "version": "2", "name": "proof", "description": "A proof."} + if !reflect.DeepEqual(reference, want) { + t.Fatalf("installed metadata changed: %v", reference) + } + for _, selector := range []string{`"2"`, `null`, `"latest"`} { + raw := json.RawMessage(`{"type":"openai_hosted","skills":[{"type":"skill_reference","skill_id":"skill-owned","version":` + selector + `,"name":"proof","description":"A proof."}]}`) + result, err := environmentResponse(store.Environment{ID: "environment-owned", Status: "pending", Configuration: raw}) + if selector != `"2"` { + if err == nil { + t.Fatalf("unresolved installed selector accepted: %s", selector) + } + continue + } + if err != nil || len(result.Skills) != 1 || json.Unmarshal(result.Skills[0], &reference) != nil || !reflect.DeepEqual(reference, want) { + t.Fatalf("stored environment projection: %+v %v", result, err) + } + } +} diff --git a/services/agents-api/internal/api/environment_skills.go b/services/agents-api/internal/api/environment_skills.go index 328a9b992..3e72e7763 100644 --- a/services/agents-api/internal/api/environment_skills.go +++ b/services/agents-api/internal/api/environment_skills.go @@ -33,9 +33,8 @@ func decodeEnvironmentSkills(raw json.RawMessage) ([]store.EnvironmentSkill, err return nil, store.ErrInvalidInput } metadata := store.EnvironmentSkillMetadata{Type: reference.Type, SkillID: reference.SkillID} - if len(reference.Version) > 0 { - // Null selection semantics are unconfirmed; do not silently select default. - if bytes.Equal(bytes.TrimSpace(reference.Version), []byte("null")) || json.Unmarshal(reference.Version, &metadata.Version) != nil || metadata.Version == "" { + if len(reference.Version) > 0 && !bytes.Equal(bytes.TrimSpace(reference.Version), []byte("null")) { + if json.Unmarshal(reference.Version, &metadata.Version) != nil || metadata.Version == "" { return nil, store.ErrInvalidInput } } @@ -63,7 +62,14 @@ func decodeEnvironmentSkills(raw json.RawMessage) ([]store.EnvironmentSkill, err func skillResponse(skills []store.EnvironmentSkillMetadata) []json.RawMessage { result := make([]json.RawMessage, 0, len(skills)) for _, skill := range skills { - raw, _ := json.Marshal(skill) + var projection any = skill + if skill.Type == "skill_reference" && skill.Version == "" { + projection = struct { + store.EnvironmentSkillMetadata + Version *string `json:"version"` + }{EnvironmentSkillMetadata: skill} + } + raw, _ := json.Marshal(projection) result = append(result, raw) } return result diff --git a/services/agents-api/internal/api/environment_skills_test.go b/services/agents-api/internal/api/environment_skills_test.go index 8498e35b7..f6aa88a61 100644 --- a/services/agents-api/internal/api/environment_skills_test.go +++ b/services/agents-api/internal/api/environment_skills_test.go @@ -80,7 +80,7 @@ func TestSkillReferenceParsingInheritanceAndReplacement(t *testing.T) { t.Fatal("inline reference lost retry intent", err) } } - for _, version := range []string{`null`, `1`, `""`, `"0"`} { + for _, version := range []string{`1`, `""`, `"0"`} { if _, err := decodeEnvironmentSkills([]byte(`[{"type":"skill_reference","skill_id":"skill-owned","version":` + version + `}]`)); err == nil { t.Fatal("invalid or unconfirmed selector accepted") } diff --git a/services/agents-api/internal/store/environment_skill_references_test.go b/services/agents-api/internal/store/environment_skill_references_test.go index 9f7b0c480..380ede283 100644 --- a/services/agents-api/internal/store/environment_skill_references_test.go +++ b/services/agents-api/internal/store/environment_skill_references_test.go @@ -86,6 +86,14 @@ func TestSkillReferencesFreezeWithinSessionCreation(t *testing.T) { } } assertFrozen(sessionID, "1", first) + latest := input + latest.IdempotencyKey = uuid.NewString() + latest.Initialization.Skills = []EnvironmentSkill{{Metadata: EnvironmentSkillMetadata{Type: "skill_reference", SkillID: skill.ID, Version: "latest"}}} + latestSession, err := s.CreateSession(t.Context(), tenant, latest) + if err != nil { + t.Fatal(err) + } + assertFrozen(latestSession.ID, "2", second) if input.Initialization.Skills[0].Metadata.Version != "" || len(input.Initialization.Skills[0].Archive) != 0 { t.Fatal("creation mutated caller intent") } From 2ccc729d3acfa8d7109f671d480753d74d568279 Mon Sep 17 00:00:00 2001 From: saladday <1203511142@qq.com> Date: Wed, 23 Sep 2026 01:30:36 +0800 Subject: [PATCH 2/7] test(agents-api): qualify nullable Skill template selectors --- .../store/skill_selectors_public_test.go | 65 +++++++++++++ .../tests/official_skill_selectors.py | 94 +++++++++++++++++++ 2 files changed, 159 insertions(+) create mode 100644 services/agents-api/internal/store/skill_selectors_public_test.go create mode 100644 services/agents-api/tests/official_skill_selectors.py diff --git a/services/agents-api/internal/store/skill_selectors_public_test.go b/services/agents-api/internal/store/skill_selectors_public_test.go new file mode 100644 index 000000000..24ac26a5d --- /dev/null +++ b/services/agents-api/internal/store/skill_selectors_public_test.go @@ -0,0 +1,65 @@ +package store_test + +import ( + "bytes" + "context" + "encoding/json" + "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/store" + "github.com/google/uuid" +) + +func TestSkillSelectorsOfficialClientPostgres(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{63}, 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: "test-runner", TokenSHA256: device.HashCredential(token), TenantID: uuid.NewString()}, + {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "test-runner", TokenSHA256: device.HashCredential(foreign), TenantID: uuid.NewString()}, + }) + if err != nil { + t.Fatal(err) + } + h, err := api.NewHandler(s, auth, "codex", api.WithSkills(s)) + if err != nil { + t.Fatal(err) + } + server := httptest.NewServer(h) + defer server.Close() + recoveredStore := store.NewWithCredentialCipher(pool, cipher) + h, err = api.NewHandler(recoveredStore, auth, "codex", api.WithSkills(recoveredStore)) + if err != nil { + t.Fatal(err) + } + recovered := httptest.NewServer(h) + defer recovered.Close() + settings, err := json.Marshal(map[string]string{"base": server.URL, "recovered": recovered.URL, "token": token, "foreign": foreign}) + if err != nil { + t.Fatal(err) + } + ctx, cancel := context.WithTimeout(t.Context(), time.Minute) + defer cancel() + command := exec.CommandContext(ctx, python, "../../tests/official_skill_selectors.py") + command.Stdin = bytes.NewReader(settings) + output, err := command.CombinedOutput() + if err != nil { + t.Fatalf("Skill selector official client: %v %s", err, output) + } + t.Log(string(output)) +} diff --git a/services/agents-api/tests/official_skill_selectors.py b/services/agents-api/tests/official_skill_selectors.py new file mode 100644 index 000000000..85f0615b4 --- /dev/null +++ b/services/agents-api/tests/official_skill_selectors.py @@ -0,0 +1,94 @@ +"""Pinned SDK/raw Template selector acceptance on Core and real PostgreSQL. + +This verifies resource admission/projection, not Session resolution or native use. +""" +import base64 +import json +import secrets +import sys + +import httpx +import openai +from openai import DefaultHttpxClient, OpenAI + +from official_skills import bundle + + +def main(): + assert openai.__version__ == "3.13.0", openai.__version__ + settings = json.load(sys.stdin) + options = dict(max_retries=0, _strict_response_validation=True) + client = OpenAI(base_url=settings["base"] + "/v1", api_key=settings["token"], http_client=DefaultHttpxClient(trust_env=False), **options) + recovered = OpenAI(base_url=settings["recovered"] + "/v1", api_key=settings["token"], http_client=DefaultHttpxClient(trust_env=False), **options) + api = client.beta.agents.environments.templates + restarted = recovered.beta.agents.environments.templates + base = settings["base"] + "/v1" + path = "/agents/environments/templates" + headers = {"Authorization": "Bearer " + settings["token"], "OpenAI-Beta": "agents=v1"} + foreign = {**headers, "Authorization": "Bearer " + settings["foreign"]} + marker = "private-skill-selector-" + secrets.token_hex(16) + archive, manifest = bundle(marker) + private_values = [marker, base64.b64encode(archive).decode(), settings["token"], settings["foreign"]] + owned = [] + skill_id = None + + def safe(response): + assert all(value not in response.text for value in private_values), "private content in response" + + def projection(response, selector, status): + assert response.status_code == status, response.text + safe(response.http_response) + body = response.http_response.json() + assert set(body) == {"id", "object", "created_at", "updated_at", "name", "network", "packages", "capability_directories", "files", "plugins", "skills"} + assert body["skills"] == [{"type": "skill_reference", "skill_id": skill_id, "version": selector}], body["skills"] + parsed = response.parse() + assert parsed.skills[0].version == selector + assert parsed.skills[0].skill_id == skill_id + return parsed.id + + with httpx.Client(timeout=20, trust_env=False) as http: + try: + uploaded = client.skills.with_raw_response.create(files=[("proof/SKILL.md", manifest, "text/markdown")]) + skill_id = uploaded.parse().id + safe(uploaded.http_response) + for selector_fields in ({}, {"version": None}): + reference = {"type": "skill_reference", "skill_id": skill_id, **selector_fields} + created = api.with_raw_response.create(skills=[reference]) + # Register ownership before projection assertions so failure still cleans up. + template_id = created.http_response.json()["id"] + owned.append(template_id) + assert projection(created, None, 201) == template_id + assert projection(api.with_raw_response.retrieve(template_id), None, 200) == template_id + for fields, expected in (({}, None), ({"version": None}, None), ({"version": "latest"}, "latest"), ({"version": "1"}, "1")): + reference = {"type": "skill_reference", "skill_id": skill_id, **fields} + projection(api.with_raw_response.update(template_id, skills=[reference]), expected, 200) + projection(restarted.with_raw_response.retrieve(template_id), expected, 200) + raw = http.get(base + path + "/" + template_id, headers=headers) + assert raw.status_code == 200 + safe(raw) + assert raw.json()["skills"] == [{"type": "skill_reference", "skill_id": skill_id, "version": expected}] + # Omitting the whole skills field leaves the existing selector intact. + projection(api.with_raw_response.update(template_id), "1", 200) + for method, body in (("GET", None), ("POST", {"skills": [{"type": "skill_reference", "skill_id": skill_id, "version": None}]}), ("DELETE", None)): + response = http.request(method, base + path + "/" + template_id, headers=foreign, json=body) + assert response.status_code == 404 + safe(response) + assert response.json()["error"]["type"] == "not_found_error" + projection(api.with_raw_response.retrieve(template_id), "1", 200) + for suffix in ("", "/content", "/versions/1", "/versions/1/content"): + response = http.get(base + "/skills/" + skill_id + suffix, headers=foreign) + assert response.status_code == 404 + safe(response) + assert response.json()["error"] == {"message": "Resource not found.", "type": "invalid_request_error", "code": None, "param": None} + print(json.dumps({"sdk": openai.__version__, "template_selectors": ["omitted", "null", "latest", "1"], "postgres": True, "sessions": 0, "native_model": False, "result": "passed"})) + finally: + for template_id in owned: + api.delete(template_id) + if skill_id is not None: + client.skills.delete(skill_id) + client.close() + recovered.close() + + +if __name__ == "__main__": + main() From f84d4b0f5ed77a18609f696c29208f1ffe58f7e7 Mon Sep 17 00:00:00 2001 From: saladday <1203511142@qq.com> Date: Wed, 23 Sep 2026 01:31:43 +0800 Subject: [PATCH 3/7] Align Files missing-resource error parameters --- services/agents-api/internal/api/errors.go | 4 +- .../agents-api/internal/api/source_files.go | 4 +- .../internal/api/source_files_content.go | 6 +- .../internal/api/source_files_errors_test.go | 60 ++++++++++++++ .../internal/api/source_files_list.go | 2 +- .../store/source_files_errors_public_test.go | 44 +++++++++++ .../tests/official_source_file_errors.py | 78 +++++++++++++++++++ 7 files changed, 190 insertions(+), 8 deletions(-) create mode 100644 services/agents-api/internal/api/source_files_errors_test.go create mode 100644 services/agents-api/internal/store/source_files_errors_public_test.go create mode 100644 services/agents-api/tests/official_source_file_errors.py diff --git a/services/agents-api/internal/api/errors.go b/services/agents-api/internal/api/errors.go index 1934973f8..03710e53e 100644 --- a/services/agents-api/internal/api/errors.go +++ b/services/agents-api/internal/api/errors.go @@ -39,7 +39,7 @@ func writeError(w http.ResponseWriter, status int, code, message string, param . 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) { +func writeStoreError(w http.ResponseWriter, r *http.Request, err error, notFoundParam ...string) { switch { case errors.Is(err, store.ErrDefaultSkillVersion): writeError(w, http.StatusBadRequest, "invalid_request", "Change the default version before deleting this Skill version.") @@ -61,7 +61,7 @@ func writeStoreError(w http.ResponseWriter, r *http.Request, err error) { if strings.HasPrefix(r.URL.Path, "/v1/files/") || strings.HasPrefix(r.URL.Path, "/v1/skills/") || r.URL.Path == "/v1/files" || r.URL.Path == "/v1/skills" { code = "" } - writeError(w, http.StatusNotFound, code, "Resource not found.") + writeError(w, http.StatusNotFound, code, "Resource not found.", notFoundParam...) case errors.Is(err, store.ErrTurnConflict): writeError(w, http.StatusConflict, "turn_conflict", "The Turn cannot accept this input in its current state.") case errors.Is(err, store.ErrIdempotencyConflict): diff --git a/services/agents-api/internal/api/source_files.go b/services/agents-api/internal/api/source_files.go index e63248981..5ee7f04bc 100644 --- a/services/agents-api/internal/api/source_files.go +++ b/services/agents-api/internal/api/source_files.go @@ -59,7 +59,7 @@ func (h *Handler) getSourceFile(w http.ResponseWriter, r *http.Request) { defer cancel() file, err := h.sourceFiles.GetSourceFile(ctx, tenantID(r), chi.URLParam(r, "file_id")) if err != nil { - writeStoreError(w, r, err) + writeStoreError(w, r, err, "id") return } writeJSON(w, http.StatusOK, sourceFileResponse(file)) @@ -82,7 +82,7 @@ func (h *Handler) deleteSourceFile(w http.ResponseWriter, r *http.Request) { defer cancel() id := chi.URLParam(r, "file_id") if err := h.sourceFiles.DeleteSourceFile(ctx, tenantID(r), id); err != nil { - writeStoreError(w, r, err) + writeStoreError(w, r, err, "id") return } writeJSON(w, http.StatusOK, v1.SourceFileDeleted{ID: id, Object: "file", Deleted: true}) diff --git a/services/agents-api/internal/api/source_files_content.go b/services/agents-api/internal/api/source_files_content.go index 5848c44cd..327a86511 100644 --- a/services/agents-api/internal/api/source_files_content.go +++ b/services/agents-api/internal/api/source_files_content.go @@ -29,10 +29,10 @@ func (h *Handler) sourceFileContent(w http.ResponseWriter, r *http.Request) { return h.sourceFiles.ReadSourceFile(ctx, tenantID(r), chi.URLParam(r, "file_id"), func(file store.SourceFile, body io.Reader) error { return consume(file.Filename, file.SizeBytes, body) }) - }) + }, "id") } -func serveStoredContent(w http.ResponseWriter, r *http.Request, read func(context.Context, func(string, int64, io.Reader) error) error) { +func serveStoredContent(w http.ResponseWriter, r *http.Request, read func(context.Context, func(string, int64, io.Reader) error) error, notFoundParam ...string) { deadline := time.Now().Add(sourceTransferTimeout) if http.NewResponseController(w).SetWriteDeadline(deadline) != nil { writeError(w, http.StatusServiceUnavailable, "file_transfer_unavailable", "Bounded file transfer is unavailable.") @@ -59,6 +59,6 @@ func serveStoredContent(w http.ResponseWriter, r *http.Request, read func(contex if started { panic(http.ErrAbortHandler) } - writeStoreError(w, r, err) + writeStoreError(w, r, err, notFoundParam...) } } diff --git a/services/agents-api/internal/api/source_files_errors_test.go b/services/agents-api/internal/api/source_files_errors_test.go new file mode 100644 index 000000000..afdb23cfd --- /dev/null +++ b/services/agents-api/internal/api/source_files_errors_test.go @@ -0,0 +1,60 @@ +package api + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/store" +) + +func TestSourceFileMissingErrorParameters(t *testing.T) { + for _, tc := range []struct{ method, path, param string }{ + {http.MethodGet, "/v1/files/file-missing", "id"}, + {http.MethodDelete, "/v1/files/file-missing", "id"}, + {http.MethodGet, "/v1/files/file-missing/content", "id"}, + {http.MethodGet, "/v1/files?after=file-missing", "after"}, + } { + t.Run(tc.method+tc.path, func(t *testing.T) { + f := &sourceFilesFixture{listErr: fmt.Errorf("wrapped: %w", store.ErrNotFound)} + h, _ := environmentFileCreateHandler(t, WithSourceFiles(f)) + server := newSourceFileServer(t, h) + status, raw := sourceRequest(t, server, tc.method, tc.path, "files-key", "", nil) + var body map[string]map[string]any + if status != http.StatusNotFound || json.Unmarshal(raw, &body) != nil { + t.Fatalf("missing: %d %s", status, raw) + } + e := body["error"] + if e["type"] != "invalid_request_error" || e["code"] != nil || e["param"] != tc.param || e["message"] != "Resource not found." { + t.Fatalf("error projection: %s", raw) + } + }) + } +} + +func TestStoreErrorOptionalParameterPreservesOtherErrors(t *testing.T) { + for _, tc := range []struct { + path string + err error + status int + code any + param []string + }{ + {"/v1/skills/skill_missing", store.ErrNotFound, 404, nil, nil}, + {"/v1/agents/agent_missing", store.ErrNotFound, 404, "not_found_error", nil}, + {"/v1/files/file-missing", store.ErrInvalidInput, 400, "invalid_request", []string{"id"}}, + {"/v1/files/file-missing", fmt.Errorf("database unavailable"), 500, "internal_error", []string{"id"}}, + } { + w := httptest.NewRecorder() + writeStoreError(w, httptest.NewRequest(http.MethodGet, tc.path, nil), tc.err, tc.param...) + var body map[string]map[string]any + if w.Code != tc.status || json.Unmarshal(w.Body.Bytes(), &body) != nil { + t.Fatalf("unexpected error: %d %s", w.Code, w.Body.String()) + } + if body["error"]["code"] != tc.code || body["error"]["param"] != nil { + t.Fatalf("unrelated error changed: %s", w.Body.String()) + } + } +} diff --git a/services/agents-api/internal/api/source_files_list.go b/services/agents-api/internal/api/source_files_list.go index 9aa057377..298c20da2 100644 --- a/services/agents-api/internal/api/source_files_list.go +++ b/services/agents-api/internal/api/source_files_list.go @@ -28,7 +28,7 @@ func (h *Handler) listSourceFiles(w http.ResponseWriter, r *http.Request) { } page, err := h.sourceFiles.ListSourceFiles(r.Context(), tenantID(r), options.after, options.limit, options.ascending, purpose) if err != nil { - writeStoreError(w, r, err) + writeStoreError(w, r, err, "after") return } response := v1.SourceFileList{Object: "list", Data: make([]v1.SourceFile, 0, len(page.Files)), HasMore: page.NextCursor != ""} diff --git a/services/agents-api/internal/store/source_files_errors_public_test.go b/services/agents-api/internal/store/source_files_errors_public_test.go new file mode 100644 index 000000000..47feff891 --- /dev/null +++ b/services/agents-api/internal/store/source_files_errors_public_test.go @@ -0,0 +1,44 @@ +package store_test + +import ( + "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/store" + "github.com/google/uuid" +) + +func TestSourceFileErrorsOfficialClientPostgres(t *testing.T) { + python := os.Getenv("PARSAR_OFFICIAL_SDK_PYTHON") + if python == "" { + t.Skip("pinned official Python SDK required") + } + s, _ := store.NewTestStore(t) + token, foreign := uuid.NewString(), uuid.NewString() + auth, err := api.NewAuthenticator([]api.APIKey{ + {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "files-owner", TokenSHA256: device.HashCredential(token), TenantID: uuid.NewString()}, + {OrganizationID: "test-org", ProjectID: uuid.NewString(), SubjectKind: "service_account", SubjectID: "files-foreign", TokenSHA256: device.HashCredential(foreign), TenantID: uuid.NewString()}, + }) + if err != nil { + t.Fatal(err) + } + h, err := api.NewHandler(s, auth, "codex", api.WithSourceFiles(s)) + if err != nil { + t.Fatal(err) + } + server := httptest.NewServer(h) + defer server.Close() + ctx, cancel := context.WithTimeout(t.Context(), time.Minute) + defer cancel() + output, err := exec.CommandContext(ctx, python, "../../tests/official_source_file_errors.py", server.URL, token, foreign).CombinedOutput() + if err != nil { + t.Fatalf("official Files errors acceptance: %v %s", err, output) + } + t.Log(string(output)) +} diff --git a/services/agents-api/tests/official_source_file_errors.py b/services/agents-api/tests/official_source_file_errors.py new file mode 100644 index 000000000..08a7b198b --- /dev/null +++ b/services/agents-api/tests/official_source_file_errors.py @@ -0,0 +1,78 @@ +"""Qualified Files errors through real Core HTTP/PostgreSQL and the pinned SDK.""" + +import importlib.metadata +import json +import secrets +import sys +from contextlib import ExitStack +from pathlib import Path + +import httpx2 +from openai import DefaultHttpxClient, NotFoundError, 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)) + clients = [cleanup.enter_context(OpenAI( + api_key=key, base_url=base + "/v1", max_retries=0, + _strict_response_validation=True, http_client=DefaultHttpxClient(trust_env=False), + )) for key in (token, foreign)] + owner, outsider = clients + secret = "private-file-content-" + secrets.token_hex(16) + file = owner.files.create(file=("private-owned-fixture.txt", secret.encode()), purpose="user_data") + cleanup.callback(owner.files.delete, file.id) + before = owner.files.retrieve(file.id).to_dict() + missing = "file-" + secrets.token_hex(24) + rejections = 0 + for operation, method, suffix, param in [ + ("retrieve", "GET", "", "id"), + ("content", "GET", "/content", "id"), + ("delete", "DELETE", "", "id"), + ("list", "GET", "", "after"), + ]: + def path(file_id): + return "/v1/files?after=" + file_id if operation == "list" else "/v1/files/" + file_id + suffix + + def invoke(client, file_id): + return client.files.list(after=file_id) if operation == "list" else getattr(client.files, operation)(file_id) + + bodies = [] + for key, client, file_id in [(token, owner, missing), (foreign, outsider, file.id)]: + response = raw.request(method, base + path(file_id), headers={"Authorization": "Bearer " + key}) + assert response.status_code == 404, (operation, response.status_code) + assert response.headers["cache-control"] == "no-store" + body = response.json()["error"] + assert set(body) == {"type", "code", "param", "message"} + assert (body["type"], body["code"], body["param"]) == ("invalid_request_error", None, param), body + assert isinstance(body["message"], str) and body["message"] + assert all(value not in response.text for value in (file.id, file.filename, secret, token, foreign)) + bodies.append(response.json()) + try: + invoke(client, file_id) + except NotFoundError as error: + assert error.status_code == 404 and error.body == body + else: + raise AssertionError("SDK accepted " + operation) + rejections += 2 + assert bodies[0] == bodies[1], operation + unauthorized = raw.request(method, base + path(file.id)) + assert unauthorized.status_code == 401 + assert all(value not in unauthorized.text for value in (file.id, file.filename, secret)) + assert owner.files.retrieve(file.id).to_dict() == before + assert owner.files.content(file.id).read() == secret.encode() + assert list(outsider.files.list()) == [] + tail = owner.files.list(after=file.id) + assert tail.data == [] and tail.has_more is False + assert [value.id for value in owner.files.list()] == [file.id] + print(json.dumps({"result": "passed", "sdk_and_raw_rejections": rejections, + "operations": 4, "tenant_isolation": True, "postgres": True})) + + +if __name__ == "__main__": + main() From 9d9639bd90bc74e7c27832b28dca27aac6c18781 Mon Sep 17 00:00:00 2001 From: saladday <1203511142@qq.com> Date: Wed, 23 Sep 2026 01:32:51 +0800 Subject: [PATCH 4/7] Document bounded Skill selector and Files error alignment --- CONTRIBUTING.md | 11 +++-- contracts/agents-api/README.md | 4 ++ contracts/agents-api/environment-templates.md | 10 ++-- contracts/agents-api/operation-evidence.md | 22 +++++---- .../agents-api/resource-selector-semantics.md | 47 +++++++++++++++++++ contracts/agents-api/source-files.md | 4 ++ 6 files changed, 79 insertions(+), 19 deletions(-) create mode 100644 contracts/agents-api/resource-selector-semantics.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0f521af86..b0ffb31be 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -380,11 +380,12 @@ Resolve references inside the Session creation transaction, after the creation upsert establishes ownership. Lock referenced resources in a stable order; freeze the selected version, descriptive metadata and bytes together. Creation retries recover the recorded intent before reading mutable templates or Skill sources. -Templates preserve omitted/default, latest and explicit version selectors. Session -responses contain concrete versions; only validated installation metadata crosses -the Runtime boundary. A supplied Session Skill list replaces the template list; -omission inherits. Explicit null reference selectors and null list overrides remain -unqualified and reject rather than silently changing selection. +Templates preserve default, latest and explicit version selectors. An omitted or +null reference version selects the default at Session creation and projects as +`version: null` in Template responses. Session responses contain concrete versions; +only validated installation metadata crosses the Runtime boundary. A supplied +Session Skill list replaces the template list; omission inherits. Null list +overrides remain unqualified and reject rather than silently changing selection. Inline and referenced Skill ZIPs use the same confidential initialization snapshot and installer. Core validates portable manifests and bounded regular-file archives, returns only diff --git a/contracts/agents-api/README.md b/contracts/agents-api/README.md index c1154c3e3..788906e4a 100644 --- a/contracts/agents-api/README.md +++ b/contracts/agents-api/README.md @@ -14,6 +14,10 @@ Python **SDK** is a separate future dependency for business Team orchestration i Parsar, not the HTTP contract. Design rules live in [CONTRIBUTING.md](../../CONTRIBUTING.md#design-and-compatibility-requirements). +The [resource selector and error qualification](resource-selector-semantics.md) +records nullable Skill references and source Files not-found parameter fields, +with official observations separated from Core acceptance. + ## Implementation direction Keep the independent service, authentication, PostgreSQL/sqlc persistence, diff --git a/contracts/agents-api/environment-templates.md b/contracts/agents-api/environment-templates.md index 8db02b082..1e05b8179 100644 --- a/contracts/agents-api/environment-templates.md +++ b/contracts/agents-api/environment-templates.md @@ -118,13 +118,15 @@ SDK 3.13.0 drops a single FileTypes tuple during multipart extraction before sen it. Use raw HTTP for a single ZIP with this fixed client; Core does not synthesize missing bytes or alter the pinned SDK. -Templates preserve reference selectors: omission selects default at Session +Templates preserve reference selectors: omission or null selects default at Session creation, `"latest"` selects latest, and a positive version string selects that version. A Session freezes tenant-authorized bytes and concrete version metadata in its creation transaction. Later source deletion, default changes or template updates cannot change that Session or its committed creation retry. A supplied -Session Skill list replaces the template list; omission inherits. Explicit null -reference versions and null list overrides are not qualified and reject. +Session Skill list replaces the template list; omission inherits. Template +responses include `version: null` for an unresolved default selector; resolved +Session references retain a concrete version string. Null list overrides remain +unqualified and reject. See [resource selector qualification](resource-selector-semantics.md). References return type/skill_id/version/name/description in Session metadata, while template responses retain unresolved selectors. Confidential bundle content never appears in these metadata responses. The common Runtime installation path @@ -803,7 +805,7 @@ index under `~/.parsar/remediation/20260921/template-capabilities-design/`. The current upload profile accepts at most 500 regular files, 5 MiB compressed and 20 MiB expanded per bundle. These are qualified implementation limits, not -published protocol maxima. Exact hosted error parity, null version selection, +published protocol maxima. Exact hosted error parity, unversioned content selection, top-level metadata across version changes and last/default/latest deletion semantics remain recorded gaps. Current resource behavior selects default for unversioned content, preserves initial top-level diff --git a/contracts/agents-api/operation-evidence.md b/contracts/agents-api/operation-evidence.md index f4b1126cd..4c9d76faa 100644 --- a/contracts/agents-api/operation-evidence.md +++ b/contracts/agents-api/operation-evidence.md @@ -35,6 +35,8 @@ Repository paths below are relative to the inspected worktree; private evidence | I | `contracts/agents-api/environment-templates.md`: separate recorded initial-file (:589), env/setup/npm/Python (:621), inline-Skill (:657), system-package (:692), capability-directory, Plugin MCP (:241), composition (:327) and restricted-network (:523) qualification. Exact historical run roots are in each section. No combinatorial/full hosted parity inference. | | O | `services/agents-api/oauth-credentials.md:144`: recorded genuine Keycloak 26.7.4 PKCE grants, real TLS MCP and Kimi execution. Codex Basic client-auth initial/refresh/restart/replacement/revocation/delete; Claude POST initial/refresh. Trusted `none` profiles only; not MiniMax, hosted, arbitrary-provider or complete error equivalence. | +| V | [Resource selector and error qualification](resource-selector-semantics.md); private September23 `skill-version-alignment/official/` and `files-error-alignment/official-probe.json`: owned Skill/Template/Session selector observations and seven missing File/cursor requests. Preserve each probe phase and distinguish actual hosted execution from metadata-only reads. | + ## Per-operation evidence matrix Paths in the appendix include `/v1`. SDK names here omit `client.`. `P` means partial implementation. General unresolved status/error/null/default/header/pagination/race semantics apply even where a row lists a narrower gap. @@ -84,19 +86,19 @@ Paths in the appendix include `/v1`. SDK names here omit `client.`. `P` means pa | 41 | beta.agents.vaults.credentials.list | P: safe scoped metadata/status list | R `credential-list` | C DB mixed list/rejected-write/restart; O recorded resource checks | Complete status-filter/page limits/archive behavior | | 42 | beta.agents.vaults.credentials.delete | P: encrypted resource deletion and dispatch denial | R `cleanup` at Credential paths | C DB; O recorded live Codex refusal after deletion | Cannot recall already-delivered token; exact hosted withdrawal/error timing | | 43 | files.create | P: immutable multipart purpose=user_data | None located | F recorded DB upload; D/K recorded native source consumption | Other purposes/expires_after unsupported, 512 MiB local size vs pinned 512 MB wording; full multipart/errors | -| 44 | files.retrieve | P: project-owned metadata | E missing-ID 404 only | F recorded DB workflow; C missing-ID DB replay | Successful official shape/default/status purpose union and error param unknown | -| 45 | files.list | P: purpose filter, default/max 10,000, cursor list | None located | F recorded actual PostgreSQL SDK/raw pages/autocontinuation/restart/isolation | Exact official ordering/cursor/error/concurrent-mutation semantics | -| 46 | files.delete | P: transactional metadata/body unlink | None located | F recorded DB deletion; D/K recorded retained workspace copies | Admitted immutable read may complete after deletion; hosted concurrency/retention unknown | -| 47 | files.content | P: immutable binary stream | None located | F recorded DB; D/K recorded source workflow | Range/header/partial-transfer and exact errors; retrieve_content is same HTTP operation | -| 48 | skills.create | P: encrypted bounded directory/ZIP bundle | None located | K recorded DB resources and Live three-harness upload/reference | Fixed SDK single-tuple multipart issue; 500 files/5 MiB compressed/20 MiB expanded local limits | -| 49 | skills.retrieve | P: safe top-level metadata | E missing-ID 404 only | K recorded DB resource checks; C missing-ID DB replay | Metadata evolution across versions is local policy, successful official semantics unknown | -| 50 | skills.update | P: change default version only | None located | K recorded DB resources; frozen Session behavior in Live reference workflow | Default/latest/explicit/null semantics and top-level metadata evolution unknown | +| 44 | files.retrieve | P: project-owned metadata | E/V missing-ID404 param=id; V fixedSDK exception | F recorded DB workflow; V actual HTTP/PG foreign==missing | Successful official shape/default/status purpose union; other errors | +| 45 | files.list | P: purpose filter, default/max 10,000, cursor list | Q explicit-order error; V missing-after404 param=after | F recorded actual PostgreSQL SDK/raw pages/restart/isolation; V missing cursor | Exact successful official ordering/concurrent-mutation and other query semantics | +| 46 | files.delete | P: transactional metadata/body unlink | V random nonexistent ID404 param=id | F recorded DB deletion; V actualHTTP/PG foreign delete denial preserves bytes; D/K retained workspace copies | Admitted immutable read may complete after deletion; hosted concurrency/retention unknown | +| 47 | files.content | P: immutable binary stream | V missing-ID404 param=id | F recorded DB; V actualHTTP/PG missing/foreign content denial; D/K source workflow | Range/header/partial-transfer and other errors; retrieve_content is same HTTP operation | +| 48 | skills.create | P: encrypted bounded directory/ZIP bundle | V successful owned directory uploads | K recorded DB resources and Live three-harness upload/reference | Fixed SDK single-tuple multipart issue; local upload limits remain partial | +| 49 | skills.retrieve | P: safe top-level metadata | E missing-ID404; V default1/latest2 metadata | K recorded DB checks; C missing-ID DB replay | Top-level metadata evolution across differing manifests remains unqualified | +| 50 | skills.update | P: change default version only | V default1→2 mutation; preexisting Session selectors unchanged | K recorded DB resources; frozen Session behavior in Live reference workflow | Top-level metadata evolution and error cases remain unqualified | | 51 | skills.list | P: scoped resource list | None located | K recorded DB resources; no model needed | Current documentation minimum zero is only a lead; fixed/common 1–100 implementation and exact official behavior require evidence | | 52 | skills.delete | P: remove owned source, retain committed Session content | None located | K recorded DB and Live source deletion/continuation | Exact hosted deletion/idempotence/default/latest semantics | | 53 | skills.content.retrieve | P: unversioned content selects default | None located | K recorded DB resource checks | Default-vs-latest unversioned selection unverified; headers/errors | -| 54 | skills.versions.create | P: immutable increasing version; optional default change | None located | K recorded DB resource checks | Numbering/default/top-level metadata/error/null semantics; same upload limits | -| 55 | skills.versions.retrieve | P: owned immutable version metadata | None located | K recorded DB resource checks and Live concrete Session freeze | Selector/metadata/error parity; reference version:null rejects locally | -| 56 | skills.versions.list | P: scoped version cursor list | None located | K recorded DB resource checks | Exact ordering/cursors/zero/default/max limits and concurrent version mutation | +| 54 | skills.versions.create | P: immutable increasing version; optional default change | V second version default=false preserves default1/latest2 | K recorded DB resources | Broader numbering/default/top-level metadata/error/null semantics; upload limits | +| 55 | skills.versions.retrieve | P: owned immutable version metadata | V immediate version1 read404, bounded delayed read200 | K recorded DB resources and Live concrete Session freeze | Visibility timing is observational; full selector/metadata/error parity unqualified | +| 56 | skills.versions.list | P: scoped version cursor list | V delayed owned list contains created versions | K recorded DB resource checks | Exact ordering/cursors/zero/default/max limits and concurrent version mutation | | 57 | skills.versions.delete | P: nondefault deletion; default deletion rejects | None located | K recorded DB resources; exact per-variant live case not claimed | Last/default/latest deletion behavior and non-reused numbers are unverified local choices | | 58 | skills.versions.content.retrieve | P: decrypt/read owned concrete bundle | None located | K recorded DB checks; Live native frozen supporting files | Content headers/errors and source deletion/read races; consumption does not prove download wire parity | diff --git a/contracts/agents-api/resource-selector-semantics.md b/contracts/agents-api/resource-selector-semantics.md new file mode 100644 index 000000000..a3236c31a --- /dev/null +++ b/contracts/agents-api/resource-selector-semantics.md @@ -0,0 +1,47 @@ +# Resource selector and error semantics + +This bounded September 23 alignment uses openai-python **3.13.0**, upstream +`d7c41efee1b0802b79f3f88a678ef2052b06e9ce`, and `agents=v1` for beta resources. +It does not qualify complete Skills, Templates, Sessions or Files compatibility. + +## Skill reference versions + +The pinned Template create/update and Session environment request types all import +`HostedSkillParam`, whose reference version is `Optional[str]`. The unrelated +`BetaSkillReferenceParam` is not their request type. An omitted or null selector +uses the Skill default; `latest` selects the latest version and a concrete string +selects that version. A Template retains unresolved intent and emits `version: +null` for the default selector. A Session exposes the concrete installed version. + +The existing tenant-scoped creation transaction freezes metadata and bundle bytes +together. Later source/default/Template changes do not modify the installation or +cause a creation retry to resolve mutable sources again. This change does not +redefine omitted-vs-null creation idempotency equivalence. Null Skill-list overrides, +version deletion rules, unversioned content selection, query bounds, and Template +plus inline initialization composition remain outside this batch. + +Official qualification uses owned resources. The first immediate version read +returned 404 despite successful creation; a separate bounded follow-up observed +that version after approximately 31 seconds. This is a recorded transient +visibility observation, not a guaranteed consistency interval or a behavior Core +should imitate. Template omission/null admission and projection, latest and exact +selectors are separately recorded. A further hosted Session probe uses divergent +default version 1 and latest version 2 to distinguish resolution from mere request +acceptance. Private evidence is under +`~/.parsar/remediation/20260923/skill-version-alignment/official/`. + +## Source File errors + +For general `/v1/files`, missing retrieve/content/delete resources use HTTP 404, +`type: invalid_request_error`, `code: null`, and `param: id`. A missing list cursor +uses the same envelope with `param: after`. Foreign resources retain the same +missing-resource response. These parameter hints pass through the existing error +serializer only for a not-found store error; other failures and Skills responses +retain their own mappings. + +Seven official requests qualify these cases, including raw HTTP, fixed SDK +exceptions and one DELETE of a random nonexistent identifier. No account files +were read or deleted. Evidence: +`~/.parsar/remediation/20260923/files-error-alignment/official-probe.json`. +Exact error prose, additional parser detail, purpose filtering, bounds and lookup +order are not changed or claimed as aligned. diff --git a/contracts/agents-api/source-files.md b/contracts/agents-api/source-files.md index ef809d1bc..3ccd7b29f 100644 --- a/contracts/agents-api/source-files.md +++ b/contracts/agents-api/source-files.md @@ -63,6 +63,10 @@ These concurrency/error choices are local policies, not verified hosted parity. Ambiguous upload commits are not automatically retried; clients may need to retain their source request evidence. Destination unknown-write handling remains unchanged. +Missing source Files and missing cursors expose the measured `id` and `after` +error parameters without revealing foreign resource existence. See the bounded +[resource error qualification](resource-selector-semantics.md#source-file-errors). + ## Remaining scope and verification Other upload purposes, `expires_after`, resumable Uploads, quotas, From a45ea05f99e91fcdcc68cfe1fc20d6e5aaa6ff1a Mon Sep 17 00:00:00 2001 From: saladday <1203511142@qq.com> Date: Wed, 23 Sep 2026 01:36:47 +0800 Subject: [PATCH 5/7] Record official and native Skill version evidence --- contracts/agents-api/operation-evidence.md | 9 +++--- .../agents-api/resource-selector-semantics.md | 31 +++++++++++++++++-- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/contracts/agents-api/operation-evidence.md b/contracts/agents-api/operation-evidence.md index 4c9d76faa..86c64ed54 100644 --- a/contracts/agents-api/operation-evidence.md +++ b/contracts/agents-api/operation-evidence.md @@ -34,7 +34,6 @@ Repository paths below are relative to the inspected worktree; private evidence | K | `contracts/agents-api/environment-templates.md:94,764`: recorded fixed-SDK/raw/PostgreSQL Skill resource checks plus all-three-harness Docker reference workflows (Codex/Claude Kimi K3, MiniMax M2.7). Remote `~/.parsar/remediation/20260921/template-skill-references/`; local index `~/.parsar/remediation/20260921/template-capabilities-design/`. Covers upload, frozen template/Session resolution, native supporting files, source/template deletion, retry and cold continuation. Individual resource mutation cases not explicitly claimed by the summary remain DB-only or unspecified. | | I | `contracts/agents-api/environment-templates.md`: separate recorded initial-file (:589), env/setup/npm/Python (:621), inline-Skill (:657), system-package (:692), capability-directory, Plugin MCP (:241), composition (:327) and restricted-network (:523) qualification. Exact historical run roots are in each section. No combinatorial/full hosted parity inference. | | O | `services/agents-api/oauth-credentials.md:144`: recorded genuine Keycloak 26.7.4 PKCE grants, real TLS MCP and Kimi execution. Codex Basic client-auth initial/refresh/restart/replacement/revocation/delete; Claude POST initial/refresh. Trusted `none` profiles only; not MiniMax, hosted, arbitrary-provider or complete error equivalence. | - | V | [Resource selector and error qualification](resource-selector-semantics.md); private September23 `skill-version-alignment/official/` and `files-error-alignment/official-probe.json`: owned Skill/Template/Session selector observations and seven missing File/cursor requests. Preserve each probe phase and distinguish actual hosted execution from metadata-only reads. | ## Per-operation evidence matrix @@ -71,9 +70,9 @@ Paths in the appendix include `/v1`. SDK names here omit `client.`. `P` means pa | 26 | beta.agents.environments.retrieve | P: durable status, safe configured installation metadata | None located | D/I/K recorded native readiness and metadata | All lifecycle timing and installation inventory; configured metadata is not arbitrary workspace discovery | | 27 | beta.agents.environments.files.create | P: inline/source-file copy to qualified workspace | None located | F/D/K recorded real copy, hashes/consumption/retention | 50 MiB local bound, parent/path/overwrite/error/unknown-write semantics | | 28 | beta.agents.environments.files.list | P: direct regular-file directory, opaque cursor | None located | F/D/K recorded live workspace listing | 1,024-entry prefilter bound; no recursion/symlinks; exact defaults/path/errors/mutation invalidation unknown | -| 29 | beta.agents.environments.templates.create | P: reusable network/files/env/setup/packages/Skills/Plugins/capability config, 201 | R `template-create` | C DB; I/K recorded real frozen-reference initialization | Restricted forms and unqualified combinations; complete hosted initialization semantics | -| 30 | beta.agents.environments.templates.retrieve | P: safe resource read | R `template-read`, deleted owned read | C DB; I/K recorded reference workflow | Full field/default/redaction parity; no live-secret projection inference | -| 31 | beta.agents.environments.templates.update | P: field replacement/null clearing, empty timestamp touch | R `template-patch`, `template-null`, `template-noop` | C DB no-op; I/K recorded frozen Session behavior | Referenced Session files/env/setup/packages overrides reject; null network/list/Skill-version remains unresolved | +| 29 | beta.agents.environments.templates.create | P: reusable network/files/env/setup/packages/Skills/Plugins/capability config, 201 | R `template-create`; V nullable Skill selector projection | C DB; I/K recorded real frozen-reference initialization | Restricted forms and unqualified combinations; complete hosted initialization semantics | +| 30 | beta.agents.environments.templates.retrieve | P: safe resource read | R `template-read`, deleted owned read; V nullable Skill selector projection | C DB; I/K recorded reference workflow | Full field/default/redaction parity; no live-secret projection inference | +| 31 | beta.agents.environments.templates.update | P: field replacement/null clearing, empty timestamp touch | R `template-patch`, `template-null`, `template-noop`; V nullable Skill selector projection | C DB no-op; I/K recorded frozen Session behavior | Referenced Session files/env/setup/packages overrides reject; null network/list/Skill-version remains unresolved | | 32 | beta.agents.environments.templates.list | P: scoped cursor list | R `template-list-empty-scoped` | Recorded resource DB checks; no positive C list replay | Full nonempty/multipage/mutation/default/error parity | | 33 | beta.agents.environments.templates.delete | P: delete resource, preserve committed Session snapshot | R `cleanup` at Template path, post-delete read | C DB; K recorded live deletion then continuation | Concurrent references/delete and exact errors | | 34 | beta.agents.vaults.create | P: tenant resource, 201 | R `vault-create`, `vault-empty-token-fixture` | C DB; O recorded MCP attachment workflow | Archive lifecycle, full defaults and selection parity | @@ -106,7 +105,7 @@ Paths in the appendix include `/v1`. SDK names here omit `client.`. `P` means pa 1. **Public generic semantics:** sampled create/event/envelope/error/no-op corrections are merged. Resource-by-resource omissions/null/default/error params, malformed queries, list caps, unknown/empty query handling, concurrent mutation and deletion require separate evidence. Metadata U+0000 remains an implementation-validation question from the prior audit, not a newly reproduced result here. 2. **Session differences:** The Session admission batch removes idle `none` creation and empty metadata update. Local durable creation idempotency remains an explicit difference. Whitespace-only input succeeds officially but is rejected by the existing Core message validator; this newly observed difference is queued separately. Session agent updates, newer Environment shapes and root Item turn_id are baseline-upgrade questions. -3. **Template/Skill composition:** referenced files/env/setup/packages override rejection is a known implementation gap; exact merge/replacement/null semantics need evidence. Null reference versions/null override lists, unversioned Skill content, top-level version metadata and last/default/latest deletion remain unresolved. +3. **Template/Skill composition:** referenced files/env/setup/packages override rejection is a known implementation gap; exact merge/replacement/null semantics need evidence. Null override lists, unversioned Skill content, top-level version metadata and last/default/latest deletion remain unresolved. 4. **Execution coverage:** use T's qualified matrix, not a blanket missing-image/structured-output claim. MiniMax functions/service MCP, optional tool combinations, unsupported images/placements and broader native lifecycle are explicit restrictions. PTC omission retains approved native behavior; Claude/MiniMax public Usage remains null; child settlement cadence/native close limits remain visible. No second executor/model loop or guessed counters are justified. 5. **Workspace and resources:** live Files bounds, symlink/path/cursor choices, artifact overwrite/republishing/headers/cancellation edges, full Environment metadata/lifecycle and Vault archive/in-flight-token semantics remain partial or unknown. Retired Core-managed E2B acceptance cannot qualify current user enrollment. diff --git a/contracts/agents-api/resource-selector-semantics.md b/contracts/agents-api/resource-selector-semantics.md index a3236c31a..02f6f65ab 100644 --- a/contracts/agents-api/resource-selector-semantics.md +++ b/contracts/agents-api/resource-selector-semantics.md @@ -25,9 +25,12 @@ returned 404 despite successful creation; a separate bounded follow-up observed that version after approximately 31 seconds. This is a recorded transient visibility observation, not a guaranteed consistency interval or a behavior Core should imitate. Template omission/null admission and projection, latest and exact -selectors are separately recorded. A further hosted Session probe uses divergent -default version 1 and latest version 2 to distinguish resolution from mere request -acceptance. Private evidence is under +selectors are separately recorded. A further hosted Session probe made 36 calls: with default version 1 and latest +version 2, omitted/null resolved to 1, latest to 2, and exact "1" to 1. All four +retained their versions after the default changed to 2. Two actual gpt-6-astra +Turns read installed files and returned distinct private markers, proving frozen +null/default version 1 and latest version 2 content. All four Sessions and the +Skill were deleted; asynchronous physical sandbox destruction was not observed. Private evidence is under `~/.parsar/remediation/20260923/skill-version-alignment/official/`. ## Source File errors @@ -45,3 +48,25 @@ were read or deleted. Evidence: `~/.parsar/remediation/20260923/files-error-alignment/official-probe.json`. Exact error prose, additional parser detail, purpose filtering, bounds and lookup order are not changed or claimed as aligned. + +## Core acceptance + +`TestSkillSelectorsOfficialClientPostgres` and +`TestSourceFileErrorsOfficialClientPostgres` exercise real HTTP/PostgreSQL with +fixed SDK and raw requests. They cover exact Template reference projection, +independent handler reads, missing and foreign resources, safe errors and retained +owned data. They do not represent native model execution. + +The separate Codex/Docker run at `2ccc729d3acfa8d7109f671d480753d74d568279` +passed on its first attempt with two real Kimi K3 Turns. Null through a Template +froze version 1; direct latest froze version 2. Changing source default and Template +selectors left same-key creation retries unchanged and created no Turn. After +both sources were deleted and Core restarted, each Session still exposed its +concrete version and returned only its own previously undisclosed Skill marker. +Foreign Session reads failed. Source/binary/image hashes, request/event/history +records, secret scans and verified owned-resource cleanup are retained under +`~/.parsar/remediation/20260923/skill-version-alignment/live/`. + +The integrated Files error change does not alter Skill selection, initialization, +Runtime or adapter code. This live run does not qualify another harness/Provider, +upstream physical retention, or omitted/null cross-form retry equivalence. From 756654b318b5cdd841213e99cca1e368045f1485 Mon Sep 17 00:00:00 2001 From: saladday <1203511142@qq.com> Date: Wed, 23 Sep 2026 01:39:47 +0800 Subject: [PATCH 6/7] Record integrated resource alignment validation --- contracts/agents-api/resource-selector-semantics.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/contracts/agents-api/resource-selector-semantics.md b/contracts/agents-api/resource-selector-semantics.md index 02f6f65ab..dc9bb8769 100644 --- a/contracts/agents-api/resource-selector-semantics.md +++ b/contracts/agents-api/resource-selector-semantics.md @@ -70,3 +70,11 @@ records, secret scans and verified owned-resource cleanup are retained under The integrated Files error change does not alter Skill selection, initialization, Runtime or adapter code. This live run does not qualify another harness/Provider, upstream physical retention, or omitted/null cross-form retry equivalence. + +At integrated source `9d9639bd90bc74e7c27832b28dca27aac6c18781`, server +`make -o check-web check` passed, including the real PostgreSQL suite, fixed SDK +resource checks, byte-for-byte sqlc generation, native package checks and builds. +`make openapi` produced no schema change. Web/client/dependencies are byte-unchanged +from `c86b5bb`; the preceding full Web acceptance is reused, not claimed as a new +run. Optional live adapter profiles and the 512 MiB storage stress case are not +newly qualified. Subsequent changes only record evidence in documentation. From 2e4c56abe056bf7ddfd3cc8376f299757b64898e Mon Sep 17 00:00:00 2001 From: saladday <1203511142@qq.com> Date: Wed, 23 Sep 2026 01:47:10 +0800 Subject: [PATCH 7/7] Record fresh Web gate completing batch checks --- contracts/agents-api/resource-selector-semantics.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/contracts/agents-api/resource-selector-semantics.md b/contracts/agents-api/resource-selector-semantics.md index dc9bb8769..4d6f6121d 100644 --- a/contracts/agents-api/resource-selector-semantics.md +++ b/contracts/agents-api/resource-selector-semantics.md @@ -74,7 +74,9 @@ upstream physical retention, or omitted/null cross-form retry equivalence. At integrated source `9d9639bd90bc74e7c27832b28dca27aac6c18781`, server `make -o check-web check` passed, including the real PostgreSQL suite, fixed SDK resource checks, byte-for-byte sqlc generation, native package checks and builds. -`make openapi` produced no schema change. Web/client/dependencies are byte-unchanged -from `c86b5bb`; the preceding full Web acceptance is reused, not claimed as a new -run. Optional live adapter profiles and the 512 MiB storage stress case are not -newly qualified. Subsequent changes only record evidence in documentation. +`make openapi` produced no schema change. A fresh local `make check-web` at +`756654b` passed typechecks, Core doctor tests, 287 client tests, 583 Web tests, +build and all 76 browser cases on isolated ports. These split runs cover every +`make check` target; the commits between them change only evidence documentation. +Optional live adapter profiles and the 512 MiB storage stress case are not newly +qualified. Subsequent changes only record evidence in documentation.