Add Workspace options to restrict the checkout and agent credentials

KN
2d ago
+1941-44

What type of PR is this?

/kind api

What this PR does / why we need it:

A Workspace with a secretRef always hands the agent container the GitHub token (GITHUB_TOKEN/GH_TOKEN, the mounted token file, and the credential helper written into .git/config), and always checks out the whole repository with its .git directory. There is no way to give an agent part of a private repository without also giving it the means to fetch the rest, and even a depth-1 clone keeps every path reachable through .git.

This adds three optional, backward-compatible fields to Workspace (v1alpha2 only):

  • spec.clone.paths: check out only the listed repository paths (directories or files). git-clone uses a partial clone (--filter=blob:none --no-checkout) and a non-cone sparse checkout with root-anchored patterns, so root files such as CLAUDE.md are not checked out unless listed (cone mode always includes them). Works for branch, tag and full-SHA refs, keeping the existing depth and branch behavior. Entries are validated by CEL (relative, no empty/./.. segments, no trailing space before or after an optional trailing slash, no *?[\ or control characters, 1-64 entries of at most 256 characters) and again when the Job is built.
  • spec.clone.removeGitDir: delete /workspace/repo/.git as the last step of git-clone. Features that need .git are rejected rather than silently broken: CEL rejects it together with spec.remotes, and a Task with spec.branch or a Session with spec.initialBranch fails at Job build with a message naming the conflict (JobBuildFailed / StatefulSetBuildFailed).
  • spec.agentCredentials (Token, the default, or None): with None the token reaches only git-clone and branch-setup. The agent container gets no GITHUB_TOKEN, GH_TOKEN, GH_ENTERPRISE_TOKEN, GH_CONFIG_DIR, KELOS_GITHUB_TOKEN_FILE or token volume mount, and the credential helper is not persisted into .git/config. Session restarts on a persistent volume no longer re-persist it either. branch-setup passes the helper inline to its checkout commands, because a partial clone fetches blobs there.

Sessions with a volumeClaimTemplate reject clone.paths and clone.removeGitDir (the Session fails with StatefulSetBuildFailed and a message naming the conflict). Their workspace keeps the checkout it was created with, so a restriction added later could not take effect, and any record of it on that volume could be rewritten by the agent. Sessions without persistent storage clone again on every Pod start, so they always match the current Workspace. agentCredentials: None works with both.

Together they let a Workspace produce a checkout the agent can read but cannot widen.

The new fields are carried through a v1alpha1 round trip in a kelos.dev/v1alpha2-workspace-checkout annotation, the same way AgentConfig and TaskSpawner preserve their v1alpha2-only fields, so an update through v1alpha1 cannot silently turn agentCredentials: None back into Token. The path limits keep that annotation under about 96 KiB in the worst case.

Which issue(s) this PR is related to:

N/A

Special notes for your reviewer:

  • #1809 also introduces Workspace.spec.clone (with branches). The two structs merge without overlap; whichever PR lands second needs a small rebase of WorkspaceClone.
  • WorkerPools have their own pod builder and a long-lived checkout, and they do not implement these fields. Rather than build worker pods that are less restricted than the Workspace asks for, the WorkerPool reconciler rejects such a Workspace right after fetching it, before minting a token or touching the Service or StatefulSet, and records an UnsupportedWorkspace Warning event. The reconcile returns without an error so nothing retries with backoff; the Workspace watch re-enqueues the pool when it changes. Existing worker pods are left in place, but Tasks sent to the pool fail instead of being assigned to them, since they were built with the full checkout and token, and while the pool's Workspace is missing its Tasks wait instead of running on those pods. This is documented in the reference.
  • The token re-mint path for GitHub App secrets only rewrites the derived Secret's data. With None the agent container never mounts that Secret, so refreshes do not reach it.
  • The commit-SHA path sets remote.origin.promisor and remote.origin.partialclonefilter explicitly before the filtered fetch, as git clone --filter would, and runs the sparse checkout before checkout --detach so only the listed blobs are fetched. Both checkouts that fetch blobs lazily run with the credential helper.
  • agentCredentials is named after what it controls in the agent container. It does not limit network access, and podOverrides entries that reference the token Secret are applied as written; the reference says so.

Testing:

  • Unit: table tests for the rendered git-clone args and script (sparse, filter and .git removal only when set; credential helper persisted only for Token without removeGitDir), agent env and mounts for omitted/Token/None on github.com and GHE, validation of paths and of removeGitDir with a branch or remotes, Session restart handling, WorkerPool rejection and the WorkerPool task-assignment guard. TestSessionPodRestrictedCheckoutNeedsEphemeralStorage covers the persistent-Session rejection. TestBranchSetupFetchesBlobsWithoutSavedCredentials serves the repository through git http-backend behind basic auth and checks that branch-setup can check out a branch whose blobs the partial clone has not downloaded, with no helper saved in .git/config. TestWorkspaceCloneScriptProducesRestrictedCheckout runs the rendered git-clone command with real git against a local repository for branch and SHA refs and checks the files on disk, that .git is gone, and that no credential helper is left in .git/config under None.
  • Integration (envtest): CEL accepts and rejects the expected Workspaces, a Task with a restricted Workspace gets the expected Job, a Task combining removeGitDir with a branch fails with the conflict message, and a restricted Workspace updated through v1alpha1 keeps its restrictions (this spec fails without the conversion change).
  • e2e: a Task whose Workspace sets all three fields checks from setupCommand (which runs in the agent container) that only the listed paths exist, .git is gone and no token is present.
  • make update, make verify, make test, make test-integration, make build pass locally. e2e was not run locally.

Does this PR introduce a user-facing change?

Workspaces gain three optional fields for restricted checkouts: `spec.clone.paths` checks out only the listed repository paths using a partial clone and sparse checkout, `spec.clone.removeGitDir` deletes `.git` after the clone, and `spec.agentCredentials: None` keeps the `secretRef` GitHub token out of the agent container while still using it to clone. `removeGitDir` cannot be combined with `spec.remotes`, a Task `spec.branch` or a Session `spec.initialBranch`. Sessions with a `volumeClaimTemplate` do not support `clone.paths` or `clone.removeGitDir`, and WorkerPools do not support any of the three fields.