GitHub Codespaces lifecycle management, SSH transport, and credential relay for Copilot CLI.
A copilot-extensions plugin that provides:
- SSH transport -- multiplexed SSH connections to CodeSpaces via
ssh-manager, wrapping
gh codespace ssh --config - Lifecycle management -- list/pool, create/reuse, wait, stop, finalize, prune/delete, and status for CodeSpaces
- Credential relay -- contribute the CodeSpace relay profile to the
agent-bridge-owned relay, then expose it to the CodeSpace over SSH reverse
forwards (git credentials through host GCM; optional Azure tokens through
az-login) - Agent-bridge provider -- when agent-bridge is installed, a session-start
hook drops a
providers.dmanifest socodespace:<name>agents resolve live over the agent-codespaces CLI boundary - Resource obligations -- a borrowed CodeSpace is an accountable
obligation on the borrowing worktree:
sshjournals anactivecodespaceclaim onto its ledger, a clean disconnect settles it toat-restand mirrors that disposition onto the shared exclusion lease (cross-machine visible), so the worktree'sagent-worktrees finalizeis gated until the box is safe. Seedocs/resource-obligations.mdand theborrowing-codespacesskill. - Session context map -- a
sessionStarthook injects a briefadditionalContextmap of the repos delegated to CodeSpaces (derived fromagent-worktrees related list), so every session knows which repos have no local checkout and must be worked via a CodeSpace
Most repos need no config at all. agent-codespaces works out of the box on standard GitHub CodeSpaces by convention:
- machine
largePremiumLinux, locationEastUs - in-CodeSpace checkout at
/workspaces/<repo-basename> - credential relay serving
github.comand Azure DevOps (via the host Git Credential Manager) when the agent-bridge relay is running
So agent-codespaces create <your-org>/<standard-repo> just works -- no file to
author.
Add a supplementary config only when a repo deviates from convention (a
split CodeSpaces-vs-product repo, a pinned devcontainer, an ADO host, a
provision hook). It lives in the adopting repo, in the canonical
.copilot-extensions/<plugin>/ namespace:
<repo>/.copilot-extensions/agent-codespaces/config.yaml
Scaffold and adopt it in one step from inside the repo:
agent-codespaces config init # writes .copilot-extensions/agent-codespaces/config.yaml (+ auto-adopts)On Windows, the noninteractive workspace-discovery command used by config init
runs with console-window suppression.
Running a command inside a repo that carries the file auto-discovers it (no
manual config adopt); adoption persists it for the detached daemon and for
extra/multi-repo setups. Legacy .agent-codespaces/config.yaml and repo-root
codespaces.yaml are still read as fallbacks -- relocate them with
agent-codespaces config migrate.
# .copilot-extensions/agent-codespaces/config.yaml -- SUPPLEMENTARY, in-repo. Add ONLY what
# deviates from convention; everything omitted is derived.
repos:
org/my-app-codespaces:
workspace_repo: my-app # split repo -> agents land in /workspaces/my-app
machine_type: largePremiumLinux256gb
devcontainer_path: .devcontainer/devcontainer.json # pin if repo ships >1
credentials:
ado_host: my-org.visualstudio.com # only for bare ADO get-access-tokenThe service reads config live from the repo -- no generated intermediate config. All org/account/URL values live in your repo, never in the plugin.
A repo's venue policy does not have to live in an adopted control-plane repo. A plugin can ship the venue's repo provenance with itself and make it discoverable with no control-plane repo. Two convention-discovered seams are honored here:
- Config declaration (
codespaceConfig). The active plugin'splugin.jsonnames one string path relative to its payload root, for example"codespaceConfig": "references/agent-codespaces/config.yaml". agent-codespaces resolves effectively active plugins, reads the declaration from the identity-verified root, rejects path escapes and non-regular or malformed targets, and parses the file as the normal supplementary config shape. NosessionStarthook or user-level pointer is required. - Hygiene and compatibility. Legacy and operator-owned
config.dinputs remain supported and independently diagnosed. A pointer for an identity with a valid active declaration is reported as superseded and cannot override or reject the declaration. Invalid, disabled, missing, duplicate, or transient contributions are isolated from peers; indeterminate reads retain only their own last-known contribution. Runtime warnings are bounded and deduplicated;agent-codespaces doctor(ordoctor --json) reports exhaustive findings and precise remediation without deleting any entry. - Precedence.
load_merged_configconsumes provider configs at the lowest precedence — adopted-repo/cwd config always wins. Active plugin declarations precede compatibilityconfig.dinputs. - Repo provenance (
workspace_repo). The provider config'srepos.<vessel>.workspace_repo: <product>is what makeseffective_acp_command_for(<vessel>)launch the agent in/workspaces/<product>(the product checkout) rather than the vessel folder, and whatresolved_workspace_folder_forpublishes as the dispatched agent's ACPsession/newcwd. - In-venue plugins (
codespacePlugins). The harness plugin'splugin.jsonalso declares which plugins to inject into the CodeSpace on connect (the<product>-agent), scoped byforWorkspaceRepo(seecodespace_plugins.py). A source backed by a remote marketplace is registered + pre-installed into the CodeSpace's user settings; a source backed by the harness repo's own local (.ai/directory) marketplace can't be installed on an egress-restricted CodeSpace (its repo-relativepathdoesn't exist there), so its host payload is staged (tar+base64 copy) and folded into the--acplaunch as--plugin-dir-- the same lane the related-repo plugins use.
Authoring a <repo>-harness plugin that uses these seams is the
authoring-harness-plugins skill (customizing-copilot) and the pattern
docs/patterns/codespace-repo-provenance.md.
The reference implementation is example-web-harness (example-marketplace).
agent-codespaces is a standalone CLI/binstub. Listing, creating, deleting,
waiting, stopping, and diagnostic SSH do not require registering the current
repo as an agent-worktrees harness. The bridge namespace and shared credential
relay are optional sibling composition: if agent-bridge is absent or stopped,
codespace: dispatch and relay-backed auth stay dark, but the CLI remains
usable (use --no-relay for relay-free diagnostics).
agent-codespaces ssh <name> # SSH into a CodeSpace
agent-codespaces ssh --stdio <name> # Structured SSH for agent-bridge
agent-codespaces list # List active CodeSpaces
agent-codespaces pool # Pool view: disposition + core budget
agent-codespaces allocate <owner/repo> # Reuse/create/recycle/pressure decision
agent-codespaces create <owner/repo> # Create, guarded by reuse/budget checks
agent-codespaces wait <name> # Patiently wait for Available
agent-codespaces stop <name> # Recover sessions, then stop (preserve)
agent-codespaces finalize <name> # Recover, stop, mark recovered/reusable
agent-codespaces finalize <name> --delete # Recover, verify off-box safety, delete
agent-codespaces verify <name> # Publish git-cleanliness safety verdict
agent-codespaces delete <name> # Delete a CodeSpace (--force to skip prompt)
agent-codespaces config init # Scaffold .copilot-extensions/agent-codespaces/config.yaml (+ auto-adopt)
agent-codespaces config adopt # Register a repo's config for the daemon
agent-codespaces config migrate # Relocate legacy config -> .copilot-extensions/agent-codespaces/config.yaml
agent-codespaces config show # Show resolved config
agent-codespaces config validate # Validate resolved config
agent-codespaces cleanup # Remove stale local state (SSH configs, sockets)
agent-codespaces doctor # Check gh auth + config-provider hygiene
agent-codespaces doctor --json # Exhaustive structured auth/config report
agent-codespaces status # Runtime/config/gh/ssh overview
agent-codespaces version # Show versionThere are also bridge-facing seams (namespace-list, namespace-resolve,
namespace-target-repo, namespace-ensure-ready, relay-profile,
relay-launch-env, provision-command, acp-model-flags). They are invoked by
agent-bridge and are not the normal human/operator surface.
agent-codespaces create <owner/repo> \
--branch <branch> \ # branch to create on (default: repo default)
--display-name <name> \ # CodeSpace display name
--devcontainer-path <path> \ # only needed to override multi-devcontainer resolution
--timeout 300 \ # seconds to wait for Available (default 300)
--force-create \ # bypass reuse-before-create / core-budget guard
--no-wait # don't wait / skip provisioningMachine type and location default by convention (largePremiumLinux / EastUs)
and can be overridden per-repo in .copilot-extensions/agent-codespaces/config.yaml. After the
CodeSpace is Available, any on_create provisioning hooks from that config run
automatically. Without --force-create, create first consults the pool
planner: it reuses a suitable idle CodeSpace or refuses when the configured core
budget is already under pressure.
Once agent-codespaces is installed, its sessionStart hook drops a
namespace-provider manifest into ~/.agent-bridge/providers.d/. agent-bridge
discovers it there and registers the live codespace: namespace resolver, so
CodeSpaces are addressable as codespace:<name> (raw or friendly) — listed and
resolved live, with no expiry, including newly-created ones. There is no
bridge register step; installing the plugin is all that's needed.
The manifest is versioned and attributes its plugin source/root. If its binstub
or payload disappears, bridge leaves the namespace inactive, warns without
breaking other providers, and reports exact cleanup through
agent-bridge doctor.
The current bridge integration is process-boundary first, not PATH/import
coupled: the manifest carries the absolute agent-codespaces binstub, and
agent-bridge invokes namespace-* commands to list/resolve targets. The
credential-relay and Session Host helper paths similarly prefer CLI seams
(relay-profile, relay-launch-env, provision-command) with in-process import
fallbacks only when the bridge venv happens to vendor the package. This follows
the repo's à-la-carte independence pattern: the agent-codespaces CLI owns its
runtime; agent-bridge only lights up optional dispatch/relay features.
Host-side gh operations (gh codespace list/create/delete/stop/ssh, gh api,
and the gh codespace ssh --config fetch) run under the gh account that can
access the target repo's org — not whatever account is active in the gh
keyring. With two accounts backing different orgs (e.g. ThomasMichon for
github/* and example-operator for example-org/*), the active-account
default would hide or 403/404 the other org's CodeSpaces entirely.
- The owner→login mapping is owned by agent-worktrees (its
repos.yamlaccount_map+accounts.yamlcatalog). agent-codespaces shells out toagent-worktrees repos account-for <owner/name>(loose coupling — separate venvs) and mints a per-accountGH_TOKENfor eachghsubprocess. - Cross-account discovery:
gh codespace listonly returns the active account's CodeSpaces, solist(and status/resolve) enumerate under every mapped account plus the ambient one and merge, tagging each CodeSpace with its owning account. Per-CodeSpace ops (stop/delete/ssh) then pinghto that account. - Auth preflight verifies each mapped account is logged in with the
codespacescope, surfacing the account's recordedaccounts.yamllogin flow as the remedy. - Fully additive: with no
account_mapconfigured, everything collapses to a single ambientghcall — today's behavior.
Setting up a second account on a remote box — gh auth login / gh auth refresh -s codespace, and likewise az login / devtunnel user login — runs an
interactive device-code flow that polls for a minute-plus while a human
authorizes in a browser. Do not run it as a foreground command over SSH. A
Windows SSH session is a network logon whose session (and its entire child
process tree) is torn down the moment the connection drops — and a
Start-Process … -WindowStyle Hidden child launched from that SSH shell is
still parented to it, so it dies too. Any tunnel blip (acute on dtssh, and on
hibernate-prone cloud dev boxes) kills the poller and the code silently expires
(context deadline exceeded).
Run the auth under Task Scheduler, which owns the process in a session that outlives the SSH connection:
# over ssh: write a runner, register+run a one-shot task, redirect output to a file
Set-Content $env:USERPROFILE\ghauth.ps1 'gh auth refresh -h github.com -s codespace *> "$env:USERPROFILE\ghauth.out"'
schtasks /Create /TN ghauth /TR "pwsh -NoProfile -File $env:USERPROFILE\ghauth.ps1" /SC ONCE /ST 00:00 /F
schtasks /Run /TN ghauth
# then, over FRESH ssh connections, poll the file for the device code + completion:
# Get-Content $env:USERPROFILE\ghauth.out
# clean up: schtasks /Delete /TN ghauth /F ; Remove-Item $env:USERPROFILE\ghauth.ps1,$env:USERPROFILE\ghauth.outSurface the device code from the output file, have the human authorize it (in an
incognito window signed in as the target account — otherwise the code
authorizes whatever account the browser is already on), then poll the same file
for ✓ Authentication complete. Note gh auth refresh targets the active
account (no -u/--user on many gh builds), so gh auth switch --user <login>
first and restore afterward.
The relay forwards git-credential requests from a CodeSpace back to the host
over the SSH tunnel, resolving them through the host's Git Credential Manager
(GCM) — which serves both GitHub (github.com) and Azure DevOps
(*.visualstudio.com, dev.azure.com) credentials. The relay server is owned
by agent-bridge; agent-codespaces contributes the CodeSpace policy/profile and
sets up the SSH reverse-forward on connect.
Provisioning installs the relay-first wrapper only as ~/ado-auth-helper.
It deliberately leaves ~/azure-auth-helper to the native Azure tooling so
interactive az login keeps working. Reconnecting with a newer
agent-codespaces version repairs older installations that shadowed the Azure
helper, restoring a preserved native helper when one exists and otherwise
removing the stale relay wrapper.
To avoid the failure mode where a missing/expired credential causes a CodeSpace
git fetch to hang indefinitely on git credential fill:
- Host GCM runs non-interactively (
GIT_TERMINAL_PROMPT=0,GCM_INTERACTIVE=never), so it errors fast instead of blocking on a prompt. - The relay replies
quit=1when a gitget/fillrequest can't be resolved, which makes git in the CodeSpace abort immediately (fatal: credential helper ... told us to quit) rather than dropping to an interactive prompt. CodeSpace SSH sessions also exportGIT_TERMINAL_PROMPT=0. - On connect, remote-domain auth is verified up front: the workspace's
git remote -vdomains are probed against the host credential store, and any domain lacking local auth is reported as a[WARN]so it can be fixed (az login/ GCM sign-in) before work begins, rather than discovered mid-fetch.
This is a public repo, so internal org/account/repo names and personal
aliases must never land in it. The generated
.copilot-extensions/agent-codespaces/config.yaml
scaffold is checked for such leaks by tests/test_config_init.py, and the whole
working tree by tools/check-no-internal-identifiers.py
(wire it up as a git pre-push hook).
A denylist that named those identifiers would itself leak them, so it is never stored in the repo. Both guards read it privately from:
- env
COPILOT_EXTENSIONS_FORBIDDEN_IDS(comma-separated), and ~/.agent-codespaces/forbidden-identifiers.txt(one per line; blank lines and#comments ignored).
With neither configured (a fresh clone / CI) the identifier check is a no-op, so the guards are safe to ship. Populate one of the sources on your own machine — e.g.:
# ~/.agent-codespaces/forbidden-identifiers.txt
my-internal-org
my-internal-repo
my-alias
Matching is case-insensitive (substring). The host file lives in $HOME, outside
any repo, so it is never committed.
cd plugins\agent-codespaces
uv venv .venv
uv pip install --python .venv\Scripts\python.exe -e ".[dev]"
python ..\..\tools\run-plugin-tests.py agent-codespaces --guards