feat(environments): expose boot-relevant drift attribution in the custom-image overview (#11751)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Paperclip lets operators prepare and use custom images for sandbox
environments
> - The custom-image overview detected drift but did not show which boot
source changed
> - Operators need the changed field and values to understand why a
template no longer matches
> - This pull request adds safe drift attribution to the overview API
and the out-of-sync banner
> - The benefit is faster diagnosis without exposing secrets or internal
snapshot data

## Linked Issues or Issue Description

**Subsystem affected**

Cross-cutting (server and UI).

**Problem or motivation**

The custom-image overview reported drift without identifying the changed
boot source. Operators had to inspect other data to find the cause.

**Proposed solution**

Return a classified drift summary with changed paths and their prior and
current values. Show the boot-source field in the UI banner. Keep legacy
templates and unclassified drift on the generic message.

**Alternatives considered**

The change does not expose the full snapshot or fingerprint. This keeps
the overview contract small and avoids secret disclosure.

**Roadmap alignment**

The change supports the existing custom-image environment workflow and
does not duplicate a roadmap item.

## What Changed

- Add `activeTemplateDrift` to the custom-image overview response.
- Classify drift as `boot_source_drift`, `knob_only`, or `unclassified`.
- Return drifted paths with safe `from` and `to` values.
- Show the changed boot-source field and values in the out-of-sync
banner.
- Keep legacy templates fail-closed and exclude secrets, fingerprints,
and raw snapshots.
- Add server and UI tests for the new behavior.

## Verification

- `npx vitest run
server/src/__tests__/environment-custom-images-service.test.ts` passes
with 27 tests.
- `npx vitest run ui/src/pages/CompanyEnvironments.test.tsx` passes with
27 tests.
- `pnpm --filter @paperclipai/ui exec tsc --noEmit` passes.
- Review the overview response and banner cases for boot-source,
knob-only, and legacy drift.

## Risks

The overview response gains one optional field. Legacy templates remain
compatible because they return `unclassified` and keep the generic
banner. The service excludes secret values, fingerprints, and raw
snapshots.

## Model Used

OpenAI Codex, GPT-5, tool use and code execution enabled. The model
reviewed the handoff and managed the pull request.

## 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] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [ ] I will address all Greptile and reviewer comments before
requesting merge

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Nicky LeachandPaperclip authored and GitHub committed 2026-08-19 16:36:18 -07:00
1 parent 2eb9a09c0c
commit b8a76081ec
5 files changed
+258 -5

No files matched your search

+12
View File
@@ -20,10 +20,22 @@ export interface EnvironmentCustomImageOverview {
* back to the base image until a new image is captured. `null` when unknown.
*/
activeTemplateMatchesConfig?: boolean | null;
/**
* Boot-relevant drift attribution for the active template. It names the
* classification and the drifted paths with their `from`/`to` values, so the
* banner can name the changed field. `null` or absent when there is no active
* template or the driver is not `sandbox`.
*/
activeTemplateDrift?: EnvironmentCustomImageActiveTemplateDrift | null;
activeSession: EnvironmentCustomImageSetupSession | null;
latestSession: EnvironmentCustomImageSetupSession | null;
}
export interface EnvironmentCustomImageActiveTemplateDrift {
classification: EnvironmentCustomImageRelinkClassification;
driftedPaths: EnvironmentCustomImageDriftedPath[];
}
export type EnvironmentCustomImageReconciliation =
| { action: "relinked"; template: EnvironmentCustomImageTemplate }
| { action: "detached"; template: EnvironmentCustomImageTemplate };