Merge audited upstream for raw B1 qualification

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-11-03 22:53:51 -06:00
commit 07061a601c
211 files changed
+16449 -4667

No files matched your search

@@ -65,6 +65,14 @@ runs:
pnpm install --frozen-lockfile
fi
- name: Replay workspace install hooks after restore
if: steps.installed.outputs.cache-hit == 'true'
shell: bash
working-directory: ${{ inputs.working-directory }}
run: |
pnpm -r --if-present run postinstall
pnpm run postinstall
- name: Restore certified shared build outputs (read only)
if: inputs.restore-cache == 'true' && steps.inputs.outputs.cacheable == 'true'
id: builds
@@ -71,6 +71,36 @@ test('unmodelled workspace lifecycle scripts disable materialized reuse', () =>
}
});
test('known Copilot hooks permit caching, track their helpers, and still run on restore', () => {
const files = { ...entries,
'server/package.json': JSON.stringify({ scripts: { postinstall: 'node scripts/install-copilot-assets.mjs' } }),
'packages/paperclip-runner/package.json': JSON.stringify({ scripts: { postinstall: 'node scripts/install-copilot-assets.mjs' } }),
'server/scripts/install-copilot-assets.mjs': 'server bootstrap',
'packages/paperclip-runner/scripts/install-copilot-assets.mjs': 'runner bootstrap',
'packages/paperclip-runner/scripts/materialize-copilot-binary.mjs': 'native materializer',
'packages/paperclip-runner/scripts/copilot-inner-distribution.mjs': 'closure pins',
};
const compute = (input) => cachePlan({ files: Object.keys(input),
read: file => input[file] == null ? null : Buffer.from(input[file]), scope,
packageDirs: ['.', 'server', 'packages/paperclip-runner'],
});
const original = compute(files);
assert.equal(original.cacheable, true);
for (const file of Object.keys(files).filter(file => /^(server|packages\/paperclip-runner)\/scripts\//.test(file))) {
for (const replacement of ['changed', null]) {
assert.notEqual(compute({ ...files, [file]: replacement }).dependencyKey, original.dependencyKey, file);
}
}
assert.equal(cachePlan({ files: ['elsewhere/package.json'],
read: () => Buffer.from(files['server/package.json']), scope, packageDirs: ['elsewhere'],
}).cacheable, false, 'the command is allowed only in its reviewed owners');
const action = readFileSync(new URL('../../actions/setup-ci-dependencies/action.yml', import.meta.url), 'utf8');
const replay = action.split(' - name: Replay workspace install hooks after restore')[1].split(' - name:')[0];
assert.match(replay, /if: steps\.installed\.outputs\.cache-hit == 'true'/);
assert.match(replay, /pnpm -r --if-present run postinstall\s+pnpm run postinstall/);
assert.ok(action.indexOf('Replay workspace install hooks') < action.indexOf('Validate workspace links'));
});
test('cache restores are exact and only a frozen canonical master push can publish entries', () => {
const action = readFileSync(new URL('../../actions/setup-ci-dependencies/action.yml', import.meta.url), 'utf8');
const installed = action.slice(action.indexOf(' - name: Restore installed'), action.indexOf(' - name: Locate pnpm'));
@@ -97,6 +127,15 @@ test('paired test timings remain available when lifecycle hooks disable material
assert.match(step(name), /if: steps.qualification.outputs.cacheable == 'true'/);
}
assert.match(step('Explain disabled materialized reuse'), /cacheable != 'true'/);
assert.doesNotMatch(step('Prepare unchanged baseline'), /if:/);
assert.doesNotMatch(step('Compare complete commands and executed case inventories'), /if:/);
for (const name of ['Prepare unchanged baseline', 'Compare complete commands and executed case inventories']) {
assert.match(step(name), /if: steps\.test_scope\.outputs\.run == 'true'/);
assert.doesNotMatch(step(name), /if:.*cacheable/);
}
const scope = step('Select changed server test work');
assert.match(scope, /git diff --name-only HEAD\^1 HEAD --/);
for (const file of ['scripts/benchmark-ci-work.mjs', 'scripts/run-vitest-stable.mjs',
'server/src/__tests__/setup-supertest.ts', 'server/src/__tests__/workspace-runtime.test.ts',
'server/src/services/workspace-git-snapshot-streaming.test.ts']) assert.ok(scope.includes(file));
assert.doesNotMatch(scope, /scripts\/ci-dependency-cache|\.github\/actions\/setup-ci-dependencies/);
assert.match(step('Exercise restored dependencies for cache-only changes'), /pnpm exec vitest run --project @paperclipai\/shared/);
});
@@ -10,7 +10,8 @@ const base = {
ref: "refs/heads/master", event_name: "push", sha, run_attempt: 1,
};
const expectedJobs = {
"cloud-readiness.yml": [],
"docker.yml": ["source_verification", "publication_verification"],
"cloud-readiness.yml": ["admission", "source_verified"],
"cloud-migrator-artifacts.yml": [],
"release-verify.yml": ["typecheck", "general_tests", "serialized_tests", "runner_workflow_evals", "verify_paperclip_runner", "build"],
"runner-chaos-evals.yml": ["chaos_and_recovery"],
@@ -59,9 +60,9 @@ for (const [file, expectedNames] of Object.entries(expectedJobs)) {
cases.push({ name: "stable release", inputs: { channel: "stable" } });
}
cases.push({ name: "first attempt as a string", github: { run_attempt: "1" }, expected: fleet });
cases.push({ name: "rerun after runner interruption", github: { run_attempt: 2 } });
cases.push({ name: "later rerun", github: { run_attempt: 3 } });
cases.push({ name: "rerun attempt as a string", github: { run_attempt: "2" } });
cases.push({ name: "admitted retry", github: { run_attempt: 2 }, expected: release ? "ubuntu-latest" : fleet });
cases.push({ name: "later retry", github: { run_attempt: 3 }, expected: release ? "ubuntu-latest" : fleet });
cases.push({ name: "retry attempt as a string", github: { run_attempt: "2" }, expected: release ? "ubuntu-latest" : fleet });
cases.push({ name: "missing attempt", github: { run_attempt: undefined } });
for (const { name, github = {}, inputs: overrides = {}, enabled = "true", expected = "ubuntu-latest" } of cases) {
test(`${file}/${job}: ${name}`, () => {
@@ -88,7 +89,7 @@ for (const [file, expectedNames] of Object.entries(expectedJobs)) {
}
test("Cloud readiness bookkeeping never waits for the AWS verification fleet", () => {
test("Cloud source proof keeps source-only gating on protected verification capacity", () => {
const workflow = readFileSync(new URL("../../workflows/cloud-readiness.yml", import.meta.url), "utf8");
const bodies = new Map();
for (const [name, needs] of [
@@ -97,7 +98,7 @@ test("Cloud readiness bookkeeping never waits for the AWS verification fleet", (
const body = workflow.match(new RegExp(`^ ${name}:\\n([\\s\\S]*?)(?=^ [a-z_]+:|(?![\\s\\S]))`, "m"))?.[1];
assert.ok(body, `missing ${name} job`);
bodies.set(name, body);
assert.match(body, /^ runs-on: ubuntu-latest$/m);
assert.match(body, /runs-on:.*paperclip-post-merge-x64/);
assert.doesNotMatch(body, /^ +continue-on-error:|^ +if:.*always\(\)/m);
assert.match(body, /^ if: github.repository == 'paperclipai\/paperclip' && github.ref == 'refs\/heads\/master'$/m);
assert.match(body, /^ +SOURCE_SHA: \$\{\{ github.sha \}\}$/m);
@@ -48,11 +48,27 @@ test("every rust-cache restore in the PR workflow belongs to a guarded reader jo
const keyInputs = [
/uses: Swatinem\/rust-cache@([0-9a-f]{40}) # v[0-9.]+/,
/workspaces: (\$\{\{ steps\.runner_rust_workspace\.outputs\.path \}\} -> target)/,
/shared-key: (release-runner-v2)/,
/shared-key: (release-runner-v2-.+)/,
/cache-workspace-crates: (false)/,
/cache-bin: (false)/,
];
test("virtual workspace profiles and ancestor Cargo configuration partition every dependency cache", () => {
for (const [name, workflow] of [["PR", prWorkflow], ["release", releaseWorkflow]]) {
const keys = [...workflow.matchAll(/^\s+shared-key: (.+)$/gm)].map(([, key]) => key);
assert.ok(keys.length > 0);
for (const key of keys) {
assert.ok(key.includes("hashFiles('packages/paperclip-runner/runner/Cargo.toml'"), name);
for (const directory of ['.', 'packages', 'packages/paperclip-runner', 'packages/paperclip-runner/runner']) {
const prefix = directory === '.' ? '' : `${directory}/`;
for (const file of ['config', 'config.toml']) {
assert.ok(key.includes(`'${prefix}.cargo/${file}'`), `${name}: ${prefix}.cargo/${file}`);
}
}
}
}
});
test("every PR reader restores the Rust cache under the same key the master push writes", () => {
for (const [name, pr] of readers) {
for (const pattern of keyInputs) {
+141
View File
@@ -7,6 +7,9 @@ on:
- .github/actions/setup-ci-dependencies/**
- .github/workflows/ci-work-benchmark.yml
- scripts/ci-dependency-cache.mjs
- .github/workflows/pr-trusted.yml
- .github/workflows/release-verify.yml
- scripts/runner-debug-crate-cache.mjs
- scripts/benchmark-ci-work.mjs
- scripts/run-vitest-stable.mjs
- server/src/__tests__/setup-supertest.ts
@@ -39,6 +42,21 @@ jobs:
fetch-depth: 2
persist-credentials: false
- name: Select changed server test work
id: test_scope
run: |
changed=$(git diff --name-only HEAD^1 HEAD -- \
scripts/benchmark-ci-work.mjs \
scripts/run-vitest-stable.mjs \
server/src/__tests__/setup-supertest.ts \
server/src/__tests__/workspace-runtime.test.ts \
server/src/services/workspace-git-snapshot-streaming.test.ts)
if [ -n "$changed" ]; then
echo "run=true" >> "$GITHUB_OUTPUT"
else
echo "run=false" >> "$GITHUB_OUTPUT"
fi
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
@@ -123,6 +141,7 @@ jobs:
echo "Materialized dependency reuse is disabled by an unmodelled install hook. Retaining cold setup and comparing test commands only." >> "$GITHUB_STEP_SUMMARY"
- name: Prepare unchanged baseline
if: steps.test_scope.outputs.run == 'true'
run: |
base_sha=$(git rev-parse HEAD^1)
git worktree add --detach ../baseline "$base_sha"
@@ -135,12 +154,17 @@ jobs:
pnpm --filter @paperclipai/plugin-sdk ensure-build-deps
- name: Compare complete commands and executed case inventories
if: steps.test_scope.outputs.run == 'true'
env:
BENCHMARK_OUTPUT: ${{ runner.temp }}/ci-work-benchmark
run: |
node scripts/benchmark-ci-work.mjs ../baseline "$BENCHMARK_OUTPUT"
cat "$BENCHMARK_OUTPUT/summary.md" >> "$GITHUB_STEP_SUMMARY"
- name: Exercise restored dependencies for cache-only changes
if: steps.test_scope.outputs.run != 'true' && steps.qualification.outputs.cacheable == 'true'
run: pnpm exec vitest run --project @paperclipai/shared
- name: Retain timings, inventories and failure logs
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
@@ -148,3 +172,120 @@ jobs:
name: ci-work-benchmark
path: ${{ runner.temp }}/ci-work-benchmark
retention-days: 14
rust_prime:
if: >-
contains(github.event.pull_request.labels.*.name, 'profile-ci') &&
(github.event.action != 'labeled' || github.event.label.name == 'profile-ci')
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
ref: ${{ github.sha }}
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: 24
package-manager-cache: false
- name: Select the pinned compiler and workspace path
id: workspace
working-directory: packages/paperclip-runner
run: |
toolchain=$(rustup show active-toolchain | awk '{print $1}')
echo "RUSTUP_TOOLCHAIN=$toolchain" >> "$GITHUB_ENV"
rustup toolchain list | awk '{print $1}' | grep -vx "$toolchain" | xargs -r -n1 rustup toolchain uninstall || true
pinned="$HOME/paperclip-runner-rust"
ln -s "$GITHUB_WORKSPACE/packages/paperclip-runner/runner" "$pinned"
echo "path=$pinned" >> "$GITHUB_OUTPUT"
- name: Qualify the dependency cache
id: dependencies
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
workspaces: ${{ steps.workspace.outputs.path }} -> target
shared-key: qualification-${{ github.run_id }}-${{ github.run_attempt }}-${{ hashFiles('packages/paperclip-runner/runner/Cargo.toml', '.cargo/config', '.cargo/config.toml', 'packages/.cargo/config', 'packages/.cargo/config.toml', 'packages/paperclip-runner/.cargo/config', 'packages/paperclip-runner/.cargo/config.toml', 'packages/paperclip-runner/runner/.cargo/config', 'packages/paperclip-runner/runner/.cargo/config.toml') }}
cache-workspace-crates: false
cache-bin: false
save-if: 'true'
- name: Build the exact debug units
working-directory: packages/paperclip-runner
run: cargo build --manifest-path runner/Cargo.toml --locked --workspace --bins
- name: Compute workspace artifact identity
id: debug
run: node scripts/runner-debug-crate-cache.mjs plan
- name: Save workspace artifacts in this PR only
id: artifacts
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9
with:
key: qualification-${{ github.run_id }}-${{ github.run_attempt }}-${{ steps.debug.outputs.key }}
path: ${{ steps.debug.outputs.paths }}
rust_restore:
needs: rust_prime
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
ref: ${{ github.sha }}
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: 24
package-manager-cache: false
- name: Select the pinned compiler and workspace path
id: workspace
working-directory: packages/paperclip-runner
run: |
toolchain=$(rustup show active-toolchain | awk '{print $1}')
echo "RUSTUP_TOOLCHAIN=$toolchain" >> "$GITHUB_ENV"
rustup toolchain list | awk '{print $1}' | grep -vx "$toolchain" | xargs -r -n1 rustup toolchain uninstall || true
pinned="$HOME/paperclip-runner-rust"
ln -s "$GITHUB_WORKSPACE/packages/paperclip-runner/runner" "$pinned"
echo "path=$pinned" >> "$GITHUB_OUTPUT"
- name: Qualify the dependency cache
id: dependencies
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
workspaces: ${{ steps.workspace.outputs.path }} -> target
shared-key: qualification-${{ github.run_id }}-${{ github.run_attempt }}-${{ hashFiles('packages/paperclip-runner/runner/Cargo.toml', '.cargo/config', '.cargo/config.toml', 'packages/.cargo/config', 'packages/.cargo/config.toml', 'packages/paperclip-runner/.cargo/config', 'packages/paperclip-runner/.cargo/config.toml', 'packages/paperclip-runner/runner/.cargo/config', 'packages/paperclip-runner/runner/.cargo/config.toml') }}
cache-workspace-crates: false
cache-bin: false
save-if: 'false'
- name: Compute workspace artifact identity
id: debug
run: node scripts/runner-debug-crate-cache.mjs plan
- name: Restore workspace artifacts in this PR only
id: artifacts
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9
with:
key: qualification-${{ github.run_id }}-${{ github.run_attempt }}-${{ steps.debug.outputs.key }}
path: ${{ steps.debug.outputs.paths }}
- name: Verify fresh-host cache reuse
env:
DEPENDENCIES_HIT: ${{ steps.dependencies.outputs.cache-hit }}
ARTIFACTS_HIT: ${{ steps.artifacts.outputs.cache-hit }}
run: |
test "$DEPENDENCIES_HIT" = true
test "$ARTIFACTS_HIT" = true
node scripts/runner-debug-crate-cache.mjs mark-current
cd packages/paperclip-runner
started=$(date +%s)
cargo build --manifest-path runner/Cargo.toml --locked --workspace --bins --message-format=json > "$RUNNER_TEMP/rust-restored-build.jsonl"
echo "Restored Rust build: $(($(date +%s) - started)) seconds." >> "$GITHUB_STEP_SUMMARY"
node --input-type=module <<'NODE'
import { readFileSync } from 'node:fs';
const artifacts = readFileSync(`${process.env.RUNNER_TEMP}/rust-restored-build.jsonl`, 'utf8')
.trim().split('\n').map(line => JSON.parse(line)).filter(row => row.reason === 'compiler-artifact');
if (!artifacts.length || artifacts.some(row => !row.fresh)) {
throw new Error(`Cache hit rebuilt units: ${JSON.stringify(artifacts.filter(row => !row.fresh).map(row => row.package_id))}`);
}
console.log(`All ${artifacts.length} Cargo units were fresh on a new host.`);
NODE
- name: Retain Cargo reuse evidence
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: rust-cache-qualification
path: ${{ runner.temp }}/rust-restored-build.jsonl
retention-days: 14
+27 -3
View File
@@ -12,9 +12,33 @@ concurrency:
cancel-in-progress: false
jobs:
build:
admission:
name: Cloud image candidate admission v1
if: github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master'
runs-on: ubuntu-latest
runs-on: ${{ vars.CORE_PUBLICATION_RUNNER || 'cloud-release-32' }}
timeout-minutes: 5
permissions:
contents: read
actions: read
outputs:
admitted: ${{ steps.admission.outputs.admitted }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
persist-credentials: false
sparse-checkout: scripts
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: 24
- id: admission
env:
GITHUB_TOKEN: ${{ github.token }}
run: node scripts/cloud-release-admission.mjs image-only
build:
needs: admission
if: github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && needs.admission.outputs.admitted == 'true'
runs-on: ${{ vars.CORE_PUBLICATION_RUNNER || 'cloud-release-32' }}
timeout-minutes: 10
permissions:
contents: read
@@ -52,7 +76,7 @@ jobs:
publish:
needs: build
if: github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master'
runs-on: ubuntu-latest
runs-on: ${{ vars.CORE_PUBLICATION_RUNNER || 'cloud-release-32' }}
timeout-minutes: 5
permissions:
contents: read
+29 -4
View File
@@ -8,14 +8,38 @@ on:
permissions: {}
# Source verification must start outside the full npm release's queue.
# Source verification stays independent of npm publication and image builds.
concurrency:
group: cloud-readiness-${{ github.sha }}
cancel-in-progress: false
jobs:
admission:
name: Cloud candidate admission v1
if: github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master'
runs-on: ${{ vars.AWS_POST_MERGE_CI_ENABLED == 'true' && github.run_attempt >= 1 && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && 'runs-on/fleet=paperclip-post-merge-x64/env=public-ci' || 'ubuntu-latest' }}
timeout-minutes: 5
permissions:
contents: read
actions: read
outputs:
admitted: ${{ steps.admission.outputs.admitted }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
persist-credentials: false
sparse-checkout: scripts
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: 24
- id: admission
env:
GITHUB_TOKEN: ${{ github.token }}
run: node scripts/cloud-release-admission.mjs
verify:
if: github.repository == 'paperclipai/paperclip' && github.ref == 'refs/heads/master'
needs: admission
if: github.repository == 'paperclipai/paperclip' && github.ref == 'refs/heads/master' && needs.admission.outputs.admitted == 'true'
permissions:
contents: read
uses: ./.github/workflows/release-verify.yml
@@ -29,8 +53,9 @@ jobs:
name: Cloud source verified v1
needs: [verify]
if: github.repository == 'paperclipai/paperclip' && github.ref == 'refs/heads/master'
# Bookkeeping must not wait for the AWS builders it observes.
runs-on: ubuntu-latest
# Keep the final proof off the shared hosted queue too. The protected
# fleet has a separate job ceiling and launches Spot workers on demand.
runs-on: ${{ vars.AWS_POST_MERGE_CI_ENABLED == 'true' && github.run_attempt >= 1 && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && 'runs-on/fleet=paperclip-post-merge-x64/env=public-ci' || 'ubuntu-latest' }}
timeout-minutes: 5
permissions:
contents: read
+71 -10
View File
@@ -33,6 +33,29 @@ concurrency:
cancel-in-progress: false
jobs:
source_verification:
name: Watch exact-source verification
if: github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master'
runs-on: ${{ vars.AWS_POST_MERGE_CI_ENABLED == 'true' && github.run_attempt >= 1 && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && 'runs-on/fleet=paperclip-post-merge-x64/env=public-ci' || 'ubuntu-latest' }}
timeout-minutes: 44
permissions:
contents: read
actions: write
outputs:
verified: ${{ steps.watch.outputs.verified }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: 24
- name: Stop images when source checks fail and report runner waits
id: watch
env:
GITHUB_TOKEN: ${{ github.token }}
run: node scripts/watch-release-source.mjs
reuse_canary_image:
if: github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && startsWith(github.ref, 'refs/tags/canary/v')
runs-on: ubuntu-latest
@@ -100,11 +123,10 @@ jobs:
- platform: linux/arm64
runner: ubuntu-24.04-arm
arch: arm64
runs-on: ${{ matrix.runner }}
# Native builds land well inside this; it is a backstop, not a budget.
# (The interim fix while this PR landed raised the single QEMU job's cap
# to 120 minutes; native per-arch legs make that headroom unnecessary.)
timeout-minutes: 60
runs-on: ${{ vars.CORE_RELEASE_AWS_ENABLED == 'true' && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && format('runs-on/fleet=paperclip-core-image-{0}/env=public-ci', matrix.arch == 'amd64' && 'x64' || 'arm64') || matrix.runner }}
# Stop before the AWS worker's 45-minute maximum lifetime. Hosted tag
# builds retain their existing limit.
timeout-minutes: ${{ vars.CORE_RELEASE_AWS_ENABLED == 'true' && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && 40 || 60 }}
permissions:
contents: read
packages: write
@@ -115,6 +137,9 @@ jobs:
# Full history and tags so `git describe` below can compute the
# release version to stamp into the image.
fetch-depth: 0
# Keep the version's full commit graph without downloading blobs
# from every historical revision of this large repository.
filter: blob:none
# `.git` is dockerignored, so a running image cannot derive its own
# version and otherwise reports the source package.json placeholder in
@@ -183,6 +208,7 @@ jobs:
echo "Using refreshed pnpm-lock.yaml in the Docker build context."
- name: Free runner disk
if: ${{ !(vars.CORE_RELEASE_AWS_ENABLED == 'true' && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master') }}
run: |
set -euo pipefail
echo "Disk before cleanup:"
@@ -304,16 +330,40 @@ jobs:
if-no-files-found: error
retention-days: 1
# Wait for proof on ephemeral verification capacity. Publication runners do
# only registry/signing work. Each Spot observer stays below its 45m lifetime.
publication_verification:
name: Require exact-source verification
if: ${{ !cancelled() && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && needs.build-and-push.result == 'success' && needs.source_verification.result == 'success' && needs.source_verification.outputs.verified != 'true' }}
needs: [build-and-push, source_verification]
runs-on: ${{ vars.AWS_POST_MERGE_CI_ENABLED == 'true' && github.run_attempt >= 1 && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && 'runs-on/fleet=paperclip-post-merge-x64/env=public-ci' || 'ubuntu-latest' }}
timeout-minutes: 44
permissions:
contents: read
actions: write # restart only exact-source producers deferred before admission
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: 24
- name: Require source proof and migrator admission before publication
env:
GITHUB_TOKEN: ${{ github.token }}
run: node scripts/watch-release-source.mjs --require-proof
# Names the per-architecture digests as one manifest list under the real
# tags. Nothing is publicly tagged until this runs, so a half-published
# multi-arch image is not a state anything can pull.
merge-and-push:
if: ${{ !cancelled() && needs.build-and-push.result == 'success' }}
needs: build-and-push
runs-on: ubuntu-latest
if: ${{ !cancelled() && needs.build-and-push.result == 'success' && (needs.source_verification.result == 'success' || needs.source_verification.result == 'skipped') && (needs.publication_verification.result == 'success' || (needs.publication_verification.result == 'skipped' && (github.repository != 'paperclipai/paperclip' || github.repository_id != '1170821064' || github.ref != 'refs/heads/master' || needs.source_verification.outputs.verified == 'true'))) }}
needs: [build-and-push, source_verification, publication_verification]
runs-on: ${{ github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && (vars.CORE_PUBLICATION_RUNNER || 'cloud-release-32') || 'ubuntu-latest' }}
timeout-minutes: 20
permissions:
contents: read
actions: read
packages: write
id-token: write
attestations: write
@@ -322,8 +372,6 @@ jobs:
# script the verification step pipes in.
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
- name: Download digests
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
@@ -375,6 +423,19 @@ jobs:
io.github.paperclipai.schema.last-migration=${{ steps.schema.outputs.last }}
io.github.paperclipai.schema.migration-count=${{ steps.schema.outputs.count }}
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: 24
# Revalidate the latest attempt after runner assignment without waiting
# here. A concurrent source retry must invalidate the earlier proof.
- name: Revalidate exact-source proof before publication
if: github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master'
env:
GITHUB_TOKEN: ${{ github.token }}
run: node scripts/watch-release-source.mjs --check-proof
- name: Create manifest list and push
working-directory: /tmp/digests
run: |
+4 -4
View File
@@ -370,7 +370,7 @@ jobs:
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
workspaces: ${{ steps.runner_rust_workspace.outputs.path }} -> target
shared-key: release-runner-v2
shared-key: release-runner-v2-${{ hashFiles('packages/paperclip-runner/runner/Cargo.toml', '.cargo/config', '.cargo/config.toml', 'packages/.cargo/config', 'packages/.cargo/config.toml', 'packages/paperclip-runner/.cargo/config', 'packages/paperclip-runner/.cargo/config.toml', 'packages/paperclip-runner/runner/.cargo/config', 'packages/paperclip-runner/runner/.cargo/config.toml') }}
# Mirror the master writer: these also feed the cache key.
cache-workspace-crates: false
cache-bin: false
@@ -784,7 +784,7 @@ jobs:
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
workspaces: ${{ steps.runner_rust_workspace.outputs.path }} -> target
shared-key: release-runner-v2
shared-key: release-runner-v2-${{ hashFiles('packages/paperclip-runner/runner/Cargo.toml', '.cargo/config', '.cargo/config.toml', 'packages/.cargo/config', 'packages/.cargo/config.toml', 'packages/paperclip-runner/.cargo/config', 'packages/paperclip-runner/.cargo/config.toml', 'packages/paperclip-runner/runner/.cargo/config', 'packages/paperclip-runner/runner/.cargo/config.toml') }}
# Mirror the master writer: these also feed the cache key.
cache-workspace-crates: false
cache-bin: false
@@ -1035,7 +1035,7 @@ jobs:
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
workspaces: ${{ steps.runner_rust_workspace.outputs.path }} -> target
shared-key: release-runner-v2
shared-key: release-runner-v2-${{ hashFiles('packages/paperclip-runner/runner/Cargo.toml', '.cargo/config', '.cargo/config.toml', 'packages/.cargo/config', 'packages/.cargo/config.toml', 'packages/paperclip-runner/.cargo/config', 'packages/paperclip-runner/.cargo/config.toml', 'packages/paperclip-runner/runner/.cargo/config', 'packages/paperclip-runner/runner/.cargo/config.toml') }}
# Mirror the master writer: these also feed the cache key.
cache-workspace-crates: false
cache-bin: false
@@ -1376,7 +1376,7 @@ jobs:
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
workspaces: ${{ steps.runner_rust_workspace.outputs.path }} -> target
shared-key: release-runner-v2
shared-key: release-runner-v2-${{ hashFiles('packages/paperclip-runner/runner/Cargo.toml', '.cargo/config', '.cargo/config.toml', 'packages/.cargo/config', 'packages/.cargo/config.toml', 'packages/paperclip-runner/.cargo/config', 'packages/paperclip-runner/.cargo/config.toml', 'packages/paperclip-runner/runner/.cargo/config', 'packages/paperclip-runner/runner/.cargo/config.toml') }}
# Mirror the master writer: these also feed the cache key.
cache-workspace-crates: false
cache-bin: false
+9 -9
View File
@@ -34,7 +34,7 @@ jobs:
typecheck:
if: ${{ !inputs.runner_binaries_only }}
name: Typecheck
runs-on: ${{ vars.AWS_POST_MERGE_CI_ENABLED == 'true' && github.run_attempt == 1 && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.sha != '' && inputs.ref == github.sha && 'runs-on/fleet=paperclip-post-merge-x64/env=public-ci' || 'ubuntu-latest' }}
runs-on: ${{ vars.AWS_POST_MERGE_CI_ENABLED == 'true' && github.run_attempt >= 1 && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.sha != '' && inputs.ref == github.sha && 'runs-on/fleet=paperclip-post-merge-x64/env=public-ci' || 'ubuntu-latest' }}
timeout-minutes: 20
permissions:
contents: read
@@ -92,7 +92,7 @@ jobs:
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
workspaces: ${{ steps.runner_rust_workspace.outputs.path }} -> target
shared-key: release-typecheck-v2
shared-key: release-typecheck-v2-${{ hashFiles('packages/paperclip-runner/runner/Cargo.toml', '.cargo/config', '.cargo/config.toml', 'packages/.cargo/config', 'packages/.cargo/config.toml', 'packages/paperclip-runner/.cargo/config', 'packages/paperclip-runner/.cargo/config.toml', 'packages/paperclip-runner/runner/.cargo/config', 'packages/paperclip-runner/runner/.cargo/config.toml') }}
# Rebuild workspace code and rerun every check. Cache only compiled
# dependencies; never restore installed executables from cargo/bin.
cache-workspace-crates: false
@@ -120,7 +120,7 @@ jobs:
general_tests:
if: ${{ !inputs.runner_binaries_only }}
name: General tests (${{ matrix.group_label }})
runs-on: ${{ vars.AWS_POST_MERGE_CI_ENABLED == 'true' && github.run_attempt == 1 && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.sha != '' && inputs.ref == github.sha && 'runs-on/fleet=paperclip-post-merge-x64/env=public-ci' || 'ubuntu-latest' }}
runs-on: ${{ vars.AWS_POST_MERGE_CI_ENABLED == 'true' && github.run_attempt >= 1 && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.sha != '' && inputs.ref == github.sha && 'runs-on/fleet=paperclip-post-merge-x64/env=public-ci' || 'ubuntu-latest' }}
timeout-minutes: 20
permissions:
contents: read
@@ -232,7 +232,7 @@ jobs:
serialized_tests:
if: ${{ !inputs.runner_binaries_only }}
name: Serialized tests (${{ matrix.shard_label }})
runs-on: ${{ vars.AWS_POST_MERGE_CI_ENABLED == 'true' && github.run_attempt == 1 && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.sha != '' && inputs.ref == github.sha && 'runs-on/fleet=paperclip-post-merge-x64/env=public-ci' || 'ubuntu-latest' }}
runs-on: ${{ vars.AWS_POST_MERGE_CI_ENABLED == 'true' && github.run_attempt >= 1 && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.sha != '' && inputs.ref == github.sha && 'runs-on/fleet=paperclip-post-merge-x64/env=public-ci' || 'ubuntu-latest' }}
timeout-minutes: 20
permissions:
contents: read
@@ -282,7 +282,7 @@ jobs:
runner_workflow_evals:
if: ${{ !inputs.runner_binaries_only }}
name: Runner workflow eval scorer contract
runs-on: ${{ vars.AWS_POST_MERGE_CI_ENABLED == 'true' && github.run_attempt == 1 && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.sha != '' && inputs.ref == github.sha && 'runs-on/fleet=paperclip-post-merge-x64/env=public-ci' || 'ubuntu-latest' }}
runs-on: ${{ vars.AWS_POST_MERGE_CI_ENABLED == 'true' && github.run_attempt >= 1 && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.sha != '' && inputs.ref == github.sha && 'runs-on/fleet=paperclip-post-merge-x64/env=public-ci' || 'ubuntu-latest' }}
timeout-minutes: 10
permissions:
contents: read
@@ -313,7 +313,7 @@ jobs:
verify_paperclip_runner:
if: ${{ !inputs.runner_binaries_only }}
name: Verify Paperclip Runner (${{ matrix.lane }})
runs-on: ${{ vars.AWS_POST_MERGE_CI_ENABLED == 'true' && github.run_attempt == 1 && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.sha != '' && inputs.ref == github.sha && 'runs-on/fleet=paperclip-post-merge-x64/env=public-ci' || 'ubuntu-latest' }}
runs-on: ${{ vars.AWS_POST_MERGE_CI_ENABLED == 'true' && github.run_attempt >= 1 && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.sha != '' && inputs.ref == github.sha && 'runs-on/fleet=paperclip-post-merge-x64/env=public-ci' || 'ubuntu-latest' }}
timeout-minutes: 20
permissions:
contents: read
@@ -398,7 +398,7 @@ jobs:
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
workspaces: ${{ steps.runner_rust_workspace.outputs.path }} -> target
shared-key: release-runner-v2
shared-key: release-runner-v2-${{ hashFiles('packages/paperclip-runner/runner/Cargo.toml', '.cargo/config', '.cargo/config.toml', 'packages/.cargo/config', 'packages/.cargo/config.toml', 'packages/paperclip-runner/.cargo/config', 'packages/paperclip-runner/.cargo/config.toml', 'packages/paperclip-runner/runner/.cargo/config', 'packages/paperclip-runner/runner/.cargo/config.toml') }}
# Rebuild workspace code and rerun every check. Cache only compiled
# dependencies; never restore installed executables from cargo/bin.
cache-workspace-crates: false
@@ -523,7 +523,7 @@ jobs:
build:
if: ${{ !inputs.runner_binaries_only }}
name: Build
runs-on: ${{ vars.AWS_POST_MERGE_CI_ENABLED == 'true' && github.run_attempt == 1 && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.sha != '' && inputs.ref == github.sha && 'runs-on/fleet=paperclip-post-merge-x64/env=public-ci' || 'ubuntu-latest' }}
runs-on: ${{ vars.AWS_POST_MERGE_CI_ENABLED == 'true' && github.run_attempt >= 1 && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.sha != '' && inputs.ref == github.sha && 'runs-on/fleet=paperclip-post-merge-x64/env=public-ci' || 'ubuntu-latest' }}
timeout-minutes: 20
permissions:
contents: read
@@ -680,7 +680,7 @@ jobs:
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6
with:
workspaces: ${{ steps.native_rust_workspace.outputs.path }} -> target
shared-key: release-native-v2-${{ matrix.host }}-${{ matrix.target }}-${{ hashFiles('packages/paperclip-runner/protocol/**', 'packages/paperclip-runner/src/**', 'packages/paperclip-runner/*.json') }}
shared-key: release-native-v2-${{ hashFiles('packages/paperclip-runner/runner/Cargo.toml', '.cargo/config', '.cargo/config.toml', 'packages/.cargo/config', 'packages/.cargo/config.toml', 'packages/paperclip-runner/.cargo/config', 'packages/paperclip-runner/.cargo/config.toml', 'packages/paperclip-runner/runner/.cargo/config', 'packages/paperclip-runner/runner/.cargo/config.toml') }}-${{ matrix.host }}-${{ matrix.target }}-${{ hashFiles('packages/paperclip-runner/protocol/**', 'packages/paperclip-runner/src/**', 'packages/paperclip-runner/*.json') }}
cache-workspace-crates: false
cache-bin: false
cache-on-failure: false
+3 -3
View File
@@ -320,12 +320,12 @@ jobs:
verify_canary:
name: Reuse exact-source verification
if: github.repository == 'paperclipai/paperclip' && github.event_name == 'push' && github.ref == 'refs/heads/master'
if: github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.event_name == 'push' && github.ref == 'refs/heads/master'
runs-on: ubuntu-latest
timeout-minutes: 50
timeout-minutes: 70
permissions:
contents: read
actions: read
actions: write # restart only an exact-source push deferred before admission
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
+2 -2
View File
@@ -20,8 +20,8 @@ concurrency:
jobs:
chaos_and_recovery:
name: Restart, replay, trace, and recovery faults
runs-on: ${{ vars.AWS_POST_MERGE_CI_ENABLED == 'true' && github.run_attempt == 1 && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.sha != '' && inputs.ref == github.sha && 'runs-on/fleet=paperclip-post-merge-x64/env=public-ci' || 'ubuntu-latest' }}
timeout-minutes: ${{ vars.AWS_POST_MERGE_CI_ENABLED == 'true' && github.run_attempt == 1 && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.sha != '' && inputs.ref == github.sha && 40 || 45 }}
runs-on: ${{ vars.AWS_POST_MERGE_CI_ENABLED == 'true' && github.run_attempt >= 1 && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.sha != '' && inputs.ref == github.sha && 'runs-on/fleet=paperclip-post-merge-x64/env=public-ci' || 'ubuntu-latest' }}
timeout-minutes: ${{ vars.AWS_POST_MERGE_CI_ENABLED == 'true' && github.run_attempt >= 1 && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.sha != '' && inputs.ref == github.sha && 40 || 45 }}
permissions:
contents: read
+17 -1
View File
@@ -1095,6 +1095,22 @@ verifying Vite/HMR behavior.
For normal issue work, start with the smallest targeted check that proves the change. Reserve repo-wide typecheck/build/test runs for PR-ready handoff or changes broad enough that narrow checks do not cover the risk.
The Runner transport suite keeps attachment recovery, warm attachment, live
adoption, and suspension in separate test files. Vitest can schedule these
independent fixtures on its existing file workers; cases within each file stay
serial. The package sequencer starts the ten files measured above 50 seconds in
the November 4, 2026 AWS run first, ordered by approximate duration. This includes
transport deadlines, settlement, provider launch, recovery, and streaming waits,
so fresh CI runners do not have to infer duration from file size. Package CI uses
up to four workers, bounded by available CPUs; local runs retain Vitest defaults.
Vitest's file selection and shard membership are unchanged. Real lease/deadline
waits and assertions are unchanged. Run the complete
family, including the separate deadline and settlement regressions, with:
```sh
pnpm --filter @paperclipai/paperclip-runner exec vitest run runnerd-codex-transport
```
#### Embedded Postgres template cache
To run the suite with one owned PostgreSQL server and a separate disposable
@@ -3187,7 +3203,7 @@ Run the credential-free real-process restart suite with:
```sh
pnpm --filter @paperclipai/paperclip-runner build:runner-binaries
pnpm exec vitest run server/src/services/native-runtime/native-runner-restart-recovery.integration.test.ts
pnpm --filter @paperclipai/paperclip-runner exec vitest run src/live/runnerd-codex-transport.test.ts -t 'adopts a live runner'
pnpm --filter @paperclipai/paperclip-runner exec vitest run src/live/runnerd-codex-transport-adoption.test.ts -t 'adopts a live runner'
```
The suite uses isolated PostgreSQL state, isolated `PAPERCLIP_HOME` roots, real
+34 -4
View File
@@ -368,7 +368,14 @@ readiness can succeed. A failed lane does not cancel the other lane.
Both lanes restore Cargo dependencies with the pinned Rust Cache action. The
compiler comes from the Runner package's `rust-toolchain.toml` before the action
computes its key. Compiler and Cargo metadata changes select a new cache. The
`release-runner-v2` shared key avoids separate copies for these lanes.
`release-runner-v2` shared key avoids separate copies for these lanes. Its suffix
hashes the virtual workspace's `runner/Cargo.toml` and ancestor Cargo configuration.
Rust Cache discovers member manifests through Cargo metadata, which omits this
virtual manifest. Without the suffix, changing `[profile.dev]` can restore an
exact but obsolete dependency cache on every run. GitHub caches are immutable,
so the successful rebuild cannot replace that old entry. The profile/configuration
hash also partitions fallback restores. Every reader and writer uses the same
inputs, including the separate typecheck and cross-target caches.
Only the Rust lane saves this cache. After verification it also runs `build:rust`
to warm the debug dependencies used by the protocol lane; its own tests already
warm release dependencies. The cache writer is shorter than the protocol lane.
@@ -385,13 +392,36 @@ paths and the PR lanes can restore master's entry. The guard tests under
Rust cache step in both workflows.
Workspace crates and installed Cargo binaries are excluded from this entry.
Every run runs all assigned checks, including on a cache hit. Only an
own-repository master-push run verifying that push's exact SHA can restore the
cache, and only a successful Rust lane saves it. PR, tag, and manual candidate
Every run runs all assigned checks, including on a cache hit. In release
verification, only an own-repository master-push run verifying that push's exact
SHA can restore the cache, and only a successful Rust lane saves it. The PR
workflow restores master's cache read only. Tag and manual candidate release
verification compile without this cache. A miss or eviction costs compilation
time but does not change the checks. To discard old dependency caches, increment
the shared-key version and let the next successful master verification warm it.
### Installed JavaScript dependencies
`setup-ci-dependencies` can restore exact `node_modules` trees and separately
certified shared/SDK build outputs. `scripts/ci-dependency-cache.mjs` hashes
manifests, resolution, patches, install scripts, and the runtime/checkout layout.
The root workspace-link hook and the server/Runner Copilot asset hooks are known
postinstall commands. An exact dependency restore replays these hooks before
validating workspace links and build outputs. Copilot defers installation in an
unbuilt source checkout; its normal build installs and verifies the assets.
Provider assets are not included in the dependency cache. Any unrecognized
workspace lifecycle hook still disables materialized reuse. Only a frozen install
from the canonical master push can publish the shared entries.
For CI cache changes, the `profile-ci` label runs the paired setup benchmark
and exercises restored dependencies with the shared-package tests. The longer
server test comparison runs when its measured scripts or fixtures change;
cache-only changes do not require unchanged test commands to run faster.
It also builds Rust units on one GitHub-hosted runner and restores the dependency
and workspace caches on a second runner with a fresh checkout. Both restores
must be exact, and Cargo must report every unit as fresh. These qualification
caches are scoped to the PR and run; they cannot populate master's caches.
### Runner release crate cache
The dependency entry leaves the Runner's own crate out, and a fresh checkout
+73 -15
View File
@@ -1,30 +1,42 @@
# Cloud build readiness
The `Cloud readiness` workflow starts for every master push and retains the
versioned `Cloud source verified v1` job. It calls the full `Release Verify`
workflow for that exact commit, including typecheck, builds, general and
serialized tests, and Runner verification. The source proof depends on every
source check and fails closed if verification fails, is cancelled, or is skipped.
The `Cloud readiness` workflow starts a small admission check for each master
push. Full verification starts only when the exact source has a running master
Docker or automatic npm release workflow. Superseded pending sources avoid the
full test matrix. The migrator producer similarly waits for image admission.
A selected consumer restarts a previously deferred push run, preserving its
original source and push identity. This restarts only a successful admission
job followed entirely by skipped jobs. A failed check, cancelled job, incomplete
listing, or identity mismatch cannot trigger this automatic retry. Explicit
manual runs and whole-workflow retries request full verification directly.
Admitted source runs retain the versioned `Cloud source verified v1` job. They
call the full `Release Verify` workflow for that exact commit, including
typecheck, builds, general and serialized tests, and Runner verification. The
proof depends on every source check and fails closed if verification fails, is
cancelled, or is skipped. Consumers accept only the latest attempt's proof.
The recurring public `-cloud` publisher and its `Cloud deployable v1` gate are
retired. The workflow no longer builds a legacy image or waits for one.
Keep its filename and source-proof job name stable: npm canary publication and
downstream image composers consume that exact contract.
Standard images still publish independently through `docker.yml`. The
Standard images build in parallel through `docker.yml`. Master image publication
waits for the exact-source proof; a failed source check cancels its image build
without waiting for the remaining test shards. The
`cloud-migrator-artifacts.yml` workflow still publishes signed exact-source
migrators independently. A downstream composer must verify those artifacts,
build and test its own image, and record separate deployment readiness.
Verification runs outside the full npm release's concurrency group. Different
commits have independent groups. The npm canary release reuses the source proof
Verification runs outside the full npm release's concurrency group. Each source retains its independent verification run. The npm canary release reuses the source proof
for its exact master push instead of starting another `Release Verify` run.
Stable releases and candidate-branch betas still run full verification.
The canary consumer requires the expected workflow ID and path, upstream source
repository, master push event, full SHA, and a successful job in the latest run
attempt. It checks the run again after reading the jobs to reject a concurrent
rerun. Missing proof waits for up to 45 minutes; failed, skipped, cancelled,
rerun. Missing proof waits for up to 60 minutes; failed, skipped, cancelled,
ambiguous, or mismatched proof cannot authorize publication. API failures fail
closed. If a source check fails, fix it and rerun Cloud readiness before retrying
the release. Use **Re-run all jobs** when a later attempt did not rerun the source
@@ -120,15 +132,16 @@ excluded or sleeping targets, retries, and failures with the fleet result. Recor
runner queue conditions and cache state; one warm or cold run is a sample, not a
latency guarantee.
## Reserved AWS verification capacity
## Dedicated AWS verification capacity
`AWS_POST_MERGE_CI_ENABLED=true` routes cloud source verification and
exact-master migrator preparation to the
`paperclip-post-merge` runner group. The separate Fleet label is
`runs-on/fleet=paperclip-post-merge-x64/env=public-ci`. Its 36 reserved slots use
the same four-vCPU, 16-GiB machines as approved PR jobs. PR capacity is reduced
to 64; the separately provisioned image capacity is unchanged by this retirement.
This keeps PR bursts from consuming every post-merge verification slot.
`runs-on/fleet=paperclip-post-merge-x64/env=public-ci`. The protected fleet uses
separate 8-vCPU, 32-GiB Spot workers that launch only when jobs arrive. Its
job ceiling and public PR capacity are managed independently. There are no
hot or stopped pools. Quota ceilings do not reserve hardware; measure cold
launch and runner assignment times as well as job duration.
Every selector checks the canonical repository name and ID, master ref, and a
push or manual event. Reusable verification also requires `inputs.ref` to equal
@@ -142,7 +155,8 @@ its trusted-publisher identity.
Before enabling the switch, deploy the separate Fleet and restrict its GitHub
runner group to repository ID `1170821064` and these workflows at
`refs/heads/master`: `cloud-readiness.yml`,
`release-verify.yml`, `runner-chaos-evals.yml`, and `release.yml`. Do not authorize
`release-verify.yml`, `runner-chaos-evals.yml`, `release.yml`, and `docker.yml`.
The Docker workflow uses this group for its exact-source monitor and proof wait. Do not authorize
PR-controlled workflow versions. The direct migrator producer always uses
GitHub-hosted runners and needs no AWS runner-group authorization. PR placement retains its independent pinned
workflow and six-account author/actor allowlist.
@@ -158,6 +172,43 @@ route a public image job after this retirement. This source change does not
delete runner groups, Fleets, credentials, registry images, or cache tags. Review
shared infrastructure ownership separately before removing those resources.
## Protected standard-image builders
`CORE_RELEASE_AWS_ENABLED=true` routes canonical master image jobs to native
`paperclip-core-image-x64` and `paperclip-core-image-arm64` fleets. Each has two
32-vCPU / 128-GiB Spot slots, for four large image jobs in total. Workers
launch for jobs and terminate afterward; no idle pool is maintained. The `paperclip-core-release` runner group permits only
Core's `docker.yml@refs/heads/master`. PRs, branches, and tag releases keep their
existing GitHub-hosted placement. Disable the switch and rerun the whole image
workflow to restore hosted placement.
Image checkout retains the complete commit graph for version stamping but filters
out historical blobs. The manifest job needs only a shallow source checkout.
Prepared AWS image disks skip the standard hosted runner's disk cleanup. Native
architecture builds, digest publication, the orphan-reaping smoke test, and
attestation remain required. The source monitor has Actions write permission to cancel its own canonical
master Docker run and failed exact-source verification siblings. It never
automatically retries a failed source check. Its guarded retry permission can
restart only an exact-source producer deferred before full admission. It checks run, attempt, SHA,
repository, ref, and workflow identity before cancellation. Its bounded
observation window does not cancel healthy pending checks. A successful observer
records proof for publication immediately. If healthy checks remain pending after
40 minutes, a separate required proof job waits for up to another 40 minutes on
the verification fleet. Both jobs fit within the Spot worker lifetime. Neither
wait occupies the four-slot publication pool. The hosted manifest job requires
successful source proof and admitted migrator work before it starts; a queued
migrator job alone is not evidence of admission. It retains its 20-minute budget
for manifest verification and signing. It revalidates the latest source attempt
once before publishing; changed or missing proof fails immediately without
starting another wait on the publication runner.
Image jobs retain
their existing package permissions.
The monitor emits an Actions warning and step summary when an image or source job waits
more than 60 seconds for a runner. This measures runner assignment, not image
execution. Compare automatic merge-to-ready and merge-to-healthy-target timings;
a manual dispatch alone does not prove deployment latency.
### Typecheck Rust dependency cache
Source verification's typecheck job builds the native Runner binary through the
@@ -236,3 +287,10 @@ fi
A subsequent master install can populate the missing entry. Check the saved
archive size and package reuse in install logs; a cache hit alone does not prove
that the entry contains dependencies.
Image manifest verification and attestation, plus exact-source migrator build
and publication, use the protected four-slot GitHub-hosted `cloud-release-32`
pool. `CORE_PUBLICATION_RUNNER=ubuntu-latest` restores standard hosted placement.
The signing jobs remain GitHub-hosted because downstream provenance explicitly
requires that certificate claim. Their master-only group also serves the private
Cloud publisher and harness deployment; it excludes PRs and branch benchmarks.
+6
View File
@@ -77,6 +77,12 @@ repeat an existing instance approval.
## Identity resolution
Fresh setup does not reuse an archived personal connection found by name and
provider. It creates a separate application and connection with an available
name, such as `GitHub (2)`. The archived identity, credentials, grants, and
history stay unchanged. An explicit reconnect or application-targeted setup
continues the existing identity and requires its personal owner.
Every MCP call, `gh` invocation, native Git operation, checkout, health check,
and webhook binding uses the same order:
+10 -2
View File
@@ -1,5 +1,7 @@
# Paperclip evaluation guide
The explicit deployment-targeted [saved agent instructions journey](../tests/runner-e2e/product-journeys/AGENT-SETTINGS.md) checks an owned agent’s ordinary UI save, persisted instruction bytes, one current-run output, and restoration. The task request omits its instruction-only reference. It reuses the existing Product E2E target and report path; local controls do not establish a staging pass.
The explicit-only [verified API artifact transfer](../tests/runner-e2e/README.md#verified-api-artifact-transfer)
cell checks a native Codex agent on Daytona installing an authorized saved API
response and delivering unchanged bytes. The oracle binds the source capture,
@@ -111,14 +113,20 @@ reply with the late requirement, and persistence after reload. Its attached and
managed targets share the same story and permit one steered run or one normal
successor. Registration and calibration are separate from a live staging pass.
The explicit [dependency continuation journey](../tests/runner-e2e/product-journeys/task-dependency-resume.md) checks a finite blocked wait and one automatic native continuation after a browser-completed blocker. Local evidence controls are separate from a deployed QA pass.
[Reusable Product journeys](../tests/runner-e2e/PRODUCT-JOURNEYS.md) add explicit
deployment targets and starting-state recipes. The shared task-properties
journey and task-document create/edit/reopen journey use no model; Cloud
onboarding supplies reusable verified state. The document journey checks exact
saved content and both revision records through independent API reads. Cloud
fleet operations and trusted CI remain private. Local eval gateway qualification
saved content and both revision records through independent API reads. The explicit `chat-continuity` selection verifies browser reload/reopen, prompt-hidden recall,
ordinary `/new` reset, fresh native context and retained history against an attached target.
It requires a fresh owned agent with no prior work runs and uses three provider turns plus one synthetic reset row; it does not restart a shared stack.
Cloud fleet operations and trusted CI remain private. Local eval gateway qualification
does not qualify Cloud routes, and these selections stay outside `--all`.
The deployment-targeted [saved skill journey](../tests/runner-e2e/product-journeys/saved-skill-use.md) checks one company skill assigned by key, its exact admitted immutable version, a bound native source read/load, and a run-authored saved result from skill-only inputs. It uses the reusable Product journey target and remains unqualified until a real target run passes.
The explicit-only [case handoff workflow](../tests/runner-e2e/README.md#persistent-case-handoffs-explicit-only)
contains arithmetic and receipt smoke cells for a same-task round trip among three native agents using real case/task
APIs, saved documents, handoff receipts, and browser readback. Receipt smoke has
+62 -4
View File
@@ -937,6 +937,13 @@ identity, unknown failures, active leases, and incomplete cleanup still require
their normal reconciliation and termination proof. Unknown historical setup errors remain
outside this policy, and generic recovery cannot renew its exhausted budget.
When an admitted continuation reuses a sandbox after verified cleanup, an
unpublished preparation uses a directory bound to its writer epoch and lease.
Earlier unpublished directories and their receipts stay intact. Repeating the
same preparation retains its directory and receipt check. A published workspace
on the same allocation retains its recorded directory. Automatic bootstrap
timeout retries still require a fresh allocation under the policy above.
### Runner authentication and ACP startup handshake bounds
The native Runner waits up to the ten-second connection setup budget for the
@@ -1258,6 +1265,18 @@ checks coordinator ownership in its terminal update, so a result recorded after
the process check is also protected. Terminal task status remains authoritative;
exhausted finalization retries do not keep an otherwise orphaned run alive.
A native failure that schedules a same-run resume marks the run `failed` but
keeps the task's execution and checkout locks for that resume. The sweep leaves
those locks in place and clears them only when the task is done or cancelled.
If a scheduled resume finds its execution lock free, restart recovery reclaims
it, as the local resume claim does, unless another run for the task is queued,
running, or scheduled. A lock held by another run still blocks the resume.
Another task can hold the same routine execution slot even when this task's
lock is free. If that unique claim fails, restart recovery rolls back the whole
candidate claim and waits through the existing recovery deadline. It preserves
the current routine owner, checkpoint, and provider attempt, and continues the
startup batch. Other database failures still fail the recovery pass.
Automatic productivity reviews are retired. Run counts, missing comments, and elapsed task time do not create review tasks or impose continuation holds. Bounded continuation, provider recovery, budget limits, explicit blockers, and normal review/approval stages remain in force. Existing productivity-review tasks, comments, assignments, and dependencies remain unchanged and readable; their historical origins still identify them as recovery work for recursion suppression.
### Issue-thread interaction resolution
@@ -1834,9 +1853,10 @@ An interrupted conversation does not permanently block its task. For local conve
Shutdown, process loss, and provider failure use the existing durable failure retry counter and delay. Ordinary failure recovery permits at most two automatic retries in a failure chain. Accepted-interaction infrastructure recovery retains its existing bounded policy. Repeated scheduler visits reuse the same successor; restarting the server does not reset the counter. After exhaustion, automatic attempts stop. A new explicit user message can start a fresh run and failure budget. Productive max-turn continuation and confirmed workspace waits keep their separate existing semantics.
If shutdown interrupts Paperclip Runner preparation before native execution was
selected, recovery verifies that no provider, process launch, native result or
coordinator exists. The previous controller must have settled or lost its lease.
If shutdown or a recorded unexpected system interruption stops Paperclip Runner
preparation before native execution was selected, recovery verifies that no
provider, process launch, native result or coordinator exists. The previous
controller must have settled or lost its lease.
Required workspace recovery must have an exact saved-file receipt and stopped
leases before the task can continue. Once these checks pass, the server retires
its own unchanged bootstrap-failure block and schedules one bounded retry.
@@ -1845,7 +1865,8 @@ restart. A pending Retry or saved message owns its continuation instead of
starting a competing automatic attempt. Retry uses the same preparation proof
and preserves the recovered workspace. Current task ownership, human blocks,
pauses, approvals, budgets and the existing retry limit remain effective.
Operator Stops retain their explicit continuation requirements.
Operator Stops retain their explicit continuation requirements. Other ambiguous
interruptions without a server system-cancellation record remain held.
Graceful shutdown interrupts an owned legacy adapter through its cancellation
control, then waits within one shared 30-second window for provider stop and
@@ -2612,6 +2633,26 @@ configuration, and controller-owned credential or instruction resources use
verified suspension and saved-history resume. This recovery can restart a
provider; it does not suspend every healthy warm turn.
A different task can reuse the same physical sandbox after its prior task
finishes. Provider ingress has one listener per allocation. Before a new logical
session starts, Runner retires a locally retained idle session that still owns
that listener. The new run must hold the current native claim, task assignment,
environment lease and remote workspace writer. Company, agent, provider,
allocation, workspace and directory must match. The prior run must be committed
and accounted, with its original controller generation and a clean released or
expired lease. Retirement saves the prior session through its own checkpoint and
file-collection callbacks and waits for close before the new listener binds.
After close, admission rechecks the canonical native claim, task, workspace and
leases before staging files or invoking the new backend. Ownership loss, Stop
or a changed writer during retirement cannot authorize the new execution.
The new task keeps its own logical session and authority.
Active owners, a changed controller or writer, missing canonical proof and failed
close block admission. A retained entry stays available for verified retirement;
no process is selected by its port number. This rule covers current-controller
warm entries with recorded physical identity. A historical process outside that
registry still needs the existing exact-owner recovery proof.
A fresh native session admitted for the current task may replace a terminal
predecessor's goal source epoch when the saved goal is absent or complete.
It must hold the task's execution lock and assignee. Its accepted terminal
@@ -2732,6 +2773,23 @@ replay or access: the saved input and current principal, repository, conversatio
runtime fence and execution containment still pass normal admission. Explicit
conflicting App bindings remain denials.
When comments join a queued run with an unclaimed queued wake, admission saves
the accepted batch in both the run context and its authoritative primary wake in
the same transaction as the coalesced receipt. Claim rebuilds input from that primary wake and filters deleted
comments. It does not reconstruct input from historical coalesced receipts.
Admission locks and rechecks the run and its same-company, same-agent primary
wake. A changed status, mismatched binding, or mismatched durable-receipt actor preserves the incoming
input as deferred work instead of consuming it into the old source. Legacy wakes
without an authoritative batch retain their existing context-based claim path.
An already claimed wake keeps its source unchanged for its existing recovery
and replay path; this change does not grant it a new claim.
Ordinary mixed-author comments keep their existing coalescing behavior. The
primary wake retains its original actor, and each new receipt records its own
actual actor. Coalescing updates the queued batch while preserving the original
trigger comment fields, so recovery still verifies the original source actor.
Same-named agents with different IDs retain separate authority;
their new input is deferred without changing the other agent's queued work.
### Queued input after a native Stop
A run-only Stop ends the current response. It does not discard queued user
+2 -1
View File
@@ -210,7 +210,8 @@ diagnostic line on boot and continues without tracing — your server stays up.
## Scope
The OpenTelemetry export carries **traces only**. Metrics and log exporters
are out of scope and intentionally not configured here. Auto-instrumentations
are out of scope. The SDK receives explicit empty metric-reader and log-processor
lists so its default OTLP exporters cannot emit unrequested metrics or logs. Auto-instrumentations
for `fs`, `dns`, and `net` are disabled by default because they are too chatty
for this workload; everything else from
`@opentelemetry/auto-instrumentations-node` is on (HTTP, Express, PG, etc.).
@@ -102,3 +102,26 @@ Controller leases, in-flight recovery, active writers, and uncertain workspace
operations remain blockers. Tests must prove migration leaves failure state
intact, rejects stale copyback even if the old folder exists, and preserves
recovery source validation and company boundaries.
## Queued turns during offline enrollment
The task drain stops new executions. It does not consume queued turns. After
Cloud has stopped every application deployment, offline maintenance preserves
never-started queued run and wake rows unchanged. Workspace, instruction-file,
and historical-log migration use the same execution predicate at this boundary.
The replacement uses normal queue admission against the migrated authority.
Running executions still block migration. A queued row also blocks if it has
started, retains process/session/controller evidence, or has a coordinator or
environment lease. Task checkout and execution bindings remain separate blockers.
Live migration retains its original queued-run exclusion. The offline flag alone
is not evidence that old deployments have stopped; Cloud owns that provider fence.
PostgreSQL/S3 tests cover queued turns and wake payloads surviving migration and
loss of the predecessor filesystem, plus queued execution evidence that must
remain blocked. The packaged maintenance entry must also retain an unstarted
queued turn while it archives historical run output. The assembled heartbeat
test resumes that same queued run after deleting the old disk, verifies saved
workspace bytes and instructions at provider dispatch, and checks one result and
one wake consumption even after a second queue sweep. Its sandbox transport and
provider session are scripted; database, S3, admission and restoration are real.
+244
View File
@@ -0,0 +1,244 @@
# Sleep configuration qualification
This opt-in harness boots the actual server against an explicitly supplied fresh
test database. It measures local SQL, connection and network quiet. A local pass
does not establish Railway or Neon suspension. Keep a separate verdict for each
layer: quiet, eligibility, database suspension, host suspension, backup persistence,
inbound wake, timed wake and return to idle.
## Local configured services
Use a reviewed integrated source containing the incoming-wake and domain-deadline
contracts. Provision and migrate a fresh dedicated PostgreSQL database ending in
`_test` through the normal test operator. The suite does not allocate a database,
start embedded PostgreSQL or delete a database. It preserves its fixture rows.
Never supply a live tenant database.
The configured exporter needs the exact optional peer versions already declared
in `server/package.json`. With Node 24 and npm available, explicitly install the
fixture peers into a new temporary prefix and link only missing packages in this
checkout:
```sh
node scripts/sleep-qualification/setup-otel.mjs
```
The setup leaves application manifests and lockfiles unchanged. Retain the printed
setup receipt and temporary package lock. A version mismatch fails without
replacing an installed package. An opt-in CI job must run the same setup command;
setting an OTLP endpoint alone does not qualify an exporter.
The helper records its exact symlink targets in
`server/node_modules/.paperclip-sleep-otel-links.json`. If an owned temporary prefix
disappears, rerunning repairs only those matching recorded symlinks. Existing
matching packages are left alone. An unrecorded dangling link or other entry is
preserved with a recovery error; its pathname is not evidence of ownership.
`node scripts/sleep-qualification/setup-otel.mjs --cleanup` unlinks only matching
owned links. It preserves foreign replacements and keeps temporary install
prefixes for setup evidence. Remove those owned prefixes separately when their
receipts are no longer needed.
Set `PAPERCLIP_SLEEP_CONFIGURED_ACCEPTANCE=1`,
`PAPERCLIP_SLEEP_ACCEPTANCE_TEST_DATABASE_URL` to the owned migrated fixture and
`PAPERCLIP_SLEEP_CLOUD_SHA` to the exact reviewed Cloud source SHA. Set
`PAPERCLIP_SLEEP_CLOUD_CHECKOUT` to that built private checkout; the suite verifies
its HEAD and imports its production broker adapter without copying private source.
Retain artifacts
with `PAPERCLIP_MAINTENANCE_KEEP_ARTIFACTS=1`. Run the two scoped Vitest files with
one worker and no file parallelism:
```sh
pnpm exec vitest run server/src/__tests__/sleep-otel-export.test.ts \
server/src/__tests__/sleep-configuration-acceptance.test.ts \
--maxWorkers=1 --no-file-parallelism
```
The first case requires a successful named span at the real local OTLP collector
and no metric or log export during 90 real seconds, beyond the SDK's default
60-second metric cadence. It fails if the production SDK falls back to no-op.
The assembled case keeps
normal automatic backups, announcements, first-party telemetry and OTel enabled.
It creates an agent, useful completed process execution, API credential and future
routine through normal APIs. It waits the real ten-minute authenticated-use
cooldown, then measures at least 90 real seconds without tenant HTTP/SQL observer
polling. A passive listener on the actual app HTTP server counts incoming requests
and WebSocket upgrades, including direct clients outside the harness API helper.
It requires the app's existing request and upgrade handlers before installing;
it cannot become the sole upgrade listener and change unsupported-upgrade handling.
IPC snapshots record its capture identity, installation/validity, cumulative safe-integer
counts and observation times. Both snapshots must enclose the complete quiet interval,
remain valid and yield an exact zero delta. No URL, headers or body are recorded;
snapshot reads themselves make no HTTP requests or SQL queries. Missing or detached
capture, malformed counts and the legacy empty `observerRequests` list cannot prove
quiet. The passive protocol proxy separately measures database effects. It obtains
two owned signed safety reports after the window and preserves fixture history.
After ordinary API reactivation it waits another full real ten-minute cooldown,
measures another 90 seconds of quiet and requests fresh owned eligibility proof.
The helper keeps its legacy defaults for existing maintenance tests.
Announcements are enabled and the actual route/feed service validates and caches
the known fixture. An owned test-child module wrapper injects the existing feed
transport seam for the exact default HTTPS URL; unmatched requests fail. Startup
fails if the wrapper did not intercept the actual app constructor. The receipt
requires one named fetch and no subsequent calls during quiet. Browser mode also
requires the fixture copy to render. The production DNS/HTTPS address guard stays
unchanged and is not exercised in this combined case; its owning tests cover that
boundary separately. Active user activity can legitimately keep an instance awake.
The assembled scenario includes real tool approval rejection/delivery retirement
and signed chat-webhook completion followed by ordinary endpoint removal. That
removed-endpoint subcase is separate from the retained enabled GitHub connection:
the latter uses actual encrypted user credentials, refresh/continuity owners,
signed broker bindings and the production event delivery worker. The host wake
capability comes from the production connector store. Local in-memory broker
storage and synthetic GitHub HTTP responses remain explicit external boundaries;
this does not establish deployed broker durability or host suspension. These
assembled paths require a successful run against the reviewed integration before
their receipts can qualify them; source presence alone is not evidence.
Unknown plugins, persistent provider sessions, unfinished delivery/accounting,
uncertain writes and cleanup remain real blockers. Every inventory family needs
an explicit disposition before claiming all configurations qualified.
## Evaluate a receipt
```sh
pnpm exec vitest run server/scripts/sleep-qualification.test.mjs --maxWorkers=1
node scripts/sleep-qualification/evidence.mjs receipt.json expected.json report.json
```
`expected.json` binds `appSha`, `cloudSha`, `target` and a SHA-256 `configDigest`.
The digest uses `logicalSleepConfiguration(configuration)`: explicit backup interval
and checkpoint policy, announcements, telemetry activation, OTLP activation/protocol,
and UI activation in a fixed order. It excludes ephemeral loopback ports. Compute
the expected logical contract independently before starting the fixture; the
evaluator recomputes the receipt digest. Keep actual `collectorOrigins.telemetry`
and `.otel` separately, matching the actual configuration and optional expected
origin bindings.
Configured coverage must also have recorded observations. Announcements require
actual interception, a request for the expected feed, and no unmatched URL.
`signalObservation` records telemetry requests, trace requests, actual OTLP span
records and the named qualification span separately. Metric, log, malformed-trace
and unexpected request counts must be explicitly zero. The assembled case freezes
these observations after each quiet window; feature booleans are derived from
them and the observed backup checkpoint. Missing or malformed counts fail local
qualification even when a feature flag claims success.
Before the normal cooldown, a signed diagnostic-only inventory saves both raw
reports. It explicitly expects the active API cooldown aggregate and the skipped
backup diagnostic; every other reported blocker must be absent. Aggregate local
probes cannot identify which probe is present, so this early inventory never
proves eligibility. Its hold is released before the full unchanged cooldown.
After quiet, raw ordinary reports are saved before assertions; a refusal also saves
a diagnostic report under that hold.
Automatic backup coverage requires a new archive from the normal idle checkpoint,
not an enabled flag. After quiet counters freeze, the harness reads every new
owned gzip through its trailer, hashes compressed bytes, checks nonempty SQL for
the fixture identifier without retaining contents, and observes the wake marker
and absence of backup failure markers. This local evidence carries no provider
volume or restore claim. The owning backup suite separately qualifies restoration.
Archive provenance uses a positive finite opened-file birthtime, falling back to
positive finite content mtime only when the filesystem reports birthtime as zero.
The selected timestamp and its source are recorded. Existing names, old positive
birthtimes, malformed timestamps and future chronology remain disqualifying.
Original receipts are retained as produced; the new evidence fields are not
retroactively added to earlier runs.
The historical Full Y at `487e450b` and short composition at `57744f4e` wrote an
unmeasured empty observer-request list. Their recorded SQL, connection, outbound,
browser and producer observations remain pinned to those runs, but that list does
not meet the corrected inbound-observation contract. A new combined qualification
requires both real cooldowns and quiet windows with the installed incoming counter;
a short injected-poll regression cannot retroactively establish observer absence.
Receipt finalization follows all observations and runs the same evaluator before
writing the final receipt. A return-to-idle claim also needs explicit in-run quiet
start/end timestamps spanning at least 90 real seconds after the latest wake;
a duration field alone cannot prove it.
The evaluator rejects mismatched identities and chronology. Missing evidence stays
`INCONCLUSIVE`; a measured failed condition is `NOT VERIFIED`, including an
identity-verified active Neon endpoint or exact Railway service with running deployments. It does not produce
provider proof from local counters, fake time, a registry sleep flag or a successful
control response. Preserve original failures and their raw logs after a retry.
## Deployed boundary
No deployment target is authorized by this document. The private Cloud companion
provides a gated read-only provider observer. An operator must first approve one
exact owned disposable staging target, revisions, configuration, provider refs,
fixture operations and finite observation deadline. A named connector or shared
QA workspace is not that authorization.
Capture app/Cloud identity and effective policy while awake. Use only provider and
central control-plane metadata during sleep; tenant health/API/SQL probes can wake
the target. Confirm the exact Neon endpoint is idle and all owned Railway services
have stopped deployments. Observe ordinary authorized API, broker and deadline
wake cycles separately. Verify useful persisted results and return to idle.
Automatic backup persistence additionally requires an exact attached volume
covering the configured home/backup directory and a fsynced sentinel/archive
continuity receipt across the owned stop/wake. A writable local fallback directory
is insufficient. Missing live access leaves these layers `INCONCLUSIVE`.
For actual foreground/background browser coverage, first build the shipped UI into
`server/ui-dist` in the same reviewed checkout. Install the normal Playwright
Chromium runtime and run on a display-capable worker. Set
`PAPERCLIP_SLEEP_BROWSER_ACCEPTANCE=1` for the configured suite. The harness loads
the actual UI, records an ordinary active reload, opens another real foreground
tab and requires Chromium to report the app tab hidden. It observes 90 real
seconds without app API traffic, then closes the browser before the server quiet
window. Failure to establish real visibility fails that mode; it never substitutes
a fake visibility event or frozen page. Without this explicit mode, the receipt
claims closed-browser coverage only.
The browser uses a fresh owned Chromium profile and loopback debugging endpoint.
It attaches with `connectOverCDP({ noDefaults: true })` to the native default
context. Normal Playwright contexts install focus emulation that kept real
background tabs visible in the measured failure. This path avoids that override.
The receipt includes native window IDs, initial/final visibility and passive
visibility events for the entire 90-second interval. An intervening activation
fails the observation. Cleanup disconnects, terminates only the owned process and
removes its temporary profile. It never attaches to a user's existing browser.
## Managed fixture preflight
Configured qualification uses the supported managed authenticated profile. The
actual app runs on loopback, with a lifetime-stable fixture vault key and the
normal managed configuration. A fixture Cloud front door supplies trusted tenant
headers to invoke the real primary-company/user/membership provisioning path;
ordinary company creation remains forbidden. Bearer-key and signed-control
requests omit tenant headers. Native browser headers can reach only the owned
app origin; foreign HTTP/WebSocket transports are blocked and fail qualification.
Managed application storage requires S3. A bounded loopback protocol fixture
supplies the remote object service for actual SDK requests; unsupported operations,
framing and conditional requests fail closed. This establishes neither provider
IAM nor durable S3 storage. The instance home and database archives remain real
local filesystem paths. Normal managed migration, historical cleanup and routine
owners remain enabled. The logical digest binds managed activation, auth mode and
storage provider; actual ephemeral transport origins are recorded separately.
Before another long campaign, run the opt-in `sleep-managed-preflight.test.ts` on
its own root-leased fresh external database using
`PAPERCLIP_SLEEP_MANAGED_PREFLIGHT=1` and an explicit dedicated
`PAPERCLIP_SLEEP_MANAGED_PREFLIGHT_TEST_DATABASE_URL`. Retain artifacts with
`PAPERCLIP_MAINTENANCE_KEEP_ARTIFACTS=1`. It uses the same profile and negotiated
broker receiver, provisions real authority, admits and completes one process run,
and proves real key activity through signed diagnostics. It performs no browser,
ten-minute wait or quiet-window claim. No embedded allocation or automatic drop.
Loopback execution requires the host tool's explicit network/runtime permission.
Setup and successful browser receipts are written immediately before later
assertions. Early diagnostics first use the existing owner-settled contract; they
reject durable blockers immediately and retry only observed local in-flight /
generation inspection incompleteness, up to three rounds. Every attempt is saved.
Both accepted reports must be strict, complete, structurally matching and under
the exact live hold, with each internal `inspection.generation=none` and no active
work before/after. Real HTTP authentication itself changes the global work
counter, so equality across deliberate observer requests is not asserted.
A fresh successful opaque-key request anchors the later full real cooldown.
Neither setup stabilization nor the fast preflight substitutes for both real
600100ms cooldowns, both untouched 90000ms windows and useful reactivation. S3
observations are saved before and after owned app cleanup, including failures;
cleanup preserves the original error. Full provider sleep remains a separate
unqualified layer until an exact deployed target is authorized and observed.
+23
View File
@@ -540,3 +540,26 @@ explicit incomplete usage report before failure; this preserves known budget
exposure without settling ambiguous usage. That required report and its receipt
retain the accounting measurements and matching saved prior identity, so an
optional diagnostic acknowledgement cannot discard reconciliation evidence.
## Shared allocation ingress retirement
The `retires the completed child listener` cases in
`native-session-executor.test.ts` use the actual executor and warm registry,
PostgreSQL ownership rows, and real local TCP listeners. The provider process
boundary is supplied by the fixture. The listeners use an ephemeral test port;
production provider ingress keeps its fixed port. A completed child's listener
must close before a distinct parent session binds in that allocation. The child's
accepted result stays unchanged.
```sh
pnpm --filter @paperclipai/server exec vitest run \
src/services/native-runtime/native-session-executor.test.ts \
-t 'retires the completed child listener'
```
Controls cover foreign companies, allocations and workspaces; live or unaccounted
owners; changed controllers, reassignment and competing leases; workspace-lock
and cancellation races; unconfirmed close; and controller, Stop and writer
changes inside awaited close. The latter must prevent parent backend invocation
even when the old listener has closed. These local checks do not identify
a hosted conflicting PID or prove a hosted provider Stop or recovery.
+23
View File
@@ -726,6 +726,29 @@ actual shell discovery, successful durable activity recording, and denial of unr
Runner state, descriptor writes and ambient secrets. The normal suite covers
bounded framing, concurrent publishers, fixed-path cleanup and attachment fencing.
Codex receives write access to Runner's private activity-channel directory, with
the lock file explicitly read-only. The FIFO itself must not be a writable
filesystem root: Codex adds protected metadata children to writable roots, which
cannot exist beneath a FIFO. The helper and discovery descriptor remain read-only;
the activity grant does not expose Runner state or change workspace/network policy.
To check restrictive Plan mode without credentials or a model call, run the
actual channel/policy producer against the pinned Linux Codex sandbox:
```sh
CODEX_NOTIFICATION_SANDBOX_COMMAND=/absolute/path/to/pinned/codex \
CODEX_NOTIFICATION_SANDBOX_RESOURCES=/absolute/path/to/pinned/executable/resources \
cargo test --manifest-path runner/Cargo.toml --locked -p paperclip-runner-core --lib \
read_only_codex_opens_with_runtime_activity -- --ignored
```
This checks instruction reads, FIFO writes, and denial of workspace writes,
unrelated Runner-state reads, descriptor writes, and lock writes/removal. An
isolated non-Linux host can instead set `CODEX_NOTIFICATION_FIXTURE_EXPORT` to a
JSON file and omit `CODEX_NOTIFICATION_SANDBOX_COMMAND`; that only exports a
synthetic producer fixture for a separately controlled Linux sandbox. Exporting
the fixture does not execute or verify the sandbox.
## Direct live protocol qualification
The canonical direct live protocol suite lives in the separate
@@ -70,6 +70,35 @@ a scripted harness, removes the controller's launch files, then recovers the
same process and provider session and completes another turn. This exercises the
real transport; it does not qualify a hosted OpenCode or ACPX deployment.
## Settled ACPX sessions across a Runner upgrade
Saved ACPX provider state records the digest of the launch profile that wrote
it: the provider-pack authority, the sidecar command and arguments, each launch
artifact's SHA-256, and any candidate admission. A deploy that ships a new
provider pack or sidecar build changes the digest. The state stores only the
digest, so restore cannot tell which part changed.
Restore re-binds a saved session to the current launch profile, and keeps the
conversation, only when all of these are true:
- The session is suspended. It has no active turn, pending runtime request,
undelivered provider event, or unconfirmed provider exit.
- The startup has no candidate admission.
- The saved descriptor names this startup's sidecar command and arguments.
Every other binding stays exact. The saved descriptor must match this build's
qualified agent profile (ACPX version, agent server package and version, and
command digest). Session start verifies each artifact and the restored provider
identity. Candidate grants and run bindings keep their own checks.
Any other launch-profile mismatch, and a saved descriptor that names a release
this build does not qualify, fails restore with the closed command-result
classification `provider_state_incompatible`. The controller reads the classification from the
authenticated result, not from the message. If the run allows a replacement
after a failed resume, it opens a fresh provider session as before. Otherwise the
run fails with `native_provider_state_incompatible` and is not retried. A board
Retry then continues the task in a fresh provider session in the same workspace.
## Profile identity
Every ACPX provider's `commandDigest` attests only its own upstream release.
@@ -26,7 +26,7 @@ use crate::durable::QualifiedLaunchArtifact;
use crate::durable::{
create_private_temporary_file, open_private_regular_file, verify_private_directory,
AcpxLaunchProfile, Command, CommandExecution, CommandExecutor, DurableRunnerConfig,
DurableRunnerError, EventPriority, PolledEvent,
DurableRunnerError, EventPriority, PolledEvent, PROVIDER_STATE_INCOMPATIBLE,
};
use crate::generated_acpx_sidecar_contract::GeneratedAcpxSidecarCommand;
use crate::process_supervisor::{VerifiedProcessArgument, VerifiedProcessLaunch};
@@ -279,6 +279,19 @@ fn only_registered_asset_text_changed(
}
impl AcpxProviderDescriptor {
// Whether this build qualifies the release that the descriptor names.
fn names_qualified_release(&self) -> bool {
acpx_release_profile(&self.agent).is_some_and(|expected| {
self.provider_version == QUALIFIED_ACPX_VERSION
&& self.acpx_version == QUALIFIED_ACPX_VERSION
&& self.agent_server_package == expected.agent_server_package
&& self.agent_server_version == expected.agent_server_version
&& self.agent_runtime_package.as_deref() == expected.agent_runtime_package
&& self.agent_runtime_version.as_deref() == expected.agent_runtime_version
&& self.command_digest == expected.command_digest
})
}
fn validate_session(
&self,
context: &AcpxEventProjectionContext,
@@ -622,11 +635,21 @@ impl AcpxDurableState {
// so its persisted descriptor legitimately carries the prior run ID
// until run.attach rotates authority. Fresh command descriptors still
// use validate(), which binds them to the current run.
self.descriptor.validate_session(context)?;
// A deploy that changes the agent's qualified release also leaves
// saved state that this build cannot restore. Keep the same check and
// only name its class.
self.descriptor.validate_session(context).map_err(|error| {
if self.descriptor.names_qualified_release() {
error
} else {
error.with_classification(PROVIDER_STATE_INCOMPATIBLE)
}
})?;
if self.launch_profile_digest != expected_launch_profile_digest {
return Err(DurableRunnerError::invalid(
"ACPX durable launch profile digest does not match runner startup",
));
)
.with_classification(PROVIDER_STATE_INCOMPATIBLE));
}
validate_pending_runtime_requests(&self.pending_runtime_requests)?;
let mut ids = HashSet::new();
@@ -960,6 +983,34 @@ impl AcpxCommandExecutor {
prior.canonical_digest()
}
// A deploy changes the provider-pack authority and the sidecar and Node
// artifact digests, so a saved digest from the previous build never
// matches. The digest is a hash, so restore cannot tell which part
// changed. Re-bind only when no provider work is in flight:
// - The session is suspended. No turn, request, undelivered event, or
// unconfirmed provider exit remains. Nothing from the old provider
// process continues; the next launch only loads the conversation.
// - The startup is ordinary. Candidate admission carries company, model
// and expiry authority in the digest, so candidate sessions stay
// exactly bound.
// - The saved descriptor names this startup's sidecar command and
// arguments. Only the qualified content at those paths changed.
// validate() still binds the descriptor to this build's qualified agent
// profile (ACPX, agent server and command digest), and session start
// still verifies every artifact and the restored provider identity.
fn rebinds_settled_session(&self, state: &AcpxDurableState) -> bool {
self.launch_profile.as_ref().is_some_and(|profile| {
profile.candidate_admission.is_none()
&& state.descriptor.sidecar_command == profile.command
&& state.descriptor.sidecar_args == profile.args
}) && state.lifecycle == "suspended"
&& state.identity.is_some()
&& state.active_turn_id.is_none()
&& !state.provider_exit_unconfirmed
&& state.pending_events.is_empty()
&& state.pending_runtime_requests.is_empty()
}
fn restore(&mut self) -> Result<(), DurableRunnerError> {
self.ensure_persistence_healthy()?;
if self.restore_checked {
@@ -1016,10 +1067,15 @@ impl AcpxCommandExecutor {
file.read_to_end(&mut bytes).map_err(|error| {
DurableRunnerError::invalid(format!("failed to read ACPX provider state: {error}"))
})?;
let state: AcpxDurableState = serde_json::from_slice(&bytes).map_err(|error| {
let mut state: AcpxDurableState = serde_json::from_slice(&bytes).map_err(|error| {
DurableRunnerError::invalid(format!("ACPX provider state is malformed: {error}"))
})?;
let launch_profile_digest = self.restore_launch_profile_digest(&state)?;
if state.launch_profile_digest != launch_profile_digest
&& self.rebinds_settled_session(&state)
{
state.launch_profile_digest = launch_profile_digest.clone();
}
state.validate(&self.context, &launch_profile_digest)?;
self.state = Some(state);
self.restore_session_if_needed()
@@ -5298,14 +5354,11 @@ mod tests {
"grant_run",
"injected_candidate",
"context_run",
"authority",
"command",
"args",
"artifact",
"missing_profile",
"saved_digest",
] {
let (directory, mut config, payload, before, sentinel, candidate) =
let (directory, mut config, payload, _, sentinel, candidate) =
kimi_cold_restore_fixture("kimi-released-cold-reject");
assert!(config
.acpx_launch_profile
@@ -5316,26 +5369,11 @@ mod tests {
match field {
"context_run" => config.run_id = "foreign-run".into(),
"missing_profile" => config.acpx_launch_profile = None,
"saved_digest" => {
let mut state: AcpxDurableState = serde_json::from_slice(&before).unwrap();
state.launch_profile_digest = format!("sha256:{}", "0".repeat(64));
fs::write(
directory.join("acpx-provider-state.json"),
serde_json::to_vec(&state).unwrap(),
)
.unwrap();
}
_ => {
let profile = config.acpx_launch_profile.as_mut().unwrap();
match field {
"authority" => {
profile.authority_digest = format!("sha256:{}", "0".repeat(64))
}
"command" => profile.command = directory.join("foreign-command"),
"args" => profile.args.push("foreign-argument".into()),
"artifact" => {
profile.artifacts[0].sha256 = format!("sha256:{}", "0".repeat(64))
}
_ => {
let mut grant = candidate;
match field {
@@ -5382,6 +5420,60 @@ mod tests {
}
}
#[test]
#[cfg(any(
all(target_os = "macos", target_arch = "aarch64"),
all(target_os = "linux", target_arch = "x86_64")
))]
fn released_kimi_cold_restore_rebinds_a_settled_session_to_new_launch_content() {
// An ordinary startup re-binds a settled session after a provider-pack
// or artifact change at the same launch paths. The checks above for
// candidate grants, command, arguments and run binding still apply.
for field in ["authority", "artifact", "saved_digest"] {
let (directory, mut config, payload, before, sentinel, _) =
kimi_cold_restore_fixture("kimi-released-cold-rebind");
let profile = config.acpx_launch_profile.as_mut().unwrap();
match field {
"authority" => profile.authority_digest = format!("sha256:{}", "0".repeat(64)),
"artifact" => {
let command = profile.command.clone();
write_artifact(&command, b"#!/bin/sh\nexit 2\n", true);
profile.artifacts[0] = artifact(&command);
}
"saved_digest" => {
let mut state: AcpxDurableState = serde_json::from_slice(&before).unwrap();
state.launch_profile_digest = format!("sha256:{}", "0".repeat(64));
fs::write(
directory.join(ACPX_PROVIDER_STATE_FILE),
serde_json::to_vec(&state).unwrap(),
)
.unwrap();
}
_ => unreachable!(),
}
config.validate().unwrap();
let mut recovered = AcpxCommandExecutor::with_runner_config(&directory, &config);
recovered.restore().expect(field);
recovered.attach_run(&payload).expect(field);
let saved: AcpxDurableState =
serde_json::from_slice(&fs::read(recovered.state_path()).unwrap()).unwrap();
assert_eq!(saved.descriptor.run_id, "run-2", "{field}");
assert_eq!(
saved.launch_profile_digest,
config
.acpx_launch_profile
.as_ref()
.unwrap()
.canonical_digest()
.unwrap(),
"{field}"
);
assert!(recovered.session.is_none(), "{field}");
assert!(!sentinel.exists(), "{field}");
fs::remove_dir_all(directory).unwrap();
}
}
#[test]
#[cfg(any(
all(target_os = "macos", target_arch = "aarch64"),
@@ -5498,4 +5590,217 @@ mod tests {
assert!(!sentinel.exists());
fs::remove_dir_all(directory).unwrap();
}
// A settled Claude session saved by one runner build. The next deploy
// ships a new sidecar build and provider-pack authority at the same
// qualified launch paths, and the next run cold-attaches in a new runner.
#[cfg(unix)]
fn settled_session_after_runner_upgrade(
label: &str,
) -> (PathBuf, DurableRunnerConfig, Value, Vec<u8>) {
let directory = temporary_directory(label);
let command = directory.join("node");
let sidecar = directory.join("acpx-runtime-sidecar.cjs");
write_artifact(&command, b"#!/bin/sh\nexit 1\n", true);
write_artifact(&sidecar, b"// sidecar build 1\n", false);
let profile = AcpxLaunchProfile {
candidate_admission: None,
authority_digest: format!("sha256:{}", "d".repeat(64)),
command: command.clone(),
args: vec![sidecar.to_string_lossy().into_owned()],
artifacts: vec![artifact(&command), artifact(&sidecar)],
};
let runtime = directory.join("runtime");
let workspace = directory.join("workspace");
fs::create_dir_all(&runtime).unwrap();
fs::create_dir_all(&workspace).unwrap();
fs::set_permissions(&runtime, fs::Permissions::from_mode(0o700)).unwrap();
fs::set_permissions(&workspace, fs::Permissions::from_mode(0o700)).unwrap();
let mut value = descriptor("claude");
value["runtimeDirectory"] = json!(runtime);
value["cwd"] = json!(workspace);
value["sidecarCommand"] = json!(command);
value["sidecarArgs"] = json!([sidecar]);
let desc: AcpxProviderDescriptor = serde_json::from_value(value.clone()).unwrap();
let identity = AcpxProviderSessionIdentity {
kind: "acpx".to_owned(),
normalized_session_id: "session-1".to_owned(),
acpx_record_id: "record-1".to_owned(),
backend_session_id: "backend-1".to_owned(),
agent_session_id: "agent-1".to_owned(),
profile_digest: desc.command_digest.clone(),
workspace_digest: format!("sha256:{}", "a".repeat(64)),
requested_model: desc.model.clone(),
effective_model: desc.model.clone(),
permission_mode: Some(desc.permission_mode),
mode: None,
pi_thinking_level: None,
claude_effort: None,
grok_effort: None,
grok_disable_web_search: None,
provider_lifetime_fence_candidates: [60_001, 60_002, 60_003],
};
let operations = Vec::new();
let tools = AuthorizedToolSet {
schema: TOOL_SET_SCHEMA.to_owned(),
schema_version: 1,
catalog_digest: authorized_tool_catalog_digest(&operations).unwrap(),
operations,
};
let mut state = AcpxDurableState::new(desc, tools, profile.canonical_digest().unwrap());
state.lifecycle = "suspended".to_owned();
state.identity = Some(identity);
let config = test_config(&directory, Some(profile));
let mut executor = AcpxCommandExecutor::with_runner_config(&directory, &config);
executor.state = Some(state);
executor.save_state().unwrap();
let saved = fs::read(executor.state_path()).unwrap();
write_artifact(&sidecar, b"// sidecar build 2\n", false);
let mut next_config = config;
next_config.run_id = "run-2".to_owned();
next_config.turn_id = "turn-2".to_owned();
next_config.item_id = "item-2".to_owned();
let next_profile = next_config.acpx_launch_profile.as_mut().unwrap();
next_profile.authority_digest = format!("sha256:{}", "e".repeat(64));
next_profile.artifacts = vec![artifact(&command), artifact(&sidecar)];
next_config.validate().unwrap();
let mut provider = value;
provider["runId"] = json!("run-2");
(
directory,
next_config,
json!({ "provider": provider }),
saved,
)
}
#[cfg(unix)]
#[test]
fn settled_session_rebinds_to_the_upgraded_runner_launch_profile() {
let (directory, config, payload, saved) =
settled_session_after_runner_upgrade("upgrade-rebind");
let prior: AcpxDurableState = serde_json::from_slice(&saved).unwrap();
let current_digest = config
.acpx_launch_profile
.as_ref()
.unwrap()
.canonical_digest()
.unwrap();
assert_ne!(prior.launch_profile_digest, current_digest);
let mut recovered = AcpxCommandExecutor::with_runner_config(&directory, &config);
recovered
.restore()
.expect("a settled session survives a new qualified sidecar build");
assert!(recovered.session.is_none());
assert!(recovered.poll_events().unwrap().is_empty());
assert_eq!(fs::read(recovered.state_path()).unwrap(), saved);
recovered
.attach_run(&payload)
.expect("the next run cold-attaches the same conversation");
assert!(recovered.session.is_none());
let attached: AcpxDurableState =
serde_json::from_slice(&fs::read(recovered.state_path()).unwrap()).unwrap();
assert_eq!(attached.launch_profile_digest, current_digest);
assert_eq!(attached.descriptor.run_id, "run-2");
assert_eq!(attached.lifecycle, "suspended");
assert_eq!(attached.identity, prior.identity);
assert_eq!(
attached.descriptor.command_digest,
prior.descriptor.command_digest
);
fs::remove_dir_all(directory).unwrap();
}
#[cfg(unix)]
#[test]
fn unrestorable_saved_sessions_keep_strict_bindings_and_name_their_class() {
for field in [
"active_turn",
"pending_request",
"pending_event",
"session_open",
"exit_unconfirmed",
"no_identity",
"command_moved",
"args_changed",
"agent_server_release",
"acpx_release",
] {
let (directory, mut config, _, saved) =
settled_session_after_runner_upgrade("upgrade-strict");
let state_path = directory.join(ACPX_PROVIDER_STATE_FILE);
let mut state: AcpxDurableState = serde_json::from_slice(&saved).unwrap();
let profile = config.acpx_launch_profile.as_mut().unwrap();
match field {
"active_turn" => {
state.lifecycle = "turn_active".into();
state.active_turn_id = Some("turn-1".into());
}
"pending_request" => {
state.lifecycle = "turn_active".into();
state.active_turn_id = Some("turn-1".into());
state
.push(NormalizedProviderEvent {
event_type: "runtime_request.created".into(),
priority: EventPriority::P0,
payload: json!({"request":pending_input_request("input-1")}),
})
.unwrap();
state.pending_events.clear();
}
"pending_event" => state
.push(NormalizedProviderEvent {
event_type: "session.updated".into(),
priority: EventPriority::P1,
payload: json!({}),
})
.unwrap(),
"session_open" => state.lifecycle = "session_open".into(),
"exit_unconfirmed" => {
state.lifecycle = "closed".into();
state.provider_exit_unconfirmed = true;
}
"no_identity" => {
state.lifecycle = "prepared".into();
state.identity = None;
}
"command_moved" => {
let moved = directory.join("relocated-node");
write_artifact(&moved, b"#!/bin/sh\nexit 1\n", true);
profile.command = moved.clone();
profile.artifacts[0] = artifact(&moved);
}
"args_changed" => profile.args.push("--changed".into()),
"agent_server_release" => state.descriptor.agent_server_version = "0.0.1".into(),
"acpx_release" => {
state.descriptor.acpx_version = "0.0.1".into();
state.descriptor.provider_version = "0.0.1".into();
}
_ => unreachable!(),
}
config.validate().unwrap();
fs::write(&state_path, serde_json::to_vec(&state).unwrap()).unwrap();
let retained = fs::read(&state_path).unwrap();
let mut recovered = AcpxCommandExecutor::with_runner_config(&directory, &config);
let error = recovered.restore().unwrap_err();
let expected = if field.ends_with("_release") {
"does not match a qualified immutable profile"
} else {
"launch profile digest does not match runner startup"
};
assert!(error.to_string().contains(expected), "{field}: {error}");
assert_eq!(
error.classification(),
Some(crate::durable::PROVIDER_STATE_INCOMPATIBLE),
"{field}"
);
assert!(recovered.session.is_none(), "{field}");
assert_eq!(fs::read(&state_path).unwrap(), retained, "{field}");
fs::remove_dir_all(directory).unwrap();
}
}
}
@@ -61,8 +61,13 @@ pub(crate) fn runtime_notification_codex_args(
let Some(filesystem) = config["filesystem"].as_inline_table_mut() else {
continue;
};
for (path, mode) in paths.iter().zip(["read", "read", "read", "write"]) {
filesystem.insert(path.to_string_lossy().as_ref(), mode.into());
let Some(channel_directory) = paths[3].parent().filter(|path| path.parent().is_some())
else {
continue;
};
filesystem.insert(channel_directory.to_string_lossy().as_ref(), "write".into());
for path in &paths[..3] {
filesystem.insert(path.to_string_lossy().as_ref(), "read".into());
}
extra.extend(["-c".into(), format!("{prefix}{filesystem}")]);
}
@@ -4522,8 +4527,8 @@ mod tests {
let paths = [
PathBuf::from("/runtime/runnerd"),
PathBuf::from("/state/private/runtime-notifications.json"),
PathBuf::from("/state/private/runtime-activity.lock"),
PathBuf::from("/tmp/prn-channel"),
PathBuf::from("/tmp/prn-channel/activity.lock"),
PathBuf::from("/tmp/prn-channel/activity.fifo"),
];
let extra =
runtime_notification_codex_args(&args, "paperclip-runner-workspace-only", &paths);
@@ -4562,7 +4567,7 @@ mod tests {
("/state/private", "none"),
("/runtime/runnerd", "read"),
("/state/private/runtime-notifications.json", "read"),
("/state/private/runtime-activity.lock", "read"),
("/tmp/prn-channel/activity.lock", "read"),
("/tmp/prn-channel", "write"),
] {
assert_eq!(
@@ -4570,6 +4575,7 @@ mod tests {
Some(mode)
);
}
assert!(filesystem.get("/tmp/prn-channel/activity.fifo").is_none());
}
assert!(!serialized.contains("network."));
assert!(!serialized.contains("PAPERCLIP_RUN_ID"));
@@ -37,12 +37,17 @@ pub fn redact_diagnostic_text(input: &str) -> String {
state::redact_text(input)
}
/// Saved provider state was written under a launch or provider profile that
/// this runner cannot adopt. The outcome is deterministic for this runner
/// build: retrying the same restore cannot succeed.
pub const PROVIDER_STATE_INCOMPATIBLE: &str = "provider_state_incompatible";
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DurableRunnerError(String, Option<serde_json::Value>);
pub struct DurableRunnerError(String, Option<serde_json::Value>, Option<&'static str>);
impl DurableRunnerError {
pub fn invalid(message: impl Into<String>) -> Self {
Self(message.into(), None)
Self(message.into(), None, None)
}
pub fn with_provider_failure(mut self, value: Option<serde_json::Value>) -> Self {
@@ -53,6 +58,17 @@ impl DurableRunnerError {
pub fn provider_failure(&self) -> Option<serde_json::Value> {
self.1.clone()
}
/// Adds a closed failure class that the controller reads from the command
/// result instead of parsing the message.
pub fn with_classification(mut self, classification: &'static str) -> Self {
self.2 = Some(classification);
self
}
pub fn classification(&self) -> Option<&'static str> {
self.2
}
}
impl Display for DurableRunnerError {
@@ -1930,6 +1930,9 @@ fn process_command<E: CommandExecutor>(
"code": "command_execution_failed",
"message": message,
});
if let Some(classification) = error.classification() {
failure["classification"] = json!(classification);
}
if let Some(provider_failure) = error.provider_failure() {
failure["providerFailure"] = provider_failure;
}
@@ -2489,6 +2492,17 @@ mod tests {
}
}
struct IncompatibleStateExecutor;
impl CommandExecutor for IncompatibleStateExecutor {
fn execute(&mut self, _command: &Command) -> Result<CommandExecution, DurableRunnerError> {
Err(
DurableRunnerError::invalid("saved provider state cannot be adopted")
.with_classification(crate::durable::PROVIDER_STATE_INCOMPATIBLE),
)
}
}
impl CommandExecutor for ShutdownFailingExecutor {
fn execute(&mut self, _command: &Command) -> Result<CommandExecution, DurableRunnerError> {
Ok(CommandExecution::result(json!({"status": "completed"})))
@@ -4865,6 +4879,46 @@ mod tests {
.diagnostics
.iter()
.all(|diagnostic| !diagnostic.contains("test-secret")));
assert!(failed.result.get("classification").is_none());
fs::remove_dir_all(directory).unwrap();
}
#[test]
fn classified_executor_failure_keeps_its_closed_class_in_the_durable_result() {
let directory = std::env::temp_dir().join(format!(
"paperclip-runner-command-classified-failure-{}",
std::process::id()
));
let _ = fs::remove_dir_all(&directory);
let config = config(directory.clone());
let store = DurableStateStore::new(&directory).unwrap();
let (mut state, _) = store.load_or_create(&config).unwrap();
let command = command("run.attach");
let (failed, _) = process_command(
&mut state,
&store,
&config,
&mut IncompatibleStateExecutor,
&command,
)
.unwrap();
let (mut recovered, _) = store.load_or_create(&config).unwrap();
let replay = process_command(
&mut recovered,
&store,
&config,
&mut IncompatibleStateExecutor,
&command,
)
.unwrap()
.0;
assert_eq!(failed.status, "failed");
assert_eq!(failed.result["code"], "command_execution_failed");
assert_eq!(
failed.result["classification"],
"provider_state_incompatible"
);
assert_eq!(failed, replay);
fs::remove_dir_all(directory).unwrap();
}
@@ -666,6 +666,8 @@ mod tests {
assert_eq!(fs::read(nested.join("protected")).unwrap(), b"unchanged");
fs::remove_dir_all(pipe.parent().unwrap()).unwrap();
}
include!("runtime_notifications/tests/codex_sandbox.rs");
#[test]
#[ignore = "requires CODEX_NOTIFICATION_SANDBOX_COMMAND and PAPERCLIP_RUNTIME_NOTIFICATION_HELPER"]
fn publisher_crosses_real_codex_tool_boundary_without_runner_state_access() {
@@ -709,7 +711,8 @@ mod tests {
if cat "$1" >/dev/null 2>&1; then exit 71; fi
if /bin/sh -c ': >> "$PAPERCLIP_RUNTIME_NOTIFICATION_CONTEXT"' 2>/dev/null; then exit 72; fi
if touch "$2/probe" 2>/dev/null; then exit 75; fi
if touch "$3/probe" 2>/dev/null; then exit 76; fi
touch "$3/probe" || exit 76
rm "$3/probe" || exit 76
if /bin/sh -c ': >> "$1"' ignored "$4" 2>/dev/null; then exit 77; fi
context=$("$PAPERCLIP_RUNTIME_NOTIFICATION_HELPER" notification-context) || exit 73
"$PAPERCLIP_RUNTIME_NOTIFICATION_HELPER" record-activity "$context" example.runtime desktop.used || exit 74
@@ -0,0 +1,157 @@
#[test]
fn codex_activity_grant_uses_only_its_own_private_channel_directory() {
let (_directory, config, _store, _state, server) = setup();
let (_other_directory, _other_config, _other_store, _other_state, other) = setup();
let channel = server.pipe.0.parent().unwrap();
let other_channel = other.pipe.0.parent().unwrap();
assert_ne!(channel, other_channel);
verify_private_directory(channel).unwrap();
assert!(fs::symlink_metadata(&server.pipe.0)
.unwrap()
.file_type()
.is_fifo());
let paths = [
PathBuf::from("/runtime/runnerd"),
context_path(&config.state_dir),
server.pipe.0.with_extension("lock"),
server.pipe.0.clone(),
];
let profile = "paperclip-runner-workspace-read-only";
let args = vec![
"shell_environment_policy.include_only=[]".into(),
format!("permissions.{profile}.filesystem={{\":root\"=\"none\"}}"),
];
let projected = crate::codex_provider::runtime_notification_codex_args(&args, profile, &paths);
let prefix = format!("permissions.{profile}.filesystem=");
let value = projected
.iter()
.find_map(|arg| arg.strip_prefix(&prefix))
.unwrap();
let config = format!("filesystem={value}")
.parse::<toml_edit::DocumentMut>()
.unwrap();
let filesystem = config["filesystem"].as_inline_table().unwrap();
assert_eq!(
filesystem
.get(channel.to_str().unwrap())
.and_then(toml_edit::Value::as_str),
Some("write")
);
assert!(filesystem.get(other_channel.to_str().unwrap()).is_none());
assert!(filesystem
.get(paths[1].parent().unwrap().to_str().unwrap())
.is_none());
}
#[test]
#[ignore = "requires source-pinned Linux CODEX_NOTIFICATION_SANDBOX_COMMAND or CODEX_NOTIFICATION_FIXTURE_EXPORT"]
fn read_only_codex_opens_with_runtime_activity_without_widening_file_access() {
use std::process::Command;
let directory = TestDirectory::create(&std::env::temp_dir()).unwrap();
let config = config(directory.directory.join("state"));
fs::DirBuilder::new()
.mode(0o700)
.create(&config.state_dir)
.unwrap();
let server = RuntimeNotifications::bind(&config.state_dir).unwrap();
let workspace = directory.directory.join("workspace");
let home = directory.directory.join("home");
fs::create_dir(&workspace).unwrap();
fs::create_dir(&home).unwrap();
fs::write(
home.join("credential-fixture"),
b"synthetic private credential",
)
.unwrap();
fs::write(
workspace.join("AGENTS.md"),
b"Read-only fixture instructions.\n",
)
.unwrap();
fs::write(
config.state_dir.join("private-state"),
b"must remain inaccessible",
)
.unwrap();
let helper = config.state_dir.join("helper");
fs::write(&helper, b"synthetic executable resource\n").unwrap();
let paths = [
helper,
context_path(&config.state_dir),
server.pipe.0.with_extension("lock"),
server.pipe.0.clone(),
];
let profile = "paperclip-runner-workspace-read-only";
let resources = std::env::var("CODEX_NOTIFICATION_SANDBOX_RESOURCES")
.expect("set the source-pinned Codex executable resource directory");
let resources = serde_json::to_string(&resources).unwrap();
let args = vec![
"-c".into(),
"shell_environment_policy.include_only=[\"PATH\"]".into(),
"-c".into(),
format!("permissions.{profile}.filesystem={{\":root\"=\"none\",\":minimal\"=\"read\",\":tmpdir\"=\"none\",{resources}=\"read\",\"{}\"=\"none\",\"{}\"=\"read\"}}", home.display(), workspace.display()),
"-c".into(),
format!("permissions.{profile}.network.enabled=false"),
];
let extra = crate::codex_provider::runtime_notification_codex_args(&args, profile, &paths);
let script = r#"
set -eu
test -z "${UNRELATED_SECRET:-}"
test "$(cat "$4/AGENTS.md")" = 'Read-only fixture instructions.'
if touch "$4/forbidden" 2>/dev/null; then exit 71; fi
if cat "$1/private-state" >/dev/null 2>&1; then exit 72; fi
if sh -c ': >> "$1"' ignored "$2" 2>/dev/null; then exit 73; fi
if sh -c ': >> "$1"' ignored "$3" 2>/dev/null; then exit 74; fi
if rm "$3" 2>/dev/null; then exit 75; fi
if cat "$6/credential-fixture" >/dev/null 2>&1; then exit 76; fi
cat "$2" >/dev/null
cat "$3" >/dev/null
printf 'synthetic notification frame\n' > "$5"
"#;
let mut args = args;
args.extend(extra);
args.extend(["sandbox", "-P", profile, "-C"].map(str::to_owned));
args.push(workspace.to_string_lossy().into_owned());
args.extend(["--", "/bin/sh", "-c", script, "notification-sandbox"].map(str::to_owned));
args.extend(
[
&config.state_dir,
&paths[1],
&paths[2],
&workspace,
&paths[3],
&home,
]
.map(|path| path.to_string_lossy().into_owned()),
);
if let Some(path) = std::env::var_os("CODEX_NOTIFICATION_FIXTURE_EXPORT") {
fs::write(
path,
serde_json::to_vec_pretty(&json!({
"args": args, "stateDirectory": config.state_dir,
"pipe": server.pipe.0, "home": home, "workspace": workspace,
"descriptor": fs::read_to_string(&paths[1]).unwrap(),
}))
.unwrap(),
)
.unwrap();
return;
}
let command = std::env::var_os("CODEX_NOTIFICATION_SANDBOX_COMMAND").unwrap();
let output = Command::new(command)
.args(args)
.current_dir(&workspace)
.env_clear()
.env("PATH", std::env::var_os("PATH").unwrap())
.env("HOME", &home)
.env("CODEX_HOME", &home)
.env("UNRELATED_SECRET", "must-not-reach-command")
.output()
.unwrap();
assert!(
output.status.success(),
"source-pinned sandbox failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
@@ -0,0 +1,27 @@
import { BaseSequencer } from "vitest/node";
const slowFiles = [
"runnerd-codex-transport.test.ts",
"runnerd-codex-transport-settlement.test.ts",
"cursor-cloud-runner-launch.test.ts",
"runnerd-codex-transport-deadlines.test.ts",
"opencode-server-driver.test.ts",
"acpx-controller-recovery.test.ts",
"runnerd-codex-transport-warm-attachment.test.ts",
"runnerd-opencode-streaming-tools.test.ts",
"live-session.test.ts",
"runnerd-codex-transport-adoption.test.ts",
];
const priority = (file) => {
const name = file.moduleId.replaceAll("\\", "/").split("/").at(-1);
const index = slowFiles.indexOf(name);
return index < 0 ? slowFiles.length : index;
};
export class RunnerSequencer extends BaseSequencer {
async sort(files) {
const sorted = await super.sort(files);
return sorted.sort((a, b) => priority(a) - priority(b));
}
}
@@ -0,0 +1,61 @@
import assert from "node:assert/strict";
import { test } from "vitest";
import { BaseSequencer } from "vitest/node";
import { RunnerSequencer } from "./vitest-sequencer.mjs";
const project = { name: "runner", config: { isolate: true, sequence: { groupOrder: 0 } } };
const files = [
"large.test.ts",
"runnerd-codex-transport.test.ts",
"small.test.ts",
"runnerd-codex-transport-settlement.test.ts",
"runnerd-codex-transport-deadlines.test.ts",
].map((name) => ({ moduleId: `/repo/src/live/${name}`, project }));
const ctx = {
config: { root: "/repo", shard: { index: 1, count: 2 } },
cache: {
getFileTestResults: () => undefined,
getFileStats: (key) => ({ size: 100 - files.findIndex((file) => key.endsWith(file.moduleId.slice(6))) }),
},
};
test("starts real-wait transport files first without changing test membership", async () => {
const sorted = await new RunnerSequencer(ctx).sort(files);
assert.deepEqual(sorted, [files[1], files[3], files[4], files[0], files[2]]);
assert.equal(new Set(sorted).size, files.length);
assert.deepEqual(await new BaseSequencer(ctx).sort(files), files);
});
test("selected subsets stay selected and retain default ordering", async () => {
const selected = [files[2], files[0]];
assert.deepEqual(await new RunnerSequencer(ctx).sort(selected), await new BaseSequencer(ctx).sort(selected));
assert.deepEqual(await new RunnerSequencer(ctx).sort([files[4]]), [files[4]]);
assert.deepEqual(await new RunnerSequencer(ctx).sort([]), []);
});
test("keeps Vitest shard membership and schedules waits within each shard", async () => {
const all = [];
for (const index of [1, 2]) {
const shardCtx = { ...ctx, config: { ...ctx.config, shard: { index, count: 2 } } };
const sequencer = new RunnerSequencer(shardCtx);
const shard = await sequencer.shard(files);
assert.deepEqual(shard, await new BaseSequencer(shardCtx).shard(files));
const sorted = await sequencer.sort(shard);
assert.deepEqual(new Set(sorted), new Set(shard));
all.push(...sorted);
}
assert.equal(all.length, files.length);
assert.deepEqual(new Set(all), new Set(files));
});
test("recognizes Windows paths", async () => {
const deadline = { ...files[4], moduleId: "C:\\repo\\src\\live\\runnerd-codex-transport-deadlines.test.ts" };
assert.deepEqual(await new RunnerSequencer(ctx).sort([files[0], deadline]), [deadline, files[0]]);
});
test("starts measured slow provider files before short files without adding work", async () => {
const streaming = { ...files[0], moduleId: "/repo/src/live/runnerd-opencode-streaming-tools.test.ts" };
const launch = { ...files[0], moduleId: "/repo/src/control-plane/cursor-cloud-runner-launch.test.ts" };
assert.deepEqual(await new RunnerSequencer(ctx).sort([files[0], streaming, files[4], launch]),
[launch, files[4], streaming, files[0]]);
});
@@ -1,6 +1,7 @@
import { describe, expect, it, vi } from "vitest";
import type { HarnessDriver, HarnessSession, PersistedHarnessSession } from "../contracts/harness-driver.js";
import { PROVIDER_STATE_INCOMPATIBLE, runnerRecoveryFailureClass } from "../contracts/harness-driver.js";
import type { PrpEvent, PrpStructuredRunResult, PrpTerminalState } from "../protocol/replay-contract.js";
import { NativeSessionProtocolIntegrityError } from "../contracts/native-session-backend.js";
import { HarnessDriverBackend } from "./harness-driver-backend.js";
@@ -323,6 +324,36 @@ describe("HarnessDriverBackend", () => {
expect(recoveredDispositionAllowance).toBe(true);
});
it("keeps the runner's closed class of a failed recovery", async () => {
const commandFailure = Object.defineProperty(new Error("PRP command run.attach failed"), "runnerCommandResult", {
value: { status: "failed", commandType: "run.attach",
result: { code: "command_execution_failed", message: "saved launch profile changed",
classification: PROVIDER_STATE_INCOMPATIBLE } },
enumerable: false,
});
expect(runnerRecoveryFailureClass(commandFailure)).toBe(PROVIDER_STATE_INCOMPATIBLE);
expect(runnerRecoveryFailureClass(new Error("wrapped", { cause: commandFailure }))).toBe(PROVIDER_STATE_INCOMPATIBLE);
expect(runnerRecoveryFailureClass(new Error(`text only: ${PROVIDER_STATE_INCOMPATIBLE}`))).toBeUndefined();
const unclassified = Object.defineProperty(new Error("PRP command run.attach failed"), "runnerCommandResult", {
value: { status: "failed", result: { code: "command_execution_failed", classification: "other" } },
enumerable: false,
});
expect(runnerRecoveryFailureClass(unclassified)).toBeUndefined();
const backend = new HarnessDriverBackend({
...driver,
recoverSession: async () => ({ recovered: false, reason: String(commandFailure),
failureClass: runnerRecoveryFailureClass(commandFailure) }),
});
await expect(backend.recoverSession({
backendKind: "runner", driverKind: "fake", sessionId: "driver-1", providerSessionId: "provider-1",
providerIdentity,
identity: { runId: "run-2", sessionId: "session-1", companyId: "company-1", issueId: "issue-1", agentId: "agent-1" },
}, { signal: new AbortController().signal })).resolves.toEqual({
recovered: false, reason: "Error: PRP command run.attach failed", failureClass: PROVIDER_STATE_INCOMPATIBLE,
});
});
it("preserves a consumed disposition allowance in native snapshots", async () => {
class ConsumedRecoverySession extends FakeHarnessSession {
override async snapshot(): Promise<PersistedHarnessSession> {
@@ -162,7 +162,11 @@ export class HarnessDriverBackend implements NativeSessionBackend {
recoveryOptions,
);
if (!recovered.recovered || recovered.session === undefined) {
return { recovered: false, reason: recovered.reason };
return {
recovered: false,
reason: recovered.reason,
...(recovered.failureClass ? { failureClass: recovered.failureClass } : {}),
};
}
try {
assertProviderSessionIdentity(
@@ -496,10 +496,29 @@ export interface PersistedHarnessSession {
| "allow_replacement_after_resume_failure";
}
/** Runner proof that saved provider state cannot be restored by this runner
* build. Restoring the same state again fails the same way. */
export const PROVIDER_STATE_INCOMPATIBLE = "provider_state_incompatible";
export interface HarnessSessionRecoveryResult {
recovered: boolean;
session?: HarnessSession;
reason?: string;
/** Closed runner class of a deterministic recovery failure. */
failureClass?: typeof PROVIDER_STATE_INCOMPATIBLE;
}
/** Reads the closed class of an authenticated runner command failure. */
export function runnerRecoveryFailureClass(
error: unknown,
depth = 0,
): typeof PROVIDER_STATE_INCOMPATIBLE | undefined {
if (!error || typeof error !== "object" || depth > 5) return undefined;
const value = error as Record<string, unknown>;
const result = value.runnerCommandResult as Record<string, unknown> | undefined;
const detail = result?.result as Record<string, unknown> | undefined;
if (detail?.classification === PROVIDER_STATE_INCOMPATIBLE) return PROVIDER_STATE_INCOMPATIBLE;
return runnerRecoveryFailureClass(value.cause, depth + 1);
}
export interface HarnessSession {
@@ -15,6 +15,7 @@ import type {
HarnessRuntimeRequestHandoff,
HarnessRuntimeRequestResolution,
HarnessGoalOperation,
HarnessSessionRecoveryResult,
HarnessThreadGoal,
HarnessThreadLineageEntry,
NativeRuntimeContextCapabilities,
@@ -82,6 +83,7 @@ export interface NativeSessionRecoveryResult {
recovered: boolean;
session?: NativeSession;
reason?: string;
failureClass?: HarnessSessionRecoveryResult["failureClass"];
}
/** Runner-owned cause: the process was restored, but its active turn was lost. */
@@ -278,6 +280,17 @@ export interface NativeSessionBackend {
): Promise<NativeSessionRecoveryResult>;
}
/** Same-session recovery is forbidden here, and the runner proved that the
* saved provider session cannot be restored by its build. Retrying the same
* run cannot succeed. */
export class NativeProviderStateIncompatibleError extends Error {
readonly code = "native_provider_state_incompatible";
constructor(reason: string) {
super(`native_session_recovery_failed: ${reason}`);
this.name = "NativeProviderStateIncompatibleError";
}
}
/** A provider failed terminal is not a missing completion proposal. */
export class NativeProviderTerminalFailure extends Error {
readonly code = "native_provider_terminal_failed";
@@ -20,7 +20,7 @@ import type {
PersistedHarnessTurnTerminal,
} from "../../contracts/harness-driver.js";
import { NativeSessionProtocolIntegrityError, nativeRestartInterruptedTurnId } from "../../contracts/native-session-backend.js";
import { HarnessReconciliationError } from "../../contracts/harness-driver.js";
import { HarnessReconciliationError, runnerRecoveryFailureClass } from "../../contracts/harness-driver.js";
import {
CODEX_CODEX_PROTOCOL_VERSION,
CODEX_SKILLLESS_BASE_INSTRUCTIONS,
@@ -677,7 +677,12 @@ export class CodexAppServerDriver implements HarnessDriver {
await cancellation.close().catch(() => {});
if (error instanceof NativeSessionProtocolIntegrityError) throw error;
if (options.signal.aborted) options.signal.throwIfAborted();
return { recovered: false, reason: redactCodexDiagnostic(String(error)) };
const failureClass = runnerRecoveryFailureClass(error);
return {
recovered: false,
reason: redactCodexDiagnostic(String(error)),
...(failureClass ? { failureClass } : {}),
};
} finally {
cancellation.detach();
}
@@ -994,21 +994,32 @@ describe("Cursor Cloud native driver", () => {
try {
const turn = await f.session.startTurn({ message: { role: "user", text: rejection === "unsent_payload" ? "\u0001".repeat(400_000) : "Rejected task" } });
await entered.promise;
const observeHeldCheckpoint = async () => {
const checkpointPath = join(f.root, (await readdir(f.root)).find(name => /^checkpoint-.*\.json$/.test(name))!);
expect(JSON.parse(await readFile(checkpointPath, "utf8")).phase).not.toBe("idle");
return checkpointPath;
};
let checkpointPath: string;
if (overlap === "stop_write") {
stop(); await stopHeld.promise;
rejectCreate.resolve(); await new Promise<void>(accept => setImmediate(accept));
// The Stop checkpoint gate still holds its cancellation clock here.
checkpointPath = await observeHeldCheckpoint();
releaseStop.resolve();
await waitFor(() => pausedRejection || stopDone);
await Promise.race([rejectionHeld.promise, stopped!]);
expect(pausedRejection, "the definite rejection must reach durable settlement even while stopping").toBe(true);
} else {
rejectCreate.resolve(); await rejectionHeld.promise;
// File observations must not consume Stop's 300ms settlement budget.
checkpointPath = await observeHeldCheckpoint();
stop(); await new Promise<void>(accept => setImmediate(accept));
}
expect(stopDone, "Stop must wait for the rejected-create checkpoint").toBe(false);
expect(await f.session.read()).toMatchObject({ phase: "stopping" });
await expect(f.session.startTurn({ message: { role: "user", text: "Too early" } })).rejects.toThrow("unresolved remote work");
const checkpointPath = join(f.root, (await readdir(f.root)).find(name => /^checkpoint-.*\.json$/.test(name))!);
expect(JSON.parse(await readFile(checkpointPath, "utf8")).phase).not.toBe("idle");
expect(peer.getAgent).toHaveBeenCalledTimes(before.getAgent);
expect(peer.getRun).toHaveBeenCalledTimes(before.getRun);
expect(peer.cancelRun).toHaveBeenCalledTimes(before.cancel);
releaseRejection.resolve();
expect(await stopped).toEqual({ error: null });
await waitFor(() => f.events.some(event => event.turnId === turn.turnId && event.eventType === "turn.failed" && event.payload.remoteWorkAccepted === false));
@@ -0,0 +1,556 @@
import {
createRunnerProviderLaunch,
runnerLaunchRequirementsDigest,
type RunnerProviderLaunchRequirements,
} from "./runner-provider-launch.js";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { spawn } from "node:child_process";
import { createServer } from "node:http";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { expect, it, vi } from "vitest";
import { NativeSessionCloseUnrecoverableError } from "../contracts/native-session-backend.js";
import { DurablePrpControlPlane } from "../control-plane/durable-prp-control-plane.js";
import * as durableControlPlane from "../control-plane/durable-prp-control-plane.js";
import { codexSemanticToolSpecs } from "../drivers/codex/codex-app-server-driver.js";
import { createCapabilityRunnerdCodexTransport, readRunnerdArtifactBinding } from "./runnerd-codex-transport.js";
import { defaultCapabilityRunnerdBinary, fakeCodex, fakeCodexArgs } from "../../test/support/runnerd-transport.js";
async function verifyLiveRunnerAdoption(
mismatchedCheckpoint: boolean,
mismatchedArtifact = false,
goalMidTurn = false,
detachBeforeCleanup = false,
startWithoutCheckpoint = false,
preparedTask = false,
adoptionAuthorization: "allow" | "deny" | "legacy" | "legacy-missing" | "missing-binding" = "allow",
toolHandlerOrder?: "before" | "after" | "completion-before" | "completion-after",
uncertainAtClose = false,
semanticRecoveryMode?: "denied-before-cancel" | "cleanup-only",
stopExitProofFault?: "missing" | "false",
) {
const stateDirectory = await mkdtemp(join(tmpdir(), "runnerd-live-adopt-"));
const pendingCompletion = toolHandlerOrder?.startsWith("completion-") === true;
const completionPath = join(stateDirectory, "pending-completion.json");
if (pendingCompletion) await writeFile(completionPath, JSON.stringify({
schema: "paperclip.run_result.v1", reportedWorkDisposition: "done", summary: "Completed the synthetic work",
completionClaim: { contractRevision: "1", objectiveSatisfied: true, criteria: [], remainingWork: [] },
evidence: [], verification: [], attentionRequests: [], artifacts: [],
}));
const server = createServer();
let authority: DurablePrpControlPlane | null = null;
const checkpoint = vi.fn(async () => undefined);
server.on("upgrade", (request, socket, head) => {
if (!authority) {
socket.destroy();
return;
}
authority.handleUpgrade(request, socket, "/runner", head);
});
await new Promise<void>((resolveListen) =>
server.listen(0, "127.0.0.1", resolveListen),
);
const address = server.address();
if (!address || typeof address === "string")
throw new Error("Expected adoption test listener");
const registration = vi.fn(async (next: DurablePrpControlPlane) => {
authority = next;
return {
connectUrl: `ws://127.0.0.1:${address.port}/runner`,
...(mismatchedArtifact || uncertainAtClose || semanticRecoveryMode === "cleanup-only" ? { checkpoint } : {}),
release: async () => {
if (authority === next) authority = null;
},
};
});
const identity = {
runnerInstanceId: "runner-live-adopt",
environmentLeaseId: "lease-live-adopt",
runId: "run-live-adopt",
normalizedSessionId: "session-live-adopt",
turnId: "turn-live-adopt",
itemId: "item-live-adopt",
};
const sharedOptions = {
runnerBinary: defaultCapabilityRunnerdBinary(),
codexCommand: fakeCodex,
codexArgs: fakeCodexArgs(stateDirectory, ...(pendingCompletion ? ["--terminal-result-file", completionPath] : toolHandlerOrder ? ["--emit-tool-call"] : []), ...(goalMidTurn ? ["--goal-autostart", "--goal-item-trigger", join(stateDirectory, "emit-goal-item")] : [])),
stateDirectory,
prpIdentity: identity,
lifecyclePolicy: { mode: "warm" as const, idleTimeoutMs: 60_000 },
controlPlaneRegistration: registration,
};
const prepareLaunch = vi.fn(async (requirements: RunnerProviderLaunchRequirements) => ({
configurationId: "adopted-preparation", requirementsDigest: runnerLaunchRequirementsDigest(requirements),
provider: createRunnerProviderLaunch(requirements, { options: { codexArgs: sharedOptions.codexArgs },
root: stateDirectory, cwd: tmpdir(), codexHome: join(stateDirectory, "codex-home"),
codexCommand: fakeCodex, providerNodeCommand: process.execPath, opencodeProxyPath: "", acpxSidecarPath: "" }),
}));
const options = { ...sharedOptions, ...(preparedTask ? {
prepareLaunch, runnerArtifact: readRunnerdArtifactBinding(defaultCapabilityRunnerdBinary()),
runnerProcessLauncher: (spec: durableControlPlane.RunnerProcessLaunchSpec) => {
const child = spawn(spec.command, [...spec.args], { cwd: spec.cwd, env: { ...process.env, ...spec.environment },
detached: process.platform !== "win32", stdio: "ignore" });
return { child, processGroupId: child.pid ?? null, completion: new Promise<{ code: number | null; signal: NodeJS.Signals | null; stdout: string; stderr: string }>((resolveExit, reject) => {
child.once("error", reject);
child.once("exit", (code, signal) => resolveExit({ code, signal, stdout: "", stderr: "" }));
}) };
},
} : {}) };
const first = createCapabilityRunnerdCodexTransport(options);
const pendingTool = vi.fn(() => new Promise<Record<string, unknown>>(() => undefined));
if (toolHandlerOrder) first.transport.setServerRequestHandler(pendingTool);
let releaseRecovery!: () => void;
const recoveryGate = new Promise<void>(resolve => { releaseRecovery = resolve; });
const recoveryDenied = new Error("execution authority revoked after Stop");
const recoveredTool = vi.fn(async () => {
if (semanticRecoveryMode) {
// Model server authority denying replay after Stop cleared the task lock.
// The gate holds that denial until reattachment has returned its session,
// before the caller invokes the eventual onSession cancellation callback.
await recoveryGate;
throw recoveryDenied;
}
return { result: { receipt: "known-outcome" }, isError: false };
});
const redispatchedTool = vi.fn(async () => ({ success: true, contentItems: [] }));
let runnerPid: number | null = null;
let runnerStartedAt: string | null = null;
let adopted: ReturnType<typeof createCapabilityRunnerdCodexTransport> | null =
null;
let restoreIntervalSpies: () => void = () => undefined;
try {
let opened: Record<string, unknown>;
try {
opened = await first.transport.request("thread/start", {
cwd: tmpdir(),
dynamicTools: toolHandlerOrder && !pendingCompletion ? [{ name: "get_task_context", description: "Read current task", inputSchema: { type: "object", properties: {}, additionalProperties: false } }] : codexSemanticToolSpecs(),
});
} catch (error) {
const stderr = await readFile(
join(stateDirectory, "diagnostics", "runnerd.stderr.log"),
"utf8",
).catch(() => "");
throw new Error(
`${String(error)}\n${JSON.stringify(first.evidence())}${stderr ? `\n${stderr}` : ""}`,
);
}
runnerPid = first.evidence().runnerPid;
runnerStartedAt = first.transport.processInfo().startedAt;
expect(runnerPid).toEqual(expect.any(Number));
if (goalMidTurn) {
await first.transport.request("thread/goal/set", { objective: "Recover a live goal", status: "active" });
for await (const event of first.transport.notifications()) {
if (event.method === "turn/started") break;
}
}
if (toolHandlerOrder) {
await first.transport.request("turn/start", { input: [{ type: "text", text: "Read current task" }] });
await vi.waitFor(() => expect(pendingTool).toHaveBeenCalledOnce(), { timeout: 5_000 });
}
await first.detachControllerForRestart();
expect(() => process.kill(runnerPid!, 0)).not.toThrow();
if (goalMidTurn) {
await writeFile(join(stateDirectory, "emit-goal-item"), "emit");
// runnerd need not poll the provider into its PRP outbox while disconnected.
// Wait for flushed provider output, not a platform-dependent final poll
// racing the disconnect. Adoption must still bind that buffered item.
await vi.waitFor(async () => {
expect(await readFile(join(stateDirectory, "emit-goal-item.sent"), "utf8")).toBe("sent");
}, { timeout: 5_000 });
}
const controlPlaneStatePath = join(
stateDirectory,
"control-plane",
"control-plane-state.json",
);
const compactProviderIdentityEvents = async () => {
const controlPlaneState = JSON.parse(
await readFile(controlPlaneStatePath, "utf8"),
) as { committedEvents: Array<{ eventType: string }> };
controlPlaneState.committedEvents =
controlPlaneState.committedEvents.filter(
(event) =>
event.eventType !== "harness.ready" &&
event.eventType !== "session.started" &&
event.eventType !== "session.resumed",
);
await writeFile(
controlPlaneStatePath,
`${JSON.stringify(controlPlaneState, null, 2)}\n`,
{ mode: 0o600 },
);
};
if (!startWithoutCheckpoint) await compactProviderIdentityEvents();
if (preparedTask) {
if (adoptionAuthorization !== "legacy") await rm(join(stateDirectory, "launch-preparation.json"));
prepareLaunch.mockClear();
registration.mockClear();
if (adoptionAuthorization === "missing-binding") {
const retained = JSON.parse(await readFile(controlPlaneStatePath, "utf8"));
delete retained.preparedLaunchConfigurations;
await writeFile(controlPlaneStatePath, JSON.stringify(retained), { mode: 0o600 });
}
}
const authorizeRunnerAdoption = vi.fn(async () => {
expect(registration).not.toHaveBeenCalled();
if (adoptionAuthorization === "deny") throw new Error("project grant revoked");
});
const duplicateLauncher = vi.fn(() => {
throw new Error("duplicate runner spawn attempted");
});
const openedThread = opened.thread as Record<string, unknown>;
const signal = vi.fn(() => true);
const probeError = new Error("fixture remote process identity is unavailable");
let rejectLiveness = false;
const intervals = uncertainAtClose ? vi.spyOn(globalThis, "setInterval") : null;
const clearedIntervals = uncertainAtClose ? vi.spyOn(globalThis, "clearInterval") : null;
restoreIntervalSpies = () => { intervals?.mockRestore(); clearedIntervals?.mockRestore(); };
adopted = createCapabilityRunnerdCodexTransport({
...options,
...(preparedTask && !adoptionAuthorization.startsWith("legacy") ? { authorizeRunnerAdoption } : {}),
...(uncertainAtClose ? { closeGraceMs: 2_000 } : {}),
...(semanticRecoveryMode === "cleanup-only" ? { recoveryPurpose: "stop_cleanup" as const } : {}),
// Hash different stable bytes without replacing the real runner artifact
// used by concurrent tests. Adoption must never execute this path.
...(mismatchedArtifact
? {
runnerBinary: resolve(import.meta.dirname, "../../package.json"),
runnerReconnectGraceMs: 150,
}
: {}),
...(toolHandlerOrder && !pendingCompletion ? { recoverSemanticToolInput: recoveredTool } : {}),
resumeDynamicTools: pendingCompletion ? codexSemanticToolSpecs() : toolHandlerOrder ? [{ name: "get_task_context", description: "Read current task", inputSchema: { type: "object", properties: {}, additionalProperties: false } }] : [],
resumeProviderSession: startWithoutCheckpoint ? undefined : {
driverSessionId: String(openedThread.id),
providerSessionId: mismatchedCheckpoint
? "wrong-provider-session"
: String(openedThread.sessionId),
},
runnerProcessLauncher: duplicateLauncher,
adoptExistingRunner: {
pid: runnerPid!,
processGroupId: runnerPid,
startedAt: runnerStartedAt,
signal,
isAlive: () => {
if (rejectLiveness) return Promise.reject(probeError);
try {
process.kill(runnerPid!, 0);
return true;
} catch {
return false;
}
},
},
});
if (preparedTask && ["deny", "legacy-missing", "missing-binding"].includes(adoptionAuthorization)) {
await expect(adopted.transport.request("thread/read", {})).rejects.toThrow(
adoptionAuthorization === "deny" ? "project grant revoked" : adoptionAuthorization === "legacy-missing"
? "runner_launch_recovery_requirements_unavailable" : "runner_launch_recovery_configuration_unavailable");
expect(registration).not.toHaveBeenCalled();
expect(prepareLaunch).not.toHaveBeenCalled();
expect(duplicateLauncher).not.toHaveBeenCalled();
expect(signal).not.toHaveBeenCalled();
expect(() => process.kill(runnerPid!, 0)).not.toThrow();
expect(authorizeRunnerAdoption).toHaveBeenCalledTimes(adoptionAuthorization === "deny" ? 1 : 0);
return;
}
if (mismatchedArtifact) {
await expect(
adopted.transport.request("thread/read", {}),
).rejects.toThrow("native_adopted_runner_authentication_timeout");
expect(authority?.activeRunnerConnectionCount()).toBe(0);
await expect(
adopted.transport.request("turn/start", {
input: [{ type: "text", text: "must not be dispatched" }],
}),
).rejects.toThrow("native_adopted_runner_authentication_timeout");
await adopted.transport.close();
expect(signal).not.toHaveBeenCalled();
expect(checkpoint).not.toHaveBeenCalled();
expect(duplicateLauncher).not.toHaveBeenCalled();
expect(() => process.kill(runnerPid!, 0)).not.toThrow();
const retained = JSON.parse(
await readFile(controlPlaneStatePath, "utf8"),
) as {
identity: unknown;
commands: Array<{ type: string; status: string }>;
};
expect(retained.identity).toEqual(identity);
expect(
retained.commands.some(
(command) =>
command.type === "runner.drain" && command.status === "pending",
),
).toBe(true);
expect(retained.commands.map((command) => command.type)).not.toEqual(
expect.arrayContaining(["runner.suspend"]),
);
expect(retained.commands.map((command) => command.type)).not.toEqual(
expect.arrayContaining(["turn.stop"]),
);
return;
}
if (mismatchedCheckpoint) {
await expect(
adopted.transport.request("thread/read", {}),
).rejects.toThrow("native_adopted_provider_identity_mismatch");
expect(duplicateLauncher).not.toHaveBeenCalled();
expect(() => process.kill(runnerPid!, 0)).not.toThrow();
return;
}
if (toolHandlerOrder?.endsWith("before")) adopted.transport.setServerRequestHandler(redispatchedTool);
await expect(adopted.transport.request(startWithoutCheckpoint ? "thread/start" : "thread/read", {})).resolves.toEqual(
expect.objectContaining({
thread: expect.objectContaining({ id: "codex-thread-1" }),
}),
);
if (toolHandlerOrder?.endsWith("after")) adopted.transport.setServerRequestHandler(redispatchedTool);
if (semanticRecoveryMode) {
const retained = authority as DurablePrpControlPlane | null;
expect(retained).not.toBeNull();
expect(retained!.store.state.committedEvents.some(event => event.eventType === "semantic_tool.input")).toBe(true);
expect(retained!.store.state.commands.filter(command => command.type === "turn.start")).toHaveLength(1);
expect(retained!.store.state.commands.some(command => command.type === "turn.interrupt")).toBe(false);
if (semanticRecoveryMode === "denied-before-cancel") {
await vi.waitFor(() => expect(recoveredTool).toHaveBeenCalledOnce());
// This is a real Core replay callback, not an artificial transport error.
// Deliver its denial before giving the caller its cancellation turn.
releaseRecovery();
await vi.waitFor(async () => {
await expect(adopted!.transport.request("thread/read", {})).rejects.toBe(recoveryDenied);
});
await expect(adopted.transport.request("turn/interrupt", {})).rejects.toBe(recoveryDenied);
expect(retained!.store.state.commands.some(command => command.type === "turn.interrupt")).toBe(false);
} else {
// Let all startup/handler microtasks and a real reconciliation read run.
// Cleanup must not enter replay even while its authority would deny it.
await adopted.transport.request("thread/read", {});
expect(recoveredTool).not.toHaveBeenCalled();
expect(redispatchedTool).not.toHaveBeenCalled();
releaseRecovery();
await expect(adopted.transport.request("turn/interrupt", {})).resolves.toEqual({});
expect(retained!.store.state.commands.filter(command => command.type === "turn.interrupt")).toMatchObject([{ status: "completed" }]);
}
expect(redispatchedTool).not.toHaveBeenCalled();
expect(retained!.store.state.commands.some(command => command.type === "semantic_tool.result")).toBe(false);
expect(retained!.store.state.commands.filter(command => command.type === "turn.start")).toHaveLength(1);
expect(duplicateLauncher).not.toHaveBeenCalled();
if (semanticRecoveryMode === "cleanup-only") {
const faultedStopProof = vi.fn();
if (stopExitProofFault) {
const queue = retained!.queueCommand.bind(retained!);
vi.spyOn(retained!, "queueCommand").mockImplementation((type, ...args) => {
const command = queue(type, ...args);
if (type === "turn.stop") {
let result = command.result;
Object.defineProperty(command, "result", {
enumerable: true,
configurable: true,
get: () => result,
set: value => {
const receipt = value as { result?: Record<string, unknown> } | null;
if (receipt?.result && typeof receipt.result === "object") {
faultedStopProof(receipt.result.providerExitConfirmed);
const stopped = { ...receipt.result };
if (stopExitProofFault === "missing") delete stopped.providerExitConfirmed;
else stopped.providerExitConfirmed = false;
result = { ...receipt, result: stopped };
} else result = value;
},
});
}
return command;
});
await expect(adopted.transport.close()).rejects.toBeInstanceOf(NativeSessionCloseUnrecoverableError);
expect(faultedStopProof).toHaveBeenCalledWith(true);
expect(checkpoint).toHaveBeenCalledExactlyOnceWith("unsettled");
} else {
await expect(adopted.transport.close()).resolves.toBeUndefined();
expect(checkpoint).toHaveBeenCalledExactlyOnceWith("settled");
expect(retained!.store.state.commands.filter(command => command.type === "turn.stop")).toMatchObject([{
status: "completed", result: { result: { providerExitConfirmed: true } },
}]);
}
expect(retained!.semanticToolResultsSettled()).toBe(false);
expect(retained!.store.state.commands.some(command => command.type === "semantic_tool.result")).toBe(false);
expect(retained!.store.state.committedEvents.some(event => event.eventType === "semantic_tool.input")).toBe(true);
if (!stopExitProofFault) {
expect(retained!.store.state.commands.filter(command => command.type === "runner.suspend")).toMatchObject([{ status: "completed" }]);
const runnerState = JSON.parse(await readFile(join(stateDirectory, "runner", "runner-state.json"), "utf8"));
expect(runnerState).toMatchObject({ ...identity, lifecycle: "suspended" });
}
const providerState = JSON.parse(await readFile(join(stateDirectory, "runner", "codex-provider-state.json"), "utf8"));
expect(providerState).toMatchObject({ activeProviderTurnId: null, pendingEvents: [], queuedEvents: [] });
expect(recoveredTool).not.toHaveBeenCalled();
expect(redispatchedTool).not.toHaveBeenCalled();
}
return;
}
if (toolHandlerOrder) {
await vi.waitFor(() => {
expect(pendingCompletion ? redispatchedTool : recoveredTool).toHaveBeenCalledOnce();
const result = authority!.store.state.commands.find(command => command.type === "semantic_tool.result");
expect(result?.status).toBe("completed");
}, { timeout: 5_000 });
if (pendingCompletion) {
expect(recoveredTool).not.toHaveBeenCalled();
expect(redispatchedTool).toHaveBeenCalledWith(expect.objectContaining({ params: expect.objectContaining({ tool: "paperclip_finish" }) }));
} else expect(redispatchedTool).not.toHaveBeenCalled();
}
expect(adopted.evidence().runnerPid).toBe(runnerPid);
expect(adopted.transport.processInfo().startedAt).toBe(runnerStartedAt);
if (startWithoutCheckpoint) {
const retained = JSON.parse(await readFile(controlPlaneStatePath, "utf8"));
for (const type of ["run.prepare", "session.open"]) {
expect(retained.commands.filter((command: { type: string }) => command.type === type)).toHaveLength(1);
}
}
if (goalMidTurn) {
const observed = await Promise.race([
(async () => {
for await (const notification of adopted!.transport.notifications()) {
if (notification.method === "item/started") return notification;
}
throw new Error("recovered goal item was lost");
})(),
new Promise<never>((_, reject) => setTimeout(() => reject(new Error("recovered item timed out")), 5_000)),
]);
expect(observed.params).toMatchObject({ threadId: "codex-thread-1", turnId: "provider-goal-turn-1" });
}
expect(duplicateLauncher).not.toHaveBeenCalled();
if (preparedTask) {
if (adoptionAuthorization === "legacy") {
expect(prepareLaunch).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ identity }), expect.objectContaining({ configurationId: "adopted-preparation" }));
expect(authorizeRunnerAdoption).not.toHaveBeenCalled();
} else {
expect(prepareLaunch).not.toHaveBeenCalled();
expect(authorizeRunnerAdoption).toHaveBeenCalledExactlyOnceWith("adopted-preparation", identity);
}
expect((await adopted.transport.request("thread/read", {})).thread).toMatchObject({ cwd: tmpdir() });
}
if (uncertainAtClose) {
const before = JSON.parse(await readFile(controlPlaneStatePath, "utf8"));
const retainedAuthority = authority as DurablePrpControlPlane | null;
rejectLiveness = true;
await expect(adopted.transport.close()).rejects.toBe(probeError);
expect(retainedAuthority?.activeRunnerConnectionCount()).toBe(0);
expect(intervals!.mock.results.length).toBeGreaterThan(0);
for (const interval of intervals!.mock.results) if (interval.type === "return") expect(clearedIntervals).toHaveBeenCalledWith(interval.value);
const notificationsClosed = (async () => { for await (const _event of adopted!.transport.notifications()) {} return true; })();
let queueDeadline: ReturnType<typeof setTimeout> | undefined;
try {
expect(await Promise.race([notificationsClosed, new Promise<boolean>(resolve => { queueDeadline = setTimeout(() => resolve(false), 250); })])).toBe(true);
} finally { clearTimeout(queueDeadline); }
await expect(adopted.transport.close()).rejects.toBe(probeError);
expect(adopted.transport.processInfo()).toMatchObject({ exited: false });
expect(() => process.kill(runnerPid!, 0)).not.toThrow();
expect(signal).not.toHaveBeenCalled();
expect(checkpoint).not.toHaveBeenCalled();
expect(duplicateLauncher).not.toHaveBeenCalled();
const retained = JSON.parse(await readFile(controlPlaneStatePath, "utf8"));
expect(retained.identity).toEqual(before.identity);
expect(retained.commands).toEqual(before.commands);
return;
}
expect(adopted.evidence().diagnostics).toContain(
`adopted runner ${runnerPid} authenticated to its durable PRP authority`,
);
if (!startWithoutCheckpoint) {
expect(adopted.evidence().diagnostics).toContain(
"restored adopted provider identity from the exact durable checkpoint after PRP event compaction; awaiting live confirmation",
);
expect(adopted.evidence().diagnostics).toContain(
"confirmed adopted provider identity against authenticated recovery session.snapshot",
);
}
if (detachBeforeCleanup) {
await adopted.detachControllerForRestart();
await adopted.transport.close("old controller finalizer");
expect(signal).not.toHaveBeenCalled();
expect(() => process.kill(runnerPid!, 0)).not.toThrow();
const retained = JSON.parse(await readFile(controlPlaneStatePath, "utf8"));
const commandTypes = retained.commands.map((command: { type: string }) => command.type);
expect(commandTypes).not.toContain("turn.stop");
expect(commandTypes).not.toContain("runner.suspend");
}
} finally {
releaseRecovery();
await adopted?.transport.close().catch(() => undefined);
if (uncertainAtClose) {
const providerPid = adopted?.evidence().codexPid;
if (providerPid) {
try { process.kill(providerPid, "SIGKILL"); }
catch (error) { if ((error as NodeJS.ErrnoException).code !== "ESRCH") throw error; }
}
await (authority as DurablePrpControlPlane | null)?.stop();
}
restoreIntervalSpies();
if (runnerPid) {
try {
process.kill(-runnerPid, "SIGKILL");
} catch {
// The adopted runner normally exits after its durable suspend command.
}
}
server.closeAllConnections();
if (server.listening) {
await new Promise<void>((resolveClose) =>
server.close(() => resolveClose()),
);
}
await rm(stateDirectory, { recursive: true, force: true });
}
}
it(
"adopts a live runner on the same durable authority without spawning a duplicate",
() => verifyLiveRunnerAdoption(false),
30_000,
);
it("adopts a prepared runner after losing its controller launch cache without preparing resources again",
() => verifyLiveRunnerAdoption(false, false, false, false, false, true), 30_000);
it.each(["deny", "legacy", "legacy-missing", "missing-binding"] as const)("preserves adoption authorization with %s",
mode => verifyLiveRunnerAdoption(false, false, false, false, false, true, mode), 30_000);
it("retains authenticated adopted-runner ownership when a close probe rejects", () => verifyLiveRunnerAdoption(false, false, false, false, false, false, "allow", undefined, true), 30_000);
it(
"blocks adopted runner artifact drift without duplicate launch, checkpoint replacement, or process signals",
() => verifyLiveRunnerAdoption(false, true),
15_000,
);
it(
"rejects a live runner whose provider identity mismatches the compacted checkpoint",
() => verifyLiveRunnerAdoption(true),
30_000,
);
it("binds buffered mid-goal items only after the authenticated recovery snapshot", () => verifyLiveRunnerAdoption(false, false, true), 30_000);
it("keeps an adopted runner alive when the detached controller finalizer closes", () => verifyLiveRunnerAdoption(false, false, true, true), 30_000);
it("adopts an opening session without a checkpoint instead of bootstrapping a duplicate provider", () => verifyLiveRunnerAdoption(false, false, false, false, true), 30_000);
it.each(["before", "after"] as const)("settles a saved tool outcome with the handler installed %s recovery startup", order =>
verifyLiveRunnerAdoption(false, false, false, false, false, false, "allow", order), 30_000);
it.each(["completion-before", "completion-after"] as const)("recovers a pending finish without an ordinary-tool lookup (%s)", order =>
verifyLiveRunnerAdoption(false, false, false, false, false, false, "allow", order), 30_000);
it("exposes semantic recovery before cancellation when the authority has been revoked", () =>
verifyLiveRunnerAdoption(false, false, false, false, false, false, "allow", "before", false, "denied-before-cancel"), 30_000);
it.each(["before", "after", "completion-before", "completion-after"] as const)("allows cleanup-only adoption to cancel without replaying a pending semantic input (%s)", order =>
verifyLiveRunnerAdoption(false, false, false, false, false, false, "allow", order, false, "cleanup-only"), 30_000);
it.each(["missing", "false"] as const)("refuses cleanup settlement when the exact stop receipt has %s provider exit proof", fault =>
verifyLiveRunnerAdoption(false, false, false, false, false, false, "allow", "before", false, "cleanup-only", fault), 30_000);
@@ -0,0 +1,769 @@
import { mkdtemp, open, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { expect, it, vi } from "vitest";
import type { ControlPlanePort } from "../contracts/control-plane-port.js";
import type { NativeExecutionInputV1 } from "../contracts/native-execution.js";
import type { NativeSession, NativeSessionBackend } from "../contracts/native-session-backend.js";
import type { PrpEvent, PrpStructuredRunResult, PrpTerminalState } from "../protocol/replay-contract.js";
import { executeNativeSession } from "../native-session-runtime.js";
import { createCodexTaskEnvelope } from "../contracts/codex.js";
import { CodexAppServerDriver } from "../drivers/codex/codex-app-server-driver.js";
import { createCapabilityRunnerdCodexTransport } from "./runnerd-codex-transport.js";
import { defaultCapabilityRunnerdBinary, expectTurnStarted, fakeCodex, fakeCodexArgs } from "../../test/support/runnerd-transport.js";
it("rejects active work and buffered tools from a resumed stopped checkpoint", async () => {
const stateDirectory = await mkdtemp(
join(tmpdir(), "runnerd-stopped-resume-active-"),
);
const identity = {
runnerInstanceId: "runner-stopped-active",
environmentLeaseId: "lease-stopped-active",
runId: "run-stopped-first",
normalizedSessionId: "session-stopped-active",
turnId: "turn-stopped-first",
itemId: "item-stopped-first",
};
const options = {
runnerBinary: defaultCapabilityRunnerdBinary(),
codexCommand: fakeCodex,
codexArgs: fakeCodexArgs(
stateDirectory,
"--linger-after-turn-start",
"--resume-unowned-turn-when-marked",
"--emit-tool-call-on-resume",
),
stateDirectory,
lifecyclePolicy: { mode: "per_turn" as const, idleTimeoutMs: null },
};
const first = createCapabilityRunnerdCodexTransport({
...options,
prpIdentity: identity,
});
let second:
ReturnType<typeof createCapabilityRunnerdCodexTransport> | undefined;
const semanticHandler = vi.fn(async () => ({
success: true,
contentItems: [],
}));
first.transport.setServerRequestHandler(semanticHandler);
try {
const opened = await first.transport.request("thread/start", {
cwd: tmpdir(),
dynamicTools: [
{
name: "get_task_context",
description: "Read the current task.",
inputSchema: {
type: "object",
properties: {},
additionalProperties: false,
},
},
],
});
await first.transport.request("turn/start", {
input: [{ type: "text", text: "Wait for another instruction." }],
});
await expectTurnStarted(
first.transport.notifications()[Symbol.asyncIterator](),
);
await first.transport.close();
const providerPath = join(
stateDirectory,
"runner",
"codex-provider-state.json",
);
expect(JSON.parse(await readFile(providerPath, "utf8"))).toMatchObject({
lifecycle: "prepared",
activeProviderTurnId: null,
});
await writeFile(join(stateDirectory, "resume-unowned-turn"), "armed");
const secondIdentity = {
...identity,
runId: "run-stopped-second",
turnId: "turn-stopped-second",
itemId: "item-stopped-second",
};
second = createCapabilityRunnerdCodexTransport({
...options,
prpIdentity: secondIdentity,
});
second.transport.setServerRequestHandler(semanticHandler);
await expect(second.transport.request("thread/read", {})).rejects.toThrow(
"prepared provider checkpoint resumed unexpected active work",
);
// Exercise the normal durable transfer before inspecting the rejection:
// provider pendingEvents is an acknowledged queue, not an event journal.
await vi.waitFor(
async () => {
const provider = JSON.parse(await readFile(providerPath, "utf8"));
expect(provider.pendingEvents).toEqual([]);
const control = JSON.parse(
await readFile(
join(stateDirectory, "control-plane", "control-plane-state.json"),
"utf8",
),
) as {
committedEvents: Array<{
eventType: string;
envelope: { payload: { payload: { code?: string } } };
}>;
};
const rejections = control.committedEvents.filter(
(event) =>
event.eventType === "harness.diagnostic" &&
event.envelope.payload.payload.code ===
"prepared_provider_checkpoint_has_active_work",
);
expect(rejections).toEqual([
expect.objectContaining({
logicalEffectCount: 1,
envelope: expect.objectContaining({
...secondIdentity,
payload: expect.objectContaining({
payload: expect.objectContaining({
code: "prepared_provider_checkpoint_has_active_work",
paperclipAccepted: false,
providerReportedActive: true,
}),
}),
}),
}),
]);
},
{ timeout: 5_000 },
);
const closed = JSON.parse(await readFile(providerPath, "utf8"));
expect(closed).toMatchObject({
lifecycle: "closed",
threadId: (opened.thread as Record<string, unknown>).id,
activeProviderTurnId: null,
});
expect(semanticHandler).not.toHaveBeenCalled();
expect(closed.pendingEvents).toEqual([]);
} finally {
await Promise.allSettled([
first.transport.close(),
second?.transport.close(),
]);
await rm(stateDirectory, { recursive: true, force: true });
}
}, 30_000);
it("does not retry a real memoized transport close whose suspension proof is unavailable", async () => {
const identity = {
runId: "run-recovery",
sessionId: "session-recovery",
companyId: "company-recovery",
issueId: "issue-recovery",
agentId: "agent-recovery",
};
const result: PrpStructuredRunResult = {
schema: "paperclip.run_result.v1",
reportedWorkDisposition: "done",
summary: "Recovered native work completed.",
completionClaim: {
contractRevision: "1",
objectiveSatisfied: true,
criteria: [
{ criterionId: "objective", status: "satisfied", evidenceRefs: [] },
],
remainingWork: [],
},
evidence: [],
verification: [{ commandOrCheck: "recovery", status: "passed" }],
attentionRequests: [],
artifacts: [],
};
const terminal: PrpTerminalState = {
schema: "paperclip.prp.terminal.v1",
turnTerminalState: "completed",
runTerminalState: "succeeded",
reportedWorkDisposition: "done",
};
const input: NativeExecutionInputV1 = {
schema: "paperclip.native-execution-input.v1",
binding: {
companyId: identity.companyId,
runId: identity.runId,
issueId: identity.issueId,
agentId: identity.agentId,
executionWorkspaceId: "workspace-recovery",
},
task: {
identifier: "PAP-RECOVERY",
title: "Recover native work",
description: null,
prompt: "# PAP-RECOVERY: Recover native work",
workMode: "standard",
},
workspace: {
cwd: "/workspace",
repoUrl: null,
repoRef: null,
branchName: null,
},
session: {
normalizedSessionId: identity.sessionId,
driverKind: "codex_app_server",
protocolVersion: 1,
},
provider: { kind: "codex", model: null },
completionContract: {
id: "contract-recovery",
sha256: "contract-recovery-sha",
schemaVersion: "paperclip.completion-contract.v1",
contract: {
revision: "1",
objective: "Recover native work",
criteria: [{ id: "objective", requirement: "Complete after recovery" }],
},
},
interactionResponses: [],
credentialBindings: [],
};
function runnerEvent(
sourceSeq: number,
eventType: PrpEvent["eventType"],
payload: Record<string, unknown> = {},
): PrpEvent {
return {
schema: "paperclip.prp.event.v1",
sourceEventId: `runner-recovery:${identity.runId}:${sourceSeq}`,
sourceSeq,
sourceInstanceId: "runner-recovery",
sourceKind: "runner",
runId: identity.runId,
normalizedSessionId: identity.sessionId,
turnId: "turn-recovery",
eventType,
schemaVersion: 1,
priority: 0,
emittedAt: "2026-08-09T00:00:00.000Z",
payload,
};
}
const stateDirectory = await mkdtemp(
join(tmpdir(), "native-close-quarantine-"),
);
const readRunnerState = vi.fn(async () => ({
schema: "paperclip.runner.durable.state.v1",
runnerInstanceId: "runner-close-quarantine",
environmentLeaseId: "lease-close-quarantine",
runId: identity.runId,
normalizedSessionId: identity.sessionId,
turnId: "turn-close-quarantine",
itemId: "item-close-quarantine",
lifecycle: "ready",
}));
const bundle = createCapabilityRunnerdCodexTransport({
runnerBinary: defaultCapabilityRunnerdBinary(),
codexCommand: resolve(
import.meta.dirname,
"../../runner/target/debug/fake-codex-app-server",
),
codexArgs: ["--state-file", join(stateDirectory, "fake-codex-state.json")],
stateDirectory,
closeGraceMs: 400,
lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null },
prpIdentity: await readRunnerState(),
readRunnerState,
// A checkpoint owner requires durable suspension proof. A local transport
// without a checkpoint can simply terminate its process on close.
controlPlaneRegistration: async (authority) => {
await authority.start();
return {
connectUrl: authority.connectUrl,
checkpoint: async () => {},
release: async () => {},
};
},
});
try {
await bundle.transport.request("thread/start", {
cwd: stateDirectory,
dynamicTools: [],
});
const failedClose = bundle.transport.close();
const failure = await failedClose.catch((error: unknown) => error);
expect(failure).toMatchObject({
code: "native_session_close_unrecoverable",
});
expect(bundle.transport.close()).toBe(failedClose);
vi.useFakeTimers();
const close = vi.fn(({ reason }: { reason: string }) =>
bundle.transport.close(reason),
);
const capabilities = {
resume: true,
typedEvents: true,
steering: false,
interruption: true,
structuredResult: true,
};
const session: NativeSession = {
identity: () => identity,
async capabilities() {
return capabilities;
},
async *events() {
yield runnerEvent(1, "turn.completed");
},
async startTurn() {
return { turnId: "turn-recovery" };
},
async result() {
return { result, terminal, turnId: "turn-recovery" };
},
async snapshot() {
return {
backendKind: "mock",
sessionId: identity.sessionId,
identity,
providerSessionId: "provider-recovery",
cursor: null,
activeTurnId: null,
pendingRuntimeRequests: [],
lineage: [],
};
},
close,
};
const openSession = vi.fn(async () => session);
const backend: NativeSessionBackend = {
async descriptor() {
return {
kind: "mock",
name: "real-memoized-close-quarantine",
version: "1",
capabilities,
};
},
openSession,
};
const port: ControlPlanePort = {
async openRun() {},
async checkpointSession() {},
async appendEvent() {
return {
cursor: 1,
highestContiguousSourceSeq: 1,
disposition: "committed",
};
},
async replayEvents() {
return { events: [], highestContiguousSourceSeq: 0 };
},
async completeRun() {},
};
const execute = () =>
executeNativeSession({
input,
backend,
controlPlane: port,
runnerInstanceId: "runner-recovery",
controlPlaneInstanceId: "control-recovery",
requireSessionCloseBeforeReturn: true,
});
await expect(execute()).rejects.toBe(failure);
const readsAfterClose = readRunnerState.mock.calls.length;
await expect(execute()).rejects.toMatchObject({
code: "native_session_cleanup_quarantined",
recovery: "operator_required",
});
await vi.advanceTimersByTimeAsync(600_000);
await expect(execute()).rejects.toMatchObject({
code: "native_session_cleanup_quarantined",
});
expect(openSession).toHaveBeenCalledOnce();
expect(close).toHaveBeenCalledOnce();
expect(readRunnerState).toHaveBeenCalledTimes(readsAfterClose);
} finally {
vi.useRealTimers();
await bundle.transport.close().catch(() => undefined);
await rm(stateDirectory, { recursive: true, force: true });
}
}, 10_000);
it.each(["not_suspended", "wrong_identity"] as const)(
"does not report local runner close healthy with %s durable evidence",
async (mode) => {
const stateDirectory = await mkdtemp(
join(tmpdir(), "runnerd-local-close-unproven-"),
);
const identity = {
runnerInstanceId: "runner-close-unproven",
environmentLeaseId: "lease-close-unproven",
runId: "run-close-unproven",
normalizedSessionId: "session-close-unproven",
turnId: "turn-close-unproven",
itemId: "item-close-unproven",
};
const bundle = createCapabilityRunnerdCodexTransport({
runnerBinary: defaultCapabilityRunnerdBinary(),
codexCommand: fakeCodex,
codexArgs: fakeCodexArgs(stateDirectory),
stateDirectory,
closeGraceMs: 400,
lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null },
prpIdentity: identity,
readRunnerState: async () => ({
schema: "paperclip.runner.durable.state.v1",
...identity,
...(mode === "wrong_identity" ? { runId: "some-other-run" } : {}),
lifecycle: mode === "not_suspended" ? "ready" : "suspended",
}),
});
bundle.transport.setServerRequestHandler(async () => ({
success: true,
contentItems: [],
}));
try {
await bundle.transport.request("thread/start", {
cwd: tmpdir(),
dynamicTools: [],
});
await expect(bundle.transport.close()).rejects.toThrow(
"runner did not durably suspend before checkpoint",
);
const control = JSON.parse(
await readFile(
join(stateDirectory, "control-plane", "control-plane-state.json"),
"utf8",
),
);
expect(control.identity).toEqual(identity);
expect(await readdir(stateDirectory)).toContain("runner");
} finally {
await bundle.transport.close().catch(() => undefined);
await rm(stateDirectory, { recursive: true, force: true });
}
},
10_000,
);
it.each([
{
label: "interrupted",
args: ["--hold-turn"],
terminal: "turn.interrupted",
disposition: "needs_review",
},
{
label: "failed",
args: ["--fail-turn-immediately"],
terminal: "turn.failed",
disposition: "needs_review",
},
{
label: "completed",
args: [],
terminal: "turn.completed",
disposition: "done",
},
])("retains a $label runner's result before closing its provider turn", async ({
label, args, terminal, disposition,
}) => {
const stateDirectory = await mkdtemp(join(tmpdir(), "runnerd-stop-result-"));
const bundle = createCapabilityRunnerdCodexTransport({
runnerBinary: defaultCapabilityRunnerdBinary(),
codexCommand: fakeCodex,
codexArgs: fakeCodexArgs(stateDirectory, ...args),
stateDirectory,
});
const driver = new CodexAppServerDriver({
taskEnvelope: createCodexTaskEnvelope({
objective: "Stop and retain unfinished work.",
}),
environment: {
PATH: process.env.PATH,
HOME: join(tmpdir(), "runnerd-stop-host-home"),
PAPERCLIP_WORKSPACE_CWD: stateDirectory,
},
approvalPolicy: "never",
transportFactory: () => bundle.transport,
});
const session = await driver.openSession({
runId: "run-stop-result",
normalizedSessionId: "session-stop-result",
workingDirectory: stateDirectory,
});
try {
const turn = await session.startTurn({
message: { role: "user", text: "Keep working until interrupted." },
});
if (label === "interrupted") {
await session.interrupt({
turnId: turn.turnId,
reason: "Stopped by the user",
});
}
const events: PrpEvent[] = [];
for await (const event of session.events()) {
events.push(event);
if (event.eventType === "session.failed" || event.eventType === terminal) {
break;
}
}
// Let already committed durable suffix events reach the facade as well.
await bundle.transport.request("thread/read", {});
const snapshot = await session.snapshot();
expect(events.some((event) => event.eventType === "session.failed")).toBe(false);
expect(events.map((event) => event.eventType)).toContain("run.result.proposed");
expect(events.at(-1)?.eventType).toBe(terminal);
expect(snapshot).toMatchObject({
activeTurnId: null,
semanticResult: {
turnId: turn.turnId,
result: { reportedWorkDisposition: disposition },
},
});
} finally {
await session.close().catch(() => undefined);
await rm(stateDirectory, {
recursive: true,
force: true,
maxRetries: 5,
retryDelay: 25,
});
}
}, 30_000);
it.each([0, 193 * 1024 * 1024])(
"probes an exact-authority resume with %i extra journal bytes and confirms its live provider identity",
async (extraJournalBytes) => {
const stateDirectory = await mkdtemp(
join(tmpdir(), "runnerd-exact-authority-resume-"),
);
const identity = {
runnerInstanceId: "runner-exact-resume",
environmentLeaseId: "lease-exact-resume",
runId: "run-exact-resume",
normalizedSessionId: "session-exact-resume",
turnId: "turn-exact-resume",
itemId: "item-exact-resume",
};
const options = {
runnerBinary: defaultCapabilityRunnerdBinary(),
codexCommand: fakeCodex,
codexArgs: fakeCodexArgs(stateDirectory, "--durable-turn-ids"),
stateDirectory,
lifecyclePolicy: { mode: "per_turn" as const, idleTimeoutMs: null },
prpIdentity: identity,
};
const first = createCapabilityRunnerdCodexTransport(options);
first.transport.setServerRequestHandler(async () => ({
success: true,
contentItems: [],
}));
let providerThread: { id: string; sessionId: string } | null = null;
try {
const opened = await first.transport.request("thread/start", {
cwd: tmpdir(),
dynamicTools: [],
});
const thread = opened.thread as Record<string, unknown>;
providerThread = {
id: String(thread.id),
sessionId: String(thread.sessionId),
};
} finally {
await first.transport.close();
}
if (providerThread === null) {
throw new Error(
"exact-authority fixture did not return a provider thread",
);
}
const statePath = join(
stateDirectory,
"control-plane",
"control-plane-state.json",
);
if (extraJournalBytes > 0) {
const padding = Buffer.alloc(1024 * 1024, 0x20);
const stateHandle = await open(statePath, "a");
try {
for (let remaining = extraJournalBytes; remaining > 0;) {
const bytesToWrite = Math.min(remaining, padding.length);
await stateHandle.write(padding, 0, bytesToWrite);
remaining -= bytesToWrite;
}
} finally {
await stateHandle.close();
}
expect((await stat(statePath)).size).toBeGreaterThan(
64 * 1024 * 1024,
);
}
const beforeResume = JSON.parse(await readFile(statePath, "utf8")) as {
commands: Array<{ type: string }>;
committedEvents: Array<{ eventType: string }>;
};
expect(
beforeResume.commands.some((command) => command.type === "run.attach"),
).toBe(false);
const priorResumeEvents = beforeResume.committedEvents.filter(
(event) => event.eventType === "session.resumed",
).length;
const priorSnapshots = beforeResume.commands.filter(
(command) => command.type === "session.snapshot",
).length;
const resumed = createCapabilityRunnerdCodexTransport({
...options,
resumeProviderSession: {
driverSessionId: providerThread.id,
providerSessionId: providerThread.sessionId,
},
});
resumed.transport.setServerRequestHandler(async () => ({
success: true,
contentItems: [],
}));
try {
const read = await resumed.transport.request("thread/read", {});
expect(read.thread).toMatchObject(providerThread);
const afterResume = JSON.parse(await readFile(statePath, "utf8")) as {
commands: Array<{ commandId: string; type: string; status: string }>;
committedEvents: Array<{ eventType: string }>;
};
expect(afterResume.commands).toContainEqual(
expect.objectContaining({
commandId: expect.stringMatching(/^command_resume_probe_/),
type: "runner.drain",
status: "completed",
}),
);
expect(afterResume.commands).toContainEqual(
expect.objectContaining({
type: "session.snapshot",
status: "completed",
}),
);
expect(
afterResume.commands.filter(
(command) => command.type === "session.snapshot",
),
).toHaveLength(priorSnapshots + 2);
// The authenticated snapshot above proves the live provider identity.
// Control-first dispatch may deliver that command before the independent
// session event is ingested. Still require exactly one durable event;
// don't mistake an immediate file read for an event-delivery barrier.
await vi.waitFor(
async () => {
const delivered = JSON.parse(await readFile(statePath, "utf8")) as {
committedEvents: Array<{ eventType: string }>;
};
expect(
delivered.committedEvents.filter(
(event) => event.eventType === "session.resumed",
),
).toHaveLength(priorResumeEvents + 1);
},
{ timeout: 3_000, interval: 25 },
);
} finally {
await resumed.transport.close();
await rm(stateDirectory, { recursive: true, force: true });
}
},
30_000,
);
it("still fails closed when a real close grace period cannot fit a durable suspension round trip", async () => {
const stateDirectory = await mkdtemp(
join(tmpdir(), "runnerd-close-grace-too-small-"),
);
const identity = {
runnerInstanceId: "runner-close-grace-too-small",
environmentLeaseId: "lease-close-grace-too-small",
runId: "run-close-grace-too-small",
normalizedSessionId: "session-close-grace-too-small",
turnId: "turn-close-grace-too-small",
itemId: "item-close-grace-too-small",
};
const bundle = createCapabilityRunnerdCodexTransport({
runnerBinary: defaultCapabilityRunnerdBinary(),
codexCommand: fakeCodex,
codexArgs: fakeCodexArgs(stateDirectory),
stateDirectory,
// No real durable command round trip can complete this fast. A wider
// budget for the provider-drain proof must not turn this barrier into
// one that always passes; it still needs the actual proof to arrive.
closeGraceMs: 1,
lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null },
prpIdentity: identity,
});
bundle.transport.setServerRequestHandler(async () => ({
success: true,
contentItems: [],
}));
try {
await bundle.transport.request("thread/start", {
cwd: tmpdir(),
dynamicTools: [],
});
const closed = bundle.transport.close();
await expect(closed).rejects.toMatchObject({
code: "native_session_close_unrecoverable",
semanticToolsSettled: true,
});
// Retirement preserves the original rejected promise and its proof.
expect(bundle.transport.close()).toBe(closed);
} finally {
await rm(stateDirectory, { recursive: true, force: true });
}
}, 30_000);
it("persists an active provider as settled before bounded suspension", async () => {
const stateDirectory = await mkdtemp(
join(tmpdir(), "runnerd-active-suspension-"),
);
const bundle = createCapabilityRunnerdCodexTransport({
runnerBinary: defaultCapabilityRunnerdBinary(),
codexCommand: fakeCodex,
codexArgs: fakeCodexArgs(stateDirectory, "--linger-after-turn-start"),
stateDirectory,
});
bundle.transport.setServerRequestHandler(async () => ({
success: true,
contentItems: [],
}));
try {
await bundle.transport.request("initialize", {});
await bundle.transport.request("thread/start", {
cwd: tmpdir(),
dynamicTools: [],
});
await bundle.transport.request("turn/start", {
input: [{ type: "text", text: "Wait for another instruction." }],
});
const notifications = bundle.transport
.notifications()
[Symbol.asyncIterator]();
await expectTurnStarted(notifications);
await bundle.transport.close();
const providerState = JSON.parse(
await readFile(
join(stateDirectory, "runner", "codex-provider-state.json"),
"utf8",
),
) as Record<string, unknown>;
expect(providerState).toMatchObject({
lifecycle: "prepared",
activeProviderTurnId: null,
});
} finally {
await bundle.transport.close();
await rm(stateDirectory, { recursive: true, force: true });
}
}, 30_000);
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -15,10 +15,12 @@ import type {
PersistedNativeSession,
} from "./contracts/native-session-backend.js";
import {
NativeProviderStateIncompatibleError,
NativeSessionCloseUnrecoverableError,
NativeSessionCleanupQuarantinedError,
NativeSessionProtocolIntegrityError,
} from "./contracts/native-session-backend.js";
import { PROVIDER_STATE_INCOMPATIBLE } from "./contracts/harness-driver.js";
import type {
PrpEvent,
PrpStructuredRunResult,
@@ -5641,6 +5643,40 @@ describe("executeNativeSession recovery", () => {
expect(openSession).not.toHaveBeenCalled();
});
it("types a runner-proven incompatible session when same-session recovery is required", async () => {
for (const providerRecoveryPolicy of ["same_session_only", "allow_replacement_after_governed_wait"] as const) {
const checkpoint: PersistedNativeSession = {
backendKind: "mock", sessionId: "driver-incompatible", identity,
providerSessionId: "provider-incompatible", providerRecoveryPolicy,
cursor: "0", activeTurnId: null, pendingRuntimeRequests: [], lineage: [],
};
const openRun = vi.fn(async () => undefined);
const openSession = vi.fn();
const openReplacementSession = vi.fn();
const backend: NativeSessionBackend = {
descriptor: async () => ({ kind: "mock", name: "incompatible", version: "1",
capabilities: { resume: true, typedEvents: true, steering: false, interruption: true } }),
openSession, openReplacementSession,
recoverSession: async () => ({ recovered: false, reason: "saved launch profile changed",
failureClass: PROVIDER_STATE_INCOMPATIBLE }),
};
const port: ControlPlanePort = {
openRun, appendEvent: vi.fn(), completeRun: vi.fn(),
loadSessionCheckpoint: async () => structuredClone(checkpoint),
replayEvents: async () => ({ events: [], highestContiguousSourceSeq: 0 }),
};
const failure = await executeNativeSession({
input, backend, controlPlane: port,
runnerInstanceId: "runner-recovery", controlPlaneInstanceId: "control-recovery",
}).catch((error: unknown) => error);
expect(failure, providerRecoveryPolicy).toBeInstanceOf(NativeProviderStateIncompatibleError);
expect((failure as Error).message).toBe("native_session_recovery_failed: saved launch profile changed");
expect(openRun).not.toHaveBeenCalled();
expect(openSession).not.toHaveBeenCalled();
expect(openReplacementSession).not.toHaveBeenCalled();
}
});
it("bounds paginated recovery replay with one signal and observes a late rejection", async () => {
vi.useFakeTimers();
let rejectStalledReplay = (_error: Error) => {};
@@ -19,8 +19,9 @@ import type {
NativeSessionBackend,
} from "./contracts/native-session-backend.js";
import type { PersistedNativeSession } from "./contracts/native-session-backend.js";
import type { HarnessThreadGoal } from "./contracts/harness-driver.js";
import { PROVIDER_STATE_INCOMPATIBLE, type HarnessThreadGoal } from "./contracts/harness-driver.js";
import {
NativeProviderStateIncompatibleError,
NativeProviderTerminalFailure,
NativeSessionCloseUnrecoverableError,
NativeSessionCleanupQuarantinedError,
@@ -2164,6 +2165,9 @@ export async function executeNativeSession(
};
if (!recovery.recovered || !recovery.session) {
if (!replacementAllowed) {
if (recovery.failureClass === PROVIDER_STATE_INCOMPATIBLE) {
throw new NativeProviderStateIncompatibleError(recovery.reason ?? "unknown");
}
throw new Error(
`native_session_recovery_failed: ${recovery.reason ?? "unknown"}`,
);
@@ -0,0 +1,88 @@
import { join, resolve } from "node:path";
import { expect } from "vitest";
import {
NATIVE_RUNTIME_ASSET_SCHEMA,
PAPERCLIP_EXECUTION_PROMPT,
PAPERCLIP_EXECUTION_PROMPT_REVISION,
canonicalNativeRuntimeContextDigest,
nativeRuntimePromptDigest,
type NativeRuntimeContextSnapshot,
} from "../../src/contracts/runtime-context.js";
import { defaultCapabilityRunnerdBinary as qualifiedCapabilityRunnerdBinary } from "../../src/live/runnerd-codex-transport.js";
export const defaultCapabilityRunnerdBinary = () =>
process.env.PAPERCLIP_ATTACH_TRANSITION_RUNNER ??
qualifiedCapabilityRunnerdBinary();
export async function expectTurnStarted(
notifications: AsyncIterator<{ method: string }>,
) {
for (let index = 0; index < 32; index += 1) {
const next = await notifications.next();
expect(next.done).not.toBe(true);
if (next.value.method === "turn/started") return;
// Startup ownership and capability facts can precede the active turn.
expect(next.value.method).toBe("paperclip/canonicalProviderEvent");
}
throw new Error(
"provider turn did not start within the bounded notification prefix",
);
}
export const fakeCodex = resolve(
import.meta.dirname,
"../../runner/target/debug/fake-codex-app-server",
);
export function fakeCodexArgs(stateDirectory: string, ...args: string[]): string[] {
return [
"--state-file",
join(stateDirectory, "fake-codex-state.json"),
...args,
];
}
export function assignedRuntimeContext(
skillRoot: string,
instructionRoot: string,
): NativeRuntimeContextSnapshot {
const digest = "0".repeat(64);
const value = {
prompt: {
revision: PAPERCLIP_EXECUTION_PROMPT_REVISION,
text: PAPERCLIP_EXECUTION_PROMPT,
digest: nativeRuntimePromptDigest(),
},
instructions: {
entryPath: "AGENTS.md",
bundle: {
schema: NATIVE_RUNTIME_ASSET_SCHEMA,
digest,
manifestDigest: digest,
rootPath: instructionRoot,
fileCount: 1,
totalBytes: 1,
},
},
skills: [
{
key: "company/assigned",
runtimeName: "assigned",
versionId: "version-1",
bundle: {
schema: NATIVE_RUNTIME_ASSET_SCHEMA,
digest,
manifestDigest: digest,
rootPath: skillRoot,
fileCount: 1,
totalBytes: 1,
},
},
],
mcp: { assignmentSetId: "assigned", digest, bindingId: "binding" },
} satisfies Omit<NativeRuntimeContextSnapshot, "aggregateDigest">;
return {
...value,
aggregateDigest: canonicalNativeRuntimeContextDigest(value),
};
}
@@ -1,7 +1,9 @@
import { availableParallelism } from "node:os";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { configDefaults, defineConfig } from "vitest/config";
import { RunnerSequencer } from "./scripts/vitest-sequencer.mjs";
const packageRoot = fileURLToPath(new URL(".", import.meta.url));
@@ -17,6 +19,8 @@ export default defineConfig({
? { reporters: [...configDefaults.reporters, resolve(packageRoot, "../../scripts/ci-profile/vitest-reporter.mjs")] }
: {}),
environment: "node",
...(process.env.GITHUB_ACTIONS === "true" ? { maxWorkers: Math.min(4, availableParallelism()) } : {}),
sequence: { sequencer: RunnerSequencer },
include: [
"src/**/*.test.ts",
"src/**/*.test.tsx",
@@ -27,6 +31,7 @@ export default defineConfig({
"examples/scenario-explorer/src/**/*.test.tsx",
"scripts/local-runner-browser-server.test.mjs",
"scripts/live-console-browser-server.test.mjs",
"scripts/vitest-sequencer.test.mjs",
],
},
});
+18
View File
@@ -65,6 +65,24 @@ provider-supplied error name that may contain a credential. This is a privacy
boundary, not enum canonicalization. Test the value reaching the telemetry
helper as well as the separate crash-reporting and local logging sinks.
## Server Startup Privacy
The startup helper emits the raw Node.js operating system family once when an
enabled server telemetry client initializes. It omits OS versions, CPU
architecture, hostnames, and other machine details. The usual telemetry opt-out
and CI suppression apply before client initialization. If state storage fails
at startup, the server disables telemetry for that process and continues to run.
The warning contains no storage path or underlying error. Container and WSL servers
report their runtime platform; this does not identify the underlying physical
host's OS. Count distinct installation identifiers by platform to compare
installations, since restart event counts also reflect restart frequency.
Contract syncs may also register existing proposals. Remove each adopted
proposal marker and update its helper and transport tests in the same change.
The connector helpers preserve raw enum values and send only catalog connector
keys and the approved lifecycle and invocation dimensions. Connection names,
credentials, tool arguments, and tool results stay out of those events.
## Interaction Resolver Events
`interaction.created` records the interaction kind and whether the create
@@ -28,7 +28,7 @@ function makeClient(config?: { enabled?: boolean }) {
};
}
function trackAllProposedConnectorEvents(client: TelemetryClient) {
function trackAllConnectorEvents(client: TelemetryClient) {
trackConnectionCreated(client, {
connector_key: "github",
transport: "mcp_remote",
@@ -56,28 +56,33 @@ function trackAllProposedConnectorEvents(client: TelemetryClient) {
});
}
describe("proposed connector events against the real TelemetryClient", () => {
describe("registered connector events against the real TelemetryClient", () => {
afterEach(() => {
vi.restoreAllMocks();
vi.unstubAllEnvs();
vi.unstubAllGlobals();
});
it("cannot queue or send: state stays untouched and nothing hits the wire", async () => {
it("queues and sends the three registered connector events", async () => {
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: true }));
const { client, stateFactory } = makeClient();
trackAllProposedConnectorEvents(client);
trackAllConnectorEvents(client);
await client.flush();
expect(stateFactory).not.toHaveBeenCalled();
expect(fetch).not.toHaveBeenCalled();
expect(stateFactory).toHaveBeenCalledTimes(1);
expect(fetch).toHaveBeenCalledTimes(1);
const body = JSON.parse(String(vi.mocked(fetch).mock.calls[0]?.[1]?.body));
expect(body.events.map((event: { name: string }) => event.name)).toEqual([
"connection.created", "connection.updated", "connection.invoked",
]);
});
it("differential control: a registered event through the same client does send", async () => {
it("batches connector events with other registered events", async () => {
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: true }));
const { client } = makeClient();
trackAllProposedConnectorEvents(client);
trackAllConnectorEvents(client);
client.track("project.created", {});
await client.flush();
@@ -86,15 +91,18 @@ describe("proposed connector events against the real TelemetryClient", () => {
String((vi.mocked(fetch).mock.calls[0]?.[1] as RequestInit).body),
);
expect(body.events).toEqual([
expect.objectContaining({ name: "connection.created" }),
expect.objectContaining({ name: "connection.updated" }),
expect.objectContaining({ name: "connection.invoked" }),
expect.objectContaining({ name: "project.created" }),
]);
});
it("reports the three connector event names as unregistered", () => {
it("reports the three connector event names as registered", () => {
const { client } = makeClient();
expect(client.isRegisteredEventName("connection.created")).toBe(false);
expect(client.isRegisteredEventName("connection.updated")).toBe(false);
expect(client.isRegisteredEventName("connection.invoked")).toBe(false);
expect(client.isRegisteredEventName("connection.created")).toBe(true);
expect(client.isRegisteredEventName("connection.updated")).toBe(true);
expect(client.isRegisteredEventName("connection.invoked")).toBe(true);
expect(client.isRegisteredEventName("project.created")).toBe(true);
});
@@ -102,6 +110,7 @@ describe("proposed connector events against the real TelemetryClient", () => {
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: true }));
const { client, stateFactory } = makeClient({ enabled: false });
trackAllConnectorEvents(client);
client.track("project.created", {});
await client.flush();
+43 -44
View File
@@ -156,70 +156,69 @@ export function trackAgentTaskRun(
export function trackConnectionCreated(
client: TelemetryClient,
dims: {
connector_key: RawDimension<"custom">;
transport: RawDimension<"mcp_remote" | "rest_api" | "local_stdio">;
auth_kind: RawDimension<"oauth" | "api_key" | "none">;
setup_flow: RawDimension<"gallery" | "api" | "example">;
status: RawDimension<"draft" | "active" | "disabled" | "archived">;
connector_key: string;
transport: RawDimension<EventDimensionsMap["connection.created"]["transport"]>;
auth_kind: RawDimension<EventDimensionsMap["connection.created"]["auth_kind"]>;
setup_flow: RawDimension<EventDimensionsMap["connection.created"]["setup_flow"]>;
status: RawDimension<EventDimensionsMap["connection.created"]["status"]>;
enabled: boolean;
},
): void {
client.track(
// @ts-expect-error -- proposed-telemetry(https://github.com/paperclipai/paperclip/issues/13578): measure which catalog connectors installations create connections for
"connection.created",
dims,
);
client.track("connection.created", {
...dims,
transport: asEventDimension(dims.transport),
auth_kind: asEventDimension(dims.auth_kind),
setup_flow: asEventDimension(dims.setup_flow),
status: asEventDimension(dims.status),
});
}
export function trackConnectionUpdated(
client: TelemetryClient,
dims: {
connector_key: RawDimension<"custom">;
transport: RawDimension<"mcp_remote" | "rest_api" | "local_stdio">;
auth_kind: RawDimension<"oauth" | "api_key" | "none">;
change_source: RawDimension<
| "api"
| "gallery"
| "oauth_callback"
| "credential_refresh"
| "archive"
| "example"
>;
previous_status: RawDimension<"draft" | "active" | "disabled" | "archived">;
status: RawDimension<"draft" | "active" | "disabled" | "archived">;
connector_key: string;
transport: RawDimension<EventDimensionsMap["connection.updated"]["transport"]>;
auth_kind: RawDimension<EventDimensionsMap["connection.updated"]["auth_kind"]>;
change_source: RawDimension<EventDimensionsMap["connection.updated"]["change_source"]>;
previous_status: RawDimension<EventDimensionsMap["connection.updated"]["previous_status"]>;
status: RawDimension<EventDimensionsMap["connection.updated"]["status"]>;
previous_enabled: boolean;
enabled: boolean;
},
): void {
client.track(
// @ts-expect-error -- proposed-telemetry(https://github.com/paperclipai/paperclip/issues/13578): measure connector lifecycle transitions (configured, paused, archived) after creation
"connection.updated",
dims,
);
client.track("connection.updated", {
...dims,
transport: asEventDimension(dims.transport),
auth_kind: asEventDimension(dims.auth_kind),
change_source: asEventDimension(dims.change_source),
previous_status: asEventDimension(dims.previous_status),
status: asEventDimension(dims.status),
});
}
export function trackConnectionInvoked(
client: TelemetryClient,
dims: {
connector_key: RawDimension<"custom">;
transport: RawDimension<"mcp_remote" | "rest_api" | "local_stdio">;
status: RawDimension<
| "succeeded"
| "failed"
| "denied"
| "cancelled"
| "timed_out"
| "rate_limited"
>;
origin: RawDimension<"setup_test" | "agent" | "user" | "system" | "plugin">;
connector_key: string;
transport: RawDimension<EventDimensionsMap["connection.invoked"]["transport"]>;
status: RawDimension<EventDimensionsMap["connection.invoked"]["status"]>;
origin: RawDimension<EventDimensionsMap["connection.invoked"]["origin"]>;
duration_seconds?: number;
},
): void {
client.track(
// @ts-expect-error -- proposed-telemetry(https://github.com/paperclipai/paperclip/issues/13578): measure whether connected connectors are successfully used and where invocations fail; records completed invocation attempts and their terminal status, never invocation starts
"connection.invoked",
dims,
);
client.track("connection.invoked", {
...dims,
transport: asEventDimension(dims.transport),
status: asEventDimension(dims.status),
origin: asEventDimension(dims.origin),
});
}
export function trackServerStarted(
client: TelemetryClient,
dims: { os_platform: RawDimension<EventDimensionsMap["server.started"]["os_platform"]> },
): void {
client.track("server.started", { os_platform: asEventDimension(dims.os_platform) });
}
export function trackErrorHandlerCrash(
@@ -37,6 +37,34 @@ source_ref?: string
source_ref_hashed?: boolean
}
export interface PaperclipConnectionCreatedDimensions {
auth_kind: ("oauth" | "api_key" | "none")
connector_key: string
enabled: boolean
setup_flow: ("gallery" | "api" | "example")
status: ("draft" | "active" | "disabled" | "archived")
transport: ("mcp_remote" | "rest_api" | "local_stdio")
}
export interface PaperclipConnectionInvokedDimensions {
connector_key: string
duration_seconds?: number
origin: ("setup_test" | "agent" | "user" | "system" | "plugin")
status: ("succeeded" | "failed" | "denied" | "cancelled" | "timed_out" | "rate_limited")
transport: ("mcp_remote" | "rest_api" | "local_stdio")
}
export interface PaperclipConnectionUpdatedDimensions {
auth_kind: ("oauth" | "api_key" | "none")
change_source: ("api" | "gallery" | "oauth_callback" | "credential_refresh" | "archive" | "example")
connector_key: string
enabled: boolean
previous_enabled: boolean
previous_status: ("draft" | "active" | "disabled" | "archived")
status: ("draft" | "active" | "disabled" | "archived")
transport: ("mcp_remote" | "rest_api" | "local_stdio")
}
export interface PaperclipErrorHandlerCrashDimensions {
error_code: string
}
@@ -58,7 +86,6 @@ interaction_kind: ("suggest_tasks" | "ask_user_questions" | "request_confirmatio
used_deprecated_resolver_policy_alias: boolean
}
/** Emit only after the complete resolution transaction commits, including response provenance. */
export interface PaperclipInteractionResolvedDimensions {
interaction_kind: ("suggest_tasks" | "ask_user_questions" | "request_confirmation" | "request_checkbox_confirmation" | "request_item_verdicts" | "other")
status: ("accepted" | "rejected" | "answered" | "cancelled" | "expired" | "failed" | "other")
@@ -95,9 +122,11 @@ source: ("schedule" | "manual" | "api" | "webhook" | "other")
status: ("received" | "coalesced" | "skipped" | "issue_created" | "completed" | "failed" | "other")
}
export interface PaperclipServerStartedDimensions {
os_platform: ("aix" | "android" | "darwin" | "freebsd" | "haiku" | "linux" | "openbsd" | "sunos" | "win32" | "cygwin" | "netbsd" | "other")
}
export interface PaperclipSkillImportedDimensions {
/** skill_ref may identify a known public catalog source only. Omit it for
* GitHub-synced sources, which may be private; never emit repository paths or names. */
source_type: ("local_path" | "github" | "url" | "catalog" | "skills_sh" | "unknown")
skill_ref?: string
}
@@ -108,6 +137,9 @@ export type PaperclipEventName =
| "agent.task_completed"
| "agent.task_run"
| "company.imported"
| "connection.created"
| "connection.invoked"
| "connection.updated"
| "error.handler_crash"
| "goal.created"
| "install.completed"
@@ -117,6 +149,7 @@ export type PaperclipEventName =
| "project.created"
| "routine.created"
| "routine.run"
| "server.started"
| "skill.imported";
export interface EventDimensionsMap {
@@ -125,6 +158,9 @@ export interface EventDimensionsMap {
"agent.task_completed": PaperclipAgentTaskCompletedDimensions;
"agent.task_run": PaperclipAgentTaskRunDimensions;
"company.imported": PaperclipCompanyImportedDimensions;
"connection.created": PaperclipConnectionCreatedDimensions;
"connection.invoked": PaperclipConnectionInvokedDimensions;
"connection.updated": PaperclipConnectionUpdatedDimensions;
"error.handler_crash": PaperclipErrorHandlerCrashDimensions;
"goal.created": PaperclipGoalCreatedDimensions;
"install.completed": PaperclipInstallCompletedDimensions;
@@ -134,6 +170,7 @@ export interface EventDimensionsMap {
"project.created": PaperclipProjectCreatedDimensions;
"routine.created": PaperclipRoutineCreatedDimensions;
"routine.run": PaperclipRoutineRunDimensions;
"server.started": PaperclipServerStartedDimensions;
"skill.imported": PaperclipSkillImportedDimensions;
}
@@ -143,6 +180,9 @@ export const PAPERCLIP_EVENTS = {
"agent.task_completed": "agent.task_completed",
"agent.task_run": "agent.task_run",
"company.imported": "company.imported",
"connection.created": "connection.created",
"connection.invoked": "connection.invoked",
"connection.updated": "connection.updated",
"error.handler_crash": "error.handler_crash",
"goal.created": "goal.created",
"install.completed": "install.completed",
@@ -152,6 +192,7 @@ export const PAPERCLIP_EVENTS = {
"project.created": "project.created",
"routine.created": "routine.created",
"routine.run": "routine.run",
"server.started": "server.started",
"skill.imported": "skill.imported",
} as const;
@@ -277,6 +318,83 @@ export const PAPERCLIP_ENUM_DESCRIPTIONS = {
"unknown": "Source type could not be classified."
}
},
"connection.created": {
"auth_kind": {
"oauth": "Connection authenticates with an OAuth flow.",
"api_key": "Connection authenticates with a stored API key.",
"none": "Connection requires no authentication."
},
"setup_flow": {
"gallery": "Connection was created through the app-gallery setup flow.",
"api": "Connection was created through the connections API.",
"example": "Connection was created or upserted as a bundled example connection."
},
"status": {
"draft": "Connection row exists but setup has not produced a usable configuration yet.",
"active": "Connection is fully configured and available for use.",
"disabled": "Connection is disabled and excluded from tool use.",
"archived": "Connection is archived and excluded from tool use."
},
"transport": {
"mcp_remote": "Connection talks to a remote MCP server over HTTP.",
"rest_api": "Connection calls the provider's REST API directly.",
"local_stdio": "Connection runs a local MCP server over stdio."
}
},
"connection.invoked": {
"origin": {
"setup_test": "A human ran the connection's setup test, identified by durable invocation columns: user actor with no run, issue, or gateway reference and a connection id.",
"agent": "An agent run invoked the tool.",
"user": "A human invoked the tool outside the setup-test shape.",
"system": "A system-initiated process invoked the tool.",
"plugin": "A plugin invoked the tool."
},
"status": {
"succeeded": "The tool invocation completed successfully.",
"failed": "The tool invocation reached a terminal failure.",
"denied": "The tool invocation was denied by policy or by a human approval decision.",
"cancelled": "The tool invocation was cancelled before completion. Declared in the application status enum; no current application code path writes this value.",
"timed_out": "The tool invocation exceeded its time limit.",
"rate_limited": "The tool invocation was rejected by rate limiting."
},
"transport": {
"mcp_remote": "Connection talks to a remote MCP server over HTTP.",
"rest_api": "Connection calls the provider's REST API directly.",
"local_stdio": "Connection runs a local MCP server over stdio."
}
},
"connection.updated": {
"auth_kind": {
"oauth": "Connection authenticates with an OAuth flow.",
"api_key": "Connection authenticates with a stored API key.",
"none": "Connection requires no authentication."
},
"change_source": {
"api": "A connections API update changed the persisted lifecycle state.",
"gallery": "An app-gallery setup step (finish, OAuth access finalization, or reconnect; Apps gallery and inline task cards) changed the persisted lifecycle state.",
"oauth_callback": "An OAuth callback completion (provider OAuth, Paperclip Cloud connector, or Vercel Connect) changed the persisted lifecycle state.",
"credential_refresh": "A credential refresh changed the persisted lifecycle state.",
"archive": "An archive operation changed the persisted lifecycle state.",
"example": "An example-connection upsert changed the persisted lifecycle state."
},
"previous_status": {
"draft": "Connection row exists but setup has not produced a usable configuration yet.",
"active": "Connection is fully configured and available for use.",
"disabled": "Connection is disabled and excluded from tool use.",
"archived": "Connection is archived and excluded from tool use."
},
"status": {
"draft": "Connection row exists but setup has not produced a usable configuration yet.",
"active": "Connection is fully configured and available for use.",
"disabled": "Connection is disabled and excluded from tool use.",
"archived": "Connection is archived and excluded from tool use."
},
"transport": {
"mcp_remote": "Connection talks to a remote MCP server over HTTP.",
"rest_api": "Connection calls the provider's REST API directly.",
"local_stdio": "Connection runs a local MCP server over stdio."
}
},
"goal.created": {
"goal_level": {
"company": "Goal applies at company scope.",
@@ -402,6 +520,22 @@ export const PAPERCLIP_ENUM_DESCRIPTIONS = {
"other": "Fallback when the status is unknown or not represented by the tracked enum."
}
},
"server.started": {
"os_platform": {
"aix": "Server host runs IBM AIX.",
"android": "Server host runs Android.",
"darwin": "Server host runs macOS.",
"freebsd": "Server host runs FreeBSD.",
"haiku": "Server host runs Haiku.",
"linux": "Server host runs Linux, including WSL and container hosts.",
"openbsd": "Server host runs OpenBSD.",
"sunos": "Server host runs SunOS, Solaris, or illumos.",
"win32": "Server host runs Windows, both 32-bit and 64-bit.",
"cygwin": "Server host runs under Cygwin on Windows.",
"netbsd": "Server host runs NetBSD.",
"other": "Fallback when the platform is unknown or not represented by the tracked enum."
}
},
"skill.imported": {
"source_type": {
"local_path": "Import source came from a filesystem path on the operator's machine.",
+1
View File
@@ -21,6 +21,7 @@ export {
trackConnectionCreated,
trackConnectionUpdated,
trackConnectionInvoked,
trackServerStarted,
} from "./events.js";
export type {
TelemetryConfig,
@@ -47,4 +47,5 @@ export const EVENT_RETENTION_CLASS: Partial<Record<string, RetentionClass>> = {
"codex.credential_health": "operational_enum_count",
"interaction.created": "operational_enum_count",
"interaction.resolved": "operational_enum_count",
"server.started": "operational_enum_count",
};
@@ -0,0 +1,69 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { TelemetryClient } from "./client.js";
import { trackServerStarted } from "./events.js";
import type { TelemetryState } from "./types.js";
const TEST_STATE: TelemetryState = {
installId: "test-install",
salt: "test-salt",
createdAt: "2026-01-01T00:00:00Z",
firstSeenVersion: "0.0.0",
};
function makeClient(enabled = true) {
const stateFactory = vi.fn(() => TEST_STATE);
return {
client: new TelemetryClient(
{ enabled, endpoint: "http://localhost:9999/ingest" },
stateFactory,
"0.0.0-test",
() => 0.5,
),
stateFactory,
};
}
describe("registered server.started event", () => {
afterEach(() => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
});
it("passes the raw process.platform value through without normalization", () => {
const { client } = makeClient();
const track = vi.spyOn(client, "track");
trackServerStarted(client, { os_platform: "win32" });
trackServerStarted(client, { os_platform: "some-future-platform" });
expect(track.mock.calls).toEqual([
["server.started", { os_platform: "win32" }],
["server.started", { os_platform: "some-future-platform" }],
]);
});
it("sends the registered event with only the raw OS family", async () => {
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: true }));
const { client, stateFactory } = makeClient();
expect(client.isRegisteredEventName("server.started")).toBe(true);
trackServerStarted(client, { os_platform: process.platform });
await client.flush();
expect(stateFactory).toHaveBeenCalledTimes(1);
expect(fetch).toHaveBeenCalledTimes(1);
const body = JSON.parse(String(vi.mocked(fetch).mock.calls[0]?.[1]?.body));
expect(body.events).toEqual([
{ name: "server.started", occurredAt: expect.any(String), dimensions: { os_platform: process.platform } },
]);
});
it("leaves state and network untouched when disabled", async () => {
vi.stubGlobal("fetch", vi.fn());
const { client, stateFactory } = makeClient(false);
trackServerStarted(client, { os_platform: process.platform });
await client.flush();
expect(stateFactory).not.toHaveBeenCalled();
expect(fetch).not.toHaveBeenCalled();
});
});
@@ -61,6 +61,39 @@ test("Docker Hub mirrors apply to both image builders without widening publicati
"the registry-only manifest merge does not need a Docker Hub mirror");
});
test("source proof gates publication without occupying a publication runner", () => {
const workflow = readWorkflow("docker.yml");
const observer = workflow.split(" source_verification:\n")[1].split(" reuse_canary_image:")[0];
const gate = workflow.split(" publication_verification:\n")[1].split(" merge-and-push:")[0];
const publisher = workflow.split(" merge-and-push:\n")[1].split(" promote_canary_channel:")[0];
assert.match(observer, /verified: \$\{\{ steps.watch.outputs.verified \}\}/);
assert.match(gate, /run: node scripts\/watch-release-source.mjs --require-proof/);
assert.match(gate, /actions: write # restart only exact-source producers deferred before admission/);
assert.match(gate, /needs: \[build-and-push, source_verification\]/);
assert.match(publisher, /needs: \[build-and-push, source_verification, publication_verification\]/);
assert.doesNotMatch(publisher, /--require-proof|actions: write/);
assert.ok(publisher.indexOf("run: node scripts/watch-release-source.mjs --check-proof") < publisher.indexOf("- name: Create manifest list and push"));
assert.match(publisher, /actions: read/);
assert.match(publisher, /timeout-minutes: 20/);
const expression = publisher.match(/^ if: \$\{\{ (.+) \}\}$/m)[1].replaceAll("needs.build-and-push", "needs.build");
for (const [ref, observed, fallback, expected, repository = "paperclipai/paperclip", repository_id = "1170821064"] of [
["refs/heads/master", "true", "skipped", true],
["refs/heads/master", "false", "skipped", false],
["refs/heads/master", undefined, "skipped", false],
["refs/heads/master", "false", "success", true],
["refs/heads/master", "false", "failure", false],
["refs/tags/v1", undefined, "skipped", true],
["refs/heads/master", undefined, "skipped", true, "fork/paperclip", "123"],
["refs/heads/master", undefined, "skipped", true, "paperclipai/paperclip", "123"],
]) {
const canonical = repository === "paperclipai/paperclip" && repository_id === "1170821064" && ref === "refs/heads/master";
assert.equal(runInNewContext(expression, { cancelled: () => false, github: { ref, repository, repository_id }, needs: {
build: { result: "success" }, source_verification: { result: canonical ? "success" : "skipped", outputs: { verified: observed } },
publication_verification: { result: fallback },
} }), expected);
}
});
test("chaos verification isolates callers that verify the same source commit", () => {
const chaosWorkflow = readWorkflow("runner-chaos-evals.yml");
const group = chaosWorkflow.match(/^ group: (.+)$/m)?.[1];
@@ -82,11 +115,11 @@ test("chaos verification isolates callers that verify the same source commit", (
assert.match(chaosWorkflow, /cancel-in-progress: true/);
});
test("canary reuses exact-source proof while stable keeps full verification", () => {
test("canary can admit deferred exact-source checks while stable keeps full verification", () => {
const releaseWorkflow = readWorkflow("release.yml");
const canary = releaseWorkflow.match(/ verify_canary:\n[\s\S]*?(?=\n [a-z_]+:)/)[0];
assert.match(canary, /github\.repository == 'paperclipai\/paperclip' && github\.event_name == 'push' && github\.ref == 'refs\/heads\/master'/);
assert.match(canary, /actions: read/);
assert.match(canary, /github\.repository == 'paperclipai\/paperclip' && github\.repository_id == '1170821064' && github\.event_name == 'push' && github\.ref == 'refs\/heads\/master'/);
assert.match(canary, /actions: write # restart only an exact-source push deferred before admission/);
assert.match(canary, /ref: \$\{\{ github\.sha \}\}/);
assert.match(canary, /SOURCE_SHA: \$\{\{ github\.sha \}\}/);
assert.match(canary, /run: node scripts\/cloud-source-verification\.mjs "\$SOURCE_SHA"/);
@@ -933,7 +966,9 @@ test('native dependency caches are target-bound and unmerged master callers cann
const cache = producer.split(' - name: Cache native Rust dependencies\n')[1].split('\n - ')[0];
assert.match(cache, /Swatinem\/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6/);
assert.match(cache, /workspaces: \$\{\{ steps.native_rust_workspace.outputs.path \}\} -> target/);
assert.match(cache, /shared-key: release-native-v2-\$\{\{ matrix.host \}\}-\$\{\{ matrix.target \}\}-\$\{\{ hashFiles\('packages\/paperclip-runner\/protocol\/\*\*', 'packages\/paperclip-runner\/src\/\*\*', 'packages\/paperclip-runner\/\*.json'\) \}\}/);
const key = cache.match(/^\s*shared-key: (.+)$/m)?.[1];
assert.ok(key?.startsWith("release-native-v2-${{ hashFiles('packages/paperclip-runner/runner/Cargo.toml', "));
assert.ok(key?.endsWith("-${{ matrix.host }}-${{ matrix.target }}-${{ hashFiles('packages/paperclip-runner/protocol/**', 'packages/paperclip-runner/src/**', 'packages/paperclip-runner/*.json') }}"));
for (const option of ['cache-workspace-crates', 'cache-bin', 'cache-on-failure']) {
assert.match(cache, new RegExp(option + ': false'));
}
+10 -2
View File
@@ -7,11 +7,19 @@ import { readPluginsUnder } from "./link-plugin-dev-sdk.mjs";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const postinstallHooks = new Map([
[".", "node scripts/link-plugin-dev-sdk.mjs"],
["server", "node scripts/install-copilot-assets.mjs"],
["packages/paperclip-runner", "node scripts/install-copilot-assets.mjs"],
]);
export function cachePlan({ files, read, scope, packageDirs }) {
const dependencyInputs = files.filter((file) =>
/(^|\/)package\.json$/.test(file)
|| ["pnpm-lock.yaml", "pnpm-workspace.yaml", ".npmrc", ".pnpmfile.cjs", "pnpmfile.cjs"].includes(file)
|| file.startsWith("scripts/")
|| file.startsWith("server/scripts/")
|| file.startsWith("packages/paperclip-runner/scripts/")
|| file.startsWith("patches/"));
const digest = (inputs, salt) => {
const hash = createHash("sha256").update(JSON.stringify(salt));
@@ -28,8 +36,8 @@ export function cachePlan({ files, read, scope, packageDirs }) {
if (manifest === null) return false;
const scripts = JSON.parse(manifest).scripts ?? {};
return ['preinstall', 'install', 'postinstall', 'prepare'].every((hook) =>
scripts[hook] === undefined || (directory === '.' && hook === 'postinstall'
&& scripts[hook] === 'node scripts/link-plugin-dev-sdk.mjs'));
scripts[hook] === undefined || (hook === 'postinstall'
&& scripts[hook] === postinstallHooks.get(directory)));
});
const buildInputs = files.filter((file) =>
file.startsWith("packages/shared/") || file.startsWith("packages/plugins/sdk/")
+123
View File
@@ -0,0 +1,123 @@
import { appendFile } from "node:fs/promises";
import { pathToFileURL } from "node:url";
import { createActionsReader } from "./github-actions-reader.mjs";
const repository = "paperclipai/paperclip";
const repositoryId = 1170821064;
const workflows = new Set(["docker.yml", "release.yml", "cloud-readiness.yml", "cloud-migrator-artifacts.yml"]);
export const sourceAdmissionJob = "Cloud candidate admission v1";
export const migratorAdmissionJob = "Cloud image candidate admission v1";
function assertSha(sha) {
if (!/^[a-f0-9]{40}$/.test(sha ?? "")) throw new Error("Admission requires a full source SHA.");
}
function trusted(run, sha, workflow) {
return run.workflow_id === workflow.id && run.path === workflow.path && run.head_sha === sha &&
run.repository?.id === repositoryId && run.head_repository?.id === repositoryId &&
run.repository?.full_name === repository && run.head_repository?.full_name === repository &&
run.head_branch === "master" && run.event === "push" &&
Number.isSafeInteger(run.id) && run.id > 0 && Number.isSafeInteger(run.run_attempt) && run.run_attempt > 0;
}
async function producer(file, sha, api, status) {
assertSha(sha);
if (!workflows.has(file)) throw new Error("Unknown release workflow.");
const workflow = await api(`/repos/${repository}/actions/workflows/${file}`);
if (workflow.path !== `.github/workflows/${file}` || !Number.isSafeInteger(workflow.id) || workflow.id < 1) throw new Error("Release workflow identity mismatch.");
const list = await api(`/repos/${repository}/actions/workflows/${workflow.id}/runs?branch=master&event=push&head_sha=${sha}&per_page=100${status ? `&status=${status}` : ""}`);
if (!Array.isArray(list.workflow_runs) || !Number.isSafeInteger(list.total_count) || list.total_count < 0 ||
list.total_count > 100 || list.workflow_runs.length !== list.total_count) throw new Error("Incomplete release workflow listing.");
const run = list.workflow_runs.filter(run => trusted(run, sha, workflow) && (!status || run.status === status)).sort((a, b) => b.id - a.id)[0];
return { workflow, run };
}
// A retry is an explicit request for this exact immutable source. Normal push
// work starts only for a candidate admitted by an image build or npm canary.
export async function releaseAdmitted(sha, { api, event = "push", attempt = 1, imageOnly = false }) {
assertSha(sha);
if (!Number.isSafeInteger(attempt) || attempt < 1) throw new Error("Invalid release attempt.");
if (!["push", "workflow_dispatch"].includes(event)) return false;
if (event === "workflow_dispatch" || (Number.isSafeInteger(attempt) && attempt > 1)) return true;
for (const file of imageOnly ? ["docker.yml"] : ["docker.yml", "release.yml"]) {
const { workflow, run } = await producer(file, sha, api, "in_progress");
if (!run) continue;
const current = await api(`/repos/${repository}/actions/runs/${run.id}`);
if (current.id === run.id && trusted(current, sha, workflow) && current.run_attempt === run.run_attempt && current.status === "in_progress") return true;
}
return false;
}
export function deferredProducer(jobs, run, admissionJob) {
const admission = jobs.filter(job => job.name === admissionJob);
const remaining = jobs.filter(job => job.name !== admissionJob);
return run.status === "completed" && run.conclusion === "success" && admission.length === 1 && admission[0].status === "completed" && admission[0].conclusion === "success" && remaining.length > 0 &&
remaining.every(job => job.status === "completed" && job.conclusion === "skipped") &&
jobs.every(job => job.run_id === run.id && job.run_attempt === run.run_attempt && job.head_sha === run.head_sha);
}
// Only skipped admission is restartable here. A failed check or build never
// becomes an automatic retry, and every write revalidates canonical identity.
export async function requestDeferredProducer({ file, sha, admissionJob, api, rerun, requested = new Set() }) {
if (({ "cloud-readiness.yml": sourceAdmissionJob, "cloud-migrator-artifacts.yml": migratorAdmissionJob })[file] !== admissionJob) throw new Error("Unknown deferred producer contract.");
const { workflow, run } = await producer(file, sha, api);
if (!run) return "pending";
const key = `${run.id}/${run.run_attempt}`;
if (requested.has(key)) return "pending";
const jobs = [];
let total;
for (let page = 1; page <= 10; page++) {
const batch = await api(`/repos/${repository}/actions/runs/${run.id}/attempts/${run.run_attempt}/jobs?per_page=100&page=${page}`);
if (!Array.isArray(batch.jobs) || !Number.isSafeInteger(batch.total_count) || batch.total_count < 0) throw new Error("Invalid release producer job list.");
if ((total !== undefined && total !== batch.total_count) ||
batch.jobs.length !== Math.min(100, Math.max(0, batch.total_count - (page - 1) * 100))) throw new Error("Incomplete release producer job list.");
total = batch.total_count;
jobs.push(...batch.jobs);
if (page * 100 >= total) break;
if (page === 10) throw new Error("Incomplete release producer job list.");
}
if (run.status !== "completed") {
const current = jobs.filter(job => job.run_id === run.id && job.run_attempt === run.run_attempt && job.head_sha === sha);
// Admission itself succeeds for both outcomes. Only an actual build is
// evidence that its output admitted work; queued jobs may still be skipped.
return current.some(job => job.name !== admissionJob && job.labels?.length > 0 && ["in_progress", "completed"].includes(job.status) && job.conclusion !== "skipped")
? "admitted" : "pending";
}
if (!deferredProducer(jobs, run, admissionJob)) return "finished";
const current = await api(`/repos/${repository}/actions/runs/${run.id}`);
if (current.id !== run.id || !trusted(current, sha, workflow) || current.run_attempt !== run.run_attempt || current.status !== "completed" || current.conclusion !== "success") return "pending";
try {
await rerun(`/repos/${repository}/actions/runs/${run.id}/rerun`);
} catch (error) {
// Docker and npm may select the same source at the same time. A rejected
// duplicate write is harmless only when a fresh read proves a retry began.
const latest = await api(`/repos/${repository}/actions/runs/${run.id}`);
if (latest.id !== run.id || !trusted(latest, sha, workflow)) throw error;
if (!(latest.run_attempt > run.run_attempt || (latest.run_attempt === run.run_attempt &&
["queued", "in_progress", "requested", "pending", "waiting"].includes(latest.status)))) {
// GitHub may reject a duplicate before its read API shows the retry.
// Leave it pending for the next poll; this is not proof of admission.
if ([409, 422].includes(error.status) && latest.run_attempt === run.run_attempt && latest.status === "completed" && latest.conclusion === "success") return "pending";
throw error;
}
}
requested.add(key);
return "pending";
}
export function createReleaseRerunner(token, fetchImpl = fetch) {
return async path => {
if (!/^\/repos\/paperclipai\/paperclip\/actions\/runs\/[1-9]\d*\/rerun$/.test(path)) throw new Error("Invalid release retry path.");
const response = await fetchImpl(`https://api.github.com${path}`, { method: "POST", redirect: "error",
headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json" }, signal: AbortSignal.timeout(30_000) });
if (!response.ok) throw Object.assign(new Error(`Could not admit deferred release work: HTTP ${response.status}.`), { status: response.status });
};
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
if (process.env.GITHUB_REPOSITORY !== repository || process.env.GITHUB_REF !== "refs/heads/master" || !process.env.GITHUB_TOKEN) throw new Error("Canonical master admission context required.");
const admitted = await releaseAdmitted(process.env.GITHUB_SHA, { api: createActionsReader({ token: process.env.GITHUB_TOKEN }),
event: process.env.GITHUB_EVENT_NAME, attempt: Number(process.env.GITHUB_RUN_ATTEMPT), imageOnly: process.argv[2] === "image-only" });
console.log(admitted ? "Release candidate admitted." : "Full build deferred until a release consumer admits this source.");
if (process.env.GITHUB_OUTPUT) await appendFile(process.env.GITHUB_OUTPUT, `admitted=${admitted}\n`);
}
+40 -80
View File
@@ -1,9 +1,19 @@
import { appendFile } from "node:fs/promises";
import { pathToFileURL } from "node:url";
import { createActionsReader } from "./github-actions-reader.mjs";
import { deferredProducer, requestDeferredProducer, sourceAdmissionJob, createReleaseRerunner } from "./cloud-release-admission.mjs";
const repository = "paperclipai/paperclip";
const workflowPath = ".github/workflows/cloud-readiness.yml";
export const sourceVerificationJob = "Cloud source verified v1";
export class SourceVerificationFailed extends Error {
constructor(message, run) {
super(message);
this.runId = run.id;
this.attempt = run.run_attempt;
}
}
export class SourceVerificationDeferred extends Error {}
function assertSha(sha) {
if (!/^[a-f0-9]{40}$/.test(sha ?? "")) throw new Error("A full lowercase source SHA is required.");
@@ -19,7 +29,7 @@ function trustedRun(run, sha, workflowId) {
// Consume one versioned job, independent of image/migrator availability. A
// failed image build must not invalidate source checks that already passed.
export async function readSourceVerification(sha, api) {
export async function readSourceVerification(sha, api, { failFast = false, onJobs } = {}) {
assertSha(sha);
const workflow = await api(`/repos/${repository}/actions/workflows/cloud-readiness.yml`);
if (workflow.path !== workflowPath || !Number.isSafeInteger(workflow.id) || workflow.id < 1) {
@@ -46,6 +56,17 @@ export async function readSourceVerification(sha, api) {
}
const matches = jobs.filter((job) => job.name === sourceVerificationJob);
if (matches.length > 1) throw new Error("Cloud source verification job is ambiguous.");
await onJobs?.(jobs.filter(job => job.head_sha === sha && job.run_id === run.id && job.run_attempt === run.run_attempt));
if (failFast) {
const failure = jobs.find((job) => job.status === "completed" &&
["failure", "cancelled", "timed_out", "action_required", "startup_failure", "stale"].includes(job.conclusion) &&
job.head_sha === sha && job.run_id === run.id && job.run_attempt === run.run_attempt);
if (failure) {
const current = await api(`/repos/${repository}/actions/runs/${run.id}`);
if (!trustedRun(current, sha, workflow.id) || current.run_attempt !== run.run_attempt) return undefined;
throw new SourceVerificationFailed(`Cloud source verification failed: ${failure.name} (${failure.conclusion}).`, run);
}
}
const job = matches[0];
if (job?.status === "completed" && job.conclusion === "success" &&
job.head_sha === sha && job.run_id === run.id && job.run_attempt === run.run_attempt) {
@@ -56,95 +77,33 @@ export async function readSourceVerification(sha, api) {
return { sha, runId: run.id, attempt: run.run_attempt, jobId: job.id };
}
if (job?.status === "completed" || run.status === "completed") {
throw new Error(`Cloud source verification did not pass for ${sha} (run ${run.id}, attempt ${run.run_attempt}). Rerun Cloud readiness before retrying the release.`);
const current = await api(`/repos/${repository}/actions/runs/${run.id}`);
if (!trustedRun(current, sha, workflow.id) || current.run_attempt !== run.run_attempt) return undefined;
if (job?.conclusion === "skipped" && current.status !== "completed") return undefined;
if (deferredProducer(jobs, current, sourceAdmissionJob)) throw new SourceVerificationDeferred("Source checks are deferred until release admission.");
throw new SourceVerificationFailed(`Cloud source verification did not pass for ${sha} (run ${run.id}, attempt ${run.run_attempt}). Rerun Cloud readiness before retrying the release.`, run);
}
return undefined;
}
// Statuses that say "ask again", not "the answer is no". A release must not be
// blocked because GitHub returned a gateway error during a 45-minute poll.
const TRANSIENT_READ_STATUSES = new Set([408, 425, 429, 500, 502, 503, 504]);
// A rate-limited read also says "ask again", and GitHub reports both primary
// and secondary rate limits as 403. Only the headers separate that from a
// token that may not read Actions, which must still fail at once.
function rateLimited(response) {
if (response.status !== 403) return false;
const header = (name) => response.headers?.get?.(name) ?? null;
return header("retry-after") !== null || header("x-ratelimit-remaining") === "0";
}
/** Milliseconds from a Retry-After header, when it carries a sane delay. */
function retryAfterMs(response, capMs) {
const value = Number(response?.headers?.get?.("retry-after"));
if (!Number.isFinite(value) || value <= 0) return null;
return Math.min(value * 1_000, capMs);
}
/**
* The GitHub Actions read used by the polling below, with transient transport
* failures retried: network errors, the statuses above, rate-limited 403s, and
* a body that fails while it is being read. Any other non-OK status throws on
* the first response, because waiting out a wrong token only delays the news.
*
* `deadlineAt` bounds retries by the caller's own polling deadline, so a read
* cannot extend the wait past the timeout it belongs to.
*/
export function createActionsReader({
token, fetchImpl = fetch, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
attempts = 4, backoffMs = 1_000, log = console.log,
now = Date.now, deadlineAt = () => Infinity, maxRetryAfterMs = 60_000,
} = {}) {
return async (path) => {
for (let attempt = 1; ; attempt += 1) {
// Retry only while both the attempt budget and the caller's deadline
// leave room for the wait this attempt would cost.
const waitMs = backoffMs * attempt;
const retryable = attempt < attempts && now() + waitMs < deadlineAt();
const pause = (response) => sleep(retryAfterMs(response, maxRetryAfterMs) ?? waitMs);
let response;
try {
response = await fetchImpl(`https://api.github.com${path}`, {
headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28" },
signal: AbortSignal.timeout(30_000), redirect: "error",
});
} catch (cause) {
if (!retryable) throw new Error(`GitHub Actions read failed: ${cause.message}`);
log(`GitHub Actions read failed (${cause.message}); retrying (${attempt}/${attempts - 1}).`);
await pause();
continue;
}
if (response.ok) {
try {
// A 200 whose body is truncated or undecodable is a transport
// failure like any other, so it belongs inside the retry.
return await response.json();
} catch (cause) {
if (!retryable) throw new Error(`GitHub Actions read failed: ${cause.message}`);
log(`GitHub Actions read body failed (${cause.message}); retrying (${attempt}/${attempts - 1}).`);
await pause();
continue;
}
}
if (!retryable || !(TRANSIENT_READ_STATUSES.has(response.status) || rateLimited(response))) {
throw new Error(`GitHub Actions read failed (HTTP ${response.status}).`);
}
log(`GitHub Actions read failed (HTTP ${response.status}); retrying (${attempt}/${attempts - 1}).`);
await pause(response);
}
};
}
export { createActionsReader };
export async function waitForSourceVerification(sha, {
api, now = Date.now, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
timeoutMs = 45 * 60_000, intervalMs = 30_000, log = console.log,
timeoutMs = 60 * 60_000, intervalMs = 30_000, log = console.log, rerun,
} = {}) {
assertSha(sha);
const deadline = now() + timeoutMs;
const requested = new Set();
log(`Waiting for ${sourceVerificationJob} for ${sha}.`);
while (now() < deadline) {
const result = await readSourceVerification(sha, api);
if (result) return result;
try {
const result = await readSourceVerification(sha, api);
if (result) return result;
} catch (error) {
if (!(error instanceof SourceVerificationDeferred) || !rerun) throw error;
await requestDeferredProducer({ file: "cloud-readiness.yml", sha, admissionJob: sourceAdmissionJob, api, rerun, requested });
}
const remaining = deadline - now();
if (remaining > 0) await sleep(Math.min(intervalMs, remaining));
}
@@ -156,10 +115,11 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
if (!process.env.GITHUB_TOKEN) throw new Error("GITHUB_TOKEN with Actions read access is required.");
// One deadline for both layers: the reader stops retrying when the poll it
// serves is out of time, instead of extending the wait past its timeout.
const timeoutMs = 45 * 60_000;
const timeoutMs = 60 * 60_000;
const deadline = Date.now() + timeoutMs;
const api = createActionsReader({ token: process.env.GITHUB_TOKEN, deadlineAt: () => deadline });
const proof = await waitForSourceVerification(process.argv[2], { api, timeoutMs });
const rerun = process.env.GITHUB_REPOSITORY === repository && process.env.GITHUB_REF === "refs/heads/master" ? createReleaseRerunner(process.env.GITHUB_TOKEN) : undefined;
const proof = await waitForSourceVerification(process.argv[2], { api, timeoutMs, rerun });
const message = `Source verification passed for ${proof.sha}: https://github.com/${repository}/actions/runs/${proof.runId}/attempts/${proof.attempt} (job ${proof.jobId}).`;
console.log(message);
if (process.env.GITHUB_STEP_SUMMARY) await appendFile(process.env.GITHUB_STEP_SUMMARY, `${message}\n`);
+418 -3
View File
@@ -1,8 +1,68 @@
import assert from "node:assert/strict";
import test from "node:test";
import { createActionsReader, readSourceVerification, sourceVerificationJob, waitForSourceVerification } from "./cloud-source-verification.mjs";
import test, { describe } from "node:test";
import { spawnSync } from "node:child_process";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { createActionsReader, readSourceVerification, sourceVerificationJob, waitForSourceVerification, SourceVerificationDeferred } from "./cloud-source-verification.mjs";
import { releaseAdmitted, deferredProducer, requestDeferredProducer, sourceAdmissionJob, migratorAdmissionJob, createReleaseRerunner } from "./cloud-release-admission.mjs";
import { cancelImageRun, runnerWaits, watchReleaseSource } from "./watch-release-source.mjs";
const sha = "a".repeat(40);
function runAdmissionCli({ active = false, imageOnly = false, repositoryName = "paperclipai/paperclip" } = {}) {
const root = mkdtempSync(join(tmpdir(), "release-admission-cli-"));
try {
const preload = join(root, "github-fixture.mjs");
const installFixture = (active) => {
const repository = { id: 1170821064, full_name: "paperclipai/paperclip" };
const run = { id: 77, workflow_id: 21, path: ".github/workflows/docker.yml", head_sha: "a".repeat(40),
repository, head_repository: repository, head_branch: "master", event: "push", run_attempt: 1, status: "in_progress" };
globalThis.fetch = async url => {
const parsed = new URL(url);
if (parsed.origin !== "https://api.github.com") throw new Error("Unexpected admission request origin");
const path = parsed.pathname;
if (path.endsWith("/docker.yml")) return Response.json({ id: 21, path: ".github/workflows/docker.yml" });
if (path.endsWith("/release.yml")) return Response.json({ id: 22, path: ".github/workflows/release.yml" });
if (path.endsWith("/workflows/21/runs")) return Response.json({ total_count: active ? 1 : 0, workflow_runs: active ? [run] : [] });
if (path.endsWith("/workflows/22/runs")) return Response.json({ total_count: 0, workflow_runs: [] });
if (path.endsWith("/runs/77")) return Response.json(run);
throw new Error("Unexpected admission request path");
};
};
writeFileSync(preload, "(" + installFixture.toString() + ")(" + JSON.stringify(active) + ");\n");
const output = join(root, "output");
const result = spawnSync(process.execPath, ["--import", preload, fileURLToPath(new URL("./cloud-release-admission.mjs", import.meta.url)), ...(imageOnly ? ["image-only"] : [])], {
encoding: "utf8", timeout: 5_000,
env: { ...process.env, GITHUB_REPOSITORY: repositoryName, GITHUB_REF: "refs/heads/master", GITHUB_SHA: "a".repeat(40),
GITHUB_EVENT_NAME: "push", GITHUB_RUN_ATTEMPT: "1", GITHUB_TOKEN: "fixture", GITHUB_OUTPUT: output },
});
return { ...result, output: result.status === 0 ? readFileSync(output, "utf8") : "" };
} finally {
rmSync(root, { recursive: true, force: true });
}
}
describe("admission CLI startup", () => {
for (const imageOnly of [false, true]) {
for (const active of [false, true]) {
test("executes " + (imageOnly ? "image-only" : "source") + " admission with active consumer=" + active, () => {
const result = runAdmissionCli({ active, imageOnly });
assert.equal(result.status, 0, result.stderr || result.error?.message);
assert.equal(result.output, "admitted=" + active + "\n");
});
}
}
test("rejects a noncanonical CLI context before discovering consumers", () => {
const result = runAdmissionCli({ repositoryName: "fork/paperclip" });
assert.equal(result.status, 1);
assert.match(result.stderr, /Canonical master admission context required/);
assert.equal(result.output, "");
});
});
const workflow = { id: 123, path: ".github/workflows/cloud-readiness.yml" };
const baseRun = {
id: 456, workflow_id: workflow.id, path: workflow.path, run_attempt: 2,
@@ -62,7 +122,8 @@ test("job identities and terminal failures fail closed", async () => {
{ head_sha: "b".repeat(40) }, { run_id: 999 }, { run_attempt: 1 },
{ conclusion: "failure" }, { conclusion: "cancelled" }, { conclusion: "skipped" },
]) {
const { api } = fixture({ jobs: [{ ...baseJob, ...overrides }] });
const { api } = fixture({ jobs: [{ ...baseJob, ...overrides }],
current: overrides.conclusion === "skipped" ? { ...baseRun, status: "completed", conclusion: "success" } : baseRun });
await assert.rejects(readSourceVerification(sha, api), /did not pass/);
}
});
@@ -231,3 +292,357 @@ test("retries stop at the caller's deadline rather than outliving the poll", asy
assert.equal(seen.length, 1);
assert.deepEqual(waits, []);
});
describe("image build source monitor", () => {
const sha = "a".repeat(40);
const runId = 50;
const attempt = 1;
const workflow = { id: 123, path: ".github/workflows/cloud-readiness.yml" };
const sourceRun = { id: 99, workflow_id: 123, path: workflow.path, run_attempt: 1,
repository: { full_name: "paperclipai/paperclip" }, head_repository: { full_name: "paperclipai/paperclip" },
head_sha: sha, head_branch: "master", event: "push", status: "in_progress" };
const imageRun = { id: runId, run_attempt: attempt, head_sha: sha, head_branch: "master", event: "push",
repository: { id: 1170821064 }, head_repository: { id: 1170821064 },
path: ".github/workflows/docker.yml", status: "in_progress" };
const proof = { id: 111, name: "Cloud source verified v1", run_id: 99, run_attempt: 1,
head_sha: sha, status: "completed", conclusion: "success" };
function fixture({ jobs = [proof], run = sourceRun, current = run, image = imageRun, imageJobs = [], missing = false } = {}) {
const cancellations = [];
const messages = [];
const api = async path => {
if (path.endsWith("/workflows/cloud-readiness.yml")) return workflow;
if (path.includes("/workflows/123/runs?")) return { workflow_runs: missing ? [] : [run], total_count: missing ? 0 : 1 };
if (path.includes("/runs/99/attempts/1/jobs?")) return { jobs };
if (path.endsWith("/runs/99")) return current;
if (path.includes(`/runs/${runId}/attempts/${attempt}/jobs?`)) return { jobs: imageJobs, total_count: imageJobs.length };
if (path.endsWith(`/runs/${runId}`)) return image;
throw new Error(`Unexpected request: ${path}`);
};
return { sha, runId, attempt, api, cancel: async path => cancellations.push(path),
cancellations, messages, log: message => messages.push(message), summary: async message => messages.push(message) };
}
test("successful exact-source proof permits publication without canceling image work", async () => {
const f = fixture();
assert.equal((await watchReleaseSource(f)).jobId, proof.id);
assert.deepEqual(f.cancellations, []);
});
test("a required check failing cancels the same image attempt before siblings finish", async () => {
const f = fixture({ jobs: [{ ...proof, name: "General tests", conclusion: "failure" }] });
await assert.rejects(watchReleaseSource(f), /General tests/);
assert.deepEqual(f.cancellations, ["/repos/paperclipai/paperclip/actions/runs/99/cancel", `/repos/paperclipai/paperclip/actions/runs/${runId}/cancel`]);
});
test("a superseded readiness attempt cancels image work with no deployable future", async () => {
const f = fixture({ jobs: [], run: { ...sourceRun, status: "completed", conclusion: "cancelled" } });
await assert.rejects(watchReleaseSource(f), /did not pass/);
assert.equal(f.cancellations.length, 1);
});
test("a rerun racing a failed job is retried rather than canceling the image", async () => {
const f = fixture({ jobs: [{ ...proof, name: "General tests", conclusion: "failure" }],
current: { ...sourceRun, run_attempt: 2 } });
let time = 0;
const api = f.api;
f.api = async path => time > 0 ? fixture().api(path) : api(path);
const result = await watchReleaseSource({ ...f, now: () => time, sleep: async ms => { time += ms; } });
assert.equal(result.jobId, proof.id);
assert.deepEqual(f.cancellations, []);
});
test("a transport failure does not issue a destructive cancellation", async () => {
const f = fixture();
await assert.rejects(watchReleaseSource({ ...f, api: async () => { throw new Error("GitHub unavailable"); } }), /unavailable/);
assert.deepEqual(f.cancellations, []);
});
test("the observation window ends without cancelling healthy pending verification", async () => {
const f = fixture({ missing: true });
let time = 0;
const proof = await watchReleaseSource({ ...f, now: () => time, sleep: async ms => { time += ms; }, timeoutMs: 20 });
assert.equal(proof, undefined);
assert.equal(time, 20);
assert.deepEqual(f.cancellations, []);
assert.ok(f.messages.some(message => message.includes("final publication still requires exact-source verification")));
});
test("final publication requires proof even after the early observation window ends", async () => {
const f = fixture({ missing: true });
let time = 0;
await assert.rejects(watchReleaseSource({ ...f, requireProof: true, now: () => time,
sleep: async ms => { time += ms; }, timeoutMs: 20 }), /before the publication deadline/);
assert.deepEqual(f.cancellations, []);
assert.equal((await watchReleaseSource({ ...fixture(), requireProof: true })).jobId, proof.id);
});
test("cancellation rejects another SHA, repository, ref, workflow, or attempt", async () => {
for (const change of [{ head_sha: "b".repeat(40) }, { head_branch: "v1" }, { event: "pull_request" },
{ path: ".github/workflows/release.yml" }, { run_attempt: 2 }, { id: 51 },
{ repository: { id: 42 } }, { head_repository: { id: 42 } }]) {
const f = fixture({ image: { ...imageRun, ...change } });
await assert.rejects(cancelImageRun(f), /different image workflow identity/);
assert.deepEqual(f.cancellations, []);
}
});
test("queue measurement does not count running time or skipped dependency jobs", () => {
const created_at = "2020-01-01T00:00:00Z";
const started_at = "2020-01-01T00:00:20Z";
const job = { id: 1, name: "amd64", labels: ["release"], created_at, started_at, runner_id: 10, status: "in_progress" };
const now = Date.parse(created_at) + 120_000;
assert.deepEqual(runnerWaits([job, { ...job, status: "completed", conclusion: "skipped" }], now), [{ id: 1, job: "amd64", seconds: 20 }]);
assert.equal(runnerWaits([{ ...job, status: "queued", runner_id: 0 }], now)[0].seconds, 120);
});
test("a runner wait over 60 seconds produces an Actions warning and summary", async () => {
const f = fixture({ imageJobs: [{ id: 1, name: "amd64", labels: ["release"], status: "queued", runner_id: 0, created_at: "2020-01-01T00:00:00Z" }] });
await watchReleaseSource({ ...f, now: () => Date.parse("2020-01-01T00:02:00Z") });
assert.ok(f.messages.some(message => message.startsWith("::warning") && message.includes("120s")));
assert.ok(f.messages.some(message => message.includes("Release runner wait: amd64")));
});
});
describe("release candidate admission", () => {
const repository = { id: 1170821064, full_name: "paperclipai/paperclip" };
const ids = { "docker.yml": 101, "release.yml": 102, "cloud-readiness.yml": 123, "cloud-migrator-artifacts.yml": 124 };
function run(file, change = {}) {
return { id: ids[file] + 1000, workflow_id: ids[file], path: `.github/workflows/${file}`,
repository, head_repository: repository, head_sha: sha, head_branch: "master", event: "push",
run_attempt: 1, status: "in_progress", conclusion: null, ...change };
}
function job(run, name, conclusion = "skipped") {
return { id: 789, run_id: run.id, run_attempt: run.run_attempt, head_sha: run.head_sha,
name, status: "completed", conclusion };
}
function fixture(file = "cloud-readiness.yml", change = {}) {
const producer = run(file, { status: "completed", conclusion: "success", ...change });
const admission = file === "cloud-migrator-artifacts.yml" ? migratorAdmissionJob : sourceAdmissionJob;
const f = { runs: { [file]: [producer] }, jobs: { [producer.id]: [job(producer, admission, "success"), job(producer, sourceVerificationJob)] },
current: {}, writes: [], calls: [], requested: new Set(), file, sha, admissionJob: admission };
f.api = async path => {
f.calls.push(path);
const url = new URL(`https://api.github.com${path}`);
const fileMatch = path.match(/\/workflows\/([^/?]+\.yml)$/);
if (fileMatch) return { id: ids[fileMatch[1]], path: `.github/workflows/${fileMatch[1]}` };
const listMatch = path.match(/\/workflows\/(\d+)\/runs\?/);
if (listMatch) {
const name = Object.keys(ids).find(file => ids[file] === Number(listMatch[1]));
const runs = f.runs[name] ?? [];
return { workflow_runs: runs, total_count: f.total ?? runs.length };
}
const jobMatch = path.match(/\/runs\/(\d+)\/attempts\/(\d+)\/jobs\?/);
if (jobMatch) {
const page = Number(url.searchParams.get("page") ?? "1");
const jobs = f.jobs[Number(jobMatch[1])] ?? [];
return { total_count: f.jobTotal ?? jobs.length, jobs: jobs.slice((page - 1) * 100, page * 100) };
}
const currentMatch = path.match(/\/runs\/(\d+)$/);
if (currentMatch) return f.current[Number(currentMatch[1])] ?? Object.values(f.runs).flat().find(run => run.id === Number(currentMatch[1]));
throw new Error(`Unexpected admission API path: ${path}`);
};
f.rerun = async path => f.writes.push(path);
return f;
}
test("only a running image or npm release admits a normal push", async () => {
for (const file of ["docker.yml", "release.yml"]) {
const f = fixture();
assert.equal(await releaseAdmitted(sha, f), false);
f.runs[file] = [run(file)];
assert.equal(await releaseAdmitted(sha, f), true);
assert.equal(await releaseAdmitted(sha, { ...f, imageOnly: true }), file === "docker.yml");
for (const status of ["queued", "pending", "completed"]) {
f.runs[file] = [run(file, { status })];
assert.equal(await releaseAdmitted(sha, f), false, status);
}
}
});
test("manual and retry requests admit exact source without consumer discovery", async () => {
const api = () => assert.fail("explicit request needs no discovery");
assert.equal(await releaseAdmitted(sha, { api, event: "workflow_dispatch" }), true);
assert.equal(await releaseAdmitted(sha, { api, attempt: 2 }), true);
assert.equal(await releaseAdmitted(sha, { api, event: "pull_request", attempt: 2 }), false);
for (const attempt of [0, "2"]) {
await assert.rejects(releaseAdmitted(sha, { api, attempt }), /Invalid release attempt/);
}
});
test("forks, stale identities, tags, and non-push consumers do not admit work", async () => {
for (const change of [{ repository: { ...repository, id: 2 } }, { head_repository: { ...repository, id: 2 } },
{ repository: { ...repository, full_name: "fork/paperclip" } }, { path: ".github/workflows/pr.yml" },
{ workflow_id: 999 }, { head_sha: "b".repeat(40) }, { head_branch: "v1" }, { event: "workflow_dispatch" },
{ id: 0 }, { run_attempt: 0 }]) {
const f = fixture();
f.runs["docker.yml"] = [run("docker.yml", change)];
assert.equal(await releaseAdmitted(sha, f), false, JSON.stringify(change));
}
const f = fixture();
const active = run("docker.yml");
f.runs["docker.yml"] = [active];
f.current[active.id] = { ...active, run_attempt: 2 };
assert.equal(await releaseAdmitted(sha, f), false);
});
test("incomplete discovery and transport failure fail closed", async () => {
for (const total of [1, 101, -1]) {
const f = fixture();
f.total = total;
await assert.rejects(releaseAdmitted(sha, f), /Incomplete/);
assert.deepEqual(f.writes, []);
}
await assert.rejects(releaseAdmitted(sha, { api: async () => { throw new Error("API unavailable"); } }), /unavailable/);
await assert.rejects(releaseAdmitted("master", { api: () => assert.fail() }), /full source SHA/);
});
test("a selected consumer restarts only a strictly deferred producer, once", async () => {
for (const file of ["cloud-readiness.yml", "cloud-migrator-artifacts.yml"]) {
const f = fixture(file);
assert.equal(await requestDeferredProducer(f), "pending");
assert.deepEqual(f.writes, [`/repos/paperclipai/paperclip/actions/runs/${ids[file] + 1000}/rerun`]);
await requestDeferredProducer(f);
assert.equal(f.writes.length, 1);
}
});
test("failed, incomplete, ambiguous, or mismatched work is never automatically retried", async () => {
for (const change of [jobs => { jobs[1].conclusion = "failure"; }, jobs => { jobs[0].conclusion = "failure"; },
jobs => { jobs[1].run_attempt = 2; }, jobs => { jobs[1].head_sha = "b".repeat(40); },
jobs => { jobs[1].run_id = 2; }, jobs => { jobs[1].status = "queued"; },
jobs => { jobs.push({ ...jobs[0] }); }, jobs => { jobs.splice(1); }]) {
const f = fixture();
change(f.jobs[1123]);
assert.equal(await requestDeferredProducer(f), "finished");
assert.deepEqual(f.writes, []);
}
const f = fixture();
for (const change of [{ run_attempt: 2 }, { status: "in_progress" }, { head_sha: "b".repeat(40) }, { id: 2 }]) {
f.current[1123] = { ...f.runs[f.file][0], ...change };
assert.equal(await requestDeferredProducer(f), "pending");
assert.deepEqual(f.writes, []);
}
});
test("an explicitly cancelled or failed producer cannot be restarted by admission", async () => {
for (const conclusion of ["cancelled", "failure", "timed_out"]) {
const f = fixture("cloud-readiness.yml", { conclusion });
assert.equal(await requestDeferredProducer(f), "finished");
assert.deepEqual(f.writes, []);
}
});
test("a truncated job page cannot hide a failed check and trigger a retry", async () => {
const f = fixture();
f.jobTotal = 3;
await assert.rejects(requestDeferredProducer(f), /Incomplete release producer job list/);
assert.deepEqual(f.writes, []);
f.jobTotal = "2";
await assert.rejects(requestDeferredProducer(f), /Invalid release producer job list/);
assert.deepEqual(f.writes, []);
});
test("producer retries reject arbitrary workflow and admission contracts", async () => {
const f = fixture();
for (const change of [{ file: "docker.yml" }, { admissionJob: migratorAdmissionJob }]) {
await assert.rejects(requestDeferredProducer({ ...f, ...change }), /Unknown deferred/);
}
assert.deepEqual(f.calls, []);
});
test("a deferred completion racing the run listing does not become a source failure", async () => {
const f = fixture();
const completed = f.runs[f.file][0];
f.runs[f.file] = [{ ...completed, status: "in_progress", conclusion: null }];
f.current[completed.id] = completed;
await assert.rejects(readSourceVerification(sha, f.api), SourceVerificationDeferred);
delete f.current[completed.id];
assert.equal(await readSourceVerification(sha, f.api), undefined);
});
test("a competing consumer's accepted retry does not fail the losing consumer", async () => {
const f = fixture();
f.rerun = async () => {
f.current[1123] = run(f.file, { run_attempt: 2 });
throw new Error("duplicate restart rejected");
};
assert.equal(await requestDeferredProducer(f), "pending");
assert.equal(f.requested.size, 1);
const rejected = fixture();
rejected.rerun = async () => { throw new Error("token denied"); };
await assert.rejects(requestDeferredProducer(rejected), /token denied/);
assert.equal(rejected.requested.size, 0);
});
test("a queued migrator is pending until its build actually starts", async () => {
const f = fixture("cloud-migrator-artifacts.yml", { status: "in_progress", conclusion: null });
const producer = f.runs[f.file][0];
const build = { ...job(producer, "Build migrator", null), status: "queued", labels: ["cloud-release-32"] };
f.jobs[producer.id] = [job(producer, migratorAdmissionJob, "success"), build];
assert.equal(await requestDeferredProducer(f), "pending");
build.status = "in_progress";
assert.equal(await requestDeferredProducer(f), "admitted");
build.run_attempt = 2;
assert.equal(await requestDeferredProducer(f), "pending", "a stale build cannot admit work");
assert.deepEqual(f.writes, []);
});
test("a rejected duplicate with stale read visibility remains pending without claiming admission", async () => {
for (const status of [409, 422, 403]) {
const f = fixture();
f.rerun = async () => { throw Object.assign(new Error("retry rejected"), { status }); };
if (status === 403) await assert.rejects(requestDeferredProducer(f), /retry rejected/);
else assert.equal(await requestDeferredProducer(f), "pending");
assert.equal(f.requested.size, 0);
}
});
test("source proof waits for admission, restarts it, and accepts only the new attempt", async () => {
const f = fixture();
await assert.rejects(readSourceVerification(sha, f.api), SourceVerificationDeferred);
let time = 0;
const proof = await waitForSourceVerification(sha, { ...f, now: () => time, intervalMs: 10, timeoutMs: 30, log: () => {},
sleep: async ms => {
time += ms;
const selected = run(f.file, { run_attempt: 2 });
f.runs[f.file] = [selected];
f.jobs[selected.id] = [job(selected, sourceAdmissionJob, "success"), job(selected, sourceVerificationJob, "success")];
} });
assert.equal(proof.attempt, 2);
assert.equal(time, 10);
assert.equal(f.writes.length, 1);
});
test("the image monitor admits a late migrator even when source proof is ready", async () => {
const f = fixture("cloud-migrator-artifacts.yml");
const source = run("cloud-readiness.yml");
f.runs["cloud-readiness.yml"] = [source];
f.jobs[source.id] = [job(source, sourceVerificationJob, "success")];
const image = run("docker.yml");
f.runs["docker.yml"] = [image];
f.jobs[image.id] = [];
let time = 0;
const proof = await watchReleaseSource({ ...f, runId: image.id, attempt: 1, cancel: () => assert.fail("must not cancel"),
now: () => time, intervalMs: 10, timeoutMs: 30, log: () => {}, summary: async () => {}, sleep: async ms => {
time += ms;
const selected = run(f.file, { run_attempt: 2 });
f.runs[f.file] = [selected];
f.jobs[selected.id] = [{ ...job(selected, "Build migrator", null), status: "in_progress", labels: ["cloud-release-32"] }];
} });
assert.equal(proof.sha, sha);
assert.equal(time, 10);
assert.equal(f.writes.length, 1);
});
test("the retry transport is a bounded POST to a canonical run", async () => {
const calls = [];
const rerun = createReleaseRerunner("fixture", async (url, options) => { calls.push({ url, options }); return { ok: true }; });
await assert.rejects(rerun("/repos/fork/paperclip/actions/runs/1/rerun"), /Invalid release retry/);
await rerun("/repos/paperclipai/paperclip/actions/runs/1/rerun");
assert.equal(calls.length, 1);
assert.equal(calls[0].options.method, "POST");
assert.equal(calls[0].options.redirect, "error");
});
});
+73
View File
@@ -0,0 +1,73 @@
// Statuses that say "ask again", not "the answer is no". A release must not be
// blocked because GitHub returned a gateway error during a 60-minute poll.
const TRANSIENT_READ_STATUSES = new Set([408, 425, 429, 500, 502, 503, 504]);
// A rate-limited read also says "ask again", and GitHub reports both primary
// and secondary rate limits as 403. Only the headers separate that from a
// token that may not read Actions, which must still fail at once.
function rateLimited(response) {
if (response.status !== 403) return false;
const header = (name) => response.headers?.get?.(name) ?? null;
return header("retry-after") !== null || header("x-ratelimit-remaining") === "0";
}
/** Milliseconds from a Retry-After header, when it carries a sane delay. */
function retryAfterMs(response, capMs) {
const value = Number(response?.headers?.get?.("retry-after"));
if (!Number.isFinite(value) || value <= 0) return null;
return Math.min(value * 1_000, capMs);
}
/**
* The GitHub Actions read used by the polling below, with transient transport
* failures retried: network errors, the statuses above, rate-limited 403s, and
* a body that fails while it is being read. Any other non-OK status throws on
* the first response, because waiting out a wrong token only delays the news.
*
* `deadlineAt` bounds retries by the caller's own polling deadline, so a read
* cannot extend the wait past the timeout it belongs to.
*/
export function createActionsReader({
token, fetchImpl = fetch, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
attempts = 4, backoffMs = 1_000, log = console.log,
now = Date.now, deadlineAt = () => Infinity, maxRetryAfterMs = 60_000,
} = {}) {
return async (path) => {
for (let attempt = 1; ; attempt += 1) {
// Retry only while both the attempt budget and the caller's deadline
// leave room for the wait this attempt would cost.
const waitMs = backoffMs * attempt;
const retryable = attempt < attempts && now() + waitMs < deadlineAt();
const pause = (response) => sleep(retryAfterMs(response, maxRetryAfterMs) ?? waitMs);
let response;
try {
response = await fetchImpl(`https://api.github.com${path}`, {
headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28" },
signal: AbortSignal.timeout(30_000), redirect: "error",
});
} catch (cause) {
if (!retryable) throw new Error(`GitHub Actions read failed: ${cause.message}`);
log(`GitHub Actions read failed (${cause.message}); retrying (${attempt}/${attempts - 1}).`);
await pause();
continue;
}
if (response.ok) {
try {
// A 200 whose body is truncated or undecodable is a transport
// failure like any other, so it belongs inside the retry.
return await response.json();
} catch (cause) {
if (!retryable) throw new Error(`GitHub Actions read failed: ${cause.message}`);
log(`GitHub Actions read body failed (${cause.message}); retrying (${attempt}/${attempts - 1}).`);
await pause();
continue;
}
}
if (!retryable || !(TRANSIENT_READ_STATUSES.has(response.status) || rateLimited(response))) {
throw new Error(`GitHub Actions read failed (HTTP ${response.status}).`);
}
log(`GitHub Actions read failed (HTTP ${response.status}); retrying (${attempt}/${attempts - 1}).`);
await pause(response);
}
};
}
@@ -0,0 +1,54 @@
export interface SleepQualificationIdentity {
appSha: string;
cloudSha: string;
target: string;
configDigest: string;
collectorOrigins?: { telemetry: string; otel: string };
}
export interface SleepQualificationReport {
version: number;
target: string;
appSha: string;
cloudSha: string;
configDigest: string;
layers: Record<string, { verdict: 'VERIFIED' | 'NOT VERIFIED' | 'INCONCLUSIVE'; reason: string }>;
}
export function logicalSleepConfiguration(configuration: unknown): {
automaticBackups: { intervalMinutes: number; idleCheckpoint: boolean };
announcements: boolean;
telemetry: boolean;
otel: { enabled: boolean; protocol: 'http/json' };
serveUi: boolean;
managedInstance: boolean;
deploymentMode: 'authenticated' | 'local_trusted';
storageProvider: 's3' | 'local_disk';
};
export function sleepConfigurationDigest(configuration: unknown): string;
export interface SleepSignalObservation {
telemetryRequests: number; traceRequests: number; exportedSpans: number; qualificationSpans: number;
metricRequests: number; logRequests: number; malformedTraceRequests: number; unexpectedRequests: number;
}
export interface IncomingHttpSnapshot {
source: 'node-http-server-events';
captureId: string;
installed: boolean;
valid: boolean;
requests: number;
observedAt: string;
}
export interface IncomingHttpWindow {
before: IncomingHttpSnapshot;
after: IncomingHttpSnapshot;
requests: number;
}
export function observeIncomingHttpWindow(before: IncomingHttpSnapshot, after: IncomingHttpSnapshot): IncomingHttpWindow;
export function observedNoIncomingHttp(observation: unknown, quiet: { startedAt: string; finishedAt: string }): boolean;
export function observeConfiguredSignals(requests: ReadonlyArray<{ path: string; body: unknown }>): SleepSignalObservation;
export function configuredSleepFeatures(observations: Record<string, unknown>): {
telemetry: boolean; announcements: boolean; automaticBackups: boolean; otel: boolean; exportedSpans: number;
};
export function evaluateSleepQualification(receipt: unknown, expected: SleepQualificationIdentity): SleepQualificationReport;
export function finalizeLocalSleepReceipt<T extends Record<string, unknown>>(observations: T, expected: SleepQualificationIdentity, now?: () => string): {
receipt: T & { finishedAt: string };
report: SleepQualificationReport;
};
+231
View File
@@ -0,0 +1,231 @@
import { createHash } from 'node:crypto';
import { readFile, writeFile } from 'node:fs/promises';
import { pathToFileURL } from 'node:url';
const sha = value => typeof value === 'string' && /^[a-f0-9]{40}$/.test(value);
const finite = value => typeof value === 'number' && Number.isFinite(value) && value >= 0;
const stamp = value => typeof value === 'string' && Number.isFinite(Date.parse(value));
const layers = ['workerQuiet', 'eligibility', 'databaseSuspension', 'hostSuspension', 'backupPersistence', 'inboundWake', 'timedWake', 'returnToIdle'];
const result = (verdict, reason) => ({ verdict, reason });
const announcementFeed = 'https://pages.paperclip.ing/announcements/v1/current.json';
const observedAnnouncement = announcement => announcement?.intercepted === true
&& Array.isArray(announcement.requests) && announcement.requests.length > 0
&& announcement.requests.every(url => url === announcementFeed)
&& Array.isArray(announcement.unmatched) && announcement.unmatched.length === 0;
const signalKeys = ['telemetryRequests', 'traceRequests', 'exportedSpans', 'qualificationSpans', 'metricRequests', 'logRequests', 'malformedTraceRequests', 'unexpectedRequests'];
const knownSignalCounts = signal => signal && signalKeys.every(key => Number.isSafeInteger(signal[key]) && signal[key] >= 0);
/** Preserve actual cumulative app-server snapshots; IPC reads create no HTTP traffic. */
export function observeIncomingHttpWindow(before, after) {
return { before, after, requests: after?.requests - before?.requests };
}
const knownIncomingHttpSnapshot = snapshot => snapshot?.source === 'node-http-server-events'
&& typeof snapshot.captureId === 'string' && /^[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}$/.test(snapshot.captureId)
&& snapshot.installed === true && snapshot.valid === true
&& Number.isSafeInteger(snapshot.requests) && snapshot.requests >= 0 && stamp(snapshot.observedAt);
export const observedNoIncomingHttp = (observation, quiet) => knownIncomingHttpSnapshot(observation?.before)
&& knownIncomingHttpSnapshot(observation?.after) && observation.before.captureId === observation.after.captureId
&& stamp(quiet?.startedAt) && stamp(quiet?.finishedAt) && Date.parse(quiet.finishedAt) >= Date.parse(quiet.startedAt)
&& Date.parse(observation.before.observedAt) <= Date.parse(quiet.startedAt)
&& Date.parse(observation.after.observedAt) >= Date.parse(quiet.finishedAt)
&& Number.isSafeInteger(observation.requests) && observation.requests === 0
&& observation.after.requests - observation.before.requests === observation.requests;
/** Count actual collector requests and OTLP span records, never request-as-span estimates. */
export function observeConfiguredSignals(requests) {
const observed = Object.fromEntries(signalKeys.map(key => [key, 0]));
for (const request of requests) {
if (typeof request?.path !== 'string' || !request.path.startsWith('/') || request.path.startsWith('//')) { observed.unexpectedRequests++; continue; }
const pathname = new URL(request.path, 'http://owned-collector').pathname;
if (pathname === '/telemetry') { observed.telemetryRequests++; continue; }
if (pathname === '/v1/metrics') { observed.metricRequests++; continue; }
if (pathname === '/v1/logs') { observed.logRequests++; continue; }
if (pathname !== '/v1/traces') { observed.unexpectedRequests++; continue; }
observed.traceRequests++;
try {
if (!Array.isArray(request.body?.resourceSpans)) throw new Error('Missing resourceSpans');
let spans = 0, qualificationSpans = 0;
for (const resource of request.body.resourceSpans) {
if (!Array.isArray(resource?.scopeSpans)) throw new Error('Missing scopeSpans');
for (const scope of resource.scopeSpans) {
if (!Array.isArray(scope?.spans)) throw new Error('Missing spans');
for (const span of scope.spans) {
if (typeof span?.name !== 'string' || !span.name) throw new Error('Invalid span');
spans++; if (span.name === 'sleep-qualification-stimulus') qualificationSpans++;
}
}
}
observed.exportedSpans += spans; observed.qualificationSpans += qualificationSpans;
} catch { observed.malformedTraceRequests++; }
}
return observed;
}
/** Coverage booleans come from recorded activity; the evaluator also checks their provenance. */
export function configuredSleepFeatures({ announcement, signalObservation, backupCheckpoint }) {
const known = knownSignalCounts(signalObservation);
return { telemetry: Boolean(known && signalObservation.telemetryRequests > 0), announcements: Boolean(observedAnnouncement(announcement)),
automaticBackups: backupCheckpoint?.owner === 'normal-idle-checkpoint' && backupCheckpoint.wakeMarkerObserved === true
&& backupCheckpoint.failureObserved === false && Array.isArray(backupCheckpoint.archives) && backupCheckpoint.archives.length > 0,
otel: Boolean(known && signalObservation.traceRequests > 0 && signalObservation.exportedSpans > 0
&& signalObservation.qualificationSpans > 0 && signalObservation.qualificationSpans <= signalObservation.exportedSpans
&& signalObservation.metricRequests === 0 && signalObservation.logRequests === 0
&& signalObservation.malformedTraceRequests === 0 && signalObservation.unexpectedRequests === 0),
exportedSpans: known ? signalObservation.exportedSpans : 0 };
}
const validBackupCheckpoint = (backup, inRun, quietFinishedAt) => backup?.owner === 'normal-idle-checkpoint' && backup.wakeMarkerObserved === true
&& backup.failureObserved === false && inRun(backup.startedAt) && inRun(backup.observedAt)
&& Date.parse(backup.startedAt) >= Date.parse(quietFinishedAt) && Date.parse(backup.observedAt) >= Date.parse(backup.startedAt) && Array.isArray(backup.archives) && backup.archives.length > 0
&& backup.archives.every(archive => typeof archive.name === 'string' && /^[^/\\]+\.sql\.gz$/.test(archive.name)
&& finite(archive.bytes) && archive.bytes > 0 && finite(archive.sqlBytes) && archive.sqlBytes > 0
&& /^[a-f0-9]{64}$/.test(archive.sha256 ?? '') && archive.gzipVerified === true && archive.fixtureIdentifierFound === true
&& ['birthtime', 'mtime'].includes(archive.createdAtSource)
&& inRun(archive.createdAt) && Date.parse(archive.createdAt) >= Date.parse(backup.startedAt) && Date.parse(archive.createdAt) <= Date.parse(backup.observedAt));
/** Canonical logical configuration: ephemeral collector addresses are bound separately. */
export function logicalSleepConfiguration(configuration) {
if (!configuration || typeof configuration.announcements !== 'boolean' || typeof configuration.serveUi !== 'boolean'
|| typeof configuration.telemetryOrigin !== 'string' || typeof configuration.otelOrigin !== 'string'
|| (configuration.managedInstance !== undefined && typeof configuration.managedInstance !== 'boolean')
|| (configuration.deploymentMode !== undefined && !['authenticated', 'local_trusted'].includes(configuration.deploymentMode))
|| (configuration.storageProvider !== undefined && !['s3', 'local_disk'].includes(configuration.storageProvider))
|| typeof configuration.automaticBackups?.idleCheckpoint !== 'boolean'
|| !finite(configuration.automaticBackups?.intervalMinutes) || configuration.automaticBackups.intervalMinutes < 1) {
throw new Error('sleep_qualification_configuration_invalid');
}
return { automaticBackups: { intervalMinutes: configuration.automaticBackups.intervalMinutes,
idleCheckpoint: configuration.automaticBackups.idleCheckpoint }, announcements: configuration.announcements,
telemetry: Boolean(configuration.telemetryOrigin), otel: { enabled: Boolean(configuration.otelOrigin), protocol: 'http/json' }, serveUi: configuration.serveUi, managedInstance: configuration.managedInstance ?? false,
deploymentMode: configuration.deploymentMode ?? 'local_trusted', storageProvider: configuration.storageProvider ?? 'local_disk' };
}
export const sleepConfigurationDigest = configuration => createHash('sha256').update(JSON.stringify(logicalSleepConfiguration(configuration))).digest('hex');
/** Finish after observations, then validate the exact serialized local receipt. */
export function finalizeLocalSleepReceipt(observations, expected, now = () => new Date().toISOString()) {
const receipt = { ...observations, finishedAt: now() };
const report = evaluateSleepQualification(receipt, expected);
if (report.layers.workerQuiet.verdict !== 'VERIFIED' || report.layers.eligibility.verdict !== 'VERIFIED') {
throw new Error('sleep_qualification_local_evidence_invalid');
}
return { receipt, report };
}
/** Evaluate this campaign's measured boundaries. A local run cannot certify a provider. */
export function evaluateSleepQualification(receipt, expected) {
if (!receipt || receipt.version !== 1 || !expected || !sha(expected.appSha)
|| !sha(expected.cloudSha) || typeof expected.target !== 'string' || !expected.target
|| typeof expected.configDigest !== 'string' || !/^[a-f0-9]{64}$/.test(expected.configDigest)
|| !stamp(receipt.startedAt) || !stamp(receipt.finishedAt)
|| Date.parse(receipt.finishedAt) < Date.parse(receipt.startedAt)
|| ['appSha', 'cloudSha', 'target', 'configDigest'].some(key => receipt[key] !== expected[key])) {
throw new Error('sleep_qualification_identity_or_window_invalid');
}
if (sleepConfigurationDigest(receipt.configuration) !== expected.configDigest) throw new Error('sleep_qualification_configuration_digest_invalid');
const origins = receipt.collectorOrigins;
if (!origins || origins.telemetry !== receipt.configuration.telemetryOrigin || origins.otel !== receipt.configuration.otelOrigin
|| (expected.collectorOrigins && (origins.telemetry !== expected.collectorOrigins.telemetry || origins.otel !== expected.collectorOrigins.otel))) {
throw new Error('sleep_qualification_collector_origins_invalid');
}
const output = Object.fromEntries(layers.map(layer => [layer, result('INCONCLUSIVE', 'No evidence for this layer')]));
const inRun = value => stamp(value) && Date.parse(value) >= Date.parse(receipt.startedAt) && Date.parse(value) <= Date.parse(receipt.finishedAt);
const quiet = receipt.quiet;
if (quiet) {
const observedFeatures = configuredSleepFeatures(receipt);
const valid = inRun(quiet.startedAt) && inRun(quiet.finishedAt) && finite(quiet.elapsedMs)
&& Date.parse(quiet.finishedAt) - Date.parse(quiet.startedAt) >= 90_000 && quiet.elapsedMs >= 90_000
&& quiet.clock === 'real' && finite(quiet.sqlQueries) && finite(quiet.connectionsOpened)
&& finite(quiet.connectionsRemaining) && finite(quiet.nonDatabaseRequests) && finite(quiet.nonDatabaseBytes)
&& observedNoIncomingHttp(quiet.observerRequests, quiet)
&& inRun(quiet.observerRequests.before.observedAt) && inRun(quiet.observerRequests.after.observedAt)
&& observedFeatures.telemetry && observedFeatures.announcements && observedFeatures.otel
&& Object.entries(observedFeatures).every(([key, value]) => receipt.features?.[key] === value)
&& receipt.features?.telemetry === true && receipt.features?.announcements === true
&& receipt.features?.automaticBackups === true && validBackupCheckpoint(receipt.backupCheckpoint, inRun, quiet.finishedAt)
&& receipt.features?.otel === true && receipt.features?.exportedSpans > 0;
const idle = valid && quiet.sqlQueries === 0 && quiet.connectionsOpened === 0 && quiet.connectionsRemaining === 0
&& quiet.nonDatabaseRequests === 0 && quiet.nonDatabaseBytes === 0;
output.workerQuiet = result(idle ? 'VERIFIED' : 'NOT VERIFIED', idle ? 'Real configured quiet window, including closed DB connections' : 'Configured real quiet window missing, contaminated, or active');
}
const eligibility = receipt.eligibility;
if (eligibility) {
const valid = inRun(eligibility.observedAt) && eligibility.backgroundWork === 'none'
&& eligibility.ownedHold === true && eligibility.unchangedGeneration === true
&& finite(eligibility.apiCooldownElapsedMs) && eligibility.apiCooldownElapsedMs >= 600_000;
output.eligibility = result(valid ? 'VERIFIED' : 'NOT VERIFIED', valid ? 'Owned stable safety proof after real API activity cooldown' : 'Safety proof or real API activity cooldown not established');
}
const provider = receipt.provider;
if (provider) {
if (!provider.live || provider.target !== expected.target || provider.configDigest !== expected.configDigest
|| provider.appSha !== expected.appSha || provider.cloudSha !== expected.cloudSha
|| !inRun(provider.observedAt)) {
throw new Error('sleep_qualification_provider_identity_invalid');
}
const untouched = Array.isArray(provider.observerRequests)
&& provider.observerRequests.every(entry => entry.boundary === 'provider-control-plane' && (entry.method === 'GET' || (entry.provider === 'railway' && entry.method === 'POST' && entry.operation === 'query-only')));
const endpoint = provider.neon;
if (untouched && endpoint?.identityVerified === true && endpoint.currentState === 'idle' && endpoint.pendingState === null) {
output.databaseSuspension = result('VERIFIED', 'Exact endpoint idle from live Neon control-plane metadata');
}
if (untouched && endpoint?.identityVerified === true && (endpoint.currentState === 'active'
|| (endpoint.currentState === 'idle' && endpoint.pendingState === 'active'))) {
output.databaseSuspension = result('NOT VERIFIED', 'Exact Neon endpoint is active or becoming active');
}
const services = provider.railway?.services;
if (untouched && provider.railway?.identityVerified === true && provider.railway.state === 'OBSERVED' && Array.isArray(services) && services.length > 0
&& services.every(service => service.identityVerified === true && service.state === 'OBSERVED' && service.allDeploymentsStopped === true
&& typeof service.backupsEnabled === 'boolean' && (service.backupsEnabled === false || service.backupStorageVerified === true))) {
output.hostSuspension = result('VERIFIED', 'Every exact owned service has stopped deployments');
}
if (untouched && provider.railway?.identityVerified === true && Array.isArray(services)
&& services.some(service => service.identityVerified === true && service.state === 'OBSERVED' && service.allDeploymentsStopped === false)) {
output.hostSuspension = result('NOT VERIFIED', 'An exact observed owned service still has running deployments');
}
const storage = provider.storage;
if (output.hostSuspension.verdict === 'VERIFIED' && storage?.identityVerified === true
&& storage.volumeId && storage.beforeVolumeId === storage.volumeId && storage.afterVolumeId === storage.volumeId
&& storage.mountPath === '/paperclip' && storage.backupsEnabledBefore === true && storage.backupsEnabledAfter === true
&& /^[a-f0-9]{64}$/.test(storage.sentinelBefore ?? '') && storage.sentinelBefore === storage.sentinelAfter
&& inRun(storage.beforeAt) && inRun(storage.afterAt) && Date.parse(storage.beforeAt) < Date.parse(provider.observedAt)
&& Date.parse(storage.afterAt) > Date.parse(provider.observedAt)) {
output.backupPersistence = result('VERIFIED', 'Attached volume and fsynced sentinel survive owned stop/wake with backups enabled');
}
}
for (const [layer, kind] of [['inboundWake', 'inbound'], ['timedWake', 'deadline']]) {
const wake = receipt[layer];
if (!wake) continue;
const sleeping = output.databaseSuspension.verdict === 'VERIFIED' && output.hostSuspension.verdict === 'VERIFIED';
const chronology = provider && inRun(wake.stimulusAt) && inRun(wake.finishedAt)
&& Date.parse(wake.stimulusAt) > Date.parse(provider.observedAt) && Date.parse(wake.finishedAt) >= Date.parse(wake.stimulusAt);
const alarm = kind !== 'deadline' || (inRun(wake.alarmPersistedAt)
&& Date.parse(wake.alarmPersistedAt) < Date.parse(provider.observedAt) && wake.sameFinalGeneration === true);
if (!sleeping) { output[layer] = result('INCONCLUSIVE', 'Exact provider sleep and known backup policy have not been established'); continue; }
const verified = sleeping && chronology && alarm && wake.ordinaryProducer === true
&& wake.authorizationVerified === true && wake.persistedOutcomeVerified === true && wake.duplicateEffects === 0;
output[layer] = result(verified ? 'VERIFIED' : 'NOT VERIFIED', verified ? `Useful authorized ${kind} wake after provider sleep` : `Missing provider sleep, chronology, authorization or useful ${kind} outcome`);
}
if (receipt.returnToIdle) {
const presentWakes = ['inboundWake', 'timedWake'].filter(layer => Object.hasOwn(receipt, layer));
const everyWakeInRun = presentWakes.every(layer => inRun(receipt[layer]?.stimulusAt) && inRun(receipt[layer]?.finishedAt)
&& Date.parse(receipt[layer].finishedAt) >= Date.parse(receipt[layer].stimulusAt));
const lastWake = Math.max(...presentWakes.map(layer => Date.parse(receipt[layer]?.finishedAt)));
const idle = receipt.returnToIdle;
const usefulWake = ['inboundWake', 'timedWake'].some(layer => output[layer].verdict === 'VERIFIED');
const valid = usefulWake && everyWakeInRun && Number.isFinite(lastWake) && inRun(idle.observedAt) && Date.parse(idle.observedAt) > lastWake
&& inRun(idle.quietStartedAt) && inRun(idle.quietFinishedAt)
&& Date.parse(idle.quietStartedAt) >= lastWake && Date.parse(idle.quietFinishedAt) <= Date.parse(idle.observedAt)
&& Date.parse(idle.quietFinishedAt) - Date.parse(idle.quietStartedAt) >= 90_000
&& finite(idle.freshQuietWindowMs) && idle.freshQuietWindowMs >= 90_000
&& idle.freshQuietWindowMs <= Date.parse(idle.quietFinishedAt) - Date.parse(idle.quietStartedAt) && idle.backgroundWork === 'none' && idle.ownedHold === true;
output.returnToIdle = result(valid ? 'VERIFIED' : 'NOT VERIFIED', valid ? 'Fresh quiet owned proof after useful wake' : 'No fresh quiet proof after useful wake');
}
return { version: 1, target: expected.target, appSha: expected.appSha, cloudSha: expected.cloudSha, configDigest: expected.configDigest, layers: output };
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const [receiptPath, expectedPath, outputPath] = process.argv.slice(2);
if (!receiptPath || !expectedPath || !outputPath) throw new Error('Usage: node scripts/sleep-qualification/evidence.mjs receipt.json expected.json output.json');
const report = evaluateSleepQualification(JSON.parse(await readFile(receiptPath, 'utf8')), JSON.parse(await readFile(expectedPath, 'utf8')));
await writeFile(outputPath, JSON.stringify(report, null, 2) + '\n', { mode: 0o600 });
process.stdout.write(JSON.stringify(report) + '\n');
if (Object.values(report.layers).some(layer => layer.verdict === 'NOT VERIFIED')) process.exitCode = 1;
}
+125
View File
@@ -0,0 +1,125 @@
import { lstat, mkdtemp, mkdir, readFile, readlink, rename, rm, symlink, unlink, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { randomUUID } from 'node:crypto';
import path from 'node:path';
import { spawn } from 'node:child_process';
import { fileURLToPath, pathToFileURL } from 'node:url';
// Explicit opt-in test setup. No manifests, lockfiles or other worktrees change.
const server = fileURLToPath(new URL('../../server/', import.meta.url));
export const otelPeerNames = ['@opentelemetry/sdk-node', '@opentelemetry/auto-instrumentations-node', '@opentelemetry/exporter-trace-otlp-http', '@opentelemetry/resources', '@opentelemetry/semantic-conventions'];
const ledgerPath = root => path.join(root, 'node_modules', '.paperclip-sleep-otel-links.json');
async function entryAt(destination) {
try { return await lstat(destination); } catch (error) { if (error.code !== 'ENOENT') throw error; }
}
async function versionAt(root, name) {
try { return JSON.parse(await readFile(path.join(root, 'node_modules', name, 'package.json'), 'utf8')).version; }
catch (error) { if (error.code !== 'ENOENT') throw error; return undefined; }
}
async function readOwnership(root) {
const file = ledgerPath(root), entry = await entryAt(file);
if (!entry) return [];
if (!entry.isFile() || entry.isSymbolicLink()) throw new Error('OTel ownership ledger must be a regular file');
const record = JSON.parse(await readFile(file, 'utf8'));
if (record.version !== 1 || record.serverDirectory !== root || !Array.isArray(record.links)
|| record.links.length > otelPeerNames.length || new Set(record.links.map(link => link.name)).size !== record.links.length
|| record.links.some(link => !otelPeerNames.includes(link.name) || typeof link.target !== 'string'
|| !path.isAbsolute(link.target) || path.resolve(link.target) !== link.target
|| !link.target.endsWith(path.join('node_modules', link.name)))) throw new Error('Invalid OTel ownership ledger');
return record.links;
}
async function writeOwnership(root, links) {
const file = ledgerPath(root);
if (!links.length) { await rm(file, { force: true }); return; }
await mkdir(path.dirname(file), { recursive: true });
const temporary = `${file}.${randomUUID()}.tmp`;
try {
await writeFile(temporary, JSON.stringify({ version: 1, serverDirectory: root, links }, null, 2), { flag: 'wx' });
await rename(temporary, file);
} finally { await rm(temporary, { force: true }); }
}
async function ownsLink(destination, record) {
const entry = await entryAt(destination);
return Boolean(record && entry?.isSymbolicLink()
&& path.resolve(path.dirname(destination), await readlink(destination)) === record.target);
}
async function installWithNpm(directory) {
await new Promise((resolve, reject) => {
const child = spawn('npm', ['install', '--prefix', directory, '--ignore-scripts', '--no-audit', '--no-fund', '--prefer-offline'], { stdio: 'inherit', shell: false });
child.once('error', reject); child.once('exit', code => code === 0 ? resolve() : reject(new Error(`OTel fixture installation failed (${code})`)));
});
}
/** Only the exact recorded symlink may be removed; install prefixes retain evidence. */
export async function cleanupSleepOtelLinks({ serverDirectory = server } = {}) {
const root = path.resolve(serverDirectory), records = await readOwnership(root), remaining = [], removed = [], errors = [];
for (const record of records) {
const destination = path.join(root, 'node_modules', record.name);
if (!await entryAt(destination)) continue;
if (!await ownsLink(destination, record)) {
remaining.push(record); errors.push(new Error(`Refusing to clean unrelated ${record.name}`)); continue;
}
await unlink(destination); removed.push(record.name);
}
await writeOwnership(root, remaining);
if (errors.length) throw new AggregateError(errors, 'Some OTel destinations no longer match owned links');
return { removed, installPrefixes: [...new Set(records.map(record => path.dirname(path.dirname(path.dirname(record.target)))))], boundary: 'Only recorded matching symlinks removed; temporary install prefixes are not deleted' };
}
export async function setupSleepOtelPeers({ serverDirectory = server, temporaryRoot = tmpdir(), install = installWithNpm } = {}) {
const root = path.resolve(serverDirectory), manifest = JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8'));
const owned = await readOwnership(root), missing = [];
for (const name of otelPeerNames) {
const exact = manifest.peerDependencies[name];
if (!/^\d+\.\d+\.\d+$/.test(exact ?? '')) throw new Error(`Exact existing peer version required for ${name}`);
const installed = await versionAt(root, name);
if (installed && installed !== exact) throw new Error(`Installed ${name} does not match ${exact}; refusing to replace it`);
if (installed) continue;
const destination = path.join(root, 'node_modules', name);
if (await entryAt(destination) && !await ownsLink(destination, owned.find(link => link.name === name))) {
throw new Error(`Refusing to replace unrelated ${name}; restore its target or verify ownership before removing the unrecorded entry`);
}
missing.push(name);
}
if (!missing.length) return { linked: [], alreadyInstalled: true };
const directory = await mkdtemp(path.join(temporaryRoot, 'paperclip-sleep-otel-'));
let linked = 0;
try {
await writeFile(path.join(directory, 'package.json'), JSON.stringify({ private: true, dependencies: Object.fromEntries(otelPeerNames.map(name => [name, manifest.peerDependencies[name]])) }, null, 2));
await install(directory);
const resolved = Object.fromEntries(await Promise.all(otelPeerNames.map(async name => [name, await versionAt(directory, name)])));
for (const name of otelPeerNames) if (resolved[name] !== manifest.peerDependencies[name]) throw new Error(`Resolved ${name} differs from pinned peer`);
const records = [...owned];
for (const name of missing) {
const destination = path.join(root, 'node_modules', name), previous = owned.find(record => record.name === name);
await mkdir(path.dirname(destination), { recursive: true });
if (await entryAt(destination)) {
if (!await ownsLink(destination, previous)) throw new Error(`OTel destination changed; refusing to replace ${name}`);
await unlink(destination);
}
const target = path.join(directory, 'node_modules', name);
await symlink(target, destination, 'dir'); linked++;
const updated = [...records.filter(record => record.name !== name), { name, target }];
try { await writeOwnership(root, updated); }
catch (error) {
if (await ownsLink(destination, { name, target })) { await unlink(destination); linked--; }
throw error;
}
records.splice(0, records.length, ...updated);
}
const receipt = { version: 1, directory, packages: resolved, linked: missing, ownershipLedger: ledgerPath(root), cleanup: 'node scripts/sleep-qualification/setup-otel.mjs --cleanup' };
await writeFile(path.join(directory, 'qualification-setup.json'), JSON.stringify(receipt, null, 2));
return receipt;
} catch (error) {
// A prefix with published links stays available for explicit owned cleanup.
if (!linked) await rm(directory, { recursive: true, force: true });
throw error;
}
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const args = process.argv.slice(2);
if (args.length > 1 || (args.length && args[0] !== '--cleanup')) throw new Error('Usage: node scripts/sleep-qualification/setup-otel.mjs [--cleanup]');
const receipt = args[0] === '--cleanup' ? await cleanupSleepOtelLinks() : await setupSleepOtelPeers();
process.stdout.write(JSON.stringify(receipt) + '\n');
}
+108
View File
@@ -0,0 +1,108 @@
import { appendFile } from "node:fs/promises";
import { pathToFileURL } from "node:url";
import { createActionsReader, readSourceVerification, SourceVerificationFailed, SourceVerificationDeferred } from "./cloud-source-verification.mjs";
import { requestDeferredProducer, sourceAdmissionJob, migratorAdmissionJob, createReleaseRerunner } from "./cloud-release-admission.mjs";
const repository = "paperclipai/paperclip";
export function runnerWaits(jobs, now = Date.now()) {
return jobs.filter(job => ["queued", "in_progress", "completed"].includes(job.status) &&
job.conclusion !== "skipped" && job.labels?.length > 0).map(job => ({
job: job.name, id: job.id,
seconds: Math.max(0, Math.floor(((job.runner_id > 0 && job.started_at ? Date.parse(job.started_at) : now) - Date.parse(job.created_at)) / 1000)),
})).filter(job => Number.isFinite(job.seconds));
}
// The write is limited to this canonical master Docker run. Never cancel
// another workflow, a tag release, or a run selected from untrusted input.
export async function cancelImageRun({ api, cancel, runId, attempt, sha }) {
const run = await api(`/repos/${repository}/actions/runs/${runId}`);
if (run.id !== runId || run.run_attempt !== attempt || run.head_sha !== sha || run.head_branch !== "master" ||
run.repository?.id !== 1170821064 || run.head_repository?.id !== 1170821064 ||
run.path !== ".github/workflows/docker.yml" || !["push", "workflow_dispatch"].includes(run.event)) {
throw new Error("Refusing to cancel a different image workflow identity.");
}
if (run.status !== "completed") await cancel(`/repos/${repository}/actions/runs/${runId}/cancel`);
}
async function cancelFailedVerification({ api, cancel, sha }, failure) {
const run = await api(`/repos/${repository}/actions/runs/${failure.runId}`);
// A concurrent retry owns the new attempt; a late observer leaves it alone.
if (run.run_attempt !== failure.attempt) return;
if (run.id !== failure.runId || run.head_sha !== sha || run.head_branch !== "master" || run.event !== "push" ||
run.repository?.full_name !== repository || run.head_repository?.full_name !== repository ||
run.path !== ".github/workflows/cloud-readiness.yml") throw new Error("Failed verification identity changed.");
if (run.status !== "completed") await cancel(`/repos/${repository}/actions/runs/${failure.runId}/cancel`);
}
export async function watchReleaseSource({ sha, runId, attempt, api, cancel, rerun, now = Date.now,
sleep = ms => new Promise(resolve => setTimeout(resolve, ms)), log = console.log,
timeoutMs = 40 * 60_000, intervalMs = 30_000, requireProof = false, summary = async () => {} }) {
if (!/^[a-f0-9]{40}$/.test(sha ?? "") || !Number.isSafeInteger(runId) || runId < 1 ||
!Number.isSafeInteger(attempt) || attempt < 1) throw new Error("Invalid release identity.");
const deadline = now() + timeoutMs;
const warned = new Set();
const requested = new Set();
let migratorAdmitted = !rerun;
const reportWaits = async jobs => {
for (const wait of runnerWaits(jobs, now())) {
if (wait.seconds > 60 && !warned.has(wait.id)) {
warned.add(wait.id);
log(`::warning title=Release runner waited over 60 seconds::Job ${wait.id} waited ${wait.seconds}s for a runner.`);
await summary(`Release runner wait: ${wait.job} — ${wait.seconds}s (job ${wait.id}).\n`);
}
}
};
while (now() < deadline) {
if (!migratorAdmitted) {
migratorAdmitted = await requestDeferredProducer({ file: "cloud-migrator-artifacts.yml", sha, admissionJob: migratorAdmissionJob, api, rerun, requested }) !== "pending";
}
const batch = await api(`/repos/${repository}/actions/runs/${runId}/attempts/${attempt}/jobs?per_page=100`);
if (!Array.isArray(batch.jobs) || batch.total_count > 100) throw new Error("Incomplete image job listing.");
await reportWaits(batch.jobs);
try {
const proof = await readSourceVerification(sha, api, { failFast: true, onJobs: reportWaits });
if (proof && migratorAdmitted) {
await summary(`Exact source verified: ${sha}; readiness run ${proof.runId}, attempt ${proof.attempt}.\n`);
return proof;
}
} catch (error) {
if (error instanceof SourceVerificationDeferred && rerun) {
await requestDeferredProducer({ file: "cloud-readiness.yml", sha, admissionJob: sourceAdmissionJob, api, rerun, requested });
} else {
if (error instanceof SourceVerificationFailed) {
await cancelFailedVerification({ api, cancel, sha }, error);
await cancelImageRun({ api, cancel, runId, attempt, sha });
}
throw error;
}
}
await sleep(Math.min(intervalMs, Math.max(0, deadline - now())));
}
// This early failure observer is not the publication deadline. Healthy
// checks can outlive a Spot worker; a separate exact-source gate still
// waits for proof before publishing any image.
if (requireProof) throw new Error("Exact-source verification did not become ready before the publication deadline.");
await summary("Source monitoring window ended; final publication still requires exact-source verification.\n");
return undefined;
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const token = process.env.GITHUB_TOKEN;
if (!token || process.env.GITHUB_REPOSITORY !== repository || process.env.GITHUB_REF !== "refs/heads/master") throw new Error("Canonical master token required.");
const requireProof = process.argv.includes("--require-proof");
const checkProof = process.argv.includes("--check-proof");
const api = createActionsReader({ token });
const proof = checkProof ? await readSourceVerification(process.env.GITHUB_SHA, api, { failFast: true }) : await watchReleaseSource({ requireProof, sha: process.env.GITHUB_SHA, runId: Number(process.env.GITHUB_RUN_ID), attempt: Number(process.env.GITHUB_RUN_ATTEMPT),
api,
rerun: createReleaseRerunner(token),
cancel: async path => {
const response = await fetch(`https://api.github.com${path}`, { method: "POST", redirect: "error",
headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json" }, signal: AbortSignal.timeout(30_000) });
if (!response.ok) throw new Error(`Could not cancel failed-source image work: HTTP ${response.status}.`);
},
summary: text => process.env.GITHUB_STEP_SUMMARY ? appendFile(process.env.GITHUB_STEP_SUMMARY, text) : Promise.resolve(),
});
if (checkProof && !proof) throw new Error("Exact-source proof changed before publication.");
if (process.env.GITHUB_OUTPUT) await appendFile(process.env.GITHUB_OUTPUT, `verified=${Boolean(proof)}\n`);
}
+67
View File
@@ -0,0 +1,67 @@
import { it } from 'vitest';
import assert from 'node:assert/strict';
import { mkdtemp, mkdir, readFile, readlink, readdir, lstat, rm, symlink, unlink, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { cleanupSleepOtelLinks, otelPeerNames, setupSleepOtelPeers } from '../../scripts/sleep-qualification/setup-otel.mjs';
async function fixture(run) {
const directory = await mkdtemp(path.join(tmpdir(), 'sleep-otel-setup-test-'));
const serverDirectory = path.join(directory, 'server'), temporaryRoot = path.join(directory, 'installs');
await mkdir(serverDirectory); await mkdir(temporaryRoot);
await writeFile(path.join(serverDirectory, 'package.json'), JSON.stringify({ peerDependencies: Object.fromEntries(otelPeerNames.map(name => [name, '1.2.3'])) }));
let installs = 0;
const install = async prefix => {
installs++;
const manifest = JSON.parse(await readFile(path.join(prefix, 'package.json'), 'utf8'));
for (const [name, version] of Object.entries(manifest.dependencies)) {
const packageDirectory = path.join(prefix, 'node_modules', name);
await mkdir(packageDirectory, { recursive: true });
await writeFile(path.join(packageDirectory, 'package.json'), JSON.stringify({ version }));
}
};
const options = { serverDirectory, temporaryRoot, install };
try { await run({ directory, options, install, installs: () => installs, destination: name => path.join(serverDirectory, 'node_modules', name) }); }
finally { await rm(directory, { recursive: true, force: true }); }
}
it('reruns after losing its temporary peers and cleans only its recorded matching links', async () => fixture(async f => {
const first = await setupSleepOtelPeers(f.options);
assert.equal(first.linked.length, 5); assert.equal(f.installs(), 1);
assert.equal((await setupSleepOtelPeers(f.options)).alreadyInstalled, true); assert.equal(f.installs(), 1);
await rm(first.directory, { recursive: true });
assert.equal((await lstat(f.destination(otelPeerNames[0]))).isSymbolicLink(), true);
const repaired = await setupSleepOtelPeers(f.options);
assert.equal(f.installs(), 2); assert.notEqual(repaired.directory, first.directory);
for (const name of otelPeerNames) assert.equal(await readlink(f.destination(name)), path.join(repaired.directory, 'node_modules', name));
const cleaned = await cleanupSleepOtelLinks(f.options); assert.equal(cleaned.removed.length, 5);
for (const name of otelPeerNames) await assert.rejects(lstat(f.destination(name)), { code: 'ENOENT' });
assert.ok(await readFile(path.join(repaired.directory, 'qualification-setup.json'), 'utf8')); // Evidence retained.
}));
it('preserves unowned dangling links, incomplete directories and mismatched installed packages', async () => {
for (const kind of ['dangling', 'directory', 'mismatch']) await fixture(async f => {
const destination = f.destination(otelPeerNames[0]); await mkdir(path.dirname(destination), { recursive: true });
if (kind === 'dangling') await symlink(path.join(f.directory, 'foreign-missing'), destination);
else { await mkdir(destination); await writeFile(path.join(destination, kind === 'mismatch' ? 'package.json' : 'keep.txt'), kind === 'mismatch' ? JSON.stringify({ version: '9.9.9' }) : 'foreign'); }
await assert.rejects(setupSleepOtelPeers(f.options), /refusing|Refusing/); assert.equal(f.installs(), 0);
if (kind === 'dangling') assert.equal(await readlink(destination), path.join(f.directory, 'foreign-missing'));
else assert.ok(await readFile(path.join(destination, kind === 'mismatch' ? 'package.json' : 'keep.txt'), 'utf8'));
});
});
it('keeps healthy unrelated packages and refuses cleanup after an owned link is replaced', async () => fixture(async f => {
const destination = f.destination(otelPeerNames[0]); await mkdir(destination, { recursive: true });
await writeFile(path.join(destination, 'package.json'), JSON.stringify({ version: '1.2.3' }));
const receipt = await setupSleepOtelPeers(f.options); assert.equal(receipt.linked.length, 4);
const replaced = f.destination(otelPeerNames[1]); await unlink(replaced);
const foreign = path.join(f.directory, 'foreign'); await mkdir(foreign); await writeFile(path.join(foreign, 'keep.txt'), 'foreign');
await symlink(foreign, replaced);
await assert.rejects(cleanupSleepOtelLinks(f.options), /no longer match/);
assert.equal(await readlink(replaced), foreign); assert.equal(await readFile(path.join(foreign, 'keep.txt'), 'utf8'), 'foreign');
assert.equal(JSON.parse(await readFile(path.join(destination, 'package.json'), 'utf8')).version, '1.2.3');
}));
it('preserves an entry introduced during installation and removes an unpublished failed install prefix', async () => fixture(async f => {
await assert.rejects(setupSleepOtelPeers({ ...f.options, install: async prefix => {
await f.install(prefix); const destination = f.destination(otelPeerNames[0]); await mkdir(destination, { recursive: true }); await writeFile(path.join(destination, 'keep.txt'), 'new foreign entry');
} }), /destination changed/);
assert.equal(await readFile(path.join(f.destination(otelPeerNames[0]), 'keep.txt'), 'utf8'), 'new foreign entry');
assert.equal((await readdir(f.options.temporaryRoot)).length, 0);
}));
+233
View File
@@ -0,0 +1,233 @@
import { it as test } from 'vitest';
import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import { mkdtemp, writeFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { spawnSync } from 'node:child_process';
import { configuredSleepFeatures, evaluateSleepQualification, observeConfiguredSignals, observeIncomingHttpWindow, sleepConfigurationDigest, finalizeLocalSleepReceipt } from '../../scripts/sleep-qualification/evidence.mjs';
const configuration = { automaticBackups: { intervalMinutes: 60, idleCheckpoint: true }, announcements: true, telemetryOrigin: 'http://127.0.0.1:4318/telemetry', otelOrigin: 'http://127.0.0.1:4318', serveUi: true };
// Independent preflight contract, not copied from the receipt/digest implementation.
const configDigest = createHash('sha256').update(JSON.stringify({ automaticBackups: { intervalMinutes: 60, idleCheckpoint: true }, announcements: true, telemetry: true, otel: { enabled: true, protocol: 'http/json' }, serveUi: true, managedInstance: false, deploymentMode: 'local_trusted', storageProvider: 'local_disk' })).digest('hex');
const expected = { appSha: 'a'.repeat(40), cloudSha: 'b'.repeat(40), target: 'owned-fixture', configDigest };
function fixture() {
return { version: 1, ...expected, configuration: structuredClone(configuration), collectorOrigins: { telemetry: configuration.telemetryOrigin, otel: configuration.otelOrigin },
announcement: { intercepted: true, requests: ['https://pages.paperclip.ing/announcements/v1/current.json'], unmatched: [] },
signalObservation: { telemetryRequests: 1, traceRequests: 1, exportedSpans: 1, qualificationSpans: 1, metricRequests: 0, logRequests: 0, malformedTraceRequests: 0, unexpectedRequests: 0 },
backupCheckpoint: { owner: 'normal-idle-checkpoint', startedAt: '2099-01-01T00:12:31Z', observedAt: '2099-01-01T00:13:00Z', wakeMarkerObserved: true, failureObserved: false, archives: [{ name: 'owned.sql.gz', bytes: 128, sqlBytes: 256, sha256: 'd'.repeat(64), gzipVerified: true, fixtureIdentifierFound: true, createdAt: '2099-01-01T00:12:45Z', createdAtSource: 'birthtime' }] }, startedAt: '2099-01-01T00:00:00Z', finishedAt: '2099-01-01T01:00:00Z',
features: { telemetry: true, announcements: true, automaticBackups: true, otel: true, exportedSpans: 1 },
quiet: { startedAt: '2099-01-01T00:11:00Z', finishedAt: '2099-01-01T00:12:30Z', elapsedMs: 90_000, clock: 'real', sqlQueries: 0, connectionsOpened: 0, connectionsRemaining: 0, nonDatabaseRequests: 0, nonDatabaseBytes: 0,
observerRequests: observeIncomingHttpWindow(
{ source: 'node-http-server-events', captureId: '11111111-1111-4111-8111-111111111111', installed: true, valid: true, requests: 12, observedAt: '2099-01-01T00:11:00Z' },
{ source: 'node-http-server-events', captureId: '11111111-1111-4111-8111-111111111111', installed: true, valid: true, requests: 12, observedAt: '2099-01-01T00:12:30Z' }) },
eligibility: { observedAt: '2099-01-01T00:13:00Z', backgroundWork: 'none', ownedHold: true, unchangedGeneration: true, apiCooldownElapsedMs: 600_000 } };
}
function deployed() {
const f = fixture();
f.provider = { ...expected, live: true, observedAt: '2099-01-01T00:14:00Z', observerRequests: [{ method: 'GET', boundary: 'provider-control-plane' }],
neon: { identityVerified: true, currentState: 'idle', pendingState: null },
railway: { identityVerified: true, state: 'OBSERVED', services: [{ identityVerified: true, state: 'OBSERVED', backupsEnabled: false, backupStorageVerified: false, allDeploymentsStopped: true }] } };
return f;
}
test('local configured quiet leaves every provider and wake claim inconclusive', () => {
const r = evaluateSleepQualification(fixture(), expected).layers;
assert.equal(r.workerQuiet.verdict, 'VERIFIED'); assert.equal(r.eligibility.verdict, 'VERIFIED');
for (const key of ['hostSuspension', 'databaseSuspension', 'backupPersistence', 'inboundWake', 'timedWake']) assert.equal(r[key].verdict, 'INCONCLUSIVE');
});
test('wrong source, target and configuration evidence cannot be combined', () => {
for (const key of Object.keys(expected)) { const f = fixture(); f[key] = 'wrong'; assert.throws(() => evaluateSleepQualification(f, expected), /identity/); }
const f = deployed(); f.provider.target = 'foreign'; assert.throws(() => evaluateSleepQualification(f, expected), /provider_identity/);
});
test('fake time, a no-op exporter, open connections, observer health requests and a shortened cooldown fail', () => {
for (const change of [f => f.quiet.clock = 'qualified', f => f.features.exportedSpans = 0, f => f.quiet.connectionsRemaining = 1,
f => { f.quiet.observerRequests.after.requests++; f.quiet.observerRequests.requests++; }, f => f.quiet.nonDatabaseBytes = 1, f => f.quiet.elapsedMs = 89_999]) {
const f = fixture(); change(f); assert.equal(evaluateSleepQualification(f, expected).layers.workerQuiet.verdict, 'NOT VERIFIED');
}
const f = fixture(); f.eligibility.apiCooldownElapsedMs = 599_999; assert.equal(evaluateSleepQualification(f, expected).layers.eligibility.verdict, 'NOT VERIFIED');
});
test('incoming HTTP observation must be installed, intact and cover the whole window; legacy empty lists are not evidence', () => {
const changes = [f => f.quiet.observerRequests = [], f => delete f.quiet.observerRequests,
f => f.quiet.observerRequests.before.installed = false, f => f.quiet.observerRequests.after.installed = false,
f => f.quiet.observerRequests.before.valid = false, f => f.quiet.observerRequests.after.valid = false,
f => delete f.quiet.observerRequests.before, f => delete f.quiet.observerRequests.after,
f => f.quiet.observerRequests.before.source = 'harness-api-only', f => f.quiet.observerRequests.after.source = 'unknown',
f => f.quiet.observerRequests.after.captureId = '22222222-2222-4222-8222-222222222222',
f => f.quiet.observerRequests.before.captureId = '',
f => f.quiet.observerRequests.before.observedAt = '2099-01-01T00:11:00.001Z',
f => f.quiet.observerRequests.after.observedAt = '2099-01-01T00:12:29.999Z',
f => f.quiet.observerRequests.before.observedAt = '2098-12-31T23:59:59Z',
f => f.quiet.observerRequests.after.observedAt = '2099-01-01T01:00:01Z',
f => f.quiet.observerRequests.after.observedAt = 'invalid',
f => f.quiet.observerRequests.after.requests--,
// A forged zero cannot hide an actual request while SQL and outbound counters remain zero.
f => f.quiet.observerRequests.after.requests++,
f => f.quiet.observerRequests.requests = 1];
for (const point of ['before', 'after']) {
for (const value of [-1, 0.5, NaN, Infinity, Number.MAX_SAFE_INTEGER + 1, '12']) {
changes.push(f => f.quiet.observerRequests[point].requests = value);
}
}
for (const value of [-1, 0.5, NaN, Infinity, Number.MAX_SAFE_INTEGER + 1, '0']) changes.push(f => f.quiet.observerRequests.requests = value);
for (const change of changes) {
const f = fixture(); change(f);
assert.equal(f.quiet.sqlQueries, 0); assert.equal(f.quiet.nonDatabaseRequests, 0);
assert.equal(evaluateSleepQualification(f, expected).layers.workerQuiet.verdict, 'NOT VERIFIED');
}
});
test('configured feature claims require complete announcement and integer collector observations', () => {
const changes = [f => delete f.announcement, f => f.announcement.intercepted = false,
f => f.announcement.requests = [], f => f.announcement.requests = ['https://foreign.example/feed'],
f => f.announcement.unmatched.push('https://foreign.example/feed'), f => delete f.announcement.unmatched,
f => delete f.signalObservation, f => f.signalObservation.telemetryRequests = 0,
f => f.signalObservation.traceRequests = 0, f => f.signalObservation.exportedSpans = 0,
f => f.signalObservation.qualificationSpans = 0, f => f.signalObservation.qualificationSpans = 2,
f => f.signalObservation.metricRequests = 1, f => f.signalObservation.logRequests = 1,
f => f.signalObservation.malformedTraceRequests = 1, f => f.signalObservation.unexpectedRequests = 1,
f => f.features.telemetry = false, f => f.features.announcements = false, f => f.features.otel = false];
for (const key of Object.keys(fixture().signalObservation)) {
changes.push(f => delete f.signalObservation[key]);
for (const value of [-1, 0.5, NaN, Infinity, '1']) changes.push(f => f.signalObservation[key] = value);
}
for (const change of changes) { const f = fixture(); change(f); assert.equal(evaluateSleepQualification(f, expected).layers.workerQuiet.verdict, 'NOT VERIFIED'); }
});
test('assembled receipts derive coverage from actual named spans and requests, including unwanted exporters', () => {
const requests = [{ path: '/telemetry', body: {} }, { path: '/v1/traces', body: { resourceSpans: [{ scopeSpans: [{ spans: [
{ name: 'ordinary-http' }, { name: 'sleep-qualification-stimulus' },
] }] }] } }];
const f = fixture(); f.signalObservation = observeConfiguredSignals(requests); f.features = configuredSleepFeatures(f);
assert.deepEqual(f.signalObservation, { telemetryRequests: 1, traceRequests: 1, exportedSpans: 2, qualificationSpans: 1,
metricRequests: 0, logRequests: 0, malformedTraceRequests: 0, unexpectedRequests: 0 });
assert.equal(f.features.exportedSpans, 2); // One trace request contained two real span records.
assert.equal(finalizeLocalSleepReceipt(f, expected, () => f.finishedAt).report.layers.workerQuiet.verdict, 'VERIFIED');
for (const extra of [{ path: '/v1/metrics?batch=1', body: {} }, { path: '/v1/logs', body: {} },
{ path: '/unmatched', body: {} }, { path: '/v1/traces', body: { resourceSpans: [{}] } }]) {
const changed = fixture(); changed.signalObservation = observeConfiguredSignals([...requests, extra]); changed.features = configuredSleepFeatures(changed);
assert.equal(evaluateSleepQualification(changed, expected).layers.workerQuiet.verdict, 'NOT VERIFIED');
}
const noop = fixture(); noop.signalObservation = observeConfiguredSignals([{ path: '/telemetry', body: {} },
{ path: '/v1/traces', body: { resourceSpans: [{ scopeSpans: [{ spans: [{ name: 'other', attributes: [{ value: 'sleep-qualification-stimulus' }] }] }] }] } }]);
noop.features = configuredSleepFeatures(noop); assert.equal(noop.signalObservation.qualificationSpans, 0);
assert.equal(evaluateSleepQualification(noop, expected).layers.workerQuiet.verdict, 'NOT VERIFIED');
});
test('registry sleep, incomplete deployment inventory and a failed metadata read cannot prove providers stopped', () => {
const f = deployed(); f.provider.neon = { currentState: 'sleeping', identityVerified: true }; f.provider.railway.services = [];
let r = evaluateSleepQualification(f, expected).layers; assert.equal(r.databaseSuspension.verdict, 'INCONCLUSIVE'); assert.equal(r.hostSuspension.verdict, 'INCONCLUSIVE');
f.provider.neon = { currentState: 'idle', pendingState: 'active', identityVerified: true }; r = evaluateSleepQualification(f, expected).layers; assert.equal(r.databaseSuspension.verdict, 'NOT VERIFIED');
});
test('useful authorized deadline outcome requires a durable alarm before the exact stop proof', () => {
const f = deployed(); f.timedWake = { stimulusAt: '2099-01-01T00:20:00Z', finishedAt: '2099-01-01T00:21:00Z', ordinaryProducer: true, authorizationVerified: true, persistedOutcomeVerified: true, duplicateEffects: 0, sameFinalGeneration: true, alarmPersistedAt: '2099-01-01T00:13:30Z' };
assert.equal(evaluateSleepQualification(f, expected).layers.timedWake.verdict, 'VERIFIED');
for (const change of [x => x.timedWake.alarmPersistedAt = '2099-01-01T00:15:00Z', x => x.timedWake.sameFinalGeneration = false, x => x.timedWake.persistedOutcomeVerified = false, x => x.timedWake.duplicateEffects = 1]) {
const x = structuredClone(f); change(x); assert.equal(evaluateSleepQualification(x, expected).layers.timedWake.verdict, 'NOT VERIFIED');
}
});
test('writable path alone cannot establish backup persistence across stop and wake', () => {
const f = deployed(); f.provider.storage = { identityVerified: true, volumeId: 'volume', beforeVolumeId: 'volume', afterVolumeId: 'volume', mountPath: '/paperclip', backupsEnabledBefore: true, backupsEnabledAfter: true, sentinelBefore: 'd'.repeat(64), sentinelAfter: 'd'.repeat(64), beforeAt: '2099-01-01T00:13:00Z', afterAt: '2099-01-01T00:25:00Z' };
assert.equal(evaluateSleepQualification(f, expected).layers.backupPersistence.verdict, 'VERIFIED');
for (const change of [x => delete x.provider.storage.volumeId, x => x.provider.storage.afterVolumeId = 'replacement', x => x.provider.storage.backupsEnabledAfter = false, x => x.provider.storage.sentinelAfter = 'e'.repeat(64)]) {
const x = structuredClone(f); change(x); assert.equal(evaluateSleepQualification(x, expected).layers.backupPersistence.verdict, 'INCONCLUSIVE');
}
});
test('unknown backup policy and unverified enabled storage cannot certify a stopped host or wake', () => {
for (const change of [x => delete x.provider.railway.state, x => x.provider.railway.state = 'INCONCLUSIVE',
x => delete x.provider.railway.services[0].backupsEnabled, x => x.provider.railway.services[0].backupsEnabled = null,
x => x.provider.railway.services[0].state = 'INCONCLUSIVE', x => x.provider.railway.services[0].backupsEnabled = true]) {
const f = deployed(); change(f);
f.inboundWake = { stimulusAt: '2099-01-01T00:20:00Z', finishedAt: '2099-01-01T00:21:00Z', ordinaryProducer: true, authorizationVerified: true, persistedOutcomeVerified: true, duplicateEffects: 0 };
f.timedWake = { ...f.inboundWake, alarmPersistedAt: '2099-01-01T00:13:30Z', sameFinalGeneration: true };
const r = evaluateSleepQualification(f, expected).layers;
assert.equal(r.hostSuspension.verdict, 'INCONCLUSIVE'); assert.equal(r.inboundWake.verdict, 'INCONCLUSIVE'); assert.equal(r.timedWake.verdict, 'INCONCLUSIVE');
}
const f = deployed(); assert.equal(evaluateSleepQualification(f, expected).layers.hostSuspension.verdict, 'VERIFIED');
f.provider.railway.services[0].backupsEnabled = true; f.provider.railway.services[0].backupStorageVerified = true;
assert.equal(evaluateSleepQualification(f, expected).layers.hostSuspension.verdict, 'VERIFIED');
});
function woken() {
const f = deployed();
f.inboundWake = { stimulusAt: '2099-01-01T00:20:00Z', finishedAt: '2099-01-01T00:21:00Z', ordinaryProducer: true, authorizationVerified: true, persistedOutcomeVerified: true, duplicateEffects: 0 };
f.returnToIdle = { quietStartedAt: '2099-01-01T00:31:00Z', quietFinishedAt: '2099-01-01T00:32:30Z', observedAt: '2099-01-01T00:33:00Z', freshQuietWindowMs: 90_000, backgroundWork: 'none', ownedHold: true };
return f;
}
test('return to idle requires a real 90-second window after the latest wake', () => {
assert.equal(evaluateSleepQualification(woken(), expected).layers.returnToIdle.verdict, 'VERIFIED');
for (const finishedAt of ['invalid', '2098-12-31T23:59:59Z', '2099-01-01T01:00:01Z']) {
const f = woken(); f.timedWake = { ...f.inboundWake, finishedAt, alarmPersistedAt: '2099-01-01T00:13:30Z', sameFinalGeneration: true };
assert.equal(evaluateSleepQualification(f, expected).layers.inboundWake.verdict, 'VERIFIED');
assert.equal(evaluateSleepQualification(f, expected).layers.returnToIdle.verdict, 'NOT VERIFIED');
}
for (const change of [
f => f.returnToIdle.quietFinishedAt = '2099-01-01T00:31:00.001Z',
f => { f.returnToIdle.quietStartedAt = '2099-01-01T00:19:00Z'; f.returnToIdle.quietFinishedAt = '2099-01-01T00:20:30Z'; },
f => f.returnToIdle.quietStartedAt = '2098-12-31T23:00:00Z',
f => f.returnToIdle.quietFinishedAt = '2099-01-01T01:00:01Z',
f => f.returnToIdle.quietFinishedAt = '2099-01-01T00:30:00Z',
f => f.returnToIdle.observedAt = '2099-01-01T00:32:00Z',
f => f.returnToIdle.freshQuietWindowMs = Infinity,
f => f.returnToIdle.freshQuietWindowMs = 90_001,
f => delete f.returnToIdle.quietStartedAt,
]) {
const f = woken(); change(f); assert.equal(evaluateSleepQualification(f, expected).layers.returnToIdle.verdict, 'NOT VERIFIED');
}
});
test('exact observed active providers are negative evidence while unknown identity stays inconclusive', () => {
for (const state of [{ currentState: 'active', pendingState: null }, { currentState: 'idle', pendingState: 'active' }]) {
const f = deployed(); Object.assign(f.provider.neon, state);
assert.equal(evaluateSleepQualification(f, expected).layers.databaseSuspension.verdict, 'NOT VERIFIED');
f.provider.neon.identityVerified = false;
assert.equal(evaluateSleepQualification(f, expected).layers.databaseSuspension.verdict, 'INCONCLUSIVE');
}
const f = deployed(); f.provider.railway.services[0].allDeploymentsStopped = false;
assert.equal(evaluateSleepQualification(f, expected).layers.hostSuspension.verdict, 'NOT VERIFIED');
f.provider.railway.services[0].identityVerified = false;
assert.equal(evaluateSleepQualification(f, expected).layers.hostSuspension.verdict, 'INCONCLUSIVE');
});
test('logical digest is stable across origins and detects changed enabled configuration', () => {
assert.equal(sleepConfigurationDigest(configuration), configDigest);
const f = fixture(); f.configuration.telemetryOrigin = 'http://127.0.0.1:5555/telemetry'; f.configuration.otelOrigin = 'http://127.0.0.1:5555';
f.collectorOrigins = { telemetry: f.configuration.telemetryOrigin, otel: f.configuration.otelOrigin };
assert.equal(evaluateSleepQualification(f, expected).layers.workerQuiet.verdict, 'VERIFIED');
assert.throws(() => evaluateSleepQualification(f, { ...expected, collectorOrigins: fixture().collectorOrigins }), /collector_origins/);
for (const change of [x => x.configuration.announcements = false, x => x.configuration.serveUi = false,
x => x.configuration.automaticBackups.intervalMinutes = 5, x => x.configuration.automaticBackups.idleCheckpoint = false,
x => x.configuration.managedInstance = true, x => x.configuration.deploymentMode = 'authenticated', x => x.configuration.storageProvider = 's3',
x => x.configuration.otelOrigin = '', x => x.configuration.telemetryOrigin = '']) {
const changed = fixture(); change(changed); assert.throws(() => evaluateSleepQualification(changed, expected), /configuration_digest/);
}
});
test('configured backup feature requires observed verified owned archives and a wake marker', () => {
for (const change of [f => delete f.backupCheckpoint, f => f.backupCheckpoint.archives = [],
f => f.backupCheckpoint.wakeMarkerObserved = false, f => f.backupCheckpoint.failureObserved = true,
f => f.backupCheckpoint.archives[0].gzipVerified = false, f => f.backupCheckpoint.archives[0].fixtureIdentifierFound = false,
f => f.backupCheckpoint.archives[0].bytes = 0, f => f.backupCheckpoint.archives[0].sha256 = 'invalid',
f => f.backupCheckpoint.archives[0].createdAt = '2098-01-01T00:00:00Z',
f => f.backupCheckpoint.archives[0].createdAtSource = 'unknown',
f => f.backupCheckpoint.startedAt = '2099-01-01T00:11:00Z']) {
const f = fixture(); change(f); assert.equal(evaluateSleepQualification(f, expected).layers.workerQuiet.verdict, 'NOT VERIFIED');
}
});
test('receipt finalization follows observations and validates the actual resulting receipt', () => {
const f = fixture(); delete f.finishedAt;
let tick = Date.parse(f.eligibility.observedAt);
const { receipt, report } = finalizeLocalSleepReceipt(f, expected, () => new Date(++tick).toISOString());
assert.ok(Date.parse(receipt.finishedAt) > Date.parse(receipt.eligibility.observedAt));
assert.equal(report.layers.eligibility.verdict, 'VERIFIED');
assert.throws(() => finalizeLocalSleepReceipt(f, expected, () => f.startedAt), /local_evidence/);
});
test('CLI exits one for known active providers and zero for inconclusive identity', async () => {
const directory = await mkdtemp(path.join(tmpdir(), 'sleep-evidence-cli-'));
try {
const input = path.join(directory, 'receipt.json'), expectedPath = path.join(directory, 'expected.json'), output = path.join(directory, 'report.json');
await writeFile(expectedPath, JSON.stringify(expected));
const f = deployed(); f.provider.neon.currentState = 'active';
for (const [identityVerified, status] of [[true, 1], [false, 0]]) {
f.provider.neon.identityVerified = identityVerified; await writeFile(input, JSON.stringify(f));
const child = spawnSync(process.execPath, [fileURLToPath(new URL('../../scripts/sleep-qualification/evidence.mjs', import.meta.url)), input, expectedPath, output], { encoding: 'utf8' });
assert.equal(child.status, status, child.stderr);
}
} finally { await rm(directory, { recursive: true, force: true }); }
});
@@ -290,6 +290,9 @@ import { createHeartbeatQueueTestDependencies } from "./helpers/heartbeat-queue.
async function interruptedBootstrap() {
const f = await seed(); await f.finish(); const run = await f.run();
const interrupted = await terminalizeLegacyExecution({ db, run, status: "interrupted", patch: {
// The real controller claim records preparation before terminal status
// records completion. The delivery fixture otherwise leaves both unset.
executionStage: "preparing", finishedAt: new Date(),
errorCode: "server_shutdown_interrupted", runnerProfileJson: { adapterDispatch: { adapterType: "paperclip_runner" } },
} });
expect(await getExecutionBlocker(db, f.companyId, f.sourceId)).toMatchObject({ runId: run.id });
@@ -338,8 +341,10 @@ import { createHeartbeatQueueTestDependencies } from "./helpers/heartbeat-queue.
expect(await settleInterruptedNativeBootstrap(db, { run, providerDispatchStarted: false })).not.toBeNull();
expect(await issueService(db).getById(f.sourceId)).toMatchObject({ status: "blocked" });
});
it.each(["provider-entered", "runtime-selected", "other-adapter", "restore-unsafe", "active-lease", "cleanup-failed", "retry-exhausted"])("retains an interrupted bootstrap hold for %s", async kind => {
it.each(["provider-entered", "runtime-selected", "other-adapter", "restore-unsafe", "active-lease", "cleanup-failed", "retry-exhausted", "missing-stage", "unfinished"])("retains an interrupted bootstrap hold for %s", async kind => {
const { f, run } = await interruptedBootstrap();
if (kind === "missing-stage") await db.update(heartbeatRuns).set({ executionStage: null }).where(eq(heartbeatRuns.id, run.id));
if (kind === "unfinished") await db.update(heartbeatRuns).set({ finishedAt: null }).where(eq(heartbeatRuns.id, run.id));
if (kind === "runtime-selected") await db.update(heartbeatRuns).set({ runtimeModeResolvedAt: new Date() }).where(eq(heartbeatRuns.id, run.id));
if (kind === "other-adapter") await db.update(heartbeatRuns).set({ runnerProfileJson: { adapterDispatch: { adapterType: "process" } } }).where(eq(heartbeatRuns.id, run.id));
if (kind === "restore-unsafe") await db.update(heartbeatRuns).set({ resultJson: { workspaceRestoreFailure: "restore_unsafe_archive" } }).where(eq(heartbeatRuns.id, run.id));
@@ -330,7 +330,7 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu
await db.transaction(async tx => {
await createWakeAdmissionWriter().coalesceIntoActiveExecutionRun(
createAdmissionTransactionScope(f.companyId, tx as unknown as typeof db), {
companyId: f.companyId, activeExecutionRunId: seeded.id, mergedContextSnapshot: chatContextSnapshot(),
companyId: f.companyId, activeExecutionRunId: seeded.id, activeExecutionRunStatus: seeded.status, mergedContextSnapshot: chatContextSnapshot(),
agentId: f.agentId, source: "on_demand", triggerDetail: null, payload: { issueId: f.issueId },
requestedByActorType: "user", requestedByActorId: f.userId, idempotencyKey: null,
});
@@ -3808,17 +3808,23 @@ describeEmbeddedPostgres("environmentRuntimeService", () => {
} as unknown as PluginWorkerManager;
const runtimeWithPlugin = environmentRuntimeService(db, { pluginWorkerManager: workerManager });
await expect(runtimeWithPlugin.acquireRunLease({
companyId,
environment,
issueId: null,
agentId,
heartbeatRunId: runId,
persistedExecutionWorkspace: {
id: executionWorkspaceId,
mode: "shared_workspace",
},
})).rejects.toThrow("the lease was preserved and no replacement was created");
// Keep the exact configured budget stable across deadline creation and RPC dispatch.
const now = vi.spyOn(Date, "now").mockReturnValue(Date.now());
try {
await expect(runtimeWithPlugin.acquireRunLease({
companyId,
environment,
issueId: null,
agentId,
heartbeatRunId: runId,
persistedExecutionWorkspace: {
id: executionWorkspaceId,
mode: "shared_workspace",
},
})).rejects.toThrow("the lease was preserved and no replacement was created");
} finally {
now.mockRestore();
}
expect(workerManager.call).toHaveBeenNthCalledWith(1, pluginId, "environmentResumeLease", expect.objectContaining({
driverKey: "fake-plugin",
@@ -10,7 +10,7 @@ import { randomBytes, randomUUID } from "node:crypto";
import { S3Client, CreateBucketCommand, DeleteBucketCommand, DeleteObjectsCommand, ListObjectsV2Command, PutObjectCommand } from "@aws-sdk/client-s3";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { eq } from "drizzle-orm";
import { activityLog, agents, agentFileRoots, agentInstructionWorkingCopies, companies, executionWorkspaces, executionWorkspaceSnapshots, issues, workspaceOperations, createDb, closeFileStorageDb, heartbeatRuns, providerTraceRecords } from "@paperclipai/db";
import { activityLog, agentWakeupRequests, agents, agentFileRoots, agentInstructionWorkingCopies, companies, executionWorkspaces, executionWorkspaceSnapshots, issues, workspaceOperations, createDb, closeFileStorageDb, heartbeatRuns, providerTraceRecords } from "@paperclipai/db";
import { startEmbeddedPostgresTestDatabase } from "./helpers/embedded-postgres.js";
import { readRemoteWorkspaceAuthority } from "@paperclipai/shared";
import { getWorkspaceSnapshot, loadWorkspaceSnapshotArchive } from "../services/remote-workspace-snapshot-storage.js";
@@ -53,6 +53,7 @@ describe.skipIf(!process.env.PAPERCLIP_TEST_S3_ENDPOINT)("existing-volume enroll
await db.delete(issues).where(eq(issues.companyId, companyId));
await db.delete(executionWorkspaces).where(eq(executionWorkspaces.companyId, companyId));
await db.delete(heartbeatRuns).where(eq(heartbeatRuns.companyId, companyId));
await db.delete(agentWakeupRequests).where(eq(agentWakeupRequests.companyId, companyId));
await db.delete(agents).where(eq(agents.companyId, companyId));
await db.delete(companies).where(eq(companies.id, companyId));
let token: string | undefined;
@@ -104,7 +105,8 @@ describe.skipIf(!process.env.PAPERCLIP_TEST_S3_ENDPOINT)("existing-volume enroll
const parent = path.join(home, "project"), child = path.join(parent, ".paperclip/worktrees/task");
await fs.mkdir(child, { recursive: true });
const fixtures = [{ cwd: parent, file: "project.txt", bytes: "project work" }, { cwd: child, file: "task.txt", bytes: "nested task work" }];
const ids: string[] = [];
const ids: string[] = [], queued = [];
const queuedAgent = await agent();
for (const fixture of fixtures) {
await fs.writeFile(path.join(fixture.cwd, fixture.file), fixture.bytes);
const [issue] = await db.insert(issues).values({ companyId, title: fixture.file }).returning();
@@ -112,10 +114,20 @@ describe.skipIf(!process.env.PAPERCLIP_TEST_S3_ENDPOINT)("existing-volume enroll
name: fixture.file, mode: "isolated_workspace", strategyType: "project_primary", providerType: "local_fs", cwd: fixture.cwd }).returning();
ids.push(workspace!.id);
await db.update(issues).set({ executionWorkspaceId: workspace!.id }).where(eq(issues.id, issue!.id));
const [wake] = await db.insert(agentWakeupRequests).values({ companyId, agentId: queuedAgent.id,
source: "on_demand", payload: { issueId: issue!.id, text: "Continue my saved work" } }).returning();
const [run] = await db.insert(heartbeatRuns).values({ companyId, agentId: queuedAgent.id,
issueId: issue!.id, wakeupRequestId: wake!.id, status: "queued", contextSnapshot: { issueId: issue!.id,
executionWorkspaceId: workspace!.id } }).returning();
queued.push({ run: run!, wake: wake! });
}
expect(await migrate()).toMatchObject({ eligibility: { eligible: true }, counts: { workspaces: 2 } });
await fs.rm(home, { recursive: true }); home = await fs.mkdtemp(path.join(os.tmpdir(), "workspace-successor-"));
vi.stubEnv("PAPERCLIP_HOME", home);
for (const { run, wake } of queued) {
expect((await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, run.id)))[0]).toEqual(run);
expect((await db.select().from(agentWakeupRequests).where(eq(agentWakeupRequests.id, wake.id)))[0]).toEqual(wake);
}
for (const [index, workspaceId] of ids.entries()) {
const [workspace] = await db.select().from(executionWorkspaces).where(eq(executionWorkspaces.id, workspaceId));
const authority = readRemoteWorkspaceAuthority(workspace!.metadata)!;
@@ -137,6 +149,8 @@ describe.skipIf(!process.env.PAPERCLIP_TEST_S3_ENDPOINT)("existing-volume enroll
await db.insert(heartbeatRuns).values({ id: runId, companyId, agentId: row.id, status: "interrupted",
runtimeMode: "native", logStore: "local_file", logRef });
vi.stubEnv("PAPERCLIP_DECISION_SIGNING_SECRET", "");
const [queued] = await db.insert(heartbeatRuns).values({ companyId, agentId: row.id, status: "queued",
contextSnapshot: { input: "Resume this turn after maintenance" } }).returning();
const original = resolveDecisionSigningSecret();
const listener = createServer();
await new Promise<void>(resolve => listener.listen(0, "127.0.0.1", resolve));
@@ -167,6 +181,7 @@ describe.skipIf(!process.env.PAPERCLIP_TEST_S3_ENDPOINT)("existing-volume enroll
}
expect(status).toMatchObject({ id, commit: stamp.commit, state: "ready", eligibility: { eligible: true } });
expect(JSON.stringify(status)).not.toContain(original);
expect((await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, queued!.id)))[0]).toEqual(queued);
const exported = await fetch(`${origin}/export`, { method: "POST", headers }).then(r => r.json());
expect(exported).toMatchObject({ id, commit: stamp.commit, decisionSigningSecret: original });
const [run] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, runId));
@@ -1445,7 +1445,7 @@ describeEmbeddedPostgres("generic remote MCP connections", () => {
]);
});
it("does not let another user take over an archived personal URL connection", async () => {
it("creates a separate fresh personal URL connection and keeps explicit reconnect owner-bound", async () => {
const fixture = installMcpOAuthFixture({ auth: "oauth" });
const company = await createCompany(db);
const service = toolAccessService(db);
@@ -1456,18 +1456,36 @@ describeEmbeddedPostgres("generic remote MCP connections", () => {
await db.insert(companyMemberships).values({
companyId: company.id, principalType: "user", principalId: "other-user", status: "active", membershipRole: "admin",
});
const archivedConnection = await db.select().from(toolConnections).where(eq(toolConnections.id, first.connectionId));
const archivedApplication = await db.select().from(toolApplications).where(eq(toolApplications.id, first.application.id));
fixture.fetchMock.mockClear();
await expect(service.connectGalleryApp(company.id, input, {
actorType: "user", actorId: "other-user",
})).rejects.toMatchObject({ status: 403, message: "Only the existing personal identity can reconnect this connection" });
for (const target of [{ reconnectConnectionId: first.connectionId }, { applicationId: first.application.id }]) {
await expect(service.connectGalleryApp(company.id, { ...input, ...target }, {
actorType: "user", actorId: "other-user",
})).rejects.toMatchObject({ status: 403, message: "Only the existing personal identity can reconnect this connection" });
}
expect(fixture.fetchMock).not.toHaveBeenCalled();
const fresh = await service.connectGalleryApp(company.id, input, { actorType: "user", actorId: "other-user" });
expect(fresh.connectionId).not.toBe(first.connectionId);
expect(fresh.application.id).not.toBe(first.application.id);
expect(fresh.auth).toMatchObject({ kind: "oauth" });
await expect(service.getConnection(fresh.connectionId)).resolves.toMatchObject({
createdByUserId: "other-user", credentialPolicy: "per_user", credentialSecretRefs: [],
});
await expect(db.select().from(toolConnections).where(eq(toolConnections.id, first.connectionId)))
.resolves.toEqual(archivedConnection);
await expect(db.select().from(toolApplications).where(eq(toolApplications.id, first.application.id)))
.resolves.toEqual(archivedApplication);
// OAuth grants are created only after authorization; discovery is not a grant.
await expect(db.select().from(connectionGrants)).resolves.toHaveLength(0);
await expect(service.getConnection(first.connectionId)).resolves.toMatchObject({
status: "archived", createdByUserId: "board-user",
});
const resumed = await service.connectGalleryApp(company.id, input, { actorType: "user", actorId: "board-user" });
const resumed = await service.connectGalleryApp(company.id, {
...input, reconnectConnectionId: first.connectionId,
}, { actorType: "user", actorId: "board-user" });
expect(resumed.connectionId).toBe(first.connectionId);
expect(resumed.auth).toMatchObject({ kind: "oauth" });
});
@@ -1544,7 +1562,7 @@ describeEmbeddedPostgres("generic remote MCP connections", () => {
return jsonResponse({ jsonrpc: "2.0", id: "paperclip-catalog-refresh", result: { tools: FIXTURE_TOOLS } });
});
const failure = service.connectGalleryApp(company.id, {
link: MCP_URL, name: "Personal retry rollback", grantKind: "user",
link: MCP_URL, name: "Personal retry rollback", grantKind: "user", reconnectConnectionId: first.connectionId,
}, actor).then(() => null, (error: unknown) => error);
await Promise.race([catalogPending, failure.then((error) => { throw error ?? new Error("Retry finished before the catalog probe"); })]);
try {
@@ -1587,7 +1605,9 @@ describeEmbeddedPostgres("generic remote MCP connections", () => {
FOR EACH ROW EXECUTE FUNCTION test_personal_profile_failure()
`);
try {
await expect(service.connectGalleryApp(company.id, input, actor)).rejects.toThrow();
await expect(service.connectGalleryApp(company.id, {
...input, reconnectConnectionId: first.connectionId,
}, actor)).rejects.toThrow();
await expect(db.select().from(toolCatalogEntries).where(eq(toolCatalogEntries.connectionId, first.connectionId)))
.resolves.toEqual(catalogBefore);
await expect(db.select().from(toolProfiles).where(eq(toolProfiles.companyId, company.id))).resolves.toHaveLength(0);
@@ -0,0 +1,214 @@
import fs from "node:fs/promises";
import path from "node:path";
import { randomUUID } from "node:crypto";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import { eq } from "drizzle-orm";
import { S3Client, CreateBucketCommand, DeleteBucketCommand, DeleteObjectsCommand, ListObjectsV2Command } from "@aws-sdk/client-s3";
import { agents, authUsers, companies, companyMemberships, createDb, closeRegisteredClients, environmentLeases,
environments, executionWorkspaces, heartbeatRuns, agentWakeupRequests, issues, issueWorkProducts, nativeRunResults } from "@paperclipai/db";
import type { AdapterExecutionTarget } from "@paperclipai/adapter-utils/execution-target";
import type { CommandManagedRuntimeRunner } from "@paperclipai/adapter-utils/command-managed-runtime";
import type { NativeExecutionInput, NativeSession, NativeSessionBackend, PrpEvent } from "@paperclipai/paperclip-runner";
import type { EnvironmentLease } from "@paperclipai/shared";
import { startEmbeddedPostgresTestDatabase } from "./helpers/embedded-postgres.js";
import { createIsolatedTestHome } from "./helpers/isolated-test-home.js";
import { heartbeatService, startTaskDrain, stopTaskDrain } from "../services/heartbeat.js";
import { instanceSettingsService } from "../services/instance-settings.js";
import { environmentRuntimeService } from "../services/environment-runtime.js";
import * as environmentTarget from "../services/environment-execution-target.js";
import { migrateEphemeralServerData } from "../maintenance/ephemeral-migration-data.js";
import { resolveManagedInstructionsRoot } from "../services/agent-instructions.js";
import { materializeInstructionBytes } from "../services/agent-instruction-files.js";
import { flushInFlightRunLogMirrors } from "../services/run-log-store.js";
import { CONTROL_PLANE_CONFORMANCE_RESULT, CONTROL_PLANE_CONFORMANCE_TERMINAL } from "../vendor/paperclip-runner/testing.js";
vi.mock("@paperclipai/adapter-utils/execution-target", async importOriginal => ({
...await importOriginal<typeof import("@paperclipai/adapter-utils/execution-target")>(),
// The scripted session makes no sandbox-to-host API calls.
startAdapterExecutionTargetPaperclipBridge: async () => null,
}));
// Real database, migration, object storage, queue claim, workspace restoration,
// instruction materialization and result settlement. Only the sandbox transport
// and provider session are scripted; neither can read the deleted predecessor.
describe.skipIf(!process.env.PAPERCLIP_TEST_S3_ENDPOINT)("queued turn after preserving offline enrollment", () => {
let database: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>>;
let db: ReturnType<typeof createDb>, root: string, bucket: string, s3: S3Client;
let home: ReturnType<typeof createIsolatedTestHome>;
let replacement: ReturnType<typeof heartbeatService> | undefined;
beforeAll(async () => {
database = await startEmbeddedPostgresTestDatabase("queued-enrollment-");
db = createDb(database.connectionString);
home = createIsolatedTestHome(); root = await fs.realpath(home.home);
bucket = `queued-enrollment-${randomUUID()}`;
s3 = new S3Client({ endpoint: process.env.PAPERCLIP_TEST_S3_ENDPOINT, region: "us-east-1", forcePathStyle: true });
await s3.send(new CreateBucketCommand({ Bucket: bucket }));
vi.stubEnv("DATABASE_URL", database.connectionString);
vi.stubEnv("PAPERCLIP_HOME", path.join(root, "predecessor"));
vi.stubEnv("PAPERCLIP_SECRETS_MASTER_KEY", "74".repeat(32));
vi.stubEnv("PAPERCLIP_DURABLE_AGENT_FILES", "true");
vi.stubEnv("PAPERCLIP_EPHEMERAL_MIGRATION", "true");
vi.stubEnv("PAPERCLIP_EPHEMERAL_SERVER", "false");
vi.stubEnv("PAPERCLIP_STORAGE_PROVIDER", "s3");
vi.stubEnv("PAPERCLIP_STORAGE_S3_BUCKET", bucket);
vi.stubEnv("PAPERCLIP_STORAGE_S3_REGION", "us-east-1");
vi.stubEnv("PAPERCLIP_STORAGE_S3_ENDPOINT", process.env.PAPERCLIP_TEST_S3_ENDPOINT!);
vi.stubEnv("PAPERCLIP_STORAGE_S3_FORCE_PATH_STYLE", "true");
}, 90_000);
afterAll(async () => {
stopTaskDrain();
await replacement?.drainActiveRunExecutions();
await flushInFlightRunLogMirrors();
vi.restoreAllMocks();
if (database) await closeRegisteredClients(database.connectionString, { scope: "database" });
await database?.cleanup();
if (s3) {
for (;;) {
const page = await s3.send(new ListObjectsV2Command({ Bucket: bucket }));
if (!page.Contents?.length) break;
await s3.send(new DeleteObjectsCommand({ Bucket: bucket, Delete: { Objects: page.Contents.map(row => ({ Key: row.Key! })) } }));
}
await s3.send(new DeleteBucketCommand({ Bucket: bucket })); s3.destroy();
}
home?.cleanup();
vi.unstubAllEnvs();
});
it("admits the original queued run once and reads its saved workspace and instructions without the old disk", async () => {
const companyId = randomUUID(), agentId = randomUUID(), issueId = randomUUID(), userId = randomUUID();
const workProductId = randomUUID(), evidenceRef = `work_product:${workProductId}`;
await instanceSettingsService(db).updateExperimental({ enableNativeRunner: true, enableIsolatedWorkspaces: true });
await db.insert(authUsers).values({ id: userId, name: "Owner", email: `${userId}@example.test`, createdAt: new Date(), updatedAt: new Date() });
await db.insert(companies).values({ id: companyId, name: "Queued enrollment", issuePrefix: "QEM", defaultResponsibleUserId: userId });
await db.insert(companyMemberships).values({ companyId, principalType: "user", principalId: userId, status: "active", membershipRole: "owner" });
const [environment] = await db.insert(environments).values({ name: `Scripted sandbox ${companyId}`, driver: "sandbox",
status: "active", config: { provider: "daytona" } }).returning();
const instructions = "Preserved instructions: read saved.txt before completing this turn.";
const instructionsRoot = resolveManagedInstructionsRoot({ id: agentId, companyId, name: "Queued worker", adapterConfig: {} });
await materializeInstructionBytes(instructionsRoot, "AGENTS.md", Buffer.from(instructions));
await db.insert(agents).values({ id: agentId, companyId, name: "Queued worker", adapterType: "paperclip_runner", status: "idle",
defaultEnvironmentId: environment.id, adapterConfig: { provider: "codex", instructionsBundleMode: "managed", instructionsRootPath: instructionsRoot },
runtimeConfig: { heartbeat: { wakeOnDemand: true, maxConcurrentRuns: 1 } } });
await db.insert(issues).values({ id: issueId, companyId, title: "Continue saved work", issueNumber: 1, identifier: "QEM-1",
status: "todo", assigneeAgentId: agentId, responsibleUserId: userId, executionWorkspacePreference: "reuse_existing",
executionWorkspaceSettings: { mode: "isolated_workspace" } });
const oldHome = process.env.PAPERCLIP_HOME!, cwd = path.join(oldHome, "task");
await fs.mkdir(cwd, { recursive: true }); await fs.writeFile(path.join(cwd, "saved.txt"), "unfinished work before migration");
const [workspace] = await db.insert(executionWorkspaces).values({ companyId, sourceIssueId: issueId, name: "Saved task",
mode: "isolated_workspace", strategyType: "project_primary", providerType: "local_fs", cwd }).returning();
await db.update(issues).set({ executionWorkspaceId: workspace.id }).where(eq(issues.id, issueId));
await db.insert(issueWorkProducts).values({ id: workProductId, companyId, issueId, type: "artifact", provider: "paperclip",
title: "Migration fixture evidence", status: "ready_for_review", reviewState: "approved" });
const predecessor = heartbeatService(db);
startTaskDrain({});
await predecessor.wakeup(agentId, { source: "on_demand", triggerDetail: "manual", reason: "manual",
requestedByActorType: "user", requestedByActorId: userId, payload: { issueId }, contextSnapshot: { issueId, skipIssueComment: true } });
await predecessor.drainActiveRunExecutions();
const [queued] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.companyId, companyId));
const [wake] = await db.select().from(agentWakeupRequests).where(eq(agentWakeupRequests.companyId, companyId));
expect(queued).toMatchObject({ status: "queued", startedAt: null, wakeupRequestId: wake.id });
expect(wake.status).toBe("queued");
expect(await migrateEphemeralServerData(db, { signal: AbortSignal.timeout(60_000), phase: () => {} }))
.toMatchObject({ eligibility: { eligible: true }, counts: { workspaces: 1 } });
expect((await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, queued.id)))[0]).toEqual(queued);
expect((await db.select().from(agentWakeupRequests).where(eq(agentWakeupRequests.id, wake.id)))[0]).toEqual(wake);
await fs.rm(oldHome, { recursive: true });
await expect(fs.stat(oldHome)).rejects.toMatchObject({ code: "ENOENT" });
vi.stubEnv("PAPERCLIP_HOME", path.join(root, "replacement"));
vi.stubEnv("PAPERCLIP_EPHEMERAL_MIGRATION", "false");
const remoteCwd = path.join(root, "independent-sandbox"); await fs.mkdir(remoteCwd);
const exec = promisify(execFile);
const transfer: NonNullable<CommandManagedRuntimeRunner["syncIn"]> = async operations => {
const results = [];
for (const operation of operations) {
for (const file of operation.files) {
await fs.mkdir(path.dirname(file.targetPath), { recursive: true });
await fs.cp(file.sourcePath, file.targetPath, { recursive: file.kind === "directory" });
}
results.push({ operationId: operation.operationId, filesTransferred: operation.files.length, bytesTransferred: 0 });
}
return { operations: results };
};
const runner: CommandManagedRuntimeRunner = { syncIn: transfer, syncOut: transfer, execute: async input => {
const command = exec(input.command, input.args ?? [], { cwd: input.cwd ?? remoteCwd, env: { ...process.env, ...input.env },
timeout: input.timeoutMs, maxBuffer: 4 * 1024 * 1024 });
command.child.stdin?.end(input.stdin ?? "");
const result = await command;
return { exitCode: 0, stdout: result.stdout, stderr: result.stderr, signal: null, timedOut: false, pid: process.pid, startedAt: new Date().toISOString() };
} };
vi.spyOn(environmentTarget, "resolveEnvironmentExecutionTarget").mockImplementation(async input => ({
kind: "remote", transport: "sandbox", remoteCwd: typeof input.leaseMetadata?.remoteCwd === "string" ? input.leaseMetadata.remoteCwd : remoteCwd,
runner, provider: "daytona", leaseId: input.leaseId, sandboxId: `fixture-${queued.id}`, timeoutMs: 60_000,
}) as AdapterExecutionTarget);
const runtime = environmentRuntimeService(db);
vi.spyOn(runtime, "acquireRunLease").mockImplementation(async input => {
const [lease] = await db.insert(environmentLeases).values({ companyId, environmentId: environment.id, heartbeatRunId: input.heartbeatRunId,
issueId, executionWorkspaceId: workspace.id, provider: "daytona", providerLeaseId: `fixture-${queued.id}`, status: "active", metadata: { remoteCwd } }).returning();
return { environment: input.environment, lease: lease as EnvironmentLease,
leaseContext: { executionWorkspaceId: workspace.id, executionWorkspaceMode: "isolated_workspace" } };
});
// The scripted provider owns no external allocation. Retain its directory
// until after the normal native workspace finalizer has completed.
vi.spyOn(runtime, "releaseRunLeases").mockResolvedValue([]);
const startTurn = vi.fn();
const backendFactory = (execution: NativeExecutionInput): NativeSessionBackend => {
const turnId = `turn-${queued.id}`;
const identity = { companyId, agentId, issueId, runId: queued.id, sessionId: execution.session.normalizedSessionId! };
const result = structuredClone(CONTROL_PLANE_CONFORMANCE_RESULT);
result.completionClaim.contractRevision = execution.completionContract.contract.revision;
result.completionClaim.criteria = execution.completionContract.contract.criteria.map(criterion => ({
...result.completionClaim.criteria[0]!, criterionId: criterion.id, evidenceRefs: [evidenceRef],
}));
result.evidence = [{ kind: "work_product", ref: evidenceRef }]; result.verification[0]!.artifactRef = evidenceRef;
let started!: () => void;
const turnStarted = new Promise<void>(resolve => { started = resolve; });
const session: NativeSession = {
identity: () => identity,
async capabilities() { return { resume: true, typedEvents: true, steering: false, interruption: true, structuredResult: true }; },
async *events() {
await turnStarted;
const [run] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, queued.id));
yield { schema: "paperclip.prp.event.v1", sourceEventId: `${queued.id}:terminal`, sourceSeq: 1, sourceInstanceId: run.runnerInstanceId!,
sourceKind: "runner", runId: queued.id, normalizedSessionId: identity.sessionId, turnId, eventType: "turn.completed",
schemaVersion: 1, priority: 0, emittedAt: new Date().toISOString(), payload: {} } satisfies PrpEvent;
},
async startTurn(input) {
expect(execution.binding.runId).toBe(queued.id);
expect(execution.workspace.cwd).toBeTruthy();
expect(await fs.readFile(path.join(execution.workspace.cwd!, "saved.txt"), "utf8")).toBe("unfinished work before migration");
if (!("runtimeContext" in execution)) throw new Error("Expected native runtime context");
const bundle = execution.runtimeContext.instructions;
const instructionPath = path.join(bundle.bundle.rootPath, bundle.entryPath);
expect(instructionPath.startsWith(`${oldHome}/`)).toBe(false);
expect(await fs.readFile(instructionPath, "utf8")).toBe(instructions);
await expect(fs.stat(path.join(cwd, "saved.txt"))).rejects.toMatchObject({ code: "ENOENT" });
startTurn(input); started(); return { turnId };
},
async result() { await turnStarted; return { result, terminal: CONTROL_PLANE_CONFORMANCE_TERMINAL, turnId }; },
async snapshot() { return { backendKind: "mock", sessionId: "queued-migration", identity, providerSessionId: "queued-migration",
activeTurnId: null, pendingRuntimeRequests: [], lineage: [], cursor: "1" }; },
async close() {},
};
return { async descriptor() { return { kind: "mock", name: "queued-migration", version: "1",
capabilities: { resume: true, typedEvents: true, steering: false, interruption: true, structuredResult: true },
runtimeContextCapabilities: { instructions: "native", skills: "native", mcp: "native" } }; },
async openSession() { return session; } };
};
replacement = heartbeatService(db, { environmentRuntime: runtime, nativeSessionBackendFactory: backendFactory });
stopTaskDrain();
await replacement.resumeQueuedRunsAfterDrain(); await replacement.drainActiveRunExecutions();
const finished = await replacement.getRun(queued.id);
expect(finished?.status, JSON.stringify({ errorCode: finished?.errorCode, error: finished?.error, result: finished?.resultJson })).toBe("succeeded");
expect(finished?.wakeupRequestId).toBe(wake.id);
await replacement.resumeQueuedRunsAfterDrain(); await replacement.drainActiveRunExecutions();
expect(startTurn).toHaveBeenCalledOnce();
expect(await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.companyId, companyId))).toHaveLength(1);
expect(await db.select().from(nativeRunResults).where(eq(nativeRunResults.runId, queued.id))).toHaveLength(1);
expect(await db.select().from(agentWakeupRequests).where(eq(agentWakeupRequests.companyId, companyId)))
.toEqual([expect.objectContaining({ id: wake.id, status: "completed", runId: queued.id,
claimedAt: expect.any(Date), finishedAt: expect.any(Date), payload: wake.payload })]);
}, 120_000);
});
@@ -0,0 +1,36 @@
import { mock } from 'node:test';
import { DEFAULT_ANNOUNCEMENT_FEED_URL, announcementManifestSchema } from '@paperclipai/shared';
/** Test-child transport seam: actual route/feed/cache, no DNS or HTTPS claim. */
export async function installMaintenanceAnnouncementBoundary() {
const actual = await import('../../../app.js');
const requests: string[] = [], unmatched: string[] = [];
let intercepted = false;
const manifest = announcementManifestSchema.parse({ schemaVersion: 1, announcement: {
id: 'new-projects', eyebrow: 'New', title: 'Projects', description: 'Organize your work.',
primaryAction: { kind: 'route', label: 'Open', path: '/projects' },
} });
const wrapped: typeof actual.createApp = (db, options) => {
const announcements = options.announcements;
if (!announcements?.enabled || announcements.feedUrl !== DEFAULT_ANNOUNCEMENT_FEED_URL) {
throw new Error('Configured announcement boundary requires the exact enabled default feed');
}
intercepted = true;
const fetch = async (url: URL, init: RequestInit) => {
if (url.href !== DEFAULT_ANNOUNCEMENT_FEED_URL || init.method !== 'GET') {
unmatched.push(url.href);
throw new Error('Unmatched announcement transport is forbidden');
}
requests.push(url.href);
return Response.json(manifest, { headers: { ETag: '"qualification-v1"' } });
};
const withTransport = { ...options, announcements: { ...announcements, fetch } };
return actual.createApp(db, withTransport);
};
mock.module(new URL('../../../app.ts', import.meta.url).href, { namedExports: { ...actual, createApp: wrapped } });
// Fail before server startup if the TS loader/module cache bypasses the mock.
if ((await import('../../../app.js')).createApp !== wrapped) throw new Error('Announcement module boundary did not intercept the startup import');
return { assertIntercepted() { if (!intercepted) throw new Error('Actual server bypassed the announcement boundary'); },
snapshot: () => ({ intercepted, requests: [...requests], unmatched: [...unmatched],
boundary: 'Feed transport injected through existing fetch seam; actual route/feed validation/cache/UI. DNS and HTTPS guard are not exercised in this combined case.' }) };
}
@@ -0,0 +1,54 @@
import { createHash } from 'node:crypto';
import { constants } from 'node:fs';
import { lstat, open, readdir } from 'node:fs/promises';
import path from 'node:path';
import { Transform, Writable } from 'node:stream';
import { pipeline } from 'node:stream/promises';
import { createGunzip } from 'node:zlib';
/** Inspect only archives created by the normal idle owner after the quiet counters freeze.
* Stream through EOF to validate the gzip trailer; retain no SQL/credential contents. */
export async function observeIdleBackupCheckpoint(directory: string, beforeNames: string[], startedAt: string, fixtureId: string) {
if (!/^[a-f0-9-]{36}$/.test(fixtureId)) throw new Error('Invalid owned backup fixture identifier');
const checkpointMs = Date.parse(startedAt);
if (!Number.isFinite(checkpointMs) || checkpointMs <= 0) throw new Error('Invalid owned checkpoint timestamp');
const backupDir = path.join(directory, 'backups');
const marker = await lstat(path.join(backupDir, '.idle-backup-wake-required'));
if (!marker.isFile() || marker.isSymbolicLink() || marker.size === 0) throw new Error('Missing normal idle backup wake marker');
for (const failure of [path.join(directory, 'health/db-backup-to-s3.failure'), path.join(directory, 'db-backup-to-s3.failure'), path.join(backupDir, 'db-backup-to-s3.failure')]) {
try { await lstat(failure); throw new Error('Observed database backup failure marker'); }
catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; }
}
const names = (await readdir(backupDir)).filter(name => name.endsWith('.sql.gz') && !beforeNames.includes(name)).sort();
if (!names.length) throw new Error('Normal idle owner produced no new archive');
const archives = [];
for (const name of names) {
const handle = await open(path.join(backupDir, name), constants.O_RDONLY | constants.O_NOFOLLOW);
try {
const stat = await handle.stat();
// Some filesystems report birthtime=0. The new-name filter and opened
// non-symlink file still apply; use its content mtime only in that case.
// An old positive birthtime must not be rescued by a recent modification.
const createdAtSource = stat.birthtimeMs === 0 ? 'mtime' : 'birthtime';
const createdMs = stat.birthtimeMs === 0 ? stat.mtimeMs : stat.birthtimeMs;
if (!stat.isFile() || stat.size <= 0 || !Number.isFinite(createdMs) || createdMs <= 0
|| createdMs < checkpointMs || Math.trunc(createdMs) > Date.now()) throw new Error('Archive predates owned checkpoint or is invalid');
const hash = createHash('sha256'); let sqlBytes = 0, tail = '', fixtureIdentifierFound = false;
await pipeline(handle.createReadStream({ autoClose: false }),
new Transform({ transform(chunk, _encoding, done) { hash.update(chunk); done(null, chunk); } }), createGunzip(),
new Writable({ write(chunk, _encoding, done) {
sqlBytes += chunk.length;
if (sqlBytes > 512 * 1024 * 1024) { done(new Error('Owned fixture archive exceeded bounded observation size')); return; }
const text = tail + chunk.toString('utf8'); fixtureIdentifierFound ||= text.includes(fixtureId);
tail = text.slice(-fixtureId.length); done();
} }));
if (!sqlBytes || !fixtureIdentifierFound) throw new Error('Archive does not contain the owned fixture');
archives.push({ name, bytes: stat.size, sqlBytes, sha256: hash.digest('hex'), createdAt: new Date(createdMs).toISOString(), createdAtSource, gzipVerified: true, fixtureIdentifierFound });
} finally { await handle.close(); }
}
return { owner: 'normal-idle-checkpoint', startedAt, observedAt: new Date().toISOString(), archives, wakeMarkerObserved: true, failureObserved: false };
}
export async function idleBackupArchiveNames(directory: string) {
try { return (await readdir(path.join(directory, 'backups'))).filter(name => name.endsWith('.sql.gz')); }
catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return []; throw error; }
}
@@ -0,0 +1,82 @@
import { setTimeout as delay } from 'node:timers/promises';
import { withNativeMaintenanceBrowser } from './native-browser.js';
/** Actual Chromium visibility; never substitutes a fake document or frozen lifecycle. */
export async function observeMaintenanceBrowser(origin: string, announcementDescription?: string, tenantHeaders?: Record<string, string>) {
return withNativeMaintenanceBrowser(async (browser, context) => {
const unmatchedOrigins: string[] = [];
if (tenantHeaders) {
// Both HTTP and native WS need the simulated Cloud front door identity.
// Gate every possible foreign transport BEFORE networking; no fixture
// header can reach another origin. about:blank does not use networking.
await context.route('**/*', async route => {
if (new URL(route.request().url()).origin !== origin) { unmatchedOrigins.push(route.request().url()); await route.abort('blockedbyclient'); return; }
await route.continue();
});
await context.routeWebSocket(url => url.origin.replace(/^ws/, 'http') !== origin, route => {
unmatchedOrigins.push(route.url()); route.close({ code: 1008, reason: 'Outside owned app origin' });
});
await context.setExtraHTTPHeaders(tenantHeaders);
}
const page = await context.newPage();
const requests: Array<{ at: number; path: string }> = [];
page.on('request', request => { const url = new URL(request.url()); if (url.origin === origin) requests.push({ at: performance.now(), path: url.pathname }); });
await page.goto(origin, { waitUntil: 'networkidle' });
if (announcementDescription) await page.getByText(announcementDescription, { exact: true }).waitFor({ state: 'visible' });
await page.bringToFront();
if (await page.evaluate(() => document.visibilityState) !== 'visible') throw new Error('Browser active-use observation requires a visible page');
const beforeActive = requests.length;
await page.reload({ waitUntil: 'networkidle' });
if (!requests.slice(beforeActive).some(request => request.path.startsWith('/api/'))) throw new Error('Active browser did not make ordinary app requests');
const visibilityEvents: Array<{ state: string; rendererAt: number }> = [];
const prefix = 'paperclip-sleep-visibility:';
page.on('console', message => { if (message.text().startsWith(prefix)) visibilityEvents.push(JSON.parse(message.text().slice(prefix.length))); });
// Passive native events only; never replace document visibility properties.
await page.evaluate(prefix => document.addEventListener('visibilitychange', () => console.log(prefix + JSON.stringify({ state: document.visibilityState, rendererAt: performance.now() }))), prefix);
const visibility = () => page.evaluate(() => ({ state: document.visibilityState, focused: document.hasFocus(), rendererAt: performance.now(),
ownVisibilityOverride: Boolean(Object.getOwnPropertyDescriptor(document, 'visibilityState')), ownHiddenOverride: Boolean(Object.getOwnPropertyDescriptor(document, 'hidden')),
ownFocusOverride: Boolean(Object.getOwnPropertyDescriptor(document, 'hasFocus')) }));
const initialVisibility = await visibility();
if (initialVisibility.state !== 'visible') throw new Error('Native browser is not visible before the actual tab switch');
const foreground = await context.newPage();
await foreground.goto('about:blank'); await foreground.bringToFront();
const native = await browser.newBrowserCDPSession();
const windowIdentity = async (target: typeof page) => {
const session = await context.newCDPSession(target);
try {
const { targetInfo } = await session.send('Target.getTargetInfo');
const { windowId } = await native.send('Browser.getWindowForTarget', { targetId: targetInfo.targetId });
return { targetId: targetInfo.targetId, windowId };
} finally { await session.detach(); }
};
const appWindow = await windowIdentity(page), foregroundWindow = await windowIdentity(foreground);
if (appWindow.windowId !== foregroundWindow.windowId) throw new Error('Background browser qualification requires an actual same-window tab switch');
const hiddenDeadline = performance.now() + 5_000;
while (await page.evaluate(() => document.visibilityState) !== 'hidden') {
if (performance.now() > hiddenDeadline) throw new Error('Browser could not establish a real background tab; visibility simulation is not qualification');
await delay(50);
}
// Let requests already admitted by the transition settle before the untouched window.
await delay(2_000);
const initialHidden = await visibility();
if (initialHidden.state !== 'hidden') throw new Error('Browser lost native hidden state during settling');
if (!visibilityEvents.some(event => event.state === 'hidden' && event.rendererAt <= initialHidden.rendererAt)) throw new Error('Native hidden event was not observed');
const hiddenRequests = requests.length, started = performance.now();
await delay(90_000);
const finalHidden = await visibility();
const hiddenRealElapsedMs = performance.now() - started;
if (hiddenRealElapsedMs < 90_000) throw new Error('Native hidden observation was shorter than 90 real seconds');
const intervalVisibilityEvents = visibilityEvents.filter(event => event.rendererAt >= initialHidden.rendererAt && event.rendererAt <= finalHidden.rendererAt);
if (finalHidden.state !== 'hidden' || intervalVisibilityEvents.some(event => event.state !== 'hidden')) throw new Error(`Browser left native hidden state: ${JSON.stringify({ initialHidden, finalHidden, intervalVisibilityEvents })}`);
if ([initialVisibility, initialHidden, finalHidden].some(state => state.ownVisibilityOverride || state.ownHiddenOverride || state.ownFocusOverride)) throw new Error('Browser visibility or focus properties were overridden');
const hidden = requests.slice(hiddenRequests);
if (hidden.some(request => request.path.startsWith('/api/'))) throw new Error(`Background tab produced tenant traffic: ${JSON.stringify(hidden)}`);
if (unmatchedOrigins.length) throw new Error(`Browser attempted non-owned transport: ${JSON.stringify(unmatchedOrigins)}`);
await page.bringToFront(); const restoredVisibility = await visibility();
if (restoredVisibility.state !== 'visible') throw new Error('Native app tab did not become visible after activation');
if (!visibilityEvents.some(event => event.state === 'visible' && event.rendererAt >= finalHidden.rendererAt)) throw new Error('Native visible restoration event was not observed');
return { activeRequests: requests.slice(beforeActive, hiddenRequests).length, hiddenRealElapsedMs,
hiddenRequests: hidden, unmatchedOrigins, visibility: 'Native Chromium default context with no Playwright focus override', initialVisibility, initialHidden, finalHidden,
intervalVisibilityEvents, visibilityEvents, restoredVisibility, appWindow, foregroundWindow, announcementRendered: Boolean(announcementDescription), authBoundary: tenantHeaders ? 'Owned Cloud front-door headers; actual tenant resolver/membership' : 'Legacy local board', closed: true };
});
}
@@ -0,0 +1,21 @@
import assert from 'node:assert/strict';
import type { Db } from '@paperclipai/db';
import { githubIngressFixture } from '../github-webhook-ingress.js';
import { chatChannelService } from '../../../services/chat-channels.js';
/** The fixture supplies the named external signed-webhook boundary; domain owners write receipts. */
export async function produceChatHistory(db: Db) {
const fixture = await githubIngressFixture(db);
const action = await fixture.insertAction();
await fixture.service.processGitHubWebhookIngress(action.id);
const finished = await fixture.readAction(action.id);
assert.equal(fixture.calls.length, 1);
assert.equal(finished.status, 'processed');
const service = chatChannelService(db, { heartbeat: { wakeup: async () => { throw new Error('Unexpected chat turn'); } },
fetch: async () => { throw new Error('Unexpected provider HTTP outside signed-webhook fixture'); } });
try {
await service.configure(fixture.endpoint.id, { action: 'remove' }, 'local-board');
} finally { await service.shutdown(); }
return { companyId: fixture.companyId, actionId: action.id, endpointId: fixture.endpoint.id,
boundary: 'Existing signed webhook fixture input; actual chat ingress completion and ordinary endpoint removal preserve history' };
}
@@ -0,0 +1,48 @@
import type { createDb } from '@paperclipai/db';
import { expect } from 'vitest';
import { randomUUID } from 'node:crypto';
import { readFile, writeFile } from 'node:fs/promises';
import path from 'node:path';
import type { startMaintenanceServer } from './server-harness.js';
/** Real routes own request creation, human rejection, run cancellation and outbox retirement. */
export async function produceCompletedToolReview(server: Awaited<ReturnType<typeof startMaintenanceServer>>, db: ReturnType<typeof createDb>, companyId: string) {
await server.api('POST', `/companies/${companyId}/tools/policies`, { name: 'Review harmless qualification writes', policyType: 'require_approval', selectors: { toolName: 'mcp-remote-fixture:update_note' } });
const readyPath = path.join(server.directory, `tool-review-ready-${randomUUID()}`);
const readyProgram = `const fs=require('node:fs');const marker=${JSON.stringify(readyPath)};fs.writeFileSync(marker+'.pending',process.env.PAPERCLIP_RUN_ID,{flag:'wx'});fs.renameSync(marker+'.pending',marker);setTimeout(()=>process.exit(0),60000)`;
const agent = await server.api<{ id: string }>('POST', `/companies/${companyId}/agents`, { name: 'Owned tool review process', role: 'general', adapterType: 'process',
adapterConfig: { command: process.execPath, args: ['-e', readyProgram], cwd: server.directory }, runtimeConfig: { heartbeat: { enabled: true, intervalSec: 0, wakeOnDemand: true } } });
const issue = await server.api<{ id: string }>('POST', `/companies/${companyId}/issues`, { title: 'Harmless tool review history', status: 'todo', assigneeAgentId: agent.id });
let runId: string | undefined;
await expect.poll(async () => {
const [run] = await db.$client`select id,status from heartbeat_runs where issue_id=${issue.id} order by created_at desc limit 1`;
runId = run?.id;
if (run?.status !== 'running') return false;
// The process runs only after its awaited adapter.invoke write can commit
// behind the final dispatch gate's run lock. Status alone precedes setup.
try { return await readFile(readyPath, 'utf8') === run.id; }
catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false; throw error; }
}, { timeout: 30_000 }).toBe(true);
const readiness = { runId, marker: path.basename(readyPath), observedAt: new Date().toISOString(), source: 'actual-process-run-id-marker' };
await writeFile(path.join(server.directory, 'sleep-tool-review-readiness.json'), JSON.stringify(readiness, null, 2));
const session = await server.api<{ sessionId: string; token: string }>('POST', '/tool-gateway/sessions', { companyId, agentId: agent.id, runId, issueId: issue.id });
const call = await fetch(`http://127.0.0.1:${server.port}/api/tool-gateway/tools/call`, { method: 'POST', signal: AbortSignal.timeout(15_000),
headers: { 'Content-Type': 'application/json', 'x-paperclip-tool-gateway-token': session.token },
body: JSON.stringify({ tool: 'mcp-remote-fixture:update_note', parameters: { noteId: 'qualification', body: 'Harmless denied operation' } }) });
expect(call.ok).toBe(false);
expect(JSON.stringify(await call.json())).toContain('approval_required');
const [request] = await db.$client`select id,status from tool_action_requests where issue_id=${issue.id}`;
expect(request.status).toBe('pending');
await server.api('POST', `/tool-gateway/action-requests/${request.id}/decline`, { companyId, reason: 'Qualification declines the harmless write' });
await server.api('PATCH', `/issues/${issue.id}`, { status: 'done' });
await expect.poll(async () => {
const [run] = await db.$client`select status from heartbeat_runs where id=${runId!}`;
return ['succeeded', 'failed', 'cancelled', 'timed_out'].includes(run?.status);
}, { timeout: 75_000 }).toBe(true);
await expect.poll(async () => {
const [delivery] = await db.$client`select delivered_at is not null as delivered from tool_action_deliveries where action_request_id=${request.id}`;
return delivery?.delivered;
}, { timeout: 30_000 }).toBe(true);
await server.api('POST', `/tool-gateway/sessions/${session.sessionId}/revoke`, { companyId });
return { requestId: request.id, issueId: issue.id, runId, readiness, boundary: 'Actual request/rejection/session revocation and closed-task retirement; no provider write executed' };
}
@@ -0,0 +1,52 @@
import assert from 'node:assert/strict';
import { assertEarlySleepDiagnostics } from './early-sleep-diagnostics.js';
import type { startMaintenanceServer } from './server-harness.js';
type Report = { ownerId: string; draining: boolean; quiescent: boolean; expiresAt: string;
idleSleepSafety: { diagnostics?: { complete?: boolean; checks?: Array<{ reason: string; state: string }> } } };
const transientStates = new Map([['inspection.generation', 'unknown'], ['local.tracked_work', 'present'], ['plugins.runtime_drain', 'not_checked']]);
export function classifyEarlyDiagnostic(report: Report, elapsedSinceApiMs: number): 'strict' | 'transient' {
const checks = report.idleSleepSafety.diagnostics?.checks;
if (!Array.isArray(checks)) throw new Error('Missing early diagnostic checks');
// A real durable blocker fails now, even if unrelated inspection work is in flight.
const expectedStates = new Map([['local.registered_probes', 'present'], ['local.inspection', 'present'], ['backup.checkpoint', 'not_checked']]);
for (const check of checks) {
if (check.state === (expectedStates.get(check.reason) ?? 'none')) continue;
if (transientStates.get(check.reason) !== check.state) throw new Error(`Unexpected early sleep blocker: ${check.reason}=${check.state}`);
}
if (checks.some(check => transientStates.get(check.reason) === check.state)) return 'transient';
assertEarlySleepDiagnostics(report, elapsedSinceApiMs);
return 'strict';
}
/** Bounded setup-only observation, never a quiet-window poll or an uncertainty exemption. */
export async function stabilizeEarlyDiagnostics(server: Awaited<ReturnType<typeof startMaintenanceServer>>, options: {
ownerId: string; apiSucceededAt: number; read: () => Promise<Report>; persist: (observations: unknown[]) => Promise<void>;
}) {
const observations: unknown[] = [];
for (let round = 1; round <= 3; round++) {
const before = await server.settled();
observations.push({ round, kind: 'settled-before', at: new Date().toISOString(), snapshot: before }); await options.persist(observations);
assert(before.managed?.apiActivityActive, 'Actual successful managed API key activity is not held');
const reports: Report[] = []; let transient = false;
for (const position of ['initial', 'final']) {
const report = await options.read(); reports.push(report);
observations.push({ round, kind: position, at: new Date().toISOString(), report }); await options.persist(observations);
assert(report.ownerId === options.ownerId && report.draining && report.quiescent && Date.parse(report.expiresAt) > Date.now(), 'Early diagnostic lost exact live hold');
const classification = classifyEarlyDiagnostic(report, performance.now() - options.apiSucceededAt);
transient = classification === 'transient' || transient;
}
const after = await server.snapshot();
observations.push({ round, kind: 'snapshot-after', at: new Date().toISOString(), snapshot: after }); await options.persist(observations);
if (transient) continue;
assert(before.idle.active === 0 && after.idle.active === 0, 'Accepted early diagnostic has active owned work');
// Each real HTTP authentication is tracked work. Its generation changes are
// observer stimuli; each report separately proves its own scan stayed stable.
assert.deepEqual(reports[0]!.idleSleepSafety, reports[1]!.idleSleepSafety, 'Accepted early diagnostic pair differs');
assert(after.managed?.apiActivityActive, 'Real managed API activity expired during setup');
return { rounds: round, initial: reports[0]!, final: reports[1]!, observations,
boundary: 'Strict complete stable setup inventory with real API cooldown active; not sleep eligibility' };
}
throw new Error('Early inspection did not stabilize in three bounded owner-settled rounds');
}
@@ -0,0 +1,18 @@
type Check = { reason: string; state: string };
/** Early diagnostics are only a fail-fast blocker inventory, never sleep proof.
* The registered-probe aggregate includes the real 10-minute API activity hold;
* it cannot distinguish other probes, so the later full cooldown/proof is mandatory. */
export function assertEarlySleepDiagnostics(report: { idleSleepSafety?: { diagnostics?: { complete?: boolean; checks?: Check[] } } }, elapsedSinceApiMs: number) {
const diagnostics = report.idleSleepSafety?.diagnostics;
if (!Number.isFinite(elapsedSinceApiMs) || elapsedSinceApiMs < 0 || elapsedSinceApiMs >= 600_000) throw new Error('Early diagnostic requires the actual API cooldown still active');
if (diagnostics?.complete !== true || !Array.isArray(diagnostics.checks)) throw new Error('Early diagnostic coverage incomplete');
const expected = new Map([['local.registered_probes', 'present'], ['local.inspection', 'present'], ['backup.checkpoint', 'not_checked']]);
const seen = new Set<string>();
for (const check of diagnostics.checks) {
if (seen.has(check.reason) || check.state !== (expected.get(check.reason) ?? 'none')) throw new Error(`Unexpected early sleep blocker: ${check.reason}=${check.state}`);
seen.add(check.reason);
}
for (const required of [...expected.keys(), 'inspection.hold', 'inspection.generation', 'durable.snapshot', 'deadlines.catalog']) {
if (!seen.has(required)) throw new Error(`Missing early sleep diagnostic: ${required}`);
}
}
@@ -0,0 +1,47 @@
import { eq } from 'drizzle-orm';
import { randomUUID } from 'node:crypto';
import { toolApplications, toolConnections, connectionGrants, type Db } from '@paperclipai/db';
import { writeConnectionCredential } from '../../../services/connection-credentials.js';
import { secretService } from '../../../services/secrets.js';
import { createOAuthCredentialStore } from '../../../services/tool-access/oauth/credential-store.js';
import { createOAuthRefreshLeases } from '../../../services/tool-access/oauth/refresh-leases.js';
import { createOAuthGrantRefresh } from '../../../services/tool-access/oauth/grant-refresh.js';
import { createGitHubGrantContinuity } from '../../../services/tool-access/github-grant-continuity.js';
import { createPaperclipCloudConnector, paperclipCloudConnectorConfigFromEnv } from '../../../services/paperclip-cloud-connector.js';
/** Bootstrap the named OAuth consent boundary, then use real vault, binding and continuity owners. */
export async function prepareGitHubConfiguration(db: Db, companyId: string) {
const owner = 'local-board', now = Date.now();
const [application] = await db.insert(toolApplications).values({ companyId, applicationKey: randomUUID(), name: 'GitHub qualification', type: 'mcp_http', status: 'active' }).returning();
const [connection] = await db.insert(toolConnections).values({ companyId, applicationId: application.id, uid: randomUUID(), name: 'Retained GitHub', status: 'active', enabled: false,
transport: 'mcp_remote', authKind: 'oauth', credentialPolicy: 'per_user', credentialSource: 'paperclip_vault', transportConfig: {},
config: { url: 'https://api.github.com/mcp', sourceTemplateKey: 'github', oauth: { strategy: 'paperclip_cloud_connector', connectorProfile: 'github.code' } } }).returning();
const refs = [];
for (const [configPath, value] of [['oauth.access_token', 'qualification-access'], ['oauth.refresh_token', 'qualification-refresh']] as const) {
const saved = await writeConnectionCredential(db, { companyId, connectionName: 'GitHub', configPath, label: configPath, value, ownerUserId: owner });
refs.push({ configPath, secretId: saved.secret.id, versionSelector: 'latest' as const, required: true });
}
const [grant] = await db.insert(connectionGrants).values({ companyId, connectionId: connection.id, kind: 'user', subjectUserId: owner, status: 'active', credentialSecretRefs: refs,
providerTenant: { oauth: { strategy: 'paperclip_cloud_connector', accessTokenExpiresAt: new Date(now + 8 * 3_600_000).toISOString(), refreshedAt: new Date(now).toISOString() } } }).returning();
await createOAuthCredentialStore(db, secretService(db)).syncCredentialBindings(connection, refs);
return { connectionId: connection.id, grantId: grant.id };
}
export async function settleGitHubConfiguration(db: Db, input: { connectionId: string; grantId: string }) {
const getConnection = async () => (await db.select().from(toolConnections).where(eq(toolConnections.id, input.connectionId)))[0];
const connection = await getConnection();
const [grant] = await db.select().from(connectionGrants).where(eq(connectionGrants.id, input.grantId));
const config = paperclipCloudConnectorConfigFromEnv();
if (!config) throw new Error('Owned qualification connector is not configured');
const connector = createPaperclipCloudConnector({ config });
const credentials = createOAuthCredentialStore(db, secretService(db));
const leases = createOAuthRefreshLeases(db, () => new Date(), getConnection, credentials.getOAuthGrantRow);
const refresh = createOAuthGrantRefresh(db, { now: () => new Date(), store: credentials, leases, getConnectionRow: getConnection,
currentCloudConnector: () => connector, exchangeOAuthToken: async () => { throw new Error('Unexpected direct OAuth outside managed GitHub fixture'); } });
const continuity = createGitHubGrantContinuity(db, { now: () => new Date(), currentCloudConnector: () => connector,
refreshOAuthGrantCredentials: refresh, resolveOAuthGrantSecret: credentials.resolveOAuthGrantSecret });
const settled = await continuity.refreshManagedGitHubGrantAccess(connection, grant, { actorType: 'system', actorId: 'qualification' });
if (settled.providerTenant?.github?.bindingSync || settled.providerTenant?.github?.continuityRetry) throw new Error('Real GitHub binding debt did not settle');
return { connectionId: connection.id, grantId: grant.id, credentialRefs: settled.credentialSecretRefs.map(ref => ref.configPath),
boundary: 'Actual encrypted user credential/version/binding owners and GitHub authority refresh; real signed broker binding; named external GitHub HTTP only' };
}
@@ -0,0 +1,50 @@
import assert from 'node:assert/strict';
import { installMaintenanceIdleWorkCapture } from './idle-work-capture.js';
// Isolated Node/TS-loader identity probe. Never imports app/index or opens a DB.
const original = await import('../../../services/task-admission.js');
const before = process.argv[2] === 'unhandled' ? undefined : original.beginIdleTrackedWork();
const capture = await installMaintenanceIdleWorkCapture();
try {
const observed = await import('../../../services/task-admission.js');
assert.equal(observed.idleWorkSnapshot, original.idleWorkSnapshot);
if (process.argv[2] === 'unhandled') {
const reason = new Error('original unhandled tracked rejection');
let listener!: (reason: unknown, promise: Promise<unknown>) => void;
let timer: ReturnType<typeof setTimeout> | undefined;
const event = new Promise<{ reason: unknown; promise: Promise<unknown> }>((resolve, reject) => {
listener = (reason, promise) => resolve({ reason, promise });
process.once('unhandledRejection', listener);
timer = setTimeout(() => reject(new Error('Original tracked rejection did not reach unhandledRejection')), 1500);
});
try {
// Deliberately never await or attach a handler to this original promise.
const tracked = observed.trackIdleWork(Promise.reject(reason));
const unhandled = await event;
assert.equal(unhandled.promise, tracked);
assert.equal(unhandled.reason, reason);
assert.equal(original.idleWorkSnapshot().active, 0);
assert.equal(capture.snapshot().observedActive, 0);
console.log(JSON.stringify({ unhandledOriginalPromise: true, unhandledOriginalReason: true, finalActive: 0 }));
} finally {
if (timer) clearTimeout(timer);
process.removeListener('unhandledRejection', listener);
}
} else {
assert.equal(observed.idleWorkSnapshot().active, 1); // Existing state was not reset.
assert.equal(capture.snapshot().observedActive, 0); // The pre-install path is explicitly missing.
before!();
const finish = observed.beginIdleTrackedWork();
assert.equal(original.idleWorkSnapshot().active, 1);
finish(); finish();
assert.equal(original.idleWorkSnapshot().active, 0);
assert.equal(await observed.trackIdleWork(Promise.resolve('original-result')), 'original-result');
assert.equal(original.idleWorkSnapshot().active, 0);
const result = capture.summary();
assert(result.snapshot.missingPaths > 0);
console.log(JSON.stringify({ identity: true, originalStatePreserved: true, finalActive: original.idleWorkSnapshot().active, missingPathsExplicit: result.snapshot.missingPaths }));
}
} finally {
before?.();
capture.dispose();
}
@@ -0,0 +1,137 @@
import { mock } from 'node:test';
import { promiseHooks } from 'node:v8';
type Admission = typeof import('../../../services/task-admission.js');
type Entry = { id: number; kind: 'begin' | 'track'; sourceId: number; startedAt: number; generation: number };
type Completion = Entry & { finishedAt: number; finishedGeneration: number };
export type IdleWorkCaptureSnapshot = {
actual: { active: number; generation: number }; observedActive: number; active: Entry[]; completionTail: Completion[];
newSources: Array<{ id: number; callers: string[] }>; overflow: number; sourceOverflow: number;
missingCallsites: number; missingPaths: number; observerErrors: number; disposed: boolean;
};
/** Test-only metadata. No original work, promise, drain or counter is changed. */
export function createMaintenanceIdleWorkCapture(actual: Pick<Admission, 'beginIdleTrackedWork' | 'trackIdleWork' | 'idleWorkSnapshot'>,
controls: { now?: () => number; stack?: () => string | undefined } = {}) {
const active = new Map<number, Entry>(), sources = new Map<string, { id: number; callers: string[] }>();
const tail: Completion[] = [];
let trackedPromises = new WeakMap<Promise<unknown>, number>();
let disposed = false;
let serial = 0, emittedSourceId = 0, overflow = 0, sourceOverflow = 0, missingCallsites = 0, missingPaths = 0, observerErrors = 0;
const now = controls.now ?? Date.now;
function start(kind: Entry['kind']): number | undefined {
try {
if (disposed) return;
if (active.size >= 512) { overflow++; return; }
const callers: string[] = [];
for (const line of (controls.stack?.() ?? new Error().stack ?? '').split('\n')) {
const match = line.match(/[/\\]server[/\\](?:src|dist)[/\\]((?:(?:services|middleware|routes|modules)[/\\][A-Za-z0-9_./\\-]+|(?:app|index))\.[cm]?[jt]s):(\d+):(\d+)/);
if (!match || match[1]!.split(/[/\\]/).includes('..')) continue;
const caller = `${match[1]!.replaceAll('\\', '/')}:${match[2]}:${match[3]}`;
if (!callers.includes(caller)) callers.push(caller);
if (callers.length === 4) break;
}
if (!callers.length) missingCallsites++;
const key = JSON.stringify(callers); let source = sources.get(key);
if (!source) {
if (sources.size >= 512) { sourceOverflow++; return; }
source = { id: sources.size + 1, callers }; sources.set(key, source);
}
const id = ++serial;
active.set(id, { id, kind, sourceId: source.id, startedAt: now(), generation: actual.idleWorkSnapshot().generation });
return id;
} catch { observerErrors++; return; }
}
// V8 observes settlement without attaching a continuation or marking a rejected
// original promise handled. Unrelated promises only perform a WeakMap lookup.
const stopSettlementHook = promiseHooks.onSettled(promise => {
try {
const id = trackedPromises.get(promise);
if (id === undefined) return;
trackedPromises.delete(promise);
finish(id);
} catch { observerErrors++; }
});
function finish(id: number | undefined) {
try {
if (id === undefined) return;
const entry = active.get(id); if (!entry) return;
active.delete(id);
tail.push({ ...entry, finishedAt: now(), finishedGeneration: actual.idleWorkSnapshot().generation });
if (tail.length > 64) tail.shift();
} catch { observerErrors++; }
}
const beginIdleTrackedWork: Admission['beginIdleTrackedWork'] = () => {
const originalFinish = actual.beginIdleTrackedWork(); const id = start('begin');
return () => { try { return originalFinish(); } finally { finish(id); } };
};
const trackIdleWork: Admission['trackIdleWork'] = <T>(work: Promise<T>) => {
const original = actual.trackIdleWork(work); const id = start('track');
try { if (id !== undefined) trackedPromises.set(original, id); } catch { observerErrors++; }
return original;
};
return { beginIdleTrackedWork, trackIdleWork,
dispose() {
if (disposed) return;
disposed = true;
try { stopSettlementHook(); } finally { trackedPromises = new WeakMap(); }
},
snapshot(): IdleWorkCaptureSnapshot {
const state = actual.idleWorkSnapshot();
if (state.active !== active.size) missingPaths++;
const newSources = [...sources.values()].filter(source => source.id > emittedSourceId).map(source => ({ ...source, callers: [...source.callers] }));
emittedSourceId = sources.size;
return { actual: state, observedActive: active.size, active: [...active.values()].map(entry => ({ ...entry })), completionTail: tail.map(entry => ({ ...entry })),
newSources, overflow, sourceOverflow, missingCallsites, missingPaths, observerErrors, disposed };
},
summary() { return { sourceCatalog: [...sources.values()], snapshot: this.snapshot() }; },
};
}
export async function installMaintenanceIdleWorkCapture() {
const actual = await import('../../../services/task-admission.js');
const capture = createMaintenanceIdleWorkCapture(actual);
let moduleCapture: ReturnType<typeof mock.module> | undefined;
const disposeCapture = capture.dispose;
capture.dispose = () => { try { disposeCapture(); } finally { const installed = moduleCapture; moduleCapture = undefined; installed?.restore(); } };
try {
moduleCapture = mock.module(new URL('../../../services/task-admission.ts', import.meta.url).href, {
namedExports: { ...actual, beginIdleTrackedWork: capture.beginIdleTrackedWork, trackIdleWork: capture.trackIdleWork },
});
const observed = await import('../../../services/task-admission.js');
if (observed.beginIdleTrackedWork !== capture.beginIdleTrackedWork || observed.trackIdleWork !== capture.trackIdleWork
|| Object.keys(observed).sort().join(',') !== Object.keys(actual).sort().join(',')
|| Object.keys(actual).some(key => !['beginIdleTrackedWork', 'trackIdleWork'].includes(key)
&& observed[key as keyof Admission] !== actual[key as keyof Admission])) throw new Error('Idle work module capture did not preserve original exports/identity');
return capture;
} catch (error) {
try { capture.dispose(); } catch (cleanupError) { throw new AggregateError([error, cleanupError], 'Idle work capture installation and cleanup failed'); }
throw error;
}
}
/** Reuse existing settle-loop observations. No extra poll, timer, SQL or raw stack per sample. */
export function createSettledWorkSamples(sourceCatalog: () => Array<{ id: number; callers: string[] }>) {
// Finite longest setup observation at the existing 25ms cadence, plus its
// terminal sample. Overflow still makes diagnostic completion fail closed.
const sampleLimit = Math.ceil(61_000 / 25) + 1;
const samples: Array<{ id: number; at: number; queryTotal: number; queryDelta: number; pending: number;
idle: { active: number; generation: number }; activeIds: number[] }> = [];
let previous: number | undefined, overflowSamples = 0, lastCapture: IdleWorkCaptureSnapshot | undefined;
const startedAt = Date.now();
return {
sample(snapshot: { now: number; idle: { active: number; generation: number }; workCapture?: IdleWorkCaptureSnapshot }, queryTotal: number, pending: number) {
lastCapture = snapshot.workCapture;
if (samples.length < sampleLimit) samples.push({ id: samples.length + 1, at: snapshot.now, queryTotal,
queryDelta: previous === undefined ? 0 : queryTotal - previous, pending, idle: { ...snapshot.idle }, activeIds: lastCapture?.active.map(entry => entry.id) ?? [] });
else overflowSamples++;
previous = queryTotal;
},
report() { return { startedAt, finishedAt: Date.now(), settleDeadlineMs: 30_000, sampleLimit, overflowSamples,
samples, sourceCatalog: sourceCatalog(), lastCapture }; },
assertComplete() {
if (!lastCapture || overflowSamples || lastCapture.overflow || lastCapture.sourceOverflow || lastCapture.missingCallsites
|| lastCapture.missingPaths || lastCapture.observerErrors || lastCapture.disposed) throw new Error('Passive idle work diagnostic capture incomplete');
},
};
}
@@ -0,0 +1,40 @@
import { randomUUID } from "node:crypto";
import type { Server } from "node:http";
import type { IncomingHttpSnapshot } from "../../../../../scripts/sleep-qualification/evidence.mjs";
/** Passive test-child observation of the actual app listener, including direct clients. */
export function observeMaintenanceIncomingHttp(server: Server) {
// A sole upgrade listener suppresses Node's default unsupported-upgrade close.
// The real app installs its WebSocket handlers before startServer returns.
if (server.listenerCount("request") === 0 || server.listenerCount("upgrade") === 0) {
throw new Error("Maintenance HTTP observation requires existing app request and upgrade handlers");
}
const captureId = randomUUID();
let requests = 0, installed = true, valid = true;
const count = () => {
if (requests === Number.MAX_SAFE_INTEGER) valid = false;
else requests++;
};
const detached = (event: string | symbol, listener: unknown) => {
if ((event === "request" || event === "upgrade") && listener === count) valid = false;
};
// Count before application handlers without inspecting or retaining request data.
server.prependListener("request", count);
server.prependListener("upgrade", count);
server.on("removeListener", detached);
return {
snapshot(): IncomingHttpSnapshot {
return { source: "node-http-server-events", captureId, installed,
valid: valid && installed && server.listening
&& server.listeners("request").includes(count) && server.listeners("upgrade").includes(count),
requests, observedAt: new Date().toISOString() };
},
dispose() {
if (!installed) return;
installed = false; valid = false;
server.off("removeListener", detached);
server.off("request", count);
server.off("upgrade", count);
},
};
}
@@ -0,0 +1,34 @@
import assert from 'node:assert/strict';
import { authUsers, companyMemberships, type Db } from '@paperclipai/db';
import { and, eq } from 'drizzle-orm';
import { cloudTenantPrimaryCompanyId } from '../../../services/cloud-instance.js';
import type { MaintenanceConfiguration, MaintenanceServerOptions, startMaintenanceServer } from './server-harness.js';
export const managedTenantActor = { userId: 'local-board', email: 'sleep-owner@example.test', name: 'Owned sleep operator' };
export function managedSleepConfiguration(collectorOrigin: string, serveUi: boolean): MaintenanceConfiguration {
return { automaticBackups: { intervalMinutes: 60, idleCheckpoint: true }, announcements: true,
telemetryOrigin: `${collectorOrigin}/telemetry`, otelOrigin: collectorOrigin, serveUi,
managedInstance: true, deploymentMode: 'authenticated', storageProvider: 's3' };
}
export function managedSleepServerOptions(configuration: MaintenanceConfiguration, jwks: string, s3Origin: string,
broker: { origin: string; variables: Record<string, string> }): MaintenanceServerOptions {
return { configuration, controlJwks: jwks, cloud: { jwks, s3Origin, standby: false }, tenantActor: managedTenantActor,
githubOrigin: broker.origin, connector: { origin: broker.origin,
instanceId: broker.variables.PAPERCLIP_CLOUD_CONNECTOR_INSTANCE_ID,
signPrivateKey: broker.variables.PAPERCLIP_CLOUD_CONNECTOR_SIGN_PRIVATE_KEY,
sealPrivateKey: broker.variables.PAPERCLIP_CLOUD_CONNECTOR_SEAL_PRIVATE_KEY } };
}
export async function provisionManagedSleepCompany(server: Awaited<ReturnType<typeof startMaintenanceServer>>, db: Db) {
// This real request invokes resolveCloudTenantActor, never a table seed or a bypassed company-create route.
const companies = await server.api<Array<{ id: string }>>('GET', '/companies');
const id = cloudTenantPrimaryCompanyId('maintenance-stack');
assert(companies.some(company => company.id === id), 'Actual managed primary company was not provisioned');
const [authority] = await db.select({ userId: authUsers.id, status: companyMemberships.status, membershipRole: companyMemberships.membershipRole })
.from(authUsers).innerJoin(companyMemberships, and(eq(companyMemberships.principalId, authUsers.id), eq(companyMemberships.principalType, 'user')))
.where(and(eq(authUsers.id, managedTenantActor.userId), eq(companyMemberships.companyId, id)));
assert.deepEqual(authority, { userId: managedTenantActor.userId, status: 'active', membershipRole: 'owner' });
await server.api('PATCH', `/companies/${id}`, { defaultResponsibleUserId: managedTenantActor.userId });
const observation = (await server.snapshot()).managed;
assert(observation?.isCloudManaged && observation.deploymentMode === 'authenticated' && observation.storageProvider === 's3', 'Actual supported managed profile not active');
return { id, authority, observation, boundary: 'Fixture Cloud front-door headers; actual resolver creates canonical company/user/member/default grants' };
}
@@ -0,0 +1,65 @@
import { spawn, type ChildProcess } from 'node:child_process';
import { mkdtemp, readFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { setTimeout as delay } from 'node:timers/promises';
import type { Browser, BrowserContext } from '@playwright/test';
/** A fresh owned browser, not a user's profile. Never terminate by process name. */
export async function withNativeMaintenanceBrowser<T>(run: (browser: Browser, context: BrowserContext) => Promise<T>): Promise<T> {
const { chromium } = await import('@playwright/test');
const profile = await mkdtemp(path.join(tmpdir(), 'paperclip-sleep-browser-'));
let child: ChildProcess | undefined, browser: Browser | undefined, exited: Promise<void> | undefined;
let exitObserved = false, launchError: Error | undefined, stderr = '', failure: { error: unknown } | undefined;
try {
child = spawn(chromium.executablePath(), [`--user-data-dir=${profile}`, '--remote-debugging-address=127.0.0.1', '--remote-debugging-port=0',
'--no-first-run', '--no-default-browser-check', '--disable-background-networking', '--disable-component-update', '--disable-sync', 'about:blank'], { stdio: ['ignore', 'ignore', 'pipe'] });
const ownedChild = child;
exited = new Promise<void>(resolve => {
ownedChild.once('exit', () => { exitObserved = true; resolve(); });
ownedChild.once('error', error => { launchError = error; if (!ownedChild.pid) resolve(); });
});
child.stderr?.on('data', chunk => { stderr = (stderr + String(chunk)).slice(-16_384); });
const deadline = performance.now() + 10_000;
let port: number | undefined;
while (!port) {
if (launchError) throw launchError;
if (child.exitCode !== null || child.signalCode !== null || performance.now() > deadline) throw new Error(`Owned Chromium startup failed: ${stderr}`);
try {
const candidate = Number((await readFile(path.join(profile, 'DevToolsActivePort'), 'utf8')).split('\n')[0]);
if (Number.isInteger(candidate) && candidate > 0 && candidate < 65536) port = candidate;
} catch { /* The owned startup file is not ready yet. */ }
if (!port) await delay(50);
}
// Playwright1.62 installs focus emulation on normal pages. Measured native
// tab switches stayed visible there. The supported noDefaults/default-context
// path avoids installing that override (types.d.ts ConnectOverCDPOptions).
browser = await chromium.connectOverCDP(`http://127.0.0.1:${port}`, { noDefaults: true, timeout: 10_000 });
const context = browser.contexts()[0];
if (!context) throw new Error('Owned Chromium has no native default context');
return await run(browser, context);
} catch (error) { failure = { error }; throw error; }
finally {
const errors: unknown[] = [];
let disconnectTimer: ReturnType<typeof setTimeout> | undefined;
try {
await Promise.race([browser?.close(), new Promise<never>((_, reject) => { disconnectTimer = setTimeout(() => reject(new Error('Owned browser disconnect timed out')), 5_000); })]);
} catch (error) { errors.push(error); }
finally { if (disconnectTimer) clearTimeout(disconnectTimer); }
if (child && exited) {
const waitExit = async () => {
let timer: ReturnType<typeof setTimeout> | undefined;
try { return await Promise.race([exited!.then(() => true), new Promise<boolean>(resolve => { timer = setTimeout(() => resolve(false), 5_000); })]); }
finally { if (timer) clearTimeout(timer); }
};
try {
if (child.exitCode === null && child.signalCode === null && !launchError) child.kill('SIGTERM');
if (!(await waitExit())) { child.kill('SIGKILL'); if (!(await waitExit())) throw new Error('Owned Chromium did not exit after termination'); }
} catch (error) { errors.push(error); }
}
if (!child?.pid || exitObserved) {
try { await rm(profile, { recursive: true, force: true }); } catch (error) { errors.push(error); }
} else errors.push(new Error(`Owned Chromium exit was not confirmed; profile preserved at ${profile}`));
if (errors.length) throw new AggregateError([...(failure ? [failure.error] : []), ...errors], 'Native browser work or cleanup failed');
}
}
@@ -0,0 +1,10 @@
import { getStartupTracer, instrumentationReady, shutdownInstrumentation } from '../../../instrumentation.js';
await instrumentationReady;
const span = getStartupTracer('paperclip.sleep-qualification').startSpan('sleep-qualification-stimulus');
span.end();
// Keep the real SDK alive beyond its 60s default metric-reader interval. This
// observes unwanted exporter traffic without disabling any exporter through env.
if (process.argv.includes('--observe-exporters')) {
await new Promise(resolve => setTimeout(resolve, 90_000));
}
await shutdownInstrumentation();
@@ -0,0 +1,79 @@
import { createHash } from 'node:crypto';
import { createServer } from 'node:http';
/** Adapted from the real-SDK loopback fixture in managed-cloud-prerequisites.test.ts.
* Small owned objects only; no provider credentials/IAM/durability qualification. */
export function createMaintenanceS3Boundary() {
const objects = new Map<string, { bytes: Buffer; etag: string; contentType: string }>();
const requests: Array<{ method: string; path: string; status: number }> = [];
const errors: string[] = [];
const xml = (text: string) => text.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
const server = createServer(async (req, res) => {
const url = new URL(req.url!, 'http://fixture.invalid');
const operation = { method: req.method!, path: url.pathname + url.search, status: 0 }; requests.push(operation);
const reply = (status: number, body?: string | Buffer) => { operation.status = status; res.statusCode = status; res.end(body); };
try {
if (!url.pathname.startsWith('/maintenance-test/') && url.pathname !== '/maintenance-test') throw new Error('Unknown S3 fixture bucket');
const key = decodeURIComponent(url.pathname.slice('/maintenance-test/'.length));
if (req.method === 'GET' && ['/maintenance-test', '/maintenance-test/'].includes(url.pathname) && url.searchParams.get('list-type') === '2'
&& [...url.searchParams.keys()].every(key => ['list-type', 'prefix', 'max-keys', 'x-id'].includes(key))) {
const keys = [...objects.keys()].filter(key => key.startsWith(url.searchParams.get('prefix') ?? '')).sort();
if (keys.length > Math.min(1000, Number(url.searchParams.get('max-keys') ?? 1000))) throw new Error('Listing exceeds bounded single-page fixture');
res.setHeader('content-type', 'application/xml');
reply(200, `<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/"><IsTruncated>false</IsTruncated>${keys.map(key => `<Contents><Key>${xml(key)}</Key><Size>${objects.get(key)!.bytes.length}</Size></Contents>`).join('')}</ListBucketResult>`); return;
}
if (!key || [...url.searchParams.keys()].some(key => key !== 'x-id')) throw new Error('Unknown S3 fixture operation/query');
if (req.method === 'PUT') {
const chunks: Buffer[] = []; let length = 0;
for await (const chunk of req) { length += chunk.length; if (length > 17 * 1024 * 1024) throw new Error('S3 fixture object too large'); chunks.push(Buffer.from(chunk)); }
let bytes = Buffer.concat(chunks);
if (req.headers['content-encoding'] === 'aws-chunked') bytes = decodeMaintenanceAwsChunks(bytes);
else if (req.headers['content-encoding']) throw new Error('Unknown S3 fixture content framing');
const decodedLength = req.headers['x-amz-decoded-content-length'];
if (decodedLength !== undefined && Number(decodedLength) !== bytes.length) throw new Error('S3 decoded length mismatch');
const previous = objects.get(key);
if ((req.headers['if-none-match'] !== undefined && req.headers['if-none-match'] !== '*')
|| (req.headers['if-match'] !== undefined && typeof req.headers['if-match'] !== 'string')) throw new Error('Unknown conditional S3 request');
if ((req.headers['if-none-match'] === '*' && previous) || (req.headers['if-match'] && previous?.etag !== req.headers['if-match'])) {
res.setHeader('content-type', 'application/xml'); reply(412, '<Error><Code>PreconditionFailed</Code></Error>'); return;
}
const total = [...objects.values()].reduce((sum, object) => sum + object.bytes.length, 0) - (previous?.bytes.length ?? 0) + bytes.length;
if (bytes.length > 16 * 1024 * 1024 || total > 256 * 1024 * 1024 || objects.size >= 2048) throw new Error('S3 fixture storage bound exceeded');
const object = { bytes, etag: `"${createHash('md5').update(bytes).digest('hex')}"`, contentType: String(req.headers['content-type'] ?? 'application/octet-stream') };
objects.set(key, object); res.setHeader('ETag', object.etag); reply(200); return;
}
if (req.method === 'DELETE') { objects.delete(key); reply(204); return; }
if (req.method !== 'GET' && req.method !== 'HEAD') throw new Error('Unknown S3 fixture method');
const object = objects.get(key);
if (!object) { res.setHeader('content-type', 'application/xml'); reply(404, req.method === 'GET' ? '<Error><Code>NoSuchKey</Code></Error>' : undefined); return; }
if (req.headers.range) throw new Error('Range is outside this small-object fixture contract');
res.setHeader('ETag', object.etag); res.setHeader('content-type', object.contentType); res.setHeader('content-length', object.bytes.length);
reply(200, req.method === 'GET' ? object.bytes : undefined);
} catch (error) {
errors.push(`${req.method} ${url.pathname + url.search}: ${error instanceof Error ? error.message : String(error)}`);
if (!res.headersSent) { res.setHeader('content-type', 'application/xml'); reply(501, '<Error><Code>NotImplemented</Code></Error>'); } else res.destroy();
}
});
return { server, snapshot: () => ({ boundary: 'Actual AWS SDK HTTP; bounded in-memory S3 protocol fixture, no IAM/SigV4/provider durability proof', requests: requests.map(request => ({ ...request })), errors: [...errors],
objects: [...objects].map(([key, object]) => ({ key, bytes: object.bytes.length, sha256: createHash('sha256').update(object.bytes).digest('hex') })) }) };
}
/** Decode SDK checksum framing without pretending to validate provider signatures. */
export function decodeMaintenanceAwsChunks(wire: Buffer) {
const chunks: Buffer[] = []; let offset = 0;
while (offset < wire.length) {
const end = wire.indexOf('\r\n', offset); if (end < 0) throw new Error('Malformed AWS chunk header');
const header = wire.subarray(offset, end).toString('ascii');
if (!/^[0-9a-f]+$/i.test(header)) throw new Error('Unknown AWS chunk extension');
const length = Number.parseInt(header, 16); offset = end + 2;
if (!Number.isSafeInteger(length) || length < 0 || offset + length + 2 > wire.length) throw new Error('Truncated AWS chunk');
if (length === 0) {
const trailers = wire.subarray(offset).toString('ascii');
if (!/^(?:x-amz-checksum-[a-z0-9]+:[A-Za-z0-9+/=]+\r\n)*\r\n$/.test(trailers)) throw new Error('Unknown AWS checksum trailer');
return Buffer.concat(chunks);
}
chunks.push(wire.subarray(offset, offset + length)); offset += length;
if (wire.subarray(offset, offset + 2).toString('ascii') !== '\r\n') throw new Error('Malformed AWS chunk terminator'); offset += 2;
}
throw new Error('Missing AWS final chunk');
}
@@ -2,6 +2,7 @@ import { channel } from "node:diagnostics_channel";
import { mock } from "node:test";
import type { Db } from "@paperclipai/db";
import type { Socket } from "node:net";
import { observeMaintenanceIncomingHttp } from "./incoming-http-observation.js";
const realTimeout = globalThis.setTimeout, realInterval = globalThis.setInterval, clearRealInterval = globalThis.clearInterval, realNow = Date.now;
const fakeClock = process.env.PAPERCLIP_MAINTENANCE_FAKE_CLOCK === "true";
@@ -40,14 +41,42 @@ const observeRequest = () => { outgoingHttpRequests++; };
channel("undici:request:create").subscribe(observeRequest);
channel("http.client.request.start").subscribe(observeRequest);
let workCapture: Awaited<ReturnType<typeof import('./idle-work-capture.js')['installMaintenanceIdleWorkCapture']>> | undefined;
let databaseCapture: ReturnType<typeof import('@paperclipai/db/response-capture')['databaseResponseCaptureFromEnv']>;
let incomingHttp: ReturnType<typeof observeMaintenanceIncomingHttp> | undefined;
try {
// Must precede the announcement wrapper's first import of the real app graph.
if (process.env.PAPERCLIP_MAINTENANCE_IDLE_WORK_CAPTURE === '1') {
workCapture = await (await import('./idle-work-capture.js')).installMaintenanceIdleWorkCapture();
}
const announcement = process.env.PAPERCLIP_MAINTENANCE_ANNOUNCEMENT_FIXTURE === '1'
? await (await import('./announcement-boundary.js')).installMaintenanceAnnouncementBoundary() : undefined;
const { startServer } = await import("../../../index.js");
const { idleWorkSnapshot, startTaskDrain, stopTaskDrain } = await import("../../../services/task-admission.js");
const { getStartupRecoveryState } = await import("../../../startup-recovery-state.js");
const server = await startServer();
if (workCapture) databaseCapture = (await import('@paperclipai/db/response-capture')).databaseResponseCaptureFromEnv();
const managedObservation = process.env.PAPERCLIP_MAINTENANCE_MANAGED_TENANT === '1' ? await (async () => {
const { loadConfig } = await import('../../../config.js');
const { isCloudManagedInstance } = await import('../../../services/cloud-instance.js');
const { idleApiActivity } = await import('../../../services/idle-api-activity.js');
const config = loadConfig();
return () => ({ isCloudManaged: isCloudManagedInstance(), deploymentMode: config.deploymentMode,
storageProvider: config.storageProvider, apiActivityActive: idleApiActivity.active() });
})() : undefined;
const app = server.server.listeners("request")[0] as unknown as { locals: { paperclipDb: Db } };
try { incomingHttp = observeMaintenanceIncomingHttp(server.server); }
catch (error) {
try { await server.shutdown(); }
catch (cleanupError) { throw new AggregateError([error, cleanupError], "Incoming HTTP observation and owned startup cleanup failed"); }
throw error;
}
async function shutdownServer() {
try { await server.shutdown(); } finally { incomingHttp?.dispose(); }
}
try { announcement?.assertIntercepted(); } catch (error) { await shutdownServer(); throw error; }
process.send?.({ kind: "ready", port: server.listenPort });
process.on("message", async (message: { id: number; command: string; ms?: number; issueUpdate?: { id: string; status: "done" }; orphanLease?: { companyId: string }; publication?: { companyId: string; scopeDigest: string; backupRoot: string; pendingRoot: string } }) => {
process.on("message", async (message: { id: number; command: string; ms?: number; issueUpdate?: { id: string; status: "done" }; orphanLease?: { companyId: string }; publication?: { companyId: string; scopeDigest: string; backupRoot: string; pendingRoot: string }; github?: { companyId?: string; connectionId?: string; grantId?: string } }) => {
try {
if (message.command === "snapshot" || message.command === "advance" || message.command === "drain" || message.command === "resume" || message.command === "freeze" || message.command === "realtime") {
if (message.command === "freeze" && bootClock) { clearRealInterval(bootClock); bootClock = realInterval(() => mock.timers.tick(0), 10); }
@@ -59,7 +88,23 @@ try {
if (message.command === "drain") startTaskDrain({ purpose: "idle" });
if (message.command === "resume") stopTaskDrain();
if (fakeClock) mock.timers.tick(0);
process.send?.({ id: message.id, kind: "snapshot", idle: idleWorkSnapshot(), startup: getStartupRecoveryState(), outgoingHttpRequests, network: network(), now: Date.now() });
process.send?.({ id: message.id, kind: "snapshot", idle: idleWorkSnapshot(), startup: getStartupRecoveryState(), outgoingHttpRequests, network: network(), incomingHttp: incomingHttp?.snapshot(), now: Date.now(), announcement: announcement?.snapshot(), managed: managedObservation?.(), workCapture: workCapture?.snapshot() });
} else if (message.command === "prepareGitHub" || message.command === "settleGitHub") {
const { prepareGitHubConfiguration, settleGitHubConfiguration } = await import("./github-configuration-producer.js");
const input = message.github;
if (!input) throw new Error("Missing GitHub fixture identity");
const result = message.command === "prepareGitHub"
? await prepareGitHubConfiguration(app.locals.paperclipDb, input.companyId!)
: await settleGitHubConfiguration(app.locals.paperclipDb, { connectionId: input.connectionId!, grantId: input.grantId! });
process.send?.({ id: message.id, kind: "githubConfiguration", result });
} else if (message.command === "produceChatHistory") {
const { produceChatHistory } = await import("./chat-history-producer.js");
process.send?.({ id: message.id, kind: "chatHistory", result: await produceChatHistory(app.locals.paperclipDb) });
} else if (message.command === "trace") {
const { getStartupTracer } = await import("../../../instrumentation.js");
const span = getStartupTracer("paperclip.sleep-qualification").startSpan("sleep-qualification-stimulus");
span.end();
process.send?.({ id: message.id, kind: "traceEnded" });
} else if (message.command === "createOrphanLease") {
if (!message.orphanLease) throw new Error("Missing orphan lease fixture");
const { environmentLeases } = await import("@paperclipai/db");
@@ -83,7 +128,20 @@ try {
process.send?.({ id: message.id, kind: "published" });
} else if (message.command === "shutdown") {
if (fakeClock) runClock();
await server.shutdown();
if (workCapture) {
const failures: unknown[] = [];
let before: ReturnType<typeof workCapture.summary> | undefined;
try { before = workCapture.summary(); } catch (error) { failures.push(error); }
try { await shutdownServer(); } catch (error) { failures.unshift(error); }
// A passive flush failure must not replace the original shutdown error.
try { databaseCapture?.flush(); } catch (error) { failures.push(error); }
try {
await (await import('node:fs/promises')).writeFile((await import('node:path')).join(process.env.PAPERCLIP_HOME!, 'idle-work-capture-cleanup.json'), JSON.stringify({ before, after: workCapture.summary() }, null, 2));
} catch (error) { failures.push(error); }
finally { try { workCapture.dispose(); } catch (error) { failures.push(error); } }
if (failures.length > 1) throw new AggregateError(failures, 'Owned shutdown and passive evidence failed');
if (failures.length) throw failures[0];
} else await shutdownServer();
if (bootClock) { clearRealInterval(bootClock); bootClock = undefined; }
process.send?.({ id: message.id, kind: "stopped", outgoingHttpRequests });
if (fakeClock) mock.timers.reset();
@@ -93,6 +151,16 @@ try {
} catch (error) { process.send?.({ id: message.id, kind: "error", error: error instanceof Error ? error.stack : String(error) }); }
});
} catch (error) {
incomingHttp?.dispose();
if (workCapture) {
try {
// Existing bounded DB capture performs no SQL while flushing metadata.
databaseCapture ??= (await import('@paperclipai/db/response-capture')).databaseResponseCaptureFromEnv();
databaseCapture?.flush();
await (await import('node:fs/promises')).writeFile((await import('node:path')).join(process.env.PAPERCLIP_HOME!, 'idle-work-capture-startup-failure.json'), JSON.stringify(workCapture.summary(), null, 2));
} catch { console.warn('Passive startup evidence could not be persisted; original error preserved'); }
finally { try { workCapture.dispose(); } catch { console.warn('Passive capture cleanup failed; original startup error preserved'); } }
}
if (bootClock) clearRealInterval(bootClock);
process.send?.({ kind: "error", error: error instanceof Error ? error.stack : String(error) });
process.exitCode = 1;
@@ -1,3 +1,5 @@
import type { IdleWorkCaptureSnapshot } from './idle-work-capture.js';
import type { IncomingHttpSnapshot } from '../../../../../scripts/sleep-qualification/evidence.mjs';
import { fork } from "node:child_process";
import type { Server as HttpServer } from "node:http";
import { randomBytes, randomUUID } from "node:crypto";
@@ -42,9 +44,54 @@ export async function withMaintenanceProvider<T>(provider: HttpServer, run: (ori
}
}
type Snapshot = { idle: { active: number; generation: number }; startup: { phase: string }; outgoingHttpRequests: number; network: { nonDatabaseConnections: number; databaseConnections: number; nonDatabaseBytesWritten: number }; now: number };
type Snapshot = { idle: { active: number; generation: number }; startup: { phase: string }; outgoingHttpRequests: number; network: { nonDatabaseConnections: number; databaseConnections: number; nonDatabaseBytesWritten: number }; now: number;
incomingHttp: IncomingHttpSnapshot;
managed?: { isCloudManaged: boolean; deploymentMode: string; storageProvider: string; apiActivityActive: boolean };
workCapture?: IdleWorkCaptureSnapshot;
announcement?: { intercepted: boolean; requests: string[]; unmatched: string[]; boundary: string } };
export async function startMaintenanceServer(databaseUrl: string, options: { artifactRoot?: string; enabled?: boolean; clock?: boolean; suppressed?: boolean; githubOrigin?: string; connector?: { origin: string; signPrivateKey: string; sealPrivateKey: string }; cloud?: { jwks: string; s3Origin: string; standby: boolean } } = {}) {
export interface MaintenanceConfiguration {
automaticBackups?: { intervalMinutes: number; idleCheckpoint: boolean };
announcements?: boolean;
telemetryOrigin?: string;
otelOrigin?: string;
serveUi?: boolean;
managedInstance?: boolean;
deploymentMode?: "local_trusted" | "authenticated";
storageProvider?: "local_disk" | "s3";
}
export interface MaintenanceServerOptions {
artifactRoot?: string; enabled?: boolean; clock?: boolean; suppressed?: boolean; githubOrigin?: string;
connector?: { origin: string; signPrivateKey: string; sealPrivateKey: string; instanceId?: string };
cloud?: { jwks: string; s3Origin: string; standby: boolean };
configuration?: MaintenanceConfiguration;
controlJwks?: string;
captureIdleWork?: boolean;
tenantActor?: { userId: string; email: string; name: string };
}
export async function startMaintenanceServer(databaseUrl: string, options: MaintenanceServerOptions = {}) {
const configuration = options.configuration;
if (options.captureIdleWork && options.clock) throw new Error("Passive setup diagnostics require the real clock");
if (options.tenantActor && (!options.cloud || !configuration?.managedInstance || configuration.deploymentMode !== 'authenticated' || configuration.storageProvider !== 's3')) {
throw new Error('Managed tenant fixture requires the supported authenticated S3 profile');
}
const tenantHeaders: Record<string, string> = options.tenantActor ? {
'x-paperclip-cloud-tenant-token': 'maintenance-tenant-token', 'x-paperclip-cloud-stack-id': 'maintenance-stack',
'x-paperclip-cloud-stack-role': 'owner', 'x-paperclip-cloud-user-id': options.tenantActor.userId,
'x-paperclip-cloud-user-email': options.tenantActor.email, 'x-paperclip-cloud-user-name': options.tenantActor.name,
'x-paperclip-cloud-paperclip-company-name': 'Configured sleep acceptance',
} : {};
for (const origin of [configuration?.telemetryOrigin, configuration?.otelOrigin]) {
if (!origin) continue;
const endpoint = new URL(origin);
if (endpoint.protocol !== "http:" || endpoint.hostname !== "127.0.0.1" || endpoint.username || endpoint.password || endpoint.search || endpoint.hash) {
throw new Error("Maintenance collectors must be explicit loopback HTTP endpoints");
}
}
if (configuration?.automaticBackups && (!Number.isInteger(configuration.automaticBackups.intervalMinutes) || configuration.automaticBackups.intervalMinutes < 1)) {
throw new Error("Maintenance backup interval must be a positive integer");
}
if (configuration && options.clock) throw new Error("Configured acceptance requires the real clock");
const artifacts = await createMaintenanceArtifacts(options.artifactRoot);
const { directory, retainedDirectory } = artifacts;
const reservation = createServer();
@@ -72,43 +119,57 @@ export async function startMaintenanceServer(databaseUrl: string, options: { art
const configPath = path.join(directory, "config.json");
await writeFile(configPath, JSON.stringify({
$meta: { version: 1, updatedAt: new Date().toISOString(), source: "onboard" },
database: { mode: "postgres", connectionString: proxy.url, backup: { enabled: false, dir: path.join(directory, "backups") } },
database: { mode: "postgres", connectionString: proxy.url, backup: { enabled: Boolean(configuration?.automaticBackups), intervalMinutes: configuration?.automaticBackups?.intervalMinutes ?? 60, dir: path.join(directory, "backups") } },
logging: { mode: "file", logDir: path.join(directory, "logs") },
server: { deploymentMode: "local_trusted", exposure: "private", bind: "loopback", host: "127.0.0.1", port: address.port, serveUi: false },
server: { deploymentMode: options.tenantActor ? "authenticated" : "local_trusted", exposure: "private", bind: "loopback", host: "127.0.0.1", port: address.port, serveUi: configuration?.serveUi ?? false },
storage: options.cloud ? { provider: "s3", s3: { bucket: "maintenance-test", region: "us-east-1", endpoint: options.cloud.s3Origin, forcePathStyle: true } }
: { provider: "local_disk", localDisk: { baseDir: path.join(directory, "storage") } },
secrets: { provider: "local_encrypted", strictMode: false, localEncrypted: { keyFilePath: path.join(directory, "master.key") } },
telemetry: { enabled: false }, updates: { checkEnabled: false },
...(options.tenantActor ? { auth: { baseUrlMode: "explicit", publicBaseUrl: `http://127.0.0.1:${address.port}`, disableSignUp: true } } : {}),
telemetry: { enabled: Boolean(configuration?.telemetryOrigin) }, updates: { checkEnabled: false },
}), { mode: 0o600 });
await new Promise<void>(resolve => reservation.close(() => resolve()));
const captureStartedAt = options.captureIdleWork ? Date.now() : undefined;
const captureSources = options.captureIdleWork ? new Map<number, string[]>() : undefined;
let settledDiagnosticId = 0;
const child = fork(fileURLToPath(new URL("./server-child.ts", import.meta.url)), [], {
cwd: fileURLToPath(new URL("../../../../..", import.meta.url)),
execArgv: ["--import", fileURLToPath(new URL("../../../../node_modules/tsx/dist/loader.mjs", import.meta.url))],
execArgv: [...((configuration?.announcements || options.captureIdleWork) ? ['--experimental-test-module-mocks'] : []), "--import", fileURLToPath(new URL("../../../../node_modules/tsx/dist/loader.mjs", import.meta.url))],
env: {
...(captureStartedAt === undefined ? {} : {
PAPERCLIP_MAINTENANCE_IDLE_WORK_CAPTURE: '1',
PAPERCLIP_DB_RESPONSE_CAPTURE_WINDOW: `${new Date(captureStartedAt).toISOString()}/${new Date(captureStartedAt + 60_000).toISOString()}`,
}),
PATH: process.env.PATH, TMPDIR: process.env.TMPDIR, NODE_ENV: "production",
PAPERCLIP_HOME: directory, PAPERCLIP_CONFIG: configPath, PAPERCLIP_INSTANCE_ID: "maintenance-test",
PAPERCLIP_RUNNER_STATE_DIR: path.join(directory, "durable-sessions"),
DATABASE_URL: proxy.url, DATABASE_POOL_MAX: "6", DATABASE_APPLICATION_NAME: "maintenance-integration-test",
DATABASE_URL: proxy.url, DATABASE_POOL_MAX: configuration ? "4" : "6", DATABASE_APPLICATION_NAME: "maintenance-integration-test",
PAPERCLIP_AGENT_JWT_SECRET: randomUUID(), PAPERCLIP_DECISION_SIGNING_SECRET: randomUUID(),
PAPERCLIP_TOOL_ACTION_SIGNING_SECRET: randomUUID(), BETTER_AUTH_SECRET: randomUUID(),
CODEX_HOME: path.join(directory, "codex"), PAPERCLIP_ANNOUNCEMENTS_ENABLED: "false",
PAPERCLIP_DB_BACKUP_ENABLED: "false", PAPERCLIP_UI_DEV_MIDDLEWARE: "false",
CODEX_HOME: path.join(directory, "codex"), PAPERCLIP_ANNOUNCEMENTS_ENABLED: String(configuration?.announcements ?? false),
...(configuration?.announcements ? { PAPERCLIP_MAINTENANCE_ANNOUNCEMENT_FIXTURE: '1' } : {}),
PAPERCLIP_DB_BACKUP_ENABLED: String(Boolean(configuration?.automaticBackups)), PAPERCLIP_UI_DEV_MIDDLEWARE: "false",
...(configuration?.automaticBackups ? { PAPERCLIP_DB_BACKUP_IDLE_CHECKPOINT_ENABLED: configuration.automaticBackups.idleCheckpoint ? "1" : "0" } : {}),
...(configuration?.telemetryOrigin ? { PAPERCLIP_TELEMETRY_ENDPOINT: configuration.telemetryOrigin } : {}),
...(configuration?.otelOrigin ? { OTEL_EXPORTER_OTLP_ENDPOINT: configuration.otelOrigin, OTEL_EXPORTER_OTLP_PROTOCOL: "http/json" } : {}),
HEARTBEAT_SCHEDULER_ENABLED: String(options.enabled ?? true),
PAPERCLIP_MAINTENANCE_FAKE_CLOCK: String(options.clock ?? false),
...(options.suppressed ? { PAPERCLIP_IN_WORKTREE: "true" } : {}),
...(options.githubOrigin ? { PAPERCLIP_MAINTENANCE_GITHUB_ORIGIN: options.githubOrigin } : {}),
...(options.connector ? {
PAPERCLIP_CLOUD_CONNECTOR_INSTANCE_ID: "maintenance-connector", PAPERCLIP_CLOUD_CONNECTOR_ENVIRONMENT: "test",
PAPERCLIP_CLOUD_CONNECTOR_INSTANCE_ID: options.connector.instanceId ?? "maintenance-connector", PAPERCLIP_CLOUD_CONNECTOR_ENVIRONMENT: "test",
PAPERCLIP_CLOUD_CONNECTOR_SIGN_PRIVATE_KEY: options.connector.signPrivateKey,
PAPERCLIP_CLOUD_CONNECTOR_SEAL_PRIVATE_KEY: options.connector.sealPrivateKey,
PAPERCLIP_CLOUD_CONNECTOR_BASE_URL: options.connector.origin,
} : {}),
...(options.controlJwks ? { PAPERCLIP_CLOUD_STACK_ID: "maintenance-stack", PAPERCLIP_CLOUD_RUNTIME_IDENTITY_JWKS: options.controlJwks } : {}),
...(options.cloud ? {
PAPERCLIP_MANAGED_CONFIG: JSON.stringify({ v: 1, mode: "cloud", catalogVersion: "2026.720.0", features: {}, plugins: { autoInstall: [] } }),
PAPERCLIP_SECRETS_MASTER_KEY: randomBytes(32).toString("hex"),
PAPERCLIP_CLOUD_TENANT_SERVER_TOKEN: "maintenance-tenant-token", PAPERCLIP_CLOUD_STACK_ID: "maintenance-stack",
PAPERCLIP_CLOUD_RUNTIME_IDENTITY_JWKS: options.cloud.jwks, PAPERCLIP_CLOUD_WARM_STANDBY: options.cloud.standby ? "1" : "0",
PAPERCLIP_PUBLIC_URL: "https://pool-maintenance.staging.paperclip.app", PAPERCLIP_PRIMARY_HOST: "pool-maintenance.staging.paperclip.app",
PAPERCLIP_PUBLIC_URL: options.tenantActor ? `http://127.0.0.1:${address.port}` : "https://pool-maintenance.staging.paperclip.app", PAPERCLIP_PRIMARY_HOST: options.tenantActor ? "127.0.0.1" : "pool-maintenance.staging.paperclip.app",
...(options.tenantActor ? { PAPERCLIP_MAINTENANCE_MANAGED_TENANT: "1" } : {}),
AWS_ACCESS_KEY_ID: "maintenance-fixture", AWS_SECRET_ACCESS_KEY: "maintenance-fixture", AWS_EC2_METADATA_DISABLED: "true",
} : {}),
PAPERCLIP_MIGRATION_PROMPT: "never", PAPERCLIP_MIGRATION_AUTO_APPLY: "true", PAPERCLIP_MANAGED_RUNTIME_HTTPS: "off",
@@ -139,12 +200,18 @@ export async function startMaintenanceServer(databaseUrl: string, options: { art
for (const request of requests.values()) request.reject(error);
requests.clear();
});
async function request<T>(command: string, payload: { ms?: number; issueUpdate?: { id: string; status: "done" }; orphanLease?: { companyId: string }; publication?: { companyId: string; scopeDigest: string; backupRoot: string; pendingRoot: string } } = {}): Promise<T> {
async function request<T>(command: string, payload: { ms?: number; issueUpdate?: { id: string; status: "done" }; orphanLease?: { companyId: string }; publication?: { companyId: string; scopeDigest: string; backupRoot: string; pendingRoot: string }; github?: { companyId?: string; connectionId?: string; grantId?: string } } = {}): Promise<T> {
const id = ++requestId;
const result = new Promise<T>((resolve, reject) => requests.set(id, { resolve, reject }));
child.send({ id, command, ...payload });
try { return await withDeadline(result, 30_000, () => `Maintenance child ${command} timed out: ${output.slice(-8_000)}`); }
finally { requests.delete(id); }
try {
const value = await withDeadline(result, 30_000, () => `Maintenance child ${command} timed out: ${output.slice(-8_000)}`);
if (captureSources) {
const capture = (value as { workCapture?: IdleWorkCaptureSnapshot }).workCapture;
for (const source of capture?.newSources ?? []) captureSources.set(source.id, source.callers);
}
return value;
} finally { requests.delete(id); }
}
stopChild = async () => {
try {
@@ -165,7 +232,7 @@ export async function startMaintenanceServer(databaseUrl: string, options: { art
if (options.clock) await request("realtime");
try {
const response = await fetch(`http://127.0.0.1:${port}/api${route}`, { method, signal: AbortSignal.timeout(30_000),
headers: { "Content-Type": "application/json", ...headers }, ...(body === undefined ? {} : { body: JSON.stringify(body) }) });
headers: { "Content-Type": "application/json", ...(Object.keys(headers ?? {}).some(key => ["authorization", "x-paperclip-cloud-control"].includes(key.toLowerCase())) ? {} : tenantHeaders), ...headers }, ...(body === undefined ? {} : { body: JSON.stringify(body) }) });
const value = await response.json();
if (!response.ok) throw new Error(`Maintenance API ${method} ${route}: ${response.status} ${JSON.stringify(value)}`);
return value as T;
@@ -182,6 +249,16 @@ export async function startMaintenanceServer(databaseUrl: string, options: { art
finally { if (options.clock) await request("freeze"); }
},
publishBackup: (publication: { companyId: string; scopeDigest: string; backupRoot: string; pendingRoot: string }) => request("publishBackup", { publication }),
async prepareGitHub(companyId: string) {
const result = await request<{ result: { connectionId: string; grantId: string } }>("prepareGitHub", { github: { companyId } });
return result.result;
},
async settleGitHub(input: { connectionId: string; grantId: string }) {
return request("settleGitHub", { github: input });
},
tenantHeaders,
produceChatHistory: () => request("produceChatHistory"),
trace: () => request("trace"),
snapshot: () => request<Snapshot>("snapshot"),
drain: () => request<Snapshot>("drain"), resume: () => request<Snapshot>("resume"),
realtime: () => request<Snapshot>("realtime"),
@@ -197,17 +274,38 @@ export async function startMaintenanceServer(databaseUrl: string, options: { art
},
freeze: () => request<Snapshot>("freeze"),
get output() { return output; },
async settled(quietMs = 500, deferredStartupHolds = 0) {
async settled(quietMs = 500, deferredStartupHolds = 0, settleOptions: { managedSetupRecovery?: true } = {}) {
// A successful managed health check self-notifies, then reconciles its
// future deadline on the next existing 30s pass. Observe both passes +
// 500ms quiet + 500ms margin without changing worker timing/admission.
const settleTimeoutMs = settleOptions.managedSetupRecovery ? 61_000 : 30_000;
const diagnostic = options.captureIdleWork ? (await import('./idle-work-capture.js')).createSettledWorkSamples(
() => [...captureSources!].map(([id, callers]) => ({ id, callers }))) : undefined;
const diagnosticId = diagnostic ? ++settledDiagnosticId : undefined;
let failure: { error: unknown } | undefined;
let previous = -1, quietSince = Date.now();
for (const deadline = Date.now() + 30_000; Date.now() < deadline;) {
try {
for (const deadline = Date.now() + settleTimeoutMs; Date.now() < deadline;) {
const snapshot = await request<Snapshot>("snapshot");
diagnostic?.sample(snapshot, proxy.queries.length, proxy.pending);
if (proxy.errors.length) throw new AggregateError(proxy.errors, "PostgreSQL observer failed");
if (snapshot.idle.active !== deferredStartupHolds || proxy.pending !== 0 || proxy.queries.length !== previous) quietSince = Date.now();
previous = proxy.queries.length;
if (snapshot.idle.active === deferredStartupHolds && proxy.pending === 0 && Date.now() - quietSince >= quietMs) {
diagnostic?.assertComplete(); return snapshot;
}
await delay(25);
}
const snapshot = await request<Snapshot>("snapshot");
if (proxy.errors.length) throw new AggregateError(proxy.errors, "PostgreSQL observer failed");
if (snapshot.idle.active !== deferredStartupHolds || proxy.pending !== 0 || proxy.queries.length !== previous) quietSince = Date.now();
previous = proxy.queries.length;
if (snapshot.idle.active === deferredStartupHolds && proxy.pending === 0 && Date.now() - quietSince >= quietMs) return snapshot;
await delay(25);
diagnostic?.sample(snapshot, proxy.queries.length, proxy.pending);
throw new Error(`Maintenance did not settle: ${JSON.stringify({ timeoutMs: settleTimeoutMs, quietMs, queries: proxy.queries.length, pending: proxy.pending, snapshot })}\n${output.slice(-12_000)}`);
} catch (error) { failure = { error }; throw error; }
finally {
if (diagnostic) {
try { await writeFile(path.join(directory, `idle-work-settled-samples-${diagnosticId}.json`), JSON.stringify({ ...diagnostic.report(), settleDeadlineMs: settleTimeoutMs }, null, 2)); }
catch (error) { if (failure) throw new AggregateError([failure.error, error], 'Original settling and passive evidence failed'); throw error; }
}
}
throw new Error(`Maintenance did not settle: ${JSON.stringify({ queries: proxy.queries.length, pending: proxy.pending, snapshot: await request("snapshot") })}\n${output.slice(-12_000)}`);
},
};
} catch (error) {
@@ -0,0 +1,64 @@
import { randomUUID } from "node:crypto";
import { eq } from "drizzle-orm";
import * as tables from "@paperclipai/db";
import type { NativeExecutionInputV1, NativeExecutionInputV3 } from "@paperclipai/paperclip-runner";
import { startRecoveryPostgres } from "./recovery-postgres.js";
import { prepareNativeHeartbeatRun } from "../../services/native-runtime/prepare-native-run.js";
import { buildNativeCompletionContract } from "../../services/native-runtime/completion-contracts.js";
import { nativeRuntimeContextFixture } from "../../services/native-runtime/runtime-context.test-fixture.js";
export async function createNativeWarmIngressFixture(base: NativeExecutionInputV1, cwd: string) {
const database = await startRecoveryPostgres("shared-ingress-owner-");
const db = tables.createDb(database.connectionString);
const companyId = randomUUID(), agentId = randomUUID(), environmentId = randomUUID(), workspaceId = randomUUID();
const providerLeaseId = `allocation-${randomUUID()}`;
await db.insert(tables.companies).values({ id: companyId, name: "Shared ingress", issuePrefix: `I${companyId.slice(0, 7)}` });
await db.insert(tables.agents).values({ id: agentId, companyId, name: "Shared worker", adapterType: "paperclip_runner" });
await db.insert(tables.environments).values({ id: environmentId, name: "Shared allocation", driver: "sandbox" });
await db.insert(tables.executionWorkspaces).values({ id: workspaceId, companyId, name: "Shared workspace", mode: "shared_workspace",
strategyType: "project_primary", providerType: "cloud_sandbox", cwd });
const defaultCompanyId = companyId, defaultAgentId = agentId, defaultWorkspaceId = workspaceId, defaultProviderLeaseId = providerLeaseId;
let epoch = 0;
async function admit(options?: { companyId?: string; agentId?: string; workspaceId?: string; providerLeaseId?: string }) {
const ownerCompanyId = options?.companyId ?? defaultCompanyId, ownerAgentId = options?.agentId ?? defaultAgentId;
const ownerWorkspaceId = options?.workspaceId ?? defaultWorkspaceId, ownerProviderLeaseId = options?.providerLeaseId ?? defaultProviderLeaseId;
const companyId = ownerCompanyId, agentId = ownerAgentId, workspaceId = ownerWorkspaceId, providerLeaseId = ownerProviderLeaseId;
const issueId = randomUUID(), runId = randomUUID(), leaseId = randomUUID();
const [issue] = await db.insert(tables.issues).values({ id: issueId, companyId, title: "Shared handoff", status: "in_progress",
assigneeAgentId: agentId, executionWorkspaceId: workspaceId }).returning();
const [run] = await db.insert(tables.heartbeatRuns).values({ id: runId, companyId, agentId, nativeIssueId: issueId,
status: "running", runtimeMode: "native", contextSnapshot: { issueId } }).returning();
await db.update(tables.issues).set({ executionRunId: runId }).where(eq(tables.issues.id, issueId));
await db.insert(tables.environmentLeases).values({ id: leaseId, companyId, environmentId, executionWorkspaceId: workspaceId,
issueId, heartbeatRunId: runId, provider: "daytona", providerLeaseId, leasePolicy: "reuse_by_environment" });
await db.insert(tables.nativeRunFinalizations).values({ runId, companyId, issueId, phase: "observed" });
await prepareNativeHeartbeatRun({ db, run, issue, environmentLeaseId: leaseId });
const [contract] = await db.select().from(tables.completionContracts).where(eq(tables.completionContracts.issueId, issueId));
const execution: NativeExecutionInputV3 = { ...base, schema: "paperclip.native-execution-input.v3", executionMode: "default", planningContext: null,
runtimeContext: nativeRuntimeContextFixture(), binding: { companyId, agentId, issueId, runId, executionWorkspaceId: workspaceId },
completionContract: { id: contract.id, sha256: contract.canonicalSha256, schemaVersion: "paperclip.completion-contract.v1",
contract: buildNativeCompletionContract(issue, { revision: contract.revision }) },
workspace: { cwd, repoUrl: null, repoRef: null, branchName: null },
session: { ...base.session, normalizedSessionId: `logical-${issueId}`, lifecyclePolicy: { mode: "warm", idleTimeoutMs: 60_000 } } };
await db.update(tables.executionWorkspaces).set({ metadata: { remoteWorkspace: {
schema: "paperclip.remote-workspace.v1", epoch: ++epoch, snapshotId: null, source: { kind: "empty" }, writerRunId: runId, dirty: true,
location: { environmentId, leaseId, providerLeaseId, remoteCwd: cwd },
} } }).where(eq(tables.executionWorkspaces.id, workspaceId));
const target = { kind: "remote" as const, transport: "sandbox" as const, providerKey: "daytona", environmentId, leaseId, remoteCwd: cwd,
effectiveCapabilities: { runnerWebSocketIngress: true } as never,
sandboxLeaseAcquisition: { outcome: "resumed" as const, providerLeaseId } };
return { execution, target, issueId, runId, leaseId };
}
async function settle(owner: Awaited<ReturnType<typeof admit>>, result: Record<string, unknown>) {
const resultId = randomUUID();
await db.insert(tables.nativeRunResults).values({ id: resultId, companyId, issueId: owner.issueId, runId: owner.runId,
completionContractId: owner.execution.completionContract.id, schemaStatus: "accepted", resultJson: result,
serverFingerprint: randomUUID(), canonicalSha256: randomUUID() });
await db.update(tables.nativeRunFinalizations).set({ phase: "committed", resultId, leaseOwner: null, leaseExpiresAt: null }).where(eq(tables.nativeRunFinalizations.runId, owner.runId));
await db.update(tables.heartbeatRuns).set({ status: "succeeded", finishedAt: new Date(), nativePhase: "committed", costAccountingPending: false }).where(eq(tables.heartbeatRuns.id, owner.runId));
await db.update(tables.issues).set({ status: "done", executionRunId: null, checkoutRunId: null }).where(eq(tables.issues.id, owner.issueId));
await db.update(tables.environmentLeases).set({ status: "expired", cleanupStatus: "success", releasedAt: new Date() }).where(eq(tables.environmentLeases.id, owner.leaseId));
return resultId;
}
return { db, companyId, agentId, environmentId, workspaceId, providerLeaseId, admit, settle, cleanup: () => database.cleanup() };
}
@@ -0,0 +1,30 @@
import { afterEach, expect, it, vi } from 'vitest';
const sdk = vi.hoisted(() => ({ options: [] as Array<Record<string, unknown>>, start: vi.fn(), shutdown: vi.fn(async () => {}) }));
vi.mock('../peer-version-check.js', () => ({ checkExactPeerVersions: () => ({ ok: true }) }));
vi.mock('@opentelemetry/sdk-node', () => ({ NodeSDK: class {
constructor(options: Record<string, unknown>) { sdk.options.push(options); }
start = sdk.start;
shutdown = sdk.shutdown;
} }));
vi.mock('@opentelemetry/auto-instrumentations-node', () => ({ getNodeAutoInstrumentations: () => [] }));
vi.mock('@opentelemetry/resources', () => ({ resourceFromAttributes: (attributes: unknown) => attributes }));
vi.mock('@opentelemetry/semantic-conventions', () => ({ ATTR_SERVICE_NAME: 'service.name', ATTR_SERVICE_VERSION: 'service.version' }));
vi.mock('@opentelemetry/exporter-trace-otlp-http', () => ({ OTLPTraceExporter: class {} }));
afterEach(() => { vi.unstubAllEnvs(); vi.restoreAllMocks(); });
it('configures current plural SDK signal options so an opted-in trace exporter cannot enable default metrics or logs', async () => {
vi.stubEnv('OTEL_EXPORTER_OTLP_ENDPOINT', 'http://collector.invalid:4318');
vi.stubEnv('OTEL_EXPORTER_OTLP_PROTOCOL', 'http/json');
vi.spyOn(process, 'once').mockReturnValue(process);
vi.resetModules();
const instrumentation = await import('../instrumentation.js');
await instrumentation.instrumentationReady;
expect(sdk.options).toHaveLength(1);
expect(sdk.options[0]).toMatchObject({ metricReaders: [], logRecordProcessors: [], traceExporter: expect.any(Object) });
expect(sdk.options[0]).not.toHaveProperty('metricReader');
expect(sdk.options[0]).not.toHaveProperty('logRecordProcessor');
expect(sdk.start).toHaveBeenCalledOnce();
await instrumentation.shutdownInstrumentation();
expect(sdk.shutdown).toHaveBeenCalledOnce();
});
@@ -90,6 +90,24 @@ function makeRecoveryActionRow(overrides: Record<string, unknown> = {}) {
const LATEST_RUN_TIMES = { startedAt: new Date(), createdAt: new Date() };
describe("issueRecoveryActionService", () => {
it.each(["unrelated unique constraint", "other SQLSTATE", "cyclic cause"])("does not retry %s errors", async kind => {
const constraint = "issue_recovery_actions_active_source_uq";
const error: Error & { cause?: unknown } = new Error("Wrapped database failure");
error.cause = kind === "unrelated unique constraint" ? { code: "23505", constraint_name: "other_unique_constraint" }
: kind === "other SQLSTATE" ? { code: "23503", constraint_name: constraint } : error;
const query = { from() { return this; }, where() { return this; }, orderBy() { return this; },
limit() { return Promise.resolve([]); } };
const fakeDb = {
async transaction<T>(fn: (tx: unknown) => Promise<T>): Promise<T> { return fn(fakeDb); },
select: vi.fn(() => query),
insert: vi.fn(() => ({ values: () => ({ returning: async () => { throw error; } }) })),
};
await expect(issueRecoveryActionService(fakeDb as never).upsertSourceScoped({ companyId: "company", sourceIssueId: "issue",
kind: "missing_disposition", cause: "saved-debt", fingerprint: "same-source", nextAction: "Restore existing work." }))
.rejects.toBe(error);
expect(fakeDb.select).toHaveBeenCalledOnce();
expect(fakeDb.insert).toHaveBeenCalledOnce();
});
it("does not reactivate an action resolved between the active read and update", async () => {
const existingRow = makeRecoveryActionRow({ id: "existing-action", attemptCount: 1 });
const createdRow = makeRecoveryActionRow({ id: "new-action", attemptCount: 1 });
@@ -413,6 +431,49 @@ describeEmbeddedPostgres("issue recovery actions", () => {
},
);
it("converges two service instances after their real inserts compete for one active source", async () => {
const { companyId, managerId, sourceIssueId } = await seedCompany();
const input = { companyId, sourceIssueId, kind: "stranded_assigned_issue" as const, ownerType: "agent" as const,
ownerAgentId: managerId, cause: "stranded_assigned_issue", fingerprint: "concurrent-source",
evidence: { sourceRunId: "concurrent-source-run" }, lastAttemptAt: new Date("2026-11-03T12:00:00Z"),
nextAction: "Restore the existing execution path." };
let release!: () => void, acquired!: () => void;
const gate = new Promise<void>(resolve => { release = resolve; });
const locked = new Promise<void>(resolve => { acquired = resolve; });
const lock = db.transaction(async tx => {
await tx.execute(sql`lock table issue_recovery_actions in share mode`);
acquired(); await gate;
});
await locked;
const outcomes = Promise.allSettled([issueRecoveryActionService(db).upsertSourceScoped(input),
issueRecoveryActionService(db).upsertSourceScoped(input)]);
try {
await vi.waitFor(async () => {
const [waiting] = await db.execute<{ count: number }>(sql`select count(*)::int as count from pg_stat_activity
where datname = current_database() and state = 'active' and wait_event_type = 'Lock'
and query like 'insert into "issue_recovery_actions"%'`);
expect(waiting.count).toBe(2);
}, { timeout: 5000, interval: 20 });
} finally { release(); await lock; await outcomes; }
const results = await outcomes;
const active = await db.select().from(issueRecoveryActions).where(and(eq(issueRecoveryActions.companyId, companyId),
eq(issueRecoveryActions.sourceIssueId, sourceIssueId), sql`${issueRecoveryActions.status} in ('active', 'escalated')`));
expect(active).toHaveLength(1);
expect(results.flatMap(result => result.status === "rejected" ? [{
name: result.reason?.constructor?.name, code: result.reason?.code,
causeName: result.reason?.cause?.constructor?.name, causeCode: result.reason?.cause?.code,
causeConstraint: result.reason?.cause?.constraint_name,
}] : [])).toEqual([]);
expect(results.map(result => result.status === "fulfilled" ? result.value.id : null)).toEqual([active[0]!.id, active[0]!.id]);
// The lost insert uses the existing upsert merge, just like a second sequential call.
expect(active[0]).toMatchObject({
status: "active", attemptCount: 2, maxAttempts: null, timeoutAt: null,
ownerType: "agent", ownerAgentId: managerId, ownerUserId: null,
evidence: input.evidence, lastAttemptAt: input.lastAttemptAt,
nextAction: input.nextAction, wakePolicy: null, monitorPolicy: null,
});
});
it("upserts one active source-scoped action per issue and keeps company scoping explicit", async () => {
const { companyId, managerId, sourceIssueId } = await seedCompany();
const svc = issueRecoveryActionService(db);
@@ -125,3 +125,11 @@ describe("owned maintenance providers", () => {
}
});
});
describe("configured acceptance boundaries", () => {
it("rejects remote collectors and fake-clock configuration before opening PostgreSQL", async () => {
await expect(startMaintenanceServer("not-a-database-url", { configuration: { telemetryOrigin: "https://telemetry.example.test" } })).rejects.toThrow("loopback");
await expect(startMaintenanceServer("not-a-database-url", { clock: true, configuration: { announcements: true } })).rejects.toThrow("real clock");
await expect(startMaintenanceServer("not-a-database-url", { configuration: { automaticBackups: { intervalMinutes: 0, idleCheckpoint: true } } })).rejects.toThrow("positive integer");
});
});
@@ -206,7 +206,7 @@ const support = external ? { supported: true } : await getEmbeddedPostgresTestSu
await db.update(heartbeatRuns).set({ status: "running", finishedAt: null }).where(eq(heartbeatRuns.id, f.runId));
await db.transaction(async tx => {
await createWakeAdmissionWriter().coalesceIntoActiveExecutionRun(createAdmissionTransactionScope(f.companyId, tx as unknown as typeof db), {
companyId: f.companyId, agentId: f.agentId, activeExecutionRunId: f.runId,
companyId: f.companyId, agentId: f.agentId, activeExecutionRunId: f.runId, activeExecutionRunStatus: "running",
mergedContextSnapshot: { issueId: f.issueId }, source: "automation", triggerDetail: "system",
payload: { issueId: f.issueId }, requestedByActorType: "system", requestedByActorId: null,
idempotencyKey: key,
@@ -0,0 +1,61 @@
import { expect, it, vi } from 'vitest';
import { mkdtemp, mkdir, open, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { gzipSync } from 'node:zlib';
import { observeIdleBackupCheckpoint } from './helpers/maintenance/backup-observation.js';
it('observes complete owned gzip bytes without retaining SQL and refuses missing, truncated, foreign, historical or failed checkpoints', async () => {
const directory = await mkdtemp(path.join(tmpdir(), 'sleep-backup-observer-'));
const id = '12345678-1234-1234-1234-123456789012';
const startedAt = new Date(Date.now() - 1000).toISOString();
const backupDir = path.join(directory, 'backups'); await mkdir(backupDir);
try {
const observe = () => observeIdleBackupCheckpoint(directory, [], startedAt, id);
await expect(observe()).rejects.toThrow();
await writeFile(path.join(backupDir, '.idle-backup-wake-required'), 'normal owner marker');
await expect(observe()).rejects.toThrow('no new archive');
const archive = path.join(backupDir, 'owned.sql.gz'), bytes = gzipSync(`COPY fixture;\n${id}\n`);
await writeFile(archive, bytes);
const receipt = await observe();
expect(receipt.archives[0]).toMatchObject({ bytes: bytes.length, gzipVerified: true, fixtureIdentifierFound: true });
expect(receipt.archives[0].sha256).toMatch(/^[a-f0-9]{64}$/);
expect(JSON.stringify(receipt)).not.toContain(id);
await expect(observeIdleBackupCheckpoint(directory, ['owned.sql.gz'], startedAt, id)).rejects.toThrow('no new archive');
await expect(observeIdleBackupCheckpoint(directory, [], new Date(Date.now() + 1000).toISOString(), id)).rejects.toThrow('predates');
await writeFile(archive, bytes.subarray(0, bytes.length - 4)); await expect(observe()).rejects.toThrow();
await writeFile(archive, gzipSync('foreign fixture')); await expect(observe()).rejects.toThrow('owned fixture');
await writeFile(archive, bytes); await writeFile(path.join(backupDir, 'db-backup-to-s3.failure'), 'failed');
await expect(observe()).rejects.toThrow('failure marker');
} finally { await rm(directory, { recursive: true, force: true }); }
});
it('uses opened-file mtime when birthtime is zero and still rejects old or malformed provenance', async () => {
const directory = await mkdtemp(path.join(tmpdir(), 'sleep-backup-no-birthtime-'));
const id = '12345678-1234-1234-1234-123456789012', startedAt = new Date(Date.now() - 1000).toISOString();
const backupDir = path.join(directory, 'backups'); await mkdir(backupDir);
const archive = path.join(backupDir, 'owned.sql.gz');
await writeFile(archive, gzipSync(`COPY fixture;\n${id}\n`));
await writeFile(path.join(backupDir, '.idle-backup-wake-required'), 'normal owner marker');
const handle = await open(archive, 'r'), prototype = Object.getPrototypeOf(handle), originalStat = prototype.stat;
await handle.close();
let override: { birthtimeMs: number; mtimeMs?: number } = { birthtimeMs: 0 };
const spy = vi.spyOn(prototype, 'stat').mockImplementation(async function (this: unknown, ...args: unknown[]) {
return Object.assign(await originalStat.apply(this, args), override);
});
try {
const observed = await observeIdleBackupCheckpoint(directory, [], startedAt, id);
expect(observed.archives[0]).toMatchObject({ createdAtSource: 'mtime', gzipVerified: true, fixtureIdentifierFound: true });
expect(Date.parse(observed.archives[0]!.createdAt)).toBeGreaterThanOrEqual(Date.parse(startedAt));
for (const invalid of [
{ birthtimeMs: 0, mtimeMs: Date.parse(startedAt) - 1 }, { birthtimeMs: 0, mtimeMs: 0 },
{ birthtimeMs: 0, mtimeMs: NaN }, { birthtimeMs: NaN }, { birthtimeMs: -1 },
{ birthtimeMs: 0, mtimeMs: Date.now() + 60_000 },
{ birthtimeMs: Date.parse(startedAt) - 1, mtimeMs: Date.now() },
]) {
override = invalid;
await expect(observeIdleBackupCheckpoint(directory, [], startedAt, id)).rejects.toThrow('predates');
}
await expect(observeIdleBackupCheckpoint(directory, [], 'invalid', id)).rejects.toThrow('checkpoint timestamp');
} finally { spy.mockRestore(); await rm(directory, { recursive: true, force: true }); }
});
@@ -0,0 +1,312 @@
import { createHash, generateKeyPairSync, randomUUID, sign } from 'node:crypto';
import { execFileSync } from 'node:child_process';
import { pathToFileURL } from 'node:url';
import { createServer } from 'node:http';
import { writeFile } from 'node:fs/promises';
import path from 'node:path';
import { setTimeout as delay } from 'node:timers/promises';
import { beforeAll, describe, expect, it } from 'vitest';
import { createDb } from '@paperclipai/db';
import { resolveExternalTestDatabaseUrl } from './helpers/external-test-database.js';
import { configuredSleepFeatures, finalizeLocalSleepReceipt, observeConfiguredSignals, observeIncomingHttpWindow, observedNoIncomingHttp, sleepConfigurationDigest } from '../../../scripts/sleep-qualification/evidence.mjs';
import { idleBackupArchiveNames, observeIdleBackupCheckpoint } from './helpers/maintenance/backup-observation.js';
import { stabilizeEarlyDiagnostics } from './helpers/maintenance/early-diagnostic-stabilization.js';
import { createMaintenanceS3Boundary } from './helpers/maintenance/s3-boundary.js';
import { managedSleepConfiguration, managedSleepServerOptions, provisionManagedSleepCompany } from './helpers/maintenance/managed-profile.js';
import { prepareMaintenanceRuntime } from './helpers/maintenance/runtime-build.js';
import { produceCompletedToolReview } from './helpers/maintenance/configuration-producers.js';
import { observeMaintenanceBrowser } from './helpers/maintenance/browser-observation.js';
import { startMaintenanceServer, withMaintenanceProvider } from './helpers/maintenance/server-harness.js';
import { CLOUD_CONTROL_AUDIENCE, CLOUD_CONTROL_HEADER, CLOUD_CONTROL_JWS_TYPE, CLOUD_RUNTIME_IDENTITY_ISSUER } from '../services/cloud-runtime-identity.js';
const databaseUrl = resolveExternalTestDatabaseUrl('PAPERCLIP_SLEEP_ACCEPTANCE_TEST_DATABASE_URL');
if (process.env.PAPERCLIP_SLEEP_CONFIGURED_ACCEPTANCE === '1' && !databaseUrl) throw new Error('Configured acceptance requires an explicit dedicated PAPERCLIP_SLEEP_ACCEPTANCE_TEST_DATABASE_URL; embedded allocation is forbidden');
const cloudSha = process.env.PAPERCLIP_SLEEP_CLOUD_SHA;
if (process.env.PAPERCLIP_SLEEP_CONFIGURED_ACCEPTANCE === '1' && !/^[a-f0-9]{40}$/.test(cloudSha ?? '')) throw new Error('Configured acceptance requires the exact PAPERCLIP_SLEEP_CLOUD_SHA');
const cloudCheckout = process.env.PAPERCLIP_SLEEP_CLOUD_CHECKOUT;
if (process.env.PAPERCLIP_SLEEP_CONFIGURED_ACCEPTANCE === '1' && !cloudCheckout) throw new Error('Configured acceptance requires a reviewed PAPERCLIP_SLEEP_CLOUD_CHECKOUT with its built qualification broker adapter');
const enabled = process.env.PAPERCLIP_SLEEP_CONFIGURED_ACCEPTANCE === '1' && Boolean(databaseUrl);
const quietMs = 90_000, apiCooldownMs = 600_000;
if (enabled && process.env.PAPERCLIP_MAINTENANCE_KEEP_ARTIFACTS !== '1') throw new Error('Configured qualification requires retained raw evidence');
describe.skipIf(!enabled)('configured assembled sleep qualification (explicit owned external PostgreSQL only)', () => {
beforeAll(async () => { await prepareMaintenanceRuntime(); }, 210_000);
it('retains normal credentials, completed work and a future routine through a real cooldown and quiet window', async () => {
const phase = (name: string) => console.info(JSON.stringify({ qualificationPhase: name, at: new Date().toISOString() }));
phase('starting-owned-fixtures');
const collectorRequests: Array<{ path: string; body: unknown }> = [];
const collector = createServer(async (req, res) => {
const chunks: Buffer[] = []; for await (const chunk of req) chunks.push(Buffer.from(chunk));
collectorRequests.push({ path: req.url!, body: JSON.parse(Buffer.concat(chunks).toString('utf8')) });
res.setHeader('Content-Type', 'application/json'); res.end('{}');
});
if (execFileSync('git', ['-C', cloudCheckout!, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim() !== cloudSha) throw new Error('Qualification Cloud checkout does not match the exact requested SHA');
const { startQualificationBroker } = await import(pathToFileURL(path.join(cloudCheckout!, 'scripts/sleep-qualification-broker.mjs')).href);
const broker = await startQualificationBroker();
const s3 = createMaintenanceS3Boundary();
try {
await withMaintenanceProvider(s3.server, async s3Origin => {
await withMaintenanceProvider(collector, async origin => {
const pair = generateKeyPairSync('ed25519');
const jwk = { ...pair.publicKey.export({ format: 'jwk' }), kid: 'sleep-qualification', alg: 'EdDSA', use: 'sig' };
const configuration = managedSleepConfiguration(origin, process.env.PAPERCLIP_SLEEP_BROWSER_ACCEPTANCE === '1');
// Independent intended logical contract; collector ports are observed separately.
const expected = { target: 'owned-local-assembled', appSha: execFileSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf8' }).trim(), cloudSha: cloudSha!,
configDigest: createHash('sha256').update(JSON.stringify({ automaticBackups: { intervalMinutes: 60, idleCheckpoint: true }, announcements: true,
telemetry: true, otel: { enabled: true, protocol: 'http/json' }, serveUi: process.env.PAPERCLIP_SLEEP_BROWSER_ACCEPTANCE === '1',
managedInstance: true, deploymentMode: 'authenticated', storageProvider: 's3' })).digest('hex'),
collectorOrigins: { telemetry: `${origin}/telemetry`, otel: origin } };
expect(sleepConfigurationDigest(configuration)).toBe(expected.configDigest);
const startedAt = new Date().toISOString();
const server = await startMaintenanceServer(databaseUrl!, managedSleepServerOptions(configuration, JSON.stringify({ keys: [jwk] }), s3Origin, broker));
phase('actual-server-ready');
let fixture = createDb(databaseUrl!, { maxConnections: 1 });
const control = (action: string, idleCapabilities?: unknown) => {
const encode = (value: unknown) => Buffer.from(JSON.stringify(value)).toString('base64url');
const now = Math.floor(Date.now() / 1000);
const header = encode({ alg: 'EdDSA', typ: CLOUD_CONTROL_JWS_TYPE, kid: jwk.kid });
const payload = encode({ v: 1, iss: CLOUD_RUNTIME_IDENTITY_ISSUER, aud: CLOUD_CONTROL_AUDIENCE, sub: 'maintenance-stack', action, requestId: randomUUID(), iat: now, exp: now + 60, ...(idleCapabilities ? { idleCapabilities } : {}) });
return { [CLOUD_CONTROL_HEADER]: `${header}.${payload}.${sign(null, Buffer.from(`${header}.${payload}`, 'ascii'), pair.privateKey).toString('base64url')}` };
};
let failure: { error: unknown } | undefined;
try {
await writeFile(path.join(server.directory, 'sleep-managed-startup.json'), JSON.stringify(await server.snapshot(), null, 2));
const company = await provisionManagedSleepCompany(server, fixture);
expect(await server.api('GET', '/announcements/current')).toMatchObject({ id: 'new-projects' });
const announcement = (await server.snapshot()).announcement;
expect(announcement?.intercepted).toBe(true);
expect(announcement?.requests).toEqual(['https://pages.paperclip.ing/announcements/v1/current.json']);
expect(announcement?.unmatched).toEqual([]);
const agent = await server.api<{ id: string }>('POST', `/companies/${company.id}/agents`, { name: 'Owned process worker', role: 'general', adapterType: 'process',
adapterConfig: { command: process.execPath, args: ['-e', "console.log('sleep-acceptance-useful-work')"], cwd: server.directory }, runtimeConfig: { heartbeat: { enabled: true, intervalSec: 0, wakeOnDemand: true } } });
const chatHistory = await server.produceChatHistory();
const toolHistory = await produceCompletedToolReview(server, fixture, company.id);
const github = await server.prepareGitHub(company.id);
await server.api('PATCH', `/tool-connections/${github.connectionId}`, { enabled: true });
const githubContinuity = await server.settleGitHub(github);
await expect.poll(async () => (await broker.binding())?.bindings.length, { timeout: 30_000 }).toBe(1);
await broker.enqueue();
await broker.deliver(`http://127.0.0.1:${server.port}`, () => control('connector-events:wake')[CLOUD_CONTROL_HEADER]);
// The normally enabled connection owner leases and acknowledges the real sealed event.
await expect.poll(async () => {
const [delivery] = await fixture.$client`select status from connection_event_deliveries where provider_delivery_id='qualification-delivery'`;
return delivery?.status;
}, { timeout: 30_000 }).toBe('processed');
expect(await broker.pending()).toEqual([]);
const brokerBinding = await broker.binding();
expect(brokerBinding?.bindings).toHaveLength(1);
const key = await server.api<{ id: string; token: string }>('POST', `/agents/${agent.id}/keys`, { name: 'Retained qualification credential' });
const issue = await server.api<{ id: string }>('POST', `/companies/${company.id}/issues`, { title: 'Owned completed history', status: 'todo', assigneeAgentId: agent.id });
await expect.poll(async () => {
const [run] = await fixture.$client`select status from heartbeat_runs where issue_id=${issue.id} order by created_at desc limit 1`;
return run?.status;
}, { timeout: 30_000 }).toBe('succeeded');
await server.api('PATCH', `/issues/${issue.id}`, { status: 'done' });
const routine = await server.api<{ id: string }>('POST', `/companies/${company.id}/routines`, { title: 'Harmless future obligation', assigneeAgentId: agent.id });
await server.api('POST', `/routines/${routine.id}/triggers`, { kind: 'schedule', cronExpression: (() => { const next = new Date(Date.now() + 24 * 60 * 60_000); return `0 ${next.getUTCHours()} ${next.getUTCDate()} ${next.getUTCMonth() + 1} *`; })(), timezone: 'UTC' });
await server.api('GET', '/agents/me', undefined, { Authorization: `Bearer ${key.token}` });
const lastAuthenticatedApiAt = performance.now();
await writeFile(path.join(server.directory, 'sleep-setup-receipt.json'), JSON.stringify({ ...expected, configuration, company,
agentId: agent.id, keyId: key.id, announcement, chatHistory, toolHistory, githubContinuity, brokerBinding,
brokerBoundary: broker.boundary, s3: s3.snapshot(), snapshot: await server.snapshot(),
boundary: 'Actual managed startup/tenant provisioning/producers; Cloud front door, GitHub HTTP and S3 service are owned simulated transports' }, null, 2));
expect(s3.snapshot().errors).toEqual([]);
let browser;
try {
browser = process.env.PAPERCLIP_SLEEP_BROWSER_ACCEPTANCE === '1' ? await observeMaintenanceBrowser(`http://127.0.0.1:${server.port}`, 'Organize your work.', server.tenantHeaders) : undefined;
await writeFile(path.join(server.directory, 'sleep-browser-receipt.json'), JSON.stringify({ browser, observedAt: new Date().toISOString() }, null, 2));
} catch (error) {
await writeFile(path.join(server.directory, 'sleep-browser-error.json'), JSON.stringify({ observedAt: new Date().toISOString(), error: error instanceof Error ? error.message : String(error) }, null, 2));
throw error;
}
// Setup-only owner settling never substitutes for the later full quiet window.
await server.settled(500, 0, { managedSetupRecovery: true });
const earlyDrain = await server.api<{ ownerId: string }>('POST', '/instance/task-drain', { purpose: 'idle', ttlMs: 240_000 }, control('task-drain:start'));
try {
const earlyCapabilities = { incomingWake: await broker.capability(earlyDrain.ownerId), deadlineCatalog: 'domain_deadlines_v1' };
const earlyRead = () => server.api<any>('GET', `/instance/task-drain?idleSleepSafety=1&idleSleepDiagnostics=1&scheduledWake=domain-deadlines-v1&ownerId=${earlyDrain.ownerId}`, undefined, control('task-drain:read', earlyCapabilities));
const early = await stabilizeEarlyDiagnostics(server, { ownerId: earlyDrain.ownerId, apiSucceededAt: lastAuthenticatedApiAt, read: earlyRead,
persist: observations => writeFile(path.join(server.directory, 'sleep-early-diagnostic-attempts.json'), JSON.stringify(observations, null, 2)) });
await writeFile(path.join(server.directory, 'sleep-early-diagnostics.json'), JSON.stringify(early, null, 2));
} finally { await server.api('DELETE', `/instance/task-drain?ownerId=${earlyDrain.ownerId}`, undefined, control('task-drain:stop')); }
// Anchor the full cooldown to a genuine successful opaque-key request.
await server.api('GET', '/agents/me', undefined, { Authorization: `Bearer ${key.token}` });
const lastApiActivity = performance.now();
await fixture.$client.end({ timeout: 1 });
await server.trace();
phase('first-real-ten-minute-api-cooldown');
// The real authenticated-use cooldown is part of ordinary behavior.
// No fake clock, production timing override, tenant health poll or SQL observer.
await delay(apiCooldownMs + 100);
await server.settled();
const activeSignals = observeConfiguredSignals(collectorRequests);
expect(activeSignals.qualificationSpans).toBeGreaterThan(0);
expect(activeSignals.telemetryRequests).toBeGreaterThan(0);
const before = await server.snapshot(), queries = server.proxy.queries.length, transport = server.proxy.transport;
const wallStart = performance.now(), quietStartedAt = new Date().toISOString();
phase('first-real-ninety-second-quiet-window');
await delay(quietMs);
const elapsedMs = performance.now() - wallStart, quietFinishedAt = new Date().toISOString();
const after = await server.snapshot(), finalTransport = server.proxy.transport;
const quiet = { startedAt: quietStartedAt, finishedAt: quietFinishedAt, elapsedMs, clock: 'real',
sqlQueries: server.proxy.queries.length - queries,
connectionsOpened: finalTransport.connectionsOpened - transport.connectionsOpened,
connectionsRemaining: finalTransport.connectionsOpened - finalTransport.connectionsClosed,
nonDatabaseRequests: after.outgoingHttpRequests - before.outgoingHttpRequests,
nonDatabaseBytes: after.network.nonDatabaseBytesWritten - before.network.nonDatabaseBytesWritten,
observerRequests: observeIncomingHttpWindow(before.incomingHttp, after.incomingHttp) };
await writeFile(path.join(server.directory, 'sleep-first-frozen-quiet.json'), JSON.stringify({ quiet, announcementBefore: before.announcement, announcementAfter: after.announcement }, null, 2));
const signalObservation = observeConfiguredSignals(collectorRequests);
await writeFile(path.join(server.directory, 'sleep-first-signal-observation.json'), JSON.stringify(signalObservation, null, 2));
expect(signalObservation.qualificationSpans).toBeGreaterThan(0);
expect(signalObservation.telemetryRequests).toBeGreaterThan(0);
expect(signalObservation.metricRequests).toBe(0); expect(signalObservation.logRequests).toBe(0);
expect(signalObservation.malformedTraceRequests).toBe(0); expect(signalObservation.unexpectedRequests).toBe(0);
expect(quiet.elapsedMs).toBeGreaterThanOrEqual(quietMs);
expect(quiet.sqlQueries).toBe(0);
expect(quiet.connectionsOpened).toBe(0);
expect(quiet.connectionsRemaining).toBe(0);
expect(quiet.nonDatabaseRequests).toBe(0);
expect(quiet.nonDatabaseBytes).toBe(0);
// The installed listener must have observed the actual setup HTTP traffic.
expect(quiet.observerRequests.before.requests).toBeGreaterThan(0);
expect(observedNoIncomingHttp(quiet.observerRequests, quiet)).toBe(true);
expect(after.announcement).toEqual(before.announcement);
const backupNamesBefore = await idleBackupArchiveNames(server.directory);
const checkpointStartedAt = new Date().toISOString();
const drain = await server.api<{ ownerId: string }>('POST', '/instance/task-drain', { purpose: 'idle', ttlMs: 240_000 }, control('task-drain:start'));
const capabilities = { incomingWake: await broker.capability(drain.ownerId), deadlineCatalog: 'domain_deadlines_v1' };
const read = () => server.api<any>('GET', `/instance/task-drain?idleSleepSafety=1&scheduledWake=domain-deadlines-v1&ownerId=${drain.ownerId}`, undefined, control('task-drain:read', capabilities));
const initial = await read();
await writeFile(path.join(server.directory, 'sleep-initial-safety.json'), JSON.stringify(initial, null, 2));
const final = await read();
phase('first-owned-eligibility-reports-read');
// Persist raw refusal evidence before any assertion can terminate the case.
await writeFile(path.join(server.directory, 'sleep-initial-final-safety.json'), JSON.stringify({ initial, final }, null, 2));
if (initial.idleSleepSafety.backgroundWork !== 'none' || final.idleSleepSafety.backgroundWork !== 'none') {
const diagnostic = await server.api<any>('GET', `/instance/task-drain?idleSleepSafety=1&idleSleepDiagnostics=1&scheduledWake=domain-deadlines-v1&ownerId=${drain.ownerId}`, undefined, control('task-drain:read', capabilities));
await writeFile(path.join(server.directory, 'sleep-refusal-diagnostics.json'), JSON.stringify(diagnostic, null, 2));
}
const { parseApiWakeAdmission, authorizeApiWakeAdmission } = await import('../services/idle-api-wake-admission.js');
const assertIncomingProof = (report: any, incomingWake: any, ownerId: string) => {
expect(report.idleSleepSafety.incomingWake).toEqual(incomingWake);
const admission = parseApiWakeAdmission(report.idleSleepSafety.apiWakeAdmission);
expect(admission).toMatchObject({ stackId: 'maintenance-stack', ownerId,
capabilityGeneration: incomingWake.generation, challenge: incomingWake.apiKeyProof.challenge });
expect(authorizeApiWakeAdmission(admission, { stackId: 'maintenance-stack', token: key.token, now: Date.now() })).toBeDefined();
};
assertIncomingProof(initial, capabilities.incomingWake, drain.ownerId);
assertIncomingProof(final, capabilities.incomingWake, drain.ownerId);
expect(final.idleSleepSafety.apiWakeAdmission).toEqual(initial.idleSleepSafety.apiWakeAdmission);
const proof = final.idleSleepSafety;
const backupCheckpoint = await observeIdleBackupCheckpoint(server.directory, backupNamesBefore, checkpointStartedAt, company.id);
const observations = { version: 1, ...expected, startedAt,
configuration, browser, s3: s3.snapshot(), announcement: after.announcement, chatHistory, toolHistory, githubContinuity, brokerBinding, brokerBoundary: broker.boundary, backupCheckpoint,
signalObservation, features: configuredSleepFeatures({ announcement: after.announcement, signalObservation, backupCheckpoint }),
quiet,
eligibility: { observedAt: new Date().toISOString(), backgroundWork: proof.backgroundWork, ownedHold: final.ownerId === drain.ownerId, apiCooldownElapsedMs: performance.now() - lastApiActivity,
unchangedGeneration: /^[a-f0-9]{64}$/.test(proof.deadlineProof?.generation ?? '') && initial.idleSleepSafety.deadlineProof?.generation === proof.deadlineProof?.generation }, initial, final,
live: { databaseSuspension: 'INCONCLUSIVE', hostSuspension: 'INCONCLUSIVE', backupPersistence: 'INCONCLUSIVE', reason: 'Local actual server with simulated Cloud signer; no deployed provider target authorized' },
coverage: { announcements: 'Actual route, validated feed/cache and UI with one named injected provider transport fetch; DNS/HTTPS guard covered separately, not exercised here.', browser: browser ? 'Actual Chromium foreground/background measured, then closed before server quiet window' : 'Closed; set PAPERCLIP_SLEEP_BROWSER_ACCEPTANCE=1 with built UI and headed Chromium for background/active evidence', broker: 'Retained enabled connection, actual encrypted OAuth vault, actual continuity/binding owners and signed/sealed production broker delivery; host capability from actual ConnectorStore', history: 'Normal task/credential/routine, actual tool review rejection/outbox retirement, actual signed chat ingress completion and endpoint archival' } };
await writeFile(path.join(server.directory, 'sleep-configuration-observations.json'), JSON.stringify(observations, null, 2));
const { receipt, report } = finalizeLocalSleepReceipt(observations, expected);
await writeFile(path.join(server.directory, 'sleep-configuration-receipt.json'), JSON.stringify(receipt, null, 2));
await writeFile(path.join(server.directory, 'sleep-configuration-evaluation.json'), JSON.stringify(report, null, 2));
expect(proof.version).toBe(3); expect(proof.backgroundWork).toBe('none');
expect(proof.deadlineProof).toMatchObject({ catalog: 'domain_deadlines_v1', generation: expect.stringMatching(/^[a-f0-9]{64}$/) });
fixture = createDb(databaseUrl!, { maxConnections: 1 });
const [retained] = await fixture.$client`select (select count(*)::int from agent_api_keys where id=${key.id}) as keys, (select count(*)::int from issues where id=${issue.id}) as issues, (select count(*)::int from routines where id=${routine.id}) as routines, (select enabled from tool_connections where id=${github.connectionId}) as github_enabled`;
expect(retained).toEqual({ keys: 1, issues: 1, routines: 1, github_enabled: true });
await server.api('DELETE', `/instance/task-drain?ownerId=${drain.ownerId}`, undefined, control('task-drain:stop'));
const nextIssue = await server.api<{ id: string }>('POST', `/companies/${company.id}/issues`, { title: 'First ordinary producer after quiet', status: 'todo', assigneeAgentId: agent.id }, { Authorization: `Bearer ${key.token}` });
const reactivationApiAt = performance.now();
await expect.poll(async () => {
const [run] = await fixture.$client`select status from heartbeat_runs where issue_id=${nextIssue.id} order by created_at desc limit 1`;
return run?.status;
}, { timeout: 30_000 }).toBe('succeeded');
await server.api('PATCH', `/issues/${nextIssue.id}`, { status: 'done' });
const [executions] = await fixture.$client`select count(*)::int as count from heartbeat_runs where issue_id=${nextIssue.id}`;
expect(executions.count).toBe(1);
await fixture.$client.end({ timeout: 1 });
phase('reactivation-real-ten-minute-api-cooldown');
await delay(apiCooldownMs + 100);
await server.settled();
const resumed = await server.snapshot(), resumedQueries = server.proxy.queries.length, resumedTransport = server.proxy.transport;
const resumedStart = performance.now(), resumedStartedAt = new Date().toISOString();
phase('reactivation-real-ninety-second-quiet-window');
await delay(quietMs);
const resumedElapsedMs = performance.now() - resumedStart, resumedFinishedAt = new Date().toISOString();
const resumedEnd = await server.snapshot(), resumedFinalTransport = server.proxy.transport;
// Freeze every measurement before signed drain/proof requests do SQL.
const resumedQuiet = { startedAt: resumedStartedAt, finishedAt: resumedFinishedAt, elapsedMs: resumedElapsedMs, clock: 'real',
sqlQueries: server.proxy.queries.length - resumedQueries,
connectionsOpened: resumedFinalTransport.connectionsOpened - resumedTransport.connectionsOpened,
connectionsRemaining: resumedFinalTransport.connectionsOpened - resumedFinalTransport.connectionsClosed,
nonDatabaseRequests: resumedEnd.outgoingHttpRequests - resumed.outgoingHttpRequests,
nonDatabaseBytes: resumedEnd.network.nonDatabaseBytesWritten - resumed.network.nonDatabaseBytesWritten,
observerRequests: observeIncomingHttpWindow(resumed.incomingHttp, resumedEnd.incomingHttp) };
await writeFile(path.join(server.directory, 'sleep-resumed-frozen-quiet.json'), JSON.stringify({ quiet: resumedQuiet, announcementBefore: resumed.announcement, announcementAfter: resumedEnd.announcement }, null, 2));
const resumedSignals = observeConfiguredSignals(collectorRequests);
await writeFile(path.join(server.directory, 'sleep-resumed-signal-observation.json'), JSON.stringify(resumedSignals, null, 2));
expect(resumedSignals.qualificationSpans).toBeGreaterThan(0);
expect(resumedSignals.telemetryRequests).toBeGreaterThan(0);
expect(resumedSignals.metricRequests).toBe(0); expect(resumedSignals.logRequests).toBe(0);
expect(resumedSignals.malformedTraceRequests).toBe(0); expect(resumedSignals.unexpectedRequests).toBe(0);
expect(resumedQuiet.elapsedMs).toBeGreaterThanOrEqual(quietMs);
expect(resumedQuiet.sqlQueries).toBe(0);
expect(resumedQuiet.connectionsOpened).toBe(0);
expect(resumedQuiet.connectionsRemaining).toBe(0);
expect(resumedQuiet.nonDatabaseRequests).toBe(0);
expect(resumedQuiet.nonDatabaseBytes).toBe(0);
expect(resumedQuiet.observerRequests.before.requests).toBeGreaterThan(0);
expect(observedNoIncomingHttp(resumedQuiet.observerRequests, resumedQuiet)).toBe(true);
expect(resumedEnd.announcement).toEqual(resumed.announcement);
expect(resumedEnd.announcement?.unmatched).toEqual([]);
const resumedDrain = await server.api<{ ownerId: string }>('POST', '/instance/task-drain', { purpose: 'idle', ttlMs: 240_000 }, control('task-drain:start'));
const resumedCapabilities = { incomingWake: await broker.capability(resumedDrain.ownerId), deadlineCatalog: 'domain_deadlines_v1' };
const readResumed = () => server.api<any>('GET', `/instance/task-drain?idleSleepSafety=1&scheduledWake=domain-deadlines-v1&ownerId=${resumedDrain.ownerId}`, undefined, control('task-drain:read', resumedCapabilities));
const resumedInitial = await readResumed();
await writeFile(path.join(server.directory, 'sleep-resumed-initial-safety.json'), JSON.stringify(resumedInitial, null, 2));
const resumedProof = await readResumed();
await writeFile(path.join(server.directory, 'sleep-resumed-initial-final-safety.json'), JSON.stringify({ initial: resumedInitial, final: resumedProof }, null, 2));
if (resumedInitial.idleSleepSafety.backgroundWork !== 'none' || resumedProof.idleSleepSafety.backgroundWork !== 'none') {
const diagnostic = await server.api<any>('GET', `/instance/task-drain?idleSleepSafety=1&idleSleepDiagnostics=1&scheduledWake=domain-deadlines-v1&ownerId=${resumedDrain.ownerId}`, undefined, control('task-drain:read', resumedCapabilities));
await writeFile(path.join(server.directory, 'sleep-resumed-refusal-diagnostics.json'), JSON.stringify(diagnostic, null, 2));
}
assertIncomingProof(resumedInitial, resumedCapabilities.incomingWake, resumedDrain.ownerId);
assertIncomingProof(resumedProof, resumedCapabilities.incomingWake, resumedDrain.ownerId);
expect(resumedProof.idleSleepSafety.apiWakeAdmission).toEqual(resumedInitial.idleSleepSafety.apiWakeAdmission);
expect(resumedProof.idleSleepSafety.deadlineProof?.generation).toMatch(/^[a-f0-9]{64}$/);
expect(resumedProof.idleSleepSafety.deadlineProof?.generation).toBe(resumedInitial.idleSleepSafety.deadlineProof?.generation);
expect(resumedProof.idleSleepSafety.backgroundWork).toBe('none');
await writeFile(path.join(server.directory, 'sleep-local-reactivation-receipt.json'), JSON.stringify({
version: 1, actualProducer: 'POST company issues with process assignee using the retained opaque key after owned drain release', executions: executions.count,
realApiCooldownElapsedMs: performance.now() - reactivationApiAt,
quiet: resumedQuiet,
signalObservation: resumedSignals,
realElapsedMs: resumedQuiet.elapsedMs, sqlQueries: resumedQuiet.sqlQueries,
networkRequests: resumedQuiet.nonDatabaseRequests,
initial: resumedInitial,
eligibility: resumedProof.idleSleepSafety,
boundary: 'Actual local application reactivation, real cooldown and fresh eligibility; no provider suspension or deployed inbound-wake claim',
}, null, 2));
const s3Evidence = s3.snapshot();
await writeFile(path.join(server.directory, 'sleep-s3-boundary.json'), JSON.stringify(s3Evidence, null, 2));
expect(s3Evidence.errors).toEqual([]);
phase('configured-qualification-receipts-complete');
} catch (error) { failure = { error }; throw error; }
finally {
const errors: unknown[] = [];
try { await writeFile(path.join(server.directory, 'sleep-s3-before-cleanup.json'), JSON.stringify(s3.snapshot(), null, 2)); } catch (error) { errors.push(error); }
try { await server.stop(); } catch (error) { errors.push(error); }
try { await fixture.$client.end(); } catch (error) { errors.push(error); }
// Shutdown can flush storage; capture and validate only after child exit.
try { await writeFile(path.join(server.directory, 'sleep-s3-after-cleanup.json'), JSON.stringify(s3.snapshot(), null, 2)); } catch (error) { errors.push(error); }
if (s3.snapshot().errors.length) errors.push(new Error(`S3 fixture boundary failed: ${JSON.stringify(s3.snapshot().errors)}`));
if (errors.length) throw new AggregateError([...(failure ? [failure.error] : []), ...errors], 'Configured work or owned cleanup failed');
}
});
});
} finally { await broker.stop(); }
}, 1_800_000);
});
@@ -0,0 +1,15 @@
import { expect, it } from 'vitest';
import { assertEarlySleepDiagnostics } from './helpers/maintenance/early-sleep-diagnostics.js';
const fixture = () => ({ idleSleepSafety: { diagnostics: { complete: true, checks: [
{ reason: 'local.registered_probes', state: 'present' }, { reason: 'local.inspection', state: 'present' },
{ reason: 'backup.checkpoint', state: 'not_checked' }, ...['inspection.hold', 'inspection.generation', 'durable.snapshot', 'deadlines.catalog', 'issues'].map(reason => ({ reason, state: 'none' })),
] } } });
it('retains only explicit early cooldown/diagnostic-backup reasons and refuses every unexpected or unknown blocker', () => {
expect(() => assertEarlySleepDiagnostics(fixture(), 90_000)).not.toThrow();
for (const [reason, state] of [['issues', 'present'], ['deadlines.catalog', 'unknown'], ['inspection.hold', 'not_checked'], ['local.registered_probes', 'unknown']]) {
const f = fixture(); f.idleSleepSafety.diagnostics.checks.find(check => check.reason === reason)!.state = state;
expect(() => assertEarlySleepDiagnostics(f, 90_000)).toThrow('Unexpected');
}
const missing = fixture(); missing.idleSleepSafety.diagnostics.checks.shift(); expect(() => assertEarlySleepDiagnostics(missing, 90_000)).toThrow('Missing');
for (const duration of [NaN, Infinity, -1, 600_000]) expect(() => assertEarlySleepDiagnostics(fixture(), duration)).toThrow('cooldown');
});
@@ -0,0 +1,90 @@
import { afterEach, expect, it } from 'vitest';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { fileURLToPath } from 'node:url';
import { createMaintenanceIdleWorkCapture as createCapture, createSettledWorkSamples } from './helpers/maintenance/idle-work-capture.js';
const captures: ReturnType<typeof createCapture>[] = [];
function createMaintenanceIdleWorkCapture(...args: Parameters<typeof createCapture>) {
const capture = createCapture(...args); captures.push(capture); return capture;
}
afterEach(() => { for (const capture of captures.splice(0)) capture.dispose(); });
const stack = () => 'Error\n at begin (/checkout/server/src/services/owned-work.ts:12:3)\n at owner (/checkout/server/src/index.ts:100:5)';
function original() {
let active = 0, generation = 0, finishes = 0, tracked: Promise<unknown> | undefined;
const beginIdleTrackedWork = () => {
active++; generation++; let done = false;
return () => { finishes++; if (!done) { done = true; active--; generation++; } };
};
const trackIdleWork = <T>(work: Promise<T>) => {
const finish = beginIdleTrackedWork(); const result = work.finally(finish); tracked = result; return result;
};
return { beginIdleTrackedWork, trackIdleWork, idleWorkSnapshot: () => ({ active, generation }),
finishes: () => finishes, lastTracked: () => tracked };
}
it('delegates original finish calls/idempotence and returns the exact original tracked promise and rejection', async () => {
const actual = original(), capture = createMaintenanceIdleWorkCapture(actual, { stack });
const finish = capture.beginIdleTrackedWork(); expect(actual.idleWorkSnapshot().active).toBe(1);
finish(); finish(); expect(actual.finishes()).toBe(2); expect(actual.idleWorkSnapshot().active).toBe(0);
let resolve!: (value: string) => void;
const tracked = capture.trackIdleWork(new Promise<string>(done => { resolve = done; }));
expect(tracked).toBe(actual.lastTracked()); expect(capture.snapshot().observedActive).toBe(1);
resolve('same'); await expect(tracked).resolves.toBe('same'); expect(capture.snapshot().observedActive).toBe(0);
const failure = new Error('original rejection'), rejected = capture.trackIdleWork(Promise.reject(failure));
expect(rejected).toBe(actual.lastTracked()); await expect(rejected).rejects.toBe(failure);
expect(capture.snapshot().observedActive).toBe(0); expect(actual.idleWorkSnapshot().active).toBe(0);
capture.dispose(); capture.dispose(); expect(capture.snapshot().disposed).toBe(true);
await expect(capture.trackIdleWork(Promise.resolve('after-dispose'))).resolves.toBe('after-dispose');
expect(actual.idleWorkSnapshot().active).toBe(0); expect(capture.snapshot().observedActive).toBe(0);
});
it('cannot throw from passive metadata into actual work and makes missing observation explicit', () => {
const actual = original(), capture = createMaintenanceIdleWorkCapture(actual, { stack, now: () => { throw new Error('private observer detail'); } });
let finish!: () => void; expect(() => { finish = capture.beginIdleTrackedWork(); }).not.toThrow();
expect(actual.idleWorkSnapshot().active).toBe(1); expect(capture.snapshot().missingPaths).toBeGreaterThan(0);
expect(() => finish()).not.toThrow(); expect(actual.idleWorkSnapshot().active).toBe(0);
expect(capture.snapshot().observerErrors).toBeGreaterThan(0); expect(JSON.stringify(capture.summary())).not.toContain('private observer detail');
});
it('bounds active entries, completion tail and filtered callsites without clearing original work', () => {
const actual = original();
const callers = () => 'private outside value\n' + Array.from({ length: 8 }, (_, i) => ` at work (/root/server/src/services/owned-${i}.ts:1:2)`).join('\n');
const capture = createMaintenanceIdleWorkCapture(actual, { stack: callers });
const finish = Array.from({ length: 513 }, () => capture.beginIdleTrackedWork());
const busy = capture.snapshot(); expect(busy.active).toHaveLength(512); expect(busy.overflow).toBe(1);
expect(busy.newSources[0]!.callers).toHaveLength(4); expect(JSON.stringify(busy)).not.toContain('private outside value');
expect(actual.idleWorkSnapshot().active).toBe(513); expect(busy.missingPaths).toBeGreaterThan(0);
finish.forEach(done => done()); expect(actual.idleWorkSnapshot().active).toBe(0); expect(capture.snapshot().completionTail).toHaveLength(64);
});
it('bounds unique source metadata and preserves original finisher failures', () => {
const actual = original(); let source = 0;
const capture = createMaintenanceIdleWorkCapture(actual, { stack: () => `Error\n at owner (/checkout/server/src/services/source-${source++}.ts:1:1)` });
for (let i = 0; i < 513; i++) capture.beginIdleTrackedWork()();
const state = capture.snapshot(); expect(capture.summary().sourceCatalog).toHaveLength(512); expect(state.sourceOverflow).toBe(1);
expect(actual.idleWorkSnapshot().active).toBe(0);
const failure = new Error('original finisher'), begin = actual.beginIdleTrackedWork;
actual.beginIdleTrackedWork = () => { const finish = begin(); return () => { finish(); throw failure; }; };
const observed = createMaintenanceIdleWorkCapture(actual, { stack }); const finish = observed.beginIdleTrackedWork();
expect(() => finish()).toThrow(failure); expect(actual.idleWorkSnapshot().active).toBe(0); expect(observed.snapshot().observedActive).toBe(0);
});
it('bounds settle samples and stores callsites once, while missing paths forbid diagnostic completion', () => {
const actual = original(), capture = createMaintenanceIdleWorkCapture(actual, { stack }); const finish = capture.beginIdleTrackedWork();
const state = capture.snapshot(), catalog = state.newSources;
const samples = createSettledWorkSamples(() => catalog);
for (let i = 0; i < 2442; i++) samples.sample({ now: 1000 + i, idle: actual.idleWorkSnapshot(), workCapture: state }, 20 + i, 0);
const report = samples.report(); expect(report.samples).toHaveLength(2441); expect(report.sampleLimit).toBe(2441); expect(report.overflowSamples).toBe(1);
expect(report.samples[1]!.queryDelta).toBe(1); expect(report.sourceCatalog).toHaveLength(1);
expect(JSON.stringify(report.samples)).not.toContain('owned-work'); expect(() => samples.assertComplete()).toThrow('incomplete');
finish();
const missing = createSettledWorkSamples(() => []); missing.sample({ now: 0, idle: actual.idleWorkSnapshot() }, 0, 0);
expect(() => missing.assertComplete()).toThrow('incomplete');
});
it('intercepts the actual Node24 TS module and preserves every other original export and existing state', async () => {
const result = await promisify(execFile)(process.execPath, ['--experimental-test-module-mocks', '--import', fileURLToPath(new URL('../../node_modules/tsx/dist/loader.mjs', import.meta.url)),
fileURLToPath(new URL('./helpers/maintenance/idle-work-capture-probe.ts', import.meta.url))], { timeout: 15_000, maxBuffer: 128 * 1024 });
expect(JSON.parse(result.stdout.trim())).toMatchObject({ identity: true, originalStatePreserved: true, finalActive: 0 });
});
it('preserves actual Node unhandledRejection with the same original tracked promise and reason', async () => {
const result = await promisify(execFile)(process.execPath, ['--experimental-test-module-mocks', '--import', fileURLToPath(new URL('../../node_modules/tsx/dist/loader.mjs', import.meta.url)),
fileURLToPath(new URL('./helpers/maintenance/idle-work-capture-probe.ts', import.meta.url)), 'unhandled'], { timeout: 15_000, maxBuffer: 128 * 1024 });
expect(JSON.parse(result.stdout.trim())).toMatchObject({ unhandledOriginalPromise: true, unhandledOriginalReason: true, finalActive: 0 });
});
@@ -0,0 +1,100 @@
import { createServer, request } from "node:http";
import { setImmediate } from "node:timers/promises";
import { expect, it } from "vitest";
import { observeIncomingHttpWindow, observedNoIncomingHttp } from "../../../scripts/sleep-qualification/evidence.mjs";
import { observeMaintenanceIncomingHttp } from "./helpers/maintenance/incoming-http-observation.js";
import { withMaintenanceProvider } from "./helpers/maintenance/server-harness.js";
it("counts a direct SQL-free health fetch without retaining credentials or changing its response", async () => {
// This fixture has no database or outgoing client; its only work is answering HTTP.
const server = createServer((_req, res) => { res.setHeader("Content-Type", "application/json"); res.end('{"status":"ok"}'); });
server.on("upgrade", (_req, socket) => socket.destroy());
const capture = observeMaintenanceIncomingHttp(server);
try {
await withMaintenanceProvider(server, async origin => {
const before = capture.snapshot(), startedAt = new Date().toISOString();
expect(before).toMatchObject({ installed: true, valid: true, requests: 0 });
expect(observedNoIncomingHttp(observeIncomingHttpWindow(before, before), { startedAt: before.observedAt, finishedAt: before.observedAt })).toBe(true);
const response = await fetch(`${origin}/api/health?fixture=private-query`, { signal: AbortSignal.timeout(5_000), headers: { Authorization: "Bearer fixture-private-token" } });
expect(response.status).toBe(200);
expect(await response.json()).toEqual({ status: "ok" });
const finishedAt = new Date().toISOString(), after = capture.snapshot();
expect(after).toMatchObject({ installed: true, valid: true, captureId: before.captureId });
const observed = observeIncomingHttpWindow(before, after);
expect(observed.requests).toBe(1);
expect(observedNoIncomingHttp(observed, { startedAt, finishedAt })).toBe(false);
expect(JSON.stringify(observed)).not.toMatch(/private-query|fixture-private-token|Authorization|\/api\/health/);
});
} finally { capture.dispose(); }
});
it("observes a genuinely request-free interval without making snapshot reads into HTTP", async () => {
const server = createServer((_req, res) => res.end());
server.on("upgrade", (_req, socket) => socket.destroy());
const capture = observeMaintenanceIncomingHttp(server);
try {
await withMaintenanceProvider(server, async () => {
const before = capture.snapshot(), startedAt = new Date().toISOString();
capture.snapshot(); await setImmediate(); capture.snapshot();
const finishedAt = new Date().toISOString(), after = capture.snapshot();
expect(observeIncomingHttpWindow(before, after).requests).toBe(0);
expect(observedNoIncomingHttp(observeIncomingHttpWindow(before, after), { startedAt, finishedAt })).toBe(true);
// This short boundary test does not claim the configured case's 90-second quiet proof.
});
} finally { capture.dispose(); }
});
it("counts an actual HTTP upgrade as incoming activity", async () => {
const server = createServer((_req, res) => res.end());
const upgrade = (_req: unknown, socket: import("node:stream").Duplex) => {
socket.end("HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: fixture\r\n\r\n");
};
server.on("upgrade", upgrade);
const capture = observeMaintenanceIncomingHttp(server);
try {
await withMaintenanceProvider(server, async origin => {
const before = capture.snapshot();
const client = request(origin, { signal: AbortSignal.timeout(5_000), headers: { Connection: "Upgrade", Upgrade: "fixture" } });
try {
await new Promise<void>((resolve, reject) => {
client.once("error", reject);
client.once("upgrade", (_response, socket) => { socket.destroy(); resolve(); });
client.end();
});
} finally { client.destroy(); }
expect(observeIncomingHttpWindow(before, capture.snapshot()).requests).toBe(1);
});
} finally { capture.dispose(); server.off("upgrade", upgrade); }
});
it("invalidates a detached listener and disposes only its own listeners", async () => {
const app = (_req: unknown, res: import("node:http").ServerResponse) => res.end("app");
const server = createServer(app);
const unrelatedUpgrade = () => {};
server.on("upgrade", unrelatedUpgrade);
const originalRequest = server.listeners("request"), originalUpgrade = server.listeners("upgrade");
const capture = observeMaintenanceIncomingHttp(server);
try {
await withMaintenanceProvider(server, async origin => {
const counter = server.listeners("request")[0]! as (...args: unknown[]) => void;
server.off("request", counter);
expect(capture.snapshot()).toMatchObject({ installed: true, valid: false });
// Reattaching cannot erase an unobserved interval.
server.prependListener("request", counter);
expect(capture.snapshot().valid).toBe(false);
capture.dispose(); capture.dispose();
expect(capture.snapshot()).toMatchObject({ installed: false, valid: false });
expect(server.listeners("request")).toEqual(originalRequest);
expect(server.listeners("upgrade")).toEqual(originalUpgrade);
expect(await (await fetch(origin, { signal: AbortSignal.timeout(5_000) })).text()).toBe("app");
});
} finally { capture.dispose(); }
});
it("leaves Node's unsupported-upgrade behavior alone when the app has no upgrade handler", () => {
const server = createServer((_req, res) => res.end());
const requestHandlers = server.listeners("request"), upgradeHandlers = server.listeners("upgrade");
expect(() => observeMaintenanceIncomingHttp(server)).toThrow("requires existing app request and upgrade handlers");
expect(server.listeners("request")).toEqual(requestHandlers);
expect(server.listeners("upgrade")).toEqual(upgradeHandlers);
});
@@ -0,0 +1,69 @@
import { afterEach, expect, it, vi } from 'vitest';
import { createMaintenanceS3Boundary, decodeMaintenanceAwsChunks } from './helpers/maintenance/s3-boundary.js';
import { withMaintenanceProvider } from './helpers/maintenance/server-harness.js';
import { createS3StorageProvider } from '../storage/s3-provider.js';
import { classifyEarlyDiagnostic, stabilizeEarlyDiagnostics } from './helpers/maintenance/early-diagnostic-stabilization.js';
afterEach(() => vi.unstubAllEnvs());
it('uses real small-object SDK HTTP with conditional bytes, reads and deletion; unknown operations fail closed', async () => {
vi.stubEnv('AWS_ACCESS_KEY_ID', 'owned-fixture'); vi.stubEnv('AWS_SECRET_ACCESS_KEY', 'owned-fixture'); vi.stubEnv('AWS_SESSION_TOKEN', undefined);
const fixture = createMaintenanceS3Boundary();
await withMaintenanceProvider(fixture.server, async endpoint => {
const provider = createS3StorageProvider({ bucket: 'maintenance-test', region: 'us-east-1', endpoint, forcePathStyle: true });
const input = { objectKey: 'owned/value', contentType: 'text/plain', contentLength: 3, body: Buffer.from('one') };
const first = await provider.putObjectConditional!({ ...input, ifNoneMatch: '*' });
expect(first.etag).toBeTruthy();
await expect(provider.putObjectConditional!({ ...input, ifNoneMatch: '*' })).rejects.toMatchObject({ name: 'PreconditionFailed' });
await expect(provider.putObjectConditional!({ ...input, ifMatch: 'wrong' })).rejects.toMatchObject({ name: 'PreconditionFailed' });
await provider.putObjectConditional!({ ...input, body: Buffer.from('two'), ifMatch: first.etag });
const object = await provider.getObject({ objectKey: input.objectKey }); const chunks: Buffer[] = [];
for await (const chunk of object.stream) chunks.push(Buffer.from(chunk));
expect(Buffer.concat(chunks).toString()).toBe('two'); expect((await provider.headObject({ objectKey: input.objectKey })).exists).toBe(true);
expect((await provider.listObjects!({ prefix: 'owned/' })).keys).toEqual([input.objectKey]);
await provider.deleteObject({ objectKey: input.objectKey }); expect((await provider.headObject({ objectKey: input.objectKey })).exists).toBe(false);
expect(fixture.snapshot().errors).toEqual([]);
const unknown = await fetch(`${endpoint}/maintenance-test/key?uploads`, { method: 'POST' }); expect(unknown.status).toBe(501);
expect(fixture.snapshot().errors).toHaveLength(1);
});
});
it('decodes only complete bounded AWS checksum chunks and refuses malformed framing', () => {
expect(decodeMaintenanceAwsChunks(Buffer.from('3\r\none\r\n3\r\ntwo\r\n0\r\nx-amz-checksum-crc32:YWJj\r\n\r\n')).toString()).toBe('onetwo');
for (const wire of ['3\r\none', '3;signature=unknown\r\none\r\n0\r\n\r\n', '0\r\nunknown:secret\r\n\r\n']) expect(() => decodeMaintenanceAwsChunks(Buffer.from(wire))).toThrow();
});
it('retries only observed transient inspection states and rejects durable blockers even alongside them', () => {
const checks = [
{ reason: 'local.registered_probes', state: 'present' }, { reason: 'local.inspection', state: 'present' },
{ reason: 'backup.checkpoint', state: 'not_checked' }, { reason: 'inspection.hold', state: 'none' },
{ reason: 'inspection.generation', state: 'unknown' }, { reason: 'local.tracked_work', state: 'present' },
{ reason: 'plugins.runtime_drain', state: 'not_checked' }, { reason: 'durable.snapshot', state: 'none' },
{ reason: 'deadlines.catalog', state: 'none' },
];
const report = { ownerId: 'owned', draining: true, quiescent: true, expiresAt: '2099-01-01T00:00:00Z', idleSleepSafety: { diagnostics: { complete: false, checks } } };
expect(classifyEarlyDiagnostic(report, 90_000)).toBe('transient');
const durable = structuredClone(report); durable.idleSleepSafety.diagnostics.checks.push({ reason: 'chat_endpoints', state: 'present' });
expect(() => classifyEarlyDiagnostic(durable, 90_000)).toThrow('chat_endpoints');
const unknown = structuredClone(report); unknown.idleSleepSafety.diagnostics.checks.push({ reason: 'deadlines.reader', state: 'unknown' });
expect(() => classifyEarlyDiagnostic(unknown, 90_000)).toThrow('deadlines.reader');
for (const check of report.idleSleepSafety.diagnostics.checks) if (['inspection.generation', 'local.tracked_work', 'plugins.runtime_drain'].includes(check.reason)) check.state = 'none';
report.idleSleepSafety.diagnostics.complete = true; expect(classifyEarlyDiagnostic(report, 90_000)).toBe('strict');
});
it('retains observer generation changes and still rejects a second durable report after a transient first', async () => {
const checks = [
{ reason: 'local.registered_probes', state: 'present' }, { reason: 'local.inspection', state: 'present' },
{ reason: 'backup.checkpoint', state: 'not_checked' }, ...['inspection.hold', 'inspection.generation', 'durable.snapshot', 'deadlines.catalog'].map(reason => ({ reason, state: 'none' })),
];
const strict = { ownerId: 'owned', draining: true, quiescent: true, expiresAt: '2099-01-01T00:00:00Z', idleSleepSafety: { diagnostics: { complete: true, checks } } };
const observations: unknown[][] = [];
const server = { settled: async () => ({ idle: { active: 0, generation: 10 }, managed: { apiActivityActive: true } }),
snapshot: async () => ({ idle: { active: 0, generation: 14 }, managed: { apiActivityActive: true } }) } as unknown as Parameters<typeof stabilizeEarlyDiagnostics>[0];
const accepted = await stabilizeEarlyDiagnostics(server, { ownerId: 'owned', apiSucceededAt: performance.now(), read: async () => structuredClone(strict), persist: async values => { observations.push(structuredClone(values)); } });
expect(accepted.rounds).toBe(1); expect(observations.at(-1)).toHaveLength(4);
const transient = structuredClone(strict); transient.idleSleepSafety.diagnostics.complete = false;
transient.idleSleepSafety.diagnostics.checks.find(check => check.reason === 'inspection.generation')!.state = 'unknown';
const blocked = structuredClone(transient); blocked.idleSleepSafety.diagnostics.checks.push({ reason: 'chat_endpoints', state: 'present' });
let reads = 0;
await expect(stabilizeEarlyDiagnostics(server, { ownerId: 'owned', apiSucceededAt: performance.now(), read: async () => reads++ ? blocked : transient,
persist: async values => { observations.push(structuredClone(values)); } })).rejects.toThrow('chat_endpoints');
expect(reads).toBe(2);
});
Loaded 100 of 211 files, more files were not shown because too many files have changed in this diff. Show more