Skip to content

Add Workspace options to restrict the checkout and agent credentials - #1831

Open
knechtionscoding wants to merge 1 commit into
kelos-dev:mainfrom
datagravity-ai:feature/workspace-restricted-checkout
Open

knechtionscoding wants to merge 1 commit into
kelos-dev:mainfrom
datagravity-ai:feature/workspace-restricted-checkout

Conversation

@knechtionscoding

@knechtionscoding knechtionscoding commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

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:

  • feat(workspace): add clone.branches to clone only the checked-out ref #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.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 14 files

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

Comment thread api/v1alpha2/workspace_types.go
Comment thread internal/controller/workerpool_controller.go Outdated
Comment thread internal/controller/workerpool_controller.go Outdated
Comment thread docs/reference.md Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 17 files

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

Comment thread internal/controller/workerpool_controller.go Outdated
Comment thread internal/controller/session_controller.go Outdated
Comment thread internal/controller/workerpool_controller_test.go Outdated
Comment thread internal/controller/workerpool_controller.go Outdated
Comment thread internal/conversion/workspace.go
Comment thread docs/reference.md Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 10 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

View guided diff | Re-trigger cubic

Comment thread internal/conversion/workspace_test.go Outdated
Comment thread internal/controller/workerpool_controller_test.go Outdated
A Workspace with a secretRef always gives the agent container the
GitHub token, through 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. An agent that should only
read part of a private repository can therefore fetch the rest.

Add three optional Workspace fields:

- spec.clone.paths: check out only the listed repository paths. The
  clone uses --filter=blob:none --no-checkout and a non-cone sparse
  checkout with root-anchored patterns, so root files are not included
  unless listed. Works for branch, tag and commit SHA refs. Up to 64
  paths of 256 characters, validated by CEL and again when the Job is
  built.
- spec.clone.removeGitDir: delete .git at the end of git-clone. CEL
  rejects it together with spec.remotes, and a Task with spec.branch or
  a Session with spec.initialBranch fails at Job build, since both need
  .git.
- spec.agentCredentials (Token, the default, or None): with None the
  token reaches only git-clone and branch-setup. The agent container
  gets no token env vars, GH_CONFIG_DIR, token mount or
  KELOS_GITHUB_TOKEN_FILE, and the credential helper is not persisted
  into .git/config. Session restarts no longer re-persist it either.
  branch-setup passes the helper inline to the checkout commands,
  since a partial clone fetches blobs there.

Sessions with a volumeClaimTemplate reject clone.paths and
clone.removeGitDir: their workspace keeps the checkout it was created
with, so a restriction added later could not take effect. Sessions
without persistent storage clone again on every Pod start.

The fields survive a v1alpha1 round trip through a preservation
annotation, so an update through v1alpha1 cannot silently drop
agentCredentials None or the clone restrictions.

WorkerPools do not implement these fields. A WorkerPool whose Workspace
sets them is rejected before any token is minted or pool resource is
touched, with an UnsupportedWorkspace Warning event, and Tasks sent to
it fail instead of being assigned to existing worker pods. While the
pool's Workspace is missing, its Tasks wait rather than run on workers
built from it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@knechtionscoding

Copy link
Copy Markdown
Contributor Author

@gjkim42 bump here

This branch is waiting to be deployed

1 waiting deployment
ok-to-test — 4d798892 Waiting Oct 19, 2026 by knechtionscoding via fork-e2e / e2e-with-environment #1933
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/api Categorizes issue or PR as related to API changes needs-actor needs-priority needs-triage release-note

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant