feat(runner): deliver native Kimi Code (#18391)

Deliver native Kimi Code setup, exact models, scoped credentials, verified packages and runtime packs. Preserve current upstream release attestations and restore verified same-run remote retry dispatch without weakening process ownership.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip authored and GitHub committed 2026-10-30 22:26:15 -05:00
1 parent 100b364264
commit 59da15066a
91 files changed
+3678 -391

No files matched your search

+2
View File
@@ -29,6 +29,8 @@ packages/paperclip-runner/**/*.test.ts
packages/paperclip-runner/**/*.test.tsx
packages/paperclip-runner/runner/crates/*/tests
packages/paperclip-runner/scripts/*-smoke.mjs
# The provider-pack builder imports this helper during image builds.
!packages/paperclip-runner/scripts/kimi-provider-pack-smoke.mjs
# Exceptions (last match wins): the image build re-runs the runner's
# generated-file drift checks, so their committed outputs and inputs must
# survive the slimming above. 2026-09-04: the *.md rule stripped the
+2
View File
@@ -24,6 +24,8 @@ WORKDIR /context
COPY . .
# Verify ACPX release declarations against the exact Docker context.
RUN node packages/paperclip-runner/scripts/generate-acpx-profiles.mjs --check
# The provider-pack builder imports this offline smoke helper during image builds.
RUN node -e 'import("./packages/paperclip-runner/scripts/kimi-provider-pack-smoke.mjs")'
# Committed artifacts the image build reads whose drift checks cannot run
# here (they need the locked dependency tree or compiled dist/). Existence
# in the context is the property this probe guards; content correctness is
+1 -1
View File
@@ -193,7 +193,7 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Prepare hosted image build capacity
if: (inputs.build_eval_image || inputs.publish_eval_image) && inputs.release_run_id == ''
if: inputs.build_eval_image || inputs.publish_eval_image
run: |
# These preinstalled SDKs are absent from the image build context.
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc
+12 -1
View File
@@ -3,7 +3,7 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import { afterEach, expect, it } from "vitest";
import { resolveAntigravityProvisioner, resolveCursorProvisioner } from "../commands/runtime.js";
import { resolveAntigravityProvisioner, resolveCursorProvisioner, resolveKimiProvisioner } from "../commands/runtime.js";
const roots: string[] = [];
afterEach(async () => { await Promise.all(roots.splice(0).map(root => rm(root, { recursive: true, force: true }))); });
@@ -37,6 +37,17 @@ it("rejects a provisioner linked outside the installed package", async () => {
await expect(resolveCursorProvisioner(f.url)).rejects.toThrow("escapes");
});
it("resolves only Kimi's installed provisioner and rejects a redirected setup", async () => {
const f = await fixture("kimi");
expect(await resolveKimiProvisioner(f.url)).toBe(f.provisioner);
const other = await fixture("kimi");
await rm(f.provisioner); await symlink(other.provisioner, f.provisioner);
await expect(resolveKimiProvisioner(f.url)).rejects.toThrow("escapes");
await writeFile(join(other.root, "package.json"), JSON.stringify({ name: "untrusted" }));
await expect(resolveKimiProvisioner(other.url)).rejects.toThrow("identity is invalid");
});
it("resolves Antigravity setup from the public server without importing it", async () => {
const f = await fixture("antigravity");
expect(await resolveAntigravityProvisioner(f.url)).toBe(f.provisioner);
+1 -1
View File
@@ -28,7 +28,7 @@ it("rejects foreign package identity and a setup entrypoint outside that package
});
it("rejects unsupported runtime setup providers before resolution", async () => {
const program = new Command(); registerRuntimeCommands(program);
await expect(program.parseAsync(["node", "paperclipai", "runtime", "setup", "untrusted"])).rejects.toThrow("Supported runtime setup: paperclipai runtime setup pi, cursor, or antigravity");
await expect(program.parseAsync(["node", "paperclipai", "runtime", "setup", "untrusted"])).rejects.toThrow("Supported runtime setup: paperclipai runtime setup pi, cursor, kimi, or antigravity");
});
it("resolves the companion importer only inside the actual public server tar layout", async () => {
+38 -3
View File
@@ -58,7 +58,6 @@ export async function setupPiRuntime(): Promise<void> {
} finally { process.off("SIGINT", cancel); process.off("SIGTERM", cancel); }
}
/** Resolve the installed public dependency without importing or starting it. */
export async function resolveCursorProvisioner(serverUrl: string): Promise<string> {
const url = new URL(serverUrl);
if (url.protocol !== "file:" || url.search || url.hash) throw new Error("Cursor setup requires an installed Paperclip server");
@@ -131,6 +130,41 @@ export async function setupAntigravityRuntime(): Promise<void> {
}
export async function resolveKimiProvisioner(serverUrl: string): Promise<string> {
const url = new URL(serverUrl);
if (url.protocol !== "file:" || url.search || url.hash) throw new Error("Kimi setup requires an installed Paperclip server");
const entry = await realpath(fileURLToPath(url));
if (!entry.endsWith("/dist/index.js")) throw new Error("Kimi setup requires the published server layout");
const root = resolve(dirname(entry), "..");
const manifest = join(root, "package.json");
const info = await lstat(manifest);
if (!info.isFile() || info.isSymbolicLink() || info.size > 65536 || JSON.parse(await readFile(manifest, "utf8")).name !== "@paperclipai/server") throw new Error("Kimi setup server package identity is invalid");
const provisioner = join(root, "dist/vendor/paperclip-runner/cli/provision-kimi.cjs");
if (await realpath(provisioner) !== provisioner || !(await lstat(provisioner)).isFile()) throw new Error("Kimi setup entrypoint escapes its server package");
return provisioner;
}
export async function setupKimiRuntime(): Promise<void> {
const provisioner = await resolveKimiProvisioner(import.meta.resolve("@paperclipai/server"));
const child = spawn(process.execPath, ["--use-env-proxy", provisioner], {
stdio: "inherit", env: buildPiSetupEnvironment(),
});
const cancel = () => { if (child.exitCode === null && child.signalCode === null) child.kill("SIGTERM"); };
process.on("SIGINT", cancel); process.on("SIGTERM", cancel);
try {
await new Promise<void>((accept, reject) => {
let spawnError: Error | undefined;
child.on("error", error => { spawnError = error; });
child.once("close", (code, signal) => {
if (spawnError) reject(spawnError);
else if (code !== 0 || signal) reject(new Error("Kimi setup did not finish. Review its error; an invalid installation is never replaced automatically."));
else accept();
});
});
} finally { process.off("SIGINT", cancel); process.off("SIGTERM", cancel); }
}
export function registerRuntimeCommands(program: Command): void {
const runtime = program.command("runtime").description("Manage explicitly installed agent runtimes");
runtime.command("import-remote <directory>")
@@ -149,11 +183,12 @@ export function registerRuntimeCommands(program: Command): void {
} finally { process.off("SIGINT", cancel); process.off("SIGTERM", cancel); }
});
runtime.command("setup <provider>")
.description("Install and verify a pinned Pi, Cursor, or Antigravity runtime (public downloads; no model calls)")
.description("Install and verify the pinned Pi, Cursor, Kimi Code, or Antigravity runtime for this host (public downloads; no model calls)")
.action(async (provider: string) => {
if (provider === "pi") await setupPiRuntime();
else if (provider === "cursor") await setupCursorRuntime();
else if (provider === "kimi") await setupKimiRuntime();
else if (provider === "antigravity") await setupAntigravityRuntime();
else throw new Error("Supported runtime setup: paperclipai runtime setup pi, cursor, or antigravity");
else throw new Error("Supported runtime setup: paperclipai runtime setup pi, cursor, kimi, or antigravity");
});
}
+7
View File
@@ -376,6 +376,13 @@ Include both agent configuration and task-level model selection where supported.
These checks establish correct routing and authority. They do not turn a discovery
list into a model allowlist: explicit compatible models still follow section 6.1.
For Kimi Code, when `PAPERCLIP_RUNNER_REMOTE_PROVIDER_PACK_PATH` selects a
controller pack, remote Test prepares and verifies that current pack through the
normal installer before its 35-second metadata/catalog budget starts.
Installation adds to total Test time. Without a selected pack, remote targets
retain manual setup and preinstalled-runtime verification. Neither path sends a
model prompt.
## 7. Deliver instructions, tools, and credentials
### 7.1 Prove instruction delivery at the native boundary
+6
View File
@@ -1,5 +1,11 @@
# Paperclip evaluation guide
The explicit-only [Kimi Code qualification suite](../tests/runner-e2e/README.md#kimi-code-qualification)
registers twenty candidate cells across local and Daytona. It reuses the
browser, semantic interaction, file/artifact, Stop and continuation oracles with
scoped candidate admission and an encrypted Code credential. Registration does
not establish a live pass, native question/plan support or usable cost accounting.
The public directory calls full-product testing **E2E tests**. Suites describe
what is tested; deployment targets describe which app instance was tested.
QA smoke is a named selection of existing journeys against QA, and appears in
@@ -0,0 +1,955 @@
# Kimi Code native Runner integration — 2026-10-28
Owner: this chat, with separate provider, distribution, and product owners.
Baseline: `dce004259394486f6361d0c0dd5a33a5272acc0a`.
Branch: `codex/kimi-code-runner` in the existing managed worktree.
Delivery: tested, reviewable PR stack: Runner foundation, shared Core Stop
recovery, product setup and acceptance, then qualified pack delivery. Each slice
must compile and group cohesive behavior for review.
Merge, publication, and production deployment
are separate from this handoff. The user confirmed local plus Daytona acceptance.
## Merge integration — 2026-10-30
The master integration keeps the existing released Kimi revision 17 and adopts
master's upstream-only release attestation. The old source-bound revision 17
fixture remains immutable historical evidence with no current launch authority.
The pinned Kimi 2.3.3 distribution and platform closure pins are unchanged.
Generated profiles, current digest admission, and shared remote installation
must pass again on the integrated source. Earlier native acceptance and public
packages retain their original source and digest; they do not establish fresh
live or ordinary UI acceptance for this integrated identity.
## Release contract
An operator selects Kimi Code in native Runner, saves and reopens its exact
model/environment configuration, tests the runtime, assigns ordinary work, sees
streamed tools and output, receives independently verified files/artifacts and
durable task completion, and can Stop owned work. Existing legacy Kimi agents
and their history retain their current execution path.
- First environment: isolated local macOS arm64; remote: Daytona Linux x64.
- Runtime under investigation: official `@moonshot-ai/kimi-code@2.3.3`, ACP over
stdio, behind the existing ACPX driver. The ambient Mac binary is 0.41.0 and
cannot establish the new runtime's compatibility.
- Model: explicit caller-selected identifier, acknowledged before the first
prompt. Qualification uses authenticated `kimi-for-coding`;
discovery is never a product allowlist.
- Authentication: explicit company-secret `KIMI_API_KEY` environment binding.
Qualification uses only the active global login's short-lived access token;
the provider accepted a real model/tool turn. Refresh tokens and login homes
are never transferred. Managed Kimi account connections are deferred.
- Instructions: exact composed content must reach native model instructions on
create and load, with correlated acknowledgment; missing proof rejects prompt
admission. Current official ACP does not consume this field, so a pinned,
provider-owned runtime patch is required unless a richer interface proves better.
- Required: model/instruction admission, scoped authenticated tools, streaming,
semantic questions, permission decisions, cancellation, session continuation,
terminal settlement, cleanup, clean installation, and the ordinary product path.
Native question/plan controls are inventoried separately before any claim.
- Deferred unless separately qualified: managed browser login, active steering,
inbound media, native question/plan controls, native child/background agents,
first-class child projections, additional OS/architectures, and usage accounting.
Native ACP reports context occupancy, not usable per-run token/cost totals.
Dollar budget enforcement is therefore not a qualified capability.
- Spending: proportionate goal-authorized test/provider/infrastructure work;
record material costs and unknowns, diagnose first divergence before paid retry,
stop uninformative attempts. No recurring infrastructure commitment.
## Architecture and ownership
Provider translation and native patch remain Kimi-owned. Shared Runner supervision,
PRP transport, durable decisions, authorization, and task finalization retain
their existing contracts. Distribution is a pinned installed closure with no
fallback to ambient Kimi. Runtime admission remains pending until required live
qualification passes. Provider registration does not migrate legacy agents.
## Current release state — 2026-10-30
The pending11 campaign is historical baseline evidence. Its final Daytona
Stop/new/resume attempt failed because canonical sandbox reuse left an earlier
lease for the same allocation without an explicit ownership handoff. The run
created all three provider turns and completed the authored reset, but source inspection correctly
rejected ambiguous ownership before file preservation. The original failure and
cleanup evidence remain unchanged; the campaign has nineteen passing distinct
cases, not twenty.
The repair records an atomic server-owned handoff on both lease rows. Recovery
accepts only the complete backward chain of inactive, finalized native owners
with matching company, task, agent, workspace, provider and allocation. Missing,
foreign, active, conflicting or additional owners remain blocked. The exact
producer regression fails before the repair and passes with real file copyback;
all 114 owning recovery tests pass. This is deterministic recovery evidence,
not a replacement for the live Daytona case.
The foundation was reconciled with master
`a8e07500db68f187b19de83823eb7fabf1196021`. Shared permission, turn attribution
and runtime activity changes required new native artifacts and fresh pending14
qualification. Its immutable declaration and Mac/Linux artifacts remain recorded
at their actual source identities. The pending11 nineteen passes remain historical
baseline evidence with no pending14 or later runtime qualification credit.
CI rejected a stale Pi fixture in a shared Rust unit test after candidate13 was
sealed. The corrected test advanced Kimi to pending14 before its shipping build
or live cases. Candidate13 and the original failure remain unchanged.
Pending14 has two accepted local cases: Stop/new and message-marker. Both retain
their original grade, scoped native14 audit and full physical host closure. The
next local Plan-revise-accept attempt failed before its revised model turn at
PRP `run.attach`: `ACPX durable launch profile digest does not match runner startup`.
The sequence stopped. Seven other local cases and all ten pending14 Daytona task
cases remain unstarted. The original failed Plan and earlier zero-capture diagnostic
stay unchanged; the diagnostic has no acceptance credit.
A credential-free owner regression reproduces the cold restore error. New-run
startup authority validates, but the saved launch digest still binds the prior-run
grant. The reviewed native repair proves that exact prior digest by changing only
the candidate run binding, retains full company/model/profile/closure/expiry/session
and artifact checks, and separates bare cold attach from authenticated warm rotation.
Its exact-final baseline fails; five focused tests and all thirty-five owning backend
tests pass. The initial sandbox TCP-bind failures remain recorded separately from
the successful loopback-authorized owning run. These controls are not model acceptance.
The cold repair shipped as pending15 at source
`9e82c00ad8e2eba628a33a2e1857379769ad7128`. All ten local task cases passed
with original grades, scoped native15 audits and physical host closure. The
causal Plan-revise-accept case reused the same native/provider session across its
first and revised runs. No pending15 Daytona model case ran. Its immutable Linux
image was built, independently checked, loaded once and published once at
`sha256:61b097ec5e1c581a0868c174881497fab556c9197d92261595ddc75469d1787f`.
The earlier public-push review rejection and later exact human authorization stay
recorded. This is an original-source artifact, not acceptance of a later runtime.
Master advanced to `0c70cedce9a803bdc201324528abb35e423e7cd0` before remote
qualification. The real reconciliation preserves upstream runtime-context and
sidecar contract generation, confirmation replay and provider-pack contract digest
verification. The critical Kimi inventory now has 58 source inputs. Maintained
identity generation derives pending16, Copilot91 and Grok18; Pi36 is unchanged.
Colliding Copilot88 and Grok15 historical fixtures retain the upstream canonical
filenames while the unmerged Foundation variants remain byte-exact under
branch-qualified names with explicit lineage and no execution authority.
All earlier pending15 passes and artifacts remain historical, source-bound
evidence. They supply zero pending16 acceptance credit. The earlier held qualified16
proposal is obsolete and remains immutable. The reconciled source requires new
native artifacts on both targets, exact installed pack verification and a fresh
20-case local/Daytona matrix with full resource, key and host closure before a new
ordinary qualification proposal. The existing immutable image may be a physical
base only when the normal installer verifies the current full Linux pack and
native daemon; its old pack is not relabelled as current. No new artifact build,
model test or promotion is authorized by this source proposal.
The pending14 zero-model Daytona preflight passed native create/load, HTTPS checks,
raw sandbox absence, complete scoped inventory and child-key revocation/list absence.
It made no model request and supplies zero task-qualification credit. The retained pending15
Foundation, Core and Product exact-head CI and reviews are green; the new private
master reconciliation has no published-head CI claim. Original runner-shutdown attempts and the single successful
failed-jobs-only retry request per Core/Product run remain recorded.
At pending16 source `603756c22b2e39de9b5b5c3c26a0eb23c67604a1`, the
causal local Plan and message cases passed. The next local ask-question case
failed with `native_finalization_missing: session returned no semantic result`.
Its native terminal reported completed/yielded without a semantic result. The
sequence stopped, and original/physical cleanup passed. The original failure is
retained for owner diagnosis. Current acceptance is two passed, one failed and
seventeen unstarted; no paid retry or source repair is claimed here.
A proposed ordinary qualified17 transition is prepared separately and remains
HELD. It must not be applied, built for release or deployed until Root accepts all
twenty pending16 original cases and their full closure at the frozen execution
source. Earlier pending15 local passes remain historical and supply no pending16
credit. This ledger does not claim a pending16 pass count, a qualified17 artifact,
a hosted result or permission to promote.
The held transition changes the top Product source only. Foundation and Core stay
at their reviewed pending16 prerequisite identities; no extra merge or mirrored
generation exists only to make branch metadata equal. The final qualified build
must bind its actual top revision, complete native58 inventory, full pack contract
and exact target artifacts. Ordinary startup and Test must work without a candidate
grant, and retained pending16 identities must remain historical-only for execution.
The actual accepted pending16 evidence and final qualified limitations must be
recorded before the eventual qualified-source commit and artifact build.
Pending qualification uses the existing trusted startup-admission channel with
exact company, model, profile, platform closure, expiry, run and session bindings.
A warm handoff may rotate only the authenticated run identity through the normal
Runner authority transition. Saved agent configuration cannot grant candidate
authority. Ordinary installed and hosted qualified setup must work without that
test authority.
| Required gate | Current evidence | Remaining proof |
| --- | --- | --- |
| Merged native behavior | Cold repair RED/GREEN retained; current master union and pending16 deterministic checks reviewed separately | Freeze reconciled source; build and verify both native targets and current pack contract |
| Local and Daytona acceptance | Pending15 ten local passes; pending14 Plan failure and all original records unchanged | Fresh pending16 twenty-case matrix and full closure; zero carried acceptance credit |
| Credential isolation | Protected startup and private-stdin SSH checks pass; pending14 zero-model preflight closes | Verify normal qualified setup and Test without candidate authority |
| Normal distribution | Maintained closure has eighteen public packages and four daemon targets | Exact qualified-source build, full checks and clean installed consumption |
| Ordinary product setup | Isolated hosted TEST workspace reserved | Installed Mac and hosted Linux setup/Test/save/reopen, assigned skill, download and reconnect |
Delivery remains a tested, reviewable PR stack and the isolated hosted TEST
workspace. Production deployment, fleet defaults, stable publication and GitHub
merge remain outside this handoff.
## Historical pending11 acceptance ledger
| Required gate | Environment | Observation | Verdict |
| --- | --- | --- | --- |
| Exact model and instructions | local and Daytona | Pending11 pinned native create/load/model/instruction controls passed on both platforms; the current authenticated product matrix remains incomplete | partial |
| Scoped tools and credentials | local and Daytona | Native v6 credential fences passed; the first pending11 Daytona task received its exact full scoped context result and reached the intended unanswered Write | partial |
| Ordinary setup | installed local and isolated hosted test | Ordinary connect/save/reopen/Test/run/reconnect and assigned-skill behavior remain required | unattempted |
| Useful task | local and Daytona | Pending11 local and Daytona file editing passed; ordinary installed and hosted downloads remain | partial |
| Human input and Stop | local and Daytona | All pending11 local human-input and Stop journeys passed; Daytona question, Plan and Write Stop passed; remote Stop/new/resume remains | partial |
| Continuation and child loss | local and Daytona | All pending11 local continuation journeys and authenticated Daytona native worker loss passed; remote Stop/new/resume remains | partial |
| Installed distribution | macOS arm64 and Linux x64 | Pending11 native closures, Mac binaries and immutable Linux qualification image verified; final normal public-package installation remains | partial |
| Regression and repository checks | candidate revision | Prior foundation/product heads passed CI and review; shared recovery and admission controls passed real PostgreSQL owning suites and focused tests; final integrated full checks remain | partial |
Current pending11 totals remain 0 fully passed, 7 partial, 1 unattempted of 8
gates. The two-environment matrix has nineteen distinct passing case records:
all ten local journeys and nine Daytona journeys. Daytona Stop/new/resume remains
required. Four original cases retain Core/source
`0ca0d6b87d6d69b8a900621fb834ed74cb1ae446` and suite V4. Nine later cases retain
source `d721f2f9052e19370985e53a7493e8792e319313` and suite V5. Six Daytona
cases retain source `e972a46e5aedfcb48d7cb61328e81fb70ce7dccb` and suite V5.
A fresh local Plan workflow at that last revision is representative handoff
evidence and adds no distinct case. Each record retains its original expected
terminal state; conversation continuity can finish in review. None is relabelled
as a later source or suite. Original material failures remain failed.
The final local Stop/new/resume result and cleanup passed. Its original strict
audit rejected the server-only `/new` record because that row retains the default
legacy field. A separate reviewed classification verifies the saved human command,
company/task/session generation, the complete two-event terminal record, and the
unchanged server branch that returns before provider selection or execution. It
also checks all three actual provider runs against the exact native Kimi11 model,
profile and scope. The original audit remains failed; the derived classification
has positive and negative controls, including conflicting duplicate rows in both
orders. It does not treat the reset operation as inference.
The next Daytona plan-revision attempt failed during shared agent-directory
adoption before the second provider turn started. Its original failed result,
cleanup failure and raw package remain unchanged. A real PostgreSQL regression
reproduced a metadata-only race: the collection worker changes `nextAttemptAt`
and `updatedAt` between the final predecessor read and the adoption claim. The
repair in code commit `759aad8f4fa8262b8af6e797617c0d89061681be` locks and rereads
the predecessor under the existing agent and lease locks, compares its complete
authority except those two scheduling fields, and retains the original claim
predicates with the locked timestamp. Eighteen real-database controls and server
no-emit validation pass, including competing owners, same-baseline receipt or
checkpoint changes, retirement, canonical changes and foreign scope. The original
incident did not retain its exact failed predicate, so the reproduced race is not
claimed as conclusive incident causality. Closed-field conflict diagnostics will
make a further failure discriminating.
The failed remote campaign's key is revoked. Additional cleanup uses raw sandbox
resource APIs with an explicit verified organization: a toolbox lookup returning
404 cannot establish sandbox deletion. Exact filtered inventory exposed two
archived verification probes omitted by the earlier summary. All eight known
owned allocations, including preflight, now return authoritative resource 404.
The provider list retains stale entries; every returned owned entry was separately
checked through the resource API. The earlier summary and failed cleanup grade
remain preserved. Future campaigns require the corrected raw-resource cleanup
and its real-SDK mocked-HTTP regression before allocation.
A separate host prerequisite failed before any test or model request because
PostgreSQL 18.1 exhausted macOS System V semaphores. Qualification uses a verified
official PostgreSQL 18.6 build with named POSIX semaphores through an isolated,
recorded worktree dependency binding. A fresh migrated control plane reached
health 200 at the actual source revision; a same-cluster SQL check confirmed its
version, database and empty company schema. Owned processes and ports closed.
The binding must be restored before normal package validation. Earlier PostgreSQL
18.1 evidence retains its original environment identity.
The source-mode Product E2E control plane serves the checked-out source. Its
source revision, production-byte baseline and separately compiled Core artifact
are distinct roles. Native Mac binaries and the immutable Linux image retain
actual build source `3a2dc2d78df0da51ef56dd6d898a2993496d3567`; neither is
relabelled or rebuilt for a Core-only ownership repair.
The shared directory repair passed a fresh local and Daytona Plan workflow,
including three native turns, the exact human revision decisions and resource
cleanup. Five subsequent Daytona cases passed: ordinary and structured questions,
structured-question restart, file editing, and restart continuity. The last
Stop/new/resume case failed before reset: the cancelled native turn retained a
workspace preservation obligation, and the saved `/new` message did not cross
that hold. The original failed result and its original cleanup grade are retained.
Independent raw resource checks found one stopped allocation despite an empty
filtered inventory; exact run, organization and ownership evidence authorized
its separate deletion, followed by an authoritative resource 404.
Native admission renewed the real preparation lease and changed the run to native
mode without retiring that earlier ownership. The recorded future expiry can
block workspace recovery after the provider has stopped. The original inspector
log does not identify whether its coordinator, active-control or preparation-lease
predicate fired first. A real PostgreSQL regression reproduces the stale fields
on both local and remote admission. The repair clears `controllerBootId`,
`controllerLeaseExpiresAt` and `executionStage` in the existing row-locked native
admission transaction. The current preparation owner is still validated before
the native coordinator commits. All twenty-eight controls pass, including foreign
and expired ownership, cancellation before admission, transaction rollback, live
preparation recovery and a later workspace synchronization failure. Workspace
preservation, native ownership and physical Stop guards stay unchanged.
The preparation handoff repair passed a fresh local Stop/new/resume workflow at
`a44ebc8abe8ebc766a207a9e68460db9829d6129`: three native runs, one authored
server reset, generation one and a fresh final native session. Original result,
raw evidence, native/control classification and physical cleanup pass. It is a
representative regression check and adds no distinct case.
A second real PostgreSQL regression found that a valid Stop can arrive after a
workspace check has saved a twenty-four to thirty-six second retry delay. The
existing post-cleanup callback did not advance that check when physical Stop
became confirmed. The shared repair records one marker bound to the same run,
recovery generation and closed native/physical receipt digest. Only the normal
post-executor cleanup producer records that evidence. The recovery service can
advance its matching Stop recheck once, preserving retries, window and provider
cooldown. A marker arriving while the worker runs stays pending until its failure
commit, so the notification cannot be lost. File preservation and model admission
remain under their existing owners.
The exact final positive fails against the prior implementation and passes with
this repair. Nineteen final focused controls and typecheck pass. The earlier owning
recovery and run-control suites passed one hundred thirty-two tests; those results
retain their earlier test-module identity. The positive uses the actual resolved
preserve-without-replay writer, saved human `/new` intent, real archive copy-back
and normal explicit continuation admission. It proves timely reset eligibility;
actual browser generation and a fresh native turn still require the final Daytona
case. No blocker, timeout, grading, schema or polling change is included.
The six shared Core source/test changes are a separate prerequisite,
[#18245](https://github.com/paperclipai/paperclip/pull/18245), based on the frozen
Runner foundation. Product retains its Kimi plan, UI, fixtures and provider files.
Product contains the real prerequisite ancestry, including queued-input repair
`5c964f5d3cfd65e493b92c53cfe0c80b0d21f54d`. The final compiled Core must
identify the integrated product source; earlier Core, native and image identities
are retained separately.
A further real PostgreSQL control reproduced an already queued `/new` request
being cancelled before provider admission and its saved wake marked skipped.
The shared dispatch owner now keeps only that exact current human wake deferred
inside the cancellation transaction. It requires the canonical no-execution proof,
matching native preservation action and unfinalized native baseline. The existing
queued-writer guard orders copyback after that commit. Real copyback and the
normal callback then adopt the original input once, including concurrent callback
delivery. Foreign, terminal and unverified requests remain fenced. The unchanged
production patch passed 164 owning tests; final focused controls, the actual
stranded-recovery retry discriminator and no-emit validation passed. A saved prior
session reference remains a hint. The deferred wake prevents a competing automatic
retry; no new retry policy, queue hook or polling was added.
The extra cancelled admission receipt remains in the raw Product E2E evidence.
Kimi suite V6 classifies it separately from the three native provider turns and
the single authored reset. It requires the complete server event receipt and final
public proof of the human command, stopped source, preservation, authorization,
reset and fresh native session. Provider counts, deadlines and earlier grades stay
unchanged. Forty-nine focused controls and narrow typecheck pass. Controlled
Chromium exercised the prior count failure and corrected full flow; that is
fixture calibration, not live model acceptance. The nineteen accepted originals
retain V4/V5. Only the remaining fresh case uses V6, fingerprint
`f7883fff6c2a5fda14571ef150849e3d2514be036fed941e257276e6b956e5d7`.
Next, complete the one remaining Daytona Stop/new/resume case. The local
representative remains valid because the additional event behavior requires a
remote physical termination receipt and preserves local Stop policy. The
controller must check every exact sandbox ID from authoritative run records,
including failed attempts, even when filtered inventory is empty. Retain the
nineteen passing cases only under the explicit source impact record. Qualification
promotion remains held until twenty distinct cases and physical resource and key
closure are verified.
Afterward build the normal four-platform daemon and eighteen-package distribution,
verify ordinary installed local setup, and deploy the exact qualified revision
only to `kimi.test.paperclip.app` for normal configuration, assigned-skill, CSV
download and persistence checks. Those installation and hosted journeys remain
required. Production, fleet defaults, stable publication and GitHub merge remain
outside this handoff.
## Earlier pending11 recovery observations
The following observations retain their original source, artifacts and next-step
context. The current state above supersedes their campaign instructions.
At this earlier snapshot, pending11 totals: 0 fully passed, 7 partial, 1 unattempted of 8 gates.
The current twenty-cell matrix has four passing cells on frozen Core/source
`0ca0d6b87d6d69b8a900621fb834ed74cb1ae446`, pending Kimi11 and suite V4:
local native Write Stop, local native worker loss, local message-marker, and
Daytona native Write Stop. Their original reports retain that identity; sixteen
cells remain. Original failed attempts remain failed. The fixture-only V5 successor
clarifies Plan marker formatting and expands detailed run evidence without
changing their task prompts, graders, Core, native closures, or immutable image.
The remote observer source hash changes with its new diagnostics and remains an
explicit test-identity derivative in the scoped context; full model-facing byte
equality is not claimed. Original Stop did not invoke the fault branch, common
observer functions remain unchanged, and fresh authenticated worker-loss coverage
through the new observer is required before final acceptance. Profile9 results remain
historical support. These grouped gates do not replace per-cell evidence.
The explicit-only `kimi-qualification` Product E2E suite has 20 required cells:
message-marker, plan-revise-accept, ask-question, structured-question-resume,
structured-question-restart-resume, file-edit-validate, continuity-restart, and
stop-new-resume, native pending-permission Stop, and native pending-permission worker loss, each on local and Daytona. Each has one required repetition and
zero automatic paid retries. Historically, all ten profile9 local cells passed
with report packaging and cleanup. Stop retains source `821256d6`, campaign
`local-2026-10-29T14-46-04-979Z`, run `91557abb-b9c0-422d-af1e-f0e82e948e96`.
The other nine retain source `f7ac439e` and their individual campaigns between
15:10 and 15:33 UTC. The qualification-only observer correction leaves all native
runtime/profile/image inputs unchanged. It reads argc-bounded kernel argv while
retaining exact compiled-loader hashes, sealed native bytes, inode, ancestry and
run identity; a real compiled native/ACPX peer proved title mutation, excluded
an environment canary, rejected foreign/ambiguous targets and retired the child.
All ten original result hashes were independently checked. The four earlier local
failures remain unchanged alongside their later diagnoses and cleanup receipts.
Historical profile9 Daytona qualification stopped at its first lifecycle case.
A later independent message-marker control passed all six matchers, durable Done,
native Succeeded and cleanup on source `50736cb6`, run
`0a809b5e-9a82-4590-9283-cbaa359f676f`. That is one case, not the full matrix.
The first launch stopped at
admission because two executable digests were missing; no task or sandbox was
created. With the exact image Node/runnerd digests supplied, the first actual
Stop cell failed before provider startup: setup Test could not reach the global
Kimi catalog. Its failed/cleanup-failed grade is preserved. Six archived Test
sandboxes were subsequently ownership-verified, deleted and checked absent404;
that post-failure recovery does not change the original grade.
Assigned-skill behavior and ordinary connect/save/reopen/Test/run/reconnect remain
separate requirements, including the requested hosted test workspace.
### Earlier state before fixture V5
Four current cells passed with independently checked report hashes, native profile/model identity, terminal state, and cleanup. Local Plan acceptance then stopped because the model omitted the required draft marker in its actual `write_document` input; saved bytes, API reads, and the Plan UI matched that input. The strict marker and revision/approval checks remain. A fixture-only successor makes the literal first-line format explicit and increments the Kimi definition to V5; a later passing response does not establish reliability or alter the original failure.
The first current Daytona worker-loss attempt failed inside its one-shot fault observer. Its original diagnostic erased the failing guard and cannot establish whether a signal occurred. A separate no-network Linux x64 control used the immutable image, actual Kimi and compiled ACPX, and a simulated model. Python 3.13.5 process-handle signaling and all fourteen Linux selector controls passed. The real pending Write was reached, then inspection failed with a missing-file error before any fault signal. Normal Stop retired all seven owned processes, and both controllers independently verified the sandbox absent before the temporary key was revoked. This is diagnostic evidence, not authenticated task acceptance. A second inspect-only control located the exact read: the selector resolved the authenticated parent executable to a deleted Linux memfd alias, then tried to reopen that name instead of its held descriptor. It failed before signaling. Its normal Stop retired all seven processes, and sandbox deletion and key revocation were independently verified. The descriptor correction then passed a same-process Linux comparison: the old selector reproduced that exact error, the corrected selector verified the held executable and its kernel seals, and the unchanged process-handle operation killed only the owned native Kimi child. The unanswered permission expired with `provider_process_lost` and replay disabled; a correctly scoped stale approval failed, no file effect or second model request occurred, and all seven owned processes retired. Sandbox deletion and temporary-key revocation passed independently. All model responses in these controls were simulated. Fresh authenticated worker-loss acceptance remains required. The existing TypeScript wrapper still expected fourteen Python tests after the suite grew to twenty-five. It reproduced that count failure, then passed all four checks after its expected count was updated. This correction changes test accounting only.
Pending Kimi11 and Copilot83 are frozen after the actual lock-refresh prerequisite
was merged. The prerequisite's squash commit has the same complete tree as the
reviewed source. Genuine merges preserved all published identity fixtures and
the managed lockfile; no history-only reconciliation or policy bypass was used.
Fresh pending11 Mac native controls passed. The Linux qualification image was
built, published under an immutable digest, and independently checked against all
42 exported layers, native executable hashes, and installed metadata readers.
These are delivery prerequisites, not the twenty authenticated Product E2E cells.
The first pending11 Daytona Stop attempt reached real inference, one normal UI
Allow once for the scoped empty-input context read, and the exact intended native
Write permission. It failed before submitting Stop. The fixture incorrectly
equated the native turn with the controller envelope turn and tried to parse a
bounded display event as the full tool result. The official encrypted checkpoint
retained the full matching result and its completed delivery acknowledgment.
The run then reached its configured 120-second timeout. Recovery later overwrote
that error after mistaking the remote numeric PID for an unrelated live process
on the controller host. Commit `a24ebd5e7e3fa0d086e91cd3824efd52c8eaee0b`
requires exact current host-authored local process identity before local probes
or clearing. Remote or unknown identities stay under native recovery authority.
Real PostgreSQL tests reproduce both remote collision states, preserve verified
local live/dead behavior, and retain authenticated remote termination handling.
The corrected fixture passed 62 deterministic controls, targeted typecheck,
and the real MCP/sidecar/Core-to-official-PostgreSQL boundary with seven
negative scope/session controls. It positively bridges native and controller
turns through the exact authenticated request and delivered command. It binds
the logical workspace separately from the physical observer lease and retains
the original calibration fixture as version1 beside the new version2 fixture.
The current suite is version4 and remains explicitly pending.
The next controlled attempt, on `23958e5ad07e0b93b8eef098a9d0038c215bad15`,
passed the full scoped context checkpoint proof and reached the native Write.
It then failed before Stop because the UI check counted all visible request
cards: the resolved context request and the pending Write. Its original
failed/cleanup-failure grade, 49 raw files and 65 executable snapshots remain
unchanged. Separate cleanup again verified all owned cloud resources absent and
revoked the temporary key. This is a second failed attempt, not Stop acceptance.
The corrected selector binds the exact target card to the independently verified
native pending callback before and after lookup. It retains resolved history and
the same card after cancellation or expiry. Ten Chromium controls reproduce the
old failure and reject wrong targets, duplicates, changed callback identity and
disabled decisions. The existing native/context controls and targeted typecheck
pass; suite version4 and its promised behavior are unchanged.
Fresh CI also exposed intended-local test fixtures that inserted bare PIDs without
using the real process-metadata writer. Those fixtures now use that writer and
retain their original recovery, contract and concurrency assertions. The separate
unknown-owner test kept its original assertion and exposed a real guard-order
regression: an observed run could lose its ownership warning. The minimal repair
restores the existing observed-owner warning before the unknown-ID hold, without
probing host PIDs or granting clearing, signaling or retry authority. Both unknown
and remote observed-owner controls pass, as do all 65 recovery-module controls and
the original assembled recovery cases. Real remote timeouts retain their errors.
The original failed/cleanup-failure verdict and raw reports remain unchanged.
Separate cleanup verified every owned sandbox absent and revoked the temporary
test key. The fixture's later close error did not establish its retirement seal;
no cleanup check is relaxed. The generic ACPX display clipping metadata defect
is pre-existing and remains a documented limitation; full model-facing delivery
uses a separate acknowledged path.
The repair changes controller recovery and qualification fixtures. It does not
change the Kimi native patch, either closure, the tested Mac binaries, or the
Linux image inputs. A new matrix revision must record the exact reviewed delta
and rebuilt server identity while retaining the original artifact build SHAs.
The next paid action is one corrected Daytona Stop cell after the rebuilt controller and
strict checkpoint/card admission controls pass. Its original grade, raw files and physical cleanup must
pass independent audit before the remaining nine remote and ten local cells.
After all twenty cells pass, promote ordinary admission in a separate reviewable
qualification PR, build the normal four-platform daemon/public-package set, and
verify the installed local UI. Then deploy the exact qualified revision only to
the user-approved isolated `kimi.test.paperclip.app` test workspace and drive the
ordinary hosted configuration, assigned-skill, artifact and persistence journey.
Production, fleet defaults, stable publication and GitHub merge remain outside
this handoff.
## Reconciliation and earlier qualification history
The following records describe earlier candidate states and their original
evidence. The current state above supersedes their next-step and blocker text.
The next shipping prerequisite is now frozen at the existing lock-refresh PR
[#18185](https://github.com/paperclipai/paperclip/pull/18185), head
`9600623783d118a79f8008deb9ab05d80715a15a`. Reconciliation is in progress;
the earlier master freeze below remains historical. The trusted repository
policy permits lock changes only through its maintained refresh branch or
Dependabot. This existing prerequisite contains the exact thirteen-line lock
correction already used by the candidate, with matching reviewed report proof.
It also inherits three newer master changes, including native finalization and
the Antigravity builder. Actual foundation merge conflicts make a genuine
reconciliation necessary. The new behavior and published identity bytes must
survive; no policy exception, mixed-branch rename, or history-only merge is used.
The builder is one of Kimi's 31 attested source files, so that reconciliation
requires a new pending identity, expected version11, and a later qualified
identity, expected version12. No full paid pending10 cell was started. Its
successful Linux build, zero-prompt native create/load proof, complete exported
image integrity checks, and controlled Mac native fence remain evidence for
pending10 only. The exported image is retained without ECR publication. A fresh
build and all twenty live cells will bind the reconciled candidate. Later
unrelated master activity alone does not reopen this prerequisite freeze.
The revised remote fixture also passed the actual maintained MCP-to-checkpoint
boundary and an isolated real PostgreSQL observer check. That check used the
official encrypted checkpoint loader in a read-only transaction, verified the
saved checkpoint remained unchanged, and closed the observer before stopping
its owned database. The integrated observer's bytes match the calibrated source.
These controls are prerequisites for the first live native Stop case; they do
not establish authenticated model behavior or full recovery acceptance.
The shipping baseline is now frozen at master `9f3fc2094f61e311b2c5adb4d5beccefc0199943`.
The reconciled foundation is `7c6cffafa26c70e50c241c71dd536aa79406041d` and
product code is `60f0bb7b3171e773f21fe80fb524d9130e36500f`, with 97 and 77
review paths respectively. Both existing PR branches were updated by fast-forward.
Published master identity bytes win over conflicting unmerged variants; original
candidate variants remain in preserved Git history and their evidence receipts.
Current Kimi10 is pending and hidden from ordinary selection, command digest
`sha256:c8a39af9d580cdb5fe20a3333a566e2f97a1b4ec3a0025cb647d2144df358604`.
The native2.3.3 patch v6 and vendor closures are unchanged. Current dependent
identities are Pi35, Copilot82 and Grok14. Earlier prepared qualified10 and
Copilot82 digests and the `9ba72696` draft image identity are obsolete and must
not be reused. Qualification will promote a new identity only after live gates.
Master adds reachable notification context/helper environment and Unix Runner
FIFO/listener/thread behavior, plus connection/restart ownership fixes. This
changes actual Kimi and native Bash execution, beyond identity text. All twenty
local/Daytona cells will therefore refresh against pending10, preceded by actual
native credential/context controls. Old9 grades and native binaries are not
relabeled as pending10 or as the final release.
The pending10 image build is pinned to product `60f0bb7b`, canonical candidate
content `af2d4e8d8ea38381eb265dfdf8141d23ca31369bcb7ae12db8c6daa1b34a4e15`.
Its source was verified through an anonymous public checkout; no operator or
provider credential is sent to the build. Later test-only changes require a
canonical image-input comparison, not a rewritten image provenance claim.
The foundation PR #18028 at `08a5516b` and product PR #18029 at `6123a67a`
had green CI at those heads. The product review was Greptile 5/5 with one
actionable report configuration load-order comment. That comment is addressed
in the subsequent harness repair; fresh-head CI/review remain required. Both
PRs remain draft because real acceptance is incomplete. Full typecheck and build passed. The
local full test command failed three AI-connection cases after a first 15-second
timeout, then accumulated an idle-transaction/TRUNCATE wait chain in the unchanged
Git-expiration suite. The three connection cases passed separately without
source changes. The full command was interrupted after recording the lock chain;
all owned processes and its database port were independently verified stopped.
This is not a full local test pass or an established explanation of every failure.
Recovery and re-evidence on 2026-10-29:
- The previous checkout and its worktree registration disappeared. The cause is
unknown. Pushed source was restored into a newly attached managed worktree;
frozen dependency installation and the full build passed again. Historical
local pass logs and all ten audited result hashes survived, but the original
raw report packages did not. Those passes are not reconstructed from logs or
synthetic calibration evidence.
- Commit `6123a67a` adds an explicit absolute report-root override with unchanged
default storage, physical output containment, and traversal/symlink rejection.
Actual final publication was checked across checkout removal, early failure,
redaction, and missing mandatory evidence. The repair passed 123 focused
evidence tests, seven sentinel tests, harness typecheck, actual CLI admission
checks, and independent outside-sentinel preservation. These are harness
controls, not provider acceptance.
- All ten fresh local profile9 cells passed at exact `6123a67a`, including final
packaging and cleanup. Their original packages now live outside the checkout.
Independent collection rehashed all 221 files without mismatch; each owned
server and embedded database stopped and each temporary data root was absent.
The 19 retained run records are not a model-call count.
- An independent temporary Cloud TEST credential reached the global Kimi endpoint
and the exact installed Linux9 runtime passed native startup. The first
controller attempt then failed before any test body: a Promise resolver
shadowed the path resolver during event validation. The original failed and
cleanup-failed grade remains. Credential-free actual-event replay reproduced
the error and verified the correction. Empty JUnit suites, retained Playwright
events, and source readiness gates establish that product fixture allocation
and provider admission were never reached. Owned processes/ports were absent;
the independent preflight sandbox was deleted and checked404, and the temporary
credential was revoked and checked absent.
- The corrected controller reached the actual first Daytona cell. That attempt
failed closed at `remote_native_fixture:lease_metadata`: its fixture selected a
reusable warm environment, while the native lifecycle observer requires an
isolated non-reusable lease. The observer guard is retained. Both observed
owned sandboxes were separately deleted and checked404; its temporary credential
was revoked and checked absent. The original failed and cleanup-failed grade
remains; model usage is unavailable. A narrow fixture-producer correction and
fresh Daytona acceptance are required before qualification.
The follow-up harness repair at `d5fc5c42` changes only the two remote native
lifecycle fixture configurations to isolated non-reusable leases. The eight
other Daytona cases remain warm and all ten local execution inputs are
unchanged. This per-case override is included in the suite fingerprint. The real
fixture-producer regression first failed both lifecycle cases, then passed with
three unchanged controls. The strict remote observer is unchanged. The same
repair loads the normal campaign report setting from its local environment file
after admission, with a credential-free actual-launch red/green regression;
listing and installed-startup admission retain their prior behavior. Focused
catalog/producer/observer/storage checks passed 227 tests with one existing skip,
and harness typecheck passed. Native profile9, vendor closures and image inputs
are unchanged; retain the fresh local evidence at `6123a67a` and pin a fresh
Daytona campaign to the repaired source rather than relabeling old attempts.
Additional profile9 Daytona first divergences and shipping reconciliation:
- Restoring source did not restore its ignored Linux companion manifest/binary.
The next run failed closed before launch. Exact image9 assets were recovered
under a Linux-only release descriptor with their original source `5f4c9edc`;
no other platform binary was invented or relabeled. Actual source, compiled
Runner and compiled server-vendor readers passed strict manifest/profile/
closure/binary checks, while the Mac daemon remained unchanged. This proves
the scoped controller asset recovery, not a complete four-platform release.
- After those assets were restored, a real Kimi turn requested native Read for
the bootstrap file. Its canonical request had no proven file target and was
never approved. The lifecycle case timed out and failed session recovery;
the original failed/cleanup-failed verdict is retained. All observed owned
sandboxes and temporary credentials were separately retired and verified.
- A bounded real pinned-native control established that approve-reads delegates
these provider requests; it does not automatically approve native Read.
The proposed replacement uses server-owned get_task_context, whose empty-input
schema and active-task authority are independently checked, with one explicit
fixture-human approval after observer baseline and continuation publication.
Unknown native Read is still unapproved and unqualified. Native Write must
remain genuinely pending before Stop/loss. Synthetic proof rows alone do not
establish delivery: production boundary calibration identified semantic input
in the durable checkpoint and a delivered result command, rather than a
semantic result event in the public run log. The checkpoint observer and full
fresh runtime acceptance remain gates before using this replacement.
- The master merge preserved sealed private-handoff custody ahead of ordinary
report-root configuration, including after dotenv. Actual private/public
launch regressions passed 57 tests and a wrong-root mutation failed all four
relevant cases. A missing upstream tar-stream lock importer was repaired by
the canonical pinned package manager: exactly thirteen added lines, with no
unrelated resolution churn. Frozen installation and Runner TypeScript build
passed. Combined harness checks passed 325 tests with one existing skip, plus
137 Node tests and six Rust notification controls with one existing opt-in
ignore. Full repository checks and current-head review remain release gates.
- All ten fresh local9 report packages remain outside the checkout, and the
explicitly approved 14.8 MB private backup was verified by full checksum,
byte count and encryption read-back. Future raw payloads have no implied
upload authorization from that one archive approval.
Material local attempts remain distinct:
- The first profile9 Stop attempt failed its outdated final UI oracle. Inspection
also found duplicated partial assistant text; the provider-neutral consumer now
respects the existing nested progress marker. Its retained event regression
demonstrated red then green.
- A later Stop passed all behavior/cleanup checks but failed report packaging:
custom screenshot filenames did not satisfy the existing `final-state.png`
contract. Two capture names were corrected, with positive and missing-final
packager checks. The original grade remains failed.
- One replacement failed before provider admission because two of its three
credential-home quorum ports belonged to unrelated listeners dating from
October 4 and 21. The guard correctly refused ownership. Foreign processes
were untouched, and separate scoped cleanup verified our processes retired.
- The next local Stop passed behavior, report evidence and physical cleanup in
33.8 seconds. The following provider-loss attempt reached an unanswered Write
but refused fault injection: macOS `ps` renders loader bytes differently from
the pinned argv. An unpaid real guarded-native probe reproduced that exact
hash divergence. The corrected observer reads argc-bounded argv with those exact byte and
ownership checks retained; the subsequent real loss cell passed.
Profile9 distribution proof now covers eighteen actual public-layout tarballs,
a clean scripts-disabled consumer, fresh installed CLI setup through the admitted
Node/npm proxy, network-denied reuse, unchanged lockfile and installed native ACP
create/load/model/EOF. No developer login home, refresh token, private database
override or model prompt was used for that packaging proof.
The native Linux image was built from frozen source `5f4c9edc`; later source
changes affect UI and test observers, with image inputs unchanged. An initial
export timed out after image construction and saved no smoke proof. The corrected
job `14af59a4-93be-4f83-990e-e308625ab1cc` completed in 9m40s with timestamped
stages, independently saved native smoke proof and a streaming compressed export.
Image ID is `sha256:139869415cdd7c7cd2a59f21626b8bdbbe0088f17209abf8cdda4f6a80b99718`;
content identity is `4560ebf62a9640e43aff893b1a6dd497746eeb2787732818b62acd911cdb0b7b`.
Create/load instruction acknowledgments and bounded EOF passed without prompts or
credentials. Local collection verified the full archive, source, profile,
closure, pack and daemon hashes. Its published immutable reference is
`public.ecr.aws/q2k8x0v0/paperclip/kimi-qualification-eed3@sha256:bad9994dadf1a55743dabed3cd3f785210131938d4407c8b0ca92a30a922c177`.
A real credential-free Daytona import/native smoke passed in 122.4 seconds on
Linux x64 with 4 CPU, 4 GiB memory and 10 GiB disk, then its exact sandbox was
deleted and checked404. It verified native create/load/model/instruction/EOF
boundaries without a model prompt. No authenticated Daytona task has passed. All
build failures are retained; the rejected IAM expansion
was never applied and no operator/provider token was sent to CodeBuild.
The user additionally requested hosted verification and confirmed an isolated
workspace at `https://kimi.test.paperclip.app`. It was created through the normal
Cloud UI under the existing authorized owner; the shared QA identity lease was
left untouched. It is active and empty on verified Core
`f8484a9f8d1f68e9534fee4f29e00e716dc680f4`, distribution `cloud-40777791731-1`,
not the Kimi build. A singleton TEST campaign enabled Native Runner on only this
workspace with matching applied/desired hashes; fleet defaults stayed unchanged.
Use the maintained Core preview deployment for this unmerged branch, record its
exact serving SHA/image, and qualify the Kimi user journey there. This does not
certify the private Cloud UI composition. No production or fleet-default change
is in scope.
The active remote blocker is now narrowed by independent controls. The same fresh
selected credential verifies through the host catalog path, while the actual
remote metadata helper returns `KIMI_PROVIDER_ERROR`. Installed helper/Node bytes,
profile admission and key/model forwarding match the expected boundary. A fixed
remote GET failed with `ECONNRESET` before HTTP, with sandbox blocking false and
no allowlists. Credential-free controls also reset for example.com, including
direct IPv4 curl, with no proxy variables present. All three diagnostic sandboxes
were destroyed and checked404. No TLS protection was weakened, endpoint switched
or inference attempted during those diagnostics.
[Daytona's current network policy](https://www.daytona.io/docs/en/network-limits/)
says Tier1/2 organization restrictions override sandbox settings. Its essential
service list includes `*.kimi.com` and `*.moonshot.ai`, but not the selected
account's official global `api.kimi.ai` endpoint. This is consistent with the
observations, not a confirmed cause: this key cannot read its organization policy
(HTTP401). It advertises only the public US target. The user was asked to confirm
full egress or another suitable test credential, while the existing TEST Cloud
provider setup is being checked. The global account must not be routed to the
separate CN endpoint as a workaround.
Critical path: establish legitimate Daytona egress to the global Kimi endpoint,
then run ten authenticated Daytona cells. Only after native qualification should
a new immutable profile promote ordinary admission/default packaging, with final
installed artifact/image checks and ordinary setup/assigned skills. Deploy the
exact candidate to the isolated test workspace, complete its hosted journey, and
finish the PR stack. A promotion patch has been prepared and tested only in TEMP;
it is not applied and does not count as qualification.
## Evidence and first divergences
- Frozen root dependency installation passed with the normal network-enabled
package manager. Restricted-network signature fetch failures were retained;
no signature gate was disabled.
- Runner TypeScript/Rust typecheck passed before final sidecar registration edits.
Focused Runner contracts: 297 tests passed. Product setup/admission, token,
catalog, and route checks have separate passing results; these are not live proof.
- Both locked runtime closures built in 8.4 seconds with 33 required npm packages.
macOS arm64 installed native metadata and Linux x64 metadata under emulation
passed create/model/load/EOF; Linux emulation is not Daytona qualification.
- Real bundled ACPX manager admitted exact `kimi-for-coding` and current bound
instructions with physical cleanup. The first two attempted turns failed before
semantic tool delivery. The second retained the native 403: account subscription
lacked Kimi Code entitlement. Native CLI login succeeded, and the user then
subscribed. Those failures remain failures; no cost counters were available.
- The first entitled turn completed native execution and called the exact MCP
echo with the instruction-only nonce. Its smoke observer incorrectly expected
`paperclip_echo` at a handler that receives canonical `echo`. Native arguments,
MCP receiver error, and terminal completion establish a grading/observer failure,
not a successful tool round trip. The corrected observer then passed a real
authenticated semantic echo, native terminal completion, and physical cleanup.
Its profile2 evidence is retained separately from the final candidate.
- Review found and corrected a real normal-path gap: the packaged sidecar's
handwritten agent parser omitted Kimi even though direct driver metadata passed.
Final sidecar checks and real Rust/sidecar product acceptance remain required.
- Native controlled-model probes use the actual pinned Kimi and bundled ACPX.
They found upstream approval construction omitted `toolInput`; the owned patch
now carries exact nested and UTF-8 arguments, including empty objects. Denied
Write caused no file effect, and native HTTP418 remained a typed failed turn.
- The next probe found provider credentials inherited into native Bash. The
patch filters `KIMI_API_KEY` and private instruction transport from native
subprocesses while preserving explicitly assigned task variables. Actual ACPX
always advertises terminal capability, which routed Bash through its own host;
the final provider patch forces the owned filtered native process path instead.
Final verification must observe zero `terminal/create` requests and no secret
in the actual child, rather than merely inspecting a filtered object.
- Bundling exposed a `Function.toString()` source difference that changed patched
native bytes. The patch now embeds one canonical static helper source; raw and
archived CJS setup produce the same pinned output. Final source comment removal
changes its byte identity again before profile4 freeze; old evidence keeps its
original closure and profile identity.
- Required repository checks remaining: final full typecheck, supported full test
run with the original infrastructure failure retained, full build, and the
ordinary setup/save/reopen/Test/run/reconnect path. Token gates and map structure
currently pass. Setup Test now uses the official authenticated model catalog;
it does not claim inference, quota, or task execution from a metadata response.
The full repository build passed on candidate profile5. Its first Product E2E
message-marker attempt reached real UI/server/database setup, then stayed paused:
the installed-runtime-only Test warning is rejected by the ordinary agent lifecycle.
No model turn started. This candidate failure is preserved separately from the
retry. The correction performs a fixed-endpoint authenticated model-catalog probe
before reporting a successful Test. A real catalog request accepted the credential
and returned exact `kimi-for-coding`; the official `KimiCLI/2.3.3` client header
is required (a generic client was rejected with HTTP403). The selected ID is never
substituted. The first release also disables native question and plan tools so
only the promised semantic interaction path remains available.
Historical profile8 command digest:
`sha256:53c63af8722894e14d48ed55526f99fcd79e06c86ae1c7aa384ef7ee8c56efd4`.
Native runtime patch v6 SHA256:
`79980aa95c54f325a05cbdf883f74443705db28f3e412a1b5ae22929d939eb66`.
Closure pins: macOS arm64 `6038325abd0431c30097ff1f572a9b29816e1b4da372d8cb86b2ca5d18ea34e7`,
Linux x64 `105d8394adbbc76a953a12cc51b5288eb940ad693291e584f6cdc6407213164d`.
The actual native Kimi plus bundled ACPX controlled-service probes passed seven
requests for instructions/arguments/auth fencing/denial/failure, then nine for
pending-permission Stop and spontaneous owned native-worker loss. Both pending
callbacks aborted, stale approval caused no response/write/new model request,
and workers physically retired within five seconds. The model service was simulated;
these results do not establish authenticated product acceptance.
To retain native macOS Runner acceptance without changing shared IPC state, an
official checksummed PostgreSQL 18.6 build with named POSIX semaphores passed fresh
initialization, normal migrations, SQL empty-database checks, and cleanup. Only
this worktree's unshared native dependency entry was temporarily pointed at that
private build. It was restored before canonical public-package staging. The
accepted database process/version must be disclosed with each affected result;
no database fixture contract or tracked dependency file was changed.
Early campaign cost record: subscription model calls have no reliable native
usage counters; cost is unknown. At that point one cloud job was stopped before
building and no image or Daytona sandbox had been created. Original
403s, the grading defect, credential-boundary failures, and database setup failure
remain separate failed or blocked attempts; none are overwritten by retries.
The Code endpoint for this finite release is the global coding API
`https://api.kimi.ai/coding/v1`, matching the successfully restored account.
Mainland-region routing and managed OAuth refresh are outside this release.
Authenticated qualification binds only the existing login's access token to the
isolated run; refresh tokens and the ambient login home are not transferred.
The second local message-marker attempt reached the real model and tools. Run
`4d394aef-0e6a-49a8-9fb0-873651d0241f` finished in 24 seconds, accepted the finish
contract, saved task Done, and passed physical cleanup. Its original verdict is
failed: progress text and final text shared one provider item, so the visible
final bubble contained both. Native patch v5 now forwards actual turn-step UUID
boundaries. The Kimi projection validates their provenance and order before
passing generic assistant boundaries to Runner. It does not infer boundaries
from text, tools, or HTTP response identifiers. The actual native Kimi/ACPX
controlled peer passed this framing case alongside instructions, tool arguments,
credential isolation, denial, typed failure, Stop, and native-worker loss.
Historical profile7 verification: 162 sidecar/projection/metadata/instruction tests,
144 driver/projection tests, and 18 Rust provider-state tests passed. TypeScript,
Rust daemon staging, bundled entrypoints, and all three affected immutable
profile checks passed. The archived installer passed fresh public downloads,
offline reuse, tamper rejection and cancellation on profile7. Final installed
CLI/server tarballs remain to be rebuilt and checked. The full repository test
run continues against the owned migrated PostgreSQL 18.6 Docker database; the
earlier host semaphore and unmigrated-database setup failures remain retained.
The third local message-marker attempt passed all six independent matchers,
accepted finish, durable Done, and cleanup on profile7/source
`5fe7d496e1603626ceb5838f37c4fecce23580ee`. Campaign
`local-2026-10-28T14-24-09-928Z`, run
`68705f91-d195-4532-9fd4-e8b1826d6ed9`, completed in 45.1 seconds including
cleanup and evidence collection. Its rendered final reply is the exact marker.
The profile7 installed CLI/server tarballs also passed fresh public setup,
offline reuse, exact native create/model/load/instruction receipts and EOF.
Lifecycle authoring then found that native Kimi approval omitted its tool kind.
Patch v6 carries the native tool's existing canonical kind. The actual native
callback now passes shared normalization: exact Write target with Allow once,
Deny and Cancel; missing/foreign workspace contexts offer only Deny/Cancel and
reject acceptance. The complete nine-request controlled native peer passed.
The engine metadata correction and v6 change advance the installed closures.
Profile7 evidence retains its original identity; the affected permission and
installed-runtime paths require profile8 evidence before qualification.
CI found stale dependent provider attestations and fixture expectations. Pi31
attests the changed shared environment/sandbox sources without changing its native
closure. Copilot73 attests historical-profile decoding changes; Grok11 remains
current. Old fixtures stay immutable. Ninety-five Pi/Copilot installation tests
passed with one existing opt-in skip, plus 18 image contract tests and four Grok
identity-generator tests. Kimi lifecycle authoring passed 44 deterministic cases;
the exact Linux pidfd selector passed 14 real cases on native ARM64 Linux and 110
TypeScript observer/contract tests with one existing skip. Linux x64 emulation
returned ENOSYS for pidfd and is retained as an unsuccessful infrastructure
attempt; only actual Daytona Linux x64 can qualify that target.
The rebased profile7 full typecheck/build passed and profile8 full build passed
before the final dependent-provider identity refresh. The local full-test run
was interrupted after unrelated shared-database cleanup failures and stale
migration module errors. A fresh current-build isolated PostgreSQL migration
probe passed. A replacement full run and final checks remain required. Later
focused HTTP tests hit import/hook timeouts while host load was 63/120 with 92%
CPU use; their original failures remain retained, without committed deadline
increases. CI for the corrected source will provide an independent Linux check.
Review branches are published as draft PRs [Runner foundation #18028](https://github.com/paperclipai/paperclip/pull/18028)
and [product setup #18029](https://github.com/paperclipai/paperclip/pull/18029).
The first foundation review was 5/5 with no comments on its exact prior head;
the corrected final head requires a fresh review. Neither PR is ready for merge
until required product acceptance and checks pass.
### 2026-10-30: native behavior and cleanup recovery accepted
The profile 16 candidate (`e027eb4e…`, SDK 2.3.3, native artifact source
`603756c22b2e39de9b5b5c3c26a0eb23c67604a1`) has evidence for ten distinct
local behaviors and ten distinct Daytona behaviors. These are functional
acceptances, not twenty originally green attempts: local Ask retains its first
failure; the Daytona permission case retains an accounting-observer failure;
the final provider-loss case retains its original cleanup-failure grade.
Fixture/Core sources for the remote evidence are `8bc5ce3b…` and `e7f65045…`;
the fixture-only rebuild changed only build-info among 10,993 compiled files.
The final Daytona case delivered loss to the exact owned native child while
Write approval was pending. Its six matchers passed, including no file effect,
expiry of the original callback, rejection of stale Allow, one failed native
run, blocked task state, and complete native process retirement. Its workspace
recovery completed and confirmed stop, then deliberately queued sandbox
destruction for five minutes later. The test's 120-second environment-delete
wait ended before that ordinary cleanup deadline.
A separate recovery restored the same captured control-plane state to an
isolated working copy and used the normal server, company-secret rotation,
cleanup scheduler, and environment DELETE route. Cleanup became successful,
DELETE returned its documented HTTP 200, and the environment was absent from
the database. No model turn, new run, new lease, or direct domain SQL update was
used for recovery. Both the original and recovery processes, ports, provider
resources, and temporary keys were closed. Root's combined acceptance record
is SHA256 `25d0eae952eab91c6d54abb6850d4abf1b69f1045e7d7bb05333f8b87c184c0a`;
it preserves the original failed grades and distinguishes native proof from
later normal cleanup recovery.
The readiness correction waits for the same agent before changing permission
policy, avoiding invalidation of an in-flight verification. Setup and native
work now have separate finite windows, and remote deadline diagnostics identify
the operation without retaining SDK text. Canonical focused verification passed
223 tests with one existing Linux-only skip on Mac, plus the full fixture
TypeScript check. Native runtime bytes and model/callback limits were unchanged.
Profile 17 public-package installation and the actual hosted walkthrough on
`kimi.test.paperclip.app` remain separate delivery gates; this section does not
claim either has passed.
The cleanup fixture now uses one absolute Kimi deadline at the original test
start plus 660 seconds, within the unchanged 690-second total limit. It leaves
30 seconds for final evidence. Each DELETE request and retry sleep uses only
the remaining time. Other flows retain their 120-second default. The actual
canonical fixture tests passed 73 assertions across three files, including
ordinary deferred cleanup beyond 120 seconds, deadline exhaustion, request
timeout forwarding, and HTTP success versus pending cleanup. This source
correction does not regrade the preserved failed live attempt.
+13
View File
@@ -77,6 +77,19 @@ qualified selection, not successful authentication or task execution. Native
Claude retains existing account grants and company/responsible-user boundaries.
Source: [adapter discovery](../../server/src/adapters/registry.ts).
Kimi Code has a qualified native ACPX profile on Mac ARM64 and Linux x64. It
requires an explicit model and a company-secret `KIMI_API_KEY` binding, and uses
the global Code API. Ordinary selection, setup and Runtime Test require no
candidate grant. Runtime Test queries the authenticated Code model catalog and
sends no model prompt. Future pending profiles retain exact trusted startup
admission; saved agent configuration and public requests cannot supply authority.
Inactive setup drafts can precede model entry; Test and hire still require it.
The native model route returns no suggestions and accepts typed identifiers.
Managed Kimi account connections, ambient OAuth borrowing, and automatic legacy
migration are outside this release. Source/control evidence only; see the
[Kimi release ledger](../../doc/plans/2026-10-28-kimi-code-runner-integration.md)
for the separately recorded runtime and product results.
Gaps: Raw instance adapter listing requires board, while company model discovery exists
for eligible agents.
+18
View File
@@ -375,3 +375,21 @@ cleanup before hire and retains its unconfirmed recovery state.
### `delegated-configuration`
See the canonical [delegated configuration guidance and acceptance evidence](connection-setup.md#delegated-configuration).
### Kimi Code verification
The qualified native Kimi profile uses `paperclipai runtime setup kimi` on Mac
ARM64 or Linux x64, an exact typed model, and a `KIMI_API_KEY` company secret.
Ordinary setup, hiring and Runtime Test require no candidate grant. Runtime Test
queries the authenticated Code model catalog without inference. Existing legacy
Kimi agents keep their saved runtime. Future pending profiles retain protected
startup admission; saved agent configuration and public requests cannot grant it.
Use the explicit `kimi-qualification` Product E2E suite for local and Daytona
reply, semantic question/plan, file/download, Stop, and continuation cases.
Verify the actual task result, persisted state and process cleanup. Run the
ordinary New Agent draft → model/secret → Test → save/reopen → task journey
through ordinary released selection. Native filesystem/shell tools are denied in read-only
Kimi work; scoped Paperclip tools retain control-plane authorization. This recipe
is a test path, not a statement that live acceptance passed. Current evidence:
[Kimi release ledger](../../doc/plans/2026-10-28-kimi-code-runner-integration.md).
@@ -12,9 +12,9 @@ import {
} from "./paperclip-runner-permissions.js";
describe("Paperclip Runner permission defaults", () => {
it("keeps Kimi Code pending with an explicit runtime credential", () => {
it("exposes qualified Kimi Code with an explicit runtime credential", () => {
expect(PAPERCLIP_RUNNER_ACPX_PROFILES.find(profile => profile.value === "kimi"))
.toMatchObject({ qualified: false, credentialEnvironment: ["KIMI_API_KEY"] });
.toMatchObject({ qualified: true, credentialEnvironment: ["KIMI_API_KEY"] });
});
it("marks Pi and Copilot qualified with their provider credential bindings", () => {
expect(PAPERCLIP_RUNNER_ACPX_PROFILES.find(profile => profile.value === "pi"))
@@ -253,7 +253,7 @@ export const PAPERCLIP_RUNNER_ACPX_PROFILES = Object.freeze([
{ value: "cursor", label: "Cursor", qualified: true, credentialEnvironment: ["CURSOR_API_KEY", "CURSOR_AUTH_TOKEN"] },
{ value: "copilot", label: "GitHub Copilot", qualified: true, credentialEnvironment: ["COPILOT_GITHUB_TOKEN"] },
{ value: "pi", label: "Pi", qualified: true, credentialEnvironment: ["OPENROUTER_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GEMINI_API_KEY"] },
{ value: "kimi", label: "Kimi Code", qualified: false, credentialEnvironment: ["KIMI_API_KEY"] },
{ value: "kimi", label: "Kimi Code", qualified: true, credentialEnvironment: ["KIMI_API_KEY"] },
{ value: "antigravity", label: "Google Antigravity", qualified: false, credentialEnvironment: ["GEMINI_API_KEY"] },
] as const);
@@ -50,6 +50,21 @@ it("keeps Pi draft models empty and preserves saved native Pi settings", () => {
expect(normalizePaperclipRunnerAdapterConfig("paperclip_runner", saved)).toMatchObject(saved);
});
it.each([undefined, "", "kimi-for-coding"])("keeps Kimi draft model selection explicit: %s", model => {
const config = { provider: "acpx", acpxAgent: "kimi", ...(model === undefined ? {} : { model }),
acpxPermissionMode: "approve-reads", lifecycleMode: "warm" };
const normalized = normalizePaperclipRunnerAdapterConfig("paperclip_runner", config);
expect(normalized).toMatchObject(config);
expect(normalized.model).toBe(model);
});
it("keeps Claude draft defaults and explicit model selection", () => {
expect(normalizePaperclipRunnerAdapterConfig("paperclip_runner", { provider: "acpx", acpxAgent: "claude" }))
.toMatchObject({ model: "claude-opus-5-5" });
expect(normalizePaperclipRunnerAdapterConfig("paperclip_runner", { provider: "acpx", acpxAgent: "claude", model: "claude-sonnet-5" }))
.toMatchObject({ model: "claude-sonnet-5" });
});
it("reserves identity credentials even for mixed-case inherited or configured keys", () => {
const keys = ["PAPERCLIP_AGENT_KEY_ID", "Paperclip_Agent_Public_Key", "paperclip_agent_private_key"];
const inherited = Object.fromEntries(keys.map(key => [key, "host-override"]));
+3 -1
View File
@@ -4299,7 +4299,9 @@ export function normalizePaperclipRunnerAdapterConfig(
}
if (next.provider === "acpx") {
next.acpxAgent ??= "claude";
if (next.acpxAgent !== "pi") next.model = resolvePaperclipRunnerModel("acpx", config.model);
if (next.acpxAgent !== "pi" && next.acpxAgent !== "kimi") {
next.model = resolvePaperclipRunnerModel("acpx", config.model);
}
}
return normalizePaperclipOperationalSkillPreference(adapterType, next);
}
@@ -36,6 +36,14 @@ function makeValues(overrides: Partial<CreateConfigValues> = {}): CreateConfigVa
}
describe("buildCodexLocalConfig", () => {
it("round-trips qualified Kimi Code with its exact selected model", () => {
const first = buildPaperclipRunnerConfig(makeValues({ model: "explicit-kimi-model", adapterSchemaValues: { provider: "acpx", acpxAgent: "kimi" } }));
const reopened = buildPaperclipRunnerConfig(makeValues({ model: "", adapterSchemaValues: JSON.parse(JSON.stringify(first)) }));
expect(reopened).toMatchObject({ provider: "acpx", acpxAgent: "kimi", model: "explicit-kimi-model" });
expect(first).not.toHaveProperty("candidateHarnesses");
expect(() => buildPaperclipRunnerConfig(makeValues({ model: "", adapterSchemaValues: { provider: "acpx", acpxAgent: "kimi" } })))
.toThrow("kimi requires an explicit provider model");
});
it("refuses pending Antigravity selection before saving Runner config", () => {
expect(() => buildPaperclipRunnerConfig(makeValues({ model: "exact-antigravity-model",
adapterSchemaValues: { provider: "acpx", acpxAgent: "antigravity" } })))
@@ -131,7 +131,7 @@ export function buildPaperclipRunnerConfig(v: CreateConfigValues, context?: {
if (provider === "acpx" && acpxAgent === "copilot" && (!configuredModel && !schemaModel || ["auto", "default"].includes((configuredModel || schemaModel).toLowerCase()))) {
throw new Error("Select an available Copilot model before saving.");
}
if (provider === "acpx" && ["cursor", "pi"].includes(acpxAgent) && !configuredModel && !schemaModel) {
if (provider === "acpx" && ["cursor", "pi", "kimi"].includes(acpxAgent) && !configuredModel && !schemaModel) {
throw new Error(`${acpxAgent} requires an explicit provider model`);
}
const managedProfileId = typeof schemaValues.managedProfileId === "string"
+9 -4
View File
@@ -981,10 +981,11 @@ while daemon work might still be running. The socket and its parent must be priv
canonical paths owned by the same account. Local stdio assignment requires Unix;
other platforms retain the HTTP path.
## Kimi Code candidate
## Kimi Code
Kimi Code uses the pinned `@moonshot-ai/kimi-code@2.3.3` distribution through
ACPX. Candidate support is pending local and Daytona Product E2E qualification.
ACPX through the qualified native profile. Ordinary selection and Runtime Test
require no candidate grant.
The installation targets are macOS ARM64 and Linux x64. Platform startup,
authenticated driver checks, and product acceptance are separate evidence levels.
@@ -998,8 +999,12 @@ The account needs Kimi Code entitlement. Runtime Test verifies installation and
queries the authenticated Code model catalog for the exact selected model. It
sends no model prompt; an actual task verifies inference and tool execution.
The ordinary hiring path stays hidden while qualification is pending. Existing
legacy Kimi agents retain their runtime and history. Managed Kimi connections,
The qualified native profile uses ordinary setup, hiring, metadata Test and task
startup on these two supported targets without operator admission flags. Protected
candidate startup records remain reserved for future pending profiles; saved agent
configuration and public requests cannot supply that authority.
Existing legacy Kimi agents retain their runtime and history. Managed Kimi connections,
ambient login borrowing, other regions/platforms, steering, native child work,
and per-run token/dollar accounting are outside this release. Read-only Kimi work
uses scoped Paperclip tools; native filesystem and shell tools are denied.
+3 -4
View File
@@ -69,14 +69,13 @@
"requiresProviderPolicy": false
},
"kimi": {
"agentProfileVersion": 16,
"agentProfileVersion": 17,
"agentServerPackage": "@moonshot-ai/kimi-code",
"agentServerVersion": "2.3.3",
"agentRuntimePackage": null,
"agentRuntimeVersion": null,
"commandDigest": "sha256:0c1e6ce460407ec973a37a2d192d665fcde744e2b9875c791c90308afbf4c52b",
"requiresProviderPolicy": true,
"qualificationStatus": "pending"
"commandDigest": "sha256:6ee5fb7ac18c1361ac63effe9117d08ed129add8349756a140e73e0c86ba5da7",
"requiresProviderPolicy": true
}
}
}
@@ -127,10 +127,11 @@ An administrator-owned read-only Runner can therefore use a user-writable cache,
but this provides weaker protection against that same user than hashing every
executable on each admission. Removing the cache restores full hashing.
Kimi keeps revision 16 from its pending candidate history. Its release
attestation replaces the earlier source-bound digest without advancing that
revision. Historical candidate records stay readable, while exact current
digest admission refuses their old launch identities. Kimi remains pending.
Kimi retains released revision 17, which separates ordinary admission from
pending revision 16. Its upstream-only release attestation replaces the earlier
source-bound digest without another revision bump. Historical candidate and
released records remain unchanged and readable; exact current digest admission
refuses their old launch identities.
## External controller checkpoints
@@ -272,14 +272,15 @@ mod tests {
}
#[test]
fn kimi_candidate_requires_exact_two_platform_closures_scope_and_time() {
fn released_kimi_rejects_candidate_records_on_both_native_platforms() {
assert!(!acpx_release_profile("kimi").unwrap().qualification_pending);
for (platform, architecture, os, host_arch) in [
("darwin", "arm64", "macos", "aarch64"),
("linux", "x64", "linux", "x86_64"),
] {
let candidate = kimi_admission(platform, architecture);
let now = candidate.expires_at - 1;
candidate.validate_at(now, os, host_arch).unwrap();
assert!(candidate.validate_at(now, os, host_arch).is_err());
for (wrong_os, wrong_arch) in [
("windows", "x86_64"),
("linux", "aarch64"),
@@ -323,7 +324,8 @@ mod tests {
all(target_os = "macos", target_arch = "aarch64"),
all(target_os = "linux", target_arch = "x86_64")
))]
fn kimi_next_run_rebinds_only_the_authenticated_run_identity() {
fn released_kimi_rejects_candidate_rebinding_and_startup_records() {
assert!(!acpx_release_profile("kimi").unwrap().qualification_pending);
let (platform, architecture) = if cfg!(target_os = "macos") {
("darwin", "arm64")
} else {
@@ -332,7 +334,11 @@ mod tests {
let current = kimi_admission(platform, architecture);
let mut next = current.clone();
next.run_id = "kimi-next-run".to_owned();
current
assert!(current.validate().is_err());
assert!(
AcpxCandidateAdmission::parse_json(&serde_json::to_string(&current).unwrap()).is_err()
);
assert!(current
.validate_next_run(
&next,
"kimi",
@@ -340,7 +346,7 @@ mod tests {
"kimi-next-run",
"kimi-session",
)
.unwrap();
.is_err());
assert!(current
.validate_next_run(
&current,
@@ -4945,7 +4945,8 @@ mod tests {
all(target_os = "macos", target_arch = "aarch64"),
all(target_os = "linux", target_arch = "x86_64")
))]
fn kimi_candidate_attachment_atomically_rebinds_exact_grant_and_profile_digest() {
fn released_kimi_attachment_preserves_ordinary_profile_without_candidate_admission() {
assert!(!acpx_release_profile("kimi").unwrap().qualification_pending);
let directory = temporary_directory("kimi-candidate-attachment");
let mut grant = crate::acpx_candidate_admission::test_admission("run-1", "session-1");
grant.agent = "kimi".to_owned();
@@ -4977,7 +4978,7 @@ mod tests {
let command = directory.join("owned-sidecar");
write_artifact(&command, b"#!/bin/sh\nexit 1\n", true);
let profile = AcpxLaunchProfile {
candidate_admission: Some(grant.clone()),
candidate_admission: None,
authority_digest: format!("sha256:{}", "d".repeat(64)),
command: command.clone(),
args: vec![],
@@ -5029,21 +5030,31 @@ mod tests {
executor.state = Some(state);
executor.save_state().unwrap();
let before = fs::read(executor.state_path()).unwrap();
let mut next = grant.clone();
next.run_id = "run-2".to_owned();
let mut provider = value;
provider["runId"] = json!("run-2");
let payload = json!({"provider":provider,"paperclipNextAuthority":{
"identity":{"runId":"run-2","normalizedSessionId":"session-1"},"candidateAdmission":next
"identity":{"runId":"run-2","normalizedSessionId":"session-1"}
}});
let mut injected_profile = executor.launch_profile.as_ref().unwrap().clone();
injected_profile.candidate_admission = Some(grant.clone());
let injected_config = test_config(&directory, Some(injected_profile.clone()));
assert!(injected_config.validate().is_err());
let prior = executor.state.as_ref().unwrap();
assert!(prior
.descriptor
.session_config(
prior.tool_set.clone(),
prior.identity.clone(),
Some(&injected_profile),
)
.is_err());
assert_eq!(fs::read(executor.state_path()).unwrap(), before);
for (field, changed) in [
("companyId", json!("foreign")),
("model", json!("wrong")),
("expiresAt", json!(grant.expires_at + 1)),
("runId", json!("unbound")),
("normalizedSessionId", json!("foreign-session")),
("runId", json!("run-1")),
] {
let mut invalid = payload.clone();
invalid["paperclipNextAuthority"]["candidateAdmission"][field] = changed;
invalid["paperclipNextAuthority"]["identity"][field] = changed;
assert!(executor.attach_run(&invalid).is_err(), "{field}");
assert_eq!(fs::read(executor.state_path()).unwrap(), before);
}
@@ -5053,8 +5064,8 @@ mod tests {
assert_eq!(updated.descriptor.run_id, "run-2");
assert_eq!(executor.context.run_id, "run-1");
let profile = executor.launch_profile.as_ref().unwrap();
assert_eq!(profile.candidate_admission.as_ref().unwrap(), &next);
assert_ne!(updated.launch_profile_digest, previous_digest);
assert!(profile.candidate_admission.is_none());
assert_eq!(updated.launch_profile_digest, previous_digest);
assert_eq!(
updated.launch_profile_digest,
profile.canonical_digest().unwrap()
@@ -5067,7 +5078,7 @@ mod tests {
Some(profile),
)
.unwrap();
assert_eq!(launch.candidate_admission.as_ref().unwrap(), &next);
assert!(launch.candidate_admission.is_none());
let saved: AcpxDurableState =
serde_json::from_slice(&fs::read(executor.state_path()).unwrap()).unwrap();
assert_eq!(saved.descriptor.run_id, "run-2");
@@ -5081,7 +5092,15 @@ mod tests {
))]
fn kimi_cold_restore_fixture(
label: &str,
) -> (PathBuf, DurableRunnerConfig, Value, Vec<u8>, PathBuf) {
) -> (
PathBuf,
DurableRunnerConfig,
Value,
Vec<u8>,
PathBuf,
crate::acpx_candidate_admission::AcpxCandidateAdmission,
) {
assert!(!acpx_release_profile("kimi").unwrap().qualification_pending);
let directory = temporary_directory(label);
let mut grant = crate::acpx_candidate_admission::test_admission("run-1", "session-1");
grant.agent = "kimi".to_owned();
@@ -5118,7 +5137,7 @@ mod tests {
true,
);
let profile = AcpxLaunchProfile {
candidate_admission: Some(grant.clone()),
candidate_admission: None,
authority_digest: format!("sha256:{}", "d".repeat(64)),
command: command.clone(),
args: vec![],
@@ -5174,14 +5193,7 @@ mod tests {
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_grant = next_config
.acpx_launch_profile
.as_mut()
.unwrap()
.candidate_admission
.as_mut()
.unwrap();
next_grant.run_id = "run-2".to_owned();
grant.run_id = "run-2".to_owned();
next_config.validate().unwrap();
let mut next_provider = value;
next_provider["runId"] = json!("run-2");
@@ -5191,6 +5203,7 @@ mod tests {
json!({"provider": next_provider}),
before,
sentinel,
grant,
)
}
@@ -5212,16 +5225,16 @@ mod tests {
all(target_os = "macos", target_arch = "aarch64"),
all(target_os = "linux", target_arch = "x86_64")
))]
fn kimi_candidate_governed_wait_cold_restore_uses_authenticated_next_startup() {
let (directory, next_config, payload, before, sentinel) =
fn released_kimi_governed_wait_cold_restore_uses_ordinary_startup_without_grant() {
let (directory, next_config, payload, before, sentinel, _) =
kimi_cold_restore_fixture("kimi-candidate-cold-governed-wait");
let mut recovered = AcpxCommandExecutor::with_runner_config(&directory, &next_config);
recovered.restore().expect("authorized cold Kimi restore must retain the settled prior session until authenticated attachment");
recovered.restore().expect("released cold Kimi restore retains its settled prior session under the same full ordinary profile");
assert!(recovered.session.is_none());
assert!(recovered.poll_events().unwrap().is_empty());
assert!(!sentinel.exists());
recovered.attach_run(&payload)
.expect("cold replacement uses its already authenticated startup authority, not a second warm next-authority grant");
.expect("released cold replacement attaches with the ordinary current startup and no candidate record");
let saved: AcpxDurableState =
serde_json::from_slice(&fs::read(recovered.state_path()).unwrap()).unwrap();
assert_eq!(saved.descriptor.run_id, "run-2");
@@ -5245,7 +5258,7 @@ mod tests {
all(target_os = "macos", target_arch = "aarch64"),
all(target_os = "linux", target_arch = "x86_64")
))]
fn kimi_candidate_cold_restore_rejects_changed_startup_authority_before_provider_open() {
fn released_kimi_cold_restore_rejects_candidate_injection_and_changed_full_profile() {
for field in [
"company",
"model",
@@ -5257,25 +5270,26 @@ mod tests {
"architecture",
"session",
"grant_run",
"injected_candidate",
"context_run",
"authority",
"command",
"args",
"artifact",
"missing_grant",
"missing_profile",
"saved_digest",
] {
let (directory, mut config, payload, before, sentinel) =
kimi_cold_restore_fixture("kimi-candidate-cold-reject");
let (directory, mut config, payload, before, sentinel, candidate) =
kimi_cold_restore_fixture("kimi-released-cold-reject");
assert!(config
.acpx_launch_profile
.as_ref()
.unwrap()
.candidate_admission
.is_none());
match field {
"context_run" => config.run_id = "foreign-run".into(),
"missing_grant" => {
config
.acpx_launch_profile
.as_mut()
.unwrap()
.candidate_admission = None
}
"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));
@@ -5287,20 +5301,7 @@ mod tests {
}
_ => {
let profile = config.acpx_launch_profile.as_mut().unwrap();
let grant = profile.candidate_admission.as_mut().unwrap();
match field {
"company" => grant.company_id = "foreign-company".into(),
"model" => grant.model = "foreign-model".into(),
"expiry" => grant.expires_at += 1,
"expired" => grant.expires_at = 1,
"profile" => grant.profile_digest = format!("sha256:{}", "0".repeat(64)),
"closure" => {
grant.native_closure_digest = format!("sha256:{}", "0".repeat(64))
}
"platform" => grant.platform = "foreign-platform".into(),
"architecture" => grant.architecture = "foreign-architecture".into(),
"session" => grant.normalized_session_id = "foreign-session".into(),
"grant_run" => grant.run_id = "foreign-run".into(),
"authority" => {
profile.authority_digest = format!("sha256:{}", "0".repeat(64))
}
@@ -5309,7 +5310,32 @@ mod tests {
"artifact" => {
profile.artifacts[0].sha256 = format!("sha256:{}", "0".repeat(64))
}
_ => unreachable!(),
_ => {
let mut grant = candidate;
match field {
"company" => grant.company_id = "foreign-company".into(),
"model" => grant.model = "foreign-model".into(),
"expiry" => grant.expires_at += 1,
"expired" => grant.expires_at = 1,
"profile" => {
grant.profile_digest = format!("sha256:{}", "0".repeat(64))
}
"closure" => {
grant.native_closure_digest =
format!("sha256:{}", "0".repeat(64))
}
"platform" => grant.platform = "foreign-platform".into(),
"architecture" => {
grant.architecture = "foreign-architecture".into()
}
"session" => grant.normalized_session_id = "foreign-session".into(),
"grant_run" => grant.run_id = "foreign-run".into(),
"injected_candidate" => {}
_ => unreachable!(),
}
profile.candidate_admission = Some(grant);
assert!(config.validate().is_err(), "{field}");
}
}
}
}
@@ -5335,7 +5361,7 @@ mod tests {
all(target_os = "macos", target_arch = "aarch64"),
all(target_os = "linux", target_arch = "x86_64")
))]
fn kimi_candidate_cold_attach_rejects_unsettled_or_changed_session_before_provider_open() {
fn released_kimi_cold_attach_rejects_unsettled_or_changed_session_before_provider_open() {
for field in [
"run",
"session",
@@ -5347,7 +5373,7 @@ mod tests {
"identity",
"closed",
] {
let (directory, config, mut payload, before, sentinel) =
let (directory, config, mut payload, before, sentinel, _) =
kimi_cold_restore_fixture("kimi-candidate-cold-binding");
let mut recovered = AcpxCommandExecutor::with_runner_config(&directory, &config);
match field {
@@ -5399,8 +5425,8 @@ mod tests {
all(target_os = "macos", target_arch = "aarch64"),
all(target_os = "linux", target_arch = "x86_64")
))]
fn kimi_candidate_cold_attach_post_rename_failure_quarantines_public_execution() {
let (directory, config, payload, before, sentinel) =
fn released_kimi_cold_attach_post_rename_failure_quarantines_public_execution() {
let (directory, config, payload, before, sentinel, _) =
kimi_cold_restore_fixture("kimi-candidate-cold-write-failure");
let mut recovered = AcpxCommandExecutor::with_runner_config(&directory, &config);
recovered.restore().unwrap();
@@ -5411,7 +5437,7 @@ mod tests {
assert!(error
.to_string()
.contains("injected post-rename sync failure"));
assert_eq!(recovered.state, previous_state);
assert_ne!(recovered.state, previous_state);
assert!(recovered.session.is_none());
assert!(!sentinel.exists());
let disk = fs::read(recovered.state_path()).unwrap();
@@ -5149,7 +5149,12 @@ mod tests {
all(target_os = "macos", target_arch = "aarch64"),
all(target_os = "linux", target_arch = "x86_64")
))]
fn kimi_candidate_handoff_binds_the_existing_authenticated_next_authority() {
fn released_kimi_handoff_uses_ordinary_authority_and_rejects_candidate_injection() {
assert!(
!crate::generated_acpx_profiles::acpx_release_profile("kimi")
.unwrap()
.qualification_pending
);
let directory =
std::env::temp_dir().join(format!("kimi-grant-handoff-{}", std::process::id()));
let mut current = config(directory);
@@ -5190,7 +5195,7 @@ mod tests {
path: PathBuf::from("/fixture/node"),
sha256: format!("sha256:{}", "b".repeat(64)),
}],
candidate_admission: Some(grant.clone()),
candidate_admission: None,
});
current.validate().unwrap();
let mut next_grant = grant.clone();
@@ -5199,27 +5204,22 @@ mod tests {
attach.payload = json!({"paperclipNextAuthority":{
"identity":{"runnerInstanceId":current.runner_instance_id,"environmentLeaseId":current.environment_lease_id,
"runId":"run_2","normalizedSessionId":current.normalized_session_id,"turnId":"turn_2","itemId":"item_2"},
"connection":{"mode":"external"},"candidateAdmission":next_grant
"connection":{"mode":"external"}
}});
let next = next_authority_config(&attach, &current).unwrap().unwrap();
let actual = next
assert!(next
.acpx_launch_profile
.as_ref()
.unwrap()
.candidate_admission
.as_ref()
.unwrap();
assert_eq!(actual, &next_grant);
assert_eq!(
current
.acpx_launch_profile
.as_ref()
.unwrap()
.candidate_admission
.as_ref()
.unwrap(),
&grant
);
.is_none());
assert_eq!(next.acpx_launch_profile, current.acpx_launch_profile);
assert_eq!(next.run_id, "run_2");
assert_eq!(next.turn_id, "turn_2");
assert_eq!(next.item_id, "item_2");
let mut injected = attach.clone();
injected.payload["paperclipNextAuthority"]["candidateAdmission"] = json!(next_grant);
assert!(next_authority_config(&injected, &current).is_err());
assert_eq!(next.runner_instance_id, current.runner_instance_id);
assert_eq!(next.environment_lease_id, current.environment_lease_id);
assert_eq!(next.normalized_session_id, current.normalized_session_id);
@@ -5236,7 +5236,7 @@ mod tests {
("runId", json!("unbound-run")),
("normalizedSessionId", json!("foreign-session")),
] {
let mut changed = attach.clone();
let mut changed = injected.clone();
changed.payload["paperclipNextAuthority"]["candidateAdmission"][field] = value;
assert!(
next_authority_config(&changed, &current).is_err(),
@@ -5255,18 +5255,19 @@ mod tests {
"{field}"
);
}
let mut missing = attach.clone();
let mut missing = injected.clone();
missing.payload["paperclipNextAuthority"]
.as_object_mut()
.unwrap()
.remove("candidateAdmission");
assert!(next_authority_config(&missing, &current).is_err());
let mut no_prior = current.clone();
no_prior
assert!(next_authority_config(&missing, &current).unwrap().is_some());
let mut stale_candidate = current.clone();
stale_candidate
.acpx_launch_profile
.as_mut()
.unwrap()
.candidate_admission = None;
assert!(next_authority_config(&attach, &no_prior).is_err());
.candidate_admission = Some(grant);
assert!(stale_candidate.validate().is_err());
assert!(next_authority_config(&injected, &stale_candidate).is_err());
}
}
@@ -103,9 +103,9 @@ pub(crate) fn acpx_release_profile(agent: &str) -> Option<AcpxReleaseProfile> {
agent_runtime_package: None,
agent_runtime_version: None,
command_digest:
"sha256:0c1e6ce460407ec973a37a2d192d665fcde744e2b9875c791c90308afbf4c52b",
"sha256:6ee5fb7ac18c1361ac63effe9117d08ed129add8349756a140e73e0c86ba5da7",
requires_provider_policy: true,
qualification_pending: true,
qualification_pending: false,
},
_ => return None,
})
@@ -21,6 +21,7 @@ import { dirname, join, relative, resolve } from "node:path";
import { createRequire } from "node:module";
import { fileURLToPath } from "node:url";
import { parseProviderPackArguments, materializeCandidateProviderPack, providerPackProviders, providerPackManifestFields } from "./candidate-provider-pack.mjs";
import { smokeKimiProviderPack } from "./kimi-provider-pack-smoke.mjs";
import { buildNodeStartupTimeout } from "./build-node-startup-timeout.mjs";
import { writePortableExecutableShim, writePortableCopilotShims } from "./provider-pack-executable-shims.mjs";
import { providerPackLibc, removeControllerArtifactsFromProviderPack, removeForeignLibcPackagesFromProviderPack } from "./provider-pack-runtime-tree.mjs";
@@ -183,7 +184,7 @@ try {
|| !/^sha256:[a-f0-9]{64}$/.test(metadata.profileDigest)
|| !/^sha256:[a-f0-9]{64}$/.test(metadata.closureDigest)) throw new Error("Candidate builder omitted its pinned identity");
candidateProviders[provider] = { version: metadata.version, profileDigest: metadata.profileDigest,
closureDigest: metadata.closureDigest, qualification: ["pi", "cursor", "copilot"].includes(provider) ? "qualified" : "pending", path: assetPath,
closureDigest: metadata.closureDigest, qualification: ["pi", "cursor", "copilot"].includes(provider) || provider === "kimi" && profiles.profiles.kimi.qualificationStatus !== "pending" ? "qualified" : "pending", path: assetPath,
sha256: sha256Tree(join(temporaryRoot, assetPath)) };
}
@@ -386,6 +387,11 @@ try {
{ mode: 0o600 },
);
if (payload.providers?.kimi && profiles.profiles.kimi.qualificationStatus !== "pending") {
const smoke = await smokeKimiProviderPack(temporaryRoot);
process.stdout.write(`${JSON.stringify(smoke)}\n`);
}
rmSync(outputRoot, { recursive: true, force: true });
renameSync(temporaryRoot, outputRoot);
process.stdout.write(`${outputRoot}\n`);
@@ -2,18 +2,21 @@ import { materializePiDistribution } from "./materialize-pi-distribution.mjs";
import profiles from "../acpx-profiles.json" with { type: "json" };
import { materializePinnedCursorDistribution } from "./materialize-cursor-distribution.mjs";
const CANDIDATES = new Set(["cursor", "copilot", "pi", "kimi"]);
const releasedKimi = () => profiles.profiles.kimi.qualificationStatus !== "pending";
const ordinaryProviders = () => ["pi", "cursor", "copilot", ...(releasedKimi() ? ["kimi"] : [])];
/** Only materialized providers enter the serialized manifest and its digest. */
export function providerPackManifestFields(providers, candidates) {
return {
...(["pi", "cursor", "copilot"].some(provider => providers[provider]) ? { providers: Object.fromEntries(["pi", "cursor", "copilot"].filter(provider => providers[provider]).map(provider => [provider, providers[provider]])) } : {}),
...(["pi", ...candidates].some(provider => provider !== "copilot" && providers[provider]) ? { candidateProviders: Object.fromEntries([...new Set(["pi", ...candidates])].filter(provider => provider !== "copilot" && providers[provider]).map(provider => [provider, providers[provider]])) } : {}),
...(ordinaryProviders().some(provider => providers[provider]) ? { providers: Object.fromEntries(ordinaryProviders().filter(provider => providers[provider]).map(provider => [provider, providers[provider]])) } : {}),
...(["pi", ...candidates].some(provider => provider !== "copilot" && !(provider === "kimi" && releasedKimi()) && providers[provider]) ? { candidateProviders: Object.fromEntries([...new Set(["pi", ...candidates])].filter(provider => provider !== "copilot" && !(provider === "kimi" && releasedKimi()) && providers[provider]).map(provider => [provider, providers[provider]])) } : {}),
};
}
export function providerPackProviders(platform, architecture, candidates) {
const nativeSupported = ["darwin-arm64", "darwin-x64", "linux-x64"].includes(`${platform}-${architecture}`);
return [...new Set([...(nativeSupported ? ["pi", "cursor", "copilot"] : []), ...candidates])];
const kimiSupported = ["darwin-arm64", "linux-x64"].includes(`${platform}-${architecture}`);
return [...new Set([...(nativeSupported ? ["pi", "cursor", "copilot"] : []), ...(releasedKimi() && kimiSupported ? ["kimi"] : []), ...candidates])];
}
export function parseProviderPackArguments(args) {
@@ -46,17 +46,21 @@ test("candidate options stay explicit and supported native assets are included b
assert.deepEqual(parseProviderPackArguments(["/pack", "--candidate-providers=kimi"]), { output: "/pack", candidates: ["kimi"] });
});
test("Kimi stays explicit, pending and separate from released provider inventory", () => {
const kimi = { version: "2.3.3", qualification: "pending", profileDigest: "sha256:profile", closureDigest: "sha256:closure" };
test("released Kimi enters ordinary inventory on only its two supported targets", () => {
const kimi = { version: "2.3.3", qualification: "qualified", profileDigest: "sha256:profile", closureDigest: "sha256:closure" };
for (const [platform, architecture] of [["darwin", "arm64"], ["linux", "x64"]]) {
assert.equal(providerPackProviders(platform, architecture, []).filter(provider => provider === "kimi").length, 1);
assert.equal(providerPackProviders(platform, architecture, ["kimi"]).filter(provider => provider === "kimi").length, 1);
}
for (const [platform, architecture] of [["darwin", "x64"], ["linux", "arm64"], ["win32", "x64"]]) {
assert.equal(providerPackProviders(platform, architecture, []).includes("kimi"), false);
assert.equal(providerPackProviders(platform, architecture, ["kimi"]).includes("kimi"), true);
}
for (const candidates of [[], ["kimi"]]) {
const fields = providerPackManifestFields({ kimi }, candidates);
assert.equal(fields.providers.kimi, kimi);
assert.equal(fields.candidateProviders?.kimi, undefined);
assert.equal(digest(JSON.parse(JSON.stringify(fields))), digest(fields));
}
const fields = providerPackManifestFields({ kimi }, ["kimi"]);
assert.equal(fields.providers?.kimi, undefined);
assert.equal(fields.candidateProviders.kimi, kimi);
assert.equal(providerPackManifestFields({ kimi }, []).candidateProviders?.kimi, undefined);
assert.equal(digest(JSON.parse(JSON.stringify(fields))), digest(fields));
});
test("Kimi's candidate builder rejects invalid paths before any public download", async t => {
@@ -67,8 +71,8 @@ test("Kimi's candidate builder rejects invalid paths before any public download"
});
test("normal packs include Cursor and Copilot on their three pinned targets without breaking other hosts", () => {
for (const [platform, architecture] of [["darwin", "arm64"], ["darwin", "x64"], ["linux", "x64"]]) {
assert.deepEqual(providerPackProviders(platform, architecture, []), ["pi", "cursor", "copilot"]);
assert.deepEqual(providerPackProviders(platform, architecture, ["cursor"]), ["pi", "cursor", "copilot"]);
assert.deepEqual(providerPackProviders(platform, architecture, []), ["pi", "cursor", "copilot", ...((platform === "darwin" && architecture === "arm64") || platform === "linux" ? ["kimi"] : [])]);
assert.deepEqual(providerPackProviders(platform, architecture, ["cursor"]), providerPackProviders(platform, architecture, []));
}
assert.deepEqual(providerPackProviders("linux", "arm64", []), []);
assert.deepEqual(providerPackProviders("win32", "x64", []), []);
@@ -65,7 +65,7 @@ test("Kimi metadata CLI is self-contained and rejects incomplete assets without
runId: "metadata-operation", normalizedSessionId: "metadata-session" };
const child = spawnSync(node, ["--require", preload, entry], {
env: { KIMI_API_KEY: "selected-secret-canary", PAPERCLIP_KIMI_PROBE_MODEL: "selected-model",
PAPERCLIP_ACPX_CANDIDATE_ADMISSION: JSON.stringify(admission),
...(profile.qualificationStatus === "pending" ? { PAPERCLIP_ACPX_CANDIDATE_ADMISSION: JSON.stringify(admission) } : {}),
PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT: root, PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST: manifest,
KIMI_MODEL_DEFAULT: "unselected-model", HOME: "/private/unselected-home", HTTPS_PROXY: "http://127.0.0.1:9",
ANTHROPIC_API_KEY: "unselected-secret-canary", NODE_OPTIONS: "", NODE_PATH: "/unselected-module-path" },
@@ -80,16 +80,31 @@ test("Kimi metadata CLI is self-contained and rejects incomplete assets without
? "Install the current verified Kimi Code runtime in this execution environment."
: "Kimi metadata verification requires the pinned Node runtime.", promptSent: false });
const unqualified = spawnSync(node, ["--require", preload, entry], {
const withoutAdmission = spawnSync(node, ["--require", preload, entry], {
env: { KIMI_API_KEY: "selected-secret-canary", PAPERCLIP_KIMI_PROBE_MODEL: "selected-model",
PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT: root, PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST: manifest },
encoding: "utf8", timeout: 10_000,
});
assert.ifError(unqualified.error); assert.equal(unqualified.status, 1);
assert.ifError(withoutAdmission.error); assert.equal(withoutAdmission.status, 1);
if (process.versions.node === "24.21.0" || node === pinnedNode) {
assert.equal(JSON.parse(unqualified.stdout).code, "KIMI_QUALIFICATION_REQUIRED");
assert.equal(JSON.parse(withoutAdmission.stdout).code,
profile.qualificationStatus === "pending" ? "KIMI_QUALIFICATION_REQUIRED" : "KIMI_INSTALLATION_INVALID");
}
assert.doesNotMatch(withoutAdmission.stdout + withoutAdmission.stderr, /UNEXPECTED_NETWORK_OR_CHILD|selected-secret-canary/);
if (profile.qualificationStatus !== "pending") {
const formerAdmission = spawnSync(node, ["--require", preload, entry], {
env: { KIMI_API_KEY: "selected-secret-canary", PAPERCLIP_KIMI_PROBE_MODEL: "selected-model",
PAPERCLIP_ACPX_CANDIDATE_ADMISSION: JSON.stringify(admission),
PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT: root, PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST: manifest,
HOME: "/private/unselected-home", HTTPS_PROXY: "http://127.0.0.1:9", ANTHROPIC_API_KEY: "unselected-secret-canary" },
encoding: "utf8", timeout: 10_000,
});
assert.ifError(formerAdmission.error); assert.equal(formerAdmission.status, 1); assert.equal(formerAdmission.signal, null);
assert.deepEqual(JSON.parse(formerAdmission.stdout), { schema: "paperclip.kimi.metadata.v1", status: "failed",
code: "KIMI_REQUEST_FAILED", message: "Kimi Code metadata verification failed.", promptSent: false });
assert.doesNotMatch(formerAdmission.stdout + formerAdmission.stderr, /UNEXPECTED_NETWORK_OR_CHILD|selected-secret-canary|unselected-secret-canary/);
}
assert.doesNotMatch(unqualified.stdout + unqualified.stderr, /UNEXPECTED_NETWORK_OR_CHILD|selected-secret-canary/);
for (const layout of ["runner", "server"]) {
const packageDirectory = join(root, layout);
@@ -5,6 +5,24 @@ import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
export function assertKimiProviderPackInventory(manifest, profile, closures, platform = process.platform, architecture = process.arch) {
assert.equal(manifest.schema, "paperclip-runner/remote-provider-pack/v1");
assert.deepEqual(manifest.payload.target, { platform, architecture });
const target = `${platform}-${architecture}`;
assert.ok(["darwin-arm64", "linux-x64"].includes(target));
const pending = profile.qualificationStatus === "pending";
const identity = pending ? manifest.payload.candidateProviders?.kimi : manifest.payload.providers?.kimi;
assert.equal(pending ? manifest.payload.providers?.kimi : manifest.payload.candidateProviders?.kimi, undefined);
assert.equal(identity?.qualification, pending ? "pending" : "qualified");
assert.equal(identity.version, profile.agentServerVersion);
assert.equal(identity.path, `provider-assets/kimi/${target}`);
assert.equal(identity.profileDigest, profile.commandDigest);
assert.match(closures[target] ?? "", /^[a-f0-9]{64}$/);
assert.equal(identity.closureDigest, `sha256:${closures[target]}`);
assert.match(identity.sha256, /^sha256:[a-f0-9]{64}$/);
return identity;
}
export async function smokeKimiProviderPack(directory) {
const pack = await realpath(resolve(directory));
const manifest = JSON.parse(await readFile(join(pack, "provider-pack.json"), "utf8"));
@@ -12,17 +30,11 @@ export async function smokeKimiProviderPack(directory) {
assert.equal(manifest.payload.target.platform, process.platform);
assert.equal(manifest.payload.target.architecture, process.arch);
if (process.env.PAPERCLIP_EXPECTED_SOURCE_REVISION) assert.equal(manifest.payload.runnerSourceRevision, process.env.PAPERCLIP_EXPECTED_SOURCE_REVISION);
const candidate = manifest.payload.candidateProviders?.kimi;
assert.equal(candidate?.qualification, "pending");
assert.equal(candidate.version, "2.3.3");
assert.equal(candidate.path, `provider-assets/kimi/${process.platform}-${process.arch}`);
assert.equal(manifest.payload.providers?.kimi, undefined);
const load = path => import(pathToFileURL(join(pack, "dist/drivers/acpx", path)));
const [{ verifyKimiInstallation }, { QUALIFIED_ACPX_PROFILES }, { kimiManagedConfiguration }, { kimiInstructionBinding }, { KIMI_DISTRIBUTION_CLOSURE_SHA256 }] = await Promise.all([
load("kimi-installation.js"), load("qualified-profiles.js"), load("kimi-config.js"), load("kimi-instructions.js"), load("kimi-closure-pins.js"),
]);
assert.equal(candidate.profileDigest, QUALIFIED_ACPX_PROFILES.kimi.commandDigest);
assert.equal(candidate.closureDigest, `sha256:${KIMI_DISTRIBUTION_CLOSURE_SHA256[`${process.platform}-${process.arch}`]}`);
const provider = assertKimiProviderPackInventory(manifest, QUALIFIED_ACPX_PROFILES.kimi, KIMI_DISTRIBUTION_CLOSURE_SHA256);
const keys = ["PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT", "PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST"];
const previous = keys.map(key => process.env[key]);
process.env[keys[0]] = pack; process.env[keys[1]] = join(pack, "package.json");
@@ -104,7 +116,7 @@ export async function smokeKimiProviderPack(directory) {
assert.equal(loaded.configOptions.find(option => option.id === "model").currentValue, model);
const loadExitMs = await second.close();
return { schema: "paperclip.kimi-provider-pack-smoke.v1", sourceRevision: manifest.payload.runnerSourceRevision, providerPackDigest: manifest.digest,
platform: process.platform, architecture: process.arch, runtimeVersion: "2.3.3", profileDigest: candidate.profileDigest, closureDigest: candidate.closureDigest,
platform: process.platform, architecture: process.arch, runtimeVersion: "2.3.3", profileDigest: provider.profileDigest, closureDigest: provider.closureDigest,
createAcknowledged: true, loadAcknowledged: true, model, createExitMs, loadExitMs, providerPrompts: 0, providerCredentials: false,
qualification: "installed runtime only; no model entitlement or task completion" };
} finally {
@@ -0,0 +1,62 @@
import assert from "node:assert/strict";
import test from "node:test";
import { readFileSync } from "node:fs";
import { assertKimiProviderPackInventory } from "./kimi-provider-pack-smoke.mjs";
import profiles from "../acpx-profiles.json" with { type: "json" };
import historical from "../test/fixtures/kimi-profile-v14-identity.json" with { type: "json" };
import { KIMI_DISTRIBUTION_CLOSURE_SHA256 as closures } from "../src/drivers/acpx/kimi-closure-pins.ts";
function fixture(profile, target) {
const [platform, architecture] = target.split("-");
const pending = profile.qualificationStatus === "pending";
return { schema: "paperclip-runner/remote-provider-pack/v1", payload: { target: { platform, architecture },
[pending ? "candidateProviders" : "providers"]: { kimi: { version: profile.agentServerVersion,
qualification: pending ? "pending" : "qualified", profileDigest: profile.commandDigest,
closureDigest: `sha256:${closures[target]}`, path: `provider-assets/kimi/${target}`, sha256: `sha256:${"a".repeat(64)}` } } } };
}
for (const target of ["darwin-arm64", "linux-x64"]) {
test(`ordinary ${target} smoke needs released current17 and no candidate entry`, () => {
const profile = profiles.profiles.kimi;
assert.equal(profile.agentProfileVersion, 17);
assert.notEqual(profile.qualificationStatus, "pending");
const manifest = fixture(profile, target), [platform, architecture] = target.split("-");
assert.equal(assertKimiProviderPackInventory(manifest, profile, closures, platform, architecture), manifest.payload.providers.kimi);
for (const change of [{ profileDigest: historical.commandDigest }, { closureDigest: "sha256:" + "b".repeat(64) },
{ qualification: "pending" }, { path: "provider-assets/kimi/foreign" }]) {
const invalid = structuredClone(manifest); Object.assign(invalid.payload.providers.kimi, change);
assert.throws(() => assertKimiProviderPackInventory(invalid, profile, closures, platform, architecture));
}
const duplicate = structuredClone(manifest); duplicate.payload.candidateProviders = { kimi: duplicate.payload.providers.kimi };
assert.throws(() => assertKimiProviderPackInventory(duplicate, profile, closures, platform, architecture));
assert.throws(() => assertKimiProviderPackInventory(manifest, profile, closures, "linux", "arm64"));
});
test(`historical pending14 ${target} remains a distinct inventory contract`, () => {
const profile = { ...historical.declaration, commandDigest: historical.commandDigest };
const manifest = fixture(profile, target), [platform, architecture] = target.split("-");
assert.equal(profile.qualificationStatus, "pending");
assert.equal(assertKimiProviderPackInventory(manifest, profile, closures, platform, architecture), manifest.payload.candidateProviders.kimi);
assert.throws(() => assertKimiProviderPackInventory(manifest, profiles.profiles.kimi, closures, platform, architecture));
});
}
test("the actual builder verifies ordinary Kimi before replacing the retained pack", async () => {
const source = readFileSync(new URL("./build-provider-pack.mjs", import.meta.url), "utf8");
const begin = source.indexOf(' if (payload.providers?.kimi && profiles.profiles.kimi.qualificationStatus !== "pending") {');
const end = source.indexOf('\n} finally {', begin);
assert.ok(begin > 0 && end > begin);
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
const run = new AsyncFunction("payload", "profiles", "smokeKimiProviderPack", "temporaryRoot", "outputRoot", "process", "rmSync", "renameSync", source.slice(begin, end));
for (const failed of [false, true]) {
const order = [], smoke = async root => { assert.equal(root, "/owned/temporary"); order.push("smoke"); if (failed) throw Error("smoke failed"); return { providerPrompts: 0 }; };
const work = run({ providers: { kimi: {} } }, profiles, smoke, "/owned/temporary", "/owned/output",
{ stdout: { write() {} } }, () => order.push("remove-old"), () => order.push("publish"));
if (failed) { await assert.rejects(work, /smoke failed/); assert.deepEqual(order, ["smoke"]); }
else { await work; assert.deepEqual(order, ["smoke", "remove-old", "publish"]); }
}
for (const [payload, status] of [[{ candidateProviders: { kimi: {} } }, "pending"], [{ providers: {} }, undefined]]) {
let calls = 0;
await run(payload, { profiles: { kimi: { qualificationStatus: status } } }, () => { calls++; }, "/owned/temporary", "/owned/output",
{ stdout: { write() {} } }, () => {}, () => {});
assert.equal(calls, 0);
}
});
@@ -2,8 +2,9 @@ import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { access, mkdir, mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises";
import { createServer } from "node:http";
import { createRequire } from "node:module";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { dirname, join } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import test from "node:test";
import { runInNewContext } from "node:vm";
@@ -236,25 +237,50 @@ test("real pinned Kimi and ACPX exchange instructions/tools/denial/errors with a
return;
}
const { tsImport } = await import("tsx/esm/api");
const [{ verifyKimiInstallation }, { QUALIFIED_ACPX_PROFILES }, { kimiManagedConfiguration }, instructionModule, { createKimiMessageProjection }, { normalizeAcpxPermission }] = await Promise.all([
const selectorPeer = process.env.PAPERCLIP_TEST_KIMI_FAULT_SELECTOR_PEER === "1";
let compiledFactory;
if (selectorPeer) {
assert.equal(process.platform, "darwin"); assert.equal(process.arch, "arm64");
const cli = join(packageRoot, "dist/cli/acpx-runtime-sidecar.cjs");
const compiled = await readFile(cli, "utf8");
const boundary = "var lines = (0, import_node_readline.createInterface)({";
const at = compiled.indexOf(boundary);
assert.ok(at > 0 && compiled.lastIndexOf(boundary) === at && Buffer.byteLength(compiled) <= 16 * 1024 * 1024);
const module = { exports: {} };
new Function("require", "module", "exports", "__filename", "__dirname", compiled.slice(0, at).replace(/^#![^\n]*\n/, "\n")
+ "\nmodule.exports={verifyKimiInstallation,QUALIFIED_ACPX_PROFILES,acquireAcpxProviderLifetimeLease,createAcpxSidecarHostEnvironment,createSanitizedAcpxSpawnInput};")(
createRequire(cli), module, module.exports, cli, dirname(cli));
compiledFactory = module.exports;
}
const [{ verifyKimiInstallation }, { QUALIFIED_ACPX_PROFILES }, { kimiManagedConfiguration }, instructionModule, { createKimiMessageProjection }, { normalizeAcpxPermission }, environmentModule, { configuredEnvironmentProjection, CONFIGURED_ENVIRONMENT_KEYS }] = await Promise.all([
tsImport("../src/drivers/acpx/kimi-installation.ts", import.meta.url),
tsImport("../src/drivers/acpx/qualified-profiles.ts", import.meta.url),
tsImport("../src/drivers/acpx/kimi-config.ts", import.meta.url),
tsImport("../src/drivers/acpx/kimi-instructions.ts", import.meta.url),
tsImport("../src/drivers/acpx/kimi-message-boundary.ts", import.meta.url),
tsImport("../src/drivers/acpx/acp-permission-adapter.ts", import.meta.url),
tsImport("../src/drivers/acpx/environment.ts", import.meta.url),
tsImport("../src/configured-environment.ts", import.meta.url),
]);
const { createAcpRuntime, createFileSessionStore } = await import(pathToFileURL(join(packageRoot, "node_modules/acpx/dist/runtime.js")));
const root = await realpath(await mkdtemp(join(tmpdir(), "kimi-simulated-model-peer-")));
const home = join(root, "home"), workspace = join(root, "workspace");
await mkdir(home); await mkdir(workspace);
await mkdir(home, { mode: 0o700 }); await mkdir(workspace);
const instructions = "\nExact governance café 日本語 ${cwd} ${base_prompt}\nKeep the final LF.\n";
const selectedModel = "kimi-for-coding";
const binding = instructionModule.kimiInstructionBinding(instructions);
const argumentsObject = { payload: "café 🧪\n${literal}", nested: { count: 0, empty: {}, flag: false } };
const toolReceipt = sha(JSON.stringify(argumentsObject));
const deniedPath = join(workspace, "denied-peer.txt");
const bashArguments = { command: 'if test -n "${KIMI_API_KEY+x}"; then printf "PROVIDER_KEY_PRESENT"; exit 1; fi; if test -n "${PAPERCLIP_KIMI_INSTRUCTIONS+x}"; then printf "PRIVATE_BINDING_PRESENT"; exit 1; fi; if test "$PAPERCLIP_TASK_SCOPED" != "assigned-task-canary"; then printf "TASK_VARIABLE_MISSING"; exit 1; fi; printf "KIMI_CHILD_CREDENTIAL_FENCE_OK"', timeout: 5 };
const notificationEnvironment = {
PAPERCLIP_RUNTIME_NOTIFICATION_CONTEXT: join(root, "controlled-notification-context.json"),
PAPERCLIP_RUNTIME_NOTIFICATION_HELPER: join(root, "controlled-notification-helper"),
};
await writeFile(notificationEnvironment.PAPERCLIP_RUNTIME_NOTIFICATION_CONTEXT, '{"controlledEnvironmentOnly":true}\n', { flag: "wx", mode: 0o600 });
await writeFile(notificationEnvironment.PAPERCLIP_RUNTIME_NOTIFICATION_HELPER, "controlled path fixture\n", { flag: "wx", mode: 0o700 });
const shellQuote = value => "'" + value.replaceAll("'", "'\\''") + "'";
const notificationChecks = Object.entries(notificationEnvironment).map(([key, value]) => `if test "$${key}" != ${shellQuote(value)}; then printf "NOTIFICATION_DISCOVERY_CHANGED"; exit 1; fi; `).join("");
const bashArguments = { command: 'if test -n "${KIMI_API_KEY+x}"; then printf "PROVIDER_KEY_PRESENT"; exit 1; fi; if test -n "${PAPERCLIP_KIMI_INSTRUCTIONS+x}"; then printf "PRIVATE_BINDING_PRESENT"; exit 1; fi; if test "$PAPERCLIP_TASK_SCOPED" != "assigned-task-canary"; then printf "TASK_VARIABLE_MISSING"; exit 1; fi; ' + notificationChecks + 'printf "KIMI_CHILD_CREDENTIAL_FENCE_OK"', timeout: 5 };
const modelRequests = [], mcpCalls = [], permissions = [], deliveries = [], nativeMessages = [], owners = [];
let scenario = "echo", modelStep = 0, peerFailure, permissionGate;
const complete = (response, chunks) => {
@@ -306,7 +332,7 @@ test("real pinned Kimi and ACPX exchange instructions/tools/denial/errors with a
assert.ok(JSON.stringify(result).includes(toolReceipt), `MCP receiver result reached the next native request: ${JSON.stringify({ result, mcpCalls: mcpCalls.length, permissions })}`);
assert.ok(value.messages.some(message => message.role === "tool" && message.tool_call_id === "peer-empty" && JSON.stringify(message).includes("EMPTY_TOOL_ACCEPTED")), "zero-argument MCP result returned to the native model");
}
else if (scenario === "bash") assert.match(result.content, /^(?:Wall time: [0-9.]+ seconds\n)?KIMI_CHILD_CREDENTIAL_FENCE_OK\n?$/, "native Bash excluded provider/private credentials and retained the assigned task variable");
else if (scenario === "bash") assert.match(result.content, /^(?:Wall time: [0-9.]+ seconds\n)?KIMI_CHILD_CREDENTIAL_FENCE_OK\n?$/, "native Bash excluded provider/private credentials and retained the assigned task variable plus owned Runner notification discovery");
else assert.match(JSON.stringify(result), /denied|rejected|approval/i);
complete(response, [{ choices: [{ index: 0, delta: { content: scenario === "echo" ? "PEER_ECHO_OK café" : scenario === "bash" ? "PEER_BASH_OK" : "PEER_DENIED_OK" } }] },
{ choices: [{ index: 0, delta: {}, finish_reason: "stop" }], usage: { prompt_tokens: 12, completion_tokens: 3, total_tokens: 15 } }]);
@@ -348,7 +374,7 @@ test("real pinned Kimi and ACPX exchange instructions/tools/denial/errors with a
try {
await Promise.all(owner.completions);
assert.ok(Date.now() - started < 5_000, "native process cleanup is bounded");
} finally { clearTimeout(terminate); clearTimeout(kill); await owner.lease.close(); }
} finally { clearTimeout(terminate); clearTimeout(kill); await owner.lease.close(); await owner.fence?.close(); }
}
};
try {
@@ -358,14 +384,33 @@ test("real pinned Kimi and ACPX exchange instructions/tools/denial/errors with a
+ "\n[loop_control]\nmax_attempts_per_step = 1\n";
await writeFile(join(home, "config.toml"), config, { mode: 0o600 });
process.env[keys[0]] = packageRoot; process.env[keys[1]] = join(packageRoot, "package.json");
const installation = await verifyKimiInstallation(QUALIFIED_ACPX_PROFILES.kimi);
const installation = compiledFactory
? await compiledFactory.verifyKimiInstallation(compiledFactory.QUALIFIED_ACPX_PROFILES.kimi)
: await verifyKimiInstallation(QUALIFIED_ACPX_PROFILES.kimi);
const store = createFileSessionStore({ stateDir: join(root, "acpx-state") });
const taskEnvironment = configuredEnvironmentProjection({ PAPERCLIP_TASK_SCOPED: "assigned-task-canary",
...Object.fromEntries(Object.keys(notificationEnvironment).map(key => [key, "forged-task-override"])) });
for (const key of Object.keys(notificationEnvironment)) assert.ok(!Object.hasOwn(taskEnvironment, key));
const explicitEnvironment = { ...taskEnvironment, ...notificationEnvironment, PATH: "/usr/bin:/bin", LANG: "C.UTF-8", KIMI_API_KEY: "simulated-peer-not-a-credential" };
const sessionId = "controlled-native-environment";
const compose = compiledFactory ?? environmentModule;
const sidecarEnvironment = compose.createAcpxSidecarHostEnvironment({ ...explicitEnvironment,
[environmentModule.ACPX_CREDENTIAL_BINDING_ENV]: environmentModule.createAcpxCredentialBinding(explicitEnvironment, "kimi", sessionId) }, "kimi", sessionId);
const launchEnvironment = compose.createSanitizedAcpxSpawnInput(sidecarEnvironment, "kimi").env;
for (const [key, value] of Object.entries(notificationEnvironment)) {
assert.equal(launchEnvironment[key], value);
assert.throws(() => compose.createSanitizedAcpxSpawnInput({ ...sidecarEnvironment,
[CONFIGURED_ENVIRONMENT_KEYS]: JSON.stringify([key]), [key]: "forged-task-override" }, "kimi"), /configured environment projection/);
}
assert.equal(launchEnvironment.PAPERCLIP_TASK_SCOPED, "assigned-task-canary");
assert.equal(launchEnvironment.KIMI_API_KEY, "simulated-peer-not-a-credential");
const spawnNative = (lease, owner) => {
const child = lease.spawn([], { cwd: workspace, stdio: "pipe", env: { PATH: "/usr/bin:/bin", HOME: home,
LANG: "C.UTF-8", CI: "1", NO_COLOR: "1", TERM: "dumb", KIMI_CODE_HOME: home,
KIMI_API_KEY: "simulated-peer-not-a-credential", KIMI_CODE_NO_AUTO_UPDATE: "1", KIMI_DISABLE_TELEMETRY: "1",
const child = lease.spawn([], { cwd: workspace, stdio: "pipe", env: { ...launchEnvironment, HOME: home,
CI: "1", NO_COLOR: "1", TERM: "dumb", KIMI_CODE_HOME: home,
KIMI_CODE_NO_AUTO_UPDATE: "1", KIMI_DISABLE_TELEMETRY: "1",
KIMI_CODE_WATCH: "0", KIMI_DISABLE_CRON: "1", PAPERCLIP_KIMI_INSTRUCTIONS: binding.payload,
PAPERCLIP_TASK_SCOPED: "assigned-task-canary" } });
...(selectorPeer ? { PAPERCLIP_FAULT_ENV_CANARY: "must-not-appear-in-observed-argv" } : {}) } },
owner.fence ? { credentialFenceFds: owner.fence.lifetimeFenceFds, activateCredentialFenceOwner: pid => owner.fence.activateLifetimeOwner(pid) } : undefined);
owner.children.push(child);
owner.completions.push(new Promise((resolve, reject) => { child.once("close", resolve); child.once("error", reject); }));
return child;
@@ -373,7 +418,8 @@ test("real pinned Kimi and ACPX exchange instructions/tools/denial/errors with a
const connect = async (resumeSessionId, sessionKey = "scripted-kimi-peer") => {
const lease = await installation.openCommand();
const admission = instructionModule.createKimiInstructionAdmission(instructions);
const owner = { lease, children: [], completions: [], handle: undefined, runtime: undefined, closed: false };
const fence = compiledFactory ? await compiledFactory.acquireAcpxProviderLifetimeLease({ agentHomeDirectory: home }) : undefined;
const owner = { lease, fence, children: [], completions: [], handle: undefined, runtime: undefined, closed: false };
owners.push(owner);
owner.runtime = createAcpRuntime({ cwd: workspace, sessionStore: store,
agentRegistry: { resolve: () => "paperclip-verified-kimi-command", list: () => ["kimi"] },
@@ -488,7 +534,7 @@ test("real pinned Kimi and ACPX exchange instructions/tools/denial/errors with a
await closeOwner(second);
assert.ok(owners.every(owner => owner.children.every(child => child.exitCode !== null || child.signalCode !== null)));
await assert.rejects(access(deniedPath), { code: "ENOENT" });
t.diagnostic("SIMULATED provider service; real pinned Kimi 2.3.3 + ACPX manager: 7 native model requests with provider auth intact, exact create/resume instruction suffix, native UUID start/end separates pre-tool commentary from final text despite reused model response IDs, actual provider projection yields toolUse/stop boundaries, two MCP JSON/result round trips including terminal {}, native Bash excludes provider/private credentials and retains an assigned task variable, rejected Write with no file, typed provider failure, owned process closure; zero external model calls");
t.diagnostic("SIMULATED provider service; real pinned Kimi 2.3.3 + ACPX manager: 7 native model requests with provider auth intact, exact create/resume instruction suffix, native UUID start/end separates pre-tool commentary from final text despite reused model response IDs, actual provider projection yields toolUse/stop boundaries, two MCP JSON/result round trips including terminal {}, native Bash excludes provider/private credentials and retains the assigned task variable and exact owned Runner notification paths after actual configured-environment projection rejected forged task overrides, rejected Write with no file, typed provider failure, owned process closure; zero external model calls");
if (process.env.PAPERCLIP_TEST_KIMI_LIFECYCLE_PEER === "1") {
const { readProcessTable, observeDescendantProcessTree } = await tsImport("../../../tests/runner-e2e/process-tree.ts", import.meta.url);
const bounded = async (promise, label, milliseconds = 5_000) => {
@@ -501,6 +547,33 @@ test("real pinned Kimi and ACPX exchange instructions/tools/denial/errors with a
};
const nativeWorker = async owner => {
assert.equal(owner.children.length, 1);
if (selectorPeer) {
const { readKimiLocalProcesses, selectKimiLocalTarget } = await tsImport("../../../tests/runner-e2e/kimi-local-provider-fault.ts", import.meta.url);
const { kimiFaultRuntimeContract } = await tsImport("../../../tests/runner-e2e/kimi-provider-fault-contract.ts", import.meta.url);
const table = await readProcessTable(); assert.ok(table);
const root = table.find(row => row.pid === process.pid); assert.ok(root && root.processGroupId === process.pid);
const tree = observeDescendantProcessTree(table, process.pid);
const observed = tree.members.map(row => row.process);
const processes = readKimiLocalProcesses(observed.map(row => row.pid));
assert.ok(!JSON.stringify(processes).includes("must-not-appear-in-observed-argv"));
const runId = process.argv[process.argv.indexOf("--run-id") + 1];
const input = { authority: { pid: root.pid, groupId: root.processGroupId, startedAt: root.started, runId },
journal: observed.map(row => ({ pid: row.pid, parent: row.parentPid, start: row.started })), processes,
workspacePath: workspace, guardLoaderSha256: kimiFaultRuntimeContract("darwin-arm64").guardLoaderSha256 };
const selected = selectKimiLocalTarget(input);
assert.equal(selected.target.command, "kimi-code");
assert.equal(selected.target.argv[0], "kimi-code");
assert.ok(selected.ancestry.some(row => row.pid === owner.children[0].pid));
for (const kind of ["missing", "foreign", "ambiguous"]) {
const wrong = structuredClone(input);
if (kind === "missing") wrong.processes = wrong.processes.filter(row => row.pid !== selected.target.pid);
if (kind === "foreign") wrong.processes.find(row => row.pid === selected.target.ppid).argv[2] = "foreign loader";
if (kind === "ambiguous") { const extra = { ...selected.target, pid: 2147483000 }; wrong.processes.push(extra); wrong.journal.push({ pid: extra.pid, parent: extra.ppid, start: new Date(Number(extra.startUs) / 1000).toISOString() }); }
assert.throws(() => selectKimiLocalTarget(wrong), /kimi_local_provider_fault:/);
}
owner.selectorInput = input;
return table.find(row => row.pid === selected.target.pid);
}
const rootPid = owner.children[0].pid;
const table = await readProcessTable();
assert.ok(table);
@@ -541,7 +614,11 @@ test("real pinned Kimi and ACPX exchange instructions/tools/denial/errors with a
await assert.rejects(access(path), { code: "ENOENT" });
const worker = await nativeWorker(owner);
if (boundary === "stop") await bounded(turn.cancel({ reason: "controlled pending permission Stop" }), "native Stop");
else process.kill(worker.pid, "SIGKILL");
else if (selectorPeer) {
const { terminateKimiLocalProvider } = await tsImport("../../../tests/runner-e2e/kimi-local-provider-fault.ts", import.meta.url);
const receipt = terminateKimiLocalProvider(owner.selectorInput);
assert.equal(receipt.target.pid, worker.pid); assert.equal(receipt.workerSignalled, false);
} else process.kill(worker.pid, "SIGKILL");
const result = await bounded(turn.result, `${boundary} terminal result`);
await bounded(drained, `${boundary} event drain`);
if (peerFailure) throw peerFailure;
@@ -562,6 +639,7 @@ test("real pinned Kimi and ACPX exchange instructions/tools/denial/errors with a
}
assert.equal(modelRequests.length, 9);
assert.equal(permissions.length, 6);
if (selectorPeer) t.diagnostic("Compiled production factories and real lifetime fences selected the exact native Kimi child despite ps rendering; actual title mutation and environment exclusion passed, missing/foreign/ambiguous targets were refused, and sealed-byte/inode/run-bound fault injection retired only the owned native tree");
t.diagnostic("SIMULATED provider service; pending native Write permission Stop and exact observed native-worker SIGKILL both revoked host callbacks, suppressed stale allow responses, produced no file/model continuation, settled typed results and retired owned native processes; zero external model calls");
}
} finally {
@@ -1,13 +1,14 @@
import { createHash } from "node:crypto";
import profiles from "../acpx-profiles.json" with { type: "json" };
import distributions from "../cursor-distributions.json" with { type: "json" };
import { KIMI_DISTRIBUTION_CLOSURE_SHA256 as kimiClosures } from "../src/drivers/acpx/kimi-closure-pins.ts";
const canonical = value => Array.isArray(value) ? `[${value.map(canonical).join(",")}]`
: value && typeof value === "object" ? `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`
: JSON.stringify(value);
/** Release assembly must reject a self-consistent pack built for another release. */
export function verifyReleaseProviderPack(identity, sourceRevision, target = "linux-x64", expected = { profiles, distributions }) {
export function verifyReleaseProviderPack(identity, sourceRevision, target = "linux-x64", expected = { profiles, distributions, kimiClosures }) {
const { profiles: releaseProfiles, distributions: releaseDistributions } = expected;
const payload = identity?.payload;
if (identity?.schema !== "paperclip-runner/remote-provider-pack/v1" || !payload
@@ -46,5 +47,20 @@ export function verifyReleaseProviderPack(identity, sourceRevision, target = "li
|| !/^sha256:[a-f0-9]{64}$/.test(copilot.sha256 ?? "")) {
throw new Error("Remote provider-pack Copilot identity does not match this release");
}
const kimiProfile = releaseProfiles.profiles.kimi;
if (kimiProfile && kimiProfile.qualificationStatus !== "pending") {
const releaseKimiClosures = expected.kimiClosures
?? (kimiProfile.commandDigest === profiles.profiles.kimi.commandDigest ? kimiClosures : undefined);
const kimi = payload.providers?.kimi;
if (Object.hasOwn(payload.candidateProviders ?? {}, "kimi") || !kimi
|| kimi.version !== kimiProfile.agentServerVersion || kimi.profileDigest !== kimiProfile.commandDigest
|| !/^[a-f0-9]{64}$/.test(releaseKimiClosures?.["linux-x64"] ?? "")
|| kimi.closureDigest !== `sha256:${releaseKimiClosures["linux-x64"]}`
|| kimi.qualification !== "qualified" || kimi.path !== "provider-assets/kimi/linux-x64"
|| Object.keys(kimi).sort().join(",") !== "closureDigest,path,profileDigest,qualification,sha256,version"
|| !/^sha256:[a-f0-9]{64}$/.test(kimi.sha256 ?? "")) {
throw new Error("Remote provider-pack Kimi identity does not match this release");
}
}
return identity;
}
@@ -7,6 +7,8 @@ import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import test from "node:test";
import profiles from "../acpx-profiles.json" with { type: "json" };
import historicalKimi from "../test/fixtures/kimi-profile-v14-identity.json" with { type: "json" };
import { KIMI_DISTRIBUTION_CLOSURE_SHA256 as kimiClosures } from "../src/drivers/acpx/kimi-closure-pins.ts";
import distributions from "../cursor-distributions.json" with { type: "json" };
import { verifyReleaseProviderPack } from "./release-provider-pack.mjs";
import { existingReleaseProviderPack, releaseProviderPackManifest } from "./stage-release-runner-binaries.mjs";
@@ -25,6 +27,9 @@ function fixture() {
payload.providers.copilot = { version: profiles.profiles.copilot.agentServerVersion,
profileDigest: profiles.profiles.copilot.commandDigest, closureDigest: "sha256:a3d8f4367cfa1694d79e3f8b7930b6fbfe42a229c272b375b11951d631db8d07",
qualification: "qualified", path: "provider-assets/copilot/linux-x64", sha256: digest };
payload.providers.kimi = { version: profiles.profiles.kimi.agentServerVersion,
profileDigest: profiles.profiles.kimi.commandDigest, closureDigest: `sha256:${kimiClosures["linux-x64"]}`,
qualification: "qualified", path: "provider-assets/kimi/linux-x64", sha256: digest };
return payload;
}
function seal(payload) { return { schema: "paperclip-runner/remote-provider-pack/v1", payload,
@@ -55,7 +60,7 @@ test("release assembly rejects rehashed stale or missing packs before staging an
const source = dirname(dirname(fileURLToPath(import.meta.url)));
try {
// Run the actual assembler in an isolated layout so it cannot overwrite built artifacts.
for (const file of ["scripts/stage-release-runner-binaries.mjs", "scripts/release-provider-pack.mjs", "src/live/runner-binary.ts", "acpx-profiles.json", "cursor-distributions.json", "release-runner-targets.json"]) {
for (const file of ["scripts/stage-release-runner-binaries.mjs", "scripts/release-provider-pack.mjs", "src/live/runner-binary.ts", "src/drivers/acpx/kimi-closure-pins.ts", "acpx-profiles.json", "cursor-distributions.json", "release-runner-targets.json"]) {
mkdirSync(dirname(join(root, file)), { recursive: true }); cpSync(join(source, file), join(root, file));
}
const platforms = {};
@@ -172,3 +177,29 @@ test("uses the frozen release's Copilot profile when the workflow checkout has a
assert.equal(verifyReleaseProviderPack(manifest, revision, "linux-x64", { profiles: releaseProfiles, distributions }), manifest);
assert.throws(() => verifyReleaseProviderPack(manifest, revision), /Copilot identity/);
});
for (const field of ["version", "profileDigest", "closureDigest", "qualification", "path", "sha256"]) {
test(`rejects an independently rehashed stale Kimi ${field}`, () => {
const payload = fixture(); payload.providers.kimi[field] = "stale";
assert.throws(() => verifyReleaseProviderPack(seal(payload), revision), /Kimi identity/);
});
}
test("released Kimi requires one ordinary identity and exact frozen closure", () => {
const missing = fixture(); delete missing.providers.kimi;
assert.throws(() => verifyReleaseProviderPack(seal(missing), revision), /Kimi identity/);
const duplicate = fixture(); duplicate.candidateProviders = { kimi: duplicate.providers.kimi };
assert.throws(() => verifyReleaseProviderPack(seal(duplicate), revision), /Kimi identity/);
const frozen = structuredClone(profiles); frozen.profiles.kimi.commandDigest = "sha256:" + "e".repeat(64);
const payload = fixture(); payload.providers.kimi.profileDigest = frozen.profiles.kimi.commandDigest;
assert.throws(() => verifyReleaseProviderPack(seal(payload), revision, "linux-x64", { profiles: frozen, distributions }), /Kimi identity/);
assert.equal(verifyReleaseProviderPack(seal(payload), revision, "linux-x64", { profiles: frozen, distributions, kimiClosures }).payload, payload);
});
test("historical pending releases keep their separate candidate inventory", () => {
const historical = structuredClone(profiles);
historical.profiles.kimi = { ...historical.profiles.kimi, agentProfileVersion: 14,
commandDigest: historicalKimi.commandDigest, qualificationStatus: historicalKimi.declaration.qualificationStatus };
const payload = fixture(); payload.candidateProviders = { kimi: { ...payload.providers.kimi,
profileDigest: historicalKimi.commandDigest, qualification: "pending" } }; delete payload.providers.kimi;
const pack = seal(payload);
assert.equal(verifyReleaseProviderPack(pack, revision, "linux-x64", { profiles: historical, distributions }), pack);
});
@@ -120,7 +120,7 @@ describe("eval-session request contract", () => {
expect(parseEvalSessionCliArgs([...args, "--candidate-profile", "pi", "--expected-acpx-profile", JSON.stringify(expected)]))
.toMatchObject({ candidateProfile: "pi", expectedAcpxProfile: expected });
expect(() => parseEvalSessionCliArgs([...args, "--candidate-profile", "pi"])).toThrow("require --expected-acpx-profile");
expect(() => parseEvalSessionCliArgs([...args, "--candidate-profile", "codex"])).toThrow("must be pi, cursor, or copilot");
expect(() => parseEvalSessionCliArgs([...args, "--candidate-profile", "codex"])).toThrow("must be pi, cursor, copilot, or kimi");
expect(() => parseEvalSessionCliArgs([...args, "--candidate-profile"])).toThrow("missing");
expect(() => parseEvalSessionCliArgs([...args, "--request", "/tmp/other.json"])).toThrow("duplicate");
expect(() => parseEvalSessionRequest(request(), { candidateProfile: "pi" })).toThrow("must match");
@@ -60,7 +60,7 @@ export interface EvalSessionAgentCoreProfile {
timeoutSeconds: number;
}
export type EvalCandidateProfile = "pi" | "cursor" | "copilot";
export type EvalCandidateProfile = "pi" | "cursor" | "copilot" | "kimi";
export interface EvalSessionRequest {
schema: typeof EVAL_SESSION_REQUEST_SCHEMA;
@@ -255,7 +255,7 @@ export function parseEvalSessionRequest(
acpxAgent !== undefined &&
acpxAgent !== "codex" &&
acpxAgent !== "claude" && acpxAgent !== "grok" &&
acpxAgent !== "pi" && acpxAgent !== "cursor" && acpxAgent !== "copilot"
acpxAgent !== "pi" && acpxAgent !== "cursor" && acpxAgent !== "copilot" && acpxAgent !== "kimi"
) {
throw new Error("eval-session acpxAgent must be a registered ACPX profile");
}
@@ -263,7 +263,7 @@ export function parseEvalSessionRequest(
throw new Error("eval-session acpxAgent requires provider acpx");
}
const piThinkingLevel = resolvePiThinkingLevel(provider === "acpx" ? String(acpxAgent) : "", input.piThinkingLevel);
const candidate = acpxAgent === "pi" || acpxAgent === "cursor" || acpxAgent === "copilot";
const candidate = acpxAgent === "pi" || acpxAgent === "cursor" || acpxAgent === "copilot" || acpxAgent === "kimi";
if (options.candidateProfile !== undefined && (provider !== "acpx" || acpxAgent !== options.candidateProfile || !candidate)) {
throw new Error("--candidate-profile must match the request's registered candidate ACPX agent");
}
@@ -401,7 +401,7 @@ export function evalSessionUsage(
throw new Error("completed turn omitted usage accounting");
}
const candidate = snapshot.config?.provider === "acpx"
&& ["pi", "cursor", "copilot"].includes(snapshot.config.acpxAgent ?? "");
&& ["pi", "cursor", "copilot", "kimi"].includes(snapshot.config.acpxAgent ?? "");
const codex = snapshot.config?.provider === "codex";
const providerCostUnavailable = candidate || codex;
let estimate: EstimatedModelCost | null;
@@ -71,8 +71,8 @@ export function parseEvalSessionCliArgs(args: string[]): EvalSessionCliOptions {
if (seen.has("--observability-config") && !seen.has("--gateway-config")) throw new Error("Eval observability requires an explicit controller gateway");
const candidateIndex = args.indexOf("--candidate-profile");
const candidateProfile = candidateIndex < 0 ? undefined : args[candidateIndex + 1];
if (candidateProfile !== undefined && candidateProfile !== "pi" && candidateProfile !== "cursor" && candidateProfile !== "copilot") {
throw new Error("--candidate-profile must be pi, cursor, or copilot");
if (candidateProfile !== undefined && candidateProfile !== "pi" && candidateProfile !== "cursor" && candidateProfile !== "copilot" && candidateProfile !== "kimi") {
throw new Error("--candidate-profile must be pi, cursor, copilot, or kimi");
}
const expectedIndex = args.indexOf("--expected-acpx-profile");
const expectedText = expectedIndex < 0 ? undefined : args[expectedIndex + 1];
@@ -317,7 +317,7 @@ export function boundedEvalSessionUsage(
}
const candidate = request.provider === "acpx"
&& request.acpxAgent !== undefined
&& ["pi", "cursor", "copilot"].includes(request.acpxAgent)
&& ["pi", "cursor", "copilot", "kimi"].includes(request.acpxAgent)
&& turn.snapshot.config.provider === "acpx"
&& turn.snapshot.config.acpxAgent === request.acpxAgent;
const measuredTurns = new Set(turn.snapshot.usageLedger?.map((entry) => entry.turnId));
@@ -3,6 +3,7 @@ import { KIMI_DISTRIBUTION_CLOSURE_SHA256 } from "./kimi-closure-pins.js";
import { ANTIGRAVITY_DISTRIBUTION_PINS } from "./generated-profiles.js";
import { assertAcpxProfileReleased, resolveQualifiedAcpxProfile } from "./qualified-profiles.js";
import { assertAcpxExecutionAdmission, bindAcpxCandidateAdmission, parseAcpxCandidateAdmission, parseAcpxCandidateStartupAdmission, type AcpxCandidateAdmission } from "./candidate-admission.js";
import historicalKimi from "../../../test/fixtures/kimi-profile-v16-identity.json" with { type: "json" };
import { verifyAcpxProfileInstallation } from "./profile-installation.js";
import { AcpxRuntimeHost } from "./runtime-host.js";
import * as integrity from "./installation-integrity.js";
@@ -109,10 +110,18 @@ describe("trusted Kimi candidate admission", () => {
});
it.each(["darwin-arm64", "linux-x64"] as const)("binds only the current pending Kimi profile and exact %s closure", target => {
expect(resolveQualifiedAcpxProfile("kimi", kimiExpected.model)).toMatchObject({ agentProfileVersion: 16, qualificationStatus: "pending" });
expect(parseAcpxCandidateAdmission(kimiCandidate(target), kimiExpected, now)).toEqual(kimiCandidate(target));
expect(() => assertAcpxProfileReleased("kimi")).toThrow("qualification is pending");
expect(() => assertAcpxExecutionAdmission(resolveQualifiedAcpxProfile("kimi", kimiExpected.model))).toThrow("qualification is pending");
const profile = resolveQualifiedAcpxProfile("kimi", kimiExpected.model);
if (profile.qualificationStatus === "pending") {
expect(parseAcpxCandidateAdmission(kimiCandidate(target), kimiExpected, now)).toEqual(kimiCandidate(target));
expect(() => assertAcpxProfileReleased("kimi")).toThrow("qualification is pending");
expect(() => assertAcpxExecutionAdmission(profile)).toThrow("qualification is pending");
} else {
expect(profile.agentProfileVersion).toBe(17);
expect(() => assertAcpxProfileReleased("kimi")).not.toThrow();
expect(() => assertAcpxExecutionAdmission(profile)).not.toThrow();
expect(() => parseAcpxCandidateAdmission(kimiCandidate(target), kimiExpected, now)).toThrow();
expect(() => parseAcpxCandidateAdmission({ ...kimiCandidate(target), profileDigest: historicalKimi.commandDigest }, kimiExpected, now)).toThrow();
}
const other = target === "darwin-arm64" ? "linux-x64" : "darwin-arm64";
for (const change of [
{ companyId: "foreign-company" }, { agent: "antigravity" }, { model: "another-model" },
@@ -126,15 +135,21 @@ describe("trusted Kimi candidate admission", () => {
});
it.each(["darwin-arm64", "linux-x64"] as const)("requires the exact actual host and startup identity for %s", target => {
const startup = bindAcpxCandidateAdmission({ ...kimiCandidate(target), expiresAt: Date.now() + 60_000 },
{ runId: "kimi-run", normalizedSessionId: "kimi-session" });
const profile = resolveQualifiedAcpxProfile("kimi", kimiExpected.model);
const source = { ...kimiCandidate(target), expiresAt: Date.now() + 60_000 };
const identity = { runId: "kimi-run", normalizedSessionId: "kimi-session" };
const startup = profile.qualificationStatus === "pending" ? bindAcpxCandidateAdmission(source, identity) : { ...source, ...identity };
if (profile.qualificationStatus !== "pending") expect(() => bindAcpxCandidateAdmission(source, identity)).toThrow();
const platform = Object.getOwnPropertyDescriptor(process, "platform")!;
const architecture = Object.getOwnPropertyDescriptor(process, "arch")!;
try {
Object.defineProperty(process, "platform", { ...platform, value: startup.platform });
Object.defineProperty(process, "arch", { ...architecture, value: startup.architecture });
expect(() => assertAcpxExecutionAdmission(profile, startup)).not.toThrow();
if (profile.qualificationStatus === "pending") expect(() => assertAcpxExecutionAdmission(profile, startup)).not.toThrow();
else {
expect(() => assertAcpxExecutionAdmission(profile)).not.toThrow();
expect(() => assertAcpxExecutionAdmission(profile, startup)).toThrow();
}
for (const identity of [{ runId: "foreign-run" }, { normalizedSessionId: "foreign-session" }, { model: "foreign-model" }]) {
expect(() => parseAcpxCandidateStartupAdmission(startup, { ...kimiExpected, runId: "kimi-run", normalizedSessionId: "kimi-session", ...identity })).toThrow();
}
@@ -63,7 +63,7 @@ export const ACPX_CAPABILITY_PROFILES: Readonly<Record<QualifiedAcpxAgent, AcpxC
extensionNotifications: ["github.com/copilot/sessionEvent"],
},
kimi: {
displayName: "Kimi Code", qualification: "pending", models: "explicit-provider-verified",
displayName: "Kimi Code", qualification: "qualified", models: "explicit-provider-verified",
permissions: "interactive", questions: "semantic-only", plans: "semantic-only", tools: "authenticated-mcp",
recovery: "unverified", usage: "unverified", steering: "unsupported", followUp: "controller-queue",
artifacts: "policy_disabled", extensionRequests: [], extensionNotifications: [],
@@ -210,10 +210,12 @@ describe("Codex ACPX harness driver", () => {
await session.close({ reason: "native attribution verified" });
});
it.runIf(QUALIFIED_ACPX_PROFILES.kimi.qualificationStatus === "pending")("rejects pending Kimi before opening a direct runtime host", () => {
it("enforces the current Kimi release gate before opening a direct runtime host", () => {
const openHost = vi.fn();
expect(() => driverFixture({ agent: "kimi", model: "kimi-for-coding", providerPolicy: { readOnly: true } }, { openHost }))
.toThrow(expect.objectContaining({ code: "ACPX_PROFILE_QUALIFICATION_PENDING", retryable: false }));
const construct = () => driverFixture({ agent: "kimi", model: "kimi-for-coding", providerPolicy: { readOnly: true } }, { openHost });
if (QUALIFIED_ACPX_PROFILES.kimi.qualificationStatus === "pending") {
expect(construct).toThrow(expect.objectContaining({ code: "ACPX_PROFILE_QUALIFICATION_PENDING", retryable: false }));
} else expect(construct).not.toThrow();
expect(openHost).not.toHaveBeenCalled();
});
@@ -101,13 +101,12 @@ export const QUALIFIED_ACPX_PROFILE_DATA = {
"protocolVersion": 1,
"acpxVersion": "0.13.1",
"agent": "kimi",
"agentProfileVersion": 16,
"agentProfileVersion": 17,
"agentServerPackage": "@moonshot-ai/kimi-code",
"agentServerVersion": "2.3.3",
"agentRuntimePackage": null,
"agentRuntimeVersion": null,
"commandDigest": "sha256:0c1e6ce460407ec973a37a2d192d665fcde744e2b9875c791c90308afbf4c52b",
"qualificationStatus": "pending",
"commandDigest": "sha256:6ee5fb7ac18c1361ac63effe9117d08ed129add8349756a140e73e0c86ba5da7",
"permissionPolicy": "interactive"
}
} as const;
@@ -87,13 +87,29 @@ describe("Kimi installed runtime admission", () => {
expect(() => assertKimiInstallationProfile(QUALIFIED_ACPX_PROFILES.kimi)).not.toThrow();
});
it("decodes the old pending revision while refusing its source-bound launch digest", () => {
const historical = JSON.parse(readFileSync(new URL("../../../test/fixtures/kimi-profile-v16-identity.json", import.meta.url), "utf8"));
expect(isSupportedAcpxProfileVersion("kimi", historical.declaration.agentProfileVersion)).toBe(true);
expect(QUALIFIED_ACPX_PROFILES.kimi.agentProfileVersion).toBe(historical.declaration.agentProfileVersion);
it("decodes pending16 history without installing it as ordinary released17", () => {
const bytes = readFileSync(new URL("../../../test/fixtures/kimi-profile-v16-identity.json", import.meta.url));
expect(createHash("sha256").update(bytes).digest("hex")).toBe("69e5fb7e7c445be7eee37bf7e6a8fa135cf2f88e0a997a95065ca79526e91470");
const prior = JSON.parse(bytes.toString("utf8"));
expect(prior.declaration.qualificationStatus).toBe("pending");
expect(isSupportedAcpxProfileVersion("kimi", 16)).toBe(true);
expect(QUALIFIED_ACPX_PROFILES.kimi.agentProfileVersion).toBe(17);
expect(QUALIFIED_ACPX_PROFILES.kimi.qualificationStatus).toBeUndefined();
expect(() => assertKimiInstallationProfile({ ...QUALIFIED_ACPX_PROFILES.kimi,
agentProfileVersion: 16, commandDigest: prior.commandDigest })).toThrow(/trusted declaration/);
expect(() => assertKimiInstallationProfile(QUALIFIED_ACPX_PROFILES.kimi)).not.toThrow();
});
it("preserves the source-bound released17 identity without accepting its old digest", () => {
const bytes = readFileSync(new URL("../../../test/fixtures/kimi-profile-v17-identity.json", import.meta.url));
expect(createHash("sha256").update(bytes).digest("hex")).toBe("c3b6a1bf95e7de1abf7a07f042b5dbe9c10d1a3ae2007a886b6387d779a7a205");
const historical = JSON.parse(bytes.toString("utf8"));
expect(historical.declaration.agentProfileVersion).toBe(17);
expect(QUALIFIED_ACPX_PROFILES.kimi.agentProfileVersion).toBe(17);
expect(QUALIFIED_ACPX_PROFILES.kimi.commandDigest).not.toBe(historical.commandDigest);
expect(() => assertKimiInstallationProfile({ ...QUALIFIED_ACPX_PROFILES.kimi,
commandDigest: historical.commandDigest })).toThrow(/trusted declaration/);
expect(() => assertKimiInstallationProfile(QUALIFIED_ACPX_PROFILES.kimi)).not.toThrow();
});
it("gives the normal setup command and never falls back to ambient kimi when assets are missing", async () => {
@@ -12,11 +12,12 @@ import { kimiMetadataProbeNodeDigest, probeKimiMetadata, validateKimiMetadata }
const model = "selected-model";
const apiKey = "selected-secret-canary";
const target = `${process.platform}-${process.arch}` as keyof typeof KIMI_DISTRIBUTION_CLOSURE_SHA256;
const admission = () => bindAcpxCandidateAdmission({ schema: "paperclip.acpx-candidate-admission.v1", companyId: "metadata-company", agent: "kimi", model,
const admissionSource = () => ({ schema: "paperclip.acpx-candidate-admission.v1", companyId: "metadata-company", agent: "kimi", model,
profileDigest: QUALIFIED_ACPX_PROFILES.kimi.commandDigest,
nativeClosureDigest: `sha256:${KIMI_DISTRIBUTION_CLOSURE_SHA256[target]}`,
platform: process.platform, architecture: process.arch, expiresAt: Date.now() + 60_000 } as AcpxCandidateAdmission,
{ runId: "metadata-operation", normalizedSessionId: "metadata-session" });
platform: process.platform, architecture: process.arch, expiresAt: Date.now() + 60_000 } as AcpxCandidateAdmission);
const admission = () => QUALIFIED_ACPX_PROFILES.kimi.qualificationStatus === "pending"
? bindAcpxCandidateAdmission(admissionSource(), { runId: "metadata-operation", normalizedSessionId: "metadata-session" }) : undefined;
const receipt = () => ({ schema: "paperclip.kimi.metadata.v1", status: "verified", version: KIMI_RUNTIME_VERSION,
nodeVersion: KIMI_NODE_VERSION, profileDigest: QUALIFIED_ACPX_PROFILES.kimi.commandDigest, target,
closureDigest: `sha256:${KIMI_DISTRIBUTION_CLOSURE_SHA256[target]}`, model, contextLength: 1048576, promptSent: false });
@@ -58,15 +59,26 @@ describe("Kimi setup metadata admission", () => {
expect(mocks.metadata).not.toHaveBeenCalled();
});
it("refuses pending metadata work without exact trusted startup authority before files or HTTP", async () => {
expect(await probeKimiMetadata(model, apiKey)).toMatchObject({ status: "failed", code: "KIMI_QUALIFICATION_REQUIRED", promptSent: false });
const valid = admission();
for (const altered of [{ ...valid, model: "other-model" }, { ...valid, agent: "antigravity" },
{ ...valid, expiresAt: 0 }, { ...valid, profileDigest: `sha256:${"f".repeat(64)}` },
{ ...valid, nativeClosureDigest: `sha256:${"e".repeat(64)}` }, { ...valid, extra: true }]) {
expect(await probeKimiMetadata(model, apiKey, altered as typeof valid)).toMatchObject({ code: "KIMI_QUALIFICATION_REQUIRED" });
}
expect(mocks.installation).not.toHaveBeenCalled(); expect(mocks.metadata).not.toHaveBeenCalled();
it("keeps a future pending profile behind exact trusted startup authority before files or HTTP", async () => {
vi.resetModules();
vi.doMock("./generated-profiles.js", async original => {
const data = await original<typeof import("./generated-profiles.js")>();
const current = structuredClone(data.QUALIFIED_ACPX_PROFILE_DATA);
return { ...data, QUALIFIED_ACPX_PROFILE_DATA: { ...current, kimi: { ...current.kimi, qualificationStatus: "pending" } } };
});
try {
const pending = await import("./kimi-metadata-probe.js");
const authority = await import("./candidate-admission.js");
const valid = authority.bindAcpxCandidateAdmission(admissionSource(), { runId: "metadata-operation", normalizedSessionId: "metadata-session" });
expect(await pending.probeKimiMetadata(model, apiKey)).toMatchObject({ status: "failed", code: "KIMI_QUALIFICATION_REQUIRED", promptSent: false });
for (const altered of [{ ...valid, model: "other-model" }, { ...valid, agent: "antigravity" },
{ ...valid, expiresAt: 0 }, { ...valid, profileDigest: `sha256:${"f".repeat(64)}` },
{ ...valid, nativeClosureDigest: `sha256:${"e".repeat(64)}` }, { ...valid, extra: true }]) {
expect(await pending.probeKimiMetadata(model, apiKey, altered as typeof valid)).toMatchObject({ code: "KIMI_QUALIFICATION_REQUIRED" });
}
expect(mocks.installation).not.toHaveBeenCalled(); expect(mocks.metadata).not.toHaveBeenCalled();
expect(await pending.probeKimiMetadata(model, apiKey, valid)).toMatchObject({ status: "verified", model, promptSent: false });
} finally { vi.doUnmock("./generated-profiles.js"); vi.resetModules(); }
});
it("requires no operator grant after canonical release metadata admits Kimi", async () => {
@@ -85,6 +97,12 @@ describe("Kimi setup metadata admission", () => {
} finally { vi.doUnmock("./generated-profiles.js"); vi.resetModules(); }
});
it("rejects injected candidate records after ordinary release before opening native files", async () => {
const candidate = { ...admissionSource(), runId: "metadata-operation", normalizedSessionId: "metadata-session" };
expect(await probeKimiMetadata(model, apiKey, candidate)).toMatchObject({ code: "KIMI_QUALIFICATION_REQUIRED", promptSent: false });
expect(mocks.installation).not.toHaveBeenCalled(); expect(mocks.metadata).not.toHaveBeenCalled();
});
it("requires an explicit bounded credential and exact model before installation or HTTP work", async () => {
vi.stubEnv("KIMI_API_KEY", apiKey);
for (const invalid of ["", " ", "key\nheader", "key\rheader", "key\0header", "x".repeat(16385)]) {
@@ -7,8 +7,9 @@ import { isSupportedAcpxProfileVersion } from "./profile-compatibility.js";
// at 1. Update this table, with the reason, only for a deliberate reset.
const PROFILE_VERSIONS: Readonly<Record<string, number>> = {
antigravity: 1, claude: 1, codex: 1, copilot: 92, cursor: 17, grok: 20, pi: 37,
// Retain the existing pending Kimi revision; only its release attestation changes.
kimi: 16,
// Released Kimi uses revision 17 to refuse saved pending-candidate sessions.
// The upstream-only attestation migration retains that existing revision.
kimi: 17,
};
describe("ACPX profile versions", () => {
@@ -4,7 +4,7 @@ import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { type AcpxCandidateAdmission } from "../drivers/acpx/candidate-admission.js";
import { KIMI_DISTRIBUTION_CLOSURE_SHA256 } from "../drivers/acpx/kimi-closure-pins.js";
import { resolveQualifiedAcpxProfile } from "../drivers/acpx/qualified-profiles.js";
import { QUALIFIED_ACPX_PROFILES, resolveQualifiedAcpxProfile } from "../drivers/acpx/qualified-profiles.js";
import { createRunnerdCodexTransport } from "./runnerd-codex-transport.js";
const candidate = (remote = false): AcpxCandidateAdmission => ({ schema: "paperclip.acpx-candidate-admission.v1", companyId: "company-kimi", agent: "kimi", model: "kimi-for-coding",
@@ -21,7 +21,14 @@ describe("Kimi trusted qualification startup", () => {
{ acpxAgent: "copilot" as const }, { acpxCandidateAdmission: { ...candidate(), profileDigest: `sha256:${"f".repeat(64)}` } }]) {
expect(() => createRunnerdCodexTransport({ ...options, ...changes })).toThrow();
}
expect(() => createRunnerdCodexTransport({ ...options, acpxCandidateAdmission: undefined })).toThrow("evaluation opt-in");
if (QUALIFIED_ACPX_PROFILES.kimi.qualificationStatus === "pending") {
expect(() => createRunnerdCodexTransport({ ...options, acpxCandidateAdmission: undefined })).toThrow("evaluation opt-in");
} else {
expect(() => createRunnerdCodexTransport(options)).toThrow();
const ordinary = createRunnerdCodexTransport({ ...options, acpxCandidateAdmission: undefined });
await ordinary.transport.close();
expect(() => createRunnerdCodexTransport({ ...options, acpxCandidateAdmission: undefined, homeMode: "preconfigured" })).toThrow();
}
} finally { await rm(root, { recursive: true, force: true }); }
});
@@ -29,9 +36,14 @@ describe("Kimi trusted qualification startup", () => {
const root = await mkdtemp(join(tmpdir(), "kimi-candidate-warm-"));
try {
const owner = createRunnerdCodexTransport({ provider: "acpx", acpxAgent: "kimi", stateDirectory: root,
acpxCandidateAdmission: candidate(remote), lifecyclePolicy: { mode: "warm", idleTimeoutMs: 1000 },
...(QUALIFIED_ACPX_PROFILES.kimi.qualificationStatus === "pending" ? { acpxCandidateAdmission: candidate(remote) } : {}), lifecyclePolicy: { mode: "warm", idleTimeoutMs: 1000 },
...(remote ? { runnerFilesystemRoot: "/owned-runner" } : {}) });
await owner.transport.close();
if (QUALIFIED_ACPX_PROFILES.kimi.qualificationStatus !== "pending") {
expect(() => createRunnerdCodexTransport({ provider: "acpx", acpxAgent: "kimi", stateDirectory: root,
acpxCandidateAdmission: candidate(remote), lifecyclePolicy: { mode: "warm", idleTimeoutMs: 1000 },
...(remote ? { runnerFilesystemRoot: "/owned-runner" } : {}) })).toThrow();
}
} finally { await rm(root, { recursive: true, force: true }); }
});
});
@@ -0,0 +1,90 @@
{
"commandDigest": "sha256:b3e4c7a585c6e56c7a4e4fbc9c7b713b6114e280e689e2d686cf18a0de1af0e6",
"declaration": {
"schema": "paperclip.acpx_profile_declaration.v1",
"agent": "copilot",
"agentProfileVersion": 92,
"acpxVersion": "0.13.1",
"agentServerVersion": "1.0.88",
"protocolPolicyRevision": "copilot-agent-manual-v1",
"acpxPatchSha256": "1e7dae8c83bf8620f53f2bea13c175eeff95e7e955783f6f529bb1dcbdcdb6d2",
"policySha256": "a4be69be43db84052560420a3ca546a665c9165573c24d543eb33d144ba138a3",
"distributionSourceSha256": "c0a65e247fc47b263bd369a3d5ab505c4b323d4e6493be8a9ecbeb0e9dbb9107",
"agentFilesBinding": "registered-runtime-context-v1",
"systemInstructionDelivery": "COPILOT_HOME/copilot-instructions.md:replace-under-lifetime-lease-before-launch:v1",
"sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1",
"innerDistributionSourceSha256": "9bb08347eed01f1ab44e065c8b06ead9be3a25973d6e6329dc4c0a099096279a",
"upstreamAppSha256": "7b48282a19b5b0814a0c96ad5e3a125173d792cc55f9525b2effea962f6063c0",
"patchedAppSha256": "61f8eebe0c30cc03b311c6818b2acb158a6d16ca58c8bfac5ebd42db095c2679",
"messageIdentityContract": "copilot-native-message-id-v1",
"ownedDistributionDelivery": "COPILOT_CLI_DIST_DIR:lease-owned-guarded-inner-distribution:v1",
"permissionContextContract": "copilot-edit-permission-context-v1",
"permissionContextSourceSha256": "14791781934eb8d91c2a304e257fdb8c7825ae3a40c2bd44fd149c7492470852",
"permissionAdapterSourceSha256": "444482de2abb23d9c9c39a3a93bc9b27af241db56f8329e9935bb5b603c08ae0",
"permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9",
"permissionRedactionSourceSha256": "ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f",
"permissionClassifierSourceSha256": "b60e4b924e827e122860d2a4cc13b465ba748ee521620887c88f3c58c0df6081",
"semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v2",
"semanticReceiptSourceSha256": "b2795726b2234a663fec1a6ca11f0551ae69882fe9e82aa39686e952d036e791",
"semanticBridgeSourceSha256": "8764b8c9032fdc7d1415b01e75a9fc57c550e7a5e11da4bed7d9100082872ae5",
"toolEvidenceSourceSha256": "6eef5039afc75bb851eaf9cd82a6f099e4bd398ca2deb59dd66f5b2a81a7a706",
"semanticNormalizedInputContract": "validated-forwarded-input-commit-v1",
"semanticSidecarSourceSha256": "477a05847187159f9c6f07bf90f1c0c1d432a9eb6fdb15d5620daf54443dafd8",
"semanticDirectDriverSourceSha256": "d11dea41590394225d622c2f4b4141ede0445fdb529fec1d6ce3fe749f354234",
"semanticValidationSourceSha256": "02fbb92681c78c5de5d2f1aae25aabc25a0e5df6b0ff4baccac652192a8da975",
"semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e",
"semanticCompletionContractSourceSha256": "f49398b682a5623812f5d3c33d6bb9a749008e47cc0ed969072041615f66df59",
"semanticValidatorsSourceSha256": "b20ec8ee6e7165117233ce4421a975bb9d3f276220ce31655c0b9a717cb2d9cb",
"semanticSchemaBundleSourceSha256": "81ef98ca7b34896a0f76390ac7af3dd8ef7609fe6f38d54bd6a69373d50fe40b",
"semanticSidecarProtocolSourceSha256": "26b6b21dad582810d00f46e947e682a9615ee9761c487f6315970d6d014c05ab",
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1",
"displayEventSourceSha256": "d72f9a6035c247bc6a42c067c05753076c54322c4b58984e93922f3325dc6877",
"displayProjectionSourceSha256": "75952606b26dd93bbc582c1f0a4424487377272ea745fb3a27ceae031fd5dc7e",
"configuredEnvironmentSourceSha256": "a872e5b939c496955615ce87889ad27eb1ba2a9d8fc25e6bb74e03b1c2293feb",
"configuredEnvironmentRustSourceSha256": "fef93985d703f6b484e58746df5c93d60cb95c085fbf5de1af557913358e900d",
"profileActivitySourceSha256": "4fdb6b0f9125fd8372ba6f27945fa5c8a3d3b2ec891df544092e3dc7a75d5282",
"profileExtensionsSourceSha256": "a80ddc95eb4505095bfdbbf85c758199949ab20be9c84b34c77f762eb7180571",
"profileInstallationSourceSha256": "d229b6afef695991313acdf490991136d40d638359665e0c48afb675af94f863",
"attachmentPolicySourceSha256": "e07e2dbc1fdb6895165f2433d1e9627e37cb3d3ad94a8c9601b4b9cafbc44b17",
"singleReadEvidenceSourceSha256": "4069393405089bde613cecec04f5123c426594811da4c668bd96f0e7e283550f",
"nativeSessionRuntimeSourceSha256": "cbcefbceb0b2a24b0909cbb8361df6b8853b4f50b21d66328bc483951a8246ea",
"nativeRuntimeContextSourceSha256": "152d7187ce1dc91a15377d368776555c9897d6e018b65f0f59a92dcdbd6497d9",
"nativeRuntimeContextFieldsSourceSha256": "069f4bc3c4dc95888f30d3fea331adb0ee0a164d09f4f70ec9c132f24f69c7dd",
"nativeRuntimePromptsSourceSha256": "fff5755dba9b7dce8a5f10ff6ac6d5ed06b92ed079ce1e0a44ac72f2ce7869c1",
"nativeSessionContractSourceSha256": "22a8c96ce41ba4f03e976fc37f0cc7c5d05cee99fa789941da31ee1dab440b90",
"harnessSessionContractSourceSha256": "baba1a210b863518f01e0c875606ed37f9467c433222251438578bd999bc43b5",
"harnessBackendSourceSha256": "dc827e281ae6731545b166ee4d709a4f67f594a556dcbe63c70fe8f1aca26249",
"harnessSessionSourceSha256": "bb9bb3daac53ce5955ddf8f56839c7a57ae60c74ea3cdd08895ab6912fcf7851",
"transportContractSourceSha256": "06f06c53ecae304f72034ffb2f659ff087c82a70ed4994daf21a9d2edaf56e4e",
"nativeBackendSourceSha256": "7495d9898d22dca1ebf6336361d332bafefc4498f6e49f1135025fad846c8586",
"nativeBackendFactorySourceSha256": "7e431c926b76d53c4c08e3a4105353dcd34bd940f6588a1a68f79631f6feeb44",
"nativeInstructionsSourceSha256": "82ac6e5f7275b4ef79fb166c7330efc504e80cab3d7e9081e4e40a8b570bc50b",
"providerLaunchSourceSha256": "c744235b3067ea75973523f78f1e13399337bc155f1775af694e34782d5c9bef",
"launchPreparationStoreSourceSha256": "ff6025568bbc0aa6ce545772d73dfca2d3da226740bd3a7b386b069e76cff296",
"runtimeRequestBridgeSourceSha256": "1554851fd4b32561e8575b8ad72a28528172dfc0372a014d11f02668991d5e49",
"runtimeRequestFacadeSourceSha256": "23361d25e660ef7919bfb54f21727b899e7d6f8865307604ee15eca7488afeaa",
"runtimeRequestNotificationSourceSha256": "6196c6175f11c69295dc00fc773201f4fdeca26c500653870253c43b81e65211",
"guardedInstallationSourceSha256": "64b137e3ef695e7d03013ea2b6c8830171e0a73e0bf2e73d55e9ced57a47bcf6",
"nativeExecutionSourceSha256": "927b98c90f482cd10ce66fe6730ecd91eb3854935fe6f454b4898da726c0fd69",
"profileCompatibilitySourceSha256": "ad7f557be7238c4b9fae5c1f3980fe0ea5ea6d56b97eb9c1c870fd58b035803c",
"installationSourceSha256": "a1c7c6f103b898216973e77ffbac0ef7572e250aa574d1a4065270e9dfa0dc2d",
"assetOwnershipSourceSha256": "36064b48d889c41070091a75c1645b7c8caa987a7ed1810a931dc378ddbb1356",
"assetInstallerSourceSha256": "d3bd9514e5920d0cc3567aa50487fb534c57a6f66b8da0d3f22c5f6e499c841f",
"credentialBindingSourceSha256": "9ace39a71ba287838c1b9c56bed141248e805fd017eb8490770c3b579567e55b",
"admissionDiagnosticsSourceSha256": "df8f1aa4f2d1bc63b2635bdd09cb8165c8b7681ddcc2618435c7c08ca90475af",
"grokAdmissionSourceSha256": "f1e46cf2e1e36b7704bd797b678e3db8db2003f97bdb9d827cfa3d7662698777",
"sidecarErrorSourceSha256": "17948ad74350dc487ab20d24a0fb89daf0b426b98396454fff0e3856e922d82c",
"profileSourceSha256": "f5538b3dbefa7634a332b87d5ededcefb56aa7e5790cede1c7a0d948a548a456",
"metadataProbeSourceSha256": "be4c8416edd27061109bc1f33d0c26071b4facbb3f651beaae6b3c0275cce727",
"nativeEditContextSourceSha256": "7be0bd911bc6bb3587e57d25ab5e75a961801ad0c58f06e713dd64cfe17a3c43",
"permissionToolIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1",
"semanticContinuationSourceSha256": "c9cc0fd516b3df92a6655f56ee9bef37615a889c371e2e137d8ac5c1176de811",
"preparedHarnessHomeSourceSha256": "bb286f7ddc6e9597c56d46bf7ff6e3b556c5b680d7e147fd46518724432b083e",
"runtimeHostSourceSha256": "f44e5555b722553ab7ea1c982d355b1694d3c5fe1cdf19b9868e4bfbaa587b72",
"runtimeAdapterSourceSha256": "c676b83d9dbef86d781069a8b25e160615b288177729974267fe0c2da25afb71",
"runtimeSandboxSourceSha256": "86dcc0abbbe2e32f88febbbab22f662d8490d438a9e9cb5df04589fdf66ebd88",
"cancellationSourceSha256": "a07d1a9e982dec0ae130c1ad1efa5bc75c6a39e013b730ba3ee45256f09ceeca",
"agentFilesSourceSha256": "fb32f6badc13dfa22f231479509b631b2f0eba0c99b1ab9f3cd5c0c601aa03ba",
"nativeDistributionSourceSha256": "aed327c389e957483a503cb1bb8b247ee7cd7ff4b528fb4567c39bcb4577690b"
}
}
@@ -0,0 +1,36 @@
{
"commandDigest": "sha256:97485a223798b21e753da56c8ea3ccbd090699af9f8277f390e8c31a6d17038b",
"sourceDigest": "sha256:37918e4d09cf00e8ebf2557045fc87deb30d61b1dbe47bae937c71f110918813",
"declaration": {
"schema": "paperclip.grok-profile-identity.v1",
"agent": "grok",
"agentProfileVersion": 19,
"agentServerPackage": "builtin:grok-acp",
"agentServerVersion": "1",
"agentRuntimePackage": "native:grok",
"agentRuntimeVersion": "1.0.13",
"requiresProviderPolicy": false,
"acpxVersion": "0.13.1",
"sources": {
"patches/acpx@0.13.1.patch": "sha256:1e7dae8c83bf8620f53f2bea13c175eeff95e7e955783f6f529bb1dcbdcdb6d2",
"packages/paperclip-runner/src/cli/acpx-runtime-sidecar.ts": "sha256:477a05847187159f9c6f07bf90f1c0c1d432a9eb6fdb15d5620daf54443dafd8",
"packages/paperclip-runner/src/cli/acpx-sidecar-input.ts": "sha256:17948ad74350dc487ab20d24a0fb89daf0b426b98396454fff0e3856e922d82c",
"packages/paperclip-runner/src/drivers/acpx/admission-diagnostics.ts": "sha256:df8f1aa4f2d1bc63b2635bdd09cb8165c8b7681ddcc2618435c7c08ca90475af",
"packages/paperclip-runner/src/drivers/acpx/grok-effort.ts": "sha256:f1e46cf2e1e36b7704bd797b678e3db8db2003f97bdb9d827cfa3d7662698777",
"packages/paperclip-runner/src/drivers/acpx/grok-web-search.ts": "sha256:754b5ca2345c045421f6007f34999508d996e1cea035f0f1952b3d5f00072b9c",
"packages/paperclip-runner/src/drivers/acpx/grok-credentials.ts": "sha256:1c4ff0be6da798b6fcc604d20a9198dcc8523a3806d563b5b5345b7260549f73",
"packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts": "sha256:c676b83d9dbef86d781069a8b25e160615b288177729974267fe0c2da25afb71",
"packages/paperclip-runner/src/drivers/acpx/runtime-host.ts": "sha256:f44e5555b722553ab7ea1c982d355b1694d3c5fe1cdf19b9868e4bfbaa587b72",
"packages/paperclip-runner/src/drivers/preconfigured-home.ts": "sha256:bb286f7ddc6e9597c56d46bf7ff6e3b556c5b680d7e147fd46518724432b083e",
"packages/paperclip-runner/src/drivers/acpx/runtime-sandbox.ts": "sha256:86dcc0abbbe2e32f88febbbab22f662d8490d438a9e9cb5df04589fdf66ebd88",
"packages/paperclip-runner/src/drivers/acpx/sidecar-protocol.ts": "sha256:26b6b21dad582810d00f46e947e682a9615ee9761c487f6315970d6d014c05ab",
"packages/paperclip-runner/src/drivers/acpx/generated-sidecar-contract.ts": "sha256:b60e4b924e827e122860d2a4cc13b465ba748ee521620887c88f3c58c0df6081",
"packages/paperclip-runner/src/drivers/acpx/native-distribution-integrity.ts": "sha256:aed327c389e957483a503cb1bb8b247ee7cd7ff4b528fb4567c39bcb4577690b",
"packages/paperclip-runner/src/providers/grok/launcher.cjs": "sha256:f0b698395a3704ed2ffaf84ea19bdb20c36c8a0a70b7c629c7b6ffe144e59e55",
"packages/paperclip-runner/src/providers/grok/platforms.json": "sha256:931eed3abbf2bf041e6c72e6e9486d976f4352613cf069b216ddf010c9a8e704",
"packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_backend.rs": "sha256:7fe0e9d892d449aee2be5d14ad3fa8ce93378f6ef5c1d8924f749bdd395569f7",
"packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_session.rs": "sha256:fc8d7cce62e0c02efddece8696e8d24f6a9fe19e5288a5e67a01ffbf931a3f6e",
"packages/paperclip-runner/runner/crates/runner-core/src/acpx_sidecar_transport.rs": "sha256:37a552c1a8a59b1815df99b79552e0fda711cfa934055073ffd682cd17d7fb54"
}
}
}
@@ -0,0 +1,74 @@
{
"declaration": {
"schema": "paperclip.kimi-profile-identity.v1",
"agent": "kimi",
"agentProfileVersion": 17,
"agentServerPackage": "@moonshot-ai/kimi-code",
"agentServerVersion": "2.3.3",
"agentRuntimePackage": null,
"agentRuntimeVersion": null,
"requiresProviderPolicy": true,
"acpxVersion": "0.13.1",
"sources": {
"scripts/patch-kimi-runtime.mjs": "sha256:dd2f3608a875cf49e9093a9b76d6bda77fa9df6067fb5d5c40765d701247d97b",
"scripts/kimi-instructions-runtime.mjs": "sha256:c9a62629a7ed58119ed383a4ffdcc8def4337f1c429bb95363d41b8ffb23a746",
"scripts/materialize-kimi-distribution.mjs": "sha256:8960525611571023a18832e7b0fa8673dc52fd289433d6e555de066f68e9e879",
"scripts/provision-kimi.mjs": "sha256:b234438e37acd5f3698382bc53f14babd261bbf041334f8f793c055d4f694045",
"scripts/kimi-distribution/package.json": "sha256:3abd5a37af052ac1e4417f675453ed47b433edfa3e072c42bb7725811cfe7bbb",
"scripts/kimi-distribution/package-lock.json": "sha256:87f854ddd7f92254ccd8596b85962684ec176f92e93863771843bf416825f16b",
"src/drivers/acpx/kimi-closure-pins.ts": "sha256:d1ffc2f0be70c2f7eba3c5c67d3a24394b5bb5aed879003a31e333d189f50e2f",
"src/drivers/acpx/kimi-config.ts": "sha256:2307b7dd06bf33f2474243c801ee7d1a0a692ab3b47d2c709b6b4397ac4919c2",
"src/drivers/acpx/kimi-installation.ts": "sha256:83b9d821b5dda0a09b40d8828e6ac3b4ebeaafb58467ee14847c7ffa2efde453",
"src/drivers/acpx/kimi-instructions.ts": "sha256:6b306f98ba4c9ebe40cac24edc952f58177f4014def37718ed017ca2018f2991",
"src/drivers/acpx/session-context-admission.ts": "sha256:c4026e6c51b9a7fb731b178e03077917f6f0f71835da0cdb083dce11f2ddae93",
"src/drivers/acpx/kimi-runtime-files.ts": "sha256:9ff7ea33588c423a50d0194b4327aa2840bbd8bdc7da9ec4b6da62fa827f054b",
"src/configured-environment.ts": "sha256:a872e5b939c496955615ce87889ad27eb1ba2a9d8fc25e6bb74e03b1c2293feb",
"src/drivers/acpx/environment.ts": "sha256:9ace39a71ba287838c1b9c56bed141248e805fd017eb8490770c3b579567e55b",
"src/drivers/acpx/codex-runtime-adapter.ts": "sha256:c676b83d9dbef86d781069a8b25e160615b288177729974267fe0c2da25afb71",
"src/drivers/acpx/runtime-host.ts": "sha256:f44e5555b722553ab7ea1c982d355b1694d3c5fe1cdf19b9868e4bfbaa587b72",
"src/drivers/acpx/runtime-sandbox.ts": "sha256:86dcc0abbbe2e32f88febbbab22f662d8490d438a9e9cb5df04589fdf66ebd88",
"src/cli/acpx-runtime-sidecar.ts": "sha256:477a05847187159f9c6f07bf90f1c0c1d432a9eb6fdb15d5620daf54443dafd8",
"scripts/build-verified-provider-entrypoints.mjs": "sha256:70f7b314e86a22ad2df192eb6ff984e5f286002e165eec71292fff75064f7b9f",
"src/drivers/acpx/kimi-models.ts": "sha256:9d4bb2d1e0db4f929ed819150c1b030dbf3c917d15d15acb7418db2479dd7a92",
"src/drivers/acpx/kimi-metadata-probe.ts": "sha256:0f6f2865065500d77460ec761fa13661a3149dd34422812201026709f7cb84f8",
"src/cli/kimi-metadata-probe.ts": "sha256:306e2caca87977c3ea511ec43c937fd801f4c8cf9b4337b1ed8a3e315c1cdb37",
"src/drivers/acpx/kimi-message-boundary.ts": "sha256:11dbdabaa07d7909540b5041c642f3f60b20b660f72150e7b39155cf6abbfdd0",
"src/drivers/acpx/codex-acpx-driver.ts": "sha256:d11dea41590394225d622c2f4b4141ede0445fdb529fec1d6ce3fe749f354234",
"src/drivers/acpx/sidecar-protocol.ts": "sha256:26b6b21dad582810d00f46e947e682a9615ee9761c487f6315970d6d014c05ab",
"src/drivers/acpx/acp-edit-context.ts": "sha256:7be0bd911bc6bb3587e57d25ab5e75a961801ad0c58f06e713dd64cfe17a3c43",
"src/drivers/acpx/acp-permission-adapter.ts": "sha256:444482de2abb23d9c9c39a3a93bc9b27af241db56f8329e9935bb5b603c08ae0",
"src/drivers/acpx/safe-locations.ts": "sha256:f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9",
"src/drivers/acpx/generated-sidecar-contract.ts": "sha256:b60e4b924e827e122860d2a4cc13b465ba748ee521620887c88f3c58c0df6081",
"src/semantic-tools/redaction.ts": "sha256:ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f",
"runner/crates/runner-core/src/acpx_provider_state.rs": "sha256:be0e1006763902efb6bdd3952edd58058ab9139859647d413ada4c853110a324",
"src/drivers/acpx/candidate-admission.ts": "sha256:72a3c8b1e97f6aeedaeff4fd4a75edfbd2104000607517c190dd4c34f6d6327c",
"src/drivers/acpx/qualified-profiles.ts": "sha256:f0c3948ba291aa4b9ae54c0aebd2bd7967739ce40d48ccbe717491500fd9105b",
"src/drivers/acpx/profile-installation.ts": "sha256:d229b6afef695991313acdf490991136d40d638359665e0c48afb675af94f863",
"src/drivers/acpx/driver-profile.ts": "sha256:857d75d11cb1b82d22275443dab79ef674214b35f103dff30e1ab14915d80a47",
"src/live/runnerd-codex-transport.ts": "sha256:1554851fd4b32561e8575b8ad72a28528172dfc0372a014d11f02668991d5e49",
"runner/crates/runner-core/src/acpx_candidate_admission.rs": "sha256:9c87cd1ccb63b6b183f4a3c9c5d496ad34931b4a2df670094efc4e41e8591cef",
"runner/crates/runner-core/src/acpx_provider_backend.rs": "sha256:7fe0e9d892d449aee2be5d14ad3fa8ce93378f6ef5c1d8924f749bdd395569f7",
"runner/crates/runner-core/src/acpx_provider_session.rs": "sha256:fc8d7cce62e0c02efddece8696e8d24f6a9fe19e5288a5e67a01ffbf931a3f6e",
"runner/crates/runner-core/src/acpx_sidecar_transport.rs": "sha256:37a552c1a8a59b1815df99b79552e0fda711cfa934055073ffd682cd17d7fb54",
"runner/crates/runner-core/src/native_provider_backend.rs": "sha256:d2aa6a65be9195e5db0fbe83103be7f6897dedb8195872191d3eb0392cb4a1cc",
"runner/crates/runner-core/src/durable/runner.rs": "sha256:71d7d9ce3a22a44561e0419363b364d8935dc94b0cfc1baa945f3079f5faf395",
"runner/crates/runner-core/src/durable/mod.rs": "sha256:b5989da5304dfdfe04d0060a0a249e853c784af1cdd933bcf0d5a8fd07f0e895",
"src/contracts/runtime-context.ts": "sha256:152d7187ce1dc91a15377d368776555c9897d6e018b65f0f59a92dcdbd6497d9",
"src/contracts/generated-runtime-context-contract.ts": "sha256:069f4bc3c4dc95888f30d3fea331adb0ee0a164d09f4f70ec9c132f24f69c7dd",
"src/contracts/runtime-context-prompts.ts": "sha256:fff5755dba9b7dce8a5f10ff6ac6d5ed06b92ed079ce1e0a44ac72f2ce7869c1",
"src/provider-events.ts": "sha256:a6b77d969fd2f767dd8e583e81b907973c429b6e32d2c2d7fef0856f3568f07a",
"src/native-session-runtime.ts": "sha256:cbcefbceb0b2a24b0909cbb8361df6b8853b4f50b21d66328bc483951a8246ea",
"src/control-plane/durable-prp-control-plane.ts": "sha256:a8a8bed373cdd18397a762b9f67b04c789d9e5d63208f06c5ebe000faff090ac",
"src/drivers/codex/codex-harness-session.ts": "sha256:bb9bb3daac53ce5955ddf8f56839c7a57ae60c74ea3cdd08895ab6912fcf7851",
"src/drivers/codex/codex-session-notifications.ts": "sha256:23361d25e660ef7919bfb54f21727b899e7d6f8865307604ee15eca7488afeaa",
"src/drivers/codex/codex-session-state.ts": "sha256:c2f1059c5f1532cf3c2059792324a6f55d0c06ec8a62c3a4ab2c9451d617988a",
"runner/crates/runner-core/src/provider_backend.rs": "sha256:03a57d83901f0fbccfb7bcdaeae79baf8918f658ceb3463406211431944a461e",
"runner/crates/runner-core/src/provider_bridge.rs": "sha256:d417840ecd87e1a8a407629074f6c64bad4071c5e9add4b5a2aef0b3403b5529",
"runner/crates/runner-core/src/provider_events.rs": "sha256:91dfa4985691f86ab7f60514e46d924fb815900d6d456d78780bd94d29bff461",
"runner/crates/runner-core/src/acpx_event_scope.rs": "sha256:088f4b5c67b5663cbce2ef0b8b8cb3e6fe04d2b5c9cfa7a8139f8f0bf91e476e",
"runner/crates/runner-core/src/acpx_event_payload.rs": "sha256:f3b2f6ea9c2e9cf673790eef2f7d4e738c36a2e6347e1fd61ae501793cef1f15",
"scripts/generate-acpx-profiles.mjs": "sha256:e878a3f8afa9ce38940e61343cfbdc55d6431e9574108ca5fb17f0c72ead7423"
}
},
"commandDigest": "sha256:e3da3859ef3fe4904d2b917c2cbdde6181f4dbeb6480eba43d37614fb29ddd70"
}
@@ -142,6 +142,16 @@
"version": 15,
"commandDigest": "sha256:658e6d41ce6fc7080aa6655d7a97121b3cae6214f2d4c074284d958b95bbe769",
"launchAuthority": false
},
{
"path": "packages/paperclip-runner/test/fixtures/kimi-profile-v17-identity.json",
"originRevision": "2935b71c5d47152b7d2a9c593913390a14aea1e5",
"originPath": "packages/paperclip-runner/test/fixtures/kimi-profile-v17-identity.json",
"sha256": "c3b6a1bf95e7de1abf7a07f042b5dbe9c10d1a3ae2007a886b6387d779a7a205",
"agent": "kimi",
"version": 17,
"commandDigest": "sha256:e3da3859ef3fe4904d2b917c2cbdde6181f4dbeb6480eba43d37614fb29ddd70",
"launchAuthority": false
}
]
}
@@ -1,7 +1,7 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { createHash } from 'node:crypto';
import { validateReleaseRun, selectReleaseArtifacts, validateReleaseImageBase, validateImageBindings, decodeReleaseJson } from '../prepare-runner-release-image.mjs';
import { validateReleaseRun, selectReleaseArtifacts, validateReleaseImageBase, validateImageBindings, decodeReleaseJson, decodeReleaseKimiClosures } from '../prepare-runner-release-image.mjs';
const source = 'a'.repeat(40);
const run = { head_sha: source, conclusion: 'success', path: '.github/workflows/release-smoke.yml', repository: { full_name: 'paperclipai/paperclip' } };
@@ -81,3 +81,16 @@ test('frozen release JSON must be the bounded requested source file', () => {
assert.throws(() => decodeReleaseJson({ ...file, ...patch }, path));
}
});
test('frozen Kimi closure data is decoded without executing release source', () => {
const path = 'packages/paperclip-runner/src/drivers/acpx/kimi-closure-pins.ts';
const source = `throw new Error("must not execute");\nexport const KIMI_DISTRIBUTION_CLOSURE_SHA256 = Object.freeze({ "darwin-arm64": "${'a'.repeat(64)}", "linux-x64": "${'b'.repeat(64)}", });`;
const bytes = Buffer.from(source), file = { type: 'file', path, encoding: 'base64', size: bytes.length, content: bytes.toString('base64') };
assert.deepEqual(decodeReleaseKimiClosures(file, path), { 'darwin-arm64': 'a'.repeat(64), 'linux-x64': 'b'.repeat(64) });
for (const patch of [{ path: 'other.ts' }, { type: 'symlink' }, { size: bytes.length + 1 }, { size: 1024 * 1024 + 1 },
{ content: Buffer.from(source.replace('linux-x64', 'linux-arm64')).toString('base64') }]) {
assert.throws(() => decodeReleaseKimiClosures({ ...file, ...patch }, path));
}
const duplicate = Buffer.from(source + source);
assert.throws(() => decodeReleaseKimiClosures({ ...file, content: duplicate.toString('base64'), size: duplicate.length }, path));
});
+17 -1
View File
@@ -61,6 +61,17 @@ export function decodeReleaseJson(file, path) {
return JSON.parse(bytes.toString('utf8'));
}
export function decodeReleaseKimiClosures(file, path) {
if (file?.type !== 'file' || file.path !== path || file.encoding !== 'base64'
|| !Number.isSafeInteger(file.size) || file.size < 1 || file.size > 1024 * 1024
|| typeof file.content !== 'string') throw Error('Invalid frozen Kimi closure source');
const bytes = Buffer.from(file.content, 'base64');
if (bytes.length !== file.size) throw Error('Frozen Kimi closure source size mismatch');
const matches = [...bytes.toString('utf8').matchAll(/export const KIMI_DISTRIBUTION_CLOSURE_SHA256 = Object\.freeze\(\{\s*"darwin-arm64": "([a-f0-9]{64})",\s*"linux-x64": "([a-f0-9]{64})",?\s*\}\);/g)];
if (matches.length !== 1) throw Error('Invalid frozen Kimi closure declaration');
return { 'darwin-arm64': matches[0][1], 'linux-x64': matches[0][2] };
}
const digestFile = async file => {
const hash = createHash('sha256');
for await (const chunk of createReadStream(file)) hash.update(chunk);
@@ -134,7 +145,12 @@ z.extractall(dest)
const path = `packages/paperclip-runner/${name}`;
return decodeReleaseJson(await api(`contents/${path}?ref=${source}`), path);
}));
verifyReleaseProviderPack(pack, source, 'linux-x64', { profiles, distributions });
let kimiClosures;
if (profiles.profiles.kimi && profiles.profiles.kimi.qualificationStatus !== 'pending') {
const path = 'packages/paperclip-runner/src/drivers/acpx/kimi-closure-pins.ts';
kimiClosures = decodeReleaseKimiClosures(await api(`contents/${path}?ref=${source}`), path);
}
verifyReleaseProviderPack(pack, source, 'linux-x64', { profiles, distributions, kimiClosures });
const packages = JSON.parse(await readFile(join(dirs[2], 'manifest.json'), 'utf8'));
if (packages.schema !== 'paperclip.runner.public-packages/v1' || packages.sourceRevision !== source) throw Error('Wrong public-package source');
const server = packages.archives?.filter(a => a.name === '@paperclipai/server');
@@ -183,6 +183,33 @@ describe("adapter model refresh route", () => {
await unregisterTestAdapter(refreshableAdapterType);
});
it.each([
{ qualification: "pending", qualified: false, status: 422 },
{ qualification: "released", qualified: true, status: 200 },
])("keeps $qualification Kimi model selection separate from other provider catalogs", async ({ qualified, status }) => {
const { PAPERCLIP_RUNNER_ACPX_PROFILES } = await import("@paperclipai/adapter-utils");
const kimi = PAPERCLIP_RUNNER_ACPX_PROFILES.find(profile => profile.value === "kimi")! as { qualified: boolean };
const previousQualified = kimi.qualified;
const previousAdmission = process.env.PAPERCLIP_ACPX_CANDIDATE_ADMISSION;
try {
kimi.qualified = qualified;
delete process.env.PAPERCLIP_ACPX_CANDIDATE_ADMISSION;
const app = await createApp();
const res = await requestApp(app, baseUrl =>
request(baseUrl).get("/api/companies/company-1/adapters/paperclip_runner/models?provider=acpx&acpxAgent=kimi&refresh=1"),
);
expect(res.status, JSON.stringify(res.body)).toBe(status);
if (qualified) expect(res.body).toEqual([]);
else expect(res.body).toMatchObject({ code: "paperclip_runner_acpx_agent_unavailable" });
expect(mockListOpenCodeModels).not.toHaveBeenCalled();
} finally {
kimi.qualified = previousQualified;
if (previousAdmission === undefined) delete process.env.PAPERCLIP_ACPX_CANDIDATE_ADMISSION;
else process.env.PAPERCLIP_ACPX_CANDIDATE_ADMISSION = previousAdmission;
}
});
it("uses refreshModels when refresh=1 is requested", async () => {
const listModels = vi.fn(async () => [{ id: "stale-model", label: "stale-model" }]);
const refreshModels = vi.fn(async () => [{ id: "fresh-model", label: "fresh-model" }]);
@@ -31,6 +31,8 @@ vi.mock("../vendor/paperclip-runner/live/index.js", () => ({
probeAcpxGrokInstallation: vi.fn(async () => undefined),
probeAcpxPiInstallation: vi.fn(async () => undefined),
probeAcpxCursorInstallation: vi.fn(async () => undefined),
probeAcpxKimiInstallation: vi.fn(async () => undefined),
probeKimiModels: vi.fn(async () => undefined),
}));
it("advertises tool-refresh recovery for the selected legacy harness", () => {
+4 -1
View File
@@ -487,9 +487,12 @@ describe("adapter routes", () => {
default: "claude",
meta: { visibleWhen: { key: "provider", value: "acpx" } },
options: [
{ value: "claude", label: "Claude" },
{ value: "grok", label: "Grok Build" },
{ value: "claude", label: "Claude" },
{ value: "cursor", label: "Cursor" },
{ value: "copilot", label: "GitHub Copilot" },
{ value: "pi", label: "Pi" },
{ value: "kimi", label: "Kimi Code" },
],
});
expect(JSON.stringify(res.body)).not.toContain("Codex via ACPX");
@@ -1002,6 +1002,52 @@ describe("agent routes adapter validation", () => {
expect(mockAgentService.update.mock.calls.at(-1)?.[1]).toMatchObject({ adapterType: "paperclip_runner", adapterConfig: { provider: "acpx", acpxAgent: "pi", model: "" } });
});
it("keeps Kimi draft models unset through the public create route", async () => {
mockInstanceSettingsService.getExperimental.mockResolvedValue({ enableNativeRunner: true });
const originalCreate = mockAgentService.create.getMockImplementation()!;
mockAgentService.create.mockImplementation(async (...args) => ({ ...await originalCreate(...args), lifecycleHolds: ["setup"] }));
const app = await createApp();
const input = { name: "Kimi draft", adapterType: "paperclip_runner", adapterConfig: { provider: "acpx", acpxAgent: "kimi" } };
const draft = await requestApp(app, base => request(base).post("/api/companies/company-1/agents")
.send({ ...input, setupId: "00000000-0000-4000-8000-000000000092" }));
expect(draft.status, JSON.stringify(draft.body)).toBe(201);
const saved = mockAgentService.create.mock.calls.at(-1)?.[1];
expect(saved).toMatchObject(input);
expect(saved.adapterConfig.model).toBeUndefined();
expect(mockAgentService.create.mock.calls.at(-1)?.[2]).toMatchObject({ setupOnly: true, runnerResolved: true });
mockAgentService.create.mockClear();
const active = await requestApp(app, base => request(base).post("/api/companies/company-1/agents").send(input));
expect(active.status).toBe(422);
expect(mockAgentService.create).not.toHaveBeenCalled();
});
it.each([
["process", undefined, ""],
["kimi_local", "kimi-for-coding", "kimi-for-coding"],
["pi_local", "openrouter/anthropic/claude-sonnet-4.6", ""],
] as const)("preserves Kimi model selection only from the same harness: %s", async (previousAdapterType, previousModel, expectedModel) => {
mockInstanceSettingsService.getExperimental.mockResolvedValue({ enableNativeRunner: true });
mockAgentService.getById.mockResolvedValue({ ...(await mockAgentService.getById()), adapterType: previousAdapterType,
adapterConfig: previousModel === undefined ? {} : { model: previousModel },
status: "paused", lifecycleState: "paused", pauseReason: "setup", lifecycleHolds: ["setup"] });
const app = await createApp();
const response = await requestApp(app, base => request(base).patch("/api/agents/11111111-1111-4111-8111-111111111111")
.send({ adapterType: "paperclip_runner", adapterConfig: { provider: "acpx", acpxAgent: "kimi" } }));
expect(response.status, JSON.stringify(response.body)).toBe(200);
expect(mockAgentService.update.mock.calls.at(-1)?.[1]).toMatchObject({ adapterType: "paperclip_runner",
adapterConfig: { provider: "acpx", acpxAgent: "kimi", model: expectedModel } });
});
it("persists an explicit Kimi model through the public create route", async () => {
mockInstanceSettingsService.getExperimental.mockResolvedValue({ enableNativeRunner: true });
const app = await createApp();
const input = { name: "Kimi", adapterType: "paperclip_runner",
adapterConfig: { provider: "acpx", acpxAgent: "kimi", model: "kimi-for-coding" } };
const response = await requestApp(app, base => request(base).post("/api/companies/company-1/agents").send(input));
expect(response.status, JSON.stringify(response.body)).toBe(201);
expect(mockAgentService.create.mock.calls.at(-1)?.[1]).toMatchObject(input);
});
it.each([{ thinking: "high", status: 200 }, { thinking: "medium", status: 422 }, { command: "custom-pi", status: 422 }])("validates saved Pi settings during direct adapter conversion: %j", async ({ status, ...settings }) => {
const selection = await import("../services/agent-runner-selection.js");
vi.spyOn(selection, "resolveNewAgentRunnerForCompany").mockImplementation(async (_db, _companyId, input) =>
@@ -27,6 +27,7 @@ import { remoteTerminationReceipt } from "../services/remote-execution-terminati
import { AgentDirectoryReuseInvalidatedError, agentDirectoryWorkingCopyService } from "../services/agent-directory-working-copies.js";
import { withDirectoryMergeLock } from "@paperclipai/adapter-utils/workspace-restore-merge";
import { heartbeatRunEvents } from "@paperclipai/db";
import { logger } from "../middleware/logger.js";
const external = resolveExternalTestDatabaseUrl("PAPERCLIP_AGENT_DIRECTORY_TEST_DATABASE_URL");
@@ -393,6 +394,106 @@ describe("persistent agent directories", () => {
expect((await copies.get(companyId, first.runId))?.receipt?.retainedByRunId).toBeUndefined();
await copies.collectStopped({ companyId, runId: first.runId });
});
it("adopts unchanged ownership after collection retry metadata changes before claim", async () => {
const first = await run();
expect(await copies.hasChanges({ companyId, runId: first.runId })).toBe(false);
await db.update(heartbeatRuns).set({ status: "succeeded", runtimeMode: "native" }).where(eq(heartbeatRuns.id, first.runId));
const secondId = await nextRun();
const transaction = db.transaction.bind(db);
let transactions = 0;
let injected = false;
const spy = vi.spyOn(db, "transaction").mockImplementation((async (callback, options) => {
transactions++;
if (transactions === 2 && !injected) {
injected = true;
const before = (await copies.get(companyId, first.runId))!;
vi.useFakeTimers({ toFake: ["Date"] });
vi.setSystemTime(new Date(before.updatedAt.getTime() + 1000));
try { await copies.recoverStopped(); } finally { vi.useRealTimers(); }
const after = (await copies.get(companyId, first.runId))!;
expect(after.updatedAt.getTime()).toBeGreaterThan(before.updatedAt.getTime());
expect(after.nextAttemptAt).toBeInstanceOf(Date);
for (const field of ["state", "baseHash", "localRoot", "executionRoot", "processStoppedAt", "receipt", "attempts"] as const)
expect(after[field]).toEqual(before[field]);
}
return transaction(callback, options);
}) as typeof db.transaction);
try {
const adopted = await copies.adopt({ companyId, agentId, runId: secondId, previousRunId: first.runId, cwd: home });
expect(injected).toBe(true);
expect(adopted).toMatchObject({ runId: secondId, localRoot: first.localRoot, executionRoot: first.executionRoot });
expect((await copies.get(companyId, first.runId))?.receipt?.retainedByRunId).toBe(secondId);
} finally { spy.mockRestore(); vi.useRealTimers(); expect(injected).toBe(true); }
});
it.each(["materialization", "checkpoint", "stopped", "state"])("rejects changed %s authority with the same baseline before adoption", async change => {
const first = await run();
expect(await copies.hasChanges({ companyId, runId: first.runId })).toBe(false);
const secondId = await nextRun();
const warning = vi.spyOn(logger, "warn");
const privateValue = "private-materialization-/tmp/untrusted-token-do-not-log";
const transaction = db.transaction.bind(db);
let calls = 0;
let injected = false;
const spy = vi.spyOn(db, "transaction").mockImplementation((async (callback, options) => {
if (++calls === 2) {
injected = true;
const prior = (await copies.get(companyId, first.runId))!;
const values = change === "materialization" ? { receipt: { ...prior.receipt, materializationIdentity: privateValue } }
: change === "checkpoint" ? { receipt: { ...prior.receipt, checkpointState: "saved" } }
: change === "stopped" ? { processStoppedAt: new Date() } : { state: "pending_collection" };
await db.update(agentInstructionWorkingCopies).set({ ...values, updatedAt: new Date(Date.now() + 1000) })
.where(eq(agentInstructionWorkingCopies.runId, first.runId));
expect((await copies.get(companyId, first.runId))!.baseHash).toBe(prior.baseHash);
}
return transaction(callback, options);
}) as typeof db.transaction);
try {
await expect(copies.adopt({ companyId, agentId, runId: secondId, previousRunId: first.runId, cwd: home }))
.rejects.toMatchObject({ status: 409, details: { stage: "locked_adoption_authority", predecessorPresent: true,
observedAfterInitialRead: true, changedFields: expect.arrayContaining([change === "materialization" || change === "checkpoint" ? "receipt" : change === "stopped" ? "processStoppedAt" : "state"]) } });
expect(injected).toBe(true);
expect(warning).toHaveBeenCalledWith({ companyId, agentId, runId: secondId, previousRunId: first.runId,
adoptionConflict: { stage: "locked_adoption_authority", predecessorPresent: true, observedAfterInitialRead: true,
changedFields: [change === "materialization" || change === "checkpoint" ? "receipt" : change === "stopped" ? "processStoppedAt" : "state"] } },
"Agent directory adoption conflict");
expect(JSON.stringify(warning.mock.calls)).not.toContain(privateValue);
expect(await copies.get(companyId, secondId)).toBeNull();
expect((await copies.get(companyId, first.runId))?.receipt?.retainedByRunId).toBeUndefined();
expect(await fs.readFile(path.join(root, entryFile), "utf8")).toBe(initial);
} finally { spy.mockRestore(); warning.mockRestore(); }
});
it("reports closed claim predicates when retirement wins before the final owner read", async () => {
const first = await run();
expect(await copies.hasChanges({ companyId, runId: first.runId })).toBe(false);
const secondId = await nextRun();
const warning = vi.spyOn(logger, "warn");
const transaction = db.transaction.bind(db);
let injected = false;
const spy = vi.spyOn(db, "transaction").mockImplementation((async (callback, options) => {
const result = await transaction(callback, options);
if (!injected) {
injected = true;
await db.update(agentInstructionWorkingCopies).set({ state: "pending_collection" })
.where(eq(agentInstructionWorkingCopies.runId, first.runId));
}
return result;
}) as typeof db.transaction);
try {
await expect(copies.adopt({ companyId, agentId, runId: secondId, previousRunId: first.runId, cwd: home }))
.rejects.toMatchObject({ status: 409, details: { stage: "adoption_claim", observedAfterClaim: true,
predecessorPresent: true, conditions: { unchangedTurn: false, timestampMatches: true, baselineMatches: true,
localRootMatches: true, unstopped: true, successorAbsent: true, receiptMatches: true } } });
expect(warning).toHaveBeenCalledWith({ companyId, agentId, runId: secondId, previousRunId: first.runId,
adoptionConflict: { stage: "adoption_claim", observedAfterClaim: true, predecessorPresent: true,
conditions: { unchangedTurn: false, timestampMatches: true, baselineMatches: true, localRootMatches: true,
unstopped: true, successorAbsent: true, receiptMatches: true } } }, "Agent directory adoption conflict");
expect(await copies.get(companyId, secondId)).toBeNull();
expect((await copies.get(companyId, first.runId))?.receipt?.retainedByRunId).toBeUndefined();
} finally { spy.mockRestore(); warning.mockRestore(); }
});
it("allows only one stale concurrent adoption with identical updatedAt timestamps", async () => {
const fixed = new Date("2030-01-01T00:00:00.000Z");
vi.useFakeTimers({ toFake: ["Date"] });
@@ -1,3 +1,4 @@
import { persistHeartbeatRunProcessMetadata } from "../services/heartbeat/run-lifecycle.js";
import { enableRunFailureReporting } from "./helpers/run-failure-reporting.js";
import { EMBEDDED_POSTGRES_TEST_TIMEOUT_MS } from "@paperclipai/db";
import { createSystemRecovery } from "../services/system-recovery-state.js";
@@ -2418,6 +2419,8 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => {
updatedAt: new Date(0),
}).returning())[0].id }
: await seedEnvironmentLeaseFixture(fixture);
await persistHeartbeatRunProcessMetadata(db, runId, { pid: 2_000_000_000, processGroupId: null,
startedAt: new Date(0).toISOString(), targetKind: "local" });
return { ...fixture, ...lease, complete };
}
@@ -2940,6 +2943,39 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => {
expect(wakeup?.status).toBe("claimed");
});
it.each(["running", "failed"] as const)("holds a remote %s native PID despite a colliding live host child", async status => {
const child = spawnAliveProcess();
childProcesses.add(child);
expect(child.pid).toBeTypeOf("number");
const { companyId, agentId, runId, issueId } = await seedRunFixture({ adapterType: "process",
runtimeMode: "native", runStatus: status,
runErrorCode: status === "failed" ? "native_session_interrupted" : null,
runError: status === "failed" ? "native session timed out after 120000ms" : null });
if (status === "failed") {
await db.update(heartbeatRuns).set({ nativeIssueId: issueId, nativePhase: "retryable_failure", finishedAt: new Date() })
.where(eq(heartbeatRuns.id, runId));
await db.insert(nativeRunFinalizations).values({ runId, companyId, issueId,
phase: "retryable_failure", nextAttemptAt: new Date(0) });
}
await persistHeartbeatRunProcessMetadata(db, runId, { pid: child.pid!, processGroupId: null,
startedAt: new Date().toISOString(), targetKind: "remote" });
const heartbeat = heartbeatService(db);
const before = await heartbeat.getRun(runId);
expect(await heartbeat.reapOrphanedRuns()).toEqual({ reaped: 0, runIds: [] });
expect(await heartbeat.getRun(runId)).toMatchObject({ status, processPid: child.pid,
processStartedAt: before!.processStartedAt, errorCode: before!.errorCode, error: before!.error });
expect(isPidAlive(child.pid!)).toBe(true);
expect(mockTerminateLocalService).not.toHaveBeenCalled();
expect(mockAdapterExecute).not.toHaveBeenCalled();
child.kill("SIGTERM");
await waitForPidExit(child.pid!);
expect(await heartbeat.reapOrphanedRuns()).toEqual({ reaped: 0, runIds: [] });
expect(await heartbeat.getRun(runId)).toMatchObject({ status, processPid: child.pid,
processStartedAt: before!.processStartedAt, errorCode: before!.errorCode, error: before!.error });
expect(await selectRecoveryRunsByAgent(agentId)).toHaveLength(1);
expect(mockAdapterExecute).not.toHaveBeenCalled();
});
it("keeps a native run active without granting legacy retry or signal authority", async () => {
const child = spawnAliveProcess();
childProcesses.add(child);
@@ -2951,6 +2987,8 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => {
processPid: child.pid ?? null,
includeIssue: false,
});
await persistHeartbeatRunProcessMetadata(db, runId, { pid: child.pid!, processGroupId: null,
startedAt: new Date().toISOString(), targetKind: "local" });
const heartbeat = heartbeatService(db);
const result = await heartbeat.reapOrphanedRuns();
@@ -3229,6 +3267,8 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => {
processPid: 999_999_999,
includeIssue: false,
});
await persistHeartbeatRunProcessMetadata(db, runId, { pid: 999_999_999, processGroupId: null,
startedAt: new Date().toISOString(), targetKind: "local" });
// The persisted run remains native even after the agent's current adapter
// changes to one that normally owns a legacy local child.
await db
@@ -33,6 +33,7 @@ vi.mock("../sentry.js", async () => {
});
import { heartbeatService } from "../services/heartbeat.ts";
import { persistHeartbeatRunProcessMetadata } from "../services/heartbeat/run-lifecycle.js";
function agentTaskRunCalls(fromIndex: number) {
return mockTelemetryClient.track.mock.calls
@@ -113,6 +114,9 @@ describeEmbeddedPostgres(
processPid: process.pid,
contextSnapshot: { issueId },
});
await persistHeartbeatRunProcessMetadata(db, runId, {
pid: process.pid, processGroupId: null, startedAt: new Date().toISOString(), targetKind: "local",
});
await db.insert(nativeRunFinalizations).values({
runId,
companyId,
@@ -0,0 +1,432 @@
import { mkdtemp, writeFile, rm } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { spawn } from "node:child_process";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { AdapterExecutionTarget } from "@paperclipai/adapter-utils/execution-target";
import { KIMI_DISTRIBUTION_CLOSURE_SHA256, KIMI_NODE_DISTRIBUTIONS, KIMI_NODE_VERSION, KIMI_RUNTIME_VERSION } from "../../../packages/paperclip-runner/src/drivers/acpx/kimi-closure-pins.js";
import { QUALIFIED_ACPX_PROFILES } from "../../../packages/paperclip-runner/src/drivers/acpx/qualified-profiles.js";
const mocks = vi.hoisted(() => ({ command: vi.fn(), local: vi.fn(), digest: vi.fn(), ssh: vi.fn(), prepare: vi.fn(), select: vi.fn(), wire: [] as Array<{ file: string; args: string[]; stdin: boolean }> }));
vi.mock("@paperclipai/adapter-utils/execution-target", () => ({ runAdapterExecutionTargetShellCommand: mocks.command }));
vi.mock("@paperclipai/adapter-utils/ssh", () => ({ runSshCommand: mocks.ssh }));
vi.mock("../services/native-runtime/native-session-executor.js", () => ({
prepareRemoteProviderPackOnTarget: mocks.prepare, resolveRemoteProviderPackSource: mocks.select,
}));
vi.mock("node:child_process", async importOriginal => {
const actual = await importOriginal<typeof import("node:child_process")>();
return { ...actual,
execFile: (file: string, args: string[], options: object, callback: Parameters<typeof actual.execFile>[3]) => {
if (file !== "ssh") return actual.execFile(file, args, options, callback);
mocks.wire.push({ file, args, stdin: false });
return actual.execFile(process.execPath, ["-e", 'process.stdout.write("{}")'], options, callback);
},
spawn: (file: string, args: string[], options: Parameters<typeof actual.spawn>[2]) => {
if (file !== "ssh") return actual.spawn(file, args, options);
mocks.wire.push({ file, args, stdin: true });
return actual.spawn(process.execPath, ["-e", 'process.stdout.write(require("node:fs").readFileSync(0,"utf8"))'], options);
},
};
});
vi.mock("../vendor/paperclip-runner/live/index.js", async () => {
const provider = await import("../../../packages/paperclip-runner/src/drivers/acpx/kimi-metadata-probe.js");
return { probeKimiMetadata: mocks.local, kimiMetadataProbeDigest: mocks.digest,
validateKimiMetadata: provider.validateKimiMetadata, kimiMetadataProbeNodeDigest: provider.kimiMetadataProbeNodeDigest };
});
import { probeKimiExecutionTarget as rawProbeKimiExecutionTarget } from "../services/kimi-connection-probe.js";
const model = "selected-model'; $(never-execute)";
const apiKey = "selected-secret'; $(never-execute)";
const digest = `sha256:${"a".repeat(64)}`;
const remote = { kind: "remote", transport: "sandbox", remoteCwd: "/workspace" } as AdapterExecutionTarget;
const verified = () => ({ schema: "paperclip.kimi.metadata.v1", status: "verified", version: KIMI_RUNTIME_VERSION,
nodeVersion: KIMI_NODE_VERSION, profileDigest: QUALIFIED_ACPX_PROFILES.kimi.commandDigest, target: "linux-x64",
closureDigest: `sha256:${KIMI_DISTRIBUTION_CLOSURE_SHA256["linux-x64"]}`, model, contextLength: 1048576, promptSent: false });
const response = (value: unknown = verified(), exitCode = 0) => ({ stdout: JSON.stringify(value), stderr: "", exitCode, signal: null, timedOut: false });
const failed = (code: string) => ({ schema: "paperclip.kimi.metadata.v1", status: "failed", code, message: apiKey, promptSent: false });
const companyId = "candidate-company";
const pending = QUALIFIED_ACPX_PROFILES.kimi.qualificationStatus === "pending";
function probeKimiExecutionTarget(apiKey: string, target: AdapterExecutionTarget | null | undefined, model: string) {
const platform = target?.kind === "remote" ? "linux" : process.platform;
const architecture = target?.kind === "remote" ? "x64" : process.arch;
vi.stubEnv("PAPERCLIP_ACPX_CANDIDATE_ADMISSION", JSON.stringify({ schema: "paperclip.acpx-candidate-admission.v1",
companyId, agent: "kimi", model, profileDigest: QUALIFIED_ACPX_PROFILES.kimi.commandDigest,
platform, architecture, expiresAt: Date.now() + 60_000,
nativeClosureDigest: `sha256:${KIMI_DISTRIBUTION_CLOSURE_SHA256[`${platform}-${architecture}` as "darwin-arm64" | "linux-x64"]}` }));
return rawProbeKimiExecutionTarget(apiKey, target, model, { companyId });
}
beforeEach(() => {
vi.resetAllMocks();
mocks.digest.mockResolvedValue(digest);
mocks.command.mockResolvedValue(response());
mocks.ssh.mockResolvedValue({ stdout: JSON.stringify(verified()), stderr: "" });
mocks.wire.length = 0;
mocks.local.mockResolvedValue(verified());
});
afterEach(() => { vi.restoreAllMocks(); vi.unstubAllEnvs(); });
describe("Kimi setup on a selected execution target", () => {
it.each(["sandbox", "ssh"])("uses admitted fixed bytes and only explicit credentials on %s", async transport => {
vi.stubEnv("KIMI_API_KEY", "unselected-ambient-secret");
vi.stubEnv("KIMI_CODE_BASE_URL", "https://unselected.example");
const target = { ...remote, transport } as AdapterExecutionTarget;
await expect(probeKimiExecutionTarget(apiKey, target, model)).resolves.toEqual(verified());
expect(mocks.local).not.toHaveBeenCalled();
const call = transport === "ssh" ? mocks.ssh.mock.calls[0] : mocks.command.mock.calls[0];
expect(call).toBeTruthy();
const command = call![transport === "ssh" ? 1 : 2], options = call![transport === "ssh" ? 2 : 3];
if (transport === "ssh") {
expect(mocks.command).not.toHaveBeenCalled();
expect(mocks.ssh).toHaveBeenCalledOnce();
} else {
expect(mocks.command).toHaveBeenCalledOnce();
expect(call![1]).toBe(target);
}
expect(command).toContain("pack=/opt/paperclip-runner/provider-pack");
expect(command).toContain('node="$pack/provider-assets/kimi/linux-x64/runtime/node/bin/node"');
expect(command).toContain('/usr/bin/sha256sum "$entry"');
expect(command).toContain(digest.slice(7));
expect(command).toContain('/usr/bin/sha256sum "$node"');
expect(command).toContain(KIMI_NODE_DISTRIBUTIONS["linux-x64"].executableSha256);
expect(command.indexOf('/usr/bin/sha256sum "$entry"')).toBeLessThan(command.indexOf('exec /usr/bin/env -i'));
expect(command.indexOf('/usr/bin/sha256sum "$node"')).toBeLessThan(command.indexOf('exec /usr/bin/env -i'));
expect(command).toContain("unset NODE_OPTIONS NODE_PATH NODE_EXTRA_CA_CERTS");
expect(command).toContain('PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT="$pack"');
expect(command).not.toContain(model);
expect(command).not.toContain(apiKey);
if (transport === "ssh") {
expect(options.env).toBeUndefined();
const input = JSON.parse(options.stdin);
expect(input).toMatchObject({ apiKey, model });
if (pending) expect(input.candidateAdmission).toMatchObject({ companyId, agent: "kimi", model, platform: "linux", architecture: "x64", runId: expect.stringMatching(/^kimi-metadata-/), normalizedSessionId: expect.stringMatching(/^metadata-/) });
else expect(input).not.toHaveProperty("candidateAdmission");
expect(options.timeoutMs).toBeGreaterThan(0);
expect(options.timeoutMs).toBeLessThanOrEqual(32000);
} else {
expect(options).toEqual({ cwd: "/workspace", env: { KIMI_API_KEY: apiKey, PAPERCLIP_KIMI_PROBE_MODEL: model, ...(pending ? { PAPERCLIP_ACPX_CANDIDATE_ADMISSION: expect.any(String) } : {}) }, timeoutSec: expect.any(Number), graceSec: 3 });
expect(options.timeoutSec).toBeGreaterThan(0);
expect(options.timeoutSec).toBeLessThanOrEqual(32);
}
expect(JSON.stringify(options)).not.toContain("unselected");
});
it("keeps local verification explicit and does not run a remote command", async () => {
for (const target of [undefined, null, { kind: "local" } as AdapterExecutionTarget]) {
await expect(probeKimiExecutionTarget(apiKey, target, model)).resolves.toEqual(verified());
}
expect(mocks.local).toHaveBeenCalledTimes(3);
if (pending) expect(mocks.local).toHaveBeenCalledWith(model, apiKey, expect.objectContaining({ companyId, agent: "kimi", model, platform: process.platform, architecture: process.arch, runId: expect.stringMatching(/^kimi-metadata-/), normalizedSessionId: expect.stringMatching(/^metadata-/) }));
else expect(mocks.local).toHaveBeenCalledWith(model, apiKey, undefined);
expect(mocks.command).not.toHaveBeenCalled();
expect(mocks.digest).not.toHaveBeenCalled();
mocks.local.mockResolvedValue(failed("KIMI_AUTH_REJECTED"));
const error = await probeKimiExecutionTarget(apiKey, null, model).catch(error => error);
expect(error).toMatchObject({ status: 422, details: { code: "KIMI_AUTH_REJECTED" } });
expect(error.message).not.toContain(apiKey);
});
it("rejects task-only plugin hosting before credentials leave the server", async () => {
const runnerTask = vi.fn();
await expect(probeKimiExecutionTarget(apiKey, { ...remote, runnerTask } as AdapterExecutionTarget, model))
.rejects.toMatchObject({ details: { code: "KIMI_INSTALLATION_INVALID" } });
expect(runnerTask).not.toHaveBeenCalled();
expect(mocks.local).not.toHaveBeenCalled();
expect(mocks.digest).not.toHaveBeenCalled();
expect(mocks.command).not.toHaveBeenCalled();
});
it("rejects absent or invalid key/model inputs before transmission", async () => {
vi.stubEnv("KIMI_API_KEY", apiKey);
for (const key of ["", " ", "key\nheader", "key\0header", "x".repeat(16385)]) {
await expect(probeKimiExecutionTarget(key, remote, model)).rejects.toMatchObject({ details: { code: "KIMI_AUTH_REQUIRED" } });
}
for (const selected of ["", " ", "x\0y", "x".repeat(513), "\ud800"]) {
await expect(probeKimiExecutionTarget(apiKey, remote, selected)).rejects.toMatchObject({ details: { code: "KIMI_MODEL_UNAVAILABLE" } });
}
expect(mocks.command).not.toHaveBeenCalled();
expect(mocks.local).not.toHaveBeenCalled();
});
});
describe("remote Kimi installation fallback and failure admission", () => {
it("prepares the selected current pack before testing an older SDK image", async () => {
// Actual credential-free artifacts from the failed source-16 qualification:
// the immutable SDK image carries this older helper, while the controller
// and complete selected Linux pack require the newer bytes.
const imageHelper = "9e87ba56ce4663aa36f310a7eb6224bebb7d838be479c1741c3393577f2953c7";
const selectedHelper = "22bf5125adbda95208d26762d4889de2b365a351b403ac92863f8f838f6d36fc";
const installedRoot = "/workspace/.paperclip-runtime/paperclip-runner/provider-pack";
const runner = { execute: vi.fn() };
const selected = { configuredRoot: "/controller/complete-linux-pack",
manifest: { payload: { target: { platform: "linux", architecture: "x64" } } } };
mocks.select.mockReturnValue(selected);
mocks.prepare.mockResolvedValue({ root: installedRoot, manifest: selected.manifest, source: "uploaded" });
mocks.digest.mockResolvedValue(`sha256:${selectedHelper}`);
vi.stubEnv("PAPERCLIP_RUNNER_REMOTE_PROVIDER_PACK_PATH", "/controller/complete-linux-pack");
vi.stubEnv("PAPERCLIP_RUNNER_REMOTE_BINARY_PATH", "/controller/linux-runnerd");
mocks.command.mockImplementation(async (_id, _target, command) => {
expect(command).toContain(selectedHelper);
if (command.includes("pack=/opt/paperclip-runner/provider-pack")) {
expect(imageHelper).not.toBe(selectedHelper);
return { ...response(), stdout: "", exitCode: 126 };
}
if (command.includes('pack="$HOME/.local/share/paperclip-runner/provider-pack"')) {
return { ...response(), stdout: "", exitCode: 127 };
}
expect(command).toContain(`pack='${installedRoot}'`);
return response();
});
const target = { ...remote, runner } as AdapterExecutionTarget;
await expect(probeKimiExecutionTarget(apiKey, target, model)).resolves.toEqual(verified());
expect(mocks.select).toHaveBeenCalledWith(selected.configuredRoot);
expect(mocks.prepare).toHaveBeenCalledWith({ target, runner, configuredProviderPackRoot: selected.configuredRoot, manifest: selected.manifest });
expect(mocks.command).toHaveBeenCalledOnce();
expect(mocks.prepare.mock.invocationCallOrder[0]).toBeLessThan(mocks.command.mock.invocationCallOrder[0]!);
expect(mocks.local).not.toHaveBeenCalled();
});
it.each(["unreadable-source", "wrong-platform", "missing-transport", "failed-install"])("does not transmit credentials or use the baked image after %s", async failure => {
vi.stubEnv("PAPERCLIP_RUNNER_REMOTE_PROVIDER_PACK_PATH", "/controller/selected-pack");
mocks.select.mockReturnValue({ configuredRoot: "/controller/selected-pack",
manifest: { payload: { target: { platform: failure === "wrong-platform" ? "darwin" : "linux", architecture: "x64" } } } });
if (failure === "unreadable-source") mocks.select.mockImplementation(() => { throw new Error(apiKey); });
if (failure === "failed-install") mocks.prepare.mockRejectedValue(new Error(apiKey));
const target = { ...remote, ...(failure === "missing-transport" ? {} : { runner: { execute: vi.fn() } }) } as AdapterExecutionTarget;
const error = await probeKimiExecutionTarget(apiKey, target, model).catch(error => error);
expect(error).toMatchObject({ status: 422, details: { code: "KIMI_INSTALLATION_INVALID" } });
expect(error.message).not.toContain(apiKey);
expect(mocks.command).not.toHaveBeenCalled();
expect(mocks.local).not.toHaveBeenCalled();
if (failure !== "failed-install") expect(mocks.prepare).not.toHaveBeenCalled();
});
it("uses the current qualification policy before selected-pack preparation", async () => {
vi.stubEnv("PAPERCLIP_RUNNER_REMOTE_PROVIDER_PACK_PATH", "/controller/selected-pack");
vi.stubEnv("PAPERCLIP_ACPX_CANDIDATE_ADMISSION", JSON.stringify({ schema: "paperclip.acpx-candidate-admission.v1",
companyId: "other-company", agent: "kimi", model, profileDigest: QUALIFIED_ACPX_PROFILES.kimi.commandDigest,
platform: "linux", architecture: "x64", expiresAt: Date.now() + 60_000,
nativeClosureDigest: `sha256:${KIMI_DISTRIBUTION_CLOSURE_SHA256["linux-x64"]}` }));
if (pending) {
await expect(rawProbeKimiExecutionTarget(apiKey, remote, model, { companyId }))
.rejects.toMatchObject({ details: { code: "KIMI_QUALIFICATION_REQUIRED" } });
expect(mocks.select).not.toHaveBeenCalled();
expect(mocks.prepare).not.toHaveBeenCalled();
expect(mocks.command).not.toHaveBeenCalled();
} else {
const runner = { execute: vi.fn() };
const selected = { configuredRoot: "/controller/selected-pack",
manifest: { payload: { target: { platform: "linux", architecture: "x64" } } } };
mocks.select.mockReturnValue(selected);
mocks.prepare.mockResolvedValue({ root: "/workspace/.paperclip-runtime/paperclip-runner/provider-pack", manifest: selected.manifest, source: "uploaded" });
await expect(rawProbeKimiExecutionTarget(apiKey, { ...remote, runner } as AdapterExecutionTarget, model, { companyId })).resolves.toEqual(verified());
expect(mocks.prepare).toHaveBeenCalledOnce();
expect(mocks.command.mock.calls[0]![3].env).not.toHaveProperty("PAPERCLIP_ACPX_CANDIDATE_ADMISSION");
}
});
it.each(["missing", "different-cli-bytes", "installation", "profile", "closure"])("tries only the fixed home pack after %s", async variant => {
const first = variant === "missing" ? { ...response(), stdout: "", exitCode: 127 }
: variant === "different-cli-bytes" ? { ...response(), stdout: "", exitCode: 126 }
: variant === "installation" ? response(failed("KIMI_INSTALLATION_INVALID"), 1)
: response({ ...verified(), [variant === "profile" ? "profileDigest" : "closureDigest"]: `sha256:${"0".repeat(64)}` });
mocks.command.mockResolvedValueOnce(first).mockResolvedValueOnce(response());
await expect(probeKimiExecutionTarget(apiKey, remote, model)).resolves.toEqual(verified());
expect(mocks.command).toHaveBeenCalledTimes(2);
expect(mocks.command.mock.calls[1]![2]).toContain('pack="$HOME/.local/share/paperclip-runner/provider-pack"');
expect(mocks.local).not.toHaveBeenCalled();
});
it.each(["KIMI_AUTH_REQUIRED", "KIMI_AUTH_REJECTED", "KIMI_MODEL_UNAVAILABLE", "KIMI_METADATA_TIMEOUT", "KIMI_METADATA_INVALID", "KIMI_PROVIDER_ERROR"])("keeps %s final and strips remote text", async code => {
mocks.command.mockResolvedValueOnce(response(failed(code), 1));
const error = await probeKimiExecutionTarget(apiKey, remote, model).catch(error => error);
expect(error).toMatchObject({ status: 422, details: { code } });
expect(error.message).not.toContain(apiKey);
expect(mocks.command).toHaveBeenCalledOnce();
expect(mocks.local).not.toHaveBeenCalled();
});
it.each([
{ stdout: "not json" }, { stdout: "x".repeat(65537) }, { signal: "SIGTERM" }, { timedOut: true },
{ exitCode: 1 }, { stdout: JSON.stringify({ ...verified(), promptSent: true }) },
{ stdout: JSON.stringify({ ...verified(), arbitrary: true }) },
])("never accepts malformed or interrupted success %#", alteration => {
mocks.command.mockResolvedValue({ ...response(), ...alteration });
return expect(probeKimiExecutionTarget(apiKey, remote, model)).rejects.toMatchObject({ details: { code: "KIMI_REQUEST_FAILED" } });
});
it("fails closed when no fixed remote installation matches without trying the controller", async () => {
mocks.command.mockResolvedValue({ ...response(), stdout: "", exitCode: 127 });
await expect(probeKimiExecutionTarget(apiKey, remote, model)).rejects.toMatchObject({ details: { code: "KIMI_INSTALLATION_INVALID" } });
expect(mocks.command).toHaveBeenCalledTimes(2);
expect(mocks.local).not.toHaveBeenCalled();
});
it("rejects a receipt for another model or release platform", async () => {
mocks.command.mockResolvedValue(response({ ...verified(), model: "unselected-model" }));
await expect(probeKimiExecutionTarget(apiKey, remote, model)).rejects.toMatchObject({ details: { code: "KIMI_MODEL_UNAVAILABLE" } });
expect(mocks.command).toHaveBeenCalledOnce();
mocks.command.mockResolvedValue(response({ ...verified(), target: "darwin-arm64", closureDigest: `sha256:${KIMI_DISTRIBUTION_CLOSURE_SHA256["darwin-arm64"]}` }));
await expect(probeKimiExecutionTarget(apiKey, remote, model)).rejects.toMatchObject({ details: { code: "KIMI_INSTALLATION_INVALID" } });
});
it("requires the current bundled probe before transmitting credentials", async () => {
for (const value of ["", "arbitrary-path", `sha256:${"f".repeat(63)}`]) {
mocks.digest.mockResolvedValue(value);
await expect(probeKimiExecutionTarget(apiKey, remote, model)).rejects.toMatchObject({ details: { code: "KIMI_INSTALLATION_INVALID" } });
}
mocks.digest.mockRejectedValue(new Error(`missing build ${apiKey}`));
const error = await probeKimiExecutionTarget(apiKey, remote, model).catch(error => error);
expect(error.message).not.toContain(apiKey);
expect(mocks.command).not.toHaveBeenCalled();
});
it("shares the 35 second budget across local digest and both remote locations", async () => {
let now = 1000;
vi.spyOn(Date, "now").mockImplementation(() => now);
mocks.digest.mockImplementation(async () => { now += 2000; return digest; });
mocks.command.mockImplementationOnce(async () => { now += 20_000; return { ...response(), stdout: "", exitCode: 127 }; });
await expect(probeKimiExecutionTarget(apiKey, remote, model)).resolves.toEqual(verified());
expect(mocks.command.mock.calls[0]![3].timeoutSec).toBe(30);
expect(mocks.command.mock.calls[1]![3].timeoutSec).toBe(10);
mocks.command.mockReset().mockImplementation(async () => { now += 35_001; return response(); });
await expect(probeKimiExecutionTarget(apiKey, remote, model)).rejects.toMatchObject({ details: { code: "KIMI_REQUEST_FAILED" } });
});
it("sanitizes transport errors without trying another host or local credentials", async () => {
mocks.command.mockRejectedValue(new Error(`transport ${apiKey}`));
const error = await probeKimiExecutionTarget(apiKey, remote, model).catch(error => error);
expect(error).toMatchObject({ details: { code: "KIMI_REQUEST_FAILED" } });
expect(error.message).not.toContain(apiKey);
expect(mocks.command).toHaveBeenCalledOnce();
expect(mocks.local).not.toHaveBeenCalled();
});
});
it("keeps the selected SSH key out of actual ssh argv and delivers it only over the existing stdin pipe", async () => {
const wireApiKey = "wire-private-sentinel-abc123", wireModel = "wire-model";
mocks.command.mockResolvedValue(response({ ...verified(), model: wireModel }));
mocks.ssh.mockResolvedValue({ stdout: JSON.stringify({ ...verified(), model: wireModel }), stderr: "" });
const spec = { host: "fixture.invalid", username: "fixture", port: 22, remoteWorkspacePath: "/workspace",
privateKey: null, knownHosts: null, strictHostKeyChecking: true };
const target = { kind: "remote", transport: "ssh", remoteCwd: "/workspace", spec } as AdapterExecutionTarget;
await probeKimiExecutionTarget(wireApiKey, target, wireModel);
const selected = mocks.ssh.mock.calls[0] ?? [spec, mocks.command.mock.calls[0]![2], { env: mocks.command.mock.calls[0]![3].env }];
const { runSshCommand } = await vi.importActual<typeof import("@paperclipai/adapter-utils/ssh")>("@paperclipai/adapter-utils/ssh");
const result = await runSshCommand(spec, selected[1], selected[2]);
expect(mocks.wire).toHaveLength(1);
const argv = JSON.stringify(mocks.wire[0]!.args);
for (const value of [wireApiKey, wireModel, Buffer.from(wireApiKey).toString("base64"), Buffer.from(wireApiKey).toString("hex")]) expect(argv).not.toContain(value);
expect(mocks.wire[0]!.stdin).toBe(true);
const input = JSON.parse(result.stdout);
expect(input).toMatchObject({ apiKey: wireApiKey, model: wireModel });
if (pending) expect(input.candidateAdmission).toMatchObject({ companyId, model: wireModel, platform: "linux", architecture: "x64" });
else expect(input).not.toHaveProperty("candidateAdmission");
});
it("loads the verified entry after decoding private stdin without interpreting key or model text", async () => {
await probeKimiExecutionTarget(apiKey, { ...remote, transport: "ssh" } as AdapterExecutionTarget, model);
const command = mocks.ssh.mock.calls[0]![1] as string;
const bootstrap = command.match(/ -e '([^']+)' "\$entry"/)?.[1];
expect(bootstrap).toBeTruthy();
const root = await mkdtemp(path.join(os.tmpdir(), "kimi-probe-stdin-"));
try {
const entry = path.join(root, "entry.cjs");
await writeFile(entry, 'process.stdout.write(JSON.stringify({ key:process.env.KIMI_API_KEY, model:process.env.PAPERCLIP_KIMI_PROBE_MODEL, ambient:process.env.UNSELECTED_SECRET }));');
const execute = (input: string) => new Promise<{ code: number | null; stdout: string; stderr: string }>((resolve, reject) => {
const child = spawn(process.execPath, ["-e", bootstrap!, entry], { env: {}, stdio: ["pipe", "pipe", "pipe"] });
let stdout = "", stderr = "";
child.stdout.on("data", chunk => { stdout += chunk; }); child.stderr.on("data", chunk => { stderr += chunk; });
child.on("error", reject); child.on("close", code => resolve({ code, stdout, stderr })); child.stdin.end(input);
});
const result = await execute(JSON.stringify({ apiKey, model }));
expect(result.code).toBe(0);expect(result.stderr).toBe("");expect(JSON.parse(result.stdout)).toEqual({ key:apiKey, model });
for (const bad of ["not-json-private-sentinel", JSON.stringify({ apiKey: 1, model })]) {
const result = await execute(bad);expect(result.code).toBe(1);expect(result.stdout).toBe("");expect(result.stderr).toBe("");
}
} finally { await rm(root, { recursive:true, force:true }); }
});
it("preserves SSH installation fallback and typed rejection while stripping transport text", async () => {
const target = { ...remote, transport:"ssh" } as AdapterExecutionTarget;
mocks.ssh.mockRejectedValueOnce(Object.assign(new Error(apiKey), { code:127, stdout:"" }));
await expect(probeKimiExecutionTarget(apiKey, target, model)).resolves.toEqual(verified());
expect(mocks.ssh).toHaveBeenCalledTimes(2);
expect(mocks.ssh.mock.calls[1]![1]).toContain('pack="$HOME/.local/share/paperclip-runner/provider-pack"');
for (const failure of [Object.assign(new Error(apiKey), { code:1, stdout:JSON.stringify(failed("KIMI_AUTH_REJECTED")) }),
Object.assign(new Error(apiKey), { code:"ETIMEDOUT" })]) {
mocks.ssh.mockRejectedValueOnce(failure);
const error = await probeKimiExecutionTarget(apiKey,target,model).catch(error=>error);
expect(error.details.code).toBe(failure.code===1?"KIMI_AUTH_REJECTED":"KIMI_REQUEST_FAILED");expect(error.message).not.toContain(apiKey);
}
});
it("uses trusted authority only for a pending metadata Test and ordinary no-grant execution after release", async () => {
if (!pending) {
vi.stubEnv("PAPERCLIP_ACPX_CANDIDATE_ADMISSION", "unrelated-invalid-authority");
for (const context of [{}, { companyId }, { companyId, candidateAdmission: { allowAll: true } }]) {
await expect(rawProbeKimiExecutionTarget(apiKey, remote, model, context)).resolves.toEqual(verified());
}
expect(mocks.command).toHaveBeenCalledTimes(3);
for (const call of mocks.command.mock.calls) {
expect(call[2]).not.toContain("PAPERCLIP_ACPX_CANDIDATE_ADMISSION");
expect(call[3].env).not.toHaveProperty("PAPERCLIP_ACPX_CANDIDATE_ADMISSION");
}
} else {
vi.stubEnv("PAPERCLIP_ACPX_CANDIDATE_ADMISSION", undefined);
vi.stubEnv("PAPERCLIP_RUNNER_ACPX_QUALIFICATION", JSON.stringify([{ agent: "kimi", model }]));
for (const context of [{}, { companyId }, { companyId, candidateAdmission: { allowAll: true } }]) {
await expect(rawProbeKimiExecutionTarget(apiKey, remote, model, context)).rejects.toMatchObject({ details: { code: "KIMI_QUALIFICATION_REQUIRED" } });
}
expect(mocks.local).not.toHaveBeenCalled(); expect(mocks.digest).not.toHaveBeenCalled();
expect(mocks.command).not.toHaveBeenCalled(); expect(mocks.ssh).not.toHaveBeenCalled();
await probeKimiExecutionTarget(apiKey, remote, model);
mocks.command.mockClear(); mocks.digest.mockClear();
for (const context of [{ companyId: "foreign" }, {}]) {
await expect(rawProbeKimiExecutionTarget(apiKey, remote, model, context)).rejects.toMatchObject({ details: { code: "KIMI_QUALIFICATION_REQUIRED" } });
}
const grant = JSON.parse(process.env.PAPERCLIP_ACPX_CANDIDATE_ADMISSION!);
vi.stubEnv("PAPERCLIP_ACPX_CANDIDATE_ADMISSION", JSON.stringify({ ...grant, expiresAt: 0 }));
await expect(rawProbeKimiExecutionTarget(apiKey, remote, model, { companyId })).rejects.toMatchObject({ details: { code: "KIMI_QUALIFICATION_REQUIRED" } });
expect(mocks.command).not.toHaveBeenCalled(); expect(mocks.digest).not.toHaveBeenCalled();
}
});
it("omits candidate authority entirely for released metadata Test, including the actual remote shell environment", async () => {
const path = "../vendor/paperclip-runner/index.js";
vi.doMock(path, async () => {
const actual = await vi.importActual<typeof import("../vendor/paperclip-runner/index.js")>(path);
return { ...actual, QUALIFIED_ACPX_PROFILES: { ...actual.QUALIFIED_ACPX_PROFILES,
kimi: { ...actual.QUALIFIED_ACPX_PROFILES.kimi, qualificationStatus: undefined } } };
});
vi.resetModules();
const root = await mkdtemp(os.tmpdir() + "/kimi-released-probe-");
try {
vi.stubEnv("PAPERCLIP_ACPX_CANDIDATE_ADMISSION", undefined);
const { probeKimiExecutionTarget } = await import("../services/kimi-connection-probe.js");
const { resolveKimiCandidateAdmission } = await import("../services/native-runtime/acpx-qualification.js");
expect(resolveKimiCandidateAdmission({ companyId, model }, { PAPERCLIP_ACPX_CANDIDATE_ADMISSION: "stale-invalid-candidate" })).toBeUndefined();
await probeKimiExecutionTarget(apiKey, remote, model, { companyId });
const command = mocks.command.mock.calls[0]![2] as string;
const options = mocks.command.mock.calls[0]![3];
expect(command).not.toContain("PAPERCLIP_ACPX_CANDIDATE_ADMISSION");
expect(options.env).not.toHaveProperty("PAPERCLIP_ACPX_CANDIDATE_ADMISSION");
const entry = root + "/environment.cjs";
await writeFile(entry, 'process.stdout.write(JSON.stringify({candidatePresent:Object.hasOwn(process.env,"PAPERCLIP_ACPX_CANDIDATE_ADMISSION")}));');
const suffix = command.slice(command.lastIndexOf("exec /usr/bin/env -i"));
const result = await new Promise<{code:number|null;stdout:string;stderr:string}>((resolve, reject) => {
const child = spawn("/bin/sh", ["-c", suffix], { env: { node: process.execPath, entry, pack: root,
KIMI_API_KEY: apiKey, PAPERCLIP_KIMI_PROBE_MODEL: model, PAPERCLIP_ACPX_CANDIDATE_ADMISSION: "ambient-forgery" }, stdio: ["ignore", "pipe", "pipe"] });
let stdout = "", stderr = "";
child.stdout.on("data", chunk => { stdout += chunk; }); child.stderr.on("data", chunk => { stderr += chunk; });
child.on("error", reject); child.on("close", code => resolve({ code, stdout, stderr }));
});
expect(result).toEqual({ code: 0, stdout: JSON.stringify({ candidatePresent: false }), stderr: "" });
} finally {
await rm(root, { recursive: true, force: true });
vi.doUnmock(path); vi.resetModules();
}
});
@@ -79,6 +79,7 @@ vi.mock("../sentry.js", async () => {
});
import { heartbeatService } from "../services/heartbeat.js";
import { persistHeartbeatRunProcessMetadata } from "../services/heartbeat/run-lifecycle.js";
import { instanceSettingsService } from "../services/instance-settings.js";
describe("P6-25 pre-result native session recovery", () => {
@@ -1043,13 +1044,10 @@ describe.each(["unchanged", "newer_active", "stale_idle"] as const)(
);
await once(staleProviderProcess, "spawn");
expect(staleProviderProcess.pid).toEqual(expect.any(Number));
await db
.update(heartbeatRuns)
.set({
processPid: staleProviderProcess.pid!,
processStartedAt: new Date("2026-08-09T04:00:00.000Z"),
})
.where(eq(heartbeatRuns.id, runId));
await persistHeartbeatRunProcessMetadata(db, runId, {
pid: staleProviderProcess.pid!, processGroupId: null,
startedAt: new Date("2026-08-09T04:00:00.000Z").toISOString(), targetKind: "local",
});
const factoryInputs: NativeExecutionInput[] = [];
const freshStartTurn = vi.fn<NativeSession["startTurn"]>();
const freshBackend = (
+66 -2
View File
@@ -3,18 +3,25 @@ import { assertValidAdapterLoginCapability } from "@paperclipai/adapter-utils";
import { listServerAdapters, requireServerAdapter } from "./registry.js";
import * as executionTarget from "@paperclipai/adapter-utils/execution-target";
import { BUILTIN_ADAPTER_TYPES } from "./builtin-adapter-types.js";
import { QUALIFIED_ACPX_PROFILES, KIMI_DISTRIBUTION_CLOSURE_SHA256 } from "../vendor/paperclip-runner/index.js";
const { probeInstallation, probeGrokInstallation, probePiInstallation, verifyApiKey } = vi.hoisted(() => ({
const { probeInstallation, probeGrokInstallation, probePiInstallation, probeKimiInstallation, probeKimiModels, probeKimiRemote, verifyApiKey } = vi.hoisted(() => ({
verifyApiKey: vi.fn(),
probeInstallation: vi.fn(),
probeGrokInstallation: vi.fn(),
probePiInstallation: vi.fn(),
probeKimiInstallation: vi.fn(),
probeKimiModels: vi.fn(),
probeKimiRemote: vi.fn(),
}));
vi.mock("../services/ai-api-key-test.js", () => ({ validateAiApiKey: verifyApiKey }));
vi.mock("../services/kimi-connection-probe.js", () => ({ probeKimiExecutionTarget: probeKimiRemote }));
vi.mock("../vendor/paperclip-runner/live/index.js", () => ({
probeAcpxClaudeInstallation: probeInstallation,
probeAcpxGrokInstallation: probeGrokInstallation,
probeAcpxPiInstallation: probePiInstallation,
probeAcpxKimiInstallation: probeKimiInstallation,
probeKimiModels,
probeAcpxCursorInstallation: vi.fn(async () => undefined),
}));
@@ -102,8 +109,11 @@ describe("native ACPX environment checks", () => {
probeInstallation.mockReset().mockResolvedValue(undefined);
probeGrokInstallation.mockReset().mockResolvedValue(undefined);
probePiInstallation.mockReset().mockResolvedValue(undefined);
probeKimiInstallation.mockReset().mockResolvedValue(undefined);
probeKimiModels.mockReset().mockResolvedValue(undefined);
probeKimiRemote.mockReset().mockResolvedValue(undefined);
});
afterEach(() => vi.restoreAllMocks());
afterEach(() => { vi.restoreAllMocks(); vi.unstubAllEnvs(); });
const context = {
companyId: "company-test",
@@ -111,6 +121,53 @@ describe("native ACPX environment checks", () => {
config: { provider: "acpx", acpxAgent: "claude", model: "claude-sonnet-5" },
};
it("requires an explicit Kimi credential and probes its own installed runtime during qualification", async () => {
const model = "explicit-kimi-model";
authorizeKimi(model);
const config = { provider: "acpx", acpxAgent: "kimi", model };
const missing = await requireServerAdapter("paperclip_runner").testEnvironment!({ ...context, config });
expect(missing.status).toBe("fail");
expect(missing.checks[0].code).toBe("KIMI_AUTH_REQUIRED");
expect(probeKimiInstallation).not.toHaveBeenCalled();
const ready = await requireServerAdapter("paperclip_runner").testEnvironment!({ ...context, config: { ...config, env: { KIMI_API_KEY: "fixture-key" } } });
expect(ready.status).toBe("pass");
expect(probeKimiRemote).toHaveBeenCalledExactlyOnceWith("fixture-key", undefined, model, { companyId: context.companyId });
expect(probeKimiInstallation).not.toHaveBeenCalled();
expect(probeKimiModels).not.toHaveBeenCalled();
expect(probeInstallation).not.toHaveBeenCalled();
expect(verifyApiKey).not.toHaveBeenCalled();
expect(ready.checks[0]).toMatchObject({ code: "kimi_metadata_verified", message: expect.stringContaining("No model prompt was sent") });
});
it("keeps a rejected Kimi catalog credential out of the ready lifecycle", async () => {
const model = "explicit-kimi-model";
authorizeKimi(model);
probeKimiRemote.mockRejectedValue(Object.assign(new Error("The Kimi Code credential was rejected by the model catalog."), { code: "KIMI_AUTH_REQUIRED" }));
const result = await requireServerAdapter("paperclip_runner").testEnvironment!({ ...context,
config: { provider: "acpx", acpxAgent: "kimi", model, env: { KIMI_API_KEY: "fixture-key" } },
});
expect(result.status).toBe("fail");
expect(result.checks[0].message).toContain("credential was rejected");
});
it("requires the selected remote Kimi runtime and authenticated catalog instead of a host installation", async () => {
const model = "explicit-kimi-model";
authorizeKimi(model);
const runner = { execute: vi.fn() };
const target = { kind: "remote" as const, transport: "sandbox" as const, remoteCwd: "/workspace", providerKey: "test", runner };
const result = await requireServerAdapter("paperclip_runner").testEnvironment!({ ...context,
config: { provider: "acpx", acpxAgent: "kimi", model, env: { KIMI_API_KEY: "fixture-key" } },
executionTarget: target,
});
expect(result.status).toBe("pass");
expect(result.checks[0]).toMatchObject({ code: "kimi_metadata_verified" });
expect(probeKimiRemote).toHaveBeenCalledExactlyOnceWith("fixture-key", target, model, { companyId: context.companyId });
expect(runner.execute).not.toHaveBeenCalled();
expect(probeKimiInstallation).not.toHaveBeenCalled();
expect(probeKimiModels).not.toHaveBeenCalled();
});
it("verifies a native Claude key without the global CLI or a model prompt", async () => {
const result = await requireServerAdapter("paperclip_runner").testEnvironment!({ ...context,
config: { ...context.config, env: { ANTHROPIC_API_KEY: "test-key" } },
@@ -256,3 +313,10 @@ describe("native ACPX environment checks", () => {
expect(result.checks[0].code).toBe("acpx_runtime_unavailable");
});
});
function authorizeKimi(model: string) {
vi.stubEnv("PAPERCLIP_ACPX_CANDIDATE_ADMISSION", JSON.stringify({ schema: "paperclip.acpx-candidate-admission.v1",
companyId: "company-test", agent: "kimi", model, profileDigest: QUALIFIED_ACPX_PROFILES.kimi.commandDigest,
platform: "linux", architecture: "x64", nativeClosureDigest: `sha256:${KIMI_DISTRIBUTION_CLOSURE_SHA256["linux-x64"]}`,
expiresAt: Date.now() + 60_000 }));
}
+18 -3
View File
@@ -9,6 +9,7 @@ import {
buildSandboxNpmInstallCommand,
getAdapterSessionManagement,
PAPERCLIP_RUNNER_PERMISSION_CAPABILITIES,
PAPERCLIP_RUNNER_ACPX_PROFILES,
} from "@paperclipai/adapter-utils";
import type { AdapterLoginCapability } from "@paperclipai/adapter-utils";
import { runAdapterExecutionTargetShellCommand } from "@paperclipai/adapter-utils/execution-target";
@@ -469,8 +470,22 @@ const paperclipRunnerAdapter: ServerAdapterModule = {
checks: [{ code: "copilot_metadata_verified", level: "info" as const,
message: "The verified Copilot runtime authenticated this account and accepted the selected model. No model prompt was sent." }] };
}
if (profile.acpxAgent !== "claude" && profile.acpxAgent !== "grok" && profile.acpxAgent !== "cursor" && profile.acpxAgent !== "pi") throw new Error("Select Codex to use the native Codex runner.");
if (profile.acpxAgent !== "claude" && profile.acpxAgent !== "grok" && profile.acpxAgent !== "cursor" && profile.acpxAgent !== "pi" && profile.acpxAgent !== "kimi") throw new Error("Select Codex to use the native Codex runner.");
if (profile.acpxAgent === "kimi") {
const environment = context.config.env as Record<string, unknown> | undefined;
if (typeof environment?.KIMI_API_KEY !== "string" || !environment.KIMI_API_KEY.trim()) {
return { adapterType: "paperclip_runner", status: "fail" as const, testedAt: new Date().toISOString(),
checks: [{ code: "KIMI_AUTH_REQUIRED", level: "error" as const, message: "Bind a company secret to KIMI_API_KEY in the agent environment." }] };
}
}
const target = context.executionTarget;
if (profile.acpxAgent === "kimi") {
const { probeKimiExecutionTarget } = await import("../services/kimi-connection-probe.js");
await probeKimiExecutionTarget((context.config.env as Record<string, string>).KIMI_API_KEY, target, profile.model, { companyId: context.companyId });
return { adapterType: "paperclip_runner", status: "pass" as const, testedAt: new Date().toISOString(),
checks: [{ code: "kimi_metadata_verified", level: "info" as const,
message: "The selected environment verified the pinned Kimi Code runtime. Its Code model catalog accepted the credential and lists the exact selected model. No model prompt was sent." }] };
}
if (target?.kind === "remote") {
const probe = await runAdapterExecutionTargetShellCommand(
`acpx-platform-${crypto.randomUUID()}`, target, "uname -s && uname -m",
@@ -478,7 +493,7 @@ const paperclipRunnerAdapter: ServerAdapterModule = {
);
if (probe.timedOut || probe.exitCode !== 0) throw new Error("Could not verify the remote ACPX runner platform.");
const [os, arch] = probe.stdout.trim().split(/\s+/);
if (!((os === "Linux" && (arch === "x86_64" || (profile.acpxAgent === "claude" && ["aarch64", "arm64"].includes(arch)))) || (os === "Darwin" && (arch === "arm64" || (profile.acpxAgent !== "grok" && arch === "x86_64"))))) {
if (!((os === "Linux" && (arch === "x86_64" || (profile.acpxAgent === "claude" && ["aarch64", "arm64"].includes(arch)))) || (os === "Darwin" && (arch === "arm64" || (!["grok"].includes(profile.acpxAgent) && arch === "x86_64"))))) {
throw new Error(`ACPX ${profile.acpxAgent} requires a qualified Linux ${profile.acpxAgent === "claude" ? "x64/ARM64" : "x64"} or macOS architecture.`);
}
return verifyNativeClaudeApiKey(context.config, {
@@ -615,7 +630,7 @@ const paperclipRunnerAdapter: ServerAdapterModule = {
},
{
key: "acpxAgent", label: "ACP agent", type: "select" as const, default: "claude",
options: [{ value: "claude", label: "Claude" }, { value: "grok", label: "Grok Build" }, { value: "copilot", label: "GitHub Copilot" }],
options: PAPERCLIP_RUNNER_ACPX_PROFILES.filter(profile => profile.qualified).map(profile => ({ value: profile.value, label: profile.label })),
meta: { visibleWhen: { key: "provider", value: "acpx" } },
},
{
+14 -5
View File
@@ -41,7 +41,7 @@ import { toolConnections } from "@paperclipai/db";
import { aiConnectionService } from "../services/ai-connections.js";
import { probeCopilotConnection, probeCopilotExecutionTarget } from "../services/copilot-connection-probe.js";
import { antigravityConnectionModels } from "../services/antigravity-connection-models.js";
import { resolveAntigravityCandidateAdmission } from "../services/native-runtime/acpx-qualification.js";
import { resolveAntigravityCandidateAdmission, resolveKimiCandidateAdmission } from "../services/native-runtime/acpx-qualification.js";
import { defaultAiConnectionForHire } from "../services/agent-ai-connection-default.js";
import { assertAiConnectionCreateAccess, canInstallSharedAiConnectionForNewAgent, responsibleUserForAiRequest } from "./ai-connections.js";
import { isAiConnectionCompatible } from "@paperclipai/shared";
@@ -1874,6 +1874,11 @@ export function agentRoutes(
try { antigravityCandidate = resolveAntigravityCandidateAdmission({ companyId, model: selected.model }); }
catch { throw unprocessable("This Google Antigravity candidate requires the exact enabled model and a current operator authorization.", { code: "paperclip_runner_antigravity_candidate_invalid" }); }
}
let kimiCandidate: ReturnType<typeof resolveKimiCandidateAdmission>;
if (companyId && selected?.provider === "acpx" && selected.acpxAgent === "kimi" && typeof selected.model === "string") {
try { kimiCandidate = resolveKimiCandidateAdmission({ companyId, model: selected.model }); }
catch { throw unprocessable("This Kimi candidate requires the exact enabled model and a current operator authorization.", { code: "paperclip_runner_kimi_candidate_invalid" }); }
}
if (asRecord(config)?.provider === "openai_dot") {
if (experimental.enableOpenAiDot !== true) throw unprocessable(
"OpenAI Dot is experimental and disabled on this instance.",
@@ -1886,7 +1891,7 @@ export function agentRoutes(
} else if (!([undefined, "codex", "opencode"].includes(asRecord(config)?.provider as string | undefined)) &&
!(asRecord(config)?.provider === "acpx" && asRecord(config)?.acpxAgent === "copilot") &&
!["claude_local", "pi_local", "grok_local"].includes(agentHarnessType("paperclip_runner", asRecord(config) ?? {})) &&
experimental.enableNativeRunner !== true && !antigravityCandidate) {
experimental.enableNativeRunner !== true && !antigravityCandidate && !kimiCandidate) {
throw unprocessable(
"Paperclip Runner is experimental and disabled on this instance.",
{ code: "paperclip_runner_rollout_disabled" },
@@ -1961,9 +1966,9 @@ export function agentRoutes(
throw unprocessable("Select a Paperclip Runner provider before converting this agent.");
}
const next = { ...defaults, ...input.nextAdapterConfig };
if (next.provider === "acpx" && next.acpxAgent === "pi") {
if (next.provider === "acpx" && (next.acpxAgent === "pi" || next.acpxAgent === "kimi")) {
next.model = input.nextAdapterConfig.model
?? (input.previousAdapterType === "pi_local" ? input.previousAdapterConfig.model : "");
?? (input.previousAdapterType === `${next.acpxAgent}_local` ? input.previousAdapterConfig.model : "");
} else if (!asNonEmptyString(next.model)) next.model = defaults.model;
return normalizeLegacyRunnerProvider(next);
}
@@ -2200,7 +2205,7 @@ export function agentRoutes(
);
const incompleteModelSetup = input.setupOnly && input.adapterType === "paperclip_runner"
&& (normalizedAdapterConfig.provider === "opencode"
|| (normalizedAdapterConfig.provider === "acpx" && normalizedAdapterConfig.acpxAgent === "pi"))
|| (normalizedAdapterConfig.provider === "acpx" && ["pi", "kimi"].includes(String(normalizedAdapterConfig.acpxAgent))))
&& (normalizedAdapterConfig.model === undefined || normalizedAdapterConfig.model === "");
// Inactive drafts can precede model selection. Test/hire always validate the complete profile.
if (!incompleteModelSetup) await assertAdapterConfigConstraints(
@@ -3076,6 +3081,10 @@ export function agentRoutes(
return;
}
const provider = asNonEmptyString(req.query.provider);
if (type === "paperclip_runner" && provider === "acpx" && req.query.acpxAgent === "kimi") {
res.json([]);
return;
}
if (type === "paperclip_runner" && provider === "acpx" && req.query.acpxAgent === "antigravity") {
if (environment && environment.driver !== "local") throw unprocessable("Google Antigravity account model discovery requires the Paperclip host.");
let binding: AiConnectionBinding = { provider: "google", method: "subscription", mode: "responsible_user" };
@@ -229,7 +229,9 @@ describe("run failure diagnostics", () => {
const result = sanitizeRunFailureDiagnostics(collectRunFailureDiagnostics(run({ errorCode: "adapter_failed" }), { error: thrown }));
expect(result.execution).toEqual({ remoteRunnerRecoveryStage: "process_verification", remoteRunnerRecoveryReason: "process_not_running" });
expect(result.exceptions).toMatchObject([{ code: "runner_remote_process_identity_unavailable" }]);
expect(JSON.stringify(result)).not.toContain("private");
expect(JSON.stringify(result)).not.toContain("private-path");
expect(result.exceptions[0]).not.toHaveProperty("path");
expect(result.exceptions[0]).not.toHaveProperty("pid");
const transport = attachRemoteRunnerRecoveryDiagnostic(new Error("socket closed"),
{ stage: "identity_marker", reason: "probe_transport_failed" });
@@ -1,6 +1,7 @@
import { notifyInstructionCollection } from "./instruction-collection-notifications.js";
import fs from "node:fs/promises";
import path from "node:path";
import { isDeepStrictEqual } from "node:util";
import { and, eq, inArray, isNull, sql } from "drizzle-orm";
import { agents, environmentLeases, environments, agentInstructionWorkingCopies as copies, type Db } from "@paperclipai/db";
import { syncDirectoryToSsh, restoreWorkspaceFromSshExecution } from "@paperclipai/adapter-utils/ssh";
@@ -20,6 +21,11 @@ import { cachedAgentFileManifest, captureAgentFileCheckpoint, checkpointBaseline
import { logger } from "../middleware/logger.js";
type Copy = typeof copies.$inferSelect;
function adoptionAuthority(row: Copy) {
const { updatedAt: _updatedAt, nextAttemptAt: _nextAttemptAt, ...authority } = row;
return authority;
}
export class AgentDirectoryReuseInvalidatedError extends Error {}
const completed = new Set(["saved", "unchanged", "resolved", "unavailable"]);
const transports = new Map<string, PreparedAdapterExecutionTargetRuntime>();
@@ -291,10 +297,22 @@ export function agentDirectoryWorkingCopyService(db: Db, get: (companyId: string
// adopt_remote constructs a collector on the successor target without
// uploading the stale host mirror over the retained remote directory.
const replacement = remote ? await transport({ ...prior, runId: input.runId }, remote, true) : undefined;
const expectedPrior = prior;
let adopted: Copy | null;
try { adopted = await store.locked(input.companyId, input.agentId, bound, false, async (tx, _agent, canonical) => {
if (remote && !validLeases(await tx.select().from(environmentLeases).where(and(
eq(environmentLeases.companyId, input.companyId), inArray(environmentLeases.id, leaseIds))).for("update"))) return null;
const [lockedPrior] = await tx.select().from(copies).where(and(eq(copies.companyId, input.companyId),
eq(copies.runId, expectedPrior.runId), eq(copies.agentId, input.agentId))).for("update");
if (!lockedPrior || !isDeepStrictEqual(adoptionAuthority(lockedPrior), adoptionAuthority(expectedPrior))) {
const fields = ["state", "receipt", "baseHash", "localRoot", "executionRoot", "location", "processStoppedAt", "responsibleUserId", "entryFile", "attempts", "candidateHash", "errorCode", "errorMessage", "baseRevisionId", "candidateBase64", "createdAt"] as const;
const diagnostic = { stage: "locked_adoption_authority", predecessorPresent: Boolean(lockedPrior), observedAfterInitialRead: true,
changedFields: lockedPrior ? fields.filter(field => !isDeepStrictEqual(lockedPrior[field], expectedPrior[field])) : [] };
logger.warn({ companyId: input.companyId, agentId: input.agentId, runId: input.runId, previousRunId: input.previousRunId,
adoptionConflict: diagnostic }, "Agent directory adoption conflict");
throw conflict("Agent directory ownership changed", diagnostic);
}
prior = lockedPrior;
const current = await inspectAgentDirectory(canonical);
const canonicalChanged = agentDirectoryBaselineDigest(current.snapshot) !== agentDirectoryBaselineDigest(baseline(prior));
if (canonicalChanged && !input.allowRetirementHandoff) return null;
@@ -304,7 +322,18 @@ export function agentDirectoryWorkingCopyService(db: Db, get: (companyId: string
.where(and(eq(copies.companyId, input.companyId), eq(copies.runId, prior.runId), eq(copies.agentId, input.agentId), eq(copies.state, "unchanged_turn"),
eq(copies.updatedAt, prior.updatedAt), eq(copies.baseHash, prior.baseHash), eq(copies.localRoot, prior.localRoot),
isNull(copies.processStoppedAt), sql`${copies.receipt}->>'retainedByRunId' IS NULL`)).returning();
if (!claimed) throw conflict("Agent directory ownership changed");
if (!claimed) {
const [conditions] = await tx.select({ unchangedTurn: eq(copies.state, "unchanged_turn"),
timestampMatches: eq(copies.updatedAt, prior.updatedAt), baselineMatches: eq(copies.baseHash, prior.baseHash),
localRootMatches: eq(copies.localRoot, prior.localRoot), unstopped: isNull(copies.processStoppedAt),
successorAbsent: sql<boolean>`${copies.receipt}->>'retainedByRunId' IS NULL`,
receiptMatches: sql<boolean>`${copies.receipt} IS NOT DISTINCT FROM ${JSON.stringify(prior.receipt)}::jsonb`,
}).from(copies).where(and(eq(copies.companyId, input.companyId), eq(copies.runId, prior.runId), eq(copies.agentId, input.agentId)));
const diagnostic = { stage: "adoption_claim", observedAfterClaim: true, predecessorPresent: Boolean(conditions), conditions: conditions ?? null };
logger.warn({ companyId: input.companyId, agentId: input.agentId, runId: input.runId, previousRunId: input.previousRunId,
adoptionConflict: diagnostic }, "Agent directory adoption conflict");
throw conflict("Agent directory ownership changed", diagnostic);
}
const { createdAt: _createdAt, updatedAt: _updatedAt, ...values } = prior;
const [next] = await tx.insert(copies).values({ ...values, runId: input.runId, responsibleUserId: bound.onBehalfOfUserId!, state: "prepared",
receipt: { ...prior.receipt, materializationRunId: prior.receipt?.materializationRunId ?? prior.runId,
+303 -2
View File
@@ -1,6 +1,8 @@
import { randomUUID } from "node:crypto";
import { spawn } from "node:child_process";
import { once } from "node:events";
import type { Db } from "@paperclipai/db";
import { activityLog, agents, applyPendingMigrations, companies, createDb, EMBEDDED_POSTGRES_TEST_TIMEOUT_MS, environments, environmentLeases, heartbeatRuns, issueRecoveryActions, issues, nativeRunFinalizations, nativeRunnerCheckpoints } from "@paperclipai/db";
import { activityLog, agents, applyPendingMigrations, companies, createDb, EMBEDDED_POSTGRES_TEST_TIMEOUT_MS, environments, environmentLeases, heartbeatRunEvents, heartbeatRuns, issueRecoveryActions, issues, nativeRunFinalizations, nativeRunnerCheckpoints } from "@paperclipai/db";
import { eq } from "drizzle-orm";
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase } from "../../__tests__/helpers/embedded-postgres.js";
@@ -19,7 +21,10 @@ import { logger } from "../../middleware/logger.js";
import { listExpiredNativeRunnerAuthorities } from "../native-runtime/native-runner-checkpoint.js";
import { DELIVERY_QUEUES, subscribeDeliveryWork } from "../delivery-work-notifications.js";
import { resolveExternalTestDatabaseUrl } from "../../__tests__/helpers/external-test-database.js";
import { hasRemoteTerminationReceipt } from "../remote-execution-termination.js";
import { hasRemoteTerminationReceipt, remoteTerminationReceipt } from "../remote-execution-termination.js";
import { persistHeartbeatRunProcessMetadata } from "./run-lifecycle.js";
import { appendHeartbeatRunEvent } from "../heartbeat-run-events.js";
import { hasNativeLocalProcessStop, PROCESS_START_REQUESTED, releaseSettledNativeRunProcess } from "../native-local-process-stop.js";
type Run = typeof heartbeatRuns.$inferSelect;
@@ -202,6 +207,302 @@ describePostgres("heartbeat recovery module ownership", () => {
return (await db.insert(heartbeatRuns).values({ id: runId, companyId, agentId, issueId, invocationSource: "automation", status: "running", runtimeMode: "legacy" }).returning())[0];
}
async function ownedProcess() {
const child = spawn(process.execPath, ["-e", "setInterval(() => {}, 1000)", `paperclip-recovery-${randomUUID()}`], {
stdio: "ignore", detached: true, env: { PATH: process.env.PATH },
});
await once(child, "spawn");
return {
pid: child.pid!,
async stop() {
if (child.exitCode !== null || child.signalCode !== null) return;
const exited = once(child, "exit");
child.kill("SIGTERM");
await exited;
},
alive() { return process.kill(child.pid!, 0); },
};
}
async function nativeProcessFixture(pid: number, targetKind: "local" | "remote", status: "running" | "failed") {
const run = await seedRun();
await db.update(heartbeatRuns).set({ runtimeMode: "native", nativeIssueId: run.issueId, status,
nativePhase: status === "failed" ? "retryable_failure" : "running",
errorCode: status === "failed" ? "native_session_interrupted" : null,
error: status === "failed" ? "native session timed out after 120000ms" : null,
finishedAt: status === "failed" ? new Date() : null,
}).where(eq(heartbeatRuns.id, run.id));
await db.insert(nativeRunFinalizations).values({ runId: run.id, companyId: run.companyId, issueId: run.issueId!,
phase: status === "failed" ? "retryable_failure" : "running", nextAttemptAt: new Date(Date.now() - 1) });
await persistHeartbeatRunProcessMetadata(db, run.id, { pid, processGroupId: pid,
startedAt: new Date().toISOString(), targetKind });
const deps = callbacks(db);
deps.setRunStatusFromLive.mockImplementation(async (id, nextStatus, fromStatuses, patch) => {
const [current] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, id));
if (!current || !fromStatuses.includes(current.status)) return { updated: false, run: current ?? null };
const [updated] = await db.update(heartbeatRuns).set({ ...patch, status: nextStatus })
.where(eq(heartbeatRuns.id, id)).returning();
return { updated: true, run: updated };
});
return {
run, deps,
current: async () => (await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, run.id)))[0],
async cleanup() {
await Promise.all([...deps.activeRunExecutionPromises]);
await db.delete(environmentLeases).where(eq(environmentLeases.heartbeatRunId, run.id));
await db.delete(nativeRunFinalizations).where(eq(nativeRunFinalizations.runId, run.id));
await db.delete(heartbeatRunEvents).where(eq(heartbeatRunEvents.runId, run.id));
await db.delete(heartbeatRuns).where(eq(heartbeatRuns.id, run.id));
},
};
}
it.each(["failed", "running"] as const)("does not attribute a remote %s run's PID to a live host process", async status => {
const child = await ownedProcess();
const f = await nativeProcessFixture(child.pid, "remote", status);
try {
await db.update(agents).set({ adapterType: "process" }).where(eq(agents.id, f.run.agentId));
const before = await f.current();
expect(await createHeartbeatRecovery(db, f.deps).reapOrphanedRuns()).toEqual({ reaped: 0, runIds: [] });
expect(await f.current()).toMatchObject({ status, errorCode: before.errorCode, error: before.error,
processPid: child.pid, processGroupId: child.pid, processStartedAt: before.processStartedAt });
expect(f.deps.setRunStatusFromLive).not.toHaveBeenCalled();
expect(f.deps.executeRun).not.toHaveBeenCalled();
expect(f.deps.terminateHeartbeatRunProcess).not.toHaveBeenCalled();
expect(await hasNativeLocalProcessStop(db, f.run.companyId, f.run.id)).toBe(false);
expect(child.alive()).toBe(true);
} finally { try { await f.cleanup(); } finally { await child.stop(); } }
});
async function remoteRetryFixture(pid: number, target: "workspace" | "plugin_task" = "workspace") {
const fixture = await nativeProcessFixture(pid, "remote", "failed");
const sessionId = randomUUID();
const [lease] = await db.insert(environmentLeases).values({ companyId: fixture.run.companyId,
heartbeatRunId: fixture.run.id, issueId: fixture.run.issueId, status: "active", provider: "daytona",
providerLeaseId: randomUUID(), metadata: target === "plugin_task" ? { runnerTaskLease: true } : {},
}).returning();
await db.update(agents).set({ adapterType: "paperclip_runner" }).where(eq(agents.id, fixture.run.agentId));
await db.update(issues).set({ executionRunId: fixture.run.id }).where(eq(issues.id, fixture.run.issueId!));
await db.update(heartbeatRuns).set({ nativeSessionId: sessionId, runnerProfileJson: {
sessionCheckpoint: { identity: { runId: fixture.run.id, companyId: fixture.run.companyId,
issueId: fixture.run.issueId, agentId: fixture.run.agentId, sessionId }, providerSessionId: "existing-provider" },
...(target === "workspace" ? { nativeWorkspaceSync: { schema: "paperclip.native-workspace-sync/v3", state: "prepared",
workspaceId: randomUUID(), leaseId: lease.id, providerLeaseId: lease.providerLeaseId, remoteCwd: "/work",
descriptorSha256: "a".repeat(64), baselineSha256: "b".repeat(64), finalHostSha256: null,
resourceDisposition: "keep_running" } } : {}),
} }).where(eq(heartbeatRuns.id, fixture.run.id));
await db.insert(nativeRunnerCheckpoints).values({ runId: fixture.run.id, companyId: fixture.run.companyId,
issueId: fixture.run.issueId!, part: "opaque-checkpoint-record", material: { fixture: true } });
fixture.deps.getRun.mockImplementation(async id => (await db.select().from(heartbeatRuns)
.where(eq(heartbeatRuns.id, id)))[0] ?? null);
native.claims.mockImplementation(claimNativeRestartRecoveries);
return { ...fixture, lease };
}
it.each(["failed", "running"] as const)("routes a due remote %s retry through verified recovery without local PID inspection", async status => {
const child = await ownedProcess();
const f = await remoteRetryFixture(child.pid);
const originalKill = process.kill.bind(process);
const kill = vi.spyOn(process, "kill").mockImplementation(originalKill);
try {
await db.update(heartbeatRuns).set({ status }).where(eq(heartbeatRuns.id, f.run.id));
await db.update(nativeRunFinalizations).set({ nextAttemptAt: null }).where(eq(nativeRunFinalizations.runId, f.run.id));
const recovery = createHeartbeatRecovery(db, f.deps);
await recovery.reapOrphanedRuns();
await Promise.all([...f.deps.activeRunExecutionPromises]);
expect(f.deps.executeRun).toHaveBeenCalledExactlyOnceWith(f.run.id, expect.objectContaining({
nativeRestartRecovery: expect.objectContaining({ kind: "reattach_remote_runner", runId: f.run.id,
remote: { providerLeaseId: f.lease.providerLeaseId, remoteCwd: "/work" } }),
}));
expect(native.claims).toHaveBeenCalledWith(expect.objectContaining({ runIds: [f.run.id] }));
expect(await f.current()).toMatchObject({ status: "running", processPid: child.pid, processGroupId: child.pid });
expect(kill.mock.calls.filter(([pid]) => Math.abs(pid as number) === child.pid)).toEqual([]);
expect(await hasNativeLocalProcessStop(db, f.run.companyId, f.run.id)).toBe(false);
await recovery.reapOrphanedRuns();
expect(f.deps.executeRun).toHaveBeenCalledOnce();
expect(f.deps.terminateHeartbeatRunProcess).not.toHaveBeenCalled();
kill.mockRestore();
expect(child.alive()).toBe(true);
} finally { kill.mockRestore(); try { await f.cleanup(); } finally { await child.stop(); } }
});
it("routes a due plugin-task retry through its existing remote lease", async () => {
const child = await ownedProcess();
const f = await remoteRetryFixture(child.pid, "plugin_task");
try {
await createHeartbeatRecovery(db, f.deps).reapOrphanedRuns();
await Promise.all([...f.deps.activeRunExecutionPromises]);
expect(f.deps.executeRun).toHaveBeenCalledExactlyOnceWith(f.run.id, expect.objectContaining({
nativeRestartRecovery: expect.objectContaining({ kind: "reattach_remote_runner", runId: f.run.id,
remote: { kind: "plugin_task", leaseId: f.lease.id } }),
}));
expect(await f.current()).toMatchObject({ processPid: child.pid, processGroupId: child.pid });
expect(await hasNativeLocalProcessStop(db, f.run.companyId, f.run.id)).toBe(false);
expect(child.alive()).toBe(true);
} finally { try { await f.cleanup(); } finally { await child.stop(); } }
});
it.each(["future_retry", "live_owner", "missing_checkpoint", "active_execution"] as const)(
"does not execute a remote retry without current authority: %s", async condition => {
const child = await ownedProcess();
const f = await remoteRetryFixture(child.pid);
try {
if (condition === "future_retry") await db.update(nativeRunFinalizations)
.set({ nextAttemptAt: new Date(Date.now() + 60_000) }).where(eq(nativeRunFinalizations.runId, f.run.id));
if (condition === "live_owner") await db.update(nativeRunFinalizations)
.set({ leaseOwner: "other-controller", leaseExpiresAt: new Date(Date.now() + 60_000) })
.where(eq(nativeRunFinalizations.runId, f.run.id));
if (condition === "missing_checkpoint") {
const run = await f.current();
await db.update(heartbeatRuns).set({ runnerProfileJson: { ...run.runnerProfileJson, sessionCheckpoint: null } })
.where(eq(heartbeatRuns.id, f.run.id));
}
if (condition === "active_execution") f.deps.activeRunExecutions.add(f.run.id);
await createHeartbeatRecovery(db, f.deps).reapOrphanedRuns();
expect(f.deps.executeRun).not.toHaveBeenCalled();
expect(f.deps.terminateHeartbeatRunProcess).not.toHaveBeenCalled();
expect(await f.current()).toMatchObject({ processPid: child.pid, processGroupId: child.pid });
if (condition === "future_retry") expect(native.claims).not.toHaveBeenCalled();
if (condition === "missing_checkpoint") {
const [coordinator] = await db.select().from(nativeRunFinalizations).where(eq(nativeRunFinalizations.runId, f.run.id));
expect(coordinator).toMatchObject({ phase: "terminal_failure", recoveryState: "blocked",
failureCode: "native_restart_recovery_blocked" });
}
expect(child.alive()).toBe(true);
} finally { f.deps.activeRunExecutions.delete(f.run.id); try { await f.cleanup(); } finally { await child.stop(); } }
},
);
it.each(["unknown", "remote"] as const)("exposes expired observed %s ownership without attributing host liveness", async target => {
const child = await ownedProcess();
const f = await nativeProcessFixture(child.pid, "remote", "running");
const originalKill = process.kill.bind(process);
let restoreKill: (() => void) | undefined;
try {
if (target === "unknown") await db.delete(heartbeatRunEvents).where(eq(heartbeatRunEvents.runId, f.run.id));
await db.update(heartbeatRuns).set({ nativePhase: null })
.where(eq(heartbeatRuns.id, f.run.id));
await db.update(nativeRunFinalizations).set({ phase: "observed", attempt: 2,
leaseOwner: "prior-native-owner", leaseExpiresAt: new Date(0), nextAttemptAt: null })
.where(eq(nativeRunFinalizations.runId, f.run.id));
const before = await f.current();
const coordinatorBefore = await db.select().from(nativeRunFinalizations)
.where(eq(nativeRunFinalizations.runId, f.run.id));
const kill = vi.spyOn(process, "kill").mockImplementation(originalKill);
restoreKill = () => kill.mockRestore();
expect(await createHeartbeatRecovery(db, f.deps).reapOrphanedRuns()).toEqual({ reaped: 0, runIds: [] });
expect(kill.mock.calls.filter(([pid]) => Math.abs(pid as number) === child.pid)).toEqual([]);
expect(await f.current()).toMatchObject({ status: "running", nativePhase: null,
processPid: child.pid, processGroupId: child.pid, processStartedAt: before.processStartedAt,
errorCode: NATIVE_OWNERSHIP_UNVERIFIED_ERROR_CODE });
expect(f.deps.appendRunEvent).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({
payload: { reason: "observed_owner_unverified" },
}));
expect(await db.select().from(nativeRunFinalizations)
.where(eq(nativeRunFinalizations.runId, f.run.id))).toEqual(coordinatorBefore);
expect(f.deps.executeRun).not.toHaveBeenCalled();
expect(f.deps.terminateHeartbeatRunProcess).not.toHaveBeenCalled();
kill.mockRestore(); restoreKill = undefined;
expect(child.alive()).toBe(true);
} finally { restoreKill?.(); try { await f.cleanup(); } finally { await child.stop(); } }
});
it.each(["failed", "running"] as const)("protects a verified local %s run's live process", async status => {
const child = await ownedProcess();
const f = await nativeProcessFixture(child.pid, "local", status);
try {
await db.update(agents).set({ adapterType: "paperclip_runner", adapterConfig: { driver: "daytona" } })
.where(eq(agents.id, f.run.agentId));
await createHeartbeatRecovery(db, f.deps).reapOrphanedRuns();
expect(await f.current()).toMatchObject({ status, processPid: child.pid, processGroupId: child.pid,
errorCode: NATIVE_OWNERSHIP_UNVERIFIED_ERROR_CODE });
expect(f.deps.appendRunEvent).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({
payload: { reason: "live_process_identifier", processPidAlive: true, processGroupAlive: true },
}));
expect(f.deps.executeRun).not.toHaveBeenCalled();
expect(f.deps.terminateHeartbeatRunProcess).not.toHaveBeenCalled();
expect(child.alive()).toBe(true);
} finally { try { await f.cleanup(); } finally { await child.stop(); } }
});
it.each(["missing", "new_launch", "wrong_birth", "foreign_host", "wrong_agent", "wrong_pid", "wrong_group", "native_source", "remote_workspace", "remote_lease"] as const)(
"holds a native PID without local authority: %s", async evidence => {
const child = await ownedProcess();
await child.stop();
const f = await nativeProcessFixture(child.pid, "local", "failed");
try {
if (evidence === "missing") await db.delete(heartbeatRunEvents).where(eq(heartbeatRunEvents.runId, f.run.id));
if (evidence === "new_launch") await appendHeartbeatRunEvent(db, { companyId: f.run.companyId,
runId: f.run.id, agentId: f.run.agentId, eventType: PROCESS_START_REQUESTED });
if (["wrong_birth", "foreign_host", "wrong_agent", "wrong_pid", "wrong_group", "native_source"].includes(evidence)) {
const [event] = await db.select().from(heartbeatRunEvents).where(eq(heartbeatRunEvents.runId, f.run.id));
const foreignAgentId = randomUUID();
if (evidence === "wrong_agent") await db.insert(agents).values({ id: foreignAgentId,
companyId: f.run.companyId, name: "Other process owner", role: "engineer", adapterType: "process" });
await db.update(heartbeatRunEvents).set({
...(evidence === "wrong_agent" ? { agentId: foreignAgentId } : {}),
...(evidence === "native_source" ? { sourceEventId: "provider-event" } : {}),
payload: { ...event.payload, ...(evidence === "wrong_birth"
? { processStartedAt: new Date(0).toISOString() } : evidence === "foreign_host" ? { hostname: "foreign-host" }
: evidence === "wrong_pid" ? { processPid: child.pid + 1 }
: evidence === "wrong_group" ? { processGroupId: child.pid + 1 } : {}) },
}).where(eq(heartbeatRunEvents.id, event.id));
}
if (evidence === "remote_workspace") await db.update(heartbeatRuns).set({
runnerProfileJson: { nativeWorkspaceSync: { schema: "paperclip.native-workspace-sync/v3", state: "prepared",
workspaceId: randomUUID(), leaseId: randomUUID(), providerLeaseId: randomUUID(), remoteCwd: "/work",
descriptorSha256: "a".repeat(64), baselineSha256: "b".repeat(64), finalHostSha256: null, resourceDisposition: "destroy" } },
}).where(eq(heartbeatRuns.id, f.run.id));
if (evidence === "remote_lease") await db.insert(environmentLeases).values({ companyId: f.run.companyId,
heartbeatRunId: f.run.id, issueId: f.run.issueId, status: "active", provider: "daytona", providerLeaseId: randomUUID() });
const before = await f.current();
await createHeartbeatRecovery(db, f.deps).reapOrphanedRuns();
expect(await f.current()).toMatchObject({ processPid: child.pid, processGroupId: child.pid,
processStartedAt: before.processStartedAt, errorCode: "native_session_interrupted" });
expect(f.deps.executeRun).not.toHaveBeenCalled();
expect(f.deps.terminateHeartbeatRunProcess).not.toHaveBeenCalled();
expect(await hasNativeLocalProcessStop(db, f.run.companyId, f.run.id)).toBe(false);
} finally { await f.cleanup(); }
},
);
it("clears a verified stopped local retry identity and records its stop", async () => {
const child = await ownedProcess();
await child.stop();
const f = await nativeProcessFixture(child.pid, "local", "failed");
try {
await createHeartbeatRecovery(db, f.deps).reapOrphanedRuns();
expect(await f.current()).toMatchObject({ processPid: null, processGroupId: null, processStartedAt: null });
expect(await hasNativeLocalProcessStop(db, f.run.companyId, f.run.id)).toBe(true);
expect(f.deps.terminateHeartbeatRunProcess).not.toHaveBeenCalled();
} finally { await f.cleanup(); }
});
it("retires a remote identity only with an exact authenticated termination receipt", async () => {
const child = await ownedProcess();
const f = await nativeProcessFixture(child.pid, "remote", "failed");
try {
await db.update(heartbeatRuns).set({ runnerProfileJson: { sessionCheckpoint: { fixture: true } } })
.where(eq(heartbeatRuns.id, f.run.id));
await db.update(nativeRunFinalizations).set({ phase: "terminal_failure" })
.where(eq(nativeRunFinalizations.runId, f.run.id));
const [lease] = await db.insert(environmentLeases).values({ companyId: f.run.companyId,
heartbeatRunId: f.run.id, issueId: f.run.issueId, status: "active", provider: "daytona", providerLeaseId: randomUUID() }).returning();
expect(await releaseSettledNativeRunProcess(db, f.run.id)).toBe("pending");
const receipt = remoteTerminationReceipt(lease, { state: "destroyed", providerLeaseId: lease.providerLeaseId })!;
await db.update(environmentLeases).set({ status: "released", cleanupStatus: "success", releasedAt: new Date(),
metadata: { remoteExecutionTermination: { ...receipt, runId: randomUUID() } } }).where(eq(environmentLeases.id, lease.id));
expect(await releaseSettledNativeRunProcess(db, f.run.id)).toBe("pending");
expect(await f.current()).toMatchObject({ processPid: child.pid, processGroupId: child.pid });
await db.update(environmentLeases).set({ metadata: { remoteExecutionTermination: receipt } })
.where(eq(environmentLeases.id, lease.id));
expect(await releaseSettledNativeRunProcess(db, f.run.id)).toBe("released");
expect(await f.current()).toMatchObject({ processPid: null, processGroupId: null, processStartedAt: null });
expect(await hasNativeLocalProcessStop(db, f.run.companyId, f.run.id)).toBe(false);
expect(child.alive()).toBe(true);
} finally { try { await f.cleanup(); } finally { await child.stop(); } }
});
it.each(["ephemeral", "reuse_by_environment", "live_owner", "live_executor", "mismatched_source", "finalizing"] as const)(
"reconstructs missing native preservation during orphan cleanup without replay: %s", async state => {
const run = await seedRun();
+60 -12
View File
@@ -18,7 +18,7 @@ import {
hasStopOnlyCleanup,
settleStopOnlyCleanup,
} from "../sandbox-stop-and-retain.js";
import { discoverSettledNativeRunProcesses, recordNativeLocalProcessStop, retryPendingSettledNativeRunProcesses } from "../native-local-process-stop.js";
import { discoverSettledNativeRunProcesses, PROCESS_IDENTITY_RECORDED, PROCESS_START_REQUESTED, recordNativeLocalProcessStop, retryPendingSettledNativeRunProcesses } from "../native-local-process-stop.js";
import { pendingCaseHandoffForRun } from "./case-handoff-lifecycle.js";
import {
legacyControllerBootId,
@@ -30,9 +30,11 @@ import { runUsedConversationAdapter } from "../conversation-continuation.js";
import { legacyExecutionNeedsReconciliationWithEvidence, terminalizeLegacyExecution } from "../legacy-execution-recovery.js";
import { reconcileRetainedWorkspaces } from "../workspace-recovery.js";
import { randomUUID } from "node:crypto";
import os from "node:os";
import {
and,
asc,
desc,
eq,
gt,
inArray,
@@ -46,6 +48,7 @@ import type { Db } from "@paperclipai/db";
import {
agents,
environmentLeases,
heartbeatRunEvents,
heartbeatRuns,
issues,
nativeRunFinalizations,
@@ -1836,6 +1839,29 @@ export function createHeartbeatRecovery(db: Db, dependencies: HeartbeatRecoveryD
return { swept: rows.length, destroyed, capped };
}
async function nativeProcessIsLocal(run: HeartbeatRun) {
if (parseObject(run.runnerProfileJson).nativeWorkspaceSync != null) return false;
const [identity] = await db.select({
eventType: heartbeatRunEvents.eventType,
agentId: heartbeatRunEvents.agentId,
payload: heartbeatRunEvents.payload,
}).from(heartbeatRunEvents).where(and(
eq(heartbeatRunEvents.companyId, run.companyId),
eq(heartbeatRunEvents.runId, run.id),
isNull(heartbeatRunEvents.sourceEventId),
inArray(heartbeatRunEvents.eventType, [PROCESS_START_REQUESTED, PROCESS_IDENTITY_RECORDED]),
)).orderBy(desc(heartbeatRunEvents.seq)).limit(1);
const recorded = identity?.payload;
if (identity?.eventType !== PROCESS_IDENTITY_RECORDED || identity.agentId !== run.agentId ||
recorded?.targetKind !== "local" || recorded.hostname !== os.hostname() ||
recorded.processPid !== run.processPid || recorded.processGroupId !== run.processGroupId ||
!run.processStartedAt || recorded.processStartedAt !== run.processStartedAt.toISOString()) return false;
const leases = await db.select({ provider: environmentLeases.provider }).from(environmentLeases).where(and(
eq(environmentLeases.companyId, run.companyId), eq(environmentLeases.heartbeatRunId, run.id),
));
return leases.every(lease => lease.provider === "local");
}
async function markNativeOwnershipUnverified(
run: typeof heartbeatRuns.$inferSelect,
evidence: {
@@ -2030,13 +2056,12 @@ export function createHeartbeatRecovery(db: Db, dependencies: HeartbeatRecoveryD
);
});
// A retryable native run can retain process identifiers from the failed
// attempt. Inspect them before the recovery claim: a live identifier is
// unowned and blocks recovery, while identifiers that are all dead can be
// cleared with a compare-and-set so the explicit retryable failure becomes
// claimable in this same sweep.
// Retryable runs can retain process identifiers from the failed attempt.
// Only verified local identities may be checked and cleared on this host.
// Due remote retries instead re-enter the existing restart-recovery owner,
// which verifies the saved checkpoint, controller lease, and remote target.
const retryableNativeProcesses = await db
.select({ run: heartbeatRuns })
.select({ run: heartbeatRuns, nextAttemptAt: nativeRunFinalizations.nextAttemptAt })
.from(heartbeatRuns)
.innerJoin(
nativeRunFinalizations,
@@ -2051,12 +2076,27 @@ export function createHeartbeatRecovery(db: Db, dependencies: HeartbeatRecoveryD
),
);
const claimableNativeRunIds = new Set<string>();
for (const { run } of retryableNativeProcesses) {
const remoteRetryRunIds: string[] = [];
for (const { run, nextAttemptAt } of retryableNativeProcesses) {
if (isNativeRunnerOwnershipHeld(run)) continue;
if (!run.processPid && !run.processGroupId) {
claimableNativeRunIds.add(run.id);
continue;
}
if (!await nativeProcessIsLocal(run)) {
if (nextAttemptAt && nextAttemptAt > now) continue;
if (activeRunExecutions.has(run.id) || runningProcesses.has(run.id)) continue;
if (parseObject(run.runnerProfileJson).nativeWorkspaceSync != null) {
remoteRetryRunIds.push(run.id);
} else {
const [taskLease] = await db.select({ id: environmentLeases.id }).from(environmentLeases).where(and(
eq(environmentLeases.companyId, run.companyId), eq(environmentLeases.heartbeatRunId, run.id),
sql`${environmentLeases.metadata}->>'runnerTaskLease' = 'true'`,
)).limit(1);
if (taskLease) remoteRetryRunIds.push(run.id);
}
continue;
}
const processPidAlive =
!!run.processPid && isProcessAlive(run.processPid);
const processGroupAlive =
@@ -2082,6 +2122,7 @@ export function createHeartbeatRecovery(db: Db, dependencies: HeartbeatRecoveryD
and(
eq(heartbeatRuns.id, run.id),
eq(heartbeatRuns.runtimeMode, "native"),
eq(heartbeatRuns.nextEventSeq, run.nextEventSeq),
run.processPid === null
? isNull(heartbeatRuns.processPid)
: eq(heartbeatRuns.processPid, run.processPid),
@@ -2101,6 +2142,10 @@ export function createHeartbeatRecovery(db: Db, dependencies: HeartbeatRecoveryD
if (cleared) claimableNativeRunIds.add(cleared.id);
}
const remoteRetryClaims = remoteRetryRunIds.length
? (await recoverNativeRunsAfterRestart(now, remoteRetryRunIds)).claims
: [];
const executingRun = (runId: string) => activeRunExecutions.has(runId) || runningProcesses.has(runId);
if (!settledProcessesDiscovered) {
settledProcessesDiscovered = await discoverSettledNativeRunProcesses(db, { skip: executingRun }).then(() => true, (error) => {
@@ -2148,7 +2193,7 @@ export function createHeartbeatRecovery(db: Db, dependencies: HeartbeatRecoveryD
return [];
});
const resumedRunIds = new Set(
nativeResumeClaims.map((claim) => claim.runId),
[...nativeResumeClaims, ...remoteRetryClaims].map((claim) => claim.runId),
);
// A terminal issue transition writes this intent in the same transaction
@@ -2300,10 +2345,12 @@ export function createHeartbeatRecovery(db: Db, dependencies: HeartbeatRecoveryD
// preserves its explicit physical-copyback hold. Keep its source lease.
if (nativeWorkspaceFinalizationPending) continue;
const nativeRun = run.runtimeMode === "native";
const nativeLocalProcess = nativeRun && !!(run.processPid || run.processGroupId) &&
await nativeProcessIsLocal(run);
const nativeProcessPidAlive =
nativeRun && !!run.processPid && isProcessAlive(run.processPid);
nativeLocalProcess && !!run.processPid && isProcessAlive(run.processPid);
const nativeProcessGroupAlive =
nativeRun &&
nativeLocalProcess &&
!!run.processGroupId &&
isProcessGroupAlive(run.processGroupId);
const coordinatorOwnedByCurrentController =
@@ -2360,6 +2407,7 @@ export function createHeartbeatRecovery(db: Db, dependencies: HeartbeatRecoveryD
});
continue;
}
if (nativeRun && !locallyTracked && (run.processPid || run.processGroupId) && !nativeLocalProcess) continue;
if (resumedRunIds.has(run.id)) continue;
if (locallyTracked) continue;
if (await hasLiveLegacyController(db, run)) continue;
@@ -2379,7 +2427,7 @@ export function createHeartbeatRecovery(db: Db, dependencies: HeartbeatRecoveryD
// for a read-only liveness check so a lost in-memory handle cannot cause
// overlapping provider/tool execution while that child is still alive.
const checksPersistedChildLiveness =
currentAdapterTracksLocalChild || run.runtimeMode === "native";
nativeRun ? nativeLocalProcess : currentAdapterTracksLocalChild;
const processPidAlive =
checksPersistedChildLiveness &&
run.processPid &&
@@ -0,0 +1,120 @@
import { randomUUID } from "node:crypto";
import { runAdapterExecutionTargetShellCommand, type AdapterExecutionTarget } from "@paperclipai/adapter-utils/execution-target";
import { kimiMetadataProbeDigest, kimiMetadataProbeNodeDigest, probeKimiMetadata, validateKimiMetadata } from "../vendor/paperclip-runner/live/index.js";
import { unprocessable } from "../errors.js";
import { createSshCommandManagedRuntimeRunner, runSshCommand } from "@paperclipai/adapter-utils/ssh";
import { QUALIFIED_ACPX_PROFILES } from "../vendor/paperclip-runner/index.js";
import { resolveKimiCandidateAdmission } from "./native-runtime/acpx-qualification.js";
const REQUEST_FAILED = () => unprocessable("Kimi Code metadata verification failed.", { code: "KIMI_REQUEST_FAILED" });
const INSTALLATION_INVALID = () => unprocessable("Install the current verified Kimi Code runtime in this execution environment.", { code: "KIMI_INSTALLATION_INVALID" });
const SSH_INPUT_PROBE = 'try { const input = JSON.parse(require("node:fs").readFileSync(0, "utf8")); if (typeof input.apiKey !== "string" || typeof input.model !== "string") process.exit(1); process.env.KIMI_API_KEY = input.apiKey; process.env.PAPERCLIP_KIMI_PROBE_MODEL = input.model; if (input.candidateAdmission !== undefined) process.env.PAPERCLIP_ACPX_CANDIDATE_ADMISSION = JSON.stringify(input.candidateAdmission); require(process.argv[1]); } catch { process.exit(1); }';
export async function probeKimiExecutionTarget(apiKey: string, target: AdapterExecutionTarget | null | undefined, model: string, context: { companyId?: string } = {}) {
if (typeof apiKey !== "string" || !apiKey.trim() || /[\0\r\n]/.test(apiKey) || Buffer.byteLength(apiKey) > 16 * 1024) {
throw unprocessable("Bind a valid company secret to KIMI_API_KEY.", { code: "KIMI_AUTH_REQUIRED" });
}
if (typeof model !== "string" || !model.trim() || model.includes("\0") || Buffer.byteLength(model) > 512 || Buffer.from(model).toString("utf8") !== model) {
throw unprocessable("Select the exact Kimi Code model.", { code: "KIMI_MODEL_UNAVAILABLE" });
}
let candidateAdmission: Parameters<typeof probeKimiMetadata>[2];
if (QUALIFIED_ACPX_PROFILES.kimi.qualificationStatus === "pending") {
try {
const admission = resolveKimiCandidateAdmission({ companyId: context.companyId, model,
target: target?.kind === "remote" ? { platform: "linux", architecture: "x64" } : { platform: process.platform, architecture: process.arch } });
if (!admission) throw new Error("Missing candidate admission");
const probeId = `kimi-metadata-${randomUUID()}`;
candidateAdmission = { ...admission, runId: probeId, normalizedSessionId: `metadata-${randomUUID()}` };
} catch {
throw unprocessable("Kimi Code qualification requires current trusted operator admission for this company and execution target.", { code: "KIMI_QUALIFICATION_REQUIRED" });
}
}
if (!target || target.kind === "local") {
const result = validateKimiMetadata(await probeKimiMetadata(model, apiKey, candidateAdmission), model);
if (result.status !== "verified") throw unprocessable(result.message, { code: result.code });
return result;
}
if (target.transport === "sandbox" && target.runnerTask) {
throw unprocessable("Kimi Code metadata verification is unavailable in this hosting environment.", { code: "KIMI_INSTALLATION_INVALID" });
}
let locations = ["/opt/paperclip-runner/provider-pack", '"$HOME/.local/share/paperclip-runner/provider-pack"'];
const selectedPackRoot = process.env.PAPERCLIP_RUNNER_REMOTE_PROVIDER_PACK_PATH?.trim();
if (selectedPackRoot) {
try {
// Test uses the same complete-pack installer as native execution. It
// needs no native run/session or DB context and opens no model session.
const { prepareRemoteProviderPackOnTarget, resolveRemoteProviderPackSource } = await import("./native-runtime/native-session-executor.js");
const selected = resolveRemoteProviderPackSource(selectedPackRoot);
if (selected.manifest.payload.target.platform !== "linux" || selected.manifest.payload.target.architecture !== "x64") throw INSTALLATION_INVALID();
const runner = target.transport === "ssh" ? createSshCommandManagedRuntimeRunner({ spec: target.spec, defaultCwd: target.remoteCwd }) : target.runner;
if (!runner) throw INSTALLATION_INVALID();
const prepared = await prepareRemoteProviderPackOnTarget({ target, runner,
configuredProviderPackRoot: selected.configuredRoot, manifest: selected.manifest });
locations = [`'${prepared.root.replaceAll("'", "'\\''")}'`];
} catch { throw INSTALLATION_INVALID(); }
}
// Preparation retains the normal installer's bounded stages. The existing
// metadata/HTTP budget begins only after that preparation has completed.
const deadlineAt = Date.now() + 35_000;
let digest: string;
try { digest = await kimiMetadataProbeDigest(); }
catch { throw INSTALLATION_INVALID(); }
const nodeDigest = kimiMetadataProbeNodeDigest();
if (!/^sha256:[a-f0-9]{64}$/.test(digest) || !/^sha256:[a-f0-9]{64}$/.test(nodeDigest)) throw INSTALLATION_INVALID();
for (const location of locations) {
const remainingMs = deadlineAt - Date.now();
if (remainingMs <= 3_000) throw REQUEST_FAILED();
const command = [
`pack=${location}`,
'entry="$pack/dist/cli/kimi-metadata-probe.cjs"; node="$pack/provider-assets/kimi/linux-x64/runtime/node/bin/node"',
'[ -f "$pack/provider-pack.json" ] && [ -f "$entry" ] && [ ! -L "$entry" ] && [ -x "$node" ] && [ ! -L "$node" ] || exit 127',
'unset NODE_OPTIONS NODE_PATH NODE_EXTRA_CA_CERTS NODE_USE_ENV_PROXY NODE_TLS_REJECT_UNAUTHORIZED OPENSSL_CONF SSL_CERT_FILE SSL_CERT_DIR HTTP_PROXY HTTPS_PROXY ALL_PROXY NO_PROXY http_proxy https_proxy all_proxy no_proxy LD_PRELOAD LD_LIBRARY_PATH LD_AUDIT GLIBC_TUNABLES DYLD_INSERT_LIBRARIES DYLD_LIBRARY_PATH DYLD_FRAMEWORK_PATH',
'PATH=/usr/bin:/bin; export PATH',
'actual=$(/usr/bin/env -i /usr/bin/sha256sum "$entry" 2>/dev/null) || exit 126',
`actual=\${actual%% *}; [ "$actual" = "${digest.slice(7)}" ] || exit 126`,
'actual=$(/usr/bin/env -i /usr/bin/sha256sum "$node" 2>/dev/null) || exit 126',
`actual=\${actual%% *}; [ "$actual" = "${nodeDigest.slice(7)}" ] || exit 126`,
target.transport === "ssh"
? `exec /usr/bin/env -i PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT="$pack" PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST="$pack/package.json" "$node" -e '${SSH_INPUT_PROBE}' "$entry"`
: ['exec /usr/bin/env -i KIMI_API_KEY="$KIMI_API_KEY" PAPERCLIP_KIMI_PROBE_MODEL="$PAPERCLIP_KIMI_PROBE_MODEL"',
...(candidateAdmission ? ['PAPERCLIP_ACPX_CANDIDATE_ADMISSION="$PAPERCLIP_ACPX_CANDIDATE_ADMISSION"'] : []),
'PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT="$pack" PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST="$pack/package.json" "$node" "$entry"'].join(" "),
].join("; ");
let response: Pick<Awaited<ReturnType<typeof runAdapterExecutionTargetShellCommand>>, "stdout" | "exitCode" | "signal" | "timedOut">;
try {
if (target.transport === "ssh") {
try {
const result = await runSshCommand(target.spec, command, {
stdin: JSON.stringify({ apiKey, model, ...(candidateAdmission ? { candidateAdmission } : {}) }), timeoutMs: Math.max(1, Math.floor((remainingMs - 3_000) / 1000)) * 1000,
});
response = { stdout: result.stdout, exitCode: 0, signal: null, timedOut: false };
} catch (error) {
const failure = error as { code?: unknown; stdout?: unknown; signal?: string | null };
if (typeof failure.code !== "number") throw error;
response = { stdout: typeof failure.stdout === "string" ? failure.stdout : "", exitCode: failure.code,
signal: failure.signal ?? null, timedOut: false };
}
} else {
response = await runAdapterExecutionTargetShellCommand(`kimi-metadata-${randomUUID()}`, target, command, {
cwd: target.remoteCwd, env: { KIMI_API_KEY: apiKey, PAPERCLIP_KIMI_PROBE_MODEL: model,
...(candidateAdmission ? { PAPERCLIP_ACPX_CANDIDATE_ADMISSION: JSON.stringify(candidateAdmission) } : {}) },
timeoutSec: Math.max(1, Math.floor((remainingMs - 3_000) / 1000)), graceSec: 3,
});
}
} catch { throw REQUEST_FAILED(); }
if (Date.now() > deadlineAt || response.timedOut || response.signal || Buffer.byteLength(response.stdout) > 64 * 1024) throw REQUEST_FAILED();
if ([126, 127].includes(response.exitCode ?? -1)) continue;
let payload: unknown;
try { payload = JSON.parse(response.stdout.trim()); } catch { throw REQUEST_FAILED(); }
const result = validateKimiMetadata(payload, model);
if (result.status !== "verified") {
if (result.code === "KIMI_INSTALLATION_INVALID") continue;
throw unprocessable(result.message, { code: result.code });
}
if (response.exitCode !== 0) throw REQUEST_FAILED();
if (result.target !== "linux-x64") throw INSTALLATION_INVALID();
return result;
}
throw INSTALLATION_INVALID();
}
@@ -1,6 +1,7 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { readFileSync } from "node:fs";
import { ACPX_QUALIFICATION_ENV, resolveAcpxQualification } from "./acpx-qualification.js";
import { ACPX_QUALIFICATION_ENV, ANTIGRAVITY_CANDIDATE_ADMISSION_ENV, resolveAcpxQualification, resolveKimiCandidateAdmission } from "./acpx-qualification.js";
import { QUALIFIED_ACPX_PROFILES, KIMI_DISTRIBUTION_CLOSURE_SHA256 } from "../../vendor/paperclip-runner/index.js";
import type { NativeExecutionInput } from "../../vendor/paperclip-runner/index.js";
import { resolvePaperclipRunnerProviderProfile, resolvePaperclipRunnerNativeProviderInput } from "./provider-profile.js";
@@ -9,6 +10,20 @@ const authorize = (value: unknown) => ({ [ACPX_QUALIFICATION_ENV]: JSON.stringif
describe("host ACPX qualification admission", () => {
afterEach(() => vi.unstubAllEnvs());
it.each([undefined, "malformed", JSON.stringify(kimiAdmission("linux", "x64"))])("admits released Kimi without candidate authority (%s)", authority => {
vi.stubEnv(ANTIGRAVITY_CANDIDATE_ADMISSION_ENV, authority);
vi.stubEnv(ACPX_QUALIFICATION_ENV, "malformed-unrelated-authority");
const config = { provider: "acpx", acpxAgent: "kimi", model: "kimi-for-coding" };
expect(QUALIFIED_ACPX_PROFILES.kimi.agentProfileVersion).toBe(17);
expect(QUALIFIED_ACPX_PROFILES.kimi.qualificationStatus).not.toBe("pending");
expect(resolveKimiCandidateAdmission({ companyId: "candidate-company", model: config.model })).toBeUndefined();
expect(resolvePaperclipRunnerProviderProfile(config, { companyId: "candidate-company" })).toMatchObject(config);
expect(resolvePaperclipRunnerNativeProviderInput({ companyId: "candidate-company", backend: "acpx_runtime", adapterConfig: config }))
.toMatchObject({ ...config, acpxPermissionMode: "approve-all" });
expect(() => resolvePaperclipRunnerProviderProfile({ ...config, model: "" }))
.toThrow(expect.objectContaining({ code: "paperclip_runner_acpx_model_required" }));
});
it.each([undefined, "malformed", JSON.stringify([{ agent: "antigravity", model: "exact-model" }])])(
"keeps pending Antigravity closed despite host authorization (%s)", host => {
vi.stubEnv(ACPX_QUALIFICATION_ENV, host);
@@ -97,3 +112,9 @@ it.each([undefined, "off", "low", "high", "max"] as const)("validates saved Pi t
expect(resolvePaperclipRunnerNativeProviderInput({ backend: "acpx_runtime", adapterConfig })).toMatchObject({ piThinkingLevel: piThinkingLevel ?? "low" });
for (const alias of ["medium", "minimal", "xhigh"]) expect(() => resolvePaperclipRunnerProviderProfile({ ...adapterConfig, piThinkingLevel: alias })).toThrow(expect.objectContaining({ code: "paperclip_runner_pi_thinking_invalid" }));
});
function kimiAdmission(platform: "darwin" | "linux", architecture: "arm64" | "x64") {
return { schema: "paperclip.acpx-candidate-admission.v1", companyId: "candidate-company", agent: "kimi", model: "kimi-for-coding",
platform, architecture, expiresAt: Date.now() + 60_000, profileDigest: QUALIFIED_ACPX_PROFILES.kimi.commandDigest,
nativeClosureDigest: `sha256:${KIMI_DISTRIBUTION_CLOSURE_SHA256[`${platform}-${architecture}` as "darwin-arm64" | "linux-x64"]}` };
}
@@ -1,5 +1,5 @@
import { PAPERCLIP_RUNNER_ACPX_PROFILES } from "@paperclipai/adapter-utils";
import { parseAcpxCandidateAdmission, type AcpxCandidateAdmission, type NativeExecutionInput } from "../../vendor/paperclip-runner/index.js";
import { parseAcpxCandidateAdmission, QUALIFIED_ACPX_PROFILES, type AcpxCandidateAdmission, type NativeExecutionInput } from "../../vendor/paperclip-runner/index.js";
/** Trusted operator startup authority, separate from saved agent configuration. */
export const ANTIGRAVITY_CANDIDATE_ADMISSION_ENV = "PAPERCLIP_ACPX_CANDIDATE_ADMISSION";
@@ -23,9 +23,31 @@ export function resolveAntigravityCandidateAdmission(
});
}
export function resolveKimiCandidateAdmission(
input: { companyId?: string; model?: string; target?: { platform: string; architecture: string } },
hostEnvironment: NodeJS.ProcessEnv = process.env,
): AcpxCandidateAdmission | undefined {
if (QUALIFIED_ACPX_PROFILES.kimi.qualificationStatus !== "pending") return undefined;
const encoded = hostEnvironment[ANTIGRAVITY_CANDIDATE_ADMISSION_ENV];
if (!encoded || !input.companyId) return undefined;
let value: unknown;
try { value = JSON.parse(encoded); } catch { throw new Error("Invalid Kimi candidate admission"); }
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid Kimi candidate admission");
const record = value as Record<string, unknown>;
if (record.companyId !== input.companyId || record.agent !== "kimi") return undefined;
const admission = parseAcpxCandidateAdmission(value, {
companyId: input.companyId, agent: "kimi",
model: input.model ?? (typeof record.model === "string" ? record.model : ""),
});
if (input.target && (admission.platform !== input.target.platform || admission.architecture !== input.target.architecture)) {
throw new Error("Kimi candidate admission does not match the execution target");
}
return admission;
}
/** Operator-only admission for an exact candidate/model during qualification. */
export const ACPX_QUALIFICATION_ENV = "PAPERCLIP_RUNNER_ACPX_QUALIFICATION";
export type AcpxQualificationCandidate = "cursor" | "copilot" | "pi";
export type AcpxQualificationCandidate = "cursor" | "copilot" | "pi" | "kimi";
export function resolveAcpxQualification(
provider: NativeExecutionInput["provider"] | { kind: "acpx"; agent: string; model: string },
@@ -45,14 +67,14 @@ export function resolveAcpxQualification(
if (!encoded) return undefined;
let entries: unknown;
try { entries = JSON.parse(encoded); } catch { throw new Error("Invalid ACPX qualification authorization"); }
if (!Array.isArray(entries) || entries.length < 1 || entries.length > 3) {
if (!Array.isArray(entries) || entries.length < 1 || entries.length > 4) {
throw new Error("Invalid ACPX qualification authorization");
}
const agents = new Set<string>();
for (const entry of entries) {
if (!entry || typeof entry !== "object" || Array.isArray(entry)
|| Object.keys(entry).sort().join(",") !== "agent,model"
|| !["cursor", "copilot", "pi"].includes(entry.agent)
|| !["cursor", "copilot", "pi", "kimi"].includes(entry.agent)
|| typeof entry.model !== "string" || !entry.model.trim() || entry.model !== entry.model.trim()
|| agents.has(entry.agent)) throw new Error("Invalid ACPX qualification authorization");
agents.add(entry.agent);
@@ -47,6 +47,7 @@ import {
import {
canonicalNativeRuntimeContextDigest,
CURSOR_DISTRIBUTION_PINS,
KIMI_DISTRIBUTION_CLOSURE_SHA256,
QUALIFIED_ACPX_PROFILES,
acpxRuntimeSessionDirectoryName,
resolveAcpxRuntimeRoot,
@@ -391,6 +392,8 @@ import {
REMOTE_RUNNER_CHILD_LAUNCH_SCRIPT,
verifyRemoteRunnerReattachment,
readRemoteProviderPackManifest,
prepareRemoteProviderPackOnTarget,
resolveRemoteProviderPackSource,
readBundledRemoteProviderPackManifest,
providerSessionIdentityFromDurableProviderState,
durableProviderCheckpointFailureReason,
@@ -1946,6 +1949,40 @@ describe("remote provider pack manifest", () => {
expect(readRemoteProviderPackManifest(root).payload.pins.opencode).toBe(
"1.18.34",
);
// Both execution and setup Test consume this same normal installer. Reuse
// the complete controller fixture here instead of inventing a thin pack.
const selectedPack = resolveRemoteProviderPackSource(root);
for (const mode of ["staged", "preinstalled", "uploaded", "failed-final", "missing-transport"] as const) {
let transferred = false;
const syncIn = vi.fn(async (operations: { files: { sourcePath?: string; targetPath: string; kind: string }[] }[]) => {
expect(operations[0]?.files[0]).toMatchObject({ sourcePath: root,
targetPath: "/workspace/.paperclip-runtime/paperclip-runner/provider-pack", kind: "directory" });
transferred = true;
return { operations: [] };
});
const execute = vi.fn(async (command: { command: string; args?: string[]; timeoutMs?: number }) => {
expect(command.timeoutMs).toBeGreaterThan(0);
expect(command.timeoutMs).toBeLessThanOrEqual(30_000);
const verification = command.args?.[0] === "-e";
const packRoot = command.args?.[2];
const valid = mode === "staged" || mode === "preinstalled" && packRoot === "/opt/paperclip-runner/provider-pack"
|| transferred && mode !== "failed-final";
return { pid: null, startedAt: null, exitCode: verification && !valid ? 1 : 0, timedOut: false, signal: null,
stdout: command.args?.[0] === "--version" ? "1.18.34\n"
: command.args?.some(value => value.includes("for candidate in")) ? "/opt/paperclip-runner/provider-pack\n" : "",
stderr: verification && !valid ? "manifest mismatch" : "" };
});
const runner = { execute, ...(mode === "missing-transport" ? {} : { syncIn }) };
const preparation = prepareRemoteProviderPackOnTarget({
target: { kind: "remote", transport: "sandbox", remoteCwd: "/workspace", runner } as never,
runner, configuredProviderPackRoot: selectedPack.configuredRoot, manifest: selectedPack.manifest,
});
if (mode === "failed-final") await expect(preparation).rejects.toThrow("provider pack verification");
else if (mode === "missing-transport") await expect(preparation).rejects.toThrow("transport cannot stage a provider pack");
else await expect(preparation).resolves.toMatchObject({ source: mode, root: "/workspace/.paperclip-runtime/paperclip-runner/provider-pack",
manifest: selectedPack.manifest });
expect(syncIn).toHaveBeenCalledTimes(mode === "uploaded" || mode === "failed-final" ? 1 : 0);
}
const cursorPath = "provider-assets/cursor/linux-x64";
await mkdir(join(root, cursorPath), { recursive: true });
await writeFile(join(root, cursorPath, "runtime"), "pinned Cursor runtime");
@@ -1980,6 +2017,78 @@ describe("remote provider pack manifest", () => {
await writeFile(join(root, cursorPath, "runtime"), "substitute Cursor runtime");
expect(() => readRemoteProviderPackManifest(root)).toThrow("asset tree digest mismatch");
await writeFile(join(root, cursorPath, "runtime"), "pinned Cursor runtime");
const kimiPath = "provider-assets/kimi/linux-x64";
await mkdir(join(root, kimiPath), { recursive: true });
await writeFile(join(root, kimiPath, "runtime"), "pinned Kimi runtime");
const kimi = { version: QUALIFIED_ACPX_PROFILES.kimi.agentServerVersion,
profileDigest: QUALIFIED_ACPX_PROFILES.kimi.commandDigest,
closureDigest: `sha256:${KIMI_DISTRIBUTION_CLOSURE_SHA256["linux-x64"]}`,
qualification: "qualified", path: kimiPath, sha256: sha256DirectoryTree(join(root, kimiPath)) };
Object.assign(payload, { providers: { cursor, kimi } });
await mkdir(releaseMetadata, { recursive: true });
const writeKimiManifest = async () => {
await writeManifest();
await cp(join(root, "provider-pack.json"), manifestPath);
};
await writeKimiManifest();
expect(readRemoteProviderPackManifest(root).payload.providers?.kimi).toEqual(kimi);
expect(readBundledRemoteProviderPackManifest(manifestPath).payload.providers?.kimi?.qualification).toBe("qualified");
for (const field of ["version", "profileDigest", "closureDigest"] as const) {
const previous = kimi[field];
kimi[field] = field === "version" ? "older-kimi" : digest(`old-kimi-${field}`);
await writeKimiManifest();
expect(() => readRemoteProviderPackManifest(root)).toThrow("Kimi profile or closure");
expect(() => readBundledRemoteProviderPackManifest(manifestPath)).toThrow("Kimi profile or closure");
kimi[field] = previous;
}
kimi.closureDigest = `sha256:${KIMI_DISTRIBUTION_CLOSURE_SHA256["darwin-arm64"]}`;
await writeKimiManifest();
expect(() => readBundledRemoteProviderPackManifest(manifestPath)).toThrow("Kimi profile or closure");
kimi.closureDigest = `sha256:${KIMI_DISTRIBUTION_CLOSURE_SHA256["linux-x64"]}`;
for (const target of [{ platform: "linux", architecture: "arm64" }, { platform: "darwin", architecture: "arm64" }]) {
payload.target = target;
kimi.path = `provider-assets/kimi/${target.platform}-${target.architecture}`;
Object.assign(payload, { providers: {} });
await writeKimiManifest();
expect(() => readBundledRemoteProviderPackManifest(manifestPath)).toThrow("bundled image pack must target linux-x64");
}
payload.target = originalTarget;
kimi.path = kimiPath;
Object.assign(payload, { providers: { cursor, kimi } });
kimi.qualification = "pending";
await writeKimiManifest();
expect(() => readBundledRemoteProviderPackManifest(manifestPath)).toThrow("invalid candidate identity");
kimi.qualification = "qualified";
Object.assign(payload, { providers: { cursor, kimi }, candidateProviders: { kimi } });
await writeKimiManifest();
expect(() => readBundledRemoteProviderPackManifest(manifestPath)).toThrow("Kimi profile or closure");
Object.assign(payload, { providers: { cursor, kimi } });
Reflect.deleteProperty(payload, "candidateProviders");
await writeKimiManifest();
await writeFile(join(root, kimiPath, "runtime"), "substitute Kimi runtime");
expect(() => readRemoteProviderPackManifest(root)).toThrow("candidate asset tree digest mismatch");
await writeFile(join(root, kimiPath, "runtime"), "pinned Kimi runtime");
const threeCandidates = { cursor,
pi: { ...kimi, version: "1.0.0", qualification: "qualified", path: "provider-assets/pi/linux-x64" },
copilot: { ...kimi, qualification: "pending", version: QUALIFIED_ACPX_PROFILES.copilot.agentServerVersion,
profileDigest: QUALIFIED_ACPX_PROFILES.copilot.commandDigest,
closureDigest: "sha256:a3d8f4367cfa1694d79e3f8b7930b6fbfe42a229c272b375b11951d631db8d07",
path: "provider-assets/copilot/linux-x64" } };
for (const candidate of [threeCandidates.pi, threeCandidates.copilot]) {
await mkdir(join(root, candidate.path), { recursive: true });
await writeFile(join(root, candidate.path, "runtime"), "candidate runtime");
candidate.sha256 = sha256DirectoryTree(join(root, candidate.path));
}
Object.assign(payload, { candidateProviders: threeCandidates });
await writeKimiManifest();
expect(Object.keys(readRemoteProviderPackManifest(root).payload.candidateProviders!)).toHaveLength(3);
Object.assign(payload, { candidateProviders: { ...threeCandidates, unknown: kimi } });
await writeKimiManifest();
expect(() => readBundledRemoteProviderPackManifest(manifestPath)).toThrow("invalid candidate inventory");
Object.assign(payload, { candidateProviders: { unknown: kimi } });
await writeKimiManifest();
expect(() => readBundledRemoteProviderPackManifest(manifestPath)).toThrow("invalid candidate identity");
await rm(releaseMetadata, { recursive: true, force: true });
const candidatePath = "provider-assets/pi/linux-x64";
await mkdir(join(root, candidatePath), { recursive: true });
await writeFile(join(root, candidatePath, "runtime"), "pinned runtime");
@@ -3791,10 +3900,10 @@ describe("runtime question fallback", () => {
});
describe("native provider bootstrap environment", () => {
it.each(["pi", "cursor", "copilot", "antigravity"] as const)("does not promote ambient %s credentials when bindings are omitted", agent => {
it.each(["pi", "cursor", "copilot", "antigravity", "kimi"] as const)("does not promote ambient %s credentials when bindings are omitted", agent => {
const provider = { kind: "acpx", agent } as NativeExecutionInput["provider"];
const host = { PATH: "/host/bin", HOME: "/host/home", OPENROUTER_API_KEY: "ambient-pi",
CURSOR_API_KEY: "ambient-cursor", CURSOR_AUTH_TOKEN: "ambient-cursor-login", COPILOT_GITHUB_TOKEN: "ambient-copilot",
CURSOR_API_KEY: "ambient-cursor", CURSOR_AUTH_TOKEN: "ambient-cursor-login", COPILOT_GITHUB_TOKEN: "ambient-copilot", KIMI_API_KEY: "ambient-kimi",
GEMINI_API_KEY: "ambient-google", GOOGLE_API_KEY: "ambient-other-google", GEMINI_HOME: "/ambient-google-home",
GOOGLE_APPLICATION_CREDENTIALS: "/ambient-google-credential.json", PAPERCLIP_ACPX_ANTIGRAVITY_AUTH_JSON_SECRET: "ambient-google-subscription",
PAPERCLIP_ACPX_CREDENTIAL_BINDING: "ambient-forged-binding" };
@@ -40,7 +40,7 @@ import { apiKeyBillingType } from "@paperclipai/adapter-utils/api-billing-type";
import { agents } from "@paperclipai/db";
import { isCloudManagedInstance } from "../cloud-instance.js";
import { dotRunnerBroker } from "../dot-runner-broker.js";
import { CURSOR_DISTRIBUTION_PINS, QUALIFIED_ACPX_PROFILES, QUALIFIED_ACPX_VERSION } from "../../vendor/paperclip-runner/index.js";
import { CURSOR_DISTRIBUTION_PINS, KIMI_DISTRIBUTION_CLOSURE_SHA256, QUALIFIED_ACPX_PROFILES, QUALIFIED_ACPX_VERSION } from "../../vendor/paperclip-runner/index.js";
import { isProviderMode } from "../../vendor/paperclip-runner/index.js";
import { PROVIDER_PACK_COMPATIBILITY, bundledRemoteProviderPackManifestPath, bundledRemoteRunnerBinary, runnerBinaryTarget } from "../../vendor/paperclip-runner/index.js";
import { nativeRetryCancellationEligible, rethrowNativeCancellationLockConflict, assertCancellationRequest, cancellationIntentId as callerCancellationIntentId, cancellationRequestId } from "./native-cancellation-request.js";
@@ -48,7 +48,7 @@ import { readNativePlanWait } from "./native-plan-wait.js";
import { nativeProviderLifecycle } from "./provider-lifecycle.js";
import { NativePermissionDeclinedError, readCompletedNativePermissionDecline } from "./native-permission-decline.js";
import { activeIssueInteractionCondition } from "../issue-question-context.js";
import { resolveAcpxQualification, resolveAntigravityCandidateAdmission } from "./acpx-qualification.js";
import { resolveAcpxQualification, resolveAntigravityCandidateAdmission, resolveKimiCandidateAdmission } from "./acpx-qualification.js";
import { readLocalAiCredentialFile } from "../local-ai-credential-file.js";
import { prepareGrokRunnerCredentials } from "./grok-runner-credentials.js";
import { bindAntigravityCredentialTurn, bindAntigravityTransportRetirement, prepareAntigravityRunnerCredentials } from "./antigravity-runner-credentials.js";
@@ -929,7 +929,7 @@ export function resolveNativeProviderEnvironment(
host: NodeJS.ProcessEnv = process.env,
): NodeJS.ProcessEnv {
if (configured !== undefined) return configured;
if (provider.kind === "acpx" && ["pi", "cursor", "copilot", "antigravity"].includes(provider.agent)) {
if (provider.kind === "acpx" && ["pi", "cursor", "copilot", "kimi", "antigravity"].includes(provider.agent)) {
return buildNativeProviderEnvironment({}, host);
}
return host;
@@ -8109,6 +8109,14 @@ async function executePaperclipNativeSessionWithinScope(
throw new Error("paperclip_runner_antigravity_candidate_ineligible: exact local per-turn candidate admission is required");
}
}
if (input.execution.provider.kind === "acpx" && input.execution.provider.agent === "kimi"
&& QUALIFIED_ACPX_PROFILES.kimi.qualificationStatus === "pending") {
const target = input.runnerExecutionTarget?.kind === "remote"
? { platform: "linux", architecture: "x64" } : { platform: process.platform, architecture: process.arch };
if (!input.useRunnerd || input.backend || !resolveKimiCandidateAdmission({
companyId: input.execution.binding.companyId, model: input.execution.provider.model, target,
}, process.env)) throw new Error("paperclip_runner_kimi_candidate_ineligible: exact trusted target admission is required");
}
const restoredController = input.restartRecovery?.kind === "reattach_remote_runner"
? await restoreNativeRunnerCheckpoint(input.db, input.execution.binding, scopedRunnerdStateRoot(input.execution))
: null;
@@ -10405,6 +10413,8 @@ const REMOTE_PROVIDER_PACK_ARTIFACT_PATHS = {
opencodeProxy: "dist/cli/opencode-app-server-proxy.cjs",
acpxSidecar: "dist/cli/acpx-runtime-sidecar.cjs",
} as const;
const REMOTE_PROVIDER_PACK_PROVIDERS = ["pi", "cursor", "copilot", ...(QUALIFIED_ACPX_PROFILES.kimi.qualificationStatus !== "pending" ? ["kimi"] as const : [])] as const;
const REMOTE_PROVIDER_PACK_CANDIDATES = ["pi", "cursor", "copilot", ...(QUALIFIED_ACPX_PROFILES.kimi.qualificationStatus === "pending" ? ["kimi"] as const : [])] as const;
type RemoteProviderPackManifest = {
schema: typeof REMOTE_PROVIDER_PACK_SCHEMA;
@@ -10417,11 +10427,11 @@ type RemoteProviderPackManifest = {
distDigest: string;
bridgeDigest: string;
acpxProfileDigests: typeof REMOTE_PROVIDER_PACK_PROFILE_DIGESTS;
providers?: Partial<Record<"pi" | "cursor" | "copilot", {
providers?: Partial<Record<typeof REMOTE_PROVIDER_PACK_PROVIDERS[number], {
version: string; profileDigest: string; closureDigest: string; qualification: "qualified" | "pending";
path: string; sha256: string;
}>>;
candidateProviders?: Partial<Record<"cursor" | "copilot" | "pi", {
candidateProviders?: Partial<Record<typeof REMOTE_PROVIDER_PACK_CANDIDATES[number], {
version: string; profileDigest: string; closureDigest: string; qualification: "qualified" | "pending";
path: string; sha256: string;
}>>;
@@ -10602,14 +10612,15 @@ function readRemoteProviderPackIdentity(packRoot: string, verifyControllerFiles:
}
for (const [inventory, candidates] of [["providers", payload.providers], ["candidateProviders", payload.candidateProviders]] as const) {
if (candidates === undefined) continue;
if (!candidates || typeof candidates !== "object" || Array.isArray(candidates) || Object.keys(candidates).length > 3) {
const allowedProviders: readonly string[] = inventory === "providers" ? REMOTE_PROVIDER_PACK_PROVIDERS : REMOTE_PROVIDER_PACK_CANDIDATES;
if (!candidates || typeof candidates !== "object" || Array.isArray(candidates) || Object.keys(candidates).length > allowedProviders.length) {
throw new Error("runner_remote_provider_artifact_incompatible: invalid candidate inventory");
}
for (const [provider, candidate] of Object.entries(candidates)) {
const expectedPath = `provider-assets/${provider}/${payload.target.platform}-${payload.target.architecture}`;
if (!(inventory === "providers" ? ["pi", "cursor", "copilot"] : ["cursor", "copilot", "pi"]).includes(provider) || !candidate
if (!allowedProviders.includes(provider) || !candidate
|| Object.keys(candidate).some(key => !["version", "profileDigest", "closureDigest", "qualification", "path", "sha256"].includes(key))
|| candidate.qualification !== (["pi", "cursor"].includes(provider) || inventory === "providers" && provider === "copilot" ? "qualified" : "pending") || candidate.path !== expectedPath
|| candidate.qualification !== (["pi", "cursor"].includes(provider) || inventory === "providers" && (provider === "copilot" || provider === "kimi" && QUALIFIED_ACPX_PROFILES.kimi.qualificationStatus !== "pending") ? "qualified" : "pending") || candidate.path !== expectedPath
|| typeof candidate.version !== "string" || !candidate.version || candidate.version.length > 120
|| !/^sha256:[a-f0-9]{64}$/.test(candidate.profileDigest)
|| !/^sha256:[a-f0-9]{64}$/.test(candidate.closureDigest)
@@ -10642,6 +10653,16 @@ function readRemoteProviderPackIdentity(packRoot: string, verifyControllerFiles:
throw new Error("runner_remote_provider_artifact_incompatible: Copilot profile or closure does not match this release");
}
}
if (provider === "kimi") {
if (inventory !== (QUALIFIED_ACPX_PROFILES.kimi.qualificationStatus === "pending" ? "candidateProviders" : "providers")
|| payload[QUALIFIED_ACPX_PROFILES.kimi.qualificationStatus === "pending" ? "providers" : "candidateProviders"]?.kimi
|| payload.target.platform !== "linux" || payload.target.architecture !== "x64"
|| candidate.version !== QUALIFIED_ACPX_PROFILES.kimi.agentServerVersion
|| candidate.profileDigest !== QUALIFIED_ACPX_PROFILES.kimi.commandDigest
|| candidate.closureDigest !== `sha256:${KIMI_DISTRIBUTION_CLOSURE_SHA256["linux-x64"]}`) {
throw new Error("runner_remote_provider_artifact_incompatible: Kimi profile or closure does not match this release");
}
}
if (verifyControllerFiles && sha256DirectoryTree(resolve(packRoot, candidatePath)) !== candidate.sha256) {
throw new Error("runner_remote_provider_artifact_incompatible: candidate asset tree digest mismatch");
}
@@ -10848,6 +10869,142 @@ export async function stageRemoteRunnerDirectory(input: {
}
}
/** The normal remote pack installer, without run/session/DB authority. Both
* native execution and environment Test use the same admitted manifest and
* complete archive/directory verification before a provider is invoked. */
export async function prepareRemoteProviderPackOnTarget(input: {
target: Extract<AdapterExecutionTarget, { kind: "remote" }>;
runner: CommandManagedRuntimeRunner;
configuredProviderPackRoot: string | null;
manifest: RemoteProviderPackManifest;
trace?: NativeRunTrace;
onLog?: (stream: "stdout" | "stderr", chunk: string) => void | Promise<void>;
}): Promise<{ root: string; manifest: RemoteProviderPackManifest; source: "staged" | "preinstalled" | "uploaded" }> {
const remoteTarget = input.target, remoteCommandRunner = input.runner;
const configuredProviderPackRoot = input.configuredProviderPackRoot;
const expectedProviderPackManifest = input.manifest;
const stagedRemoteProviderPackRoot = posix.join(remoteTarget.remoteCwd, ".paperclip-runtime", "paperclip-runner", "provider-pack");
let activeRemoteProviderPackRoot: string | null = null;
let preinstalledProviderPackRoots: string[] = [];
const verifyRemoteProviderPack = async (packRoot: string) => {
await verifyRemoteProviderPackIdentity({ runner: remoteCommandRunner,
cwd: remoteTarget.remoteCwd, packRoot, manifest: expectedProviderPackManifest });
};
const discoverPreinstalledProviderPack = async () => {
if (!remoteTarget || !remoteCommandRunner) return [];
const result = await remoteCommandRunner.execute({
command: "sh",
args: [
"-c",
'for candidate in /opt/paperclip-runner/provider-pack "$HOME/.local/share/paperclip-runner/provider-pack"; do if [ -f "$candidate/provider-pack.json" ]; then printf \'%s\\n\' "$candidate"; fi; done',
],
cwd: remoteTarget.remoteCwd,
bypassSession: true,
timeoutMs: 10_000,
});
if (result.exitCode !== 0 || result.timedOut) return [];
return result.stdout.split(/\r?\n/).map(parseRemoteExecutableCandidate)
.filter((candidate): candidate is string => candidate !== null);
};
const packSource = await prepareVerifiedRemoteProviderPack({
trace: input.trace,
verifyStaged: () => measureNativeRunnerSpan(
input.trace,
"provider_pack.verify",
() => verifyRemoteProviderPack(stagedRemoteProviderPackRoot),
),
usePreinstalled: async () => useVerifiedPreinstalledRemoteProviderPack({
candidates: preinstalledProviderPackRoots = await measureNativeRunnerSpan(input.trace, "provider_pack.discover", discoverPreinstalledProviderPack),
verifyAndLink: async (preinstalledProviderPack) => {
await measureNativeRunnerSpan(
input.trace,
"provider_pack.verify_preinstalled",
() => verifyRemoteProviderPack(preinstalledProviderPack),
);
const escapedSource = preinstalledProviderPack.replaceAll(
"'",
"'\\''",
);
const escapedTarget = stagedRemoteProviderPackRoot.replaceAll(
"'",
"'\\''",
);
const escapedParent = posix
.dirname(stagedRemoteProviderPackRoot)
.replaceAll("'", "'\\''");
const linked = await measureNativeRunnerSpan(input.trace, "provider_pack.link_preinstalled", () => remoteCommandRunner.execute({
command: "sh",
args: [
"-c",
`umask 077; mkdir -p '${escapedParent}' && rm -rf '${escapedTarget}' && ln -s '${escapedSource}' '${escapedTarget}'`,
],
cwd: remoteTarget.remoteCwd,
bypassSession: true,
timeoutMs: 10_000,
}));
if (linked.exitCode !== 0 || linked.timedOut) {
throw new Error(
"runner_remote_provider_artifact_incompatible: preinstalled provider pack could not be linked",
);
}
activeRemoteProviderPackRoot = stagedRemoteProviderPackRoot;
await input.onLog?.(
"stderr",
"[paperclip-runner] using manifest-matched preinstalled provider pack\n",
);
},
}),
stageAndVerify: async () => {
if (!configuredProviderPackRoot) {
throw new Error("runner_remote_provider_artifact_incompatible: install the matching Paperclip package and Daytona image; the image provider pack did not match the bundled release identity");
}
if (!remoteCommandRunner.syncIn) {
throw new Error(
"runner_remote_provider_artifact_incompatible: this remote transport cannot stage a provider pack; preinstall the exact manifest-matched pack",
);
}
await uploadVerifiedProviderPack({
root: configuredProviderPackRoot,
expectedDigest: expectedProviderPackManifest.digest,
runner: remoteCommandRunner,
cwd: remoteTarget.remoteCwd,
targetPath: stagedRemoteProviderPackRoot,
reuseRoots: preinstalledProviderPackRoots,
trace: input.trace,
verifyDirectory: () => { readRemoteProviderPackManifest(configuredProviderPackRoot); },
stageDirectory: () => stageRemoteRunnerDirectory({
target: remoteTarget,
runner: remoteCommandRunner,
sourcePath: configuredProviderPackRoot,
targetPath: stagedRemoteProviderPackRoot,
mode: 0o700,
}),
verifyUploaded: () => verifyRemoteProviderPack(stagedRemoteProviderPackRoot),
});
activeRemoteProviderPackRoot = stagedRemoteProviderPackRoot;
},
});
activeRemoteProviderPackRoot = stagedRemoteProviderPackRoot;
if (packSource === "staged") {
await input.onLog?.(
"stderr",
"[paperclip-runner] reusing manifest-matched provider pack from the workspace\n",
);
}
return { root: activeRemoteProviderPackRoot!, manifest: expectedProviderPackManifest, source: packSource };
}
/** Read selection identity only; verify controller contents only if upload is needed. */
export function resolveRemoteProviderPackSource(configuredPath?: string | null): {
configuredRoot: string; manifest: RemoteProviderPackManifest;
} {
const configuredRoot = configuredPath?.trim() || null;
if (!configuredRoot || !existsSync(configuredRoot) || !lstatSync(configuredRoot).isDirectory()) {
throw new Error("runner_remote_provider_artifact_incompatible: configure PAPERCLIP_RUNNER_REMOTE_PROVIDER_PACK_PATH with the build-owned provider pack");
}
return { configuredRoot, manifest: readRemoteProviderPackIdentity(configuredRoot, false) };
}
async function remoteRunnerPathExists(input: {
runner: CommandManagedRuntimeRunner;
path: string;
@@ -12317,13 +12474,7 @@ async function createRunnerdBackendWithinSessionClaim(
? readBundledRemoteProviderPackManifest(bundledClaudeManifest, remoteClaudeTarget)
: readBundledRemoteProviderPackManifest();
}
if (!configuredProviderPackRoot || !existsSync(configuredProviderPackRoot)
|| !lstatSync(configuredProviderPackRoot).isDirectory()) {
throw new Error("runner_remote_provider_artifact_incompatible: configure PAPERCLIP_RUNNER_REMOTE_PROVIDER_PACK_PATH with the build-owned provider pack");
}
// Selection reads identity only. Content verification belongs to the
// install path; a compatible image pack needs no controller tree scan.
return readRemoteProviderPackIdentity(configuredProviderPackRoot, false);
return resolveRemoteProviderPackSource(configuredProviderPackRoot).manifest;
})();
return currentProviderPackManifest;
};
@@ -12435,7 +12586,6 @@ async function createRunnerdBackendWithinSessionClaim(
}
: sourceRuntimeContext;
let remotePrepared = false;
let preinstalledProviderPackRoots: string[] = [];
let remoteHarnessStatePrepared = false;
let selectedRemoteMode: "dial_wss" | "listen_ws" | null = null;
let remoteCaBundleMapping: { sourcePath: string; targetPath: string } | null =
@@ -12456,23 +12606,6 @@ async function createRunnerdBackendWithinSessionClaim(
providerPackVerified = true;
};
const discoverPreinstalledProviderPack = async () => {
if (!remoteTarget || !remoteCommandRunner) return [];
const result = await remoteCommandRunner.execute({
command: "sh",
args: [
"-c",
'for candidate in /opt/paperclip-runner/provider-pack "$HOME/.local/share/paperclip-runner/provider-pack"; do if [ -f "$candidate/provider-pack.json" ]; then printf \'%s\\n\' "$candidate"; fi; done',
],
cwd: remoteTarget.remoteCwd,
bypassSession: true,
timeoutMs: 10_000,
});
if (result.exitCode !== 0 || result.timedOut) return [];
return result.stdout.split(/\r?\n/).map(parseRemoteExecutableCandidate)
.filter((candidate): candidate is string => candidate !== null);
};
const remoteNativeArtifacts = remoteTarget && remoteCommandRunner && remoteBinary && remoteRuntimeRoot
? createRemoteNativeArtifactPreparation({
target: remoteTarget, runner: remoteCommandRunner, remoteBinary, controllerRunnerBinary,
@@ -12557,91 +12690,12 @@ async function createRunnerdBackendWithinSessionClaim(
expectedProviderPackManifest &&
stagedRemoteProviderPackRoot
) {
const packSource = await prepareVerifiedRemoteProviderPack({
trace: input.trace,
verifyStaged: () => measureNativeRunnerSpan(
input.trace,
"provider_pack.verify",
() => verifyRemoteProviderPack(stagedRemoteProviderPackRoot),
),
usePreinstalled: async () => useVerifiedPreinstalledRemoteProviderPack({
candidates: preinstalledProviderPackRoots = await measureNativeRunnerSpan(input.trace, "provider_pack.discover", discoverPreinstalledProviderPack),
verifyAndLink: async (preinstalledProviderPack) => {
await measureNativeRunnerSpan(
input.trace,
"provider_pack.verify_preinstalled",
() => verifyRemoteProviderPack(preinstalledProviderPack),
);
const escapedSource = preinstalledProviderPack.replaceAll(
"'",
"'\\''",
);
const escapedTarget = stagedRemoteProviderPackRoot.replaceAll(
"'",
"'\\''",
);
const escapedParent = posix
.dirname(stagedRemoteProviderPackRoot)
.replaceAll("'", "'\\''");
const linked = await measureNativeRunnerSpan(input.trace, "provider_pack.link_preinstalled", () => remoteCommandRunner.execute({
command: "sh",
args: [
"-c",
`umask 077; mkdir -p '${escapedParent}' && rm -rf '${escapedTarget}' && ln -s '${escapedSource}' '${escapedTarget}'`,
],
cwd: remoteTarget.remoteCwd,
bypassSession: true,
timeoutMs: 10_000,
}));
if (linked.exitCode !== 0 || linked.timedOut) {
throw new Error(
"runner_remote_provider_artifact_incompatible: preinstalled provider pack could not be linked",
);
}
activeRemoteProviderPackRoot = stagedRemoteProviderPackRoot;
await input.onLog?.(
"stderr",
"[paperclip-runner] using manifest-matched preinstalled provider pack\n",
);
},
}),
stageAndVerify: async () => {
if (!configuredProviderPackRoot) {
throw new Error("runner_remote_provider_artifact_incompatible: install the matching Paperclip package and Daytona image; the image provider pack did not match the bundled release identity");
}
if (!remoteCommandRunner.syncIn) {
throw new Error(
"runner_remote_provider_artifact_incompatible: this remote transport cannot stage a provider pack; preinstall the exact manifest-matched pack",
);
}
await uploadVerifiedProviderPack({
root: configuredProviderPackRoot,
expectedDigest: expectedProviderPackManifest!.digest,
runner: remoteCommandRunner,
cwd: remoteTarget.remoteCwd,
targetPath: stagedRemoteProviderPackRoot,
reuseRoots: preinstalledProviderPackRoots,
trace: input.trace,
verifyDirectory: () => { readRemoteProviderPackManifest(configuredProviderPackRoot); },
stageDirectory: () => stageRemoteRunnerDirectory({
target: remoteTarget,
runner: remoteCommandRunner,
sourcePath: configuredProviderPackRoot,
targetPath: stagedRemoteProviderPackRoot,
mode: 0o700,
}),
verifyUploaded: () => verifyRemoteProviderPack(stagedRemoteProviderPackRoot),
});
activeRemoteProviderPackRoot = stagedRemoteProviderPackRoot;
},
const prepared = await prepareRemoteProviderPackOnTarget({
target: remoteTarget, runner: remoteCommandRunner, configuredProviderPackRoot,
manifest: expectedProviderPackManifest, trace: input.trace, onLog: input.onLog,
});
activeRemoteProviderPackRoot = stagedRemoteProviderPackRoot;
if (packSource === "staged") {
await input.onLog?.(
"stderr",
"[paperclip-runner] reusing manifest-matched provider pack from the workspace\n",
);
}
activeRemoteProviderPackRoot = prepared.root;
providerPackVerified = true;
}
remotePrepared = true;
};
@@ -13664,7 +13718,10 @@ async function createRunnerdBackendWithinSessionClaim(
: resolveAcpxQualification(input.execution.provider, process.env),
acpxCandidateAdmission: input.execution.provider.agent === "antigravity"
? resolveAntigravityCandidateAdmission({ companyId: input.execution.binding.companyId, model: input.execution.provider.model }, process.env)
: undefined,
: input.execution.provider.agent === "kimi" && QUALIFIED_ACPX_PROFILES.kimi.qualificationStatus === "pending"
? resolveKimiCandidateAdmission({ companyId: input.execution.binding.companyId, model: input.execution.provider.model,
target: remoteTarget ? { platform: "linux", architecture: "x64" } : { platform: process.platform, architecture: process.arch } }, process.env)
: undefined,
acpxPermissionMode: input.execution.provider.permissionMode,
acpxMode: input.execution.provider.mode,
piThinkingLevel: input.execution.provider.piThinkingLevel,
@@ -91,6 +91,23 @@ it.skipIf(!tar || !hasZstd)("reuses only byte-matched image files, keeps modes,
expect(await fs.readlink(path.join(targetPath, "tool"))).toBe("bin/tool");
});
it.skipIf(!tar || !hasZstd)("stages a complete layered pack into a fresh nested runtime directory", async () => {
const { root, pack, archive } = await layeredFixture();
const remote = await runner(root);
const workspace = path.join(root, "workspace");
await fs.mkdir(workspace);
const targetPath = path.join(workspace, ".paperclip-runtime", "paperclip-runner", "provider-pack");
await expect(fs.lstat(path.dirname(targetPath))).rejects.toMatchObject({ code: "ENOENT" });
expect(await stageProviderPackArchive({ archive, runner: remote, targetPath, reuseRoots: [pack] })).toBe(true);
expect(await fs.readFile(path.join(targetPath, "bin/large"))).toEqual(await fs.readFile(path.join(pack, "bin/large")));
expect(await fs.readFile(path.join(targetPath, "provider-pack.json"))).toEqual(await fs.readFile(path.join(pack, "provider-pack.json")));
expect(await fs.readlink(path.join(targetPath, "tool"))).toBe("bin/tool");
for (const directory of [targetPath, path.dirname(targetPath), path.dirname(path.dirname(targetPath))]) {
expect((await fs.stat(directory)).mode & 0o777).toBe(0o700);
}
expect((await fs.stat(path.join(targetPath, "bin/large"))).ino).not.toBe((await fs.stat(path.join(pack, "bin/large"))).ino);
});
it.skipIf(!tar || !hasZstd).each(["missing", "wrong-digest", "symlink", "ancestor-symlink"])("transfers a large layer when the image file is %s", async reason => {
const { root, pack, archive } = await layeredFixture();
const remote = await runner(root);
@@ -135,13 +152,18 @@ it.skipIf(!tar || !hasZstd)("rejects a corrupt missing layer before transfer and
expect(remote.syncIn).not.toHaveBeenCalled();
});
it.skipIf(!tar || !hasZstd)("does not follow a layered target symlink or accept invalid reuse receipts", async () => {
it.skipIf(!tar || !hasZstd).each(["directory", "file", "symlink"])("rejects an existing layered target %s or invalid reuse receipts", async kind => {
const { root, archive } = await layeredFixture();
const remote = await runner(root);
const targetPath = path.join(root, "target"), other = path.join(root, "other");
await fs.mkdir(other); await fs.symlink(other, targetPath);
await fs.mkdir(other);
if (kind === "directory") await fs.mkdir(targetPath);
else if (kind === "file") await fs.writeFile(targetPath, "untouched");
else await fs.symlink(other, targetPath);
await expect(stageProviderPackArchive({ archive, runner: remote, targetPath })).rejects.toThrow("runner_remote_provider_archive_reuse_failed");
expect(await fs.readdir(other)).toEqual([]);
if (kind === "directory") expect(await fs.readdir(targetPath)).toEqual([]);
if (kind === "file") expect(await fs.readFile(targetPath, "utf8")).toBe("untouched");
remote.execute.mockResolvedValue({ exitCode: 0, timedOut: false, stdout: "[999999]", stderr: "", signal: null, pid: null, startedAt: null });
await expect(stageProviderPackArchive({ archive, runner: remote, targetPath: `${targetPath}-2` })).rejects.toThrow("runner_remote_provider_archive_reuse_failed");
expect(remote.syncIn).not.toHaveBeenCalled();
@@ -70,7 +70,8 @@ const reuseScript = `
(async()=>{
const fs=require('node:fs'),p=require('node:path'),c=require('node:crypto');
const spec=JSON.parse(Buffer.from(process.argv[1],'base64').toString('utf8'));
const f=fs.promises;await f.mkdir(spec.target,{mode:448});
const f=fs.promises;await f.mkdir(p.dirname(spec.target),{recursive:true,mode:448});
await f.mkdir(spec.target,{mode:448});
const missing=[];
const regularPath=async(root,relative)=>{
let current=root;if(!(await f.lstat(current)).isDirectory())return false;
@@ -18,7 +18,7 @@ import {
AGENTCORE_QUALIFIED_MODEL,
CLAUDE_MANAGED_QUALIFIED_MODEL,
} from "../provider-profile-qualification.js";
import { resolveAcpxQualification, resolveAntigravityCandidateAdmission, type AcpxQualificationCandidate } from "./acpx-qualification.js";
import { resolveAcpxQualification, resolveAntigravityCandidateAdmission, resolveKimiCandidateAdmission, type AcpxQualificationCandidate } from "./acpx-qualification.js";
import { compatibleCodexModel } from "./codex-model-fallback.js";
export const QUALIFIED_OPENCODE_RUNNER_VERSION = "1.18.34" as const;
@@ -34,6 +34,7 @@ export const DEFAULT_ACPX_RUNNER_MODELS = {
cursor: null,
copilot: null,
pi: null,
kimi: null,
} as const;
export type QualifiedPaperclipRunnerAcpxAgent =
@@ -564,6 +565,15 @@ export function resolvePaperclipRunnerProviderProfile(
throw new PaperclipRunnerProviderProfileError("paperclip_runner_acpx_qualification_invalid", "Antigravity candidate admission requires the authorized company, exact model and runtime identity.");
}
}
if (pendingAcpxProfile.value === "kimi") {
try {
if (resolveKimiCandidateAdmission({ companyId: context.companyId, model }, process.env)) {
return { provider: "acpx", backend: "acpx_runtime", model, acpxAgent: "kimi" };
}
} catch {
throw new PaperclipRunnerProviderProfileError("paperclip_runner_acpx_qualification_invalid", "Kimi candidate admission requires the authorized company, exact model, target and runtime identity.");
}
}
// Qualification authority belongs to the isolated server operator. Never
// read this allowlist from adapter config, credential refs, or run env.
let qualification: AcpxQualificationCandidate | undefined;
@@ -577,10 +587,10 @@ export function resolvePaperclipRunnerProviderProfile(
}
throw new PaperclipRunnerProviderProfileError("paperclip_runner_acpx_agent_unavailable", `${pendingAcpxProfile.label} is awaiting native Runner acceptance. Its profile is not enabled for production runs.`);
}
if ((acpxAgent === "cursor" || acpxAgent === "pi") && !model) {
if ((acpxAgent === "cursor" || acpxAgent === "pi" || acpxAgent === "kimi") && !model) {
throw new PaperclipRunnerProviderProfileError("paperclip_runner_acpx_model_required", "This ACPX agent requires an explicit model ID; there is no default model.");
}
if (acpxAgent !== "claude" && acpxAgent !== "codex" && acpxAgent !== "grok" && acpxAgent !== "cursor" && acpxAgent !== "copilot" && acpxAgent !== "pi") {
if (acpxAgent !== "claude" && acpxAgent !== "codex" && acpxAgent !== "grok" && acpxAgent !== "cursor" && acpxAgent !== "copilot" && acpxAgent !== "pi" && acpxAgent !== "kimi") {
throw new PaperclipRunnerProviderProfileError(
"paperclip_runner_acpx_agent_unavailable",
"Paperclip Runner ACPX requires a qualified agent profile.",
@@ -10,7 +10,7 @@ export function shouldUseBundledAcpxImageAssets(input: {
}): boolean {
return input.requiresRemoteProviderPack && !input.configuredProviderPackRoot &&
input.provider.kind === "acpx" &&
["cursor", "copilot"].includes(input.provider.agent);
["cursor", "copilot", "kimi"].includes(input.provider.agent);
}
type VerificationCommandResult = {
+1
View File
@@ -162,6 +162,7 @@ export const resolveGrokEffort = runner.resolveGrokEffort;
export const QUALIFIED_ACPX_PROFILES = runner.QUALIFIED_ACPX_PROFILES;
export const QUALIFIED_ACPX_VERSION = runner.QUALIFIED_ACPX_VERSION;
export const CURSOR_DISTRIBUTION_PINS = runner.CURSOR_DISTRIBUTION_PINS;
export const KIMI_DISTRIBUTION_CLOSURE_SHA256 = runner.KIMI_DISTRIBUTION_CLOSURE_SHA256;
export const resolveSourceCodexHome = runner.resolveSourceCodexHome;
export const resolveCodexCommand = runner.resolveCodexCommand;
export const resolvePinnedCodexCommand = runner.resolvePinnedCodexCommand;
+7
View File
@@ -12,3 +12,10 @@ export const probeAcpxClaudeInstallation = runner.probeAcpxClaudeInstallation;
export const probeAcpxGrokInstallation = runner.probeAcpxGrokInstallation;
export const probeAcpxCursorInstallation = runner.probeAcpxCursorInstallation;
export const probeAcpxPiInstallation = runner.probeAcpxPiInstallation;
export const probeAcpxKimiInstallation = runner.probeAcpxKimiInstallation;
export const probeKimiModels = runner.probeKimiModels;
export const KimiMetadataProbeError = runner.KimiMetadataProbeError;
export const probeKimiMetadata = runner.probeKimiMetadata;
export const validateKimiMetadata = runner.validateKimiMetadata;
export const kimiMetadataProbeDigest = runner.kimiMetadataProbeDigest;
export const kimiMetadataProbeNodeDigest = runner.kimiMetadataProbeNodeDigest;
@@ -64,6 +64,7 @@ export interface AdapterDisplayInfo {
const adapterDisplayMap: Record<string, AdapterDisplayInfo> = {
antigravity_runtime: { label: "Google Antigravity", description: "Antigravity through Paperclip Runner", icon: Gem, hideFromVisualSelection: true },
copilot_runtime: { label: "GitHub Copilot", description: "Copilot through Paperclip Runner", icon: Code, hideFromVisualSelection: true },
kimi_runtime: { label: "Kimi Code (Runner)", description: "Kimi Code through Paperclip Runner", icon: Moon, hideFromVisualSelection: true },
acpx_local: {
label: "ACPX (retired)",
description: "Retired standalone ACPX adapter",
@@ -310,7 +310,7 @@ export function CodexLocalConfigFields({
checked={runnerSchemaValue("allowUnmeteredProvider", false) === true} onChange={value => updateRunnerSchemaValue("allowUnmeteredProvider", value)} />
</>}
{runnerManaged && !hideRunnerHarness && runnerProvider === "acpx" && runnerSchemaValue("acpxAgent", "claude") !== "grok" && (
<Field configSection="adapter" label="ACP agent" hint="Cursor uses Paperclip questions; per-run cost is unavailable. Pi accepts any provider/model ID. GitHub Copilot awaits qualification.">
<Field configSection="adapter" label="ACP agent" hint="Choose a qualified harness and enter its exact model ID.">
<Select
value={String(isCreate ? values!.adapterSchemaValues?.acpxAgent ?? "claude" : eff("adapterConfig", "acpxAgent", config.acpxAgent ?? "claude"))}
onValueChange={(value) => {
+4
View File
@@ -11,6 +11,10 @@ const brandMarks: Record<string, { src: string; dark?: string }> = {
src: "/brands/adapters/kimi-color-light.svg",
dark: "/brands/adapters/kimi-color.svg",
},
kimi_runtime: {
src: "/brands/adapters/kimi-color-light.svg",
dark: "/brands/adapters/kimi-color.svg",
},
...Object.fromEntries(
[
["cursor", "cursor"],
@@ -1311,6 +1311,16 @@ describe("AgentConfigForm environment selector", () => {
expect(result.container.querySelector('[aria-label="Environment override"]')).toBeNull();
});
it("requests the native Kimi model catalog while preserving an explicit saved model", async () => {
mockAgentsApi.adapterModels.mockResolvedValue([]);
const result = await renderForm([], {
adapterType: "paperclip_runner", adapterConfig: { provider: "acpx", acpxAgent: "kimi", model: "explicit-kimi-model" },
});
roots.push(result.root);
await vi.waitFor(() => expect(mockAgentsApi.adapterModels).toHaveBeenCalledWith("company-1", "paperclip_runner", expect.objectContaining({ provider: "acpx", acpxAgent: "kimi" })));
expect(result.container.textContent).toContain("explicit-kimi-model");
});
it.each(["api_key", "subscription"] as const)("loads saved Antigravity %s models through its selected connection without a Claude fallback", async method => {
const binding = { provider: "google", method, mode: "responsible_user" } as const;
const accounts = vi.spyOn(aiConnectionsApi, "list").mockResolvedValue({ currentUserId: "you", canManageConnections: true, connections: [{
+2 -2
View File
@@ -1017,7 +1017,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
? String(isCreate ? props.values.adapterSchemaValues?.acpxAgent ?? "claude" : eff("adapterConfig", "acpxAgent", config.acpxAgent ?? "claude")) : undefined;
const modelAiBinding = aiConnectionBindingSchema.safeParse((overlay.runtime.runtimeConfig as Record<string, unknown> | undefined)?.aiConnection ?? runtimeConfig.aiConnection).data;
const accountBoundCatalog = isAccountBoundAcpxAgent(runnerAcpxAgent);
const modelCacheIdentity = accountBoundCatalog ? `${modelProvider}:${runnerAcpxAgent}:${isCreate ? "new" : props.agent.id}:${JSON.stringify(modelAiBinding ?? null)}` : modelProvider;
const modelCacheIdentity = accountBoundCatalog ? `${modelProvider}:${runnerAcpxAgent}:${isCreate ? "new" : props.agent.id}:${JSON.stringify(modelAiBinding ?? null)}` : runnerAcpxAgent ? `${modelProvider}:${runnerAcpxAgent}` : modelProvider;
const connectionModels = useConnectionModels(isExternalRunner ? null : selectedCompanyId, isCreate ? undefined : modelAiBinding, aiRoutingHarness(adapterType, runnerProvider, runnerAcpxAgent));
// Fetch adapter models for the effective provider, including unsaved changes.
const modelAdapterType = accountBoundCatalog ? "paperclip_runner" : isRunnerHarness ? harnessType : adapterType;
@@ -1411,7 +1411,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
setRefreshingModels(true);
setRefreshModelsError(null);
try {
const refreshed = await agentsApi.adapterModels(selectedCompanyId, modelAdapterType, { refresh: true, environmentId: currentDefaultEnvironmentId || null, provider: modelProvider, ...(accountBoundCatalog ? { acpxAgent: runnerAcpxAgent, aiConnection: modelAiBinding, agentId: !isCreate ? props.agent.id : undefined } : {}) });
const refreshed = await agentsApi.adapterModels(selectedCompanyId, modelAdapterType, { refresh: true, environmentId: currentDefaultEnvironmentId || null, provider: modelProvider, ...(runnerAcpxAgent ? { acpxAgent: runnerAcpxAgent } : {}), ...(accountBoundCatalog ? { aiConnection: modelAiBinding, agentId: !isCreate ? props.agent.id : undefined } : {}) });
queryClient.setQueryData(modelQueryKey, refreshed);
} catch (error) {
setRefreshModelsError(error instanceof Error ? error.message : "Failed to refresh adapter models.");
@@ -757,16 +757,18 @@ export function IssueProperties({
const assigneeOverrideChrome = assigneeAdapterType === "claude_local"
&& assigneeOverrideAdapterConfig.chrome === true;
const catalogProvider = assigneeAdapterType === "paperclip_runner" ? String(normalizeLegacyRunnerProvider(assigneePrimaryAdapterConfig).provider ?? "codex") : undefined;
const catalogAcpxAgent = catalogProvider === "acpx" ? String(assigneePrimaryAdapterConfig.acpxAgent ?? "claude") : undefined;
const catalogEnvironmentId = nativeCatalog ? assignee?.defaultEnvironmentId ?? null : null;
const nativeCatalogScope = JSON.stringify(nativeCatalog ?? null);
const requestNativeModels = Boolean(nativeCatalog && assigneeOptionsOpen && showAssigneeAdapterOptions && supportsAssigneeOverrides);
const { data: assigneeAdapterModels, refetch: refreshAssigneeModels } = useQuery({
queryKey:
companyId && assigneeAdapterType
? [...queryKeys.agents.adapterModels(companyId, assigneeAdapterType, catalogEnvironmentId, catalogProvider), nativeCatalog ?? null]
? [...queryKeys.agents.adapterModels(companyId, assigneeAdapterType, catalogEnvironmentId, catalogProvider), catalogAcpxAgent ?? null, nativeCatalog ?? null]
: ["agents", "none", "adapter-models", assigneeAdapterType ?? "none"],
queryFn: () => agentsApi.adapterModels(companyId!, assigneeAdapterType!, {
provider: catalogProvider,
...(catalogAcpxAgent ? { acpxAgent: catalogAcpxAgent } : {}),
...(nativeCatalog ? { environmentId: catalogEnvironmentId, ...nativeCatalog } : {}),
}),
...(nativeCatalog ? ACCOUNT_BOUND_MODEL_DISCOVERY_QUERY_OPTIONS : {}),
@@ -91,6 +91,8 @@ export function AgentBasicsDialog({
});
const nativeRunner = adapters?.find(adapter => adapter.type === "paperclip_runner" && adapter.loaded && !adapter.disabled);
const copilotAvailable = Boolean(nativeRunner) && PAPERCLIP_RUNNER_ACPX_PROFILES.some(profile => profile.value === "copilot" && profile.qualified);
const kimiAvailable = Boolean(nativeRunner) && experimental.data?.enableNativeRunner === true
&& PAPERCLIP_RUNNER_ACPX_PROFILES.some(profile => profile.value === "kimi" && profile.qualified);
const antigravityQualified = PAPERCLIP_RUNNER_ACPX_PROFILES.some(profile => profile.value === "antigravity" && profile.qualified);
const antigravityCandidate = nativeRunner?.candidateHarnesses?.some(candidate => candidate.agent === "antigravity");
const antigravityAvailable = Boolean(nativeRunner);
@@ -106,7 +108,8 @@ export function AgentBasicsDialog({
!["process", "http"].includes(adapter.type) &&
!getAdapterDisplay(adapter.type).comingSoon,
), ...(copilotAvailable ? [{ ...nativeRunner!, type: "copilot_runtime" }] : []),
...(antigravityAvailable ? [{ ...nativeRunner!, type: "antigravity_runtime" }] : [])];
...(antigravityAvailable ? [{ ...nativeRunner!, type: "antigravity_runtime" }] : []),
...(kimiAvailable ? [{ ...nativeRunner!, type: "kimi_runtime" }] : [])];
const experimentalRunnerAvailable = experimental.data?.enableNativeRunner === true
&& adapters?.some(adapter => adapter.type === "paperclip_runner" && adapter.loaded && !adapter.disabled);
const validAdapter = choices.some((adapter) => adapter.type === adapterType)
@@ -131,16 +134,16 @@ export function AgentBasicsDialog({
agent = await agentsApi.create(companyId, {
setupId: setupId.current, name: name.trim(), appearance: appearanceDraft.appearance,
role: existing.length ? "general" : "ceo", ...(leader ? { reportsTo: leader.id } : {}),
adapterType: ["copilot_runtime", "antigravity_runtime"].includes(provisionalAdapter) ? "paperclip_runner" : provisionalAdapter,
...(["copilot_runtime", "antigravity_runtime"].includes(provisionalAdapter) ? { adapterConfig: { provider: "acpx", acpxAgent: provisionalAdapter === "copilot_runtime" ? "copilot" : "antigravity" } } : {}),
adapterType: ["copilot_runtime", "antigravity_runtime", "kimi_runtime"].includes(provisionalAdapter) ? "paperclip_runner" : provisionalAdapter,
...(["copilot_runtime", "antigravity_runtime", "kimi_runtime"].includes(provisionalAdapter) ? { adapterConfig: { provider: "acpx", acpxAgent: provisionalAdapter === "copilot_runtime" ? "copilot" : provisionalAdapter === "kimi_runtime" ? "kimi" : "antigravity" } } : {}),
metadata: { setupAdapterPending: true },
runtimeConfig: buildNewAgentRuntimeConfig({ heartbeatEnabled: false }),
});
if (active.current) setName(agent.name);
}
saved.current = agent;
const nextAdapter = ["openai_dot", "copilot_runtime", "antigravity_runtime"].includes(adapterType) ? "paperclip_runner" : adapterType;
const nextProvider = adapterType === "antigravity_runtime" ? "antigravity" : adapterType === "copilot_runtime" ? "copilot" : adapterType === "openai_dot" ? "openai_dot" : adapterType === "paperclip_runner" ? runnerProvider : "codex";
const nextAdapter = ["openai_dot", "copilot_runtime", "antigravity_runtime", "kimi_runtime"].includes(adapterType) ? "paperclip_runner" : adapterType;
const nextProvider = adapterType === "kimi_runtime" ? "kimi" : adapterType === "antigravity_runtime" ? "antigravity" : adapterType === "copilot_runtime" ? "copilot" : adapterType === "openai_dot" ? "openai_dot" : adapterType === "paperclip_runner" ? runnerProvider : "codex";
// Pending providers can show setup while the saved name-only draft stays held.
const pendingAntigravity = nextProvider === "antigravity" && !antigravityQualified;
if (step === "adapter" || (!creatingRecord && agent.name !== name.trim())) {
@@ -154,8 +157,8 @@ export function AgentBasicsDialog({
...(step === "adapter" && !pendingAntigravity ? {
adapterType: nextAdapter, runner: "auto" as const,
adapterConfig: nextAdapter === "paperclip_runner" ? {
provider: ["claude", "grok", "copilot", "antigravity"].includes(nextProvider) ? "acpx" : nextProvider,
...(["claude", "grok", "copilot", "antigravity"].includes(nextProvider) ? { acpxAgent: nextProvider } : {}),
provider: ["claude", "grok", "copilot", "antigravity", "kimi"].includes(nextProvider) ? "acpx" : nextProvider,
...(["claude", "grok", "copilot", "antigravity", "kimi"].includes(nextProvider) ? { acpxAgent: nextProvider } : {}),
...(nextProvider === "grok" ? { model: "grok-4.7" } : {}),
} : {},
metadata: { ...metadata, setupAdapterPending: false },
+19 -14
View File
@@ -134,8 +134,9 @@ function Setup({
const [runner, setRunner] = useState<AgentRunnerChoice>(initialRunner === "legacy" || initialRunner === "paperclip" ? initialRunner : "auto");
const isRunner = adapterType === "paperclip_runner";
const isDot = isRunner && runnerProvider === "openai_dot";
const isKimi = isRunner && runnerProvider === "kimi";
const brandType = isDot ? "openai_dot" : isRunner
? runnerProvider === "antigravity"
? isKimi ? "kimi_runtime" : runnerProvider === "antigravity"
? "antigravity_runtime"
: runnerProvider === "copilot"
? "copilot_runtime"
@@ -158,7 +159,7 @@ function Setup({
const providerKeys = setupProviderKeys(brandType);
const chooseProvider = multiProvider || brandType === "hermes_local";
const hasCredentialField =
chooseProvider || Boolean(SETUP_CREDENTIAL_KEYS[adapterType]);
isKimi || chooseProvider || Boolean(SETUP_CREDENTIAL_KEYS[adapterType]);
const showModel = !isDot && !["cursor_cloud", "hermes_gateway"].includes(adapterType);
const [allowUnmeteredProvider, setAllowUnmeteredProvider] = useState(false);
const [gatewayUrl, setGatewayUrl] = useState("");
@@ -338,7 +339,8 @@ function Setup({
const accountModelScope = JSON.stringify([companyId, environmentId, aiBinding]);
const models = useQuery({
queryKey: queryKeys.agents.adapterModels(companyId, brandType, environmentId, accountBoundCatalog ? JSON.stringify(aiBinding ?? null) : aiBinding?.provider),
queryFn: () => agentsApi.adapterModels(companyId, accountBoundCatalog ? "paperclip_runner" : brandType, accountBoundCatalog
queryFn: () => agentsApi.adapterModels(companyId, isKimi || accountBoundCatalog ? "paperclip_runner" : brandType, isKimi
? { provider: "acpx", acpxAgent: "kimi", environmentId } : accountBoundCatalog
? { provider: "acpx", acpxAgent: runnerProvider, aiConnection: aiBinding, environmentId }
: { provider: aiBinding?.provider, environmentId }),
...(accountBoundCatalog ? ACCOUNT_BOUND_MODEL_DISCOVERY_QUERY_OPTIONS : {}),
@@ -379,7 +381,7 @@ function Setup({
environment?.driver === "sandbox" &&
caps.data?.sandboxProviders?.[sandboxProvider]?.supportsLoginPty === true;
const envKey =
SETUP_CREDENTIAL_KEYS[adapterType] ?? providerKeys[provider] ?? "API_KEY";
isKimi ? "KIMI_API_KEY" : SETUP_CREDENTIAL_KEYS[adapterType] ?? providerKeys[provider] ?? "API_KEY";
const savedKey = userSecrets.data?.find(
(entry) => entry.definition.key === envKey && entry.secret,
);
@@ -473,9 +475,9 @@ function Setup({
...(isRunner
? {
adapterSchemaValues: {
provider: ["claude", "grok", "copilot", "pi", "antigravity"].includes(runnerProvider) ? "acpx" : runnerProvider,
provider: ["claude", "grok", "copilot", "pi", "antigravity", "kimi"].includes(runnerProvider) ? "acpx" : runnerProvider,
...(isDot ? { allowUnmeteredProvider } : {}),
...(["claude", "grok", "copilot", "pi", "antigravity"].includes(runnerProvider) ? { acpxAgent: runnerProvider } : {}),
...(["claude", "grok", "copilot", "pi", "antigravity", "kimi"].includes(runnerProvider) ? { acpxAgent: runnerProvider } : {}),
},
}
: {}),
@@ -493,8 +495,8 @@ function Setup({
}
if (isRunner)
Object.assign(config, {
provider: ["claude", "grok", "copilot", "pi", "antigravity"].includes(runnerProvider) ? "acpx" : runnerProvider,
...(["claude", "grok", "copilot", "pi", "antigravity"].includes(runnerProvider) ? { acpxAgent: runnerProvider } : {}),
provider: ["claude", "grok", "copilot", "pi", "antigravity", "kimi"].includes(runnerProvider) ? "acpx" : runnerProvider,
...(["claude", "grok", "copilot", "pi", "antigravity", "kimi"].includes(runnerProvider) ? { acpxAgent: runnerProvider } : {}),
...(model ? { model } : {}),
});
if (multiProvider && created) {
@@ -542,6 +544,8 @@ function Setup({
return config;
}
function preparedConfig(nextConnection = connection, setupRunner?: AgentRunner) {
if (isKimi && !model.trim()) throw new Error("Enter the exact Kimi model ID.");
if (isKimi && !apiKey.trim() && !selectedBinding) throw new Error("Enter KIMI_API_KEY or select an organization secret.");
if (brandType === "antigravity_runtime" && (!model.trim() || model !== model.trim() || /\s/.test(model) || ["auto", "default"].includes(model.toLowerCase())))
throw new Error("Choose or enter an exact Google Antigravity model ID.");
if (brandType === "copilot_runtime" && !models.data?.some(candidate => candidate.id === model))
@@ -601,8 +605,8 @@ function Setup({
role: existing.length ? "general" : "ceo", ...(leader ? { reportsTo: leader.id } : {}),
adapterType: requestAdapterType, runner,
adapterConfig: requestAdapterType === "paperclip_runner" ? {
provider: ["claude", "grok", "copilot", "pi", "antigravity"].includes(runnerProvider) ? "acpx" : runnerProvider,
...(["claude", "grok", "copilot", "pi", "antigravity"].includes(runnerProvider) ? { acpxAgent: runnerProvider } : {}),
provider: ["claude", "grok", "copilot", "pi", "antigravity", "kimi"].includes(runnerProvider) ? "acpx" : runnerProvider,
...(["claude", "grok", "copilot", "pi", "antigravity", "kimi"].includes(runnerProvider) ? { acpxAgent: runnerProvider } : {}),
...(antigravityCandidate ? { model: antigravityCandidate.model } : {}),
} : {}, defaultEnvironmentId: environmentOverride || (forced.forced || managedOnly ? environmentId : null),
runtimeConfig: buildNewAgentRuntimeConfig({ heartbeatEnabled: false }),
@@ -1222,8 +1226,8 @@ function Setup({
}}
open={modelOpen}
onOpenChange={setModelOpen}
allowDefault={!multiProvider && !accountBoundCatalog}
required={multiProvider || accountBoundCatalog}
allowDefault={!isKimi && !multiProvider && !accountBoundCatalog}
required={isKimi || multiProvider || accountBoundCatalog}
creatable={brandType !== "copilot_runtime"}
groupByProvider={multiProvider && !connectionModels}
preserveOrder={Boolean(connectionModels) || adapterCuratesModelOrder(brandType)}
@@ -1314,10 +1318,10 @@ function Setup({
placeholder={
selectedBinding
? "Using saved key"
: [
: (isKimi || [
"cursor_cloud",
"hermes_gateway",
].includes(adapterType)
].includes(adapterType))
? "Required"
: "Optional if already configured"
}
@@ -1368,6 +1372,7 @@ function Setup({
<p className="text-xs text-muted-foreground sm:col-span-2">
New keys are saved as organization secrets when
you finish setup.
{isKimi && " Use a Kimi Code API key for this runner."}
{multiProvider && ` Use a ${provider}/model ID.`}
</p>
</div>
@@ -29,6 +29,13 @@ describe("composer run settings", () => {
expect(composerEfforts(native, "grok-4.7", ["grok-4.7"])).toEqual(["low", "medium", "high", "xhigh"]);
expect(readComposerRunSettings({ adapterConfig: native.adapterConfig }, native.adapterType)).toMatchObject({ effort: "high" });
});
it("preserves the explicit native Kimi model without inventing a default or effort control", () => {
const native = { ...agent("paperclip_runner", "acpx"), adapterConfig: { provider: "acpx", acpxAgent: "kimi" } };
expect(composerDefaultModel(native)).toBe("");
const configured = { ...native, adapterConfig: { ...native.adapterConfig, model: "exact-kimi-model" } };
expect(composerDefaultModel(configured)).toBe("exact-kimi-model");
expect(composerEfforts(configured, "exact-kimi-model", ["exact-kimi-model"])).toEqual([]);
});
it("shows only effort levels known for the selected harness and model", () => {
expect(composerEfforts(agent("codex_local"), "gpt-6-astra", [])).toContain("ultra");
expect(composerEfforts(agent("paperclip_runner", "codex"), "gpt-6-astra", [])).toContain("ultra");
@@ -46,6 +46,7 @@ export function composerDefaultModel(agent: Agent | undefined): string {
const provider = composerCatalogProvider(agent);
if (provider === "codex" || provider === "opencode") return resolvePaperclipRunnerModel(provider, config.model);
if (provider === "acpx" && config.acpxAgent === "grok") return "grok-4.7";
if (provider === "acpx" && config.acpxAgent === "kimi") return "";
if (provider === "acpx" && config.acpxAgent !== "grok") return resolvePaperclipRunnerModel(provider, config.model);
}
return "";
@@ -0,0 +1 @@
{"provenance":{"campaign":"local-2026-10-29T13-45-22-982Z","cell":"kimi-qualification/runner-acpx-kimi/local/native-write-permission-stop/attempt-1","source":"snapshots/api-state.json","sourceSha256":"bec6f6ddcbc5a0d68ad130f33e7d8d206c29d42b8c64a8a555a46d117b1e2ed9","selection":"Assistant deltas, runtime request lifecycle, and provider turn terminal; event rows preserved without modification."},"run":{"id":"3cf126bf-b5ab-4e90-a4d6-d07c4a21ab56","status":"cancelled"},"events":[{"id":37,"companyId":"fca72f94-9495-4730-a35f-9524506819d0","runId":"3cf126bf-b5ab-4e90-a4d6-d07c4a21ab56","agentId":"e694fde7-335f-4094-8ae1-f7b2aaed4f12","seq":37,"eventType":"item.delta","stream":"system","level":"info","color":null,"message":null,"payload":{"prpEvent":{"runId":"3cf126bf-b5ab-4e90-a4d6-d07c4a21ab56","itemId":"acpx-assistant-82f602d2a9b187dc78734f41f7e466a72696f26ac5fe833f8fde19cc4aeff804","schema":"paperclip.prp.event.v1","turnId":"turn_lab_dfcc5b7f413e4171819cbc8b70249ac6","payload":{"kind":"agentMessage","text":"","update":{"kind":"agentMessage","text":"","delta":"","itemId":"acpx-assistant-82f602d2a9b187dc78734f41f7e466a72696f26ac5fe833f8fde19cc4aeff804","turnId":"turn_lab_dfcc5b7f413e4171819cbc8b70249ac6","channel":"progress","provider":"acpx","threadId":"paperclip-23b917558bef54de9b9b83414cbac5f5cc1f211f26d5e5a7c3e664ff46c3677b","providerItemId":"kimi-message-6cf6835a988c62b9ba9d4fe943d2ba7da14221c6e4e717cfeee574fae03cecab","providerMethod":"runtime.event"},"channel":"unknown","providerMethod":"item/agentMessage/delta"},"priority":1,"emittedAt":"2026-10-29T13:45:55.106Z","eventType":"item.delta","sourceSeq":10,"sourceKind":"runner","schemaVersion":1,"sourceEventId":"98c5a554-f7e8-45de-a6f5-6c373b19c18d:3cf126bf-b5ab-4e90-a4d6-d07c4a21ab56:10","sourceInstanceId":"98c5a554-f7e8-45de-a6f5-6c373b19c18d","normalizedSessionId":"91fa84b1-572b-4258-bf60-ba4e96a1e4ba"}},"sourceInstanceId":"98c5a554-f7e8-45de-a6f5-6c373b19c18d","sourceEventId":"98c5a554-f7e8-45de-a6f5-6c373b19c18d:3cf126bf-b5ab-4e90-a4d6-d07c4a21ab56:10","sourceSeq":10,"sourcePayloadSha256":"15547d7830dabf9f1060c42f50b916031ecb577e87545fbc636abf316cd15f70","protocolSchemaVersion":1,"createdAt":"2026-10-29T13:45:55.107Z"},{"id":38,"companyId":"fca72f94-9495-4730-a35f-9524506819d0","runId":"3cf126bf-b5ab-4e90-a4d6-d07c4a21ab56","agentId":"e694fde7-335f-4094-8ae1-f7b2aaed4f12","seq":38,"eventType":"item.delta","stream":"system","level":"info","color":null,"message":null,"payload":{"prpEvent":{"runId":"3cf126bf-b5ab-4e90-a4d6-d07c4a21ab56","itemId":"acpx-assistant-82f602d2a9b187dc78734f41f7e466a72696f26ac5fe833f8fde19cc4aeff804","schema":"paperclip.prp.event.v1","turnId":"turn_lab_dfcc5b7f413e4171819cbc8b70249ac6","payload":{"kind":"agentMessage","text":"I'll","update":{"kind":"agentMessage","text":"I'll","delta":"I'll","itemId":"acpx-assistant-82f602d2a9b187dc78734f41f7e466a72696f26ac5fe833f8fde19cc4aeff804","turnId":"turn_lab_dfcc5b7f413e4171819cbc8b70249ac6","channel":"progress","provider":"acpx","threadId":"paperclip-23b917558bef54de9b9b83414cbac5f5cc1f211f26d5e5a7c3e664ff46c3677b","providerItemId":"kimi-message-6cf6835a988c62b9ba9d4fe943d2ba7da14221c6e4e717cfeee574fae03cecab","providerMethod":"runtime.event"},"channel":"unknown","providerMethod":"item/agentMessage/delta"},"priority":1,"emittedAt":"2026-10-29T13:46:00.313Z","eventType":"item.delta","sourceSeq":11,"sourceKind":"runner","schemaVersion":1,"sourceEventId":"98c5a554-f7e8-45de-a6f5-6c373b19c18d:3cf126bf-b5ab-4e90-a4d6-d07c4a21ab56:11","sourceInstanceId":"98c5a554-f7e8-45de-a6f5-6c373b19c18d","normalizedSessionId":"91fa84b1-572b-4258-bf60-ba4e96a1e4ba"}},"sourceInstanceId":"98c5a554-f7e8-45de-a6f5-6c373b19c18d","sourceEventId":"98c5a554-f7e8-45de-a6f5-6c373b19c18d:3cf126bf-b5ab-4e90-a4d6-d07c4a21ab56:11","sourceSeq":11,"sourcePayloadSha256":"07a76f399becfc77a6a2cf6adbfd9ce829c254893e9077bffdfc9affae254870","protocolSchemaVersion":1,"createdAt":"2026-10-29T13:46:00.314Z"},{"id":41,"companyId":"fca72f94-9495-4730-a35f-9524506819d0","runId":"3cf126bf-b5ab-4e90-a4d6-d07c4a21ab56","agentId":"e694fde7-335f-4094-8ae1-f7b2aaed4f12","seq":41,"eventType":"item.delta","stream":"system","level":"info","color":null,"message":null,"payload":{"prpEvent":{"runId":"3cf126bf-b5ab-4e90-a4d6-d07c4a21ab56","itemId":"acpx-assistant-82f602d2a9b187dc78734f41f7e466a72696f26ac5fe833f8fde19cc4aeff804","schema":"paperclip.prp.event.v1","turnId":"turn_lab_dfcc5b7f413e4171819cbc8b70249ac6","payload":{"kind":"agentMessage","text":" use","update":{"kind":"agentMessage","text":" use","delta":" use","itemId":"acpx-assistant-82f602d2a9b187dc78734f41f7e466a72696f26ac5fe833f8fde19cc4aeff804","turnId":"turn_lab_dfcc5b7f413e4171819cbc8b70249ac6","channel":"progress","provider":"acpx","threadId":"paperclip-23b917558bef54de9b9b83414cbac5f5cc1f211f26d5e5a7c3e664ff46c3677b","providerItemId":"kimi-message-6cf6835a988c62b9ba9d4fe943d2ba7da14221c6e4e717cfeee574fae03cecab","providerMethod":"runtime.event"},"channel":"unknown","providerMethLine truncated
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
import type { HeartbeatRunEvent } from "@paperclipai/shared";
import { nativeRunEventsToTranscript } from "./native-run-events";
import { paperclipRunnerFinalResponse, settledRunChildren, transcriptToTaskChatItems } from "../task-chat/transcript-adapter";
const RUN_ID = "10000000-0000-4000-8000-000000000001";
@@ -525,6 +526,50 @@ describe("nativeRunEventsToTranscript", () => {
]);
});
it("keeps retained stopped output as progress without fabricating a final response", () => {
const fixture = JSON.parse(readFileSync(
new URL("./fixtures/stopped-native-progress.json", import.meta.url),
"utf8",
)) as { run: { id: string; status: string }; events: HeartbeatRunEvent[] };
const entries = nativeRunEventsToTranscript(fixture.events);
const parsed = transcriptToTaskChatItems(entries, { runId: fixture.run.id, running: false });
expect(fixture.run.status).toBe("cancelled");
expect(paperclipRunnerFinalResponse(parsed, { allowFallback: true })).toBeUndefined();
expect(parsed.filter((item) => item.kind === "message")).toEqual([
expect.objectContaining({
channel: "progress",
interstitial: true,
text: "I'll use the native Write tool once for the exact path, leaving the permission request pending.",
}),
]);
const children = settledRunChildren(parsed);
expect(children.filter((item) => item.kind === "activity_phase" && item.interstitial)).toHaveLength(1);
expect(children.filter((item) => item.kind === "protocol" && item.surface === "runtime_request")).toEqual([
expect.objectContaining({ status: "cancelled" }),
]);
});
it.each([
["final", "progress", "final"],
["progress", "final", "progress"],
["unknown", "final", "unknown"],
["unknown", "invalid", "unknown"],
["unknown", null, "unknown"],
[undefined, "progress", "progress"],
] as const)("preserves outer channel %s with nested channel %s", (outer, nested, expected) => {
expect(nativeRunEventsToTranscript([
itemEvent(1, "item.delta", "message-1", {
kind: "agentMessage",
...(outer === undefined ? {} : { channel: outer }),
text: "Provider message.",
update: { channel: nested },
}),
])).toEqual([
expect.objectContaining({ kind: "assistant", channel: expected }),
]);
});
it("streams deltas until a loss-resistant completed item is available", () => {
expect(nativeRunEventsToTranscript([
event(1, "item.delta", { itemId: "message-1", kind: "agentMessage", text: "Still " }),
@@ -50,7 +50,9 @@ function assistantChannel(
): "progress" | "final" | "unknown" {
const item = normalizedItem(payload);
const value = text(payload.channel) ?? text(item.channel);
if (value === "progress" || value === "final" || value === "unknown") return value;
if (value === "progress" || value === "final") return value;
if ((!value || value === "unknown") && record(payload.update)?.channel === "progress") return "progress";
if (value === "unknown") return value;
return fallback;
}
+74
View File
@@ -1014,6 +1014,80 @@ describe("New agent setup", () => {
expect(api.hire.mock.calls[0][1].adapterConfig.env[key]).toMatchObject({ type: "secret_ref", secretId: "org-secret-1" });
expect(container.querySelector('[aria-label="Thinking effort"]')).toBeNull();
});
it("offers native Kimi separately from the existing legacy Kimi under a qualified release fixture", async () => {
const profile = PAPERCLIP_RUNNER_ACPX_PROFILES.find(entry => entry.value === "kimi") as { qualified: boolean };
const previousQualified = profile.qualified;
profile.qualified = true;
try {
const setupId = "00000000-0000-4000-8000-000000000094";
state.params = new URLSearchParams({ name: "Atlas", adapterType: "process", setupAgentId: setupId });
api.get.mockResolvedValue({ id: setupId, companyId: "company-1", name: "Atlas", adapterType: "process", adapterConfig: {}, runtimeConfig: {}, metadata: { setupAdapterPending: true }, pauseReason: "setup", status: "paused" });
await renderCurrentParams();
await settle();
expect(document.querySelector('input[name="new-agent-adapter"][value="kimi_local"]')).not.toBeNull();
const native = document.querySelector<HTMLInputElement>('input[name="new-agent-adapter"][value="kimi_runtime"]');
expect(native).not.toBeNull();
await act(async () => native!.click());
await act(async () => [...document.querySelectorAll("button")].find(button => button.textContent?.trim() === "Configure agent")!.click());
await settle();
expect(api.update).toHaveBeenCalledWith(setupId, expect.objectContaining({ adapterType: "paperclip_runner", adapterConfig: { provider: "acpx", acpxAgent: "kimi" } }), "company-1");
expect(state.navigate).toHaveBeenLastCalledWith(expect.stringContaining("runnerProvider=kimi"), { replace: true });
} finally { profile.qualified = previousQualified; }
});
it("keeps an unqualified native Kimi declaration behind qualification", async () => {
const profile = PAPERCLIP_RUNNER_ACPX_PROFILES.find(entry => entry.value === "kimi") as { qualified: boolean };
const previousQualified = profile.qualified;
try {
profile.qualified = false;
await render("paperclip_runner", "kimi");
await fill("KIMI_API_KEY", "kimi-test-key");
await fill("Model", "explicit-kimi-model");
await click("Finish setup");
expect(api.testEnvironment).not.toHaveBeenCalled();
expect(api.hire).not.toHaveBeenCalled();
expect(container.textContent).toContain("Kimi Code is not enabled for production");
} finally { profile.qualified = previousQualified; }
});
it("saves and reopens native Kimi with its exact model and one organization secret under a qualified release fixture", async () => {
const profile = PAPERCLIP_RUNNER_ACPX_PROFILES.find(entry => entry.value === "kimi") as { qualified: boolean };
const previousQualified = profile.qualified;
profile.qualified = true;
try {
await render("paperclip_runner", "kimi");
expect(container.querySelector('[aria-label="Kimi API model name"]')).toBeNull();
await click("Run test");
expect(api.testEnvironment).not.toHaveBeenCalled();
expect(container.textContent).toContain("Enter the exact Kimi model ID");
await fill("Model", "explicit-unlisted-kimi-model");
await fill("KIMI_API_KEY", "kimi-test-key");
await click("Run test");
expect(api.testEnvironment).toHaveBeenLastCalledWith("company-1", "paperclip_runner", expect.objectContaining({
adapterConfig: expect.objectContaining({ provider: "acpx", acpxAgent: "kimi", model: "explicit-unlisted-kimi-model" }),
testCredentials: { KIMI_API_KEY: "kimi-test-key" },
}));
await click("Save & exit");
const setupId = api.create.mock.calls[0][1].setupId;
expect(api.update.mock.calls.at(-1)![1].adapterConfig).toMatchObject({
provider: "acpx", acpxAgent: "kimi", model: "explicit-unlisted-kimi-model",
env: { KIMI_API_KEY: { type: "secret_ref", secretId: "org-secret-1" } },
});
await act(async () => root.render(null));
cache.clear();
state.params = new URLSearchParams({ name: "Atlas", adapterType: "paperclip_runner", runnerProvider: "kimi", setupAgentId: setupId });
await renderCurrentParams();
await settle();
expect((container.querySelector('[aria-label="Model"]') as HTMLInputElement).value).toBe("explicit-unlisted-kimi-model");
expect(container.querySelector('[aria-label="KIMI_API_KEY"]')?.getAttribute("placeholder")).toBe("Using saved key");
await click("Finish setup");
expect(api.hire).toHaveBeenCalledWith("company-1", expect.objectContaining({
adapterType: "paperclip_runner", adapterConfig: expect.objectContaining({ provider: "acpx", acpxAgent: "kimi", model: "explicit-unlisted-kimi-model" }),
}));
expect(secrets.create).toHaveBeenCalledTimes(1);
} finally { profile.qualified = previousQualified; }
});
it("defines a Kimi API model without overriding it with a CLI model alias", async () => {
await render("kimi_local");
await fill("KIMI_MODEL_API_KEY", "kimi-test-key");