From b77dbf8ad8b307a18115bdcf808572f9ad76f71e Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 9 Aug 2026 00:06:26 +0800 Subject: [PATCH 1/2] fix(e2b): mount sandbox policy in live PTY test --- packages/e2b/e2b/package.json | 1 + packages/e2b/e2b/tests/composition.e2e.ts | 8 +++++--- pnpm-lock.yaml | 3 +++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/e2b/e2b/package.json b/packages/e2b/e2b/package.json index a77e3d9464..5abacb278a 100644 --- a/packages/e2b/e2b/package.json +++ b/packages/e2b/e2b/package.json @@ -35,6 +35,7 @@ "devDependencies": { "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-loader-smoke": "workspace:^", + "@deepseek-ai/dsh-sandbox-policy": "workspace:^", "cordis": "^4.0.0-rc.7" } } diff --git a/packages/e2b/e2b/tests/composition.e2e.ts b/packages/e2b/e2b/tests/composition.e2e.ts index 01316aeb2d..6da102a827 100644 --- a/packages/e2b/e2b/tests/composition.e2e.ts +++ b/packages/e2b/e2b/tests/composition.e2e.ts @@ -13,6 +13,7 @@ import { } from '@deepseek-ai/dsh-e2b' import PtyService, { PtySessionId } from '@deepseek-ai/dsh-pty' import { LocalPtyBackend } from '@deepseek-ai/dsh-pty-local' +import SandboxPolicyService from '@deepseek-ai/dsh-sandbox-policy' import { Session, SessionId } from '@deepseek-ai/dsh-session' import E2BSubprocessService from '@deepseek-ai/dsh-subprocess-e2b' @@ -51,10 +52,10 @@ describe.skipIf(!process.env.E2B_API_KEY)('E2B live Loader composition', () => { runtimeRoot: '/home/user/.dsh-e2b', getSandbox: async () => sandbox, } as never) - ctx.provide('sandboxPolicy', { - defaultMode: 'danger-full-access', + const sandboxPolicyFiber = await ctx.plugin(SandboxPolicyService, { + mode: 'danger-full-access', workspaceRoot: '/home/user', - } as never) + }) const ptyFiber = await ctx.plugin(PtyService) const subprocessFiber = await ctx.plugin(E2BSubprocessService) const node = await ctx.subprocess.resolveExecutable('node') @@ -114,6 +115,7 @@ describe.skipIf(!process.env.E2B_API_KEY)('E2B live Loader composition', () => { await session.close('environment test complete') await subprocessFiber.dispose() await ptyFiber.dispose() + await sandboxPolicyFiber.dispose() } finally { await sandbox.kill().catch(() => false) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fab00ba9cb..a92883786a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3156,6 +3156,9 @@ importers: '@deepseek-ai/dsh-loader-smoke': specifier: workspace:^ version: link:../../support/loader-smoke + '@deepseek-ai/dsh-sandbox-policy': + specifier: workspace:^ + version: link:../../sandbox/sandbox-policy cordis: specifier: ^4.0.0-rc.7 version: link:../../../vendor/cordis From f34b2bb634b4959beb342b4cb6bdb38235b08a5c Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 9 Aug 2026 00:51:56 +0800 Subject: [PATCH 2/2] ci(e2b): add manual live sandbox workflow --- .github/workflows/e2b-e2e.yml | 58 +++++++++++++++++++++++++++++++++++ scripts/ci-workflow.spec.ts | 27 ++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 .github/workflows/e2b-e2e.yml diff --git a/.github/workflows/e2b-e2e.yml b/.github/workflows/e2b-e2e.yml new file mode 100644 index 0000000000..abbd0482ed --- /dev/null +++ b/.github/workflows/e2b-e2e.yml @@ -0,0 +1,58 @@ +name: E2E (E2B sandbox) + +# This suite provisions external E2B sandboxes and is intentionally opt-in. +# It has no push, pull_request, schedule, or workflow_call trigger. +on: + workflow_dispatch: + +permissions: + contents: read + +env: + # CI runs must never report to the production telemetry endpoint baked + # into apps/cli/cordis.yml (AppCLIEntry disables the row when set). + DSH_TELEMETRY_DISABLED: '1' + +jobs: + e2b: + runs-on: ubuntu-latest + name: E2B live Loader composition + timeout-minutes: 30 + steps: + - uses: actions/checkout@v6 + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v6 + with: + node-version: 24 + cache: pnpm + + - name: Install (immutable) + run: pnpm install --frozen-lockfile + + # The tests self-skip locally when the credential is absent. A manually + # dispatched run must fail instead of reporting an all-skipped green. + - name: Preflight (require E2B API key) + env: + E2B_API_KEY: ${{ secrets.E2B_API_KEY_EXTERNAL }} + run: | + set -euo pipefail + if [ -z "${E2B_API_KEY:-}" ]; then + echo "::error::E2B_API_KEY is empty. Configure the E2B_API_KEY_EXTERNAL repository secret." + exit 1 + fi + echo "E2B_API_KEY present." + + # The Loader smoke runs package exports under plain Node in lib mode. + - name: Build (lib for the E2B Loader smoke) + run: pnpm run build + + - name: E2B tests (live sandbox) + env: + E2B_API_KEY: ${{ secrets.E2B_API_KEY_EXTERNAL }} + DSH_E2E_MAX_WORKERS: '1' + DSH_EXAMPLE_MODE: lib + run: >- + pnpm exec vitest run --config vitest.e2e.config.ts + packages/e2b/e2b/tests/composition.e2e.ts diff --git a/scripts/ci-workflow.spec.ts b/scripts/ci-workflow.spec.ts index baeac7a8c0..0395fe8c36 100644 --- a/scripts/ci-workflow.spec.ts +++ b/scripts/ci-workflow.spec.ts @@ -28,6 +28,33 @@ describe('CI workflow', () => { }) }) +describe('E2B e2e workflow', () => { + it('is manual-only and fails loud before running the focused live suite', () => { + const workflow = loadWorkflow('.github/workflows/e2b-e2e.yml') + expect(workflow.on).toEqual({ workflow_dispatch: null }) + if (!isRecord(workflow.jobs) || !isRecord(workflow.jobs.e2b) || !Array.isArray(workflow.jobs.e2b.steps)) { + throw new TypeError('E2B e2e workflow must define the e2b job steps') + } + + const steps = workflow.jobs.e2b.steps.filter(isRecord) + const preflight = steps.find(step => step.name === 'Preflight (require E2B API key)') + const e2b = steps.find(step => step.name === 'E2B tests (live sandbox)') + + expect(preflight).toMatchObject({ + env: { E2B_API_KEY: '${{ secrets.E2B_API_KEY_EXTERNAL }}' }, + }) + expect(preflight?.run).toContain('E2B_API_KEY_EXTERNAL repository secret') + expect(e2b).toMatchObject({ + env: { + E2B_API_KEY: '${{ secrets.E2B_API_KEY_EXTERNAL }}', + DSH_E2E_MAX_WORKERS: '1', + DSH_EXAMPLE_MODE: 'lib', + }, + }) + expect(e2b?.run).toContain('packages/e2b/e2b/tests/composition.e2e.ts') + }) +}) + describe('Issue lifecycle workflow', () => { it('uses review signals instead of rerunning when a draft becomes ready', () => { const lifecycle = loadWorkflow('.github/workflows/issue-lifecycle.yml')