mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-28 12:04:41 +01:00
Fix connection controls for inherited hiring environments (#17944)
<!-- entire-trail-link-start --> https://entire.com/gh/paperclipai/-/trails/106/changes/paperclip/106 <!-- entire-trail-link-end --> ## Thinking Path > - Paperclip lets people hire agents and select execution environments. > - The name and adapter dialog saves an inactive draft before model connection. > - That draft can inherit its environment through a null defaultEnvironmentId. > - Managed-only and forced Kubernetes policy resolve that inheritance to an environment ID. > - The readiness gate compared the stored null with that ID and treated the selection as unsaved. > - This change accepts unchanged inheritance and keeps preparation and save-failure gates. > - People can select auth, open Advanced, and test a saved account before finishing setup. ## Linked Issues or Issue Description **What happened?** The Connect a model screen disabled all auth controls for a draft from the name and adapter dialog under managed-only or forced Kubernetes policy. Reloading did not recover it. The draft had no explicit environment, which is a valid inherited selection. **Expected behavior** Keep connection controls usable once the draft and environment are ready. Test the saved agent against its resolved environment before hiring finishes. Continue to block tests during preparation or an unsaved environment change. **Steps to reproduce** 1. Enable managed-only execution with a valid instance default, or force Kubernetes. 2. Open New agent, enter a name, and choose Claude or Codex. 3. Reach Connect a model with the saved draft's defaultEnvironmentId still null. 4. Try the saved subscription, API-key tile, or Advanced gateway. This follows #17761. The roadmap and related PR search found no existing fix for this readiness regression. ## What Changed - Accept the inherited selection when neither the draft nor the user specifies an environment. - Retain the existing preparation, loading, selection-save, and connection-test gates. - Cover Claude and Codex under managed plugin, managed sandbox, and forced Kubernetes policy. - Extend preparation coverage to both explicit and inherited environments. - Add the inherited-draft journey to the feature map. ## Verification - Hosted reproduction on Coree71a23c01from a macOS browser: one owned draft was created through the real name/adapter UI. Its persisted environment was null. Auth controls were disabled before and after reload. The instance required managed execution. - Cleanup used the normal Terminate action. Readback confirmed lifecycleState=terminated with no lifecycle error. The roster returned to its original two active agents. No existing agent, task, account, or instance setting was changed. - Before the fix, four inherited-connection cases and the inherited preparation case failed. The explicit-environment preparation control passed. - After the fix, 140 tests passed across NewAgent.test.tsx, test-agent-setup.test.ts, and qualify-claude-setup.test.ts. Coverage includes same-agent probes before Finish, preparation waits, account preservation, failed-save retry, stale reads, and runner qualification. - npm --prefix ui run typecheck passed, including Storybook catalog generation and tsc -b. Direct UI TypeScript, token gates, map checks, and git diff --check also passed. - The pnpm entry point stalled while resolving its pinned version on this host; both owned attempts were stopped. Running the same UI typecheck script through npm passed. Full local workspace typecheck/build were not repeated because Cargo is absent. The exact-head CI run supplies the full workspace validation. - Automated CI onf2a5a2df7passed all checks. Standard review found only the explanatory source comment, which contributor policy forbids. The comment was removed in8f43a6d80, and its thread was resolved. The runtime expression and tests are unchanged. Fresh CI passes on8f43a6d80: 55 successful checks and two intentional skips. The full workflow includes build, typecheck, all test shards, Rust, and end-to-end checks: https://github.com/paperclipai/paperclip/actions/runs/40591187819. Standard Greptile returned 5/5, then Apex returned 5/5 with zero new comments on the same head: https://github.com/paperclipai/paperclip/pull/17944#issuecomment-6454535506. No review threads remain unresolved; the PR is conflict-free. - The diff was reviewed for secrets, PII, and run artifacts. Evidence stays outside git. - No deployment, live post-fix provider test, or VM provisioning was performed. Component tests simulate API responses; the hosted check proves the original failure. ## Risks This changes only how the wizard recognizes an inherited environment. It does not change the environment resolver, lifecycle, approval policy, or lease admission. The existing preparation and failed-save guards remain active. No migration is required. Hosted acceptance of the fix requires a later authorized deployment. ## Model Used OpenAI Codex, based on GPT-6, with reasoning, tool use, browser inspection, and code execution. The exact serving model ID and context window size are not exposed. ## 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 described the issue in-PR following the bug report fields - [x] I have not referenced internal or instance-local Paperclip issues or links - [x] My branch name describes the change and contains no internal ticket id - [x] I have run the targeted tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation - [x] I have considered and documented the risks - [x] All Paperclip CI gates are green - [x] Standard Greptile and Apex are 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>
This commit is contained in:
1 parent
b5b9491f95
commit
3f8149066c
3 files changed
+44
-4
No files matched your search
@@ -99,6 +99,9 @@ For an agent-owned plugin environment, verify preparation before connecting,
|
||||
same-agent probes, account preservation after an environment change, failed
|
||||
selection-save retry, and switching back while the computer stops. Confirm the
|
||||
setup hold still blocks ordinary work and API key creation before finishing.
|
||||
Start through the name/adapter dialog with a managed-only or forced Kubernetes
|
||||
policy and a draft that inherits its environment. Once preparation completes,
|
||||
confirm the auth choices, Advanced gateway, and saved subscription remain usable.
|
||||
The [setup lifecycle tests](../../server/src/modules/agent-lifecycle/__tests__/setup.test.ts)
|
||||
exercise durable transitions with synthetic plugin resources; they do not prove
|
||||
live VM provisioning.
|
||||
|
||||
@@ -414,8 +414,8 @@ function Setup({
|
||||
);
|
||||
const needsCurrentSetup = ["codex_local", "claude_local", "opencode_local", "pi_local"].includes(adapterType);
|
||||
const preparingComputer = created?.pauseReason === "setup" && ["preparing", "pausing"].includes(created.lifecycleState ?? "");
|
||||
const environmentSelectionSaved = (created?.defaultEnvironmentId ?? null)
|
||||
=== (environmentOverride || (forced.forced || managedOnly ? environmentId : null));
|
||||
const environmentSelectionSaved = (!environmentOverride && created?.defaultEnvironmentId == null)
|
||||
|| (created?.defaultEnvironmentId ?? null) === (environmentOverride || (forced.forced || managedOnly ? environmentId : null));
|
||||
const ready = Boolean(created) && environmentSelectionSaved && !environmentSaving && !preparingComputer && connectionSettingsReady && (!needsCurrentSetup || (
|
||||
envs.isSuccess && !envs.isFetching
|
||||
&& settings.isSuccess && !settings.isFetching
|
||||
|
||||
@@ -10,6 +10,42 @@ import {
|
||||
} from "./NewAgent.test-support";
|
||||
|
||||
describe("New agent setup", () => {
|
||||
it.each([
|
||||
["claude_local", "Claude", "plugin"], ["codex_local", "OpenAI", "plugin"],
|
||||
["claude_local", "Claude", "sandbox"], ["codex_local", "OpenAI", "sandbox"],
|
||||
["claude_local", "Claude", "kubernetes"], ["codex_local", "OpenAI", "kubernetes"],
|
||||
] as const)("connects an inherited %s draft through the saved %s account under %s policy", async (adapter, provider, policy) => {
|
||||
const id = "00000000-0000-4000-8000-000000000093";
|
||||
const draft = { id, companyId: "company-1", name: "Atlas", role: "general", adapterType: "paperclip_runner",
|
||||
adapterConfig: adapter === "claude_local" ? { provider: "acpx", acpxAgent: "claude" } : { provider: "codex" },
|
||||
runtimeConfig: {}, lifecycleState: "paused", pauseReason: "setup", defaultEnvironmentId: null, status: "paused" };
|
||||
state.params = new URLSearchParams({ name: "Atlas", adapterType: adapter, runner: "auto", setupAgentId: id });
|
||||
api.get.mockResolvedValue(draft);
|
||||
api.update.mockImplementation(async (_id, patch) => ({ ...draft, ...patch }));
|
||||
settings.get.mockResolvedValue({ defaultEnvironmentId: "remote" });
|
||||
settings.getExperimental.mockResolvedValue({ enableManagedSandboxOnly: policy !== "kubernetes" });
|
||||
settings.getGeneral.mockResolvedValue({ executionMode: policy === "kubernetes" ? "kubernetes" : "any" });
|
||||
envApi.list.mockResolvedValue([{ id: "remote", name: "Remote", status: "active",
|
||||
driver: policy === "plugin" ? "plugin" : "sandbox", config: policy === "plugin" ? {} : { provider: policy === "sandbox" ? "daytona" : "kubernetes" } }]);
|
||||
managedApi.list.mockResolvedValue({ currentUserId: "user-1", connections: [{
|
||||
id: "00000000-0000-4000-8000-000000000021", grantId: "00000000-0000-4000-8000-000000000022",
|
||||
companyId: "company-1", provider: adapter === "claude_local" ? "anthropic" : "openai", method: "subscription",
|
||||
name: "Saved subscription", ownership: "shared", isDefault: false, status: "connected",
|
||||
}] } as any);
|
||||
await renderCurrentParams();
|
||||
await click("AdvancedCustom Gateway");
|
||||
expect(container.querySelector('[role="combobox"][aria-label="Connection"]')).toBeTruthy();
|
||||
await click(`${provider}API key`);
|
||||
expect(container.querySelector('[aria-label="API key"]')).toBeTruthy();
|
||||
await click(`${provider}Subscription`);
|
||||
await click("Use saved subscription");
|
||||
expect(container.textContent).toContain("Configure your agent");
|
||||
expect(api.testEnvironment).toHaveBeenCalledExactlyOnceWith("company-1", adapter, expect.objectContaining({ agentId: id, environmentId: "remote" }));
|
||||
await click("Finish setup");
|
||||
expect(api.hire).toHaveBeenCalledExactlyOnceWith("company-1", expect.objectContaining({ setupAgentId: id, defaultEnvironmentId: "remote" }));
|
||||
expect(api.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("retains the selected account while a company requires approval before preparing the computer", async () => {
|
||||
settings.get.mockResolvedValue({ defaultEnvironmentId: "computer" });
|
||||
envApi.list.mockResolvedValue([{ id: "computer", name: "Agent computer", status: "active", driver: "plugin", config: {} }]);
|
||||
@@ -92,13 +128,14 @@ describe("New agent setup", () => {
|
||||
expect(api.hire).toHaveBeenCalledWith("company-1", expect.objectContaining({ setupAgentId: id, defaultEnvironmentId: "computer" }));
|
||||
});
|
||||
|
||||
it("waits for background preparation and tests the same saved computer after it completes", async () => {
|
||||
it.each([null, "computer"])("waits for background preparation and tests the same saved computer (environment=%s)", async (defaultEnvironmentId) => {
|
||||
const id = "00000000-0000-4000-8000-000000000092";
|
||||
const draft = { id, companyId: "company-1", name: "Atlas", adapterType: "paperclip_runner", adapterConfig: { provider: "codex" },
|
||||
runtimeConfig: {}, lifecycleState: "preparing", pauseReason: "setup", defaultEnvironmentId: "computer", status: "paused" };
|
||||
runtimeConfig: {}, lifecycleState: "preparing", pauseReason: "setup", defaultEnvironmentId, status: "paused" };
|
||||
state.params = new URLSearchParams({ name: "Atlas", adapterType: "codex_local", setupAgentId: id });
|
||||
api.get.mockResolvedValue(draft);
|
||||
settings.get.mockResolvedValue({ defaultEnvironmentId: "computer" });
|
||||
settings.getExperimental.mockResolvedValue({ enableManagedSandboxOnly: true });
|
||||
envApi.list.mockResolvedValue([{ id: "computer", name: "Agent computer", status: "active", driver: "plugin", config: {} }]);
|
||||
await renderCurrentParams();
|
||||
expect(container.textContent).toContain("Preparing your agent computer");
|
||||
|
||||
Reference in new issue
Block a user