Repository navigation
Keep pasted pack tokens out of messages and the clone config - #2082
Merged
Merged
Conversation
An operator can import an agent pack from a pasted git URL that carries a token in its userinfo: x-access-token:<token>@, oauth2:<token>@, or the token alone as the user. Several places kept it or passed it on. A failed clone's PackImportException named the pasted URL verbatim, and that message reaches the API error body, the UI and the mediator's error log. git hides a password in its own output, but when a token is pasted as the user alone it asks for a password and names that user (decoded by git 2.33, re-encoded by 2.50), so git's stderr carried it as well. The host allowlist echoed a URL it could not parse, and named the scheme it refused; a URL pasted without https:// parses the token before its colon as that scheme. A token pasted as the user alone carries no password, so the clone did not run as a tokened command: git handed the token to the operator's credential helpers when it asked them for the missing password, and wrote it to their trace2 targets. The clone's .git/config held the tokened origin from before the transfer until after the import walked the checkout, under the worker's temp dir with default permissions, and until the janitor's sweep when a worker died mid-import. The failure now names the URL without its userinfo, through the same SanitizeUrl the launch-base resolver uses. git's stderr loses the userinfo of every http(s) URL in it, then the part that carries the credential (the password, or the user when none is given) as bare text in each spelling git echoes it; a user named beside a password, an account and never echoed outside a URL, is left so git's reason stays readable. Neither allowlist refusal echoes the input. The clone runs as a tokened command whenever the URL carries any credential. TokenedGitCommand.IsTokened is unchanged: a stored https://user@mirror workspace URL may authenticate through an operator helper. The clone directory is owner-only before git runs, and once cloned, origin is rewritten through the workspace provider's own fail-closed strip, so a clone that can shed it neither way is refused and deleted. An ssh URL's git@ names an account and is left as written. Not changed here: - A successful import stores the pasted URL, token included, as pack.url, and the pack list and detail queries return it to every team member, Viewers included. Sync and the add-from-sync flow re-clone from that URL, so storing or returning it without the token needs the credential kept server-side (an encrypted reference, a migration, and an add that resolves the pack instead of its URL). - During the clone the token is still in git's argv and in .git/config, now readable by the worker's own uid only. Passing it through the environment as an http.extraHeader would close that, for the workspace provider's clones too. - For a token pasted as the user alone, git with no helper left to ask falls through to a core.askPass the operator's config may set. Verified against git 2.33.0 and 2.50.1.
6 of 8 tasks
ppXD
added a commit
that referenced
this pull request
Oct 7, 2026
A pack imported from a git URL that embedded a token stored that URL
verbatim in pack.url. The pack list and detail returned it to every
team member, Viewers included; the Library rendered it as a link; and
the add-after-sync flow sent it back to import-url.
pack.url now holds the URL without userinfo and is the pack's identity.
The URL exactly as cloned is sealed with IPayloadEncryptor in
pack.encrypted_clone_url, set only when it carried userinfo. Sync and
the new POST /api/packs/{id}/import decrypt it just for the clone, so a
private pack keeps syncing and the add imports into the pack by id
rather than re-resolving a URL that can no longer clone. Re-pasting
with a rotated token now updates the same pack instead of forking one.
Both hand the decrypted URL to PackCloneFetcher, so this builds on
#2082, which names a failed clone's URL without its userinfo, redacts
it from git's stderr and strips it from the checkout's origin. Without
it, a Sync or an add whose clone fails after authenticating (a deleted
ref, say) returns the token in the error body to any member who may
write agents, and logs it.
SQL cannot run the encryptor, so existing rows are sealed by a
ten-minute recurring backfill. Until it reaches a row, the row still
syncs from its URL and the read model strips userinfo on the way out.
A re-import of that repository lands in the row and seals it: the
lookup also matches a stored URL that differs only by its credential,
so pasting the same or a rotated token neither forks the pack nor
leaves its history on the pack the backfill would mark a duplicate.
Legacy forks of one repository settle into a holder plus duplicates
(duplicate_of_pack_id) that keep syncing from their own source.
Pods that predate this change cannot read the seal. Until the rollout
completes they fail to Sync a sealed private pack, and their
import-url fails for a repository whose legacy fork became a holder
plus a duplicate. The backfill runs only where Hangfire processes
jobs, so in an Api/Worker split roll the Api pods out first.
Tokens pasted before this change were already exposed and should be
rotated.
ppXD
added a commit
that referenced
this pull request
Oct 7, 2026
A pack imported from a git URL that embedded a token stored that URL
verbatim in pack.url. The pack list and detail returned it to every
team member, Viewers included; the Library rendered it as a link; and
the add-after-sync flow sent it back to import-url.
pack.url now holds the URL without userinfo and is the pack's identity.
The URL exactly as cloned is sealed with IPayloadEncryptor in
pack.encrypted_clone_url, set only when it carried userinfo. Sync and
the new POST /api/packs/{id}/import decrypt it just for the clone, so a
private pack keeps syncing and the add imports into the pack by id
rather than re-resolving a URL that can no longer clone. Re-pasting
with a rotated token now updates the same pack instead of forking one.
Both hand the decrypted URL to PackCloneFetcher, so this builds on
#2082, which names a failed clone's URL without its userinfo, redacts
it from git's stderr and strips it from the checkout's origin. Without
it, a Sync or an add whose clone fails after authenticating (a deleted
ref, say) returns the token in the error body to any member who may
write agents, and logs it.
SQL cannot run the encryptor, so existing rows are sealed by a
ten-minute recurring backfill. Until it reaches a row, the row still
syncs from its URL and the read model strips userinfo on the way out.
A re-import of that repository lands in the row and seals it: the
lookup also matches a stored URL that differs only by its credential,
so pasting the same or a rotated token neither forks the pack nor
leaves its history on the pack the backfill would mark a duplicate.
Legacy forks of one repository settle into a holder plus duplicates
(duplicate_of_pack_id) that keep syncing from their own source.
Pods that predate this change cannot read the seal. Until the rollout
completes they fail to Sync a sealed private pack, and their
import-url fails for a repository whose legacy fork became a holder
plus a duplicate. The backfill runs only where Hangfire processes
jobs, so in an Api/Worker split roll the Api pods out first.
Tokens pasted before this change were already exposed and should be
rotated.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
x-access-token:<token>@,oauth2:<token>@, or<token>@). When a clone fails, thePackImportExceptionnow names the URL without its userinfo, usingRemoteTipResolver.SanitizeUrl. That message reaches the API error body, the UI and the mediator log.PackHostAllowlistrefusal echoes the input anymore. A URL pasted withouthttps://parses the token as its scheme.TokenedGitCommand.AsTokened. This includes a token pasted as the user alone, which git used to pass to the operator's credential helpers (when asking for the missing password) and write to their trace2 targets.TokenedGitCommand.IsTokenedis unchanged, so a storedhttps://user@mirrorworkspace URL still authenticates through the operator's helper.LocalGitWorkspaceProvider.StripTokenFromRemoteAsync. That step fails closed: if the token cannot be stripped, the clone is refused and deleted.Not in this change
pack.url(PackImportService.Commit.cs:109).ListPacksQueryandGetPackQueryreturn it to every team member, Viewers included (PackService.cs:109).pack.urlback as the import URL (SyncResultModal.tsx:46)..git/config..git/configis now readable only by the worker's own uid. Passing the token through the environment as anhttp.extraHeaderwould close this gap, for the workspace provider's clones too.core.askPassset in the operator's config.Test plan
PackCloneFetcherCredentialTests,PackCloneFetcherArgsTests,PackHostAllowlistTestsandTokenedGitCommandTests(77/77). Full unit suite: 12140 passed, 1 skipped.PackCloneCredentialFlowTestssets a logging credential helper and trace2 targets, with positive controls. The token-only paste reaches neither, and the checkout directory stays 0700.TokenedGitCommand.Spec.dotnet build CodeSpace.sln: 0 errors.