Skip to content

Add agent workspace provisioning helpers (workspace-seeder + workspace-mount)#208

Merged
willwashburn merged 6 commits into
mainfrom
feat/agent-workspace-provisioning
May 26, 2026
Merged

Add agent workspace provisioning helpers (workspace-seeder + workspace-mount)#208
willwashburn merged 6 commits into
mainfrom
feat/agent-workspace-provisioning

Conversation

@willwashburn

Copy link
Copy Markdown
Member

Summary

  • Adds src/workspace-seeder.ts (createWorkspaceIfNeeded, seedAclRules, seedWorkspace, seedWorkflowAcls, seedWorkspaceTar) and src/workspace-mount.ts (ensureRelayfileMount + MountConfig/MountHandle).
  • Re-exports them from the package root and exposes @relayfile/sdk/workspace-seeder and @relayfile/sdk/workspace-mount subpaths.
  • Bumps @relayfile/sdk to 0.8.0 (additive public API).

Why

These helpers have always been RelayFileClient wrappers + relayfile-mount binary lifecycle helpers. They were sitting in @agent-relay/sdk only because the original consumer (the workflow runner) lived there. We're untangling that: workflows move to @relayfile/sdk for relayfile primitives, agent-relay shrinks to broker concerns, and @relayflows/core consumes both.

Once this merges and a new @relayfile/sdk@0.8.0 ships, the matching changes in agent-relay (drop the in-package seeder/mount) and relayflows (depend on the new exports) can land.

Test plan

  • npm --prefix packages/sdk/typescript run typecheck ✓ (already passes locally)
  • npm --prefix packages/sdk/typescript test (no new tests added — these files were already battle-tested in agent-relay; existing relayfile suites should be unaffected)
  • Verify the dist tarball after build includes `dist/workspace-seeder.js`/`dist/workspace-mount.js` and their `.d.ts` files

🤖 Generated with Claude Code

Lifts seedWorkspace / seedWorkflowAcls / createWorkspaceIfNeeded /
seedAclRules / seedWorkspaceTar and the ensureRelayfileMount lifecycle
helper out of @agent-relay/sdk and into @relayfile/sdk where they belong.

These have always been RelayFileClient wrappers and relayfile-mount
binary lifecycle helpers — they were sitting in agent-relay only
because the original consumer (the workflow runner) lived there.
Splitting them here lets agent-relay shrink to broker concerns and
keeps relayfile-server interactions inside the relayfile SDK.

Two new files:
  src/workspace-seeder.ts ── workspace creation, file/ACL seeding
  src/workspace-mount.ts  ── ensureRelayfileMount + MountConfig/MountHandle

Both re-exported from the package root and accessible via the
@relayfile/sdk/workspace-seeder and @relayfile/sdk/workspace-mount
subpaths.

Bumps to 0.8.0 (minor — additive public API surface).
@gemini-code-assist

Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@willwashburn, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 52 minutes and 1 second. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: cae29f65-6bb0-4816-b863-af1a96eb375c

📥 Commits

Reviewing files that changed from the base of the PR and between 899940c and 5b72301.

📒 Files selected for processing (1)
  • packages/sdk/typescript/src/workspace-mount.ts
📝 Walkthrough

Walkthrough

The PR adds workspace seeding and relayfile-mount lifecycle management to the TypeScript SDK. It introduces workspace-seeder and workspace-mount subpath exports, each with installation/provisioning utilities: binary download with checksum verification, mount process supervision, and file/ACL seeding strategies with HTTP fallback.

Changes

Workspace Provisioning and Mount Features

