Skip to content

fix: give external SSH build repositories an operator identity surface - #32

Merged
ivanopcode merged 2 commits into
mainfrom
bug/BUG-260807-3092wj-operator-ssh
Aug 7, 2026
Merged

fix: give external SSH build repositories an operator identity surface#32
ivanopcode merged 2 commits into
mainfrom
bug/BUG-260807-3092wj-operator-ssh

Conversation

@ivanopcode

Copy link
Copy Markdown
Owner

Problem

The schema-7 go-repository-v1 fetch runs in a private empty HOME with an empty PATH and no inherited agent socket, and pointed GIT_SSH at the bare operator ssh binary. No flag, config key, or environment variable selected an SSH identity or agent socket, so every private SSH build repository failed closed with build_repository_source_unavailable.

SSHPolicy / exact_ssh_command implemented the reviewed operator identity/agent option shape but were unreferenced outside tests.

Operator surface

Surface Flag Environment variable
Identity --build-ssh-identity PATH CSK_BUILD_SSH_IDENTITY
Agent socket --build-ssh-agent [SOCKET] CSK_BUILD_SSH_AGENT
Host keys --build-ssh-known-hosts PATH CSK_BUILD_SSH_KNOWN_HOSTS

Flags win over the environment. --build-ssh-agent with no value (or auto) adopts the operator's live SSH_AUTH_SOCK. Host keys default to the operator home's .ssh/known_hosts because the fetch pins StrictHostKeyChecking=yes, and are copied into the private root so a fetch cannot rewrite operator state. Credentials are captured once at process entry, next to the existing operator search path capture.

Wiring

acquire_network materializes a private wrapper carrying the argv exact_ssh_command produced and points GIT_SSH_COMMAND at it. Git runs GIT_SSH_COMMAND through its compiled-in shell and appends the host and upload-pack arguments as real argv, so the wrapper observes exactly the invocation it was pinned to on both macOS and Windows — GIT_SSH would need a per-platform executable stub and a shebang the empty PATH cannot resolve. The wrapper refuses to exec on any other argv. The operator's own ssh on PATH is used unchanged and never has to be shadowed.

Two findings this surfaced

  1. Combined identity + agent mode. exact_ssh_command previously required exactly one of identity/agent. Agent-only sets IdentitiesOnly=no, which offers every loaded key in turn — against a real server this dies with Received disconnect ... Too many authentication failures before reaching the right key. Identity + agent (IdentitiesOnly=yes + IdentityAgent=<socket> + -i <pubkey>) pins which agent key is offered, and is the only mode that works for a passphrase-protected key.

  2. -o SendEnv=GIT_PROTOCOL. Git appends this option before the host whenever protocol.version > 0, which would never match the pinned three-element argv. _strict_fetch_args already sets protocol.version=0, so the real invocation is exactly (wrapper, host, "git-upload-pack '<path>'") — verified empirically against the live remote.

Fail-closed

An SSH source with no operator selection now fails with the actionable build_repository_ssh_credential_missing before any launcher, snapshot, or cache artifact is written.

Verification

Live csk install of a manifest locking git@gitlab.wildberries.ru:portals/agentic-infra/cli/sentry-cli.git at 3f6cff5bd76b1b4c95c4ef196f21fd04233e56fe, from a clean-room csk home with a non-editable install and stock /usr/bin/ssh on PATH:

$ csk install app --build-ssh-agent --build-ssh-identity ~/.ssh/<key>.pub
external build wb-sentry.sentry: ... source=sha256:de129f9c… cache=sha256:5194bfc3…
app: wb-sentry tag v1 ff648ab context=yes commands=[] via=<project> installed

$ .agents/bin/sentry --help
Read-only CLI for the Sentry REST API

$ csk status app
  BUILD wb-sentry/sentry  sha256:5194bfc3…  current (marker, protected receipt-v2/artifact, and managed shim agree)

Credential-free, same manifest, fresh home:

app: build_repository_ssh_credential_missing: SSH build repositories require an
operator identity or agent; pass --build-ssh-identity/--build-ssh-agent or set
CSK_BUILD_SSH_IDENTITY/CSK_BUILD_SSH_AGENT

