diff --git a/CHANGELOG.md b/CHANGELOG.md index bc95e9b..a0e5bcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ existing `0.1.0` release; earlier development prereleases are not listed. ### Fixed +- Preserve Identity `tools` arrays and explicit null when loading JSON into request parameters, so empty-array replacement and clearing overrides retain their intended semantics. - Align Forward Outcome rubric objects and Managed Agent/Dream model configuration with the official API. Dream model defaults to `auto` when omitted; object configuration supports `effort` and `context_window`. ### Breaking changes diff --git a/docs/api/reference.md b/docs/api/reference.md index 4944ff0..0f45ae8 100644 --- a/docs/api/reference.md +++ b/docs/api/reference.md @@ -399,6 +399,7 @@ import "github.com/QoderAI/qoder-cloud-agents-sdk-go/forward" - [func \(r \*IdentityConfigSpec\) UnmarshalJSON\(data \[\]byte\) error](<#IdentityConfigSpec.UnmarshalJSON>) - [type IdentityConfigSpecParam](<#IdentityConfigSpecParam>) - [func \(r IdentityConfigSpecParam\) MarshalJSON\(\) \(\[\]byte, error\)](<#IdentityConfigSpecParam.MarshalJSON>) + - [func \(r \*IdentityConfigSpecParam\) UnmarshalAPIJSON\(data \[\]byte\) error](<#IdentityConfigSpecParam.UnmarshalAPIJSON>) - [func \(r \*IdentityConfigSpecParam\) UnmarshalJSON\(data \[\]byte\) error](<#IdentityConfigSpecParam.UnmarshalJSON>) - [type IdentityConfigUpsertParams](<#IdentityConfigUpsertParams>) - [func \(r IdentityConfigUpsertParams\) MarshalJSON\(\) \(\[\]byte, error\)](<#IdentityConfigUpsertParams.MarshalJSON>) @@ -453,6 +454,7 @@ import "github.com/QoderAI/qoder-cloud-agents-sdk-go/forward" - [func \(r \*IdentityToolsUnion\) UnmarshalJSON\(data \[\]byte\) error](<#IdentityToolsUnion.UnmarshalJSON>) - [type IdentityToolsUnionParam](<#IdentityToolsUnionParam>) - [func \(r IdentityToolsUnionParam\) MarshalJSON\(\) \(\[\]byte, error\)](<#IdentityToolsUnionParam.MarshalJSON>) + - [func \(r \*IdentityToolsUnionParam\) UnmarshalAPIJSON\(data \[\]byte\) error](<#IdentityToolsUnionParam.UnmarshalAPIJSON>) - [func \(r \*IdentityToolsUnionParam\) UnmarshalJSON\(data \[\]byte\) error](<#IdentityToolsUnionParam.UnmarshalJSON>) - [type IdentityUpdateParams](<#IdentityUpdateParams>) - [func \(r IdentityUpdateParams\) MarshalJSON\(\) \(\[\]byte, error\)](<#IdentityUpdateParams.MarshalJSON>) @@ -6031,6 +6033,15 @@ func (r IdentityConfigSpecParam) MarshalJSON() ([]byte, error) + +### func \(\*IdentityConfigSpecParam\) [UnmarshalAPIJSON]() + +```go +func (r *IdentityConfigSpecParam) UnmarshalAPIJSON(data []byte) error +``` + + + ### func \(\*IdentityConfigSpecParam\) [UnmarshalJSON]() @@ -6635,6 +6646,15 @@ func (r IdentityToolsUnionParam) MarshalJSON() ([]byte, error) + +### func \(\*IdentityToolsUnionParam\) [UnmarshalAPIJSON]() + +```go +func (r *IdentityToolsUnionParam) UnmarshalAPIJSON(data []byte) error +``` + + + ### func \(\*IdentityToolsUnionParam\) [UnmarshalJSON]() diff --git a/forward/configuration_types.go b/forward/configuration_types.go index 329802b..8bc246e 100644 --- a/forward/configuration_types.go +++ b/forward/configuration_types.go @@ -1,6 +1,7 @@ package forward import ( + "bytes" "encoding/json" "fmt" "github.com/QoderAI/qoder-cloud-agents-sdk-go/convention/apijson" @@ -609,8 +610,22 @@ func (r IdentityConfigSpecParam) MarshalJSON() ([]byte, error) { type shadow IdentityConfigSpecParam return param.MarshalObject(r, (*shadow)(&r)) } + +func (r *IdentityConfigSpecParam) UnmarshalAPIJSON(data []byte) error { return r.UnmarshalJSON(data) } + func (r *IdentityConfigSpecParam) UnmarshalJSON(data []byte) error { - return apijson.UnmarshalRoot(data, r) + if err := apijson.UnmarshalRoot(data, r); err != nil { + return err + } + var fields map[string]json.RawMessage + if err := json.Unmarshal(data, &fields); err != nil { + return err + } + if tools, ok := fields["tools"]; ok { + // The generic decoder treats null union fields as absent. + return r.Tools.UnmarshalJSON(tools) + } + return nil } type SessionEventParam struct { @@ -671,8 +686,25 @@ type IdentityToolsUnionParam struct { func (r IdentityToolsUnionParam) MarshalJSON() ([]byte, error) { return param.MarshalUnion(r, r.OfOverrides, r.OfArray) } + +func (r *IdentityToolsUnionParam) UnmarshalAPIJSON(data []byte) error { return r.UnmarshalJSON(data) } + func (r *IdentityToolsUnionParam) UnmarshalJSON(data []byte) error { - return apijson.UnmarshalRoot(data, r) + *r = IdentityToolsUnionParam{} + data = bytes.TrimSpace(data) + if bytes.Equal(data, []byte("null")) { + *r = param.NullStruct[IdentityToolsUnionParam]() + return nil + } + if len(data) > 0 { + switch data[0] { + case '[': + return json.Unmarshal(data, &r.OfArray) + case '{': + return json.Unmarshal(data, &r.OfOverrides) + } + } + return fmt.Errorf("identity tools must be an object, an array, or null") } type IdentityToolsUnion struct { diff --git a/forward/official_contract_test.go b/forward/official_contract_test.go index 5bab9df..70a7f9b 100644 --- a/forward/official_contract_test.go +++ b/forward/official_contract_test.go @@ -116,6 +116,44 @@ func TestOfficialIdentityNullAndArrayOverrides(t *testing.T) { } } +func TestOfficialIdentityToolsJSONRoundTrip(t *testing.T) { + for _, tools := range []string{ + `[]`, + `[{"type":"agent_toolset_20260401"}]`, + `{}`, + `{"Bash":{"enabled":false}}`, + `null`, + } { + t.Run(tools, func(t *testing.T) { + var params forward.IdentityConfigUpsertParams + input := `{"identity_config":{"tools":` + tools + `}}` + if err := json.Unmarshal([]byte(input), ¶ms); err != nil { + t.Fatal(err) + } + client := testClient(func(req *http.Request) (*http.Response, error) { + body, err := io.ReadAll(req.Body) + if err != nil { + t.Fatal(err) + } + var got, want any + if err := json.Unmarshal(body, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(input), &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("request changed from %s to %s", input, body) + } + return reply(req, 200, `{}`), nil + }) + if _, err := client.Identities.Configs.Upsert(context.Background(), "idn", "tmpl", params); err != nil { + t.Fatal(err) + } + }) + } +} + func TestOfficialMountPaginationRetainsScope(t *testing.T) { for _, reverse := range []bool{false, true} { calls := 0