Layer / File(s) Summary
Package Configuration and Documentation
CHANGELOG.md, package.json, src/index.ts
Version bumped to 0.8.0; exports map gains ./workspace-seeder and ./workspace-mount subpaths; dependencies adds ignore and tar alongside @relayfile/core pin; index.ts documents Node-only helper exclusion from default entry.
Mount Binary Download and Verification
src/workspace-mount.ts (lines 1–281)
Platform target resolution, release asset URL construction, binary download with manual redirect handling, checksum file retrieval, SHA-256 verification, macOS codesign re-signing, and version-aware caching under ~/.agent-relay/bin.
Mount Process Lifecycle and Handle Management
src/workspace-mount.ts (lines 283–436)
Command execution via spawn with stdout/stderr capture, process health checking, graceful SIGTERM/forceful SIGKILL termination, and ensureRelayfileMount entry point that provisions mount directory, runs initial sync, spawns persistent daemon, validates startup, and returns handle with stop() method.
Workspace Seeding Functions and Strategies
src/workspace-seeder.ts
Exports createWorkspaceIfNeeded (workspace creation via client/HTTP fallback), seedWorkspace (file collection with exclusion, batched 50-file bulk upload), seedAclRules (.relayfile.acl JSON generation), seedWorkflowAcls (agent permission merging), and seedWorkspaceTar (Git-tracked file tarball with 404→bulk fallback). Core utilities include base URL/workspace ID normalization, UTF-8 content detection, directory recursion with symlink cycle guards, and bulk write response parsing.
Workspace Seeding and Tar Upload Test Coverage
src/workspace-seeder.test.ts, src/workspace-seeder-tar.test.ts
Vitest suites validating file collection and encoding, ACL rule generation, workflow ACL agent permission merging (allow/deny unions), tar upload with Git-tracked file discovery, exclude directory filtering, 404 fallback to bulk write, non-git filesystem traversal, and HTTP error propagation.

Sequence Diagram(s)

Mount binary installation and lifecycle are covered in the hidden review stack artifact above.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • AgentWorkforce/relayfile#123: Adds documentation and user-facing examples for the SDK workspace-mount helper integration introduced in this PR.
  • AgentWorkforce/relayfile#122: Implements post-auth mount session workflows (mountWorkspace, ensureMountedWorkspace) that depend on this PR's ensureRelayfileMount process lifecycle and mount handle management.

Poem

🐰 A rabbit hops through workspace trees,
Seeding files on gentle breeze,
Mount and sync with cheerful might—
ACLs shared, permissions right!
From tar to bulk, fallback flows free,
Relay mounts, hooray for thee! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.85% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main change: adding two new helper modules (workspace-seeder and workspace-mount) for agent workspace provisioning to the SDK.
Description check ✅ Passed The description clearly explains what was added, why it was moved from @agent-relay/sdk to @relayfile/sdk, and outlines the test plan and downstream dependencies.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/agent-workspace-provisioning

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented May 26, 2026

Copy link
Copy Markdown

Relayfile Eval Review

Run: .relayfile/evals/runs/2026-05-26T20-50-47-485Z-HEAD-provider
Mode: provider
Git SHA: cb6a1dc

Passed: 4 | Needs human: 0 | Reviewable: 0 | Missing output: 0 | Failed: 0 | Skipped: 0

Human Review Cases

No reviewable human-review cases captured Relayfile output.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 050eb38bd4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}
stopped = true;
await stopMountProcess(mountProc).catch(() => undefined);
await rm(mountPoint, { recursive: true, force: true }).catch(() => undefined);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve caller-provided mount directory on stop

ensureRelayfileMount always removes mountPoint recursively in stop(), even when the path came from config.mountPoint. If callers pass an existing directory, calling stop() can delete that entire directory tree and cause data loss; cleanup should be limited to internally-created temp mount paths (or be explicitly opt-in).

