act-cli 0.7.5__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.
Files changed (29) hide show
  1. {act_cli-0.7.5 → act_cli-0.7.6}/Cargo.lock +2 -2
  2. {act_cli-0.7.5 → act_cli-0.7.6}/Cargo.toml +1 -1
  3. {act_cli-0.7.5 → act_cli-0.7.6}/PKG-INFO +1 -1
  4. {act_cli-0.7.5 → act_cli-0.7.6}/act-cli/src/http.rs +50 -8
  5. {act_cli-0.7.5 → act_cli-0.7.6}/act-cli/src/main.rs +55 -3
  6. {act_cli-0.7.5 → act_cli-0.7.6}/act-cli/src/rmcp_bridge.rs +111 -5
  7. {act_cli-0.7.5 → act_cli-0.7.6}/README.md +0 -0
  8. {act_cli-0.7.5 → act_cli-0.7.6}/act-cli/Cargo.toml +0 -0
  9. {act_cli-0.7.5 → act_cli-0.7.6}/act-cli/README.md +0 -0
  10. {act_cli-0.7.5 → act_cli-0.7.6}/act-cli/build.rs +0 -0
  11. {act_cli-0.7.5 → act_cli-0.7.6}/act-cli/src/config.rs +0 -0
  12. {act_cli-0.7.5 → act_cli-0.7.6}/act-cli/src/format.rs +0 -0
  13. {act_cli-0.7.5 → act_cli-0.7.6}/act-cli/src/resolve.rs +0 -0
  14. {act_cli-0.7.5 → act_cli-0.7.6}/act-cli/src/runtime/bindings/mod.rs +0 -0
  15. {act_cli-0.7.5 → act_cli-0.7.6}/act-cli/src/runtime/effective.rs +0 -0
  16. {act_cli-0.7.5 → act_cli-0.7.6}/act-cli/src/runtime/fs_matcher.rs +0 -0
  17. {act_cli-0.7.5 → act_cli-0.7.6}/act-cli/src/runtime/fs_policy.rs +0 -0
  18. {act_cli-0.7.5 → act_cli-0.7.6}/act-cli/src/runtime/http_client.rs +0 -0
  19. {act_cli-0.7.5 → act_cli-0.7.6}/act-cli/src/runtime/http_policy.rs +0 -0
  20. {act_cli-0.7.5 → act_cli-0.7.6}/act-cli/src/runtime/mod.rs +0 -0
  21. {act_cli-0.7.5 → act_cli-0.7.6}/act-cli/src/runtime/network.rs +0 -0
  22. {act_cli-0.7.5 → act_cli-0.7.6}/act-cli/src/runtime/sessions.rs +0 -0
  23. {act_cli-0.7.5 → act_cli-0.7.6}/act-cli/src/runtime/sockets_policy.rs +0 -0
  24. {act_cli-0.7.5 → act_cli-0.7.6}/act-cli/wit/deps/act-core/act-core.wit +0 -0
  25. {act_cli-0.7.5 → act_cli-0.7.6}/act-cli/wit/deps/act-tools/act-tools.wit +0 -0
  26. {act_cli-0.7.5 → act_cli-0.7.6}/act-cli/wit/deps.lock +0 -0
  27. {act_cli-0.7.5 → act_cli-0.7.6}/act-cli/wit/deps.toml +0 -0
  28. {act_cli-0.7.5 → act_cli-0.7.6}/act-cli/wit/world.wit +0 -0
  29. {act_cli-0.7.5 → 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.5"
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.5"
38
+ version = "0.7.6"
39
39
  dependencies = [
40
40
  "act-types",
41
41
  "anyhow",
@@ -3,7 +3,7 @@ members = ["act-cli"]
3
3
  resolver = "3"
4
4
 
5
5
  [workspace.package]
6
- version = "0.7.5"
6
+ version = "0.7.6"
7
7
  edition = "2024"
8
8
  license = "MIT OR Apache-2.0"
9
9
  repository = "https://github.com/actcore/act-cli"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: act-cli
3
- Version: 0.7.5
3
+ Version: 0.7.6
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -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
- .route(
530
- "/sessions/open-args-schema",
531
- axum::routing::any(session_open_args_schema_dispatcher),
532
- )
533
- .route("/sessions", axum::routing::post(session_open))
534
- .route("/sessions/{id}", axum::routing::delete(session_close))
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
- return rmcp_bridge::run_http(addr, pc.info, pc.handle, pc.metadata, pc.has_sessions).await;
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
- return rmcp_bridge::run_stdio(pc.info, pc.handle, pc.metadata, pc.has_sessions).await;
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.metadata.clone(),
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
- let mut tools = convert_tool_definitions(&list.tools, self.has_sessions);
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.has_sessions {
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.has_sessions {
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