Files
WeHub Mirror 6bf8bebf51
CI / Test and Build (push) Failing after 1s
CI / Migrate Dev DB (push) Has been skipped
CI / Migrate DB (push) Has been skipped
CodeQL / Analyze actions (push) Has been cancelled
CodeQL / Analyze javascript-typescript (push) Has been cancelled
CI / Detect Version (push) Has been cancelled
CI / Detect Desktop Changes (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/cron.Dockerfile, ubuntu-latest, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build AMD64 (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/cron.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/db.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/pii.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/realtime.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-8vcpu-ubuntu-2404-arm, ./docker/app.Dockerfile, linux-arm64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
Helm Chart / Lint, test, and validate chart (push) Has been cancelled
Helm Chart / Chart version bumped (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Build Dev ECR (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core) (push) Has been cancelled
CI / Promote Images (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Check Desktop Signing Secrets (push) Has been cancelled
CI / Desktop Release (push) Has been cancelled
CI / Create Desktop Prerelease (push) Has been cancelled
CI / Desktop Prerelease Build (push) Has been cancelled
CI / Publish Desktop Prerelease (push) Has been cancelled
CI / Prune Desktop Prereleases (push) Has been cancelled
Helm Chart / Install on kind and run helm test (push) Has been cancelled
WeHub snapshot of cb28d14c6f2c081de7a0d8729a8c816c9adef67a
2026-08-10 11:17:50 +08:00

208 lines
10 KiB
TypeScript

/**
* Shared helpers for the Pi sandbox backends.
* Keeps E2B path constants, the finalize/push scripts, abort racing, marker
* parsing, and credential-bearing git diagnostic redaction in one place so backends cannot drift on
* security-sensitive details.
*/
import { getMaxExecutionTimeout } from '@/lib/core/execution-limits'
import { resolvePiSandboxLifetimeMs } from '@/lib/execution/remote-sandbox/pi-lifetime'
import { PI_EVENT_FILTER_PATH } from '@/executor/handlers/pi/cloud/event-filter-source'
import { scrubPiSecrets } from '@/executor/handlers/pi/core/redaction'
export const REPO_DIR = '/workspace/repo'
export const PROMPT_PATH = '/workspace/pi-prompt.txt'
export const DIFF_PATH = '/workspace/pi.diff'
export const COMMIT_MSG_PATH = '/workspace/pi-commit.txt'
export const PUSH_ERR_PATH = '/workspace/pi-push-err.txt'
export const CLONE_TIMEOUT_MS = 10 * 60 * 1000
export const FINALIZE_TIMEOUT_MS = 10 * 60 * 1000
export const MAX_DIFF_BYTES = 200_000
export const PUSH_ERROR_MAX = 1000
/**
* Floor for {@link resolvePiTimeoutMs}. Reachable whenever the sandbox lifetime
* is too short to reserve every surrounding command's worst-case ceiling —
* either a configured lifetime, or a run whose own execution deadline is shorter
* than those reserves. Such a run can still finish, since those ceilings are
* pessimistic, so the floor leaves a short turn rather than refusing one.
*/
export const MIN_PI_TIMEOUT_MS = 60 * 1000
/**
* How long one Pi CLI invocation may run, given the lifetime its sandbox was
* created with. Without a cap a hung CLI would sit there until the provider
* reaped the sandbox and surface as an opaque SDK error.
*
* The reserve matters as much as the cap. The sandbox clock starts at create,
* and authoring has three commands around the agent turn: the clone before it,
* then the commit and push after it, the last two sharing
* {@link FINALIZE_TIMEOUT_MS}. Plan has no finalize phase and sets that reserve
* to zero. Capping at the bare lifetime would mean the sandbox died first.
*
* Takes the lifetime as an argument rather than reading the provider ceiling
* itself, because that ceiling is no longer the only lifetime a run can get: a
* caller that narrowed it to the execution deadline has to reserve against the
* lifetime it actually asked for, or it re-opens exactly the bug above on every
* plan whose deadline is shorter than the ceiling.
*
* What is reserved is each command's timeout ceiling, not its measured elapsed
* time — a clone takes seconds in practice — so this is a budget that adds up,
* not a guarantee that the sandbox outlives the run.
*
* Both adapters impose an absolute lifetime: E2B through `timeoutMs` and Daytona
* through `ttlMinutes`. Reserving the surrounding commands keeps the agent turn
* inside the lifetime the selected provider actually received.
*/
export function resolvePiTimeoutMs(
lifetimeMs = resolvePiSandboxLifetimeMs(),
options?: { finalizePhases?: number }
): number {
const finalizePhases = options?.finalizePhases ?? 2
return Math.min(
getMaxExecutionTimeout(),
Math.max(
lifetimeMs - CLONE_TIMEOUT_MS - finalizePhases * FINALIZE_TIMEOUT_MS,
MIN_PI_TIMEOUT_MS
)
)
}
/**
* Marker carrying a digest of the cloned repository's git config. A clone script
* emits it as its *last* line, after any `git remote set-url` rewrite — a digest
* taken before that rewrite mismatches at push time and every push fails.
*
* Every phase that clones in order to push emits it. The Babysit continuation
* verifies it deliberately alone, because verification is not a pure
* tightening — a run that legitimately writes repo-local config would fail its
* push.
*/
export const GIT_CONFIG_DIGEST_MARKER = '__GIT_CONFIG_DIGEST__='
/**
* Digests the only git-config scope a sandbox agent can still write once
* `GIT_CONFIG_NOSYSTEM` and `GIT_CONFIG_GLOBAL` neutralize the system and global
* scopes. One comparison covers every dangerous key — `url.*.insteadOf`,
* `url.*.pushInsteadOf`, `http.proxy`, `core.sshCommand`, `include.path` —
* including keys nobody enumerated. Runs with the repository as its working
* directory, and tolerates a missing `.git/config.worktree`.
*/
export const GIT_CONFIG_DIGEST_LINE = `cat .git/config .git/config.worktree 2>/dev/null | sha256sum | cut -d' ' -f1 | sed "s/^/${GIT_CONFIG_DIGEST_MARKER}/"`
/**
* Stages, commits, and diffs without the GitHub token because repository config
* can execute filters, fsmonitor, external diffs, or textconv during these git
* operations. Commit tolerates an empty tree; the marker checks whether HEAD
* advanced before the separately authenticated push.
*
* The name-listing diff sets `core.quotePath=false` so a path with a non-ASCII
* byte reaches the `changedFiles` output as itself rather than as git's
* `"\303\251"`-escaped rendering.
*/
export const PREPARE_SCRIPT = `set -e
cd ${REPO_DIR}
git -c core.hooksPath=/dev/null add -A
git -c core.hooksPath=/dev/null -c user.email="pi@sim.ai" -c user.name="Sim Pi Agent" commit -F ${COMMIT_MSG_PATH} >/dev/null 2>&1 || true
git -c core.quotePath=false diff --name-only "$BASE_SHA" HEAD | sed "s/^/__CHANGED__=/"
git diff "$BASE_SHA" HEAD > ${DIFF_PATH} 2>/dev/null || true
if git diff --quiet "$BASE_SHA" HEAD; then echo "__NO_CHANGES__=1"; else echo "__NEEDS_PUSH__=1"; fi`
/**
* The only token-bearing command. It neutralizes repository-configured hooks,
* credential helpers, and fsmonitor before pushing agent-authored changes, and
* must be run with `GIT_CONFIG_NOSYSTEM=1` and `GIT_CONFIG_GLOBAL=/dev/null` in
* its env, which the `-c` flags cannot substitute for.
*
* Be precise about what that pair buys. It closes system- and global-scope
* config, so it removes two of the three places a `url.*.insteadOf` rewrite
* could send the token's userinfo to another host. Repository-local config —
* the scope a root agent inside the checkout can actually write — still
* rewrites the push URL. Babysit compares the
* {@link GIT_CONFIG_DIGEST_MARKER} digest before pushing; Create PR does not,
* keeping the exposure it always had.
*
* Git is invoked by absolute path so a shim planted earlier on `$PATH` is not
* what runs. Both sandbox images apt-install git on Debian (see
* `scripts/pi-sandbox-packages.ts`), so this is an image-shape dependency. It
* reduces rather than removes exposure — every sandbox command runs as root, so
* the binary itself is writable too.
*
* The refspec is explicit: `HEAD:refs/heads/$BRANCH` pushes the commit that was
* just verified rather than whatever the local branch ref happens to point at,
* which differ if the agent left HEAD detached or on another branch.
*/
export const PUSH_SCRIPT = `cd ${REPO_DIR}
/usr/bin/git -c core.hooksPath=/dev/null -c credential.helper= -c core.fsmonitor= push "https://x-access-token:$GITHUB_TOKEN@github.com/$REPO_OWNER/$REPO_NAME.git" "HEAD:refs/heads/$BRANCH" >/dev/null 2>${PUSH_ERR_PATH} && echo "__PUSHED__=1"`
/**
* The Pi CLI invocation for the sandbox modes, piped through the sandbox event filter. The command
* names {@link PI_EVENT_FILTER_PATH}, so every caller must have written `PI_EVENT_FILTER_SOURCE`
* there first — skipping that write does not fall back to the raw stream, it fails the run on the
* missing module.
*
* Selects `/bin/bash` explicitly because `pipefail` is not portable to `/bin/sh`, and without it
* the pipeline reports the filter's exit code rather than Pi's, so an upstream crash would read as
* a clean run. Both dedicated Pi images are Debian-based and provide Bash, so provider
* default-shell behavior cannot change whether an upstream Pi failure reaches the caller.
*
* With no options the repository resources Pi loads are exactly what Create PR always had. With an
* `extensionPath`, `--no-extensions` drops any extension the cloned repository ships while leaving
* the explicit `-e` path loaded, so the loaded set is exactly Sim's own extension. That is deliberate —
* a repository must not be able to register tools into a run holding the workspace's keys — but it
* does mean enabling search also stops loading a repository's own Pi extensions, which is why the
* flag is not passed on Create PR's no-search path. Babysit supplies
* `disableRepositoryResources` in every round, which also disables repository prompt templates,
* skills, and project trust.
*/
export function buildPiScript(
extensionPath?: string,
options?: { disableRepositoryResources?: boolean }
): string {
const repositoryArgs = options?.disableRepositoryResources
? ' --no-extensions --no-prompt-templates --no-skills --no-approve'
: extensionPath
? ' --no-extensions'
: ''
const extensionArgs = extensionPath ? ` -e ${extensionPath}` : ''
return `/bin/bash -o pipefail -c 'cd ${REPO_DIR}
pi -p --mode json --provider "$PI_PROVIDER" --model "$PI_MODEL" --thinking "$PI_THINKING"${repositoryArgs}${extensionArgs} < ${PROMPT_PATH} | node ${PI_EVENT_FILTER_PATH}'`
}
export function raceAbort<T>(promise: Promise<T>, signal?: AbortSignal): Promise<T> {
if (!signal) return promise
if (signal.aborted) return Promise.reject(new Error('Pi run aborted'))
return new Promise<T>((resolve, reject) => {
const onAbort = () => reject(new Error('Pi run aborted'))
signal.addEventListener('abort', onAbort, { once: true })
promise.then(
(value) => {
signal.removeEventListener('abort', onAbort)
resolve(value)
},
(error) => {
signal.removeEventListener('abort', onAbort)
reject(error)
}
)
})
}
export function extractMarkerValues(stdout: string, prefix: string): string[] {
return stdout
.split('\n')
.filter((line) => line.startsWith(prefix))
.map((line) => line.slice(prefix.length).trim())
.filter(Boolean)
}
/**
* Redacts the GitHub token from git output before it is surfaced in an error.
* Removes the literal token and any URL userinfo (`//user:token@`), so a failure
* message can quote git's real stderr without leaking the credential.
*/
export function scrubGitSecrets(text: string, token: string): string {
const withoutToken = scrubPiSecrets(text, [token])
return withoutToken.replace(/\/\/[^/@\s]+@/g, '//***@')
}