…with no .agents/bin, no external-builds/artifacts, and no external-builds/snapshots.

  • New tests/test_git_admission_ssh.py (18 tests) drives the real fetch path through a stand-in ssh program: identity mode, agent+identity mode, agent-only mode, credential-free fail-closed, missing known_hosts, wrapper argv refusal, no ambient SSH state in the child environment, endpoint derivation for scp-like and ssh:// forms, and capture precedence/validation.
  • New installer-level fail-closed test asserting no launcher, no cache artifact, and no marker.
  • Full suite: 1352 passed, 243 skipped. mypy strict: clean.

Note

GitTool.ssh_wrapper is renamed to GitTool.ssh — it always held the operator ssh program, and the wrapper is now a distinct manager-generated artifact.

The schema-7 go-repository-v1 fetch runs in a private empty HOME with an
empty PATH and no inherited agent socket, and pointed GIT_SSH at the bare
operator ssh binary. Nothing selected an identity or an agent, so every
private SSH build repository failed closed with
build_repository_source_unavailable. SSHPolicy and exact_ssh_command
implemented the reviewed option shape but were unreferenced outside tests.

Add the operator surface and wire it to the real fetch:

- capture_operator_ssh_credentials resolves --build-ssh-identity,
  --build-ssh-agent and --build-ssh-known-hosts (and the matching
  CSK_BUILD_SSH_* variables) once at process entry, next to the existing
  operator search path capture. --build-ssh-agent with no value adopts
  SSH_AUTH_SOCK; host keys default to the operator home's known_hosts and
  are copied into the private root, so a fetch cannot rewrite operator
  state. Operator paths are resolved rather than refused for being
  symbolic links, because a live agent socket is conventionally a stable
  symlink onto a per-session rendezvous point.

- acquire_network materializes a private wrapper carrying the argv
  exact_ssh_command produced and points GIT_SSH_COMMAND at it. Git runs
  GIT_SSH_COMMAND through its compiled-in shell and appends the host and
  upload-pack arguments as real argv, so the wrapper observes exactly the
  invocation it was pinned to on both macOS and Windows; GIT_SSH would
  need a per-platform stub and a shebang the empty PATH cannot resolve.
  The wrapper refuses to exec on any other argv.

- exact_ssh_command accepts an identity and an agent together. The agent
  holds the key and the identity pins which agent key is offered;
  otherwise a populated agent walks every loaded key and the server
  closes the connection on MaxAuthTries before reaching the one that
  authenticates. This is the only mode that works for a
  passphrase-protected key.

An SSH source with no operator selection now fails closed with the
actionable build_repository_ssh_credential_missing before any launcher,
snapshot, or cache artifact is written.

GitTool.ssh_wrapper becomes GitTool.ssh: it always held the operator ssh
program, and the wrapper is now a distinct manager-generated artifact.
…ssh resolves

The assertion only holds once _external_git_tool can freeze an ssh
program; without one the run fails earlier on tool discovery, which is an
environment gap rather than a regression.
@ivanopcode

Copy link
Copy Markdown
Owner Author

Coverage gap to flag at review

GIT_SSH_COMMAND was chosen specifically so one code path serves macOS and Windows — Git runs it through its compiled-in shell and appends the host and upload-pack arguments as real argv, so there is no quoting or shebang dependency. But only macOS is proven.

tests/test_git_admission_ssh.py carries pytestmark = skipif(os.name == "nt") because the stand-in ssh program relies on POSIX exec semantics, so the Windows wrapper path has no automated coverage and no live run behind it. What Windows CI does exercise here is the credential-boundary fail-closed test and the unchanged non-SSH paths.

Windows qualification of the SSH wrapper is a separate task — nothing in this PR claims it is proven.

@ivanopcode
ivanopcode merged commit eadff9d into main Aug 7, 2026
14 checks passed
@ivanopcode
ivanopcode deleted the bug/BUG-260807-3092wj-operator-ssh branch August 7, 2026 16:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant