feat(pi): add pi actor integration - #5767
eersnington wants to merge 9 commits into
Conversation
|
This PR was not deployed automatically as @eersnington does not have access to the Railway project. In order to get automatic PR deploys, please add @eersnington to your workspace on Railway. |
Review:
|
4221a4d to
4f4fcc4
Compare
4f4fcc4 to
b004815
Compare
| const stdout: Uint8Array[] = []; | ||
| const stderr: Uint8Array[] = []; | ||
| const result = (exitCode: number | null, timedOut: boolean): SandboxExecResult => ({ | ||
| exitCode, | ||
| timedOut, | ||
| stdout: Buffer.concat(stdout).toString(), | ||
| stderr: Buffer.concat(stderr).toString(), | ||
| }); | ||
| const deadline = | ||
| options.timeoutMs === undefined ? undefined : Date.now() + options.timeoutMs; | ||
| let after: number | undefined; | ||
|
|
||
| while (true) { | ||
| if (options.signal?.aborted) { | ||
| await killQuietly(process); | ||
| throw new Error("aborted"); | ||
| } | ||
| const { chunks, exit } = await process.poll(after); | ||
| for (const chunk of [...chunks].sort((a, b) => a.sequence - b.sequence)) { | ||
| if (after !== undefined && chunk.sequence <= after) continue; | ||
| after = chunk.sequence; | ||
| (chunk.stream === "stdout" ? stdout : stderr).push(chunk.data); |
There was a problem hiding this comment.
🟠 Medium · Bound output retained by remote processes
Every chunk is retained until exit and then concatenated, even when the caller only needs streamed onData plus the exit code (as Pi's bash tool does). An authorized prompt or executeBash call can emit data continuously and exhaust the actor's memory before its timeout; the remote backend already retains the same logs, so this is also duplicate buffering. Add a bounded/spooled collection policy, and let streaming-only callers disable full stdout/stderr accumulation.
| while (true) { | ||
| if (options.signal?.aborted) { | ||
| await killQuietly(process); | ||
| throw new Error("aborted"); | ||
| } | ||
| const { chunks, exit } = await process.poll(after); |
There was a problem hiding this comment.
🟠 Medium · Enforce cancellation while a poll is in flight
The timeout and abort checks cannot run while process.poll() is pending. A stalled agentOS action or sandbox-agent HTTP request therefore defeats both controls: the helper never reaches the deadline check and never kills the remote process, despite its contract. Race each poll against the abort signal and remaining deadline, then kill the process when either wins.
| return { isDirectory: () => stat.isDirectory }; | ||
| }, | ||
| }, | ||
| }); | ||
| const find = createFindToolDefinition(root, { | ||
| operations: { | ||
| exists: (path) => sandbox.exists(resolvePath(path)), | ||
| glob: async (pattern, searchDirectory, options) => { | ||
| const searchRoot = resolvePath(searchDirectory); |
There was a problem hiding this comment.
🟠 Medium · Preserve ignore semantics in sandbox search tools
This replacement for Pi's built-in find discards options.ignore and enumerates every file except two hard-coded directories; the custom grep below has the same limitation. Pi advertises these tools as respecting .gitignore, so sandboxed projects now return ignored build artifacts and secret files that the normal tools omit, and large ignored trees can dominate/truncate results. Implement the operation's ignore list plus repository ignore rules (for example with fd/rg or an equivalent fallback) for both tools.
b004815 to
1e7248d
Compare
There was a problem hiding this comment.
🟠 5 medium-severity findings
Reviewed commit 1e7248d.
🟠 Medium · Bound output retained by remote processes
Every chunk is still retained until exit and then concatenated, even when the caller only needs streamed onData plus the exit code (as Pi's bash tool does). A high-output command can exhaust the actor's memory before its timeout; agentOS already retains the same logs, so this is duplicate buffering. Add a bounded/spooled collection policy, and let streaming-only callers disable full stdout/stderr accumulation.
Original location: "shared/typescript/sandbox-adapter/src/remote-process.ts":56 (new side, not submitted inline).
🟠 Medium · Enforce cancellation while a poll is in flight
The timeout and abort checks still cannot run while process.poll() is pending. A stalled agentOS action therefore defeats both controls: the helper never reaches the deadline check and never kills the remote process, despite its contract. Race each poll against the abort signal and remaining deadline, then kill the process when either wins.
Original location: "shared/typescript/sandbox-adapter/src/remote-process.ts":52 (new side, not submitted inline).
🟠 Medium · Honor abort signals in the Daytona adapter
This provider never observes options.signal, although Sandbox.exec promises that aborting kills the process and rejects with Error("aborted"). Consequently abort(), actor action timeouts, and shutdown cannot stop a Daytona command; executeCommand continues until its own optional timeout (or indefinitely when none was supplied). Use a cancellable/background Daytona process API and terminate it when the signal fires, including the already-aborted case.
Original location: "shared/typescript/sandbox-adapter/src/daytona.ts":54 (new side, not submitted inline).
🟠 Medium · Report provider command timeouts as timeouts
Both new providers hard-code timedOut: false (also e2b.ts:74), so a provider-side command deadline can never satisfy SandboxExecResult's timeout contract. createSandboxBashOperations only converts timedOut: true into Pi's expected timeout:<seconds> error; with these adapters a timed-out command is instead surfaced as a normal nonzero exit or an SDK exception. Detect each SDK's timeout result/error and return { exitCode: null, timedOut: true, ... }.
Original location: "shared/typescript/sandbox-adapter/src/daytona.ts":62 (new side, not submitted inline).
| return { isDirectory: () => stat.isDirectory }; | ||
| }, | ||
| }, | ||
| }); | ||
| const find = createFindToolDefinition(root, { | ||
| operations: { | ||
| exists: (path) => sandbox.exists(resolvePath(path)), | ||
| glob: async (pattern, searchDirectory, options) => { | ||
| const searchRoot = resolvePath(searchDirectory); |
There was a problem hiding this comment.
🟠 Medium · Preserve ignore semantics in sandbox search tools
This replacement for Pi's built-in find still discards options.ignore and enumerates every file except two hard-coded directories; the custom grep below has the same limitation. Pi advertises these tools as respecting .gitignore, so ignored build artifacts and secret files enter results and large ignored trees can dominate/truncate them. Implement the operation's ignore list plus repository ignore rules for both tools.
0dbd4fe to
e89e085
Compare
1e7248d to
1448347
Compare
e89e085 to
c791b5c
Compare
1448347 to
5719ca6
Compare
Adds
@rivet-dev/pi, which runs one Pi coding-agent session per Rivet Actor.pi()wrapsactor(). Pi's session methods are actions, and every Pi event is broadcast onevent.setModelswitches only withinscopedModels.credentialssupplies provider logins, such as a user's own subscription.invoke_agent pispan.Security:
setModelaccepts onlyscopedModels, so a client cannot send a key to its own URL.This is part 2 of 3 in a stack: