Files
PaperClipAI/packages/db/src/schema/environment_custom_image_setup_sessions.ts
T
Devin FoleyandPaperclip b4815bf964 Scope environment custom images to instance environments (#8850)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Environments are now managed as instance-level runtime resources
rather than per-company rows
> - The custom environment image setup tables were introduced with their
own `company_id` columns and route query parameters
> - That split made one saved environment image state depend on an extra
company context even though the environment itself is the durable owner
> - It also made saved-environment probes harder because applying the
active custom image template could require a company context when no
secret-backed config needed one
> - This pull request scopes custom image templates and setup sessions
directly to the saved environment
> - The benefit is that reusable environment images follow the same
instance-scoped model as environments while secret resolution still uses
company context only when secrets require it

## Linked Issues or Issue Description

No matching public GitHub issue was found.

Bug report:

### What happened?

saved environment custom-image routes and persistence required a
`companyId` even though environments are instance-scoped, and saved
sandbox probes did not opt into active custom-image template application
unless a company context was present.

### Expected behavior

custom-image templates and setup sessions should be owned by the saved
environment, and saved sandbox probes should apply the active template
while still requiring a company context only for secret-backed runtime
config.

### Steps to reproduce

1. Configure an instance-scoped sandbox environment with custom-image
setup support.
2. Start or inspect a custom-image session or template for that saved
environment.
3. Probe the saved environment without a custom-image-specific
`companyId` query parameter.

### Paperclip version or commit

current `master` after the environment custom-image template migration.

### Deployment mode

Local dev (pnpm dev) or authenticated local Paperclip instance.

### Installation method

Built from source (pnpm dev / pnpm build).

### Agent adapter(s) involved

Not adapter-specific (core bug).

### Database mode

Embedded PGlite/Postgres dev database.

### Access context

Board human operator.

### Privacy checklist

No logs, secrets, tokens, private URLs, or local machine paths are
included.

Duplicate search performed:

- `gh search prs "environment custom image companyId
repo:paperclipai/paperclip" --state open --limit 20`
- `gh search prs "custom image environment scoped
repo:paperclipai/paperclip" --state open --limit 20`
- `gh search issues "environment custom image
repo:paperclipai/paperclip" --state open --limit 20`

The returned results were unrelated adapter, Docker, auth, or
stale-workspace items.

## What Changed

- Removed redundant `company_id` columns from environment custom-image
templates and setup sessions.
- Added migration `0127_environment_custom_images_instance_scoped` to
collapse duplicate active rows per environment before dropping the old
company-scoped indexes/columns.
- Updated custom-image services, route handlers, shared validators, and
UI API/query keys to use environment-scoped custom-image state.
- Kept runtime secret resolution company-aware only when secret refs or
bindings require a company context.
- Made saved sandbox environment probes opt into active custom-image
template application.
- Updated DB, shared, server, and UI tests for the new
environment-scoped contract.

## Verification

- `pnpm --filter @paperclipai/db run check:migrations`
- `pnpm exec vitest run
packages/db/src/environment-custom-images-schema.test.ts
packages/shared/src/environment-custom-images.test.ts
server/src/__tests__/environment-custom-image-routes.test.ts
server/src/__tests__/environment-custom-images-service.test.ts
server/src/__tests__/environment-routes.test.ts
ui/src/pages/CompanyEnvironments.test.tsx`
- `pnpm -r typecheck`
- `pnpm test:run` before rebasing onto latest `master`; after the rebase
only the migration number changed, and the migration check plus focused
suite, typecheck, and build were rerun.
- `pnpm build`

## Risks

- Migration safety: the migration supersedes duplicate active templates
per environment and fails duplicate active setup sessions before adding
environment-only unique indexes. Operators with duplicate historical
active rows should review which active template is kept.
- Behavior shift: plugin custom-image setup calls now receive
`companyId: "instance"` when no secret binding determines a concrete
company context.
- Secret-backed configs still require an explicit or uniquely inferable
company context; environments with secret bindings spread across
multiple companies continue to fail fast.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

OpenAI Codex via the `codex_local` adapter, GPT-5-based coding model
with tool-enabled repository inspection, editing, testing, git, and
GitHub CLI access. Exact context-window metadata was not exposed by the
runtime.

## 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
- [ ] All Paperclip CI gates are green
- [ ] 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>
2026-07-01 23:57:31 -07:00

54 lines
3.0 KiB
TypeScript

import { sql } from "drizzle-orm";
import { index, jsonb, pgTable, text, timestamp, uniqueIndex, uuid } from "drizzle-orm/pg-core";
import type {
EnvironmentCustomImageSetupConnectionSummary,
EnvironmentCustomImageSetupSessionStatus,
} from "@paperclipai/shared";
import { agents } from "./agents.js";
import { environmentLeases } from "./environment_leases.js";
import { environmentCustomImageTemplates } from "./environment_custom_image_templates.js";
import { environments } from "./environments.js";
export const environmentCustomImageSetupSessions = pgTable(
"environment_custom_image_setup_sessions",
{
id: uuid("id").primaryKey().defaultRandom(),
environmentId: uuid("environment_id").notNull().references(() => environments.id, { onDelete: "cascade" }),
templateId: uuid("template_id").references(() => environmentCustomImageTemplates.id, { onDelete: "set null" }),
promotedTemplateId: uuid("promoted_template_id")
.references(() => environmentCustomImageTemplates.id, { onDelete: "set null" }),
provider: text("provider").notNull(),
providerLeaseId: text("provider_lease_id"),
environmentLeaseId: uuid("environment_lease_id").references(() => environmentLeases.id, { onDelete: "set null" }),
status: text("status").$type<EnvironmentCustomImageSetupSessionStatus>().notNull().default("starting"),
startedByUserId: text("started_by_user_id"),
startedByAgentId: uuid("started_by_agent_id").references(() => agents.id, { onDelete: "set null" }),
baseTemplateRef: text("base_template_ref"),
expiresAt: timestamp("expires_at", { withTimezone: true }),
finishedAt: timestamp("finished_at", { withTimezone: true }),
failureReason: text("failure_reason"),
connectionSummary: jsonb("connection_summary").$type<EnvironmentCustomImageSetupConnectionSummary | null>(),
connectionSecretRef: text("connection_secret_ref"),
metadata: jsonb("metadata").$type<Record<string, unknown>>(),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
},
(table) => ({
environmentStatusIdx: index("environment_custom_image_setup_sessions_environment_status_idx").on(
table.environmentId,
table.status,
),
environmentActiveUq: uniqueIndex("environment_custom_image_setup_sessions_environment_active_uq")
.on(table.environmentId)
.where(sql`${table.status} IN ('starting', 'waiting_for_user', 'capturing')`),
templateIdx: index("environment_custom_image_setup_sessions_template_idx").on(table.templateId),
promotedTemplateIdx: index("environment_custom_image_setup_sessions_promoted_template_idx")
.on(table.promotedTemplateId),
expiresIdx: index("environment_custom_image_setup_sessions_expires_idx").on(table.expiresAt),
providerLeaseIdx: index("environment_custom_image_setup_sessions_provider_lease_idx").on(
table.provider,
table.providerLeaseId,
),
}),
);