Skip to content

Commit a1c1eea

Browse files
committed
fix: restore fork-specific patches after upstream rebase
Re-apply fork customizations that failed to apply during the automated rebase of upstream d5b9f6c..d785b19: - SurfaceGuard wrapping for company settings routes (App.tsx) - Cloud billing detection in SidebarAccountMenu - Cloud company creation in CompanySwitcher - Missing imports in Layout.tsx (useFeatures, findCompanyByUrlSegment) - Brand directory support in server app - Fork-specific route and service changes - Adapter execution fork patches (credential rotation, managed instance) Work in progress — additional fork patches being applied by parallel agents.
1 parent 3904649 commit a1c1eea

16 files changed

Lines changed: 270 additions & 58 deletions

File tree

packages/adapters/codex-local/src/server/execute.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,7 @@ export async function assertCodexCredentialsLaunchable(input: {
390390
companyId: string;
391391
configuredCodexHome: string | null;
392392
configuredApiKey: string | null;
393+
configuredAuthJson?: string | null;
393394
effectiveCodexHome: string;
394395
target: MaybeResolvedExecutionTarget;
395396
cwd: string;
@@ -401,6 +402,7 @@ export async function assertCodexCredentialsLaunchable(input: {
401402
companyId: input.companyId,
402403
configuredCodexHome: input.configuredCodexHome,
403404
configuredApiKey: input.configuredApiKey,
405+
configuredAuthJson: input.configuredAuthJson,
404406
});
405407
if (!credentialReadiness.managed || credentialReadiness.ready) return;
406408

@@ -442,8 +444,9 @@ export async function assertCodexCredentialsLaunchable(input: {
442444
throw new Error(
443445
`no Codex credentials provisioned for managed home "${input.effectiveCodexHome}" ` +
444446
`(no usable auth.json and OPENAI_API_KEY is empty). ` +
445-
`Sign in to Codex on the host with a ChatGPT subscription, or configure a per-agent ` +
446-
`OPENAI_API_KEY.`,
447+
`Configure a per-agent OPENAI_API_KEY, or supply a ChatGPT-plan credential ` +
448+
`(CODEX_AUTH_JSON) minted with \`codex login\` on your own machine. On a ` +
449+
`self-hosted install, signing in to Codex on the host also works.`,
447450
);
448451
}
449452

@@ -729,6 +732,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
729732
companyId: agent.companyId,
730733
configuredCodexHome,
731734
configuredApiKey: configuredOpenAiApiKey,
735+
configuredAuthJson: configuredCodexAuthJson,
732736
effectiveCodexHome,
733737
target: executionTarget,
734738
cwd,

packages/adapters/opencode-local/src/server/execute.test.ts

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { mkdtemp, readdir, rm, mkdir } from "node:fs/promises";
12
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
23
import fs from "node:fs/promises";
34
import os from "node:os";
@@ -8,7 +9,7 @@ vi.mock("@paperclipai/adapter-utils/execution-target", async (importOriginal) =>
89
return { ...actual, runAdapterExecutionTargetProcess: vi.fn() };
910
});
1011

11-
import { ensureRemoteOpenCodeModelConfiguredAndAvailable, execute } from "./execute.js";
12+
import { buildOpenCodeSkillsDir, ensureRemoteOpenCodeModelConfiguredAndAvailable, execute } from "./execute.js";
1213
import { runAdapterExecutionTargetProcess } from "@paperclipai/adapter-utils/execution-target";
1314

1415
const runProcessMock = vi.mocked(runAdapterExecutionTargetProcess);
@@ -33,6 +34,109 @@ function probeResult(overrides: Record<string, unknown>) {
3334
} as never;
3435
}
3536

37+
describe("buildOpenCodeSkillsDir create-agent inclusion", () => {
38+
const cleanupDirs: string[] = [];
39+
40+
afterEach(async () => {
41+
while (cleanupDirs.length > 0) {
42+
const dir = cleanupDirs.pop();
43+
if (!dir) continue;
44+
await rm(dir, { recursive: true, force: true }).catch(() => undefined);
45+
}
46+
});
47+
48+
async function makeConfigWithSkills() {
49+
const root = await mkdtemp(path.join(os.tmpdir(), "paperclip-opencode-skilltest-"));
50+
cleanupDirs.push(root);
51+
const createAgentSource = path.join(root, "paperclip-create-agent");
52+
const coordinationSource = path.join(root, "paperclip");
53+
const memorySource = path.join(root, "para-memory-files");
54+
await mkdir(createAgentSource, { recursive: true });
55+
await mkdir(coordinationSource, { recursive: true });
56+
await mkdir(memorySource, { recursive: true });
57+
// Runtime skills are configured directly on the adapter config so the helper
58+
// resolves them without touching the packaged skills directory.
59+
return {
60+
paperclipRuntimeSkills: [
61+
{
62+
key: "paperclipai/paperclip/paperclip-create-agent",
63+
runtimeName: "paperclip-create-agent",
64+
source: createAgentSource,
65+
},
66+
{
67+
key: "paperclipai/paperclip/paperclip",
68+
runtimeName: "paperclip",
69+
source: coordinationSource,
70+
},
71+
{
72+
key: "paperclipai/paperclip/para-memory-files",
73+
runtimeName: "para-memory-files",
74+
source: memorySource,
75+
},
76+
],
77+
} as Record<string, unknown>;
78+
}
79+
80+
it("includes the paperclip-create-agent skill when the agent can hire", async () => {
81+
const config = await makeConfigWithSkills();
82+
const dir = await buildOpenCodeSkillsDir(config, { canCreateAgents: true });
83+
cleanupDirs.push(path.dirname(dir));
84+
const entries = await readdir(dir);
85+
expect(entries).toContain("paperclip-create-agent");
86+
});
87+
88+
it("excludes the paperclip-create-agent skill when the agent cannot hire", async () => {
89+
const config = await makeConfigWithSkills();
90+
const dir = await buildOpenCodeSkillsDir(config, { canCreateAgents: false });
91+
cleanupDirs.push(path.dirname(dir));
92+
const entries = await readdir(dir);
93+
expect(entries).not.toContain("paperclip-create-agent");
94+
});
95+
96+
// Managed agents run instruction bundles (ceo/AGENTS.md, HEARTBEAT.md) that
97+
// MANDATE the coordination (`paperclip`) and memory (`para-memory-files`)
98+
// skills. Those skills are never in a managed agent's explicit desiredSkills,
99+
// so they must be force-included whenever the agent is managed.
100+
it("includes coordination + memory + create-agent skills for a managed agent that can hire", async () => {
101+
const config = await makeConfigWithSkills();
102+
const dir = await buildOpenCodeSkillsDir(config, {
103+
canCreateAgents: true,
104+
managed: true,
105+
});
106+
cleanupDirs.push(path.dirname(dir));
107+
const entries = await readdir(dir);
108+
expect(entries).toContain("paperclip");
109+
expect(entries).toContain("para-memory-files");
110+
expect(entries).toContain("paperclip-create-agent");
111+
});
112+
113+
it("includes coordination + memory but NOT create-agent for a managed agent that cannot hire", async () => {
114+
const config = await makeConfigWithSkills();
115+
const dir = await buildOpenCodeSkillsDir(config, {
116+
canCreateAgents: false,
117+
managed: true,
118+
});
119+
cleanupDirs.push(path.dirname(dir));
120+
const entries = await readdir(dir);
121+
expect(entries).toContain("paperclip");
122+
expect(entries).toContain("para-memory-files");
123+
expect(entries).not.toContain("paperclip-create-agent");
124+
});
125+
126+
it("does NOT force coordination/memory skills on a non-managed (BYO) agent", async () => {
127+
const config = await makeConfigWithSkills();
128+
const dir = await buildOpenCodeSkillsDir(config, {
129+
canCreateAgents: false,
130+
managed: false,
131+
});
132+
cleanupDirs.push(path.dirname(dir));
133+
const entries = await readdir(dir);
134+
expect(entries).not.toContain("paperclip");
135+
expect(entries).not.toContain("para-memory-files");
136+
expect(entries).not.toContain("paperclip-create-agent");
137+
});
138+
});
139+
36140
describe("OpenCode local skill injection", () => {
37141
it("injects runtime skills into the configured child HOME", async () => {
38142
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-opencode-configured-home-"));

packages/adapters/opencode-local/src/server/execute.ts

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ import {
5959
isPaperclipSkillSourceMissing,
6060
readPaperclipRuntimeSkillEntries,
6161
readPaperclipIssueWorkModeFromContext,
62-
resolveLegacyPaperclipDesiredSkillNames,
62+
resolvePaperclipDesiredSkillNames,
6363
PAPERCLIP_CREATE_AGENT_SKILL_KEY,
6464
PAPERCLIP_COORDINATION_SKILL_KEY,
6565
PARA_MEMORY_FILES_SKILL_KEY,
@@ -224,12 +224,54 @@ async function ensureOpenCodeSkillsInjected(
224224
}
225225
}
226226

227-
async function buildOpenCodeSkillsDir(config: Record<string, unknown>): Promise<string> {
227+
/**
228+
* Managed-instruction agents (the seeded CEO + hired reports) mandate the
229+
* coordination + memory skills in their templates, so those skills must be
230+
* force-mounted for managed agents. BYO/external-instruction agents are left
231+
* untouched.
232+
*/
233+
function agentUsesManagedInstructions(agent: AdapterExecutionContext["agent"]): boolean {
234+
const adapterConfig = agent.adapterConfig;
235+
if (typeof adapterConfig !== "object" || adapterConfig === null) return false;
236+
return (adapterConfig as Record<string, unknown>).instructionsBundleMode === "managed";
237+
}
238+
239+
/**
240+
* Skills that must be mounted for this agent regardless of its explicit
241+
* desiredSkills configuration:
242+
* - An agent that can hire (`canCreateAgents`) needs the create-agent skill so
243+
* it has the hire-flow instructions in its sandbox.
244+
* - A managed agent (`managed`) is instructed by its bundle to use the
245+
* coordination (`paperclip`) and memory (`para-memory-files`) skills, so both
246+
* must be present even though managed agents carry no explicit desiredSkills.
247+
*/
248+
function alwaysIncludeSkillKeysForAgent(opts?: {
249+
canCreateAgents?: boolean;
250+
managed?: boolean;
251+
}): string[] {
252+
const keys: string[] = [];
253+
if (opts?.managed) {
254+
keys.push(PAPERCLIP_COORDINATION_SKILL_KEY, PARA_MEMORY_FILES_SKILL_KEY);
255+
}
256+
if (opts?.canCreateAgents) {
257+
keys.push(PAPERCLIP_CREATE_AGENT_SKILL_KEY);
258+
}
259+
return keys;
260+
}
261+
262+
export async function buildOpenCodeSkillsDir(
263+
config: Record<string, unknown>,
264+
opts?: { canCreateAgents?: boolean; managed?: boolean },
265+
): Promise<string> {
228266
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-opencode-skills-"));
229267
const target = path.join(tmp, "skills");
230268
await fs.mkdir(target, { recursive: true });
231269
const availableEntries = await readPaperclipRuntimeSkillEntries(config, __moduleDir);
232-
const desiredNames = new Set(resolveLegacyPaperclipDesiredSkillNames(config, availableEntries));
270+
const desiredNames = new Set(
271+
resolvePaperclipDesiredSkillNames(config, availableEntries, {
272+
alwaysIncludeSkillKeys: alwaysIncludeSkillKeysForAgent(opts),
273+
}),
274+
);
233275
for (const entry of availableEntries) {
234276
if (!desiredNames.has(entry.key)) continue;
235277
if (isPaperclipSkillSourceMissing(entry)) continue;
@@ -298,7 +340,12 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
298340
let effectiveExecutionCwd = adapterExecutionTargetRemoteCwd(executionTarget, cwd);
299341
await ensureAbsoluteDirectory(cwd, { createIfMissing: true });
300342
const openCodeSkillEntries = await readPaperclipRuntimeSkillEntries(config, __moduleDir);
301-
const desiredOpenCodeSkillNames = resolveLegacyPaperclipDesiredSkillNames(config, openCodeSkillEntries);
343+
const desiredOpenCodeSkillNames = resolvePaperclipDesiredSkillNames(config, openCodeSkillEntries, {
344+
alwaysIncludeSkillKeys: alwaysIncludeSkillKeysForAgent({
345+
canCreateAgents: Boolean(agent.permissions?.canCreateAgents),
346+
managed: agentUsesManagedInstructions(agent),
347+
}),
348+
});
302349
if (!executionTargetIsRemote) {
303350
await ensureOpenCodeSkillsInjected(
304351
onLog,

server/src/app.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -426,18 +426,17 @@ export async function createApp(
426426
deploymentExposure: opts.deploymentExposure,
427427
authReady: opts.authReady,
428428
companyDeletionEnabled: opts.companyDeletionEnabled,
429-
databaseBackupHealth: opts.databaseBackupHealth,
430429
}),
431430
);
432431
api.use(openApiRoutes());
433432
api.use("/cloud", cloudRoutes());
434433
api.use("/companies", companyRoutes(db, opts.storageService));
435434
api.use(llmRoutes(db));
436435
api.use(folderRoutes(db));
437-
api.use(companySkillRoutes(db));
436+
api.use(companySkillRoutes(db, { pluginWorkerManager: workerManager }));
438437
api.use(companySkillPolicyRoutes(db));
439438
api.use(inboxAgentPolicyRoutes(db));
440-
api.use(builtInAgentRoutes(db));
439+
api.use(builtInAgentRoutes(db, { pluginWorkerManager: workerManager }));
441440
api.use(summarySlotRoutes(db));
442441
api.use(statusCardRoutes(db));
443442
api.use(teamsCatalogRoutes(db));

server/src/index.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ import {
6060
issueService,
6161
instanceSettingsService,
6262
reconcileBuiltInAgentsOnStartup,
63+
reconcileCloudUpstreamRunsOnStartup,
6364
reconcileCodexLocalManagedHomesOnStartup,
6465
reconcilePersistedRuntimeServicesOnStartup,
6566
routineService,
@@ -942,6 +943,19 @@ export async function startServer(): Promise<StartedServer> {
942943
logger.error({ err }, "startup reconciliation of persisted runtime services failed");
943944
});
944945

946+
void reconcileCloudUpstreamRunsOnStartup(db as any)
947+
.then((result) => {
948+
if (result.reconciled > 0) {
949+
logger.warn(
950+
{ reconciled: result.reconciled },
951+
"reconciled cloud upstream runs from a previous server process",
952+
);
953+
}
954+
})
955+
.catch((err) => {
956+
logger.error({ err }, "startup reconciliation of cloud upstream runs failed");
957+
});
958+
945959
// Backfill auth.json into any already-isolated codex_local managed home that
946960
// was created by the #8272 isolation guard before the Phase 1 seeding fix.
947961
// Idempotent; the Phase 1 execute-time seeding covers new strandings.
@@ -1025,15 +1039,15 @@ export async function startServer(): Promise<StartedServer> {
10251039
logger.warn(managedEnvironmentsResult, "managed sandbox environments ensured from managed config");
10261040
}
10271041
} catch (err) {
1028-
const heartbeatMaxQueuedRunAgeMs = Math.max(
1029-
1,
1030-
Number(process.env.PAPERCLIP_HEARTBEAT_MAX_QUEUED_RUN_AGE_MS) || 24 * 60 * 60 * 1000,
1031-
);
1032-
10331042
logger.error({ err }, "failed to apply managed environments from managed config");
10341043
throw err;
10351044
}
10361045

1046+
const heartbeatMaxQueuedRunAgeMs = Math.max(
1047+
1,
1048+
Number(process.env.PAPERCLIP_HEARTBEAT_MAX_QUEUED_RUN_AGE_MS) || 24 * 60 * 60 * 1000,
1049+
);
1050+
10371051
let drainHeartbeatRunsForShutdown: ((
10381052
signal: "SIGINT" | "SIGTERM",
10391053
runIds?: readonly string[] | null,

server/src/middleware/auth.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -487,7 +487,6 @@ export function cloudActorHeaderSourceFromHeaders(
487487
const value = headers[name.toLowerCase()];
488488
return Array.isArray(value) ? value[0] : value;
489489
},
490-
cloudStack: { stackId, stackRole, ...(stackSlug ? { stackSlug } : {}) },
491490
};
492491
}
493492

@@ -709,14 +708,6 @@ function constantTimeStringEqual(left: string, right: string): boolean {
709708
return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer);
710709
}
711710

712-
function cloudTenantCompanyId(stackId: string): string {
713-
const bytes = createHash("sha256").update(`paperclip-cloud-tenant-company:${stackId}`).digest();
714-
bytes[6] = (bytes[6] & 0x0f) | 0x50;
715-
bytes[8] = (bytes[8] & 0x3f) | 0x80;
716-
const hex = bytes.subarray(0, 16).toString("hex");
717-
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
718-
}
719-
720711
export function humanizeCloudStackSlug(stackId: string): string {
721712
const slug = stackId
722713
.trim()

server/src/routes/agents.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@ import {
7373
collectAgentAdapterWorkspaceCommandPaths,
7474
} from "./workspace-command-authz.js";
7575
import type { PluginWorkerManager } from "../services/plugin-worker-manager.js";
76-
listServerAdapters,
7776
import { environmentService } from "../services/environments.js";
7877
import { resolveEnvironmentExecutionTarget } from "../services/environment-execution-target.js";
7978
import { environmentRuntimeService } from "../services/environment-runtime.js";
@@ -84,7 +83,6 @@ import type {
8483
AdapterEnvironmentTestResult,
8584
AdapterModelProfileDefinition,
8685
} from "@paperclipai/adapter-utils";
87-
import { getDisabledAdapterTypes } from "../services/adapter-plugin-store.js";
8886
import { skillVersionSelectionMap } from "../services/runtime-skill-selections.js";
8987
import { secretService } from "../services/secrets.js";
9088
import { authorizationDeniedDetails } from "../services/authorization.js";

server/src/routes/companies.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,14 @@ import {
2424
feedbackVoteValueSchema,
2525
hidesCompanyPage,
2626
updateCompanyBrandingSchema,
27+
updateCompanySchema,
28+
} from "@paperclipai/shared";
2729
import {
2830
canCreateStackCompany,
2931
cloudTenantCompanyId,
3032
isCompanyIdConflict,
3133
withCloudStackSlugAlias,
3234
} from "../services/cloud-tenant-company.js";
33-
updateCompanySchema,
34-
} from "@paperclipai/shared";
3535
import {
3636
COMPANY_IMPORT_TRANSFERS_ROUTE_PATH,
3737
companyImportTransferDeclarationSchema,

server/src/routes/health.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -264,23 +264,14 @@ export function healthRoutes(
264264
})
265265
: null;
266266

267-
const databaseBackup = opts.databaseBackupHealth
268-
? inspectDatabaseBackupHealth(opts.databaseBackupHealth)
269-
: undefined;
270-
const warnings = databaseBackup?.warnings.length ? databaseBackup.warnings : undefined;
271-
272267
if (!exposeFullDetails) {
273-
const redactedDatabaseBackup = databaseBackup ? redactedDatabaseBackupHealth(databaseBackup) : undefined;
274-
const redactedWarnings = redactedDatabaseBackup?.warnings.length ? redactedDatabaseBackup.warnings : undefined;
275268
res.json({
276269
status: "ok",
277270
deploymentMode: opts.deploymentMode,
278271
deploymentExposure: opts.deploymentExposure,
279272
commit,
280273
bootstrapStatus,
281274
bootstrapInviteActive,
282-
...(redactedDatabaseBackup ? { databaseBackup: redactedDatabaseBackup } : {}),
283-
...(redactedWarnings ? { warnings: redactedWarnings } : {}),
284275
...(devServer ? { devServer } : {}),
285276
// Token-authorized probe on an otherwise redacted response: the control
286277
// plane needs readiness without a board session, and nothing else about

0 commit comments

Comments
 (0)