Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0e24421
Pass remoteSession per-session to actually enable session sync export…
digitarald Jun 1, 2026
1e5f330
[cherry-pick] terminal: fix Linux sandbox wrapper env assignment (#31…
dileepyavan Jun 1, 2026
f65d12a
Update build ts versions
mjbvz Jun 1, 2026
2ed9640
Keep package.json in sync
mjbvz Jun 1, 2026
a32f3ae
revert changes to npmView (#319352)
aeschli Jun 1, 2026
f9c62da
build: clamp PT_LOAD p_align to 4 KiB on linux-x64 node binary (#319355)
deepak1556 Jun 1, 2026
8b203d3
[folding] Support `pattern`+`flags` object syntax on `folding.markers…
Copilot Jun 1, 2026
b0657a8
Fix BYOK models startup race (#319190)
dmitrivMS Jun 1, 2026
2df6460
Skip Inno spawn when another instance is already installing (#318486)
dmitrivMS Jun 1, 2026
2f1e7d6
Include pty.node for mxc (#317981)
chrmarti Jun 1, 2026
08ec2e7
Merge pull request #319389 from microsoft/dev/mjbvz/physical-alligator
mjbvz Jun 1, 2026
c2f5cea
html-language-features: include JSDoc summary and tags in <script> ho…
mohanrajvenkatesan23-04 Jun 1, 2026
88f77ca
don't throw unhandled excepton
vritant24 Jun 1, 2026
cce0e39
Merge branch 'main' into dev/vritan24/logUtilityMissing
vritant24 Jun 1, 2026
a24c0f1
fix: process leaks in extensionHostConnection (#319223)
SimonSiefke Jun 1, 2026
89cf08f
Potential fix for pull request finding
vritant24 Jun 1, 2026
2828153
Merge branch 'main' into dev/vritan24/logUtilityMissing
vritant24 Jun 1, 2026
f67418c
extensions: skip auto-update delay for trusted publishers (#319404)
sandy081 Jun 1, 2026
c01738b
sessions: focus by number, reveal clipped sessions, close all (#319335)
sandy081 Jun 1, 2026
1f03fed
Merge pull request #319410 from microsoft/dev/vritan24/logUtilityMissing
vritant24 Jun 1, 2026
939ff77
Remove experimental and advanced tags from configuration (#319368)
digitarald Jun 1, 2026
5177e1b
Replace @debounce with disposable RunOnceScheduler in TerminalResizeD…
bryanchen-d Jun 1, 2026
225f085
agentHost: move rubber duck flag to agent host config (#319430)
connor4312 Jun 1, 2026
d2f6870
cli: drop ahp-ws dependency in favor of direct tungstenite (#319451)
connor4312 Jun 1, 2026
24556d7
agentHost: avoid rewriting attachment URIs that exist on the agent ho…
connor4312 Jun 1, 2026
07cee71
Disable CLI sandbox (#319377)
chrmarti Jun 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion build/azure-pipelines/common/downloadCopilotVsix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,10 @@ async function unzip(zipPath: string, outputPath: string): Promise<string[]> {
const filePath = path.join(outputPath, entry.fileName);
fs.mkdirSync(path.dirname(filePath), { recursive: true });

const ostream = fs.createWriteStream(filePath);
// Unix file mode is stored in the upper 16 bits of externalFileAttributes.
// Preserve it so executables like node-pty's spawn-helper stay executable.
const mode = (entry.externalFileAttributes >>> 16) & 0o777;
const ostream = fs.createWriteStream(filePath, mode ? { mode } : undefined);
ostream.on('finish', () => {
result.push(filePath);
zipfile!.readEntry();
Expand Down
13 changes: 13 additions & 0 deletions build/azure-pipelines/product-copilot-recovery.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,19 @@ extends:

- template: copilot/setup-steps.yml
- template: copilot/build-steps.yml
- script: |
set -e

# The GLIBC check (from the shared extension template) scans every .node
# under $PWD. @github/copilot vendors @picovoice/pvrecorder-node, whose
# linux/x86_64 prebuild depends on GLIBC > 2.28. The native is not
# shipped in the VSIX, so just remove the pvrecorder prebuilds before
# the check runs.
find "$(Build.SourcesDirectory)" \
-type d -name pvrecorder-node \
-path "*@picovoice/pvrecorder-node" \
-print -exec rm -rf {} +
displayName: Remove pvrecorder native binaries before GLIBC check

uploadSourceMaps:
enabled: true
Expand Down
48 changes: 46 additions & 2 deletions build/gulpfile.reh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,48 @@ function extractAlpinefromDocker(nodeVersion: string, platform: string, arch: st
return es.readArray([new File({ path: 'node', contents, stat: { mode: parseInt('755', 8) } as fs.Stats })]);
}

// WSL1 binfmt_elf rejects PT_LOAD segments with p_align > PAGE_SIZE (0x1000).
// Node 24 linux-x64 ships an `lpstub` LOAD segment aligned to 2 MiB for hugepage
// remapping; clamp it so the binary still loads under WSL1.
function patchElfLoadAlign(): NodeJS.ReadWriteStream {
return es.mapSync<File, File>(file => {
if (!file.contents || !Buffer.isBuffer(file.contents)) {
return file;
}
const buf = file.contents;
if (buf.length < 64) {
return file;
}
if (buf[0] !== 0x7f || buf[1] !== 0x45 || buf[2] !== 0x4c || buf[3] !== 0x46) {
return file;
}
if (buf[4] !== 2 /* ELFCLASS64 */ || buf[5] !== 1 /* ELFDATA2LSB */) {
return file;
}
const e_phoff = Number(buf.readBigUInt64LE(0x20));
const e_phentsize = buf.readUInt16LE(0x36);
const e_phnum = buf.readUInt16LE(0x38);
if (e_phentsize !== 56) {
return file;
}
const PT_LOAD = 1;
const MAX_ALIGN = 0x1000n;
for (let i = 0; i < e_phnum; i++) {
const off = e_phoff + i * e_phentsize;
if (off + e_phentsize > buf.length) {
break;
}
if (buf.readUInt32LE(off) !== PT_LOAD) {
continue;
}
if (buf.readBigUInt64LE(off + 48) > MAX_ALIGN) {
buf.writeBigUInt64LE(MAX_ALIGN, off + 48);
}
}
return file;
});
}

const { nodeVersion, internalNodeVersion } = getNodeVersion();

BUILD_TARGETS.forEach(({ platform, arch }) => {
Expand Down Expand Up @@ -229,14 +271,16 @@ function nodejs(platform: string, arch: string): NodeJS.ReadWriteStream | undefi
fetchUrls(`/dist/v${nodeVersion}/win-${arch}/node.exe`, { base: 'https://nodejs.org', checksumSha256 }))
.pipe(rename('node.exe'));
case 'darwin':
case 'linux':
return (product.nodejsRepository !== 'https://nodejs.org' ?
case 'linux': {
const downloaded = (product.nodejsRepository !== 'https://nodejs.org' ?
fetchGithub(product.nodejsRepository, { version: `${nodeVersion}-${internalNodeVersion}`, name: expectedName!, checksumSha256 }) :
fetchUrls(`/dist/v${nodeVersion}/node-v${nodeVersion}-${platform}-${arch}.tar.gz`, { base: 'https://nodejs.org', checksumSha256 })
).pipe(flatmap(stream => stream.pipe(gunzip()).pipe(untar())))
.pipe(filter('**/node'))
.pipe(util.setExecutableBit('**'))
.pipe(rename('node'));
return platform === 'linux' && arch === 'x64' ? downloaded.pipe(patchElfLoadAlign()) : downloaded;
}
case 'alpine':
return product.nodejsRepository !== 'https://nodejs.org' ?
fetchGithub(product.nodejsRepository, { version: `${nodeVersion}-${internalNodeVersion}`, name: expectedName!, checksumSha256 })
Expand Down
24 changes: 4 additions & 20 deletions cli/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 2 additions & 3 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,8 @@ console = "0.15.7"
bytes = "1.11.1"
tar = "0.4.46"
local-ip-address = "0.6"
ahp = "0.1"
ahp-types = "0.1"
ahp-ws = "0.1"
ahp = "0.2"
ahp-types = "0.2"

[build-dependencies]
serde = { version="1.0.163", features = ["derive"] }
Expand Down
82 changes: 48 additions & 34 deletions cli/src/commands/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ use ahp::{Client, Transport, TransportError, TransportMessage};
use ahp_types::commands::{AuthenticateParams, AuthenticateResult};
use ahp_types::errors::ahp_error_codes;
use ahp_types::state::ProtectedResourceMetadata;
use ahp_types::PROTOCOL_VERSION;
use ahp_types::{ROOT_RESOURCE_URI, PROTOCOL_VERSION};
use futures::{SinkExt, StreamExt};
use tokio::io::{AsyncRead, AsyncWrite};
use tokio_tungstenite::tungstenite::Message;
use tokio_tungstenite::{connect_async, WebSocketStream};

use crate::auth::{Auth, AuthProvider};
use crate::constants::AGENT_HOST_PORT;
Expand Down Expand Up @@ -46,7 +48,7 @@ pub async fn connect(
};

client
.initialize("code-cli".into(), PROTOCOL_VERSION as i64, vec![])
.initialize("code-cli".into(), vec![PROTOCOL_VERSION.to_string()], vec![])
.await
.map_err(|e| wrap(e, "AHP initialize failed"))?;

Expand All @@ -55,50 +57,39 @@ pub async fn connect(

/// Opens a WebSocket connection and creates an AHP client.
async fn connect_ws(address: &str) -> Result<Client, AnyError> {
let transport = ahp_ws::WebSocketTransport::connect(address)
let (ws_stream, _) = connect_async(address)
.await
.map_err(|e| wrap(e, format!("Failed to connect to agent host at {address}")))?;

let transport = WsTransport::new(ws_stream, ());

Client::connect(transport, ahp::ClientConfig::default())
.await
.map_err(|e| wrap(e, "Failed to establish AHP session").into())
}

/// Connects to an agent host over a dev tunnel relay. Looks up the tunnel
/// by name, opens a direct-tcpip channel to the agent host port, performs
/// a WebSocket handshake over the raw stream, then creates an AHP client.
async fn connect_via_tunnel(ctx: &CommandContext, name: &str) -> Result<Client, AnyError> {
let auth = Auth::new(&ctx.paths, ctx.log.clone());
let mut dt = DevTunnels::new_remote_tunnel(&ctx.log, auth, &ctx.paths);

let (port_conn, _relay_handle) = dt.connect_to_tunnel_port(name, AGENT_HOST_PORT).await?;

let rw = port_conn.into_rw();
let (ws_stream, _) = tokio_tungstenite::client_async("ws://localhost/", rw)
.await
.map_err(|e| wrap(e, "WebSocket handshake over tunnel failed"))?;

let transport = TunnelWsTransport {
inner: ws_stream,
// Keep the relay handle alive so the SSH session isn't dropped.
_relay_handle,
};

Client::connect(transport, ahp::ClientConfig::default())
.await
.map_err(|e| wrap(e, "Failed to establish AHP session over tunnel").into())
/// A [`Transport`] backed by a `tokio-tungstenite` WebSocket stream.
///
/// `_guard` keeps an auxiliary resource alive for the lifetime of the
/// transport; the tunnel connection uses it to retain the relay handle so
/// the underlying SSH session isn't dropped. Use `()` when no such
/// resource is needed.
struct WsTransport<S, G = ()> {
inner: WebSocketStream<S>,
_guard: G,
}

/// A [`Transport`] backed by a WebSocket stream running over a tunnel
/// relay channel (via `PortConnectionRW`).
struct TunnelWsTransport {
inner: tokio_tungstenite::WebSocketStream<tunnels::connections::PortConnectionRW>,
/// Prevent the relay handle from being dropped, which would close the
/// underlying SSH session.
_relay_handle: tunnels::connections::ClientRelayHandle,
impl<S, G> WsTransport<S, G> {
fn new(inner: WebSocketStream<S>, guard: G) -> Self {
Self { inner, _guard: guard }
}
}

impl Transport for TunnelWsTransport {
impl<S, G> Transport for WsTransport<S, G>
where
S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
G: Send + 'static,
{
async fn send(&mut self, msg: TransportMessage) -> Result<(), TransportError> {
let frame = match msg {
TransportMessage::Parsed(m) => {
Expand Down Expand Up @@ -140,6 +131,28 @@ impl Transport for TunnelWsTransport {
}
}

/// Connects to an agent host over a dev tunnel relay. Looks up the tunnel
/// by name, opens a direct-tcpip channel to the agent host port, performs
/// a WebSocket handshake over the raw stream, then creates an AHP client.
async fn connect_via_tunnel(ctx: &CommandContext, name: &str) -> Result<Client, AnyError> {
let auth = Auth::new(&ctx.paths, ctx.log.clone());
let mut dt = DevTunnels::new_remote_tunnel(&ctx.log, auth, &ctx.paths);

let (port_conn, relay_handle) = dt.connect_to_tunnel_port(name, AGENT_HOST_PORT).await?;

let rw = port_conn.into_rw();
let (ws_stream, _) = tokio_tungstenite::client_async("ws://localhost/", rw)
.await
.map_err(|e| wrap(e, "WebSocket handshake over tunnel failed"))?;

// Keep the relay handle alive so the SSH session isn't dropped.
let transport = WsTransport::new(ws_stream, relay_handle);

Client::connect(transport, ahp::ClientConfig::default())
.await
.map_err(|e| wrap(e, "Failed to establish AHP session over tunnel").into())
}

/// Issues a JSON-RPC request, automatically handling `-32007` auth errors
/// by running the device-flow login and retrying once.
pub async fn request_with_auth<P, R>(
Expand Down Expand Up @@ -236,6 +249,7 @@ async fn authenticate_from_error(
.request(
"authenticate",
AuthenticateParams {
channel: ROOT_RESOURCE_URI.to_string(),
resource: resource.resource.clone(),
token: credential.access_token().to_string(),
},
Expand Down
14 changes: 9 additions & 5 deletions cli/src/commands/agent_logs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ pub async fn agent_logs(ctx: CommandContext, args: AgentLogsArgs) -> Result<i32,
&client,
"subscribe",
SubscribeParams {
resource: args.session.clone(),
channel: args.session.clone(),
},
)
.await?;
Expand All @@ -54,9 +54,9 @@ pub async fn agent_logs(ctx: CommandContext, args: AgentLogsArgs) -> Result<i32,
Some(SubscriptionEvent::Action(envelope)) => {
print_action(envelope.server_seq, &envelope.action);
}
Some(SubscriptionEvent::Notification(notif)) => {
Some(other) => {
let notif_style = Style::new().magenta();
println!("{}", notif_style.apply_to(format!("notification: {notif:?}")));
println!("{}", notif_style.apply_to(format!("notification: {other:?}")));
}
None => {
println!("{}", Styles::muted().apply_to("Subscription closed."));
Expand Down Expand Up @@ -85,7 +85,11 @@ fn print_initial_state(uri: &str, result: &SubscribeResult) {
uri_style.apply_to(uri)
);

if let SnapshotState::Session(ref session) = result.snapshot.state {
let Some(ref snapshot) = result.snapshot else {
return;
};

if let SnapshotState::Session(ref session) = snapshot.state {
let s = &session.summary;
if !s.title.is_empty() {
println!(" {} {}", label.apply_to("title:"), s.title);
Expand Down Expand Up @@ -116,7 +120,7 @@ fn print_initial_state(uri: &str, result: &SubscribeResult) {
}
}

println!(" {} {}", label.apply_to("seq:"), result.snapshot.from_seq);
println!(" {} {}", label.apply_to("seq:"), snapshot.from_seq);
}

fn print_action(seq: u64, action: &StateAction) {
Expand Down
14 changes: 11 additions & 3 deletions cli/src/commands/agent_ps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

use ahp_types::commands::{ListSessionsParams, ListSessionsResult};
use ahp_types::state::{SessionStatus, SessionSummary};
use ahp_types::ROOT_RESOURCE_URI;

use crate::util::errors::AnyError;

Expand All @@ -17,9 +18,16 @@ use super::CommandContext;
pub async fn agent_ps(ctx: CommandContext, args: AgentPsArgs) -> Result<i32, AnyError> {
let client = agent::connect(&ctx, args.address.as_deref(), args.tunnel.as_deref()).await?;

let result: ListSessionsResult =
agent::request_with_auth(&ctx, &client, "listSessions", ListSessionsParams::default())
.await?;
let result: ListSessionsResult = agent::request_with_auth(
&ctx,
&client,
"listSessions",
ListSessionsParams {
channel: ROOT_RESOURCE_URI.to_string(),
filter: None,
},
)
.await?;

client.shutdown().await;

Expand Down
Loading
Loading