mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-21 08:22:20 +02:00
## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies > - Agents run inside environments (local, SSH, E2B sandbox) > - Operators need to configure and manage these environments > - But environment settings were buried inside the general company settings page, making them hard to find > - Additionally, when testing an agent from the configuration form, the test always ran locally regardless of which environment was selected > - This PR moves environments into a dedicated top-level company settings section and wires the "Test Environment" button to run inside the selected environment > - The benefit is operators can find and manage environments more easily, and the test button now validates the actual environment the agent will use ## What Changed - Added a dedicated `CompanyEnvironments` settings page with its own route and sidebar entry - Updated `CompanySettingsSidebar` and `CompanySettingsNav` to include the new environments section - Modified the agent test route (`POST /agents/:id/test`) to accept an optional `environmentId` parameter - Updated all adapter `test.ts` handlers to resolve and use the specified execution target environment - Added `resolveTestExecutionTarget` to `execution-target.ts` for remote environment test resolution with cwd fallback - Moved the "Test Environment" button and its feedback display into the `NewAgent` page footer for better UX flow ## Verification - `pnpm test` — all existing and new tests pass - `pnpm typecheck` — clean - Manual: navigate to Company Settings, confirm "Environments" appears as a top-level section - Manual: configure an agent with a non-local environment, click "Test Environment", confirm the test runs inside that environment ## Risks - Low risk. UI-only routing change for the settings page. The test-in-environment change adds an optional parameter with a local fallback, so existing behavior is preserved when no environment is specified. ## Model Used Codex GPT 5.4 high via Paperclip. ## 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 run tests locally and they pass - [x] I have added or updated tests where applicable - [x] If this change affects the UI, I have included before/after screenshots - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge
152 lines
5.5 KiB
TypeScript
152 lines
5.5 KiB
TypeScript
import { z } from "zod";
|
|
import {
|
|
AGENT_ICON_NAMES,
|
|
AGENT_ROLES,
|
|
AGENT_STATUSES,
|
|
INBOX_MINE_ISSUE_STATUS_FILTER,
|
|
} from "../constants.js";
|
|
import { agentAdapterTypeSchema } from "../adapter-type.js";
|
|
import { envConfigSchema } from "./secret.js";
|
|
|
|
export const agentPermissionsSchema = z.object({
|
|
canCreateAgents: z.boolean().optional().default(false),
|
|
});
|
|
|
|
export const agentInstructionsBundleModeSchema = z.enum(["managed", "external"]);
|
|
|
|
export const updateAgentInstructionsBundleSchema = z.object({
|
|
mode: agentInstructionsBundleModeSchema.optional(),
|
|
rootPath: z.string().trim().min(1).nullable().optional(),
|
|
entryFile: z.string().trim().min(1).optional(),
|
|
clearLegacyPromptTemplate: z.boolean().optional().default(false),
|
|
});
|
|
|
|
export type UpdateAgentInstructionsBundle = z.infer<typeof updateAgentInstructionsBundleSchema>;
|
|
|
|
export const upsertAgentInstructionsFileSchema = z.object({
|
|
path: z.string().trim().min(1),
|
|
content: z.string(),
|
|
clearLegacyPromptTemplate: z.boolean().optional().default(false),
|
|
});
|
|
|
|
export type UpsertAgentInstructionsFile = z.infer<typeof upsertAgentInstructionsFileSchema>;
|
|
|
|
const adapterConfigSchema = z.record(z.unknown()).superRefine((value, ctx) => {
|
|
const envValue = value.env;
|
|
if (envValue === undefined) return;
|
|
const parsed = envConfigSchema.safeParse(envValue);
|
|
if (!parsed.success) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: "adapterConfig.env must be a map of valid env bindings",
|
|
path: ["env"],
|
|
});
|
|
}
|
|
});
|
|
|
|
export const createAgentInstructionsBundleSchema = z.object({
|
|
entryFile: z.string().trim().min(1).optional(),
|
|
files: z.record(z.string()).refine((files) => Object.keys(files).length > 0, {
|
|
message: "instructionsBundle.files must contain at least one file",
|
|
}),
|
|
});
|
|
|
|
export const createAgentSchema = z.object({
|
|
name: z.string().min(1),
|
|
role: z.enum(AGENT_ROLES).optional().default("general"),
|
|
title: z.string().optional().nullable(),
|
|
icon: z.enum(AGENT_ICON_NAMES).optional().nullable(),
|
|
reportsTo: z.string().uuid().optional().nullable(),
|
|
capabilities: z.string().optional().nullable(),
|
|
desiredSkills: z.array(z.string().min(1)).optional(),
|
|
adapterType: agentAdapterTypeSchema,
|
|
adapterConfig: adapterConfigSchema.optional().default({}),
|
|
instructionsBundle: createAgentInstructionsBundleSchema.optional(),
|
|
runtimeConfig: z.record(z.unknown()).optional().default({}),
|
|
defaultEnvironmentId: z.string().uuid().optional().nullable(),
|
|
budgetMonthlyCents: z.number().int().nonnegative().optional().default(0),
|
|
permissions: agentPermissionsSchema.optional(),
|
|
metadata: z.record(z.unknown()).optional().nullable(),
|
|
});
|
|
|
|
export type CreateAgent = z.infer<typeof createAgentSchema>;
|
|
|
|
export const createAgentHireSchema = createAgentSchema.extend({
|
|
sourceIssueId: z.string().uuid().optional().nullable(),
|
|
sourceIssueIds: z.array(z.string().uuid()).optional(),
|
|
});
|
|
|
|
export type CreateAgentHire = z.infer<typeof createAgentHireSchema>;
|
|
|
|
export const updateAgentSchema = createAgentSchema
|
|
.omit({ permissions: true })
|
|
.partial()
|
|
.extend({
|
|
permissions: z.never().optional(),
|
|
replaceAdapterConfig: z.boolean().optional(),
|
|
status: z.enum(AGENT_STATUSES).optional(),
|
|
spentMonthlyCents: z.number().int().nonnegative().optional(),
|
|
});
|
|
|
|
export type UpdateAgent = z.infer<typeof updateAgentSchema>;
|
|
|
|
export const updateAgentInstructionsPathSchema = z.object({
|
|
path: z.string().trim().min(1).nullable(),
|
|
adapterConfigKey: z.string().trim().min(1).optional(),
|
|
});
|
|
|
|
export type UpdateAgentInstructionsPath = z.infer<typeof updateAgentInstructionsPathSchema>;
|
|
|
|
export const createAgentKeySchema = z.object({
|
|
name: z.string().min(1).default("default"),
|
|
});
|
|
|
|
export type CreateAgentKey = z.infer<typeof createAgentKeySchema>;
|
|
|
|
export const agentMineInboxQuerySchema = z.object({
|
|
userId: z.string().trim().min(1),
|
|
status: z.string().trim().min(1).optional().default(INBOX_MINE_ISSUE_STATUS_FILTER),
|
|
});
|
|
|
|
export type AgentMineInboxQuery = z.infer<typeof agentMineInboxQuerySchema>;
|
|
|
|
export const wakeAgentSchema = z.object({
|
|
source: z.enum(["timer", "assignment", "on_demand", "automation"]).optional().default("on_demand"),
|
|
triggerDetail: z.enum(["manual", "ping", "callback", "system"]).optional(),
|
|
reason: z.string().optional().nullable(),
|
|
payload: z.record(z.unknown()).optional().nullable(),
|
|
idempotencyKey: z.string().optional().nullable(),
|
|
forceFreshSession: z.preprocess(
|
|
(value) => (value === null ? undefined : value),
|
|
z.boolean().optional().default(false),
|
|
),
|
|
});
|
|
|
|
export type WakeAgent = z.infer<typeof wakeAgentSchema>;
|
|
|
|
export const resetAgentSessionSchema = z.object({
|
|
taskKey: z.string().min(1).optional().nullable(),
|
|
});
|
|
|
|
export type ResetAgentSession = z.infer<typeof resetAgentSessionSchema>;
|
|
|
|
export const testAdapterEnvironmentSchema = z.object({
|
|
adapterConfig: adapterConfigSchema.optional().default({}),
|
|
/**
|
|
* Optional environment to run the adapter test inside. When omitted, the
|
|
* test runs against the local Paperclip host. When provided and the
|
|
* environment is non-local (SSH/sandbox), the test probes are executed
|
|
* inside that environment so the result reflects real agent execution.
|
|
*/
|
|
environmentId: z.string().uuid().optional().nullable(),
|
|
});
|
|
|
|
export type TestAdapterEnvironment = z.infer<typeof testAdapterEnvironmentSchema>;
|
|
|
|
export const updateAgentPermissionsSchema = z.object({
|
|
canCreateAgents: z.boolean(),
|
|
canAssignTasks: z.boolean(),
|
|
});
|
|
|
|
export type UpdateAgentPermissions = z.infer<typeof updateAgentPermissionsSchema>;
|