mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-12 18:06:11 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Adapter Test checks whether an agent adapter can run with its configured environment, and every local-driver adapter (Claude, Codex, Gemini, OpenCode, Pi, Cursor, etc.) shares this Test route and its UI resolution logic > - The Claude ACP Test lane could report pass without checking local or remote authentication, and the shared Test route and UI had gaps in environment binding, probe safety, and managed-sandbox resolution that affect every adapter that uses the Test button, not only Claude > - This pull request verifies authentication on every Claude ACP target, and closes the shared Test-route/UI gaps: tenant-binding on the route, a managed-sandbox-only redirect that matches the real run path, and a three-tier environment resolution in the UI > - The benefit is a truthful Test result with safer probe execution and tenant isolation, for Claude specifically and for every other local adapter that shares this Test surface ## Linked Issues or Issue Description **What happened?** The Claude ACP Test lane returned `status: "pass"` without checking authentication for some local and non-sandbox targets. Separately, the shared `/companies/:companyId/adapters/:type/test-environment` route — used by every local-driver adapter, not only Claude — accepted a foreign environment id, and its UI resolution did not mirror the server's managed-sandbox-only redirect. **Expected behavior** The Test lane checks the resolved credential and hello probe for every Claude ACP target. The shared adapter Test route rejects a foreign environment before it reveals environment details or starts a lease, for any adapter type. The Test's environment resolution (UI and server) matches the real run's three-tier resolution, including the managed-sandbox-only redirect. **Steps to reproduce** 1. Run the Claude ACP Test lane against a local target without a valid credential. 2. Run the adapter Test route with an environment id from another company (any adapter type). 3. Observe the pass result on step 1, or the missing tenant-binding rejection on step 2. **Paperclip version or commit** `933749e01f74e82ce5d315c071be534d04e01158` **Deployment mode** Local dev (`pnpm dev`) and server route tests. **Agent adapter(s) involved** Claude Code directly (the ACP auth-verification work). The tenant-binding guard, managed-sandbox-only redirect, and UI three-tier resolution apply to the shared adapter Test route and affect every local-driver adapter (Codex, Gemini, OpenCode, Pi, Cursor, etc.), not only Claude — see "What Changed" below for the split between Claude-only and shared changes. **Database mode** Not database-related. **Access context** Both board and agent paths use the affected Test surface, for every local-driver adapter. **Additional context** Two commits that were previously bundled into this PR — a `plugin-worker-manager` duplex-channel frame-bound fix and a `workspace-runtime` exit-persist crash fix — are unrelated to the adapter Test lane and have been split out into their own PRs: #11860 and #11861. ## What Changed Claude-only (`packages/adapters/claude-local`): - Verify `CLAUDE_CODE_OAUTH_TOKEN` and run the hello probe for every Claude ACP target. - Keep `adapter_auth_missing` sandbox-only and report missing non-sandbox credentials as a warning. - Add a deny-by-default probe environment builder for the ACP and CLI local probes. - Log only fixed probe context and allowlisted classifications. - Seed the host OAuth token into the hello probe environment. Shared, cross-adapter (`server/src/routes/agents.ts`, `ui/src/lib/adapter-test-environment.ts`, `ui/src/components/AgentConfigForm.tsx`, `ui/src/components/OnboardingWizard.tsx`): - Add a company-binding guard and a binding assertion for the generic `/companies/:companyId/adapters/:type/test-environment` route, so a foreign-company environment id is rejected before any secret resolution or sandbox lease, for every adapter type. - Resolve all three server environment tiers (agent default, instance default, local default) in the UI, and add the managed-sandbox-only redirect so the Test probes the same target a real run would use. - Enforce onboarding Test results: block hire on a failed environment test. - Add regression tests for authentication, tenant binding, probe safety, diagnostics, and UI resolution. ## Verification - Adapter suites pass for the Claude local server probe, remote, ACP, auth, probe environment, and config paths. - Server route tests pass, including the five tenant-binding cases. - UI adapter Test environment resolver tests pass for all three resolution tiers. - Adapter package `tsc --noEmit` exits 0. - Full CI must pass on this pull request. ## Risks The probe environment now denies caller variables by default. A required variable that is not on the allowlist could stop a probe from starting. The route now rejects foreign environment ids with a fixed 403 response. The managed-sandbox-only redirect changes where the Test (and the login affordance) probes for every local-driver adapter under that policy, not only Claude — operators running other local adapters under managed-sandbox-only will see their Test target move from local to the managed sandbox, matching what real runs already do. The change limits secret and diagnostic exposure. ## Model Used Original implementation: OpenAI Codex, GPT-5; exact context window not exposed in that run; tool use and code execution. This revision (commit split and title/description correction): Claude, Sonnet 5 (claude-sonnet-5). The original title and description described this PR as Claude-only; review found it also changes the shared adapter Test route and UI resolution used by every local-driver adapter, and carried two unrelated server fixes. Claude split those two commits into #11860 and #11861 via `git rebase --onto` (verified byte-identical to the original tree minus those commits) and rewrote this description to reflect the actual scope. No functional code in this PR was authored by Claude. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
129 lines
5.8 KiB
TypeScript
129 lines
5.8 KiB
TypeScript
import type { Environment } from "@paperclipai/shared";
|
|
|
|
/**
|
|
* The managed-sandbox-only policy hides the local environment and runs every
|
|
* agent in the managed sandbox, but no managed sandbox environment is
|
|
* available. The Test resolution throws this error so the Test surfaces a
|
|
* fail-closed message, the same way a real run fails closed. The Test must not
|
|
* fall back to a local host probe, because a host probe would report a result
|
|
* for a target the real run never uses.
|
|
*/
|
|
export class ManagedSandboxUnavailableForTestError extends Error {
|
|
constructor() {
|
|
super(
|
|
"This instance runs agents only in its managed sandbox environment, but no " +
|
|
"managed sandbox environment is available to test in. Check that the managed " +
|
|
"sandbox provider is active, then retry the test.",
|
|
);
|
|
this.name = "ManagedSandboxUnavailableForTestError";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Which environment should an adapter "Test" probe?
|
|
*
|
|
* The resolution mirrors the server run-time resolution
|
|
* (`resolveExecutionWorkspaceEnvironmentId`) across all three tiers: the
|
|
* agent's own environment wins, otherwise the instance default, otherwise the
|
|
* instance local-default environment. The server always resolves a run to one
|
|
* of these three tiers, so the Test must probe the same target. Without the
|
|
* local-default tier the Test would send no environment id and probe the
|
|
* Paperclip host, even though a real run resolves to the local-default
|
|
* environment. The two paths must match, so a Test result reflects a real run.
|
|
*
|
|
* The managed-sandbox-only policy (`enableManagedSandboxOnly`) redirects a
|
|
* resolution that lands on the local environment to the managed sandbox
|
|
* environment, and it fails closed when no managed sandbox exists. This
|
|
* function mirrors that redirect. A resolution that lands on the local
|
|
* environment — the local tier, or no tier at all because the policy hides the
|
|
* local tier — resolves to the managed sandbox environment instead. With no
|
|
* managed sandbox environment the function throws, so the Test does not probe
|
|
* the local host that the real run rejects. A resolution that lands on a
|
|
* non-local environment (an agent or instance default that names an ssh or a
|
|
* user sandbox) is untouched, because the policy hides local, it does not
|
|
* forbid other environments.
|
|
*
|
|
* The policy hides the local environment from the client, so the resolved id
|
|
* can name a local row the client cannot see (an agent default that still
|
|
* points at the hidden local row). The `visibleEnvironmentIds` list closes that
|
|
* gap: under the policy a resolved id that names no visible environment is the
|
|
* hidden local row, or a stale reference, so the function redirects it to the
|
|
* managed sandbox too. Omit the list to skip this visibility check.
|
|
*/
|
|
export function resolveAdapterTestEnvironmentId(input: {
|
|
agentDefaultEnvironmentId: string | null | undefined;
|
|
instanceDefaultEnvironmentId: string | null | undefined;
|
|
localDefaultEnvironmentId: string | null | undefined;
|
|
managedSandboxOnly?: boolean;
|
|
managedSandboxEnvironmentId?: string | null | undefined;
|
|
visibleEnvironmentIds?: readonly string[] | null | undefined;
|
|
}): string | null {
|
|
const resolved =
|
|
input.agentDefaultEnvironmentId ||
|
|
input.instanceDefaultEnvironmentId ||
|
|
input.localDefaultEnvironmentId ||
|
|
null;
|
|
if (input.managedSandboxOnly !== true) {
|
|
return resolved;
|
|
}
|
|
const localDefaultId = input.localDefaultEnvironmentId || null;
|
|
const resolvedNamesVisibleEnvironment =
|
|
input.visibleEnvironmentIds == null ||
|
|
resolved === null ||
|
|
input.visibleEnvironmentIds.includes(resolved);
|
|
const landsOnLocal =
|
|
resolved === null || resolved === localDefaultId || !resolvedNamesVisibleEnvironment;
|
|
if (!landsOnLocal) {
|
|
return resolved;
|
|
}
|
|
if (!input.managedSandboxEnvironmentId) {
|
|
throw new ManagedSandboxUnavailableForTestError();
|
|
}
|
|
return input.managedSandboxEnvironmentId;
|
|
}
|
|
|
|
/**
|
|
* Find the active managed sandbox environment id in an environment list. The
|
|
* platform provisioner stamps the managed environment with
|
|
* `metadata.managedByPaperclip` (see `isPlatformManagedEnvironment`). The
|
|
* local-default environment also carries that stamp, so this function excludes
|
|
* the `local` driver and returns the non-local managed environment. The function
|
|
* selects only an `active` environment, never an `archived` one, so the Test
|
|
* resolution matches the server run-time resolver that requires an active
|
|
* managed sandbox (see `findManagedSandboxEnvironment`). The Test resolution
|
|
* uses this id as the redirect target under the managed-sandbox-only policy. The
|
|
* function returns `null` when the list holds no active managed sandbox
|
|
* environment.
|
|
*/
|
|
export function resolveManagedSandboxEnvironmentId(
|
|
environments: readonly Environment[] | null | undefined,
|
|
): string | null {
|
|
if (!environments) return null;
|
|
const managed = environments.find(
|
|
(environment) =>
|
|
environment.driver !== "local" &&
|
|
environment.status === "active" &&
|
|
environment.metadata?.managedByPaperclip === true,
|
|
);
|
|
return managed?.id ?? null;
|
|
}
|
|
|
|
/**
|
|
* Find the instance local-default environment id in an environment list. The
|
|
* server auto-creates one `local` driver environment and stamps it with
|
|
* `metadata.defaultForInstance: true` (see `ensureLocalEnvironment`). The Test
|
|
* resolution uses this id as the final tier, so it probes the same environment
|
|
* a real run resolves to when neither an agent default nor an instance default
|
|
* is set. The function returns `null` when the list holds no such environment.
|
|
*/
|
|
export function resolveLocalDefaultEnvironmentId(
|
|
environments: readonly Environment[] | null | undefined,
|
|
): string | null {
|
|
if (!environments) return null;
|
|
const local = environments.find(
|
|
(environment) =>
|
|
environment.driver === "local" && environment.metadata?.defaultForInstance === true,
|
|
);
|
|
return local?.id ?? null;
|
|
}
|