Useful? React with 👍 / 👎.

}
const stat = fs.statSync(resolved);
if (stat.isDirectory()) {
collectSeedPaths(rootDir, nextRelative, excludeDirs, output);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid recursive loops through symlinked directories

When a symlink resolves to rootDir (or another ancestor inside it), this branch recurses back into the same tree via collectSeedPaths(rootDir, nextRelative, ...) without any visited-realpath guard. A repo containing a self-referential symlink can therefore cause unbounded recursion/hangs during seeding; directory symlinks should be skipped or tracked by resolved path.

Useful? React with 👍 / 👎.

? rawFiles.filter((f) => {
const segments = f.split('/');
if (DEFAULT_EXCLUDED_FILES.has(segments[segments.length - 1])) return false;
return !segments.some((seg) => excludes.has(seg));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Honor nested exclude paths for git-tracked tar seeding

In the git-tracked path, excludes are checked per segment (segments.some(...)), so a configured exclude like "dist/generated" is never matched because neither segment equals that full key. This causes nested excluded paths to still be uploaded in tar mode; filtering should evaluate full relative paths/prefixes, not only individual segments.

Useful? React with 👍 / 👎.

batch,
`seed-workspace-${workspace}-${Date.now()}-${batchIndex}`
);
seededCount += result.written;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Fail seeding when bulk-write reports per-file errors

seedWorkspace accumulates result.written but never checks result.errorCount, so partial batch failures are silently treated as success. The same module already treats nonzero errorCount as fatal in seedAclRules; this omission can leave workspaces partially seeded without surfacing an error to callers.

Useful? React with 👍 / 👎.

@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.

6 issues found across 5 files

You’re at about 97% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/sdk/typescript/src/workspace-mount.ts">

<violation number="1" location="packages/sdk/typescript/src/workspace-mount.ts:98">
P2: Add request timeouts to binary/checksum downloads; a stalled HTTPS connection can block workspace provisioning indefinitely.</violation>
</file>

<file name="packages/sdk/typescript/src/workspace-seeder.ts">

<violation number="1" location="packages/sdk/typescript/src/workspace-seeder.ts:360">
P2: File contents are fully materialized before batching, which can cause high memory usage on large workspaces.</violation>
</file>

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

Re-trigger cubic

Comment thread packages/sdk/typescript/src/workspace-mount.ts Outdated
Comment thread packages/sdk/typescript/src/workspace-seeder.ts Outdated
Comment thread packages/sdk/typescript/src/workspace-seeder.ts
Comment thread packages/sdk/typescript/src/workspace-seeder.ts
resolve: () => void,
reject: (error: Error) => void
) => {
const request = https.get(currentUrl, (res) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Add request timeouts to binary/checksum downloads; a stalled HTTPS connection can block workspace provisioning indefinitely.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/sdk/typescript/src/workspace-mount.ts, line 98:

<comment>Add request timeouts to binary/checksum downloads; a stalled HTTPS connection can block workspace provisioning indefinitely.</comment>

<file context>
@@ -0,0 +1,419 @@
+    resolve: () => void,
+    reject: (error: Error) => void
+  ) => {
+    const request = https.get(currentUrl, (res) => {
+      const status = res.statusCode ?? 0;
+      const location = res.headers.location;
</file context>

collectSeedPaths(rootDir, '', excludes, seedPaths);
const allFiles = seedPaths
.sort((left, right) => left.localeCompare(right))
.map((filePath) => buildSeedFilePayload(filePath, rootDir));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: File contents are fully materialized before batching, which can cause high memory usage on large workspaces.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/sdk/typescript/src/workspace-seeder.ts, line 360:

<comment>File contents are fully materialized before batching, which can cause high memory usage on large workspaces.</comment>

<file context>
@@ -0,0 +1,571 @@
+  collectSeedPaths(rootDir, '', excludes, seedPaths);
+  const allFiles = seedPaths
+    .sort((left, right) => left.localeCompare(right))
+    .map((filePath) => buildSeedFilePayload(filePath, rootDir));
+
+  let seededCount = 0;
</file context>

The seeder and mount helpers statically pull in node:child_process,
node:fs, and node:path. Re-exporting them from the default entry
breaks the SDK's import-safety guarantee for browser and edge
consumers, so import them from @relayfile/sdk/workspace-seeder and
@relayfile/sdk/workspace-mount instead.
The original file used node:test, which vitest does not collect, so
the suite reported 'No test suite found' and CI failed.
- workspace-mount: only remove the mount point on shutdown/failure
  when ensureRelayfileMount created it itself (mkdtemp path). When
  the caller passes their own mountPoint, leave it alone so we never
  recursively delete user-owned directories.
- workspace-seeder: send the tar upload body as a Uint8Array view
  (Buffer-backed) so vitest can introspect it and the receiving end
  still gets bytes, not a raw ArrayBuffer.
- workspace-seeder: track visited real paths during the directory
  walk so a symlinked directory cycle can no longer cause infinite
  recursion.
- workspace-seeder: surface errorCount from bulk-write batches by
  throwing when any batch reports errors, instead of silently
  swallowing partial failures.
- workspace-seeder: honor nested excludeDirs entries like
  packages/app/build when filtering git ls-files output, not just
  single-segment names.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🧹 Nitpick comments (2)
packages/sdk/typescript/src/workspace-mount.ts (1)

226-233: ⚡ Quick win

Avoid shell interpolation for codesign.

execSync() here still goes through a shell. A quote or shell metacharacter in binaryPath can break signing, and this keeps the command-injection warning alive for no real benefit. Prefer an argv-based call instead.

Suggested change
-import { execSync, spawn, type ChildProcess } from 'node:child_process';
+import { execFileSync, spawn, type ChildProcess } from 'node:child_process';

 function resignBinaryForMacOS(binaryPath: string): void {
   if (os.platform() !== 'darwin') {
     return;
   }

   try {
-    execSync(`codesign --force --sign - "${binaryPath}"`, { stdio: 'pipe' });
+    execFileSync('codesign', ['--force', '--sign', '-', binaryPath], { stdio: 'pipe' });
   } catch {
     // Ignore best-effort re-sign failures.
   }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/sdk/typescript/src/workspace-mount.ts` around lines 226 - 233, In
resignBinaryForMacOS replace the shell-interpolated execSync call with an
argv-based child process call (e.g., child_process.execFileSync or spawnSync) so
the binaryPath is passed as a separate argument instead of interpolated into a
shell string; update the call in resignBinaryForMacOS to invoke "codesign" with
arguments ["--force","--sign","-","<binaryPath>"] (using the actual binaryPath
variable) and keep the same stdio options and try/catch behavior so command
injection and shell-escaping issues are eliminated.
packages/sdk/typescript/src/workspace-seeder.test.ts (1)

61-323: ⚡ Quick win

Add focused tests for createWorkspaceIfNeeded behavior matrix.

This suite skips the new public provisioning entrypoint. Please add cases for SDK overload attempts, HTTP fallback body variants, and 409 as success to protect release-critical workspace creation flows.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/sdk/typescript/src/workspace-seeder.test.ts` around lines 61 - 323,
Add new focused tests for createWorkspaceIfNeeded: simulate the SDK path by
spying on RelayFileClient.prototype.createWorkspace to throw an overload/error
and assert the function falls back to HTTP POST with the correct request body
variants (e.g., with and without optional fields) by mocking globalThis.fetch
and inspecting its payload; also add a test where the HTTP POST returns status
409 and ensure createWorkspaceIfNeeded treats 409 as success (resolves) rather
than throwing. Target the createWorkspaceIfNeeded callsite in your tests, and
use the existing patterns (vi.spyOn(RelayFileClient.prototype,
'createWorkspace') and globalThis.fetch mocks) and helpers like
parseFetchBody/singleFetchCall to validate behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/sdk/typescript/src/workspace-mount.ts`:
- Around line 422-434: The stop() implementation currently flips the stopped
flag and then runs asynchronous cleanup, allowing concurrent callers to return
early; fix by memoizing the in-flight shutdown Promise: add a local variable
(e.g., stoppingPromise) alongside stopped, and when stop() is first invoked
assign stoppingPromise = (async () => { await
stopMountProcess(mountProc).catch(() => undefined); await cleanupMountPoint();
})(); set stopped = true immediately and always return await stoppingPromise for
subsequent callers so all callers await the same shutdown sequence involving
stopMountProcess and cleanupMountPoint.
- Around line 283-312: runCommandCapture currently can hang if the child never
exits; add a one-shot timeout that kills the spawned process and rejects the
promise with a clear timeout error; specifically, inside runCommandCapture after
spawning proc, create a timer (e.g., via setTimeout) that calls proc.kill() and
rejects with a new Error like "command timed out", store the timer id, and
ensure you clearTimeout(timer) in the proc.on('close') and proc.on('error')
handlers so the timer is cleaned up; make sure the rejection on timeout happens
only once (remove/guard listeners or check a settled flag) so you don't end up
calling resolve/reject twice.

---

Nitpick comments:
In `@packages/sdk/typescript/src/workspace-mount.ts`:
- Around line 226-233: In resignBinaryForMacOS replace the shell-interpolated
execSync call with an argv-based child process call (e.g.,
child_process.execFileSync or spawnSync) so the binaryPath is passed as a
separate argument instead of interpolated into a shell string; update the call
in resignBinaryForMacOS to invoke "codesign" with arguments
["--force","--sign","-","<binaryPath>"] (using the actual binaryPath variable)
and keep the same stdio options and try/catch behavior so command injection and
shell-escaping issues are eliminated.

In `@packages/sdk/typescript/src/workspace-seeder.test.ts`:
- Around line 61-323: Add new focused tests for createWorkspaceIfNeeded:
simulate the SDK path by spying on RelayFileClient.prototype.createWorkspace to
throw an overload/error and assert the function falls back to HTTP POST with the
correct request body variants (e.g., with and without optional fields) by
mocking globalThis.fetch and inspecting its payload; also add a test where the
HTTP POST returns status 409 and ensure createWorkspaceIfNeeded treats 409 as
success (resolves) rather than throwing. Target the createWorkspaceIfNeeded
callsite in your tests, and use the existing patterns
(vi.spyOn(RelayFileClient.prototype, 'createWorkspace') and globalThis.fetch
mocks) and helpers like parseFetchBody/singleFetchCall to validate behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0aaf199f-2d58-4de8-b6fb-5ea11ae1a610

📥 Commits

Reviewing files that changed from the base of the PR and between fe415d0 and 899940c.

📒 Files selected for processing (7)
  • packages/sdk/typescript/CHANGELOG.md
  • packages/sdk/typescript/package.json
  • packages/sdk/typescript/src/index.ts
  • packages/sdk/typescript/src/workspace-mount.ts
  • packages/sdk/typescript/src/workspace-seeder-tar.test.ts
  • packages/sdk/typescript/src/workspace-seeder.test.ts
  • packages/sdk/typescript/src/workspace-seeder.ts

Comment thread packages/sdk/typescript/src/workspace-mount.ts Outdated
Comment thread packages/sdk/typescript/src/workspace-mount.ts Outdated
- ensureRelayfileMount.stop() now memoizes the in-flight shutdown
  promise so concurrent callers all await the same termination and
  cleanup sequence instead of the second caller returning early
  while stopMountProcess/cleanupMountPoint are still running.
- runCommandCapture enforces a five-minute timeout that kills the
  spawned child and rejects with a clear error, so a stalled
  relayfile-mount sync can no longer hang ensureRelayfileMount
  forever. Settled state is guarded so we never double-resolve.
- resignBinaryForMacOS calls codesign via execFileSync with an argv
  array so shell metacharacters in the cached binary path cannot
  hijack or break the signing step.

@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.

2 issues found across 6 files (changes from recent commits).

You’re at about 99% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/sdk/typescript/src/workspace-seeder.ts">

<violation number="1" location="packages/sdk/typescript/src/workspace-seeder.ts:134">
P2: The new global `visited` set can skip legitimate directory aliases, making seeded file paths nondeterministic when both a real dir and symlinked path point to the same target.</violation>
</file>

<file name="packages/sdk/typescript/src/workspace-mount.ts">

<violation number="1" location="packages/sdk/typescript/src/workspace-mount.ts:412">
P2: The `stop()` method is not safe for concurrent callers. After setting `stopped = true`, the async cleanup (`stopMountProcess` + `cleanupMountPoint`) is in-flight, but a second `await handle.stop()` will hit the early `return` and resolve before the first call finishes teardown. Memoize the shutdown promise so all callers await the same completion:

```ts
let stopPromise: Promise<void> | undefined;
async stop() {
  if (!stopPromise) {
    stopPromise = (async () => {
      await stopMountProcess(mountProc).catch(() => undefined);
      await cleanupMountPoint();
    })();
  }
  await stopPromise;
}
```</violation>
</file>

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

Re-trigger cubic

// Cycle guard: a symlinked directory pointed back to an ancestor.
return;
}
visited.add(realDir);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: The new global visited set can skip legitimate directory aliases, making seeded file paths nondeterministic when both a real dir and symlinked path point to the same target.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/sdk/typescript/src/workspace-seeder.ts, line 134:

<comment>The new global `visited` set can skip legitimate directory aliases, making seeded file paths nondeterministic when both a real dir and symlinked path point to the same target.</comment>

<file context>
@@ -117,9 +117,22 @@ function collectSeedPaths(
+    // Cycle guard: a symlinked directory pointed back to an ancestor.
+    return;
+  }
+  visited.add(realDir);
+
   const entries = fs.readdirSync(absoluteDir, { withFileTypes: true });
</file context>

if (mountProc) {
await stopMountProcess(mountProc).catch(() => undefined);
}
await cleanupMountPoint();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: The stop() method is not safe for concurrent callers. After setting stopped = true, the async cleanup (stopMountProcess + cleanupMountPoint) is in-flight, but a second await handle.stop() will hit the early return and resolve before the first call finishes teardown. Memoize the shutdown promise so all callers await the same completion:

let stopPromise: Promise<void> | undefined;
async stop() {
  if (!stopPromise) {
    stopPromise = (async () => {
      await stopMountProcess(mountProc).catch(() => undefined);
      await cleanupMountPoint();
    })();
  }
  await stopPromise;
}
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/sdk/typescript/src/workspace-mount.ts, line 412:

<comment>The `stop()` method is not safe for concurrent callers. After setting `stopped = true`, the async cleanup (`stopMountProcess` + `cleanupMountPoint`) is in-flight, but a second `await handle.stop()` will hit the early `return` and resolve before the first call finishes teardown. Memoize the shutdown promise so all callers await the same completion:

```ts
let stopPromise: Promise<void> | undefined;
async stop() {
  if (!stopPromise) {
    stopPromise = (async () => {
      await stopMountProcess(mountProc).catch(() => undefined);
      await cleanupMountPoint();
    })();
  }
  await stopPromise;
}
```</comment>

<file context>
@@ -392,13 +409,13 @@ export async function ensureRelayfileMount(config: MountConfig): Promise<MountHa
       await stopMountProcess(mountProc).catch(() => undefined);
     }
-    await rm(mountPoint, { recursive: true, force: true }).catch(() => undefined);
+    await cleanupMountPoint();
     const message = error instanceof Error ? error.message : String(error);
     throw new Error(`${startupPhase} failed for ${config.workspace}: ${message}`);
</file context>

@willwashburn

Copy link
Copy Markdown
Member Author

Pushed a follow-up batch to take the PR from red to green and address the AI reviewer findings:

  1. Make workspace provisioning helpers subpath-only (a6075d6) — the moved files statically import node:child_process, node:fs, and node:path, so re-exporting them from the package root broke the SDK's import-safety guarantee for browser/edge consumers. They're now exposed exclusively via @relayfile/sdk/workspace-seeder and @relayfile/sdk/workspace-mount, and the CHANGELOG entry was updated to match.
  2. Port workspace-seeder.test.ts to vitest (1aff67d) — the original file used node:test, which vitest does not collect, so it surfaced as "No test suite found".
  3. Address cubic findings on the moved files (899940c):
    • workspace-mount.ts P0: stop() / failure cleanup only removes the mount point when ensureRelayfileMount created it via mkdtemp. Caller-provided paths are left intact.
    • workspace-seeder.ts P1: directory walk tracks visited real paths so symlinked cycles can't recurse forever.
    • workspace-seeder.ts P1: seedWorkspace now sums errorCount across batches and throws with the collected error payload when any batch reports failures.
    • workspace-seeder.ts P1: nested excludeDirs entries (e.g. packages/app/build) are honored when filtering git ls-files output, not just single-segment names.
    • Also fixed the tar upload body to be a Uint8Array view (Buffer-backed) so the import endpoint and the tar test see the same bytes — this was the third red test.
  4. Harden mount lifecycle (5b72301) from the CodeRabbit pass:
    • stop() memoizes the in-flight shutdown promise so concurrent callers all await the same termination + cleanup.
    • runCommandCapture enforces a 5-minute timeout, kills the child on timeout, and guards against double-settle.
    • resignBinaryForMacOS switched to execFileSync with argv so a quoted/special cache path can't break or hijack the codesign call.

Remaining open items: the two cubic P2s (HTTPS download timeouts and large-file streaming for the seeder) and the CodeRabbit nitpick to add createWorkspaceIfNeeded tests — leaving those out per the original PR scope. All required CI checks are green on 5b72301.

@willwashburn willwashburn merged commit 324f013 into main May 26, 2026
9 checks passed
@willwashburn willwashburn deleted the feat/agent-workspace-provisioning branch May 26, 2026 20:58
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