act-cli 0.7.4__tar.gz → 0.7.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {act_cli-0.7.4 → act_cli-0.7.6}/Cargo.lock +2 -33
- {act_cli-0.7.4 → act_cli-0.7.6}/Cargo.toml +1 -1
- {act_cli-0.7.4 → act_cli-0.7.6}/PKG-INFO +1 -1
- {act_cli-0.7.4 → act_cli-0.7.6}/act-cli/Cargo.toml +1 -1
- {act_cli-0.7.4 → act_cli-0.7.6}/act-cli/src/http.rs +50 -8
- {act_cli-0.7.4 → act_cli-0.7.6}/act-cli/src/main.rs +55 -3
- {act_cli-0.7.4 → act_cli-0.7.6}/act-cli/src/rmcp_bridge.rs +111 -5
- {act_cli-0.7.4 → act_cli-0.7.6}/README.md +0 -0
- {act_cli-0.7.4 → act_cli-0.7.6}/act-cli/README.md +0 -0
- {act_cli-0.7.4 → act_cli-0.7.6}/act-cli/build.rs +0 -0
- {act_cli-0.7.4 → act_cli-0.7.6}/act-cli/src/config.rs +0 -0
- {act_cli-0.7.4 → act_cli-0.7.6}/act-cli/src/format.rs +0 -0
- {act_cli-0.7.4 → act_cli-0.7.6}/act-cli/src/resolve.rs +0 -0
- {act_cli-0.7.4 → act_cli-0.7.6}/act-cli/src/runtime/bindings/mod.rs +0 -0
- {act_cli-0.7.4 → act_cli-0.7.6}/act-cli/src/runtime/effective.rs +0 -0
- {act_cli-0.7.4 → act_cli-0.7.6}/act-cli/src/runtime/fs_matcher.rs +0 -0
- {act_cli-0.7.4 → act_cli-0.7.6}/act-cli/src/runtime/fs_policy.rs +0 -0
- {act_cli-0.7.4 → act_cli-0.7.6}/act-cli/src/runtime/http_client.rs +0 -0
- {act_cli-0.7.4 → act_cli-0.7.6}/act-cli/src/runtime/http_policy.rs +0 -0
- {act_cli-0.7.4 → act_cli-0.7.6}/act-cli/src/runtime/mod.rs +0 -0
- {act_cli-0.7.4 → act_cli-0.7.6}/act-cli/src/runtime/network.rs +0 -0
- {act_cli-0.7.4 → act_cli-0.7.6}/act-cli/src/runtime/sessions.rs +0 -0
- {act_cli-0.7.4 → act_cli-0.7.6}/act-cli/src/runtime/sockets_policy.rs +0 -0
- {act_cli-0.7.4 → act_cli-0.7.6}/act-cli/wit/deps/act-core/act-core.wit +0 -0
- {act_cli-0.7.4 → act_cli-0.7.6}/act-cli/wit/deps/act-tools/act-tools.wit +0 -0
- {act_cli-0.7.4 → act_cli-0.7.6}/act-cli/wit/deps.lock +0 -0
- {act_cli-0.7.4 → act_cli-0.7.6}/act-cli/wit/deps.toml +0 -0
- {act_cli-0.7.4 → act_cli-0.7.6}/act-cli/wit/world.wit +0 -0
- {act_cli-0.7.4 → act_cli-0.7.6}/pyproject.toml +0 -0
|
@@ -4,7 +4,7 @@ version = 4
|
|
|
4
4
|
|
|
5
5
|
[[package]]
|
|
6
6
|
name = "act-build"
|
|
7
|
-
version = "0.7.
|
|
7
|
+
version = "0.7.6"
|
|
8
8
|
dependencies = [
|
|
9
9
|
"act-types",
|
|
10
10
|
"anyhow",
|
|
@@ -35,7 +35,7 @@ dependencies = [
|
|
|
35
35
|
|
|
36
36
|
[[package]]
|
|
37
37
|
name = "act-cli"
|
|
38
|
-
version = "0.7.
|
|
38
|
+
version = "0.7.6"
|
|
39
39
|
dependencies = [
|
|
40
40
|
"act-types",
|
|
41
41
|
"anyhow",
|
|
@@ -1418,34 +1418,6 @@ dependencies = [
|
|
|
1418
1418
|
"tracing",
|
|
1419
1419
|
]
|
|
1420
1420
|
|
|
1421
|
-
[[package]]
|
|
1422
|
-
name = "h3"
|
|
1423
|
-
version = "0.0.8"
|
|
1424
|
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1425
|
-
checksum = "10872b55cfb02a821b69dc7cf8dc6a71d6af25eb9a79662bec4a9d016056b3be"
|
|
1426
|
-
dependencies = [
|
|
1427
|
-
"bytes",
|
|
1428
|
-
"fastrand",
|
|
1429
|
-
"futures-util",
|
|
1430
|
-
"http",
|
|
1431
|
-
"pin-project-lite",
|
|
1432
|
-
"tokio",
|
|
1433
|
-
]
|
|
1434
|
-
|
|
1435
|
-
[[package]]
|
|
1436
|
-
name = "h3-quinn"
|
|
1437
|
-
version = "0.0.10"
|
|
1438
|
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1439
|
-
checksum = "8b2e732c8d91a74731663ac8479ab505042fbf547b9a207213ab7fbcbfc4f8b4"
|
|
1440
|
-
dependencies = [
|
|
1441
|
-
"bytes",
|
|
1442
|
-
"futures",
|
|
1443
|
-
"h3",
|
|
1444
|
-
"quinn",
|
|
1445
|
-
"tokio",
|
|
1446
|
-
"tokio-util",
|
|
1447
|
-
]
|
|
1448
|
-
|
|
1449
1421
|
[[package]]
|
|
1450
1422
|
name = "half"
|
|
1451
1423
|
version = "2.7.1"
|
|
@@ -2510,7 +2482,6 @@ checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
|
|
|
2510
2482
|
dependencies = [
|
|
2511
2483
|
"bytes",
|
|
2512
2484
|
"cfg_aliases",
|
|
2513
|
-
"futures-io",
|
|
2514
2485
|
"pin-project-lite",
|
|
2515
2486
|
"quinn-proto",
|
|
2516
2487
|
"quinn-udp",
|
|
@@ -2760,8 +2731,6 @@ dependencies = [
|
|
|
2760
2731
|
"futures-core",
|
|
2761
2732
|
"futures-util",
|
|
2762
2733
|
"h2",
|
|
2763
|
-
"h3",
|
|
2764
|
-
"h3-quinn",
|
|
2765
2734
|
"http",
|
|
2766
2735
|
"http-body",
|
|
2767
2736
|
"http-body-util",
|
|
@@ -37,7 +37,7 @@ dirs = "6"
|
|
|
37
37
|
futures-util = "0.3"
|
|
38
38
|
indicatif = "0.18.4"
|
|
39
39
|
regex = "1"
|
|
40
|
-
reqwest = { version = "0.13.3", default-features = false, features = ["rustls", "stream", "http2"
|
|
40
|
+
reqwest = { version = "0.13.3", default-features = false, features = ["rustls", "stream", "http2"] }
|
|
41
41
|
oci-client = "0.17"
|
|
42
42
|
sha2 = "0.11.0"
|
|
43
43
|
tar = "0.4"
|
|
@@ -24,6 +24,10 @@ pub struct AppState {
|
|
|
24
24
|
pub info: act_types::ComponentInfo,
|
|
25
25
|
pub component: runtime::ComponentHandle,
|
|
26
26
|
pub metadata: Metadata,
|
|
27
|
+
/// When `Some`, a single default session was pre-opened (session-of-1,
|
|
28
|
+
/// ACT-SESSIONS §3): `/sessions*` routes are unregistered and this id is
|
|
29
|
+
/// forced into every call's `std:session-id` metadata.
|
|
30
|
+
pub default_session_id: Option<String>,
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
// ── Conversion helpers ──
|
|
@@ -84,6 +88,14 @@ fn internal_error_response(message: &str) -> axum::response::Response {
|
|
|
84
88
|
.into_response()
|
|
85
89
|
}
|
|
86
90
|
|
|
91
|
+
/// Force `std:session-id` to the default when in session-of-1 mode, overriding
|
|
92
|
+
/// any body-supplied value (ACT-SESSIONS §3 "session-of-1").
|
|
93
|
+
fn apply_default_session(meta: &mut Metadata, default: &Option<String>) {
|
|
94
|
+
if let Some(id) = default {
|
|
95
|
+
meta.insert(META_SESSION_ID, serde_json::Value::String(id.clone()));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
87
99
|
/// Format an SseEvent as an axum SSE Event.
|
|
88
100
|
fn sse_event_to_axum(event: runtime::SseEvent) -> Option<Result<Event, std::convert::Infallible>> {
|
|
89
101
|
match event {
|
|
@@ -151,6 +163,7 @@ async fn list_tools_inner(
|
|
|
151
163
|
if let Some(value) = metadata {
|
|
152
164
|
meta.extend(runtime::Metadata::from(value));
|
|
153
165
|
}
|
|
166
|
+
apply_default_session(&mut meta, &state.default_session_id);
|
|
154
167
|
|
|
155
168
|
let (reply_tx, reply_rx) = tokio::sync::oneshot::channel();
|
|
156
169
|
let request = runtime::ComponentRequest::ListTools {
|
|
@@ -361,6 +374,7 @@ async fn tool_call_dispatcher(
|
|
|
361
374
|
if let Some(value) = body.metadata {
|
|
362
375
|
metadata.extend(Metadata::from(value));
|
|
363
376
|
}
|
|
377
|
+
apply_default_session(&mut metadata, &state.default_session_id);
|
|
364
378
|
|
|
365
379
|
let metadata_wit: Vec<(String, Vec<u8>)> = metadata.into();
|
|
366
380
|
|
|
@@ -522,16 +536,25 @@ async fn protocol_version_layer(request: Request, next: Next) -> axum::response:
|
|
|
522
536
|
}
|
|
523
537
|
|
|
524
538
|
pub fn create_router(state: Arc<AppState>) -> Router {
|
|
525
|
-
Router::new()
|
|
539
|
+
let mut router = Router::new()
|
|
526
540
|
.route("/info", get(get_info))
|
|
527
541
|
.route("/tools", axum::routing::any(tools_dispatcher))
|
|
528
|
-
.route("/tools/{name}", axum::routing::any(tool_call_dispatcher))
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
542
|
+
.route("/tools/{name}", axum::routing::any(tool_call_dispatcher));
|
|
543
|
+
|
|
544
|
+
// Session-of-1 hides the session machinery (ACT-SESSIONS §3): when a
|
|
545
|
+
// default session is pre-opened, the component looks stateless and the
|
|
546
|
+
// lifecycle endpoints are absent (404).
|
|
547
|
+
if state.default_session_id.is_none() {
|
|
548
|
+
router = router
|
|
549
|
+
.route(
|
|
550
|
+
"/sessions/open-args-schema",
|
|
551
|
+
axum::routing::any(session_open_args_schema_dispatcher),
|
|
552
|
+
)
|
|
553
|
+
.route("/sessions", axum::routing::post(session_open))
|
|
554
|
+
.route("/sessions/{id}", axum::routing::delete(session_close));
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
router
|
|
535
558
|
.layer(middleware::from_fn(protocol_version_layer))
|
|
536
559
|
.with_state(state)
|
|
537
560
|
}
|
|
@@ -567,4 +590,23 @@ mod tests {
|
|
|
567
590
|
fn query_method_is_valid() {
|
|
568
591
|
assert_eq!(query_method().as_str(), "QUERY");
|
|
569
592
|
}
|
|
593
|
+
|
|
594
|
+
#[test]
|
|
595
|
+
fn apply_default_session_overrides_and_skips() {
|
|
596
|
+
// Some(id): inject, overriding any existing value.
|
|
597
|
+
let mut meta = Metadata::from(serde_json::json!({"std:session-id": "client"}));
|
|
598
|
+
apply_default_session(&mut meta, &Some("sid_default".to_string()));
|
|
599
|
+
assert_eq!(
|
|
600
|
+
meta.get_as::<String>(META_SESSION_ID).as_deref(),
|
|
601
|
+
Some("sid_default")
|
|
602
|
+
);
|
|
603
|
+
|
|
604
|
+
// None: leave metadata untouched.
|
|
605
|
+
let mut meta2 = Metadata::from(serde_json::json!({"std:session-id": "client"}));
|
|
606
|
+
apply_default_session(&mut meta2, &None);
|
|
607
|
+
assert_eq!(
|
|
608
|
+
meta2.get_as::<String>(META_SESSION_ID).as_deref(),
|
|
609
|
+
Some("client")
|
|
610
|
+
);
|
|
611
|
+
}
|
|
570
612
|
}
|
|
@@ -101,6 +101,15 @@ enum Command {
|
|
|
101
101
|
#[arg(short, long)]
|
|
102
102
|
listen: Option<String>,
|
|
103
103
|
|
|
104
|
+
/// Pre-open a single session at startup from this JSON object and
|
|
105
|
+
/// run as session-of-1: every call uses the pre-opened session, the
|
|
106
|
+
/// session machinery is hidden from clients (no virtual
|
|
107
|
+
/// open_session/close_session tools, no /sessions endpoints), and any
|
|
108
|
+
/// client-supplied std:session-id is ignored. Requires a component
|
|
109
|
+
/// that exports act:sessions/session-provider.
|
|
110
|
+
#[arg(long)]
|
|
111
|
+
session_args: Option<String>,
|
|
112
|
+
|
|
104
113
|
#[command(flatten)]
|
|
105
114
|
opts: CommonOpts,
|
|
106
115
|
},
|
|
@@ -233,8 +242,9 @@ async fn main() -> Result<()> {
|
|
|
233
242
|
mcp,
|
|
234
243
|
http,
|
|
235
244
|
listen,
|
|
245
|
+
session_args,
|
|
236
246
|
opts,
|
|
237
|
-
} => cmd_run(component, mcp, http, listen, opts).await,
|
|
247
|
+
} => cmd_run(component, mcp, http, listen, session_args, opts).await,
|
|
238
248
|
Command::Call {
|
|
239
249
|
component,
|
|
240
250
|
tool,
|
|
@@ -404,11 +414,34 @@ fn parse_listen_addr(s: &str) -> Result<SocketAddr> {
|
|
|
404
414
|
anyhow::bail!("invalid listen address: {s} (expected [host]:port or port number)")
|
|
405
415
|
}
|
|
406
416
|
|
|
417
|
+
/// If `session_args` is set, open a single default session against the
|
|
418
|
+
/// prepared component and return its id (session-of-1, ACT-SESSIONS §3). The
|
|
419
|
+
/// session is closed automatically when the component actor shuts down
|
|
420
|
+
/// (`runtime` closes every tracked session on deinit).
|
|
421
|
+
async fn maybe_open_default_session(
|
|
422
|
+
pc: &PreparedComponent,
|
|
423
|
+
session_args: &Option<String>,
|
|
424
|
+
) -> Result<Option<String>> {
|
|
425
|
+
match session_args {
|
|
426
|
+
Some(json) => {
|
|
427
|
+
if !pc.has_sessions {
|
|
428
|
+
anyhow::bail!(
|
|
429
|
+
"--session-args was set, but the component does not export \
|
|
430
|
+
act:sessions/session-provider"
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
Ok(Some(open_session_for_call(pc, json).await?))
|
|
434
|
+
}
|
|
435
|
+
None => Ok(None),
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
407
439
|
async fn cmd_run(
|
|
408
440
|
component: ComponentRef,
|
|
409
441
|
mcp: bool,
|
|
410
442
|
http: bool,
|
|
411
443
|
listen: Option<String>,
|
|
444
|
+
session_args: Option<String>,
|
|
412
445
|
opts: CommonOpts,
|
|
413
446
|
) -> Result<()> {
|
|
414
447
|
// Transport matrix:
|
|
@@ -422,7 +455,16 @@ async fn cmd_run(
|
|
|
422
455
|
None => "[::1]:3000".parse().unwrap(),
|
|
423
456
|
};
|
|
424
457
|
let pc = prepare_component(&component, &opts).await?;
|
|
425
|
-
|
|
458
|
+
let default_session_id = maybe_open_default_session(&pc, &session_args).await?;
|
|
459
|
+
return rmcp_bridge::run_http(
|
|
460
|
+
addr,
|
|
461
|
+
pc.info,
|
|
462
|
+
pc.handle,
|
|
463
|
+
pc.metadata,
|
|
464
|
+
pc.has_sessions,
|
|
465
|
+
default_session_id,
|
|
466
|
+
)
|
|
467
|
+
.await;
|
|
426
468
|
}
|
|
427
469
|
|
|
428
470
|
if mcp {
|
|
@@ -430,7 +472,15 @@ async fn cmd_run(
|
|
|
430
472
|
anyhow::bail!("--listen requires --http (MCP stdio has no listen address)");
|
|
431
473
|
}
|
|
432
474
|
let pc = prepare_component(&component, &opts).await?;
|
|
433
|
-
|
|
475
|
+
let default_session_id = maybe_open_default_session(&pc, &session_args).await?;
|
|
476
|
+
return rmcp_bridge::run_stdio(
|
|
477
|
+
pc.info,
|
|
478
|
+
pc.handle,
|
|
479
|
+
pc.metadata,
|
|
480
|
+
pc.has_sessions,
|
|
481
|
+
default_session_id,
|
|
482
|
+
)
|
|
483
|
+
.await;
|
|
434
484
|
}
|
|
435
485
|
|
|
436
486
|
if http || listen.is_some() {
|
|
@@ -440,11 +490,13 @@ async fn cmd_run(
|
|
|
440
490
|
};
|
|
441
491
|
|
|
442
492
|
let pc = prepare_component(&component, &opts).await?;
|
|
493
|
+
let default_session_id = maybe_open_default_session(&pc, &session_args).await?;
|
|
443
494
|
|
|
444
495
|
let state = Arc::new(http::AppState {
|
|
445
496
|
info: pc.info,
|
|
446
497
|
component: pc.handle,
|
|
447
498
|
metadata: pc.metadata,
|
|
499
|
+
default_session_id,
|
|
448
500
|
});
|
|
449
501
|
|
|
450
502
|
tracing::info!(%addr, "ACT host listening");
|
|
@@ -31,6 +31,11 @@ pub struct ActRmcpBridge {
|
|
|
31
31
|
/// `act:sessions/session-provider`. Controls synthesis of virtual
|
|
32
32
|
/// `open_session`/`close_session` tools and routing of those calls.
|
|
33
33
|
pub has_sessions: bool,
|
|
34
|
+
/// When `Some`, the host pre-opened a single default session
|
|
35
|
+
/// (session-of-1, ACT-SESSIONS §3): session machinery is hidden and
|
|
36
|
+
/// this id is forced into every call's `std:session-id` metadata,
|
|
37
|
+
/// overriding any client-supplied value.
|
|
38
|
+
pub default_session_id: Option<String>,
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
fn map_content_part(part: &runtime::exports::act::tools::tool_provider::ContentPart) -> Content {
|
|
@@ -194,12 +199,14 @@ pub async fn run_stdio(
|
|
|
194
199
|
handle: runtime::ComponentHandle,
|
|
195
200
|
metadata: runtime::Metadata,
|
|
196
201
|
has_sessions: bool,
|
|
202
|
+
default_session_id: Option<String>,
|
|
197
203
|
) -> anyhow::Result<()> {
|
|
198
204
|
let bridge = ActRmcpBridge {
|
|
199
205
|
handle,
|
|
200
206
|
info,
|
|
201
207
|
metadata,
|
|
202
208
|
has_sessions,
|
|
209
|
+
default_session_id,
|
|
203
210
|
};
|
|
204
211
|
|
|
205
212
|
let service = rmcp::serve_server(bridge, (tokio::io::stdin(), tokio::io::stdout()))
|
|
@@ -225,6 +232,7 @@ pub async fn run_http(
|
|
|
225
232
|
handle: runtime::ComponentHandle,
|
|
226
233
|
metadata: runtime::Metadata,
|
|
227
234
|
has_sessions: bool,
|
|
235
|
+
default_session_id: Option<String>,
|
|
228
236
|
) -> anyhow::Result<()> {
|
|
229
237
|
use rmcp::transport::streamable_http_server::{
|
|
230
238
|
StreamableHttpServerConfig, StreamableHttpService, session::local::LocalSessionManager,
|
|
@@ -237,6 +245,7 @@ pub async fn run_http(
|
|
|
237
245
|
info: info.clone(),
|
|
238
246
|
metadata: metadata.clone(),
|
|
239
247
|
has_sessions,
|
|
248
|
+
default_session_id: default_session_id.clone(),
|
|
240
249
|
})
|
|
241
250
|
},
|
|
242
251
|
Arc::new(LocalSessionManager::default()),
|
|
@@ -257,10 +266,24 @@ pub async fn run_http(
|
|
|
257
266
|
// ── ServerHandler impl ──────────────────────────────────────────────────────
|
|
258
267
|
|
|
259
268
|
impl ActRmcpBridge {
|
|
269
|
+
/// Whether session lifecycle ops are exposed to clients. False in
|
|
270
|
+
/// session-of-1 mode (a default session is pre-opened and hidden).
|
|
271
|
+
fn expose_sessions(&self) -> bool {
|
|
272
|
+
self.has_sessions && self.default_session_id.is_none()
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/// Base metadata for non-call requests (list-tools, schema fetch),
|
|
276
|
+
/// with the default session-id injected when in session-of-1 mode.
|
|
277
|
+
fn base_metadata(&self) -> runtime::Metadata {
|
|
278
|
+
let mut meta = self.metadata.clone();
|
|
279
|
+
force_session_id(&mut meta, &self.default_session_id);
|
|
280
|
+
meta
|
|
281
|
+
}
|
|
282
|
+
|
|
260
283
|
async fn list_tools_impl(&self) -> Result<rmcp::model::ListToolsResult, rmcp::ErrorData> {
|
|
261
284
|
let (reply_tx, reply_rx) = tokio::sync::oneshot::channel();
|
|
262
285
|
let req = runtime::ComponentRequest::ListTools {
|
|
263
|
-
metadata: self.
|
|
286
|
+
metadata: self.base_metadata(),
|
|
264
287
|
reply: reply_tx,
|
|
265
288
|
};
|
|
266
289
|
|
|
@@ -286,10 +309,12 @@ impl ActRmcpBridge {
|
|
|
286
309
|
// Per ACT-MCP §3.2, adapters MUST inject the `_meta` argument
|
|
287
310
|
// property into tools of components exporting session-provider
|
|
288
311
|
// so agents can supply `std:session-id` (and other `std:*`
|
|
289
|
-
// keys) without relying on transport-level `_meta`.
|
|
290
|
-
|
|
312
|
+
// keys) without relying on transport-level `_meta`. In
|
|
313
|
+
// session-of-1 mode the host forces the session-id, so the hint
|
|
314
|
+
// is suppressed — the agent must NOT be prompted to supply it.
|
|
315
|
+
let mut tools = convert_tool_definitions(&list.tools, self.expose_sessions());
|
|
291
316
|
|
|
292
|
-
if self.
|
|
317
|
+
if self.expose_sessions() {
|
|
293
318
|
let open_schema = self.fetch_open_session_args_schema().await?;
|
|
294
319
|
tools.push(virtual_open_session_tool(open_schema));
|
|
295
320
|
tools.push(virtual_close_session_tool());
|
|
@@ -347,7 +372,7 @@ impl ActRmcpBridge {
|
|
|
347
372
|
// before any argument-level `_meta` extraction. Virtual tools
|
|
348
373
|
// are session-lifecycle ops, not session-bound capability calls,
|
|
349
374
|
// so they do not participate in the argument metadata channel.
|
|
350
|
-
if self.
|
|
375
|
+
if self.expose_sessions() {
|
|
351
376
|
match request.name.as_ref() {
|
|
352
377
|
VIRTUAL_OPEN_SESSION => {
|
|
353
378
|
let mut call_metadata = self.metadata.clone();
|
|
@@ -380,6 +405,9 @@ impl ActRmcpBridge {
|
|
|
380
405
|
call_metadata.extend(act_types::types::Metadata::from(Value::Object(map)));
|
|
381
406
|
}
|
|
382
407
|
apply_transport_meta(&mut call_metadata, ctx_meta);
|
|
408
|
+
// Session-of-1: force the pre-opened default id over any
|
|
409
|
+
// client-supplied std:session-id so the façade stays stateless.
|
|
410
|
+
force_session_id(&mut call_metadata, &self.default_session_id);
|
|
383
411
|
|
|
384
412
|
let cbor_args =
|
|
385
413
|
act_types::cbor::json_to_cbor(&Value::Object(arguments_obj)).map_err(|_| {
|
|
@@ -571,6 +599,18 @@ fn session_op_meta(op: &'static str) -> rmcp::model::Meta {
|
|
|
571
599
|
rmcp::model::Meta(map)
|
|
572
600
|
}
|
|
573
601
|
|
|
602
|
+
/// Force `std:session-id` to `default` when set, overriding any existing
|
|
603
|
+
/// value. Used in session-of-1 mode so the hidden default session wins over
|
|
604
|
+
/// client-supplied ids (ACT-SESSIONS §3 "session-of-1").
|
|
605
|
+
fn force_session_id(meta: &mut act_types::types::Metadata, default: &Option<String>) {
|
|
606
|
+
if let Some(id) = default {
|
|
607
|
+
meta.insert(
|
|
608
|
+
act_types::constants::META_SESSION_ID,
|
|
609
|
+
Value::String(id.clone()),
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
574
614
|
/// Merge the MCP transport-level `_meta` (lifted by rmcp into
|
|
575
615
|
/// `RequestContext::meta`) onto `call_metadata`. Per ACT-MCP §3.3 the
|
|
576
616
|
/// transport channel overrides any same-keyed value already present
|
|
@@ -701,6 +741,71 @@ mod tests {
|
|
|
701
741
|
tx
|
|
702
742
|
}
|
|
703
743
|
|
|
744
|
+
fn bridge_with_default(default: Option<&str>) -> ActRmcpBridge {
|
|
745
|
+
ActRmcpBridge {
|
|
746
|
+
handle: fake_handle(),
|
|
747
|
+
info: fake_info(),
|
|
748
|
+
metadata: runtime::Metadata::default(),
|
|
749
|
+
has_sessions: true,
|
|
750
|
+
default_session_id: default.map(str::to_string),
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
#[test]
|
|
755
|
+
fn expose_sessions_false_when_default_set() {
|
|
756
|
+
assert!(
|
|
757
|
+
!bridge_with_default(Some("sid_0")).expose_sessions(),
|
|
758
|
+
"session-of-1 must hide session machinery"
|
|
759
|
+
);
|
|
760
|
+
assert!(
|
|
761
|
+
bridge_with_default(None).expose_sessions(),
|
|
762
|
+
"without a default session, machinery stays exposed"
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
#[test]
|
|
767
|
+
fn base_metadata_injects_default_session_id() {
|
|
768
|
+
let meta = bridge_with_default(Some("sid_0")).base_metadata();
|
|
769
|
+
assert_eq!(
|
|
770
|
+
meta.get_as::<String>(act_types::constants::META_SESSION_ID)
|
|
771
|
+
.as_deref(),
|
|
772
|
+
Some("sid_0"),
|
|
773
|
+
"base metadata must carry the default session-id"
|
|
774
|
+
);
|
|
775
|
+
let none = bridge_with_default(None).base_metadata();
|
|
776
|
+
assert!(
|
|
777
|
+
none.get_as::<String>(act_types::constants::META_SESSION_ID)
|
|
778
|
+
.is_none(),
|
|
779
|
+
"no default → no session-id seeded"
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
#[test]
|
|
784
|
+
fn force_session_id_overrides_client_value() {
|
|
785
|
+
let mut meta = act_types::types::Metadata::from(serde_json::json!({
|
|
786
|
+
"std:session-id": "client-supplied",
|
|
787
|
+
}));
|
|
788
|
+
force_session_id(&mut meta, &Some("sid_default".to_string()));
|
|
789
|
+
assert_eq!(
|
|
790
|
+
meta.get_as::<String>(act_types::constants::META_SESSION_ID)
|
|
791
|
+
.as_deref(),
|
|
792
|
+
Some("sid_default"),
|
|
793
|
+
"default must override client-supplied session-id"
|
|
794
|
+
);
|
|
795
|
+
|
|
796
|
+
let mut meta2 = act_types::types::Metadata::from(serde_json::json!({
|
|
797
|
+
"std:session-id": "client-supplied",
|
|
798
|
+
}));
|
|
799
|
+
force_session_id(&mut meta2, &None);
|
|
800
|
+
assert_eq!(
|
|
801
|
+
meta2
|
|
802
|
+
.get_as::<String>(act_types::constants::META_SESSION_ID)
|
|
803
|
+
.as_deref(),
|
|
804
|
+
Some("client-supplied"),
|
|
805
|
+
"no default → client value preserved"
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
|
|
704
809
|
#[test]
|
|
705
810
|
fn get_info_exposes_server_name_version_and_tools_capability() {
|
|
706
811
|
let bridge = ActRmcpBridge {
|
|
@@ -708,6 +813,7 @@ mod tests {
|
|
|
708
813
|
info: fake_info(),
|
|
709
814
|
metadata: runtime::Metadata::default(),
|
|
710
815
|
has_sessions: false,
|
|
816
|
+
default_session_id: None,
|
|
711
817
|
};
|
|
712
818
|
let info = rmcp::ServerHandler::get_info(&bridge);
|
|
713
819
|
assert_eq!(info.server_info.name, "example");
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|