+///
+/// **Experimental.** This type is part of an experimental wire-protocol surface
+/// and may change or be removed in future SDK or CLI releases.
+///
+///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct GitHubTelemetryNotification {
+ /// The telemetry event, in the runtime's native GitHub-shaped telemetry format.
+ pub event: GitHubTelemetryEvent,
+ /// Whether this is a restricted telemetry event (cli.restricted_telemetry). Hosts must route restricted events to first-party Microsoft stores only.
+ pub restricted: bool,
+ /// Session the telemetry event belongs to.
+ pub session_id: SessionId,
+}
+
/// Pending external tool call request ID, with the tool result or an error describing why it failed.
///
///
diff --git a/rust/tests/e2e/hooks.rs b/rust/tests/e2e/hooks.rs
index c4487ab6d..b4a211d87 100644
--- a/rust/tests/e2e/hooks.rs
+++ b/rust/tests/e2e/hooks.rs
@@ -1,64 +1,228 @@
+use std::collections::HashSet;
use std::sync::Arc;
use async_trait::async_trait;
use github_copilot_sdk::hooks::{
- HookContext, PostToolUseInput, PostToolUseOutput, PreToolUseInput, PreToolUseOutput,
- SessionHooks,
+ HookContext, PostToolUseInput, PreToolUseInput, PreToolUseOutput, SessionHooks,
};
+use tokio::sync::mpsc;
-use super::support::{assert_unsupported_hooks_error, with_e2e_context};
+use super::support::{recv_with_timeout, with_e2e_context};
#[tokio::test]
-async fn rejects_sdk_callback_hooks() {
- with_e2e_context("hooks", "rejects_sdk_callback_hooks", |ctx| {
- Box::pin(async move {
- ctx.set_default_copilot_user();
- let client = ctx.start_client().await;
- assert_unsupported_hooks(
- &client,
- ctx.approve_all_session_config()
- .with_hooks(Arc::new(RecordingHooks)),
- )
- .await;
- client.stop().await.expect("stop client");
- })
- })
+async fn should_invoke_pretooluse_hook_when_model_runs_a_tool() {
+ with_e2e_context(
+ "hooks",
+ "should_invoke_pretooluse_hook_when_model_runs_a_tool",
+ |ctx| {
+ Box::pin(async move {
+ ctx.set_default_copilot_user();
+ std::fs::write(ctx.work_dir().join("hello.txt"), "Hello from the test!")
+ .expect("write hello");
+ let (pre_tx, mut pre_rx) = mpsc::unbounded_channel();
+ let client = ctx.start_client().await;
+ let session = client
+ .create_session(ctx.approve_all_session_config().with_hooks(Arc::new(
+ RecordingHooks {
+ pre_tx: Some(pre_tx),
+ post_tx: None,
+ deny: false,
+ },
+ )))
+ .await
+ .expect("create session");
+
+ session
+ .send_and_wait("Read the contents of hello.txt and tell me what it says")
+ .await
+ .expect("send");
+
+ let input = recv_with_timeout(&mut pre_rx, "preToolUse hook").await;
+ assert_eq!(input.0, *session.id());
+ assert!(!input.1.is_empty());
+
+ session.disconnect().await.expect("disconnect session");
+ client.stop().await.expect("stop client");
+ })
+ },
+ )
.await;
}
-async fn assert_unsupported_hooks(
- client: &github_copilot_sdk::Client,
- config: github_copilot_sdk::SessionConfig,
-) {
- match client.create_session(config).await {
- Ok(session) => {
- session.disconnect().await.expect("disconnect session");
- panic!("expected SDK callback hooks to be rejected");
- }
- Err(err) => assert_unsupported_hooks_error(err),
- }
+#[tokio::test]
+async fn should_invoke_posttooluse_hook_after_model_runs_a_tool() {
+ with_e2e_context(
+ "hooks",
+ "should_invoke_posttooluse_hook_after_model_runs_a_tool",
+ |ctx| {
+ Box::pin(async move {
+ ctx.set_default_copilot_user();
+ std::fs::write(ctx.work_dir().join("world.txt"), "World from the test!")
+ .expect("write world");
+ let (post_tx, mut post_rx) = mpsc::unbounded_channel();
+ let client = ctx.start_client().await;
+ let session = client
+ .create_session(ctx.approve_all_session_config().with_hooks(Arc::new(
+ RecordingHooks {
+ pre_tx: None,
+ post_tx: Some(post_tx),
+ deny: false,
+ },
+ )))
+ .await
+ .expect("create session");
+
+ session
+ .send_and_wait("Read the contents of world.txt and tell me what it says")
+ .await
+ .expect("send");
+
+ let input = recv_with_timeout(&mut post_rx, "postToolUse hook").await;
+ assert_eq!(input.0, *session.id());
+ assert!(!input.1.is_empty());
+ assert!(input.2);
+
+ session.disconnect().await.expect("disconnect session");
+ client.stop().await.expect("stop client");
+ })
+ },
+ )
+ .await;
}
-struct RecordingHooks;
+#[tokio::test]
+async fn should_invoke_both_pretooluse_and_posttooluse_hooks_for_single_tool_call() {
+ with_e2e_context(
+ "hooks",
+ "should_invoke_both_pretooluse_and_posttooluse_hooks_for_single_tool_call",
+ |ctx| {
+ Box::pin(async move {
+ ctx.set_default_copilot_user();
+ std::fs::write(ctx.work_dir().join("both.txt"), "Testing both hooks!")
+ .expect("write both");
+ let (pre_tx, mut pre_rx) = mpsc::unbounded_channel();
+ let (post_tx, mut post_rx) = mpsc::unbounded_channel();
+ let client = ctx.start_client().await;
+ let session = client
+ .create_session(ctx.approve_all_session_config().with_hooks(Arc::new(
+ RecordingHooks {
+ pre_tx: Some(pre_tx),
+ post_tx: Some(post_tx),
+ deny: false,
+ },
+ )))
+ .await
+ .expect("create session");
+
+ session
+ .send_and_wait("Read the contents of both.txt")
+ .await
+ .expect("send");
+
+ let pre = recv_with_timeout(&mut pre_rx, "preToolUse hook").await;
+ let post = recv_with_timeout(&mut post_rx, "postToolUse hook").await;
+ assert_eq!(pre.0, *session.id());
+ assert_eq!(post.0, *session.id());
+
+ let mut pre_tools: HashSet
= HashSet::from([pre.1]);
+ while let Ok((_, tool_name)) = pre_rx.try_recv() {
+ pre_tools.insert(tool_name);
+ }
+ let mut post_tools: HashSet = HashSet::from([post.1]);
+ while let Ok((_, tool_name, _)) = post_rx.try_recv() {
+ post_tools.insert(tool_name);
+ }
+ assert!(
+ pre_tools.intersection(&post_tools).next().is_some(),
+ "expected a tool to appear in both pre and post hooks, got pre={pre_tools:?} post={post_tools:?}"
+ );
+
+ session.disconnect().await.expect("disconnect session");
+ client.stop().await.expect("stop client");
+ })
+ },
+ )
+ .await;
+}
+
+#[tokio::test]
+async fn should_deny_tool_execution_when_pretooluse_returns_deny() {
+ with_e2e_context(
+ "hooks",
+ "should_deny_tool_execution_when_pretooluse_returns_deny",
+ |ctx| {
+ Box::pin(async move {
+ ctx.set_default_copilot_user();
+ let original_content = "Original content that should not be modified";
+ let protected_path = ctx.work_dir().join("protected.txt");
+ std::fs::write(&protected_path, original_content).expect("write protected");
+ let (pre_tx, mut pre_rx) = mpsc::unbounded_channel();
+ let client = ctx.start_client().await;
+ let session = client
+ .create_session(ctx.approve_all_session_config().with_hooks(Arc::new(
+ RecordingHooks {
+ pre_tx: Some(pre_tx),
+ post_tx: None,
+ deny: true,
+ },
+ )))
+ .await
+ .expect("create session");
+
+ session
+ .send_and_wait("Edit protected.txt and replace 'Original' with 'Modified'")
+ .await
+ .expect("send");
+
+ let pre = recv_with_timeout(&mut pre_rx, "preToolUse hook").await;
+ assert_eq!(pre.0, *session.id());
+ assert_eq!(
+ std::fs::read_to_string(protected_path).expect("read protected"),
+ original_content
+ );
+
+ session.disconnect().await.expect("disconnect session");
+ client.stop().await.expect("stop client");
+ })
+ },
+ )
+ .await;
+}
+
+struct RecordingHooks {
+ pre_tx: Option>,
+ post_tx: Option>,
+ deny: bool,
+}
#[async_trait]
impl SessionHooks for RecordingHooks {
async fn on_pre_tool_use(
&self,
- _input: PreToolUseInput,
- _ctx: HookContext,
+ input: PreToolUseInput,
+ ctx: HookContext,
) -> Option {
+ if let Some(pre_tx) = &self.pre_tx {
+ let _ = pre_tx.send((ctx.session_id, input.tool_name));
+ }
Some(PreToolUseOutput {
- permission_decision: Some("allow".to_string()),
+ permission_decision: Some(if self.deny { "deny" } else { "allow" }.to_string()),
..PreToolUseOutput::default()
})
}
async fn on_post_tool_use(
&self,
- _input: PostToolUseInput,
- _ctx: HookContext,
- ) -> Option {
+ input: PostToolUseInput,
+ ctx: HookContext,
+ ) -> Option {
+ if let Some(post_tx) = &self.post_tx {
+ let _ = post_tx.send((
+ ctx.session_id,
+ input.tool_name,
+ !input.tool_result.is_null(),
+ ));
+ }
None
}
}
diff --git a/rust/tests/e2e/hooks_extended.rs b/rust/tests/e2e/hooks_extended.rs
index 6fe031c26..259d462d5 100644
--- a/rust/tests/e2e/hooks_extended.rs
+++ b/rust/tests/e2e/hooks_extended.rs
@@ -1,37 +1,45 @@
use std::sync::Arc;
use async_trait::async_trait;
+use github_copilot_sdk::handler::ApproveAllHandler;
use github_copilot_sdk::hooks::{
ErrorOccurredInput, ErrorOccurredOutput, HookContext, PostToolUseFailureInput,
PostToolUseFailureOutput, PostToolUseInput, PostToolUseOutput, PreToolUseInput,
PreToolUseOutput, SessionEndInput, SessionEndOutput, SessionHooks, SessionStartInput,
SessionStartOutput, UserPromptSubmittedInput, UserPromptSubmittedOutput,
};
+use github_copilot_sdk::tool::ToolHandler;
+use github_copilot_sdk::{Error, SessionConfig, Tool, ToolInvocation, ToolResult};
+use serde_json::json;
+use tokio::sync::mpsc;
-use super::support::{assert_unsupported_hooks_error, with_e2e_context};
+use super::support::{assistant_message_content, recv_with_timeout, with_e2e_context};
#[tokio::test]
-async fn rejects_extended_sdk_callback_hooks() {
+async fn should_invoke_onsessionstart_hook_on_new_session() {
with_e2e_context(
"hooks_extended",
- "rejects_extended_sdk_callback_hooks",
+ "should_invoke_onsessionstart_hook_on_new_session",
|ctx| {
Box::pin(async move {
ctx.set_default_copilot_user();
+ let (tx, mut rx) = mpsc::unbounded_channel();
let client = ctx.start_client().await;
- match client
+ let session = client
.create_session(
ctx.approve_all_session_config()
- .with_hooks(Arc::new(ExtendedHooks)),
+ .with_hooks(Arc::new(RecordingHooks::session_start(tx, None))),
)
.await
- {
- Ok(session) => {
- session.disconnect().await.expect("disconnect session");
- panic!("expected SDK callback hooks to be rejected");
- }
- Err(err) => assert_unsupported_hooks_error(err),
- }
+ .expect("create session");
+
+ session.send_and_wait("Say hi").await.expect("send");
+ let input = recv_with_timeout(&mut rx, "sessionStart hook").await;
+ assert_eq!(input.source, "new");
+ assert!(input.timestamp > 0);
+ assert!(!input.working_directory.as_os_str().is_empty());
+
+ session.disconnect().await.expect("disconnect session");
client.stop().await.expect("stop client");
})
},
@@ -39,83 +47,611 @@ async fn rejects_extended_sdk_callback_hooks() {
.await;
}
-struct ExtendedHooks;
+#[tokio::test]
+async fn should_invoke_onuserpromptsubmitted_hook_when_sending_a_message() {
+ with_e2e_context(
+ "hooks_extended",
+ "should_invoke_onuserpromptsubmitted_hook_when_sending_a_message",
+ |ctx| {
+ Box::pin(async move {
+ ctx.set_default_copilot_user();
+ let (tx, mut rx) = mpsc::unbounded_channel();
+ let client = ctx.start_client().await;
+ let session = client
+ .create_session(
+ ctx.approve_all_session_config()
+ .with_hooks(Arc::new(RecordingHooks::user_prompt(tx, None))),
+ )
+ .await
+ .expect("create session");
+
+ session.send_and_wait("Say hello").await.expect("send");
+ let input = recv_with_timeout(&mut rx, "userPromptSubmitted hook").await;
+ assert!(input.prompt.contains("Say hello"));
+ assert!(input.timestamp > 0);
+ assert!(!input.working_directory.as_os_str().is_empty());
-#[async_trait]
-impl SessionHooks for ExtendedHooks {
- async fn on_user_prompt_submitted(
- &self,
- _input: UserPromptSubmittedInput,
- _ctx: HookContext,
- ) -> Option {
- Some(UserPromptSubmittedOutput {
- modified_prompt: Some("not used".to_string()),
- ..UserPromptSubmittedOutput::default()
+ session.disconnect().await.expect("disconnect session");
+ client.stop().await.expect("stop client");
+ })
+ },
+ )
+ .await;
+}
+
+#[tokio::test]
+async fn should_invoke_onsessionend_hook_when_session_is_disconnected() {
+ with_e2e_context(
+ "hooks_extended",
+ "should_invoke_onsessionend_hook_when_session_is_disconnected",
+ |ctx| {
+ Box::pin(async move {
+ ctx.set_default_copilot_user();
+ let (tx, mut rx) = mpsc::unbounded_channel();
+ let client = ctx.start_client().await;
+ let session = client
+ .create_session(
+ ctx.approve_all_session_config()
+ .with_hooks(Arc::new(RecordingHooks::session_end(tx, None))),
+ )
+ .await
+ .expect("create session");
+
+ session.send_and_wait("Say hi").await.expect("send");
+ session.disconnect().await.expect("disconnect session");
+ let input = recv_with_timeout(&mut rx, "sessionEnd hook").await;
+ assert!(input.timestamp > 0);
+ assert!(!input.working_directory.as_os_str().is_empty());
+
+ client.stop().await.expect("stop client");
+ })
+ },
+ )
+ .await;
+}
+
+#[tokio::test]
+async fn should_invoke_onerroroccurred_hook_when_error_occurs() {
+ with_e2e_context(
+ "hooks_extended",
+ "should_invoke_onerroroccurred_hook_when_error_occurs",
+ |ctx| {
+ Box::pin(async move {
+ ctx.set_default_copilot_user();
+ let (tx, mut rx) = mpsc::unbounded_channel();
+ let client = ctx.start_client().await;
+ let session = client
+ .create_session(
+ ctx.approve_all_session_config()
+ .with_hooks(Arc::new(RecordingHooks::error(tx, None))),
+ )
+ .await
+ .expect("create session");
+
+ session.send_and_wait("Say hi").await.expect("send");
+ rx.try_recv()
+ .map(drop)
+ .expect_err("errorOccurred hook should not run");
+
+ session.disconnect().await.expect("disconnect session");
+ client.stop().await.expect("stop client");
+ })
+ },
+ )
+ .await;
+}
+
+#[tokio::test]
+async fn should_invoke_userpromptsubmitted_hook_and_modify_prompt() {
+ with_e2e_context(
+ "hooks_extended",
+ "should_invoke_userpromptsubmitted_hook_and_modify_prompt",
+ |ctx| {
+ Box::pin(async move {
+ ctx.set_default_copilot_user();
+ let (tx, mut rx) = mpsc::unbounded_channel();
+ let client = ctx.start_client().await;
+ let session = client
+ .create_session(ctx.approve_all_session_config().with_hooks(Arc::new(
+ RecordingHooks::user_prompt(
+ tx,
+ Some(UserPromptSubmittedOutput {
+ modified_prompt: Some(
+ "Reply with exactly: HOOKED_PROMPT".to_string(),
+ ),
+ ..UserPromptSubmittedOutput::default()
+ }),
+ ),
+ )))
+ .await
+ .expect("create session");
+
+ let answer = session
+ .send_and_wait("Say something else")
+ .await
+ .expect("send")
+ .expect("assistant message");
+ let input = recv_with_timeout(&mut rx, "userPromptSubmitted hook").await;
+ assert!(input.prompt.contains("Say something else"));
+ assert!(assistant_message_content(&answer).contains("HOOKED_PROMPT"));
+
+ session.disconnect().await.expect("disconnect session");
+ client.stop().await.expect("stop client");
+ })
+ },
+ )
+ .await;
+}
+
+#[tokio::test]
+async fn should_invoke_sessionstart_hook() {
+ with_e2e_context("hooks_extended", "should_invoke_sessionstart_hook", |ctx| {
+ Box::pin(async move {
+ ctx.set_default_copilot_user();
+ let (tx, mut rx) = mpsc::unbounded_channel();
+ let client = ctx.start_client().await;
+ let session = client
+ .create_session(ctx.approve_all_session_config().with_hooks(Arc::new(
+ RecordingHooks::session_start(
+ tx,
+ Some(SessionStartOutput {
+ additional_context: Some("Session start hook context.".to_string()),
+ ..SessionStartOutput::default()
+ }),
+ ),
+ )))
+ .await
+ .expect("create session");
+
+ session.send_and_wait("Say hi").await.expect("send");
+ let input = recv_with_timeout(&mut rx, "sessionStart hook").await;
+ assert_eq!(input.source, "new");
+
+ session.disconnect().await.expect("disconnect session");
+ client.stop().await.expect("stop client");
+ })
+ })
+ .await;
+}
+
+#[tokio::test]
+async fn should_invoke_sessionend_hook() {
+ with_e2e_context("hooks_extended", "should_invoke_sessionend_hook", |ctx| {
+ Box::pin(async move {
+ ctx.set_default_copilot_user();
+ let (tx, mut rx) = mpsc::unbounded_channel();
+ let client = ctx.start_client().await;
+ let session = client
+ .create_session(ctx.approve_all_session_config().with_hooks(Arc::new(
+ RecordingHooks::session_end(
+ tx,
+ Some(SessionEndOutput {
+ session_summary: Some("session ended".to_string()),
+ ..SessionEndOutput::default()
+ }),
+ ),
+ )))
+ .await
+ .expect("create session");
+
+ session.send_and_wait("Say bye").await.expect("send");
+ session.disconnect().await.expect("disconnect session");
+ let input = recv_with_timeout(&mut rx, "sessionEnd hook").await;
+ assert!(input.timestamp > 0);
+
+ client.stop().await.expect("stop client");
})
+ })
+ .await;
+}
+
+#[tokio::test]
+async fn should_register_erroroccurred_hook() {
+ with_e2e_context(
+ "hooks_extended",
+ "should_register_erroroccurred_hook",
+ |ctx| {
+ Box::pin(async move {
+ ctx.set_default_copilot_user();
+ let (tx, mut rx) = mpsc::unbounded_channel();
+ let client = ctx.start_client().await;
+ let session = client
+ .create_session(ctx.approve_all_session_config().with_hooks(Arc::new(
+ RecordingHooks::error(
+ tx,
+ Some(ErrorOccurredOutput {
+ error_handling: Some("skip".to_string()),
+ ..ErrorOccurredOutput::default()
+ }),
+ ),
+ )))
+ .await
+ .expect("create session");
+
+ session.send_and_wait("Say hi").await.expect("send");
+ rx.try_recv()
+ .map(drop)
+ .expect_err("errorOccurred hook should not run");
+
+ session.disconnect().await.expect("disconnect session");
+ client.stop().await.expect("stop client");
+ })
+ },
+ )
+ .await;
+}
+
+#[tokio::test]
+async fn should_allow_pretooluse_to_return_modifiedargs_and_suppressoutput() {
+ with_e2e_context(
+ "hooks_extended",
+ "should_allow_pretooluse_to_return_modifiedargs_and_suppressoutput",
+ |ctx| {
+ Box::pin(async move {
+ ctx.set_default_copilot_user();
+ let (tx, mut rx) = mpsc::unbounded_channel();
+ let client = ctx.start_client().await;
+ let session = client
+ .create_session(
+ SessionConfig::default()
+ .with_github_token(super::support::DEFAULT_TEST_TOKEN)
+ .with_permission_handler(Arc::new(ApproveAllHandler))
+ .with_tools(vec![echo_value_tool()])
+ .with_hooks(Arc::new(RecordingHooks::pre_tool(tx))),
+ )
+ .await
+ .expect("create session");
+
+ let answer = session
+ .send_and_wait(
+ "Call echo_value with value 'original', then reply with the result.",
+ )
+ .await
+ .expect("send")
+ .expect("assistant message");
+ let mut saw_echo = false;
+ while let Ok(input) = rx.try_recv() {
+ saw_echo |= input.tool_name == "echo_value";
+ }
+ assert!(saw_echo, "expected preToolUse hook for echo_value");
+ assert!(assistant_message_content(&answer).contains("modified by hook"));
+
+ session.disconnect().await.expect("disconnect session");
+ client.stop().await.expect("stop client");
+ })
+ },
+ )
+ .await;
+}
+
+#[tokio::test]
+async fn should_allow_posttooluse_to_return_modifiedresult() {
+ with_e2e_context(
+ "hooks_extended",
+ "should_allow_posttooluse_to_return_modifiedresult",
+ |ctx| {
+ Box::pin(async move {
+ ctx.set_default_copilot_user();
+ let (tx, mut rx) = mpsc::unbounded_channel();
+ let client = ctx.start_client().await;
+ let session = client
+ .create_session(
+ ctx.approve_all_session_config()
+ .with_hooks(Arc::new(RecordingHooks::post_tool(tx))),
+ )
+ .await
+ .expect("create session");
+
+ let answer = session
+ .send_and_wait(
+ "Call the view tool to read the current directory, then reply done.",
+ )
+ .await
+ .expect("send")
+ .expect("assistant message");
+ let mut saw_view = false;
+ while let Ok(input) = rx.try_recv() {
+ saw_view |= input.tool_name == "view";
+ }
+ assert!(saw_view, "expected postToolUse hook for view");
+ assert!(
+ assistant_message_content(&answer)
+ .to_lowercase()
+ .contains("done"),
+ "expected assistant message to contain 'done'"
+ );
+
+ session.disconnect().await.expect("disconnect session");
+ client.stop().await.expect("stop client");
+ })
+ },
+ )
+ .await;
+}
+
+#[tokio::test]
+#[ignore = "Fails with 1.0.64-0 runtime: built-in tools are not available when hooks restrict availableTools, so the failure path cannot be exercised. Follow up with runtime team."]
+async fn should_invoke_posttoolusefailure_hook_for_failed_tool_result() {
+ with_e2e_context(
+ "hooks_extended",
+ "should_invoke_posttoolusefailure_hook_for_failed_tool_result",
+ |ctx| {
+ Box::pin(async move {
+ ctx.set_default_copilot_user();
+ let (failure_tx, mut failure_rx) = mpsc::unbounded_channel();
+ let (post_tx, mut post_rx) = mpsc::unbounded_channel();
+ let client = ctx.start_client().await;
+ let session = client
+ .create_session(
+ ctx.approve_all_session_config()
+ .with_available_tools(["report_intent"])
+ .with_hooks(Arc::new(RecordingHooks::post_tool_failure(
+ failure_tx, post_tx,
+ ))),
+ )
+ .await
+ .expect("create session");
+
+ let answer = session
+ .send_and_wait(
+ "Call the view tool with path 'missing.txt'. If it fails, use the hook guidance to answer.",
+ )
+ .await
+ .expect("send")
+ .expect("assistant message");
+
+ let input = recv_with_timeout(&mut failure_rx, "postToolUseFailure hook").await;
+ post_rx
+ .try_recv()
+ .map(drop)
+ .expect_err("postToolUse hook should not run");
+ assert_eq!(input.tool_name, "view");
+ assert!(input.error.contains("does not exist"));
+ assert!(
+ input.tool_args["path"]
+ .as_str()
+ .is_some_and(|path| path.contains("missing.txt"))
+ );
+ assert!(input.timestamp > 0);
+ assert!(!input.working_directory.as_os_str().is_empty());
+ assert!(
+ assistant_message_content(&answer).contains("HOOK_FAILURE_GUIDANCE_APPLIED")
+ );
+
+ session.disconnect().await.expect("disconnect session");
+ client.stop().await.expect("stop client");
+ })
+ },
+ )
+ .await;
+}
+
+#[derive(Default)]
+struct RecordingHooks {
+ session_start: Option>,
+ session_start_output: Option,
+ session_end: Option>,
+ session_end_output: Option,
+ user_prompt: Option>,
+ user_prompt_output: Option,
+ error: Option>,
+ error_output: Option,
+ pre_tool: Option>,
+ post_tool: Option>,
+ post_tool_failure: Option>,
+}
+
+impl RecordingHooks {
+ fn session_start(
+ tx: mpsc::UnboundedSender,
+ output: Option,
+ ) -> Self {
+ Self {
+ session_start: Some(tx),
+ session_start_output: output,
+ ..Self::default()
+ }
+ }
+
+ fn session_end(
+ tx: mpsc::UnboundedSender,
+ output: Option,
+ ) -> Self {
+ Self {
+ session_end: Some(tx),
+ session_end_output: output,
+ ..Self::default()
+ }
}
+ fn user_prompt(
+ tx: mpsc::UnboundedSender,
+ output: Option,
+ ) -> Self {
+ Self {
+ user_prompt: Some(tx),
+ user_prompt_output: output,
+ ..Self::default()
+ }
+ }
+
+ fn error(
+ tx: mpsc::UnboundedSender,
+ output: Option,
+ ) -> Self {
+ Self {
+ error: Some(tx),
+ error_output: output,
+ ..Self::default()
+ }
+ }
+
+ fn pre_tool(tx: mpsc::UnboundedSender) -> Self {
+ Self {
+ pre_tool: Some(tx),
+ ..Self::default()
+ }
+ }
+
+ fn post_tool(tx: mpsc::UnboundedSender) -> Self {
+ Self {
+ post_tool: Some(tx),
+ ..Self::default()
+ }
+ }
+
+ fn post_tool_failure(
+ failure_tx: mpsc::UnboundedSender,
+ post_tx: mpsc::UnboundedSender,
+ ) -> Self {
+ Self {
+ post_tool: Some(post_tx),
+ post_tool_failure: Some(failure_tx),
+ ..Self::default()
+ }
+ }
+}
+
+#[async_trait]
+impl SessionHooks for RecordingHooks {
async fn on_session_start(
&self,
- _input: SessionStartInput,
- _ctx: HookContext,
+ input: SessionStartInput,
+ ctx: HookContext,
) -> Option {
- Some(SessionStartOutput {
- additional_context: Some("not used".to_string()),
- ..SessionStartOutput::default()
- })
+ assert!(!ctx.session_id.as_str().is_empty());
+ if let Some(tx) = &self.session_start {
+ let _ = tx.send(input);
+ }
+ self.session_start_output.clone()
}
async fn on_session_end(
&self,
- _input: SessionEndInput,
- _ctx: HookContext,
+ input: SessionEndInput,
+ ctx: HookContext,
) -> Option {
- Some(SessionEndOutput {
- session_summary: Some("not used".to_string()),
- ..SessionEndOutput::default()
- })
+ assert!(!ctx.session_id.as_str().is_empty());
+ if let Some(tx) = &self.session_end {
+ let _ = tx.send(input);
+ }
+ self.session_end_output.clone()
+ }
+
+ async fn on_user_prompt_submitted(
+ &self,
+ input: UserPromptSubmittedInput,
+ ctx: HookContext,
+ ) -> Option {
+ assert!(!ctx.session_id.as_str().is_empty());
+ if let Some(tx) = &self.user_prompt {
+ let _ = tx.send(input);
+ }
+ self.user_prompt_output.clone()
}
async fn on_error_occurred(
&self,
- _input: ErrorOccurredInput,
- _ctx: HookContext,
+ input: ErrorOccurredInput,
+ ctx: HookContext,
) -> Option {
- Some(ErrorOccurredOutput {
- error_handling: Some("skip".to_string()),
- ..ErrorOccurredOutput::default()
- })
+ assert!(!ctx.session_id.as_str().is_empty());
+ assert!(
+ ["model_call", "tool_execution", "system", "user_input"]
+ .contains(&input.error_context.as_str())
+ );
+ if let Some(tx) = &self.error {
+ let _ = tx.send(input);
+ }
+ self.error_output.clone()
}
async fn on_pre_tool_use(
&self,
- _input: PreToolUseInput,
+ input: PreToolUseInput,
_ctx: HookContext,
) -> Option {
- Some(PreToolUseOutput {
- permission_decision: Some("allow".to_string()),
- ..PreToolUseOutput::default()
- })
+ let output = if input.tool_name == "echo_value" {
+ PreToolUseOutput {
+ permission_decision: Some("allow".to_string()),
+ modified_args: Some(json!({ "value": "modified by hook" })),
+ suppress_output: Some(false),
+ ..PreToolUseOutput::default()
+ }
+ } else {
+ PreToolUseOutput {
+ permission_decision: Some("allow".to_string()),
+ ..PreToolUseOutput::default()
+ }
+ };
+ if let Some(tx) = &self.pre_tool {
+ let _ = tx.send(input);
+ }
+ Some(output)
}
async fn on_post_tool_use(
&self,
- _input: PostToolUseInput,
+ input: PostToolUseInput,
_ctx: HookContext,
) -> Option {
- Some(PostToolUseOutput {
- suppress_output: Some(false),
- ..PostToolUseOutput::default()
- })
+ let output =
+ (self.post_tool.is_some() && input.tool_name == "view").then(|| PostToolUseOutput {
+ modified_result: Some(json!({
+ "textResultForLlm": "modified by post hook",
+ "resultType": "success",
+ "toolTelemetry": {},
+ })),
+ suppress_output: Some(false),
+ ..PostToolUseOutput::default()
+ });
+ if let Some(tx) = &self.post_tool {
+ let _ = tx.send(input);
+ }
+ output
}
async fn on_post_tool_use_failure(
&self,
- _input: PostToolUseFailureInput,
- _ctx: HookContext,
+ input: PostToolUseFailureInput,
+ ctx: HookContext,
) -> Option {
- Some(PostToolUseFailureOutput {
- additional_context: Some("not used".to_string()),
- })
+ assert!(!ctx.session_id.as_str().is_empty());
+ if let Some(tx) = &self.post_tool_failure {
+ let _ = tx.send(input);
+ return Some(PostToolUseFailureOutput {
+ additional_context: Some("HOOK_FAILURE_GUIDANCE_APPLIED".to_string()),
+ });
+ }
+ None
+ }
+}
+
+struct EchoValueTool;
+
+fn echo_value_tool() -> Tool {
+ Tool::new("echo_value")
+ .with_description("Echoes the supplied value")
+ .with_parameters(json!({
+ "type": "object",
+ "properties": {
+ "value": { "type": "string" }
+ },
+ "required": ["value"]
+ }))
+ .with_handler(Arc::new(EchoValueTool))
+}
+
+#[async_trait]
+impl ToolHandler for EchoValueTool {
+ async fn call(&self, invocation: ToolInvocation) -> Result {
+ Ok(ToolResult::Text(
+ invocation
+ .arguments
+ .get("value")
+ .and_then(serde_json::Value::as_str)
+ .unwrap_or_default()
+ .to_string(),
+ ))
}
}
diff --git a/rust/tests/e2e/pre_mcp_tool_call_hook.rs b/rust/tests/e2e/pre_mcp_tool_call_hook.rs
index 219032bd5..973672f70 100644
--- a/rust/tests/e2e/pre_mcp_tool_call_hook.rs
+++ b/rust/tests/e2e/pre_mcp_tool_call_hook.rs
@@ -1,35 +1,183 @@
+use std::collections::HashMap;
use std::sync::Arc;
use async_trait::async_trait;
use github_copilot_sdk::hooks::{
HookContext, PreMcpToolCallInput, PreMcpToolCallOutput, SessionHooks,
};
-use serde_json::json;
+use github_copilot_sdk::{McpServerConfig, McpStdioServerConfig};
+use serde_json::{Value, json};
+use tokio::sync::mpsc;
-use super::support::{assert_unsupported_hooks_error, with_e2e_context};
+use super::support::{assistant_message_content, recv_with_timeout, with_e2e_context};
+
+fn meta_echo_mcp_servers(repo_root: &std::path::Path) -> HashMap {
+ let harness_dir = repo_root.join("test").join("harness");
+ let server_path = harness_dir
+ .join("test-mcp-meta-echo-server.mjs")
+ .to_string_lossy()
+ .to_string();
+ HashMap::from([(
+ "meta-echo".to_string(),
+ McpServerConfig::Stdio(McpStdioServerConfig {
+ tools: Some(vec!["*".to_string()]),
+ command: if cfg!(windows) {
+ "node.exe".to_string()
+ } else {
+ "node".to_string()
+ },
+ args: vec![server_path],
+ working_directory: Some(harness_dir.to_string_lossy().to_string()),
+ ..McpStdioServerConfig::default()
+ }),
+ )])
+}
+
+struct SetMetaHooks {
+ tx: mpsc::UnboundedSender,
+}
+
+#[async_trait]
+impl SessionHooks for SetMetaHooks {
+ async fn on_pre_mcp_tool_call(
+ &self,
+ input: PreMcpToolCallInput,
+ _ctx: HookContext,
+ ) -> Option {
+ let _ = self.tx.send(input);
+ Some(PreMcpToolCallOutput {
+ meta_to_use: Some(json!({"injected": "by-hook", "source": "test"})),
+ })
+ }
+}
+
+struct ReplaceMetaHooks {
+ tx: mpsc::UnboundedSender,
+}
+
+#[async_trait]
+impl SessionHooks for ReplaceMetaHooks {
+ async fn on_pre_mcp_tool_call(
+ &self,
+ input: PreMcpToolCallInput,
+ _ctx: HookContext,
+ ) -> Option {
+ let _ = self.tx.send(input);
+ Some(PreMcpToolCallOutput {
+ meta_to_use: Some(json!({"completely": "replaced"})),
+ })
+ }
+}
+
+struct RemoveMetaHooks {
+ tx: mpsc::UnboundedSender,
+}
+
+#[async_trait]
+impl SessionHooks for RemoveMetaHooks {
+ async fn on_pre_mcp_tool_call(
+ &self,
+ input: PreMcpToolCallInput,
+ _ctx: HookContext,
+ ) -> Option {
+ let _ = self.tx.send(input);
+ Some(PreMcpToolCallOutput {
+ meta_to_use: Some(Value::Null),
+ })
+ }
+}
+
+#[tokio::test]
+async fn should_set_meta_via_premcptoolcall_hook() {
+ with_e2e_context(
+ "pre_mcp_tool_call_hook",
+ "should_set_meta_via_premcptoolcall_hook",
+ |ctx| {
+ Box::pin(async move {
+ ctx.set_default_copilot_user();
+ let (tx, mut rx) = mpsc::unbounded_channel();
+ let client = ctx.start_client().await;
+ let session = client
+ .create_session(
+ ctx.approve_all_session_config()
+ .with_mcp_servers(meta_echo_mcp_servers(ctx.repo_root()))
+ .with_hooks(Arc::new(SetMetaHooks { tx })),
+ )
+ .await
+ .expect("create session");
+
+ let answer = session
+ .send_and_wait(
+ "Use the meta-echo/echo_meta tool with value 'test-set'. Reply with just the raw tool result.",
+ )
+ .await
+ .expect("send")
+ .expect("assistant message");
+ let content = assistant_message_content(&answer);
+ assert!(
+ content.contains("injected"),
+ "Expected 'injected' in response, got: {content}"
+ );
+ assert!(
+ content.contains("by-hook"),
+ "Expected 'by-hook' in response, got: {content}"
+ );
+
+ let input = recv_with_timeout(&mut rx, "preMcpToolCall hook").await;
+ assert_eq!(input.server_name, "meta-echo");
+ assert_eq!(input.tool_name, "echo_meta");
+ assert!(!input.working_directory.as_os_str().is_empty());
+ assert!(input.timestamp > 0);
+
+ session.disconnect().await.expect("disconnect session");
+ client.stop().await.expect("stop client");
+ })
+ },
+ )
+ .await;
+}
#[tokio::test]
-async fn rejects_sdk_premcptoolcall_callback_hooks() {
+async fn should_replace_meta_via_premcptoolcall_hook() {
with_e2e_context(
"pre_mcp_tool_call_hook",
- "rejects_sdk_premcptoolcall_callback_hooks",
+ "should_replace_meta_via_premcptoolcall_hook",
|ctx| {
Box::pin(async move {
ctx.set_default_copilot_user();
+ let (tx, mut rx) = mpsc::unbounded_channel();
let client = ctx.start_client().await;
- match client
+ let session = client
.create_session(
ctx.approve_all_session_config()
- .with_hooks(Arc::new(PreMcpHooks)),
+ .with_mcp_servers(meta_echo_mcp_servers(ctx.repo_root()))
+ .with_hooks(Arc::new(ReplaceMetaHooks { tx })),
+ )
+ .await
+ .expect("create session");
+
+ let answer = session
+ .send_and_wait(
+ "Use the meta-echo/echo_meta tool with value 'test-replace'. Reply with just the raw tool result.",
)
.await
- {
- Ok(session) => {
- session.disconnect().await.expect("disconnect session");
- panic!("expected SDK callback hooks to be rejected");
- }
- Err(err) => assert_unsupported_hooks_error(err),
- }
+ .expect("send")
+ .expect("assistant message");
+ let content = assistant_message_content(&answer);
+ assert!(
+ content.contains("completely"),
+ "Expected 'completely' in response, got: {content}"
+ );
+ assert!(
+ content.contains("replaced"),
+ "Expected 'replaced' in response, got: {content}"
+ );
+
+ let input = recv_with_timeout(&mut rx, "preMcpToolCall hook").await;
+ assert_eq!(input.server_name, "meta-echo");
+ assert_eq!(input.tool_name, "echo_meta");
+
+ session.disconnect().await.expect("disconnect session");
client.stop().await.expect("stop client");
})
},
@@ -37,17 +185,50 @@ async fn rejects_sdk_premcptoolcall_callback_hooks() {
.await;
}
-struct PreMcpHooks;
+#[tokio::test]
+async fn should_remove_meta_via_premcptoolcall_hook() {
+ with_e2e_context(
+ "pre_mcp_tool_call_hook",
+ "should_remove_meta_via_premcptoolcall_hook",
+ |ctx| {
+ Box::pin(async move {
+ ctx.set_default_copilot_user();
+ let (tx, mut rx) = mpsc::unbounded_channel();
+ let client = ctx.start_client().await;
+ let session = client
+ .create_session(
+ ctx.approve_all_session_config()
+ .with_mcp_servers(meta_echo_mcp_servers(ctx.repo_root()))
+ .with_hooks(Arc::new(RemoveMetaHooks { tx })),
+ )
+ .await
+ .expect("create session");
-#[async_trait]
-impl SessionHooks for PreMcpHooks {
- async fn on_pre_mcp_tool_call(
- &self,
- _input: PreMcpToolCallInput,
- _ctx: HookContext,
- ) -> Option {
- Some(PreMcpToolCallOutput {
- meta_to_use: Some(json!({"injected": "by-hook"})),
- })
- }
+ let answer = session
+ .send_and_wait(
+ "Use the meta-echo/echo_meta tool with value 'test-remove'. Reply with just the raw tool result.",
+ )
+ .await
+ .expect("send")
+ .expect("assistant message");
+ let content = assistant_message_content(&answer);
+ assert!(
+ content.contains("\"meta\":null"),
+ "Expected '\"meta\":null' in response, got: {content}"
+ );
+ assert!(
+ content.contains("test-remove"),
+ "Expected 'test-remove' in response, got: {content}"
+ );
+
+ let input = recv_with_timeout(&mut rx, "preMcpToolCall hook").await;
+ assert_eq!(input.server_name, "meta-echo");
+ assert_eq!(input.tool_name, "echo_meta");
+
+ session.disconnect().await.expect("disconnect session");
+ client.stop().await.expect("stop client");
+ })
+ },
+ )
+ .await;
}
diff --git a/rust/tests/e2e/subagent_hooks.rs b/rust/tests/e2e/subagent_hooks.rs
index 0329cadf0..99529c433 100644
--- a/rust/tests/e2e/subagent_hooks.rs
+++ b/rust/tests/e2e/subagent_hooks.rs
@@ -5,31 +5,91 @@ use github_copilot_sdk::hooks::{
HookContext, PostToolUseInput, PostToolUseOutput, PreToolUseInput, PreToolUseOutput,
SessionHooks,
};
+use parking_lot::Mutex;
-use super::support::{assert_unsupported_hooks_error, with_e2e_context};
+use super::support::with_e2e_context;
#[tokio::test]
-async fn rejects_sdk_callback_hooks_for_sub_agent_hook_propagation() {
+async fn should_invoke_pretooluse_and_posttooluse_hooks_for_sub_agent_tool_calls() {
with_e2e_context(
"subagent_hooks",
- "rejects_sdk_callback_hooks_for_sub_agent_hook_propagation",
+ "should_invoke_pretooluse_and_posttooluse_hooks_for_sub_agent_tool_calls",
|ctx| {
Box::pin(async move {
ctx.set_default_copilot_user();
- let client = ctx.start_client().await;
- match client
- .create_session(
- ctx.approve_all_session_config()
- .with_hooks(Arc::new(SubagentHooks)),
+ std::fs::write(
+ ctx.work_dir().join("subagent-test.txt"),
+ "Hello from subagent test!",
+ )
+ .expect("write test file");
+
+ let hook_log = Arc::new(Mutex::new(Vec::::new()));
+
+ let mut opts = ctx.client_options();
+ opts.env.push((
+ "COPILOT_EXP_COPILOT_CLI_SESSION_BASED_SUBAGENTS".into(),
+ "true".into(),
+ ));
+
+ let client = github_copilot_sdk::Client::start(opts)
+ .await
+ .expect("start client");
+
+ let session = client
+ .create_session(ctx.approve_all_session_config().with_hooks(Arc::new(
+ RecordingHooks {
+ log: Arc::clone(&hook_log),
+ },
+ )))
+ .await
+ .expect("create session");
+
+ session
+ .send_and_wait(
+ "Use the task tool to spawn an explore agent that reads the file \
+ subagent-test.txt in the current directory and reports its contents. \
+ You must use the task tool.",
)
.await
- {
- Ok(session) => {
- session.disconnect().await.expect("disconnect session");
- panic!("expected SDK callback hooks to be rejected");
- }
- Err(err) => assert_unsupported_hooks_error(err),
- }
+ .expect("send");
+
+ let log = hook_log.lock().clone();
+
+ // Parent tool hooks fire for "task"
+ let task_pre = log
+ .iter()
+ .find(|h| h.kind == "pre" && h.tool_name == "task");
+ assert!(
+ task_pre.is_some(),
+ "preToolUse should fire for the parent's 'task' tool call"
+ );
+
+ // Sub-agent tool hooks fire for "view"
+ let view_pre: Vec<_> = log
+ .iter()
+ .filter(|h| h.kind == "pre" && h.tool_name == "view")
+ .collect();
+ let view_post: Vec<_> = log
+ .iter()
+ .filter(|h| h.kind == "post" && h.tool_name == "view")
+ .collect();
+ assert!(
+ !view_pre.is_empty(),
+ "preToolUse should fire for the sub-agent's 'view' tool call"
+ );
+ assert!(
+ !view_post.is_empty(),
+ "postToolUse should fire for the sub-agent's 'view' tool call"
+ );
+
+ // input.session_id distinguishes parent from sub-agent
+ assert_ne!(
+ view_pre[0].session_id,
+ task_pre.unwrap().session_id,
+ "Sub-agent tool hooks should have a different sessionId than parent tool hooks"
+ );
+
+ session.disconnect().await.expect("disconnect session");
client.stop().await.expect("stop client");
})
},
@@ -37,15 +97,29 @@ async fn rejects_sdk_callback_hooks_for_sub_agent_hook_propagation() {
.await;
}
-struct SubagentHooks;
+#[derive(Clone, Debug)]
+struct HookEntry {
+ kind: String,
+ tool_name: String,
+ session_id: String,
+}
+
+struct RecordingHooks {
+ log: Arc>>,
+}
#[async_trait]
-impl SessionHooks for SubagentHooks {
+impl SessionHooks for RecordingHooks {
async fn on_pre_tool_use(
&self,
- _input: PreToolUseInput,
+ input: PreToolUseInput,
_ctx: HookContext,
) -> Option {
+ self.log.lock().push(HookEntry {
+ kind: "pre".to_string(),
+ tool_name: input.tool_name,
+ session_id: input.session_id,
+ });
Some(PreToolUseOutput {
permission_decision: Some("allow".to_string()),
..PreToolUseOutput::default()
@@ -54,9 +128,14 @@ impl SessionHooks for SubagentHooks {
async fn on_post_tool_use(
&self,
- _input: PostToolUseInput,
+ input: PostToolUseInput,
_ctx: HookContext,
) -> Option {
+ self.log.lock().push(HookEntry {
+ kind: "post".to_string(),
+ tool_name: input.tool_name,
+ session_id: input.session_id,
+ });
None
}
}
diff --git a/rust/tests/e2e/support.rs b/rust/tests/e2e/support.rs
index 1936fd889..5052ef1be 100644
--- a/rust/tests/e2e/support.rs
+++ b/rust/tests/e2e/support.rs
@@ -21,19 +21,9 @@ use tokio::sync::Semaphore;
static E2E_CONCURRENCY: LazyLock = LazyLock::new(|| Semaphore::new(e2e_concurrency()));
pub const DEFAULT_TEST_TOKEN: &str = "rust-e2e-token";
-const UNSUPPORTED_SDK_HOOKS_MESSAGE: &str = "SDK hook callbacks are no longer supported";
type TestFuture<'a> = Pin + 'a>>;
-pub fn assert_unsupported_hooks_error(err: impl std::fmt::Display) {
- let message = err.to_string();
- if message.contains(UNSUPPORTED_SDK_HOOKS_MESSAGE) {
- return;
- }
-
- panic!("expected unsupported hooks error, got: {message}");
-}
-
pub async fn with_e2e_context(category: &str, snapshot_name: &str, test: F)
where
F: for<'a> FnOnce(&'a mut E2eContext) -> TestFuture<'a>,
diff --git a/scripts/codegen/go.ts b/scripts/codegen/go.ts
index 5403fb444..57957499e 100644
--- a/scripts/codegen/go.ts
+++ b/scripts/codegen/go.ts
@@ -3784,6 +3784,7 @@ async function generateRpc(schemaPath?: string): Promise {
...collectRpcMethods(schema.server || {}),
...collectRpcMethods(schema.session || {}),
...collectRpcMethods(schema.clientSession || {}),
+ ...collectRpcMethods(schema.clientGlobal || {}),
].sort((left, right) => left.rpcMethod.localeCompare(right.rpcMethod));
// Build a combined definition map, including shared API definitions plus
diff --git a/test/harness/package-lock.json b/test/harness/package-lock.json
index 770bae27a..656982f7f 100644
--- a/test/harness/package-lock.json
+++ b/test/harness/package-lock.json
@@ -9,7 +9,7 @@
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
- "@github/copilot": "^1.0.66",
+ "@github/copilot": "^1.0.67",
"@modelcontextprotocol/sdk": "^1.26.0",
"@types/node": "^25.3.3",
"@types/node-forge": "^1.3.14",
@@ -501,9 +501,9 @@
}
},
"node_modules/@github/copilot": {
- "version": "1.0.66",
- "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.66.tgz",
- "integrity": "sha512-m3+3FLSgum90xN4+eiwnLvdrDvM+oZzur5DfhOH88duNDKBcLQvKQY9fG/I1l1t8a1iBwjpgtRpsBwykE8k3Zw==",
+ "version": "1.0.67",
+ "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.67.tgz",
+ "integrity": "sha512-5YEY9LNXBT9Q8uShjCdYcornJJJhGtdIzSYla2+pjfXYpHsDVibqYubzYjfgffOUKFChyzOpH7n/868+t56iIg==",
"dev": true,
"license": "SEE LICENSE IN LICENSE.md",
"dependencies": {
@@ -513,20 +513,20 @@
"copilot": "npm-loader.js"
},
"optionalDependencies": {
- "@github/copilot-darwin-arm64": "1.0.66",
- "@github/copilot-darwin-x64": "1.0.66",
- "@github/copilot-linux-arm64": "1.0.66",
- "@github/copilot-linux-x64": "1.0.66",
- "@github/copilot-linuxmusl-arm64": "1.0.66",
- "@github/copilot-linuxmusl-x64": "1.0.66",
- "@github/copilot-win32-arm64": "1.0.66",
- "@github/copilot-win32-x64": "1.0.66"
+ "@github/copilot-darwin-arm64": "1.0.67",
+ "@github/copilot-darwin-x64": "1.0.67",
+ "@github/copilot-linux-arm64": "1.0.67",
+ "@github/copilot-linux-x64": "1.0.67",
+ "@github/copilot-linuxmusl-arm64": "1.0.67",
+ "@github/copilot-linuxmusl-x64": "1.0.67",
+ "@github/copilot-win32-arm64": "1.0.67",
+ "@github/copilot-win32-x64": "1.0.67"
}
},
"node_modules/@github/copilot-darwin-arm64": {
- "version": "1.0.66",
- "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.66.tgz",
- "integrity": "sha512-cJPXE2rWSjR+B8GRBUUd0k9PM4euWRUe3xgHoJqi9o/jJjtRYn6DZMrmFt9xgjoYWf0WZOyrlDgedqO1V+zDAg==",
+ "version": "1.0.67",
+ "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.67.tgz",
+ "integrity": "sha512-CO3mpgFXcN6e7ZsSmjMkt1AKxMfb1+mjdn3yrf2DRnnWIURSK9kGvw+E+E1+YE37D1MBiUn/VOBmhRad5+vl0A==",
"cpu": [
"arm64"
],
@@ -541,9 +541,9 @@
}
},
"node_modules/@github/copilot-darwin-x64": {
- "version": "1.0.66",
- "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.66.tgz",
- "integrity": "sha512-44mpx2ZcRFHDx4B9xlrL5OQyTgaD/Hn+bAkeStXgcG8UkkfYSsRtLhnaxqUEQrtIEiVQrw++XWvUO0AscRrX+w==",
+ "version": "1.0.67",
+ "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.67.tgz",
+ "integrity": "sha512-M20Hpn3bOJRkVwAIVRK4ZlX66AqtmGfXZRxZBRFQC045QIwcfmVUP45sTSgXDb4uHWeK0cZgdTdniHwKGtMplw==",
"cpu": [
"x64"
],
@@ -558,9 +558,9 @@
}
},
"node_modules/@github/copilot-linux-arm64": {
- "version": "1.0.66",
- "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.66.tgz",
- "integrity": "sha512-uXtTs/rYjk6kacNs/T0s/lxn0JBvAgu78pBoZeWpU5APhICkPy9kC+lNAzLYoZujVVDOHT05IoeifHppFpQ8+w==",
+ "version": "1.0.67",
+ "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.67.tgz",
+ "integrity": "sha512-b4ePtFBow+Ior+aVLKA1hHxhR5wF+ql5CD7TSg/NHGYgc1kwD+3a9uKSENy05J5Lit/G/DZ9C6JwowvvdMWSKg==",
"cpu": [
"arm64"
],
@@ -575,9 +575,9 @@
}
},
"node_modules/@github/copilot-linux-x64": {
- "version": "1.0.66",
- "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.66.tgz",
- "integrity": "sha512-tXn3OuJCx/YEDNgYg8mdOGSFiIjmLJtTEyZ/VoEA86ffUIPxrunc0wnapEFk2zOW1unwdJeBuVIkzlB3RS1/eA==",
+ "version": "1.0.67",
+ "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.67.tgz",
+ "integrity": "sha512-4ynZyfKnWAdvEPAFDDBIz1wpFttcOTJu4Y8Mlz5oXCBA0NM/rwr8K4l7Adp8UzwbfmdrMJ9y+zivqRBMDbPInA==",
"cpu": [
"x64"
],
@@ -592,9 +592,9 @@
}
},
"node_modules/@github/copilot-linuxmusl-arm64": {
- "version": "1.0.66",
- "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.66.tgz",
- "integrity": "sha512-sHRag7W5CG0kbbX3j9v9cUmIafk/0N8MGGr2knvPeIHtxwZQYYjx397gT1nN6xagLWt5mvchkYybfQFCyCBaxg==",
+ "version": "1.0.67",
+ "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.67.tgz",
+ "integrity": "sha512-IjezxBU8fYUr/b5hEiniXqzwoOrJ4egrQSBbG96M+roLTqd9txP0MgxZtcRtKV7phRIdIGE109wwrn4H6hSqmA==",
"cpu": [
"arm64"
],
@@ -609,9 +609,9 @@
}
},
"node_modules/@github/copilot-linuxmusl-x64": {
- "version": "1.0.66",
- "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.66.tgz",
- "integrity": "sha512-bdIgHOaVZlvsF/4awzMxsby6T+4k7aWe9HZr+sr+qU8tuG19jwi/1LXGB6tKdlFeFgY78yX0lR+ywByVJc5loA==",
+ "version": "1.0.67",
+ "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.67.tgz",
+ "integrity": "sha512-Zy/rbja1lnhzDoNfn051H0EybCseCvjvH7WmbcHCayjXUjzXeKF6OmAt4hvqFZH87ttT3KbKtQ8/6oDUhhM2YQ==",
"cpu": [
"x64"
],
@@ -626,9 +626,9 @@
}
},
"node_modules/@github/copilot-win32-arm64": {
- "version": "1.0.66",
- "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.66.tgz",
- "integrity": "sha512-T7FGONCVWIPjjAxp22cu4WKqNogq56FknHGAvj7Ryn5ZoanFAR3vXXlXDsYnDKLBcshjRYGxocl2UnmRTMxgvg==",
+ "version": "1.0.67",
+ "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.67.tgz",
+ "integrity": "sha512-O3VFRS5v9NXRP8o+N1SvcFbBqECDzZP7XQBeBj2Vcrma80gdJc5GQub/w2mwmr1w5UbwgzJkRasm0Ec/jxbcoA==",
"cpu": [
"arm64"
],
@@ -643,9 +643,9 @@
}
},
"node_modules/@github/copilot-win32-x64": {
- "version": "1.0.66",
- "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.66.tgz",
- "integrity": "sha512-eroxRUSJZOJCk0luLyX6A1qqGIWs8p4w0EjZFhCzvdFvJ0abIovGyt3R/gN9DeyJM8Qs7ROPGvqevUlXh6DhCg==",
+ "version": "1.0.67",
+ "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.67.tgz",
+ "integrity": "sha512-td5tQ/nve5dB7RPvNglBZwa/6DJqiOBgacXXa1GpYcohqpCzoI8gONNkeaeyr6oF4iu5wXJ9krUNr6QXL4yB5Q==",
"cpu": [
"x64"
],
diff --git a/test/harness/package.json b/test/harness/package.json
index 3c59cb235..4e167fe7b 100644
--- a/test/harness/package.json
+++ b/test/harness/package.json
@@ -14,7 +14,7 @@
"node": "^20.19.0 || >=22.12.0"
},
"devDependencies": {
- "@github/copilot": "^1.0.66",
+ "@github/copilot": "^1.0.67",
"@modelcontextprotocol/sdk": "^1.26.0",
"@types/node": "^25.3.3",
"@types/node-forge": "^1.3.14",