Skip to content

Commit bb7b8f8

Browse files
authored
fix(unstable-v2): parse nextCursor and listed session directories strictly (#2567)
* fix(unstable-v2): parse nextCursor and listed session directories strictly * test: use assert_eq for empty checks
1 parent 609d3fa commit bb7b8f8

7 files changed

Lines changed: 45 additions & 30 deletions

File tree

‎agent-client-protocol-schema/src/v2/agent.rs‎

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1811,9 +1811,8 @@ pub struct ListSessionsResponse {
18111811
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
18121812
pub sessions: Vec<SessionInfo>,
18131813
/// Opaque cursor token. If present, pass this in the next request's cursor parameter
1814-
/// to fetch the next page. If absent, there are no more results.
1815-
#[serde_as(deserialize_as = "DefaultOnError")]
1816-
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))]
1814+
/// to fetch the next page. Omitted and `null` are equivalent and mean there are no more
1815+
/// results.
18171816
#[serde(default)]
18181817
pub next_cursor: Option<SessionListCursor>,
18191818
/// The _meta property is reserved by ACP to allow clients and agents to attach additional
@@ -1964,10 +1963,9 @@ pub struct SessionInfo {
19641963
/// Additional workspace roots reported for this session. Each path must be absolute.
19651964
///
19661965
/// When present, this is the complete ordered additional-root list reported
1967-
/// by the Agent. Omitted and empty values are equivalent: the response
1966+
/// by the Agent. Omitted, `null`, and empty values are equivalent: the response
19681967
/// reports no additional roots.
1969-
#[serde_as(deserialize_as = "DefaultOnError<VecSkipError<_>>")]
1970-
#[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true, "x-deserialize-skip-invalid-items" = true)))]
1968+
#[serde_as(deserialize_as = "DefaultOnNull")]
19711969
#[serde(default, skip_serializing_if = "Vec::is_empty")]
19721970
pub additional_directories: Vec<AbsolutePath>,
19731971

@@ -5882,6 +5880,27 @@ mod test_serialization {
58825880
json!({ "source": "test" }).as_object().unwrap().clone()
58835881
}
58845882

5883+
#[test]
5884+
fn list_sessions_cursor_and_session_directories_are_strict() {
5885+
for cursor in [json!(null), json!("next")] {
5886+
let response = json!({ "sessions": [], "nextCursor": cursor });
5887+
serde_json::from_value::<ListSessionsResponse>(response).unwrap();
5888+
}
5889+
let malformed = json!({ "sessions": [], "nextCursor": 42 });
5890+
assert!(serde_json::from_value::<ListSessionsResponse>(malformed).is_err());
5891+
5892+
let session = |dirs: serde_json::Value| json!({ "sessionId": "s", "cwd": "/w", "additionalDirectories": dirs });
5893+
let info: SessionInfo = serde_json::from_value(session(json!(null))).unwrap();
5894+
assert_eq!(info.additional_directories, Vec::<AbsolutePath>::new());
5895+
for bad in [json!(["/a", 42]), json!("/a")] {
5896+
assert!(serde_json::from_value::<SessionInfo>(session(bad)).is_err());
5897+
}
5898+
// A session with malformed directories is skipped, not truncated.
5899+
let response = json!({ "sessions": [session(json!(["/a", 42]))] });
5900+
let response: ListSessionsResponse = serde_json::from_value(response).unwrap();
5901+
assert_eq!(response.sessions, Vec::<SessionInfo>::new());
5902+
}
5903+
58855904
#[test]
58865905
fn other_fallbacks_preserve_optional_fields_verbatim() {
58875906
let valid = json!({ "type": "_custom", "methodId": "m", "name": "M",

‎docs/protocol/v2/draft/schema.mdx‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -765,7 +765,8 @@ See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/d
765765
</ResponseField>
766766
<ResponseField name="nextCursor" type={<><span><a href="#sessionlistcursor">SessionListCursor</a></span><span> | null</span></>} >
767767
Opaque cursor token. If present, pass this in the next request's cursor parameter
768-
to fetch the next page. If absent, there are no more results.
768+
to fetch the next page. Omitted and `null` are equivalent and mean there are no more
769+
results.
769770
</ResponseField>
770771
<ResponseField name="sessions" type={<a href="#sessioninfo">SessionInfo[]</a>} required>
771772
Array of session information objects.
@@ -6886,7 +6887,7 @@ See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/d
68866887
Additional workspace roots reported for this session. Each path must be absolute.
68876888

68886889
When present, this is the complete ordered additional-root list reported
6889-
by the Agent. Omitted and empty values are equivalent: the response
6890+
by the Agent. Omitted, `null`, and empty values are equivalent: the response
68906891
reports no additional roots.
68916892

68926893
</ResponseField>

‎docs/protocol/v2/draft/session-list.mdx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,7 @@ The Agent **MUST** respond with a list of sessions and optional pagination metad
118118
<ResponseField name="additionalDirectories" type="string[]">
119119
If the Agent advertises `session.additionalDirectories`, it
120120
**MAY** include this field to report the complete ordered additional-root list
121-
associated with the listed session. Omitted and empty values are
122-
equivalent: this `SessionInfo` response reports no additional roots.
121+
associated with the listed session. Omitted, `null`, and empty values are equivalent: this `SessionInfo` response reports no additional roots.
123122
Clients **MUST NOT** merge this field with prior values or infer additional
124123
roots from agent-specific state.
125124
</ResponseField>
@@ -139,7 +138,8 @@ The Agent **MUST** respond with a list of sessions and optional pagination metad
139138

140139
<ResponseField name="nextCursor" type="string">
141140
Opaque cursor token. If present, pass this in the next request's `cursor`
142-
parameter to fetch the next page. If absent, there are no more results.
141+
parameter to fetch the next page. Omitted and `null` are equivalent and mean
142+
there are no more results.
143143
</ResponseField>
144144

145145
When no sessions match the criteria, the Agent **MUST** return an empty `sessions` array.

‎docs/protocol/v2/schema.mdx‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,8 @@ See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/e
435435
</ResponseField>
436436
<ResponseField name="nextCursor" type={<><span><a href="#sessionlistcursor">SessionListCursor</a></span><span> | null</span></>} >
437437
Opaque cursor token. If present, pass this in the next request's cursor parameter
438-
to fetch the next page. If absent, there are no more results.
438+
to fetch the next page. Omitted and `null` are equivalent and mean there are no more
439+
results.
439440
</ResponseField>
440441
<ResponseField name="sessions" type={<a href="#sessioninfo">SessionInfo[]</a>} required>
441442
Array of session information objects.
@@ -5905,7 +5906,7 @@ See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/e
59055906
Additional workspace roots reported for this session. Each path must be absolute.
59065907

59075908
When present, this is the complete ordered additional-root list reported
5908-
by the Agent. Omitted and empty values are equivalent: the response
5909+
by the Agent. Omitted, `null`, and empty values are equivalent: the response
59095910
reports no additional roots.
59105911

59115912
</ResponseField>

‎docs/protocol/v2/session-list.mdx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,7 @@ The Agent **MUST** respond with a list of sessions and optional pagination metad
118118
<ResponseField name="additionalDirectories" type="string[]">
119119
If the Agent advertises `session.additionalDirectories`, it
120120
**MAY** include this field to report the complete ordered additional-root list
121-
associated with the listed session. Omitted and empty values are
122-
equivalent: this `SessionInfo` response reports no additional roots.
121+
associated with the listed session. Omitted, `null`, and empty values are equivalent: this `SessionInfo` response reports no additional roots.
123122
Clients **MUST NOT** merge this field with prior values or infer additional
124123
roots from agent-specific state.
125124
</ResponseField>
@@ -139,7 +138,8 @@ The Agent **MUST** respond with a list of sessions and optional pagination metad
139138

140139
<ResponseField name="nextCursor" type="string">
141140
Opaque cursor token. If present, pass this in the next request's `cursor`
142-
parameter to fetch the next page. If absent, there are no more results.
141+
parameter to fetch the next page. Omitted and `null` are equivalent and mean
142+
there are no more results.
143143
</ResponseField>
144144

145145
When no sessions match the criteria, the Agent **MUST** return an empty `sessions` array.

‎schema/v2/schema.json‎

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4131,16 +4131,15 @@
41314131
"x-deserialize-skip-invalid-items": true
41324132
},
41334133
"nextCursor": {
4134-
"description": "Opaque cursor token. If present, pass this in the next request's cursor parameter\nto fetch the next page. If absent, there are no more results.",
4134+
"description": "Opaque cursor token. If present, pass this in the next request's cursor parameter\nto fetch the next page. Omitted and `null` are equivalent and mean there are no more\nresults.",
41354135
"anyOf": [
41364136
{
41374137
"$ref": "#/$defs/SessionListCursor"
41384138
},
41394139
{
41404140
"type": "null"
41414141
}
4142-
],
4143-
"x-deserialize-default-on-error": true
4142+
]
41444143
},
41454144
"_meta": {
41464145
"description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Omitted and `null` are equivalent and mean no metadata.\nImplementations MUST NOT make assumptions about values at these keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/extensibility)",
@@ -4174,13 +4173,11 @@
41744173
]
41754174
},
41764175
"additionalDirectories": {
4177-
"description": "Additional workspace roots reported for this session. Each path must be absolute.\n\nWhen present, this is the complete ordered additional-root list reported\nby the Agent. Omitted and empty values are equivalent: the response\nreports no additional roots.",
4176+
"description": "Additional workspace roots reported for this session. Each path must be absolute.\n\nWhen present, this is the complete ordered additional-root list reported\nby the Agent. Omitted, `null`, and empty values are equivalent: the response\nreports no additional roots.",
41784177
"type": "array",
41794178
"items": {
41804179
"$ref": "#/$defs/AbsolutePath"
4181-
},
4182-
"x-deserialize-default-on-error": true,
4183-
"x-deserialize-skip-invalid-items": true
4180+
}
41844181
},
41854182
"title": {
41864183
"description": "Human-readable title for the session\n\nOmitted and `null` are equivalent and mean no title is provided.",

‎schema/v2/schema.unstable.json‎

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4573,16 +4573,15 @@
45734573
"x-deserialize-skip-invalid-items": true
45744574
},
45754575
"nextCursor": {
4576-
"description": "Opaque cursor token. If present, pass this in the next request's cursor parameter\nto fetch the next page. If absent, there are no more results.",
4576+
"description": "Opaque cursor token. If present, pass this in the next request's cursor parameter\nto fetch the next page. Omitted and `null` are equivalent and mean there are no more\nresults.",
45774577
"anyOf": [
45784578
{
45794579
"$ref": "#/$defs/SessionListCursor"
45804580
},
45814581
{
45824582
"type": "null"
45834583
}
4584-
],
4585-
"x-deserialize-default-on-error": true
4584+
]
45864585
},
45874586
"_meta": {
45884587
"description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Omitted and `null` are equivalent and mean no metadata.\nImplementations MUST NOT make assumptions about values at these keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/draft/extensibility)",
@@ -4616,13 +4615,11 @@
46164615
]
46174616
},
46184617
"additionalDirectories": {
4619-
"description": "Additional workspace roots reported for this session. Each path must be absolute.\n\nWhen present, this is the complete ordered additional-root list reported\nby the Agent. Omitted and empty values are equivalent: the response\nreports no additional roots.",
4618+
"description": "Additional workspace roots reported for this session. Each path must be absolute.\n\nWhen present, this is the complete ordered additional-root list reported\nby the Agent. Omitted, `null`, and empty values are equivalent: the response\nreports no additional roots.",
46204619
"type": "array",
46214620
"items": {
46224621
"$ref": "#/$defs/AbsolutePath"
4623-
},
4624-
"x-deserialize-default-on-error": true,
4625-
"x-deserialize-skip-invalid-items": true
4622+
}
46264623
},
46274624
"title": {
46284625
"description": "Human-readable title for the session\n\nOmitted and `null` are equivalent and mean no title is provided.",

0 commit comments

Comments
 (0)