Commit Graph

1445 Commits

Author SHA1 Message Date
Eric Brookfield 20482a4cb6
fix(server): gate heartbeat-fallback comment to never publish raw transcript (#10143)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Agents run on a heartbeat; when an issue-scoped run ends, the server
records the outcome on the issue's board thread.
> - Normally the agent posts its own summary comment via `POST
/comments`. When it doesn't, the server has a fallback that
auto-publishes a run summary so the board isn't left silent.
> - That fallback (`buildHeartbeatRunIssueComment` in
`server/src/services/heartbeat-run-summary.ts`) returns
`resultJson.summary` **verbatim**, with no length cap or shape check.
> - For runs that never produce a final `result`, `summary` is
concatenated **inter-tool narration** ("Let me check…", "I'll fetch…",
joined by the claude-local adapter's parser). The fallback then dumps
that raw transcript onto the public board thread.
> - In practice this produces long, confusing transcript comments that
mislead reviewers and other agents about what actually happened.
> - This PR gates the fallback so it publishes a clean summary or a
short stub, never raw transcript.
> - The benefit is that the board thread stays trustworthy: a missing
agent summary degrades to a one-line "no summary this run" note instead
of leaking internal narration.

## Linked Issues or Issue Description

No public GitHub issue exists for this; describing it here as a bug
report.

**What happened:** When an issue-scoped heartbeat run finishes without
the agent posting its own comment, the server's fallback publishes
`resultJson.summary` verbatim as the board comment. When the run
produced no final result, that value is concatenated inter-tool
narration, so raw transcript is posted to the issue thread.

**Expected behavior:** The fallback should post a concise summary when
one is available, and otherwise a short stub — never multi-hundred-line
raw narration.

**Steps to reproduce:**
1. Run an issue-scoped agent turn that ends without calling `POST
/comments` and without emitting a final `result` (only inter-tool
narration).
2. Observe the auto-published board comment: it is the full narration
transcript.

**Deployment mode:** self-hosted server
(`server/src/services/heartbeat.ts` fallback path).

**Prior attempt:** an earlier PR for this change was auto-closed when
its head branch was renamed to strip an internal ticket id from the
branch name; this PR supersedes it.

**Related PR:** #7505 (`fix(heartbeat): skip auto-mirror run-summary
comment on cross-owner wakes`) touches the same fallback area but
addresses a different case (cross-owner wakes); this PR is
complementary, gating the *content* of the fallback rather than *when*
it fires.

## What Changed

- `server/src/services/heartbeat-run-summary.ts`:
`buildHeartbeatRunIssueComment` now gates the fallback text. After
resolving `summary` → `result` → `message`, if the text opens with a
narration phrase (`let me`, `i'll`, `i need to`, `i can see`, `looking
at`, `fetching`, `checking`, `first,`) **or** exceeds
`MAX_FALLBACK_COMMENT_CHARS` (1200), it returns a fixed stub: *"Run
completed. Agent did not post a summary comment this run (transcript
withheld — see run log)."* Otherwise it returns the text unchanged.
- `server/src/__tests__/heartbeat-run-summary.test.ts`: added cases for
each narration opener, the length cap, the exact 1200-char boundary
(posts), and clean-summary passthrough.

Runs where the agent posts via the API are unaffected — the fallback
only fires when no agent comment is found for the run, and that call
site is unchanged.

## Verification

- `pnpm --filter @paperclip/server test heartbeat-run-summary` — 13/13
pass (new + existing cases).
- Manual reasoning: the gate is a pure function of the resolved text;
API-posted runs never reach it.
- **CI note:** at the time of opening, `pnpm install --frozen-lockfile`
fails on this branch's base commit with
`ERR_PNPM_LOCKFILE_CONFIG_MISMATCH` (patchedDependencies drift). This
reproduces on every PR based on the current `master` tip (e.g. #10137)
and is unrelated to this two-file change; PRs cut from the prior master
(e.g. #10135) install cleanly. This should clear once the `Refresh
Lockfile` job lands a corrected lockfile on `master` and this branch is
rebased. Happy to rebase or fold in the lockfile fix if a maintainer
prefers.

## Risks

Low risk. The change is confined to one pure function and its tests,
touches no schema or migration, and only alters the *fallback* comment
path (never the normal API-posted path). Worst case is a legitimate
clean summary that happens to open with a gated phrase gets replaced by
the stub — the run log still holds the full detail.

## Model Used

Claude Opus 4.8 (`claude-opus-4-8`), 1M-token context window, extended
thinking, with 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
(`fix/gate-heartbeat-fallback-comment`) 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 (no
user-facing docs affected)
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green (blocked on a master-side
lockfile drift, see CI note)
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
(pending re-review on this PR)
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-12 16:43:13 -07:00
Constantine 2f1c0e011e
fix(hermes): surface silent nonzero exit failures (#10107)
## Thinking Path

- Followed a silent nonzero Hermes exit from child-process result
parsing through heartbeat run, runtime, task-session, and agent
finalization.
- Found two gaps: the adapter could return `errorMessage: null` for a
numeric nonzero exit, and heartbeat later reused the nullable adapter
field instead of its normalized fallback.
- Kept timeout, signal-cancellation, and specific parsed diagnostics
authoritative.

## Linked Issue(s) / Bug Report

Related to #9751 (stderr classification) and #9519 (exit-zero
finalization), but this is a separate failure mode.

Reproduction: run Hermes with a child result equivalent to `exitCode:
1`, `timedOut: false`, and no parsed diagnostic. The heartbeat row
derives `Adapter failed`, while runtime/task-session/agent finalization
can persist null diagnostics.

## What Changed

- Give silent numeric nonzero Hermes exits a stable fallback such as
`Hermes exited with code 1`.
- Preserve specific parsed errors and timeout/signal semantics.
- Reuse the normalized persisted run error for recovered runtime state,
task-session `lastError`, and agent `errorReason`.
- Add adapter-level and embedded-Postgres regressions.

## Verification

- Hermes adapter `execute.onspawn.test.ts` — 7 passed.
- Focused heartbeat normalized-error regression — 1 passed (91 skipped).
- `pnpm --filter @paperclipai/hermes-paperclip-adapter typecheck` —
passed.
- `pnpm --filter @paperclipai/server typecheck` — passed.
- `git diff --check origin/master...HEAD` — passed.

Independent review also ran the full recovery file: the changed
regression passed; one unrelated pre-existing timing-sensitive test
timed out.

## Risks / Rollout Notes

Low risk. Fallback text is used only when a numeric nonzero exit has no
better diagnostic. Existing timeout, signal, and parsed-error precedence
remains unchanged.

## Model Used

OpenAI Codex `gpt-5.6-sol` with repository inspection, test execution,
and independent read-only review.

## 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 (not
applicable: internal diagnostics only)
- [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: cucurigoo <cucurigoo@users.noreply.github.com>
2026-08-12 16:09:35 -07:00
Daniel Sauer 91669741d2
fix(server): close tool-access cross-tenant ID oracles (#9589)
## Thinking Path

> - Paperclip's company-scoped HTTP routes must reject inaccessible
resources before returning resource-specific authorization results.
> - The shared `getAccessibleResource` helper established that
invariant, but direct tool-access routes still fetched globally unique
IDs first and then returned 403 from later authorization checks.
> - A signed-in user could therefore distinguish a valid foreign-company
resource ID from an unknown ID.
> - This change applies the existing tenant-aware lookup gate
consistently across direct tool-resource routes and rejects inaccessible
OAuth state before callback-specific authorization.

## Linked Issues or Issue Description

- No standalone issue exists. This is a security-hardening follow-up to
#3967.
- **Observed:** a member of company A can submit a known application,
connection, profile, profile-entry, or OAuth-state ID belonging to
company B and receive a different response than for a random missing ID.
- **Expected:** missing and inaccessible foreign resources are
indistinguishable at the HTTP boundary. Signed-in instance
administrators still require company membership for company-scoped
access.
- **Reproduction:** create resources in company B, authenticate as an
owner of company A without B membership, and call the direct
`/api/tool-*` routes using B's IDs. Before this change, affected calls
returned 403 while unknown IDs returned 404.

## What Changed

- Wrapped direct application, connection, profile, and profile-entry
lookups in `server/src/routes/tool-access.ts` with the shared
`getAccessibleResource` 404 gate.
- Added tenant membership validation to OAuth callback-state lookup
before session/role checks, returning the same invalid-state response as
an unknown state.
- Expanded route regressions across connection/profile endpoint
families, including grants, usage, installs, gateway-backed test calls,
OAuth, mutations, catalog/activity reads, profile entries, and
instance-admin-without-membership access.
- Updated application update/delete expectations from cross-tenant 403
to non-enumerating 404 responses.

## Verification

After rebasing onto current `master`:

- `pnpm exec vitest run src/__tests__/tool-access-service.test.ts` from
`server/` — 113 passed.
- `pnpm --filter @paperclipai/server typecheck` — previously passed on
the same implementation; affected upstream paths were unchanged before
this mechanical rebase.

## Risks

- Low implementation risk: no schema, migration, or successful
same-company response changes.
- Intentional behavior change: inaccessible foreign tool-resource IDs
now return 404 instead of 403; inaccessible OAuth states return the same
400 body as missing/expired states.
- The gate reuses `getAccessibleResource` / `hasCompanyAccess` semantics
established by #3967.

> 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, exact model ID `openai-codex/gpt-5.6-sol`; repository,
shell, test, TypeScript language-server, and GitHub CLI tool access
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 an existing issue 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
Paperclip ticket ID
- [x] I have run focused tests locally on the final rebased head and
they pass
- [x] I have added or updated tests where applicable
- [x] Documentation update — N/A: internal authorization correction only
- [x] I have considered and documented risks above
- [ ] All Paperclip CI gates are green on the new rebased head
- [x] Greptile's prior review was 5/5 with no open findings
- [x] I will address all Greptile and reviewer comments before
requesting merge

Co-authored-by: Daniel Sauer <sauerdaniel@users.noreply.github.com>
2026-08-12 16:09:18 -07:00
Jonathan Reyes 676e20a894
fix(routines): reject HMAC webhook replays (#9994)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Routines allow external systems to start recurring work through
authenticated public webhooks
> - Timestamped HMAC authentication currently verifies authenticity and
age but does not remember an accepted delivery
> - An exact signed request can therefore be reused within its replay
window, including through simultaneous duplicate delivery
> - Replay rejection must be atomic with run creation so concurrent
copies cannot both succeed
> - This pull request derives a non-secret replay identity from each
valid timestamped HMAC delivery and claims it under the existing routine
transaction lock
> - The benefit is at-most-once acceptance of an exact HMAC delivery
without changing ordinary caller-supplied idempotency semantics

## Linked Issues or Issue Description

Fixes: #9993

## What Changed

- Derive a stable, non-secret idempotency key after a timestamped HMAC
signature has been validated.
- Reject a previously claimed HMAC delivery with a conflict while
preserving coalescing for existing non-HMAC idempotency keys.
- Apply the same atomic replay claim when automatic worktree execution
is suppressed.
- Add regression coverage for sequential, concurrent, and suppressed-run
replays.

## Verification

- `pnpm exec vitest run server/src/__tests__/routines-service.test.ts` —
59 tests passed.
- `pnpm typecheck` — all workspace packages passed.
- The sequential test was observed failing on unmodified `master`: the
second identical request resolved and a second run was created.
- The concurrent regression test verifies exactly one request succeeds
and only one routine run exists.

## Risks

- Low migration risk: no schema change is required; the existing
nullable routine-run idempotency field is reused.
- The routine row lock serializes replay claims, adding a small amount
of contention only while a routine run is being created.
- Replay rejection applies only to `hmac_sha256`, which carries the
timestamp needed for a bounded replay policy. Existing `github_hmac`,
bearer, and unauthenticated trigger semantics are unchanged.

> 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 family) with reasoning, repository inspection, shell
execution, and test tooling.

## 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] Documentation does not require an update because this restores the
documented replay-window security behavior without changing
configuration or APIs
- [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-08-12 16:09:00 -07:00
Christian Lappin fc5c6ffed2
fix(server): return 404 instead of 500 for non-UUID company refs (#9959)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The board REST API is how operators and integrations read company
state; `GET /api/companies/:companyId` is one of its most basic reads
> - The route passes the raw path param into `companyService.getById`,
which queries the uuid-typed `companies.id` column directly
> - Any non-UUID ref — a slug, a typo, a stale bookmark — makes Postgres
throw `invalid input syntax for type uuid`, which surfaces as an HTTP
500 with a stack trace in the server log instead of a clean client error
> - A 500 for malformed client input is miscategorized: it pages
operators, pollutes error budgets, and hides the actual problem ("that
ref doesn't exist") from the caller
> - This pull request guards `getById` with a UUID check so non-UUID
refs resolve to `null` and the route returns its existing 404 path
> - The benefit is correct HTTP semantics for bad input, quieter logs,
and one less misleading 500 for self-hosters to chase

## Linked Issues or Issue Description

Fixes #9962 — `GET /api/companies/:companyId` returns 500 (`invalid
input syntax for type uuid`) for non-UUID refs instead of 404. Full
repro and log excerpt in the issue.

## What Changed

- `server/src/services/companies.ts`: `getById` returns `null` early for
non-UUID refs instead of passing them to the uuid-typed query.
- `server/src/__tests__/companies-service.test.ts`: regression test —
non-UUID refs (`"tumbly-haus-creative"`, `"not-a-uuid"`, `""`) resolve
to `null` without a query error.

## Verification

- `npx vitest run src/__tests__/companies-service.test.ts` — 12/12 pass
(new test included, embedded-postgres suite).
- Manual: `curl -i /api/companies/not-a-uuid` → 404 (was 500); `curl -i
/api/companies/<real-uuid>` → 200 unchanged.

## Risks

- Low. Pure input-validation guard on one read path; UUID lookups are
byte-for-byte unchanged. Only behavioral shift is 500→404 for refs that
could never have matched a row.

## Model Used

Claude Fable 5 (`claude-fable-5`, Anthropic) via Claude Code — diagnosis
from server logs, patch, and test authored with extended thinking and
tool use; human-reviewed and submitted by @christianlappin.

## 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 (n/a —
no doc references this error path)
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green (pending first CI run)
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
(pending)
- [x] I will address all Greptile and reviewer comments before
requesting merge
2026-08-12 16:08:49 -07:00
dmndbrp-oss 0db8480b19
fix(SAG-2595): land updatedSince issues-list filter on master (#9050)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The issues-list REST endpoint (`GET
/api/companies/:companyId/issues`) backs the digester and other pollers
that ask "what changed since last time".
> - The service layer supports rich filters, but there was no
`updatedSince` filter — so every routine fire re-read the full backlog
instead of just the delta.
> - A prior commit added this filter, but it was never merged to
`master`; it only ran in production because a feature branch happened to
be the live checkout, and the behavior vanished when that directory was
repurposed.
> - This pull request re-lands just the `updatedSince` filter (route
param parse + validation, service `IssueFilters` field, and the
`updatedAt` predicate) as a single-purpose change.
> - The benefit is that pollers can request only issues updated after a
timestamp, and the fix now lives durably on `master` instead of a
transient checkout.

## Linked Issues or Issue Description

No public GitHub issue exists; describing inline per the bug report
template.

**What happened**

`GET /api/companies/:companyId/issues` ignores an `updatedSince` query
parameter, so consumers (e.g. the digester and other pollers) cannot
request only the delta since a prior poll and must re-read the whole
backlog on every fire.

**Expected behavior**

Passing `updatedSince=<ISO 8601 timestamp>` returns only issues whose
`updatedAt` is strictly after that timestamp; a malformed value returns
`400`.

**Steps to reproduce**

1. Call `GET /api/companies/:companyId/issues?updatedSince=<a future ISO
8601 timestamp>`.
2. Observe the endpoint returns the full backlog instead of an empty
list (the parameter is silently ignored).

## What Changed

- `server/src/routes/issues.ts`: parse the `updatedSince` query param,
return `400` for a non-parseable timestamp, and pass it into
`svc.list()`.
- `server/src/services/issues.ts`: add `updatedSince?: string` to
`IssueFilters` and, when present and valid, add a `gt(issues.updatedAt,
since)` condition to the list query.
- `server/src/__tests__/issue-list-updatedsince-filter-routes.test.ts`:
new route+service coverage — future timestamp returns 0 issues, a past
timestamp returns only the delta, and a malformed timestamp returns 400.

## Verification

- `pnpm vitest run
src/__tests__/issue-list-updatedsince-filter-routes.test.ts` — 3/3 pass.
- `pnpm vitest run
src/__tests__/issue-list-assignee-filter-routes.test.ts` — 5/5 pass
(regression check on the sibling filter path).
- `tsc --noEmit` on `server/` — no new errors introduced (pre-existing
unrelated `plugin-sdk` build errors on `master` are untouched).

## Risks

Low risk. Purely additive: the new filter only takes effect when
`updatedSince` is supplied, so existing callers that omit it are
unaffected. Invalid timestamps fail fast with `400` rather than silently
returning all rows.

## Model Used

Claude — `claude-sonnet-4-6` (implementation) with `claude-opus-4-8`
review/merge-gate; tool use + 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 (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] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots (N/A — no UI change)
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green (in progress)
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
(in progress)
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Coder (Claude) <coder-claude@paperclip.ing>
2026-08-12 16:08:40 -07:00
edgardfrz c5574599b1
fix(routines): exclude assignee configuration from detail responses (#9818)
## Thinking Path

> - Paperclip is the open-source control plane people use to manage AI
agents for work.
> - Routines are the subsystem that schedules recurring work and returns
routine detail to authorized company actors.
> - Routine detail embedded the complete assignee database row even
though its shared contract requires only assignee identity.
> - That full row can contain protected adapter and runtime
configuration, including environment bindings.
> - The service boundary should project only the fields the routine
contract actually needs.
> - This pull request replaces the full-row query with a company-scoped
identity projection and adds sentinel-based regression coverage.
> - The benefit is useful routine detail without exposing protected
assignee configuration.

## Linked Issues or Issue Description

No public issue exactly tracks this service-level exposure.

- Related prior PR: Refs #4967, an older route-level redaction approach
with broader changes and no focused routine serialization test.
- Related closed PR: Refs #5144, an unmerged prior implementation of the
same identity-projection approach.
- Related agent-route hardening: Refs #8779; that work covers direct
agent responses, while this PR removes protected fields from the routine
embed itself.

Bug details:

- Actual behavior: `GET /api/routines/{routineId}` could serialize the
complete assignee row, including protected adapter/runtime
configuration.
- Expected behavior: routine detail exposes only the assignee identity
required by `RoutineDetail`, including its derived `urlKey`.
- Reproduction: assign an agent with sentinel-only protected
configuration to a routine, retrieve routine detail, and inspect key
presence or serialize the response; no production value is needed or
recorded.
- Version/commit reproduced: upstream `master` immediately before this
PR.
- Deployment mode: service-level embedded Postgres test; the vulnerable
serializer is shared by supported deployments.

## What Changed

- Added a company-scoped assignee summary query in
`server/src/services/routines.ts` that selects only `id`, `name`,
`role`, and `title`, then derives the non-sensitive `urlKey` from the
name.
- Updated `getDetail()` to use that projection instead of selecting the
complete agent row.
- Added focused negative and positive identity assertions, including the
derived `urlKey`, in `server/src/__tests__/routines-service.test.ts`.
- Audited routine list/detail serialization and broader embedded-agent
query sites; routine list exposes only `assigneeAgentId`, while other
agent embeds use explicit projections or authorized agent endpoints.

## Verification

- `pnpm exec vitest run server/src/__tests__/routines-service.test.ts` —
57/57 passed.
- Focused sentinel regression test — passed.
- `pnpm -r typecheck` — passed.
- Server, UI, and CLI builds — passed; UI gzip-size completion used a
4096 MB Node heap.
- `git diff --check` — passed.
- Full `pnpm test:run` — 2,699 passed, 1 skipped, 9 failed in untouched
tests. The failures reproduce outside this change and are limited to
local-adapter `nohup`/PTY behavior, macOS `/tmp` versus `/private/tmp`
normalization, and one workspace-runtime auto-port fixture.

## Risks

- Low compatibility risk: the returned shape now matches the existing
shared `RoutineDetail` contract.
- A consumer relying on undocumented protected agent fields inside
routine detail will stop receiving them.
- No schema, migration, deployment, credential, or production-secret
changes are included.
- Rollback is a single commit revert, but reverting would restore the
exposure.

> This is security hardening for the already-shipped routines subsystem;
`ROADMAP.md` marks Scheduled Routines complete, and this PR does not add
or duplicate roadmap feature work.

## Model Used

- OpenAI GPT-5 via Codex, with repository search, local code execution,
tests, TypeScript typechecking, builds, Git, and GitHub API use. The
runtime does not expose a more granular snapshot ID or 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 relevant tests pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes (no
documentation change is required for this contract-preserving security
fix)
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
5 with no open P2s, recommendations, or follow-ups/- [x] Greptile is 5/5
with no open P2s, recommendations, or follow-ups/5 with no open P2s,
recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: ClawdeBot <clawdebot@Mac-mini-de-ClawdeBot.local>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-08-12 16:08:31 -07:00
Constantine 276730d63e
fix(server): recognize cross-package Zod errors (#10168)
## Thinking Path

> - Paperclip validates API request bodies with Zod and converts
validation failures into client errors.
> - The global error handler recognized Zod failures with `instanceof
ZodError`.
> - Monorepo dependency layouts can provide more than one installed Zod
module instance.
> - A valid Zod error from another instance fails that identity check
and falls through as HTTP 500.
> - This pull request keeps the native path and adds a narrow structural
fallback for named Zod errors with an issues array.
> - The benefit is stable HTTP 400 validation semantics regardless of
package-instance identity.

## Linked Issues or Issue Description

Related but not duplicate: Refs #6908. That PR catches `instanceof
ZodError` inside validation middleware and returns 422; it does not
cover errors created by a second Zod module instance, which is the
reproduced failure here.

**What happened?**

An invalid `POST /api/issues/:id/work-products` payload raised a real
Zod validation error but returned HTTP 500 because the error came from a
different Zod package instance.

**Expected behavior**

All genuine Zod validation failures return HTTP 400 with validation
details, independent of module identity.

**Steps to reproduce**

1. Submit a work-product body missing the required `provider`,
`externalId`, and `url` fields.
2. Ensure the route schema is resolved from a different installed Zod
instance than the server error handler.
3. Observe HTTP 500 before this fix.
4. Observe HTTP 400 after this fix.

**Environment**

- Paperclip base: `14f20be92b86a49ff2c35495e5b0fa4d719998ef`
- Deployment: self-hosted, built from source
- Access context: board API
- Adapter scope: not adapter-specific

- [x] I searched open PRs for `ZodError`, validation errors, and
work-product validation and linked related work above.

## What Changed

- Add a narrow `readZodIssues` helper that accepts native Zod errors or
structurally valid cross-package Zod errors.
- Preserve existing HTTP 400 response shape and structured error
context.
- Add a regression for a Zod error object from another module instance.

## Verification

- `pnpm exec vitest run server/src/__tests__/error-handler.test.ts`
- `pnpm --filter @paperclipai/server typecheck`
- Full upstream CI test/build/e2e matrix passed.
- Local post-deploy smoke returned HTTP 400 for the previously failing
invalid work-product payload.

## Risks

- A deliberately thrown object named `ZodError` with an `issues` array
will be treated as a client validation failure. The effect is limited to
returning HTTP 400 instead of 500; no authorization or persistence
behavior changes.
- No schema or migration changes.

> This is a bug fix, not roadmap feature work.

## Model Used

OpenAI Codex `gpt-5.6-sol`, with tool use, code execution, repository
inspection, and independent read-only review agents.

## 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 related public work and described the bug
in-PR following the bug 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 considered documentation; no user-facing documentation
change is required
- [x] I have considered and documented 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: cucurigoo <cucurigoo@users.noreply.github.com>
2026-08-12 16:05:53 -05:00
Nicky Leach e31951a17d
feat: Claude agent setup-token login in a sandbox (#11286)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Claude agents that run in a remote sandbox need a safe in-product
login path
> - The existing host login route cannot open a pseudo-terminal inside
that sandbox
> - The login flow must protect the browser code, the login URL, and the
OAuth token at every step
> - This pull request adds the parser, the runner, a Daytona
pseudo-terminal transport, and a guarded, owner-bound session route
behind an injectable transport
> - The route stays inert in the default build and fails closed until a
sandbox provider binds the live transport
> - The benefit is a company-scoped setup-token flow with one-time
secret delivery, redaction, and fail-closed transport checks, ready for
a later staged production rollout

## Linked Issues or Issue Description

**Agent or provider**

Claude Code setup-token login for sandbox agents.

**Why this adapter is useful**

Sandbox agents need a supported way to sign in without host credentials.
An authorized owner completes the browser step and receives the token
one time.

**How the agent is invoked**

When a sandbox provider binds the injectable transport, the server
starts `claude setup-token` through a sandbox pseudo-terminal, sends the
browser code to the matched prompt, and returns the token through the
guarded session route. The default build does not bind the transport. In
that state the start route fails closed with a fixed no-secret `503`. It
does not start a process and it does not hold a sandbox lease.

**Additional context**

The transport is injectable, so each sandbox provider binds its own
pseudo-terminal. This pull request adds the Daytona transport but does
not bind it in the production server. A production wiring needs a lease
manager, a live pseudo-terminal factory, a durable token store, and its
own security review. The route keeps secrets out of logs, activity
details, errors, telemetry, and non-owner responses.

## What Changed

- Add strict parsers for the setup-token URL, the prompt, and the
success token.
- Add a login runner that drives the `claude setup-token` command
through a pseudo-terminal.
- Add the Daytona pseudo-terminal transport and the sandbox plugin
wiring.
- Add a company-scoped, owner-bound login session service with rate
limits, a reaper, cleanup, and one-time token delivery.
- Add the guarded session routes at
`/agents/:id/setup-token-login-sessions/*` behind an injectable
transport. The routes become the live login path only when a provider
binds the transport.
- Keep the start route fail-closed in the default build. It returns a
fixed no-secret `503` and it does not bind `setupTokenLogin`.
- Keep the existing host route `POST /agents/:id/claude-login` in place.
This pull request does not replace it.
- Keep confidential responses behind a fail-closed TLS transport guard
with `Cache-Control: no-store`, and extend redaction for the new fields.
- Export the parser and the runner from the Claude local server entry,
and document the new session routes in the OpenAPI spec.

## Verification

- `pnpm --filter @paperclipai/server exec vitest run setup-token-route
setup-token-session`
- `pnpm --filter @paperclipai/adapter-claude-local exec vitest run`
- `pnpm --filter @paperclipai/server run typecheck`
- Confirm that the pull request checks pass on GitHub.

## Risks

- Low user-facing risk on merge. The default build does not bind the
transport, so the production start route stays fail-closed with a `503`.
The merge does not change the production login behavior.
- When a provider later binds the transport, the flow starts a live
sandbox process and holds a short-lived in-memory secret. Cleanup must
stop the child before it releases the sandbox lease.
- The transport guard fails closed when the deployment does not provide
a trusted TLS path. A wrong proxy allowlist can block a valid request.
- The production wiring is out of scope. It needs a lease manager, a
live pseudo-terminal factory, a durable token store, and its own
security review before the server binds `setupTokenLogin`.

## Model Used

Anthropic Claude Opus 4.8 assisted the implementation. It used extended
reasoning, code execution, repository tool use, and a 200,000-token
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)
- [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 (the
OpenAPI spec covers the new session routes; no user-facing documentation
needs 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-08-12 13:02:49 -07:00
Devin Foley ff5fd62d07
Resolve the environment secret companyId context on first save (#11291)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Environments give agent runs an execution target, and their env vars
can carry secrets, so writes need a company scope for secret bindings
> - `PATCH /environments/:id` resolves that scope from a query param,
existing bindings, or a single-membership actor — and fails closed
otherwise
> - A fresh environment has no bindings yet, so an admin whose
memberships cannot pin one company gets a 422 on the very first env var
save and can never create the first binding
> - #11200 made this reachable from the UI: it opened the envVars-only
edit on the managed sandbox row, but the UI never sends the company it
already knows
> - This pull request sends the page's selected company on environment
updates and adds a server fallback to the instance's only company
> - The benefit is that the first env var save works on fresh
environments, while multi-company ambiguity still fails closed

## Linked Issues or Issue Description

Refs #11200

**What happened?**

On a fresh platform-managed sandbox environment, the first "Save
environment variables" in the UI fails with HTTP 422: "Environment
secret management requires a companyId context during the
instance-scoped transition." The same failure hits any environment
update that touches `envVars` or `config` when the environment has no
secret bindings yet and the actor's memberships do not name exactly one
company (for example an instance admin provisioned without a membership
row, or a user in two companies).

**Expected behavior**

The save succeeds. The client tells the server which company scopes the
secret bindings, and on a single-company instance the server can resolve
the only possible scope by itself.

**Steps to reproduce**

1. Provision a platform-managed sandbox environment (managed-config
`environments` entry) so the row exists with no secret bindings.
2. Sign in as an instance admin whose memberships do not resolve to
exactly one company.
3. Open Environments, edit the managed row, add an env var, and save.
4. The PATCH returns 422 with the companyId-context error.

## What Changed

- `ui/src/api/environments.ts`: `environmentsApi.update` accepts an
optional `companyId` and sends it as the `companyId` query param the
server already reads. Renamed the `customImageCompanyQuery` helper to
`companyIdQuery` since it now serves plain updates and probes too.
- `ui/src/pages/CompanyEnvironments.tsx`: both the managed envVars-only
save and the general environment save pass `selectedCompanyId`.
- `server/src/routes/environments.ts`:
`resolveEnvironmentSecretContextCompanyId` falls back to the instance's
only company when exactly one exists, mirroring the existing fallback in
`resolveCustomImageCompanyId`. Multi-company instances still fail
closed.
- Tests: three new route tests (explicit query context for a
multi-company actor, single-company-instance fallback for a
membership-less admin, fail-closed regression on a multi-company
instance), and updated the SSH-probe and probe-ambiguity tests for the
new fallback.

## Verification

- `cd server && pnpm vitest run
src/__tests__/environment-routes.test.ts` — 78 pass.
- `cd ui && pnpm vitest run src/pages/CompanyEnvironments.test.tsx` — 22
pass.
- Full `server` and `ui` vitest suites and `tsc --noEmit` on both
packages pass locally.
- Manual: on a managed instance, edit the managed sandbox environment,
add an env var, save. Before: 422. After: the save persists and the
PATCH carries `?companyId=`.

## Risks

- Low risk. The explicit query param path already existed on the server;
the UI now uses it.
- Behavioral shift: on single-company instances, environment
secret-context resolution (including `POST /environments/:id/probe`) now
resolves the only company instead of returning no context. That lets
probes resolve secret-backed config where they previously returned a 422
asking for an explicit companyId. Multi-company instances keep the
fail-closed behavior, covered by a regression test.
- Self-hosted single-company instances gain the same first-save fix; no
schema or config changes.

## Model Used

Claude Fable 5 (`claude-fable-5`, Anthropic) via Claude Code — extended
thinking, agentic 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
- [ ] 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-08-12 11:32:15 -07:00
Nicky Leach f1931d0e14
test(server): fix flaky workspace-busy retry-row read race (#11293)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The server heartbeat system records each agent run and its retry
state
> - A workspace-busy deferral cancels one run before it inserts the
scheduled retry row
> - The test helper can return after the cancel write and before the
retry-row insert
> - A direct read can then return no row and fail a valid retry
assertion
> - This pull request makes presence reads wait for the retry row
> - The benefit is stable test coverage without a production behavior
change

## Linked Issues or Issue Description

Refs: #10806

**What happened?**

The workspace-busy test read the retry row after the first deferral
write. The helper returned before the scheduled-retry insert completed.
The read then returned no row and failed the retry assertions.

**Expected behavior**

The test must wait until the scheduled-retry row exists before it checks
retry-row fields. The production write order must stay unchanged.

**Steps to reproduce**

1. Add a 300 ms delay between the deferral writes.
2. Run `server/src/__tests__/heartbeat-workspace-busy.test.ts`.
3. Observe failures at retry-row presence checks.
4. Add the bounded polling helper.
5. Run the test file again and observe that all presence checks pass.

**Paperclip version or commit**

Commit `d9b6e8a6e62b9b56919fc9c52d294e8ac569f70f`.

**Deployment mode**

Local test run from source.

## What Changed

- Add `waitForRetryRun`, which polls for the retry row with a 10 second
timeout and a 50 millisecond interval.
- Use the helper at every test site that reads a retry row after
deferral.
- Keep direct reads at absence assertions.
- Keep production code unchanged.

## Verification

- Injected a temporary 300 millisecond delay between the two production
writes and reproduced the five presence-site failures.
- Applied the helper with the delay and passed the test file 15 out of
15 times.
- Removed the temporary production delay.
- Ran the changed test file 25 consecutive times with 0 failures.
- Ran TypeScript checks for the changed test file with no errors.

## Risks

Low risk. This pull request changes test code only. The helper has a
bounded timeout. Production behavior and retry-row assertions remain
unchanged.

## Model Used

OpenAI Codex, GPT-5, reasoning mode, tool use, and code execution. The
runtime does not expose the 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-08-12 11:29:51 -07:00
Devin Foley 2c53437fc9
fix(server): authenticate cloud-proxied browsers on the live-events websocket (#11290)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The UI receives live run/issue events over a websocket at
`/api/companies/:id/events/ws`; the server authorizes upgrades with a
bearer token or a Better Auth session
> - On a cloud-managed deployment, browsers authenticate through trusted
`x-paperclip-cloud-*` headers injected by the managing front door — they
never hold a local Better Auth session, and the Express middleware lane
that understands those headers is not consulted for websocket upgrades
> - Every browser websocket upgrade behind the front door therefore
resolves no identity and is rejected 403: the live-events socket has
never connected on a managed instance, leaving permanent reconnect churn
and console failure noise while the UI silently degrades to polling
> - This pull request adds a cloud-actor lane to the upgrade
authorization, reusing the same trusted-header resolver the HTTP
middleware uses
> - The benefit is working realtime updates on managed instances, an end
to the reconnect churn, and unchanged self-hosted behavior

## Linked Issues or Issue Description

No existing issue. Description follows the bug template:

**What happened?**

On a cloud-managed instance, the browser console shows `WebSocket
connection to 'wss://…/api/companies/<id>/events/ws' failed:` repeating
indefinitely for every company, on a healthy instance. The server
rejects each upgrade with 403 because `authorizeUpgrade` in
`server/src/realtime/live-events-ws.ts` only knows bearer tokens and
Better Auth sessions, while cloud-proxied browsers authenticate via
`x-paperclip-cloud-*` trusted headers (handled only by the Express
`actorMiddleware` lane in `server/src/middleware/auth.ts`).

**Expected behavior**

A browser that authenticates through the trusted cloud headers can open
the live-events websocket for any company in its membership scope,
exactly as it can call the HTTP API for those companies.

**Steps to reproduce**

1. Run Paperclip in `authenticated` mode behind a proxy that injects the
`x-paperclip-cloud-*` headers with a valid
`PAPERCLIP_CLOUD_TENANT_SERVER_TOKEN`.
2. Load any company page in a browser (no local Better Auth session).
3. HTTP API calls succeed; every `/events/ws` upgrade is rejected 403
and the UI retries forever.

## What Changed

- `server/src/middleware/auth.ts`: `resolveCloudTenantActor` now accepts
a minimal `CloudActorHeaderSource` (`header(name)`) instead of an
Express `Request` — `Request` satisfies it unchanged — plus
`cloudActorHeaderSourceFromHeaders` to adapt raw
`IncomingMessage.headers`.
- `server/src/realtime/live-events-ws.ts`: `authorizeUpgrade` gains an
injected `resolveCloudActor` lane, tried before the Better Auth session
fallback in `authenticated` mode. A resolved cloud actor is
authoritative: the upgrade is authorized only for a company in the
actor's membership scope (`companyIds`, the same scope the HTTP lane
grants). Absent/unresolvable cloud headers fall through to the session
path.
- `server/src/index.ts`: wires `resolveCloudActor` through
`resolveCloudTenantActor` + the header shim. The resolver self-gates:
without `PAPERCLIP_CLOUD_TENANT_SERVER_TOKEN` and a matching trust token
it returns null, so self-hosted deployments never take this path.
- Tests: upgrade authorized for an in-scope company (session resolver
not consulted), rejected for an out-of-scope company, fall-through to
session auth when no cloud actor resolves; header-shim resolution from a
raw lowercased header map including `string[]` values.

## Verification

- `pnpm vitest run server/src/__tests__/live-events-ws.test.ts
server/src/middleware/cloud-tenant-actor.test.ts` — 25 tests pass.
- `pnpm typecheck` in `server/` — clean.
- Not verified live end-to-end: that requires a managed instance running
this build; the direct probe evidence (HTTP authenticated fine, every WS
upgrade 403) matches the code path exactly.

## Risks

Low risk. The new lane only activates when the deployment configures the
cloud trust token and the request presents it; both checks already
protect the HTTP lane. Authorization scope is the same `companyIds` set
the HTTP middleware computes (primary stack company plus the user's real
membership rows). The cloud resolver's user/company materialization
writes are debounced (existing behavior shared with the HTTP lane), so
websocket reconnect storms do not amplify database writes. Self-hosted
instances see no behavioral change, covered by the fall-through test.

## Model Used

- Claude (Anthropic), Claude Fable 5 (`claude-fable-5`), via Claude Code
CLI with extended thinking and tool use (code search, edit, test
execution; diagnosis included live websocket handshake probes against a
managed 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-08-12 11:22:50 -07:00
Nicky Leach 6a5b293240
test(server): fix onboarding first-task teardown foreign-key race (#11284)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The server coordinates issue work and heartbeat runs.
> - The onboarding first-task route sends an assignment wake in the
background.
> - The route test removes related database rows during teardown.
> - A late heartbeat run can keep foreign-key child rows alive during
teardown.
> - This pull request drains the wake and deletes run rows in
foreign-key order.
> - The benefit is a stable test that keeps the onboarding behavior
unchanged.

## Linked Issues or Issue Description

**What happened?**

The onboarding first-task route sent a background assignment wake. The
test teardown removed parent rows before the wake-created heartbeat rows
finished.

**Expected behavior**

The test teardown should wait for the background wake and remove
heartbeat rows before it removes their parent rows.

**Steps to reproduce**

1. Run the onboarding first-task route test.
2. Repeat the test many times.
3. Observe an intermittent foreign-key error during teardown.

**Paperclip version or commit**

Commit `c30fe965920eeb7e7fb88e17574a65bed8fc01a4`.

**Deployment mode**

Local dev (pnpm dev).

**Installation method**

Built from source (pnpm dev / pnpm build).

**Agent adapter(s) involved**

Not adapter-specific (core bug).

**Database mode**

Embedded PGlite (default — DATABASE_URL unset).

## What Changed

- Stub the server adapter in the route test so the dispatched run
finishes at once.
- Drain heartbeat runs to quiescence before teardown.
- Delete heartbeat runs and child rows before their parent rows.
- Delete runtime state and company skill rows in foreign-key order.
- Keep the route behavior and all three test assertions unchanged.

## Verification

- Run `pnpm exec vitest run
src/__tests__/issue-onboarding-first-task-routes.test.ts` from the
`server` package.
- The author ran the suite 25 times with 25 passes.
- The suite reproduced the teardown foreign-key error before this
change.

## Risks

Low risk. This change affects one test file and does not change product
code or route behavior.

## Model Used

OpenAI GPT-5. The model used tool calls and code review assistance. The
exact context window and reasoning mode were not exposed 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] 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-08-12 10:42:58 -07:00
Nicky Leach f9bd0438e1
fix(server): stop terminal workspace reaper starving on oldest candidates (#11238)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The server runs a scheduled reaper that archives terminal workspaces
after it checks their state.
> - The reaper reads candidates in `updatedAt` order and skips
candidates that do not qualify for archive.
> - The fixed page kept the same skipped candidates at the front, so the
reaper did not inspect later eligible workspaces.
> - This pull request adds a keyset cursor and a throttled log for
sweeps that archive no workspace.
> - The benefit is that the reaper inspects all candidates over time and
reports an inert sweep.

## Linked Issues or Issue Description

**What happened?**

The terminal workspace reaper inspected a fixed page of old candidates.
Ineligible candidates stayed in that page, so the reaper skipped later
eligible workspaces on every sweep.

**Expected behavior**

The reaper must inspect each candidate over time and archive every
eligible terminal workspace.

**Steps to reproduce**

1. Create more than 50 terminal workspace candidates.
2. Keep the oldest page ineligible for archive.
3. Place an eligible workspace after that page.
4. Run repeated reaper sweeps.
5. Observe that the later eligible workspace remains unarchived.

**Paperclip version or commit**

Commit `3efdf555e6e14a46747c796c3c554438bfc03261`.

**Deployment mode**

Built from source with the server test suite.

**Installation method**

Built from source.

**Agent adapter(s) involved**

Not adapter-specific (core bug).

**Database mode**

Not database-related.

## What Changed

- Add a keyset cursor that uses `(updatedAt, id)` order across reaper
pages.
- Reset the cursor at the end of the candidate set so the next sweep
starts at the beginning.
- Add a throttled log when a sweep inspects candidates but archives
none.
- Add regression tests for archive delivery and starvation.

## Verification

- `pnpm --filter @paperclipai/server exec vitest run
src/__tests__/execution-workspaces-service.test.ts` — 45 tests pass.
- `pnpm --filter @paperclipai/server exec vitest run
src/__tests__/server-startup-feedback-export.test.ts` — 16 tests pass.
- `pnpm --filter @paperclipai/server typecheck` — clean.

## Risks

Low risk. The change affects only candidate paging and the related
reaper log. The cursor resets after the candidate set, so the sweep
remains periodic.

## Model Used

Codex, OpenAI GPT-5, extended reasoning, 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 linked
them above (no exact duplicate found; related scheduler PR #10911 is
distinct)
- [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
documentation applies; this is an internal reaper behavior change)
- [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-08-12 10:21:00 -07:00
Nicky Leach e5a7fd7038
Add sandbox device-login for the Codex adapter (#11237)
## Thinking Path

> - Paperclip helps people manage AI agents for work.
> - Agent adapters connect Paperclip to tools such as the Codex command
line tool.
> - A sandboxed Codex agent may start without a credential.
> - The operator needs a safe sign-in flow that does not expose
credentials to the shared package or the sandbox.
> - This pull request adds a company-scoped device-login flow with a
temporary Daytona sandbox.
> - The flow promotes the credential only after readiness checks pass
and removes the temporary sandbox after use.
> - The result lets an operator sign in to a sandboxed Codex agent from
the agent form.

## Linked Issues or Issue Description

**Subsystem affected**

Cross-cutting (multiple of the above)

**Problem or motivation**

A Codex adapter that runs in a sandbox cannot authenticate when the
company has no pre-provisioned Codex credential.

**Proposed solution**

Add a company-scoped device-login session. Start a temporary sandbox,
run `codex login --device-auth`, stream the code and URL, verify
readiness, promote the credential, and delete the sandbox.

**Alternatives considered**

Pre-provisioning a credential does not support first-time sandbox login.
Keeping the credential in the login sandbox does not provide a durable
company credential.

**Roadmap alignment**

This supports the roadmap item for cloud and sandbox agents.

**Additional context**

The flow uses a five-minute cleanup reaper, compare-and-set status
changes, and a PostgreSQL advisory lock to protect promotion and
cleanup.

## What Changed

- Add the adapter login-session contract, database table, and migration.
- Add company-scoped server routes and a service for sandbox device
login.
- Add credential promotion, readiness checks, and cleanup after login.
- Add restart-safe cleanup for abandoned login sandboxes.
- Add sandbox login controls to the agent creation and edit forms.
- Keep device-login and vendor identifiers out of public shared and
adapter UI symbols.

## Verification

- `pnpm --filter @paperclipai/adapter-codex-local exec vitest run`
passed with 310 tests at the submitted commit.
- The server login route, service, and reaper tests passed with 45 tests
at the submitted commit.
- The agent form render tests passed with 26 tests at the submitted
commit.
- The public-symbol leak check passed at the submitted commit.
- A live Daytona sign-in flow still requires confirmation by a user with
a live sandbox.

## Risks

The migration adds a new company-scoped table. A promotion or cleanup
race could remove a credential or leave a sandbox active, so the service
uses claims, compare-and-set transitions, and an advisory lock. The live
Daytona flow needs operator confirmation because local tests do not
provide a real browser sign-in.

## Model Used

OpenAI Codex, GPT-5, tool use and code execution, extended reasoning.

## 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-08-12 08:58:25 -07:00
Devin Foley d5bb396518
fix: pass sandbox provider credential env vars to plugin workers; hide Local default under managed-sandbox-only (#11244)
<!-- 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
> - Environments give each agent run an execution target, and sandbox
providers (Daytona, E2B, Novita, exe.dev) run as plugin workers
> - A managed deployment provisions one platform-managed sandbox row
with no credential in config; the provider is documented to fall back to
its process env var (for example `DAYTONA_API_KEY`)
> - Plugin workers spawn with a scrubbed environment, so that fallback
never sees the host env var — probe and lease acquisition fail with
"require an API key in config or DAYTONA_API_KEY" even when the
deployment sets the var
> - Separately, the managed-sandbox-only mode hides local rows from
every list, but the instance Default picker renders a hardcoded
synthetic "Local" option that no filter touches
> - This pull request forwards each bundled provider's documented
credential env var to its own plugin worker, and gates the synthetic
Local option on the flag
> - The benefit is that the documented host-env credential fallback
works for plugin-backed providers, and managed-sandbox-only instances no
longer offer Local anywhere

## Linked Issues or Issue Description

**Subsystem affected**

Plugin worker environment construction
(`server/src/services/plugin-loader.ts`) and the environments UI
(instance Default picker, agent form inherited-environment label).

**Problem or motivation**

Two follow-ups to the managed-sandbox-only mode (#11200), both found on
a live managed deployment:

1. The deployment sets `DAYTONA_API_KEY` as a server env var and the
managed sandbox row omits `config.apiKey` by contract. "Test Connection"
fails with `Sandbox environment probe failed for provider "daytona".
Daytona sandbox environments require an API key in config or
DAYTONA_API_KEY.` A real agent run fails the same way at lease
acquisition. The cause: sandbox providers run as plugin workers, and
`buildPluginWorkerEnv` passes only model-provider keys and in-cluster
Kubernetes vars. The provider's own documented credential env var never
reaches the worker, so the in-plugin `process.env` fallback reads
nothing. The self-hosted path has the same gap: the Daytona plugin
README documents `DAYTONA_API_KEY` as a host-level fallback, and it does
not work today.

2. With `enableManagedSandboxOnly` on, the instance Default environment
picker still shows "Local". The server filters local *rows* out of the
list, and the client filter mirrors that for cached lists, but this
option is a hardcoded `<option value="">Local</option>` — not a list row
— so no filter removes it. Selecting it writes a null default, which run
selection then rejects fail-closed.

**Proposed solution**

Forward each bundled sandbox provider's documented credential env var
into its plugin worker, keyed by the manifest's declared
`environmentDrivers[].driverKey` so a worker only receives its own
provider's credential (daytona → `DAYTONA_API_KEY`, e2b → `E2B_API_KEY`,
exe-dev → `EXE_API_KEY`, novita → `NOVITA_API_KEY`). Keep the existing
gate: only plugins that declare `environment.drivers.register` receive
any passthrough. In the UI, render the synthetic Local option only when
managed-sandbox-only is off; under the flag show a disabled "Select
environment" placeholder only while no default is stamped yet, and stop
the agent form's inherited label from reading "Local".

**Alternatives considered**

Adding `DAYTONA_API_KEY` to the existing `ADAPTER_ENV_PASSTHROUGH` list
was rejected: that list goes to every environment-driver plugin, so each
provider would receive every other provider's credential. A manifest
schema field for declared credential env vars was rejected as heavier
than needed: the bundled providers are known, and the mapping lives next
to the two existing passthrough lists.

## What Changed

- `server/src/services/plugin-loader.ts`: new
`SANDBOX_PROVIDER_CREDENTIAL_ENV_PASSTHROUGH` map (driverKey →
documented credential env vars). `buildPluginWorkerEnv` reads the
manifest's `environmentDrivers` and forwards only the matching vars,
after the existing `environment.drivers.register` gate. Blank values
stay excluded.
- `server/src/__tests__/plugin-database.test.ts`: the daytona worker
receives `DAYTONA_API_KEY` and not another provider's key; a plugin
whose drivers have no mapping (kubernetes) receives no credential var.
- `ui/src/pages/CompanyEnvironments.tsx`: the Default picker's synthetic
Local option renders only when managed-sandbox-only is off. Under the
flag, a disabled "Select environment" placeholder renders only while the
default is unset.
- `ui/src/pages/CompanyEnvironments.test.tsx`: the Local option is
present by default and absent under the flag; saved non-local
environments stay selectable.
- `ui/src/components/AgentConfigForm.tsx`: the inherited-environment
label falls back to "Managed sandbox" instead of "Local" under the flag.

## Verification

- `server`: `npx vitest run src/__tests__/plugin-database.test.ts -t
buildPluginWorkerEnv` — 5 passed (3 existing, 2 new).
- `ui`: `npx vitest run src/pages/CompanyEnvironments.test.tsx` — 22
passed (2 new); `npx vitest run
src/components/AgentConfigForm.render.test.tsx` — 10 passed.
- `tsc --noEmit` clean in `server` and `ui`.
- Live managed deployment: confirmed the tenant service env carries
`DAYTONA_API_KEY` while the probe fails with the exact message above,
which pins the root cause to the worker env, not delivery.

## Risks

- The worker env grows by exactly one var per matching bundled provider,
only when the deployment sets it and only for plugins that declare a
matching environment driver. Plugins without a mapping see no change.
- Self-hosted behavioral shift is the fix itself: a host-level
`DAYTONA_API_KEY` (or E2B/EXE/NOVITA equivalent) now reaches the
provider as its README documents. Deployments that set the var but
expected it to stay inert had no working configuration to preserve — the
provider errored on every keyless probe and run.
- UI change is inert unless `enableManagedSandboxOnly` is on (default
false everywhere).

## Model Used

Claude Fable 5 (`claude-fable-5`) via Claude Code — extended thinking,
tool use, parallel read-only subagents for the two root-cause traces.

## 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
2026-08-11 19:10:45 -07:00
Devin Foley 0a95ada1be
feat(server): chunked import preview endpoint and resumable upload in the Import page and CLI (#11224)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The previous pull request added server-side chunked resumable import
transfers; without clients, large imports still ride the single fragile
upload
> - The Import page and the CLI need to slice large packages, upload
parts with retry and progress, resume after interruptions, and preview
before applying
> - Preview is the missing server piece: the browser flow is
preview-then-import, so a completed spool must be previewable without
re-uploading
> - This pull request adds the transfer preview endpoint, switches the
Import page to the chunked path for zips over 48 MB, and teaches the CLI
the same for oversized local imports
> - The benefit is that large imports get progress, per-part retry, and
resume in both clients, while small imports keep the exact single-shot
path they have today

## Linked Issues or Issue Description

**What happened?**

With only the server transfer routes in place, users still upload large
company packages as one request from the Import page and the CLI: no
progress indication, no retry below the whole file, and no resume after
a dropped connection or refresh. The preview-then-import flow also
cannot run against an uploaded transfer, forcing a second full upload.

**Expected behavior**

A large package uploads once as verified parts with visible progress;
preview and import both run against the uploaded spool; an interrupted
upload resumes with only the missing parts re-sent; packages at or below
48 MB behave exactly as before.

**Steps to reproduce**

1. Select a 500 MB zip on the Import page over an unreliable connection.
2. Watch the single upload fail near the end and restart from zero,
twice — once for preview, once for import.
3. Same story headless via the CLI.

## What Changed

- Server: `POST /import/transfers/:id/preview` runs the existing preview
logic against the completed spool (shared assembly + whole-file
verification helper with apply); preview neither completes the run nor
deletes the spool, so the subsequent apply reuses it. Missing parts
respond with the missing list.
- UI: zips over 48 MB take the chunked path in both preview and import —
the file is sliced into 32 MB parts hashed with WebCrypto (single
ArrayBuffer, no second copy), the transfer is created or resumed (the
create response's missing-parts list drives what uploads), parts upload
sequentially with three attempts each and visible progress, then
transfer preview/apply replace the multipart calls. The existing preview
pane, collision handling, adapter overrides, and async job polling are
unchanged; ≤ 48 MB keeps the single-shot path.
- CLI: oversized local `.zip` or folder imports zip/slice/hash with node
crypto, upload with resume and per-part retry and progress lines, and
use transfer preview/apply. Small packages keep the inline path
byte-identical.
- Failure honesty: adapters/API errors fail open to existing behavior; a
part failing all attempts surfaces a durable error panel with resume
intact.

## Verification

- Server: preview-then-apply on one spool (run stays open, spool intact,
then apply completes), preview with missing parts rejected — added to
the transfer route suite (embedded Postgres).
- UI suite: large file takes the chunked path (manifest shape, part
uploads, progress, apply on a resumed transfer, single-shot endpoints
never called), small file stays single-shot, part failure after three
attempts surfaces the error panel without running preview, resume
re-uploads only the missing part.
- CLI: manifest slicing/hashing, threshold behavior for zip and folder
sources, folder-zip round-trip through the real zip reader, upload
resume/retry/exhaustion/already-completed, full-command chunked and
small-zip inline flows.
- Server, ui, cli typechecks clean. Exact counts in the PR checks.

## Risks

- The 48 MB threshold only routes between two verified paths; behavior
below it is untouched.
- Chunked CLI imports use the board-scoped transfer routes, so oversized
CLI imports need board credentials (agent tokens keep the agent-safe
small-file path). No privilege change — board actors already had the
generic routes — but the two size regimes differ semantically; called
out for review.
- A CLI dry-run over the threshold uploads parts before previewing; the
spool persists (24 h sweep) and a later apply resumes without re-upload
— inherent to preview-against-spool.

Stacked on #11223 — merge that first; this PR then shows only the
preview endpoint and client changes.

## Model Used

- Claude Fable 5 (`claude-fable-5`) via Claude Code CLI, with extended
thinking and tool use (multi-agent implementation with independent
verification).

## 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-08-11 16:06:37 -07:00
Devin Foley 23a1b025c2
feat(server): chunked resumable company import transfers (#11223)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Company import moves large packages into an instance, and since the
upload cap rose to 1 GB, the transport is the weak point: one HTTP
request, buffered fully in memory, with no resume
> - A dropped connection at 90% of an 800 MB upload starts the whole
transfer over, and a server restart loses all progress
> - This pull request adds the server side of chunked resumable import
transfers: a durable run ledger and routes that accept the same import
zip as verified ~32 MB parts spooled to disk
> - An interrupted transfer resumes from the parts already uploaded —
across dropped connections, page refreshes, and server restarts — and
peak upload memory drops from the whole package to one part
> - The benefit is that large imports become reliable on real-world
connections instead of all-or-nothing

## Linked Issues or Issue Description

**What happened?**

Large company imports travel as a single HTTP upload. On a slow or flaky
connection, any interruption discards all progress and the upload
restarts from zero. The server buffers the entire compressed package in
memory during upload. A server restart mid-upload loses the transfer
entirely. With the upload cap now at 1 GB, these failure modes govern
exactly the imports the cap was raised for.

**Expected behavior**

A large import upload survives interruptions: already-transferred data
is kept and verified, only the missing remainder is re-sent, and the
server's memory use during upload is bounded by a part, not the package.

**Steps to reproduce**

1. Import a multi-hundred-MB company package over a connection that
drops mid-upload.
2. The upload fails; retrying starts from byte zero.
3. Repeat on an unstable connection and the import may never complete.

## What Changed

- New `company_transfer_runs` table (drizzle schema + migration) and
`companyTransferRunService`: one row per transfer with a content-derived
idempotency key, per-part completion recorded atomically and
idempotently, resume scoped to actor and direction, completed runs
short-circuiting retries of identical content.
- New transfer routes beside the existing import routes, same
authorization: declare a sliced zip (`POST /import/transfers` —
validates cap, 64 MB part ceiling, contiguity, size sums, sha256
format), upload parts (`PUT .../parts/:n` — raw body, hash-and-size
verified before an atomic write to a disk spool under the instance root;
re-uploads are no-op successes), poll resume state (`GET .../:id` —
missing parts recomputed from disk), and apply (`POST .../:id/apply` —
requires all parts, re-verifies the assembled zip against the whole-file
hash fail-closed, then feeds the existing import pipeline through
factored helpers rather than duplicated logic).
- Hourly sweep fails and cleans spools idle for 24 h; a swept transfer
honestly reports all parts missing on resume.
- Strict UUID gating on run ids before any filesystem path construction.
- The existing single-shot upload path is untouched; clients arrive in
the follow-up PR.

## Verification

- Transfer route suite (embedded Postgres): create/upload/status/apply
round-trip with a real imported company, out-of-order parts, wrong-hash
part rejected and unrecorded, re-upload no-op, apply-with-missing-parts
rejection, resume after failure with prior progress intact,
assembled-hash mismatch failing closed with spool deletion, actor
scoping 404s, async-job apply, sweep followed by honest resume.
- Ledger suite (embedded Postgres): part idempotency, actor/direction
scoping, completed-run short-circuit, cancelled runs staying cancelled.
- Existing portability route suite unchanged and green; server + db
typechecks clean. Exact counts in the PR checks.

## Risks

- New routes are additive; the existing import path is untouched. The
transfer routes carry the same board authorization as the import routes
they sit beside.
- Disk spool: bounded by the existing upload cap per transfer, cleaned
on success, failure, hash mismatch, and by the 24 h sweep. Spool paths
are strict-UUID-gated.
- The apply step still materializes the assembled zip in memory once
(same profile as today's single-shot import at apply time); upload-time
memory drops to one part.
- Known limitation, deliberate: transfers are keyed on content alone, so
identical package content cannot be imported twice without re-exporting
(surfaced explicitly to the caller). Acceptable for v1; noted for
review.

## Model Used

- Claude Fable 5 (`claude-fable-5`) via Claude Code CLI, with extended
thinking and tool use (multi-agent implementation with independent
verification).

## 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-08-11 15:25:07 -07:00
Dotta 2494a2a0fe
perf: add repeatable issue-detail baseline rig (#10409)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The issue detail page is a core operator surface where perceived
latency directly affects task navigation
> - Performance work needs repeatable evidence so later optimizations
can be compared against the same scenarios
> - The page did not expose stable user-timing marks for its header or
first useful content
> - There was also no isolated seeded browser rig that measured warm
navigation, cold deep links, waterfalls, or server time
> - This pull request adds the instrumentation and a one-command
Playwright baseline harness
> - The benefit is that issue-page performance changes can be validated
with reproducible median measurements instead of anecdotes

## Linked Issues or Issue Description

**Subsystem affected**

Cross-cutting: `ui/`, `server/`, and browser performance tooling.

**Problem or motivation**

The issue detail page performs a large client bootstrap and request
fan-out, but the repository lacks stable user-timing boundaries and a
repeatable benchmark. That makes performance changes difficult to
compare and allows regressions to be judged from anecdotes instead of
consistent evidence.

**Proposed solution**

Add stable header/content paint measures, development/QA-only lifecycle
vital reporting, aggregate server timing for the issue endpoint, and a
seeded Playwright command that runs warm/cold scenarios under throttled
and unthrottled profiles with N≥5 median reporting.

**Alternatives considered**

Ad hoc DevTools recordings were rejected because they are not repeatable
or reviewable. Production telemetry was rejected because this baseline
should not change production data collection. A unit-only harness was
rejected because it cannot capture browser bootstrap, rendering, and
network waterfall costs.

**Roadmap alignment**

The roadmap calls for agent performance to be measurable over time. This
change applies that evidence-first principle to a core operator page and
does not duplicate a listed roadmap deliverable.

**Additional context**

The generated report includes warm and cold medians, TTFB/FCP/LCP where
applicable, request and byte totals before first useful content,
JavaScript bytes, and issue endpoint server timing.

## What Changed

- Added `issue-detail:navigate→header-paint` and
`issue-detail:navigate→content-paint` user-timing measures to the issue
detail page.
- Added development/QA-only TTFB, LCP, and INP console reporting without
production telemetry delivery.
- Added `Server-Timing` for `GET /api/issues/:id`.
- Added `pnpm exec playwright test --config
tests/perf/issue-detail/playwright.config.ts`, which seeds an isolated
instance and runs N≥5 warm/cold samples under unthrottled and Fast 4G/4x
CPU profiles.
- Added Markdown, raw JSON, and Chrome-trace outputs with median
baseline tables and waterfall data.

## Verification

- `pnpm --filter @paperclipai/ui typecheck`
- `pnpm --filter @paperclipai/server typecheck`
- `pnpm check:token-gates`
- `npx playwright test --config
tests/perf/issue-detail/playwright.config.ts --list`
- `pnpm exec playwright test --config
tests/perf/issue-detail/playwright.config.ts` — passed 20 samples in 9.4
minutes (5 runs × 2 scenarios × 2 profiles) for the baseline;
post-review integrity reruns also exercised the corrected paths, while
this shared runner intermittently killed Chromium processes, so the rig
now performs one bounded browser-crash retry per sample.
- Baseline medians: warm unthrottled 278/447 ms header/content; cold
unthrottled 646/646 ms; warm throttled 1240/2060 ms; cold throttled
3932/3933 ms.

## Risks

- Low product risk: the new browser measurements are development/QA
tooling and the UI timing work does not change visible layout.
- `Server-Timing` exposes only aggregate handler duration, not query
contents or private identifiers.
- Native INP reporting uses supported browser event timing entries and
silently no-ops where unsupported.

> 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.4, tool-assisted coding and browser execution with
reasoning enabled; context-window size is not exposed 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] 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: Dev Agent <dev@paperclip.ing>
2026-08-11 15:18:29 -04:00
Devin Foley 0044fa8904
Let tenants edit env vars on managed sandbox environments; add managed-sandbox-only mode (#11200)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Environments give each agent run an execution target: the local
host, SSH, or a sandbox provider
> - A managed deployment can provision one platform-managed sandbox
environment through the `PAPERCLIP_MANAGED_CONFIG` `environments`
section
> - That row is fully locked today. A tenant cannot add environment
variables for their agents. There is also no way to hide local execution
— run selection falls back to the local row
> - A platform that manages the sandbox for its tenants needs both: the
tenant adds env vars (and nothing else), and local execution is neither
visible nor reachable
> - This pull request opens exactly one tenant edit (env vars) on the
managed sandbox row, and adds an `enableManagedSandboxOnly` mode that
hides local and makes run selection fail closed
> - The benefit is a complete managed-sandbox experience with no change
for self-hosted instances

## Linked Issues or Issue Description

**Subsystem affected**

Environments (managed sandbox provisioning, environment routes, run
environment selection) and the environments UI.

**Problem or motivation**

Platform-provisioned sandbox environments
(`metadata.managedByPaperclip`) reject every write on cloud-managed
instances. Agents often need environment variables inside their sandbox.
The tenant has no way to set them on the managed row. Separately, an
operator cannot remove local execution: the environment list always
shows the local row, and run selection falls back to it when no default
is set.

**Proposed solution**

Allow an envVars-only PATCH on the managed sandbox row, and echo those
env vars back for editing. Add a managed-tier feature
(`enableManagedSandboxOnly`) that hides the local environment from all
read surfaces and redirects local-landing run selection to the managed
sandbox environment, failing closed when it is unavailable.

**Alternatives considered**

UI-only hiding of the local row. This was rejected: it does not stop a
run from resolving to local, so it is presentation without enforcement.
Full unlock of the managed row was also rejected: name, driver, and
config stay platform-owned so boot reconciliation cannot fight tenant
edits.

## What Changed

- `server/src/routes/environments.ts`: the platform-provisioned write
floor admits an envVars-only PATCH on the generalized managed sandbox
row (sandbox driver, `managedByPaperclip`, not legacy kubernetes-marker
rows). Name, driver, config, status, metadata, and DELETE stay rejected.
The read floor stops blanking env vars on that row; credential-shaped
config keys stay redacted for every actor. Legacy kubernetes-marker rows
keep the full floor.
- Same file: under `enableManagedSandboxOnly`, the environments list and
the by-id read omit the local row for every actor, including instance
admins.
- `server/src/services/execution-workspace-policy.ts`:
`resolveExecutionWorkspaceEnvironmentId` gains the managed-sandbox-only
inputs. A selection that lands on the local environment is redirected to
the managed sandbox environment. With no active managed row it throws
`ManagedSandboxUnavailableError` — never local. Non-local selections
(ssh, user-created sandboxes) are untouched.
- `server/src/services/heartbeat.ts`: the run path reads the flag, looks
up the managed row (`findManagedSandboxEnvironment`, new read-only
finder in `environments.ts`), and passes both to the resolver. Mirrors
the forced-kubernetes precedent, which keeps precedence when both
regimes are on.
- `server/src/services/managed-environments.ts`: after a successful
reconcile, the instance default environment moves to the managed sandbox
row when the current default is unset, local, or dangling. A
tenant-chosen custom environment is never overridden.
- `packages/shared`: new `enableManagedSandboxOnly` key (schema default
false, catalog tier `managed`, cloudDefault false, selfHostedDefault
false) and the matching interface field.
- UI: managed rows show a "Managed by Paperclip" lock badge; editing one
opens a dedicated env-vars-only editor that sends the one PATCH shape
the server admits (the old full form failed with a 403 on save). New
`ui/src/lib/managed-sandbox-environment.ts` mirrors the local filter for
cached lists (applied in the project picker; the agent picker already
excluded local). The experimental settings page gains the toggle at its
alphabetical card position. `environmentsApi.update` now declares the
`envVars` field it already sent.
- Tests: environment route floor coverage (envVars-only accepted, mixed
bodies rejected, legacy rows still blanked and locked, local hidden and
404 under the flag, self-hosted unchanged), an embedded-postgres service
test pinning that boot reconciliation never touches tenant env vars,
resolver redirect/fail-closed cases, managed-environments
default-stamping cases, and UI lib/settings tests.

## Verification

- `pnpm --filter @paperclipai/server exec vitest run
src/__tests__/environment-routes.test.ts
src/__tests__/environment-service.test.ts
src/__tests__/execution-workspace-policy.test.ts
src/services/managed-environments.test.ts` — all pass.
- `pnpm --filter @paperclipai/shared exec vitest run` — 425 pass
(catalog/schema default parity is pinned by an existing test).
- `pnpm --filter @paperclipai/ui exec tsc --noEmit` and the affected UI
suites (CompanyEnvironments, InstanceExperimentalSettings incl.
card-order test, new lib test) — all pass.
- Full workspace `pnpm test`: 3,414 passed. 17 files report failures on
this machine; the identical 17 fail on a clean `origin/master` worktree
in the same environment (git-worktree/skills/embedded-postgres
environment dependencies and plugin-SDK zero-test collections). One
additional file (`issue-monitor-scheduler.test.ts`) failed one
timing-sensitive test in one of two full-suite runs and passes 7/7 in
isolation on this branch — a flake in a domain this diff does not touch.
The branch introduces no new failures.
- Self-hosted zero-delta: every new behavior is gated on the
cloud-managed instance check or the new flag, which defaults to false in
schema and catalog; pinned by the "does not floor platform-marked rows
on self-hosted instances" and flag-off tests.

## Risks

- Behavior is opt-in twice over: the write-floor exception applies only
to rows the managed-config provisioner stamps, and the hiding/forcing
applies only when `enableManagedSandboxOnly` is on (default false
everywhere). Self-hosted instances see no change.
- The env-vars echo is scoped to the generalized managed sandbox row;
legacy kubernetes-marker rows keep the blanket floor because
pre-generalization builds may have written platform values there.
- Fail-closed run selection means a managed instance with the flag on
and an archived managed row (provider plugin down) refuses runs with a
precise error instead of running locally. That is the intended posture.

## Model Used

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

## 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
2026-08-11 11:40:57 -07:00
Dotta b847e8b6f6
perf(server): reduce issue detail request overhead (#10414)
## Thinking Path

> - Paperclip is the open source control plane people use to coordinate
AI-agent work
> - Opening an issue fans out into several authenticated issue-detail
reads, so repeated work on that path directly affects perceived latency
> - Those reads repeated issue and authorization lookups, returned full
private JSON even when unchanged, and performed non-critical bookkeeping
writes on the request path
> - Interaction reads also performed lifecycle writes even though `GET`
must be read-only
> - This pull request adds request-scoped reuse, private conditional
responses, read-only interaction access, and bounded write debouncing
without crossing actor, request, or company boundaries
> - The result is less database, serialization, logging, and
response-body work while preserving authorization and interaction
lifecycle invariants

## Linked Issues or Issue Description

This is the server-only latency phase. Related work is tracked
separately in #10415 (aggregate view), #10416 (warm navigation, merged
into the base), and #10463 (bundle split). This pull request
intentionally excludes those scopes.

**What happened?**

Opening an issue detail view caused avoidable server costs: repeated
issue and authorization reads within one request, full private JSON
responses when a representation was unchanged, writes during
interaction-list reads, production debug transport setup, and immediate
bookkeeping writes for cloud tenant activity and board-key usage.

**Expected behavior**

All successful JSON `GET /api/issues/:id/*` responses should support
strong private ETags and `304 Not Modified`. Repeated work may be reused
only within the current request. `GET /interactions` must not modify
stored interactions. Non-critical activity timestamps may be debounced
without weakening authentication or stale instance-admin cleanup.

**Steps to reproduce**

1. Start Paperclip in local development or self-hosted server mode.
2. Open one issue and request its detail subresources with the same
authenticated actor.
3. Repeat a successful JSON request with its `ETag` in `If-None-Match`.
4. Observe `304 Not Modified`, no interaction writes from `GET
/interactions`, and unchanged authorization boundaries.

**Deployment mode / installation**

- Local development or self-hosted server
- Built from source
- Core server behavior; not adapter-specific

## What Changed

- Added strong ETags and `Cache-Control: private, must-revalidate` to
successful JSON reads under `/api/issues/:id/*`, including
standards-compliant `If-None-Match` handling.
- Added request-scoped promise memoization for issue and authorization
lookups; no authorization result survives the request.
- Made `GET /interactions` read-only, moved supersession and
terminal-state handling to mutation paths, and prevented plugin callers
from accepting or rejecting interactions after an issue closes.
- Removed the production debug-file logger transport while preserving
development formatting.
- Debounced cloud-tenant activity and board-key `lastUsedAt`
persistence, while keeping stale instance-admin deletion unconditional
and authentication checks per request.
- Added focused tests for ETags, request isolation, authorization
lifecycle behavior, interaction invariants, logger configuration, and
retry-safe debounce behavior.

## Verification

- `pnpm exec vitest run server/src/__tests__/private-json-etag.test.ts
server/src/__tests__/issue-thread-interaction-routes.test.ts` — 2 files,
23 tests passed.
- Focused Vitest run covering request memoization, authorization,
interactions, plugin orchestration, logger, cloud tenant, board auth,
and issue services — 9 files, 264 tests passed.
- `pnpm --filter @paperclipai/server typecheck` — passed.
- `git diff --check origin/master...HEAD` — passed.
- Scope guardrails: 21 changed files under `server/src`; no lockfile,
workflow, migration, UI, aggregate-view, or bundle-split changes.

## Risks

- Strong ETags hash each successful serialized JSON response. This adds
a small CPU cost but avoids transferring unchanged bodies.
- Debounced bookkeeping timestamps can lag by the bounded debounce
interval. They are non-critical usage metadata; authentication still
runs per request, and stale instance-admin deletion remains
unconditional.
- Legacy pending interactions on terminal issues are projected as
expired by reads and are finalized only by mutation paths. The stored
record remains unchanged on `GET` by design.
- No database schema or migration changes are included.

> This is a focused performance correction and does not duplicate a
planned core feature in `ROADMAP.md`.

## Model Used

OpenAI Codex using `gpt-5.3-codex` for the initial implementation and
`gpt-5.6-sol` for isolation, verification, and PR preparation, with
reasoning, repository tool use, code execution, and GitHub CLI access.
The runtimes did not expose authoritative context-window sizes.

## 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: Dev Agent <dev@paperclip.ing>
2026-08-11 14:39:23 -04:00
Dotta 3e1ea39ff3
fix(inbox): honor saved policy for explicit targets (#11221)
<!-- 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 control plane people use to manage
AI-agent companies and their work
> - Each user can let agents tidy that user's Mine inbox
> - The profile control saves either an open policy or an agent
allowlist
> - Explicit inbox archive requests checked only the separate
`inbox:manage` grant
> - This made the saved profile control ineffective for explicit user
targets
> - This pull request makes authorization honor the target user's saved
policy
> - The benefit is that the UI control and the API now enforce the same
user choice

## Linked Issues or Issue Description

**What happened?**

An agent received `403 inbox_cross_user_grant_required` when it archived
an issue with an explicit `userId`. The denial occurred even when that
user had enabled inbox management for the agent in Profile Settings. The
authorization service checked only `principal_permission_grants` for
explicit targets and ignored the saved user inbox policy.

**Expected behavior**

An explicit target is allowed when the target user saved an `open`
policy or an allowlist that contains the agent. An unsaved default-open
policy must remain limited to the responsible-user path. A scoped
`inbox:manage` grant must remain an administrative override.

**Steps to reproduce**

1. Save an inbox-agent allowlist for a user.
2. Include the acting agent in that allowlist.
3. Call `POST /api/issues/{issueId}/inbox-archive` with that user's
explicit `userId`.
4. Observe the incorrect `403 inbox_cross_user_grant_required` response
on the previous implementation.

**Paperclip version or commit**

Reproduced on `7ea2068ef8`.

**Deployment mode**

Self-hosted server.

**Installation method**

Built from source with pnpm.

**Agent adapter(s) involved**

Not adapter-specific. This is a core authorization bug.

**Database mode**

External Postgres in production. The regression tests use embedded
PostgreSQL.

**Access context**

Agent bearer authentication.

Related foundations: #9658 and #9724.

## What Changed

- Read the target user's saved inbox-agent policy before the
explicit-target decision.
- Allow saved `open` policies and matching allowlists for explicit
targets.
- Keep unsaved implicit-open policies responsible-user-only.
- Keep scoped `inbox:manage` grants as administrative overrides.
- Add service and route regressions for allow, deny, archive, unarchive,
and audit metadata.
- Update the implementation contract and agent-facing inbox API
guidance.

## Verification

- `pnpm exec vitest run
server/src/__tests__/authorization-service.test.ts
server/src/__tests__/inbox-archive-routes.test.ts` — 66 tests passed.
- `pnpm --filter @paperclipai/server typecheck` — passed.
- `git diff --check origin/master...HEAD` — passed.

## Risks

- Low risk. The change is limited to explicit inbox targets with a saved
policy.
- A missing policy row still denies explicit cross-user access.
- A non-matching allowlist and a disabled policy still deny access
unless a scoped administrative grant applies.

> 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 did not expose the exact
model build or context-window size. The agent used reasoning, repository
tools, code execution, and focused 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-08-11 13:23:03 -04:00
Dotta 7ea2068ef8
fix(files): only highlight accessible workspace file links (#11090)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Task comments can contain references to files in project and
execution workspaces.
> - Paperclip detected path-shaped inline code and showed it as an
actionable file chip.
> - The UI did not first confirm that the current board session could
open the file.
> - Missing, denied, ambiguous, remote, and unsupported files therefore
looked actionable and failed after a click.
> - This pull request adds an issue-scoped availability check and
promotes only confirmed files to chips.
> - The benefit is that the task thread shows a file action only when
that action can succeed.

## Linked Issues or Issue Description

**What happened?**

Task comments promoted path-shaped inline code to file chips before
Paperclip checked the file. A chip could point to a missing, denied,
ambiguous, remote, or non-previewable file. The action then failed after
the user selected it.

**Expected behavior**

Paperclip must show a file chip only after the server confirms that the
current board session can open the exact file reference. All other
path-shaped text must stay ordinary inline code.

**Steps to reproduce**

1. Add a task comment that contains inline code with a missing or
inaccessible workspace path.
2. Open the task thread as a board user.
3. Observe that the path looks like an actionable file chip.
4. Select the chip and observe that the file cannot open.

**Paperclip version or commit**

`19be4cf927` and earlier.

**Deployment mode**

Local dev and self-hosted server.

**Access context**

Board user.

## What Changed

- Added shared request, response, and validation contracts for batched
workspace-file availability checks.
- Added an issue-scoped server endpoint that resolves file references
with company, issue, workspace, and preview-access checks.
- Added bounded batch concurrency and tests for missing, denied,
ambiguous, remote, unsupported, and available files.
- Added an issue-scoped UI availability registry that deduplicates,
batches, caches, and invalidates file checks.
- Changed task-comment markdown rendering so only confirmed files get
chip styling and file-viewer behavior.
- Bound each chip to the exact workspace target that passed the
availability check.

## Verification

- `pnpm exec vitest run
packages/shared/src/workspace-file-resource.test.ts
server/src/__tests__/file-resources.test.ts
ui/src/components/MarkdownBody.test.tsx
ui/src/components/WorkspaceFileMarkdownBody.availability.test.tsx
ui/src/lib/remark-workspace-file-refs.test.ts
ui/src/lib/workspace-file-availability.test.ts` — 93 passed, 35 skipped.
- `pnpm check:token-gates` — clean.
- `pnpm -r typecheck` — passed.
- `pnpm build` — passed.
- `pnpm test:run` — all server and UI groups passed. One unchanged CLI
test saw the run-injected static AWS credentials and expected only its
local `AWS_PROFILE`. The same test passed, 8 of 8, after removing only
`AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` from its process
environment.

## Risks

- File chips now appear after an asynchronous availability check, so
path-shaped text can briefly render as inline code.
- Availability results use the existing 30-second file-resource cache
window. File-resource invalidation forces a new check.
- The endpoint limits each request to 100 references and the client
chunks larger sets.

> 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 service did not expose a more specific
model ID or context-window size. The agent used high-reasoning mode,
repository tools, command 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
- [ ] 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-08-11 12:11:45 -04:00
scotttong 815e49bb7c
feat: make chat-style tasks the default experience (#11101) 2026-08-11 09:06:21 -07:00
Dotta b58ce27a02
fix: isolate execution workspace summaries (#10790)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Paperclip gives operators a summary for each workspace.
> - An execution workspace detail page used the parent project-workspace
summary slot.
> - Two execution workspaces under one project workspace could therefore
show the same summary.
> - This pull request gives each execution workspace its own summary
scope.
> - It also limits the summary snapshot and generated issue to that
execution workspace.
> - The benefit is that a new or parallel execution workspace cannot
inherit unrelated status.

## Linked Issues or Issue Description

**What happened?**

An execution workspace detail page read and refreshed the summary slot
for its parent project workspace. Parallel execution workspaces could
show the same status and include issues from each other.

**Expected behavior**

Each execution workspace must have one isolated summary slot. Its
generated snapshot must include only issues assigned to that execution
workspace.

**Steps to reproduce**

1. Create two execution workspaces under one project workspace.
2. Add different issues to each execution workspace.
3. Generate the summary in the first execution workspace.
4. Open the second execution workspace.
5. Observe that the old implementation could reuse the first summary.

**Paperclip version or commit**

The problem exists on `master` before this pull request.

**Deployment mode**

The issue affects both local trusted and authenticated deployments.

## What Changed

- Added `execution_workspace` to the shared summary-slot scope contract.
- Validated execution-workspace ownership and stored generated summary
issues on the correct execution workspace.
- Limited execution-workspace snapshots to issues with the matching
execution workspace ID.
- Updated the execution workspace page to use its own summary slot.
- Updated Summarizer instructions, routine options, catalog metadata,
documentation, and regression tests.

## Verification

- `NODE_ENV=test pnpm exec vitest run
packages/shared/src/summary-slot.test.ts
server/src/__tests__/summary-slots.test.ts
ui/src/pages/ExecutionWorkspaceDetail.test.tsx` — 30 focused tests
passed; the embedded-Postgres server tests were run outside the
process-restricted sandbox.
- `pnpm check:token-gates` — passed.
- `pnpm --filter @paperclipai/skills-catalog validate` — passed with 17
catalog skills.
- [Latest-head GitHub
Actions](https://github.com/paperclipai/paperclip/actions/runs/31491475405)
— all 22 jobs passed on `beea14cbaf`, including typecheck, build,
server/workspace tests, serialized suites, e2e, canary, and aggregate
verification. One unrelated adapter cleanup test initially hit an
`ENOTEMPTY` temp-directory race; its single permitted rerun passed.
- Greptile — 5/5 confidence on `beea14cbaf`, 12 files reviewed, zero
comments added, and zero unresolved threads.

## Risks

- Low risk. The new scope is additive.
- Existing project and project-workspace summary slots keep their
current keys and behavior.
- A summary generated for an execution workspace now excludes sibling
workspace issues by design.

> 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 deployment does not expose a more
specific model ID or context-window value. It used agentic reasoning,
repository tools, code execution, and GitHub tooling.

## 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-08-11 08:56:32 -04:00
Devin Foley 35aaaa0bd0
feat(server): preserve task timestamps and hierarchy through company import/export (#11193)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Company export/import moves a whole company — agents, tasks,
comments — between instances as a portable bundle
> - The bundle never carried task timestamps or parent links: the export
writes neither, the importer lets database defaults stamp "now", and
sub-tasks arrive flattened
> - Boards sort by recency, so every imported task showing "created just
now" collapses the task list into import order, and the task hierarchy
the user built is gone
> - This pull request adds created/updated/started/completed/cancelled
timestamps and a parent link to the bundle (schema v7), preserves them
end to end on import, and keeps comment imports from clobbering a
preserved updated time
> - The benefit is that an imported company reads like the company the
user left: same recency order, same task tree

## Linked Issues or Issue Description

**What happened?**

After a company import, every task showed as created at import time.
Recency sorting collapsed to import order, and parent/child task nesting
disappeared. The user called out losing "the meaningful task hierarchy
and recency sorting". Cause: the export bundle has no fields for task
timestamps or parent links, the importer lets `defaultNow()` win on
insert, and the comment importer bumps every touched task's `updatedAt`
to now.

**Expected behavior**

An imported company preserves each task's
creation/update/start/completion times and its position in the task
tree, so sorting and nesting on the destination match the source.

**Steps to reproduce**

1. On a source instance, create tasks over several days, including
sub-tasks nested under parents.
2. Export the company and import it into another instance.
3. Every task shows the import moment as its creation/update time and
all tasks are top-level.

## What Changed

- Export writes
`createdAt`/`updatedAt`/`startedAt`/`completedAt`/`cancelledAt` (ISO,
only when set) and `parent: <taskSlug>` into each task's bundle
extension; a parent outside the export selection drops the edge with an
aggregate warning, mirroring the existing blocker-edge warning
(`server/src/services/company-portability.ts`).
- Bundle schema version 6 → 7. All new fields are optional: v5/v6
bundles import unchanged with a version-aware downlevel warning; bundles
newer than the board still fail closed.
- Manifest parsing validates the new timestamps like comment timestamps
(invalid → warn and ignore, never a hard failure); shared types and the
zod validator carry the new optional fields.
- Import resolves parent slugs to pre-generated destination ids, drops
self-references and cycles from tampered bundles with warnings, and
orders rows parents-first because the self-referencing FK is checked per
insert chunk.
- `importIssues` writes the preserved timestamps (falling back to insert
time when absent; `startedAt` stays null unless bundle-carried, per
#11191's semantics) and `parentId`.
- `addImportedComments` no longer blanket-bumps `updatedAt = now()`; it
takes `GREATEST(updated_at, newest imported comment createdAt)`, so a
preserved update time never regresses while unpreserved rows keep the
old behavior.

## Verification

- `pnpm vitest run server/src/__tests__/company-portability.test.ts
server/src/__tests__/company-portability-import-batching.test.ts
server/src/__tests__/productivity-review-service.test.ts` — 102 passed,
1 pre-existing opt-in benchmark skip. Includes: full round-trip with
exact timestamp equality and a 3-deep parent chain against embedded
Postgres; v6 back-compat (defaults + warning); forward-compat rejection
(v8); cycle/self-reference/invalid-timestamp tampered-bundle handling;
comment-bump preserve-awareness in both directions.
- `pnpm --filter @paperclipai/server typecheck` and `pnpm --filter
@paperclipai/shared typecheck` — clean.

## Risks

- **Rollout ordering**: a board on the previous build (max schema v6)
refuses bundles exported by this build (stamped v7) — the existing
newer-than-supported rejection, working as designed. Cross-instance
moves need the importing board upgraded first. Called out here so
operators aren't surprised during the transition window.
- Parent edges from tampered bundles are dropped with warnings rather
than failing the import; blocker relations already behave this way.
- Timestamps are data-only; no destination schema migration.

Stacked on #11191 (its commit is included here) — merge #11191 first;
this PR then shows only the v7 changes.

## Model Used

- Claude Fable 5 (`claude-fable-5`) via Claude Code CLI, with extended
thinking and tool use (multi-agent implementation with independent
verification).

## 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-08-10 17:01:33 -07:00
Devin Foley d816eb8095
fix(server): keep imported tasks quiescent under the productivity review sweep (#11191)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Company import brings a full company package — agents, tasks,
routines — into an instance, with `pauseAutomations` promising a quiet
landing
> - The pause covers the imported entities, but the destination's own
productivity-review sweep does not know the difference between imported
rows and live work
> - The importer stamps every imported in-progress task with `startedAt
= now()`, so six hours later the sweep's long-active check fires on
every one of them and floods the board with review tasks and agent
wakeups
> - This pull request stops fabricating `startedAt` on import and makes
the sweep skip tasks whose assignee agent is paused
> - The benefit is that an import lands quietly: no surprise review-task
storm, and paused teams stay paused until the operator activates them

## Linked Issues or Issue Description

**What happened?**

After importing a company package with automations paused, a batch of
"productivity review" tasks appeared roughly six hours later — one for
every imported in-progress task — each with an owner-agent wakeup. The
user described it as jarring and wasteful. Cause: `importIssues`
fabricates `startedAt = now()` for imported in-progress rows, and
`reconcileProductivityReviews` considers any assigned in-progress task
without checking whether the assignee agent is paused, so its
long-active-duration evidence (6 h threshold) trips on the fabricated
timestamp.

**Expected behavior**

An import with paused automations must be quiescent: no destination
sweep should generate work from imported rows until the operator
unpauses the imported team. A paused agent must not accumulate review
tasks it cannot act on.

**Steps to reproduce**

1. Import a company package containing tasks with status `in_progress`
assigned to agents, with "pause automations" enabled.
2. Wait for the productivity-review reconcile (runs at startup and on
the heartbeat scheduler tick) more than six hours after the import.
3. Observe one new review task plus an owner wakeup per imported
in-progress task.

## What Changed

- `importIssues` no longer fabricates `startedAt` for imported
`in_progress` rows; it inserts null (`server/src/services/issues.ts`).
Audited every consumer of `issues.startedAt` — all are null-tolerant,
and normal checkout/status-transition paths set the value when work
really starts.
- `reconcileProductivityReviews` skips candidates whose assignee agent
is `paused`, counting them as skipped
(`server/src/services/productivity-review.ts`). This is a general rule,
not import-specific: a paused agent cannot act on a review.
- Tests: paused-assignee candidate with an old `startedAt` creates no
review, and creates one after unpausing; imported in-progress issue
lands with null `startedAt` (embedded-Postgres import test); the
pre-existing long-active regression test still passes.

## Verification

- `pnpm vitest run
server/src/__tests__/productivity-review-service.test.ts
server/src/__tests__/company-portability-import-batching.test.ts` — 20
passed, 1 pre-existing opt-in benchmark skip.
- `pnpm vitest run server/src/__tests__/company-portability.test.ts` —
78 passed.
- `pnpm --filter @paperclipai/server typecheck` — clean.

## Risks

- Behavior change beyond imports: tasks assigned to paused agents no
longer receive productivity reviews anywhere. This is intended — the
review would target an agent that cannot respond — and reviews resume on
the first reconcile after unpausing.
- Imported in-progress tasks now carry no `startedAt` until real work
starts on the destination. The one sweep that read the fabricated value
is the one this PR quiets; all other consumers fall back safely (audit
in the commit body).
- Low risk otherwise: no schema change, no API shape change.

## Model Used

- Claude Fable 5 (`claude-fable-5`) via Claude Code CLI, with extended
thinking and tool use (multi-agent implementation with independent
verification).

## 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-08-10 17:00:32 -07:00
Devin Foley 5ca752dc81
fix(server): raise company import zip upload limit to 1 GB and make it operator-configurable (#11184)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Company import/export lets an operator move a full company package
between instances, with the Import page uploading the package as one
compressed `.zip`
> - The server caps that upload at 128 MB, and real company packages
with attachments now exceed it — imports fail at the preview step
> - The failure message tells the user to use the CLI folder import, but
that path posts inline JSON capped at 64 MB, so the advice is a dead end
for exactly these packages
> - This pull request raises the zip upload cap to a 1 GB default, makes
it operator-configurable through an environment variable, scales the
decompression-bomb guards from the cap in effect, and replaces the
misleading hint
> - The benefit is that large real-world company packages import
successfully, and operators with unusual needs can tune the cap without
a code change

## Linked Issues or Issue Description

**What happened?**

A company import fails at the preview step with `Preview failed: Import
package exceeds 134217728 bytes`. The package is a valid Paperclip
export. Its compressed size is larger than the 128 MB server cap (one
reported package is 257 MB). The error panel suggests the CLI folder
import, but that path sends the package as one inline JSON body capped
at 64 MB, so it also fails.

**Expected behavior**

A valid company package of realistic size imports successfully through
the Import page. If a package is too large, the error must state the
limit clearly and suggest a step that can work.

**Steps to reproduce**

1. Export a company with enough attachments to make the compressed
package larger than 128 MB.
2. Open the Import page and upload the `.zip`.
3. Click "Preview import".
4. The preview fails with `Import package exceeds 134217728 bytes`.

**Deployment mode**

Reported from a managed deployment; the limit applies to all deployment
modes.

## What Changed

- Raise `PORTABLE_ZIP_UPLOAD_LIMIT_BYTES` from 128 MB to a 1 GB default
(`server/src/http/body-limits.ts`).
- Add the `PAPERCLIP_IMPORT_ZIP_MAX_BYTES` environment override. Invalid
or non-positive values fall back to the default.
- Scale the zip decompression-bomb guard from the configured cap at the
import route: the aggregate inflated ceiling is 4x the cap. The
per-entry ceiling stays at 512 MB because V8's string length limit
applies to an entry regardless (`server/src/routes/companies.ts`,
`packages/shared/src/portability-zip.ts`).
- Report the 422 limit error in MB instead of raw bytes.
- Replace the "use the CLI folder import for very large packages" hint
on preview failure with advice that works: re-export the package without
large attachments (`ui/src/pages/CompanyImport.tsx`).
- Update the stale comment in `ui/src/lib/import-preflight.ts` that made
the same CLI claim.
- Add tests for the new default, the env override, and the
invalid-override fallback.

## Verification

- `pnpm vitest run server/src/__tests__/body-limits.test.ts
packages/shared/src/portability-zip.test.ts
server/src/__tests__/company-portability-routes.test.ts
server/src/__tests__/company-portability.test.ts
server/src/__tests__/company-portability-import-batching.test.ts` — all
pass.
- `pnpm vitest run ui/src/pages/CompanyImport.test.tsx` — passes,
including the updated failure-panel copy assertion.
- `pnpm typecheck` — clean across the workspace.
- Manual: upload a `.zip` larger than the configured cap; the preview
fails with `Import package exceeds the 1024 MB upload limit` and the new
hint. A package between 128 MB and 1 GB now previews and imports.

## Risks

- Peak per-import memory rises with the cap: the upload is buffered in
memory and unzipped in one pass. A 1 GB compressed package can use
several GB transiently. Imports are instance-admin actions, so the
exposure is a deliberate operator action, not anonymous traffic.
Operators on small hosts can lower the cap with
`PAPERCLIP_IMPORT_ZIP_MAX_BYTES`.
- The aggregate bomb guard moves from a fixed 512 MB to 4x the
configured cap. It still bounds expansion far below what a decompression
bomb needs.
- No migration and no API shape change. The 422 message text changes; no
code matches on the old text.

## Model Used

- Claude Fable 5 (`claude-fable-5`) via Claude Code CLI, with extended
thinking and tool use (file edits, local test runs, live-instance
inspection).

## 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-08-10 12:47:03 -07:00
LeonSGP 6a4e2e1b8c
fix(routes): return 409 for routine checkout conflicts (#3790)
## Thinking Path

> - Paperclip orchestrates AI agents and relies on issue checkout as the
core task-claiming primitive
> - The issue checkout route is the HTTP boundary that translates
service and database outcomes into agent-usable API responses
> - Routine-linked issues are protected by the partial unique index
`issues_open_routine_execution_uq`, which covers only rows whose
`execution_run_id` is set
> - `svc.checkout` sets `execution_run_id`, so a concurrent claim moves
the row into that index and can raise a 23505 mid-request
> - Unhandled, that surfaces as a 500 and crashes the agent run instead
of being a recoverable conflict
> - Drizzle wraps driver failures in its own `Failed query: ...` error,
so the Postgres error carrying `code` and the constraint name is
reachable only through `cause`
> - This pull request translates that violation into a 409 at the
checkout route, detecting it through the cause chain the way
`isReviewPathRecoveryIdempotencyConflict` already does
> - The benefit is that agents handle routine execution contention
through the normal heartbeat conflict path instead of failing on an
internal server error

## Linked Issues or Issue Description

Fixes #3660

Related pull requests found while searching for duplicates:

- #3699 — an earlier attempt at this same route-level fix, closed
unmerged. Same shape, and its check has the flat-error bug described
under Verification.
- #3633 — related work on postgres.js `constraint_name` handling in
conflict detection.
- #5662 — covers the adoption path (`assertCheckoutOwner`) that this
pull request does not.

## What Changed

- Added `server/src/db-errors.ts` with `isUniqueViolation(error,
constraintName?)`, which walks the `cause` chain (depth-capped) and
accepts the postgres.js `constraint_name`, the node-postgres
`constraint`, or the driver message as evidence of SQLSTATE 23505.
- Wrapped `svc.checkout()` in `POST /issues/:id/checkout` with a narrow
try/catch that uses that helper to return **409 Conflict** for
`issues_open_routine_execution_uq`, and rethrows every other error
unchanged.
- Added `server/src/__tests__/db-errors.test.ts` covering the wrapped
and bare error shapes, both constraint field names, the message
fallback, non-matching constraints, non-unique-violation codes, and a
self-referential cause chain.

## Verification

- The new unit test includes the wrapped case `{ cause: { code: "23505",
constraint_name: ... } }` that a flat `error.code` check fails, so it is
a real regression guard rather than a restatement of the implementation.
- The wrapped shape is what this codebase observes in practice:
`server/src/__tests__/plugin-tenant-isolation.test.ts` asserts
`cause?.code === "23505"` against embedded Postgres,
`packages/db/src/pipelines-schema.test.ts` asserts that constraint
failures throw `Failed query`, and
`server/src/services/recovery/review-path-recovery.ts` walks the same
chain.
- CI (verify, e2e, policy) exercises this change against current master
through the pull request merge ref.
- Not verified locally: no monorepo install or typecheck was run in this
environment.

## Risks

- Low. One route gains a catch that matches a single constraint and
rethrows all other errors, so no unrelated failure can be swallowed.
- The 409 body `{ error: ... }` matches the other 409 responses this
route already returns.
- Scope limit: this covers the checkout route only. The adoption path
reached through `assertCheckoutOwner` (heartbeat, plugins, and pipelines
routes) can still surface the same violation as a 500; #5662 targets
that path.
- `isUniqueViolation` is new and intentionally generic. Existing flat
23505 checks elsewhere in the server are left untouched by this pull
request.

## Model Used

- Original change: OpenAI Codex, GPT-5-class tool-using coding agent in
the Codex CLI environment; exact backend model revision is not exposed
in that runtime.
- Follow-up revision (cause-chain detection plus tests): Anthropic
Claude Opus 5 (`claude-opus-5`), tool-using coding agent with extended
thinking 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)
- [ ] 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
- [ ] 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
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Andrew Aymeloglu <aaymeloglu@gmail.com>
2026-08-09 20:20:09 -05:00
Manav Shrivastava ebf2b8ff79
fix(server): persist worktree runtime port when ambient PORT does not match (#1849) (#1930)
## What was done
Replaced the strict `!nonEmpty(process.env.PORT)` guard in
`maybePersistWorktreeRuntimePorts` with a new `isPortPinnedByRuntimeEnv`
helper function. This function checks if `process.env.PORT` is set, but
only suppresses persisting the port to configuration if the ambient
`PORT` matches the newly allocated `selectedPort`.

## Why it matters
Fixes issue #1849. Previously, if an ambient `PORT` environment variable
was exported globally (like inheriting from the shell running the parent
workspace), worktrees would silently fail to write their
collision-avoiding ports (e.g. 3103 instead of 3100) back to their
respective local `config.json` files. This resulted in orphaned
sub-worktrees and lost port tracking on reboot. With this fix, worktrees
correctly persist their assigned ports even while nested under an
inherited environment variables stack, while continuing to respect
manual, explicit pinning.

## How to verify
1. Export a port in the shell explicitly: `export PORT=3100`.
2. Launch a sub-worktree instance which receives an auto-assigned free
port (e.g., `3103`).
3. View the underlying `config.json` for that worktree inside
`.paperclip/worktrees/`.
4. The config file should correctly contain `{"server": {"port": 3103}}`
rather than dropping the write operation.

## Risks
None expected. The `Number()` and `Number.isInteger()` checks handle
parsing edge cases cleanly, defaulting robustly to preventing writes if
`process.env.PORT` is somehow malformed (e.g., set to a non-integer),
ensuring absolute safety during misconfigurations.

Co-authored-by: manavshrivastavagit <manavshrivastava@users.noreply.github.com>
2026-08-09 17:29:44 -05:00
scotttong cc35c3c395
feat: structure and humanize recovery notices (#11075)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Paperclip posts system comments when automatic run recovery cannot
continue
> - These comments currently mix the main event with recovery
identifiers and routing details
> - The task chat shell also renders these comments as large raw text
blocks
> - Operators need a short explanation first and inspectable evidence on
demand
> - This pull request emits structured recovery notices and renders them
as compact humanized rows
> - The benefit is a quieter task thread that keeps the full recovery
evidence available

## Linked Issues or Issue Description

Related prior extraction source: #11070. This pull request replaces only
its structured recovery notice slice with a focused branch based on
current master.

**What existing behavior does this improve?**

Paperclip recovery escalations and the experimental task chat
system-comment renderer.

**Current behavior**

Recovery escalation comments put action identifiers, owner details, run
details, and failure codes into the visible markdown body. The task chat
shell renders the complete system comment as a large text block.

**Proposed behavior**

The server emits a short system notice with typed metadata sections. The
task chat shell classifies known recovery families and renders one
compact row. An operator can expand the row to inspect the full body and
metadata.

**Reason and benefit**

The main thread stays readable during repeated recovery activity. Typed
links and evidence remain available without exposing raw failure text in
the default view.

**Breaking changes**

The visible recovery comment body is shorter. Recovery action
deduplication now reads the structured metadata and still recognizes
legacy body markers. No API schema or database migration changes.

## What Changed

- Emit stranded recovery escalations with `system_notice` presentation
and typed recovery, owner, run, and failure-code metadata.
- Share bounded metadata row builders across recovery notice producers
and preserve legacy deduplication compatibility.
- Humanize known recovery notice families and render compact expandable
task-chat rows.
- Route system-authored comments ahead of derived agent authorship so
recovery notices do not appear as agent bubbles.
- Add focused server and UI regression coverage.

## Verification

- `pnpm check:token-gates` — 3/3 clean.
- `pnpm --filter @paperclipai/server typecheck` — passed.
- `pnpm --filter @paperclipai/ui typecheck` — passed.
- `pnpm --filter @paperclipai/shared typecheck` — passed.
- `pnpm --filter @paperclipai/shared exec vitest run
src/validators/issue.test.ts` — 32 tests passed.
- `pnpm --filter @paperclipai/server exec vitest run
src/services/recovery/stranded-notice.test.ts
src/__tests__/issue-recovery-actions.test.ts` — 57 tests passed.
- `pnpm --filter @paperclipai/server exec vitest run
src/services/recovery/successful-run-handoff.test.ts
src/services/recovery/stranded-notice.test.ts` — 39 tests passed.
- `pnpm --filter @paperclipai/server exec vitest run
src/__tests__/heartbeat-process-recovery.test.ts -t 'escalates an
exhausted failed successful-run handoff without using generic
continuation recovery first|escalates an exhausted successful handoff
run that still leaves no disposition'` — 2 tests passed.
- `pnpm --filter @paperclipai/server exec vitest run
src/__tests__/heartbeat-process-recovery.test.ts -t 'blocks assigned
todo work after the one automatic dispatch recovery was already used'` —
passed.
- `pnpm --filter @paperclipai/ui exec vitest run
src/lib/system-notice-humanizer.test.ts
src/components/task-chat/TaskChatSystemNotice.test.tsx
src/components/task-chat/task-chat-adapter.test.ts` — 15 tests passed.
- Storybook visual baselines were not updated because this chat-shell
path has no affected snapshot baseline. Focused rendering tests and
token gates cover this change.

## Risks

- Consumers that parse recovery action identifiers from comment markdown
must move to structured metadata. Server deduplication remains backward
compatible with legacy comments.
- The humanizer uses stable recovery-family phrases. Unknown notices use
a generic truncated first-sentence fallback.
- The UI changes only the experimental task chat presentation. The
stored comment body and expanded metadata remain 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 with GPT-5, reasoning mode, repository tools, shell
execution, and GitHub integration. The runtime did not expose a
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-08-07 18:41:52 -07:00
scotttong 34fe57a024
fix(server): ignore sibling worktrees in dev watch (#11074)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Developers can run Paperclip from linked Git worktrees.
> - The server development watcher scans paths near the active checkout.
> - A main checkout can contain many complete sibling worktrees under
`.paperclip/worktrees`.
> - Scanning those sibling checkouts can stall the watcher before it
starts the server.
> - This pull request excludes the shared worktree directory from the
development watcher.
> - The benefit is that development startup stays responsive as the
number of worktrees grows.

## Linked Issues or Issue Description

**What happened?**

The server development watcher traversed sibling checkouts under
`.paperclip/worktrees`. Large worktree collections could make `pnpm dev`
stall before the watcher started the server process.

**Expected behavior**

The watcher must observe only source paths that can reload the active
checkout. It must ignore sibling worktrees in both a main checkout and a
linked worktree.

**Steps to reproduce**

1. Create several linked worktrees under `.paperclip/worktrees`.
2. Add normal dependency and build output trees to those worktrees.
3. Run `pnpm dev` from the main checkout or one linked worktree.
4. Observe the watcher scan sibling worktrees before it starts the
server.

**Paperclip version or commit**

Reproduced on `master` before this change.

**Deployment mode**

Local development with `pnpm dev`.

## What Changed

- Detect whether the active server root is inside the managed
linked-worktree directory.
- Ignore the shared `.paperclip/worktrees` root from both main and
linked checkouts.
- Add regression coverage for the resolved ignore path and its globstar
form.

## Verification

- `./node_modules/.bin/vitest run
server/src/__tests__/dev-watch-ignore.test.ts --reporter=verbose`
- `pnpm --filter @paperclipai/server typecheck`

## Risks

- Low risk. The change affects only local development watch exclusions.
- A non-standard checkout that copies the same `.paperclip/worktrees`
directory layout will receive the same exclusion.

> 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, context window not disclosed, with reasoning,
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 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-08-07 18:30:16 -07:00
Nicky Leach 6b7e0814a0
feat(acp): stream Daytona sandbox agent output and remove the host output poll (#11049)
## Thinking Path

> - Paperclip is the open source app that manages AI agents for work
> - Sandbox providers let agents run in remote and isolated environments
> - Daytona session commands need a path that sends agent output to the
host without host polling
> - Host polling adds delay and repeats provider output work
> - This pull request adds typed execute.log notifications and a log
sink for incremental output
> - This pull request adds an optional ACP session stream with
final-result replay protection
> - The benefit is lower output delay while the default flags keep
current behavior unchanged

## Linked Issues or Issue Description

**Subsystem affected**

Cross-cutting. The change spans the plugin SDK, Daytona provider,
adapter utilities, and server execution services.

**Problem or motivation**

The Daytona ACP bridge polls a host output file while an agent command
runs. This adds delay and can repeat work. The host also needs a safe
route for provider output chunks.

**Proposed solution**

Add a typed `execute.log` notification with host-issued invocation
correlation. Add an ordered log sink to the environment execute path.
Add an optional ACP session-log path that parses newline-delimited JSON
frames and removes the host output poll for that path.

**Alternatives considered**

Keep the output-file poll as the only path. This keeps the current
behavior but does not provide timely output. The new path stays behind
flags, so the existing path remains the default fallback.

**Roadmap alignment**

This change supports the shipped Cloud / Sandbox agents milestone in
`ROADMAP.md`, including Daytona support.

## What Changed

- Add the typed `execute.log` worker-to-host notification and
company-scoped host route.
- Add ordered `stdout` and `stderr` chunk delivery before the final
execute result.
- Add the Daytona session log sink and the optional ACP streamed session
path.
- Add monotonic frame handling so live and final output reach the host
once.
- Keep `useLogStream` and `streamAgentSessionOutput` off by default.
- Add unit and integration coverage for the notification, execution
target, runtime, and Daytona paths.

## Verification

- Run adapter-utils tests: 445 tests pass locally.
- Run server environment tests: 73 tests pass locally.
- Run Daytona plugin tests: 131 tests pass locally.
- Run TypeScript checks for shared, adapter-utils, and server.
- Review the pull request checks after GitHub completes them.
- All required GitHub checks pass on the current head.

## Risks

The new paths change output delivery only when a feature flag enables
them. The final execute result remains available for parsing and
fallback. The main risk is a provider stream or frame-order error; the
final-result parser limits that risk.

## Model Used

OpenAI Codex, GPT-5, tool use and code execution. The runtime did not
supply a context-window value.

## 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 operator documentation change applies because both new flags remain
disabled by default.
- [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-08-07 14:29:58 -07:00
Dotta 0a511ed1b0
feat(apps): support multiple provider connections (#11060)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The Apps subsystem connects company tools through governed provider
connections.
> - A company can need more than one account for the same provider.
> - The current database constraint and Apps flow assume one named
connection per company.
> - New quarantined actions also need an explicit review decision before
activation.
> - This pull request supports multiple provider connections and
complete action review decisions.
> - The benefit is safer access control and a clear multi-account Apps
workflow.

## Linked Issues or Issue Description

Refs: #11040

**Subsystem affected**

Cross-cutting. This change affects the Apps UI, the tool access API, the
shared request contract, and the database schema.

**Problem or motivation**

The connection name constraint prevents a company from keeping more than
one connection for a provider. The Apps UI also reuses an existing OAuth
connection when a user asks to connect another account. Action review
can enable selected entries without recording a decision for every
quarantined action.

**Proposed solution**

Remove the company and connection name uniqueness constraint. Let users
open, count, edit, and create multiple provider connections. Require the
finish request to cover every quarantined action exactly once before the
server activates reviewed entries.

**Alternatives considered**

The UI could generate unique internal names and keep the database
constraint. This would preserve a one-connection assumption in the data
model and would make display names part of identity. The server could
also infer review decisions from enabled actions. This would not
distinguish a reviewed disabled action from an action that the user did
not review.

**Roadmap alignment**

This change extends the completed MCP Tool Gateway and Apps milestone.
It also supports the Connected Apps roadmap item. It follows the
navigation and connection management work in #11040.

## What Changed

- Remove the company-scoped connection name uniqueness index with an
ordered and idempotent migration.
- Add a reviewed action list to the finish-app contract and reject
incomplete or duplicate review decisions.
- Activate reviewed entries and keep unreviewed quarantined entries
blocked.
- Enable a completed connection and preserve the company and connection
scope in all updates.
- Show provider connection counts and open the provider setup page from
Browse.
- Let users edit existing connections or connect another account without
reusing an active OAuth connection.
- Update focused server and UI coverage for multiple connections and
action review.

## Verification

- Ran the focused Apps UI suite. All 116 tests passed in 11 files.
- Ran the focused server and CLI suite. All 276 tests passed in 3 files.
- Ran `pnpm --filter @paperclipai/db check:migrations`. The migration
safety check passed.
- Ran `pnpm -r typecheck`. All projects passed.
- Ran `pnpm build`. All projects built successfully.
- Ran `pnpm test:run`. It passed 3,735 tests and skipped 4 tests. One
worktree-safety assertion failed because the execution workspace reloads
its worktree marker. The same test passed with an isolated non-worktree
marker.
- Ran `pnpm check:token-gates`. It reports 12 existing violations in the
unchanged `PaperclipOrbit3D.tsx` file from the target branch.
- Started the six affected Playwright specifications. Chromium could not
start because the host does not provide `libatk-1.0.so.0`. The GitHub
e2e jobs will verify these specifications.
- GitHub Actions passed every final-head CI gate, including all three
e2e shards and the aggregate `e2e` and `verify` jobs.
- Greptile reviewed final commit `9af9200426` at 5/5 with zero review
threads.

## Risks

- Removing the name uniqueness index permits duplicate display names.
Stable connection IDs and UIDs remain unique within a company.
- The finish-app endpoint accepts the new review field as optional for
backward compatibility. When clients send it, the server requires a
complete decision for all quarantined actions.
- Multiple OAuth connections depend on the explicit new-connection route
flag. Focused tests cover active and draft connection reuse.
- The migration is ordered after migration 0210. Its `DROP INDEX IF
EXISTS` statement is safe to repeat.

> 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 the `gpt-5.6-sol` model assisted this change. The
agent used repository tools, code execution, test execution, and agentic
reasoning. The Codex runtime manages the 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)
- [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-08-07 16:28:04 -05:00
Dotta b18b0fc39b
feat: refine app connections and legacy worktree startup (#11040)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The Apps UI manages app discovery and app connections.
> - The managed worktree runtime starts agent work in repository
worktrees.
> - The Apps routes do not match the main discovery flow, and the
connections view lacks a delete action.
> - Legacy managed worktrees can also start before their pending seed
operation runs.
> - This pull request makes app discovery the main Apps route and makes
connection management explicit.
> - It also seeds legacy managed worktrees before runtime startup and
makes the CLI read the repository-local config.
> - The benefit is a clearer Apps workflow and a safer managed-worktree
startup path.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

This improves the Apps navigation, app connection management, managed
git-worktree startup, and CLI worktree selection.

**Subsystem affected**

Cross-cutting. The change affects `ui/`, `server/`, `cli/`, and
development documentation.

**Current behavior**

The `/apps` route opens the connections list while discovery uses a
nested route. The connections list has no delete action. Some legacy
managed worktrees can start runtime work before their pending seed
operation runs. The CLI can also read an ambient Paperclip config
instead of the repository-local config.

**Proposed behavior**

The `/apps` route opens Browse, and `/apps/connections` opens the
connection list. Users can delete a connection after confirmation.
Runtime startup seeds legacy managed worktrees when required. The CLI
resolves the current worktree from the repository-local
`.paperclip/config.json` file.

**Reason and benefit**

Users can discover apps from the canonical Apps route and can manage
existing connections from a dedicated route. Legacy worktrees receive
their required repository content before agent runtime starts. CLI
worktree selection stays scoped to the current repository.

**Breaking changes**

The `/apps` and `/apps/browse` route behavior changes. Old Browse links
redirect to `/apps`. The change does not modify an API schema or
database schema.

## What Changed

- Make Browse the canonical `/apps` page and move the connection list to
`/apps/connections`.
- Align Apps navigation, redirects, attention links, empty states, and
connection actions with the new routes.
- Add connection deletion with confirmation and clear failure feedback.
- Seed legacy managed git worktrees before runtime startup when their
seed status is pending.
- Read the CLI worktree selection from the repository-local Paperclip
config.
- Update focused UI, server, CLI, and development documentation
coverage.

## Verification

- Ran 202 focused UI, server, and CLI tests. All tests passed.
- Ran `pnpm -r typecheck`. All projects passed.
- Ran `pnpm build`. All projects built successfully.
- Ran `pnpm test:run`. The server and UI stages passed 7,168 tests. The
CLI stage found one environment-sensitive secrets test because this
workspace injects static AWS credentials. The isolated CLI file passed
all 8 tests after those injected variables were unset.
- Ran `pnpm check:token-gates`. It reports 12 existing color-token
violations in the unchanged `PaperclipOrbit3D.tsx` file from the target
branch. This pull request does not modify that file.
- Ran focused regression coverage for repository-root CLI config
resolution and connection deletion state. All tests and affected package
typechecks passed.
- Collected all 27 tests in the six changed Playwright specifications
successfully.
- GitHub Actions passed every latest-head CI gate, including all three
e2e shards and the aggregate `e2e` and `verify` jobs.
- Greptile reviewed the final commit at 5/5 with zero unresolved
threads.

## Risks

- Existing bookmarks for `/apps/browse` redirect to `/apps`.
- Connection deletion changes visible connection state and requires user
confirmation.
- The legacy seed path runs only for managed git worktrees with pending
seed state. Tests cover the startup condition.
- The rebase preserves the target branch's direct OAuth policy for the
Notion connection flow.

> 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 the GPT-5 model family assisted this change. The agent
used reasoning, repository tools, code execution, and test execution.
The runtime does not expose the exact model snapshot or 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-08-07 14:26:40 -05:00
Michael Nguyen 42c73562c5
fix(heartbeat): backfill projectWorkspaceId when restoring a reused execution workspace (#10171)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Each run executes inside a persisted **execution workspace** (a row
in `execution_workspaces`) that is either freshly created or
**restored/reused** across runs of the same issue
> - Before adapter launch, a guard rejects a restored workspace whose
`projectWorkspaceId` is null while the issue resolves a concrete project
workspace (`persisted_workspace_missing_project_workspace_id`) — a
safety check against binding a run to a workspace with no
project-workspace link
> - The reuse/**restore** path updated the existing row (cwd, branch,
status, metadata…) but **never set `projectWorkspaceId`**, so a row
persisted with a null value stayed null on every restore
> - Result: for an issue that resolves a project workspace, the guard
fires, `reuse_existing` re-selects and re-binds the *same* stale null
row on the next attempt, and the run crash-loops forever with no
self-heal
> - This pull request backfills `projectWorkspaceId` during restore
(prefer the existing binding, fall back to the resolved one) so the row
heals on first reuse and the guard stops firing
> - The benefit is that reused workspaces created before their project
had a primary project workspace self-repair on next use instead of
crash-looping, while genuine mismatches are still surfaced by the guard

## Linked Issues or Issue Description

No public GitHub issue exists — describing the bug inline per the bug
report template (`.github/ISSUE_TEMPLATE/bug_report.yml`):

### What happened?
In `heartbeatService`, the execution-workspace reuse/restore branch
calls
`executionWorkspacesSvc.update(reusableExistingExecutionWorkspace.id, {
… })` without a `projectWorkspaceId` field. Only the sibling CREATE
branch sets `projectWorkspaceId`. So an execution workspace that was
persisted with a null `projectWorkspaceId` (e.g. created before its
project had a primary project workspace) is never backfilled on restore.
When such a workspace is later reused for a run whose issue resolves a
concrete project workspace, the pre-launch guard throws
`persisted_workspace_missing_project_workspace_id`, the run fails, and
`reuse_existing` re-binds the identical stale row on the next attempt —
an unbounded crash-loop with no self-heal.

### Expected behavior
On restore, the reused workspace's `projectWorkspaceId` is backfilled
from the resolved project workspace when it is currently null, so the
guard passes and the run launches. An existing non-null binding is never
overwritten (a genuine mismatch is still surfaced by the separate
`project_workspace_mismatch` guard).

### Steps to reproduce
1. Have an `execution_workspaces` row with `project_workspace_id = NULL`
that is eligible for reuse.
2. Give its project a primary project workspace (so the issue now
resolves a concrete `projectWorkspaceId`).
3. Dispatch a run for an issue in that project that reuses the
workspace. The restore `update()` leaves `project_workspace_id` null,
the launch guard throws
`persisted_workspace_missing_project_workspace_id`, and every subsequent
reuse re-binds the same null row and fails identically.

### Paperclip version or commit
`master` (branched from `14f20be92`); reproduced on a live self-hosted
instance.

### Deployment mode
Self-hosted, embedded Postgres, local adapters.

## What Changed

- New exported pure helper
`reconcileReusedExecutionWorkspaceProjectWorkspaceId(existing,
resolved)` in `server/src/services/heartbeat.ts`, returning `existing ??
resolved ?? null`. It prefers an existing binding (never nulls out a
good value or silently rebinds a genuine mismatch — the guard still
surfaces those), backfills a null binding from the resolved value, and
stays null when neither is present.
- Wire the helper into the reuse/restore
`executionWorkspacesSvc.update(...)` call so the restored row's
`projectWorkspaceId` is set to
`reconcileReusedExecutionWorkspaceProjectWorkspaceId(reusableExistingExecutionWorkspace.projectWorkspaceId,
resolvedProjectWorkspaceId)`. The CREATE branch already set
`projectWorkspaceId`; this brings the restore branch to parity.

## Verification

- Added 3-case unit coverage in
`server/src/__tests__/heartbeat-workspace-session.test.ts` for the
helper: (a) backfills a null existing binding from the resolved value,
(b) never overwrites an existing binding even when a resolved value is
present, (c) returns null when both existing and resolved are absent
(null and undefined inputs).
- Confirmed the `update()` patch type accepts the field:
`executionWorkspacesSvc.update` takes `Partial<typeof
executionWorkspaces.$inferInsert>`, and `projectWorkspaceId` is a column
on that table; both
`reusableExistingExecutionWorkspace.projectWorkspaceId` and
`resolvedProjectWorkspaceId` are `string | null`, matching the helper's
`string | null | undefined` params / `string | null` return.
- Live-instance exposure check (embedded Postgres): 354
`execution_workspaces` rows carry a null `project_workspace_id`; all of
them belong to projects with **no** project workspace, so
`expectedProjectWorkspaceId` currently resolves null and the guard does
not fire today. The fix is durable heal-on-reuse protection for the
moment any such project gains a primary project workspace (or a null row
is reused for an issue that resolves one).
- CI (full pnpm workspace install) runs the authoritative test +
typecheck for this change on this PR.

## Risks

- Low risk; scoped to the execution-workspace restore path, no schema or
API change.
- The helper only ever *adds* a `projectWorkspaceId` where the row had
none; it never overwrites an existing binding, so it cannot mask a real
`project_workspace_mismatch` (that guard still runs after).
- Complementary to (not overlapping with) #10130, which escalates a
terminal `workspace_validation_failed` run to `blocked` from the
recovery side; this PR prevents the guard from firing on reuse in the
first place. Neither depends on the other.

## Model Used

Claude Opus 4.8 (`claude-opus-4-8`), extended thinking, with tool use /
code execution (repo edit, embedded-Postgres exposure query, unit-logic
verification).

## 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 (searched open PRs touching heartbeat / execution-workspace /
reuse; only #10130 is related, and it is complementary)
- [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
- [ ] I have updated relevant documentation to reflect my changes (no
user-facing docs affected)
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green (in progress)
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
(pending)
- [x] I will address all Greptile and reviewer comments before
requesting merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-08-07 09:06:36 -07:00
Nicky Leach 01b51dc0e5
fix(runtime): stop Live badge and Working shimmer after task teardown (#10985)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The task list and the chat views show a Live badge and a Working
shimmer for an issue that has an active run.
> - A finished task kept the Live badge and the Working shimmer after
the run ended and the sandbox stopped.
> - The user interface reads run liveness from the
`heartbeat_runs.status` row. The run finalizer writes the terminal
status in a step that is separate from the agent `status=done` update.
When the sandbox or the run process stops between the two steps,
`heartbeat_runs.status` stays `running` forever.
> - A run row that stays `running` makes a finished task look
perpetually Live, and the user interface has no guard for an issue that
already reached a terminal status.
> - This pull request closes the invariant "environment lease released
implies the run is terminal" on the server, and adds a user interface
guard that suppresses live state for a terminal issue.
> - The benefit is that a finished task stops showing Live and Working,
both at the source (the run row) and at the surface (the badge and the
shimmer).

## Linked Issues or Issue Description

**Bug description**

- A completed task kept the Live badge and the Working shimmer after its
run ended and the sandbox was torn down.

**Steps to reproduce**

- Run an agent task to completion. Let the sandbox tear down while the
run finalizer is between the `status=done` update and the terminal
run-status write.
- Open the task list or the chat view for the finished task.

**Expected behavior**

- A finished task shows no Live badge and no Working shimmer.

**Actual behavior (before this change)**

- The finished task showed the Live badge and the Working shimmer
because its `heartbeat_runs.status` row stayed `running`.

This pull request supersedes the two separate pull requests #10954
(frontend) and #10955 (backend). It carries all of their changes for the
same race.

## What Changed

Server:

- Run teardown terminalizes a still-running or still-queued run before
it releases the environment lease. It writes `succeeded` when the issue
already reached `done`, `cancelled` when the issue is `cancelled`, and
`interrupted` otherwise. It never overwrites a status that another path
already made terminal.
- The recovery stale-lock sweep terminalizes an orphaned running run to
`interrupted` after it confirms the process and the sandbox are both
gone. It requires recorded process metadata, so it never terminalizes a
live run, a queued run, or a scheduled retry.
- Each terminal transition writes a run event.
- The stale-lock sweep continues and clears the lock when the audit
write fails. It logs the failure loudly.
- New server tests cover both invariants.

User interface:

- A shared guard suppresses the Live badge and the Working shimmer when
the issue status is terminal.
- The guard keeps non-terminal `queued` and `running` issues live.
- The guard prefers the newest issue live-status snapshot.
- New user interface tests cover the guard and the snapshot preference.

## Verification

Server:

- `pnpm --filter @paperclipai/plugin-sdk ensure-build-deps && tsc
--noEmit` in `server/` — 0 errors.
- `pnpm --filter server test
heartbeat-run-lease-release-terminalization.test.ts
recovery-stale-issue-lock-sweep.test.ts` — 12 tests pass.

User interface:

- `pnpm --filter @paperclipai/ui typecheck` — 0 errors.
- `pnpm exec vitest run ui/src/lib/liveIssueIds.test.ts
ui/src/lib/issue-chat-messages.test.ts` — 40 tests pass.

## Risks

- Low risk. The server change only forces a still-live run row to a
terminal status when the lease releases or when the recovery sweep
confirms the process is dead. It never overwrites an existing terminal
status, and it guards the recovery path with process metadata to avoid
terminalizing a live run.
- The user interface change is additive. The guard only suppresses live
state for a terminal issue and keeps queued and running issues live.
- No database migration. No change to any external endpoint.

## Model Used

- Claude Opus 4.8 (`claude-opus-4-8`), extended thinking, 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 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-08-07 07:23:56 -07:00
Dotta 9485ffea70
fix(config): preserve env files during managed updates (#10980)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The CLI and server both update Paperclip values in `.env` files
> - The server preserved operator content, but the CLI rebuilt the
complete file
> - A CLI rerun could remove comments, custom values, ordering, and
newline style
> - Both paths need one editor with one value encoding and duplicate key
policy
> - The final integration also needs one regression test across the
related setup and sync safety mechanisms
> - This pull request moves the editor to the shared package and adds
cross-cutting rerun-survival coverage
> - The benefit is safe setup and worktree repair reruns that preserve
operator edits

## Linked Issues or Issue Description

**What happened?**

The CLI rebuilt the complete `.env` file when it wrote a managed
Paperclip value. This action removed comments, blank lines, custom keys,
original quoting, and the original newline style.

**Expected behavior**

Paperclip must update only the managed assignments. It must preserve all
unrelated bytes. It must skip the file replacement when all managed
values are current.

**Steps to reproduce**

1. Add comments, custom keys, quoted values, and CRLF newlines to the
Paperclip `.env` file.
2. Run a CLI path that calls the agent JWT secret setup.
3. Observe that the old writer replaces the complete file.

**Paperclip version or commit**

The problem exists on `master` before this pull request.

Related public context: Refs #437.

## What Changed

- Add one shared line-preserving `.env` editor for the CLI and server.
- Define minimal and JSON value encodings in the shared helper.
- Update every stale duplicate of a managed key and preserve current
duplicate encodings.
- Preserve comments, ordering, blank lines, unknown keys, export
prefixes, trailing comments, and newline style.
- Write changed files through a same-directory temporary file and atomic
rename.
- Limit CLI updates to non-empty `PAPERCLIP_*` entries.
- Skip the write when all managed values are current.
- Add shared, CLI, and server regression coverage.
- Refresh the branch after the related config, sandbox, and skill safety
changes landed.
- Add a cross-cutting integration test for config, env-file,
managed-sandbox, and managed-instructions rerun survival.

## Verification

- `pnpm exec vitest run packages/shared/src/env-file.test.ts
packages/shared/src/config-schema.test.ts
cli/src/__tests__/agent-jwt-env.test.ts
cli/src/__tests__/config-store.test.ts
server/src/__tests__/config-file.test.ts
server/src/__tests__/worktree-config.test.ts` passes 39 tests.
- `pnpm exec vitest run
server/src/__tests__/rerun-survival.integration.test.ts` passes 4 tests.
- `pnpm -r typecheck` passes on the previous head. GitHub CI reruns it
on the refreshed head.
- The previous head passed the complete general, serialized, workspace,
and E2E matrix. GitHub CI reruns that matrix on the refreshed head.
- `pnpm build` passes on the previous head. GitHub CI reruns it on the
refreshed head.

## Risks

- Low risk. The production change only affects managed `.env`
assignments.
- Existing managed assignments can keep their original quoting when
their decoded values are current.
- Changed CLI values keep the prior minimal encoding policy. Changed
server values keep the prior JSON encoding policy.
- Duplicate managed assignments now follow one explicit rule: Paperclip
updates each stale occurrence.
- The master refresh had one import-block conflict. The resolution keeps
both the config merge imports and the env-file imports.
- The added integration file is test-only. It has no database, API, or
UI contract effect.

> 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 from the GPT-5 family produced this change with reasoning,
tool use, and code execution. The runtime did not expose the exact model
ID or 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-08-07 00:54:43 -05:00
Dotta 5da382fd59
feat(skills): require explicit merge modes (#10978)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents can select company skills and synchronize them to adapter
runtimes
> - The skill sync API replaced the complete selection without an
explicit destructive choice
> - Company package import also replaced conflicting skills by default
> - These defaults could remove operator edits during setup and import
reruns
> - This pull request adds explicit assignment merge modes and safe
package conflict handling
> - The benefit is that reruns preserve operator work unless the caller
explicitly requests replacement

## Linked Issues or Issue Description

**What existing behavior does this improve?**

This change improves agent skill synchronization and company package
import.

**Subsystem affected**

This is a cross-cutting change across the shared contracts, server, CLI,
and UI.

**Current behavior**

Agent skill synchronization replaces the full desired skill set from a
modeless request. Package import replaces a conflicting skill when the
caller does not select a conflict mode.

**Proposed behavior**

Agent skill synchronization requires `add`, `remove`, or `replace`.
Package import skips conflicts by default. Each imported skill reports
whether it was created, renamed, replaced, or skipped.

**Reason and benefit**

Setup and import reruns must preserve operator edits by default.
Explicit destructive modes make data loss less likely and make each
outcome inspectable.

**Breaking changes**

Callers of the agent skill sync API must now send `mode`. Callers that
need the former behavior must send `replace`. Package import now uses
`skip` when `onConflict` is absent.

## What Changed

- Added required `add`, `remove`, and `replace` modes to the shared
agent skill sync contract.
- Added actionable `422` validation for missing or invalid modes.
- Updated first-party UI and CLI callers with explicit modes.
- Changed package skill conflict handling to use `skip` by default.
- Kept plugin-owned and built-in stock skill imports on explicit
`replace`.
- Added created, renamed, replaced, and skipped results to company
imports.
- Added regression coverage for merge modes and package conflict
outcomes.

## Verification

- `pnpm check:token-gates`
- `pnpm -r typecheck`
- `pnpm build`
- `pnpm test:run:serialized` (128 suites passed)
- `pnpm --filter @paperclipai/skills-catalog test` (20 tests passed)
- Focused agent skill route, company skill service, portability, CLI,
and UI tests passed.
- GitHub CI passed build, typecheck, canary, all general and serialized
test shards, all browser shards, policy, security, and final
verification on commit `2cfbb3e4c5`.
- Greptile reviewed the latest commit at 5/5 with zero unresolved
threads.

## Risks

- This change intentionally rejects modeless agent skill sync requests.
- The safe package default can leave an existing skill unchanged where
the old default overwrote it.
- All first-party callers now select a mode. Regression tests cover each
outcome.

> 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 `gpt-5.6-sol` through Codex. The runtime used agentic
reasoning, tool use, code execution, and repository editing. The runtime
did not expose the 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-08-07 00:42:08 -05:00
Dotta f6c6452b25
fix(server): preserve managed environment drift on boot (#10979)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Paperclip creates a managed sandbox environment for each company
during boot
> - Operators can change environment fields after Paperclip creates the
environment
> - The boot reconciler replaced those changes without checking for
drift
> - This pull request adds stock hashes and transactional drift
reconciliation
> - The benefit is that Paperclip can update untouched stock fields
without losing operator work

## Linked Issues or Issue Description

**What happened?**

The managed sandbox boot reconciler rewrote the stock description,
configuration, metadata, and status on every start. It did not detect
operator changes first. A restart could therefore remove an operator's
changes.

**Expected behavior**

Paperclip must preserve operator changes by default. It must update an
untouched stock environment when Paperclip ships new stock values. It
must perform each row update and stock-hash update atomically.

**Steps to reproduce**

1. Start Paperclip and let it create the managed sandbox environment.
2. Change one Paperclip-owned stock field on that environment.
3. Restart Paperclip.
4. Observe that the previous reconciler replaced the change.

**Paperclip version or commit**

This bug reproduces on `master` before this change.

**Deployment mode**

Local development and self-hosted server boot are affected.

## What Changed

- Add a shared deterministic stock-hash and drift classifier for
built-in resources.
- Track the managed sandbox stock hash with the company-scoped built-in
resource binding.
- Reconcile the environment and its stock metadata in one transaction
with a row lock.
- Preserve operator-modified and unmanaged rows and report their skipped
update state.
- Use archive ownership tokens so provider recovery reactivates only
Paperclip-archived rows and preserves later operator archive decisions.
- Keep operator-owned environment variables and unrelated metadata out
of the stock fingerprint.
- Add activity records for managed environment creation, updates,
skipped drift, tracking initialization, and archive changes.
- Add regression tests for current stock, available stock updates,
operator drift, unmanaged rows, archive and reactivation, user-owned
fields, and concurrent reconciliation.

## Verification

- `pnpm -r typecheck`
- `pnpm build`
- `env -u AWS_ACCESS_KEY_ID -u AWS_SECRET_ACCESS_KEY pnpm test:run --
--mode serialized` (128 suites passed)
- Repository general server, UI, CLI, shared, skills catalog, database,
adapter, plugin SDK, and plugin creator projects passed. Two
embedded-Postgres tests exceeded the host's five-second default under
the aggregate run and passed in the complete database project with
`--testTimeout=20000`. One timing-sensitive sandbox stream test passed
on its focused retry.
- Focused managed-environment unit and integration coverage passed: 49
tests across the drift classifier, boot report, and environment service
suites.

## Risks

- The main risk is an incorrect ownership boundary in the stock
fingerprint. The fingerprint includes only Paperclip-owned stock fields.
Tests confirm that environment variables and unrelated metadata survive
reconciliation.
- Concurrent reconciliation could otherwise overwrite a late operator
edit. The implementation locks the environment row and updates the row
and hash binding in one transaction. A concurrency test covers this
path.
- There is no schema migration. Existing managed rows initialize
tracking without replacing their current values.

> 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 GPT-5 Codex. The exact serving snapshot and context-window size
were not exposed. The model used 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>
2026-08-07 00:41:42 -05:00
Dotta 35132af161
fix(config): preserve extensions and guard invalid repairs (#11005)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The CLI and server share a JSON configuration contract for local
installations and worktrees.
> - Existing config writes removed extension keys because Zod stripped
unknown object properties.
> - Invalid config files could also be replaced with defaults before an
operator preserved the original bytes.
> - Configuration updates must preserve operator edits and must not
rewrite files when the effective value is unchanged.
> - This pull request adds extension-preserving merges, guarded
invalid-config repair, atomic writes, and focused regression tests.
> - The benefit is safe setup and configuration reruns without data loss
or unnecessary mtime changes.

## Linked Issues or Issue Description

**What happened?**

Known-field updates through the CLI or server removed unknown top-level
and nested config keys. Non-interactive configure and onboard paths
could replace a present but invalid config with defaults.

**Expected behavior**

Writers preserve extension keys, skip semantic no-op writes, and require
explicit interactive confirmation before an invalid config is replaced.
Repair preserves an exact collision-safe backup first.

**Steps to reproduce**

1. Add an unknown top-level key and an unknown nested provider key to
`config.json`.
2. Update a known field through the CLI or worktree config writer.
3. Observe that the extension keys are removed on the base branch.
4. Write invalid JSON and run configure or onboard without an
interactive terminal.
5. Observe that the original file can be replaced without a durable
invalid-file backup on the base branch.

**Paperclip version or commit**

`master` at the pull request base commit.

## What Changed

- Accept unknown properties at each extensible config object boundary
while keeping every known field validated.
- Merge known-field updates into the parsed source config and preserve
only unknown extension data.
- Warn about near-match key names without deleting or changing them.
- Skip writes when the effective config is unchanged, which keeps file
mtimes stable.
- Write config changes through a temporary file, file sync, rename, and
directory sync.
- Distinguish a missing config from an invalid config in configure and
onboard.
- Back up invalid bytes as `config.json.invalid-N` and verify the source
still matches that backup before repair.
- Require interactive repair confirmation and reject non-interactive
replacement with an actionable message.
- Document the config preservation and repair behavior.

## Verification

- `pnpm exec vitest run packages/shared/src/config-schema.test.ts
cli/src/__tests__/config-store.test.ts
cli/src/__tests__/configure-repair.test.ts
cli/src/__tests__/configure.test.ts cli/src/__tests__/onboard.test.ts
server/src/__tests__/config-file.test.ts
server/src/__tests__/worktree-config.test.ts`
- `pnpm -r typecheck`
- `AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= VITEST_MAX_WORKERS=1 pnpm
test:run`
- `pnpm build`
- Confirm all pull request checks are green on the latest commit.
- Confirm Greptile reports 5/5 with no unresolved comments.

## Risks

- Passthrough keeps misspelled keys. Near-match warnings make this
visible without destructive cleanup.
- Merge behavior must distinguish unknown extension keys from optional
known keys. Schema-aware regression tests cover preservation and
known-key deletion.
- Repair must not overwrite bytes that changed after backup. The writer
compares the current source with the selected backup before atomic
replacement.
- The change does not alter database schema, company scoping, or
activity logging.

> 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 model family. The exact deployment model ID and
context window are not exposed. Agentic reasoning, tool use, and code
execution 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-08-07 00:41:19 -05:00
Nicky Leach 9ace548fd2
feat(observability): rename sandbox provider spans and add run-time wrapper spans (#10999)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Paperclip uses adapter and sandbox code to start agents and run
sandbox work
> - The current sandbox spans use mixed names and do not group related
run-time work
> - Mixed names make traces harder to read and compare across providers
> - This pull request renames provider spans, adds run-time wrapper
spans, and keeps the host allowlist closed
> - The benefit is clearer traces with the same sandbox behavior and
trust boundary

## Linked Issues or Issue Description

**What existing behavior does this improve?**

This improves OpenTelemetry span names and grouping for sandbox startup,
execution, callback relay, and agent session work.

**Subsystem affected**

Cross-cutting (multiple of the above): adapter utilities, sandbox
providers, shared telemetry documentation, and server instrumentation.

**Current behavior**

Sandbox provider spans use mixed names. Related run-time operations
expose inner `sandbox.exec` spans without a named wrapper span. The host
mapper uses a closed allowlist for provider span names.

**Proposed behavior**

Use descriptive provider-scoped span names. Add wrapper spans for agent
session input, agent session output polling, and callback relay. Keep
the host mapper allowlist closed and map unknown names to `other`.

**Reason and benefit**

Clear names make traces easier to read and reduce ambiguity during
sandbox operation analysis. Wrapper spans show the full operation while
preserving the inner execution spans.

**Breaking changes**

None. This change updates telemetry span names and grouping only. It
does not change sandbox behavior, endpoint behavior, or the host trust
boundary.

**Additional context**

Related prior work:
[#10758](https://github.com/paperclipai/paperclip/pull/10758).

## What Changed

- Rename Daytona provider sync and session spans with descriptive
provider-scoped names.
- Add three run-time wrapper spans for agent session input, output
polling, and callback relay.
- Add a shared span runner that preserves no-op behavior without a real
tracer.
- Keep the host mapper allowlist closed and map unknown names to
`other`.
- Update telemetry documentation and span-name tests.

## Verification

- Focused adapter-utils span tests pass for startup timing, callback
relay, and sandbox execution.
- Focused Daytona plugin span tests pass for renamed leaf spans and
session open or close spans.
- Focused server tests pass for host mapping and instrumentation.
- The stacked diff contains one commit on top of
`feat/daytona-persistent-session-model`.

## Risks

- Span names change for existing telemetry consumers.
- The wrapper spans add trace structure but do not change sandbox
execution.
- The host mapper keeps the existing closed allowlist and `other`
bucket.

> 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 GPT-5 (Codex agent); exact deployment revision and context window
are not exposed in this run; 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 (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-08-06 22:31:15 -07:00
Dotta 03cfad7ceb
feat(apps): connect Notion through MCP OAuth (#11009)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Apps give agents governed access to external tools.
> - The Apps gallery lists Notion, but the server required manually
configured OAuth credentials.
> - Notion's hosted MCP server supports OAuth discovery and dynamic
client registration.
> - Notion also requires HTTPS or a loopback HTTP redirect URI.
> - This pull request adds a direct Notion MCP OAuth path with PKCE and
reusable dynamic clients.
> - It also adds the current Apps UI states for connect and
reauthorization.
> - The benefit is a secure Notion connection with no manual client
credential setup.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

The Apps gallery, Apps connect route, OAuth token lifecycle, and managed
MCP gateway.

**Subsystem affected**

`server/`, `packages/shared/`, `scripts/`, and `ui/`.

**Current behavior**

The Notion gallery cards are disabled. The server uses the classic
Notion OAuth endpoints and requires operator-supplied client
credentials. It does not register an OAuth client from provider
metadata. Concurrent refreshes can also replay a rotating refresh token.

**Proposed behavior**

Enable the Notion Apps flow. Discover OAuth metadata from
`https://mcp.notion.com/mcp`. Register and reuse a public RFC 7591
client with PKCE. Require HTTPS or loopback HTTP callbacks. Serialize
refreshes, store each rotated refresh token before the new access token
can be used, and show a reconnect state for `invalid_grant`.

**Reason and benefit**

Operators can connect the built-in Notion MCP app without creating or
copying OAuth credentials. Paperclip keeps dynamic clients and rotating
tokens in the company secret store.

**Breaking changes**

None. Explicit environment client credentials still take priority.
Existing Slack and Linear OAuth endpoint hints remain unchanged. Other
OAuth apps remain disabled unless they are allowlisted.

**Additional context**

PR #10910 is a related, broader Connections v3 wizard replacement. This
PR is the focused current Apps flow. The MCP Tool Gateway and Connected
Apps items in `ROADMAP.md` cover this planned capability.

## What Changed

- Classify all 20 reviewed Notion MCP tools with provider-scoped read
and write defaults.
- Require approval for selected Notion mutations, including move,
duplicate, and convert actions that generic verb matching missed.
- Preserve company-scoped connection and catalog resolution for Notion
profiles and policies.
- Add RFC 7591 dynamic client registration with
`token_endpoint_auth_method=none` and mandatory PKCE.
- Store the dynamic client ID on the connection and store any returned
client secret in the company secret store.
- Reuse the registered client for later connects and keep explicit
environment credentials as the first choice.
- Discover protected-resource and authorization-server metadata from the
Notion MCP endpoint.
- Add `redirectConstraints: "https-or-loopback-http"` to the generated
Notion app definition and shared contract.
- Reject non-loopback plain HTTP callbacks before network access with a
TLS setup error.
- Serialize client registration and token refresh operations within the
server process.
- Store a rotated refresh token before publishing the refreshed access
token.
- Treat `invalid_grant` as terminal and move the connection to a clear
reauthorization state.
- Add focused coverage for registration reuse, callback constraints,
refresh rotation, and terminal grants.
- Enable the Notion Apps route and add connect, redirect, success,
error, and reconnect UI states.
- Keep non-allowlisted OAuth apps blocked and cover the UI policy with
regression tests.

## Verification

- The focused Notion policy integration test passed with embedded
PostgreSQL.
- The focused 20-tool classification test passed.
- The server typecheck passed on the governance head.
- `pnpm -r typecheck` passed on the rebased head.
- `pnpm --filter @paperclipai/server typecheck` passed after the
security follow-up.
- `pnpm exec vitest run server/src/__tests__/tool-access-service.test.ts
-t \u0027DCR|refresh tokens|invalid_grant|abandoned lease\u0027` passed
10 focused security tests.
- `pnpm build` passed on the rebased head.
- `pnpm exec vitest run server/src/__tests__/tool-access-service.test.ts
-t 'OAuth|oauth'` passed 14 tests.
- `pnpm exec vitest run packages/shared/src/app-definitions.test.ts`
passed 5 tests.
- The complete server group passed 3,686 tests with 4 skipped.
- The complete UI group passed 3,656 tests.
- The full local runner found one environment-only CLI failure because
this agent runtime injects static AWS credentials into a test that
expects `AWS_PROFILE` only. `env -u AWS_ACCESS_KEY_ID -u
AWS_SECRET_ACCESS_KEY pnpm exec vitest run
cli/src/__tests__/secrets.test.ts` passed all 8 tests.
- The prior UI verification passed 55 focused tests, `pnpm
check:token-gates`, the Storybook build, and review of six 1440 x 1000
screenshots.
- OAuth request sequence: protected-resource metadata `GET
https://mcp.notion.com/.well-known/oauth-protected-resource/mcp`;
authorization metadata `GET
https://mcp.notion.com/.well-known/oauth-authorization-server`; dynamic
registration `POST https://mcp.notion.com/register`; authorization `GET
https://mcp.notion.com/authorize`; token exchange and refresh `POST
https://mcp.notion.com/token`; MCP traffic `POST
https://mcp.notion.com/mcp`.
- The live metadata and registration probe confirmed that Notion accepts
HTTPS and loopback HTTP redirects. It rejects a plain HTTP private
hostname.
- A later QA task owns the full browser consent and managed gateway
tool-list dry run against a configured HTTPS deployment.

## Risks

- Notion can add tools. Unrecognized names use the generic classifier,
and new or changed risky tools stay quarantined after connection
activation.
- A deployment that uses a private non-loopback hostname must configure
HTTPS before it can connect Notion.
- Dynamic registration creates a provider-side client. Paperclip reuses
it because registration does not provide a standard delete operation.
- Refresh coordination uses a database CAS lease across service
instances. An unclean crash leaves an uncertain lease and requires
reconnect instead of risking refresh-token replay.
- The current Apps surface overlaps with PR #10910. Merge order can
require a small conflict resolution if that PR lands first.

> 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 on a GPT-5 runtime. The exact deployment ID and context
window are not exposed. The runtime used reasoning, repository tools,
code execution, and network 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>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 22:18:08 -05:00
Nicky Leach cfed36ea6b
feat(plugin-daytona): persistent session model with plain command dispatch (#10941)
## Thinking Path

> - Paperclip is the control plane for autonomous AI companies
> - One core subsystem runs agent work inside sandboxes
> - The Daytona provider uses that path to run user commands
> - The current one-shot model does not keep a shell alive across
commands
> - This pull request adds an opt-in persistent session model for
Daytona
> - The benefit is faster command dispatch with the same sandbox
boundaries

## Linked Issues or Issue Description

### Subsystem affected

Cross-cutting. This change touches `packages/adapters`, provider tests,
span names, and sandbox command behavior.

### Problem or motivation

The Daytona provider needs a persistent shell for repeated command
dispatch.
The old advisory wrapper path does not reach that goal.
It also adds cost and removes the session speed gain.

### Proposed solution

Add a `useSessions` driver flag.
Keep it off by default.
Open one Daytona session per lease when the flag is on.
Send each user command into that session.
Read stdout and stderr from the session logs endpoint.
Run each command in a subshell so `exit` does not stop the shell.
Remove the advisory `bwrap` wrapper path and its lease metadata.
Add session setup and teardown spans.
Keep a hard delete on teardown.

### Alternatives considered

Keep the advisory `bwrap` wrapper.
That path does not give a real persistent session.
It also keeps extra command overhead.
Keep a one-shot fallback for user commands.
That would weaken the session model and hide a missing session case.

### Roadmap alignment

This work fits the `Cloud / Sandbox agents` milestone in `ROADMAP.md`.
It also supports the control plane goal of safe remote sandbox
execution.

### Additional context

The handoff verification reported `tsc --noEmit` clean and 119 Daytona
unit tests passing.
The handoff also reported a clean host span allowlist test and five
expected commits on the branch.
The security review gate remains required before merge.

## What Changed

- Added an opt-in persistent session model for the Daytona sandbox
provider.
- Routed user commands through `executeSessionCommand` when sessions are
enabled.
- Removed the advisory `bwrap` command wrapper path and the lease
metadata it used.
- Added session lifecycle spans and span allowlist coverage.
- Documented the leak bound in `DIRECTORY-CONSTRAINT-FINDINGS.md`.

## Verification

- `tsc --noEmit` clean for the Daytona plugin, per handoff verification.
- Daytona unit suite passes, with 119 tests, per handoff verification.
- Host span allowlist test passes, per handoff verification.

## Risks

- Persistent sessions can leak if teardown fails.
- Session logs must keep stdout and stderr separate.
- The flag stays off by default to limit rollout risk.

## Model Used

OpenAI GPT-5, Codex, 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-08-06 17:38:00 -07:00
Dotta 52b8741b8e
perf(server): cut steady-state DB hot paths in dashboard, attention, and productivity sweeps (#10992)
<!-- ASD-STE100 -->

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The server keeps fleet health with periodic sweeps and shows a
dashboard with run activity
> - The Paperclip instance became slow again after the first round of
recovery-sweep indexes landed
> - Live profiling found four steady-state hot paths that read much more
data than they use
> - This pull request bounds the dashboard recursion, adds the missing
taskKey index, and narrows two wide reads
> - The benefit is a large drop in constant database load and a
responsive server

## Linked Issues or Issue Description

**Describe the bug**
The server becomes slow while agents work. Live query sampling shows
four hot paths:
1. The dashboard run-activity recursive CTE reads every run a company
ever had on each call. One call takes 2.85 seconds. The UI calls it
after almost every fleet event through the dashboard and sidebar-badges
routes.
2. The productivity-review sweep runs each 30 seconds. Its run-scope
filter is `issueId OR taskId OR taskKey` on the run context JSONB. No
index exists for `taskKey`. The planner must detoast every run snapshot
for the agent. One query takes 444 ms and the sweep makes one for each
of ~152 candidate issues.
3. The attention failed-run section selects the full `context_snapshot`
for every run newer than the oldest exhausted run. That fetch moves 29
MB for each feed build.
4. The retention sweep pages the attention feed with a cursor. Each page
makes a full feed rebuild.

**Expected behavior**
Periodic sweeps and dashboard queries read only the data they use, and
use indexes.

**Actual behavior**
The database stays saturated. Users see a slow server.

## What Changed

- `server/src/services/dashboard.ts`: bound both arms of the
`recovered_runs` recursive CTE to the chart window. A retry is always
newer than the run it retries, so the bound cannot change visible chart
data. Live time went from 2,852 ms to 54 ms.
- `packages/db/src/migrations/0210_heartbeat_context_taskkey_index.sql`:
add the `taskKey` expression index that completes the
issueId/taskId/taskKey trio. With all three, the planner uses a
BitmapOr. Live time for the productivity run-scope query went from 444
ms to 1.9 ms.
- `packages/db/src/schema/heartbeat_runs.ts`: mirror the new index in
the Drizzle schema.
- `server/src/services/productivity-review.ts`: select only the seven
run fields the evidence code reads. Before, the query pulled full rows
with `result_json` (up to 43 kB per row, 100 rows per issue).
- `server/src/services/attention.ts`: project `issueId`/`taskId` text
fields instead of the full `context_snapshot` in the failed-run
newer-runs query (29 MB per feed build before).
- `server/src/index.ts`: the retention sweep now builds the attention
feed once per company with `all: true` instead of one full rebuild per
cursor page.
- `packages/db/src/heartbeat-context-snapshot-index-migration.test.ts`:
cover the new index and re-run migration 0210 statements to prove
idempotency.

## Verification

- `pnpm --filter @paperclipai/db typecheck` (includes migration
numbering and safety checks) — pass.
- `npx tsc --noEmit` in `server/` — pass.
- `npx vitest run
packages/db/src/heartbeat-context-snapshot-index-migration.test.ts` —
pass (embedded Postgres, full migration chain, planner assertions,
idempotent re-run of 0209 and 0210).
- `npx vitest run` on attention, dashboard, productivity-review,
decision-retention, issue-blocker-attention, and issue-review-attention
test files — 72/72 pass.
- Live EXPLAIN ANALYZE before/after numbers are in the What Changed
list.

## Risks

- Migration 0210 builds one btree index without CONCURRENTLY inside the
transactional migration runner. The table is not in the large-table
bucket. The 0209 twin built in seconds on a 100k-row live table.
- The CTE bound excludes retry ancestors that are older than the chart
window. Those rows are not visible to the chart query, so chart output
does not change.
- The attention projection changes JSONB scalar handling in one edge
case: a non-string `issueId`/`taskId` value now casts to text instead of
reading as absent. These keys are always strings in practice.
- The retention sweep now holds one full feed in memory per company. The
cursor loop already accumulated all pages into one array, so peak memory
is unchanged.

## Model Used

Claude Fable 5 (`claude-fable-5`, Anthropic, Mythos-class tier, extended
thinking + tool use) via Paperclip agent runtime.

- [x] I searched existing PRs and issues and this change is not a
duplicate.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 11:56:40 -05:00
Dotta 656ecfa585
fix(server): keep Date fields intact through secret redaction; harden chat notice timestamps (#10984)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The task chat thread renders issue comments, system notices, and run
transcripts
> - The server routes comment payloads through the run-secret redaction
walker before it sends them
> - The walker rebuilds each object with `Object.entries`, and this
collapses `Date` instances to `{}`
> - The chat renderer then calls `.toISOString()` on an invalid date and
throws, and the thread falls back to the error banner
> - This pull request keeps `Date` instances intact in redacted
responses and makes the renderer safe against bad timestamps
> - The benefit is that task threads with system notices render
correctly again

## Linked Issues or Issue Description

**What happened**

Task threads that contain a system notice showed the banner "Chat
renderer hit an internal state error." in place of the conversation.
This occurred on many tasks.

**Expected behavior**

The thread renders all comments and system notices with correct
timestamps.

**Steps to reproduce**

1. Open a task that has at least one system notice comment (for example
a "Workspace ready" notice).
2. `GET /api/issues/{id}/comments` returns `createdAt: {}` for every
comment because the secret-redaction walker collapses `Date` objects.
3. The system-notice row calls `new Date({}).toISOString()`. This throws
`RangeError: Invalid time value` and trips the thread error boundary.

**Version / deployment**

Regression from #9934 (`e43f187ca`). It applies to all deployments that
include that commit.

## What Changed

- `server/src/services/run-secret-redaction.ts`:
`redactRegisteredSecretValues` now returns `Date` instances as-is. Dates
hold no redactable text, and the `Object.entries` rebuild turned them
into `{}`.
- `ui/src/components/IssueChatThread.tsx`: the system-notice row formats
its timestamp with a new `toValidIsoString` helper. A value that does
not parse as a date now degrades to "no timestamp" instead of a render
crash.
- Regression tests at three layers:
- Walker unit tests: `Date` values survive with and without registered
secret values.
- Route test: `GET /issues/:id/comments` serializes `createdAt` /
`updatedAt` as ISO strings.
- Render test: a system notice with a malformed `createdAt` renders
without the error boundary.

## Verification

- `npx vitest run --root server
src/__tests__/run-secret-redaction.test.ts` — 5 passed.
- `npx vitest run --root server
src/__tests__/issue-comment-redaction.test.ts` — 4 passed (embedded
Postgres route test).
- `cd ui && npx vitest run src/components/IssueChatThread.test.tsx
src/components/IssueChatThreadSystemNotice.test.tsx
src/lib/issue-chat-messages.test.ts` — 121 passed.
- Each new test was run against the unfixed code and failed there, which
confirms it guards the regression.
- A local sweep rendered 47 real issue threads through
`IssueChatThread`: 7 tripped the boundary before the fix, 0 after.

## Risks

- Low risk. The server change only preserves `Date` objects that the
walker destroyed before. String redaction behavior does not change, and
the registry-key stripping does not change.
- The UI change only affects the timestamp of system-notice rows and
omits it when the value is invalid.

## Model Used

- Claude Fable 5 (`claude-fable-5`), Anthropic. Agentic coding session
with tool use (file edits, shell, Vitest). No extended-context or
special reasoning mode.

## 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-08-06 10:29:07 -05:00
Dotta 814cb33676
feat(server): allow agents to resolve review confirmations (#10939)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Issue reviews use thread confirmations to record explicit verdicts
> - The server allowed users to resolve review confirmations but
rejected all agent actors
> - This one-way rule prevented an eligible agent reviewer from
completing a review
> - The existing review policy already defines which actor can submit a
verdict
> - This pull request applies that policy to agent confirmation verdicts
on writable issues
> - The benefit is a consistent review gate for users and agents with
preserved audit attribution

## Linked Issues or Issue Description

- Builds on: #10931 (merged into master before this PR)
- Refs #8617

## What Changed

- Allow eligible agents to accept or reject pending review confirmations
on issues they can write.
- Allow a creator agent to withdraw its own pending review confirmation
when the review policy permits it.
- Reuse the review verdict policy check for users and agents.
- Require an explicit, same-run review-confirmation binding so unrelated
board-only confirmations stay protected.
- Preserve board-only tool action confirmations and existing user
attribution.
- Add route and service tests for agent accept, reject, withdrawal,
human-only denial, and user attribution.

## Verification

- `pnpm exec vitest run packages/shared/src/validators/issue.test.ts
server/src/__tests__/issue-execution-policy-routes.test.ts
server/src/__tests__/issue-review-policy.test.ts
server/src/__tests__/issue-thread-interaction-routes.test.ts
server/src/__tests__/issue-thread-interactions-service.test.ts
server/src/__tests__/issue-stalled-review-decision-routes.test.ts` (256
passed after rebasing onto master and the atomic binding fix)
- `pnpm --filter @paperclipai/shared typecheck`
- `pnpm --filter @paperclipai/server typecheck`
- `pnpm -r typecheck`
- `env -u AWS_ACCESS_KEY_ID -u AWS_SECRET_ACCESS_KEY pnpm test:run`
- `pnpm build`

## Risks

- The change expands who can resolve pending review confirmations. The
existing issue write checks and review policy limit this access.
- Tool action confirmations remain board-only.
- The pull request depends on the review policy helper from #10931,
which is now merged into master.

> 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`.

The roadmap marks Agent Reviews and Approvals as shipped. This pull
request fixes a narrow server behavior gap in that shipped capability.

## Model Used

- OpenAI Codex, model `gpt-5.6-sol`, with reasoning, tool use, and code
execution. The runtime does not expose the 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-08-05 23:40:05 -05:00
Dotta f554d67377
fix(server): add explicit review verdict policies (#10931)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Issues use `in_review` to request a final decision from an
authorized writer
> - The server rejected an assignee agent that tried to close its own
review, even when the issue had no independent-review rule
> - This rejection stopped the default agent workflow and did not
represent the configured execution-stage rules
> - Paperclip needs an open default and explicit issue-level constraints
for teams that require an independent or human verdict
> - This pull request removes the unconditional rejection and adds
`anyone`, `not_creator`, and `human_only` review policies
> - The benefit is a working default path with opt-in, authenticated
verdict controls

## Linked Issues or Issue Description

Refs #10635, #4429, and #10671.

The related public work covers execution-stage independence,
self-approval fallback behavior, and durable review paths. This change
is distinct. It controls who can resolve an issue review verdict. It
keeps configured execution stages active.

## What Changed

- Added a nullable `review_policy` issue column. Null has the same
meaning as `anyone`. The migration does not backfill existing issues.
- Added shared create, update, response, and compact issue contracts for
`anyone`, `not_creator`, and `human_only`.
- Removed the unconditional agent self-approval rejection for
`in_review` issues.
- Added one reusable verdict-actor check for terminal status changes and
pending interaction accept or reject actions.
- Used the authenticated principal type for `human_only`. Agent keys and
run tokens remain agent principals.
- Used the latest transition into `in_review` to identify the requester
for `not_creator`.
- Added actionable 403 responses that name the policy, the allowed
actor, and the next step.
- Kept the configured execution-stage transition and signoff behavior.
- Added focused contract, helper, status-route, interaction-route, and
execution-stage regression tests.
- Updated the implementation specification for the new issue field.

## Verification

- `pnpm exec vitest run packages/shared/src/validators/issue.test.ts
server/src/__tests__/issue-review-policy.test.ts
server/src/__tests__/issue-stalled-review-decision-routes.test.ts
--reporter=dot` passed: 42 tests.
- `pnpm --filter @paperclipai/shared typecheck` passed.
- `pnpm --filter @paperclipai/db typecheck` passed, including migration
numbering and safety checks.
- `pnpm --filter @paperclipai/server typecheck` passed.
- `pnpm run typecheck:build-gaps` passed across server, CLI, plugin
SDK/examples, plugin wiki, and UI.
- `git diff --check origin/master...HEAD` passed.
- SecurityEngineer review approved the authenticated-principal checks
and accepted policy-relaxation tradeoff with no required changes.
- Greptile reviewed the latest head at 5/5 with zero inline comments or
follow-ups.
- The latest-head GitHub rollup passed build, typecheck,
server/workspace tests, serialized suites, canary, e2e, and external
security checks.

## Risks

- The migration adds one nullable text column. It has no default and no
backfill.
- `not_creator` reads the latest recorded transition into `in_review`.
It denies the verdict when it cannot identify the requester.
- Agents can change or relax `reviewPolicy` when they have issue write
access. This is intentional for this issue-level control.
- Null and `anyone` do not add a database query to the verdict path.
- Configured execution-stage checks still run after the issue-level
policy check.

> This work aligns with the completed "Agent Reviews and Approvals" and
"Enforced Outcomes" roadmap items. It does not add a new roadmap
capability.

## Model Used

- OpenAI Codex, GPT-5. The exact deployment ID and context-window size
are not exposed to the agent. The run used reasoning, repository tools,
code execution, 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 (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-08-05 23:12:41 -05:00
Dotta 5b62a3883f
feat(settings): add experimental Simplified English Interactions flag (#10934)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents ask humans for decisions through interaction blocks: plan
confirmations, structured questions, suggested tasks, and checkbox
prompts
> - Each agent writes these decision prompts in its own style, so
operators can get long or unclear text at the exact moment they must
decide
> - There is no instance-level control that makes agents use a
controlled language for these decision points only
> - This pull request adds an experimental setting that tells agents to
write all user-interaction content in ASD-STE100 Simplified Technical
English, with the context the user needs and the effect of each choice
> - The benefit is faster, clearer human decisions, with agent thinking
and normal responses unchanged

## Linked Issues or Issue Description

Refs #10410 (optional `/simplified-english` skill in the skills catalog;
this PR adds the instance-level toggle for interactions).

**Problem or motivation**

Agent-posted user interactions (plan confirmations, structured
questions, suggested-task proposals, checkbox prompts) are written in
each agent's default style. Operators who want fast, unambiguous
decisions have no way to ask agents to use a controlled language for
exactly those decision points.

**Proposed solution**

Add an experimental instance setting,
`enableSimplifiedEnglishInteractions` ("Simplified English
Interactions"). When it is on, the server sets
`simplifiedEnglishInteractions: true` in the heartbeat wake payload. The
shared wake-prompt renderer, used by every adapter, then emits a
directive: write all user-interaction content in ASD-STE100 Simplified
Technical English, state what information the user needs to decide, and
state what happens for each choice. The directive applies to interaction
content only. Thinking, comments, documents, and other responses keep
their usual style.

**Alternatives considered**

Per-agent instructions work today, but someone must maintain them on
every agent. An instance-level toggle applies uniformly and turns off in
one place. Server-side rewriting of interaction payloads was rejected:
post-hoc translation is lossy and cannot add the decision context that
only the agent has.

**Roadmap alignment**

Extends the experimental settings surface with another opt-in
agent-behavior refinement, consistent with existing prompt-side flags.

## What Changed

- Added `enableSimplifiedEnglishInteractions` to the experimental
instance-settings zod schema, mirror type, and feature catalog (default
off, tier preference) in `packages/shared`.
- Server `instance-settings.ts` normalizes the flag on both read
branches; `heartbeat.ts` reads it once and passes
`simplifiedEnglishInteractions` into `buildPaperclipWakePayload`.
- Shared adapter renderer
(`packages/adapter-utils/src/server-utils.ts`): added the field to
`PaperclipWakePayload`, normalization, and an `- interaction language
(experimental): ...` directive emitted in both fresh and resume prompt
lanes, so one injection point covers all adapters.
- UI: new experimental settings card "Simplified English Interactions"
in `ui/src/pages/InstanceExperimentalSettings.tsx`, in alphabetical card
order; fixtures updated.
- Tests: renderer coverage for flag on/off in both lanes, plus
schema/catalog/UI fixture updates.

## Verification

- From the repo root: `node_modules/.bin/vitest run
packages/adapter-utils` (88/88), `packages/shared` validators (25/25),
server instance-settings + heartbeat suites (47/47 and 70/70 consumer
tests), `ui` settings tests (32/32).
- Typecheck is clean in all four touched packages.
- Manual check: turn the flag on in Settings → Experimental, wake an
agent, and confirm the wake prompt contains the interaction-language
directive; turn it off and confirm the directive is absent.

## Risks

- Low risk: the flag defaults to off, and the only behavior change is
one extra directive line in the wake prompt when an operator turns it
on.
- The directive is advisory to the agent; models can still deviate from
STE. No data or API shape changes; no migration.

## Model Used

- Claude Fable 5 (Anthropic, model ID `claude-fable-5`), extended
thinking, agentic tool use via Claude Agent SDK.

## 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-08-05 21:50:41 -05:00
Dotta e43f187cad
feat(secrets): add human-approved secret proposals (#9934)
## Thinking Path

> - Paperclip is the control plane people use to manage AI-agent
companies.
> - Agents can encounter credentials during work.
> - Directly creating live secrets or bindings would bypass human
governance.
> - Proposal records must remain inert and separate from live secret
resolution until an authorized human approves them.
> - Approval must reuse the existing secret-create and protected
agent-config write paths.
> - This pull request adds the propose, review, approve, and reject
lifecycle.
> - The benefit is that agents can safely hand credentials into
Paperclip without exposing plaintext or gaining authority to activate
them.

## Linked Issues or Issue Description

Follow-on to #9921, which established run-bound agent secret access.

**Problem / motivation:**

Agents can receive credentials during work. There is no governed way for
them to propose a credential or binding without exposing plaintext in
work artifacts or immediately creating live access.

**Proposed solution:**

Store agent-authored proposals outside live secret tables. Encrypt each
proposed value and register exact-value redaction when Paperclip
receives it. Require an authorized human to approve or reject each
proposal. Approval executes through the normal write paths as the human
approver. Binding proposals can target only the proposer or its downward
reporting chain under the restrictive V1 policy.

**Alternatives considered:**

We rejected live secrets with a `proposed` status. That design would put
untrusted rows in resolver, list, and sync paths. It would also allow
uniqueness squatting. We rejected direct agent binding writes because a
binding is an agent-config write and must keep the existing human
permission gate.

**Roadmap alignment:**

This change extends the run-bound agent secret-access foundation in
#9921 with a governed proposal workflow.

## Security Verdict

Q0 SecEng verdict: **PASS-with-required-changes**. The review accepted
the separate proposal-table design and required the implementation to:

- fail closed unless both encryption and exact-value run redaction
registration succeed;
- scrub ciphertext idempotently on reject, withdraw, and expiry, with
audit-visible state;
- treat agent justification as hostile input and foreground action,
target, provenance, and approver permissions;
- snapshot and re-check the target agent plus reports-to chain at
approval to prevent org-chart laundering;
- make cascade approval atomic and fail closed if either secret creation
or binding authorization fails;
- deny low-trust, `skill_test`, `task_bridge`, and non-run-bound sources
consistently; and
- execute approval through the normal human secret/config write paths,
including protected-change gates.

Those requirements are implemented and covered by focused service,
route, and UI tests. Residual V1 risk remains the accepted 14-day
encrypted retention window. Proposal-time redaction also cannot clean a
value that leaked before the propose call.

## What Changed

- Added `company_secret_proposals`, migration `0207`, shared proposal
contracts, and a state-machine service for create, approve, reject,
withdraw, cascade, expiry, and ciphertext scrubbing.
- Added run-bound agent proposal routes and board review routes. The
routes derive provenance from authentication and enforce source
restrictions, company isolation, chain-of-command checks,
approval-as-approver, wake-on-resolution, and dual audit trails.
- Added durable per-run exact-value redaction registration so proposal
values remain redacted on later read surfaces.
- Added the Secrets **Proposals** tab and agent configuration **Proposed
access** rows. The UI shows fingerprint and length only. It also frames
agent justification as untrusted input, runs permission preflight,
supports approve and reject actions, and confirms cascades.
- Updated OpenAPI, agent skill guidance, API reference documentation,
and focused server and UI regression coverage.
- Rebased the branch onto current `master` and renumbered the proposal
migration after `0206`.

## QA Acceptance Results

Q5 QA verdict: **PASS — 9/9 acceptance criteria met**, with one Minor
non-blocking follow-up.

- **AC1:** proposed values never echo, never appear in live
lists/resolvers, and expose only fingerprint + length to board
reviewers.
- **AC2:** restrictive `self_and_reports` matrix passes: self/downward
allowed; upward/lateral denied.
- **AC3:** secret approval uses the normal create path, honors rename
overrides, records proposer/approver provenance, and scrubs ciphertext.
- **AC4:** approved bindings materialize and resolve through the target
agent's runtime list/fetch routes.
- **AC5:** pending-secret bindings require cascade; cascade succeeds
atomically and permission failures leave nothing applied.
- **AC6:** reject, withdraw, dependent rejection, and expiry paths scrub
ciphertext and preserve reasons/audit state.
- **AC7:** token/source and approver denial matrix passes through live
checks plus focused route tests.
- **AC8:** proposal lifecycle events and reused
`secret.created`/config-write events form the required dual audit trail;
origin-issue notification and wake are queued.
- **AC9:** both review surfaces render and execute correctly; UI
approval materializes the binding.

QA also confirmed zero plaintext occurrences for all exercised proposal
values in server logs. The single finding is that the company-level
`bindingTargetPolicy` toggle is not wired yet. V1 is hardcoded to the
restrictive `self_and_reports` policy. The matrix is correct and the
follow-up is tracked separately, so QA classified it as non-blocking.

## Verification

- Focused server proposal and redaction suite: 83 tests pass.
- Focused proposal review UI suite: 54 tests pass.
- Embedded-Postgres migration reapply test: 1 test passes with the
documented 30-second timeout.
- `pnpm --filter @paperclipai/db typecheck` passes, including migration
numbering and safety checks.
- `pnpm --filter @paperclipai/shared typecheck` passes.
- `pnpm --filter @paperclipai/ui typecheck` passes.
- `pnpm check:token-gates` passes with all gates clean.
- Q5 exercised the complete propose, review, approve, bind, and
runtime-resolve flow over real HTTP, JWT, and database paths. It
verified 9/9 acceptance criteria.

## Risks

- Proposal ciphertext is retained encrypted for up to 14 days while
pending. Terminal-state and expiry scrub paths reduce but do not remove
server-compromise risk during that window.
- The V1 target policy is restrictive but not yet company-configurable.
A separate follow-up owns that change.
- A new migration can require another renumber if another migration
lands before maintainers merge this pull request.

> 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. See `CONTRIBUTING.md`.

## Model Used

- OpenAI Codex coding agent. The exact runtime model ID and
context-window size are not exposed. The agent used reasoning,
repository editing, terminal execution, Paperclip API, and GitHub CLI
capabilities. Q3 UI work also records Claude Opus 4.8 assistance in its
commit trailers.

## 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 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>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 21:49:40 -05:00