Commit Graph
3827 Commits
Author SHA1 Message Date
DottaandPaperclip d283ccd147 test(apps): complete reconnect wizard flow
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-20 22:42:02 +00:00
DottaandPaperclip 8a1adc8c1d fix(db): make grant migration replay safe
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-20 22:29:50 +00:00
DottaandPaperclip b78d75192b fix(db): renumber connection grants migration
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-20 22:23:33 +00:00
DottaandPaperclip 51096d303f fix(apps): apply ask-first defaults in gallery setup
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-20 22:18:18 +00:00
DottaandCodex 13dd4c1eae fix(apps): preserve grant and approval defaults
Co-authored-by: Codex <codex@openai.com>
2026-08-20 22:11:52 +00:00
cb0009b097 fix: preserve recovery retries across restarts (#11817)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The control plane must keep each active issue on a clear execution
or recovery path.
> - A missing issue disposition can require more than one bounded repair
attempt.
> - A server restart could lose that repair path or move source
ownership to the recovery owner.
> - A parked or expired retry could also make the user interface show a
false healthy state.
> - Concurrent recovery loops must not schedule the same repair attempt
twice.
> - This pull request keeps retry state durable, makes scheduling
atomic, and keeps source ownership stable.
> - The benefit is that recovery continues after a restart and operators
see the correct state.

## Linked Issues or Issue Description

**What happened?**

A run that ended without a valid issue disposition could lose its repair
path after a server restart. Manager recovery could also change the
source owner. In addition, a parked or expired retry could make the
issue look healthy when no active work existed. Concurrent
reconciliation could also schedule the same repair attempt twice.

**Expected behavior**

Paperclip must keep bounded source and manager repair attempts across
restarts. Recovery ownership must stay separate from source issue
ownership. The server and user interface must report only a live retry
as active work. Each repair attempt must be scheduled at most once per
company.

**Steps to reproduce**

1. Start an agent run on an issue.
2. End the run without a valid issue disposition.
3. Let the first repair attempt schedule a retry.
4. Restart the server, let the retry time pass without a live run, or
start two reconciliation loops together.
5. Observe that the repair path can stop, the issue can show a false
healthy state, or duplicate retries can be created.

**Paperclip version or commit**

The problem existed on `master` before candidate head
`d8e620fe86bade7df18decac332007f5821ae04f`.

**Deployment mode**

The problem affects self-hosted servers and local builds that use
automatic recovery.

## What Changed

- Persist bounded source-owner and manager repair lineages with stable
fingerprints and retry limits.
- Resume incomplete disposition repairs after a server restart.
- Keep recovery ownership separate from source issue ownership and
enforce source mutation authority.
- Project live retry evidence into issue and blocker summaries.
- Show recovery owner, return owner, attempt count, and retry state in
the board user interface.
- Treat expired or parked retries as attention states unless a queued or
running attempt exists.
- Atomically deduplicate disposition-repair wake requests with a
company-scoped partial unique index.
- Reuse the winning run when concurrent reconciliation loses the
uniqueness race, without duplicate scheduling activity.
- Honor disabled on-demand wake policy before recovery scheduling and
again before delayed retry promotion.
- Keep the new index migration safe for lagging seeded databases that
already contain the index.
- Add server and user interface tests for recovery, restart, ownership,
retry, concurrency, and blocker states.
- Update the implementation and execution semantics documents.

## Verification

- Focused server recovery and ownership suites: 282 tests passed on the
repaired base candidate.
- Focused user interface recovery suites: 128 tests passed on the
repaired base candidate.
- Atomic-deduplication schema and recovery suites: 111 tests passed on
the first Greptile repair.
- Recovery and scheduled-retry wake-policy suites: 126 tests passed at
`d8e620fe86bade7df18decac332007f5821ae04f`.
- The exact lagging-source migration-order test passed after the index
migration became idempotent: 1 test passed and 62 unrelated tests were
skipped.
- `@paperclipai/db` and `@paperclipai/server` typechecks passed at the
current head.
- Migration generation and migration safety checks passed for migration
`0226_tan_colossus.sql`.
- `pnpm check:token-gates` passed on the repaired base candidate.
- `pnpm -r typecheck` passed on the repaired base candidate.
- `pnpm build` passed on the repaired base candidate.
- `pnpm test:run` passed 4,540 tests on the repaired base candidate.
Four fixed-port cases met listeners that already existed on the host.
- The two unchanged fixed-port files passed in an isolated network
namespace: 129 tests passed and 27 tests were skipped.
- Independent Security and QA reviews approved
`63c0423aab54c66f2293a20b0fb3f3b013ee3ba8`; exact-head re-review is
required after automated checks settle on
`d8e620fe86bade7df18decac332007f5821ae04f`.

## Risks

- Recovery orchestration affects issue liveness and ownership. The new
paths use bounded attempts, stable fingerprints, row locks, authority
checks, and database uniqueness.
- A conservative attention state can show more warnings when a scheduled
retry has no queued or running attempt. It does not hide stopped work.
- Migration `0226_tan_colossus.sql` creates a partial unique index on a
known-large table. Migrations run transactionally, so `CONCURRENTLY` is
unavailable. The matching disposition-repair key namespace is introduced
by this release, so deployed databases have no matching rows before the
index is added.

> 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 model family used agentic reasoning, tool
use, and code execution. The runtime did not expose the exact model ID
or context window.
- Anthropic Claude Opus 5 used a 1M context window, tool use, and code
execution for part of the user interface repair, as recorded in the
commit history.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 17:09:42 -05:00
DottaandPaperclip fda25be6b1 test(db): order grant rollback dependencies
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-20 21:51:57 +00:00
DottaandPaperclip 756dd2fbde fix(apps): mediate stdio credentials through grants
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-20 21:47:36 +00:00
DottaandPaperclip 6d4e65f621 fix(smoke-lab): seed organization grants
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-20 21:39:40 +00:00
DottaandPaperclip 89468893b9 feat(apps): resolve credentials through grants
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-20 21:23:02 +00:00
DottaandPaperclip c7cc9b51c0 fix(workspaces): allow legacy runtime rows without ports
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-20 21:20:02 +00:00
DottaandPaperclip d1f27903a9 fix(ui): repair rebased storybook types
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-20 21:20:02 +00:00
DottaandPaperclip 1880c9d21e feat: enforce connection access permissions
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-20 21:20:02 +00:00
DottaandPaperclip ad8f869278 fix(apps): tolerate stale activity API
Use the omitted-window contract for all-time activity so mixed-version restarts do not reject the page, and remove the obsolete audit footer.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-20 21:20:02 +00:00
DottaandPaperclip 5f7926a5d5 fix(apps): include connection lifecycle activity
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-20 21:20:02 +00:00
DottaandPaperclip 8ed119deb6 fix(apps): aggregate connection activity
Read the cross-app activity feed from the canonical tool call events used by each connection, preserving filters, pagination, gateway scope, and redacted invocation details.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-20 21:20:02 +00:00
DottaandPaperclip 61bf48f972 feat(apps): improve gateway pages
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-20 21:20:02 +00:00
DottaandPaperclip d947714fd4 Remove discouraging Apps sidebar copy
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-20 21:20:02 +00:00
DottaandPaperclip 6b503e114b Remove Rules and Health pages
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-20 21:20:02 +00:00
DottaandPaperclip 84c2b30358 feat(apps): confirm connections and cache MCP actions
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-20 21:20:02 +00:00
DottaandPaperclip e6a7728087 feat(ui): add dedicated BYO apps page
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-20 21:20:02 +00:00
DottaandPaperclip 649a008dd7 fix(apps): space MCP key prompt
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-20 21:20:02 +00:00
DottaandPaperclip e598c41771 fix(apps): preserve MCP follow-ups after master sync
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-20 21:20:02 +00:00
DottaandPaperclip 51e5195135 fix(workspaces): bind commands to configured runtime rows
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-20 21:20:02 +00:00
DottaandPaperclip 142bcdd1c7 fix(workspaces): restore managed runtime origin hint
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-20 21:20:02 +00:00
DottaandPaperclip 3d3a12a3e2 fix(workspaces): keep inventory reads DB-only
Backport the existing master guard so this long-lived feature worktree does not fan out Git scans across its cloned workspace inventory.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-20 21:20:02 +00:00
Dotta b4b59388e5 fix(auth): support managed loopback session cookies 2026-08-20 21:20:02 +00:00
DottaandPaperclip a5e12a6d8b fix(smoke): harden PostHog live self-test
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-20 21:20:02 +00:00
DottaandPaperclip a499410c90 fix(apps): align MCP connection copy
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-20 21:20:02 +00:00
DottaandPaperclip 4bc7044396 fix(apps): expose gateways at legacy route
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-20 21:20:02 +00:00
DottaandPaperclip 5c77534fd5 fix(apps): recognize draft connections in Browse
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-20 21:20:02 +00:00
DottaandPaperclip faaa0ce428 fix(apps): complete MCP connection follow-ups
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-20 21:20:02 +00:00
DottaandPaperclip ab5e821d6a feat(apps): improve gateway management UX
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-20 21:20:02 +00:00
DottaandPaperclip 7d758d2963 feat(apps): make connections account-aware
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-20 21:20:02 +00:00
DottaandPaperclip ca94577206 feat: enable MCP tools by default
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-20 21:20:02 +00:00
DottaandPaperclip 98f032f9a9 feat(smoke): automate Notion email verification
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-20 21:20:02 +00:00
DottaandPaperclip 0b2759fcca fix(smoke): use scoped PostHog live bindings
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-20 21:20:02 +00:00
DottaandPaperclip eb962ef283 fix(smoke): harden generic Notion live flow
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-20 21:20:02 +00:00
DottaandPaperclip 699f009275 feat(apps): support pasted MCP OAuth continuation
Add a credential-safe opt-in generic Notion live smoke and focused regression coverage.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-20 21:20:02 +00:00
DottaandPaperclip bca6c0e991 test(smoke): add PostHog MCP live harness
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-20 21:20:02 +00:00
DottaandPaperclip 18d160d66a fix(oauth): fall back from private CIMD origins
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-20 21:20:02 +00:00
DottaandPaperclip b0515d65a2 fix(security): guard connection token brokers
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-20 21:20:02 +00:00
DottaandPaperclip bfc8fd81af fix(oauth): return failed callbacks to setup
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-20 21:20:02 +00:00
DottaandClaude Opus 5 1d000bc903 fix(security): make removing an app revoke its credentials
Removing an app only flipped the connection to `archived`. Everything that
made it work stayed behind: the operator's pasted key or OAuth tokens as live
secrets, the `company_secret_bindings` that project them into runtimes, the
install, the app-managed `app:<connectionId>` profile, gateway tokens minted
against that profile, and any authorization still in flight. Reconnecting
reused the retained credential, so "remove" never actually revoked anything.

Removal is now a teardown, and the order is the security property. Every
database-side access path closes first — grants revoked, bindings and installs
deleted, the app profile removed (archived instead when a gateway's RESTRICT
foreign key forbids deleting it, with that gateway's tokens and sessions
revoked), live local runtimes stopped, catalog entries marked `removed`,
outstanding OAuth state discarded, the connection and its application archived
— so the app is undispatchable before the first call out to a secret provider.
Only then are the credentials revoked, and each secret's ref survives until
that secret is gone: a provider that errors leaves the operation failed closed
and resumable, because the secret row is marked deleted before the provider
call and the surviving refs are what a retry reads to finish the job.

A secret is destroyed only when two independent tests agree: it was minted
into the `tool_app.` namespace by connect/reconnect/OAuth, and nothing outside
this connection references it. Anything ambiguous keeps its row and is
reported as retained — deleting a credential another target still resolves is
an outage, retaining an unreachable row is not.

What stays is deliberate. Connection and application identity, names and
activity survive so a reconnect reuses them, but with no credential, install
or profile, reconnecting has to ask for fresh authentication and rebuild
access. The activity receipt carries cleanup counts only; six of those count
keys name what they counted ("secrets", "token"), so `sanitizeRecord` lets
them through while the value is a finite number and still blanks the same key
carrying anything else.

Refs PAP-17119, PAP-17118, PAP-17089 (QA finding F6).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 21:20:02 +00:00
DottaandPaperclip ba603abace fix(apps): guide generic MCP connection failures
Map OAuth origin, duplicate-name, and DNS failures to stable operator guidance, and derive collision-resistant defaults from host, port, and path.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-20 21:20:02 +00:00
DottaandPaperclip 048c3578e3 fix(server): enforce MCP wizard action scope
Prevent connection installs from adding connection-wide allow entries, keep wizard-managed app profiles additive across binding precedence, and repair legacy over-grants with a data migration. Add decision-level regression coverage for generic and curated MCP paths.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-20 21:20:02 +00:00
DottaandPaperclip 28c0c279aa fix(ui): name MCP action switches
Preserve the unverified-server trust marker on URL-only connection details and compact the mobile action step above the bottom nav.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-20 21:20:02 +00:00
DottaandPaperclip 5634c2b966 fix(security): omit query strings from HTTP logs
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-20 21:20:02 +00:00
DottaandClaude Opus 5 f7023b205c fix(security): consume and actor-bind OAuth state on a denied callback
A denial or a cancel is a final answer to an authorization request, but
`completeOAuthCallback` treated it as merely a failed callback: the row
in `tool_oauth_states` survived, so the `state` the user just refused
stayed completable for the rest of its 10-minute TTL by anyone who could
produce a code. Any linked `request_confirmation` also stayed `pending`,
so the board kept offering an authorization link for a flow the user had
already declined and the requesting agent never learned the answer.

Route every terminal callback — success, denial, cancel — through one
`consumeOAuthState` helper. Two properties pull in opposite directions
and both matter:

- a stranger's callback must not *consume* the request, so the row is
  loaded and bound to the caller (expiry, actor type/id, session, or the
  subject user) before anything is deleted, and a failed binding check
  leaves the victim's flow live and completable;
- a replayed callback must not *complete* the request, so the delete is
  the single statement that decides ownership — `DELETE … RETURNING`
  hands the row to exactly one of two concurrent callbacks and the loser
  is told the state is spent rather than exchanging a code against it.

A denial then resolves any linked interaction as `rejected`, scoped to a
still-`pending` row so a board user who answered the card directly keeps
their own answer, with a Paperclip-authored reason rather than the
provider's.

The message and its `providerError` label are PAP-17108's, unchanged.

Five regression tests, both negative-controlled: a code obtained before
a denial can no longer be exchanged; an unrecognized cancel code behaves
the same; another session's or another user's denial is refused without
spending the request, which the owner then completes; the board's prompt
ends `rejected`; and two simultaneous callbacks produce exactly one
token exchange. That last one needs a second database handle — this
driver pipelines everything issued through one handle, which serializes
the two callbacks and makes a non-atomic delete pass — and it has to
connect before racing, or the first callback wins uncontested.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 21:20:02 +00:00
DottaandClaude Opus 5 fc9f6619b1 fix(security): redact upstream OAuth provider errors instead of reflecting them
A generic remote MCP connection points at an arbitrary authorization server, so
`error`, `error_description`, `error_uri` and the response body are all
attacker-chosen. Paperclip was returning the provider's `error_description`
verbatim as the public error message on the callback-denial, token-exchange and
dynamic-client-registration paths, and the callback did it before validating the
state at all. A hostile provider could therefore plant a canary secret, ANSI
escapes, or "paste your recovery key here" instructions into API responses,
board UI copy, and 4xx log lines — in Paperclip's own voice.

The operator now only ever reads text Paperclip authored:

- `normalizeOAuthProviderError` reduces the provider's `error` to a bounded,
  allowlisted label (RFC 6749 §4.1.2.1/§5.2, RFC 7591 §3.2.2, OIDC Core
  §3.1.2.6). An unrecognized code collapses to `unrecognized` rather than being
  echoed, since the label is still untrusted input.
- `oauthProviderErrorMessage` maps that label to stable Paperclip copy. The
  label plus the upstream HTTP status stay in structured `details` for
  diagnosis; the description and body are never read on any path.
- `error_description` is gone from the callback route and from the
  `completeOAuthCallback` input type, so there is nothing left to reflect
  through, and `redactSensitive` scrubs it from the request query that
  `customProps` copies into 4xx log lines.
- The callback validates the state binding and the actor before acting on a
  provider-reported failure, and consumes the state so a denial is terminal.

Refresh goes through the same token-exchange path, and revocation makes no
provider call, so both are covered. Tests plant a canary secret, ANSI escapes
and markdown in `error_description` and assert none of it reaches an API
response, a thrown message, a console line, an audit row or an activity detail;
both assertions were negative-controlled by restoring the reflection.

PAP-17108. Finding 3 of the PAP-17088 security review.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 21:20:02 +00:00