fix(heartbeat): resolve repo-relative project workspace cwd inside its checkout (BLO-25415) - #1302
Conversation
1 similar comment
|
@ally please review at head Review focus:
|
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 4085f7b
Critical Issues (0)
Important Issues (1)
- [native-codex]
server/src/services/heartbeat.ts:11196— The new relative-path branch is selected solely by!path.isAbsolute(projectCwd), despiteproject_workspaces.source_typesupportinglocal_path,git_repo, andnon_git_path. A valid relativelocal_path/non_git_pathworkspace is now silently redirected into a managed project directory (and may be cloned or created there) instead of retaining the prior process-relative path. Restrict this behavior toworkspace.sourceType === "git_repo"(and add coverage for a relative local path) so this fix does not change the semantics of the other workspace types.
Suggestions (0)
Strengths
- The checkout containment helper correctly handles normalized paths, root selection, traversal, and sibling-prefix escapes.
- The error path preserves the existing preferred-workspace failure behavior.
Recommended Action
- Restrict managed-checkout subpath resolution to
git_repoworkspaces before merge.
|
Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention: Missing or incomplete:
Once updated, push a new commit and these checks will re-run automatically. — commitperclip |
…when repo-backed (BLO-25415) Review follow-up on #1302. The relative-cwd branch was selected on !path.isAbsolute(cwd) alone, so a local_path / non_git_path / remote_managed workspace with a relative cwd would have been redirected into a managed checkout dir instead of keeping its prior meaning. The redirect is not merely semantic: ensureManagedProjectWorkspace's repoUrl-less branch mkdirs the managed path, so such a workspace would have had an empty directory created on the shared PVC and still failed the subsequent stat. Gate the branch on the workspace being repo-backed, extracted into resolveRepoRelativeWorkspaceCwd so the decision is unit-testable rather than buried in the resolver loop. Repo-backed is tested as "not an explicitly non-repo source_type, and has a repoUrl" rather than source_type === "git_repo": source_type is an unconstrained text column and production carries a row typed "git", which an allowlist would silently skip. Requiring repoUrl also settles the repoUrl-null case raised in the review request — that workspace now keeps its cwd and fails loud without a stray directory. Audited fleet-wide: 1 workspace row still has a relative cwd (4dac485f, [P0] IWA Gateway Certificate Provisioning), and it is git_repo with a repoUrl, so it remains covered by the fix. heartbeat-workspace-session 211 passed (6 new), fail-loud 1 passed, tsc --noEmit exit 0. Co-Authored-By: Claude <noreply@anthropic.com>
|
Finding accepted, and it is worse than described — fixed in Why it is worse: the redirect is not only semantic. if (!input.repoUrl) {
if (!stats) await fs.mkdir(cwd, { recursive: true });
return { cwd, warning: null };
}So a relative- One deviation from the literal ask, and I want it visible rather than silent. You asked to gate on const NON_REPO_WORKSPACE_SOURCE_TYPES = new Set(["local_path", "non_git_path", "remote_managed"]);Two reasons, the first empirical:
I excluded If you would still prefer the strict Blast radius re-audited after the change: exactly one workspace row still has a relative Coverage added (your "add coverage for a relative local path"): the branch decision is extracted into
|
|
Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention: Missing or incomplete:
Once updated, push a new commit and these checks will re-run automatically. — commitperclip |
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: b96fd8a
Prior Findings Dispositioned (1)
- prior:4085f7b important 1 — fixed —
server/src/services/heartbeat.ts:2300—local_path,non_git_path, andremote_managedrelative CWDs are excluded from managed-checkout resolution.
Critical Issues (0)
Important Issues (1)
- [gstack/review]
server/src/services/heartbeat.ts:2287— The containment check is lexical only. A checkout can contain a repo-controlled symlink such aspackages/iwa -> /etc; withcwd: "packages/iwa",path.resolvepasses,fs.statfollows the symlink, and the run launches outside the checkout.- Resolve the checkout and target using
fs.realpathafter realization, then enforce containment on the canonical paths; add a symlink-escape regression test.
- Resolve the checkout and target using
Suggestions (0)
Strengths
- The source-type guard fixes the previous regression while preserving support for noncanonical repo-backed source types.
- The new traversal and sibling-prefix tests cover the direct lexical escape cases.
Recommended Action
- Fix the Important symlink escape before merge.
…esolution (BLO-25415) Review follow-up on #1302. resolveContainedWorkspaceSubpath checked containment lexically only. `cwd` is operator config, but the checkout's *contents* are repo-controlled: a repo carrying `packages/iwa -> /etc` passes path.resolve(), and the caller's fs.stat() then follows the link, launching the run outside its checkout on a PVC shared with every other repo. Resolve both the checkout root and the target with fs.realpath and re-assert containment on the canonical paths. The realpath pass is skipped when either does not resolve — a missing target has no link to follow, and reporting it missing stays the caller's fs.stat()'s job, so the existing fail-loud path is unchanged. The root is resolved independently because the managed dir may itself sit behind a symlink, which would otherwise fail containment for a legitimate subpath. The escape test was verified to fail against the lexical-only implementation before being committed, so it pins the behavior rather than passing incidentally. heartbeat-workspace-session 214 passed (3 new: symlink escape refused, in-checkout symlink allowed, symlinked checkout root allowed), heartbeat-preferred-workspace-fail-loud 1 passed, tsc --noEmit exit 0. Co-Authored-By: Claude <noreply@anthropic.com>
|
Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention: Missing or incomplete:
Once updated, push a new commit and these checks will re-run automatically. — commitperclip |
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: b91b1ef
Prior Findings Dispositioned (1)
- prior:b96fd8a important 1 — fixed —
server/src/services/heartbeat.ts:2306— Both checkout root and requested target are canonicalized withfs.realpath, then rechecked for containment, so an in-checkout symlink cannot route a run outside the managed checkout.
Critical Issues (0)
Important Issues (0)
Suggestions (0)
Strengths
- Repo-relative CWDs are realized only after the managed checkout is provisioned, while absolute, empty, and repo-only paths retain their existing behavior.
- Regression coverage includes lexical traversal, sibling-prefix, escaping symlink, in-repo symlink, and symlinked-checkout-root cases.
Recommended Action
- Ready to proceed with the normal merge checks.
…s checkout (BLO-25415) A git_repo project workspace may declare a repo-relative cwd such as "packages/iwa". The workspace resolver consumed workspace.cwd raw and passed it straight to fs.stat(), which resolves a relative path against the API process's own working directory. That never matches, so the run failed `preferred_workspace_unrealizable` quoting a bare relative path that looks correct — while the real checkout sat on disk untouched. The managed-checkout branch was only taken when cwd was empty or the repo-only sentinel, so a workspace with a relative cwd never got its repo cloned at all. No amount of provisioning could satisfy it, and because the refusal names a path rather than a cause, the failure reads as a missing checkout. BLO-24751 hand-cloned the repo on that reading and closed done; the next run failed byte-identically 71 seconds later. Treat a non-absolute cwd as a subdirectory of the workspace's own repo: realize the managed checkout first, then join the subpath inside it. resolveContainedWorkspaceSubpath refuses traversal that escapes the checkout root, since cwd is operator-supplied and the checkouts share a PVC. Fleet-wide this shape is 2 workspace rows, both pim-multicast-gateway / packages/iwa; between them they had 6+ non-terminal issues stranded. Co-Authored-By: Claude <noreply@anthropic.com>
…when repo-backed (BLO-25415) Review follow-up on #1302. The relative-cwd branch was selected on !path.isAbsolute(cwd) alone, so a local_path / non_git_path / remote_managed workspace with a relative cwd would have been redirected into a managed checkout dir instead of keeping its prior meaning. The redirect is not merely semantic: ensureManagedProjectWorkspace's repoUrl-less branch mkdirs the managed path, so such a workspace would have had an empty directory created on the shared PVC and still failed the subsequent stat. Gate the branch on the workspace being repo-backed, extracted into resolveRepoRelativeWorkspaceCwd so the decision is unit-testable rather than buried in the resolver loop. Repo-backed is tested as "not an explicitly non-repo source_type, and has a repoUrl" rather than source_type === "git_repo": source_type is an unconstrained text column and production carries a row typed "git", which an allowlist would silently skip. Requiring repoUrl also settles the repoUrl-null case raised in the review request — that workspace now keeps its cwd and fails loud without a stray directory. Audited fleet-wide: 1 workspace row still has a relative cwd (4dac485f, [P0] IWA Gateway Certificate Provisioning), and it is git_repo with a repoUrl, so it remains covered by the fix. heartbeat-workspace-session 211 passed (6 new), fail-loud 1 passed, tsc --noEmit exit 0. Co-Authored-By: Claude <noreply@anthropic.com>
…esolution (BLO-25415) Review follow-up on #1302. resolveContainedWorkspaceSubpath checked containment lexically only. `cwd` is operator config, but the checkout's *contents* are repo-controlled: a repo carrying `packages/iwa -> /etc` passes path.resolve(), and the caller's fs.stat() then follows the link, launching the run outside its checkout on a PVC shared with every other repo. Resolve both the checkout root and the target with fs.realpath and re-assert containment on the canonical paths. The realpath pass is skipped when either does not resolve — a missing target has no link to follow, and reporting it missing stays the caller's fs.stat()'s job, so the existing fail-loud path is unchanged. The root is resolved independently because the managed dir may itself sit behind a symlink, which would otherwise fail containment for a legitimate subpath. The escape test was verified to fail against the lexical-only implementation before being committed, so it pins the behavior rather than passing incidentally. heartbeat-workspace-session 214 passed (3 new: symlink escape refused, in-checkout symlink allowed, symlinked checkout root allowed), heartbeat-preferred-workspace-fail-loud 1 passed, tsc --noEmit exit 0. Co-Authored-By: Claude <noreply@anthropic.com>
b91b1ef to
b66c164
Compare
|
Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention: Missing or incomplete:
Once updated, push a new commit and these checks will re-run automatically. — commitperclip |
Thinking Path
Linked Issues or Issue Description
Refs BLO-25415 (Paperclip issue tracker), BLO-24751, BLO-18767.
Problem: A
git_repoproject workspace can declare a repo-relativecwdsuch aspackages/iwa. The workspace resolver inheartbeat.tsconsumedworkspace.cwdraw and handed it tofs.stat(), which resolves a relative path against the API process's own working directory. That never matches, so the run failedpreferred_workspace_unrealizable.Expected behavior: A repo-relative
cwdresolves to<managed checkout>/<cwd>, cloning the repo if it is absent.Steps to reproduce: Point an issue at a non-primary
git_repoworkspace whosecwdis a relative path (e.g.packages/iwa) and trigger a run. The run is refused before any adapter starts withpreferred_workspace_unrealizable, quoting the bare relative path.Deployment mode: on-prem k8s (
paperclipnamespace), CephFS-backed shared PVC.Two things made this expensive to diagnose:
cwdwas empty or the repo-only sentinel, so a workspace with a relativecwdskipped provisioning entirely.Selected project workspace path "packages/iwa" is not available yet.That reads as a missing checkout, so BLO-24751 hand-cloned the repo, closed the issuedone, and the next run failed byte-identically 71 seconds later. The clone was real and correctly placed; it was simply never consulted.What Changed
resolveRepoRelativeWorkspaceCwd()— decides whether a workspace'scwdis a repo-relative subpath that must be joined onto its managed checkout. Only repo-backed workspaces qualify:repoUrlmust be present, and explicitly non-reposourceTypes (local_path,non_git_path,remote_managed) keep their prior meaning.resolveContainedWorkspaceSubpath()— joins the subpath onto the realized checkout and refuses anything that escapes the checkout root. Containment is enforced twice: lexically (against..traversal and sibling-prefix paths) and again afterfs.realpath, because the checkout's contents are repo-controlled and a repo carryingpackages/iwa -> /etcwould otherwise pass the lexical check and then be followed byfs.stat().cwd, empty-cwd, and repo-only-sentinel behaviour is unchanged.Verification
Run locally against this branch rebased on
master:heartbeat-workspace-session.test.ts+heartbeat-preferred-workspace-fail-loud.test.ts— 230 passed (230), 2 test files.npx tsc --noEmit -p server/tsconfig.json— exit 0, 0 errors.node ./scripts/check-commit-author-attribution.mjs --base <master> --head <head>— passes (✓ No commits in range carry the shared allyblockcast[bot] App identity).New regression coverage in
server/src/__tests__/heartbeat-workspace-session.test.ts: the join and normalization, checkout root, lexical traversal escape, sibling-prefix (/managed/repo-evilvs/managed/repo), escaping symlink, in-repo symlink, symlinked checkout root, relativelocal_path/non_git_path/remote_managed, the out-of-enum"git"source_typespelling, andrepoUrl: null.End-to-end signal: run
0e91c938-564f-4a34-b6e7-ff05a8b4236cagainst workspacefcd6d149passed the workspace gate and produced a real run log (3103 bytes, adapter invoked), where the pre-fix runa791e306failedpreferred_workspace_unrealizablewith no log at all.Risks
Low-to-moderate, and narrowly scoped. The new behaviour only engages for a workspace whose
cwdis non-absolute and which is repo-backed (repoUrlpresent,sourceTypenot one of the non-repo kinds). Fleet-wide that is 1 remaining row (4dac485f), audited after therepoUrlgate was added — every other workspace has anullor absolutecwdand takes an unchanged code path.cwdpreviously always failed the realization stat. It now resolves. Any workspace relying on that failure would change behaviour — none exist; the failure was the bug being reported.cwdis operator-supplied config and all managed checkouts share one PVC, so an unvalidated join would let one workspace point a run at another repo. Mitigated by double containment (lexical + post-realpath), with regression tests for both escapes.cwdwithout validation. Worth a follow-up so the bad value cannot be entered again, but out of scope for this fix.Blast radius
Fleet-wide this shape was 2 workspace rows, both
pim-multicast-gateway/packages/iwa:fcd6d1494dac485fBetween them 6+ non-terminal issues were stranded. This fix covers both, including
4dac485f, which has no checkout at all — the resolver now clones it on demand rather than requiring a human to guess the path.fcd6d149was additionally unblocked immediately by rewriting itscwdto the absolute path, so it did not have to wait on this deploy.Related PRs
Searched the GitHub PR list (open + closed) before opening. Nearest neighbours, none duplicating this change:
fix(heartbeat): fail loud when preferred non-primary workspace can't be realized (BLO-8188)(merged). This is the code that emits the refusal; this PR fixes the cause behind it.fix(heartbeat): refuse claude_k8s dispatch onto the shared agent-home fallback cwd (BLO-18147)(merged). Adjacent workspace-cwd handling, different failure.fix(heartbeat): repo-less clone source for workspace-less k8s runs (BLO-18760)(merged). Adjacent clone-source handling.Model Used
Claude Opus (Anthropic), model id
claude-opus-5[1m], 1M context window, extended thinking enabled. Run via the Paperclipclaude_k8sadapter.Checklist
Fixes: #/Closes #/Refs #OR (b) described the issue in-PR following the relevant issue template