mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-26 18:31:34 +01:00
Make environments instance-scoped in DB and shared contracts
This commit is contained in:
1 parent
631b7806ed
commit
bf62f26497
12 files changed
+201
-12
No files matched your search
@@ -0,0 +1,167 @@
|
||||
ALTER TABLE "environments" ADD COLUMN "env_vars" jsonb DEFAULT '{}'::jsonb NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "instance_settings" ADD COLUMN "default_environment_id" uuid;--> statement-breakpoint
|
||||
DO $$
|
||||
BEGIN
|
||||
CREATE TEMP TABLE environment_migration_map (
|
||||
old_id uuid PRIMARY KEY,
|
||||
new_id uuid NOT NULL
|
||||
) ON COMMIT DROP;
|
||||
|
||||
WITH ranked AS (
|
||||
SELECT
|
||||
e.id,
|
||||
CASE
|
||||
WHEN e.driver = 'local' THEN '__paperclip_builtin_local__'
|
||||
WHEN e.driver = 'sandbox' AND (e.metadata ->> 'managedByPaperclip')::boolean = true
|
||||
THEN '__paperclip_managed_sandbox__'
|
||||
ELSE e.driver || ':' || e.name
|
||||
END AS group_key,
|
||||
row_number() OVER (
|
||||
PARTITION BY CASE
|
||||
WHEN e.driver = 'local' THEN '__paperclip_builtin_local__'
|
||||
WHEN e.driver = 'sandbox' AND (e.metadata ->> 'managedByPaperclip')::boolean = true
|
||||
THEN '__paperclip_managed_sandbox__'
|
||||
ELSE e.driver || ':' || e.name
|
||||
END
|
||||
ORDER BY e.created_at ASC, e.id ASC
|
||||
) AS rn
|
||||
FROM "environments" e
|
||||
),
|
||||
canonical AS (
|
||||
SELECT
|
||||
ranked.group_key,
|
||||
ranked.id AS canonical_id
|
||||
FROM ranked
|
||||
WHERE ranked.rn = 1
|
||||
)
|
||||
INSERT INTO environment_migration_map (old_id, new_id)
|
||||
SELECT ranked.id, canonical.canonical_id
|
||||
FROM ranked
|
||||
JOIN canonical ON canonical.group_key = ranked.group_key;
|
||||
|
||||
UPDATE "agents" AS agents
|
||||
SET "default_environment_id" = map.new_id
|
||||
FROM environment_migration_map AS map
|
||||
WHERE agents."default_environment_id" = map.old_id
|
||||
AND agents."default_environment_id" IS DISTINCT FROM map.new_id;
|
||||
|
||||
UPDATE "environment_leases" AS leases
|
||||
SET "environment_id" = map.new_id
|
||||
FROM environment_migration_map AS map
|
||||
WHERE leases."environment_id" = map.old_id
|
||||
AND leases."environment_id" IS DISTINCT FROM map.new_id;
|
||||
|
||||
DELETE FROM "environments" AS environments
|
||||
USING environment_migration_map AS map
|
||||
WHERE environments."id" = map.old_id
|
||||
AND map.old_id <> map.new_id;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
UPDATE "issues"
|
||||
SET "execution_workspace_settings" = "execution_workspace_settings" - 'environmentId'
|
||||
WHERE jsonb_typeof("execution_workspace_settings") = 'object'
|
||||
AND "execution_workspace_settings" ? 'environmentId';
|
||||
--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "environments_company_managed_sandbox_idx";--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "environments_company_status_idx";--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "environments_company_driver_idx";--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "environments_company_name_idx";--> statement-breakpoint
|
||||
ALTER TABLE "environments" DROP CONSTRAINT IF EXISTS "environments_company_id_companies_id_fk";--> statement-breakpoint
|
||||
ALTER TABLE "environments" DROP COLUMN "company_id";--> statement-breakpoint
|
||||
INSERT INTO "environments" (
|
||||
"id",
|
||||
"name",
|
||||
"description",
|
||||
"driver",
|
||||
"status",
|
||||
"config",
|
||||
"env_vars",
|
||||
"metadata",
|
||||
"created_at",
|
||||
"updated_at"
|
||||
)
|
||||
SELECT
|
||||
gen_random_uuid(),
|
||||
'Local',
|
||||
'Default execution environment for Paperclip runs on this machine.',
|
||||
'local',
|
||||
'active',
|
||||
'{}'::jsonb,
|
||||
'{}'::jsonb,
|
||||
'{"managedByPaperclip": true, "defaultForInstance": true}'::jsonb,
|
||||
now(),
|
||||
now()
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM "environments"
|
||||
WHERE "driver" = 'local'
|
||||
);
|
||||
--> statement-breakpoint
|
||||
UPDATE "environments"
|
||||
SET
|
||||
"metadata" = COALESCE("metadata", '{}'::jsonb) || '{"managedByPaperclip": true, "defaultForInstance": true}'::jsonb,
|
||||
"updated_at" = now()
|
||||
WHERE "driver" = 'local';
|
||||
--> statement-breakpoint
|
||||
WITH duplicate_names AS (
|
||||
SELECT
|
||||
"id",
|
||||
"name",
|
||||
"driver",
|
||||
row_number() OVER (PARTITION BY "name" ORDER BY "created_at" ASC, "id" ASC) AS rn
|
||||
FROM "environments"
|
||||
)
|
||||
UPDATE "environments" AS environments
|
||||
SET
|
||||
"name" = duplicate_names."name" || ' (' || duplicate_names."driver" || ' ' || substring(duplicate_names."id"::text from 1 for 8) || ')',
|
||||
"updated_at" = now()
|
||||
FROM duplicate_names
|
||||
WHERE environments."id" = duplicate_names."id"
|
||||
AND duplicate_names.rn > 1;
|
||||
--> statement-breakpoint
|
||||
INSERT INTO "instance_settings" (
|
||||
"id",
|
||||
"singleton_key",
|
||||
"default_environment_id",
|
||||
"general",
|
||||
"experimental",
|
||||
"created_at",
|
||||
"updated_at"
|
||||
)
|
||||
SELECT
|
||||
gen_random_uuid(),
|
||||
'default',
|
||||
local_env."id",
|
||||
'{}'::jsonb,
|
||||
'{}'::jsonb,
|
||||
now(),
|
||||
now()
|
||||
FROM (
|
||||
SELECT "id"
|
||||
FROM "environments"
|
||||
WHERE "driver" = 'local'
|
||||
ORDER BY "created_at" ASC, "id" ASC
|
||||
LIMIT 1
|
||||
) AS local_env
|
||||
ON CONFLICT ("singleton_key") DO UPDATE
|
||||
SET
|
||||
"default_environment_id" = EXCLUDED."default_environment_id",
|
||||
"updated_at" = now();
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "instance_settings"
|
||||
ADD CONSTRAINT "instance_settings_default_environment_id_environments_id_fk"
|
||||
FOREIGN KEY ("default_environment_id")
|
||||
REFERENCES "public"."environments"("id")
|
||||
ON DELETE set null
|
||||
ON UPDATE no action;
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX "environments_status_idx" ON "environments" USING btree ("status");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "environments_local_driver_idx"
|
||||
ON "environments" USING btree ("driver")
|
||||
WHERE "driver" = 'local';
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "environments_managed_sandbox_idx"
|
||||
ON "environments" USING btree ("driver")
|
||||
WHERE "driver" = 'sandbox' AND ("metadata" ->> 'managedByPaperclip')::boolean = true;
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "environments_name_idx" ON "environments" USING btree ("name");
|
||||
@@ -736,6 +736,13 @@
|
||||
"when": 1781733000000,
|
||||
"tag": "0104_issue_watchdogs",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 105,
|
||||
"version": "7",
|
||||
"when": 1781902000000,
|
||||
"tag": "0105_instance_scoped_environments",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,31 +1,30 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { index, jsonb, pgTable, text, timestamp, uniqueIndex, uuid } from "drizzle-orm/pg-core";
|
||||
import { companies } from "./companies.js";
|
||||
|
||||
export const environments = pgTable(
|
||||
"environments",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
||||
name: text("name").notNull(),
|
||||
description: text("description"),
|
||||
driver: text("driver").notNull().default("local"),
|
||||
status: text("status").notNull().default("active"),
|
||||
config: jsonb("config").$type<Record<string, unknown>>().notNull().default({}),
|
||||
envVars: jsonb("env_vars").$type<Record<string, unknown>>().notNull().default({}),
|
||||
metadata: jsonb("metadata").$type<Record<string, unknown>>(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
companyStatusIdx: index("environments_company_status_idx").on(table.companyId, table.status),
|
||||
companyDriverIdx: uniqueIndex("environments_company_driver_idx")
|
||||
.on(table.companyId, table.driver)
|
||||
statusIdx: index("environments_status_idx").on(table.status),
|
||||
localDriverIdx: uniqueIndex("environments_local_driver_idx")
|
||||
.on(table.driver)
|
||||
.where(sql`${table.driver} = 'local'`),
|
||||
companyManagedSandboxIdx: uniqueIndex("environments_company_managed_sandbox_idx")
|
||||
.on(table.companyId)
|
||||
managedSandboxIdx: uniqueIndex("environments_managed_sandbox_idx")
|
||||
.on(table.driver)
|
||||
.where(
|
||||
sql`${table.driver} = 'sandbox' AND (${table.metadata} ->> 'managedByPaperclip')::boolean = true`,
|
||||
),
|
||||
companyNameIdx: index("environments_company_name_idx").on(table.companyId, table.name),
|
||||
nameIdx: uniqueIndex("environments_name_idx").on(table.name),
|
||||
}),
|
||||
);
|
||||
@@ -1,10 +1,12 @@
|
||||
import { pgTable, uuid, text, timestamp, jsonb, uniqueIndex } from "drizzle-orm/pg-core";
|
||||
import { environments } from "./environments.js";
|
||||
|
||||
export const instanceSettings = pgTable(
|
||||
"instance_settings",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
singletonKey: text("singleton_key").notNull().default("default"),
|
||||
defaultEnvironmentId: uuid("default_environment_id").references(() => environments.id, { onDelete: "set null" }),
|
||||
general: jsonb("general").$type<Record<string, unknown>>().notNull().default({}),
|
||||
experimental: jsonb("experimental").$type<Record<string, unknown>>().notNull().default({}),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
|
||||
@@ -897,6 +897,7 @@ export {
|
||||
} from "./execution-workspace-guards.js";
|
||||
|
||||
export {
|
||||
instanceSettingsSchema,
|
||||
instanceGeneralSettingsSchema,
|
||||
patchInstanceGeneralSettingsSchema,
|
||||
type PatchInstanceGeneralSettings,
|
||||
|
||||
@@ -5,7 +5,7 @@ import type {
|
||||
EnvironmentLeaseStatus,
|
||||
EnvironmentStatus,
|
||||
} from "../constants.js";
|
||||
import type { EnvSecretRefBinding } from "./secrets.js";
|
||||
import type { AgentEnvConfig, EnvSecretRefBinding } from "./secrets.js";
|
||||
|
||||
export interface LocalEnvironmentConfig {
|
||||
[key: string]: unknown;
|
||||
@@ -56,12 +56,12 @@ export interface EnvironmentProbeResult {
|
||||
|
||||
export interface Environment {
|
||||
id: string;
|
||||
companyId: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
driver: EnvironmentDriver;
|
||||
status: EnvironmentStatus;
|
||||
config: Record<string, unknown>;
|
||||
envVars: AgentEnvConfig;
|
||||
metadata: Record<string, unknown> | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
|
||||
@@ -60,6 +60,7 @@ export interface InstanceExperimentalSettings {
|
||||
|
||||
export interface InstanceSettings {
|
||||
id: string;
|
||||
defaultEnvironmentId: string | null;
|
||||
general: InstanceGeneralSettings;
|
||||
experimental: InstanceExperimentalSettings;
|
||||
createdAt: Date;
|
||||
|
||||
@@ -162,7 +162,6 @@ export interface ProjectExecutionWorkspacePolicy {
|
||||
|
||||
export interface IssueExecutionWorkspaceSettings {
|
||||
mode?: ExecutionWorkspaceMode;
|
||||
environmentId?: string | null;
|
||||
workspaceStrategy?: ExecutionWorkspaceStrategy | null;
|
||||
workspaceRuntime?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
ENVIRONMENT_LEASE_STATUSES,
|
||||
ENVIRONMENT_STATUSES,
|
||||
} from "../constants.js";
|
||||
import { envConfigSchema } from "./secret.js";
|
||||
|
||||
export const environmentDriverSchema = z.enum(ENVIRONMENT_DRIVERS);
|
||||
export const environmentStatusSchema = z.enum(ENVIRONMENT_STATUSES);
|
||||
@@ -17,6 +18,7 @@ const environmentFields = {
|
||||
driver: environmentDriverSchema,
|
||||
status: environmentStatusSchema.optional().default("active"),
|
||||
config: z.record(z.string(), z.unknown()).optional().default({}),
|
||||
envVars: envConfigSchema.optional().default({}),
|
||||
metadata: z.record(z.string(), z.unknown()).optional().nullable(),
|
||||
};
|
||||
|
||||
@@ -29,6 +31,7 @@ export const updateEnvironmentSchema = z.object({
|
||||
driver: environmentDriverSchema.optional(),
|
||||
status: environmentStatusSchema.optional(),
|
||||
config: z.record(z.string(), z.unknown()).optional(),
|
||||
envVars: envConfigSchema.optional(),
|
||||
metadata: z.record(z.string(), z.unknown()).optional().nullable(),
|
||||
}).strict();
|
||||
export type UpdateEnvironment = z.infer<typeof updateEnvironmentSchema>;
|
||||
@@ -38,6 +41,7 @@ export const probeEnvironmentConfigSchema = z.object({
|
||||
description: z.string().optional().nullable(),
|
||||
driver: environmentDriverSchema,
|
||||
config: z.record(z.string(), z.unknown()).optional().default({}),
|
||||
envVars: envConfigSchema.optional().default({}),
|
||||
metadata: z.record(z.string(), z.unknown()).optional().nullable(),
|
||||
}).strict();
|
||||
export type ProbeEnvironmentConfig = z.infer<typeof probeEnvironmentConfigSchema>;
|
||||
@@ -1,4 +1,5 @@
|
||||
export {
|
||||
instanceSettingsSchema,
|
||||
instanceGeneralSettingsSchema,
|
||||
patchInstanceGeneralSettingsSchema,
|
||||
type InstanceGeneralSettings,
|
||||
|
||||
@@ -75,3 +75,12 @@ export type PatchInstanceExperimentalSettings = z.infer<typeof patchInstanceExpe
|
||||
export type IssueGraphLivenessAutoRecoveryRequest = z.infer<
|
||||
typeof issueGraphLivenessAutoRecoveryRequestSchema
|
||||
>;
|
||||
|
||||
export const instanceSettingsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
defaultEnvironmentId: z.string().uuid().nullable(),
|
||||
general: instanceGeneralSettingsSchema,
|
||||
experimental: instanceExperimentalSettingsSchema,
|
||||
createdAt: z.union([z.date(), z.string().datetime()]),
|
||||
updatedAt: z.union([z.date(), z.string().datetime()]),
|
||||
}).strict();
|
||||
@@ -117,7 +117,6 @@ const executionWorkspaceStrategySchema = z
|
||||
export const issueExecutionWorkspaceSettingsSchema = z
|
||||
.object({
|
||||
mode: z.enum(ISSUE_EXECUTION_WORKSPACE_PREFERENCES).optional(),
|
||||
environmentId: z.string().uuid().optional().nullable(),
|
||||
workspaceStrategy: executionWorkspaceStrategySchema.optional().nullable(),
|
||||
workspaceRuntime: z.record(z.string(), z.unknown()).optional().nullable(),
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user