Commit Graph
8047 Commits
Author SHA1 Message Date
DottaandClaude Opus 5.5 b3c3e1ef1c test(evals): open an authored skill from its Skill created card when the reply has no link
The create_skill tool shows a Skill created card whose side panel opens the skill in Skill Studio. That card is the product's own path, so the person uses it when the reply has no skill link. Fail only when neither path exists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 23:28:52 -06:00
DottaandClaude Opus 5.5 44e54d6185 test(evals): wait for the owner handoff's project link to render
The reply settled with a project link in its text, but the thread had not rendered the link yet when the step read it. Poll briefly for the rendered link before failing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 23:28:52 -06:00
DottaandClaude Opus 5.5 7b123ab136 test(evals): wait for Birch's project after reload before recording it
isVisible() does not wait, so afterReloadVisible read the page before the reloaded project rendered and failed a correct switch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 23:28:52 -06:00
Dotta 4bab27f9b2 fix(authz): link the project's access settings when an agent cannot share a project
A board-only denial for project access members had no known page, so agents told people to find sharing settings themselves. Name the project's Configuration page, where Manage access is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit b5f3ac9092)
2026-11-08 23:28:52 -06:00
DottaandClaude Opus 5.5 d7834a3408 test(evals): resolve the Birch row filter from the page root
A filter({ has }) locator is evaluated inside each candidate row, so a menu-scoped inner locator never matched. Start the inner text locator from the page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 23:28:52 -06:00
DottaandClaude Opus 5.5 4a34c17d75 test(evals): reset Morgan's sessions from the profile header actions menu
The agent profile h1 is the agent's title, not its name, and the menu item now reads "Reset sessions". Identity is already confirmed by the named avatar in openRuntimeAgentProfile; use the main landmark and a case-insensitive reset label.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 23:28:52 -06:00
DottaandClaude Opus 5.5 8680b56b89 test(evals): open an authored skill from the reply link, then find it in My Skills
Skills is no longer a primary sidebar link. The person follows the skill link the reply rendered, then uses the Skills section's My Skills link to confirm it is in the library. A reply without a link to the saved skill now fails the case. Bump skill-authoring oracle to v7.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 23:28:52 -06:00
DottaandClaude Opus 5.5 9415389f0f test(evals): review secret proposals on the chat card the agent left
The /<prefix>/secrets route no longer exists (secrets moved under company settings). The person reviews the human-only Confirm secret binding card in the chat instead; its Reject and Create binding buttons resolve the linked proposal through the same governed path, and the case waits for the saved proposal status.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 23:28:51 -06:00
DottaandClaude Opus 5.5 576b7de0ad test(evals): open the project access picker without toggling it shut
Opening Private project access focuses the person picker, which opens its search panel by itself; the scripted click then closed it. Click only while the panel is still closed, and pick Riley inside that panel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 23:28:51 -06:00
DottaandClaude Opus 5.5 30900a980a test(evals): pick Birch from the organization switcher by its visible name
Each organization row's accessible name starts with its avatar initial ("B Birch BIR"), so /^Birch/ never matched. Match the row inside the open menu by its exact organization name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 23:28:51 -06:00
DottaandClaude Opus 5.5 92d5d7771f test(evals): accept the native status committer's done transition in revoke-personal-grant
The worker completes its task through the native status committer, which
records issue.updated as a system actor with the worker's runId and
details.toStatus, not as the assignee agent with details.status. Run
42560576472 completed the task normally and failed only this lookup. The
check now also accepts that row when its runId is the same worker run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 22:51:23 -06:00
DottaandClaude Opus 5.5 93cd4299d7 test(evals): wait for the persisted pause reply's profile link before following it
Run 42560576472 observed the thread the moment the CEO run settled. The
reply was on screen with its profile link, but the persisted comment had
not rendered yet, so the link collector found nothing. The scripted person
now re-reads the rendered thread for up to 30 seconds before failing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 22:51:23 -06:00
DottaandClaude Opus 5.5 b31950c439 test(evals): grade assistant decline and revoke cases by saved state, not reply keywords
decline-consent's truthful-decline check and revoke-personal-grant's
oldAssistantGuidance matched reply keywords (the revoke act also threw on
them). decline-consent now requires a reply, no pending re-prompt, and no
agent-authored connection, grant or secret write. revoke-personal-grant
now requires a reply, the old grant still active when the person follows
the reply's assistant-connections link, and no lead update to the
delegated task, beside its existing revocation and lifecycle checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 22:51:23 -06:00
DottaandClaude Opus 5.5 16236acf33 test(evals): grade the pause-agent case by saved state and the followed profile link
pending-human-step-honest matched reply keywords. It is replaced with
structural checks: Morgan and the worker run were still running when the
person took over, no agent-authored lifecycle write to Morgan succeeded,
the reply rendered a link to Morgan's profile, and the scripted person
reached the Pause control by clicking that link. The typed-URL fallback
is gone, and the actions menu is scoped to the profile header because the
sidebar has a same-named menu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 22:51:23 -06:00
DottaandClaude Opus 5.5 8ce3389fb5 fix(api): tell agents only the person can revoke assistant connections, and where
A chat agent asked to "disconnect my old assistant" found DELETE
/api/mcp/connections/{id}, was refused by call_api as a protocol
endpoint, and then spent its whole turn searching for an agent route
that does not exist, never replying. Neither the catalog entry nor the
rejection said the action is person-only or which page holds it.

Both assistant connection catalog entries now say only the signed-in
person can list or revoke them, link the Assistant connections page,
say not to search for another route, and state that revoking leaves
already-delegated tasks running. The call_api protocol rejection now
says no other API route substitutes for the endpoint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 22:51:23 -06:00
DottaandClaude Opus 5.5 f81933d010 test(evals): stop treating the OAuth state nonce as a persisted-state secret
decline-consent and revoke-personal-grant added the public OAuth `state`
to the private secret list. The server must store `state` in
mcp_oauth_requests to return it on the redirect, so the persisted-state
scan matched it in that table's heap file (relfilenode 26324) and refused
an otherwise passing cell. Codes and tokens stay private; the server stores
them only as SHA-256 hashes, and the PKCE verifier is never stored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 22:51:22 -06:00
DottaandClaude Opus 5.5 2a607c4239 test(evals): open the profile actions menu before the human pauses Morgan
The agent profile moved Pause into its actions menu (#19535), so the
scripted human timed out looking for a header Pause button after the CEO
correctly handed the pause to the person.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 22:51:22 -06:00
DottaandClaude Opus 5.5 a6202dc0d9 test(evals): refresh source SDK public-input pins for the master-merged candidate
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 22:23:56 -06:00
DottaandClaude Opus 5.5 1fece5bbd9 fix(evals): keep proven key-shaped SDK declarations out of the sealed upload
The second paid run got every plugin-SDK cell past the carrier check, then
the private result sealer rejected the tree with "Packaged evidence contains
credential contamination". Its packaging scan checks text for provider key
shapes without the SDK public-type proof, and
packages/shared/dist/validators/agent.d.ts declares
`OPENROUTER_API_KEY: z.ZodOptional<z.ZodString>`. secrets.least-scope-delivery
was even recorded as a secret_leak.

copyRetainedSourceSdk now skips files whose bytes match a key shape. Retention
already proved each such span a public schema type, and the receipt still
records the file's size and hash. Add a test that retains, copies and seals
the real built SDK with the real sealer (skipped when the SDK is not built);
it reproduces both CI failures without the two fixes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 22:23:44 -06:00
DottaandClaude Opus 5.5 49051ccae2 fix(evals): retain SDK declarations under sealer-admitted names
With the SDK public proof fixed, the six plugin-SDK cells got past startup,
then every attempt failed at finalization with "Unsupported packaged evidence
carrier". copyRetainedSourceSdk copies the built SDK output (.d.ts and .map
files among it) into the attempt upload directory, and the private result
sealer admits only standard evidence extensions, so it rejected the whole
results tree and no evidence was uploaded.

Retain any SDK file whose name the sealer would refuse under a .txt suffix.
Bytes, hashes and the receipt are unchanged; the receipt records the new
retained path. Add a test that seals a copied SDK upload with the real sealer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 22:23:44 -06:00
DottaandClaude Opus 5.5 fb41c8ce49 test(evals): refresh stale SDK public-proof pins that blocked plugin cells
The six plugin-SDK chat cells scan the built packages/shared and plugin-sdk
output before the web server starts. packages/shared/dist/validators/agent.d.ts
contains `OPENROUTER_API_KEY: z.ZodOptional<z.ZodString>`, which matches the
OpenRouter key shape (detector 3). The scanner proves such matches are public
zod schema types only when the pinned public inputs match the tree. The pins
were stale (18 changed and 5 new shared sources, the reviewed lock moved to
2360d5..., zod moved to 4.6.7), so the proof was unavailable, the match stayed
unresolved_context, and startup aborted before the agent got the request.

