Commit Graph

1341 Commits

Author SHA1 Message Date
Dotta 9c1f8e7887
feat(decisions): add first-class propose mode (#10010)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents can currently perform many mutations directly, while humans
often need a durable review point before cross-issue or destructive
actions occur
> - Existing approvals and issue-thread interactions do not provide a
standalone, reusable object for presenting options, collecting typed
inputs, detecting stale targets, and auditing effect execution
> - The control plane therefore needs a first-class propose mode that
separates an agent's recommendation from the governed mutation it may
cause
> - This pull request adds Decisions v1 across the database, shared
contracts, server execution and telemetry, agent skill guidance, and
operator UI
> - The benefit is that agents can propose multi-option actions safely
while operators get explicit provenance, fail-closed execution,
per-effect results, and a focused attention workflow

## Linked Issues or Issue Description

### Subsystem affected

Cross-cutting: `packages/db`, `packages/shared`, `server`, and `ui`.

### Problem or motivation

Agents need a governed way to propose consequential work without
immediately mutating issues, especially when one choice can affect
several issue trees. Existing approvals and issue-thread interactions do
not provide a standalone object with typed options, target snapshots,
effect-level authorization, expiration, execution outcomes, and reusable
attention-feed presentation.

### Proposed solution

Add first-class Decisions that store options and typed inputs, surface
open proposals in the operator attention feed, validate target freshness
and the origin-agent/operator authorization intersection at decision
time, execute a bounded set of auditable effects, and retain terminal
outcomes. Decisions v1 supports comments, status and assignee changes,
follow-up issue creation, blocker resolution, and issue-tree
cancellation, plus bundle grouping, expiration/dismissal, rule-key
telemetry, and agent-facing API guidance.

### Alternatives considered

- Extend approvals with arbitrary effects: rejected because approvals
represent governed yes/no actions and would become an unsafe generic
mutation envelope.
- Model every proposal as an issue-thread interaction: rejected because
decisions can span several targets and need independent lifecycle,
telemetry, idempotency, and effect results.
- Let agents perform the mutation and ask for retrospective review:
rejected because it removes the pre-execution governance boundary this
feature is meant to provide.

### Roadmap alignment

Aligns with `ROADMAP.md` sections **Agent Reviews and Approvals**,
**Enforced Outcomes**, **MCP Tool Gateway & Apps (governed tool
access)**, and **Activity History** by making explicit decisions,
authorization gates, auditable execution, and terminal outcomes
first-class control-plane objects.

### Additional context

This does not replace existing approvals or issue-thread interactions,
and it does not add an unrestricted generic mutation effect.

## What Changed

- Added company-scoped decision, option, target, and effect-execution
schema plus migration and shared TypeScript/Zod contracts.
- Added decision routes and services for propose, list/get, decide,
dismiss, cancel, target freshness checks, authorization intersection,
idempotency, activity logging, and execution auditing.
- Added rule-key decision telemetry and attention-feed metadata so open
decisions are visible and measurable.
- Added agent skill documentation for proposing and resolving decisions
through the Paperclip API.
- Added the Decisions UI: API client, query keys, inline attention
resolver, bundle grouping, target-issue strip, terminal history,
destructive confirmation, and per-effect result rendering.
- Added server service coverage, DecisionCard state tests, and Storybook
stories for the supported visual states.

## Verification

- `pnpm -r typecheck` — passed.
- `pnpm test:run` — 2,876 passed, 1 skipped, with one unrelated
cross-suite cleanup-order failure in
`heartbeat-responsible-user-invariant.test.ts`; the failing file passes
in isolation (`6/6`).
- `pnpm --filter @paperclipai/server exec vitest run
src/__tests__/heartbeat-responsible-user-invariant.test.ts` — passed.
- `pnpm --filter @paperclipai/ui exec vitest run
src/components/DecisionCard.test.tsx` — passed (`9/9`).
- `pnpm --filter @paperclipai/server exec vitest run
src/__tests__/authz-existence-oracle-guard.test.ts
src/__tests__/openapi-routes.test.ts` — passed (`5/5`).
- `pnpm --filter @paperclipai/server exec vitest run
src/__tests__/decisions-service.test.ts` — passed (`16/16`).
- `pnpm --filter paperclipai exec vitest run
src/__tests__/company-import-export-e2e.test.ts` — passed (`1/1`).
- `pnpm --filter @paperclipai/server typecheck` and `pnpm --filter
paperclipai typecheck` — passed.
- `pnpm build` — passed.
- Rebased-head focused suite — passed (`6` files, `88` tests): shared
decision contracts, Decisions service, OpenAPI routes, startup feedback
export, DecisionCard states, and attention helpers. The follow-up
stale-secondary-target regression passes in the DecisionCard suite
(`10/10`).
- Rebased-head scoped typechecks — passed for `@paperclipai/shared`,
`@paperclipai/db`, `@paperclipai/server`, and `@paperclipai/ui`.
- Rebased-head migration numbering and safety checks — passed after
renumbering the additive migration to `0193` and making it replay-safe
for environments that applied the earlier feature-branch number.
- `pnpm check:token-gates` — passed with all gates clean.
- GitHub PR workflow and Greptile review for
`1f9f7645882d05dfdd9c99377c03a1f53f20e8be` — running after the
stale-secondary-target fix and PR metadata refresh on July 27, 2026.
- `pnpm --filter @paperclipai/ui build-storybook` exposes an existing
Storybook version mismatch (`storybook` 10.4.6 vs
`@storybook/addon-docs` 10.5.0); Decisions stories were validated with
the docs addon temporarily disabled and the tracked config remains
unchanged.

## Risks

- **Migration:** Adds replay-safe migration `0193`; migration numbering
and safety checks pass. The new tables and indexes are additive.
- **Authorization:** Effect execution intersects the proposing agent's
permissions with the responsible user context and fails closed; mistakes
could reject a valid proposal rather than silently over-authorize it.
- **Concurrency:** Target snapshots and idempotency keys protect against
stale or duplicate execution, but reviewers should focus on mixed-effect
partial outcomes and retry behavior.
- **UI:** Decisions are integrated into the existing attention feed
rather than a separate navigation surface, reducing routing risk but
increasing the importance of attention-item metadata compatibility.

> 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 CLI using `gpt-5.6-sol` for final PR preparation, review
fixes, and verification; repository tools and code execution were
enabled, and context-window size is not exposed in this runtime.
- Anthropic Claude Opus 4.8 with 1M context assisted with the Decisions
UI implementation, as recorded in the relevant commits.

## 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
- [x] All Paperclip CI gates are green
- [x] 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>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-31 19:17:02 -07:00
Dotta 71231dfa38
feat(audit): agent audit UI — company page + per-agent tab (#9744)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Operators need an audit record of agent actions across tasks,
comments, documents, approvals, and runs
> - The permission-gated audit read API provides that record, but
operators cannot inspect it in the product
> - A readable UI must preserve company boundaries, server-side
permission decisions, and redaction
> - Audit exports must also be safe to open in spreadsheet software and
must record the export itself
> - This pull request adds company and per-agent audit views plus a
guarded CSV export
> - The benefit is a searchable, filterable, and reviewable agent action
history with direct links back to work

## Linked Issues or Issue Description

**Feature.** This change adds the frontend and CSV export for the agent
action audit log.

Refs #9731 and #9735.

- Problem: agent actions are recorded, but operators have no readable
product surface to inspect or export them.
- Solution: add a company audit page and a per-agent Audit tab that use
the permission-gated audit API.
- Alternative: build a separate plugin-only surface. This was rejected
because the existing permission model already supports a unified,
server-authoritative view.

This pull request targets the audit epic branch, which contains the
merged #9735 audit API.

## What Changed

- Added a company Audit page and sidebar entry.
- Added a per-agent Audit tab with a fixed agent filter.
- Added filters for agent, responsible user, action domain, entity type,
and date range.
- Added task and run links, responsible-user context, cursor pagination,
and readable action text.
- Added a permission-denied Enterprise card for callers without
`audit:view_agent_actions`.
- Added a CSV export that is permission-gated, capped, self-audited,
CSV-escaped, and protected against spreadsheet formula injection.
- Preserved the merged audit API cursor validation, redaction, and
sub-millisecond pagination behavior.

## Verification

- `pnpm exec vitest run ui/src/pages/audit/AuditFeed.test.tsx` — 6
passed.
- `pnpm --filter @paperclipai/server exec vitest run
src/__tests__/agent-action-audit-routes.test.ts` — 8 passed with
embedded PostgreSQL.
- `pnpm -r typecheck` — passed across all workspaces.
- `pnpm build` — passed across all workspaces.
- `pnpm test:run` — all completed shards passed except one
environment-sensitive CLI assertion caused by injected static AWS
credential variables; the exact test passes 8/8 with those variables
unset.
- Manual Chromium QA exercised the populated feed, active filters,
permission-denied card, per-agent tab, and CSV export.

## Screenshots and Manual QA

- [All audit states exercised in
Chromium](https://github.com/paperclipai/paperclip/pull/9744#issuecomment-4998997001)
- [Detailed browser report and per-agent tab root
cause](https://github.com/paperclipai/paperclip/pull/9744#issuecomment-4998771061)

The per-agent redirect defect found during QA is fixed in this branch.

## Risks

Low to moderate risk. The UI and export route are additive and use the
existing company-scoped permission gate. The main risks are large
exports and spreadsheet interpretation. The export is capped at 10,000
rows, records truncation accurately, and prefixes formula-like cells as
text. There are no schema changes or migrations.

> 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

- Anthropic Claude Opus 4.8, 1M context, extended thinking, tool use,
and code execution produced the original implementation.
- OpenAI Codex, GPT-5 (deployment ID and context window not exposed),
reasoning, tool use, code execution, browser-test orchestration, and
GitHub review tooling repaired and verified 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)
- [ ] 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
- [x] All Paperclip CI gates are green
- [x] 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>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-31 19:09:13 -07:00
Dotta a29b10510c
fix(server): bound inherited workspace runtime services (#10589)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Execution workspaces can inherit runtime services from a project
workspace
> - A project workspace keeps current and historical runtime service
rows
> - The execution workspace read path returned all current rows,
including services removed from the current configuration
> - This pull request matches inherited rows to the current service
definitions
> - The benefit is bounded workspace payloads and accurate service
summaries

## Linked Issues or Issue Description

**What happened?**

Shared execution workspaces returned current historical service rows
that no longer matched the project workspace configuration. The response
size multiplied across every shared execution workspace.

**Expected behavior**

Shared execution workspaces must return only the newest runtime service
row for each service in the current project workspace configuration.

**Steps to reproduce**

1. Create one project workspace with many historical runtime service
rows.
2. Create many shared execution workspaces that inherit that project
workspace.
3. List the execution workspaces and inspect each `runtimeServices`
array.

**Paperclip version or commit**

`7301fae942c3d5826974335cb40d6f1e0d95d1e0`

**Deployment mode**

Built from source. The defect is in the server read model and is not
deployment-specific.

No duplicate or related public issue or pull request was found.

## What Changed

- Select only runtime service rows that match the current project
workspace service definitions.
- Preserve each matched service definition index in the API result.
- Avoid loading direct execution service rows for workspaces that
inherit project services.
- Add unit, integration, and volume regression coverage.

## Verification

- `pnpm --dir server exec vitest run
src/services/workspace-runtime-read-model.test.ts
src/__tests__/execution-workspaces-service.test.ts -t
'selectConfiguredRuntimeServiceRows|returns full details at the observed
volume|inherits only runtime-service rows'`
- `pnpm --filter @paperclipai/server typecheck`

The focused test run passed 4 tests and skipped 27 unrelated tests.

## Risks

The read path now omits service rows that do not match the current
configuration. This is the intended behavior for inherited runtime
services. The change does not alter service persistence or lifecycle
transitions.

> 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 with model ID `gpt-5`. The context-window size is not
exposed to this run. The run used reasoning, repository tools, code
execution, and GitHub tools.

## 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
- [x] All Paperclip CI gates are green
- [x] 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-31 18:54:11 -07:00
Dotta 2e4774ac90
fix(adapter-utils): correct confirmation wake semantics (#10588)
<!-- Write all pull request text in Simplified Technical English
(ASD-STE100): short sentences, one instruction per sentence, simple
approved vocabulary, and the active voice. -->

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Local agent wakes include a default execution contract
> - That contract tells agents how issue-thread continuation policies
behave
> - The current text says `wake_assignee` resumes a confirmation only
after acceptance
> - The server actually wakes for every non-expired resolution and
reserves acceptance-only behavior for `wake_assignee_on_accept`
> - This pull request makes the default prompt match the server contract
and strengthens the recovery follow-up regression case
> - The benefit is that agents choose the correct continuation policy
and recovery tests cover normalized agent name keys

## Linked Issues or Issue Description

Related work: Refs #5473, Refs #5060, and Refs #10562.

**What happened?**

The default local-agent prompt described `wake_assignee` as
acceptance-only for `request_confirmation`. This conflicts with the
server. The server wakes on every non-expired resolution. A recovery
follow-up test also used an already-normalized execution agent name key,
so it did not exercise the normalization seam.

**Expected behavior**

The prompt must state that `wake_assignee` resumes after acceptance or
rejection. It must direct acceptance-only flows to
`wake_assignee_on_accept`. The recovery regression must use a
display-style agent name key and prove that the follow-up path still
works after normalization.

**Steps to reproduce**

1. Read the default local-agent prompt in
`packages/adapter-utils/src/server-utils.ts`.
2. Compare its confirmation continuation text with
`queueResolvedInteractionContinuationWakeup` in
`server/src/routes/issues.ts`.
3. Observe that the prompt gives acceptance-only semantics to
`wake_assignee`.
4. Inspect the recovery hand-back test and observe that its execution
name key is already normalized.

**Paperclip version or commit**

`7301fae942`

**Deployment mode**

Local dev. The prompt and test behavior are not deployment-specific.

## What Changed

- Corrected the default agent prompt for `wake_assignee` and
`wake_assignee_on_accept`.
- Added focused prompt assertions for both the required and obsolete
text.
- Changed the recovery follow-up fixture to use a display-style agent
name key.

## Verification

- `pnpm exec vitest run packages/adapter-utils/src/server-utils.test.ts
-t 'keeps the default local-agent prompt action-oriented'` passed: 1
test.
- `pnpm exec vitest run
server/src/__tests__/heartbeat-comment-wake-batching.test.ts -t 'defers
recovery hand-back wakes until the resolving run exits'` passed: 1 test.
- `pnpm --filter @paperclipai/adapter-utils typecheck` passed.
- `pnpm --filter @paperclipai/server typecheck` passed.
- `git diff --check origin/master...HEAD` passed.

## Risks

- Low risk. The production change updates prompt text only.
- Agents that followed the old text may now choose
`wake_assignee_on_accept` for acceptance-only flows.
- The server test change only broadens an existing regression fixture.

> 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 with GPT-5. The exact serving model ID and context-window
size are not exposed to the agent. The model used reasoning, repository
tools, tests, Git, and GitHub CLI access.

## 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 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
- [x] All Paperclip CI gates are green
- [x] 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-31 18:53:53 -07:00
Dotta 627728bdde
feat: add authoritative issue PATCH receipts (#10478)
<!-- Write all pull request text in Simplified Technical English
(ASD-STE100): short sentences, one instruction per sentence, simple
approved vocabulary, and the active voice. -->

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Agents update tasks through the issue API.
> - The update response did not state which values changed.
> - Blocker updates also did not echo the scalar blocker IDs.
> - Agents therefore used an extra GET request to confirm a successful
write.
> - This pull request adds an authoritative change receipt and an
optional small response.
> - The benefit is fewer API calls with a clear and compatible write
contract.

## Linked Issues or Issue Description

No public GitHub issue exists for this change.

### Subsystem affected

Cross-cutting: `server/`, `packages/shared`, and the UI issue cache.

### Problem or motivation

A successful issue PATCH returned the updated issue, but it did not
identify the effective changes. Blocker writes returned relation
summaries without the scalar IDs. Agents could not distinguish a
confirmed clear operation from missing data. The response must confirm
committed field and blocker changes while existing UI clients continue
to receive the full issue by default.

### Proposed solution

Add a `changes` receipt. Add a conditional `blockedByIssueIds` echo.
Support `Prefer: return=minimal`. Keep the full response as the default.

### Alternatives considered

Make the small response the default for agent tokens. This would create
different response contracts by actor type, so this pull request does
not use that design.

### Roadmap alignment

This is a focused control-plane reliability improvement. It does not
duplicate an open roadmap milestone.

## What Changed

- Compute committed issue row and relation changes in the issue service.
- Omit no-op fields and truncate changed long text values to 200
characters.
- Echo blocker ID arrays for blocker set and clear requests.
- Add the opt-in `Prefer: return=minimal` response and
`Preference-Applied` header.
- Keep receipt metadata out of React Query issue caches.
- Add route and embedded Postgres tests for the new contract.

## Verification

- `pnpm exec vitest run
server/src/__tests__/issue-activity-events-routes.test.ts`
- `pnpm exec vitest run server/src/__tests__/issues-service.test.ts -t
"returns authoritative update receipts for row fields and blocker
relations"`
- `pnpm --filter @paperclipai/shared typecheck`
- `pnpm --filter @paperclipai/server typecheck`
- `pnpm --filter @paperclipai/ui typecheck`
- `pnpm check:token-gates`
- `git diff --check`

## Risks

- Low compatibility risk. The default response only adds receipt fields.
- Minimal mode is opt-in. Existing clients do not receive a smaller
body.
- The receipt excludes `updatedAt` because the response already returns
it as the freshness anchor.
- Prose API and agent workflow guidance will follow after the server
contract is available.

> 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 based on GPT-5. The exact deployment ID, context window
size, and reasoning mode are not exposed to the agent. The agent used
repository tools, code execution, and test execution.

## 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
- [x] All Paperclip CI gates are green
- [x] 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-31 18:52:59 -07:00
Dotta fc5a30805e
feat(cli): add managed install, update, and service lifecycle (#10045)
## Thinking Path

> - Paperclip is the open source control plane people use to manage
AI-agent companies
> - Operators need a predictable installation path that survives beyond
an ephemeral `npx` process
> - A durable installation needs an owned per-user payload store, stable
command shim, safe shell integration, and supported service lifecycle
> - Updates must preserve recoverability by backing up data, installing
side-by-side, verifying the new payload, and retaining rollback state
> - Bootstrap scripts and privileged service operations must fail closed
across download, filesystem, ownership, and consent boundaries
> - This pull request integrates managed install, update, rollback,
service, uninstall, doctor, bootstrap-installer, and runtime-serving
support into one workflow
> - The benefit is a recoverable, inspectable, and documented
installation lifecycle with explicit safety boundaries across Linux,
macOS, containers, WSL, npm, npx, and source checkouts

## Linked Issues or Issue Description

### Problem

Paperclip lacks a first-class durable installation and lifecycle
workflow. Operators currently have to assemble npm/npx installation,
PATH setup, background-service management, updates, rollback,
diagnostics, and uninstall behavior themselves. That makes upgrades
harder to recover, creates inconsistent behavior across platforms, and
leaves shell/download/service trust boundaries without one documented
implementation.

### Proposed Solution

Add a managed per-user install store and stable shim, a verified shell
bootstrap installer, service lifecycle commands, install-mode-aware
update/rollback behavior, doctor checks, and documentation. Managed
updates back up the database, install and smoke-test a side-by-side
payload, atomically switch `current`, and retain prior payloads. The
shell installer pins registry/download trust boundaries and requires
explicit consent for non-interactive privileged actions.

### Alternatives Considered

- Keep recommending `npx`: simple for evaluation, but ephemeral and
unsuitable for stable services, atomic updates, or rollback.
- Require global npm installation only: familiar, but cannot provide the
owned side-by-side payload store and retained rollback semantics.
- Split the capability across multiple PRs: rejected because install,
update, service, uninstall, bootstrap, and serving behavior share
contracts and security boundaries that need review together.

### Related Pull Requests

- Supersedes #10042 and #10044 with one integrated final diff.
- Incorporates and replaces the closed preparatory work in #10032 and
#10034.

## What Changed

- Added `paperclipai install`, `update`/`upgrade`, rollback, uninstall,
service lifecycle, onboarding integration, and managed-install doctor
checks.
- Added a private managed payload store, verified manifest/marker
ownership, exclusive mutation locks, atomic manifest/current/shim
writes, retained previous payloads, and provenance validation.
- Added npm and GitHub-ref install sources with exact target resolution,
registry isolation, database backup, side-by-side verification, atomic
activation, service restart coordination, and failure rollback.
- Made managed-update backups report actionable service-start and
`--no-backup` recovery guidance for unreachable databases, while clean
never-onboarded instances skip an empty backup.
- Added systemd user and launchd service definitions, status/health/log
commands, single-instance coordination, stale-port recovery, and
explicit sudo/lingering consent handling.
- Added the `scripts/install.sh` bootstrap path with checked two-stage
downloads, pinned public npm registry usage, platform checks,
dry-run/non-interactive controls, and Docker fixtures.
- Added embedded Postgres/native bootstrap integration,
hot-restart/systemd-notify serving support, passive update notices,
configuration contracts, README/CLI/install documentation, and focused
regression tests.
- Security re-review should explicitly re-verify: (1)
`addManagedPathBlock`/`removeManagedPathBlock` reject symlinked or
non-regular rc files, assert current-user ownership, preserve
restrictive modes, and replace atomically; (2) managed shim replacement
rejects unsafe parents, foreign-owned or multiply linked files, and uses
checked atomic replacement; (3) the shell installer and sudo path
preserve explicit consent and checked downloads; and (4) installed
service/runtime serving remains bound to the validated managed shim and
instance configuration.

## Verification

- `bash -n scripts/install.sh scripts/clean-install-git.sh
scripts/clean-install-npm.sh scripts/test-install-sh-docker.sh`
- `pnpm exec vitest run cli/src/__tests__/install-store.test.ts
cli/src/__tests__/install-command.test.ts
cli/src/__tests__/managed-install-check.test.ts
cli/src/__tests__/onboard-service.test.ts
cli/src/__tests__/service-health-check.test.ts
cli/src/__tests__/service-manager.test.ts
cli/src/__tests__/update-command.test.ts
cli/src/__tests__/update-notice.test.ts
packages/db/src/embedded-postgres-native.test.ts` — 9 files, 66 tests
passed
- `pnpm --dir cli typecheck`
- `pnpm --dir cli build`
- Follow-up verification: `pnpm exec vitest run
cli/src/__tests__/update-command.test.ts` (14/14), `pnpm --dir cli
typecheck`, `pnpm --dir cli build`, and `pnpm --filter
@paperclipai/server typecheck`.
- `pnpm -r typecheck`
- `pnpm build`
- Full `pnpm test:run` exercised all suites; an injected static AWS
credential changed one unrelated doctor expectation, which passed when
those credentials were removed. A second run cleared that case and
exposed stale pre-existing adapter-utils `dist` output; rebuilding
`@paperclipai/adapter-utils` made the isolated test pass. The updated PR
CI is the authoritative clean-workspace full-suite run.

## Risks

- Installer/update code writes executable shims, symlinks, shell rc
blocks, service definitions, and managed payloads; ownership,
regular-file, symlink, hard-link, marker, and path-containment checks
fail closed before destructive changes.
- The bootstrap installer executes downloaded tooling; downloads are
staged and checked before execution, npm traffic is pinned to the public
registry, and non-interactive privileged behavior requires explicit
consent.
- Linux lingering may invoke `sudo`; the command is surfaced and
confirmed before execution, and unsupported service managers fall back
to foreground-run guidance.
- Database migrations remain forward-only; payload rollback does not
reverse migrations, so managed updates create a backup before activation
unless explicitly disabled.
- Service restart and runtime serving touch process/port ownership;
lifecycle locks, health/version checks, and stable-shim service
definitions reduce split-brain and stale-process risk.

> 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 coding agents using GPT-5.5 and GPT-5.6-sol, with
reasoning, repository/API access, shell execution, and test tooling. The
runtime did not expose a reliable context-window size.

## 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
- [x] All Paperclip CI gates are green
- [x] 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>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 18:52:23 -07:00
Tonio 492555aaf9
design(decisions): flatten decision cards to two task-borrowed types (#10474)
The Decisions queue ran five parallel colour/icon vocabularies chosen by
source kind, plus a separate severity badge, so two rows needing the same
response could look unrelated and none of it matched the task list.

Every row now resolves to one of two kinds, each borrowing the task status
it corresponds to: blocking renders as `blocked`, review as `in_review`,
both through StatusGlyph and the existing --status-task-icon-* tokens.
Source kinds keep their own wording; only colour and icon merge.

Card anatomy follows the design mock: no left accent rail, rounded cards
16px apart, a "/"-separated meta breadcrumb, a named See more / See less
control, and no separately tinted drawer when expanded. Verb order is
fixed across both states. Severity moves from chrome to a toolbar filter.

Four defects fixed along the way:
- blocked rows reported themselves as their own blocker (server-side)
- the task key was missing wherever the row's subject IS the task
- the task quicklook stuck open, because closing handed focus back to a
  trigger that opens on focus
- the card ring appeared on click, and only on cards with a toggle

Also: the standard task preview is aligned to its trigger's text and
scales out of it, the task eyebrow renders its project as a tile, and the
first motion tokens land alongside the disclosure and crossfade.

Supersedes #9574 and #9575.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 18:43:40 -07:00
Devin Foley c0b875c46c
fix(codex): let sandbox runs use the sandbox image's own Codex login (#10582)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Codex agents can run inside sandbox environments, and operators can
bake a Codex login into the sandbox image during interactive image setup
> - Two credential gates (the control plane's pre-dispatch
configuration-incomplete gate and the adapter's execute-time fail-fast)
required host-side Codex credentials — a usable `auth.json` in the
managed home or a configured `OPENAI_API_KEY` — regardless of where the
run executes
> - On managed cloud hosts a local Codex login never exists, so every
sandbox run of a Codex agent failed immediately with "configuration
incomplete: no Codex credentials available for managed home …", even
though the adapter's inbound auth merge already supports the image-login
case end to end
> - This pull request makes the execute-time gate probe the sandbox for
its own `~/.codex/auth.json` before failing, and exempts
sandbox-destined runs from the pre-dispatch host check
> - The benefit is that a sandbox image signed in to Codex is a
first-class credential source, matching what the auth-merge,
precedence-warning, and copy-back machinery were already built for

## Linked Issues or Issue Description

**What happened?**

Running a `codex_local` agent in a sandbox environment whose image
carries a Codex login failed instantly with `configuration incomplete:
no Codex credentials available for managed home "…/codex-home". Sign in
to Codex on the host with a ChatGPT subscription, or bind a per-agent
OPENAI_API_KEY secret for this agent.` The host has no Codex login and
never will on a managed cloud deployment; the sandbox's own login was
never consulted.

**Steps to reproduce**

1. Configure a sandbox environment and capture a custom image after
signing in to Codex inside the interactive image setup.
2. Create a `codex_local` agent that uses that environment, on a host
with no Codex login and no `OPENAI_API_KEY` bound.
3. Start a run: it fails pre-dispatch with the configuration-incomplete
blocker above.

**Expected behavior**

The run launches and Codex authenticates with the sandbox image's own
login, the same way the adapter's host↔sandbox auth merge already keeps
the sandbox credential when the host ships none. A run should only fail
fast when neither the host, a bound `OPENAI_API_KEY`, nor the sandbox
has credentials.

**Paperclip version**

Current `master` (cloud image deployments).

**Deployment mode**

Managed cloud stacks (any deployment where the server host has no local
Codex login).

## What Changed

- Extracted the adapter's execute-time gate into
`assertCodexCredentialsLaunchable`: when host readiness fails and the
target is a sandbox, it probes `~/.codex/auth.json` in the sandbox (same
command the auth-precedence warning uses) and proceeds with a log line
naming the credential source; when the sandbox has no login either, the
error now names all three remediation options (sandbox image sign-in,
per-agent `OPENAI_API_KEY`, host sign-in). Non-sandbox targets keep
today's strict behavior byte-for-byte.
- The control plane's pre-dispatch gate in
`resolveExecutionRunAdapterConfig` now takes the selected environment's
driver and skips the host-credential check for sandbox-destined runs —
only the adapter can probe the sandbox once it is up, so the
execute-time gate is the authority there. Non-sandbox runs keep the
early, well-attributed configuration-incomplete blocker.
- The codex Test flow needed no change: it already seeds host
credentials only when they exist and otherwise leaves the sandbox's
`CODEX_HOME` alone; this aligns the run path with it.

## Verification

- `cd packages/adapters/codex-local && pnpm vitest run` — 210 tests,
including new gate cases: sandbox login present (proceeds + logs
source), sandbox and host both credential-less (fails with the extended
message), non-sandbox target (strict host requirement kept, no sandbox
probe), per-agent API key (no probe at all).
- `cd server && pnpm vitest run
src/__tests__/heartbeat-project-env.test.ts
src/__tests__/codex-local-adapter-environment.test.ts` — includes the
new sandbox-exemption case next to the existing blocker tests.
- `pnpm run typecheck` in `server` and `packages/adapters/codex-local`.

## Risks

- Sandbox-destined misconfigurations (no credentials anywhere) now
surface at adapter execute time instead of pre-dispatch, so they read as
an adapter failure with a precise message rather than a
configuration-incomplete blocker. The trade-off is deliberate: the
sandbox must be up to know whether credentials exist, and the failure
message names the exact remediations.
- The sandbox probe adds one short (5s-capped) shell command to sandbox
runs whose host has no credentials; runs with host credentials or a
bound key are untouched.
- Self-hosted behavior is unchanged for local and SSH targets.

## Model Used

Claude Fable 5 (`claude-fable-5`, Anthropic) via Claude Code — extended
thinking, agentic tool use (file edits, vitest/tsc runs). No other
models involved.

## 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
2026-07-31 18:36:36 -07:00
Dotta 7301fae942
fix(heartbeat): atomically claim due timer intervals (#10584)
<!-- Write all pull request text in Simplified Technical English
(ASD-STE100): short sentences, one instruction per sentence, simple
approved vocabulary, and the active voice. -->

## Thinking Path

> - Paperclip coordinates AI agents through scheduled heartbeat runs.
> - The heartbeat scheduler can call `tickTimers()` again before an
earlier tick has finished.
> - Each overlapping tick can read the same old `lastHeartbeatAt` value
and decide that the same agent is due.
> - The existing queue checks do not make that due-time decision atomic.
> - This pull request atomically advances the timer baseline before it
enqueues the wake.
> - The benefit is that one timer interval can create at most one
scheduled run for an agent.

## Linked Issues or Issue Description

No public GitHub issue describes this exact scheduler race. Related pull
requests address active-run overlap or queued-run buildup, but they do
not atomically claim a due timer interval: #9457, #8416, and #3858.

**What happened?**

Two overlapping calls to `tickTimers()` could both read the same due
timer baseline. Both calls could enqueue a timer run for the same agent
and interval.

**Expected behavior**

Only one scheduler tick must claim a due timer interval. A second
overlapping tick must observe that the interval was already claimed and
skip it.

**Steps to reproduce**

1. Create an active agent with a 60-second timer interval.
2. Set `lastHeartbeatAt` to more than 60 seconds in the past.
3. Call `tickTimers(now)` twice with `Promise.all()`.
4. Observe that the old code can enqueue two runs for the same interval.

**Paperclip version or commit**

Reproduced on `master` before this branch.

**Deployment mode**

Local development with embedded PostgreSQL.

## What Changed

- Added an atomic conditional update that claims a due timer interval by
advancing `lastHeartbeatAt`.
- Made `tickTimers()` enqueue only after that conditional update
succeeds.
- Preserved first-heartbeat telemetry when the timer claim advances
`lastHeartbeatAt` before run completion.
- Added regression tests for concurrent claims and first-heartbeat
telemetry.

## Verification

- `pnpm --filter @paperclipai/server exec vitest run
src/__tests__/heartbeat-stale-queue-invalidation.test.ts` — 24 tests
passed on the final rebased commit.
- `pnpm --filter @paperclipai/server exec vitest run
src/__tests__/heartbeat-process-recovery.test.ts -t "preserves
first-heartbeat telemetry after a timer interval claim|tracks the first
heartbeat with the agent role"` — 2 tests passed.
- `pnpm --filter @paperclipai/server typecheck` — passed after the
review fix.
- `pnpm -r typecheck` — passed.
- `pnpm build` — passed.
- `pnpm test:run` — 3,121 tests passed and 2 tests skipped. One
unrelated runtime-skills test exceeded its 5-second limit under
full-suite load.
- `pnpm --filter @paperclipai/server exec vitest run
src/__tests__/heartbeat-runtime-skills.test.ts` — the timed-out file
passed in isolation, 2 tests passed.
- `pnpm --filter @paperclipai/db exec vitest run
src/status-card-migrations.test.ts` — the unrelated CI timeout passed in
isolation.
- The full PR CI matrix passed after one rerun of that unrelated
timeout.
- Greptile passed with zero new comments and no unresolved review
threads.

## Risks

- Low risk. The change only affects due timer claims.
- If enqueue fails after the claim, the next timer attempt waits for one
interval. This is safer than duplicate agent execution.
- No schema, migration, API, UI, or dependency changes are included.

> 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 based on GPT-5. The exact deployment ID and
context-window size are not exposed to this runtime. Agentic reasoning,
shell tools, code execution, and GitHub operations were enabled.

## 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
- [x] All Paperclip CI gates are green
- [x] 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-31 17:30:41 -07:00
Devin Foley 90ead239a8
feat(ui/server): name cross-company environment secret refs instead of calling them missing (#10577)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Environment configs (sandbox providers, SSH) can bind stored company
secrets through `format: "secret-ref"` fields, picked in the environment
editor's secret picker
> - Environments are instance-scoped and shared by every company on an
instance, but the picker lists only the current company's secrets, so a
ref pointing at another company's secret renders as "Missing secret (…)"
in destructive styling
> - That state is indistinguishable from a genuinely deleted secret, so
operators "fix" a healthy binding by creating a duplicate secret in
their own company — the exact sequence that used to corrupt bindings
before #10576
> - This pull request adds an instance-gated metadata endpoint for an
environment's secret refs and teaches the picker to name a cross-company
secret and its owner honestly
> - The benefit is that operators can tell a healthy cross-company
binding from a broken one, and stop creating duplicate secrets

## Linked Issues or Issue Description

**Is your feature request related to a problem? Please describe.**

In the environment editor, a secret-ref field that points at a secret
owned by a different company shows "Missing secret (22095402…)" in red,
with "The previously selected secret is no longer available. Pick
another or remove the binding." The binding is actually healthy — the
current company's picker just cannot list the other company's secrets.
Operators react by creating a duplicate secret and re-pointing the
field.

**Describe the solution you'd like**

The editor should know the referenced secret's name, status, and owning
company (metadata only, never the value) and present a cross-company ref
neutrally, a deleted secret as deleted, and only an unknown id as
missing.

Related: #10576 (fixes the binding corruption this UI state used to
trigger).

## What Changed

- New `GET /environments/:id/secret-refs` returns `{ refs: [{
configPath, secretId, name, status, companyId, companyName }] }` for the
environment's config-derived secret refs. Values are never returned. The
route sits behind `assertCanAccessInstanceEnvironments`, the same gate
as environment editing.
- New `secretService.describeSecretRefs` loads that metadata across
companies; unknown ids are omitted.
- `SecretBindingPicker` reads an optional `SecretRefHintsContext` (keyed
by secret id). With a hint, a ref the company list cannot show renders
as `NAME — Owning Company` with neutral styling and the note "Owned by
the … company. The binding keeps working; selecting a secret from this
list re-points it here." A hint with `status: "deleted"` reports the
secret as deleted. Without hints, behavior is byte-identical to before —
agent editors and other picker users are unaffected.
- `CompanyEnvironments` fetches descriptors for the environment being
edited and provides them through the context.

## Verification

- `cd server && pnpm vitest run src/__tests__/environment-routes.test.ts
src/__tests__/secrets-service.test.ts` — new endpoint happy path, agent
403 (descriptors never computed), and embedded-Postgres coverage proving
cross-company names resolve and unknown ids drop out.
- `cd ui && pnpm vitest run src/components/SecretBindingPicker.test.tsx
src/components/JsonSchemaForm.test.tsx
src/pages/CompanyEnvironments.test.tsx` — hinted cross-company
rendering, hinted deleted secret, and unchanged no-hint fallback.
- `pnpm run typecheck` in `server` and `ui`.
- Manual: edit an environment whose secret-ref field references another
company's secret; the field names the secret and its owning company
instead of "Missing secret".

## Risks

- The endpoint exposes secret names and company names across companies
to instance-level environment editors. Those actors already manage
instance-shared environments (and instance admins are implicit members
of every company), so this reveals no secret material and no new reach;
the service method documents that callers must sit behind an
instance-level gate.
- UI change is additive and context-gated; pickers without a provider
render exactly as before.

## Model Used

Claude Fable 5 (`claude-fable-5`, Anthropic) via Claude Code — extended
thinking, agentic tool use (file edits, vitest/tsc runs). No other
models involved.

## 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
2026-07-31 16:46:26 -07:00
Devin Foley f51cba33fa
fix(server): keep environment secret bindings consistent when re-pointing config secrets (#10576)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents can run inside environments (SSH boxes, sandbox providers); a
sandbox environment's config can reference stored company secrets (for
example a provider API key) through `format: "secret-ref"` fields
> - Environments are instance-scoped and shared by every company on an
instance, but `company_secret_bindings` rows are company-scoped, and the
environment routes synced config-derived bindings under one guessed
"context company" resolved from the environment's existing bindings
> - When a save re-pointed a secret-ref field at a secret owned by a
different company, the binding sync threw after the config row had
already been persisted: the config referenced the new secret, the
binding still pointed at the old one, every later lease acquisition
failed with `Secret is not bound to environment:<id> at apiKey`, and the
stale cross-company binding made every later save fail with a
company-context conflict — with no route-level way to recover
> - This pull request makes config-derived bindings follow the company
that owns each referenced secret, and makes the environment write and
its binding syncs atomic
> - The benefit is that environment saves can no longer strand an
environment in a half-updated state that breaks all of its runs

## Linked Issues or Issue Description

Refs #10577 (companion UX change: the editor state that nudges operators
into this sequence).

**What happened?**

Saving an environment whose secret-ref config field points at a secret
owned by a different company than the environment's existing binding
partially applied: the config row updated, the binding sync failed
server-side, and the environment was left referencing a secret it has no
binding for. Every run that leased the environment then failed with
`lease_acquire_failed: ... Secret is not bound to environment:<id> at
apiKey`, and every later save of the environment returned 409
`Environment secret bindings already use a different company context.` —
with no route-level way to recover.

**Steps to reproduce**

1. On an instance with two companies, create a sandbox environment from
company A with a picker-bound API-key secret owned by A (the binding
lands in A).
2. From company B, create a new secret and re-point the environment's
API-key field at it, then save.
3. The save persists the config but the binding sync throws, so no
binding for B's secret exists.
4. Run any agent that uses the environment, or try to save the
environment again.

**Expected behavior**

The save either fully applies (config and bindings consistent) or fully
fails. Re-pointing a config secret ref to a secret owned by another
company moves the binding with the secret.

**Paperclip version**

Reproduced on current `master` (also present on recent release images).

**Deployment mode**

Multi-company server deployment (any mode with more than one company on
the instance).

## What Changed

- New `secretService.replaceSecretRefsForInstanceTarget`: writes each
config-derived binding under the company that owns the referenced
secret, replaces all non-`env.*` bindings of the target across every
company, and validates every ref (secret exists, not deleted,
config-path and projection-class rules) before any row is written.
`env.*` env-var bindings stay company-scoped and untouched.
- The environment create and update routes now run the environment write
and its binding syncs inside one `db.transaction`, threading the
transaction through new optional executor seams on
`environmentService.create/update` and the existing `SecretBindingDb`
seam pattern, so an invalid ref rolls the whole save back instead of
leaving a half-updated environment.
- `resolveEnvironmentSecretContextCompanyId` no longer lets existing
bindings veto the caller's context (the 409s above); it now only picks
where new raw-pasted secrets are created and how env-var bindings and
probes resolve: explicit route/query company first, then the single
company the bindings live in, then the actor's company.

## Verification

- `cd server && pnpm vitest run src/__tests__/environment-routes.test.ts
src/__tests__/environment-instance-routes.test.ts
src/__tests__/secrets-service.test.ts
src/__tests__/environment-custom-image-routes.test.ts` (165 tests,
includes new coverage below)
- New embedded-Postgres tests prove: a re-point moves the binding to the
new secret's company and deletes the stale row; refs across several
companies each bind under their own secret's company; an unknown secret
ref rejects without touching existing bindings; `env.*` rows survive
config-ref replacement.
- New route tests prove: a cross-company re-point that previously 409'd
now saves, with the update and binding replacement on the same
transaction executor; a failing ref surfaces as 422.
- `cd server && pnpm run typecheck`

## Risks

- Behavioral shift: environment saves no longer 409 on a company-context
mismatch between the caller and existing bindings; bindings follow the
referenced secret's company instead. Environment routes are
instance-admin gated, and instance admins already had access to every
company's secrets by passing the company explicitly, so this removes an
ordering trap rather than widening access.
- Runtime lease resolution is unchanged: a run still resolves
environment secrets under the run's own company, so an environment
referencing company B's secret still only leases for company B runs
(fail-closed as before).
- The delete route's per-company binding cleanup is unchanged.

## Model Used

Claude Fable 5 (`claude-fable-5`, Anthropic) via Claude Code — extended
thinking, agentic tool use (file edits, vitest/tsc runs). No other
models involved.

## 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
2026-07-31 16:19:25 -07:00
Dotta 32c1a8576c
fix(server): self-heal execution workspaces whose recorded branch no longer exists (#10578)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Each agent task gets an execution workspace (a git worktree) with a
recorded branch name; workspace validation compares that record to the
worktree before every run
> - Agents sometimes rename their task branch (for example to a `feat/*`
PR branch), so the recorded branch never existed or was deleted
> - Validation then fails every run with "expected branch does not
exist" — a deterministic `workspace_validation_failed` loop with no
self-heal path
> - A recorded branch with no resolvable commit has nothing to lose, so
adopting a clean, registered checked-out branch is trivially
forward-only
> - This pull request routes that exact case through the existing
audited forward-reconciliation path, in both the runtime and the manual
board reconcile endpoint
> - The benefit is that these stranded workspaces heal themselves while
dirty worktrees, detached HEADs, unregistered paths, and ambiguous git
states all stay fail-closed

## Linked Issues or Issue Description

No public GitHub issue exists; the underlying bug is described here per
`bug_report.yml`. Related PR: #10574 self-heals the sibling provisioning
failure loop uncovered by the same incident diagnosis.

**What happened?**
An execution workspace whose recorded branch was renamed away failed
every subsequent run with `workspace_validation_failed` ("expected
branch does not exist"). The safe-repair matrix refused the case, so the
task stayed blocked until a human intervened.

**Expected behavior**
When the recorded branch is confirmed absent and the worktree is clean
and registered with its checked-out branch matching HEAD, Paperclip
adopts the checked-out branch through the audited forward-reconciliation
path and the next run proceeds.

**Steps to reproduce**
In an isolated workspace, rename the task branch (`git branch -m
<recorded> feat/something`) or delete the recorded branch, leave the
worktree clean, then start a new run on the task. Validation fails on
every retry.

**Paperclip version or commit**
master as of the branch point of this PR.

**Deployment mode**
Local trusted deployment with git-worktree isolated workspaces.

## What Changed

- `ensureGitWorktreeBranchCoherent` (workspace runtime): a missing
recorded branch with a clean worktree, an existing checked-out branch,
and a registered branch matching HEAD now goes through audited forward
reconciliation instead of failing closed. Gated behind
`enableWorkspaceBranchReconcileForward`.
- `reconcileExecutionWorkspaceBranch` mode `forward` (service): accepts
the same case so the board reconcile endpoint can repair it manually.
- The service inspection now classifies each branch ref as `resolved` /
`missing` / `error` (`git rev-parse --verify --quiet`, distinguishing an
absent ref from git failing to inspect the repo). Adoption requires a
confirmed-missing recorded ref **and** a resolved target ref, so a git
error can never bypass ancestry validation and a nonexistent branch name
is never persisted.
- Removed the test that asserted this case fails closed; it is
superseded by tests that assert the new behavior.
- New tests: successful adoption, dirty-worktree refusal, refusal when
the checked-out branch ref does not resolve either, and disabled-flag
behavior.

## Verification

- `server`: `npx vitest run
src/__tests__/execution-workspaces-service.test.ts -t "reconcil"` — 11
passed.
- `server`: `npx vitest run src/__tests__/workspace-runtime.test.ts -t
"adopt"` — 7 passed.
- `npx tsc --noEmit` in `server/` is clean.
- Manually validated the underlying repair on a live stranded workspace
before automating it: creating the recorded branch at the clean HEAD
ended the validation-failure loop without touching the agent's PR
branch.

## Risks

- The change relaxes a fail-closed gate, so the main risk is
over-adoption. Mitigations: the exception requires flag-on, clean
worktree, registered worktree path, registered branch matching HEAD, a
confirmed-missing (not merely unreadable) recorded ref, and a resolvable
target ref; everything else still fails closed. Every adoption goes
through the audited reconcile path with an issue comment trail.
- No migrations, no API surface changes (the reconcile route returns the
same hand-picked fields).

## Model Used

Claude Fable 5 (Anthropic, model ID `claude-fable-5`), extended
thinking, agentic tool use (Claude Code harness).

## 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: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-07-31 16:19:13 -07:00
Dotta 1ee1275f11
fix(adapters): persist ACPX process identity for hot restart (#9838)
## Thinking Path

> - Paperclip is the open source control plane people use to manage AI
agents for work.
> - Local agent heartbeats need durable process identity so the server
can supervise them.
> - The ACPX runtime owns the child process used by `codex_local`
sessions.
> - ACPX did not expose the child PID and start time to the Paperclip
adapter.
> - Warm ACPX runtimes can also serve a later heartbeat without a new
spawn event.
> - A hot restart could therefore classify a live Codex run as lost
because its heartbeat row had no process identity.
> - This pull request forwards ACPX spawn identity, reuses it for
compatible warm heartbeats, and fails closed when identity cannot be
persisted.
> - The benefit is reliable hot-restart adoption for eligible local
Codex runs.

## Linked Issues or Issue Description

No matching public GitHub issue was found.

**What happened?**

A `codex_local` heartbeat could run through ACPX without a persisted
`processPid` or `processStartedAt`. A Paperclip hot restart then had no
durable identity for the live ACP child. Recovery could classify the run
as `process_lost` even while the child was still alive.

**Expected behavior**

ACPX reports the real child PID and start time before the first prompt.
A compatible warm runtime reports the same known identity to each later
heartbeat that reuses the child. ACPX stops the child if the identity is
invalid or persistence fails. Hot-restart recovery can then adopt the
live run.

**Steps to reproduce**

1. Start a `codex_local` heartbeat through the ACPX execution lane.
2. Keep the run active during a Paperclip hot restart.
3. Inspect the heartbeat row before this change.
4. Observe that the process identity can be null and recovery cannot
adopt the live child.

**Reproduced on**

- Paperclip `master` before this change.
- Linux source deployment.
- `codex_local` with ACPX `0.12.0`.

## What Changed

- Add an awaited `onAgentSpawn` lifecycle hook to the patched ACPX
runtime.
- Forward the ACP child PID and start time through the adapter `onSpawn`
callback.
- Keep a mutable callback sink for cached runtimes so a later respawn
updates the current heartbeat.
- Reuse the last known process identity when a compatible warm heartbeat
reuses the existing child.
- Kill the ACP child and fail session startup when the PID is invalid or
identity persistence rejects.
- Add ACPX and heartbeat recovery tests for callback ordering, warm
reuse, failure cleanup, durable row identity, and hot-restart adoption.
- Document the one-time drain required when an installed pre-fix run
already lacks process metadata.

## Verification

-
`PAPERCLIP_HOME="$PAPERCLIP_RUN_SCRATCH_DIR/test-home-execute-escalated"
pnpm exec vitest run
packages/adapter-utils/src/acpx-engine/execute.test.ts` — 89 passed.
-
`PAPERCLIP_HOME="$PAPERCLIP_RUN_SCRATCH_DIR/test-home-recovery-escalated"
pnpm exec vitest run
server/src/__tests__/heartbeat-process-recovery.test.ts` — 92 passed.
-
`PAPERCLIP_HOME="$PAPERCLIP_RUN_SCRATCH_DIR/test-home-remote-smoke-escalated"
pnpm exec vitest run
packages/adapter-utils/src/acpx-engine/remote-spawn-smoke.test.ts` — 3
passed.
-
`PAPERCLIP_HOME="$PAPERCLIP_RUN_SCRATCH_DIR/test-home-ci-repro-escalated"
pnpm exec vitest run
server/src/__tests__/heartbeat-dependency-scheduling.test.ts` — 6
passed.
- `pnpm --filter @paperclipai/adapter-utils typecheck` — passed.
- Reverse and forward dry-run application of `patches/acpx@0.12.0.patch`
— passed.
- `git diff --check` — passed.
- `git diff --exit-code origin/master...HEAD -- pnpm-lock.yaml` —
passed.
- `git diff --exit-code origin/master...HEAD -- .github/workflows` —
passed.

## Risks

- Runtime risk is low to moderate. ACPX now awaits process-identity
persistence during child startup.
- ACPX kills the child when persistence fails. This prevents an
unsupervised process, but it makes that heartbeat fail visibly.
- A compatible warm heartbeat reuses the identity of the existing ACP
child. Regression tests verify that identity is persisted before the
next prompt.
- The change updates the vendored ACPX patch. Package installation must
apply that patch.
- There are no schema, migration, public API, UI, workflow, or lockfile
changes.

> 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 used GPT-5.3-Codex for the earlier implementation.
- OpenAI Codex used GPT-5 for the lifecycle-hook revision and the
current fail-closed review fix. The runtime did not expose a more
specific snapshot ID or context-window size. Both runs used reasoning,
repository tools, and code execution.

## 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
- [x] All Paperclip CI gates are green
- [x] 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-31 14:43:46 -07:00
Nicky Leach 53bcf3897f
feat: sync @-mentioned projects into remote sandboxes (confined sandbox transport, flag ON) (#10564)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The run layer must move project context into the sandbox that
executes the agent
> - Local sandboxes already stage referenced projects for @-mentions
> - Remote confined sandboxes dropped the whole referenced set, so the
agent lost needed files and paths
> - This pull request keeps the confined sandbox transport aligned with
the local behavior for referenced projects
> - It does this behind a remote-only flag that defaults on, while SSH
keeps the old drop-only path
> - The benefit is that remote runs can read the same referenced project
context that local runs already provide

## Linked Issues or Issue Description

**Subsystem affected**

Cross-cutting. This change touches server orchestration, sandbox
transport, and observability.

**Problem or motivation**

A run can @-mention another project. Local targets stage each referenced
project and give the agent a path. Remote confined sandboxes dropped the
full referenced set, so the agent could not read those project files or
paths.

**Proposed solution**

Enable referenced-project sync for the confined sandbox transport behind
`PAPERCLIP_MULTI_PROJECT_WORKSPACE_SYNC_REMOTE`, which defaults on. Keep
the SSH transport out of scope and keep it dropping referenced projects.
Repoint each referenced workspace hint at its staged
`project-<projectId>` sandbox directory. Publish
`PAPERCLIP_WORKSPACES_JSON` on the confined sandbox lane. Count each
per-project remote staging failure as a `staging` failure in the
requested-vs-synced metrics.

**Alternatives considered**

Keep the remote path drop-only. That keeps the gap open. Move the change
into SSH too. That expands scope beyond the target transport and adds
risk.

**Roadmap alignment**

No matching item in `ROADMAP.md` showed up in this review.

**Additional context**

The change lands in three commits. The first commit opens the gate for
the confined sandbox transport. The second commit repoints the workspace
hints and publishes the workspace map. The third commit records
per-project staging failure data.

## What Changed

- Opened remote referenced-project sync for the confined sandbox
transport behind `PAPERCLIP_MULTI_PROJECT_WORKSPACE_SYNC_REMOTE`.
- Repointed referenced workspace hints to the staged
`project-<projectId>` sandbox directories and published
`PAPERCLIP_WORKSPACES_JSON`.
- Counted per-project remote staging failures as first-class `staging`
failures in the requested-vs-synced observability.

## Verification

- The pushed ref
`refs/heads/feat/sync-referenced-projects-remote-sandbox` resolves to
the authorized submit SHA.
- `git log --oneline
origin/master..origin/feat/sync-referenced-projects-remote-sandbox`
shows exactly the three expected commits.
- The handoff reports server typecheck clean, adapter-utils typecheck
clean, and the listed unit tests passing.
- The handoff also reports no open review comments and no Greptile score
yet.

## Risks

- The change touches authorization and sandbox path handling, so
regressions could block remote runs or expose the wrong project context.
- The new flag defaults on, so any bug in the remote path affects normal
remote use.
- SSH stays out of scope, so the two transport paths must remain
distinct.

## Model Used

OpenAI GPT-5 via Codex. Tool use enabled. Context window not reported in
this run.

## 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
- [x] All Paperclip CI gates are green
- [x] 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-31 13:31:17 -07:00
Nicky Leach b01f423cd7
fix(server): export manual OpenTelemetry spans (#10565)
## Thinking Path

> - Paperclip uses the server to coordinate agent work.
> - The server emits manual OpenTelemetry spans for startup, heartbeat,
and sandbox execution.
> - Those spans need the shared OpenTelemetry API package and a
type-safe exporter path.
> - Without the direct API dependency, the tracer stays no-op and the
spans do not reach the collector.
> - This pull request adds the direct dependency and the exporter cast.
> - The benefit is that the manual spans can export cleanly at runtime.

## Linked Issues or Issue Description

**What happened?**
The server resolved the tracer with a runtime import, but `server` did
not declare `@opentelemetry/api`. The manual spans stayed no-op, so the
collector did not receive them.

**Expected behavior**
The server should load the shared OpenTelemetry API package, create the
manual spans, and export them.

**Steps to reproduce**
1. Start the server with telemetry enabled.
2. Run startup, heartbeat, or sandbox execution paths.
3. Observe that the manual spans do not export before this change.

**Paperclip version or commit**
`f91df236dfd8e5e6210941c80efeb0a7953bbe50`

**Deployment mode**
Built from source with `pnpm dev` or `pnpm build`.

## What Changed

- Added `@opentelemetry/api` as a direct `server` dependency.
- Cast the `traceExporter` value to `never` so the type check passes
without a static `SpanExporter` import.
- Kept the optional OTLP and SDK packages behind dynamic import.

## Verification

- `pnpm build` in `server/` passed.
- `server/src/instrumentation.ts` does not import `SpanExporter`.
- `server/package.json` lists `@opentelemetry/api` at `^1.9.0`.

## Risks

- Low risk. The change touches dependency metadata and one type cast.
- Runtime telemetry still needs live collector QA.

## Model Used

- OpenAI Codex, GPT-5, tool use enabled.

## 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
- [x] I will address all Greptile and reviewer comments before
requesting merge

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-07-31 12:47:57 -07:00
Devin Foley 51bb41c7e3
Expose the running build commit on the unauthenticated health response (#10563)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Instances run self-hosted or under hosting/deploy tooling, and
operators need to observe what build a server is actually running
> - `/api/health` carries the git SHA only inside `serverInfo`, which is
gated to board/agent actors — anonymous callers get a redacted body with
no version signal at all
> - Deploy tooling that manages instances from outside (fleet rollouts,
hosting providers, upgrade scripts) therefore cannot ground-truth that a
deploy actually shipped without holding credentials
> - A build commit is a plain git SHA of this public repository — it is
not a secret, and gating it buys no security while blocking legitimate
verification
> - This pull request surfaces the running build commit as a top-level
`commit` field on every `/api/health` response, including the redacted
anonymous one
> - The benefit is credential-free deploy verification: any operator or
tool can confirm which commit an instance serves, while the fuller
`serverInfo` block stays access-controlled as before

## Linked Issues or Issue Description

No existing public issue — inline description following the feature
request template:

**Subsystem affected**

Server (API, runs, routes)

**Problem or motivation**

An anonymous `GET /api/health` returns a redacted body with no version
information; the running git SHA exists only in
`serverInfo.git.fullSha`, which requires a board/agent actor. External
deploy tooling (fleet rollouts, hosting providers, upgrade scripts)
therefore cannot verify that an instance is actually serving the build
it was just upgraded to — a rollout that silently keeps running the old
image is indistinguishable from a successful one at the health endpoint.

**Proposed solution**

Surface the running build commit as a top-level nullable `commit` field
on every `/api/health` response shape, including the redacted anonymous
one, while keeping the fuller `serverInfo` block access-controlled as
before. A build commit is a plain git SHA of this public repository —
exposing it costs nothing and enables credential-free deploy
verification, like the `version` endpoints on most server software.

**Alternatives considered**

Authenticating deploy tooling as a board actor to read `serverInfo` —
rejected: it forces credential plumbing into infrastructure that only
needs a public SHA, and adds a whole class of auth-misconfiguration
failure to deploy verification.

**Roadmap alignment**

Not on ROADMAP.md; a small operational observability improvement, no
overlap with planned core work.

## What Changed

- `server/src/routes/health.ts`: derive `commit` from the server info
snapshot (`serverInfo.git.fullSha` when git metadata is available, else
`null`) and include it as a top-level field on every `/api/health`
response shape — the redacted anonymous body, the full-details body, the
no-db body, and the 503 database-unreachable body.
- `serverInfo` itself remains gated to full-details responses exactly as
before; only the bare commit is newly public.
- `server/src/__tests__/health.test.ts`: updated exact-shape assertions
to include `commit`, and added an assertion that `commit` is `null` (not
omitted) when git metadata is unavailable. The redacted-response tests
now pin that anonymous callers receive the commit.

## Verification

- `pnpm vitest run src/__tests__/health.test.ts` in `server/` — 13 tests
pass, including the redacted-anonymous shapes (which now pin the
`commit` field) and the git-unavailable `null` case.
- `tsc -p server/tsconfig.json --noEmit` — clean.
- Manual: `curl -s https://<instance>/api/health` as an anonymous caller
returns `"commit": "<full sha>"` alongside the existing redacted fields.

## Risks

- **Version disclosure:** anonymous callers can now fingerprint the
exact running commit. This is a deliberate trade-off: the builds are of
a public repository (the SHA reveals no private code), the endpoint
already responds to anonymous callers, and the operational value —
verifying deploys actually shipped — outweighs the marginal
fingerprinting surface. Operators who consider this sensitive are
typically fronting `/api` with their own access controls already.
- Otherwise low risk: no behavioral change to any gated field, no schema
or API-surface removal; `commit: null` keeps the field shape stable when
git metadata is absent (e.g. non-git installs).

## Model Used

Claude Opus 4.8 (`claude-opus-4-8`, extended thinking, via Claude Code
with tool use and code execution) authored the change and tests;
finalized and PR'd under Claude Fable 5 (`claude-fable-5`).

## 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 (none
needed beyond code comments — health endpoint has no standalone doc)
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
2026-07-31 11:46:34 -07:00
Michael Nguyen d295251550
feat(adapter-claude): add Claude Sonnet 5 to the static model fallback (#10280)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents pick their model from a dropdown in agent config, populated
per-adapter by `listAdapterModels()` → each adapter's live provider
catalog merged over a static fallback list
> - For `claude_local`, newer model ids only reach the dropdown via the
live Anthropic `/v1/models` fetch, which needs a server
`ANTHROPIC_API_KEY`, a <5s round-trip, non-Bedrock mode, and account
entitlement; on any miss it silently falls back to the static `models`
array
> - Claude Sonnet 5 (`claude-sonnet-5`) is a current flagship but was
absent from that static fallback, so it appeared only when live
discovery happened to succeed — i.e. "the newest model doesn't
consistently show up"
> - This pull request adds `claude-sonnet-5` to the `claude_local`
static model list so it is selectable regardless of the live-discovery
path
> - The benefit is a consistent, reliable dropdown that no longer
depends on a flaky live fetch to surface a shipped flagship model

## Linked Issues or Issue Description

No public GitHub issue. The bug is described inline following the
bug-report template:

**What happened**
The `claude_local` agent-config model dropdown intermittently omitted
Claude Sonnet 5. `claude-sonnet-5` was missing from the adapter's static
fallback `models` array (`packages/adapters/claude-local/src/index.ts`),
so it only surfaced when the live Anthropic `/v1/models` discovery
happened to succeed.

**Expected behavior**
Claude Sonnet 5 is a shipped flagship model and should always be
selectable in the dropdown, independent of whether live discovery
succeeds.

**Steps to reproduce**
1. Run the server without a working live Anthropic `/v1/models` path (no
`ANTHROPIC_API_KEY`, Bedrock mode, a discovery timeout, or a cache
miss).
2. Open agent config for a `claude_local` agent and inspect the model
dropdown.
3. Observe that `claude-sonnet-5` is absent because the static fallback
list omitted it.

**Deployment mode**
Self-hosted / local adapter (`claude_local`); the server process reads
`ANTHROPIC_API_KEY` from its environment.

## What Changed

- Added `{ id: "claude-sonnet-5", label: "Claude Sonnet 5" }` to the
`claude_local` static `models` fallback, immediately after
`claude-opus-4-8` (so Opus 4.8 stays the default first option).
- Added an explicit regression assertion in
`server/src/__tests__/adapter-models.test.ts` that `claude-sonnet-5` is
present in the `claude_local` fallback when live discovery is
unavailable.

## Verification

- `pnpm -C server exec vitest run src/__tests__/adapter-models.test.ts
-t "claude fallback"` — **passes** (the new `claude-sonnet-5` assertion
included).
- Reviewed the consuming tests: the fallback test also asserts
`models[0]?.id === "claude-opus-4-8"` (still index 0 — Sonnet 5 is index
1, unaffected); `adapter-registry.test.ts` reads `builtIn?.models`
dynamically, so no exact-array snapshot breaks.
- Change is a single static-data addition plus a test assertion; no
control-flow change.

## Risks

- Low risk. Pure additive change to a fallback list; no control-flow
change. Worst case is an id that a given account isn't entitled to,
which the existing "current"/manual-model UI paths already tolerate.

## Model Used

Claude (Anthropic), model id `claude-opus-4-8` (Opus 4.8), extended
thinking + tool use, run as the Paperclip CTO agent.

## 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)
- [ ] My branch name describes the change (branch is the assigned
execution-workspace branch and cannot be renamed this run)
- [x] I have run tests locally and they pass (server adapter-models
"claude fallback" case)
- [x] I have added or updated tests where applicable (explicit
`claude-sonnet-5` fallback assertion)
- [x] I have updated relevant documentation to reflect my changes (n/a —
no docs reference this list)
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green (pending CI)
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
(pending review)
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-07-31 13:15:42 -05:00
Dotta cd7f84965c
fix(recovery): preserve hand-back wake liveness (#10562)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The heartbeat service delivers issue work to assigned agents.
> - Recovery can hand an issue back to its agent while the recovery run
is still active.
> - The hand-back wake can merge into that active run and disappear when
the run exits.
> - The stranded-work scan also treats the successful recovery run as
proof that the handed-back issue is live.
> - This pull request keeps the hand-back wake for follow-up delivery
and lets the scan repair a lost wake.
> - The benefit is that an assigned issue continues after recovery
without manual operator action.

## Linked Issues or Issue Description

No public issue exists. This is related to the wake reconciliation work
in #8943.

**What happened?**

A recovery action could hand an assigned issue back from `blocked` to
`todo`. The `issue_recovery_action_restored` wake then merged into the
recovery run that made the change. The wake disappeared when that run
exited. The stranded-work scan did not repair the issue because it
treated the successful recovery run as current liveness.

**Expected behavior**

Paperclip must dispatch the hand-back wake after the recovery run exits.
If that delivery is lost, the stranded-work scan must enqueue the
assigned `todo` issue again.

**Steps to reproduce**

1. Start a recovery run for an assigned blocked issue.
2. Resolve a recovery action with the `handed_back` outcome.
3. Move the issue to `todo` while the recovery run is still active.
4. Observe that the wake merges into the active run and no new run
starts after it exits.
5. Run the stranded-work scan and observe that the successful latest run
prevents repair.

**Paperclip version or commit**

`131d476a7e`

**Deployment mode**

Local dev (`pnpm dev`). The defect is in the core server and is not
deployment-specific.

**Agent adapter(s) involved**

Not adapter-specific. This is a core heartbeat and recovery defect.

## What Changed

- Added `issue_recovery_action_restored` to the wake reasons that
require follow-up delivery when an issue run is active.
- Made the stranded-work scan detect a resolved hand-back that occurred
during or after the latest successful run.
- Added focused regression tests for the heartbeat coalescing seam and
the stranded-work repair shape.
- Documented the hand-back liveness guarantee in execution semantics
section 9.1.

## Verification

- `pnpm exec vitest run
server/src/__tests__/heartbeat-comment-wake-batching.test.ts
server/src/__tests__/heartbeat-process-recovery.test.ts` passed: 104
tests.
- `pnpm --filter @paperclipai/server typecheck` passed.
- `pnpm -r typecheck` passed.
- `pnpm build` passed.
- `pnpm test:run` passed the server shard (3,095 passed, 2 skipped) and
UI shard (3,182 passed). One unrelated CLI test failed because the agent
environment exports static AWS credentials. `env -u AWS_ACCESS_KEY_ID -u
AWS_SECRET_ACCESS_KEY -u AWS_SESSION_TOKEN pnpm exec vitest run
cli/src/__tests__/secrets.test.ts` passed all 8 tests.
- `git diff --check` passed.
- All GitHub checks passed on commit `8b380e67e6`.
- Greptile gave 5/5 confidence with no comments or unresolved threads.

## Risks

- Low risk. The follow-up rule affects only a recovery hand-back wake
that arrives while the same issue already has an active run.
- The backstop adds one indexed recovery-action lookup for an assigned
`todo` issue whose latest run succeeded.
- The timestamp check uses the latest run start time. This includes
hand-backs made by that run and later hand-backs, but excludes older
resolved actions.

## Model Used

- OpenAI Codex with GPT-5 (`gpt-5`), agentic reasoning, tool use, and
code execution. The serving context-window size is not exposed to the
agent.

## 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
- [x] All Paperclip CI gates are green
- [x] 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-31 11:03:30 -07:00
Dotta b4a7a12985
feat: make recovery updates quieter (#10542)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The recovery subsystem restores work after an agent run stops or
loses state
> - Recovery notices currently use the same visual weight as normal work
comments
> - Recovery agents can also post long narratives that obscure the
useful hand-off
> - The server must identify recovery output because agents cannot set
presentation controls
> - This pull request adds compact recovery notices, structured action
references, and brief recovery prompts
> - The benefit is a quieter issue thread that still keeps recovery
state inspectable

## Linked Issues or Issue Description

**Subsystem affected**

Cross-cutting: `server/`, `packages/shared`, and
`packages/adapter-utils`.

**Problem or motivation**

Recovery notices and recovery-run comments can dominate an issue thread.
Operators must scan routine recovery narration before they find the work
hand-off.

**Proposed solution**

Give routine recovery output a compact system-notice presentation.
Derive the presentation on the server so agents cannot hide arbitrary
comments. Keep the successful missing-state summary fully visible
because that comment is the recovery deliverable.

**Alternatives considered**

The UI could detect recovery text. That approach is fragile and does not
provide structured action references. Agents could also set presentation
directly, but that would weaken the current board-only security
boundary.

**Roadmap alignment**

This change refines the completed “Self-healing runs & automatic
recovery” and “Enforced Outcomes” roadmap areas. It does not add a
competing roadmap capability.

**Additional context**

The scope covers shared comment validation, server recovery notices,
agent-comment derivation, and recovery prompt text. No database
migration is needed because presentation data already uses JSON.

## What Changed

- Add the `compact` issue-comment presentation density to shared
constants, types, and validation.
- Give recovery escalation, waiting, and in-place notices compact titles
and structured recovery-action metadata.
- Use recovery-action metadata for notice deduplication, with the legacy
text marker as a compatibility fallback.
- Derive compact presentation for comments from recovery-scoped runs
while preserving the board-only presentation boundary.
- Keep successful missing-state recovery summaries fully visible.
- Ask recovery participants to record outcomes in `resolutionNote` and
keep source-issue comments brief.
- Add shared, route, service, and prompt tests for the new behavior and
exceptions.

## Verification

- `pnpm -r typecheck`
- Focused Vitest coverage: 320 tests passed across shared validators,
adapter prompts, issue comments, recovery actions, and heartbeat
recovery.
- Full server phase: 292 files passed, 3,094 tests passed, and 2 tests
skipped.
- Full UI phase: 386 files passed and 3,182 tests passed.
- `pnpm build`
- Known master baseline: `cli/src/__tests__/secrets.test.ts` expects
`pass`, but the current implementation returns `warn` when strict secret
mode is disabled for Postgres. This branch does not change CLI secrets
code.

## Risks

- Low migration risk. The presentation column is JSON and needs no
database migration.
- Recovery-run detection depends on the persisted run context snapshot.
- Structured metadata becomes the primary deduplication key. The
existing body marker remains as a fallback for older comments.

> 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 with `gpt-5.6-sol`. The runtime did not expose the
context-window size. The model used agentic reasoning, repository tools,
code execution, and test execution.

## 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
- [x] All Paperclip CI gates are green
- [x] 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>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 09:55:01 -07:00
Dotta 6a3cbe1c58
fix(ui): load the full selected timeline window (#9576)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The timeline page visualizes company activity across a selected date
window
> - The UI requested only the first paginated issue batch even when the
selected zoom covered seven or thirty days
> - A busy company could therefore render an incomplete timeline while
the controls implied the full window was loaded
> - The timeline query needs to exhaust the API pagination for the
selected date range and combine each page without duplicating shared
timeline records
> - This pull request adds a paginated window loader, merges the
returned timeline data, and covers the multi-page behavior with a
regression test
> - The benefit is that the visible timeline matches the selected zoom
window instead of silently omitting later issues

## Linked Issues or Issue Description

### Pre-submission checklist

- [x] I searched existing open and closed issues and pull requests; no
matching report or implementation was found.
- [x] I reproduced the behavior against the pre-change `master`
implementation.
- [x] I confirmed the error originates in Paperclip's core timeline UI,
not an adapter, provider, or local configuration.

### What happened?

Selecting the default seven-day timeline range loaded only the first API
page (up to 500 issues). Companies with more activity therefore
displayed incomplete data even though the controls showed the full
selected window.

### Expected behavior

The timeline should load all issue pages that fall within the selected
date window.

### Steps to reproduce

1. Open the company timeline for a date range containing more than 500
issues.
2. Keep the default seven-day range or select another multi-day preset.
3. Observe that only the first page of issue-backed timeline data is
shown.

### Paperclip version or commit

Pre-change `master`.

### Deployment mode

Local dev source build. The behavior is not adapter-specific and is
independent of database mode and access context.

### Privacy checklist

- [x] No logs, configuration, personally identifiable information, or
user data are included.

## What Changed

- Added pagination parameters to the timeline API client contract.
- Added a timeline window loader that requests every issue page and
deduplicates actors, spans, events, and edges while preserving
pagination metadata.
- Switched the timeline query to use the complete-window loader.
- Added a regression test proving a 501-issue window loads both API
pages and combines their records.
- Preserved delegation events and edges when parent and child issues
fall on different API pages, with a server regression test.

## Verification

- `pnpm exec vitest run
server/src/__tests__/work-timeline-service.test.ts
ui/src/pages/Timeline.test.tsx` — 16 tests passed.
- `pnpm --filter @paperclipai/ui typecheck` — passed.
- `pnpm --filter @paperclipai/server typecheck` — passed.
- `pnpm check:token-gates` — all gates clean.
- `git diff --check origin/master...HEAD` — passed.
- Remote CI: build, typecheck, both e2e shards, canary, policy,
security, every general/serialized test shard, and the aggregate
`verify` gate passed on head `24784b28e9`.

## Risks

- Low risk: the change is isolated to timeline data loading and has no
schema or API endpoint changes.
- Large date windows now make sequential requests for all issue pages,
increasing request count for very active companies; the 500-item page
size bounds each response.
- Merged records rely on stable identifiers or composite event/edge
keys; the regression test covers cross-page combination and
deduplication behavior.

> 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 using GPT-5.4 with reasoning, repository tool use, shell
execution, and test execution. The runtime does not expose the exact
context-window size.

## 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
- [x] All Paperclip CI gates are green
- [x] 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-30 21:40:57 -07:00
Devin Foley dd1a7f5290
Ensure app-home ownership before the privilege drop, not only on remap (#10530)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The Docker image persists all instance state (project checkouts,
worktrees, run logs, uploads) under `PAPERCLIP_HOME`, and deployments
mount a volume there for durability
> - The entrypoint starts as root and drops privileges to the `node`
user, but it fixes `PAPERCLIP_HOME` ownership only when it remaps the
user's UID/GID
> - A freshly mounted volume arrives root-owned and shadows the image's
build-time `chown`, so a default-UID boot drops privileges onto an
unwritable home and the server crashes on its first `mkdir`
> - This pull request makes the entrypoint probe the home's ownership
and chown whenever it does not match the runtime user, before the
privilege drop
> - The benefit is that the image works out of the box on any
platform-managed volume, with the common already-correct boot staying
chown-free

## Linked Issues or Issue Description

No public issue exists — describing the bug inline (per the bug report
template).

**What happened?**
Running the image with a freshly created volume mounted at `/paperclip`
(a Docker named volume, a Kubernetes PV, or any platform-managed volume)
and the default `USER_UID`/`USER_GID` crashes on boot: `Error: EACCES:
permission denied, mkdir '/paperclip/instances/default/logs'`.

**Expected behavior**
The container boots and initializes its instance tree on the mounted
volume, exactly as it does when `/paperclip` is the image's own
(build-time chowned) directory.

**Steps to reproduce**
1. `docker volume create paperclip-data`
2. `docker run -v paperclip-data:/paperclip
ghcr.io/paperclipai/paperclip:<any current tag>`
3. Observe the EACCES crash on the first `mkdir` under `/paperclip`.

**Root cause**
`scripts/docker-entrypoint.sh` chowns `/paperclip` only inside its
UID/GID remap branch (`changed=1`). A fresh volume mount is root-owned
and shadows the image's build-time `chown node:node /paperclip`; with
the default 1000:1000 no remap happens, so no chown happens, and `gosu
node` drops onto an unwritable home.

**Paperclip version or commit:** reproduces on `master` and any
published image.
**Deployment mode:** any; observed on managed-cloud volume mounts and
reproducible with plain Docker named volumes.
**Installation method:** Docker image (`ghcr.io/paperclipai/paperclip`).

**Related PRs (dedup search):** no open or merged PR touches the
entrypoint ownership logic; the entrypoint's privilege-handling tests
were added previously and this extends them. No duplicate found.

## What Changed

- `scripts/docker-entrypoint.sh`: the remap-conditional `chown` is
replaced by an ownership probe — after any UID/GID remap, the entrypoint
stats `PAPERCLIP_HOME` (default `/paperclip`) and runs `chown -R
node:node` only when the owner does not match the runtime user, before
`exec gosu node`. Covers fresh root-owned mounts and trees written under
a previous UID mapping; the already-correct boot performs no chown. The
unprivileged (non-root start) branch is unchanged.
- `server/src/__tests__/docker-entrypoint.test.ts`: `stat` stub added to
the harness; new cases for the fresh root-owned mount with default
UID/GID and for `PAPERCLIP_HOME`-relative probing; the remap case now
models the post-remap ownership mismatch.

## Verification

- `pnpm vitest run server/src/__tests__/docker-entrypoint.test.ts` — 7
passed (5 existing behaviors unchanged, 2 new).
- Live on a managed deployment: a container that crash-looped with the
EACCES above boots cleanly once the home is chowned before the drop (the
same effect this entrypoint change produces; forced there by a UID remap
as an interim workaround).

## Risks

- Low. Behavior changes only for boots where `PAPERCLIP_HOME` exists
with mismatched ownership — exactly the boots that crash today. `chown
-R` on a large previously-mismatched tree adds one-time boot latency;
correctly-owned homes skip it entirely. Kubernetes restricted /
OpenShift non-root starts keep the existing exec-directly path
untouched.

## Model Used

Claude Fable 5 (`claude-fable-5`, Anthropic; Claude Code CLI with
extended thinking and tool use; tests executed locally via Vitest).

## 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 (no duplicates; extends the existing entrypoint privilege
tests)
- [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
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
2026-07-30 21:34:45 -07:00
Devin Foley 075951f6bd
Fix import completion UX: inbox flood, false-failure message, stale company list (#10538)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Company Import/Export (#10507, hardened in #10523 and #10531) now
imports a large company end to end via an async job
> - A real 1,418-issue import succeeded, but three rough edges showed up
in that success
> - Imported issues flooded the inbox, a completed import surfaced a
false "failed" message after its in-memory result expired, and the new
company didn't appear in the switcher until a manual refresh
> - This pull request keeps imported issues out of the inbox, treats an
expired-but-completed import as success, and refreshes the company list
on completion
> - The benefit is that a successful import looks and feels successful,
and doesn't bury the user's inbox in historical tasks

## Linked Issues or Issue Description

- Refs #10507 / #10523 / #10531 (Import/Export and its hardening). No
open issue; three post-import bugs described above.

## What Changed

- **Imported issues no longer flood the inbox.** The inbox "mine" tab is
a query: an issue is "touched" if the user authored a comment on it, and
import re-attributes bundled user comments to the importing user — so
every imported issue appeared. Import now seeds a per-user
`issue_inbox_archives` row for each imported issue (via a batched
`issues.archiveImportedInbox`), the exact table the inbox visibility
query excludes. Gated on an actor user id, so agent/system imports and
normal issue creation are untouched; genuine new activity still
resurfaces the issue.
- **A completed import no longer shows a false failure.** The in-memory
job's terminal retention was 5 minutes, so a poll after that 404'd and
the UI showed "failed." Retention is extended to 60 minutes — the real
mitigation for a user who steps away during a long import.
`watchImportJob` additionally treats a *server-confirmed* success whose
full result is no longer retained (a `succeeded` status carrying only
the compact summary — a cloud tenant job, or a board job whose full
in-memory result aged out) as a soft success ("import completed — open
the company"), navigating by the summary's company id. A 404 while the
job is still being watched is *not* treated as success: a running job is
never dropped by the retention sweep, so its disappearance means a
restart mid-import that may not have finished, and it surfaces the
honest "may have restarted while the import ran" error. A first-poll 404
(the id never existed) is likewise a real error.
- **The imported company appears without a refresh.** `onSuccess` now
invalidates the companies/switcher query unconditionally (covering both
the full-result and expired-but-completed paths) and navigates by the
job's company id.

## Verification

- shared/server/ui typechecks clean; 15 UI tests in the touched spec
green, plus the embedded-Postgres import batching and portability-routes
suites.
- New tests: embedded-Postgres test that imported touched issues are
archived for the actor and excluded from the inbox query while a
normally-created issue still appears; job resolvable at the old window+1
and only 404s past 60 min; UI soft success on a server-confirmed
`succeeded` job without a retained full result (no error, list
invalidated, navigates by company id), a running-then-gone job → honest
error (restart mid-import), and a first-poll 404 → error.

## Risks

- Low and import-scoped: the inbox archive only affects imported issues
for the importing user; normal issue creation and non-user
(agent/system) imports are unchanged. Retention extension is a constant;
the async job store remains in-memory by design. A restart mid-import
still 404s and is surfaced honestly as a possible failure (never masked
as success); only a server-confirmed success whose full result has
expired is reported as a soft success.

## Model Used

- Implementation: Claude Fable 5 (`claude-fable-5`, Anthropic). Review
hardening (the confirmed-success narrowing): Claude Opus 4.8
(`claude-opus-4-8`, Anthropic). Both via the Claude Code CLI with
extended thinking + tool use; root-caused against the live import.

## 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
- [x] My branch name describes the change and contains no internal
ticket id
- [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
2026-07-30 21:33:02 -07:00
Nicky Leach 9f7565f4ce
feat: activate OpenTelemetry spans on the sandbox start path (#10536)
## Thinking Path

> - Paperclip moves agent work through sandboxed execution and
control-plane services.
> - The sandbox start path now has a no-op span seam.
> - This change turns that seam on when OTLP export is configured.
> - It keeps the default path unchanged when export is off.
> - The result is structured startup traces with low-cardinality
attributes and explicit parent links.
> - The benefit is better observability without changing normal
behavior.

## Linked Issues or Issue Description

No public GitHub issue exists for this change.

### Problem
The sandbox start path has a tracer seam, but it stays a no-op unless
the OTLP export path is active.

### Proposed solution
Enable the server tracer on sandbox bring-up, open a root span, parent
each startup boundary to that root, and keep the export path opt-in
behind `OTEL_EXPORTER_OTLP_ENDPOINT`.

### Alternatives considered
- Keep the start path as a no-op. I rejected that path because it leaves
sandbox start opaque when OTLP export is already configured.
- Add broad attributes for commands and paths. I rejected that path
because the span allowlist must stay low-cardinality.

### Roadmap alignment
This follows the current OTel sandbox-start work and keeps the default
path unchanged.

## What Changed

- Add a root sandbox startup span and child spans for each named startup
boundary.
- Keep concurrent bridge spans parented to the root span.
- Inject the server tracer through the adapter deps without
OpenTelemetry imports in the engine.
- Attach host-received provider duration attributes only when the values
are finite.
- Keep span attributes inside the allowlist and keep command, path, id,
and error text out of span data.

## Verification

- The pushed branch already passed `pnpm --filter
@paperclipai/adapter-utils exec tsc --noEmit`.
- The pushed branch already passed `pnpm exec vitest run
packages/adapter-utils/src/acpx-engine/`.
- The pushed branch already passed `pnpm --filter @paperclipai/server
exec vitest run src/__tests__/environment-execution-target.test.ts
src/__tests__/instrumentation.test.ts`.
- The pushed branch already passed `pnpm --filter @paperclipai/server
exec tsc --noEmit`.

## Risks

- OTel export changes trace volume when the endpoint is set.
- The allowlist limits trace detail, so new fields need care.
- The change stays no-op when OTLP export is off.

## Model Used

- OpenAI GPT-5, tool use enabled.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used with 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 linked existing issues or described the issue in-PR
- [x] I have not referenced internal/instance-local Paperclip issues or
links
- [x] My branch name describes the change and contains no internal
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
- [x] All Paperclip CI gates are green
- [x] 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-30 21:20:58 -07:00
Devin Foley 5ec7ce76e5
Upload company import packages as compressed zip uploads (fix large-company imports through Cloud) (#10531)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Company Import (#10507, hardened in #10523) lets a user upload a
company package on the Import page
> - The page expanded the user's `.zip` into a files map and POSTed it
as ONE inline JSON body — ~40MB for a real company because attachment
blobs get base64-inflated
> - On Paperclip Cloud that body travels browser → harness proxy →
tenant, where it truncated in transit → body-parser 400 → the browser
saw "Failed to fetch", and nothing imported
> - Two compounding causes: the giant inline body itself, and the board
async opt-in riding an `x-paperclip-cloud-*` header that the Cloud
harness strips as anti-spoofing (so async never engaged and the import
held one fragile synchronous connection)
> - This pull request uploads the raw compressed `.zip` as a multipart
request (about a third the size, already compressed) parsed server-side
into the same bundle the importer consumes, and moves the async opt-in
to a proxy-safe `?async=1`
> - The benefit is that a large-company import actually completes
through Cloud: a small compressed upload, a real async job that survives
dropped connections

## Linked Issues or Issue Description

- Refs #10507 / #10523 (Import/Export and its hardening). No open issue;
problem described above (large-company browser import through a proxy:
inline JSON body truncates → 400 → "Failed to fetch"; async opt-in
header stripped by the front door → async never engages).

## What Changed

- **Multipart zip transport.** The Import page uploads the raw `File` as
`multipart/form-data` (field `package`, import options in a JSON `meta`
field); the server unzips it into `{ rootPath, files }` and runs the
exact existing preview/import logic. The `application/json` inline path
is byte-identical for CLI/programmatic callers. Bare `application/zip`
(meta via `?meta=`) is also accepted for programmatic use.
- **Shared node zip reader.** `packages/shared/src/portability-zip.ts`
(node-only subpath, not re-exported to the browser bundle — same pattern
as `portability-hash.ts`); the CLI's `zip.ts` becomes a thin re-export.
Identical codec (STORE + DEFLATE via `inflateRawSync`, rejects data
descriptors/zip64).
- **Proxy-safe async signal.** `wantsAsyncImport` = `?async=1` (board
browsers, survives the harness) OR the existing
`x-paperclip-cloud-async-import` header (cloud tenants, set
server-side). The UI async client now uses `?async=1`. Backward
compatible.
- **Size + preflight.** New `PORTABLE_ZIP_UPLOAD_LIMIT_BYTES = 128MB`;
the inline 56MB preflight no longer gates the zip path (it shows the
compressed size instead). Async submit/poll/resume, the duplicate-guard
fingerprint (now over the resolved bundle), pause-on-import,
progress/error panels, and activation all apply to the multipart path.
- OpenAPI documents json + multipart + zip bodies and the `async` query
param.

## Verification

- Full typecheck chain (shared, server, ui, cli) clean.
- 152 tests across 8 files: new `portability-zip.test.ts`
(STORE/DEFLATE/base64-blob byte-exact round-trip, truncation throws,
data-descriptor rejection); `company-portability-routes.test.ts` +7
(multipart import+preview equals the inline bundle; async multipart
202→poll→success; board async via `?async=1` with no cloud header;
cloud-tenant async via header; sync fallback with neither; truncated-zip
400, nothing imported); `CompanyImport.test.tsx` asserts the local zip
sends the raw File and the inline preflight no longer blocks;
`openapi-routes.test.ts` green.
- NOT yet measured: the end-to-end browser upload through the live Cloud
harness — verified on staging after deploy before closing out.

## Risks

- Import semantics unchanged — only transport changed; the JSON inline
path is byte-identical, the cloud-tenant header async path untouched.
Multipart parsing is server-side (memory-bound: a ~13MB zip → ~30MB
files map, fine on the server).
- The bare `application/zip` path is programmatic-only and covered by
content-type dispatch but not a dedicated route test (the multipart path
is).

## Model Used

- Claude Fable 5 (`claude-fable-5`, Anthropic), Claude Code CLI,
extended thinking + tool use; root-caused against live logs/DB and the
harness proxy source.

## 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 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
2026-07-30 18:48:22 -07:00
Nicky Leach 740554acc6
feat: add a no-op OpenTelemetry span seam for the sandbox startup path (#10522)
## Thinking Path

> - Paperclip helps people run and govern AI agent work
> - Sandbox startup needs a safe place to add telemetry spans without
forcing OpenTelemetry on every run
> - This change adds a no-op span seam, so the startup path can accept a
tracer later and still stay inert now
> - The server gets a lazy tracer accessor, and the adapter timing
helper gets an injected tracer hook
> - The change keeps the default path free of OpenTelemetry and keeps
the existing startup event path unchanged
> - The benefit is a future-safe seam with no runtime change today

## Linked Issues or Issue Description

This PR addresses a feature gap in the sandbox startup path.

### Problem

Sandbox startup has no safe span seam. A direct OpenTelemetry import
would load telemetry packages on every run.

### Proposed Solution

Add a lazy tracer accessor in the server. Add an injected no-op tracer
seam in startup timing.

### Alternatives

Import OpenTelemetry directly in the startup path. Reject that path
because the default startup flow must stay inert.

## What Changed

- Added a lazy startup tracer accessor in
`server/src/instrumentation.ts`.
- Added an injected startup tracer seam in
`packages/adapter-utils/src/acpx-engine/startup-timing.ts`.
- Kept the startup event path unchanged.
- Kept `adapter-utils` free of OpenTelemetry imports.

## Verification

- `pnpm exec vitest run
packages/adapter-utils/src/acpx-engine/startup-timing.test.ts`
- `pnpm exec vitest run server/src/__tests__/instrumentation.test.ts`
- `tsc --noEmit` for `@paperclipai/adapter-utils` and
`@paperclipai/server`

## Risks

Low risk. The default tracer is a no-op, so the runtime path stays inert
until a later change injects a real tracer.

## Model Used

OpenAI GPT-5. Tool use and code execution.

## 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 found none
- [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 or instance-local Paperclip issues
or links
- [x] My branch name describes the change 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
- [ ] 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
- [ ] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-07-30 18:23:44 -07:00
Dotta 7cfb655f60
fix(worktree): disable automatic database backups (#10520)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Paperclip creates isolated instances for linked git worktrees so
development does not affect the primary instance
> - Those instances inherited the source instance's automatic
database-backup setting and also repaired older configs without
overriding it
> - As worktrees accumulated, each isolated instance could schedule its
own backup stream, producing redundant backup churn for disposable
database clones
> - This pull request makes backup disablement an invariant of worktree
config creation and repair
> - The benefit is that automatic backups remain focused on the durable
primary instance while isolated development instances stop accumulating
redundant backup files

## Linked Issues or Issue Description

No public GitHub issue exists for this bug, so the report is included
here. The closest related open change is Refs #10266, which hardens
where worktree config repair may write; this PR changes the backup
policy applied by that repair and by worktree initialization.

### What happened?

Isolated worktree instances copied `database.backup.enabled` from their
source config. When the source instance enabled automatic backups (the
normal default), every linked worktree also enabled a scheduled backup
stream. Existing worktree configs kept that state during startup repair,
so the redundant backups continued after the policy changed.

### Expected behavior

Automatic database backups are disabled for isolated worktree instances
created by `paperclipai worktree init` or `paperclipai worktree:make`,
and legacy worktree configs are migrated to that policy during normal
startup repair. The durable primary/default instance keeps its existing
backup behavior.

### Steps to reproduce

1. Start from a Paperclip instance whose database backup setting is
enabled.
2. Create or initialize a linked worktree with `paperclipai worktree
init`.
3. Inspect the generated worktree config and environment.
4. Before this change, the config retained `database.backup.enabled:
true` and the environment had no disabling override; after this change,
the config is false and `PAPERCLIP_DB_BACKUP_ENABLED=false` is
persisted.

### Paperclip version, deployment mode, and environment

- Reproduced against `master` before commit `ea5e0a0269`.
- Deployment mode: local trusted development with linked git worktrees
and embedded PostgreSQL.
- Environment: Node.js 22, pnpm workspace install.

## What Changed

- Always generate isolated worktree configs with automatic backups
disabled.
- Persist `PAPERCLIP_DB_BACKUP_ENABLED=false` in generated worktree
environments.
- Repair existing isolated worktree configs and environments that still
enable backups.
- Add CLI and server regression coverage for creation and legacy repair
paths.
- Document the worktree-specific backup policy and primary-instance
exception.

## Verification

- `pnpm exec vitest run cli/src/__tests__/worktree.test.ts
server/src/__tests__/worktree-config.test.ts` — 52 tests passed.
- `pnpm -r typecheck` — passed.
- `pnpm build` — passed.
- All repository commands above were run with inherited worktree runtime
identity variables removed.

## Risks

- Low operational risk: the change is limited to explicitly isolated
worktree instances.
- Operators who intentionally relied on automatic backups of disposable
worktree databases will now need to run a manual backup or explicitly
manage those files outside the scheduled worktree runtime.
- No schema, migration, API, UI, lockfile, or workflow changes.

> 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 based on GPT-5 (the runtime does not expose a more
specific snapshot ID or context-window value), using reasoning, tool
use, local code execution, and GitHub CLI integration.

## 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
- [x] All Paperclip CI gates are green
- [x] 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-30 17:35:04 -07:00
Devin Foley 276ae3a75d
Harden company import: durable UI, async jobs, integrity guard, batched inserts (#10523)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Company Import/Export (#10507) moves whole companies between
instances as portability bundles
> - Real-world use on a large company (1,418 issues, ~10.6k comments)
surfaced a cluster of related failures: the import took hours and the
browser connection died while the server kept running, a retry silently
produced a second partial import, the progress/error UI gave no durable
signal, and a cloud-tenant user couldn't even open the companies
afterward
> - Root cause of the slowness: importBundle inserted every issue,
comment, and document as a separate round-trip to a network Postgres —
an N+1-over-network pattern
> - This pull request hardens the whole import path: durable
progress/error UI, an async server-side job so imports survive dropped
connections (with a duplicate-submit guard), a fail-closed guard against
incomplete payloads, and batched inserts that cut a large import from
hours to minutes
> - The benefit is that migrating a real, large company actually
completes, is legible while it runs, and can't half-import twice

## Linked Issues or Issue Description

- Refs #10507 (the Import/Export feature this hardens). Supersedes
#10513 (the progress/error-UI piece, folded in here). No open issue;
problem described above (large-company import: slow, connection-fragile,
silently duplicable, opaque UI).

## What Changed

- **Batched inserts (perf):** importBundle pre-generates entity ids in
JS and inserts in chunked multi-row statements, so children no longer
wait on parents' generated ids. A 1,418-issue import drops from ~15,600
insert statements to **82** (190×); benchmark below. Import semantics —
collision handling, pause-on-import,
label/blocker/monitor/attachment/embedded-asset handling, blob sha
verification — are unchanged (full portability suite green).
- **Async import jobs for board sessions:** the existing cloud-tenant
async job path opens to board sessions with per-actor job keys; the
import page submits, polls, and resumes watching after a reload or
dropped connection instead of holding one fragile request. A
non-terminal job blocks a duplicate submit (409 returns the running
job), preventing the double-import.
- **Fail-closed completeness guard:** an optional `expectedFileCount` on
inline imports; the server rejects (422 `import_payload_incomplete`) a
body carrying fewer files than declared, so a re-framed/short payload
fails loudly instead of half-importing.
- **Durable progress/error UI (was #10513):** persistent progress panels
with size-aware copy, persistent error panels with retry guidance, and
inline explanation when the preview button is disabled;
request-lifecycle guards so stale previews/imports can't publish or
detach.

## Verification

- `pnpm -r` typechecks (shared, server, ui) clean.
- `company-portability.test.ts` (76) +
`company-portability-routes.test.ts` (30) green — the import correctness
net — plus new `CompanyImport.test.tsx` async/resume/409 coverage and a
new batching regression test (a 50-issue import issues <50 issue-insert
statements; rows land unchanged).
- **Batching benchmark (embedded Postgres):** at 1,418 issues × 7
comments × 1 doc — 82 insert statements vs ~15,598 one-per-row (190×),
~1s wall-clock; a row-verifying run at that scale imports all 1,418
issues / 9,926 comments / 1,418 documents with unique identifiers and no
warnings (no rows dropped by chunking). Over a network DB the round-trip
reduction is the hours→minutes lever.
- What is NOT directly measured here: wall-clock against a real network
Postgres (that happens on a staging deploy); the local timing is
network-free.

## Risks

- Batching is the load-bearing change: it rewrites the import write
path. Mitigated by the unchanged 106-test correctness suite, a new
scale/row-integrity test, and per-writer transactions (a failure rolls
back its table group; not a single outer transaction across writers —
noted, correctness preserved).
- Async jobs are in-memory (lost on server restart → pollers 404 and can
resubmit); matches the pre-existing cloud-tenant job semantics.
- `expectedFileCount` is optional (older callers unaffected); over-count
is allowed, only under-count fails closed.

## Model Used

- Claude Fable 5 (`claude-fable-5`, Anthropic), Claude Code CLI,
extended thinking + tool use; implementation across Fable 5 subagents
with live diagnosis against a running instance.

## 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 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
2026-07-30 16:52:58 -07:00
Devin Foley 1c52f02d34
Let cloud tenant sessions reach companies they hold memberships in (#10524)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - On Paperclip Cloud, each stack authenticates its users to the tenant
app through trusted headers (`resolveCloudTenantActor`), which seed a
primary company for the stack
> - That actor was pinned to exactly one company — the seeded primary —
regardless of any other companies the user actually holds a membership
in
> - Companies created later (via the import flow, or company creation)
write real membership rows for the user, but the pinned actor ignored
them, so those companies showed up in listings yet returned "User does
not have access to this company" when opened
> - This pull request unions the pinned primary with the user's own
active membership rows, exactly as a locally authenticated session
already does
> - The benefit is that a Cloud user can reach every company they belong
to — most visibly, a company they just imported

## Linked Issues or Issue Description

- Refs #10507 (Import/Export — imported companies were unreachable on
Cloud stacks). No open issue; bug described above (companies visible in
listing but unreachable; expected: reachable when the user holds an
active membership).

## What Changed

- Extracted the session path's own active-membership query into
`loadActiveUserCompanyMemberships(db, userId)` (single-sourced; the
session path now calls it too).
- `resolveCloudTenantActor` unions its result with the pinned primary:
`companyIds = [primary, ...others]`, memberships likewise, primary
first. Strictly per-user; a membership-read failure degrades to
primary-only (mirrors the existing fail-closed owner-elevation pattern).
No change to owner instance-admin elevation, grant seeding, the stale
instance-admin purge, or trusted-header validation.
- Grants are seeded at membership creation across all flows (company
create, invite/join, import), not per request — so no extra seeding was
added here.

## Verification

- `@paperclipai/server` typecheck clean.
- `cloud-tenant-actor.test.ts` (+ union / other-user-excluded /
inactive-excluded / no-rows-identical cases),
`auth-session-route.test.ts` (route-level: trusted headers reach a
unioned company through `assertCompanyAccess`), plus agent-auth,
authz-company-access, cross-company-authz, portability-routes — 83 tests
green.

## Risks

- Low and tightly scoped: only widens a Cloud actor's reachable
companies to those it already holds active memberships in; users without
extra memberships, other users' rows, and owner elevation are all
unaffected. Read failure fails closed to primary-only.

## Model Used

- Claude Fable 5 (`claude-fable-5`, Anthropic), Claude Code CLI,
extended thinking + tool use.

## 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 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
2026-07-30 16:36:50 -07:00
Sam 39666aa906
fix(server): clarify execution policy decision comments (#9105)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Execution policies let issues move through enforced review and
approval stages before work is considered done.
> - Active reviewers and approvers must include decision rationale when
approving or requesting changes.
> - The server already requires the decision comment to arrive in the
same `PATCH /api/issues/:id` request as the status change.
> - When callers post a normal comment first and then send a status-only
`PATCH`, the existing 422 only says a comment is required.
> - This pull request keeps the atomic decision behavior but makes the
API error and docs explicit about the same-request requirement.
> - The benefit is that agents and API clients can recover immediately
by sending `{ status, comment }` together instead of dead-ending on an
ambiguous validation error.

## Linked Issues or Issue Description

Fixes #9049.

Duplicate/related search completed before implementation: searched open
PRs for `9049`, `Approving a review or approval stage requires a
comment`, `Requesting changes requires a comment`, and `same PATCH`. I
did not find a direct open PR for the same error-message/docs fix.
Related PRs found but not duplicates: #8302 documents cross-agent review
gates in the skill API reference, and #5487 covers human approval UI.

## What Changed

- Expanded execution-policy 422 messages for approve and request-changes
decisions to say the comment must be included in the same `PATCH`
request and prior comments are not considered.
- Tightened unit coverage so the comment-required branches assert the
new actionable guidance.
- Documented the same-request `{ status, comment }` decision shape in
the execution policy guide, issue API reference, and agent communication
guide.

## Verification

- `CI=true corepack pnpm --filter @paperclipai/server exec vitest run
src/__tests__/issue-execution-policy.test.ts --reporter verbose`
- `CI=true corepack pnpm --filter @paperclipai/plugin-sdk exec node
../../../scripts/ensure-plugin-build-deps.mjs`
- `CI=true corepack pnpm --filter @paperclipai/server exec tsc --noEmit`
- `git diff --check`

## Risks

Low risk. This does not change execution-policy state transitions or
relax the atomic decision guard; it only makes the existing requirement
explicit in errors and docs.

> 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 (GPT-5 Codex), operating in Codex desktop with repository
file access, shell validation, and GitHub CLI workflow. Exact context
window size is not surfaced by this environment.

## 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: Sami Rusani <sr@samirusani>
2026-07-30 16:26:35 -07:00
Dotta fcf66f3a91
feat(skills): add managed skill rename API (#9688)
## Thinking Path

> - Paperclip is the open source control plane people use to manage
AI-agent companies.
> - Company skills are reusable capabilities that operators install,
edit, assign, and materialize for agents.
> - Managed local skills currently lack a safe backend operation for
changing their display name and canonical slug/key together.
> - Treating rename as an ordinary save can leave duplicate records,
stale runtime materializations, or agent assignments pointing at the old
key.
> - This pull request adds a company-scoped managed-skill rename
contract, service operation, and REST endpoint with focused
authorization and activity logging.
> - The benefit is an atomic-enough, recoverable rename path that keeps
disk state, database identity, and agent skill assignments synchronized.

## Linked Issues or Issue Description

- Refs #2121
- Problem: managed company skills need a dedicated rename operation
rather than save-time duplication behavior.
- Expected behavior: renaming a managed skill updates its name, slug,
key, source directory, frontmatter, runtime materialization, and
assigned-agent references while preserving version pins.

## What Changed

- Added shared request/result types and Zod validation for managed skill
rename requests.
- Added `POST /api/companies/:companyId/skills/:skillId/rename` with
`skills.edit` policy checks and `company.skill_renamed` activity
logging.
- Restricted renames to Paperclip-managed local skills and added slug,
key, and target-directory conflict handling.
- Moved the managed directory, rewrote only the `SKILL.md` frontmatter
name, updated the database row, and rolled filesystem changes back when
persistence fails.
- Rewrote assigned agents' desired-skill keys while preserving pinned
version IDs and removed stale runtime materialization.
- Added focused route and service coverage for success, no-op, name-only
changes, conflicts, unsupported sources, assignment rewrites,
rollback-sensitive behavior, and runtime cleanup.
- Rejected multiline rename names before they can inject extra
`SKILL.md` frontmatter fields.

## Verification

- `pnpm exec vitest run
server/src/__tests__/company-skills-service.test.ts
server/src/__tests__/company-skills-routes.test.ts` — 106 tests passed.
- `pnpm --filter @paperclipai/shared typecheck` — passed.
- `pnpm --filter @paperclipai/server typecheck` — passed.

## Risks

- Filesystem and database updates cannot share one native transaction;
the service stages filesystem changes and explicitly restores the
original directory and markdown when the database transaction fails.
- Renames intentionally reject catalog, remote, project-scanned, and
unmanaged local skills to avoid changing identities owned by external
sources.
- No database migration is required; the endpoint updates existing
company-skill and agent configuration fields.

> 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 coding agent (exact underlying model ID and
context-window size were not exposed to this runtime), with reasoning,
repository tool use, code execution, and test execution. The rescued
source commit also records assistance from Claude Opus 4.8.

## 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
- [x] All Paperclip CI gates are green
- [x] 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-30 15:29:31 -07:00
Devin Foley 187a90b7bc
Add opt-in in-flight run-log mirroring with graceful-shutdown flush (#10512)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The run-log store records each agent run's output and can mirror
completed logs to S3-compatible object storage
> - The mirror uploads only on finalize, so a server restart mid-run
loses the whole in-flight log
> - Deployments and crashes are routine on ephemeral hosts, and lost run
output makes failed runs impossible to debug
> - This pull request adds an opt-in throttled mirror for still-running
logs plus a graceful-shutdown flush
> - The benefit is that a restart mid-run keeps the log tail up to the
last mirror interval, and an orderly restart keeps everything

## Linked Issues or Issue Description

No public issue exists — describing the feature inline (per the feature
request template).

**Subsystem affected**
server/ — REST API & orchestration services

**Problem or motivation**
`RUN_LOG_S3_BUCKET` gives finished run logs durability, but the mirror
uploads only on finalize. A run that is still writing when the server
restarts leaves nothing in object storage. On hosts with ephemeral disks
the local file is gone too, so the run's output is lost end to end and
failed runs cannot be debugged.

**Proposed solution**
Mirror the in-flight log to the same object key on a throttled cadence
(`RUN_LOG_S3_INFLIGHT_MIRROR_SECONDS`), and flush dirty tails during
graceful shutdown. Keep it opt-in so existing deployments see zero new
upload traffic unless they ask for it.

**Alternatives considered**
Per-append uploads (rejected: one PUT per output chunk is hostile to S3
endpoints and run latency). Chunked part objects with read-time
stitching (rejected: complicates the read path, and S3 multipart minimum
part sizes do not fit small tails). Persistent volumes (rejected
upstream already: the data dir is deliberately an emptyDir in hardened
cloud_tenant deployments).

**Roadmap alignment**
Not on ROADMAP.md; extends the existing run-log durability mirror
without changing any default behavior.

**Additional context**
Ranged reads already serve partial objects like a live tail, so the read
path needs no change; finalize overwrites the mirror with the complete
file.

**Related PRs (dedup search):** the finalize-only S3 mirror landed
previously and this extends it; no duplicate or competing PR found for
in-flight run-log mirroring.

## What Changed

- `server/src/services/run-log-store.ts`: new opt-in `inflightMirrorMs`
on the S3 options (`RUN_LOG_S3_INFLIGHT_MIRROR_SECONDS` env). When set,
appends schedule at most one upload of the current file per interval, to
the same key finalize uses. Ranged reads already serve that key, so a
partial object behaves like a live tail and needs no read-path change.
Finalize retires the in-flight bookkeeping and waits out an upload
already on the wire, so a stale partial can never overwrite a finalized
log. Upload failures warn, re-mark the tail dirty, and retry at most
once per interval.
- `server/src/services/run-log-store.ts`: new `flushInflightMirrors()`
on the store and a module-level `flushInFlightRunLogMirrors()` for the
shutdown path. Both are no-ops when the mirror is off.
- `server/src/index.ts`: graceful shutdown flushes dirty in-flight tails
after the heartbeat run drain, so runs the drain did not finalize
(timeouts, the hot-restart skip path) still persist their output.
- `server/src/services/run-log-store.test.ts`: five new tests —
off-by-default (no uploads before finalize), tail preserved after a wipe
without finalize, throttle coalescing with a single flush upload,
finalize superseding the in-flight mirror and retiring its timer, and
upload failures never breaking appends with recovery on the next flush.

## Verification

- `pnpm vitest run server/src/services/run-log-store.test.ts` — 13
passed (8 existing + 5 new).
- `pnpm vitest run server/src/__tests__/heartbeat-run-log.test.ts
server/src/__tests__/heartbeat-active-run-output-watchdog.test.ts` — 21
passed (consumers of the store, unchanged behavior).
- `pnpm -C server run typecheck` — clean.
- Self-hosted behavior is unchanged unless
`RUN_LOG_S3_INFLIGHT_MIRROR_SECONDS` is set: with the variable unset
there are zero new uploads and the finalize-only mirroring is
byte-identical (asserted by the off-by-default test).

## Risks

- Low. The feature is opt-in; unset env preserves today's behavior
exactly. When enabled, worst case is one extra PUT per interval per
active run, and every upload is best-effort — a failing endpoint warns
and never breaks appends, finalization, or shutdown. The finalize path
awaits any in-flight upload before writing the complete file, closing
the only overwrite race the design introduces. Timers are `unref`ed so
the mirror never keeps the process alive.

## Model Used

Claude Fable 5 (`claude-fable-5`, Anthropic; Claude Code CLI with
extended thinking and tool use; tests executed locally via Vitest).



## 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 (no duplicates found for in-flight run-log mirroring; the
finalize-only mirror landed previously and this extends it)
- [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
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
2026-07-30 14:01:02 -07:00
Devin Foley 916c13501f
Replace host-to-host Cloud Sync with full-fidelity company Import/Export (#10507)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A company accumulates real state — issues, labels, blockers,
documents, work products, monitors, attachments, agents, routines — and
people need to move that state between instances: self-hosted to cloud,
cloud back to self-hosted, or plain backups
> - The experimental, flag-gated Cloud Sync transport (#6548) tried to
solve this host-to-host: the source pushed into a receiver over HTTPS
with a cross-instance consent/token handshake, which required the
destination to be publicly reachable and broke for common self-hosted
topologies (plain-HTTP LAN/VPN origins); the receiver half never landed
upstream at all
> - Meanwhile the portability bundle and the existing export/import
pages already move companies offline with none of those networking
constraints — but silently dropped labels, blockers, issue documents,
work products, monitors, and every attachment
> - This pull request removes the host-to-host transport and makes
Import/Export the single data-movement path: the pages become
first-class company-settings destinations, exports declare exactly what
they do not carry, and bundle schemaVersion 6 now carries all of the
above, with attachments as content-addressed sha256 blobs verified
before a single row is written
> - The benefit is a migration and backup flow that works between any
two instances with no reachability requirements, no cross-instance auth,
and no silent data loss

## Linked Issues or Issue Description

- Refs #6548 — the original Cloud Sync sender this PR supersedes and
removes.
- Related, not duplicates: #1697 (goals in the portability manifest —
orthogonal field addition), #954 (an earlier import/export +
skill-visibility proposal predating the current portability bundle).
- No open issue describes this directly, so in brief (feature-request
shape): **Problem** — moving a company between instances silently lost
labels (imports with label references actually hard-failed), blocker
relations, issue documents, work products, monitor state, and all
attachments, and the alternative Cloud Sync transport required the
destination to be publicly reachable over HTTPS plus a consent
handshake, which failed for typical self-hosted setups. **Desired
behavior** — one Import/Export flow in company settings that produces a
portable bundle carrying all of that data, tells the operator up front
what it cannot carry, imports with automations paused, and offers real
one-click activation afterwards.

## What Changed

- New export fidelity report (`GET
/api/companies/:companyId/export/fidelity`) + an "Export fidelity" panel
on the Export page listing anything a bundle will not include (now only:
approvals, cost history, activity history)
- Imports accept `pauseAutomations`; imported agents and routines land
paused, the import result reports created routines, and the Import page
ends in an activation panel that actually resumes selected
agents/activates routines
- Export and Import pages promoted into the company-settings nav; the
Cloud Upstream wizard, ux-lab page, and API client removed; the old
settings route redirects to Export
- Host-to-host transport removed: upstream-sync/receiver-client routes
and services, CLI `cloud connect`/`cloud push` + keypair store, the
shared upstream transfer contract, and the `enableCloudSync` flag;
migration `0196` drops the two experimental `cloud_upstream_*` sender
tables
- Bundle schemaVersion 6: labels (definitions + per-task names, remapped
by name on import), blocker relations (`blockedBy` slugs,
cycle-tolerant), issue documents (`tasks/<slug>/documents/<key>.md`),
work products (system refs nulled), monitors (notes/scheduledBy
restored, imported un-armed)
- Attachments travel as content-addressed `blobs/<sha256>` entries
(deduped; comment-scoped attachments re-link via comment index); every
blob is hash-verified **before any write**, so a corrupted bundle cannot
leave a partially imported company; both zip codecs now round-trip
extensionless/binary entries byte-exactly; the Import page preflights
the inline body limit and offers continue-without-attachments
- v5 (and older) bundles still import, with an informational warning;
bundles newer than v6 are rejected cleanly
- Docs: board-operator import/export guide, CLI README, README/ROADMAP
updated

## Verification

- `pnpm -r` typechecks (shared, db incl. migration numbering/safety
checks, server, ui, cli) and `pnpm check:token-gates` — clean
- Vitest: full server + shared sweep 4,888 passed / 1 skipped, with the
only 3 failures being pre-existing on `master` (2×
heartbeat-workspace-branch-containment, 1× workspace-runtime auto-port;
reproduced identically with this change stashed); ui + cli suites green;
the embedded-Postgres export-fidelity suite applies the full migration
chain including the new `0196` against a fresh database
- Live end-to-end on a scratch instance: seeded a company with labels, a
blocker pair, an issue document, a work product, a monitor, an agent, a
routine, and two binary attachments (one comment-scoped) → export →
import into a fresh company → labels remapped to new ids, blocker edge
and document restored, monitor un-armed with notes intact, attachments
byte-identical (sha256-compared through the API), agents/routines paused
→ activation panel resumed them; a v5-shaped bundle imported with only
the info warning; flipping one byte in a blob made the import 422 with
**zero** rows created
- Reviewer repro: create a company with a labeled issue + attachment →
Settings → Export → download → Settings → Import on another
company/instance → watch the preview, apply with "start paused", then
activate

## Risks

- Migration `0196` drops
`cloud_upstream_connections`/`cloud_upstream_runs` — experimental tables
behind a default-off flag; their connection/run history is intentionally
discarded
- Breaking removals are all of experimental, flag-gated surface:
`/api/upstream-sync/*` + `/api/cloud-upstreams/*` routes, `paperclipai
cloud connect|push`, and the `enableCloudSync` flag (stale keys in
stored instance settings parse harmlessly)
- Import remains non-atomic on mid-apply errors generally (pre-existing
behavior); the new blob verification specifically moved ahead of all
writes so tampered bundles cannot create partial state
- GitHub-sourced imports do not fetch `blobs/*` and skip attachments
with a warning

## Model Used

- Claude Fable 5 (`claude-fable-5`, Anthropic), via Claude Code CLI with
extended thinking, tool use, and subagent orchestration; implementation
and review split across Fable 5 subagents, with live end-to-end
verification against a running instance

## 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
2026-07-30 11:37:00 -07:00
Nicky Leach a93a74f91a
feat(server): surface referenced-project sync warnings and enable multi-project sync by default (#10473)
## Thinking Path

> - Paperclip helps people manage AI agent work for a company
> - Workspace sync must keep one primary path and any referenced project
paths in step
> - A partial sync must not fail in silence
> - Operators also need a clear signal when the feature uses the new
default path
> - This pull request surfaces referenced-project warnings on the run
and turns the feature flag default on
> - The benefit is better visibility and a live multi-project sync path
by default

## Linked Issues or Issue Description

This pull request completes the multi-project workspace sync go-live
work.

Related pull requests:

- Refs: #10380
- Refs: #10448
- Refs: #10469

## What Changed

- Surface referenced-project warnings on the run when a project drops
during authorization or resolution.
- Record a structured failure reason for each dropped referenced
project.
- Emit one structured log line at run preparation with the requested
count, the synced count, and the failure reasons.
- Flip the workspace sync kill-switch default to on when the env value
is unset.
- Keep the primary workspace path unchanged.

## Verification

- `tsc --noEmit` passed in the server package.
- `heartbeat-referenced-projects.test.ts` and
`heartbeat-project-env.test.ts` passed.
- `workspace-runtime.test.ts` passed.
- `adapter-utils` runtime tests passed for sandbox, command, remote, and
file sync paths.

## Risks

- The new default can expose the feature to more runs if an operator
does not set the env override.
- The new surfaced warnings can change operator visible run output.
- The structured log line can add noise if a run has many referenced
project failures.

## Model Used

- OpenAI Codex, GPT-5, tool use, large context window.

## 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 linked existing issues with `Fixes: #` / `Closes #`
/ `Refs #` or described the issue in-PR following the relevant issue
template
- [x] I have not referenced internal or instance-local Paperclip issues
or links
- [x] My branch name describes the change 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
- [x] All Paperclip CI gates are green
- [x] 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-29 17:12:22 -07:00
Devin Foley 5c5366d0c1
fix(server): honor explicit plugin RPC timeouts (#10460)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Plugin workers connect Paperclip to external runtimes and sandbox
providers.
> - Some adapter heartbeats run a full sandbox session inside one
`environmentExecute` RPC.
> - The worker manager limited every RPC timeout to 15 minutes, even
when the caller gave a longer timeout.
> - This pull request keeps the normal default timeout behavior but
honors explicit caller timeouts.
> - The benefit is that long sandboxed agent sessions can continue past
15 minutes while other safety guards still bound hung work.

## Linked Issues or Issue Description

No public GitHub issue exists for this bug.

### Bug Report

Pre-submission checklist:
- Searched existing open and closed issues and did not find a duplicate.
- Confirmed the bug is reproducible on `master` from the current source
tree.
- Confirmed the error starts in Paperclip timeout handling, not in an
adapter provider or local configuration.

What happened?
- A sandbox-backed adapter heartbeat can run a full agent session inside
one `environmentExecute` plugin RPC.
- The plugin worker manager capped every RPC timeout at 15 minutes.
- The cap also applied when the caller passed a longer explicit timeout
for an execute-style call.
- A long sandbox command could fail before the adapter budget expired.

Expected behavior:
- Ordinary plugin RPC calls should keep the normal 30-second default
timeout.
- The default timeout path should still have a 15-minute maximum.
- A caller-supplied positive finite timeout should be honored, including
values above 15 minutes.

Steps to reproduce:
1. Use a plugin environment driver that calls `environmentExecute` with
an explicit timeout above 15 minutes.
2. Run a command that stays active longer than 15 minutes and remains
inside the adapter budget.
3. Observe that the worker manager times out the RPC at 15 minutes
before this fix.
4. Run the same path after this fix and observe that the explicit
timeout is used.

Paperclip version or commit:
- Reproduced from the current `master` line before this change.

Deployment mode:
- Local dev or self-hosted server with sandbox-backed execution.

Installation method:
- Built from source.

Agent adapter(s) involved:
- Codex.
- Custom or external plugin adapter.
- Core plugin worker timeout handling.

Database mode:
- Not database-related.

Access context:
- Agent execution context.

Node.js version:
- Not version-specific.

Operating system:
- Not OS-specific.

Relevant logs or output:
```shell
RPC call "environmentExecute" timed out after 900000ms
```

Relevant config:
- Not config-related.

Additional context:
- Execute-style sandbox calls already have adapter inactivity monitors,
platform silent-run checks, and provider command timeouts. This PR
removes the unintended worker-manager clamp only for explicit positive
finite caller timeouts.

Privacy checklist:
- Reviewed all pasted output for PII, user paths, API keys, tokens,
company names, and internal instance links.

Duplicate search:
- Searched open PRs and open issues in `paperclipai/paperclip` for
`environmentExecute timeout`, `MAX_RPC_TIMEOUT_MS`, and
`plugin-worker-manager timeout`.
- Searched the same terms in `HenkDz/paperclip`.
- Found no matching open PRs or issues.
- Compared this patch-id against my open PRs in `paperclipai/paperclip`;
no match was found.

## What Changed

- Added `resolveRpcCallTimeoutMs()` to keep explicit positive finite
timeouts intact.
- Kept the 15-minute maximum only for the default timeout path.
- Updated `callInternal()` to use the new resolver.
- Added unit tests for explicit long timeouts, default timeout clamping,
fractional values, and invalid explicit values.
- Clarified why notification invocation scopes still use the 15-minute
TTL.

## Verification

- `corepack pnpm install --frozen-lockfile`
- `corepack pnpm --filter @paperclipai/plugin-sdk ensure-build-deps`
- `corepack pnpm --filter @paperclipai/server exec vitest run
src/__tests__/plugin-worker-manager.test.ts`
- `corepack pnpm --filter @paperclipai/server exec tsc --noEmit`
- `git diff --check 9574cad3e8 HEAD`
- GitHub PR checks on `bf7bfd0d`: all passing; Storybook visual
regression skipped by workflow.
- Greptile on `bf7bfd0d`: 5/5, no blocking failure remains; prior P2
thread resolved.

## Risks

Low risk. The change affects RPC timeout resolution in the plugin worker
manager. Ordinary plugin calls still use the 30-second default and the
default path is still capped at 15 minutes. Callers that pass explicit
long timeouts now own that budget. Adapter inactivity monitors and
platform silent-run safety checks still bound hung runs.

## Model Used

OpenAI Codex, GPT-5-based coding agent, with shell and GitHub CLI tool
use.

## 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
- [x] All Paperclip CI gates are green
- [x] 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-29 14:27:23 -07:00
Nicky Leach 15ce70dc18
feat(server): thread plural referenced-project workspaces through run prep (#10448)
## Thinking Path

> - Paperclip coordinates work for autonomous companies.
> - A run needs a workspace view before execution starts.
> - That view now needs to cover one anchor project and more referenced
projects.
> - Those extra workspaces must stay separate and must not change the
anchor path when the feature stays off.
> - This PR threads plural workspace data through run prep behind a
default-off kill switch.
> - It also keeps extra project workspaces isolated and makes the
realization contract round-trip the new shape.
> - The benefit is safer run prep for referenced projects without
changing the current default path.

## Linked Issues or Issue Description

This PR does not link a public GitHub issue.
It follows the internal run-prep task for plural referenced-project
workspaces.

Problem:
- Run prep resolves the anchor project today, but it does not yet carry
each referenced project into the run workspace view.
- That gap blocks runs that need a second repo or sibling project during
preparation.

Proposed solution:
- Thread a plural workspace result through run prep.
- Keep the anchor path unchanged when the kill switch is off.
- Resolve each referenced project into its own managed checkout
directory when the flag is on.

Alternatives considered:
- Keep one shared workspace and layer the extra repos into it. Rejected
because it would blur isolation and make failures harder to bound.
- Upload the extra workspaces immediately. Rejected because this PR only
prepares the data path.

Roadmap alignment:
- This change sits in the workspace and sandbox path.
- It matches the roadmap work on workspace strategy and cloud or sandbox
agents.

## What Changed

- Added `additionalWorkspaces[]` to the run workspace result.
- Split workspace resolution into an anchor path and an optional
referenced-project path behind `PAPERCLIP_MULTI_PROJECT_WORKSPACE_SYNC`.
- Kept per-project failure isolation so one bad clone does not stop the
run.
- Keyed managed workspace directories by `projectId` so sibling
workspaces stay separate.
- Added `additionalSources[]` to the workspace realization request and
kept read and write paths backward compatible.
- Added tests for the anchor-only path, the new workspace shape, and the
per-project directory rule.

## Verification

- `pnpm --filter @paperclipai/server run typecheck`
- `pnpm --filter @paperclipai/shared run typecheck`
- `pnpm --filter @paperclipai/server exec vitest run
src/__tests__/heartbeat-project-env.test.ts` 21/21
- `pnpm --filter @paperclipai/server exec vitest run
src/__tests__/workspace-runtime.test.ts` 98/98

## Risks

Low risk. The new path stays behind a default-off kill switch, so the
anchor flow does not change when the flag is off.
The main risk is a bad referenced project clone. That case now drops
only the affected project and keeps the run alive.
The shared type change also needs every consumer to use the new array
field where extra workspaces matter.

## Model Used

OpenAI Codex (GPT-5; exact internal model ID not exposed in this
environment; tool use enabled)

## 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
- [x] All Paperclip CI gates are green
- [x] 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-29 14:27:05 -07:00
Nicky Leach d51f42ed64
test(server): drain heartbeat runs to quiescence in sibling suite teardown (#10464)
## Thinking Path

> - Paperclip uses heartbeats to run work.
> - Test suites share heartbeat run state during teardown.
> - Late heartbeat work can race shared table deletes.
> - That race can deadlock or fail foreign key checks.
> - The primary suite already uses a drain helper to wait for
quiescence.
> - This pull request reuses that helper in the sibling suites that
share the race.
> - The benefit is stable teardown and fewer flake failures.

## Linked Issues or Issue Description

No public GitHub issue exists for this change.
Refs: #10450
This pull request reuses the quiescence drain from the primary suite.

## What Changed

- Added `server/src/__tests__/helpers/drain-heartbeat-runs.ts`.
- Reused the shared helper in `low-trust-red-team-routes.test.ts`.
- Applied the drain to the eight sibling suites that share the race.
- Kept the existing test intent unchanged.

## Verification

- `git log --oneline
origin/master..origin/test/heartbeat-teardown-quiescence-drain-sweep`
- `git diff --stat
origin/master...origin/test/heartbeat-teardown-quiescence-drain-sweep`
- Existing local test evidence in the handoff shows the primary suite
and the guarded suites pass.
- The handoff also records a stress loop with no `40P01` or `23503`
errors.

## Risks

- Low risk. The change touches test teardown only.
- The helper waits for active runs to drain. A new real background
execution path may need the same guard.

## Model Used

OpenAI GPT-5, tool-use and code execution enabled.

## 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 linked existing issues with `Fixes: #` / `Closes #`
/ `Refs #` or described the issue in-PR following the relevant issue
template
- [x] I have not referenced internal or instance-local Paperclip issues
or links
- [x] My branch name describes the change 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, or no
docs update was required for this test-only change
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] 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-29 14:23:12 -07:00
Nicky Leach 7083c275c8
refactor(sandbox): retire the dead noProfile flag from the exec path (#10461)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The sandbox exec path starts agent commands and passes runtime
options to the server and plugin layers
> - This path kept a noProfile flag after the exec wrappers stopped
sourcing a login profile
> - The flag no longer changed behavior, so it left dead API surface in
the protocol and runtime helpers
> - This pull request removes that dead flag from the plugin protocol,
the server drivers, and the managed-runtime helpers
> - It also updates the tests and points the agent runtime README at the
sandbox requirements file
> - The benefit is a smaller and clearer exec-path contract with no
behavior change

## Linked Issues or Issue Description

- No public GitHub issue exists.

### What happened?

The sandbox exec path kept a `noProfile` field after the exec wrappers
stopped sourcing a login profile.

### Expected behavior

The plugin protocol, server drivers, and managed-runtime helpers should
not expose or forward a dead field.

### Steps to reproduce

1. Run a managed-runtime command through the sandbox exec path.
2. Inspect the protocol payload and runtime helper inputs.
3. Observe that `noProfile` is present even though it no longer changes
behavior.

### Paperclip version or commit

`60c7da86fc7a6c1dbf37bbcd86e25ecaaff01607`

### Deployment mode

Built from source (pnpm dev / pnpm build)

### Additional context

This pull request removes the dead field, updates the affected tests,
and updates the README note for the sandbox profile path.

## What Changed

- Removed noProfile from the plugin protocol and the server exec-path
call sites.
- Updated the managed-runtime helpers to use the narrower exec-path
contract.
- Updated the affected tests and added the README pointer to
SANDBOX-REQUIREMENTS.md.

## Verification

- `git grep -n "noProfile" -- packages/ server/` returns zero matches.
- `tsc --noEmit` passed for `@paperclipai/adapter-utils`,
`@paperclipai/plugin-sdk`, and `@paperclipai/server`.
- `command-managed-runtime.test.ts` passed: 22/22.
- `environment-runtime.test.ts` passed: 24/24.

## Risks

- Low risk. The flag was already a no-op.
- A hidden external caller may still send the removed field.

## Model Used

- OpenAI GPT-5, tool-enabled.

## 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
- [x] All Paperclip CI gates are green
- [x] 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-29 14:21:57 -07:00
Devin Foley 7a5a217d60
fix(server): gate sandbox/ssh execution targets by shared remote-managed adapter capability (#10459)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Paperclip can run an agent in a remote-managed environment, such as
a sandbox provider or an SSH host.
> - The server resolves an execution target for each run. The resolver
kept its own hardcoded list of allowed adapters.
> - The shared capability metadata in
`packages/shared/src/environment-support.ts` already defines which
adapters support remote-managed environments. The environment selector
and the capabilities API use it.
> - The two lists drifted. The UI offered sandbox environments to Grok
Build (`grok_local`) agents, but the resolver refused them at run time.
> - This pull request makes the resolver use the shared capability check
for both the sandbox gate and the SSH gate.
> - The benefit is one source of truth. The UI and the runtime now agree
on which adapters can use remote-managed environments.

## Linked Issues or Issue Description

No public GitHub issue exists for this bug. Inline description per the
bug report template:

**What happened?**

A Grok Build (`grok_local`) agent was assigned a sandbox environment (a
Daytona provider). The UI allowed the assignment. Every run and
primary-model test then failed with the warning: `Adapter "grok_local"
is not allowed in "<environment>" environments.`

**Expected behavior**

An adapter that the environment selector offers for a sandbox
environment must also pass the runtime gate. The Grok Build run must
start in the sandbox.

**Steps to reproduce**

1. Create a sandbox environment (for example, with a Daytona provider
plugin).
2. Create an agent that uses the `grok_local` adapter.
3. Set the agent's environment to the sandbox environment. The UI
accepts this.
4. Run the agent, or run the primary-model test. The run fails with the
adapter-not-allowed warning.

**Paperclip version or commit**

Reproduced on `master` at `0edb742f8d`.

**Deployment mode**

Local instance with a remote sandbox provider plugin. The same gate also
applies to SSH environments.

## What Changed

- `resolveEnvironmentExecutionTarget` in
`server/src/services/environment-execution-target.ts` now gates the
sandbox path with the shared
`adapterSupportsRemoteManagedEnvironments()` helper. Before, it used a
hardcoded six-adapter list that did not include `grok_local`.
- The SSH path in the same file now uses the same shared helper.
- New regression tests in
`server/src/__tests__/environment-execution-target.test.ts`: sandbox
target resolution for every remote-managed adapter (including
`grok_local`), SSH target resolution for `grok_local`, and the null path
for an adapter without remote-managed support.

## Verification

- Run `node_modules/.bin/vitest run
server/src/__tests__/environment-execution-target.test.ts`. All 10 tests
pass, including the 3 new ones.
- Confirm `grok_local` is in the `REMOTE_MANAGED_ADAPTERS` set in
`packages/shared/src/environment-support.ts`. The resolver now reads the
same set.
- On a live local instance with this fix, a `grok_local` agent assigned
to a Daytona sandbox environment no longer produces the
adapter-not-allowed warning.

## Risks

Low risk. The change routes two hardcoded checks through existing shared
capability metadata. Behavior changes only where the lists had drifted:
`grok_local`, and any future adapter added to the shared set, can now
resolve sandbox and SSH execution targets. Adapters outside the shared
set still return `null`.

## Model Used

Claude Fable 5 (`claude-fable-5`) by Anthropic, with extended thinking
and tool use, running in Claude Code.

## 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-29 12:59:51 -07:00
Devin Foley 78f8c6c3d4
Recover managed bundled plugin workers on demand (#10429)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Managed deployments can auto-provision bundled sandbox-provider
plugins so cloud or remote execution environments appear in the board UI
> - In a multi-service deployment, several server processes can share
one database and boot concurrently
> - A sibling process can create a bundled plugin row while the web
process sees it before it reaches `ready`
> - The web process correctly avoids clobbering the existing row, but
its startup `loadAll()` can miss the plugin and never start that worker
locally
> - The environments capabilities route then filters out the sandbox
provider because the plugin is ready in the database but not running in
the web process
> - This pull request adds a narrow managed-bundle recovery path that
lazily starts the missing worker when the capabilities route sees a
ready managed bundled plugin
> - The benefit is that the sandbox provider becomes visible after the
install finishes, without requiring a web-process restart

## Linked Issues or Issue Description

- No public GitHub issue found for this exact deployment race.
- Related broad plugin runtime context: Refs #432.

Bug description:

- What happened: in a managed multi-service deployment with shared
database state and bundled plugin auto-install enabled, the API-serving
process can skip a plugin row while it is still `installed`, run startup
plugin loading before that row becomes `ready`, and then permanently
omit the sandbox provider from environment capabilities.
- Expected behavior: once the managed bundled plugin row reaches
`ready`, the API-serving process should be able to start the plugin
worker and include its sandbox provider without a restart.
- Steps to reproduce: boot a web process and a sibling worker process
concurrently; have the sibling create the bundled plugin row and
transition it to `ready` after the web process has already skipped
auto-install and run `loadAll()`.
- Deployment mode: managed multi-service deployment with shared database
state and `plugins.autoInstall` configured.

## What Changed

- Added a managed bundled plugin worker recovery helper that
single-flights lazy `loadSingle()` starts and only allows configured
managed bundled plugin keys.
- Passed the managed recovery hook into the environments capabilities
route.
- Updated `listReadyPluginEnvironmentDrivers()` to attempt bounded
recovery for ready managed bundled plugins whose worker is missing in
the current process, and only for plugins that actually declare a
`sandbox_provider` environment driver.
- Made request-time recovery use `loadSingle(id, { markErrorOnFailure:
false })` so a local activation failure in one process never transitions
the shared plugin row to `error` (a sibling process may be running the
plugin successfully).
- When error writes are suppressed and activation fails after the worker
was spawned, the loader now tears down the partially-registered local
runtime (scheduler registration, event subscriptions, agent tools,
worker process) instead of leaving a half-activated worker lingering;
the teardown steps are factored out of `unloadSingle()` into a shared
helper.
- A failed recovery attempt now discards the crashed/stopped handle it
left registered in the worker manager (a worker that dies during
initialize is killed without a scheduled restart), so later capability
requests can retry recovery instead of being blocked by the
handle-presence gate until a process restart. Handles in
starting/running/backoff states are left to the worker manager's own
lifecycle; recovery only ever starts when no handle existed, so no
pre-existing worker can be affected.
- Added a regression test suite covering the installed-to-ready race,
allowlist behavior, the driver-kind gate, existing worker handles,
concurrent single-flight recovery, bounded slow recovery attempts,
suppressed shared error-state writes, partial-runtime teardown on late
activation failure, and retry after a dead handle is discarded.

## Verification

- `pnpm vitest run
src/__tests__/plugin-environment-driver-ready-recovery.test.ts` (in
`server/`) passed: 10 tests.
- `pnpm --filter @paperclipai/server typecheck` passed.

## Risks

- Low risk for self-hosted single-process deployments because lazy
recovery is only wired when managed plugin auto-install config is
present; with no managed config the capabilities route takes the exact
pre-change code path.
- The capabilities route can wait briefly while attempting recovery; the
attempt is bounded and defaults to 2 seconds.
- Failed recovery keeps the prior behavior of omitting the provider
until a later successful worker start, and now also cleans up any
partially-started local worker so retries begin from a clean slate.

## Model Used

- Initial implementation: OpenAI GPT-5 via Codex local coding agent,
with repository tool use and command execution.
- Review-feedback follow-ups (driver-kind gate, partial-runtime
teardown, expanded regression tests): Claude Fable 5 (claude-fable-5)
via Claude Code, with repository tool use and command execution.

## 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
- [x] All Paperclip CI gates are green
- [x] 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-29 12:50:16 -07:00
Nicky Leach 9574cad3e8
test(server): drain heartbeat runs to quiescence before low-trust teardown (#10450)
## Thinking Path

> - Paperclip manages AI work through tasks, comments, and heartbeats
> - A heartbeat wake can register after a test body ends
> - The low-trust red-team route suite tears down data while that wake
can still run
> - Teardown can then lock `issues` and `heartbeat_runs` in opposite
order and deadlock
> - This pull request adds a drain that waits for heartbeat runs to
reach quiescence before teardown
> - The benefit is stable test teardown without removing coverage

## Linked Issues or Issue Description

The serialized low-trust red-team route suite can deadlock in
`afterEach` teardown.
A heartbeat wake can register after the test body ends.
Teardown can then delete `heartbeat_runs` while the wake still writes
issue tables.
This change waits until no run is queued or running before any delete.

## What Changed

- Added `drainHeartbeatRunsToQuiescence` for test teardown.
- Called the drain first in the low-trust red-team route suite
`afterEach` path.
- Kept the change test-teardown only.

## Verification

- The author handoff reports `tsc -p server/tsconfig.json --noEmit` as
clean.
- The author handoff reports 60 of 60 stress-loop runs with zero
deadlocks.
- `pnpm --filter @paperclipai/server typecheck` could not run here
because this workspace lacks `node_modules/typescript/bin/tsc`.

## Risks

- Low risk.
- The change only affects test teardown.
- If a wake never reaches registration, the drain can wait longer than
expected.
- The loop re-checks the run table until no run is queued or running.

## Model Used

OpenAI GPT-5, tool-use, 256k context.

## Checklist

- [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 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
- [x] All Paperclip CI gates are green
- [x] 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-29 12:05:13 -07:00
Nicky Leach 0edb742f8d
test(server): relocate referenced-project run-prep tests to a dedicated file (#10446)
## Thinking Path

> - Paperclip keeps server tests that protect run-prep and issue service
behavior
> - The issue service test file now mixes issue service tests with
heartbeat run-prep tests
> - That mix makes the file harder to scan and harder to keep focused
> - The run-prep suites belong with the other heartbeat tests
> - This pull request moves those suites into
`heartbeat-referenced-projects.test.ts`
> - The benefit is a smaller issue service test file and a clearer home
for heartbeat tests

## Linked Issues or Issue Description

This PR has no public GitHub issue. It moves the referenced-project
run-prep suites into a dedicated heartbeat test file. The issue service
test file keeps only issue service tests.

## What Changed

- Moved the `resolveRunReferencedProjects` suite into
`server/src/__tests__/heartbeat-referenced-projects.test.ts`
- Moved the multi-project workspace sync kill-switch test into the same
file
- Left `server/src/__tests__/issues-service.test.ts` with issue service
coverage only

## Verification

- `pnpm exec tsc --noEmit`
- `pnpm exec vitest run
src/__tests__/heartbeat-referenced-projects.test.ts`
- `pnpm exec vitest run src/__tests__/issues-service.test.ts`
- `git log --oneline origin/master..HEAD` shows one commit
- `git diff --stat origin/master...HEAD` shows only the two test files

## Risks

- Low risk. This change moves tests only and does not change product
code.

## Model Used

OpenAI Codex, GPT-5, tool use enabled, local shell execution enabled.

## 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
- [ ] All Paperclip CI gates are green
- [x] 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-29 10:22:01 -07:00
Nicky Leach 11273c18d6
feat(server): resolve per-project-authorized referenced-project set for run prep (#10380)
## Thinking Path

> - Paperclip is the control plane for autonomous AI companies
> - Run prep needs to know which referenced projects belong in a run
without breaking company boundaries
> - The anchor project must keep its existing authorization path, while
additional mentioned projects must be checked independently and fail
closed if access is denied or unknown
> - This pull request adds a helper that computes the deduped,
company-scoped referenced-project set for run prep and warns when a
project is rejected
> - It also introduces an off-by-default kill-switch so downstream
consumers can adopt the set safely
> - The benefit is safer multi-project run preparation without widening
access beyond what the run actor is already allowed to read

## Linked Issues or Issue Description

This PR does not rely on a public GitHub issue. The change is
self-contained and follows the feature-request style description below
so reviewers can evaluate it without leaving the PR.

**Problem / motivation**
- Run prep needs to assemble a referenced-project set that includes the
anchor project plus additional @-mentioned projects.
- Additional projects must be authorized independently and rejected
projects must not widen access.
- The result should be safe to merge inertly behind a default-off
kill-switch until downstream consumers opt in.

**Proposed solution**
- Add `resolveRunReferencedProjects(issueId, anchorProjectId, opts)` in
`server/src/services/heartbeat.ts`.
- Compute a deduped company-scoped set with the anchor project first and
additional mentions admitted only after a fail-closed `project:read`
authorization check.
- Drop missing, foreign-company, denied, or errored projects and append
run warnings when they are rejected.
- Keep the feature inert behind a default-off kill-switch until
downstream workspace resolution is wired to consume it.

**Alternatives considered**
- Reusing company membership alone was rejected because it would
over-admit projects and widen access.
- Including all mentioned projects without per-project authorization was
rejected because it would bypass the existing access model.

**Roadmap alignment**
- This is Phase 1 only: the helper is computed but not yet consumed
downstream, so the merge is inert until a later phase turns the flag on.

## What Changed

- Added `resolveRunReferencedProjects(issueId, anchorProjectId, opts)`
in `server/src/services/heartbeat.ts`.
- Enforced company scoping, deduplication, fail-closed authorization,
and warning emission for additional referenced projects.
- Added a configurable cap for the additional referenced-project set.
- Added tests covering allowed, denied, foreign-company, thrown-auth,
dedupe, and overflow cases.
- Added a default-off kill-switch for downstream consumption of the
computed set.

## Verification

- `tsc --noEmit`
- `server/src/__tests__/issues-service.test.ts` now passes 117/117
- `git log --oneline origin/master..HEAD` shows only the expected single
commit on this branch

## Risks

- The new helper is computed but not yet consumed by workspace
resolution, so behavior only changes once downstream code is wired to
it.
- The authorization path for additional referenced projects is stricter
than before, so any unexpected access gap will surface as a dropped
project plus warning.

## Model Used

OpenAI Codex, GPT-5, tool-use enabled, 128k context.

## 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
- [x] All Paperclip CI gates are green
- [x] 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-29 09:48:54 -07:00
Nicky Leach ca92f727c5
ci: publish the cloud image in its own parallel job (#10408)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The CI workflow is responsible for producing and publishing the
Docker images that power Paperclip deployments
> - The cloud image publish was previously coupled to the stock publish
job, so a failure or delay in one path could gate the other
> - That coupling makes the release pipeline less resilient than it
needs to be
> - This pull request gives the cloud publish its own top-level job so
both publishes can run in parallel without a `needs:` dependency
> - The benefit is better failure isolation and less wasted time when
one publish path is slow or broken

## Linked Issues or Issue Description

No public GitHub issue was found for this change.

Problem statement:
- The cloud image publish was implemented as trailing work inside the
stock publish job.
- That setup meant the cloud publish could be delayed or skipped if the
stock job failed early.
- The desired behavior is for the cloud publish to run independently so
a failure in one publish path does not gate the other.

Proposed solution:
- Split the cloud publish into its own top-level workflow job.
- Keep the same cloud-specific build settings and cache behavior.
- Preserve the existing top-level concurrency behavior.

Alternatives considered:
- Keeping both publishes in one job with conditionals or later steps.
Rejected because it still couples success and runtime between the two
publish paths.

## What Changed

- Split the cloud image publish into a separate top-level Docker
workflow job.
- Removed the dependency coupling so the cloud job does not need the
stock job.
- Expanded the drift-guard test to assert the two-job structure and the
absence of `needs:` on the cloud job.

## Verification

- The workflow YAML was parsed successfully and confirmed to contain two
jobs: `build-and-push` and `build-and-push-cloud`.
- The cloud job was confirmed to have no `needs:` entry.
- The drift-guard assertions were reproduced in a dependency-free
harness and passed.
- PR #10408 completed GitHub Actions with all required checks green,
including the e2e shards.
- Greptile review completed at 5/5 with no unresolved comments.
- No documentation files changed because this is a workflow/test-only
change.

## Risks

- The workflow now duplicates the prep steps across two runners, so any
shared setup change must be kept in sync between both jobs.
- The new job increases workflow surface area slightly, which can make
future maintenance more verbose.
- Overall risk is low because the change is limited to CI orchestration
and test coverage.

## Model Used

OpenAI Codex (GPT-5, tool-using code assistant)

## 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
- [x] All Paperclip CI gates are green
- [x] 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-28 15:38:01 -07:00
Nicky Leach db02ca7402
ci: keep in-flight docker builds from being cancelled (#10403)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The repository uses GitHub Actions workflows to build and publish
Docker images
> - A workflow-level concurrency policy controls whether newer pushes
cancel in-flight builds
> - The current job-level setting can cancel a running image build
before it finishes publishing
> - That leaves the Docker image pipeline brittle when a new push
arrives during an active publish
> - This pull request moves concurrency to the workflow level and
disables cancel-in-progress so running builds finish
> - The benefit is that only pending work is superseded, while a build
already publishing is allowed to complete

## Linked Issues or Issue Description

No public GitHub issue exists for this change. This PR addresses the
Docker workflow concurrency behavior directly: it ensures in-flight
image builds are not cancelled by newer pushes, while still serializing
builds per ref.

## What Changed

- Moved the Docker workflow concurrency block from the job level to the
workflow level.
- Set `cancel-in-progress: false` so an active build can finish
publishing.
- Added a drift-guard test that parses `.github/workflows/docker.yml`
and asserts the workflow-level concurrency policy remains `false`.

## Verification

- `pnpm --filter @paperclipai/server exec vitest run
src/__tests__/cloud-image-bundled-plugins.test.ts`
- `python3 -c "import yaml;
yaml.safe_load(open('.github/workflows/docker.yml'))"`
- Verified the fetched remote branch contains a single commit on top of
`origin/master`.
- Searched GitHub for duplicate or related PRs and issues; none found.
- Checked `ROADMAP.md` and did not find overlapping planned core work.

## Risks

- Low risk: the change is limited to workflow concurrency behavior and a
targeted test assertion.
- If the workflow concurrency key is changed later, the drift-guard test
will fail and require an update.

## Model Used

OpenAI Codex (GPT-5, tool use; context window not surfaced in this
environment)

## 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
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-07-28 15:08:17 -07:00
Michael Nguyen 4eace88f6b
feat(adapter-claude): add Claude Opus 5 to the static model fallback (#10327)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents pick their model from a dropdown in agent config, populated
per-adapter by `listAdapterModels()` → each adapter's live provider
catalog merged over a static fallback list
> - For `claude_local`, newer model ids only reach the dropdown via the
live Anthropic `/v1/models` fetch, which needs a server
`ANTHROPIC_API_KEY`, a <5s round-trip, non-Bedrock mode, and account
entitlement; on any miss it silently falls back to the static `models`
array
> - Claude Opus 5 (`claude-opus-5`) is generally available — Anthropic
lists it as the recommended model for complex agentic coding and
enterprise work — but it was absent from that static fallback, so it
appeared only when live discovery happened to succeed
> - This pull request adds `claude-opus-5` to the `claude_local` static
model list so it is selectable regardless of the live-discovery path
> - The benefit is a consistent, reliable dropdown that surfaces the
current GA Opus flagship without depending on a flaky live fetch

## Linked Issues or Issue Description

No public GitHub issue. The bug is described inline following the
bug-report template:

**What happened**
The `claude_local` agent-config model dropdown omitted Claude Opus 5.
`claude-opus-5` was missing from the adapter's static fallback `models`
array (`packages/adapters/claude-local/src/index.ts`), so it only
surfaced when the live Anthropic `/v1/models` discovery happened to
succeed.

**Expected behavior**
Claude Opus 5 is a shipped, generally-available flagship (Anthropic's
recommended model for agentic coding) and should always be selectable in
the dropdown, independent of whether live discovery succeeds.

**Steps to reproduce**
1. Run the server without a working live Anthropic `/v1/models` path (no
`ANTHROPIC_API_KEY`, Bedrock mode, a discovery timeout, or a cache
miss).
2. Open agent config for a `claude_local` agent and inspect the model
dropdown.
3. Observe that `claude-opus-5` is absent because the static fallback
list omitted it.

**Deployment mode**
Self-hosted / local adapter (`claude_local`); the server process reads
`ANTHROPIC_API_KEY` from its environment.

## What Changed

- Added `{ id: "claude-opus-5", label: "Claude Opus 5" }` to the
`claude_local` static `models` fallback. Placed after the current
5-family entries and above the legacy `claude-opus-4-7`, so
`claude-opus-4-8` stays the default (index 0) option.
- Added an explicit regression assertion in
`server/src/__tests__/adapter-models.test.ts` that `claude-opus-5` is
present in the `claude_local` fallback when live discovery is
unavailable.

## Verification

- `pnpm -C server exec vitest run src/__tests__/adapter-models.test.ts`
— **17/17 pass**, including the new `claude-opus-5` assertion and the
existing `models[0] === "claude-opus-4-8"` default invariant (unaffected
— Opus 5 is inserted lower in the list).
- Change is a single static-data addition plus a test assertion; no
control-flow change.

## Risks

- Low risk. Pure additive change to a fallback list; no control-flow
change. Worst case is an id a given account isn't entitled to, which the
existing "current"/manual-model UI paths already tolerate.
- Note for reviewers: a sibling PR adds `claude-sonnet-5` to the same
static array (near `claude-opus-4-8`). Both are complementary "refresh
the static list to current GA" changes; whichever merges second may need
a one-line merge resolution in
`packages/adapters/claude-local/src/index.ts` and the matching test
assertion block.

## Model Used

Claude (Anthropic), model id `claude-opus-4-8` (Opus 4.8), extended
thinking + tool use, run as the Paperclip CTO agent.

## 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)
- [ ] My branch name describes the change (branch is the assigned
execution-workspace branch and cannot be renamed this run)
- [x] I have run tests locally and they pass (server adapter-models
suite, 17/17)
- [x] I have added or updated tests where applicable (explicit
`claude-opus-5` fallback assertion)
- [x] I have updated relevant documentation to reflect my changes (n/a —
no docs reference this list)
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green (pending CI)
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
(pending review)
- [x] I will address all Greptile and reviewer comments before
requesting merge

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-07-28 16:25:25 -05:00
Dotta 487e33b8b6
fix(codex-local): resolve GPT-5.6 model metadata at source (#9780)
## Thinking Path

> - Paperclip is the open source control plane people use to manage AI
agents for work
> - The `codex_local` adapter runs OpenAI's Codex CLI through direct CLI
and ACP execution lanes
> - The adapter defaulted to the bare `gpt-5.6` alias while the bundled
ACP Codex version lacked GPT-5.6-family metadata
> - Default and legacy-configured runs therefore emitted
fallback-metadata warnings and could use generic context limits
> - This pull request upgrades the bundled Codex ACP dependency, selects
the concrete `gpt-5.6-sol` model, and normalizes the legacy alias in
both execution lanes
> - The benefit is correct model metadata without hiding genuine stderr
or transcript warnings

## Linked Issues or Issue Description

Related public PRs: Refs #9342, Refs #9352, and Refs #9382. This PR is
narrower: it upgrades bundled Codex metadata and normalizes the legacy
bare alias in both execution lanes.

**Bug report**

### What happened

Default `codex_local` runs, and agents still configured with the bare
`gpt-5.6` model, print a model-metadata fallback warning and use generic
context-window limits.

Root cause: the ACP lane bundled a Codex release predating
GPT-5.6-family metadata, while Paperclip's default and advertised model
used the bare `gpt-5.6` alias for which Codex publishes no metadata.

### Expected behavior

A default Codex run resolves to a concrete model slug with published
metadata and does not emit a fallback-metadata warning.

### Deployment mode

Self-hosted/local `codex_local` adapter.

## What Changed

- Upgraded `@agentclientprotocol/codex-acp` from `^1.1.0` to `^1.1.4`
- Changed `DEFAULT_CODEX_LOCAL_MODEL` from `gpt-5.6` to `gpt-5.6-sol`
- Removed the bare alias from advertised models and listed concrete
GPT-5.6 Fast-mode variants
- Added `normalizeCodexModel()` and applied it in both CLI and ACP
execution lanes
- Updated adapter docs, Storybook fixtures, and regression tests
- Preserved warning visibility; no stderr, transcript, or log filtering
changed

## Verification

- `pnpm --filter @paperclipai/adapter-codex-local typecheck`
- `pnpm check:token-gates`
- `cd packages/adapters/codex-local && pnpm exec vitest run` — 205 tests
passed
- `cd server && pnpm exec vitest run
src/__tests__/adapter-models.test.ts` — 17 tests passed
- Confirmed the PR diff excludes `pnpm-lock.yaml` and
`.github/workflows/**` as required by repository policy
- Confirmed `.github/workflows/pr.yml` regenerates and uploads the PR
lockfile artifact before downstream `pnpm install --frozen-lockfile`
steps

## Risks

Low risk. The behavior change is scoped to `codex_local` model
selection. Existing concrete model IDs pass through unchanged; only the
legacy bare `gpt-5.6` alias is rewritten. Dependency resolution may
select a newer compatible `codex-acp` release within the declared range,
so CI remains the final compatibility gate.

> 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

- Original implementation: Anthropic Claude Opus 4.8 (`claude-opus-4-8`,
1M context, tool use and code execution)
- Conflict resolution and PR preparation: OpenAI GPT-5.5 (`gpt-5.5`,
Codex CLI coding agent, high-reasoning tool use and code execution;
host-managed context window)

## 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)
- [ ] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
— branch name is fixed by the assigned execution workspace and cannot be
renamed in-place
- [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
- [x] All Paperclip CI gates are green
- [x] 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-28 16:15:08 -05:00
Devin Foley dc12197cce
fix: prevent duplicate built-in agents and self-heal reconciliation (#10223)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Every company is auto-provisioned a set of built-in agents (e.g. the
Summarizer), and a startup reconciler keeps that set correct across
every company on boot.
> - Provisioning marks these agents with
`metadata.paperclipBuiltInAgent.key`, but nothing in the database
enforced one active agent per `(company, key)` —
`provision()`/`ensure()` did a check-then-insert with no guard.
> - Two concurrent server processes (e.g. a `tsx watch` double-boot)
could both read "no summarizer exists" and both insert, leaving a
company with duplicate built-in agents plus paired orphan pending
`hire_agent` approvals.
> - That data blemish then became a recurring outage: `findSingleAgent`
throws on >1 marked row, and because the throw escaped
`reconcileBuiltInAgentsOnStartup`'s sequential loop, **every company
after the affected one was silently skipped** on each boot — no
auto-provisioning, no default grants — until manual DB surgery.
> - This pull request closes the race at the database level and makes
reconciliation self-healing and fault-isolated.
> - The benefit is that concurrent provisioning can no longer create
duplicates, and even pre-existing duplicates are resolved automatically
instead of bricking startup reconciliation for unrelated companies.

## Linked Issues or Issue Description

- [x] I searched the GitHub PR list (open and recently closed) for
similar PRs and confirmed this is not a duplicate.

No public GitHub issue exists; describing the bug in-PR (bug-report
shape):

**What happened**

A dev instance booted with two concurrent server processes. Both ran
built-in agent provisioning for the same company at the same time, and
the check-then-insert in `provision()`/`ensure()`
(`server/src/services/built-in-agents.ts`) let both writers see "no
summarizer exists" and each create one — the company ended up with two
identical Summarizer agents (identical `paperclipBuiltInAgent` markers)
plus two paired pending `hire_agent` approvals.

From then on, **every** server boot logged:

```
ERROR: startup reconciliation of built-in agents failed
       Multiple built-in agents found for summarizer (built_in_agent_duplicate_instance)
```

because `findSingleAgent` throws on >1 marked row rather than resolving
the duplicate. Worse, `reconcileBuiltInAgentsOnStartup` loops companies
sequentially and the throw escaped the loop, so every company *after*
the affected one was silently skipped on every boot.

**Expected behavior**

1. Concurrent provisioning must not create duplicate built-in agents
(there was no DB uniqueness constraint on the marker key per company).
2. Reconciliation should be resilient: if duplicates exist anyway,
self-heal (keep the oldest row, terminate the newer dupe, cancel its
orphan pending `hire_agent` approval), and never let one bad company
abort reconciliation for the rest.

**Steps to reproduce**

- Race two `provision(companyId, "summarizer")` calls for a company with
board approval for new agents enabled (or simulate a double-boot); both
insert.
- Restart the server → startup reconciliation error fires, companies
later in the loop are never reconciled.

## What Changed

**Part 1 — stop creating duplicates**

- Migration `0192_built_in_agent_unique_marker` adds a **partial unique
index** on `(company_id, metadata->'paperclipBuiltInAgent'->>'key')`
where the marker exists and `status != 'terminated'`. It first resolves
any pre-existing duplicates (keep oldest by `created_at`, terminate
newer dupes, cancel their orphan pending `hire_agent` approvals, revoke
their API keys) so the index can be created on already-affected
instances.
- `provision()`/`ensure()` now catch the losing race's `23505` unique
violation (walking the driver's wrapped cause chain) and re-resolve to
the winning row instead of surfacing the error.

**Part 2 — resilient reconciliation**

- `findSingleAgent` self-heals: keeps the oldest marked row, terminates
the newer duplicates, and cancels each one's orphan pending `hire_agent`
approval (idempotent) instead of throwing.
- `reconcileBuiltInAgentsOnStartup` isolates per-company failures in
both loops so one bad company can't abort reconciliation for the rest;
it surfaces a `companyFailures` count in the startup log.
- Adds `approvalService.cancel()` for system-initiated cancellation of
an orphan approval.

## Verification

- `pnpm --filter @paperclipai/db run check:migrations` → numbering +
safety checks pass.
- `packages/db` migration test (real embedded Postgres) — seeds
pre-index duplicate state, runs the migration, asserts dupes resolved +
index enforced: **1 passed**.
- `server` `built-in-agents.test.ts` — self-heal, concurrent races
(plain and board-gated), and startup
self-heal-without-aborting-later-companies: **34 passed**.

```
pnpm --filter @paperclipai/db exec vitest run src/built-in-agent-unique-marker-migration.test.ts
pnpm --filter @paperclipai/server exec vitest run src/__tests__/built-in-agents.test.ts
```

## Risks

- **Migration safety**: the migration mutates data (terminates duplicate
rows, cancels their orphan pending approvals, revokes their API keys)
before creating the index. It keeps the oldest row per `(company, key)`
and only touches non-terminated marked rows; the destructive step is
covered by the migration test and the safety-check baseline. On a clean
instance it is a no-op cleanup followed by `CREATE UNIQUE INDEX IF NOT
EXISTS`.
- Otherwise low risk: the unique index is partial (excludes terminated
rows, so re-provisioning after a termination stays possible), and the
conflict handling degrades gracefully to re-resolving the existing
winner.

## Model Used

Claude Opus 4.8 (`claude-opus-4-8`), 1M context window, extended
thinking, with tool use.
2026-07-28 11:12:58 -07:00
Devin Foley 9f5af4ea5d
fix(server): accept secret_ref binding objects in sandbox provider environment config (#10355)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents execute in environments; sandbox provider plugins (Daytona,
Modal, e2b, …) declare their config via a JSON schema, with credentials
marked `format: "secret-ref"`
> - The environments UI renders those fields with a secret picker that
submits `{ type: "secret_ref", secretId, version }` binding objects,
while the server-side environment config paths only understood raw
string values and bare secret-id strings
> - The binding object reached the plugin worker's
`environmentValidateConfig` untouched; plugins parse non-string config
values as absent, so saving or testing an environment with a
picker-bound secret always failed validation (e.g. "Daytona sandbox
environments require an API key in config or DAYTONA_API_KEY.", "Modal
sandbox environments require tokenId and tokenSecret.")
> - Worse, an environment first saved with raw pasted values becomes
uneditable: the stored value is a secret reference, the edit form
re-submits it as a binding object, and every subsequent save fails the
same way
> - This pull request canonicalizes binding objects to the bare secret
id before plugin validation, and teaches the persistence/runtime/probe
secret-ref resolvers to accept the object shape defensively
> - The benefit is that picker-bound secrets work for every
schema-driven sandbox provider — create, edit, and Test — with no plugin
changes required

## Linked Issues or Issue Description

Fixes #10105

The same failure reproduces with the Daytona provider: Settings →
Instance settings → Environments → New, driver sandbox, provider
daytona, bind Api Key to an existing secret via the picker → Save fails
with "Daytona sandbox environments require an API key in config or
DAYTONA_API_KEY."

## What Changed

- `server/src/services/json-schema-secret-refs.ts`: new
`parseSecretRefBindingObject()` that recognizes the `{ type:
"secret_ref", secretId, version? }` shape the secret picker submits
(version defaults to `"latest"`; malformed objects return null).
- `server/src/services/plugin-environment-driver.ts`:
`validatePluginSandboxProviderConfig()` now canonicalizes binding
objects at the driver schema's `format: "secret-ref"` paths to the bare
secret id (the persisted shape) before invoking the plugin worker's
`environmentValidateConfig`. Pinned numeric versions are rejected with a
clear 422, since sandbox provider references always resolve the latest
version — silently resolving a different version would be worse.
- `server/src/services/environment-config.ts`: the persistence, runtime,
and probe secret-ref resolvers plus `collectEnvironmentSecretRefs()`
accept the binding-object shape defensively, so any previously persisted
object-shaped refs (from providers whose validation tolerated them)
resolve instead of being silently skipped; the missing-companyId runtime
guard also now fails closed for object-shaped refs.

## Verification

- `npx vitest run server/src/__tests__/json-schema-secret-refs.test.ts
server/src/__tests__/plugin-sandbox-provider-config-validation.test.ts
server/src/__tests__/environment-routes.test.ts
server/src/__tests__/environment-config.test.ts` — 82 tests pass,
including new coverage: binding-object canonicalization before plugin
validation, pinned-version rejection, raw-string pass-through, and a
route-level create with a picker-submitted binding object persisting the
bare secret id without minting a duplicate secret.
- `npx vitest run server/src/__tests__/environment-runtime.test.ts` — 24
tests pass against embedded Postgres, including a new test that persists
an object-shaped ref and verifies runtime resolution produces the
plaintext credential for the plugin worker.
- `pnpm typecheck` in `server/` — clean.

## Risks

- Low. The canonical persisted shape (bare secret-id string) is
unchanged, so existing saved environments and lease-resume fingerprints
are unaffected; raw pasted values and bare-id strings take exactly the
same code path as before.
- New behavior only triggers where a save/probe previously failed 422
(binding objects at secret-ref paths) or where an object-shaped ref was
previously skipped silently at runtime (now resolved, or failed closed
without a companyId).
- Pinned binding versions at sandbox-provider paths are now an explicit
422 instead of an accidental validation failure; no UI submits pinned
versions today (`allowVersionSelector={false}`).

## Model Used

Claude Fable 5 (`claude-fable-5`, Anthropic) with extended thinking and
agentic tool use (Claude Code harness): source diagnosis, fix, and
tests.

## 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 (no
doc surface changed)
- [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
2026-07-28 10:50:13 -07:00
dependabot[bot] f9034ab3ca
build(deps-dev): bump @types/node from 22.19.21 to 22.20.1 (#10304)
Bumps
[@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node)
from 22.19.21 to 22.20.1.
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-28 10:48:45 -07:00
dependabot[bot] e95045905b
build(deps): bump better-auth from 1.6.23 to 1.6.25 (#10306)
Bumps
[better-auth](https://github.com/better-auth/better-auth/tree/HEAD/packages/better-auth)
from 1.6.23 to 1.6.25.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/better-auth/better-auth/releases">better-auth's
releases</a>.</em></p>
<blockquote>
<h2>v1.6.25</h2>
<h2><code>better-auth</code></h2>
<h3>Bug Fixes</h3>
<ul>
<li>Fixed Apple OAuth not sending the PKCE code challenge during
authorization, causing token exchange failures (<a
href="https://redirect.github.com/better-auth/better-auth/pull/10294">#10294</a>)</li>
<li>Fixed Google One Tap creating new users when sign-up was disabled on
the Google provider (<a
href="https://redirect.github.com/better-auth/better-auth/pull/10479">#10479</a>)</li>
<li>Fixed <code>$fetch</code> and <code>$store</code> not being exposed
on the Solid client (<a
href="https://redirect.github.com/better-auth/better-auth/pull/10444">#10444</a>)</li>
<li>Fixed internal adapter queries being routed to the wrong table when
a built-in table's <code>modelName</code> was set to another table's
schema key (e.g. <code>user.modelName =
&quot;account&quot;</code>).</li>
</ul>
<p>For detailed changes, see <a
href="07a646ea19/packages/better-auth/CHANGELOG.md"><code>CHANGELOG</code></a></p>
<h2>Contributors</h2>
<p>Thanks to everyone who contributed to this release:</p>
<p><a href="https://github.com/birkskyum"><code>@​birkskyum</code></a>,
<a href="https://github.com/jsj"><code>@​jsj</code></a>, <a
href="https://github.com/krish-vachhani"><code>@​krish-vachhani</code></a></p>
<p><strong>Full changelog:</strong> <a
href="https://github.com/better-auth/better-auth/compare/v1.6.24...v1.6.25"><code>v1.6.24...v1.6.25</code></a></p>
<h2>v1.6.24</h2>
<h2><code>better-auth</code></h2>
<h3>Features</h3>
<ul>
<li>Added request context (<code>ctx</code>) as a third argument to
<code>verifyIdToken</code>, enabling custom ID token verifiers to read
request headers (<a
href="https://redirect.github.com/better-auth/better-auth/pull/10376">#10376</a>)</li>
<li>Added <code>beforeStoreCookie</code> option to the last-login-method
plugin for GDPR compliance (<a
href="https://redirect.github.com/better-auth/better-auth/pull/5753">#5753</a>)</li>
</ul>
<h3>Bug Fixes</h3>
<ul>
<li>Replaced flaky MongoDB where-coercion integration test with a direct
unit test for more reliable test runs (<a
href="https://redirect.github.com/better-auth/better-auth/pull/10369">#10369</a>)</li>
<li>Fixed the <code>get-session</code> endpoint to include
<code>no-store</code> cache control headers, preventing stale session
data from being served (<a
href="https://redirect.github.com/better-auth/better-auth/pull/10222">#10222</a>)</li>
<li>Fixed SQLite migration diffs to recognize <code>BIGINT</code> as a
valid number type, preventing spurious pending changes on rate limiter
columns (<a
href="https://redirect.github.com/better-auth/better-auth/pull/10316">#10316</a>)</li>
<li>Fixed auth requests failing when request cloning throws an error
inside verification callbacks (<a
href="https://redirect.github.com/better-auth/better-auth/pull/10336">#10336</a>)</li>
<li>Fixed <code>useSession({ throw: true })</code> incorrectly excluding
<code>null</code> from its <code>data</code> type (<a
href="https://redirect.github.com/better-auth/better-auth/pull/9787">#9787</a>)</li>
<li>Fixed auth query revalidation and signal listeners not being
restored after a client component remounts (<a
href="https://redirect.github.com/better-auth/better-auth/pull/10379">#10379</a>)</li>
<li>Fixed the <code>CookieAttributes</code> index signature type to be
more precise (<a
href="https://redirect.github.com/better-auth/better-auth/pull/10442">#10442</a>)</li>
<li>Fixed silent misrouting of adapter queries when
<code>user.modelName</code> was set to a value that collides with
another schema key (<a
href="https://redirect.github.com/better-auth/better-auth/pull/10235">#10235</a>)</li>
<li>Fixed Kysely migration generation producing duplicate indexes for
fields marked both <code>unique</code> and <code>index</code> (<a
href="https://redirect.github.com/better-auth/better-auth/pull/10357">#10357</a>)</li>
<li>Fixed magic-link and email-OTP send endpoints to validate the
<code>Origin</code> header on cookieless requests, preventing
cross-origin abuse (<a
href="https://redirect.github.com/better-auth/better-auth/pull/10368">#10368</a>)</li>
<li>Fixed remote MCP auth 401 challenge headers being hidden from
browser clients due to missing CORS exposure (<a
href="https://redirect.github.com/better-auth/better-auth/pull/10290">#10290</a>)</li>
<li>Fixed OpenAPI schema to include plugin user fields (such as
<code>username</code> and <code>displayUsername</code>) in
<code>/sign-up/email</code> and <code>/update-user</code> request bodies
(<a
href="https://redirect.github.com/better-auth/better-auth/pull/10453">#10453</a>)</li>
<li>Fixed <code>organization.listMembers</code> failing with &quot;User
not found for member&quot; for organizations with more than ~100 members
(<a
href="https://redirect.github.com/better-auth/better-auth/pull/10342">#10342</a>)</li>
<li>Fixed organization invitations to use database-generated IDs when
<code>advanced.database.generateId</code> is configured, matching the
behavior of other models (<a
href="https://redirect.github.com/better-auth/better-auth/pull/10040">#10040</a>)</li>
<li>Fixed <code>getDefaultModelName</code> to prefer exact schema key
matches over <code>modelName</code> aliases, preventing adapter queries
from being misrouted when a built-in table's name collides with another
schema key</li>
</ul>
<p>For detailed changes, see <a
href="9a661c7b7a/packages/better-auth/CHANGELOG.md"><code>CHANGELOG</code></a></p>
<h2><code>auth</code></h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/better-auth/better-auth/blob/main/packages/better-auth/CHANGELOG.md">better-auth's
changelog</a>.</em></p>
<blockquote>
<h2>1.6.25</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/better-auth/better-auth/pull/10479">#10479</a>
<a
href="5124c34879"><code>5124c34</code></a>
Thanks <a
href="https://github.com/krish-vachhani"><code>@​krish-vachhani</code></a>!
- Prevent Google One Tap from creating new users when sign-up is
disabled for the Google provider.</p>
</li>
<li>
<p><a
href="https://redirect.github.com/better-auth/better-auth/pull/10444">#10444</a>
<a
href="743935991f"><code>7439359</code></a>
Thanks <a
href="https://github.com/birkskyum"><code>@​birkskyum</code></a>! -
Expose the real <code>$fetch</code> instance and <code>$store</code>
atoms from the Solid client instead of resolving them as dynamic API
routes.</p>
</li>
<li>
<p>Updated dependencies [<a
href="0ffd1fb28d"><code>0ffd1fb</code></a>]:</p>
<ul>
<li><code>@​better-auth/core</code><a
href="https://github.com/1"><code>@​1</code></a>.6.25</li>
<li><code>@​better-auth/drizzle-adapter</code><a
href="https://github.com/1"><code>@​1</code></a>.6.25</li>
<li><code>@​better-auth/kysely-adapter</code><a
href="https://github.com/1"><code>@​1</code></a>.6.25</li>
<li><code>@​better-auth/memory-adapter</code><a
href="https://github.com/1"><code>@​1</code></a>.6.25</li>
<li><code>@​better-auth/mongo-adapter</code><a
href="https://github.com/1"><code>@​1</code></a>.6.25</li>
<li><code>@​better-auth/prisma-adapter</code><a
href="https://github.com/1"><code>@​1</code></a>.6.25</li>
<li><code>@​better-auth/telemetry</code><a
href="https://github.com/1"><code>@​1</code></a>.6.25</li>
</ul>
</li>
</ul>
<h2>1.6.24</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/better-auth/better-auth/pull/10235">#10235</a>
<a
href="03dc5a046f"><code>03dc5a0</code></a>
Thanks <a
href="https://github.com/ping-maxwell"><code>@​ping-maxwell</code></a>!
- Fixes silent foreign-key and adapter-join misrouting when a user
remaps a built-in model name to a string that collides with another
schema key</p>
</li>
<li>
<p><a
href="https://redirect.github.com/better-auth/better-auth/pull/10357">#10357</a>
<a
href="7508940376"><code>7508940</code></a>
Thanks <a href="https://github.com/c-nicol"><code>@​c-nicol</code></a>!
- Fixes Kysely migration generation for new-table fields that are both
unique: true and index: true.</p>
</li>
<li>
<p><a
href="https://redirect.github.com/better-auth/better-auth/pull/10342">#10342</a>
<a
href="bae71988ab"><code>bae7198</code></a>
Thanks <a
href="https://github.com/ping-maxwell"><code>@​ping-maxwell</code></a>!
- Fix <code>organization.listMembers</code> failing with &quot;User not
found for member&quot; for orgs with more than ~100 members by applying
the same membership limit to the users query.</p>
</li>
<li>
<p><a
href="https://redirect.github.com/better-auth/better-auth/pull/10336">#10336</a>
<a
href="ef4d27360c"><code>ef4d273</code></a>
Thanks <a
href="https://github.com/Tushar-Khandelwal-2004"><code>@​Tushar-Khandelwal-2004</code></a>!
- Prevent verification callbacks from failing auth requests when cloning
the request throws.</p>
</li>
<li>
<p><a
href="https://redirect.github.com/better-auth/better-auth/pull/10333">#10333</a>
<a
href="99dbdd7ea9"><code>99dbdd7</code></a>
Thanks <a href="https://github.com/c-nicol"><code>@​c-nicol</code></a>!
- Fixes Drizzle schema generation for fields that are both unique: true
and index: true.</p>
</li>
<li>
<p><a
href="https://redirect.github.com/better-auth/better-auth/pull/10368">#10368</a>
<a
href="086ca91f51"><code>086ca91</code></a>
Thanks <a
href="https://github.com/gaurav0107"><code>@​gaurav0107</code></a>! -
Force-validate the request <code>Origin</code> on the magic-link
(<code>/sign-in/magic-link</code>) and email-otp
(<code>/email-otp/send-verification-otp</code>) send endpoints,
including cookieless requests, to match the built-in
<code>/sign-in/email</code> and <code>/sign-up/email</code> routes. A
cookieless cross-origin POST can no longer trigger a magic-link or
verification-OTP email to an arbitrary address. Cookieless requests that
carry no <code>Origin</code> (server-to-server) are unaffected.</p>
</li>
<li>
<p><a
href="https://redirect.github.com/better-auth/better-auth/pull/10290">#10290</a>
<a
href="8f2dedd893"><code>8f2dedd</code></a>
Thanks <a
href="https://github.com/GautamBytes"><code>@​GautamBytes</code></a>! -
Expose the remote MCP auth client's 401 challenge headers to browser
clients using CORS.</p>
</li>
<li>
<p><a
href="https://redirect.github.com/better-auth/better-auth/pull/10453">#10453</a>
<a
href="4e685eef42"><code>4e685ee</code></a>
Thanks <a
href="https://github.com/ping-maxwell"><code>@​ping-maxwell</code></a>!
- OpenAPI now includes <code>user.additionalFields</code> and plugin
user schema fields (e.g. username plugin <code>username</code> /
<code>displayUsername</code>) on <code>/sign-up/email</code> and
<code>/update-user</code> request bodies.</p>
</li>
<li>
<p><a
href="https://redirect.github.com/better-auth/better-auth/pull/10190">#10190</a>
<a
href="3bf0e4981e"><code>3bf0e49</code></a>
Thanks <a
href="https://github.com/gaurav-init"><code>@​gaurav-init</code></a>! -
Pass the endpoint context as the second argument to
<code>beforeDeleteOrganization</code> and
<code>afterDeleteOrganization</code> hooks in the organization plugin,
matching the signature shown in the docs and the existing
<code>databaseHooks</code> pattern. The Stripe plugin's
<code>beforeDeleteOrganization</code> wrapper now forwards the context
to user-supplied hooks instead of dropping it.</p>
</li>
<li>
<p><a
href="https://redirect.github.com/better-auth/better-auth/pull/10040">#10040</a>
<a
href="f59a0ee789"><code>f59a0ee</code></a>
Thanks <a
href="https://github.com/shiminshen"><code>@​shiminshen</code></a>! -
Organization invitations now let the database generate their
<code>id</code> when ID generation is delegated to the database (e.g.
<code>advanced.database.generateId: &quot;uuid&quot;</code> with a
UUID-capable adapter such as Postgres), matching every other model.
Previously <code>createInvitation</code> always generated the invitation
<code>id</code> in application code, so invitation rows received an
app-generated value instead of a database-generated one while
organizations, members and teams correctly deferred to the database (<a
href="https://redirect.github.com/better-auth/better-auth/issues/10024">better-auth/better-auth#10024</a>).
A caller-provided id (e.g. via <code>beforeCreateInvitation</code>) is
still honored.</p>
</li>
<li>
<p><a
href="https://redirect.github.com/better-auth/better-auth/pull/10302">#10302</a>
<a
href="0f2cc1b33b"><code>0f2cc1b</code></a>
Thanks <a
href="https://github.com/momomuchu"><code>@​momomuchu</code></a>! -
Prefer exact schema-key matches over <code>modelName</code> aliases in
<code>getDefaultModelName</code>, so remapping a built-in table onto
another table's schema key (e.g. <code>user.modelName =
&quot;account&quot;</code>) does not reroute internal adapter queries to
the wrong table.</p>
</li>
<li>
<p><a
href="https://redirect.github.com/better-auth/better-auth/pull/9787">#9787</a>
<a
href="ae78109118"><code>ae78109</code></a>
Thanks <a
href="https://github.com/ping-maxwell"><code>@​ping-maxwell</code></a>!
- Fixes an issue where <code>useSession({ throw: true })</code>
incorrectly excluded <code>null</code> from its <code>data</code>
type.</p>
</li>
<li>
<p><a
href="https://redirect.github.com/better-auth/better-auth/pull/10222">#10222</a>
<a
href="46d2bf02c9"><code>46d2bf0</code></a>
Thanks <a
href="https://github.com/ping-maxwell"><code>@​ping-maxwell</code></a>!
- fix: add no-store cache-control headers to get-session route</p>
</li>
<li>
<p><a
href="https://redirect.github.com/better-auth/better-auth/pull/10316">#10316</a>
<a
href="29a373eaf1"><code>29a373e</code></a>
Thanks <a
href="https://github.com/vinay-oppuri"><code>@​vinay-oppuri</code></a>!
- Recognize SQLite <code>BIGINT</code> as a valid number type in
migration diffs so database-backed rate limiter columns like
<code>lastRequest</code> no longer report spurious pending changes on
every run.</p>
</li>
<li>
<p><a
href="https://redirect.github.com/better-auth/better-auth/pull/10379">#10379</a>
<a
href="f6d18fa8f7"><code>f6d18fa</code></a>
Thanks <a
href="https://github.com/ping-maxwell"><code>@​ping-maxwell</code></a>!
- fix(client): restore auth query revalidation and signal listeners
after remount</p>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="07a646ea19"><code>07a646e</code></a>
chore: release v1.6.25 (<a
href="https://github.com/better-auth/better-auth/tree/HEAD/packages/better-auth/issues/10491">#10491</a>)</li>
<li><a
href="743935991f"><code>7439359</code></a>
fix(solid): expose $fetch and $store on the solid client (<a
href="https://github.com/better-auth/better-auth/tree/HEAD/packages/better-auth/issues/10444">#10444</a>)</li>
<li><a
href="dac701c94b"><code>dac701c</code></a>
chore(deps): bump next from 16.2.6 to 16.2.11 (<a
href="https://github.com/better-auth/better-auth/tree/HEAD/packages/better-auth/issues/10493">#10493</a>)</li>
<li><a
href="5124c34879"><code>5124c34</code></a>
fix(one-tap): enforce google provider signup restrictions (<a
href="https://github.com/better-auth/better-auth/tree/HEAD/packages/better-auth/issues/10479">#10479</a>)</li>
<li><a
href="9a661c7b7a"><code>9a661c7</code></a>
chore: release v1.6.24 (<a
href="https://github.com/better-auth/better-auth/tree/HEAD/packages/better-auth/issues/10323">#10323</a>)</li>
<li><a
href="4e685eef42"><code>4e685ee</code></a>
fix(open-api): include plugin user fields on sign-up/update bodies (<a
href="https://github.com/better-auth/better-auth/tree/HEAD/packages/better-auth/issues/10453">#10453</a>)</li>
<li><a
href="d3ce782332"><code>d3ce782</code></a>
fix(cookies): tighten CookieAttributes index signature type (<a
href="https://github.com/better-auth/better-auth/tree/HEAD/packages/better-auth/issues/10441">#10441</a>)
(<a
href="https://github.com/better-auth/better-auth/tree/HEAD/packages/better-auth/issues/10442">#10442</a>)</li>
<li><a
href="ae78109118"><code>ae78109</code></a>
fix(client): preserve null in useSession().data type with throw:true (<a
href="https://github.com/better-auth/better-auth/tree/HEAD/packages/better-auth/issues/9787">#9787</a>)</li>
<li><a
href="f6d18fa8f7"><code>f6d18fa</code></a>
fix(client): restore auth query lifecycle after remount (<a
href="https://github.com/better-auth/better-auth/tree/HEAD/packages/better-auth/issues/10379">#10379</a>)</li>
<li><a
href="086ca91f51"><code>086ca91</code></a>
fix(magic-link, email-otp): force-validate Origin on cookieless send
endpoint...</li>
<li>Additional commits viewable in <a
href="https://github.com/better-auth/better-auth/commits/v1.6.25/packages/better-auth">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=better-auth&package-manager=npm_and_yarn&previous-version=1.6.23&new-version=1.6.25)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-28 10:03:52 -07:00