Commit Graph

1299 Commits

Author SHA1 Message Date
Nicky Leach ca92f727c5
ci: publish the cloud image in its own parallel job (#10408)
## Thinking Path

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

## Linked Issues or Issue Description

No public GitHub issue was found for this change.

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

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

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-07-28 15:38:01 -07:00
Nicky Leach db02ca7402
ci: keep in-flight docker builds from being cancelled (#10403)
## Thinking Path

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

## Linked Issues or Issue Description

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

---------

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

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

## Linked Issues or Issue Description

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

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

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

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

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

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

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

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

## Linked Issues or Issue Description

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

**Bug report**

### What happened

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

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

### Expected behavior

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

### Deployment mode

Self-hosted/local `codex_local` adapter.

## What Changed

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

## Verification

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

## Risks

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

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

## Model Used

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

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-07-28 16:15:08 -05:00
Devin Foley dc12197cce
fix: prevent duplicate built-in agents and self-heal reconciliation (#10223)
## Thinking Path

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

## Linked Issues or Issue Description

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

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

**What happened**

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

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

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

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

**Expected behavior**

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

**Steps to reproduce**

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

## What Changed

**Part 1 — stop creating duplicates**

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

**Part 2 — resilient reconciliation**

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

## Verification

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

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

## Risks

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

## Model Used

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

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

## Linked Issues or Issue Description

Fixes #10105

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

## What Changed

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

## Verification

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

## Risks

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

## Model Used

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

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes (no
doc surface changed)
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
2026-07-28 10:50:13 -07:00
dependabot[bot] f9034ab3ca
build(deps-dev): bump @types/node from 22.19.21 to 22.20.1 (#10304)
Bumps
[@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node)
from 22.19.21 to 22.20.1.
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node">compare
view</a></li>
</ul>
</details>
<br />

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


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

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

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

---

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

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


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-28 10:03:52 -07:00
Nicky Leach 7797995038
perf(plugin-daytona): opt-in no-profile fast path for default-PATH execs (#10352)
## Thinking Path

> - Paperclip is the control plane for autonomous AI companies
> - The Daytona adapter turns tasks into shell commands and manages
execution overhead
> - Many short-lived exec calls still pay for login-shell profile
sourcing even when the binary already resolves on the sandbox default
PATH
> - That extra startup work adds latency on the hot path for repeated
command execution
> - This pull request adds an opt-in fast path that skips profile
sourcing only when the caller explicitly requests it and the command
does not need shell initialization
> - The benefit is lower per-call latency for eligible commands without
changing the conservative default behavior for commands that need the
profile

## Linked Issues or Issue Description

This change does not reference a public GitHub issue. It follows the
same Daytona startup-speed work as merged PR #10335 and narrows the
execution path for eligible commands while keeping the default
login-shell behavior intact.

## What Changed

- Added an optional `noProfile` flag to
`PluginEnvironmentExecuteParams`.
- Refactored Daytona login-shell script assembly so the profile and nvm
sourcing block is omitted only on the explicit fast path.
- Preserved environment prefixing, `cd`, shell quoting,
`NONINTERACTIVE_GIT_ENV`, stdin handling, and `durationMs` behavior on
both paths.
- Added regression tests for the fast path omission, the preserved
execution parameters, and the default profile-sourcing path.

## Verification

- `pnpm --filter @paperclipai/sandbox-provider-daytona exec vitest run
src/plugin.test.ts`
- `pnpm --filter @paperclipai/plugin-sdk tsc --noEmit`
- Reverted the guard locally to confirm the two behavior tests fail
again, then restored the change.

## Risks

- If a caller opts into `noProfile` for a command that depends on shell
initialization, the command can fail to resolve its binary.
- The API comment and opt-in design keep that risk narrow; the default
path remains unchanged.

## Model Used

OpenAI GPT-5 (Codex tool-using coding agent)

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-07-27 21:47:05 -07:00
Dotta 1cd09ed555
perf(heartbeat): reuse task sessions for issue-scoped timer wakes and bound control-plane write retries (#10350)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents make progress in heartbeats: the server wakes an agent
session, it does a slice of work on an issue, records a disposition, and
exits
> - Benchmarking identical coding tasks run as Paperclip-orchestrated
agent pairs vs invoking the same agent harness directly measured a
1.8–2.2× wall-clock slowdown for the Paperclip pairs, dominated by
per-heartbeat orchestration overhead rather than model time
> - Two contributors stood out: (1) since PF-4 (#4838) every
`heartbeat_timer` wake starts a brand-new task session, so continuation
work on a specific issue repays the full session-start and
re-orientation cost on every heartbeat; (2) in degraded environments
agents burn many tool calls retrying the same failing control-plane
write before giving up
> - This pull request reuses the task session for issue-scoped timer
wakes (keeping the PF-4 fresh-session rule only for unscoped exploratory
wakes, which were the original context-bloat case) and adds a
bounded-retry rule to the wake prompt and core skill: after 2
consecutive failures of the same control-plane write, stop retrying it
for the rest of the heartbeat and rely on the adapter/runtime status
channel
> - The benefit is materially less wall-clock and token overhead per
heartbeat while preserving the context-bloat protection PF-4 was added
for

## Linked Issues or Issue Description

Refs #4838 (merged PF-4 change whose reset rule this refines), Refs
#5287, Refs #1907 (related timer-heartbeat session work).

No public GitHub issue exists for the slowdown itself; bug-report
fields:

- **What happened:** Agent pairs orchestrated through Paperclip
heartbeats complete identical task sets 1.8–2.2× slower (wall-clock)
than the same harness invoked directly. Profiling attributed the gap to
per-heartbeat orchestration overhead: every timer wake discards the task
session (full session start + re-orientation), and in degraded
environments agents repeatedly retry the same failing control-plane
write.
- **Expected behavior:** Heartbeat orchestration should add minimal
wall-clock overhead on top of the underlying harness; issue-scoped
continuation work should not pay a fresh-session tax each interval.
- **Steps to reproduce:** Run a fixed benchmark task set once through
Paperclip issue heartbeats and once via direct harness invocation with
the same model/config; compare wall-clock totals.
- **Version/commit:** master @ 3d23c3b2c3, self-hosted deployment.

## What Changed

- `server/src/services/heartbeat.ts`: `shouldResetTaskSessionForWake`
now resets only for `heartbeat_timer` wakes with no derivable task key
(unscoped exploratory wakes). Issue-scoped timer wakes reuse the issue's
task session. `describeSessionResetReason` updated to stay in exact
agreement.
- `server/src/__tests__/heartbeat-timer-wake-session-reset-pf4.test.ts`:
new cases for scoped vs unscoped timer wakes, plus the scoped case added
to the reset/reason agreement invariant.
- `packages/adapter-utils/src/server-utils.ts`: wake prompt template and
execution contract gain a bounded-retry rule — after 2 consecutive
failures of the same control-plane write, stop retrying it for the rest
of the heartbeat, continue useful work, report the failure in the final
response, and use the adapter/runtime status channel as the sanctioned
fallback.
- `packages/adapter-utils/src/server-utils.test.ts`: asserts the new
prompt lines are present in both the template and the rendered wake
prompt.
- `skills/paperclip/SKILL.md`: documents the same bounded write-retry
rule in the core Paperclip skill.

## Verification

- `node_modules/.bin/vitest run
packages/adapter-utils/src/server-utils.test.ts` — 1 file, 83 tests
passed
- `cd server && node_modules/.bin/vitest run
src/__tests__/heartbeat-timer-wake-session-reset-pf4.test.ts` — 1 file,
14 tests passed
- Both run on this branch rebased onto current master (3d23c3b2c3)

## Risks

- Behavioral shift: issue-scoped timer wakes now reuse sessions, so a
long-lived issue session can grow across heartbeats. Mitigated by
keeping the PF-4 reset for unscoped wakes (the originally observed bloat
case) and by existing session compaction.
- Prompt/skill text changes alter agent guidance; the new rule is scoped
narrowly to repeated failures of the same control-plane write.
- No migrations, no API or schema changes, no dependency changes.

## Model Used

- Claude (Anthropic) — `claude-fable-5` (Fable 5), extended reasoning
with tool use, driven via Claude Code / 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-07-27 22:01:29 -05:00
Devin Foley c274f10abc
feat(server): computed owner instance-admin elevation for cloud-managed instances, behind platform floors (#10343)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Cloud-managed instances authenticate tenant users through a
trusted-header path (`resolveCloudTenantActor`) that deliberately never
grants `instance_admin`, so every tenant user is company-scoped
> - On a dedicated (single-owner) managed instance that leaves the
paying owner unable to administer their own instance: instance settings,
the environments admin surface, and the custom sandbox image flow are
all instance-admin gated (the environments UI can't even show the
provider/image of the platform sandbox because the restricted read view
blanks `config` entirely)
> - Re-granting the old blanket `instance_user_roles` row would repeat
the mistake the shared-pool hardening fixed: DB rows go stale, resurrect
via restores, and elevate through every auth path
> - This pull request elevates only the stack `owner`, computed per
request at the trusted-header boundary behind a new managed-tier feature
flag, and ships that elevation together with code floors on the
platform-owned surfaces an instance admin must not control on a managed
instance
> - The benefit is that dedicated-stack owners can administer their own
instance while platform credentials, execution policy, backups, and
runtime code-install stay platform-owned, and self-hosted behavior is
unchanged

## Linked Issues or Issue Description

No public GitHub issue exists for this change; the underlying issue is
described here following the feature-request template.

### Problem or motivation

- On cloud-managed instances, tenant users resolved from trusted headers
are always company-scoped. For dedicated instances with a single paying
owner, the owner cannot reach any instance-admin surface of their own
instance (instance settings, environments administration, custom image
setup), and the restricted environment read view hides even structural
fields like the sandbox provider and image.
- The previous hardening intentionally removed blanket elevation (and
purges stale `instance_user_roles` rows on every trusted-header
authentication). That protection must not regress for shared
multi-tenant pools.

### Proposed solution

- Owner-only, computed, flag-gated elevation plus code floors on
platform-owned surfaces, in one PR so the elevation can never ship
without the floors.

### Alternatives considered

- Re-inserting an `instance_user_roles` row for owners (the
pre-hardening model): rejected — DB rows go stale, survive restores, and
elevate through every auth path; #7525 removed exactly this.
- Widening only the environments read view without any elevation:
rejected — it fixes one screen but still leaves a dedicated-instance
owner unable to administer instance settings or custom images.
- Elevating additional stack roles (`member`/`admin`/`support`):
rejected — only the owner has an ownership claim over the whole
instance; other roles stay company-scoped.

### Roadmap alignment

- Extends the shipped "Cloud deployments" roadmap work (multi-tenant
isolation, company-scoped cloud tenants, managed-instance bootstrap)
without overlapping planned core items, and leaves self-hosted behavior
unchanged.

## What Changed

- **New feature key** `enableOwnerInstanceAdmin` (`packages/shared`):
boolean flag in `instanceExperimentalSettingsSchema`, catalog tier
`managed`, `cloudDefault: true`, `selfHostedDefault: false`. Inert on
self-hosted instances — the elevation path only exists behind the cloud
tenant trust token.
- **Computed elevation** (`server/src/middleware/auth.ts`):
`resolveCloudTenantActor` now returns `isInstanceAdmin: true` only when
the trusted-header stack role is `owner` **and** the flag is enabled.
The flag is resolved through the instance-settings service so the
managed-config overlay applies (the control plane can disable elevation
fleet-wide without touching tenant databases; a DB row edit or restore
cannot resurrect it). Resolution fails closed on settings read errors.
The `instance_user_roles` never-insert and the per-request stale-row
purge are byte-identical. `member`/`admin`/`support` stack roles stay
company-scoped.
- **Authorization guard split**
(`server/src/services/authorization.ts`): the blanket-allow now trusts
the actor's *computed* `isInstanceAdmin` flag (only the attested
resolver can set it for `cloud_tenant` actors) while keeping the
`instance_user_roles` DB lookup excluded for `cloud_tenant` — a stale or
hand-inserted role row still elevates nothing.
- **Floor F1 — platform environment credentials**
(`server/src/routes/environments.ts`): on cloud-managed instances,
platform-provisioned environment rows (`managedByPaperclip` marker, plus
the legacy managed-Kubernetes marker) use a single floored view for
every reader on all environment routes (list, get, create, update,
delete responses): `envVars` are never echoed and credential-shaped
`config` keys (reusing the managed-config
`SECRET_LIKE_CONFIG_KEY_PATTERN`) are dropped — for **all** actors
including instance admins — while structural config (provider, image,
template, region, …) and the managed markers stay visible. This also
fixes the environments UI for managed sandboxes, which previously lost
the provider/image entirely in the restricted view. The floor also
covers writes: `PATCH /environments/:id` and `DELETE /environments/:id`
on a platform-provisioned row are rejected (403,
`environment_platform_managed`) for every actor including instance
admins, and the guard binds to the persisted row's markers so a patch
cannot strip the managed marker to lift the floor. The one recovery path
is a metadata-only PATCH that solely clears the marker keys
(null/false), for rows stamped through the old unrestricted API before
the markers became reserved — and it never applies to a row whose slot
markers are live platform state: the single local row
(`environments_local_driver_idx`), which `ensureLocalEnvironment` adopts
and stamps on cloud-managed instances from every caller (company
creation, the heartbeat, run orchestration), and the single marked
sandbox row (`environments_managed_sandbox_idx`) while a managed-sandbox
bootstrap path is configured (managed-config `environments` section or
`PAPERCLIP_EXECUTION_MODE=kubernetes`) and the provisioner therefore
adopts and refreshes it on every boot. Clearing a live slot row's
markers would let the next write reclassify it as tenant-managed and
bypass the floor; conversely, when no sandbox provisioning path is
configured the platform holds no claim on any sandbox row, so a platform
marker there is stale by definition and the recovery patch applies.
Every marker outside a live slot is clearable, so no legacy row is ever
locked permanently. Custom-image setup and probes on the platform
sandbox stay available to instance admins — those are the owner-facing
flows this elevation exists for. The marker keys themselves are
reserved: client create/update payloads that set `managedByPaperclip` or
`managedKubernetesSandbox` are rejected (422,
`environment_platform_marker_reserved`) on cloud-managed instances, so a
tenant row can never be stamped platform-provisioned through the API and
self-locked behind the write floor (the provisioner writes markers at
the service layer, not through these routes). Tenant-created
environments are otherwise unaffected.
- **Floor F2 — executionMode**
(`server/src/routes/instance-settings.ts`): on cloud-managed instances,
`PATCH /instance/settings/general` rejects writes that would change
`executionMode` (403, `execution_mode_platform_managed`). Same-value
echoes pass so settings forms that submit the full general-settings
object keep working. The boot-time execution-policy bootstrap path is
untouched (it calls the service directly).
- **Floor F3 — manual database backups**
(`server/src/routes/instance-database-backups.ts`): floored off on
cloud-managed instances (403, `database_backups_platform_managed`);
backups are platform-owned there, and the result would also echo a
server-side filesystem path.
- **Floor F4 — adapter code install** (`server/src/routes/adapters.ts`):
`POST /adapters/install` and `POST /adapters/:type/reinstall` are
floored off on cloud-managed instances (403,
`adapter_install_platform_managed`). Adapter packages execute in the
server process, so a runtime install would let an instance admin read
the platform trust anchors out of the process environment. This mirrors
the existing bundled-only plugin install floor; adapter code on managed
instances comes bundled with the platform image.

## Instance-admin surface audit

Before widening who can hold `isInstanceAdmin`, every
instance-admin-gated surface in `server/src` was enumerated and reviewed
for whether its response or side effects could echo process environment
values or platform credentials (tenant trust token, JWT signing keys,
database connection strings, provider API keys): 29 distinct gate
definitions covering ~90+ call sites, in four groups — sole
instance-admin gates (12), instance-admin-or-company-permission gates
(10), response-shaping/scope-widening sites (6), and the central
`allow_instance_admin` short-circuit in the authorization service (58
`decide()` call sites).

Findings and dispositions:
- **Environment read/write responses** exposed platform sandbox
`envVars`/credential-shaped config to instance admins → closed by floor
F1.
- **Manual backup trigger** echoed a server filesystem path and triggers
a platform-owned operation → closed by floor F3.
- **Adapter install/reinstall** loads externally fetched code into the
server process (indirect, complete env exposure) → closed by floor F4.
The sibling plugin-install path already had a bundled-only floor on
managed instances and needed no change.
- **Token-minting surfaces** (gateway tokens, custom-image
terminal/connection tokens) mint credentials scoped to the instance's
own resources, not platform trust anchors → acceptable for an
owner-admin of a dedicated instance; unchanged.
- All remaining gated surfaces return ordinary instance-scoped business
data; none echo `process.env` or platform secrets directly. OAuth client
secrets are referenced by env-var *name* only; SSH private keys are
stored as secret refs before persistence and are not echoed.

Operational note for managed platforms: this model assumes the process
environment of a managed instance holds only that instance's own
credentials. Platform operators should keep provider credentials
per-instance (never fleet-shared) since an instance admin ultimately
controls in-process code on their own instance.

## Verification

- `pnpm vitest run server/src/middleware/cloud-tenant-actor.test.ts` —
resolver matrix: owner × flag on/off, flag via managed overlay
(on-over-DB-off and off-over-DB-on), member/admin/support × flag on,
no-token self-hosted, fail-closed settings read, purge still runs and no
role row is ever inserted (14 tests).
- `pnpm vitest run server/src/__tests__/authorization-service.test.ts` —
computed flag elevates a `cloud_tenant` actor; a stale
`instance_user_roles` row still never does; `session` actors unchanged
(full suite, embedded Postgres).
- `pnpm vitest run server/src/__tests__/environment-routes.test.ts` —
F1: no secret echo to admins on get/list, structural config visible to
restricted readers, platform-row PATCH/DELETE rejected for admins
(including a marker-stripping patch), marker-clear recovery allowed for
stale legacy rows and for a marked sandbox row when no provisioning path
is configured, but refused on the managed local row and on the sandbox
slot row under a managed-config `environments` entry or the forced
kubernetes execution mode, client marker-stamping creates/patches
rejected, tenant rows still readable and writable, self-hosted
read+write regression (60 tests).
- `pnpm vitest run server/src/__tests__/environment-service.test.ts` —
`ensureLocalEnvironment` adopts a pre-existing local row on
cloud-managed instances (marker stamped, other metadata preserved,
idempotent — no rewrite on re-ensure) and leaves self-hosted rows
untouched (22 tests, embedded Postgres).
- `pnpm vitest run server/src/__tests__/instance-settings-routes.test.ts
server/src/__tests__/instance-database-backups-routes.test.ts` — F2
change-vs-echo matrix incl. self-hosted regression; F3 floor for both
admin shapes (32 tests).
- `pnpm vitest run server/src/__tests__/adapter-routes-authz.test.ts` —
F4 floor; self-hosted install/reinstall behavior unchanged (existing
cases).
- `pnpm vitest run server/src/__tests__/first-admin-claim.test.ts
server/src/__tests__/bootstrap-claim-routes.test.ts
server/src/__tests__/managed-config.test.ts
server/src/__tests__/health.test.ts
server/src/__tests__/instance-settings-managed-overlay.test.ts
server/src/services/managed-environments.test.ts
server/src/services/execution-policy-bootstrap.test.ts` — first-admin
bootstrap gate and managed-config behavior unchanged (91 tests).
- `pnpm vitest run packages/shared/src/feature-catalog.test.ts` —
catalog/schema sync tests cover the new key (selfHostedDefault must
equal the schema default).
- `pnpm run typecheck` — all 31 workspace projects clean.

## Risks

- Self-hosted behavior is unchanged: every floor binds to
`isCloudManagedInstance()` (tenant trust token present), the new flag
defaults off with no elevation path, and regression tests pin the
self-hosted branches.
- The elevation is fail-closed and stateless: turning the flag off
(managed overlay or DB) de-elevates on the next request; there is no
role row to clean up and restores cannot resurrect elevation.
- On a cloud-managed instance a pre-existing unmarked local row is
adopted (stamped `managedByPaperclip`) by the next ensure and becomes
platform-owned — the intended managed-product semantic: the platform
owns the single local slot. Self-hosted instances are untouched.
- F1 widens restricted readers' view of platform-provisioned rows from
fully blanked `config`/`metadata` to structural-only `config` plus
markers. Platform-delivered config is guaranteed secret-free by the
managed-config contract (secret-shaped keys fail startup), and the floor
re-drops secret-shaped keys defensively.
- One extra instance-settings read per trusted-header request for
owner-role actors (the resolver already performs several queries per
request).

## Model Used

Claude Fable 5 (Anthropic) — model id `claude-fable-5`, extended
thinking enabled, agentic tool use via Claude Code; read-only explore
subagents on the same model were used for the surface audit sweep.

## 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-07-27 18:58:31 -07:00
Dotta c3bd0c5d50
feat(skills): add beta releases for the core Paperclip skill (#10228)
## Thinking Path

> - Paperclip is the open source control plane people use to organize
and operate AI-agent companies.
> - Agent behavior depends partly on the bundled Paperclip core skill
synchronized into each runtime.
> - The existing database and runtime plumbing already supports
immutable skill-version snapshots and per-agent version selections, but
no product workflow exposed that capability.
> - Replacing the live bundled skill globally would make champion
adoption risky and difficult to compare across agents.
> - This pull request adds an experimental, instance-level beta-skills
gate plus a repository release registry, immutable seeded releases,
enforcement, and a per-agent release picker.
> - The benefit is controlled per-agent evaluation of frozen core-skill
releases while the default-off path remains behaviorally unchanged.

## Linked Issues or Issue Description

### Subsystem affected

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

### Problem or motivation

Paperclip needs a safe way to evaluate improved versions of its core
operating skill without globally replacing the live default. Today the
version-snapshot and per-agent pin plumbing exists, but operators cannot
use it. A global replacement would make regressions difficult to contain
and would prevent controlled comparisons across agents.

### Proposed solution

Add a default-off instance experiment that exposes immutable, named
core-skill releases. When enabled, operators can pin each agent to a
seeded release; when disabled, every agent resolves the live default
while saved pins remain intact. Validate pinned writes at the API
boundary, gate reads at runtime, and expose the selection in the agent
Skills tab.

### Alternatives considered

- **Replace the bundled core skill globally:** rejected because it
changes every agent at once and provides no rollback/isolation boundary.
- **Ship releases as separate skills:** rejected because releases are
versions of one core capability, not independently enabled skills.
- **Store release snapshots only outside the repository:** rejected
because repository provenance and hashes make builds reproducible and
reviewable.

### Roadmap alignment

This extends the Skills Manager / Skill Studio direction in `ROADMAP.md`
by making core-skill versions operable per agent. It does not duplicate
another open implementation PR; GitHub searches found no related
`enableBetaSkills` change.

### Additional context

The feature remains experimental and default off. The V7 champion was
selected through a multi-model evaluation process, and the frozen
release contents are verified by SHA-256 below.

## What Changed

- Added the default-off instance-level `enableBetaSkills` experimental
flag.
- Added `skills-releases/paperclip/` with the ordered release registry
and frozen `v0` plus `v7-roster` snapshots.
- Added release metadata to `company_skill_versions` and idempotent
release seeding. The migration was planned as `0191`, then renumbered to
`0192` because current `master` claimed `0191` before final rebase.
- Added read-time gating and write-time validation so disabled instances
always resolve the live default and reject pinned-version writes.
- Added the per-agent Release picker in the agent Skills tab, including
responsive layout and beta-pin state.
- Kept `EDITS.md` out of the release registry and PR diff.

### V7 Adoption Evidence

- Paid roster: 6 models, 94-case suite.
- Result: 553/564 pass-within-2, mean 92.17/94, versus the P2 baseline
of 544/564.
- Reference model improved 84→91; maximin improved 84→90.
- Final report:
https://pages.paperclip.ing/skills/optimization/paperclip/pap-14624-p3-final-20260721/

### Provenance

- `v7-roster` is the Phase 1 champion plus additions-only edits
E107–E112. Per-edit rationale remains in the evals repository at
`source/v7-roster/EDITS.md` and is deliberately excluded from this PR.
- `v0` is the `skills/paperclip` tree from commit `ea66ea81`.
- Champion selection was accepted on July 21, 2026 via board card
`9c304fc2` (PAP-14624 G3).
- This delivery mechanism was accepted on July 24, 2026 via plan
revision `2367abd2` (PAP-14858).

### QA Evidence

- P4 QA matrix comment `b7f40522-4e9b-4a3a-9821-28e86fe1a987`: all 6
acceptance criteria passed.
- Automated QA matrix: 166 tests passed with 0 failures, including real
filesystem materialization and full SHA-256 assertions.
- UI QA exercised the real agent Skills tab at desktop and mobile widths
with the experimental flag both on and off.

## Verification

- `pnpm check:token-gates`
- Focused beta-release matrix: 169 tests passed across shared
validators, server services/routes/heartbeat behavior, instance settings
UI, and release picker UI.
- `pnpm -r typecheck`
- `pnpm build`
- `pnpm test:run`: server and UI partitions passed; one CLI doctor test
inherited temporary AWS credentials from the agent heartbeat and
expected no static credentials. The isolated rerun with
`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and `AWS_SESSION_TOKEN`
unset passed 8/8.
- V7 SHA-256:
- `SKILL.md`:
`53ab290489684cbf116fdd1406a95f6b6f53c9c36358b1bf8bfeae481e253575`
- `references/cases.md`:
`3b821f59064a7761091020a14819a8d787131f24029748563d6c0e1be7e6eaec`
- `references/workflows.md`:
`69747bd6e05f7e3673d1e67b07ff295df1869c05e1fd029804d5fa9177db92cd`
- Confirmed 49 changed files, no `pnpm-lock.yaml`, no workflow changes,
and no `EDITS.md`.

## Risks

- **Migration:** low-to-moderate risk. Three nullable columns and one
partial unique index are added idempotently; existing rows remain valid.
- **Behavior:** low risk while the flag is off because read-time
resolution forces the live default and saved pins are preserved but
inactive.
- **Frozen content:** release snapshots intentionally diverge from
future live skill edits; provenance and hashes make that divergence
explicit and reproducible.
- **UI:** low risk. The picker only renders for the bundled core skill
when the experimental flag is enabled and seeded releases exist.

> This extends the existing Skills Manager / Skill Studio direction
described in `ROADMAP.md`; it does not duplicate another open
implementation PR. The GitHub PR search found no related
`enableBetaSkills` change.

## Model Used

- OpenAI Codex using `gpt-5.5` with reasoning and
terminal/code-execution tools; context-window size is not exposed by
this runtime. Earlier implementation commits also record Claude Opus 4.8
assistance where applicable.

## 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
- [ ] I have not referenced internal/instance-local Paperclip issues or
links (required governance identifiers are included above; no internal
URL is included)
- [ ] My branch name describes the change and contains no internal
Paperclip ticket id (the approved delivery plan mandated this shared
branch name)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-07-27 19:45:59 -05:00
Nicky Leach 030dd9d15c
feat(adapter-utils): provider-delegable syncIn seam with ordered post-upload commands (#10340)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The adapter/runtime layer has to move files into sandboxes safely
and efficiently
> - The current sync-in path needs a provider-delegable seam so
providers can use their native upload transport when available
> - The upload contract also needs ordered post-upload commands so
extracted content can be finalized fail-fast after transfer
> - The fallback path still has to preserve current behavior when the
provider does not expose native sync verbs
> - This pull request adds the contract and runtime seam for
provider-delegable sync-in, plus the single-stream collapse flag
> - The benefit is fewer round trips, a cleaner provider-owned upload
path, and a compatible fallback for existing runners

## Linked Issues or Issue Description

No public GitHub issue exists for this change. The underlying feature
request is described below in the repository's feature-request format.

### Problem or motivation

Paperclip needs a sync-in path that lets each provider choose the best
available upload transport instead of forcing the harness to orchestrate
uploads the same way every time. The runtime also needs a way to
describe ordered post-upload commands so providers can finalize
extracted content fail-fast after transfer.

### Proposed solution

Extend the sync contract with ordered post-upload commands, forward that
contract through the plugin and environment runtime layers, and make
client syncIn always available. When a provider advertises native sync
verbs, the client should delegate to that transport; otherwise it should
fall back to the existing tarball/write/extract behavior and then run
the post-upload commands in order.

### Alternatives considered

Keeping upload orchestration entirely host-side would avoid a contract
change, but it would block provider-specific transport optimizations and
keep the harness responsible for a path the provider can do more
efficiently. A separate post-upload API would add another surface
without improving the existing sync flow.

### Roadmap alignment

This work aligns with the broader runtime and adapter roadmap because it
improves provider integration without changing the external product
model. It is an additive contract change that preserves backward
compatibility for providers that do not expose native sync verbs.

### Additional context

The fallback path still needs to preserve existing observable behavior,
including command ordering, cwd confinement, and fail-fast execution.
The single-stream progress flag is part of the same transport
improvement so smaller writes can collapse to a single round trip when
the runner supports it.

## What Changed

- Added ordered `postUploadCommands` support to the sync operation
contract and SDK mirror.
- Plumbed the sync-in contract through the plugin and environment
runtime layers.
- Implemented a runtime client `syncIn` path that delegates to native
provider transport when available, otherwise uses the generic
tarball/write/extract fallback.
- Preserved fail-fast execution of ordered post-upload commands in the
fallback path.
- Flipped the sandbox runner's single-stream stdin progress flag to
collapse small `writeFile` operations to a single round trip.
- Added and updated tests for contract forwarding, fallback behavior,
cwd rejection, fail-fast behavior, and single-stream collapse.

## Verification

- `pnpm --filter @paperclipai/plugin-sdk exec vitest run
protocol.postupload.test.ts`
- `pnpm --filter @paperclipai/plugin-sdk exec vitest run
environment-sync-negotiation.test.ts`
- `pnpm --filter @paperclipai/adapter-utils exec vitest run
command-managed-runtime.test.ts`
- `pnpm --filter @paperclipai/server exec vitest run
environment-execution-target.test.ts`
- Local typecheck and targeted suite runs reported in the handoff passed
before PR creation.

## Risks

- The new fallback path could diverge from the previous inline upload
behavior if the tarball/extract contract changes.
- Provider-native sync handling may expose provider-specific edge cases
if a runner advertises sync verbs but does not fully honor the contract.
- The single-stream flag changes transport behavior for small uploads,
so regressions would likely show up as round-trip or upload failures.

## Model Used

OpenAI Codex (GPT-5, tool-using coding agent).

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-07-27 17:19:57 -07:00
Dotta c111ee4cb3
feat(server): add per-user document stars (#9952)
## Thinking Path

> - Paperclip is the open source control plane people use to manage
AI-agent companies and their work.
> - Artifacts and documents are first-class outputs, but users need a
personal way to keep important documents easy to find.
> - Existing resource memberships already model per-user starred
projects and agents with company scoping and activity logging.
> - Documents lacked the equivalent membership model, route, and
artifact filtering behavior.
> - The shared membership contract also needs to remain safe for
existing UI project/agent mutation helpers when documents become a
recognized resource type.
> - This pull request extends the existing resource-membership system
with per-user document stars and a starred artifacts view.
> - The benefit is a company-scoped, idempotent server foundation for a
dedicated starred-documents experience without weakening authorization
or artifact filtering semantics.

## Linked Issues or Issue Description

### Problem / Motivation

Board users cannot star individual documents, and the company artifacts
API cannot return only the current user's starred documents.

### Proposed Solution

Add company/user-scoped document memberships, a board-only document star
route, document membership data in the shared contract, and a
`starred=true` artifacts filter.

### Alternatives Considered

A document column was rejected because stars are per-user; a separate
star API was rejected because projects and agents already use resource
memberships.

### Roadmap Alignment

This extends the existing Artifacts & Work Products roadmap area and
does not duplicate another open pull request found in the repository
search.

## What Changed

- Added the `document_memberships` schema and migration with
company/user/document uniqueness and starred ordering.
- Extended shared resource-membership and artifact-query contracts for
documents and `starred=true`.
- Added company-scoped document star/unstar service and board-only route
behavior with activity logging.
- Added starred document artifact filtering, including user-authored
documents, document kinds, cursor ordering, and incompatible-kind
handling.
- Preserved idempotency under concurrent star requests and synchronized
UI membership defaults/helpers with the expanded contract.
- Added focused shared, route, service, and UI regression coverage.

## Verification

- `pnpm exec vitest run packages/shared/src/resource-memberships.test.ts
server/src/__tests__/company-artifacts-service.test.ts
server/src/__tests__/resource-memberships-routes.test.ts`
- `pnpm exec vitest run ui/src/components/SidebarAgents.test.tsx
ui/src/components/SidebarProjects.test.tsx
ui/src/components/SidebarStarredProjects.test.tsx`
- `pnpm --filter @paperclipai/db check:migrations`
- `pnpm -r typecheck`
- `pnpm test:run`
- `pnpm build`

## Risks

- The migration adds a new membership table and non-concurrent indexes;
migration safety gates pass with the repository's established policy.
- The starred artifacts query intentionally returns only documents and
relaxes the normal agent-authored/system-kind predicates for documents
the current user explicitly starred.
- Document membership mutations remain board-user-only; agent callers
receive no document-star capability.

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

## Model Used

- OpenAI Codex CLI; runtime model ID and context-window size were not
exposed to this session. Reasoning, repository tool use, code execution,
and test 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-07-27 19:13:35 -05:00
Dotta 1426494ab8
fix(agents): disable cheap model profiles by default (#10019)
## Thinking Path

> - Paperclip is the control plane people use to create and govern
AI-agent companies
> - Agent creation persists runtime configuration that controls which
model profiles future runs may select
> - Adapters can expose a `cheap` profile, and existing creation paths
implicitly left that profile available when operators made no choice
> - That made a newly created agent eligible for a lower-cost model
without an explicit operator opt-in
> - The UI also dropped an explicit opt-in when the operator selected
the adapter's default cheap model rather than a custom model ID
> - Codex additionally hardcoded `gpt-5.3-codex-spark` into its cheap
profile and static fallback model list, making Paperclip choose an
auth-dependent model rather than requiring an operator choice
> - This pull request makes new-agent creation disable an available
cheap profile by default while preserving explicit opt-in from the UI or
API
> - The Codex cheap profile now remains available for explicit
configuration but supplies no model default, so an unconfigured cheap
request stays on the primary model
> - The benefit is predictable model quality for new agents and an
intentional, auditable choice before lower-cost routing is enabled

## Linked Issues or Issue Description

**Problem**

New agents created with an adapter that exposes a `cheap` model profile
can inherit that profile without the operator explicitly enabling it. In
the UI, enabling the adapter-default cheap model is also omitted because
runtime configuration is only written when a custom model ID is present.

**Expected behavior**

- New agents default an available `cheap` model profile to `{ enabled:
false }` when the caller does not specify it.
- Explicit API configuration remains authoritative.
- UI opt-in persists even when the adapter default model is used.
- Codex does not advertise or automatically select
`gpt-5.3-codex-spark`; operators must explicitly configure any
lower-cost Codex model.

**Related public work**

- Refs #4881, which introduced cheap model profiles for local adapters.
- Supersedes the default-selection portions of #8032 and #10004 by
removing the Codex model default instead of replacing it with another
hardcoded model.

## What Changed

- Detect whether the selected adapter exposes a `cheap` model profile
during agent creation and hiring.
- Persist `runtimeConfig.modelProfiles.cheap.enabled = false` only when
the caller did not explicitly configure the profile.
- Preserve UI cheap-profile opt-in when using the adapter's default
model by writing an empty adapter config.
- Remove `gpt-5.3-codex-spark` from the Codex static model list.
- Keep the Codex `cheap` profile explicitly configurable while giving it
an empty adapter config, so Paperclip never chooses a cheap Codex model
automatically.
- Verify that a Codex cheap request without an explicit model leaves the
primary model unchanged.
- Extend server route and UI runtime-config tests for default-disable
and explicit-opt-in behavior.

## Verification

- `env -u PAPERCLIP_IN_WORKTREE -u PAPERCLIP_WORKTREE_NAME -u
PAPERCLIP_CONFIG -u PAPERCLIP_HOME -u PAPERCLIP_INSTANCE_ID -u
PAPERCLIP_CONTEXT pnpm exec vitest run
packages/adapters/codex-local/src/index.test.ts
packages/adapters/codex-local/src/server/codex-args.test.ts
server/src/__tests__/adapter-models.test.ts
server/src/__tests__/adapter-registry.test.ts
server/src/__tests__/heartbeat-model-profile.test.ts
server/src/__tests__/agent-permissions-routes.test.ts
ui/src/lib/new-agent-runtime-config.test.ts`
- Result: 7 test files passed, 105 tests passed.
- GitHub `Typecheck + Release Registry` check passed on the final head.
- `git diff --check public-gh/master...HEAD`

## Risks

- Low behavioral risk: only newly created or hired agents are
normalized; existing agents are unchanged.
- Explicit `cheap` profile settings remain untouched, including explicit
opt-in.
- Codex users who explicitly opt into the cheap lane must choose a
model; requests without a configured override intentionally continue on
the primary model.
- Adapter profile discovery is now awaited during creation, adding a
small amount of adapter metadata lookup work.
- The source branch name is automation-provided and retained as required
by the task, so it does not satisfy the preferred public branch naming
convention.

> 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.4` via Codex CLI, with reasoning, repository editing,
terminal execution, and GitHub/Paperclip tool access. 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)
- [ ] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-07-27 19:11:51 -05:00
Dotta 0cf64d36a5
feat(secrets): write through external values and deep-link details (#10196)
## Thinking Path

> - Paperclip is the open source control plane people use to manage
AI-agent companies.
> - Its secrets subsystem can resolve external references such as AWS
Secrets Manager values without copying those values into Paperclip
custody.
> - Operators also need to rotate a referenced secret's value while
preserving the same provider reference for consumers inside and outside
Paperclip.
> - Previously, external-reference rotation could only retarget
metadata, and secret detail sheets were driven by local component state
rather than shareable navigation state.
> - This pull request adds an optional provider write capability,
implements AWS Secrets Manager write-through rotation, and exposes
capability-aware rotate modes in the UI.
> - It also makes secret and each-user definition detail sheets
URL-driven and adds a copy-link action.
> - The benefit is that operators can update the canonical external
value safely while keeping AWS rotation tracking intact, and they can
share or navigate directly to secret details.

## Linked Issues or Issue Description

### Subsystem affected

Cross-cutting (`server/`, `ui/`, and `packages/shared`).

### Problem or motivation

External-reference secrets can follow a provider-managed value, but
Paperclip could not write a replacement value back to providers that
support it. Operators had to leave Paperclip, update the value
separately, and then return without an auditable Paperclip rotation
record. Secret detail sheets also could not be shared or restored
through browser history because their selection lived only in component
state.

### Proposed solution

Add an optional `updateExternalSecretValue` provider capability and
surface it as `supportsExternalValueWrites`. Implement AWS writes with
`PutSecretValue` while leaving the resolution `versionId` unset so
future reads continue following `AWSCURRENT`. Add write-value and
retarget modes to the rotate dialog for capable providers. Drive secret
detail selection from `?secret=` / `?definition=` query parameters and
provide a copy-link action.

### Alternatives considered

Converting an external reference into a Paperclip-managed secret would
break consumers that depend on the existing provider reference. Pinning
reads to the newly written AWS version would prevent later out-of-band
rotations from flowing through. Keeping sheet selection only in React
state would not support browser Back or shareable links.

### Roadmap alignment

This extends the completed “Secrets Manager with per-agent access”
roadmap capability; it does not duplicate a separate planned roadmap
item. Public GitHub searches found no duplicate or closely related issue
or PR.

### Additional context

The PR includes focused provider, service, and UI render coverage.
Cutter also generated previews for the deep-linked detail sheet and
capability-aware rotate modes.

## What Changed

- Added optional external-value write support to the secret provider
contract and provider descriptors.
- Implemented AWS Secrets Manager write-through with `PutSecretValue`,
audit material, and compensation when persistence fails after the
provider write.
- Allowed `secretService.rotate()` value updates for external references
while rejecting ambiguous value-plus-retarget combinations and
unsupported providers.
- Added capability-aware “Write new value” and “Change reference” rotate
modes with updated custody and action copy.
- Made secret and each-user definition detail sheets source their
selection from URL query parameters, compose with folder paths, close
through browser history, and expose a copy-link action.
- Added provider, service, and UI render coverage for write-through,
rollback, capability messaging, dialog modes, and deep links.

## Verification

- `pnpm vitest run
server/src/__tests__/aws-secrets-manager-provider.test.ts` — 18 passed.
- `pnpm vitest run server/src/__tests__/secrets-service.test.ts` — 75
passed.
- `pnpm vitest run ui/src/pages/Secrets.render.test.tsx` — 31 passed.
- `pnpm --filter @paperclipai/shared typecheck` — passed.
- `pnpm --filter @paperclipai/server typecheck` — passed.
- `pnpm --filter @paperclipai/ui typecheck` — passed.
- `pnpm check:token-gates` — passed with all gates clean.

## Risks

- External value writes affect the canonical provider secret and
therefore all consumers of that AWS secret; the UI explicitly labels
this custody behavior.
- A provider write can succeed before Paperclip persistence fails. The
service records the written version and AWS support includes
compensation coverage to restore the prior value where possible;
unrecoverable failures return explicit audit-safe error details.
- URL-driven sheet state changes navigation behavior; render tests cover
deep links, Back/close behavior, and composition with folder query
state.
- No database migration or breaking API requirement is introduced;
providers without the optional capability retain reference-only
behavior.

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

## Model Used

- OpenAI Codex using exact model ID `gpt-5.6-sol`, high reasoning mode,
Codex CLI `0.142.5`, with repository, shell, Git, GitHub CLI, and
code-execution tools. The runtime did not expose a context-window size.
- Earlier implementation commits were assisted by Anthropic `Claude
Fable 5` as recorded in their commit trailers; the exact backend model
ID and context-window size were not preserved in the workspace metadata.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change and contains no internal
Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes, or
confirmed no documentation change is required
- [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: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-07-27 19:08:07 -05:00
Dotta f6ab82d490
feat(interactions): add interaction withdrawal and terminal-issue expiry (#10251)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents and boards coordinate through issue-thread interactions
(request_confirmation, ask_user_questions, suggest_tasks, …) that wait
as `pending` cards until someone resolves them
> - Two lifecycle gaps existed: an interaction's creator could not take
back a card it no longer stands behind, and interactions left `pending`
on issues that reached a terminal status lingered forever as
live-looking approval requests
> - Stale pending cards mislead humans (they look actionable), distort
attention/liveness signals, and in the worst case invite acting on a
proposal whose issue is already closed or cancelled
> - This pull request adds an explicit withdraw route for pending
interactions and automatically expires pending interactions when their
issue reaches a terminal status (including a catch-up sweep for issues
closed before this change)
> - The benefit is that interaction cards now faithfully reflect
reality: only genuinely actionable requests stay pending, and creators
can retract requests that events have overtaken

## Linked Issues or Issue Description

Fixes #5787
Refs #7403

Related prior PRs found while searching for duplicates (all overlap
partially; none combine both lifecycle paths or the route-level
authorization used here): #6709 and #7312 (creator-withdraw attempts),
#8169 (terminal expiry), #8081 and #5137 (generalized cancel/expire
endpoints), #6094 (stale confirmation auto-resolve). Related merged
context: #9568 (agent cancel for ask_user_questions), #10119 (tolerating
legacy `withdrawn_by_creator` result outcomes — the reader side of the
outcome this PR writes).

## What Changed

- New route `POST /issues/:id/interactions/:interactionId/withdraw` that
resolves a `pending` interaction to status `withdrawn` with a structured
result (`outcome: "withdrawn"`, optional trimmed `reason`), stamps
`resolvedBy*`/`resolvedAt`, touches the issue, logs activity, and emits
resolved-interaction telemetry
- Withdrawal authorization: board users, the interaction's creator
agent, or the issue's current assignee agent (assignees additionally
pass the standard issue-mutation gate); task-watchdog runs are
explicitly rejected, and authorization-boundary plus low-trust
control-plane checks apply
- Withdrawing an already-resolved interaction returns `409`;
unknown/cross-issue/cross-company interaction ids return `404`
- New service method `expirePendingInteractionsForTerminalIssue`: when
an issue transitions to a terminal status, all of its `pending`
interactions are resolved to `expired` with `outcome: "issue_closed"`,
guarded by a `status = 'pending'` predicate so concurrent resolutions
are not overwritten
- The same expiry runs as a catch-up when interactions are listed on an
already-terminal issue, so cards stranded by issues closed before this
change also get cleaned up; expired request_confirmations are logged
with a distinguishing source
- Shared package: new `withdrawIssueThreadInteractionSchema` validator,
`WithdrawIssueThreadInteraction` type, and `withdrawn` / `issue_closed`
result-outcome support for all interaction kinds (kind-aware result
shapes for `ask_user_questions` and `request_item_verdicts`)
- UI helper `ui/src/lib/issue-thread-interactions.ts` recognizes the new
outcomes for card rendering
- Docs: bundled skill API reference updated with the withdraw endpoint
- Review follow-up: terminal expiry moved from the HTTP route hooks into
`issueService.update`'s status-transition block, so direct service
callers (tree control, recovery, pipelines, status cards) expire pending
cards too; the list-endpoint catch-up remains for issues closed before
this change
- Review follow-up: withdrawing or issue-close-expiring a
`request_confirmation` also settles its linked `tool_action_requests`
row (withdraw -> `cancelled`, issue closed -> `expired`), so a parked
tool call cannot stay approvable after its card is gone
- Review follow-up: interaction cards render dedicated copy for the new
outcomes ("Withdrawn" with the reason, "Expired when issue closed")
instead of falling through to superseded-by-comment / stale-target
variants; withdrawn plan reviews badge as "Withdrawn" rather than
"Changes requested"

## Screenshots

Card states rendered from a local ux-lab harness with mocked data ([full
gallery](https://pages.paperclip.ing/pr-10251-interaction-withdrawal-cards/)):

| Light | Dark |
| --- | --- |
| ![All card states,
light](https://pages.paperclip.ing/pr-10251-interaction-withdrawal-cards/all-light.png)
| ![All card states,
dark](https://pages.paperclip.ing/pr-10251-interaction-withdrawal-cards/all-dark.png)
|

## Verification

- `pnpm --filter @paperclipai/shared build` — clean tsc
- `cd server && npx vitest run
src/__tests__/issue-thread-interaction-routes.test.ts` — 22 tests pass,
including new coverage for: creator-agent withdraw success,
non-creator/non-assignee agent 403, watchdog-run 403, double-withdraw
409, and board-user withdraw
- `cd server && npx vitest run
src/services/issue-thread-interactions.test.ts` — 4 tests pass,
including terminal-issue expiry writing `issue_closed` results and
leaving already-resolved interactions untouched
- `cd ui && pnpm typecheck` — clean
- `cd server && npx vitest run src/__tests__/issues-service.test.ts` —
includes a new embedded-Postgres test proving a direct
`issueService.update` terminal transition expires pending interactions
and writes the activity-log entry
- `cd ui && npx vitest run
src/components/IssueThreadInteractionCard.test.tsx` — 32 tests,
including new coverage for withdrawn / issue-closed confirmation and
question cards
- `cd server && npx tsc --noEmit` — matches the pre-existing repo error
baseline exactly (no new errors)
- Manual: `POST /issues/:id/interactions/:interactionId/withdraw` with
`{"reason":"superseded"}` as the creator agent resolves the card to
`withdrawn`; closing an issue with a pending confirmation flips it to
`expired` with `outcome: "issue_closed"`

## Risks

- Interactions on terminal issues now auto-expire (including
retroactively via the list-time catch-up), so consumers that expected to
resolve a pending interaction on a closed issue will get `409`; this is
the intended semantics and matches how the attention feed already wants
to treat dead cards
- New result outcomes (`withdrawn`, `issue_closed`) are written to
stored results; readers were already made tolerant of these outcome
strings in #10119, so mixed-version reads are safe
- No schema/migration changes; per-row conditional updates (`status =
'pending'`) avoid clobbering concurrent resolutions
- Withdrawal is a new mutation surface, but it is strictly narrower than
existing resolve paths (board, creator, or assignee only; watchdog runs
blocked)

## Model Used

Claude Fable 5 (`claude-fable-5`, Anthropic Mythos-class tier) with
extended thinking and agentic tool use (Claude Code harness); commit
authored in a Paperclip-managed engineering session.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-07-27 19:04:46 -05:00
Devin Foley 273315a4d0
feat(server): provision managed sandbox environments from the managed config (#10324)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Hosted/managed deployments configure instances entirely from the
control plane: `PAPERCLIP_MANAGED_CONFIG` already delivers feature flags
and `plugins.autoInstall` (bundled sandbox provider plugins), parsed
fail-closed at boot
> - A plugin alone is not usable for execution: runs need an
instance-level `driver: "sandbox"` environment row pointing at the
provider, and today only Kubernetes has a boot path for that
(`PAPERCLIP_EXECUTION_MODE` → `ensureKubernetesEnvironment`); every
other provider requires a manual product-API call the control plane
cannot make on a managed instance
> - Adding one `ensureXxxEnvironment` per provider would multiply
near-identical boot hooks and env-var surfaces
> - This pull request generalizes the existing Kubernetes machinery: the
managed-config document gains an optional `environments` section that
declares a sandbox environment for any bundled provider, ensured
idempotently at boot by a provider-agnostic service function (the
Kubernetes hook becomes a thin wrapper over it)
> - The benefit is that a managed fleet can provision any sandbox
provider (Daytona, Modal, E2B, …) purely from configuration — no
per-provider code, no manual API calls, no secrets in the document —
while self-hosted behavior is untouched

## Linked Issues or Issue Description

No public issue exists. Refs #10157 (the cloud image variant that
bundles sandbox provider plugins — this PR is the configuration half
that makes an installed provider usable).

**Problem (feature-request shape):** on a managed instance the control
plane can auto-install a bundled sandbox provider plugin via
`PAPERCLIP_MANAGED_CONFIG.plugins.autoInstall`, but cannot create the
environment row that makes the provider schedulable. The only boot-time
environment provisioning is Kubernetes-specific
(`PAPERCLIP_EXECUTION_MODE=kubernetes` + `PAPERCLIP_K8S_*`). A generic,
config-driven path is needed so any bundled provider can be provisioned
without per-plugin code or manual API calls.

## What Changed

- `server/src/services/managed-config.ts`: optional `environments`
top-level section — `[{ name, description?, provider, config? }]` —
validated fail-closed: unknown keys, more than one entry (the DB permits
exactly one Paperclip-managed sandbox row,
`environments_managed_sandbox_idx`), a `provider` not present in
`plugins.autoInstall`, `config.provider`, or secret-looking config keys
at any depth (`api_key`/`token`/`secret`/`password`/`credential`) all
refuse startup. Absent section ⇒ `environments: []`, so pre-section
documents keep booting newer builds.
- `server/src/services/environments.ts`: new provider-agnostic
`ensureManagedSandboxEnvironment({ name, description?, provider,
config?, extraMetadata? })` — idempotently owns the single managed
sandbox row: refreshes name/description/config each call, adopts the
slot across provider switches (dropping the stale
`managedKubernetesSandbox` marker), adopts a same-name unmanaged sandbox
row (stamping it managed) instead of colliding on
`environments_name_idx` every boot, and falls back to keeping the
current name if the desired name belongs to a different row.
`ensureKubernetesEnvironment` is now a thin wrapper that pins `provider:
"kubernetes"` and stamps the legacy marker.
- `server/src/services/managed-environments.ts` (new):
`applyManagedEnvironments` boot step — no-op for self-hosted/empty;
throws (fail startup) when `PAPERCLIP_EXECUTION_MODE` is also set, since
both would own the same managed sandbox row; otherwise ensures each
declared environment fail-safe per entry (log + continue boot, matching
bundled-plugin provisioning posture).
- `server/src/index.ts`: runs the new boot step right after the
execution-policy bootstrap, before the heartbeat resumes queued runs.
- `server/src/services/index.ts`: exports `applyManagedEnvironments` and
`ManagedEnvironmentSpec`.
- Secrets stay out of the document by construction: provider credentials
reach managed instances only as process env vars (each provider's
documented fallback, e.g. `DAYTONA_API_KEY` for the Daytona plugin).

## Verification

```sh
cd server
pnpm exec tsc --noEmit -p tsconfig.json
pnpm exec vitest run \
  src/__tests__/managed-config.test.ts \
  src/services/managed-environments.test.ts \
  src/services/execution-policy-bootstrap.test.ts \
  src/__tests__/environment-service.test.ts \
  src/__tests__/environment-instance-routes.test.ts \
  src/__tests__/environment-routes.test.ts \
  src/__tests__/plugin-install-guard.test.ts \
  src/__tests__/environment-execution-target.test.ts \
  src/__tests__/instance-settings-managed-overlay.test.ts \
  src/__tests__/bundled-plugins.test.ts
```

All pass locally (typecheck clean; environment-service suite runs
against embedded Postgres and exercises the refactored Kubernetes
wrapper plus the new generic ensure: create/refresh, provider switch,
unmanaged-row adoption, name-conflict fallback). New tests cover the
parser (12 cases incl. secret-key rejection at depth) and the boot step
(no-op, mutual exclusion, pass-through, fail-safe).

## Risks

- **Self-hosted: none intended.** Without `PAPERCLIP_MANAGED_CONFIG`
nothing new executes; the `PAPERCLIP_EXECUTION_MODE=kubernetes` path is
regression-covered by the existing bootstrap/service/route suites (all
green).
- **Behavioral shift in `ensureKubernetesEnvironment` (deliberate):** it
now also refreshes `name`/`description` to their managed defaults each
boot (desired-state semantics, same as config today) and adopts a
`managedByPaperclip` sandbox row that lacks the Kubernetes marker —
previously that state made the ensure throw every boot.
- **New startup failure modes are all explicit misconfigurations**
(malformed section, provider not auto-installed, secret in config,
execution-mode conflict) and fail with precise errors; DB-side ensure
failures never block boot (fail-safe per entry, logged).
- No migrations; no API surface changes.

## Model Used

Claude Fable 5 (Anthropic, model ID `claude-fable-5`) with extended
thinking and tool use, driving the change end-to-end inside a Claude
Code / agent-harness session (code, tests, and verification runs).

## 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
managed-config module header is the contract doc)
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
2026-07-27 12:00:21 -07:00
Nicky Leach 197718bc00
feat(acpx): per-step round-trip + provider-latency attribution for sandbox startup (#10222)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Sandbox-backed runs need more precise startup observability so
operators can see where time is spent before an adapter is invoked
> - Aggregate startup timing hides which boundary is actually slow,
especially in remote execution where the bottleneck can move between
host round-trips, provider-boundary latency, and handshake phases
> - This pull request keeps the existing startup timing channel additive
while attributing the latency to the specific startup steps that caused
it
> - The benefit is better diagnosis of sandbox startup regressions
without changing control flow or introducing a schema migration

## Linked Issues or Issue Description

Refs: #10204

This PR extends the existing sandbox run-startup timing observability
with per-step round-trip and provider-latency attribution for the
Daytona startup path. It keeps the event payload additive and free-form,
and it leaves the control flow, database schema, and external adapter
interfaces unchanged.

## What Changed

- Added per-step round-trip counting for the host-to-sandbox execute
seam
- Added provider-boundary duration accumulation for the Daytona execute
and re-fetch steps
- Split the ACP handshake timing into `createRuntimeMs` and
`ensureSessionMs` while preserving the warm-handle skip
- Kept the startup timing payload additive and did not add a schema
migration

## Verification

- `adapter-utils` acpx-engine and startup-timing suites: pass
- Daytona plugin suite: pass with mocked SDK and injected-clock duration
assertions
- `server` environment-execution-target suite: pass
- `tsc --noEmit` for adapter-utils and server: pass
- Git validation: fetched
`origin/feat/sandbox-start-step-timing-attribution`, confirmed it
matches the authorized submit SHA
`53c573266e618af05c57a0720aaa9d9e0452de61`, and confirmed the branch
contains only the expected commit on top of `origin/master`
- Searched GitHub for duplicate or related PRs/issues; found one closely
related merged PR and no open duplicate on this branch
- Checked `ROADMAP.md`; the broad sandbox-agent roadmap section does not
call out this specific startup-timing attribution work as a duplicate

## Risks

- Low risk: the change is additive and only enriches existing timing
data
- Downstream consumers that assume aggregate-only startup timing may
need to tolerate the additional per-step fields
- The finer spawn/initialize/session split remains a follow-up in the
external ACP client because that hook is not available here yet

## Model Used

OpenAI GPT-5, tool-using coding agent

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] 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: Harold Kim <harold@paperclip.ing>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-07-27 09:27:00 -07:00
Devin Foley d1b9448b57
fix(server): stamp the real build version into images instead of the package.json placeholder (#10257)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work; it ships as a Docker image that self-hosters and managed
deployments run.
> - The server resolves its own version at runtime in
`server/src/version.ts` (`resolveServerVersion()`), which feeds
analytics and the server debug panel.
> - That resolver derives the real version from `git describe`, and
falls back to `server/package.json`'s `version` when git isn't
available.
> - But `server/package.json`'s version is a static placeholder — CI
only stamps the real CalVer at publish, so in source it is never the
real version (currently `0.3.1`).
> - A Docker image has no `.git` (it's dockerignored), so `git describe`
can't run inside it. Every image therefore falls back to the placeholder
and reports `0.3.1` in analytics and the debug panel, regardless of
which commit it was built from.
> - This PR computes the real version once on the CI build runner (where
`.git` and tags exist), bakes it into the image, and has
`resolveServerVersion()` prefer that stamp when `git describe` is
unavailable.
> - The benefit: self-hosted and cloud images report their true version
instead of a misleading placeholder, with no change to dev checkouts,
`git describe`-based resolution, or local `docker build`.

## Linked Issues or Issue Description

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

**What happened?**
Docker images built from `master` (and release tags) report the server
version as the `0.3.1` placeholder in analytics and the server debug
panel, instead of the real version of the commit the image was built
from.

**Expected behavior**
An image reports the real version of its build commit (e.g.
`2026.722.0+51.git.<sha>`), so operators can tell which build is
running.

**Steps to reproduce**
1. Build the server Docker image from any `master` commit (the `Docker`
workflow, `production` target).
2. Run the image and open the server debug panel (or inspect the version
reported to analytics).
3. Observe the version is `0.3.1` rather than the commit's real version.

**Root cause**
`resolveServerVersion()` derives the real version from `git describe`,
but the image has no `.git` (dockerignored), so it falls back to
`server/package.json`'s `version` — a static placeholder CI only
replaces with the real CalVer at publish time. Nothing bakes the real
version into the image.

**Paperclip version or commit:** reproduces on `master` (`4c55f0d8`) and
any published image.
**Deployment mode:** self-hosted and managed (both the `production` and
`-cloud` images).
**Installation method:** Docker image (`ghcr.io/paperclipai/paperclip`).

**Related PRs (dedup search):** #9103 (merged — added the `git
describe`-based source-install resolution this builds on) and #9637
(closed). Neither bakes a version into the image; this PR closes that
gap. No duplicate found.

## What Changed

- **`.github/workflows/docker.yml`** — checkout with full history + tags
(`fetch-depth: 0`), and a new `Compute build version` step that runs
`git describe --tags --match 'v*' --long --dirty` on the pristine runner
checkout. The result is passed as a `PAPERCLIP_BUILD_VERSION` build-arg
to both the `production` and `-cloud` image builds.
- **`Dockerfile`** — the `production` stage takes an `ARG
PAPERCLIP_BUILD_VERSION` (default empty) and bakes it into the runtime
`ENV`; the `cloud` stage inherits it via `FROM production`.
- **`server/src/build-version.ts`** (new) — `readBuildVersion()` /
`parseBuildVersion()`, mirroring `build-commit.ts`: reads
`PAPERCLIP_BUILD_VERSION` (or a `.paperclip-build-version` file) as a
single-token stamp.
- **`server/src/version.ts`** — `resolveServerVersion()` prefers the
baked build version when `git describe` is unavailable, parsing it with
the same rules as a live checkout (`parseGitDescribeVersion`), and
falling through to the existing `build-commit` stamp and package version
when unset. A live checkout's `git describe` still wins over any stamp.
- Tests for the new behavior and the precedence.

## Verification

- `pnpm --filter @paperclipai/plugin-sdk ensure-build-deps && tsc
--noEmit` in `server/` — clean.
- `vitest run server/src/__tests__/version.test.ts
server/src/__tests__/build-version.test.ts` — **23 tests pass**,
covering: stamped version used when git describe fails, stamp parsed to
real CalVer, stamp preferred over the build-commit fallback, on-tag
stamp collapses to the release version, a pre-resolved stamp used
verbatim, and a live git describe still winning over a stamp.
- `git describe --tags --match 'v*' --long` for this commit →
`v2026.722.0-51-g<sha>`, which `resolveServerVersion()` reports as
`2026.722.0+51.git.<sha>` — no longer `0.3.1`.
- Not run locally: the full multi-arch image build (CI-only). The
workflow change is verified by inspection; the version is computed on
the pristine checkout before any lockfile refresh, so it carries no
spurious `-dirty`.

## Risks

Low. Additive and image-only:
- No runtime behavior changes for dev checkouts (git describe still
primary and wins over any stamp) or for local `docker build` (empty arg
→ server keeps its existing fallbacks).
- Not a breaking change; no schema or API surface. The stamp is
informational (version reporting only).
- `fetch-depth: 0` makes the release-image checkout fetch full
history/tags — a modest cost on a workflow that already runs at release
cadence with a 60-minute budget.
- Rollback: revert the commit; images simply return to reporting the
placeholder.

## Model Used

Claude Opus 4.8 (`claude-opus-4-8`, 1M-context variant), extended
thinking, with tool use / code execution — agentic edits, `tsc` +
`vitest` runs, and a `git describe` resolution check.

## 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 (bugfix, not core feature work)
- [x] I have searched GitHub for duplicate or related PRs and linked
them above (#9103, #9637 — related, not duplicates)
- [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/build-version-stamp`)
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
user-facing docs affected; behavior is documented inline in `version.ts`
/ `build-version.ts` and the workflow/Dockerfile)
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-07-25 10:06:28 -07:00
Dotta c481be44e3
fix(task-watchdogs): deduplicate unchanged stopped-state wakes (#10207)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Task watchdogs review issue subtrees when no run or queued wake
keeps work live
> - Pending human interactions and approvals are valid stopped states
that still need one watchdog review
> - The existing fingerprint included volatile activity timestamps, so
unchanged stopped trees could wake repeatedly after comments, documents,
work products, or sibling completions
> - This pull request fingerprints only review-material leaf and wait
state, persists the reviewed snapshot, and suppresses shrink-only
repeats
> - The benefit is one review per materially new stopped state without
weakening liveness classification or hiding human waits

## Linked Issues or Issue Description

### What happened?

Task-watchdog stop fingerprints changed for metadata-only activity and
completed siblings, producing duplicate wakes after an unchanged stop
had already been reviewed.

### Expected behavior

Pending interactions and approvals remain classified as stopped, but a
reviewed stopped state only wakes again when waits, non-terminal leaves,
status, assignment, or blockers gain material changes.

### Steps to reproduce

1. Review a stopped watched subtree with a pending human wait or
multiple non-terminal stopped leaves.
2. Add only comment/document/work-product activity, or complete one
stopped sibling without changing the wait set.
3. Observe a duplicate wake from the timestamp-heavy fingerprint.

Related public work: Refs #9452 for overlapping task-watchdog service
edits and #10043 for related no-op fingerprint suppression.

## What Changed

- Added fingerprint v2 over non-terminal material leaves plus
subtree-wide pending wait ids, excluding volatile timestamps while
retaining them in wake context.
- Added nullable observed/reviewed JSONB stop snapshots and shrink-only
reviewed-state suppression with legacy exact-fingerprint fallback.
- Added pending interaction kinds and approval ids to watchdog wake
context, review comments, and comment metadata.
- Added classifier and scheduler coverage for waiting-leaf liveness,
metadata stability, sibling shrink suppression, material changes,
snapshot promotion, legacy rows, and unchanged idempotency keys.

## Verification

- `pnpm --filter @paperclipai/server exec vitest run
src/__tests__/task-watchdogs-classifier.test.ts
src/__tests__/task-watchdogs-scheduler.test.ts` — 2 files, 36 tests
passed.
- `pnpm --filter @paperclipai/server typecheck` — passed.
- `git diff --check origin/master...HEAD` — passed.

## Risks

- Fingerprint version 2 intentionally re-fingerprints every currently
stopped watched tree once after deployment, causing a one-time wake
burst before the new reviewed snapshots are established.
- Migration `0191_task_watchdog_stop_snapshots.sql` only adds two
nullable JSONB columns with no backfill; legacy rows continue
exact-fingerprint behavior until a post-deploy review promotes a
snapshot.
- PR #9452 edits the same service file; whichever lands second may need
a trivial rebase.

> 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 `gpt-5.6-sol`, high reasoning mode, with
repository tool use and code execution. 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
- [ ] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-07-25 08:34:41 -05:00
Dotta 4e00818574
fix(runtime): support in-place workspace realization (#10230)
## Thinking Path

> - Paperclip is the control plane people use to coordinate AI agents
and their execution environments.
> - Environment realization decides where an agent runs and which
filesystem and toolchain are authoritative.
> - Copy-based realization is unsafe for container-anchored tasks
because absolute paths such as `/app` can point outside the synchronized
tree and task-specific binaries may be absent.
> - That mismatch can let an agent successfully verify work in a phantom
writable path while sync-back silently discards the result.
> - Existing task environments already provide the authoritative
filesystem and toolchain, so they should be executed in place rather
than copied.
> - Copy mode still needs explicit confinement rules so aliases target
the synchronized workspace and unsynchronized writable paths fail
visibly.
> - This pull request adds typed realization metadata, propagates the
authoritative root through orchestration, and teaches Codex to honor it.
> - The benefit is that container-anchored tasks operate on
verifier-visible state with the intended tools, while copy mode remains
safe and backward compatible.

## Linked Issues or Issue Description

No public GitHub issue exists for this defect.

GitHub duplicate searches for in-place execution, workspace realization,
and authoritative workspace roots found no related pull request to link.

### What happened?

Environment-backed agent runs were always realized through a copied
workspace. Tasks anchored to absolute container paths could therefore
write outside the synchronized tree, and task-provided toolchains were
unavailable in the copy. A run could report success even though
sync-back discarded its output.

### Expected behavior

Existing task environments should run against their real authoritative
root and toolchain. Copy-mode runs should map declared absolute aliases
into the synchronized tree and reject writable paths that cannot be
restored.

### Steps to reproduce

1. Run a Codex task environment whose required files live under `/app`
or `/workspace` and whose required binary exists only in the task
container.
2. Observe that copy realization changes the effective
filesystem/toolchain or permits writes outside the synchronized root.
3. Complete and verify the task inside the agent sandbox.
4. Observe that the verifier cannot see out-of-tree artifacts or that
task-specific commands were unavailable.

### Reproduction context

- Paperclip commit: `3a16b91217483d2c233926de5b7f7bc3a1077924`
- Deployment: built from source in a task-container execution
environment
- Adapter: Codex local
- Database: not database-related
- Access context: agent execution

## What Changed

- Added typed `copy | in_place` workspace-realization metadata,
authoritative roots, confined aliases, and outbound restore paths to
shared execution-target contracts.
- Selected in-place realization for existing task environments and
skipped archive prepare/restore when the authoritative environment is
used directly.
- Propagated the authoritative root into adapter context so Codex uses
it for cwd and `PAPERCLIP_WORKSPACE_*` semantics, including ACP
execution.
- Bound copy-mode aliases such as `/app` to the synchronized workspace
and rejected writable out-of-tree paths without explicit restore
mappings.
- Added focused regression coverage while preserving existing copy-mode
archive restore behavior.

## Verification

- `pnpm --filter @paperclipai/shared typecheck` — passed.
- `pnpm exec vitest run
packages/adapter-utils/src/local-process-sandbox.test.ts
packages/adapters/codex-local/src/server/acp.test.ts
packages/adapters/codex-local/src/server/execute.remote.test.ts
server/src/__tests__/environment-run-orchestrator.test.ts` — 48 passed,
4 skipped.
- `pnpm -r typecheck` — passed.
- `env -u AWS_ACCESS_KEY_ID -u AWS_SECRET_ACCESS_KEY -u
AWS_SESSION_TOKEN pnpm test:run` — passed across all general and
serialized Vitest shards.
- `pnpm build` — passed.
- Codex `k=1` acceptance run completed July 24, 2026 at 23:54:30 UTC
with 4 completed, 0 exceptions, and mean reward 1.0: `build-cython-ext`,
`openssl-selfsigned-cert`, `prove-plus-comm`, and `sqlite-db-truncate`
each received terminal grade 1.0 against real task-environment paths and
toolchains.

## Risks

- In-place mode deliberately exposes the authoritative task root to the
adapter; incorrect environment metadata could point execution at the
wrong root. Typed metadata and focused orchestration tests cover
selection and propagation.
- Copy-mode writable-path validation is stricter and may reject
previously accepted unsafe configurations. The rejection is intentional
and produces a visible error instead of silently losing output.
- The acceptance run is focused on four Codex task-environment
workloads, not a broad cross-adapter benchmark. Existing copy-mode
archive tests and the full repository suite remain green.

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

## Model Used

- OpenAI Codex CLI coding agent; exact model ID and context-window size
were not exposed to this runtime. Capabilities used: extended reasoning,
repository editing, shell execution, test/build execution, Git, GitHub
CLI, and Paperclip API tool use.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-07-25 08:29:26 -05:00
Nicky Leach cca2806e57
test(tool-gateway): make idle-down slot test deterministic via injected clock (#10226)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The server has a gateway layer that coordinates tool execution and
runtime slots
> - The idle-down test for the local stdio fixture slot was relying on
real wall-clock timing
> - On slower runners, that made the test nondeterministic because the
slot could be reaped before the presence assertion ran
> - This pull request switches the test to use the existing injectable
clock seam so time only advances when the test says it should
> - The benefit is that the idle-down behavior stays covered while the
test becomes deterministic and no longer flakes under load

## Linked Issues or Issue Description

This PR fixes a flaky gateway test in the server test suite. The
`tool-gateway` idle-down scenario was asserting slot presence while also
depending on a very short real-time idle TTL and a later sleep-based
reap. On loaded runners, the intervening work could exceed the TTL,
which caused the slot to disappear early and the assertion to see an
empty list.

The fix keeps the production code path unchanged and drives the test
from the supervisor's existing injectable clock. The test now holds time
steady through the presence check, then advances the clock past the idle
deadline to trigger the reap deterministically. The original behavioral
assertions stay intact: slot reuse, counter increments, metadata, and
stop status still get verified.

## What Changed

- Replaced the real-time idle-down wait in the `tool-gateway` test with
the runtime supervisor's injectable clock seam.
- Kept the existing assertions for slot reuse, slot identity, counters,
metadata, and stop behavior.
- Removed the test's dependency on wall-clock timing so the idle-down
path is deterministic under load.

## Verification

- Targeted server typecheck passed with `tsc --noEmit`.
- `tool-gateway.test.ts` passed in full: 49/49.
- The targeted idle-down scenario passed 50/50 in a tight loop with 0
failures after the clock injection change.

## Risks

- Low risk: this is a test-only change and does not modify production
gateway logic.
- The test now exercises the idle-down logic through a controlled clock
rather than real elapsed time, which is the point of the fix but does
slightly reduce wall-clock realism in the test itself.

## Model Used

OpenAI Codex (GPT-5), tool-using coding agent; context window not
surfaced in the workspace.

## 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: Harold Kim <harold@paperclip.ing>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-07-24 16:29:47 -07:00
Dotta 30ff3d7c58
feat(routines): expose activity gate API (#9438)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Scheduled routines provide recurring control-plane work without
manual intervention
> - The new activity gate can suppress scheduled runs when no external
work occurred
> - The core scheduler and database support landed without a public
create/update contract
> - Agents, operators, and managed plugins need validated fields plus
discoverable semantics to opt in safely
> - This pull request exposes the activity gate through routine APIs,
revisions, plugin contracts, tests, and skill documentation
> - The benefit is backward-compatible control over idle scheduled work
without losing activity-triggered follow-up

## Linked Issues or Issue Description

- Refs #8534

## What Changed

- Added shared activity-gate policy and scope enums with create/PATCH
validation.
- Persisted activity-gate fields through routine creation, updates,
revision snapshots, pipeline snapshots, and revision restores.
- Defaulted legacy revision snapshots during restore and added
regression coverage for pre-field snapshots.
- Extended managed-plugin routine declarations, production
reconciliation, and the SDK test harness to preserve non-default gate
settings.
- Added end-to-end API coverage for create/PATCH/list/detail
round-trips, defaults, and invalid enum rejection.
- Documented schedule-only semantics, activity windows,
own-run/read-action exclusions, scopes, and an hourly quiet-night
watcher example.

## Verification

- `pnpm exec vitest run packages/shared/src/validators/routine.test.ts
server/src/__tests__/routines-service.test.ts
server/src/__tests__/routines-e2e.test.ts`
- `pnpm exec vitest run packages/shared/src/validators/plugin.test.ts
packages/plugins/sdk/tests/testing-actions.test.ts
server/src/__tests__/plugin-managed-routines.test.ts
server/src/__tests__/routines-service.test.ts -t 'activity
gate|preserves declared activity gate settings|resolves routine agent
and project refs'`
- `pnpm exec vitest run ui/src/lib/workspace-routines.test.ts
ui/src/pages/Routines.test.tsx`
- `pnpm --filter @paperclipai/shared typecheck`
- `pnpm --filter @paperclipai/plugin-sdk typecheck`
- `pnpm --filter @paperclipai/server typecheck`
- `pnpm --filter @paperclipai/ui typecheck`
- `pnpm check:token-gates`
- GitHub CI: all final-head checks green; Storybook visual regression
skipped by path rules.
- Greptile: 5/5 with no unresolved review threads.

## Risks

- Low risk: defaults remain `always` and `company`, preserving existing
routine behavior and old revision snapshots.
- Managed plugin manifests can now declare the same validated gate
settings as the public routine API; omitted values retain core defaults.
- Revision snapshots now include the new fields so policy changes are
not lost or treated as no-ops during restore.

> For core feature work, checked `ROADMAP.md`: this extends the existing
Scheduled Routines roadmap item and does not duplicate a separate
planned capability.

## Model Used

- OpenAI GPT-5.5 via Codex CLI, with repository tool use and code
execution; context-window size was not exposed by the runtime.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-07-24 16:47:24 -05:00
Dotta 8f08ec5ce6
feat(status-cards): join summary-mentioned issues to watched set (#10205)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Status cards summarize changing company work and watch issues so
later changes can produce useful deltas
> - A summary can explicitly reference issues that are important to the
update even when those issues do not match the card's configured queries
> - Previously, those referenced issues were not retained in the watched
set, so their later status, assignee, or comment changes could be missed
> - The watched snapshot must avoid artificial additions or removals
caused only by a summary changing which issues it references
> - This pull request resolves issue references when a summary is
written, persists them, and joins them to the watched snapshot with
stable delta semantics
> - The benefit is that status cards continue tracking the exact issues
their latest update called out while keeping follow-up updates relevant
and non-duplicative

## Linked Issues or Issue Description

### Pre-submission checklist

- [x] I have searched existing open and closed issues and this is not a
duplicate.
- [x] I am on the latest released version of Paperclip (or can reproduce
on `master`).
- [x] I have confirmed the error originates in Paperclip itself — not in
my agent adapter, API provider, or local configuration.

### What happened?

When a status-card summary explicitly referenced an issue by identifier
or `/issues/<uuid>` URL, that issue was not automatically retained in
the card's watched set unless it independently matched a configured
query. Later status, assignee, or comment changes to an issue
highlighted by the latest update could therefore be omitted.

### Expected behavior

References in the latest summary should resolve only within the card's
company, appear in dry runs and the watched-issues UI, count and
fingerprint like query matches, and enter or leave the watched set
without artificial added/removed deltas already represented by the
summary change.

### Steps to reproduce

1. Create a status card whose query does not match a second issue in the
same company.
2. Write a summary that references the second issue by identifier or
issue URL.
3. Inspect the card's watched count or Watched issues tab.
4. Change the referenced issue's status, assignee, or comments and run
the next update.
5. Before this change, the referenced issue is absent from the watched
snapshot and its later change does not produce the expected delta.

### Paperclip version or commit

- Reproduced on `master` before this PR (base commit `762ce5b4ef`).

### Deployment mode

- Local dev (`pnpm dev`), built from source.

### Agent adapter(s) involved

- Not adapter-specific (core bug).

### Database mode

- Embedded Postgres test environment; the schema change uses standard
PostgreSQL JSONB.

### Access context

- Board (human operator).

## What Changed

- Added migration `0191` and schema support for persisted
`status_cards.mentioned_issue_ids`.
- Resolved summary references by issue identifier or `/issues/<uuid>`
URL within the status card's company when summaries are written.
- Joined mentioned issues into watched counts and fingerprints so later
status, assignee, and comment changes generate normal update deltas.
- Suppressed artificial added/removed deltas when the latest summary
starts or stops mentioning an issue.
- Added `mentionedIssues` to dry-run responses and a “Mentioned in the
latest update” group in the Watched issues tab.
- Updated the summarizer prompt to explain that referenced issues
automatically join the watched set.
- Added focused server and UI coverage for reference resolution,
snapshot behavior, deltas, API responses, and rendering.

## Verification

- `pnpm --filter @paperclipai/server exec vitest run
src/__tests__/status-cards.test.ts
src/__tests__/status-card-update-engine.test.ts` — 31 tests passed.
- `pnpm --filter @paperclipai/ui exec vitest run
src/pages/StatusCards/StatusCardTile.test.tsx` — 11 tests passed.
- Earlier implementation verification also passed database/shared/server
typechecks, UI `tsc -b`, the broader StatusCards UI test set, and
embedded-Postgres migration application.

### Visual Verification

- Greptile T-Rex ran Playwright browser checks successfully and captured
the Status Card drawer Watched tab showing the new “Mentioned in the
latest update” grouping:
https://app.greptile.com/trex/runs/15796101/artifacts

## Risks

- The migration adds a nullable JSONB column and is backward-compatible;
existing cards have no mentioned issues until their next summary write.
- Reference extraction is company-scoped to prevent cross-company issue
association.
- Watched counts and future fingerprints change for cards whose latest
summaries reference issues; tests cover additions, removals, and
suppression of spurious deltas.
- This targeted status-card fix does not introduce a new roadmap
subsystem or external integration.

## Model Used

- Anthropic Claude Fable 5 (Paperclip model alias; exact underlying
provider model ID and context window were not recorded in the
implementation task metadata), with extended reasoning, tool use, and
code execution.
- OpenAI Codex coding agent (runtime model identifier and context window
not exposed to this task) prepared the PR, rebased the branch, and ran
focused verification with terminal 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)
- [ ] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-07-24 16:44:56 -05:00
Dotta 665408c6d0
fix(codex): classify mid-turn harness crashes structurally as retriable infra (#10210)
## Thinking Path

> - Paperclip is the open source control plane people use to manage AI
agents and their work
> - Heartbeat execution relies on adapters distinguishing agent failures
from failures in the harness running beneath the agent
> - Codex MCP transport crashes can kill the CLI after the JSONL
protocol has started but before it emits a protocol-terminal event
> - Those interrupted streams were left unclassified, so the control
plane terminalized the heartbeat as `heartbeat_failed` / `agent_failure`
with no continuation
> - Agent-level failure is already expressible through the JSONL
protocol via an `error` event, `turn.failed`, or `turn.completed`, so an
interrupted nonzero exit can be classified structurally without
inspecting unstable error strings
> - This pull request reports that shape as `codex_harness_crash` in the
`transient_upstream` family and routes it through Paperclip's existing
bounded retry and recovery-continuation paths
> - The benefit is that transient Codex harness failures recover safely
without misclassifying quoted agent output or depending on
transport-specific wording

## Linked Issues or Issue Description

- **What happened:** Codex MCP transport failures, including rmcp worker
death, could terminate the CLI mid-turn after protocol output began but
before any terminal JSONL event. The run then became an unclassified
terminal heartbeat failure with `continuationCount: 0`; this occurred in
3 of 44 L3 Codex-lane trials during the associated benchmark
investigation.
- **Expected behavior:** a nonzero Codex exit after the protocol starts
but before an `error`, `turn.failed`, or `turn.completed` event should
be treated as a harness/infrastructure crash and enter the existing
bounded retry policy.
- **Why structural classification:** transport error strings vary, and
stdout may quote agent output that merely discusses network failures.
The protocol boundary identifies whether the agent itself produced a
terminal result without regex matching.
- **Recovery behavior:** `codex_harness_crash` maps to `errorFamily:
transient_upstream`, using the existing `same_session` →
`safer_invocation` → `fresh_session` ladder plus the
recovery-continuation transient-infrastructure path.
- Supersedes the regex-based approach in #10150, which is closed.

## What Changed

- Added protocol-state tracking that identifies a nonzero exit after
protocol start and before any protocol-terminal event as
`codex_harness_crash`.
- Propagated the structural classification as `transient_upstream`
through the Codex adapter.
- Added parse unit coverage, including a faithful crash-shaped stream,
without matching stderr transport strings.
- Added adapter execution coverage using a fake Codex process that emits
a protocol prefix and then dies with the observed rmcp stderr line.
- Added heartbeat bounded-retry coverage, including the `errorCode`-only
fallback, and recovery-continuation classification coverage.

## Verification

- `parse.test.ts` — 16 passed.
- `codex-local-execute.test.ts` — 16 passed.
- `heartbeat-retry-scheduling.test.ts` — 30 passed.
- `service.pause-durability.test.ts` — 6 passed.
- Server and Codex adapter TypeScript checks passed.
- The branch commit is unchanged from the tested and pushed `88f5464d40`
handoff.

## Risks

- Low risk: the classification requires a nonzero exit after protocol
start and before any protocol-terminal event, so normal agent-declared
failures and completed turns keep their existing behavior.
- The change intentionally broadens recovery for structurally
interrupted Codex runs; bounded retry limits still prevent indefinite
continuation loops.
- No schema, migration, public API, UI, lockfile, or workflow changes.

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

## Model Used

- OpenAI Codex coding agent. The exact runtime model ID and
context-window size were not exposed by the execution environment;
capabilities used for the implementation included repository analysis,
reasoning, code editing, and terminal-based 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)
- [ ] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
— the pre-existing, already-pushed branch name was explicitly prescribed
for this replacement PR
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-07-24 16:40:47 -05:00
Dotta b996b71a38
Deduplicate wake-payload issue descriptions and compact resume deltas (#10216)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Heartbeat wake payloads and the task-context markdown are the two
channels that deliver an issue's brief into an agent's prompt
> - #10151 fixed wake-prompt-only adapter lanes waking without the issue
description by adding it to the structured wake payload
> - That left the description delivered twice per prompt on lanes that
also inject the task-context markdown, and re-delivered in full on every
resume wake, permanently bloating persistent-session context
> - This pull request makes the task markdown the single description
carrier on lanes that use it, and omits the description from
non-assignment resume deltas on all lanes while keeping it for
assignment-shaped and recovery wakes
> - The benefit is that every lane receives the brief exactly once when
it needs it, and long-lived sessions stop re-paying the full brief in
tokens on every wake

## Linked Issues or Issue Description

Refs #10151

Related prior work: #2883, #8402 (earlier description-delivery attempts
referenced by #10151). I searched the PR list for open work on
wake-payload description handling and found none besides the merged
#10151.

**Bug:** After #10151, adapters that inject the `Paperclip task context`
markdown (ACPX engine lanes, claude-local CLI, hermes server and
gateway) receive the issue description twice in a single prompt — once
in the wake prompt's `Issue description:` block and once in the task
markdown. Separately, resume deltas re-send the full description (up to
12k characters) on every wake even though the persistent session already
received it.

**Expected behavior:** The description appears exactly once per prompt
on every lane, and resume deltas only carry it when the resuming session
may not have seen the brief (assignment-shaped or recovery wakes),
leaving an explicit fetch breadcrumb otherwise.

**Reproduction:** Wake a claude-local or ACPX agent on an issue with a
description and inspect the assembled prompt: the description text
appears in both the wake-payload block and the task-context block. Wake
the same session again via a comment: the full description is present
again in the resume delta.

**Affected version:** Current `master` (with #10151 merged).

**Deployment mode:** Adapter-backed heartbeat execution, local and
sandboxed lanes.

## What Changed

- `renderPaperclipWakePrompt` accepts `suppressIssueDescription`; the
four task-markdown lanes pass it so the task markdown stays the single,
uncapped description carrier there.
- Non-assignment resume deltas omit the description and emit `- issue
description: omitted from this resume delta; fetch the issue if you need
the latest brief`. Assignment-shaped reasons (`issue_assigned`,
`issue_reopened_via_comment`, `issue_recovery_action_restored`,
`issue_tree_restored`) and recovery wakes still deliver the full brief.
- `buildPaperclipTaskMarkdown` gains `includeDescription`; the server
now also publishes `context.paperclipTaskMarkdownCompact` (description
stripped, directives and wake comment kept), and the new
`selectPaperclipTaskMarkdown` helper picks the right variant under the
same resume rules, falling back to the full markdown when no compact
variant exists (version skew safety).
- The wake prompt's description block now carries the same user-authored
trust framing the task markdown already had.

## Verification

- `npx vitest run packages/adapter-utils/src/server-utils.test.ts
packages/adapters/claude-local/src/server/acp.test.ts
packages/adapters/codex-local/src/server/acp.test.ts
server/src/__tests__/heartbeat-context-summary.test.ts` — 137 tests
passed, including new coverage for suppression, resume omission plus
breadcrumb, assignment-shaped resume inclusion, compact-variant
building, variant selection, and an end-to-end ACPX prompt-assembly test
asserting the description appears exactly once on fresh wakes and not at
all on comment resumes.
- `npx vitest run` in `packages/adapters/hermes` — 59 tests passed,
including a gateway execute-level test asserting the brief is sent
exactly once on fresh runs and not re-sent on stable-session resumes.
- `tsc --noEmit` in `packages/adapter-utils`,
`packages/adapters/claude-local`, `packages/adapters/hermes` — clean;
`server` matches the `master` baseline exactly (pre-existing plugin-sdk
resolution errors only, none in touched files).
- Pre-existing failures confirmed identical on clean `master`:
claude-local `execute.remote.test.ts` / `test.probe.test.ts`,
adapter-utils `mcp-isolation.integration.test.ts` (requires a newer
local Claude CLI).

## Risks

- Behavioral shift, prompt-only: a resumed session woken by a comment on
an issue it never handled (rare — assignment wakes normally precede
comment wakes) would not get the inline description; the breadcrumb plus
the standard issue-fetch path covers it.
- Additive context key (`paperclipTaskMarkdownCompact`); older adapters
ignore it and newer adapters fall back to the full markdown when it is
absent, so mixed-version deployments degrade to current behavior.
- No schema, migration, or API changes; the structured wake-payload JSON
shape is unchanged.
- Known follow-up deliberately out of scope: openclaw embeds the raw
wake-payload JSON (which still contains the description) in prompt text
for machine parsing. The hermes-gateway lane is handled: it detects
stable-session resumes (issue/agent session-key strategy plus a stored
prior session id), compacts the task markdown, and omits the description
from its prompt-embedded JSON copy.

> This is a focused correctness/efficiency fix to existing wake plumbing
and does not overlap with planned roadmap feature work.

## Model Used

- Anthropic Claude Fable 5 (`claude-fable-5`), extended thinking
enabled, with repository tool use, shell execution, and local test
execution via Claude Code.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [ ] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
(execution-workspace branch, same convention as merged #10202)
- [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
(code-level docs; no user-facing docs affected)
- [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

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 16:35:07 -05:00
Dotta d3c004d1b8
Fix issue descriptions in structured wake payloads (#10151)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Heartbeat wake payloads provide the scoped task context an agent
needs before it can act safely
> - Assignment wakes already loaded the issue description for task
markdown, but the structured wake-payload builder dropped it
> - Agents reading `PAPERCLIP_WAKE_PAYLOAD_JSON` could therefore see a
missing brief while also being told no fallback fetch was needed
> - Long descriptions also need a bounded representation so wake
environments and prompts remain safe
> - This pull request carries the description through the server and
adapter contract, and marks truncated descriptions as requiring fallback
fetch
> - The benefit is that agents receive the actual brief instead of
inventing requirements from the title

## Linked Issues or Issue Description

Fixes: #5844
Fixes: #2882

Related prior attempts: #2883 and #8402. This change adds focused
regression coverage and enforces the missing long-description fallback
invariant.

**Bug:** Issue-assignment wake payloads omitted the issue description
from the structured payload even when the issue had a populated
description.

**Expected behavior:** The structured wake payload includes the issue
description. If the description must be truncated for payload size,
`fallbackFetchNeeded` is `true`.

**Reproduction:** Assign an issue with a description to an agent and
inspect `PAPERCLIP_WAKE_PAYLOAD_JSON`; before this change,
`issue.description` was absent while `fallbackFetchNeeded` could remain
`false`.

**Affected version:** Reproduced on current `master` before this patch.

**Deployment mode:** Adapter-backed heartbeat execution, including local
Codex agents.

## What Changed

- Include `issues.description` in the server wake-payload query and
supplied issue summaries.
- Bound inline descriptions at 12,000 characters and force fallback
fetch when truncation occurs.
- Preserve and render description metadata through shared adapter
normalization and prompt rendering.
- Add focused tests for long-description fallback and exact brief-string
rendering.

## Verification

- `pnpm exec vitest run
server/src/__tests__/heartbeat-agent-session-message.test.ts
packages/adapter-utils/src/server-utils.test.ts` — 81 tests passed.
- `pnpm --filter @paperclipai/adapter-utils typecheck` — passed.
- `pnpm --filter @paperclipai/server typecheck` — passed.
- `git diff --check` — passed.

## Risks

- Low risk: the payload shape is additive.
- Very long descriptions are truncated at 12,000 characters; the payload
explicitly requests a fallback fetch for the full brief.
- Prompt size increases by the issue-description length for scoped
wakes, bounded by the same limit.

> This is a focused correctness fix and does not overlap with planned
roadmap feature work.

## Model Used

- OpenAI GPT-5.4 via Codex CLI, with reasoning, repository tool use,
shell execution, and test execution. 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-07-24 15:40:39 -05:00
Dotta 7014e46e5b
fix(recovery): wake ambiguous successful runs on normal model (#10184)
## Thinking Path

> - Paperclip is the open source control plane people use to coordinate
AI agents and their work.
> - The heartbeat recovery subsystem detects successful runs that leave
assigned issues `in_progress` without a durable disposition or
continuation path.
> - The existing corrective wake used a cheap, status-only model
profile, so the assignee could not perform missing verification or
deliverable work before choosing the issue disposition.
> - The existing wake prompt also omitted the original issue context and
the agent's own final report, making an honest finish/blocked/continue
decision harder.
> - This pull request keeps the structural handoff guards and
one-attempt loop bound, but wakes the assignee on its normal model lane
with context-rich instructions.
> - The benefit is that Paperclip asks the responsible agent to inspect
its own evidence, perform the smallest missing verification when needed,
and then record a real disposition without server-side prose
classification.

## Linked Issues or Issue Description

Related prior approach: #10154 (closed; this PR intentionally does not
reuse its regex classifier or route-level gate).

**Problem**

A succeeded agent run can leave its issue `in_progress` with no valid
disposition. Paperclip already detects this structurally and queues a
corrective handoff, but that wake currently runs as cheap/status-only
recovery and receives little context. The assignee may be unable to
create deliverables or verify the work, and the prompt does not quote
the report that caused the ambiguity.

**Expected behavior**

The corrective wake should use the assignee's normal model and adapter
settings, include the issue identifier/title/description, quote the
agent's own final report, include any recorded next action, preserve the
four disposition options, and explicitly require concrete verification
before marking the issue done.

**Scope**

This change does not classify run prose, add a route-level disposition
gate, alter run-liveness classification, or change the one-attempt
handoff loop bound.

## What Changed

- Switched successful-run corrective handoff payloads and context
snapshots from `status_only` to `normal_model`, removing cheap-model and
status-only guard hints.
- Added issue description, final-report, next-action, and
detected-progress fallback context to the handoff decision and
instruction builder.
- Reworked the instruction into clear "supposed to do / what happened /
options / what to do" sections with bounded description/report excerpts
and verbatim blockquotes.
- Added unit and heartbeat integration coverage for normal-lane
payloads, context plumbing, evidence quoting, fallback behavior, and
truncation while preserving structural skip tests.

## Verification

- `cd server && pnpm exec vitest run
src/services/recovery/successful-run-handoff.test.ts` — 24 tests passed.
- `cd server && pnpm exec vitest run
src/__tests__/heartbeat-process-recovery.test.ts -t "queues one
finish-handoff wake when a successful run leaves in-progress work
without a next action"` — 1 passed, 90 skipped.
- `pnpm --dir server typecheck` — passed.
- `git diff --check` — passed.

## Risks

- Low-to-moderate behavioral risk: an ambiguous successful run now
consumes the assignee's normal model rather than a cheap profile and may
perform verification or finish work before disposition.
- Prompt excerpts are bounded to approximately 1,200 description
characters and 2,000 report characters; very long context is
intentionally ellipsized.
- The existing structural skip guards, idempotency key, and single
corrective attempt remain unchanged to prevent loops.

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

## Model Used

- OpenAI Codex using `gpt-5.6-sol`, high reasoning effort, with
repository/tool execution. Context-window size was not exposed by the
runtime configuration.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 14:32:26 -05:00
Dotta 3a16b91217
feat(status-cards): single-message setup drives query and update prompt (#10202)
## Thinking Path

> - Paperclip is the open source control plane people use to manage
AI-agent companies and their ongoing work.
> - Status cards turn a standing question into recurring,
agent-generated summaries on the board.
> - The existing setup split intent across a watch prompt and separate
update instructions, which made creation and later behavior harder to
understand.
> - A status card should have one durable source of truth for both
deciding what to watch and telling the summarizer what each update must
contain.
> - This pull request makes the card prompt that source of truth,
simplifies creation to one step, and lets operators choose the running
agent immediately.
> - The benefit is a smaller mental model, fewer configuration modes,
and consistent update instructions throughout the card lifecycle.

## Linked Issues or Issue Description

Status cards currently require operators to express the same intent in
two places: the watch prompt and optional update instructions with
append/replace/none modes. This feature simplifies the experimental
status-card workflow so a single prompt defines both the watch query and
every generated update. The create flow must also support selecting the
responsible agent without a second setup step.

Related prior status-card work: #10101.

## What Changed

- Use the status card's single prompt to compile the watch query and
directly instruct every summary update.
- Add migration `0190_status_card_single_prompt` to remove
`status_cards.instructions_mode` and `status_cards.instructions`.
- Add `agentId` to `createStatusCardSchema`, validate company
membership, and default new cards to the built-in Summarizer.
- Replace the two-step create flow with one prompt-and-agent dialog and
extract a shared `SummarizerAgentSelect` for create/settings surfaces.
- Remove the extra-instructions settings section, reset incremental
history when the prompt changes, and rename the board page to "Status".
- Update the bundled `status-card-query` skill and board-operator
documentation, then regenerate the skills catalog manifest.

## Verification

- Server status-card suites: 29/29 passing.
- UI `StatusCards` suites: 22/22 passing.
- Skills catalog suite: 20/20 passing.
- `tsc -b` passes for server, UI, shared, and database packages.
- `pnpm check:migrations` passes.
- Light and dark mode screenshots cover the new create dialog and
settings tab.

## Risks

- Migration `0190` intentionally drops existing separate instruction
text. Existing card prompts remain and become the update instructions
under the new model; status cards are experimental and feature-flagged.
- Prompt edits now reset the incremental summary chain and trigger a
full rebuild, which is intentional because the prompt is also the update
contract.
- Agent selection is company-scoped; invalid agent ids return a
validation error rather than creating a misrouted card.

> 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

- Implementation: Anthropic Claude via the `claude_local` adapter, agent
label "Claude Fable 5"; extended reasoning, tool use, and code
execution. The exact provider model id and context-window value were not
retained in the task metadata.
- PR preparation: OpenAI GPT-5.4 through Codex CLI, with reasoning,
repository inspection, GitHub CLI, and Paperclip API 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)
- [ ] 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: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 14:06:14 -05:00
Dotta 7e40ed8c43
feat(status-cards): add experimental status card update view (#10101)
## Thinking Path

> - Paperclip is the open source control plane people use to manage
AI-agent companies.
> - Operators need a board-level way to monitor a changing slice of
company work without repeatedly rebuilding filters or reading raw task
threads.
> - Existing summaries are useful snapshots, but they do not provide a
dedicated query-backed card with refresh policy, change tracking, update
history, and per-update cost visibility.
> - The capability needs to be safe to evaluate before it becomes part
of the default product surface.
> - This pull request adds end-to-end experimental Status Cards, from
schema and query compilation through update orchestration and operator
UI.
> - The entire feature is gated behind the `enableStatusCards`
experimental toggle, including its route and sidebar entry.
> - The benefit is a governed, inspectable way to keep focused
operational rollups current while preserving explicit controls over
refresh frequency and spend.

## Linked Issues or Issue Description

### Subsystem affected

Cross-cutting (`packages/db`, `packages/shared`, `server/`, `ui/`, and
bundled skills/docs).

### Problem or motivation

Operators cannot currently define a reusable natural-language view of
company work, compile it into an inspectable query, and keep its summary
current as matching issues change. Rebuilding filters and rereading task
threads makes board-level monitoring repetitive and hides the
relationship between source changes, refresh cost, and the resulting
summary.

### Proposed solution

Add experimental Status Cards that compile operator intent into a query,
summarize matched work, record each update, expose
manual/interval/reactive refresh policies and costs, and preserve the
last good result across stale, updating, paused, and error states. The
capability is off by default and fully gated behind `enableStatusCards`,
including its route and navigation entry.

### Alternatives considered

- Extend existing one-off summaries: rejected because status cards
require persistent query provenance, refresh policy, update history, and
card-specific cost controls.
- Add a dashboard-only filter widget: rejected because it would not
provide governed background refresh, an update ledger, or an inspectable
compile pipeline.
- Ship the surface by default: rejected in favor of an experimental
toggle while behavior and operator value are evaluated.

### Roadmap alignment

This advances Paperclip’s board-level execution visibility and
output-first product goals. `ROADMAP.md` was checked and no duplicate
status-card initiative was found.

### Additional context

No related open PR was found in the public GitHub search for status
cards. The PR-only design wireframes were removed from the repository
after review; the published prototype remains external to the production
source tree.
## What Changed

- Added company-scoped status-card schema, CRUD APIs, compile
provenance, update ledger, shared contracts, validators, and OpenAPI
coverage.
- Added the text-to-query compile pipeline, bundled `status-card-query`
agent skill, query versioning, and authorized write-back flow.
- Added the experimental board, create flow, lifecycle tiles,
detail/settings/debug drawers, archived view, routing, navigation, and
instance setting.
- Added a change-gated update engine with manual, interval, and reactive
refresh policies, trigger selection, active hours, and daily token caps.
- Added per-update token/cost recording, today and lifetime rollups, and
policy-derived cost previews.
- Added operator documentation and agent-authoring hardening for compile
and update behavior.
- Added PR-prep integration coverage for settings/startup wiring and
replaced raw UI values with design-system tokens.
- Removed the PR-only `design/pap-15023-status-cards` wireframe
artifacts so the repository contains only production feature assets.

## Verification

- `pnpm -r typecheck` — passes on the PR head; includes `ui` `tsc -b`
passing. The UI compile gate was also independently recorded as passing
at `6d7f3cf96b` on July 23, 2026.
- `pnpm build` — passes.
- `pnpm check:token-gates` — passes with all three gates clean.
- `pnpm test:run` — 2,880 tests passed and 1 skipped; the sole failure
was an unrelated 10-second `afterAll` database-cleanup timeout in
`execution-workspaces-service.test.ts`.
- `pnpm --filter @paperclipai/server exec vitest run
src/__tests__/execution-workspaces-service.test.ts` — passes on
immediate focused rerun (25/25).
- `pnpm --filter @paperclipai/server exec vitest run
src/__tests__/instance-settings-service.test.ts
src/__tests__/server-startup-feedback-export.test.ts` — passes (31/31).
- `pnpm --filter @paperclipai/ui exec vitest run
src/pages/StatusCards/StatusCardSettingsForm.test.tsx
src/pages/StatusCards/StatusCardTile.test.tsx
src/pages/StatusCards/format.test.ts src/lib/status-card-state.test.ts`
— passes (26/26).
- Recorded pre-PR QA: compile-pipeline e2e PASS; full lifecycle and cost
QA PASS; security re-review PASS after write-back hardening; UX
approved.
- `pnpm exec vitest run packages/db/src/status-card-migrations.test.ts`
— passes; reapplies migrations `0185`–`0189` against an already-migrated
embedded Postgres database.
- `pnpm --filter /db check:migrations` — passes migration numbering and
safety checks.
- `pnpm --filter /db typecheck` — passes.
- Merged current `origin/master` on July 24, 2026 with no conflicts;
migrations `0185`–`0189` remain unclaimed on master.

## Risks

- The feature introduces five database migrations and a new background
update path; all new DDL is repeat-safe after partial application,
migration numbering/safety checks pass, and update execution is
company-scoped and change-gated.
- Natural-language compilation can produce invalid or overly broad
queries; compile provenance, query validation, debug visibility, and
version history make failures inspectable and recoverable.
- Reactive or interval refresh could increase spend; active hours, max
refresh frequency, daily token caps, per-update cost records, and
budget-paused states bound and expose that risk.
- The branch name contains an internal execution identifier because it
is a fixed handoff branch; it was intentionally not renamed or rebased
per the release handoff instructions.
- Overall rollout risk is limited because the route, navigation,
services, and UI are disabled by default behind `enableStatusCards`.

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

## Model Used

- OpenAI Codex using GPT-5.5 with reasoning, repository tool use, shell
execution, GitHub CLI, and test/build execution. 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; the fixed execution-workspace
identifier is documented as an authorized handoff exception
- [x] I have run tests locally and they pass, with the one cleanup
timeout passing on focused rerun
- [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: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-07-24 12:26:43 -05:00
Devin Foley 965a827ee7
feat(docker): publish a cloud image variant with built bundled plugins (#10157)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Managed (cloud-hosted) deployments configure instances through
`PAPERCLIP_MANAGED_CONFIG`, including a `plugins.autoInstall` key list
that the boot-time installer resolves against the bundled plugin catalog
> - The installer requires each bundled plugin's `dist/manifest.js`
(`server/src/services/bundled-plugins.ts`), but the published image only
ships the sandbox providers' *source* — they are intentionally excluded
from the pnpm workspace, and the Dockerfile never builds them
> - Every managed auto-install therefore logs `bundled plugin bundle not
present; skipping auto-install` and no sandbox provider can be
provisioned through managed config
> - Baking built plugins into the single published image would fix it
but makes every self-hosted pull carry the providers' `node_modules` for
a managed-only mechanism
> - This pull request adds a `cloud` Dockerfile target extending
`production` with built bundled plugins — parameterized by build arg and
currently just `daytona` — published alongside the default image with a
`-cloud` tag suffix
> - The benefit is working plugin auto-provisioning for managed
deployments while the self-hosted image stays byte-identical and the
cloud variant only carries what is actually deployed

## Linked Issues or Issue Description

Fixes #10158 (filed for this problem; no prior issue existed — searched
for duplicate/related PRs and issues around bundled plugins, docker
image variants, and auto-install). Summary: **What happened:** on a
managed instance with `plugins.autoInstall: ["daytona"]` delivered via
`PAPERCLIP_MANAGED_CONFIG`, boot logs `bundled plugin bundle not
present; skipping auto-install` with `pluginPath:
/app/packages/plugins/sandbox-providers/daytona`, and the plugin is
never installed. **Expected:** the advertised bundled-catalog keys are
installable from the published image. **Why:** the image ships plugin
source without `dist/` — nothing in the Dockerfile builds the
workspace-excluded sandbox providers.

## What Changed

- `Dockerfile`: new `cloud-plugins` stage (based on `build`, so
devDependencies are available for `tsc`) that installs and builds each
provider named in the `CLOUD_BUNDLED_PLUGINS` build arg standalone
(`pnpm install --ignore-workspace --no-lockfile && pnpm build`, exactly
as the providers' READMEs prescribe), asserting `dist/manifest.js`
exists per plugin and failing loudly on unknown names; new `cloud` stage
= `production` + the built plugin tree. The arg defaults to `daytona` —
the only provider managed deployments auto-install today; every entry
adds its `node_modules` to the image, so the list grows only with actual
need (a one-line workflow change).
- `.github/workflows/docker.yml`: the existing build step is pinned to
`target: production` (without this, the new trailing stage would
silently become the default build target — this pin is what keeps the
self-hosted image identical); new metadata + build-push steps publish
the `cloud` target (with `CLOUD_BUNDLED_PLUGINS=daytona`) under the same
tag set with a `-cloud` suffix (`sha-<short>-cloud`, `latest-cloud`,
`<version>-cloud`), same schema labels, reusing the GHA layer cache

## Verification

- All seven sandbox providers build standalone from a clean checkout
with the exact commands the new stage runs, each producing
`dist/manifest.js` — so the current `daytona` default works and future
list additions are known-good
- The stage's shell loop was dry-run against the checkout (directory
existence + per-plugin assertion logic)
- Workflow YAML lints clean
- **Not run:** a full multi-arch `docker build` (no local docker
daemon). The `cloud` stage is additive and the default target is pinned,
so the risk is contained to the new build step; the first master build
after merge proves it end-to-end

## Risks

- Self-hosted behavior: unchanged. The default image build is pinned to
the `production` target, which produces the same layers as before this
change; the `cloud` stages run only for the new build step.
- The plugin installs in the `cloud-plugins` stage use `--no-lockfile`
(the providers are workspace-excluded and lockfile-less by design), so
plugin dependency resolution is not pinned at image-build time. This
mirrors the existing Plugins-page install path, which resolves from npm
at install time.
- CI cost: one additional build-push per master push. It reuses the
layer cache from the production build, so the marginal work is the
single plugin's build layers.
- An unknown name in `CLOUD_BUNDLED_PLUGINS`, or a provider that stops
producing `dist/manifest.js`, fails the cloud build loudly rather than
publishing a broken variant.

## Model Used

Claude (Anthropic), model ID `claude-fable-5[1m]` via Claude Code CLI —
extended thinking and tool use (code edits, standalone plugin build
verification, workflow lint).

## Checklist

- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] Self-hosted behavior unchanged (default build target pinned to
`production`)
- [x] One clear change: publish a cloud image variant with built bundled
plugins
2026-07-24 08:22:34 -07:00
Dotta 7f766526a6
feat(sandbox): add task-scoped egress grants (#10155)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Confinement providers protect agent runs with default-deny network
policies
> - Kubernetes environments currently apply only provider-level,
namespace-wide egress allowances
> - Tasks that legitimately need GitHub or package registries therefore
cannot request narrow access, while network failures do not explain the
governing policy or how to request a grant
> - This pull request adds issue-scoped egress grants that become
workload-owned, run-label-selected policies and carries the effective
grant through lease audit metadata
> - The benefit is that internet-dependent work can run without enabling
broad egress for every concurrent task, and denied requests point
operators to the exact grant path

## Linked Issues or Issue Description

No public issue exists. Related but distinct: Refs #9944, which adds a
provider-wide open-internet posture; this PR keeps provider defaults
narrow and adds per-task grants.

**Problem / motivation**
Kubernetes sandbox egress is configured at the provider/tenant level. A
task that needs to clone from GitHub or install from PyPI cannot request
those destinations without changing the policy for every run in the
tenant namespace. DNS/connectivity failures also surface as generic tool
errors with no policy name or remediation path.

**Proposed solution**
Accept `executionWorkspaceSettings.networkEgress.allowFqdns` and
`allowCidrs`, forward the setting through heartbeat environment
acquisition, and create a workload-owned NetworkPolicy or
CiliumNetworkPolicy selected by `paperclip.io/run-id`. Record the
effective grant in lease activity/metadata, expose policy context
through `PAPERCLIP_NETWORK_EGRESS_*`, and append the grant path to
likely policy-related stderr failures.

**Alternatives considered**
A provider-wide open-internet switch is broader than required and is
already covered by #9944. Mutating the existing namespace policy would
leak each task's destinations to other concurrent runs. Standard
Kubernetes NetworkPolicy cannot enforce FQDNs exactly, so standard mode
uses the existing hardened public-IPv4 TCP 80/443 fallback only for the
selected run; Cilium mode remains exact.

**Roadmap alignment**
This extends the existing cloud/sandbox agent roadmap capability with
task-level control-plane policy and does not duplicate a planned roadmap
item.

## What Changed

- Added validated `networkEgress` grants to issue execution workspace
settings and forwarded them through environment lease acquisition.
- Added workload-owned, run-label-scoped
NetworkPolicy/CiliumNetworkPolicy resources for task FQDN/CIDR grants.
- Added lease audit metadata, sandbox policy environment variables, and
actionable network-denial stderr guidance.
- Added focused parser, manifest, policy creation, and denial-message
tests plus Kubernetes provider documentation.

## Verification

- `pnpm -C packages/shared exec vitest run src/validators/issue.test.ts`
— 27 passed.
- `pnpm -C packages/plugins/sandbox-providers/kubernetes test -- --run
test/unit/network-policy.test.ts test/unit/cilium-network-policy.test.ts
test/unit/scoped-network-egress.test.ts` — 21 passed.
- `pnpm -C server exec vitest run
src/__tests__/execution-workspace-policy.test.ts` — 15 passed.
- `pnpm exec vitest run
server/src/__tests__/heartbeat-plugin-environment.test.ts
server/src/__tests__/environment-runtime.test.ts` — 26 passed.
- `pnpm --dir packages/db build && pnpm --dir packages/shared build &&
pnpm --dir packages/plugins/sdk build` — passed, including migration
safety checks.
- `pnpm --dir packages/plugins/sandbox-providers/kubernetes typecheck &&
pnpm --dir server typecheck` — passed after refreshing the worktree's
frozen offline dependencies.
- End-to-end cluster validation of the `build-cython-ext` benchmark
remains for CI/maintainer Kubernetes infrastructure; the focused tests
assert `github.com` and `pypi.org` produce a policy selected only by the
granted run.

## Risks

- Standard NetworkPolicy cannot express FQDNs, so an FQDN grant allows
hardened public IPv4 TCP 80/443 for that run; use Cilium mode for exact
hostname enforcement.
- The new field is additive and absent by default, so existing runs keep
the current provider-level policy.
- Workload owner references garbage-collect scoped policies with the
Job/Sandbox; a cluster/controller that ignores owner references could
temporarily strand a policy that still selects no future run ID.

> 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 `gpt-5.6-sol`, high reasoning mode, tool
use and code execution. 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-07-24 09:58:58 -05:00
Nicky Leach 564870020b
Exclude archived projects from the default project list route (#10146) 2026-07-24 07:19:58 -07:00
Michael Nguyen caae2778f0
fix: deliver plugin agent session turns and replies (#10137)
## Thinking Path

> - Paperclip manages agent execution through heartbeat runs and
adapter-specific sessions
> - Plugins can open an agent session and send a conversational message
through the host service
> - The host previously stored that message only in opaque wake payload
metadata, so local adapters never saw it in their CLI prompt
> - The host also forwarded run log chunks but did not expose the
persisted final assistant text as the session reply
> - This pull request defines both sides of the session contract in the
shared wake renderer and terminal run event
> - The benefit is that local adapters receive the actual conversational
turn and plugins receive one canonical final reply

## Linked Issues or Issue Description

Related context: Refs #629 and Refs #2880 describe adjacent
`claude_local` final-text visibility failures. They concern issue
comments rather than plugin agent sessions, but exercise the same need
for a canonical persisted run summary.

Companion consumer change: paperclipai/paperclip-gateway#3.

Bug description:

- **Observed:** calling the plugin host's
`agents.sessions.sendMessage()` with `prompt: "hello"` woke a
`claude_local` agent, but the generated CLI prompt omitted `hello`. On
completion, the session emitted log chunks and a generic `Run completed`
done event, so callers could not reliably recover the assistant reply.
- **Expected:** the prompt becomes the user-supplied conversational turn
for that agent session, and the successful terminal event carries the
run's canonical final user-facing assistant text.
- **Reproduction:** create a plugin agent session for a local adapter,
call `sendMessage()` with a non-empty prompt, inspect the adapter prompt
and terminal session event.
- **Affected baseline:** `b517b887a` on `master`, local trusted
deployment with plugin host services and `claude_local`; `codex_local`
shared the wake-rendering gap because both use the common Paperclip wake
prompt renderer.

## What Changed

- Added a typed `agentMessage` wake payload rendered by the shared
adapter prompt path used by `claude_local`, `codex_local`, and other
local adapters.
- Labeled session content as user-supplied and explicitly
non-authoritative: it cannot expand authorization, permissions, task
scope, or company boundaries.
- Preserved ordinary heartbeat behavior by omitting the section when no
agent-session message exists.
- Added canonical `finalText` to terminal heartbeat status events from
the already-persisted run summary/result/message.
- Defined successful `AgentSessionEvent.message` as the canonical final
user-facing reply (or `null`) and forwarded it on the terminal `done`
event.
- Added host, wake-renderer, normal-heartbeat, and terminal-reply
regression coverage.

## Verification

- `pnpm exec vitest run packages/adapter-utils/src/server-utils.test.ts
server/src/__tests__/heartbeat-agent-session-message.test.ts
server/src/__tests__/heartbeat-run-status-payload.test.ts
server/src/__tests__/plugin-agent-sessions.test.ts
server/src/__tests__/heartbeat-run-summary.test.ts` — 87 passed.
- `pnpm -r typecheck` — passed across all 31 workspaces.
- `pnpm build` — passed.
- `pnpm test:run` — 2,860 passed, 1 skipped, 3 unrelated failures: two
existing macOS temp-path alias assertions (`/tmp` vs `/private/tmp`) in
workspace branch-containment tests and one reproducible auto-port
runtime-service adoption failure. The same three failures reproduce when
the two files run alone; none touch this change.
- Live Slack verification intentionally remains operator-gated because
it requires rebuilding/restarting the host.

## Risks

- User-controlled chat text now reaches the model prompt, which is an
intentional prompt-injection surface. The renderer labels it as
untrusted conversational content, while the existing plugin/session
company checks and caller authorization remain unchanged.
- `finalText` is added to company-scoped heartbeat status events. It is
derived from the same persisted summary/result/message already used for
run comments; no raw stdout or secrets are added.
- Consumers that ignore the new field remain compatible, and successful
runs without usable final text still emit `message: null`.

> 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), agentic reasoning with repository/tool use and
code execution; context-window size is not surfaced in this environment.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-07-23 20:52:42 -07:00
Nicky Leach e41ba306c5
feat(secrets): thread audit actor into skip-user-secret skills routes (#10124)
## Thinking Path

> - Paperclip manages agent work and needs auditable control over secret
resolution
> - The skip-user-secret skills routes still have to attribute access to
the real actor
> - These routes were calling the adapter config resolver without an
access context
> - That dropped actor attribution from the company `secret_ref` audit
trail
> - This pull request threads the existing actor-secret context helper
into both skills routes
> - The benefit is that audit fidelity is restored without changing
`skipUserSecrets` behavior

## Linked Issues or Issue Description

Refs #10115.

This PR fixes a gap in the skills read/sync routes where
`resolveAdapterConfigForRuntime` was being called without an audit
access context, so company secret resolution could not reliably
attribute the request to the acting user or agent. The change keeps
`skipUserSecrets: true` intact and only restores audit fidelity.

## What Changed

- Threaded `buildActorSecretContext(req, { consumerType: "agent",
consumerId })` into `GET /agents/:id/skills`
- Threaded the same actor context into `POST /agents/:id/skills/sync`
- Updated the route tests to assert a non-`undefined` actor context
reaches the resolver while `skipUserSecrets: true` stays unchanged

## Verification

- `tsc --noEmit`
- `agents` and `secrets` Vitest suites: 33 files / 448 tests green
- Route spy assertions confirm both skills routes now pass an
actor-derived context to the resolver

## Risks

- Low risk: the change is limited to audit context propagation on two
skills routes
- If a downstream resolver assumes the third argument can be
`undefined`, this makes the context explicit on these routes
- The user-secret authorization behavior does not change because
`skipUserSecrets` remains true

## Model Used

OpenAI GPT-5 via Codex, tool-using coding agent, 256k 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 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: Harold Kim <harold@paperclip.ing>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-07-23 15:31:55 -07:00
Devin Foley 176a9e8230
fix(built-in-agents): allow first-time setup of a needs_setup built-in under board-approval policy (#10129)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Companies can require **board approval for new agents**; built-in
agents (e.g. the Reflection Coach / Briefs) are provisioned through the
`built-in-agents` service `provision()`
> - Some built-in agents are *auto-provisioned* as a hire that, once
approved, resolves to an idle agent row whose `adapterConfig` is still
empty — status `needs_setup`
> - When the board operator then opens that agent's setup dialog and
submits the adapter config, `provision()` saw
`adapterType`/`adapterConfig` on an already-existing row and classified
it as a **reconfiguration**, throwing a dead-end 409: *"Built-in agent
adapter changes require board approval before they can be applied."*
> - The operator *is* the board, so there was no one left to grant an
approval they already implicitly hold — setup could never be completed
> - This pull request treats first-time adapter setup of a `needs_setup`
built-in as the first-time configuration it actually is, applying it
directly while still gating genuine reconfiguration of a live agent
> - The benefit is the board can finish setting up an auto-provisioned
built-in agent without hitting an unsatisfiable approval wall

## Linked Issues or Issue Description

<!-- No public GitHub issue exists; describing the underlying bug in-PR
following the bug_report template. -->

**What happened?**

With "require board approval for new agents" enabled, completing the
adapter setup of an auto-provisioned but unconfigured built-in agent
(status `needs_setup`, e.g. the Reflection Coach) failed with a 409 —
*"Built-in agent adapter changes require board approval before they can
be applied."* — even for the board user. Because the operator *is* the
board, no additional approver existed, so setup was permanently blocked.
Root cause: in `builtInAgentService.provision()`, any request carrying
`adapterType`/`adapterConfig` against an existing row was treated as a
reconfiguration and gated, regardless of whether that row had ever
completed its initial adapter setup. An auto-provisioned hire resolves
to an idle row with an empty `adapterConfig` (`needs_setup`), so its
very first configuration was misclassified.

**Expected behavior**

The board can complete first-time setup of an already-sanctioned
built-in agent without a fresh approval, matching the behavior when
board approval is not required. Genuine reconfiguration of an
already-configured (`ready`/`paused`) agent should still require
approval.

**Steps to reproduce**

1. In a company with `requireBoardApprovalForNewAgents` enabled, have a
built-in agent auto-provisioned so its row exists but its adapter is
unconfigured (status `needs_setup`).
2. As the board user, open that agent's setup dialog and submit an
adapter type + config.
3. Observe the 409 "Built-in agent adapter changes require board
approval before they can be applied." with no way for the board to grant
the approval.

**Deployment mode**

Local single-instance / self-hosted (server `built-in-agents` service).

## What Changed

- `server/src/services/built-in-agents.ts`: In `provision()`, when the
existing built-in row has **not** yet completed adapter setup
(`!hasCompleteAdapterConfig(...)`, i.e. `needs_setup`), first-time
adapter configuration now applies directly via `ensure()` — the same
path used when board approval is not required. The hire that created the
row was already sanctioned, so no fresh approval is required.
- Reconfiguration of an already-configured (`ready`/`paused`) built-in
agent stays gated behind board approval exactly as before, and
`pending_approval` rows are handled before the new branch.
- `server/src/__tests__/built-in-agents.test.ts`: Added a regression
test — under `requireApproval: true`, completing first-time setup of a
`needs_setup` built-in returns `approval: null`, transitions the agent
to `ready`, and creates **no** approval row.

## Verification

```bash
cd server
npx vitest run src/__tests__/built-in-agents.test.ts
# Test Files  1 passed (1)
#       Tests  31 passed (31)
```

- New test `completes first-time setup of a needs_setup built-in without
a fresh board approval` passes.
- Full `built-in-agents.test.ts` suite (31 tests) passes, including
existing tests that assert genuine reconfiguration of a configured agent
**remains** gated.

## Risks

Low risk. The change narrows an over-broad approval gate: it only opens
the direct-apply path for rows that have never completed adapter setup
(`needs_setup`), determined by the existing `hasCompleteAdapterConfig`
predicate that already drives `deriveBuiltInAgentStatus`.
Already-configured (`ready`/`paused`) agents, and `pending_approval`
rows, are unaffected and still gated. No schema or migration changes.

## Model Used

Claude Opus 4.8 (`claude-opus-4-8`), 1M context, extended thinking, with
tool use / 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 (searched my open PRs and compared patch-ids — no duplicate
exists)
- [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 considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
2026-07-23 14:53:15 -07:00
Michael Nguyen e2068319e7
fix(interactions): tolerate legacy stored result outcomes so listInteractions can't fail the whole list (#10119)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Agents and humans coordinate on issues through interaction requests
(confirmations, decisions, task suggestions and more) that are stored
per issue and listed by both the web UI and plugin workers such as chat
gateways
> - `listForIssue` hydrates every stored interaction row by hard-parsing
its persisted `result` blob against the current Zod schema
> - Stored rows outlive code: one live row written by an older build
carried `result.outcome: "withdrawn_by_creator"`, a value no longer in
the enum, and that single row made hydration throw
> - Because the throw happened inside the list mapping, it failed the
entire issue's interaction list — the web thread errored, and every
plugin consumer of `issues.listInteractions` (notification drain, digest
confirmation sweep, pending-ledger reads) failed continuously, so
interaction cards never reached chat surfaces
> - This pull request parses stored `result` blobs tolerantly — a
`parseStoredInteractionResult` helper wrapping `safeParse`, applied to
all five interaction kinds — so an unparseable result degrades to `null`
with a warning instead of failing the whole list
> - The benefit is durable robustness at the storage→hydrate boundary:
legacy or future schema drift in a single row can no longer take down an
issue's entire interaction surface

## Linked Issues or Issue Description

No pre-existing public issue; the underlying problem is described here
following the bug-report template. Related (not a duplicate): Refs #6709
— the creator-withdraw flow it explores matches the legacy outcome value
observed in the wild; whether or not that lineage wrote the row, this PR
is defensive against any such stored-schema drift.

**What happened**

Listing interactions for an issue (`GET /api/issues/:id/interactions` on
the web, or the `issues.listInteractions` plugin RPC) fails for the
entire issue when any single stored interaction row carries a
`result.outcome` written by an older build (observed live:
`"withdrawn_by_creator"`). Downstream plugin consumers that poll this
RPC fail continuously — notification drain, digest confirmation sweep,
and pending-ledger reads.

**Expected behavior**

One legacy/unreadable stored `result` should degrade gracefully — the
interaction still lists with its result treated as absent — rather than
failing the whole issue's interaction list.

**Steps to reproduce**

1. Persist a resolved `request_confirmation` interaction whose
`result.outcome` is not in the current enum (e.g.
`"withdrawn_by_creator"`, as written by an older build).
2. Call `issues.listInteractions` (or `GET
/api/issues/:id/interactions`) for that issue.
3. The call throws `invalid_enum_value` and returns nothing, instead of
returning the remaining rows.

**Version or commit**

master @ 3093c5e69 (also reproduces on a live deployment carrying
pre-enum-change rows).

**Deployment mode**

Self-hosted host with plugin workers (chat gateway).

## What Changed

- Added `parseStoredInteractionResult`, a small generic helper in
`server/src/services/issue-thread-interactions.ts` that wraps Zod
`safeParse` for stored `result` blobs: on parse failure it logs a
warning and returns `null` instead of throwing.
- Replaced all five hard `.parse()` calls in `hydrateInteraction` (one
per interaction kind) with the tolerant helper, so a single unreadable
row degrades to `result: null` rather than failing the entire
`listForIssue` mapping.
- Left payload parsing strict on purpose — payloads are written at
creation time by current code; only `result` has demonstrated legacy
drift, and keeping payloads strict preserves detection of genuine
write-path bugs.
- Added a regression test in
`server/src/__tests__/issue-thread-interactions-service.test.ts` that
seeds a resolved `request_confirmation` with `result.outcome:
"withdrawn_by_creator"` and asserts `listForIssue` returns the row with
`result: null` instead of throwing.

## Verification

- `tsc --noEmit` (server) — clean.
- `issue-thread-interactions-service.test.ts` — 39/39 pass, including
the new regression test reproducing the exact live failure value.
- Full CI on this PR is green: typecheck, serialized server suites,
general tests, e2e shards, build, canary dry run.

## Risks

- Low: server-only change at the read/hydrate boundary; no schema or
write-path changes, no SDK dist rebuild.
- Behavioral shift: a resolved interaction with an unreadable stored
`result` now lists with `result: null`. Consumers already handle
`result: null` (it is the shape of every unresolved interaction);
anything assuming "resolved ⇒ non-null result" sees the legacy row
differently than before — though previously the same row produced a hard
failure of the whole list, so this is strictly an improvement.
- The degrade path logs a warning, so stored-schema drift stays visible
rather than silent.

## Model Used

- Claude (Anthropic) — via the Claude Code CLI agent.
- Exact model ID: `claude-fable-5` (Claude Fable 5).
- Extended thinking (chain-of-thought reasoning) enabled; agentic tool
use including file editing and local 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
- [ ] I have not referenced internal/instance-local Paperclip issues or
links — *the PR title, description, and comments are clean, but the
branch commit message carries an internal ticket id from the originating
workspace; this repo squash-merges, so the final master commit takes the
clean PR title and the interim message never lands*
- [ ] My branch name describes the change and contains no internal
Paperclip ticket id — *the branch was pushed before this check; renaming
now would close this PR and discard its green CI, and the branch name is
likewise dropped at squash-merge*
- [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 is affected by this server-internal fix)
- [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

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

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-07-23 14:47:27 -07:00
Dotta e3f8380e70
feat(skills): make summarize-status actions-first (#10117)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Its built-in Summarizer keeps status slots useful for people
overseeing issue trees
> - Those summaries need to tell the reader what they must do now to
unblock progress
> - The existing skill instead imposed rigid Decide:/Review:/Recent
work: sections, cost commentary, and restrictive issue-fetch guidance
> - This pull request rewrites the summarize-status instructions to lead
with 1–3 specific, concrete unblock actions while letting the model use
its judgment for the remaining context
> - The benefit is a shorter, clearer summary that is immediately
actionable without changing slot writes or the streaming status protocol

## Linked Issues or Issue Description

Refs #9713

The built-in summarizer currently prioritizes a fixed reporting template
over the reader's immediate unblock actions. Summaries should instead
open with the 1–3 specific actions the reader needs to take right now,
then provide only the context needed to act. This prompt-only update
preserves all summary-slot mechanics and protocols.

## What Changed

- Rewrote the bundled `summarize-status` skill to open with 1–3
specific, concrete, actionable items needed right now to unblock the
work.
- Removed the rigid Decide:/Review:/Recent work: template, the Cost
discipline section, and the restrictions against fetching issue detail.
- Kept slot-write mechanics and the streaming `STATUS`/sentinel protocol
unchanged.
- Updated all materialized copies and tests for the same skill text: the
`SKILL.md` source, regenerated catalog manifest hashes, compiled
fallback string, summarizer built-in `AGENTS.md` and routine, summary
generation-issue instructions, and the two tests pinning those strings.
- Although the diff touches eight files, every file is either the same
skill text in another materialized form or a test asserting it. No
behavior outside the summarizer's prompt text changes.

## Verification

- `pnpm --filter @paperclipai/skills-catalog test` — 20/20 tests pass.
- `pnpm exec vitest run server/src/__tests__/summary-slots.test.ts
server/src/__tests__/built-in-agents.test.ts` — 46/46 tests pass.
- `git diff --check origin/master...HEAD` — clean.
- `pnpm exec vitest run server/src/__tests__/summary-slots.test.ts` —
16/16 tests pass after the Greptile consistency fix.
- Latest-head GitHub checks — 25 terminal checks, all successful,
neutral, or skipped.

## Risks

- Low risk: this intentionally changes generated summary wording and
prioritization, but does not change APIs, persistence, slot-write
behavior, or the streaming protocol.
- The branch name contains an internal task identifier because it was
pre-created and pre-pushed for this assigned change; the PR title and
body do not expose the internal ticket.

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

## Model Used

- OpenAI Codex using `gpt-5.6-sol`, high reasoning mode, with
repository, terminal, GitHub CLI, and code-execution 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)
- [ ] 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: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-07-23 15:59:44 -05:00
Dotta 148a5b11f5
Route blocked transitions to explicit unblock owners (#10112)
## Thinking Path

> - Paperclip is the open source control plane people use to coordinate
AI-agent companies.
> - Issue status transitions determine whether work keeps moving or
silently stalls.
> - A blocked issue previously could rely on prose alone, leaving the
intended unblock owner unstructured and unnotified.
> - Existing blocker-attention classification could identify stalled
chains, but the signal was not delivered to the board attention feed.
> - Blocked transitions also need rollout-safe deduplication so upgrades
do not notify for historical issues and repeated processing does not
create notification storms.
> - This pull request adds structured unblock descriptors, prospective
transition timestamps, owner delivery, and board attention routing with
focused authorization controls.
> - The benefit is that newly blocked work has an explicit, routable
next action without weakening company boundaries or allowing agents to
inject arbitrary human attention items.

## Linked Issues or Issue Description

Related documentation PR: #10094.

### Subsystem affected

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

### Problem or motivation

An issue can enter `blocked` without a machine-readable unblock path.
Prose-only ownership does not reliably wake the responsible agent or
surface human-owned work, while the existing `blockerAttention`
classifier is not delivered to an operator-facing attention feed.

### Proposed solution

Require new transitions into `blocked` to have unresolved blockers, a
pending interaction/approval, or a structured `{ owner, action }`
descriptor. Notify an allowed owner once per prospective transition,
route human-owned cases to board attention, and leave pre-rollout
blocked issues untouched.

### Alternatives considered

- Keep prose-only blockers: rejected because ownership remains
unroutable.
- Backfill all historical blocked issues: rejected because upgrades
would create notification storms.
- Let agents target arbitrary users or the board: rejected after
security review because it creates an attention-injection channel.

### Roadmap alignment

Aligns with `ROADMAP.md` → “Enforced Outcomes (watchdogs, recovery
actions, review gates)” by making blocked work carry an explicit
continuation path.

### Additional context

The implementation is prospective-only and deduplicated per blocked
transition. Agent-authored descriptors are limited to the acting agent;
board actors retain human-owner routing.

## What Changed

- Added persisted unblock descriptors and prospective blocked-transition
delivery timestamps with an idempotent migration.
- Added shared types and validation for board, user, and agent unblock
owners.
- Enforced valid blocked transitions and same-company owner validation
in the issue update route.
- Restricted agent-authored descriptors to the acting agent itself,
preventing board/user attention injection by compromised agents.
- Added one-per-transition agent wake delivery and prospective-only
rollout gating.
- Routed human-owned blocker attention into the board attention feed.
- Added focused tests for validation, prospective delivery, flap
deduplication, attention routing, route authorization, and stop-relay
compatibility.

## Verification

- `pnpm -r typecheck`
- `pnpm exec vitest run
server/src/__tests__/issue-agent-mutation-ownership-routes.test.ts
server/src/__tests__/routable-blocked.test.ts
server/src/__tests__/attention-service.test.ts
packages/shared/src/validators/issue.test.ts`
- `AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= pnpm test:run`
- `pnpm build`
- `pnpm --filter @paperclipai/db check:migrations`

## Risks

- Behavioral shift: new `blocked` transitions without a real blocker,
pending governed action, or structured descriptor now return `422`.
- Notification abuse is constrained by same-company validation, agent
self-only routing, prospective rollout gating, and transition-scoped
deduplication.
- Migration risk is low: columns are additive, nullable, and use `IF NOT
EXISTS`; historical blocked issues are not backfilled or notified.

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

## Model Used

- OpenAI Codex CLI with GPT-5.4, reasoning-enabled tool use and code
execution. The runtime did not expose 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
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-07-23 15:49:28 -05:00
Nicky Leach 81f47e70a6
feat(secrets): thread the acting user into user-scoped secret resolution (#10115)
## Thinking Path

> - Paperclip is the control plane for autonomous AI companies
> - Its agents and adapters need to resolve secrets through the same
governed runtime path that checks ownership and company boundaries
> - This change fixes a gap where user-scoped secret resolution could
lose the acting-user context before adapter runtime startup
> - Without that context, a required user secret could fail closed with
responsible_user_missing even though an authenticated user was in scope
> - This PR threads the acting user into the user-scoped secret
resolution path and keeps the owner boundary explicit
> - The benefit is adapter runtime setup can resolve the right
credential without broadening access

## Linked Issues or Issue Description

Refs #8309 (related: agent secret_ref env drift and binding context)

No exact public GitHub issue for this specific behavior.

### Bug report

- Problem: two agent-management routes resolved user-scoped secrets
without an acting-user binding, so a required `user_secret_ref` could
not be resolved before runtime.
- Expected behavior: the authenticated acting user should be threaded
into user-scoped secret resolution so the owning user secret can be
selected safely.
- Actual behavior: adapter startup paths failed closed with
`responsible_user_missing` even though a user was already in scope.
- Steps to reproduce: configure an adapter test-environment or login
flow that depends on a user-scoped secret, then invoke it with an
authenticated user context that does not carry the acting-user binding
into runtime secret resolution.
- Impact: the adapter test-environment probe and login path cannot
start, so the runtime never reaches the work it was supposed to do.

## What Changed

- Added an actor secret-context helper so the server can derive
responsible-user context without inventing config-path or binding
allowlists.
- Added an explicit user-secret mediation mode for runtime config
resolution, with an owner-scoped path that resolves by definition plus
owner boundary and fails closed when an allowlist is present.
- Wired the adapter test-environment route to owner-scoped mediation
with an audit-only consumer and kept claude-login on the declared path
with its persisted agent identity.
- Added and updated tests for the factory, owner-scoped resolver mode,
and adapter route coverage.

## Verification

- `tsc --noEmit` clean
- Factory tests: `authz-secret-context` 5/5
- Service tests: `secrets-service-user-secret-owner-scoped` 5/5,
including fail-closed allowlist coverage and company-secret
non-regression
- Route tests: `agents-adapter-config-user-secret` 5/5, including
`responsible_user_missing` and `binding_missing` coverage
- Regression suites: `agents` + `secrets` 194/194

## Risks

- A regression in the owner-scoped mediation path could accidentally
loosen secret access if the audit consumer or allowlist guard changes.
- The change depends on the server-derived responsible user; if auth
context regresses, the system should fail closed with
responsible_user_missing.
- The new mediation mode adds a branch in runtime config resolution, so
future changes need to keep declared-mode behavior intact.

## Model Used

- OpenAI GPT-5 (Codex tool-use session)

## 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 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: Harold Kim <harold@paperclip.ing>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-07-23 13:30:26 -07:00
Michael Nguyen f2f168f6a1
fix(plugins): seed proactive company scopes before worker setup() + events.subscribe resolver parity (LOOA-695) (#10113)
- [x] I searched the GitHub PR list for similar PRs (dedup search). No
open PR touches the proactive `events.subscribe` ordering path; #10103
(merged) is the predecessor whose ordering bug this fixes.

## Thinking Path

The gateway worker's outbound push path is permanently dead
(`eventSubscriptions: 0`, `notifier.received: 0`, `decisions.delivered:
0`). The plugin loader authorizes the worker's **proactive company
scopes only AFTER `startWorker` resolves**, but a proactive plugin
issues its one-shot `events.subscribe` calls from `setup()` — which runs
*while `startWorker` is still awaiting the worker's initialize
response*.

So at subscribe time `proactiveCompanyScopes` is still empty →
`contextForWorkerMessage` resolves no scope → the governed-access gate
rejects every subscribe with `company context is required`. The gateway
subscribes once and never retries, so `eventSubscriptions` stays 0 for
the worker's life. This is an **ordering bug in the #10103 fix**, not a
new method — same #9557 governed-access class as `config.get` (#10092)
and `state.get` (#10103).

Confirmed live at the 18:21:21Z worker respawn on `3093c5e` (host log),
and again at the 19:01:04Z restart (still `events.subscribe: company
context is required`, `eventSubscriptions:0`).

## What Changed

1. **Loader ordering** (`plugin-loader.ts`): load
`registry.listConfigs(pluginId)` in a new step 4b **before**
`startWorker`, and thread the configured company set into
`WorkerStartOptions.proactiveCompanyScopes` so the worker handle is
authorized *before the child process issues any host call*. The same
rows are reused for startup config delivery (step 5b) — no second
`listConfigs` round-trip. The runtime config-change path
(`routes/plugins.ts`) still refreshes scopes via
`setProactiveCompanyScopes` (unchanged).
2. **Handle seed** (`plugin-worker-manager.ts`):
`createPluginWorkerHandle` seeds its `proactiveCompanyScopes` set from
options at creation, before spawn.
3. **Resolver/gate parity** (`plugin-worker-manager.ts`):
`referencedCompanyId(method, params)` now mirrors the SDK gate
`requestedCompanyScope` exactly in the functional direction — adds
`events.subscribe → params.filter.companyId` (how `ctx.events.on(name, {
companyId }, fn)` issues its subscribe), and declines the gate's
wildcard cases (`companies.list`, `scopeKind:"company"` without
`scopeId`) so proactive access only ever grants a **single explicit
configured company, never "all"**. Answers LOOA-693 AC#4 (host/gate
extraction parity) in the functional direction.

## Tests

New `plugin-worker-manager.test.ts` cases (drive a real worker):
- a `setup()`-time `events.subscribe({ filter: { companyId } })` for an
options-seeded company is **admitted** (fails on prior code — no options
seed, no filter parity);
- an unconfigured company stays **denied**;
- an unseeded worker stays **denied**.

Full `plugin-worker-manager.test.ts` suite: **21 passed**. Server `tsc
--noEmit`: clean. All PR CI green (typecheck, server/workspace suites,
e2e, build, security scans).

## Risks

- **Scope-widening risk (primary).** The change grants proactive host
access keyed off configured company rows. Mitigated by: the authorized
set is exactly `registry.listConfigs(pluginId).map(companyId)`; wildcard
cases (`companies.list`, company-scoped key without `scopeId`) resolve
to `null`, never `{ kind: all }`; empty/whitespace ids dropped; an empty
config set grants zero proactive access. This is the surface
SecurityEngineer must sign off (see Security gate).
- **In-invocation path unchanged.** Calls carrying a host-issued
`paperclipInvocationId` keep the existing strict single-company match;
the proactive branch only applies when there is no invocation id — so no
regression to the enforced request path.
- **Blast radius.** Loader step 4b is best-effort: a `listConfigs`
failure logs and proceeds with an empty seed (fails closed — no push,
not a crash), matching today's behavior.

## Model Used

Claude Opus 4.8 (`claude-opus-4-8`) via Claude Code (agent: CTO).

## Security gate

Touches the company-scope resolution path (same surface as #10103).
Routed through **SecurityEngineer review before merge** (tracked on
LOOA-696) — must not widen beyond configured companies; in-invocation
strict single-company match untouched; wildcard cases deliberately
declined in the proactive direction.

## Verification once live

- Host log clean of `events.subscribe: company context is required` at
worker start
- loader logs `eventSubscriptions: N>0`
- beat `notifier.received` / `decisions.delivered` move on real
issue/approval activity

Parent: LOOA-629 (outbound push half of "gateway active"). LOOA-695.

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

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 12:32:10 -07:00
Michael Nguyen 3093c5e694
fix(plugin-worker): resolve a company scope for proactive worker→host calls (#10103)
Host authorizes the plugin's configured companies as the worker's proactive scopes, set by the loader right after the #10092 config-delivery step and refreshed on operator config-save. At the single worker→host chokepoint, a no-invocation call (notifier drain, decision reconcile, mirror drain, digest, aging, liveness beat) that references a configured company resolves to that company's scope, so the #9557 governed-access gate admits it. One change covers the full proactive surface (state.*, issues.*, approvals.*, config.get, secrets.resolve, etc.).

Safety: never widens beyond configured companies (any other company stays denied); in-invocation calls keep #9557's strict single-company match untouched.

Fixes the Slack gateway DM round-trip for LOOA-629. Security review PASS (LOOA-693); non-blocking LOW follow-up tracked in LOOA-694.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-07-23 11:06:02 -07:00
Dotta a17bee98f2
Allow trust-gated direct-parent issue reports (#10098)
## Thinking Path

> - Paperclip is the control plane used to coordinate and govern
AI-agent companies.
> - Agent issue access must preserve company boundaries and trust-policy
containment without preventing legitimate task coordination.
> - Checked-out standard-trust child runs need a narrow way to report
progress directly to their parent issue, but existing authorization
treated that report like an arbitrary cross-boundary write.
> - Low-trust review runs must remain contained, and stop propagation
must not copy potentially untrusted child prose into a higher-trust
parent context.
> - This pull request adds an audited, one-hop direct-parent comment
grant only for standard checked-out runs and a sanitized, idempotent
relay for blocked or cancelled child stops.
> - The benefit is restored parent/child liveness while retaining least
privilege, complete mediation, and low-trust output quarantine.

## Linked Issues or Issue Description

### What happened?

A standard-trust agent running a checked-out child issue could not post
a progress comment to the direct parent issue because the authorization
boundary treated it as an arbitrary cross-issue write. This could stall
parent/child coordination. Low-trust review runs also need stop
propagation without exposing quarantined child-authored prose.

### Expected behavior

A standard checked-out child run may add a comment only to its direct
parent issue. The grant must not allow grandparent or sibling access,
issue mutation, document writes, reopening, or resuming. Low-trust runs
remain denied unless separately mentioned, while blocked/cancelled stops
relay only sanitized system metadata once.

### Steps to reproduce

1. Create a parent issue and a child issue assigned to different
standard-trust agents.
2. Check out the child issue in a heartbeat run and authenticate as that
run.
3. Post a comment to the parent issue and observe the authorization
denial before this change.
4. Mark a low-trust child blocked or cancelled and observe that no
bounded sanitized parent notification preserves liveness before this
change.

### Paperclip version or commit

Reproduces on `master` before this PR, including base commit
`d36ea13e08`.

### Deployment mode

Local dev (`pnpm dev`).

### Installation method

Built from source (`pnpm dev` / `pnpm build`).

### Agent adapter(s) involved

Not adapter-specific (core authorization and issue-routing behavior).

### Database mode

External Postgres in the focused route regression suite; behavior is
database-mode independent.

### Access context

Agent (bearer API key associated with a checked-out heartbeat run).

### Additional context

The implementation deliberately distinguishes a direct-parent report
decision from general issue mutation permission and records successful
grants in the activity log.

### Privacy checklist

- [x] I have reviewed all pasted output for PII, API keys, tokens,
company names, and private instance references.

## What Changed

- Adds a distinct authorization decision for standard checked-out runs
commenting on their direct parent issue.
- Keeps low-trust direct-parent reports denied unless an existing
explicit mention grant applies.
- Forces direct-parent grants to remain comment-only even when a closed
parent is unassigned or assigned to the reporting agent.
- Audits successful direct-parent report grants in issue activity
details.
- Adds sanitized, parent-scoped, idempotent system comments and parent
wakeups for blocked or cancelled child stops.
- Extends the low-trust red-team route suite for allowed parent reports,
forbidden upward/sibling writes, closed-parent mutation suppression, and
non-laundering stop relays.

## Verification

- `pnpm exec vitest run
server/src/__tests__/low-trust-red-team-routes.test.ts` — 11 tests
passed after the review fix.
- `pnpm --filter @paperclipai/server typecheck` — passed after the
review fix.
- Confirmed the PR changes four files and excludes `pnpm-lock.yaml`,
workflow changes, migrations, and unrelated branch commits.

## Risks

- This is an authorization behavior change. An overly broad grant could
enable cross-boundary writes, while an overly narrow grant could
preserve the liveness failure.
- The implementation constrains the grant to a standard-trust
checked-out run, a direct parent target, and comments only; activity
auditing and red-team coverage make regressions observable.
- Stop relays intentionally contain only system-generated child
identity/status metadata and are deduplicated; child-authored prose is
not copied.
- SecurityEngineer approval is mandatory before merge.

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

## Model Used

- OpenAI Codex using GPT-5.5 with reasoning, repository tool use, shell
execution, and test 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-07-23 13:03:04 -05:00
Devin Foley 429792f1f3
fix(interactions): stop wedging confirmation accept on a terminal workspace_finalize (#10099)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - When an agent finishes work in an execution workspace, the board can
confirm the result through an issue-thread interaction (e.g. the
"Merged" / mark-done confirmation button on a `request_confirmation`).
> - That accept action is gated: it must not race a worktree sync-back
(`workspace_finalize`) that is still copying the agent's commits out of
the sandbox, or the board could act on a base that hasn't received them
yet.
> - The gate (`runWorkspaceIsFinalized`) treated the sync-back as
"settled" only when the latest `workspace_finalize` op was `succeeded` —
so a run whose finalize reached a terminal `failed` state, or died
leaving a stale `running` op, was treated as "still syncing" forever.
> - Users hit a permanent, misleading `... has not finished syncing its
workspace` error and could never click "Merged", even though nothing was
syncing and the run had long since ended.
> - This PR fixes the settle semantics so the gate blocks only while a
sync-back is genuinely pending or in flight, and treats any terminal (or
stale-orphaned) finalize as done.
> - The benefit is that a failed or abandoned sync-back no longer wedges
the human confirmation, while a genuinely in-flight sync-back on a live
run still blocks correctly.

## Linked Issues or Issue Description

No public GitHub issue exists for this. Describing the bug in-PR (bug
report):

**What happened**

Clicking the "Merged" / mark-done confirmation at the bottom of an issue
thread returns an error that the workspace "has not finished syncing its
workspace" — but nothing is actually syncing, and the run that created
the interaction has already ended. The confirmation is permanently
stuck; the only workaround is to merge and mark the task done manually.

**Expected behavior**

Once the source run's worktree sync-back has finished — whether it
succeeded, failed, or was skipped — the confirmation should be
acceptable. The gate should block only while a sync-back is genuinely
still running on a live run.

**Steps to reproduce**

Have an agent run reach `workspace_finalize` and end without a
`succeeded` finalize (e.g. the sync-back fails, or the run process dies
mid-finalize leaving a `running` op). Then attempt to accept the
`request_confirmation` interaction it created → 409 "... has not
finished syncing its workspace" with no way to proceed.

**Paperclip version or commit**

Reproduced on the current `master` line (server service); root cause is
in `runWorkspaceIsFinalized` in `server/src/services/issues.ts`.

**Deployment mode**

Local / self-hosted instance (server service).

**Root cause**

`runWorkspaceIsFinalized` returned `true` only when the latest
`workspace_finalize` operation was `succeeded`. A terminal `failed`
finalize (the sync-back ran and failed; it will not retry within that
run) and a `running` finalize left behind by a dead run both left the
gate closed forever.

## What Changed

- `runWorkspaceIsFinalized` (server/src/services/issues.ts) now treats a
sync-back as **settled** when the latest `workspace_finalize` op reached
any terminal status (`succeeded`, `failed`, or `skipped`), instead of
only `succeeded`.
- A `workspace_finalize` still marked `running` blocks only while its
owning run is alive; a `running` record left behind by a
terminal/missing run is treated as stale (settled), so a dead run can no
longer wedge the gate.
- Preserved existing behavior for the other cases: no operations
recorded at all → settled; earlier phases recorded but no
`workspace_finalize` yet → still blocks (the sync-back hasn't been
attempted).
- Extracted the run-liveness check into a shared exported helper
`heartbeatRunIsTerminalOrMissing` and reused it from the existing
`isTerminalOrMissingHeartbeatRun` closure (no behavior change there).
- Added a short comment at the confirmation-accept gate
(server/src/services/issue-thread-interactions.ts) documenting the
relaxed settle semantics.
- The dependency-readiness / blocker barrier
(`listPendingFinalizeBlockerIssueIds`) is deliberately left unchanged:
an automated dependent must not proceed onto a base that never received
a blocker's synced-back commits, so a failed finalize keeps that gate
closed. Only the human-driven confirmation accept is relaxed.
- Added regression tests for: failed finalize, stale `running` finalize
on a dead run, and a genuinely `running` finalize on a live run (must
still block).

## Verification

- `cd server && node_modules/.bin/vitest run
src/__tests__/issue-thread-interactions-service.test.ts -t "accept"` →
17 passed (includes the 3 new regression tests), 21 unrelated tests
skipped by the name filter.
- Manual reasoning walkthrough of `runWorkspaceIsFinalized` for each
op-history shape (no ops / earlier-phase-only / terminal finalize /
running-on-dead-run / running-on-live-run) confirms the intended
block-vs-settle outcome.

## Risks

- Low risk and narrowly scoped to the human confirmation-accept gate.
The only behavioral change is that a terminal (`failed`/`skipped`) or
stale-orphaned `running` finalize now settles the gate instead of
blocking forever.
- A genuinely in-flight sync-back on a live run still blocks (covered by
a regression test), so the accept cannot race commits that are actively
being synced back.
- The blocker/dependency barrier for automated dependents is unchanged,
so no dependent will be advanced onto a base missing a failed blocker's
commits.

## Model Used

- Provider/model: Claude (Anthropic), **Opus 4.8**, model ID
`claude-opus-4-8`, 1M context window.
- Capabilities used: extended thinking, tool use (repo inspection, local
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
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
2026-07-23 10:34:47 -07:00
Michael Nguyen a7186dce4b
fix(plugin-sdk): thread companyId through configChanged + fail-closed cross-tenant guard (LOOA-687) (#10096)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - First-party **plugins** run as isolated workers spawned by the host
`plugin-loader`, reading company-scoped config through a governed
`ctx.config.get(companyId)` channel.
> - The host→worker `configChanged` RPC carries `{ config, companyId }`,
but the SDK dispatch dropped the scope — `onConfigChanged(newConfig)`
was companyId-blind by design — so a **proactive** worker kept a single
worker-global config.
> - #10092 added a startup replay that fans out **every** stored
company's config through `configChanged`. With no deterministic
ordering, a plugin configured for more than one distinct company ends up
running as whichever DB row was delivered last.
> - That is a latent cross-tenant identity/secret confusion bug: one
company's bot token could be applied to another company's traffic.
> - This pull request threads `companyId` through `onConfigChanged` and
adds a fail-closed cross-tenant guard at the SDK layer, so a
single-tenant worker can never silently collapse to a second company's
config.
> - The benefit is that the config-delivery class is fixed at the SDK
boundary — before any genuinely multi-company proactive plugin ships —
without changing today's single-tenant behavior.

## Linked Issues or Issue Description

No public GitHub issue — describing in-PR (hardening / latent security):

**Latent cross-tenant config collapse.** The worker-side `configChanged`
dispatch forwarded only `config` and dropped `companyId`, so a proactive
plugin kept a single worker-global config. #10092's startup replay
delivers every configured company's config sequentially with no `ORDER
BY`, so a plugin with configs for more than one distinct company would
apply a nondeterministic last-write-wins global config (one tenant's
credential applied to another's traffic).

- Builds on and must merge after #10092.
- Not exploitable today: the only proactive consumer (the chat gateway)
has single-tenant config rows, so last-write-wins is a no-op. This is a
hardening pre-condition before any multi-company proactive plugin ships.

## What Changed

- **Thread scope through:** `onConfigChanged(newConfig, context)` with a
new exported `PluginConfigChangeContext { companyId }`. Backward
compatible — the second arg is optional; existing single-arg
implementations are unaffected.
- **Fail-closed cross-tenant guard** (`worker-rpc-host.ts`): a
single-tenant plugin that receives `configChanged` for a second,
distinct company with a *different* config is rejected with the new
`PLUGIN_RPC_ERROR_CODES.CROSS_TENANT_CONFIG` instead of silently
overwriting the applied tenant's config. Idempotent replays of the
*same* config under a different scope row remain allowed.
- **Opt-in `multiCompanyConfig: true`** on the plugin definition for
plugins that genuinely serve multiple companies from one worker (keying
per-company state on `context.companyId`); the guard is bypassed for
those.
- **Deterministic `ORDER BY companyId`** on `registry.listConfigs`, so
the startup replay binds a single-tenant worker to a stable company
across restarts.
- **Loader visibility:** a `CROSS_TENANT_CONFIG` rejection is logged at
`warn` (was best-effort `debug`) so the misconfiguration is surfaced.
- **Regression test**
(`packages/plugins/sdk/tests/worker-rpc-host.test.ts`): two distinct
companies delivered via the startup-replay path fail closed and stay
bound to the first company; an idempotent same-config replay under a
different scope row is allowed; a `multiCompanyConfig` plugin receives
per-company config with the correct `context.companyId`.

## Verification

- SDK `tsc --noEmit`: clean.
- SDK vitest `worker-rpc-host.test.ts`: 7/7 pass (incl. 3 new). The
two-distinct-company case **fails against pre-fix code** and passes
after the fix.
- #10092 embedded-postgres `plugin-config-startup-delivery.test.ts`: 3/3
pass (unaffected by the new `ORDER BY`).
- Full server `tsc --noEmit` against this SDK: clean.

## Risks

- **Low functional risk.** The second `onConfigChanged` arg is optional
and existing implementations are unchanged. Today's single-tenant
gateway keeps working — idempotent same-config replays are explicitly
allowed, so the go-live is preserved.
- **Behavioral shift on misconfig:** a genuinely multi-company plugin
that has NOT opted into `multiCompanyConfig` now fails closed
(`CROSS_TENANT_CONFIG`) rather than silently collapsing to one tenant.
This is the intended safer default; opt in with `multiCompanyConfig:
true` to serve multiple companies from one worker.
- **Not in scope (residual).** Per-company workers/connections for a
genuinely multi-company gateway increase resource use and are tracked
separately (ties into the #10092 fan-out/timeout follow-up). This PR
fixes the class and fails closed; it does not build multi-tenant
connection management.

## Model Used

Claude — Anthropic `claude-opus-4-8` (Opus 4.8), extended thinking, with
tool use / code execution via Claude Code.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes:` / `Closes`
/ `Refs` OR (b) described the issue in-PR following the relevant issue
template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [ ] My branch name describes the change and contains no internal
Paperclip ticket id — branch predates this rule; not renaming an open PR
mid-review
- [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
doc surface; internal SDK/host behavior only
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] I will address all Greptile and reviewer comments before
requesting merge

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

Co-authored-by: anicca <annica@Michaels-Mac-Studio.local>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-07-23 09:50:48 -07:00
Michael Nguyen 1ebf5254b6
fix(plugin-loader): deliver stored config to freshly-started plugin workers (#10092)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for work.
> - One capability is first-party **plugins** that run as isolated workers spawned by the host `plugin-loader`, reading company-scoped config through a governed `ctx.config.get(companyId)` channel.
> - A **proactive** plugin (e.g. a chat gateway that opens a Slack Socket Mode connection at startup) does its company work from `setup()`, where there is **no company-scoped invocation** — so `ctx.config.get()` is rejected with `company context is required`.
> - The worker swallows that error and falls back to its default (feature-off) config, so the plugin comes up **inert** even though correct config exists in the database.
> - This is a regression from #9557 ("governed access contracts"), which changed `plugin-loader.ts` `activatePlugin` from loading stored config into the worker bootstrap to `const config = {}`.
> - This pull request replays each configured company's stored config to the freshly-started worker over the **same `configChanged` host→worker path an operator config-save already uses**.
> - The benefit is that proactive plugins receive their config on worker start (both server boot and operator enable) without weakening the governed-access surface.

## Linked Issues or Issue Description

No public GitHub issue — describing in-PR (bug):

**Bug.** After a proactive plugin's worker spawns, it never receives its stored config. Governed access (`packages/plugins/sdk/src/host-client-factory.ts`) only resolves `config.get` inside a company-scoped invocation (event/action/tool, or explicit `params.companyId`). Proactive plugins operate from `setup()` where no such scope exists, so `config.get()` fails with `company context is required`, the worker falls back to defaults, and the feature stays disabled despite valid DB config.

- Regression introduced by #9557.
- Related follow-up (latent multi-company hardening): #10096.

## What Changed

- `plugin-registry.ts`: add read-only `listConfigs(pluginId)` returning all stored company config rows for a plugin (scoped `where eq(pluginConfig.pluginId, pluginId)`).
- `plugin-loader.ts`: after the worker starts in `activatePlugin`, replay each company's stored config through the existing `configChanged` host→worker RPC — one `{ config, companyId }` per row, the same payload shape as the operator config-save path in `routes/plugins.ts`. Best-effort and idempotent; covers both server-boot `loadAll` and operator enable.
- test: DB-backed `plugin-config-startup-delivery.test.ts` covering `registry.listConfigs` completeness and cross-plugin isolation.

## Verification

- `tsc --noEmit` on `@paperclipai/server` — clean.
- New `plugin-config-startup-delivery.test.ts` (embedded-postgres, 3 cases) — pass.
- Full PR CI green: typecheck, all server/e2e/serialized test shards, build, canary dry-run, verify, and the security scanners (Snyk, Socket, Superagent, Greptile).

## Risks

- **Low functional risk.** Adds an outbound host→worker push that mirrors the already-shipped operator-save path. A worker without an `onConfigChanged` handler (or momentarily unavailable) simply keeps the runtime `ctx.config.get(companyId)` model.
- **Startup fan-out.** One `configChanged` per configured company at activation (sequential, default RPC timeout). `plugin_config` rows are writable only by instance-admins, so fan-out size is operator-controlled — not a remote surface.
- **No secret-handling change.** `configJson` is delivered as-is, exactly as `config.get`/operator-save already deliver it. No new secret sink; catch-blocks log only ids + `err.message` at debug, never `configJson`.
- **Latent multi-company behavior (pre-existing, not introduced here).** The worker-side `configChanged` dispatch forwards only `config` (drops `companyId`), and `listConfigs` has no `ORDER BY`, so a plugin configured for **more than one** company would apply a nondeterministic last-write-wins global config. This is existing SDK behavior — operator-save already pushes into the same handler — and is **not reachable by the single-company consumer this fix targets**. Greptile flagged this shape (4/5). It is tracked and fixed as a separate, non-blocking hardening PR (#10096): thread `companyId` through `onConfigChanged`, deterministic ordering, bounded fan-out.

## Model Used

Claude — Anthropic `claude-opus-4-8` (Opus 4.8), extended thinking, with tool use / code execution via Claude Code.

## Checklist

- [x] I have included a thinking path that traces from project context to this change
- [x] I have specified the model used (with version and capability details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked them above
- [x] I have either (a) linked existing issues with `Fixes:` / `Closes` / `Refs` OR (b) described the issue in-PR following the relevant issue template
- [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs)
- [ ] My branch name describes the change and contains no internal Paperclip ticket id — branch predates this rule; not renaming an open PR mid-review
- [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 doc surface; internal SDK/host behavior only
- [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 — 4/5; two latent multi-company items triaged as non-blocking and fixed in follow-up #10096 (see Risks)
- [x] I will address all Greptile and reviewer comments before requesting merge — addressed: triaged as non-blocking follow-up in #10096

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


Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-07-23 09:34:42 -07:00
Michael Nguyen 5a00040c4c
feat(plugin-sdk): interactions/approvals respond + attachment read capabilities for the chat gateway (#10066)
Adds the remaining 5 plugin capabilities + 7 worker→host RPC methods (interactions read/respond, approvals read/respond, attachment read) needed by the Slack chat gateway plugin (v0.5.0) to pass manifest capability validation and load.

- Security review: PASS (LOOA-642) after the viewer-role privilege-escalation blocker (LOOA-648) was fixed on this branch (requireActiveHumanMember now rejects viewer on impersonation write-paths, matching assertCompanyAccess).
- CI: Build, Typecheck, all server suites (3/3 + serialized 4/4), workspaces, e2e shard 2/2, and all security scanners (Snyk/Socket/Superagent/Greptile/security-review) green.
- One e2e flake (signoff-policy 'non-participant cannot advance stage') is unrelated: it exercises execution-policy stage advancement (routes/issues.ts, untouched by this PR) and failed on a heartbeat_run_events FK race + 409 checkout conflict.

Unblocks LOOA-629 (Slack gateway go-live) and the interview-ask feature.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-07-22 21:30:34 -07:00
Devin Foley 0ef3b320c7
Harden `POST /plugins/install`: canonicalize `localPath` for all instances; bundled-only floor for managed instances (#10067)
**Builds on** #10058 — managed detection keys off the *presence* of the
`PAPERCLIP_MANAGED_CONFIG` env var that PR introduces, deliberately
never its parsed body.

**Summary.** Two layered hardenings of the plugin install route. (1) For
**all** instances: `localPath` installs previously skipped the
package-name validation entirely; the path is now null-byte-checked,
resolved absolute, `realpath`'d (collapsing `..` traversal and
symlinks), and required to be an existing directory before the loader
ever sees it. (2) For instances running under a managed hosting control
plane (detected by the *presence* of `PAPERCLIP_MANAGED_CONFIG` —
deliberately never its body, so a corrupted document cannot widen the
surface): registry/npm installs return 403, and `localPath` installs
must canonicalize to inside the bundled plugin catalog root
(`packages/plugins`) — a positive allowlist enforced in code at the
route, independent of any flag value. Self-hosted behavior is otherwise
unchanged.

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The plugin system lets instance admins install plugins from a
registry or from a local filesystem path, and plugin installation is
code execution on the host
> - The `localPath` branch of `POST /plugins/install` skips the
validation applied to registry installs; the raw path reaches the plugin
loader without canonicalization
> - Separately, instances operated by a managed hosting control plane
must constrain installs to the bundled plugin catalog, because there the
host belongs to the operator, not the tenant
> - This pull request canonicalizes and validates `localPath` for all
instances, and adds a bundled-only install floor for managed instances
> - The benefit is a smaller install-route attack surface everywhere,
and a positive code-enforced allowlist where the operator owns the
machine

## Linked Issues or Issue Description

No public issue exists; `bug_report` template fields for the validation
gap this PR fixes:

- **What happened:** `POST /plugins/install` with `localPath` set
bypasses the package-name validation entirely; the un-canonicalized path
(relative segments, symlinks, no existence check) is handed straight to
the plugin loader.
- **Expected behavior:** path installs are validated like registry
installs — null-byte-checked, resolved absolute, `realpath`'d, and
required to be an existing directory before the loader sees them.
- **Steps to reproduce:** as an instance admin, call `POST
/plugins/install` with a `localPath` containing `..` traversal or a
symlink pointing outside any plugin directory; observe the loader
receives the raw path. Exploitability is bounded (the route already
requires instance admin), so this is hardening of an admin-only surface
rather than an open exploit.
- **Version:** current `master`.

The managed-instance bundled-only floor layered on top is new behavior
(motivation: on managed hosting, arbitrary plugin install is arbitrary
code execution on operator infrastructure), aligned with the in-progress
"Cloud deployments" milestone in `ROADMAP.md`.

## What Changed

- New `server/src/services/plugin-install-guard.ts` — three pure
primitives: managed detection (presence-based), path canonicalization
(null-byte check → absolute resolve → `realpath` → must be an existing
directory), and segment-based containment in the bundled plugin catalog
root.
- Route enforcement in `server/src/routes/plugins.ts`: npm/registry
installs return 403 on managed instances; `localPath` installs are
canonicalized on every instance and, on managed instances, must land
inside the bundled catalog root.
- The plugin loader now receives the canonical path instead of the raw
request string.

## Verification

- 15 guard unit tests
(`server/src/__tests__/plugin-install-guard.test.ts`): traversal,
symlink escape, null byte, file-vs-directory, string-prefix sibling
root.
- 13 route security tests
(`server/src/__tests__/plugin-install-route-security.test.ts`): 403
matrix on managed instances + self-hosted happy paths.
- 36 existing plugin route authz tests green
(`server/src/__tests__/plugin-routes-authz.test.ts`).
- Server `tsc --noEmit` clean.

```bash
cd server
pnpm vitest run src/__tests__/plugin-install-guard.test.ts src/__tests__/plugin-install-route-security.test.ts src/__tests__/plugin-routes-authz.test.ts
pnpm exec tsc --noEmit
```

## Risks

- Managed instances: npm/registry installs and out-of-catalog
`localPath` installs now return 403 — intended new behavior, enforced in
code rather than configuration.
- All instances: `localPath` installs that previously pointed at
nonexistent paths or non-directories now fail with 400 before reaching
the loader (previously the loader failed later, less safely). Symlinked
deployment layouts are handled by canonicalizing both sides of the
containment check.
- Self-hosted npm install path is unchanged. Low residual risk.

## Model Used

Claude Fable 5 (`claude-fable-5`), extended thinking, agentic tool use;
independently peer-reviewed by a second AI agent before push.

## Checklist

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

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-07-22 20:48:41 -07:00
Devin Foley c4cdcc4826
Generalize bundled plugin provisioning: `ensureBundledKubernetesPlugin` → `ensureBundledPlugins` (#10063)
**Builds on** #10058 — reads `plugins.autoInstall` from the parsed
managed-config contract #10058 introduces (the interim
`readManagedPluginAutoInstall` shim is retired at rebase).

**Summary.** Boot-time bundled-plugin provisioning becomes
catalog-driven. A new bundled-plugin catalog lists the sandbox providers
shipped in-tree (keys like `kubernetes`, `daytona` → plugin key + path
under the catalog root). Managed instances read `plugins.autoInstall`
from `PAPERCLIP_MANAGED_CONFIG`; unknown keys or paths escaping the
catalog root (symlinks resolved) **throw before listen** — a managed
instance refuses to start rather than boot half-provisioned.
Installation keeps today's mechanism: an in-process, fail-safe
`loader.installPlugin({ localPath })` under a system actor — no HTTP
route, no user, no role widening. Self-hosted boot is unchanged
(kubernetes bundle only, existing env override honored, install failures
still log-and-continue).

**Semantics.** A plugin already present in any non-uninstalled state is
skipped, so an operator-disabled plugin is never silently re-enabled;
managed mode reinstalls soft-uninstalled bundles (the control plane owns
provisioning); removal from the autoInstall list never auto-uninstalls.

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Sandbox-provider plugins ship in-tree, but boot-time provisioning is
hard-coded to exactly one of them (Kubernetes) via a bespoke function
> - On managed hosting, tenant users have no install privileges, so any
bundled plugin that is not provisioned at boot is unusable
> - Widening install routes or granting roles to fix that would trade a
provisioning gap for a security regression
> - This pull request generalizes the existing boot installer into a
catalog-driven `ensureBundledPlugins`, fed by `plugins.autoInstall` from
`PAPERCLIP_MANAGED_CONFIG`
> - The benefit is that managed tenants get working bundled plugins out
of the box, through the same in-process, role-free mechanism the
codebase already trusts, while self-hosted boot is unchanged

## Linked Issues or Issue Description

No public issue exists; `feature_request` template fields:

- **Problem or motivation:** on managed instances tenant users cannot
install plugins (by design they never hold instance admin), so even
plugins shipped with the product are unusable; boot provisioning
currently knows only the Kubernetes bundle.
- **Proposed solution:** a bundled-plugin catalog plus
`ensureBundledPlugins(keys)` driven by the managed config; same
in-process `loader.installPlugin({ localPath })` under a system actor;
unknown keys or catalog-escaping paths fail startup; already-present
plugins are skipped so operator-disabled plugins are never silently
re-enabled.
- **Alternatives considered:** granting tenant users install privileges
(widens secrets/adapters/settings access to solve a one-button problem);
a separate non-admin install route for bundled plugins (new authz
surface; provisioning removes the need for any install action at all).
- **Roadmap alignment:** supports the in-progress "Cloud deployments"
milestone and builds on the shipped sandbox-provider milestone in
`ROADMAP.md`.

Refs #10058.

## What Changed

- New `server/src/services/bundled-plugins.ts`: the bundled-plugin
catalog, the fail-to-start resolver (`resolveBundledPluginInstalls`,
positive allowlist + catalog-root containment with symlinks resolved),
and the fail-safe installer (`ensureBundledPlugins`).
- `server/src/app.ts`: replaces the hard-coded
`ensureBundledKubernetesPlugin` boot hook with resolver + installer
wiring, with test hooks (`managedPluginAutoInstall`,
`bundledPluginCatalogRoot` options).
- `server/src/index.ts`: passes `plugins.autoInstall` from the single
fail-closed `PAPERCLIP_MANAGED_CONFIG` startup parse (#10058) into
`createApp`; absent env means self-hosted and changes nothing.

## Verification

- 24 new tests in `server/src/__tests__/bundled-plugins.test.ts`
(catalog resolution, containment incl. symlink and `..` escapes,
skip/reinstall matrix, self-hosted invariants, installer error paths) —
all green.
- 85 adjacent startup/plugin-route/auto-build/managed-config tests green
(`managed-config`, `instance-settings-managed-overlay`,
`plugin-install-autobuild`, `plugin-routes-authz`,
`server-startup-feedback-export`).
- Server `tsc --noEmit` clean.

```bash
cd server
npx vitest run src/__tests__/bundled-plugins.test.ts
npx vitest run src/__tests__/managed-config.test.ts src/__tests__/instance-settings-managed-overlay.test.ts src/__tests__/plugin-install-autobuild.test.ts src/__tests__/plugin-routes-authz.test.ts src/__tests__/server-startup-feedback-export.test.ts
npx tsc --noEmit
```

## Risks

- Managed instances with a malformed or unknown `plugins.autoInstall`
entry now **refuse to start** (fail closed, by design) instead of
booting half-provisioned; harness misconfiguration surfaces as a precise
startup error.
- Self-hosted behavior is unchanged (kubernetes bundle only,
`PAPERCLIP_KUBERNETES_PLUGIN_PATH` honored without containment, install
failures log-and-continue), so the default deployment path carries low
risk.
- No uninstall path exists in this module; removal from the autoInstall
list can leave a previously provisioned plugin installed (intentional v1
semantics, documented in code).

## Model Used

Claude Fable 5 (`claude-fable-5`), extended thinking, agentic tool use;
independently peer-reviewed by a second AI agent before push.

## Checklist

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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-07-22 20:35:51 -07:00