Regenerate the source pins with source-sdk-public-inputs.mjs and move the
lock and zod runtime pins to the reviewed lock (report-lock.json output) and
the zod 4.6.7 it resolves. Add a regression test with the shipped declaration
that also keeps literal values at that position strict.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 22:23:43 -06:00
Dotta 8aaa804a89 Merge remote-tracking branch 'origin/master' into chat-eval/master-candidate
* origin/master:
  fix(agents): keep pause board-only and resume grant-gated on the PATCH status route (#20164)
  fix(api-catalog): describe the real backup and restore paths for agents (#20159)
  fix(tool-access): an unscoped tools:use grant does not turn on actions a connection left off (#20160)
  feat(skills): show a line diff for a pending tracked-source update (#20155)
  fix(api-catalog): name the Connections page for company model service settings (#20152)
  test(chaos): wait for new agents to be ready before the first assignment (#20158)
  fix(authz): make board-only denials final and actionable for agents (#20151)
  fix(native): move an interrupted bootstrap's session aside on the run's own created sandbox lease (#20154)
  fix(native): restart a remote run whose controller never sent its runner a command (#20153)
  fix(execution): a pause's stop never blocks the task behind an incident with no control (#20143)
  fix(native): settle a finished owner whose journal holds a definitively answered command (#20148)
  fix(native): a failed terminal followed by an active restart continuation is not a permanent failure (#20149)

# Conflicts:
#	server/src/__tests__/company-model-service-settings-guidance.test.ts
#	server/src/__tests__/database-backup-catalog-guidance.test.ts
#	server/src/__tests__/generic-mcp-connection.test.ts
#	server/src/routes/openapi.ts
#	server/src/services/tool-access-policy.ts
2026-11-08 22:04:18 -06:00
DottaandClaude Opus 5.5 8e5959c759 fix(agents): keep pause board-only and resume grant-gated on the PATCH status route (#20164)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The board governs the agent lifecycle: pause, resume and terminate
> - The V1 permission matrix says agents may never pause an agent, and
may resume one only with a direct `agents:configure` change grant
> - `POST /api/agents/:id/pause` and `POST /api/agents/:id/resume`
enforce this, but `PATCH /api/agents/:id` with a `status` ran the same
lifecycle change after only the agent-configuration check
> - The `agents.pause-agent` chat eval caught it: the CEO agent paused
Morgan itself through `PATCH`, although the case (and the spec) expect a
board user to do it
> - This pull request applies the dedicated routes' checks to `PATCH`
status changes, as #19955 did for termination
> - The benefit is one lifecycle authority, with no second route around
it

## Linked Issues or Issue Description

Refs #19955

**What happened**

An agent that may configure another agent could pause it with `PATCH
/api/agents/{id}` `{ "status": "paused" }`, although `POST
/api/agents/{id}/pause` is board-only. The same `PATCH` with `{
"status": "idle" }` resumed an agent without the change-grant decision
and without the invalid-org-chain guard that `POST /resume` applies. In
a chat eval run, the CEO agent paused Morgan this way and reported
"Morgan is paused".

**Expected behavior**

Pause is board-only on every route (`doc/SPEC-implementation.md` section
9.3: "Pause/resume agent ... pause: no; resume: direct
`agents:configure` grant only"; "Pause, clear-error, terminate,
approval, and key-management routes remain board-only"). An agent gets
the standard final "Board access required" denial with a link to the
agent's profile. Resume through `PATCH` needs the same grant and
org-chain checks as `POST /resume`.

**Steps to reproduce**

1. As an agent with agent-configuration permission over another agent,
call `POST /api/agents/{id}/pause`. The result is 403.
2. Call `PATCH /api/agents/{id}` with `{ "status": "paused" }`. Before
this change the result is 200 and the agent is paused.

## What Changed

- `server/src/routes/agents.ts` (`PATCH /agents/:id`): `status:
"paused"` now calls `assertBoard`, the same check as `POST /pause`.
Agents get the structured `board_access_required` denial that says the
denial is final and links `/<prefix>/agents/<id>`.
- `status: "idle"` now calls `assertCanResumeAgent` (the
`agent_config:update` decision with `requiresChangeGrant`) and returns
409 with the repair guidance when the org chain is invalid, the same as
`POST /resume`.
- `server/src/__tests__/agent-permissions-routes.test.ts`: pause is
denied through both routes for an agent allowed to configure the target,
with no lifecycle change, run cancel or activity. A board user can still
pause through `PATCH`. `PATCH` idle applies the change-grant decision,
allows a granted agent, and keeps the org-chain guard.

## Verification

- `cd server && pnpm exec vitest run
src/__tests__/agent-permissions-routes.test.ts
src/__tests__/agent-cross-tenant-authz-routes.test.ts
src/__tests__/board-access-denial.test.ts`: 147 passed.
- Without the route change, three new tests fail (PATCH pause by an
agent, PATCH idle without the change grant, PATCH idle with an invalid
org chain).
- Policy evidence: `doc/SPEC-implementation.md` section 9.3 and the
`agents.pause-agent` scenario ("Pausing an agent requires board
control").

## Risks

- Non-board automation that paused agents through `PATCH` status now
gets 403. That is the intended governance boundary. Board users are not
affected.
- Agents that resumed agents through `PATCH` status now need the same
change grant as `POST /resume`. Standard agents get `agents:configure`
on creation, so ordinary behavior does not change.
- `PATCH` idle now returns 409 for an agent with an invalid org chain,
the same as `POST /resume`.

## Model Used

Anthropic Claude Opus 5.5 (`claude-opus-5-5`), via Claude Code, with
tool use (shell, file editing).

## 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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 22:01:11 -06:00
DottaandClaude Opus 5.5 f89813d005 fix(api-catalog): describe the real backup and restore paths for agents (#20159)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Instance admins and operators own database backups; a chat agent
cannot create one
> - When a person asks an agent to back up the instance and check a
restore, the agent must say who can do it and how
> - The agent reads the API catalog to find the operator path, but the
backup entry had only the summary "Trigger a database backup"
> - With no real path in the catalog, the agent told the person to use
an "Instance settings > Backups" page that does not exist
> - This pull request describes the real paths in that catalog entry:
the endpoint, the CLI, the retention setting, the result file, and how
to check a restore safely
> - The benefit is that the agent sends the person to paths that exist
and keeps a restore check off the live database

## Linked Issues or Issue Description

**What happened**

A person asked the CEO chat agent: "Back up this test instance and check
that we can restore it." The agent got 403 on `POST
/api/instance/database-backups`, which is correct. It searched the
catalog for backup and restore operations and found only that endpoint
with no description. Its reply then said: "Create a backup in Instance
settings → Backups". The web UI has no such page or control.

**Expected behavior**

The agent says that an instance admin or operator must do this. It names
real paths: the endpoint for an instance admin, `paperclipai db:backup`
on the server host, and the Backup retention setting for automatic
backups. For the restore check, it says to restore into a separate empty
database, not over the live one.

**Steps to reproduce**

1. Start a local instance and open a chat with the CEO agent.
2. Ask: "Back up this test instance and check that we can restore it."
3. Read the reply: it names a backup page in Instance settings. Look for
it in the UI: there is none.

## What Changed

- `server/src/routes/openapi.ts`: add a description to `POST
/api/instance/database-backups`. It says that only instance admins can
call it (agents get 403, cloud-managed instances run backups for you),
that automatic backups run about hourly with retention set on the
company-prefixed /company/settings page, that the UI has no create or
restore control, that an operator can run `paperclipai db:backup`, what
the result file is, and that a restore check uses psql into a separate
empty database, never over the live one.
- `server/src/routes/openapi.ts`: register the success status as `201`,
which is what the route returns, instead of `200`. The published
document already showed `201` through the created-operations fixup, so
this makes the source match it.
- `server/src/__tests__/database-backup-catalog-guidance.test.ts`: new
regression test. It runs the agent's catalog searches from the failed
run and checks the description. It also checks that the published
document shows `201` and not `200` for a successful backup.

## Verification

- `pnpm exec vitest run
src/__tests__/database-backup-catalog-guidance.test.ts
src/__tests__/openapi-routes.test.ts` (from `server/`): 40 tests pass.
Without the change, the 2 search tests fail. The status test guards the
published document, which the fixup already kept correct.
- `pnpm exec vitest run
src/__tests__/instance-database-backups-routes.test.ts`: 7 tests pass.
The server typecheck passes.
- Chat eval `instance-operations.backup-and-restore` (runner-opencode),
with this commit cherry-picked onto the eval branch: before the change,
the reply named an "Instance settings → Backups" page that does not
exist. After the change (GitHub Actions run 42552777052), the eval
passed. The first reply said the agent got 403, named `POST
/api/instance/database-backups` for an instance admin and `paperclipai
db:backup --json` for an operator, linked Backup retention on the
company settings page, and said to restore with psql into a separate,
empty database, never over the live one. The second reply reported the
operator's restore receipt and its limits correctly.

## Risks

- Low risk. The change adds catalog text and makes the registered
success status match the published 201. It does not change any API or
permission behavior.
- If a backup page or restore command is added later, this text must
change. The regression test names these facts, so such a change fails
the test and shows the text to update.

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

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), tool use and code
execution in Claude Code.

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 22:01:06 -06:00
DottaandClaude Opus 5.5 3f12046576 fix(tool-access): an unscoped tools:use grant does not turn on actions a connection left off (#20160)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - People connect apps and choose which actions of each app agents may
call; the choice is saved as the connection's tool profile
> - Every new standard agent also gets an unscoped `tools:use` grant,
which means "this agent may use tools"
> - The access policy checked explicit grants before profiles, so this
default grant allowed every action of every installed connection,
including actions the person left off
> - The agent's effective profile showed the action off, but the gateway
and the test-as-agent summary allowed it
> - This pull request makes an unscoped grant stop at a connection
profile: when a profile governs the tool's connection, only a grant
scoped to the action, connection or application allows an action the
profile leaves off
> - The benefit is that the actions a person turns off for a connection
stay off for new agents

## Linked Issues or Issue Description

**What happened**

A person connected an app with one read action and one destructive write
action. When they finished the connection, they enabled only the read
action. They installed the connection for a new agent, Morgan. Morgan's
effective profile (`GET
/api/companies/{companyId}/tools/profiles/effective/agents/{agentId}`)
allowed only the read action. But `GET
/api/tool-connections/{connectionId}/test-agents/{agentId}/access`
reported both actions as `allowed` with reason `allow_explicit_grant`,
and a chat agent told the person that Morgan could also run the
destructive write. The same policy decision admits gateway calls, so
Morgan could call the write.

**Expected behavior**

An action that the connection's profile leaves off is off for the agent.
The access summary, the effective profile and the gateway agree. A grant
that a person scopes to that action, connection or application still
allows it.

**Steps to reproduce**

1. Connect an MCP app that has a read tool and a destructive tool. In
the finish step, enable only the read tool.
2. Create a new agent through the API (it gets the default unscoped
`tools:use` grant) and install the connection for it.
3. Call `GET
/api/tool-connections/{connectionId}/test-agents/{agentId}/access`: the
destructive tool shows `allowed`, reason `allow_explicit_grant`.

## What Changed

- `server/src/services/tool-access-policy.ts`: for an agent, a tool's
connection is governed by a connection profile when the agent's
effective profiles include the connection's own access profile (the app
wizard's `app:<connectionId>` profile or the agent's connection-intent
profile, even with no entries), or any entry that names this connection
or selects this action (including application, tool-name and risk-level
selectors). For a governed connection, `explicitGrant` counts only
grants whose scope names an action, connection or application (`allow`,
`toolName`, `catalogEntryId`, `connectionId`, `applicationId`,
`applicationKey`). A grant scoped only by context (for example an agent
or project) still covers every tool, so it does not override the
profile. Board users keep their operator grant.
- `server/src/services/tool-profile-binding-precedence.ts`: export the
existing connection-profile identity check as
`isConnectionAccessProfile`, so the policy and binding precedence use
one definition.
- `server/src/__tests__/generic-mcp-connection.test.ts`: regression
tests through the gateway's access summary and the direct policy check:
- only the read action enabled: the write stays off with the unscoped
grant, and a grant scoped to the write allows it
- every action turned off (both lists empty): both stay off; a
context-only scope stays off; a connection-scoped or application-scoped
grant allows them
- an agent the connection's profile does not cover keeps the unscoped
grant
- a general profile with an application-selector exclude (no connection
id) is not overridden by the unscoped grant
- with company-wide access, the direct policy check denies the off
action for an agent and still allows it for a board user with the
unscoped grant

## Verification

- New tests: all pass with the change. On `master`, 4 of 5 fail. On the
first revision of this PR, the all-off, application-exclude and
board-user tests fail.
- Regression suites, all pass: `generic-mcp-connection`,
`tool-access-policy-service`, `tool-profile-matching`,
`tool-profile-entry-order`, `connection-management.integration` (238
tests), and `tool-access-service`, `tool-gateway-service`,
`tool-gateway`, `tool-profile-review-service` (799 tests).
- Every enforcement path uses the one `decide` function in
`tool-access-policy.ts`, and `explicitGrant` is called only there. That
covers the gateway tool listing and calls, the access summary, delegated
and Test tab calls, the connection-intent access check, the GitHub
review bot tool, and the example smoke check.
- Chat eval `app-permissions.test-as-agent` (runner-opencode). Before
the change (GitHub Actions run 42550085273), the access summary for
Morgan reported the write as `allowed` by `allow_explicit_grant`. With
the first commit cherry-picked onto the eval branch, GitHub Actions run
42552819589 passed. The summary reported `launch_read` allowed
(`allow_profile`) and `launch_update` off (`deny_default`), which
matches Morgan's effective profile, and the agent's reply said the same.

## Risks

- Medium risk. This changes an access decision. An agent that used a
connection action only through its default unscoped grant, while the
connection's profile or an explicit exclude left that action off, loses
that action. That is the intended fix. An operator who relied on it must
enable the action in the connection or give the agent a grant that names
the action, connection or application.
- A grant scoped only by context (for example `{ projectId }`) no longer
overrides a connection profile. Risk-level and provider-type scopes are
not treated as naming an action.
- Tools that no connection profile governs (for example Paperclip's own
tools), agents without a connection profile, and board users keep the
current 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

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), tool use and code
execution in Claude Code.

## Checklist

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

Related: #17884 (scoped `tools:use` grants at hire time).

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
canary/v2026.1109.0-canary.4
2026-11-08 21:58:29 -06:00
DottaandClaude Opus 5.5 28e070f8dc feat(skills): show a line diff for a pending tracked-source update (#20155)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Skills can come from a tracked GitHub source, and an update moves
the installed copy to a newer upstream revision
> - Before an update, a person asks the agent "what changed in the new
version?"
> - The product has no diff for a pending update. The agent previews two
full SKILL.md snapshots and compares them by eye
> - In two eval runs the agent reversed the same changed line. In a
third run it fetched the files with curl and ran `diff` itself, and the
turn took too long
> - This pull request adds a read-only endpoint that returns the line
diff between the installed and the latest tracked revision
> - The benefit is that review answers come from an exact diff, and they
come faster

## Linked Issues or Issue Description

No public issue exists. Feature description:

**What happened**
An agent asked to show what changed in a pending skill update had only
`POST /skill-sources/preview`, which returns one file at one commit. It
previewed both commits and compared about 9 KB of text by eye. It
reported one changed line in the wrong direction ("every new build" ->
"every build or release" instead of the reverse), and later retracted a
real change.

**Expected behavior**
The agent can request the exact changes of a pending tracked-source
update and report them with the correct direction.

**Steps to reproduce**
1. Install a skill from a GitHub source at an old commit and set the
tracking reference to a newer commit (automatic updates off).
2. Ask the chat agent: "Show me what changed in the new accessibility
skill version before we update it."
3. See that the agent can only preview full files at each commit and
must compare them itself.

## What Changed

- Add `GET /api/companies/{companyId}/skills/{skillId}/update-diff`. For
a skill from a tracked GitHub source, it compares the installed source
revision (`currentRef`) with the latest tracked revision (`latestRef`).
For each changed package file it returns `change` (added, removed,
modified, or `not_compared`), `binary`, `executableChanged`, and a
unified line diff (`-` installed, `+` latest), or `diffUnavailable`
(`binary`, `too_large`, `omitted`, `budget`) that says why there is no
diff. The endpoint is read-only. The `skillId` parameter handler checks
company access and skill visibility, the same checks other skill reads
use.
- `hasUpdate` is false and `files` is empty when the installed revision
is already the latest one. That case reads only the latest commit and
scans nothing.
- Return `supported: false` with a reason for: a skill without a tracked
source, a disconnected source, an unknown installed revision, and a
repository package that a scan could include only in part (a subset, or
skill folders only). In the last case, files that a scan limit left out
would otherwise look removed.
- A path the scan left out of a revision (too large, a symlink, or
rejected by the audit) is listed as `not_compared` instead of added or
removed.
- One response shares a work budget (24M LCS cells), a diff-text budget
(1M characters) and a file-count budget (1,000 files), and sets
`truncated` when it reaches one. The GitHub read and both scans stop
when the caller disconnects.
- Add `server/src/services/skill-update-diff.ts` with a bounded line
diff (shared head and tail trimmed, LCS size capped). An absent side has
no lines, a final newline is not a line, and a missing final newline
shows the unified-diff marker. A replaced line reads `-old` then `+new`.
- Add the shared types `SkillSourceUpdateDiff` and
`SkillSourceUpdateDiffFile`.
- Document the endpoint in the API catalog (`openapi.ts`) so agents find
it with `search_api`.

## Verification

- `cd server && npx vitest run src/__tests__/skill-update-diff.test.ts`
(10 tests):
  - direction of `-`/`+` lines, and separate hunks with bounded context
  - exact diffs for added and removed files, and final-newline handling
  - a change to only the executable bit, and paths left out of a scan
  - the response budgets
- the snapshot comparison for the selected skill path, with the abort
signal passed to both scans
  - a commit mismatch
  - refusal of partial repository scans
- `cd server && npx vitest run
src/__tests__/skill-update-diff-routes.test.ts` (6 tests):
  - the diff response
  - no pending update (no scan)
- a caller from another company (board user and agent) gets 403 before
any lookup
  - a skill the caller cannot read gets 404 before any source read
  - every unsupported case
  - a client disconnect aborts the scans
- `company-skills-routes.test.ts` and `openapi-routes.test.ts` pass (119
tests across the four files).
- `npx tsc --noEmit -p server`: no errors from this change.
- End to end: the `skills.review-update` chat eval exercises this path.
Its grader checks the quoted hunks against the real pinned diff.

## Risks

- Low risk. The endpoint is new and read-only. Existing routes do not
change.
- When an update is pending, each call scans the source twice (installed
and latest commit). It uses the same GitHub reader, scan limits and scan
slots as preview and refresh. The diff work and response size are capped
per response.
- `/update-status` and this route both read the latest commit of the
tracking ref. They are kept separate because `/update-status` compares
with the source's last scan, while this route compares with the
installed revision.
- No UI uses the endpoint yet.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), tool use and code
execution in Claude Code.

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 21:55:52 -06:00
DottaandClaude Opus 5.5 4afdf0493d fix(api-catalog): name the Connections page for company model service settings (#20152)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A company can turn on model services such as Fast response, Decision
model and Prompt Injection Prevention
> - Only people who manage connections can change these services, so a
chat agent must send the person to the right page
> - The agent finds these operations through the API catalog search, but
their catalog entries named no page
> - The agent searched for the settings page, found nothing, and linked
the general /company/settings page, which does not have the control
> - This pull request adds a description to these catalog entries that
names the section and the company-prefixed /company/settings/connections
page
> - The benefit is that the agent links the page that actually holds the
control

## Linked Issues or Issue Description

**What happened**

A person asked the CEO chat agent: "Help me enable the company's Fast
response feature so chat acknowledges my requests faster." The agent
correctly got 403 on the fast-response API. It then searched the API
catalog for "company settings page fast responses web UI" and found no
page. It linked the general company settings page
(`/<prefix>/company/settings`) and told the person to find Fast response
there. That page has no Fast response section. The section is on
/company/settings/connections.

**Expected behavior**

The agent links /<prefix>/company/settings/connections and tells the
person to use the Fast response section there. If the section is hidden,
the agent can say that an instance admin turns the experiment on.

**Steps to reproduce**

1. Turn on the fast responses experiment. Create a shared OpenRouter API
connection.
2. Open a chat with the CEO agent and ask it to enable Fast response.
3. Read the reply: it links /<prefix>/company/settings. Open that link:
the page has no Fast response control.

## What Changed

- `server/src/routes/openapi.ts`: add one shared description to the
fast-response, decision-model and prompt-injection-prevention catalog
entries. It names the section, the company-prefixed
/company/settings/connections page, the Markdown link form, and that
agent credentials cannot change or test the service. For Fast response,
it also names the instance experiment that hides the section.
-
`server/src/__tests__/company-model-service-settings-guidance.test.ts`:
new regression test. It runs the agent's catalog search (including the
query from the failed run) and checks that the results name the
Connections page and section. It also requires every page the
descriptions name to be a route that `ui/src/App.tsx` mounts, so
renaming a settings route fails CI.

## Verification

- `pnpm exec vitest run
src/__tests__/company-model-service-settings-guidance.test.ts` (from
`server/`): 6 tests pass. Without the catalog change, the 5 search tests
fail. If the Connections route in `ui/src/App.tsx` is renamed, the route
test fails.
- `pnpm exec vitest run src/__tests__/openapi-routes.test.ts`: 37 tests
pass.
- Chat eval `companies.fast-responses` (runner-opencode), with this
commit cherry-picked onto the eval branch: before the change, the reply
linked the general settings page, which has no Fast response control.
After the change (GitHub Actions run 42549422681), the eval passed. The
reply linked the Experimental settings page and the Connections page and
said to use the Fast response section there. The scripted person clicked
the reply's links and found the Fast response control on the Connections
page, then saved and tested it.

## Risks

- Low risk. The change adds catalog text only. It does not change any
API or permission behavior.
- If the settings page moves, these descriptions become wrong. The
regression test compares every page the descriptions name with the
routes in `ui/src/App.tsx`, so a route change that does not also update
the description fails CI.

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

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), tool use and code
execution in Claude Code.

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 21:55:48 -06:00
DottaandClaude Opus 5.5 f316562045 test(chaos): wait for new agents to be ready before the first assignment (#20158)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The restart chaos harness grades each run's time to first activity
against the speed budgets
> - The performance session traced an 8–12 s "first claim" delay on
every wave's first cold runs. A brand-new agent spends about 8 s in
`lifecycleState: verifying` while its harness runs a real hello probe,
and queued runs start only when it becomes ready
> - The chaos waves create agents and assign the first tasks in the same
second, so those runs carry the one-time verification. Their cold
first-activity and boot looked over budget even though dispatch after
ready takes 40–60 ms
> - This pull request makes the waves wait for each agent to be ready
before the first assignment, records how long verification took, and
fails the wave if an agent never verifies
> - The benefit is that cold-start grades measure task startup, while
agent verification stays visible as its own number

## Linked Issues or Issue Description

Refs #20119

**What happened?**
In waves `w-20261109T021322-s9801` (G1) and the K4/K6 wave, every new
agent's first cold runs waited 8–12 s in `heartbeat.queue`. The
performance session reproduced it locally: the agent is in
`lifecycleState: verifying` while a hello probe runs, and its queued
runs start on `onReady`. `ttft-baseline.ts` already waits for `ready`
before launching tasks, so it never showed the delay.

**Expected behavior**
The chaos waves start their first task after the agent is ready, as the
baseline does, and they report the verification time.

## What Changed

- `seed.ts`: new `waitForAgentsReady` polls each new agent until
`lifecycleState` is `ready`, with a 4-minute cap.
- It records `seed_agent_ready` with `readyAfterMs` per agent on the
timeline.
- It throws, failing the wave, if an agent reports a `lifecycleError` or
never becomes ready.
- `seedChaosLab` runs it when `waitForReadyAgents` is set. `main.ts`
sets it for every wave. `ttft-baseline.ts` keeps its own readiness
logic, which skips unready adapters.
- The seed test's fake API answers agent reads as ready.

## Verification

- `npx vitest run` in `tests/restart-chaos`: 205 passed. New tests cover
the wait, the verification-error failure and the timeout failure.
- `node scripts/typecheck-test-projects.mjs tests/restart-chaos`: clean.

## Risks

- Waves start about 8 s later, once per wave.
- A product regression that keeps agents in `verifying` now fails the
wave at seeding with a clear error, instead of showing as slow first
tasks.
- The one-time verification delay a user sees on a new agent's first
task is no longer in the cold grade. It stays in the timeline
(`seed_agent_ready`) for anyone tracking it.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, with tool use and code
execution in Claude Code.

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 21:32:11 -06:00
DottaandClaude Opus 5.5 43fe87795a fix(authz): make board-only denials final and actionable for agents (#20151)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - People talk to their agents in Agent Chat, and the agents act
through the Paperclip API
> - Many API routes are board-only. The shared `assertBoard` check
returns 403 `"Board access required"` to an agent, with no other detail
> - The chat guidance also told the agent to "use permitted discovery to
identify the supported operator path" after such a denial
> - So the agent keeps searching the API, `call_api` and the shell for a
second route. On a slow model, this search uses the whole reply window
and the person gets no answer
> - Earlier single-route fixes (#19990, #20041, #20008) showed the
remedy: a denial that says it is final and names where the person acts.
Reply time fell from 10-24 minutes to about 2 minutes
> - This pull request applies that remedy once, in the shared board-only
check and in the chat guidance
> - The benefit is a prompt, correct handoff for every board-only action
that an agent cannot do

## Linked Issues or Issue Description

Refs #19990, #20008, #20041 (same remedy for single routes).

**What happened**
In about 30 Agent Chat evaluation cases on a slower model, the agent got
403 "Board access required" (or read `authorization.actor: "board"` in
the API catalog). Then it searched for another route until the attempt
deadline. Examples: archive a company, set up a managed agent profile,
add a company secret. In one recorded run, the agent got the 403 at
minute 1, then tried PATCH, environment variables, the CLI and UI paths
for more than 3 more minutes, and never replied.

**Expected behavior**
A board-only denial is final. The agent stops, tells the person that a
board user must do the action, links the page in the Paperclip web app
where they can do it, and ends its reply.

**Steps to reproduce**
1. Open Agent Chat with a CEO agent.
2. Ask it to archive the company (or to add a company secret).
3. The agent calls `POST /api/companies/{id}/archive` and gets `{
"error": "Board access required" }`.
4. The body says nothing about finality or where the person acts. The
agent searches for other routes and replies late or not at all.

## What Changed

- `server/src/routes/authz.ts`: `assertBoard` now throws
`boardAccessRequired(req)`. For an agent actor, the 403 body adds `code:
"board_access_required"` and a `remediation`. The remediation says the
action is board-only by design, the denial is final, and no other route
can do it. When the API path maps to a known board page, it links that
page with a `<prefix>` placeholder: company settings, company secrets,
budgets, members, tool gateways, connection settings (decision model,
prompt injection prevention), an approval, or the agent profile tab that
holds the control (API keys, permissions, budget, identity, harness
settings). Otherwise it says to link a page only when the route is
already known, and not to search for one. Board and anonymous actors get
the same body as before.
- `environments.ts`, `instance-settings.ts` and the issue force-release
route used their own copy of the check. They now call `assertBoard`, so
they return the same denial. Status and error text do not change.
- `server/src/middleware/error-handler.ts`: for this code, the handler
does not repeat `code` and `remediation` inside `details`. They are
already top-level, and the copy only made the agent's context longer.
- `AGENT_CHAT_DIRECTIVE`: the board-identity paragraph now says that a
board-only operation (catalog actor `board`, or a 403 "Board access
required") is final. The agent must stop searching, tell the person
which control in the web app to use, and link it when it knows the
route. The old instruction to "discover the operator path" is removed.
- Claude Managed Agent setup: the Paperclip Runner configuration doc and
the `POST /api/companies/{companyId}/managed-agent-profiles` catalog
entry now say that a board operator creates and qualifies the profile
(limited environment, retention acknowledgement) and that agents cannot.
Before, an agent asked for an "Anthropic managed agent" explored hiring
and adapter docs for its whole reply window.
- Tests: new `board-access-denial.test.ts` covers the agent body for
each mapped page, the agent profile tabs, the general fallback, the
unmapped fast-response route, the org-access guard, the unchanged
non-agent body, the guidance and the managed profile catalog entry. Two
exact-body tests now also expect the new code.

## Verification

- `cd server && npx vitest run src/__tests__/board-access-denial.test.ts
src/__tests__/costs-service.test.ts
src/__tests__/browser-use-route-scope.test.ts
src/__tests__/primary-agent-guidance.test.ts
src/__tests__/agent-cross-tenant-authz-routes.test.ts
src/__tests__/company-portability-routes.test.ts
src/__tests__/issue-comment-cancel-routes.test.ts
src/__tests__/error-handler.test.ts
src/__tests__/managed-agent-profile-routes-authz.test.ts
src/__tests__/companies-route-cross-company-authz.test.ts
src/__tests__/environment-routes.test.ts
src/__tests__/environment-instance-routes.test.ts`: all pass.
- Real chat journeys on the chat evaluation branch (runner-opencode,
`deepseek-v4.1-flash`). Before this change, each case below got no reply
before its deadline:
- `companies.archive-company`: passed. Reply 47 s after the request
(before: none in 270 s). The agent read the remediation and linked
`/COM/company/settings`.
- `companies.configure-decision-model`: passed. Reply 68 s after the
request (before: none in 389 s). It linked
`/COM/company/settings/connections`.
- `companies.prompt-injection-guard`: reply 85 s after the request
(before: none in 390 s), with the correct handoff and link. The case
still fails later, in the scripted settings step: the saved model is the
default, not the model the script typed. That is a separate defect.
- `navigation-preferences.sidebar-order`: reply 99 s after the request
(before: none in 150 s). The case still fails in its scripted sidebar
step (a locator was not found).
- `secrets.configure-company-secret`: reply 6.5 min after the request
(before: none in 7.5 min). The reply is correct, but a grader keyword
check reads "chat is not a safe place to send it" as a request for the
secret.
- `agents.managed-profile`: still no reply in its 150 s window. After
the catalog change, the agent found the board-only profile setup within
30 s, but then spent about 90 s deciding what "Anthropic managed agent"
means.

## Risks

- Low risk. The HTTP status and the `error` text stay the same for every
actor. Board users and anonymous callers get the same body as before.
- Agent actors now get two more top-level fields (`code`,
`remediation`). A client that compares the whole 403 body of an agent
call exactly will see a change. Two server tests did this and are
updated.
- The page map uses the API path. A wrong map entry gives a less useful
link but does not change authorization.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`), with tool use and code
execution in Claude Code.

## Checklist

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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 21:28:52 -06:00
DottaandClaude Opus 5.5 10a37327e3 test(evals): grade ask-before-action replies by the reviewed record, not keywords
reviewOutcomeReply matched completion/negation grammar in each phase's
reply. The phase outcomes are already graded from saved records (card
resolution, action request and invocation status, provider write counts,
exact trust rule). The reply check now only requires each phase-bound
continuation to name the reviewed connection by its saved name or link
its app page. The keyword-grammar test is removed; the file path is kept
because the testops manifest lists it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 21:28:18 -06:00
DottaandClaude Opus 5.5 c9d6f615e3 test(evals): grade Studio test by its saved result record and result link
skills.studio-test graded the worker's free-form output and the chat
reply with keyword regex. The fixture skill now asks for a literal
result record (passed/failed counts and failing elements copied from
the input); the grader parses that record from the saved output and
requires a failure for the input's exact unlabeled email element, no
invented elements, and failures matching the failed count. The
completion reply must link the saved result (its test task, output
document or Studio run) instead of avoiding certain phrases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 21:28:17 -06:00
DottaandClaude Opus 5.5 2a6229161d fix(authz): do not link fast response denials to connection settings
Address review: the Connections page shows fast response controls only
while the experiment is on, so a denied fast response route now gets the
general board instruction instead of a link to a page that may lack them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 21:17:21 -06:00
DottaandClaude Opus 5.5 bc68e5c062 fix(authz): link connection settings and stop the page search
Two journeys showed agents searching for a web page after a board denial
that named none: the general fallback told them to link the most specific
page they knew. It now says to link a page only when its route is already
known and not to search for one. Prompt injection prevention, decision
model and fast response routes now link the connection settings page that
holds those controls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 21:17:21 -06:00
DottaandClaude Opus 5.5 9af18eba9d fix(authz): link the agent profile tab that holds a denied control
Address review: a denied POST /api/agents/{id}/keys linked the bare
profile, but API keys are only reachable at /api-keys. Map the agent
subresources with their own tab (keys, permissions, budgets, identity and
instructions, configuration) to that tab.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 21:17:21 -06:00
DottaandClaude Opus 5.5 41399e317a fix(agents): say who sets up a Claude Managed Agent profile
An agent asked to set up an Anthropic managed agent read the Paperclip
Runner configuration doc, which named Claude Managed but not who creates
its profile, and spent its reply window exploring hiring and adapter
configuration. The doc and the profile-creation catalog entry now say that
a board operator creates and qualifies the profile and that agents cannot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 21:17:21 -06:00
DottaandClaude Opus 5.5 e83d9bb529 fix(authz): make board-only denials final and actionable for agents
Agents that hit a board-only route got a bare 403 "Board access required"
and kept searching the API for another route until the chat attempt ran
out. The shared assertBoard denial now tells agent actors the action is
board-only by design, that no other route can do it, and names the board
page where the person acts when the route maps to one (company settings,
secrets, budgets, members, gateways, approvals, agent profile). The HTTP
status and error text are unchanged; agents also get code
board_access_required. Chat guidance treats a board-only operation as final
instead of asking the agent to discover an operator path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 21:17:20 -06:00
DottaandClaude Opus 5.5 fa7950121d fix(native): move an interrupted bootstrap's session aside on the run's own created sandbox lease (#20154)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - When restart recovery proves a remote bootstrap reached no provider
work, it bootstraps the same run again in the same sandbox. Before
relaunching, it stops the interrupted runner (#20103)
> - The relaunch starts a fresh controller record, whose command ids
restart at `command_prp_00000001`. The sandbox still held the
interrupted runner's journal, which had already recorded that command id
with different data
> - The relaunched runnerd rejected it ("commandId was reused with
different command data") and exited while waiting for `run.prepare`.
Every same-run retry collided the same way until the retry budget ran
out and the task blocked
> - The proven interrupted-bootstrap session move (#20103) ran only for
a resumed lease. A same-run retry on a per-turn sandbox reuses the run's
own created lease, so nothing moved the interrupted session aside
> - This pull request applies that same proof-gated session move on the
run's own created lease
> - The benefit is that a same-run bootstrap retry starts its runner on
a clean journal, and the old journal is kept as evidence

## Linked Issues or Issue Description

Refs #20103

**What happened?**
Daytona per-turn restart-chaos M1 rerun w-20261109T013733-s9501 (master
8e3b64a257, image 34bc12bf):
- **t10 (codex M1):** the codex subtask's run `f8a8f483` went through
eleven same-run bootstrap retries between 01:56 and 02:07.
- Each relaunched runnerd logged `paperclip-runnerd: commandId was
reused with different command data` and exited about 12 s later while
waiting for `run.prepare`.
- The run then failed `native_session_retry_exhausted` ("Provider
startup failed on 3 attempts before any provider work began"). The
subtask blocked, and so did the parent.
- **t02:** the same "runnerd exited while waiting for run.prepare"
pattern appeared on retry run `2e13568f`.

**Expected behavior**
A same-run bootstrap retry moves the interrupted session aside, keeping
it as a `.bootstrap-restart-<time>-<uuid>` sibling, and claims a fresh
session root before relaunching. The relaunched runner then cannot see
the interrupted attempt's command ids. This already happened on a
resumed lease and now also happens on the run's own created lease.

**Steps to reproduce**
1. Let a remote bootstrap reach the runner (it journals
`command_prp_00000001`), then restart before any provider work.
2. Recovery bootstraps the same run again in the same sandbox. Before
this change, the relaunched runnerd sees the old journal, rejects the
reused command id and exits, and every retry repeats this.

**Paperclip version or commit**
8e3b64a257 (wave). Branch rebased on current master.

## What Changed

- `native-session-executor.ts`: the created-lease branch of remote
harness preparation now applies the resumed branch's proven restart. It
runs when all of these hold:
  - the session is `not_suspended` and belongs to this run;
  - there is no backup;
  - the claim is `bootstrap_incomplete`;
- the stale-runner stop proved the interrupted runner gone
(`hasRemoteBootstrapRestartProof`).

It then runs `moveInterruptedBootstrapSessionAside` and
`claimUntouchedRemoteSession`. If the move or the claim fails, it throws
`runner_harness_state_mismatch: created_not_suspended` and changes
nothing.
- An earlier revision of this PR archived only the `runner` directory.
Review showed that would leave a session the existing claim cannot
reopen on a resumed lease, so that revision was replaced.
- On a fresh command namespace: moving the session aside removes every
id the relaunched runner could see, so a separate namespace isn't
needed. It would also change the persisted command id format that saved
controller state depends on.
- `native-session-executor.test.ts`: the existing "bootstraps only an
untouched provider session" matrix runs the real move and claim scripts
against a temporary filesystem. It gains two created-lease cases:
- an interrupted bootstrap: the old session is kept as a sibling and a
fresh root is claimed;
  - a symlinked parent: refused, and nothing moves.

## Verification

- Red/green: both created-lease cases fail on master and pass with the
change. The resumed cases from #20103 are unchanged: 42 cases in the
matrix pass.
- `vitest run` over the restart-recovery and bootstrap test files and
`native-session-executor.test.ts`: 7 files, 1540 tests passed (14
skipped).
- `PAPERCLIP_TYPECHECK_LOW_MEMORY=1 pnpm typecheck` (`server`): passed.
- The collision itself happens in runnerd (the production log shows it
on every retry). The live check is a Daytona per-turn K2/K4 rerun.

## Risks

- Low risk. The move reuses the resumed path's proof gates, its symlink
checks and its whole-session move, which keeps the session and never
deletes it. The new branch acts only when every gate holds; otherwise
preparation continues exactly as before.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), via Claude Code with tool use
(shell, file edits, 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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 21:06:26 -06:00
DottaandClaude Opus 5.5 510746c682 fix(native): restart a remote run whose controller never sent its runner a command (#20153)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A follow-up run on a remote sandbox inherits the previous run's
session checkpoint at admission, rebound to the new run's identity
> - A remote runner receives work only through the controller's Postgres
checkpoint, which the controller writes before every command. A run with
no checkpoint rows, no runner events and no recorded runner process
never sent its runner anything
> - Restart recovery still read the rebound admission checkpoint's
provider session id as provider evidence, and classified such a run
`reattach_remote_runner`. The executor then found the root still owned
by the previous run and failed with
`native_remote_recovery_lease_mismatch: stage=runner_verification
field=identity_run_id`
> - This pull request classifies that run as an incomplete bootstrap,
and lets the executor start it from the previous owner's settled root
> - The benefit is that a follow-up interrupted before its runner ever
got a command restarts on its own

## Linked Issues or Issue Description

Refs #20137

**What happened?**
Daytona per-turn restart-chaos M1 rerun w-20261109T013733-s9501 (master
8e3b64a257, image 34bc12bf), t02 (codex M1 parent):
1. Wake run `a867cb18` was admitted at 01:48:31 with a rebound
checkpoint inherited from run `dfeddafa`: identity of the new run,
`cursor: null`, no terminal turns, no active turn.
2. It recorded `native.process_start_requested` at 01:48:36, then a K4
hold froze the control plane.
3. At 01:53:11 recovery classified it `reattach_remote_runner`
(`hasCheckpoint: true`, `hasProviderEvidence: true`, from the rebound
checkpoint), although the run had no `native_runner_checkpoints` rows,
no runner-originated events and no recorded process identity.
4. The executor found the session root still owned by `dfeddafa` and
failed with `native_remote_recovery_lease_mismatch:
stage=runner_verification field=identity_run_id`. The task needed a
Retry.

**Expected behavior**
A remote run whose runner provably never received a command restarts its
bootstrap. The executor treats the previous owner's root as it would for
any follow-up start.

**Steps to reproduce**
1. Claim a remote sandbox follow-up that recorded
`native.process_start_requested` but has no controller checkpoint rows,
no runner events and no `native.process_identity_recorded`, with only
the admission-rebound session checkpoint.
2. Restart. Before this change, recovery claims
`reattach_remote_runner`, and the executor fails with `identity_run_id`.

**Paperclip version or commit**
8e3b64a257 (wave). Branch rebased on current master.

## What Changed

- `native-restart-recovery.ts`: the new
`remoteRunnerNeverReceivedCommands` condition.
- It applies to a remote sandbox run (not a plugin task) with a prepared
remote workspace when all of these hold:
    - no durable controller record, and no staged attachment;
- only the admission-rebound checkpoint (or none), and no provider
process events;
- no runner-originated events, and no
`native.process_identity_recorded`;
    - no run process fields;
- the same claimed and dead previous-controller conditions as
`remoteRunnerNeverLaunched`.
- It yields `bootstrap_incomplete` with the reason
`remote_runner_never_received_commands`, and does not count the rebound
checkpoint as this run's checkpoint.
- `native-session-executor.ts`:
- a `bootstrap_incomplete` claim requires the incomplete-bootstrap proof
only when the root carries this run's own identity;
- a root still owned by the previous run takes the normal previous-owner
path, and may retire a finished previous owner, as any follow-up start
does.
- Policy change, called out for review: a claimed remote run that
recorded only `native.process_start_requested` was held as
`ambiguous_remote_recovery_evidence`. Under the conditions above it now
bootstraps again.
- The controller writes every command to the Postgres checkpoint before
sending it, so with no checkpoint rows no command reached any runner,
and no provider work can exist.
- A runner process that was spawned but never commanded is stopped
before the new bootstrap by the existing stale-runner stop.
- A recorded process identity, an adapter invocation, a runner event,
process fields or a provider session still hold the run.
- Tests:
- `native-runner-restart-recovery.integration.test.ts`: the
classification for a follow-up with an inherited checkpoint and for a
first run without one, plus a held case when a runner process identity
was recorded. The old held expectation for "a runner process start was
requested" moved here;
- `native-recovered-owner-followup.test.ts`: the executor admitting a
`bootstrap_incomplete` follow-up from the previous owner's settled root.

## Verification

- Red/green:
- On master, the classification test claims `reattach_remote_runner`, as
in production. With the change it claims `bootstrap_incomplete` with
`remote_runner_never_received_commands`.
  - A recorded runner process identity keeps the run off this path.
  - The executor test is admitted with the change. On master it fails.
- `vitest run` over the restart-recovery, bootstrap, recovered-owner and
failed-workspace-recovery test files and
`native-session-executor.test.ts`: 9 files, all passing after the policy
test moved; `native-runner-restart-recovery.integration.test.ts` alone:
144 passed (14 skipped).
- `PAPERCLIP_TYPECHECK_LOW_MEMORY=1 pnpm typecheck` (`server`): passed.

## Risks

- Low risk:
- The new path requires positive proof that nothing reached a runner: no
controller record, no runner events and no recorded process. Any of
these keeps the existing classification.
- In the executor, a root that carries this run's identity still needs
the full bootstrap proof.
- A root that carries the previous run's identity goes through the same
previous-owner verification as any follow-up start.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), via Claude Code with tool use
(shell, file edits, 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

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 21:06:21 -06:00
DottaandClaude Opus 5.5 400e73d15c test(evals): keep backup-and-restore passable; note the wording gap in code
A declared coverage gap fails the catalog-complete-conditions check, so the
case could never pass. Follow the catalog practice for free-form replies
instead: grade structure, note in code that wording is not graded, and
read a passing run's replies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 21:05:25 -06:00
DottaandClaude Opus 5.5 e6f106b8f8 fix(api-catalog): describe the real backup and restore paths for agents
The database backup catalog entry had only a summary. An agent asked to
back up an instance and check a restore got 403, found no other path and
told the person to use an "Instance settings > Backups" page that does
not exist.

The entry now says who can call it, that the UI only sets backup
retention (on /company/settings), that operators can run
`paperclipai db:backup`, what the result file is, and that a restore check
goes into a separate empty database with psql, never over the live one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 21:05:24 -06:00
DottaandClaude Opus 5.5 432dacb484 test(evals): grade backup-and-restore replies by structure, record the wording gap
Both reply checks were keyword/regex lists over prose. They passed an
initial reply that invented an "Instance settings > Backups" page, and
they fail on valid rewording.

The replies are now graded by the runs and messages they belong to: the
canonical request has its own completed reply, and the second run answers
the operator's receipt in a saved, attributed reply. Reply truthfulness is
recorded as a coverage gap instead of a regex. The saved-state checks for
the archive and the separate-target restore are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 21:05:24 -06:00
DottaandClaude Opus 5.5 9926a13870 fix(tool-access): an unscoped tools:use grant does not turn on actions a connection left off
Every new standard agent gets an unscoped tools:use grant. The policy
checked explicit grants before profiles, so that default grant allowed
every action of every connection installed for the agent, including
actions the person left off when they finished the connection. The
agent's effective profile showed the action off while the test-as-agent
summary and the gateway allowed it.

When a profile in effect for the agent governs the tool's connection, only
a grant scoped to the action, connection or application now allows it. An
unscoped grant still applies to tools that no connection profile governs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 21:05:24 -06:00
DottaandClaude Opus 5.5 8384a85876 test(evals): grade test-as-agent on the product's access summary, not reply keywords
The reply check was a keyword/regex list over prose. It passed a reply that
said Morgan's destructive write was allowed while the saved scope was read
only, and it fails on valid rewording.

The reply check now keeps only structure (one saved reply after the test,
no credential disclosure). The observer reads the same test-as-agent access
summary the agent reads and requires it to report the read action allowed
and the write action off, so a misleading product summary fails the case.
A prose claim that contradicts the scope is not graded; that is recorded as
a coverage gap rather than a regex.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 21:05:24 -06:00
DottaandClaude Opus 5.5 54d2b96c2b fix(api-catalog): name the Connections page for company model service settings
Fast response, Decision model and Prompt Injection Prevention are set in
sections of /company/settings/connections, but their API catalog entries
named no page. An agent that could not change Fast response searched the
catalog for the settings page, found none and linked /company/settings,
which has no Fast response control.

The catalog descriptions now name the section, the company-prefixed
Connections page and, for Fast response, the instance experiment that
hides the section.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 21:05:24 -06:00
DottaandClaude Opus 5.5 b7c7ac5a33 test(evals): grade fast-responses by following the reply's own link to the control
The scripted human typed /company/settings/connections instead of clicking
the reply's link, and the reply checks accepted any "settings" mention, so a
reply that linked /company/settings (no Fast response section) passed.

The person now clicks each rendered reply link and must land on a page that
shows the Fast response region; with no such link the act stops before
configuring. The reply keyword checks for this case are removed. Other
services in the module keep their existing checks and oracle version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 21:05:24 -06:00
DottaandClaude Opus 5.5 763adbea09 fix(execution): a pause's stop never blocks the task behind an incident with no control (#20143)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - A budget hard stop pauses the agent, which stops its running work
> - In the restart-chaos H1 scenario, the stopped task went `blocked`
behind a board action that said "inspect and reconcile" and offered no
button, and it stayed there after the board resumed the budget
> - A pause is an intentional stop, not a failure, so it must not leave
the task with no way forward
> - This pull request treats a pause's stop as intentional: the task
keeps its status and no incident opens, so the normal Retry works
> - The benefit is that a budget or agent pause never strands a task
behind a control-less incident

## Linked Issues or Issue Description

Refs #20113, #20101

**What happened?**
Restart-chaos wave w-20261108T210118-s9802 (H1). At the budget pause,
task CHA-1 went `blocked` (21:02:27.870) with an active board
`active_run_watchdog` action, cause
`native_continuation_requires_reconciliation`, next action "Inspect the
original failure and reconcile the previous execution before
continuing." After raise_budget_and_resume it stayed blocked,
`canRetry=false`, with no Retry. The run was cancelled by the agent
lifecycle pause (`cancellation: { reason: "Agent lifecycle stop
requested", initiator: system, expected: true }`). The wake-queue
release (`recordNativeTerminalRecoveryIfNeeded`) blocks every cancelled
native run that is not an acknowledged user Stop.

**Expected behavior**
A pause's stop keeps the task's status and opens no incident; the task
can be continued.

**Steps to reproduce**
Pause an agent (budget hold or company budget pause) while its native
run is active. Before this change, the task is blocked behind a board
action with no control.

## What Changed

- New `server/src/services/governance-pause-stop.ts`:
`isGovernancePauseStop(run)` matches an expected system cancellation
whose reason is the budget pause stop, or the agent lifecycle stop with
a `pauseStop` stamp.
- `wake-queue/adapters/postgres.ts`
`recordNativeTerminalRecoveryIfNeeded`: returns early for such a run, so
the release keeps the task's status and opens no reconciliation
incident.
- `agent-lifecycle-driver.ts`: the `pausing` host step stamps
`resultJson.pauseStop = { holds, companyPauseReason }` on the agent's
active runs before stopping them (one update, company pause reason by
subquery). Termination and cleanup share the lifecycle stop reason but
write no stamp, so they still open the incident.

## Verification

- `wake-queue/adapters/postgres.test.ts`: new cases. A run stopped by
the lifecycle pause or by the budget pause keeps the task `in_progress`
with no incident (both red on master). Agent termination, and another
system stop (company archive), still open the incident.
- `agent-lifecycle/__tests__/lifecycle.test.ts`: "records what a pause
stopped each active run for". `driver-scope.test.ts` updated for the
stamp's single update.
- `wake-queue` (107), `lifecycle` (68+), `driver-scope`,
`companies-service`: 199 passed. Server typecheck (`typecheck-layers`,
low-memory): no errors.
- Not run: the H1 chaos rerun (after the follow-up continuation PR).

## Risks

- A native run that a pause stopped no longer opens a reconciliation
incident. Resuming it is the normal Retry, which continues the
conversation. Termination, archive and failures still open one.
- One extra update per pause attempt on the agent's active runs. It is
idempotent (skips runs that already have the stamp) and not on any turn
path.

> 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

- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, via Claude Code with
tool use (shell, file edits, test execution). Extended reasoning
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
- [ ] 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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 20:55:36 -06:00
DottaandClaude Opus 5.5 3eea8b87bd fix(native): settle a finished owner whose journal holds a definitively answered command (#20148)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Before a per-turn follow-up starts a new runner, it retires the
previous turn's finished owner. Retirement requires that owner's
controller journal to be settled: every command answered, then a run
terminal
> - A restart can cut off a continuation turn. The recovered runner
answers that `turn.start` with `failed` and the nested code
`execution_indeterminate` ("it will not execute twice"), and the run
continues on a later turn to an accepted result
> - Retirement counted only `completed` commands as answered. A finished
run whose journal held that answered `turn.start` never counted as
settled, so the follow-up failed with
`retirement_refused=journal_commands_unsettled(turn.start.failed)` and
needed a Retry
> - This pull request counts definitive `failed` and `rejected` answers
as settled, except for runner lifecycle commands. An unknown-outcome
`turn.start` counts as settled only once a later `turn.start` completed,
which shows the run moved past it. Every other unknown or pending
command stays unsettled
> - The benefit is that a follow-up after a restart-interrupted, then
finished, run continues on its own

## Linked Issues or Issue Description

Refs #20140

**What happened?**
Local restart-chaos M1 K4/K6 rerun w-20261109T003126-s9701 (master
8e3b64a257), t07 (claude C1, K6):
- The previous run `9cb2c0b3` lost a continuation turn to a restart. The
runner answered that `turn.start` as `failed` with `result.result.code:
"execution_indeterminate"`.
- After the next restore it ran a new continuation to an accepted
result, and the run terminal was committed at 00:48:18.
- A hard kill then landed before the runner suspended, and recovery
finalized the result.
- Follow-up `372991c8` failed at 00:48:37 with
`runner_state_identity_mismatch:
prior_owner_terminal_state_indeterminate;
retirement_refused=journal_commands_unsettled(turn.start.failed)`.

**Expected behavior**
A command the runner definitively answered (`failed` or `rejected`)
leaves nothing outstanding, so it counts as settled. A `turn.start` with
an unknown outcome is never executed again by the runner, so once a
later `turn.start` completed, the run has moved past it and it also
counts as settled. The follow-up then retires the finished owner and
continues.

The following still count as unsettled:
- an unknown-outcome `turn.start` with no later completed turn;
- any other unknown-outcome command;
- a `pending` command;
- a failed runner lifecycle command, such as a failed `runner.suspend`
(the runner did not suspend).

**Steps to reproduce**
1. Finish a per-turn run whose controller journal holds a `turn.start`
answered `failed`, followed by a committed run terminal.
2. Start a follow-up. Before this change, retirement refuses with
`journal_commands_unsettled(turn.start.failed)`.

**Paperclip version or commit**
8e3b64a257 (wave). Branch rebased on current master.

## What Changed

- `remote-warm-session-recovery.ts`: the new
`unsettledControllerCommands` decides which commands are settled.
`isSettledRemoteWarmControlState` uses it.
  - `completed` is settled.
- An unknown outcome (raw `indeterminate`, or `failed` with
`execution_indeterminate` at `result.code` or `result.result.code`) is
settled only for a `turn.start` with a later completed `turn.start`.
- Other `failed` or `rejected` answers are settled, except for
`runner.suspend`, `runner.drain` and `runner.shutdown`.
- `native-session-executor.ts`: the retirement refusal message lists
unsettled commands with the same rule.
- `native-recovered-owner-followup.test.ts`: a red/green test whose
journals are in real lifecycle order.
- Admitted: a `turn.start` answered `failed` or `rejected`; a superseded
unknown-outcome `turn.start` in the runner's stored nested shape; a
superseded raw `indeterminate`.
- Held: the same unknown-outcome answer with no later completed turn; a
failed `runner.suspend`.

## Verification

- Red/green: on master, the admitted cases fail with exactly the t07
error. With the change they are admitted. The held cases stay held. If
only `result.code` is checked, the unsuperseded case fails.
- `vitest run` of `native-recovered-owner-followup.test.ts`,
`remote-warm-session-recovery.test.ts` and
`native-session-executor.test.ts`: 3 files, 1403 tests passed.
- `PAPERCLIP_TYPECHECK_LOW_MEMORY=1 pnpm typecheck` (`server`): passed.

## Risks

- Low risk. Definitive answers to non-lifecycle commands, and
unknown-outcome turn starts that a completed later turn superseded,
become settled.
- The run terminal requirement, post-terminal live-work checks, and
every lifecycle and process proof are unchanged.
- The same rule now also applies to remote warm recovery's settled
check, where a definitively answered command is equally finished.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), via Claude Code with tool use
(shell, file edits, 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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
canary/v2026.1109.0-canary.3
2026-11-08 20:47:05 -06:00
DottaandClaude Opus 5.5 cedf335c98 fix(native): a failed terminal followed by an active restart continuation is not a permanent failure (#20149)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - When a restart loses a native run's active turn, the run records
that turn as failed with `provider_turn_lost_on_restore` and continues
on a same-conversation continuation turn
> - Restart recovery blocks a run as
`provider_checkpoint_permanently_failed` when its checkpoint carries a
failed run terminal and no pending restart interruption
> - While a continuation is running, the checkpoint keeps the lost
turn's failed run terminal and names the continuation as the active
turn. The pending-interruption check returns nothing once a turn is
active
> - So a second restart during the continuation read the run as
permanently failed, blocked replay, and needed the board
> - This pull request treats that failed terminal as the restart loss it
records whenever an active turn follows a restart-lost terminal turn
> - The benefit is that runs survive repeated restarts during their
continuations, including M1 parents and their subtasks

## Linked Issues or Issue Description

Refs #20148

**What happened?**
Local restart-chaos M1 K4/K6 rerun w-20261109T003126-s9701 (master
8e3b64a257), t05 (claude M1 parent):
1. Run `e1e37dab` lost turns to restarts at 00:41:32 and 00:47:53. Each
time it continued, and the third continuation `turn_lab_92b03dc7…` was
active.
2. At 00:48:29 the next restart blocked it:
`native_restart_recovery_blocked` /
`provider_checkpoint_permanently_failed`, with replay blocked.
3. Its saved checkpoint held `terminal.runTerminalState: "failed"` (from
the lost turn), `activeTurnId` set to the continuation, and a last
terminal turn fingerprinted `provider_turn_lost_on_restore`.

Its codex/ACPX subtask's run `f9b10d06` was blocked in the same pass
with the same shape, and its continuation had already proposed a
semantic result. Both stayed blocked. The parent's Retry was held on the
blocked child.

**Expected behavior**
A failed run terminal that records a restart loss, followed by an active
continuation turn, is not a permanent failure. Recovery claims
`resume_dead_runner`. The runtime then reconciles the active turn, and a
held result settles a lost turn as completed. A real provider failure
followed by a later turn still blocks.

**Steps to reproduce**
1. Checkpoint a run whose last terminal turn is a restart loss, whose
`terminal` is failed, and whose `activeTurnId` is a later continuation
(optionally with a semantic result).
2. Claim restart recovery. Before this change it blocks with
`provider_checkpoint_permanently_failed`.

**Paperclip version or commit**
8e3b64a257 (wave). Branch rebased on current master.

## What Changed

- `contracts/native-session-backend.ts` (runner): the new
`nativeRestartContinuationInFlight` is true when all of these hold:
  - the checkpoint has an active turn;
  - its last terminal turn is a different turn;
- that turn's fingerprint is a failed, result-less, recoverable
`provider_turn_lost_on_restore`.
- `native-session-runtime.ts`: recovery clears that stale failed
terminal for a continuation in flight, as it already does for a
checkpointed interruption. Before, it threw
`provider_checkpoint_failed_terminal` before it reconciled the active
turn.
- `native-restart-recovery.ts` (server): `checkpointFailed` now also
requires that no restart continuation be in flight. It uses the same
contract function through the vendor boundary.
- `native-session-runtime.test.ts`: an end-to-end recovery test. A
checkpoint with the stale failed terminal and an active continuation
resolves on the continuation's result without starting a new turn.
- `native-runner-restart-recovery.integration.test.ts`:
  - a codex case of the stale-terminal shape as saved in production;
  - an ACPX case with a held result;
- a held case where an active turn follows a real provider failure
(`model_not_found`).

## Verification

- Red/green: on master, the stale-terminal case is blocked with
`provider_checkpoint_permanently_failed`. With the change it claims
`resume_dead_runner`. The real-failure case stays blocked on both.
- Red/green (runtime): without the runtime change, the end-to-end test
throws `provider_checkpoint_failed_terminal`. With it, the run
completes.
- `vitest run` over the restart-recovery and failed-workspace-recovery
test files in `server/src/services/native-runtime`: 5 files, 305 tests
passed (14 skipped).
- `vitest run` over the runner restart, recovery, continuation and
terminal test files: 9 files, 385 tests passed.
- `PAPERCLIP_TYPECHECK_LOW_MEMORY=1 pnpm typecheck` (`server`) and `pnpm
run typecheck:typescript` (`packages/paperclip-runner`): passed.

## Risks

- Low risk. The exception applies only when the recorded failure is
itself a recoverable restart loss and a later turn is in flight. Every
other recovery gate is unchanged: the provider-attempt budget,
controller takeover and process proofs.

## Model Used

- Claude Opus 5.5 (`claude-opus-5-5`), via Claude Code with tool use
(shell, file edits, 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

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 20:28:12 -06:00
DottaandClaude Opus 5.5 59076e62c4 test(evals): grade hire-denied by the saved state and the rendered hiring route
The setup probe accepted any 403, but an agent key creating a process
agent is also rejected by the adapter-config gate, so the fixture passed
while the CEO title still allowed hiring through agent-hires. The probe
now requires the agents:create denial text. The grader no longer reads
the reply's wording: it requires no new agent (any name), no hire or
delegation record, and a rendered link to the hire form or the speaking
agent's permissions page, which verifyResult clicks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 20:13:57 -06:00
Dotta ec9c732c0b Merge remote-tracking branch 'origin/master' into chat-eval/master-candidate
* origin/master: (74 commits)
  test(chaos): retry a blocked M1 subtask and check the parent's stall wake (#20147)
  test(runner-e2e): hide the eval harness from local native agents (#20044)
  fix(agents): honor an explicit hiring denial on CEO agents (#20041)
  fix(native): roll back an undelivered warm attach rotation once its runner is proven dead (#20146)
  fix(native): retire a finished per-turn owner past its own quarantined interrupted bootstrap (#20140)
  fix(github): send gh repo create and fork to the connection's tools (#20142)
  fix(github): default repository owner to the only installed organization (#20141)
  fix(runner): an unknown agent id says which id and how to find the right one (#20135)
  feat(issues): wake the parent once when a child or blocker stalls on the board (#20098)
  docs(plans): record the measured seed experiment for per-turn uploads (#20139)
  fix(native): prove a drained remote start had no turn when its controller only read the goal (#20137)
  fix(native): verify a staged attachment restart on the claimed sandbox lease (#20136)
  fix(issues): a Retry held by a blocked subtask names it instead of failing silently (#20109)
  fix(recovery): settle a dead controller's result lease for a run a pause cancelled (#20113)
  test(chaos): add R1, a code change in a fresh clone of a real repository (#20111)
  test(chaos): separate workspace provisioning from agent boot; grade outage-created runs as restart (#20119)
  fix(runner): keep a restart continuation when a stale earlier continuation is reported lost (#20118)
  fix(runner): resume an ACPX turn interrupted by a deploy instead of refusing the launch digest (#20112)
  fix(runner): a finish or block waits for held calls before it is judged (#20108)
  fix(runner): reopen the ACPX session at turn.start when it can be safely reopened (#20110)
  ...

# Conflicts:
#	packages/paperclip-runner/src/cli/opencode-app-server-proxy.ts
#	server/src/__tests__/agent-conversations.test.ts
#	server/src/routes/agents.ts
#	server/src/services/agent-conversations.ts
#	tests/runner-e2e/live-fixtures.ts
2026-11-08 20:13:48 -06:00