docs/design/extension-git-credentials.md
Implemented for the daemon, Core extension manager, and TypeScript SDK. WebShell selection UI is intentionally deferred.
The daemon rejects every extension source URL that contains HTTPS userinfo.
That prevents users from installing a private repository with a narrowly scoped
personal access token, even when the token is limited to one repository. Passing
the credential through the source URL without additional handling would be
unsafe: Git can persist the URL in .git/config, process arguments can expose
it, and extension metadata, operation history, logs, or telemetry can retain it.
extension_git_credentials capability to offer stored, one-time, or cancel.Both daemon install endpoints accept:
credentialPersistence?: 'stored' | 'one_time';
The field is valid only when source is an HTTPS URL with userinfo. Omission in
that case means one_time; supplying the field without userinfo is a 400.
Credentialed sources must parse as Git after the existing public-network source
policy is applied. GitHub credentialed URLs bypass release downloads and use
Git clone directly.
The route decodes and validates userinfo before the operation is queued. Empty userinfo, malformed encoding, control characters, NUL, CR/LF, usernames over 256 UTF-8 bytes, and passwords over 4096 UTF-8 bytes are rejected. The route then removes userinfo. Only the clean URL and an in-memory credential object can cross into Core.
One-time operation history does not include the source. Successful results
expose only credentialPersistence; stored results may additionally expose the
clean source and credentialStorage (keychain or encrypted_file). No
response contains a credential or authorization header.
Clone, fetch, and remote listing always receive the clean repository URL. The credential is supplied only in the Git child environment with Git's counted configuration variables:
GIT_CONFIG_KEY_0=http.<clean-repository-url>.extraHeader
GIT_CONFIG_VALUE_0=Authorization: Basic <base64(username:password)>
The key is scoped to the exact clean repository URL. Public Git operations keep
the existing system/global Git configuration isolation, redirect and proxy
disablement, and DNS/IP pinning. GITHUB_TOKEN uses the same header mechanism
instead of being inserted into a clone URL. Newly cloned remote extensions do
not copy the root .git directory into the installed artifact.
The child environment necessarily contains the short-lived header while Git is running. The design protects durable product state and process arguments; it does not claim to protect against an already-compromised same-user process that can inspect another process's environment or system keychain.
Stored mode uses the existing hybrid secret storage. The system keychain is
preferred; when unavailable, the existing host/user-bound encrypted file is
used. The staged extension contains a mode-0600 selector with only a version,
backend, and random secret key. The secret value is a JSON object containing
the username and password and never enters the artifact.
Preparation writes the secret and selector. An artifact commit activates the
selector; failed preparation and disposal delete an unselected secret. Update
resolves the selector before any network access and copies a newly controlled
selector into the replacement artifact. Missing, malformed, forged, or
unreadable managed selectors fail with extension_credential_unavailable
without modifying the installed artifact. A repository-provided selector is
always removed before the managed selector is written.
Uninstall commits artifact removal first and then best-effort deletes the
secret. Cleanup failure does not restore the artifact; it returns an
extension_credential_cleanup_failed warning so an operator can remove the
orphaned secret.
After a one-time clone succeeds, durable install metadata is converted to the
new snapshot type. Snapshot metadata contains no repository source, ref,
commit, update flag, or credential. Catalog and status projections omit source,
report credentialPersistence: one_time, and report not updatable. An update
request fails with extension_not_updatable.
Telemetry uses the generic snapshot category rather than the repository URL. This deliberately trades updateability for the absence of a durable repository locator and credential.
Each credentialed install generates a random 64-character lowercase hexadecimal
installId. Stored updates retain it; one-time snapshots reload it from install
metadata, so restart does not change activation or Agent Plugin data identity.
Uninstall followed by reinstall creates a new id.
Existing metadata without installId continues to use the current source/name
formula. Non-credentialed installs also keep that formula. No migration or data
directory movement is performed.
The daemon advertises extension_git_credentials. A later WebShell change can
gate its three-way confirmation on that capability: store and update, install
once without updates, or cancel before sending a request. Older daemons remain
detectable because they lack the capability and continue rejecting userinfo.