This workstream exposes copilotd's existing, complete AHP server through a runtime-supervised, in-process hosting task. All six SDKs control its lifetime through generated SDK JSON-RPC operations. It does not implement an AHP server, launch a companion process or second runtime, or relay host traffic through the application.
The implementation spans github/copilot-agent-runtime and github/copilot-host.
Build from the runtime checkout; Cargo obtains the pinned copilotd-hosting
library from the host repository.
The SDK lives in the runtime repository at src/sdk; its source revision is
the runtime commit, not a separate github/copilot-sdk commit. Run SDK commands
from src/sdk unless a command names another working directory.
Both runtime and SDK pin SDK protocol version 3. The standard AHP client is
pinned to @microsoft/agent-host-protocol@0.9.0, matching the host's
rust/v0.9.0 source pin (60706330f2f351b09f150d9a9c3c0eaedfc8e8b9).
The host prototype d62d982
and SDK sample prototype 8fd2498 were inspected as references, not cherry-picked
as production implementations. Their application-side TCP relay is not the
target architecture; application callbacks use ordinary SDK RPC.
The application connects to a runtime in the usual way. client.startAhpHost(options)
asks that runtime to start the host library in a task. An in-memory duplex stream
carries ordinary SDK JSON-RPC on a separate connection to that same runtime.
This is not an AHP transport stream.
The hosting task owns the selected local WebSocket listener and/or GitHub Mission Control registration and WPS connections. The requesting SDK connection owns the task. Disposing the handle, losing that connection, or shutting down the runtime ends the host's participation. Cleanup must not independently delete sessions or terminate another owner's work. A new SDK connection has no implicit claim on an old host.
The optional Node onExit / Rust on_exit callback reports exit at most once.
On owner connection loss, already-received exits take precedence; remaining
callbacks receive ownerDisconnected (OwnerDisconnected in Rust), without an
exit code and with an explanation that cleanup cannot be acknowledged. That
report cannot prove cleanup through a transport that has already closed.
host.pid is absent for in-process listeners (undefined in Node, None in Rust).
The optional field preserves separate host process IDs from legacy runtimes,
never the hosting runtime PID.
reason: "exited" reports hosting-task failure, not runtime process death.
exitCode is absent (Rust exit_code: None). Use dispose() to stop a listener.
Hosting no longer provides process isolation: a runtime crash also ends its host.
End-to-end coverage independently observes listener closure and runtime survival,
except when the owning runtime itself shuts down.
The small AhpHost handle forwards each dispose() call to the runtime, including
concurrent and repeated calls; the runtime owns idempotence and teardown outcomes.
A successful disposal means the listener is closed, session participation is
detached, and the hosting task has stopped, not merely that shutdown was requested.
await client.start();
const host = await client.startAhpHost({
localServer: {},
onExit: (exit) => console.log(`AHP host stopped: ${exit.reason}`),
});
// Connect an AHP client using host.url and, when defined, host.token.
// Hosting lasts for this SDK client's connection; dispose early only if needed.
await client.stop();AhpHostOptions requires at least one explicit transport: localServer,
githubEnvironment, or both. There is no implicit local listener.
localServer accepts hostname, port, token, and
requireConnectionToken. The hostname defaults to 127.0.0.1; explicit
non-loopback addresses are allowed. An omitted or zero port selects an available
port; other values must be integers from 1 through 65535. The returned URL contains
the actual bound address, including IPv6 brackets where needed. The host always
uses the runtime's configured working directory, with no per-host override.
Connection-token authentication defaults on: supply a nonempty token or let the
listener generate one. requireConnectionToken: false disables only that
connection gate and returns token: undefined; supplying any token alongside
false is invalid. An empty token is always invalid. AHP resource authentication
and authorization remain in effect independently. Explicit public binding or
disabling the connection gate is the application's choice; the default remains
loopback with a generated token. Tokens travel over framed SDK RPC, not argv.
Node.js, Rust, Python, Go, .NET, and Java expose experimental thin handles over the generated host RPCs.
onExit is a local callback, not part of the serialized start request. There is no
closed promise and no public generic notification-registration API.
const host = await client.startAhpHost({
githubEnvironment: {
name: "My application",
computeId: "stable-application-installation-id",
},
// Include localServer: {} to also enable a local listener.
});
console.log(host.environmentId);Both name and computeId are required. Keep the application-supplied compute ID
stable across restarts to re-adopt the environment. The runtime uses its existing
authenticated GitHub identity; a local listener token is not an MC credential.
Startup fails if a requested transport cannot become ready, rather than silently
downgrading to local-only hosting. Select transports at startup; dispose and
recreate the host to change them.
GitHub-only hosting does not open a local listener. Its host handle has an
environment ID but no local URL or connection token. environmentId is absent
for local-only hosting. Disposal ends transport and registration activity without
deleting the saved MC environment record or application-owned sessions.
Registering an environment does not publish every application session. Use
publishSession for existing resident sessions; factory callbacks and durable
catalog behavior are unchanged. Environment management is independent of a
running host and available only through generated rpc.environments.list,
rpc.environments.get, and rpc.environments.delete operations, using each
language's naming conventions.
const { environments } = await client.rpc.environments.list({
kind: "user-local",
status: "online",
});
const selected = environments[0];
if (selected) {
const { environment } = await client.rpc.environments.get({
environmentId: selected.id,
});
// Delete only when the application intends to remove this saved environment.
await client.rpc.environments.delete({ environmentId: environment.id });
}These management operations are experimental and do not require a running host. Discovery returns safe metadata, not host-side relay credentials. GitHub-managed environments cannot be deleted through this API.
Application session factories optionally handle fresh AHP
session creation in your application. The callback receives host-selected
session configuration. Preserve its identity, workspace, and settings, add
application tools, hooks, or prompts, and return a normal session created on the
owning SDK client. Rust supplies a request-scoped Client for that purpose.
The host then attaches to the same runtime session through its existing separate
connection. Tool and hook functions stay in your application.
TypeScript
import { approveAll, type CopilotClient } from "@github/copilot-sdk";
async function startApplicationHost(client: CopilotClient) {
return client.startAhpHost({
localServer: {},
createSession: ({ config, signal }) => {
signal.throwIfAborted();
return client.createSession({
...config,
onPermissionRequest: approveAll,
});
},
onSessionReleased: async (originalSession) => {
await originalSession.disconnect();
},
});
}Rust
use std::sync::Arc;
use github_copilot_sdk::{AhpHost, AhpHostOptions, AhpSessionRequest, Client, Error};
use github_copilot_sdk::rpc::HostLocalServerOptions;
async fn start_application_host(owner: &Client) -> Result<AhpHost, Error> {
owner
.start_ahp_host(
AhpHostOptions::new()
.with_local_server(HostLocalServerOptions::default())
.with_create_session(
|request: AhpSessionRequest, client: Client| async move {
Ok(Arc::new(client.create_session(request.config).await?))
},
)
.with_on_session_released(|original| {
tokio::spawn(async move {
if let Err(error) = original.disconnect().await {
eprintln!("session cleanup failed: {error}");
}
});
}),
)
.await
}Python
from copilot import (
AhpHostOptions, AhpSessionCreateRequest, CopilotClient, CopilotSession,
PermissionHandler,
)
from copilot.rpc import HostLocalServerOptions
async def start_application_host(client: CopilotClient):
async def create(request: AhpSessionCreateRequest):
return await client.create_session(
**{**request.config, "on_permission_request": PermissionHandler.approve_all}
)
async def release(session: CopilotSession):
await session.disconnect()
return await client.start_ahp_host(
AhpHostOptions(
local_server=HostLocalServerOptions(),
create_session=create,
on_session_released=release,
)
)Go
package main
import (
"context"
copilot "github.com/github/copilot-sdk/go"
"github.com/github/copilot-sdk/go/rpc"
)
func startApplicationHost(ctx context.Context, client *copilot.Client) (*copilot.AhpHost, error) {
return client.StartAhpHost(ctx, &copilot.AhpHostOptions{
LocalServer: &rpc.HostLocalServerOptions{},
CreateSession: func(ctx context.Context, request copilot.AhpSessionCreateRequest) (*copilot.Session, error) {
request.Config.OnPermissionRequest = copilot.PermissionHandler.ApproveAll
return client.CreateSession(ctx, request.Config)
},
OnSessionReleased: func(session *copilot.Session) error {
return session.Disconnect()
},
})
}
func main() {}.NET
#pragma warning disable GHCP001
using System.Threading.Tasks;
using GitHub.Copilot;
static class HostingExample
{
public static Task<AhpHost> StartApplicationHostAsync(CopilotClient client) =>
client.StartAhpHostAsync(new AhpHostOptions
{
LocalServer = new(),
CreateSession = request =>
{
request.CancellationToken.ThrowIfCancellationRequested();
request.Config.OnPermissionRequest = PermissionHandler.ApproveAll;
return client.CreateSessionAsync(request.Config, request.CancellationToken);
},
OnSessionReleased = session => session.DisposeAsync().AsTask()
});
}Java
import com.github.copilot.AhpHost;
import com.github.copilot.AhpHostOptions;
import com.github.copilot.AllowCopilotExperimental;
import com.github.copilot.CopilotClient;
import com.github.copilot.generated.rpc.HostLocalServerOptions;
import com.github.copilot.rpc.PermissionHandler;
import java.util.concurrent.CompletableFuture;
@AllowCopilotExperimental
class HostingExample {
static CompletableFuture<AhpHost> startApplicationHost(CopilotClient client) {
return client.startAhpHost(new AhpHostOptions()
.setLocalServer(new HostLocalServerOptions(null, null, null, null))
.setCreateSession(request -> client.createSession(request.config()
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL)))
.setOnSessionReleased(session -> {
session.close();
return CompletableFuture.completedFuture(null);
}));
}
}The examples choose to disconnect the application session on release. Omit the release callback to retain it instead. No SDK automatically disconnects or destroys the returned session.
The SDK retains the original object (the same Arc<Session> allocation in Rust)
and invokes Node onSessionReleased / Rust with_on_session_released at most
once per handoff after participation ends or the handoff fails. This includes a
callback that returns its session after cancellation. Node exposes an
AbortSignal; Rust exposes AhpSessionRequest::cancellation_token. Cancellation
signals that participation ended without transferring session cleanup ownership
to the SDK.
Python exposes an asyncio.Event as cancellation_event; Go supplies a cancellable
factory context.Context; .NET supplies a CancellationToken; Java supplies a
read-only CompletionStage<Void> that completes normally on cancellation.
Cancellation ends the handoff RPC even if the factory has not finished. A late
session result still receives its release callback. Lifecycle callback failures
are logged rather than treated as successful cleanup.
Rust's release closure is synchronous. Its example spawns asynchronous cleanup; that task belongs to the application and host disposal does not await it. Retain and join cleanup tasks when application shutdown must wait for them. The other SDKs also dispatch release and exit callbacks independently of listener disposal. Await application-owned cleanup separately when it must finish before the application exits.
Omitting the creation factory keeps normal host-owned session creation. There is no custom session-listing callback.
Node's optional resumeSession callback receives an AhpSessionResumeRequest
with sessionId, host-selected config, and an abort signal. Return the
object from client.resumeSession(sessionId, { ...config, onPermissionRequest, ... }),
adding the application's tools, hooks, and handlers needed after a runtime restart.
These functions are application code, not serialized catalog data.
If the application retained its original session on the owning client, the
callback can return that object instead of creating another resumed wrapper.
It must still match the requested session identity and workspace.
Rust exposes with_resume_session and the AhpSessionResumeFactory trait.
Its AhpSessionResumeRequest carries a ResumeSessionConfig and a cooperative
cancellation_token. Use the supplied request-scoped Client to call
resume_session(request.config) with your application handlers, and return the
result in an Arc<Session>.
Returning a retained original Arc<Session> is also supported under the same
identity and ownership checks.
The other SDKs support the same create, resume, release, and publication contract:
| SDK | Start | Resume callback | Resume the requested session | Publish |
|---|---|---|---|---|
| Python | client.start_ahp_host(options) |
AhpHostOptions.resume_session |
await client.resume_session(request.session_id, **request.config) |
await host.publish_session(session.session_id) |
| Go | client.StartAhpHost(ctx, options) |
AhpHostOptions.ResumeSession |
client.ResumeSession(ctx, request.SessionID, request.Config) |
host.PublishSession(ctx, session.SessionID) |
| .NET | client.StartAhpHostAsync(options) |
AhpHostOptions.ResumeSession |
client.ResumeSessionAsync(request.SessionId, request.Config, request.CancellationToken) |
host.PublishSessionAsync(session.SessionId) |
| Java | client.startAhpHost(options) |
AhpHostOptions.setResumeSession(callback) |
client.resumeSession(request.sessionId(), request.config()) |
host.publishSession(session.getSessionId()) |
Install application tools, hooks, prompts, and permission handlers in the resume
configuration before calling these methods, just as in the creation examples.
Python and .NET handles support asynchronous context management; Java handles
support try-with-resources. Go callers explicitly invoke Dispose(ctx).
Only durable catalog entries marked as application-owned invoke the resume factory. If its resume callback is missing, restoration fails rather than silently falling back to host-owned creation. Published resident sessions attach directly and do not invoke either factory. This callback does not provide arbitrary adoption or reconfiguration of an already-resident session.
Resumed handoffs use the same ownership rules as fresh handoffs: the SDK retains
the exact object returned by this callback and releases it at most once through
onSessionReleased / with_on_session_released, including cancellation and late
completion. It does not disconnect or destroy that object automatically.
await host.publishSession(session.sessionId) (Rust:
host.publish_session(session.id().to_string()).await) publishes an existing local session
already attached to the host's owning SDK connection. It returns sessionId
and sessionUri. Publication does not call the factory, copy history, replace
the native session, or transfer ownership. Its metadata and workspace come
from the resident session, not from an application-supplied configuration.
Published sessions are discoverable only for this listener's lifetime. They are not imported into its durable catalog. Stopping the listener detaches its participation without deleting the original session or transcript. A missing or replaced resident session cannot be silently restored from disk by the listener.
With AHP_CLIENT enabled, /ahp start starts a runtime-supervised
in-process listener alongside the current CLI session. /remote share starts or reuses
that same listener and publishes the foreground local session, preserving its
ID. Neither command starts another runtime. Ordinary new AHP sessions remain
available; the application session factory is not enabled.
Both commands show the loopback endpoint, in-process host ID, and generated connection token. Sharing also shows the session URI. Use an AHP 0.9 client with the connection token and ordinary GitHub resource authentication; the connection token alone does not bypass resource authorization.
/ahp status shows connection information. /ahp stop stops the entire
listener and all its shares. /remote unshare removes the foreground session.
The pinned host has no per-session unregister operation, so unsharing drains
and restarts the listener at the same endpoint with the same connection token
before republishing other shares. Other clients must reconnect; local sessions
and their identities remain unchanged. Exiting the owning CLI stops the
listener as well. There is one listener per CLI/effective catalog, not one
listener per shared session.
--ahp-host [--listen host:port] [--workspace directory] serves this same backend
in the CLI process, using normal SDK session construction, managed policy,
hooks, authentication and telemetry. The old promotion backend and duplicate
commands have been removed. Local-owner /remote on|off|show retains its independent
Mission Control path; the old attached-AHP-client export toggle is intentionally unavailable.
COPILOT_AHP_SHARE_BIND can select a wider listener for sharing;
the CLI's managed remote-control policy still gates off-machine publication.
The CLI --ahp attachment client negotiates AHP 0.9 for these listeners, with
0.7 retained for older external hosts. Bare --ahp can start an in-process
listener when no local host exists; that listener ends with its owning CLI.
Outbound relay, host-picker and explicitly configured external-daemon controls remain.
The runtime passes its actual resolved data directory to the host library; the single AHP
catalog lives at <effective Copilot home>/ahp/sessions. It follows the same
default ~/.copilot, COPILOT_HOME, and SDK baseDirectory resolution as that
runtime. The catalog contains sessions previously created through AHP, not all
SDK/CLI sessions. Listener disposal, owner disconnect, and restart retain it.
A replacement listener can list and resume these sessions after authenticating.
Only one AHP server may own a catalog at a time. A second start for the
same location fails, including from another runtime. Ordinary runtimes, SDK
clients, and sessions do not acquire this lock and remain usable. The lock is
kernel-managed, non-blocking, held until shutdown writes finish, and released
even after forced process termination. Different effective homes have separate
catalogs. This does not change standalone copilotd defaults or concurrency
behavior, and does not add standalone/in-process shared-writer support.
In-process hosting does not yet provide traditional copilotd's propagation of an AHP session's
GHES credential to shell gh commands. Ordinary per-session authentication is
unchanged. General support belongs in the runtime's existing per-session shell
credential capability and is tracked in
runtime #22077.
The experimental credential workaround and new command-target policy have been
removed. Traditional copilotd retains its existing Enterprise-token subprocess
seeding and is unaffected.
Use the runtime checkout, including its Rust SDK in
copilot-agent-runtime/src/sdk/rust.
The runtime provider links the copilotd-hosting library at the immutable
copilot-host Git revision recorded in the runtime's Cargo.lock. Its own SDK
dependency is runtime-free; do not patch that dependency to the runtime-bearing
SDK build or stage a separate host executable.
Build both the runtime launcher and its native provider from the runtime
checkout. Point the SDK's RuntimeConnection.forStdio({ path }) at that local
launcher. A local JavaScript launcher loading a released native provider is
not a local runtime build.
Set COPILOT_RUNTIME_PROVIDER_LIB to the source-built provider for development-path
integration. The launcher and provider must both be built from the current source.
The host library is linked into the provider.
Candidate package coverage exercises the bundled launcher and adjacent provider;
it does not require a companion executable.
Only model inference is eligible for record/replay. AHP connections, session creation, streaming events, participant ownership, and listener cleanup must run against the real local runtime and host.
After building the local runtime provider, run the focused CLI suite from the runtime repository root (with root dependencies installed):
corepack pnpm run build
unset COPILOT_RUNTIME_E2E_OOP COPILOT_RUNTIME_OOP
COPILOT_CLI_PATH="$PWD/dist-cli/index.js" \
COPILOT_RUNTIME_PROVIDER_LIB="/absolute/local/runtime/runtime.node" \
COPILOT_RUNTIME_HOST_E2E=1 STRICT_CAPTURES=true \
corepack pnpm run test:cli-e2e test/cli/e2e/supervised-ahp.test.tsThis exercises the default embedded CLI runtime. Stage the source-built provider as
dist-cli/prebuilds/linux-x64/runtime.node first. The fixture verifies that
this addon matches the source provider, that no host PID is reported, and that the
actual spawned Node process owns the listener socket.
It also rejects companion hosts and additional provider-loaded runtimes.
The existing COPILOT_RUNTIME_E2E_OOP=1 harness option expects a historical
Rust-parent launcher accepting node <entrypoint> arguments. The current
source-built copilot-runtime is an SDK-only stdio/TCP server, not that launcher,
and rejects those arguments. Do not enable this option with the current
prototype artifacts or substitute a released launcher to make it pass.
This exercises existing-session sharing with local tool approval and exactly-once execution, ordinary AHP session creation, unshare, CLI exit, and listener restart in the same runtime. The Node and Rust SDK suites additionally cover occupied-port startup failure and recovery. These are not process-crash isolation tests; no task-failure injection is available. Without the opt-in environment variable, these cross-repository tests are skipped.
The Python, Go, .NET, and Java suites start a listener through their own SDK APIs
and launch the standard TypeScript AHP 0.9 client as a test-only subprocess.
nodejs/test/e2e/harness/ahpTestDriver.ts provides its JSON-lines test interface.
This process is an AHP client, not a host or additional runtime. It connects
directly to the real listener. The existing CAPI replay proxy handles only
inference traffic and has no AHP orchestration routes or state.
Install the existing Node SDK and replay-harness test dependencies before these
suites (npm --prefix nodejs ci --ignore-scripts and
npm --prefix test/harness ci --ignore-scripts, from src/sdk).
Use a source-built runtime or integrated candidate, not the released runtime pin.
As with the Node and Rust suites, set COPILOT_RUNTIME_HOST_E2E=1 to enable them.
Set COPILOT_CLI_PATH to that runtime's launcher and GITHUB_ACTIONS=true to
keep the shared snapshots read-only. Then run the relevant focused command:
# From src/sdk/python
uv run pytest test_host.py e2e/test_runtime_host_e2e.py
# From src/sdk/go
go test -race ./... -run 'Ahp|RuntimeHost'
# From src/sdk/dotnet
dotnet test test/GitHub.Copilot.SDK.Test.csproj -p:CopilotSkipCliDownload=true \
--filter 'FullyQualifiedName~ClientSessionLifetimeTests.Ahp_|FullyQualifiedName~RuntimeHostE2ETests'
# From src/sdk/java, using JDK 25+
./mvnw -pl sdk verify -Dtest=AhpHostTest -Dit.test=RuntimeHostIT \
-Dcopilot.cli.path="$COPILOT_CLI_PATH"Each language reuses the unchanged
multi_client/both_clients_see_tool_request_and_completion_events.yaml and
runtime_host/app_resume_callback_composes_tools_after_history.yaml snapshots.
The scenarios cover fresh application factories, exact resident publication,
durable application resume after a complete runtime restart, restored history,
composed application/AHP-client tools, preserved prompts and hooks, original-object
release, listener closure, and continued application-session use after disposal.
To bootstrap the accepted cross-repository dependency, the host pins the runtime-free Rust SDK implementation to an immutable companion commit recorded in its Cargo manifest and lockfile. This is a source dependency, not a claim that a corresponding SDK or runtime release has been published.
The host library must be built into the runtime provider before a runtime release can publish the integrated artifacts and checksums. The SDK can then pin that runtime release through its existing runtime-distribution mechanism. Unreleased local candidates must be staged explicitly; substituting a released runtime or host does not validate these changes.
The SDK's existing released runtime pin must be advanced only after the
companion runtime is published. Until then, startAhpHost() requires the local
source-built runtime or an assembled candidate; the currently released runtime
is not claimed to implement the new host operations. Generated bindings in this
branch come from the companion runtime's local schema, so release-based code
generation must use that same companion release when its pin is advanced.
The canonical .github/workflows/sdk-rust.yml runs SDK feature checks in active
monorepo CI and is mirrored to src/sdk/.github/workflows/sdk-rust.yml for export.
Its runtime-free feature checks do not activate the opt-in AHP E2Es.
The coordinated follow-up is: publish the integrated runtime artifacts, provision private release-read credentials, update the runtime acquisition pin, advance the SDK runtime pin, then enable the opt-in AHP E2Es in CI. CI activation is not a prerequisite for these implementation drafts; local source and assembled-candidate runs provide current integration evidence. Actual platform ABI/signing/notarization release jobs still need to execute. Unsigned debug candidates do not establish signed-release behavior.
Arbitrary existing-session adoption, application-owned AHP transport, projection relocation, general multi-harness composition, and exhaustive AHP compatibility coverage are separate workstreams.