Merge master into PAP-659-simplify-connectors-ux

Brings the branch level with master so it can run against a staging stack
whose schema is already at 0290. The branch image predated four migrations
(including 0289_drop_user_keyboard_shortcuts) and failed every
authenticated request there.

Conflicts resolved toward the one-screen connect flow:
- Master's read-only capability disclosure is folded into the existing
  single Advanced panel rather than added as a second one; the stated
  capability line ("Read & create: ...") is kept.
- Master's onCancel prop for gateway setup is kept alongside companyId.
- Master's new tests that still stepped through the Access step now use
  the single screen.
- pnpm-lock.yaml takes master's copy; the branch's lockfile diff was
  accidental patch-hash churn.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
scotttongandClaude Opus 5.5 committed 2026-09-30 13:24:18 -07:00
commit 673b03496b
786 files changed
+107799 -53083

No files matched your search

+4
View File
@@ -6,6 +6,10 @@ XAI_API_KEY=
OPENROUTER_API_KEY=
DAYTONA_API_KEY=
# Explicit-only extended-harnesses candidates.
CURSOR_AUTH_TOKEN=
COPILOT_GITHUB_TOKEN=
# Required only for Daytona cells. Use an immutable, anonymously pullable
# digest from the Runner Full-Stack E2E image job or a locally published image.
PAPERCLIP_E2E_DAYTONA_IMAGE=ghcr.io/paperclipai/paperclip-daytona-runner@sha256:REPLACE_ME
+8 -1
View File
@@ -47,7 +47,14 @@ jobs:
version: 9.15.4
- name: Install workspace dependencies
run: pnpm install --frozen-lockfile
# Master owns the lockfile; PR verification can start before its refresh.
# Resolve like the normal PR jobs, but this narrow contract needs no
# dependency or workspace lifecycle scripts, including on the fast path.
run: |
if ! pnpm install --frozen-lockfile --ignore-scripts; then
pnpm install --resolution-only --ignore-scripts --no-frozen-lockfile
pnpm install --frozen-lockfile --ignore-scripts
fi
- name: Install the audited optional SDK outside the workspace
shell: bash
+1 -4
View File
@@ -74,10 +74,7 @@ pnpm dev
1. Keep changes company-scoped.
Every domain entity should be scoped to a company and company boundaries must be enforced in routes/services.
Explicit exceptions: personal keyboard shortcut enablement is stored on the
authenticated user and applies across companies, like the user profile. Only
that user can read or change it; writes validate company membership for their
audit context. Announcement dismissals are instance-wide user preferences,
Explicit exception: announcement dismissals are instance-wide user preferences,
keyed by user and announcement so they persist across companies. Their audit
context must still validate company membership. The announcement publication-ID
registry is instance-level feed metadata; it contains no company or user data.
+2
View File
@@ -108,3 +108,5 @@ tokenize motion. Principles — reasoning only; values live in `ui/src/index.css
- **Reduced motion is honored at the token layer.** A `prefers-reduced-motion: reduce`
block collapses the duration/stagger tokens to zero, cascading to every scoped token,
in addition to each animation's own component-level guard.
Agent Chat keeps pending questions as compact “Unanswered question” entries at their original position in history. A newer user message dismisses the old question form without resolving it. Opening the history entry restores the original form and its draft; submitting later uses the same durable question response path. Actual permission reviews retain their permission checks.
+142 -93
View File
@@ -24,6 +24,10 @@
<video src="https://github.com/user-attachments/assets/773bdfb2-6d1e-4e30-8c5f-3487d5b70c8f" width="600" controls></video>
</div>
<p align="center">
<a href="https://paperclip.ing/waitlist/"><strong>Sign up for the Paperclip Cloud waitlist →</strong></a>
</p>
<br/>
# Paperclip is the app people use to manage AI agents for work.
@@ -32,7 +36,7 @@ Open-source orchestration for teams of AI agents.
**If OpenClaw is an _employee_, Paperclip is the _company_.**
Paperclip is a Node.js server and React UI that orchestrates a team of AI agents to run a business. Bring your own agents, assign goals, and track work and costs from one dashboard.
Paperclip is a Node.js server and React UI that orchestrates a team of AI agents to run a business. Bring your own agents, assign goals, and track work and costs from one dashboard. Choose models and harnesses per agent while keeping your team's tasks, skills, permissions, and history in one place.
It looks like a task manager. Under the hood: org charts, budgets, governance, goal alignment, and agent coordination.
@@ -49,13 +53,19 @@ It looks like a task manager. Under the hood: org charts, budgets, governance, g
<div align="center">
<table>
<tr>
<td align="center"><strong>Works<br/>with</strong></td>
<td align="center"><img src="doc/assets/logos/openclaw.svg" width="32" alt="OpenClaw" /><br/><sub>OpenClaw</sub></td>
<td align="center"><img src="doc/assets/logos/claude.svg" width="32" alt="Claude" /><br/><sub>Claude Code</sub></td>
<td align="center"><img src="doc/assets/logos/codex.svg" width="32" alt="Codex" /><br/><sub>Codex</sub></td>
<td align="center"><img src="doc/assets/logos/cursor.svg" width="32" alt="Cursor" /><br/><sub>Cursor</sub></td>
<td align="center"><img src="doc/assets/logos/bash.svg" width="32" alt="Bash" /><br/><sub>Bash</sub></td>
<td align="center"><img src="doc/assets/logos/http.svg" width="32" alt="HTTP" /><br/><sub>HTTP</sub></td>
<td align="center" rowspan="2"><strong>Works<br/>with</strong></td>
<td align="center" valign="top"><img src="doc/assets/logos/openclaw.svg" width="32" height="32" alt="OpenClaw" /><br/><sub>OpenClaw</sub></td>
<td align="center" valign="top"><img src="doc/assets/logos/claude.svg" width="32" height="32" alt="Claude Code" /><br/><sub>Claude Code</sub></td>
<td align="center" valign="top"><img src="ui/public/brands/codex-color.svg" width="32" height="32" alt="Codex" /><br/><sub>Codex</sub></td>
<td align="center" valign="top"><picture><source media="(prefers-color-scheme: dark)" srcset="ui/public/brands/adapters/cursor-dark.svg" /><img src="ui/public/brands/adapters/cursor.svg" width="32" height="32" alt="Cursor and Cursor Cloud" /></picture><br/><sub>Cursor<br/>+ Cloud</sub></td>
<td align="center" valign="top"><img src="ui/public/brands/adapters/gemini-color.svg" width="32" height="32" alt="Gemini CLI" /><br/><sub>Gemini CLI</sub></td>
</tr>
<tr>
<td align="center" valign="top"><picture><source media="(prefers-color-scheme: dark)" srcset="ui/public/brands/opencode-logo-dark-square.svg" /><img src="ui/public/brands/opencode-logo-light-square.svg" width="32" height="32" alt="OpenCode" /></picture><br/><sub>OpenCode</sub></td>
<td align="center" valign="top"><picture><source media="(prefers-color-scheme: dark)" srcset="ui/public/brands/adapters/pi-dark.svg" /><img src="ui/public/brands/adapters/pi.svg" width="32" height="32" alt="Pi" /></picture><br/><sub>Pi</sub></td>
<td align="center" valign="top"><picture><source media="(prefers-color-scheme: dark)" srcset="ui/public/brands/adapters/hermesagent-dark.svg" /><img src="ui/public/brands/adapters/hermesagent.svg" width="32" height="32" alt="Hermes and Hermes Gateway" /></picture><br/><sub>Hermes<br/>+ Gateway</sub></td>
<td align="center" valign="top"><picture><source media="(prefers-color-scheme: dark)" srcset="ui/public/brands/adapters/grok-dark.svg" /><img src="ui/public/brands/adapters/grok.svg" width="32" height="32" alt="Grok Build" /></picture><br/><sub>Grok Build</sub></td>
<td align="center" valign="top"><picture><source media="(prefers-color-scheme: dark)" srcset="ui/public/brands/adapters/kimi-color.svg" /><img src="ui/public/brands/adapters/kimi-color-light.svg" width="32" height="32" alt="Kimi Code" /></picture><br/><sub>Kimi Code</sub></td>
</tr>
</table>
@@ -63,6 +73,8 @@ It looks like a task manager. Under the hood: org charts, budgets, governance, g
</div>
Custom processes, HTTP endpoints, and external adapter packages extend the roster. See the [adapter overview](https://docs.paperclip.ing/reference/adapters/overview/) for setup and capabilities.
<br/>
## Paperclip is right for you if
@@ -90,9 +102,9 @@ Four things have to work for an organization of AI agents to actually produce: t
| Pillar | Built for | What it covers |
| --- | --- | --- |
| **Agentic Task Manager** — Declare intent. Agents work. You verify the output. | Everyone, daily | Tasks, approvals & review gates · proactive agent coworkers · auditable routines & workflows · verify from diffs, screenshots & tests |
| **Org Chart for Agents** — Roles, permissions & boundaries for humans and agents. | Managers | Mixed human + agent org chart · responsibilities, delegation, specialization · governance: who can do what · scoped secrets & company boundaries |
| **Agent Employee Training** — Design, train & evaluate your AI employees. | Enablers | Skill Studio & shared org-wide skills · evals & saved test runs · active learning loops & quality metrics · performance reviews for agents |
| **Agentic OS** — The infrastructure that makes the work run. | IT & platform | Cross-provider runtime: any model, any agent · sandboxing, integrations & MCP servers · SSO, GRC, RBAC & cost controls · data privacy, internal trace collection, compounding data value |
| **Org Chart for Agents** — Roles, permissions & boundaries for humans and agents. | Managers | Mixed human + agent org chart · responsibilities, delegation, specialization · governance: who can do what · scoped secrets & company boundaries · connection permissions & responsible-user identities |
| **Agent Employee Training** — Design, train & evaluate your AI employees. | Enablers | Skill Studio & shared org-wide skills · evals & saved test runs · active learning loops & quality metrics · performance reviews for agents · saved test inputs · skill version history & restore · reusable team templates |
| **Agentic OS** — The infrastructure that makes the work run. | IT & platform | Cross-provider runtime: any model, any agent · sandboxing, integrations & MCP servers · SSO, GRC, RBAC & cost controls · data privacy, internal trace collection, compounding data value · personal & shared app connections · run history & opt-in tracing |
<br/>
@@ -106,31 +118,31 @@ Any agent, any runtime, one org chart. If it can receive a heartbeat, it's hired
</td>
<td align="center" width="33%">
<h3>🎯 Goal Alignment</h3>
Every task traces back to the organization mission. Agents know <em>what</em> to do and <em>why</em>.
Link tasks and projects to your organization goals. Agents receive the goal context behind their work.
</td>
<td align="center" width="33%">
<h3>💓 Heartbeats</h3>
Agents wake on a schedule, check work, and act. Delegation flows up and down the org chart.
Agents wake for assigned work, follow-up messages, or configured schedules. Delegation flows up and down the org chart.
</td>
</tr>
<tr>
<td align="center">
<h3>💰 Cost Control</h3>
Monthly budgets per agent. When they hit the limit, they stop. No runaway costs.
Company, agent, and project <a href="https://docs.paperclip.ing/guides/day-to-day/costs/">budgets</a>. Track reported spend, get threshold alerts, and pause work at configured limits.
</td>
<td align="center">
<h3>🏢 Multi-Organization</h3>
One deployment, many organizations. Complete data isolation. One control plane for your portfolio.
One deployment, many organizations. Separate tasks, agents, permissions, and activity histories for each.
</td>
<td align="center">
<h3>🎫 Ticket System</h3>
Every conversation traced. Every decision explained. Full tool-call tracing and immutable audit log.
<h3>🎫 Task Threads</h3>
Keep conversations, plans, blockers, files, and run history attached to the work. Assign tasks to agents or people.
</td>
</tr>
<tr>
<td align="center">
<h3>🛡️ Governance</h3>
Approve hires, override strategy, pause or terminate any agent — at any time.
Configure <a href="https://docs.paperclip.ing/guides/day-to-day/approvals/">review and approval stages</a>, approve hires, and pause, reassign, or stop work when needed.
</td>
<td align="center">
<h3>📊 Org Chart</h3>
@@ -141,8 +153,38 @@ Hierarchies, roles, reporting lines. Your agents have a boss, a title, and a job
Monitor and manage your autonomous businesses from anywhere.
</td>
</tr>
<tr>
<td align="center">
<h3>🔗 Apps & Connections</h3>
<a href="https://docs.paperclip.ing/connectors/">Connect services</a> such as GitHub, Notion, and Railway, or your own MCP server. Set gateway actions to Allowed, Ask first, or Off.
</td>
<td align="center">
<h3>👥 Shared Agents, Personal Accounts</h3>
Choose <a href="https://docs.paperclip.ing/connectors/access-model/">who can use a connection and which agents can access it</a>. Managed GitHub operations can use the account of the person directing the work.
</td>
<td align="center">
<h3>🧠 Skills & Skill Studio</h3>
Install or write <a href="https://docs.paperclip.ing/guides/org/skills/">shared skills</a>, test them with saved inputs, inspect results, and restore earlier versions.
</td>
</tr>
<tr>
<td align="center">
<h3>📅 Scheduled Routines</h3>
Run <a href="https://docs.paperclip.ing/guides/projects-workflow/routines/">recurring work</a> on a schedule or trigger it through an API or webhook. Each run has a task, an owner, and a history.
</td>
<td align="center">
<h3>📎 Artifacts & Feedback</h3>
Find the <a href="https://docs.paperclip.ing/guides/day-to-day/artifacts/">files and documents agents produce</a>. Preview supported formats and leave comments on specific passages in documents.
</td>
<td align="center">
<h3>📦 Ready-Made Teams</h3>
Preview and install <a href="https://docs.paperclip.ing/guides/org/team-catalog/">teams</a> with roles, skills, projects, and routines. Choose their runtimes and make the setup your own.
</td>
</tr>
</table>
Experimental **Agent Chat** and **chat/email connectors** add conversations with agents in Paperclip and through configured services such as Slack, Discord, Telegram, and AgentMail. Enable the relevant instance settings to try them.
<br/>
## Problems Paperclip solves
@@ -152,8 +194,8 @@ Monitor and manage your autonomous businesses from anywhere.
| ❌ You have 20 Claude Code tabs open and can't track which one does what. On reboot you lose everything. | ✅ Tasks are ticket-based, conversations are threaded, sessions persist across reboots. |
| ❌ You manually gather context from several places to remind your bot what you're actually doing. | ✅ Context flows from the task up through the project and company goals — your agent always knows what to do and why. |
| ❌ Folders of agent configs are disorganized and you're re-inventing task management, communication, and coordination between agents. | ✅ Paperclip gives you org charts, ticketing, delegation, and governance out of the box — so you run a company, not a pile of scripts. |
| ❌ Runaway loops waste hundreds of dollars of tokens and max your quota before you even know what happened. | ✅ Cost tracking surfaces token budgets and throttles agents when they're out. Management prioritizes with budgets. |
| ❌ You have recurring jobs (customer support, social, reports) and have to remember to manually kick them off. | ✅ Heartbeats handle regular work on a schedule. Management supervises. |
| ❌ Runaway loops waste hundreds of dollars of tokens and max your quota before you even know what happened. | ✅ Spend tracking, budget alerts, and automatic pauses help you control the cost of ongoing work. |
| ❌ You have recurring jobs (customer support, social, reports) and have to remember to manually kick them off. | ✅ Routines create assigned tasks on a schedule, with outputs and run history you can inspect. |
| ❌ You have an idea, you have to find your repo, fire up Claude Code, keep a tab open, and babysit it. | ✅ Add a task in Paperclip. Your coding agent works on it until it's done. Management reviews their work. |
<br/>
@@ -164,13 +206,14 @@ Paperclip handles the hard orchestration details correctly.
| | |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| **Atomic execution.** | Task checkout and budget enforcement are atomic, so no double-work and no runaway spend. |
| **Persistent agent state.** | Agents resume the same task context across heartbeats instead of restarting from scratch. |
| **Atomic task checkout.** | A single assignee and execution locks prevent competing runs from claiming the same task. |
| **Persistent work context.** | Tasks, comments, and documents stay in Paperclip. Supporting adapters resume saved sessions across runs. |
| **Runtime skill injection.** | Agents can learn Paperclip workflows and project context at runtime, without retraining. |
| **Governance with rollback.** | Approval gates are enforced, config changes are revisioned, and bad changes can be rolled back safely. |
| **Goal-aware execution.** | Tasks carry full goal ancestry so agents consistently see the "why," not just a title. |
| **Accountable connections.** | Human access, agent eligibility, and gateway action permissions are separate controls. Approve a call once or save a revocable rule. |
| **Goal-aware execution.** | Linked tasks and projects carry goal ancestry so agents see the "why," not just a title. |
| **Portable company templates.** | Export/import orgs, agents, and skills with secret scrubbing and collision handling. |
| **True multi-organization isolation.** | Every entity is company-scoped, so one deployment can run many companies with separate data and audit trails. |
| **Organization boundaries.** | Company-scoped access checks keep each organization's work, agents, and activity separate within one deployment. |
<br/>
@@ -210,7 +253,7 @@ Paperclip is a full control plane, not a wrapper. Before you build any of this y
<tr>
<td width="50%">
**Identity & Access** — Two deployment modes (trusted local or authenticated), board users, agent API keys, short-lived run JWTs, company memberships, invite flows, and OpenClaw onboarding. Every mutating request is traced to an actor.
**Identity & Access** — Two deployment modes (trusted local or authenticated), human roles and permissions, agent API keys, short-lived run JWTs, company memberships, and invite flows. Responsible-user attribution follows work through delegation and supported managed connections.
</td>
<td width="50%">
@@ -222,31 +265,31 @@ Paperclip is a full control plane, not a wrapper. Before you build any of this y
<tr>
<td>
**Work & Task System** — Issues carry company/project/goal/parent links, atomic checkout with execution locks, first-class blocker dependencies, comments, documents, attachments, work products, labels, and inbox state. No double-work, no lost context.
**Work & Task System** — Issues carry company/project/goal/parent links, atomic checkout with execution locks, first-class blocker dependencies, comments, documents, attachments, work products, labels, and inbox state. Search across work, review document revisions, and leave anchored feedback.
</td>
<td>
**Heartbeat Execution** — DB-backed wakeup queue with coalescing, budget checks, workspace resolution, secret injection, skill loading, and adapter invocation. Runs produce structured logs, cost events, session state, and audit trails. Recovery handles orphaned runs automatically.
**Heartbeat Execution** — DB-backed wakeup queue with coalescing, budget checks, workspace resolution, secret injection, skill loading, and adapter invocation. Runs produce logs, usage records, and adapter-specific session state. Bounded recovery handles supported failures and surfaces cases that need human action.
</td>
</tr>
<tr>
<td>
**Workspaces & Runtime** — Project workspaces, isolated execution workspaces (git worktrees, operator branches), and runtime services (dev servers, preview URLs). Agents work in the right directory with the right context every time.
**Workspaces & Runtime** — Project repositories and workspaces, optional isolated execution workspaces (git worktrees, operator branches), and runtime services (dev servers, preview URLs). Sandbox providers extend execution beyond the local host; availability depends on the configured environment and adapter.
</td>
<td>
**Governance & Approvals** — Board approval workflows, execution policies with review/approval stages, decision tracking, budget hard-stops, agent pause/resume/terminate, and full audit logging. Nothing ships without your sign-off.
**Governance & Approvals** — Board approval workflows, execution policies with review/approval stages, decision tracking, budget hard-stops, and agent pause/resume/terminate. Configured task reviews govern completion; connection action approvals govern calls through the tool gateway.
</td>
</tr>
<tr>
<td>
**Budget & Cost Control** — Token and cost tracking by company, agent, project, goal, issue, provider, and model. Scoped budget policies with warning thresholds and hard stops. Overspend pauses agents and cancels queued work automatically.
**Budget & Cost Control** — Token and cost tracking by company, agent, project, goal, issue, provider, and model. Scoped budget policies with warning thresholds and hard stops. Enforcement uses recorded spend; usage reporting and in-flight work can delay a stop.
</td>
<td>
@@ -258,12 +301,12 @@ Paperclip is a full control plane, not a wrapper. Before you build any of this y
<tr>
<td>
**Plugins** — Instance-wide plugin system with out-of-process workers, capability-gated host services, job scheduling, tool exposure, and UI contributions. Extend Paperclip without forking it.
**[Plugins](https://docs.paperclip.ing/administration/plugins/)** — Instance-wide plugin system with out-of-process workers, capability-gated host services, job scheduling, tool exposure, and UI contributions. Extend Paperclip without forking it.
</td>
<td>
**Secrets & Storage** — Instance and company secrets, encrypted local storage, provider-backed object storage, attachments, and work products. Sensitive values stay out of prompts unless a scoped run explicitly needs them.
**Secrets & Storage** — Company secrets and per-person secret values, encrypted credential storage, local or S3-compatible file storage, attachments, and work products. Secret references supply credentials to authorized runs without copying values into ordinary agent configuration.
</td>
</tr>
@@ -275,7 +318,7 @@ Paperclip is a full control plane, not a wrapper. Before you build any of this y
</td>
<td>
**Company Portability** — Export and import entire organizations — agents, skills, projects, routines, and issues — with secret scrubbing and collision handling. One deployment, many companies, complete data isolation.
**[Company Portability](https://docs.paperclip.ing/guides/power/export-import/)** — Preview, export, and import organization packages with agents, skills, and optional projects, routines, tasks, and attachments. Referenced secret values are omitted; review packages before sharing because plain environment values and local paths can remain. Packages share an operating setup; full-instance recovery uses backups.
</td>
</tr>
@@ -287,54 +330,34 @@ Paperclip is a full control plane, not a wrapper. Before you build any of this y
| | |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| **Not a chatbot.** | Agents have jobs, not chat windows. |
| **Not just a chatbot.** | Conversations stay attached to tasks, plans, decisions, and outputs. Experimental Agent Chat can hand work off to assigned tasks. |
| **Not an agent framework.** | We don't tell you how to build agents. We tell you how to run a company made of them. |
| **Not a workflow builder.** | No drag-and-drop pipelines. Paperclip models companies — with org charts, goals, budgets, and governance. |
| **Not just a workflow builder.** | Routines and experimental pipelines operate within an organization, with roles, goals, budgets, and governance. |
| **Not a prompt manager.** | Agents bring their own prompts, models, and runtimes. Paperclip manages the organization they work in. |
| **Not a single-agent tool.** | This is for teams. If you have one agent, you probably don't need Paperclip. If you have twenty — you definitely do. |
| **Not a code review tool.** | Paperclip orchestrates work, not pull requests. Bring your own review process. |
| **Not limited to one agent.** | Start with one agent and grow into a team with shared skills, delegation, and review. |
| **Not only for code review.** | Coding and PR review fit alongside research, operations, content, and other work. |
<br/>
## Quickstart
Open source. Self-hosted. No Paperclip account required.
Open source. Self-hosted. No Paperclip account required. Follow the [guided quickstart](https://docs.paperclip.ing/guides/getting-started/five-minute-path/) to set up your first agent.
### Just ask your agent to install Paperclip
Share the [installation guide](https://docs.paperclip.ing/reference/cli/installation/) with your agent.
### Or install it yourself
With **Node.js 24.11 or newer** installed:
```bash
curl -fsSLO https://paperclip.ing/install.sh
curl -fsSLO https://paperclip.ing/install.sh.sha256
if command -v sha256sum >/dev/null 2>&1; then
sha256sum -c install.sh.sha256
else
shasum -a 256 -c install.sh.sha256
fi
bash install.sh
npx paperclipai@latest onboard --yes
```
The installer ensures Node.js 24.11 or newer is available, installs a managed
Paperclip CLI under `~/.paperclip/cli`, and starts interactive onboarding. It
can also install Paperclip as a background service on supported Linux and
macOS systems. The checksum detects transfer or publishing mistakes, but it is
served from the same origin as the script; use a release-tag or commit-pinned
GitHub copy when you need an independently hosted source.
The CLI runs from npm's cache; your instance configuration and data persist locally.
For a non-interactive managed install:
```bash
curl -fsSL https://paperclip.ing/install.sh | bash -s -- --no-prompt --no-onboard
paperclipai onboard --yes
```
The piped form requires supported Node.js, npm, and npx to already be present.
If Node.js bootstrap is required, download and review `install.sh` before
running it so no privileged dependency-install command is accepted through a
pipe.
To try Paperclip without installing anything permanently:
```bash
npx --registry https://registry.npmjs.org paperclipai onboard --yes
```
See the [installation guide](https://docs.paperclip.ing/reference/cli/installation/) for managed installs, pinned versions, canary and git-ref installs, updates, rollback, service management, and uninstalling.
For an isolated manual test instance that is already initialized with a CEO
agent, use `test-drive`. It stays in the foreground, never installs a service
@@ -352,7 +375,7 @@ Each run without `--data-dir` gets a unique, retained temporary directory; its
absolute path is printed at startup. Pass `--data-dir` to reuse one, or
`--no-browser` to leave the initialized instance unopened. When invoked from a
linked Git worktree, `test-drive` also enables task execution in that worktree.
See [`doc/CLI.md`](doc/CLI.md#isolated-manual-test-drives) for credential and
See the [test-drive guide](https://docs.paperclip.ing/reference/cli/test-drive/) for credential and
reuse behavior.
> **Troubleshooting: private npm registry `.npmrc`**
@@ -368,21 +391,18 @@ reuse behavior.
> Workaround (cross-platform; force the public npm registry for this command):
>
> ```bash
> npx --registry https://registry.npmjs.org paperclipai onboard --yes
> npx --registry https://registry.npmjs.org paperclipai@latest onboard --yes
> ```
That quickstart path now defaults to trusted local loopback mode for the fastest first run. To start in authenticated/private mode instead, choose a bind preset explicitly:
```bash
paperclipai onboard --yes --bind lan
npx paperclipai@latest onboard --yes --bind lan
# or:
paperclipai onboard --yes --bind tailnet
npx paperclipai@latest onboard --yes --bind tailnet
```
If you already have Paperclip configured, rerunning `onboard` keeps the existing config in place. Use `paperclipai configure` to edit settings.
See [`doc/INSTALLING.md`](doc/INSTALLING.md) for pinned versions, canary and
git-ref installs, updates, rollback, service management, and uninstalling.
If you already have Paperclip configured, rerunning `onboard` keeps the existing config in place. Use `npx paperclipai configure` to edit settings.
Or manually:
@@ -393,32 +413,53 @@ pnpm install
pnpm dev
```
This starts the API server at `http://localhost:3100`. An embedded PostgreSQL database is created automatically — no setup required.
This starts the UI and API at `http://localhost:3100`. An embedded PostgreSQL database is created automatically — no setup required.
> **Requirements:** Node.js 24.11+, pnpm 9.15+
Source development also builds the native Paperclip Runner when enabled (the self-hosted default). Install a Rust toolchain, or set `PAPERCLIP_RUNNER_BINARY` to a compatible prebuilt runner.
<br/>
## FAQ
**What does a typical setup look like?**
Locally, a single Node.js process manages an embedded Postgres and local file storage. For production, point it at your own Postgres and deploy however you like. Configure projects, agents, and goals — the agents take care of the rest.
**Q: Is this project maintained or just slop?**
If you're a solo entrepreneur you can use Tailscale to access Paperclip on the go. Then later you can deploy to e.g. Vercel when you need it.
**A:** Paperclip is maintained by the [Paperclip team](https://paperclip.ing). We've merged [over 2,700 pull requests](https://github.com/paperclipai/paperclip/pulls?q=is%3Apr+is%3Amerged).
**Can I run multiple companies?**
Yes. A single deployment can run an unlimited number of companies with complete data isolation.
<br/>
**How is Paperclip different from agents like OpenClaw or Claude Code?**
Paperclip _uses_ those agents. It orchestrates them into a company — with org charts, budgets, goals, governance, and accountability.
**Q: What does a typical setup look like?**
**Why should I use Paperclip instead of just pointing my OpenClaw to Asana or Trello?**
Agent orchestration has subtleties in how you coordinate who has work checked out, how to maintain sessions, monitoring costs, establishing governance - Paperclip does this for you.
**A:** Locally, a single Node.js process manages an embedded Postgres and local file storage. For production, point it at your own Postgres and deploy however you like. Configure projects, agents, and goals — the agents take care of the rest.
For remote access, use authenticated mode with a private-network bind such as Tailscale, or deploy the persistent server with Docker. See [deployment modes](https://docs.paperclip.ing/reference/deploy/deployment-modes/) and the [Docker guide](https://docs.paperclip.ing/reference/deploy/docker/).
<br/>
**Q: Can I run multiple companies?**
**A:** Yes. A single deployment can host multiple organizations with company-scoped data and access checks.
<br/>
**Q: How is Paperclip different from agents like OpenClaw or Claude Code?**
**A:** Paperclip _uses_ those agents. It orchestrates them into a company — with org charts, budgets, goals, governance, and accountability.
<br/>
**Q: Why should I use Paperclip instead of just pointing my OpenClaw to Asana or Trello?**
**A:** Agent orchestration has subtleties in how you coordinate who has work checked out, how to maintain sessions, monitoring costs, establishing governance - Paperclip does this for you.
(Bring-your-own-ticket-system is on the Roadmap)
**Do agents run continuously?**
By default, agents run on scheduled heartbeats and event-based triggers (task assignment, @-mentions). You can also hook in continuous agents like OpenClaw. You bring your agent and Paperclip coordinates.
<br/>
**Q: Do agents run continuously?**
**A:** Agents wake for assigned work and follow-up messages. Optional timer heartbeats let them check for work periodically; routines create recurring tasks on their own schedules. You can also connect externally running agents such as OpenClaw. A mention alone does not assign work or wake another agent.
<br/>
@@ -465,16 +506,22 @@ See [doc/DEVELOPING.md](doc/DEVELOPING.md) for the full development guide.
- ✅ Activity log & action attribution
- ✅ Self-healing runs & automatic recovery
- ✅ Agent evals & feedback
- ⚪ Memory / Knowledge
- ✅ Connected Apps
- ✅ Personal & Shared AI Accounts
- ✅ Shared Agents Use Personal GitHub Identities
- ✅ Skill Version History & Restore
- ✅ Document Comments & Revision History
- ✅ Company-Wide Search
- ✅ Multi-Model & Multi-Harness Teams
- 🟡 Memory / Knowledge
- ⚪ MAXIMIZER MODE
- ⚪ Work Queues
- ⚪ Self-Organization
- ⚪ Automatic Organizational Learning
- ⚪ CEO Chat
- 🟡 Cloud deployments (multi-tenant isolation & company Import/Export shipped)
- 🟡 Agent Chat
- 🟡 Cloud deployments
- ⚪ Desktop App
- ⚪ Bring-your-own-ticket-system (Asana / Linear / Jira as on-ramps)
- ⚪ Connected Apps (one-click integrations, e.g. Vercel)
This is the short roadmap preview. See the full roadmap in [ROADMAP.md](ROADMAP.md).
@@ -510,6 +557,8 @@ Telemetry is **enabled by default** and can be disabled with any of the followin
We welcome contributions. See the [contributing guide](CONTRIBUTING.md) for details.
**[We're hiring](https://paperclip.ing/about/#careers)**
<br/>
## Community
+41 -15
View File
@@ -4,6 +4,8 @@ This document expands the roadmap preview in `README.md`.
Paperclip is still moving quickly. The list below is directional, not promised, and priorities may shift as we learn from users and from operating real AI companies with the product.
Status tracks the default branch: ✅ available, 🟡 partial or experimental, ⚪ planned. Check [release notes](https://github.com/paperclipai/paperclip/releases) for packaged versions. Some capabilities require instance settings, plugins, or provider setup.
We value community involvement and want to make sure contributor energy goes toward areas where it can land.
We may accept contributions in the areas below, but if you want to work on roadmap-level core features, please coordinate with us first in Discord (`#dev`) before writing code. Bugs, docs, polish, and tightly scoped improvements are still the easiest contributions to merge.
@@ -48,9 +50,9 @@ Paperclip should support explicit review and approval stages as first-class work
Paperclip needs a clearer path from solo operator to real human teams. That means shared board access, safer collaboration, and a better model for several humans supervising the same autonomous company.
### ✅ Cloud / Sandbox agents (e2b, Cloudflare, Daytona, Modal, Novita, self-hosted Kubernetes)
### ✅ Cloud / Sandbox agent support
We want agents to run in more remote and sandboxed environments while preserving the same Paperclip control-plane model. This makes the system safer, more flexible, and more useful outside a single trusted local machine.
Sandbox provider plugins support remote execution while preserving the Paperclip control-plane model. Providers include E2B, Cloudflare, Daytona, Modal, Novita, and Kubernetes. Execution requires a configured provider and a compatible adapter; environment and isolated-workspace surfaces depend on instance settings.
### ✅ Artifacts & Work Products
@@ -76,17 +78,45 @@ Secrets need to be centrally managed without giving every agent every credential
Operators need a durable record of what changed and who initiated it. Activity history and clear action attribution make human, agent, and system actions inspectable across the control plane.
### ✅ Self-healing runs & automatic recovery
### ✅ Bounded run recovery
Agent work should recover from routine failures without waiting for a human to notice every stalled run. Recovery policies can retry safe work, route failures, and keep the issue lifecycle aligned with what actually happened.
Recovery policies handle supported transient failures and interrupted runs, retain task ownership, and surface recovery actions when work needs human intervention. Retries are bounded and remain subject to budgets, approvals, and pause gates.
### ✅ Agent evals & feedback
Agent performance should be measurable over time, not judged only from anecdotes. Evals, saved results, and structured feedback create a loop for improving skills, prompts, models, and employee quality.
Skill Studio provides saved test inputs, test runs, results, and version history. Task and document feedback helps people improve procedures. Automatic organizational learning remains a separate roadmap item below.
### ⚪ Memory / Knowledge
### ✅ Connected Apps
We want a stronger memory and knowledge surface for companies, agents, and projects. That includes durable memory, better recall of prior decisions and context, and a clearer path for knowledge-style capabilities without turning Paperclip into a generic chat app.
Apps and Connections provide a service catalog, custom MCP connections, personal and shared accounts, agent access controls, and per-action Allowed / Ask first / Off policies. Supported AI accounts also use Connections. Setup varies by provider and deployment; not every integration is one-click. Broader provider coverage and simpler setup remain ongoing work.
### ✅ Personal & Shared AI Accounts
Connect supported subscription accounts or API keys and choose personal defaults or shared accounts for compatible agents. Human access and agent eligibility are separate controls; account selection stays independent of the model and harness. See [AI Connections](doc/connections/AI-CONNECTIONS.md).
### ✅ Shared Agents Use Personal GitHub Identities
Shared agents can use the GitHub identity of the person whose instructions they are executing. Managed Git and GitHub operations resolve that identity through delegation and follow-up work, subject to connection permissions. See [GitHub identity during agent execution](doc/execution-github-identity.md).
### ✅ Skill Version History & Restore
Save skill versions, inspect earlier contents, and restore a previous version as a new revision. Saved test inputs and results support comparison as procedures evolve.
### ✅ Document Comments & Revision History
Leave comments on specific passages in task documents, follow revision history, and restore previous document revisions. Annotations carry into agent review context so feedback stays attached to the work.
### ✅ Company-Wide Search
Search tasks, comments, documents, agents, projects, and artifacts within company access boundaries. Filters and matching excerpts help people find relevant work and its outputs.
### ✅ Multi-Model & Multi-Harness Teams
Choose models and supported harnesses per agent while keeping tasks, skills, and history in one organization. Built-in adapters cover Claude Code, Codex, Cursor, Gemini CLI, OpenCode, Pi, Hermes, Grok, Kimi Code, OpenClaw, and process or HTTP integrations.
### 🟡 Memory / Knowledge
Experimental memory connections support Mem0, Zep, Supermemory, Cognee, and Honcho. The optional LLM Wiki plugin provides another knowledge workflow. A broader memory and knowledge surface for companies, agents, and projects remains a direction for future work. See [memory connections](doc/connections/MEMORY.md) for setup and availability.
### ⚪ MAXIMIZER MODE
@@ -94,7 +124,7 @@ This is the direction for higher-autonomy execution: more aggressive delegation,
### ⚪ Work Queues
Paperclip should support queue-style work streams for repeatable inputs like support, triage, review, and backlog intake. That would make it easier to route work continuously without turning every system into a one-off workflow.
Paperclip should support queue-style work streams for repeatable inputs like support, triage, review, and backlog intake. That would make it easier to route work continuously without turning every system into a one-off workflow. Existing decision queues group items awaiting input; this milestone concerns continuous work intake and routing.
### ⚪ Self-Organization
@@ -104,11 +134,11 @@ As companies grow, agents should be able to propose useful structural changes su
Paperclip should get better at turning completed work into reusable organizational knowledge. That includes capturing playbooks, recurring fixes, and decision patterns so future work starts from what the company has already learned.
### ⚪ CEO Chat
### 🟡 Agent Chat (including CEO Chat)
We want a lighter-weight way to talk to leadership agents, but those conversations should still resolve to real work objects like plans, issues, approvals, or decisions. This should improve interaction without changing the core task-and-comments model.
Experimental Agent Chat provides persistent conversations with any agent, including leadership. Conversations keep their history and plans, then hand execution off to linked tasks. Agent Chat is off by default. Experimental chat and email connectors provide additional entry points through configured external services; they have separate setup and access controls.
### 🟡 Cloud deployments (multi-tenant isolation & company Import/Export shipped)
### 🟡 Cloud deployments
Local-first remains important, but Paperclip also needs a cleaner shared deployment story. Teams should be able to run the same product in hosted or semi-hosted environments without changing the mental model.
@@ -121,7 +151,3 @@ A desktop app can make Paperclip feel more accessible and persistent for day-to-
### ⚪ Bring-your-own-ticket-system (Asana / Linear / Jira as on-ramps)
Existing ticket systems should be able to feed work into Paperclip without becoming the agent control plane themselves. Asana, Linear, and Jira can act as familiar on-ramps while Paperclip owns execution, governance, and outcomes.
### ⚪ Connected Apps (one-click integrations, e.g. Vercel)
Common services should connect without bespoke setup for every company. One-click integrations can package credentials, permissions, and useful workflows for apps such as Vercel while keeping access governed and auditable.
+1 -1
View File
@@ -1314,7 +1314,7 @@ export function registerIssueCommands(program: Command): void {
addCommonClientOptions(
issue
.command("release")
.description("Release issue back to todo and clear assignee")
.description("Release issue execution locks; clear the assignee only for unfinished issues")
.argument("<issueId>", "Issue ID")
.action(async (issueId: string, opts: BaseClientOptions) => {
try {
+24
View File
@@ -201,3 +201,27 @@ PAPERCLIP_FILE_DELIVERY_DAYTONA=1 pnpm exec vitest run server/src/__tests__/file
The live fixture deletes each sandbox before checking that its attachments
remain downloadable from Paperclip. It does not run unless explicitly enabled.
## Text attachment previews
In the task chat layout, select a text attachment in Artifacts, a work-product
card, or a chat attachment chip to open a named right-side tab. Reopening the
same attachment focuses its existing tab. Tabs can be switched and closed.
On mobile, the same viewer opens in the task details drawer.
Markdown work products keep their expandable review document, annotations,
revision indicator, and document links. **Open in tab** is a separate action.
Work-product cards keep **Download** as a direct original-file download.
Text file cards provide **Open in tab** beside their existing actions. CSV cards
keep this action before and after loading their data preview.
Markdown attachments offer **Rendered** and **Raw** views. Other supported text
files display literal text. Image references and diagram source remain inert;
opening a preview does not load attachment-selected media URLs. The viewer
provides a download action. Preview reads
are limited to 512 KiB; oversized, unsupported, or unavailable files show an
explicit fallback instead of attempting an unbounded render. A failed read can
be retried. Workspace files continue to use the existing workspace file viewer.
Storybook: **Tasks / Text file tabs** covers opening from Artifacts, Markdown,
plain text, empty files, oversized files, missing attachments, and a narrow panel.
+11
View File
@@ -442,6 +442,17 @@ revokes the previous boot identity with a conditional update. Its own claim also
expires so another sweep can finish cleanup after a restart. Historical rows keep
null ownership fields and follow the previous recovery path.
## Agent file persistence and legacy revisions
Managed agent files are current filesystem contents, using the same persistent
instance storage as other workspaces. `agent_instruction_revisions` and
`agent_instruction_heads` are retained as read-only upgrade input. Their heads
are adopted once into the managed directory; new saves never append revisions.
`agent_instruction_working_copies` holds per-run baseline hashes, state, and
capture receipts. New receipts identify `paperclip.agent-files.v1`; historical
rows retain the instruction-only format. Completed directory runs discard their
baseline and private copies. See [Persistent agent files](agent-files.md).
## Large API response snapshots
`assets.byte_size` uses PostgreSQL `bigint` so saved responses and byte ranges can
+35
View File
@@ -100,6 +100,17 @@ pnpm build-storybook
These run the `@paperclipai/ui` Storybook on port `6006` and build the static output to `ui/storybook-static/`.
Use **Design explorations → Agent chat sidebar** to review the production secondary
agent navigation. **Components** covers selection, search, loading, empty results,
and larger teams. **Pages** puts it beside the production agent chat inside the
real app shell, including landing, conversation, plan panel, light, and mobile
views. The plus button opens the existing agent picker; adding an agent creates
one sidebar entry, and choosing them again reopens the same conversation. The
**Add and reopen** component story exercises that constraint. Agent selection
and sending use local fixtures. The live `/chats` and `/chats/:agentRef` routes
use the same sidebar, picker, landing page, and existing chat surface when the
Agent Chat experimental setting is enabled.
Use **Chat & Comments → Issue Thread Interactions → Composer Questions Auto Advance**
to try the paged composer form. A single selection shows a brief checked-state animation before advancing to the
next question. Reduced-motion mode advances without animation.
@@ -115,6 +126,19 @@ pending card.
Use **Composer → Model and effort picker** to review harness-specific model
choices. Codex uses the curated adapter catalog unless the instance declares
`PAPERCLIP_ADAPTER_MODELS`; general OpenAI API models are not Codex choices.
The Paperclip Runner Codex profile shows the same known model effort levels.
Its selected effort is saved with the run and sent to Codex for each turn.
Claude Code uses model-specific effort levels; Haiku has no effort slider.
Grok uses its adapter's reasoning levels, including for its default model.
Kimi shows effort only when its agent uses the CLI engine, including with its
default model, because the default ACP engine ignores effort.
Mobile entity picker sheets use a modal popover so touch scrolling stays inside
the sheet even when it is portalled outside the new-task dialog. Run
`pnpm exec playwright test --config tests/e2e/playwright.config.ts tests/e2e/new-task-picker-touch.spec.ts`
to verify native touch scrolling and selection in both assignee and model lists.
The test uses a fixed model catalog and a reduced viewport to cover limited
space while the phone keyboard is open; it does not require a provider login.
The Storybook visual regression suite uses external PNG baselines instead of
committed screenshots:
@@ -238,6 +262,17 @@ owns committed updates.
## Hot-Restart Deploys
During a restart, the board's health, session, and access checks retry temporary
network/gateway failures and non-JSON API responses every five seconds. A new
page shows **Reconnecting to Paperclip** with a **Try again** action and waits
for startup health to become ready. Valid startup metadata remains available to
sign-in and invitation pages. An already
open page stays mounted during temporary background failures so unsaved edits
survive. Successful checks resume the same route and refresh other failed reads;
this recovery does not reload the browser or replay mutations. Authorization
failures still require sign-in or an explicit retry. Storybook **App / Connection
recovery** shows the startup recovery states.
Primary-instance rebuilds that restart `paperclip.service` can request one-shot live-run adoption instead of using the normal graceful shutdown drain. Before restarting the service, write the marker from the newly staged app with the current service PID:
```sh
+7
View File
@@ -66,6 +66,13 @@ The governing rule is: **skill permissions are opt-in restrictions, not opt-in c
Core Paperclip owns the skill runtime, company-boundary enforcement, policy evaluation contract, API denials, validation, path containment, secret redaction, and activity logging. Those safety invariants cannot be disabled by policy. Open-by-default skill work never authorizes arbitrary host-path reads, unsafe executable content, or policy edits: local imports and scans must stay within Paperclip-known workspace or managed-skill roots, remote sources must resolve to validated immutable content, and platform safety denials must stay distinct from optional administrative restrictions. Paperclip EE may provide detailed administration for per-agent, per-role, per-action, per-source, and protected-skill rules, but EE is not required to use skills and is not an enforcement boundary. Without EE, companies remain open by default and any already-configured restrictions continue to be enforced by core.
GitHub repositories are first-class sources within Skills. People import selected
skills through their existing GitHub connection and manually refresh installed
snapshots. The source records repository, branch, path, and installed commit;
the connection supplies caller-authorized access. Originals remain viewable and
testable, with independent editable copies. New upstream skills require selection,
and deselection, source disconnection, or upstream removal retains installed content.
An explicit restricted policy may deny selected operations or switch to a default-deny preset with explicit allow rules. Core exposes a stable versioned policy API so EE and other administrative clients configure and simulate the same evaluator used by skill mutation routes. Core Skill Studio only needs to perform normal skill work, explain an explicit denial, and point administrators to EE when its richer policy UI is available; it must not recreate a partial enterprise permission editor.
## Principles
+122 -17
View File
@@ -542,6 +542,12 @@ V1 non-terminal liveness rule:
Detailed ownership, execution, blocker, active-run watchdog, crash-recovery, and non-terminal liveness semantics are documented in `doc/execution-semantics.md`.
For native ordinary tasks, answering a Board comment does not authorize an
indefinite response wait when the structured result reports blocking remaining
work. Without a recorded wait condition, reject the finish report or use the
bounded incomplete-work recovery path. Preserve real governance and pause gates,
conversation lifecycles, and protection against replaying superseded requests.
## 8.3 Approval Status
- `pending -> approved | rejected | cancelled`
@@ -622,6 +628,11 @@ rollout mode, and fails closed with the cap in the error once enforcement is
active. Writes to the run's own source issue are not counted. Assignee self-comments do not
wake the assignee, and a non-assignee comment cannot mint a mention grant.
Agent @-mentions are context links only: they do not wake the mentioned agent,
assign work, or forward comments to another task. Normal comment feedback still
routes to the current assignee. Work for another agent requires explicit
assignment, delegation, or a review request.
Agent-authored issue comments persist the responsible user derived from the
authenticated actor; clients cannot choose that attribution. Each comment also
records the write-policy reason, and spoof attempts fail with an audited 422.
@@ -728,8 +739,12 @@ outcomes and resolver attribution are immutable.
An explicit named agent or user addressee and a company-configured cap may narrow
the effective audience. Only the exact named addressee may resolve an addressed
interaction; a human does not override a user addressee. A cap never widens the
requested audience. Tool-action confirmations and
interaction; a human does not override a user addressee. Explicit user recipients
must exist and be authorized to respond in the issue's company before creation.
Ordinary Agent Chat questions derive their user recipient from the persisted
conversation owner. A conflicting explicit user recipient is rejected; IDs are
never guessed or repaired. Ordinary task questions keep optional addressing.
A cap never widens the requested audience. Tool-action confirmations and
other hard-governed action cards remain `human_only` (or move to the formal approval
system) regardless of a requested open audience.
@@ -978,6 +993,14 @@ Core authorization follows these rules:
- New qualifying issue activity may invalidate an archive so the item resurfaces; archival is not a substitute for resolving or closing work.
- Viewing an issue may update its per-user read receipt, but read receipts alone do not enroll the issue in Mine. Mine participation begins with a user-authored comment, issue creation/assignment, or another audited user mutation; explicit product actions such as manually running a routine may record an audited inbox touch.
Failed-run rows in Mine and the inbox badge use the run's `responsibleUserId`.
Another user's run does not appear there, even for a shared agent. Select the
latest run per agent before checking ownership so older failures do not resurface.
Unattributed runs appear only for `local-board` in Mine; an unresolved viewer
identity shows no failed runs. All retains company-wide failed-run visibility.
Company health alerts do not contribute to the personal inbox badge.
Run list responses, including summaries, retain `responsibleUserId`.
Ownership split:
- **Core / Free:** permission key and scoped-grant enforcement; responsible-user resolution; default-open, disabled, and allowlist policy modes; archive/unarchive APIs; per-user archive persistence; resurfacing behavior; activity audit records; and stable denial codes.
@@ -1097,6 +1120,12 @@ Server behavior:
2. if updated row count is 0, return `409` with current owner/status
3. successful checkout sets `assignee_agent_id`, `status = in_progress`, and `started_at`
`POST /issues/:issueId/release` clears checkout and execution locks. For terminal
issues (`done` or `cancelled`), it preserves the assignee, status, and disposition
timestamps, including on repeated release. For unfinished issues it clears the
agent assignee; only `in_progress` changes to `todo`. Existing company access,
assignee/run ownership checks, and the `issue.released` activity event still apply.
`POST /issues/:issueId/admin/force-release` is an operator recovery endpoint for stale harness locks. It requires board access to the issue company, clears checkout and execution run lock fields, and may clear the agent assignee when `clearAssignee=true` is passed. The route must write an `issue.admin_force_release` activity log entry containing the previous checkout and execution run IDs.
## 10.5 Projects
@@ -1610,7 +1639,7 @@ for persistence, migration, rendering, and integration contracts.
`enableAgentChat` is an instance experimental flag, default false. Conversation containers remain issues, unique by `(company_id, conversation_agent_id, conversation_user_id)`. The authenticated board actor supplies ownership; local trusted mode uses `local-board`. Ordinary company task access applies. A conversation's agent assignment and identity are immutable through ordinary updates; terminal status mutations are rejected.
`GET /api/companies/:companyId/chats/:agentRef` reads an existing conversation or null. `POST` atomically resolves its issue on first send/upload. Existing issue comment, attachment, document, interaction, and run APIs apply thereafter. User chat comments require an idempotent UUID `clientRequestId`. Conversation delivery preserves comment order through the existing issue execution queue; the durable comment outbox repairs the commit-to-enqueue crash window.
`GET /api/companies/:companyId/chats/:agentRef` reads an existing conversation or null. `POST` atomically resolves its issue when adding a chat or on first send/upload. `GET /api/companies/:companyId/chats` lists only the current board user’s conversations in that company, subject to ordinary issue read access. The Chat navigation opens a searchable secondary sidebar with agent avatars and a picker for starting or reopening the same per-agent conversation. Existing issue comment, attachment, document, interaction, and run APIs apply thereafter. User chat comments require an idempotent UUID `clientRequestId`. Conversation delivery preserves comment order through the existing issue execution queue; the durable comment outbox repairs the commit-to-enqueue crash window.
The server owns conversation state: `waiting` plus `in_review` denotes a healthy idle conversation, and `active` denotes an unanswered or executing turn. Successful replies settle a turn; they do not finish the issue. Idle containers are excluded from execution-work counts, ordinary task lists, timer work, and recovery invocations. Failed/unanswered turns retain normal handling. Child completion never wakes or completes the conversation. Search and direct task access preserve history.
@@ -1647,6 +1676,12 @@ harness/model routing and fails closed without ambient credential fallback.
Legacy agents retain their authentication until validated adoption. See
[AI Connections](connections/AI-CONNECTIONS.md) for company isolation, compatible
methods, lifecycle, runtime enforcement, and migration details.
Provider login failures create a provider-specific Connections card on the task
when the run fails, before generic recovery retries. Reconnect preserves account
identity and permissions. Compatible legacy agents may explicitly adopt a
validated connection inline; late failures must not invalidate newer credentials.
### Experimental task-bound email
AgentMail channel connections extend the experimental conversation/task pipeline
@@ -1709,21 +1744,27 @@ dismissal retries after withdrawal while rejecting caller-invented IDs. It
stores no announcement content, account data or interaction events.
See [Announcements](ANNOUNCEMENTS.md) for API and publishing details.
### Personal keyboard shortcut preference
### Keyboard shortcuts
Keyboard shortcuts are off by default and are enabled in Settings → Profile.
The preference is stored on the signed-in user, applies across companies and
devices, and does not require instance administrator access. The local trusted
board user has the same preference. `GET /api/auth/preferences` returns only the
current board user's preference. `PATCH /api/auth/preferences` updates only that
user and requires an accessible `companyId` for the activity log, including viewer
memberships. The preference and audit record commit in one transaction. Both
requests require `expectedUserId` (GET query parameter or PATCH body) matching
the authenticated actor, so a cookie change cannot mix accounts in the cache.
Agents cannot
read or change these preferences. The legacy instance general setting is retained
for API compatibility but no longer controls shortcut behavior in the app;
users opt in individually after the upgrade.
Keyboard shortcuts are always enabled for every signed-in user. There is no
instance setting and no personal preference that turns them off.
### Persistent managed agent files (2026-09-28)
The Instructions Editor and agent execution share one current agent-owned
directory, scoped by company and agent. The configured instruction entry is one
file in this directory. Registered private copies synchronize supported files
across tasks and sessions, separately from task workspace persistence. Saves
require verified provider stop and current authorization, then synchronize changed
files using per-file last-sync-wins;
new content is not stored as revision history. Existing deployed revisions and
saved execution formats remain compatible during adoption. See
[Persistent agent files](agent-files.md).
Persistent-file storage limits are advisory for execution: a full folder cannot
pause the agent, fail its run, or prevent later runs. Show a warning on each run
while storage remains full, restore existing files so the agent can remove them,
and enforce the limits on saves. Cleanup clears the warning for future runs.
### Unsafe native workspace exports
@@ -1736,3 +1777,67 @@ omit the already-rejected export, clear stale repair notices, and finalize the
accepted result without another provider turn, even when its old sandbox is
unavailable. Preserve current ownership and newer-work fences. See
`native-workspace-finalization-recovery.md`.
## GitHub-synced skill sources
- Discovery supports opt-in `Accept: application/x-ndjson` on the existing discovery
endpoint. Progress events report connection/download/tree/package stages, checked skill
counts, and the current package's file counts; candidate events contain metadata,
never file contents. Only the final `complete` event makes the scan selectable.
Interrupted streams discard partial results, and cancellation stops further
provider reads. The default JSON API remains compatible. The import dialog uses
reduced-motion-aware animations and shows indeterminate progress while saving
complete packages, without inventing completion percentages.
- Sources and entries are company-scoped. Reuse GitHub connection grants and credential
refresh; every provider read authorizes the current caller. Saved connection IDs do
not confer access to another user's token. Agent reads use managed run identities.
- The import picker searches the deduplicated union of repositories from all connections
the caller can access, automatically choosing an authorized connection. Adding accounts
uses the standard GitHub setup in Apps and retains the import draft. Repository URLs
use the default branch; `/tree/<branch>` URLs select a branch, including slash-containing
names, without a separate branch or credential selector. Pasted URLs also prefer
the caller's eligible GitHub connections, including for public repositories;
anonymous access is used when no authorized connection succeeds. An explicitly
saved connection must remain authorized. Repository identity still comes from
the GitHub API; quota errors are distinct from access denials.
- Discover every `SKILL.md`, including hidden/deep directories. Resolve the tracking
ref to an immutable commit once per operation through a shallow Git fetch. Read
the complete local Git tree and blobs without checkout, archive transformations,
or per-file API calls. Git's receiving/preparing percentages feed download progress.
Git must be installed on the server. Downloads are bounded to 128 MiB and three
minutes, with two simultaneous downloads and at most four cached/downloading
snapshots. A ten-minute in-memory index of temporary repositories, scoped by
company, caller, run, and grant, reuses a pinned snapshot for preview/import;
authorization is checked before every reuse. No credentials are written to disk
or inherited from host Git configuration. Cancellation terminates the Git process
group and removes incomplete downloads. Symlinks and submodules are reported,
never traversed; nested skill roots are independent package boundaries.
- Select packages as units, with included-file trees and bounded read-only previews.
Nested packages have independent checkboxes; supporting files have no individual
selection. Persist file manifests, explicit missing/out-of-package reference findings,
and declared `compatibility` requirements with source entries. Preview reads reauthorize
the current caller and audit the requested package at the scanned immutable commit;
discovery and source metadata contain no file contents. Reference checks are advisory,
do not establish complete runtime dependencies, and never expand package boundaries.
- Stage and audit complete packages before publishing. Scripts are allowed through
the existing content audit and never run on import. Persist binary bytes and executable
flags in immutable version inventories (legacy entries default to UTF-8/non-executable).
- Refresh is serialized per source, checks saved selection revisions, and publishes
valid skills plus per-entry outcomes with required activity records in one transaction.
Failed packages retain prior versions; access/scan/download failures retain all content.
Record attempted and successful refresh times separately.
- Match by repository identity and exact path. Preserve IDs, keys, assignments, folders,
and history. New key collisions cannot overwrite unrelated skills. Only changed package
bytes or executable modes create versions. Library reads, tests, and subsequent unpinned
runs use local installed snapshots; pinned/active runs retain their versions.
- Additions require reviewed selection, with new candidates checked by default and
explicit folder exclusions retained. Upstream deletion keeps installed content with
removed status; moves are removal plus discovery. Deselect/disconnect retains content
and assignments. Originals are read-only, with the existing independent fork workflow.
- Adopt recognizable existing GitHub imports without provider calls or content changes;
resolve missing repository IDs and full snapshots on successful refresh. Exclude bundled,
catalog, local/project, skills.sh, and unsupported hosts. Existing GitHub import and update
endpoints delegate to sources while retaining response shapes.
- GitHub.com, manual refresh only. No upstream editing, polling, webhook sync, commits,
or pull-request creation in this milestone.
+54 -15
View File
@@ -34,6 +34,11 @@ human approval, decline, or scoped remembered permission. Connections and task
views resolve the same review, and the agent continues with the server-recorded
outcome. See [the implementation contract](SPEC-implementation.md#124-connection-tool-reviews).
Model authentication failures also surface a provider-specific Connections card
on the task immediately after failure. Users can reconnect inline and resume;
legacy agents keep their authentication until an explicit, validated adoption.
See [AI Connections](connections/AI-CONNECTIONS.md).
#### Board Powers (Always Available)
The Board has **unrestricted access** to the entire system at all times:
@@ -47,6 +52,11 @@ The Board has **unrestricted access** to the entire system at all times:
The Board is not just an approval gate — it's a live control surface. The human can intervene at any level at any time.
A Board status inquiry does not itself pause unfinished task execution. Native
ordinary tasks that report blocking remaining work must continue, register a
real wait, or surface a bounded recovery failure. Recorded approvals, questions,
dependencies, and pauses remain authoritative; obsolete requests must not replay.
#### Budget Delegation
The Board sets Company-level budgets. The CEO can set budgets for Agents below them, and every manager Agent can do the same for their reports. How this cascading budget delegation works in practice is TBD, but the permission structure supports it. The Board can manually override any budget at any level.
@@ -277,11 +287,20 @@ All agent communication flows through the **task system**.
There is no separate messaging or chat system. Tasks are the communication channel. This keeps all context attached to the work it relates to and creates a natural audit trail.
Experimental Agent Chat presents one persistent task per person and agent as a simplified conversation. It retains the task composer, transcript, tools, attachments, documents, and existing Subtasks panel, with ordinary company visibility. New execution tasks are ordinary project tasks, not children of the conversation. Idle conversations wait for a message without entering execution-task work queues. Agents clarify goals here and create assigned tasks for substantial execution. `/new` resets provider context at an ordered session boundary within the same task while preserving visible history. `enableAgentChat` is disabled by default; the V1 lifecycle and rollout contract is specified in `SPEC-implementation.md`.
Experimental Agent Chat presents one persistent task per person and agent as a simplified conversation. Chat has a searchable secondary sidebar with agent avatars; adding an agent starts or reopens their single conversation. It retains the task composer, transcript, tools, attachments, documents, and existing Subtasks panel, with ordinary company visibility. New execution tasks are ordinary project tasks, not children of the conversation. Idle conversations wait for a message without entering execution-task work queues. Agents clarify goals here and create assigned tasks for substantial execution. `/new` resets provider context at an ordered session boundary within the same task while preserving visible history. `enableAgentChat` is disabled by default; the V1 lifecycle and rollout contract is specified in `SPEC-implementation.md`.
### Question recipients
Ordinary Agent Chat questions use the server-owned conversation recipient.
Task questions may optionally name a particular user or agent. Explicit user
recipients must be valid and authorized in the company before a question is
saved. See `SPEC-implementation.md` §9.8.1 for the resolver contract.
### Implications
- An agent's "inbox" is: tasks assigned to them + comments on tasks they're involved in
- A human's Mine inbox and its badge include failed runs attributed to that human, not another user's runs. All retains company-wide failure visibility. Historical unattributed runs remain in the local single-user board's Mine view; see `SPEC-implementation.md` for the routing contract.
- The CEO delegates by creating tasks assigned to the CTO
- The CTO breaks those down into sub-tasks assigned to engineers
- Discussion happens in task comments, not a side channel
@@ -414,6 +433,13 @@ Tasks use **single assignment** (one agent per task) with **atomic checkout**:
No optimistic locking or CRDTs needed. The single-assignment model + atomic checkout prevents conflicts at the design level.
Agent @-mentions provide context without waking agents or changing task ownership. New work requires explicit assignment, delegation, or a review request; ordinary issue comments can still wake the current assignee.
Releasing a terminal task clears execution locks while preserving its assigned
owner and final status. Assignment remains part of the work history after Done
or Cancelled. Releasing unfinished work still relinquishes the agent assignment;
only an active `in_progress` task returns to `todo`.
### Human in the Loop
Agents can create tasks assigned to humans. The board member (or any human with access) can complete these tasks through the UI.
@@ -604,21 +630,22 @@ title matches lead; current conversation and document content supplies supportin
evidence. See [Task search relevance](SEARCH.md) for the evaluation rubric,
matching contract and reproducible quality tests.
### Personal keyboard shortcut preference
### Keyboard shortcuts
Keyboard shortcuts are off by default and are enabled in Settings → Profile.
The preference is stored on the signed-in user, applies across companies and
devices, and does not require instance administrator access. The local trusted
board user has the same preference. `GET /api/auth/preferences` returns only the
current board user's preference. `PATCH /api/auth/preferences` updates only that
user and requires an accessible `companyId` for the activity log, including viewer
memberships. The preference and audit record commit in one transaction. Both
requests require `expectedUserId` (GET query parameter or PATCH body) matching
the authenticated actor, so a cookie change cannot mix accounts in the cache.
Agents cannot
read or change these preferences. The legacy instance general setting is retained
for API compatibility but no longer controls shortcut behavior in the app;
users opt in individually after the upgrade.
Keyboard shortcuts are always enabled for every signed-in user. There is no
instance setting and no personal preference that turns them off.
Managed agents own a persistent file directory across tasks and sessions. The
Instructions Editor and stopped agent execution synchronize the same current
files, including AGENTS.md and its supporting files. Task working directories and
provider home directories remain separate concepts. Concurrent runs synchronize only
the files they change, with the last sync winning for the same file. Temporary
copies are cleaned up; this storage does not add a revision-history system. See
[agent-files.md](agent-files.md) for lifecycle and upgrade compatibility.
Full agent storage produces a run warning without stopping current or future
work. Storage limits constrain saved file changes, not the agent's ability to run
and remove files to recover space.
### Unsafe native workspace exports
@@ -631,3 +658,15 @@ omit the already-rejected export, clear stale repair notices, and finalize the
accepted result without another provider turn, even when its old sandbox is
unavailable. Preserve current ownership and newer-work fences. See
`native-workspace-finalization-recovery.md`.
### Git-backed skill library sources
Repositories can supply read-only company skills independently of project repositories.
A source tracks repository identity, branch, selected paths, and installed commits;
an existing GitHub connection supplies caller-authorized reads. Manual refresh publishes
complete local immutable versions, preserving skill identity and assignments. Selection
operates on whole skill packages, with inspectable included files, declared runtime
requirements, and advisory warnings for missing or external references. New
upstream skills require reviewed selection; removed or deselected skills remain
installed. Editing starts with an independent copy. Write-back and PR publication
are a later milestone; exact path and commit provenance provide their base.
+199
View File
@@ -0,0 +1,199 @@
# Persistent agent files
Each managed agent has one current directory, scoped by company and agent. The
Instructions Editor reads and writes this directory. `AGENTS.md` (or the
configured entry) is one file in it. Agents may create ordinary files and nested
folders for notes, memory, and other personal working material. Files in the
task working directory remain task files.
Agents can edit their own managed files under the responsible user’s current
target permissions. Access to another agent’s files additionally requires the
caller’s own target-scoped configuration permission; shared company membership
or a responsible user alone does not grant peer access.
## Layout
The canonical host directory keeps its existing physical location:
```
<instance>/companies/<company>/agents/<agent>/
instructions/ current agent files (editor)
AGENTS.md
notes/
any-supported-file
file-sync/ controller-only operational state
adopted.json
canonical-manifest.json metadata/hash cache, no file contents
runs/<initial-run>/
owner.json current run owning the writable copy
live/ writable copy for the provider lifetime
```
The process starts in its existing task workspace. `AGENT_HOME` points to the
registered writable agent copy. Adapter `HOME` and `CODEX_HOME` keep their
existing meanings and are not personal-file storage. Local copies are outside
the task workspace. Remote providers currently confine file sync to their
workspace: their independent agent copy therefore lives under the excluded
`.paperclip-runtime/agent-files/<agent>/<run>/` area. It is not included in task
workspace sync, Git staging, or task deliverables.
Regular files (including binary bytes) and directories are supported, up to
100,000 entries (files and folders), 256 MiB per file and 2 GiB total. Symlinks,
hardlinks, and special
files are rejected, rather than followed or silently skipped. The instruction
entry remains valid UTF-8, at most 1 MiB, and cannot be deleted. The editor edits
text up to 1 MiB and offers downloads for binary or larger files. The reserved
`.paperclip-runtime` directory and the compatibility-only virtual file
`promptTemplate.legacy.md` are not user storage. Task cache and Git ignore
exclusions do not apply to this directory.
These storage limits are separate from the 1 MiB instruction/editor limit. Large
files are hashed and downloaded as streams; listings bound concurrent reads,
and only editor-sized text is buffered. Storage counts uncompressed file bytes,
not allocated disk
blocks. These are sync validation limits, not live filesystem quotas: an agent
can write beyond them while running. Storage limits never pause an agent, fail
a provider run, or block future task admission. A folder at or above a limit
produces a warning on each run until enough files have been removed or shrunk.
Existing saved files are restored even when already over quota, so the agent
can continue working and clean them up with ordinary filesystem tools. Unsafe
paths and links still fail validation; bypassing a storage quota does not
bypass those checks.
An API save above a storage limit returns 422 without changing the saved files.
If a stopped run exceeds a storage limit, none of its agent-folder changes are
saved. The run shows a nonblocking storage warning and its save receipt reports
`AGENT_FILES_LIMIT_EXCEEDED` with the specific limit
and, for an oversized file, its path. The previous saved folder is used on the
next run. The temporary run copy is discarded, including on a limit failure;
there is no retained recovery archive or partial-save option. Transient sync
failures get up to three attempts at the stop boundary before cleanup and an
explicit failure receipt. Individual file writes are atomic, but an I/O failure
partway through a sync can leave some files updated; a failed receipt does not
claim whole-folder success.
Sync failures are diagnostics for the affected run, not errors on the current
files in the Instructions Editor. Historical failures remain in the run log;
the run detail also shows warnings from its save receipt. The editor only shows
preserved instruction-only candidates that may need review, alongside errors
from the current browser edit. Later successful saves do not erase run history.
Initial restoration copies the canonical folder once. Warm native Codex turns
reuse that working directory. Checkpoints enumerate file metadata, hash files
whose identity/size/mode/mtime/ctime changed, and temporarily copy and transfer
only changed file contents. Deletions and empty directories travel as manifest
entries. An unchanged image is neither rehashed nor recopied after its first
checkpoint. A modified file is transferred in full; this is a file-level delta,
not block-level deduplication. Canonical hash caches and manifests contain no
file contents. Temporary checkpoint payloads are removed after application.
The operator still provisions storage for the canonical folders, active working
copies and changed-file payloads; these are not aggregate disk quotas.
## Run lifecycle
1. Under the agent lock, restore current files into a private run copy and save
a baseline of paths, kinds, modes, and hashes. This is sync metadata, not a
revision history.
2. Stage the copy through the existing workspace transport. Point `AGENT_HOME`
and instruction guidance at that registered root.
3. For warm native Codex, capture a manifest and changed-file payload at each
terminal turn boundary before admitting another turn. Check file metadata
before and after streaming and hash the captured payload independently on
the host. Retry an unstable checkpoint up to three times; if it cannot be
validated, close the owned provider and perform the stopped collector. Other
execution paths retain their stopped-provider collection boundary.
4. Recheck the responsible user's current authorization. Under the same agent
lock used by editor writes, apply only files changed or deleted relative to
the last acknowledged baseline. For a competing edit or deletion of the same
file, the last synchronization to acquire the lock wins. Unchanged files do
not overwrite another run's changes; newly added unrelated files survive.
5. Record the save receipt and advance the baseline only after application. A
warm session keeps its directory and hands ownership to the next run using
a controller-owned marker. Old callbacks cannot collect or remove the next
owner's files. On session retirement, collect any later writes and remove
the private directory. No per-run file versions or conflict copies accumulate.
These are validated **per-file checkpoints**, not an atomic snapshot of arbitrary
background writers across an entire directory. Writes after a checkpoint remain
pending until the next checkpoint or verified session retirement. A save receipt
acknowledges only the captured bytes. Lost remote bytes or missing stop proof
cannot become a successful save.
Warm reuse requires the actual live session, the same remote environment and
provider lease, and unchanged canonical files since its last checkpoint. An
editor or another task changing canonical files retires that session before a
fresh copy is restored. A directory-path mismatch also forces retirement. Only
the loaded instruction entry participates in the runtime instruction digest;
ordinary memory/image edits do not change it. Changes to loaded instructions,
policy, credentials or provider configuration may still replace the process.
Relative supporting files are read from `AGENT_HOME`, not the read-only prompt
snapshot. External instruction bundles remain read-only and use their existing
lifecycle.
The editor supplies the hash of the file it read. A stale browser save returns
409 and retains the user's unsaved draft. Run synchronization itself uses
per-file last-sync-wins: a later run can overwrite a saved browser edit to the
same file. There is no text merge or historical copy to recover the overwritten
version. Ordinary task files continue using their existing workspace contract.
## Upgrade and recovery
Migration 0287 creates the preview tables idempotently after master’s 0285/0286.
Existing preview receipts, rows, constraints, and pending captures are retained.
On first use, while holding
the agent row lock, import any deployed revision heads into the existing managed
directory once. A controller-owned marker outside agent files prevents any
later replay of those heads. Existing revision rows remain readable for recovery;
new saves never append to them. Old UUID-based clients receive content tokens
and can still submit their previously recorded revision IDs, which are checked
against the corresponding bytes before a write.
Working-copy receipts and native runtime inputs record the new file contract.
A restored native session with no contract field keeps the old instruction-only
copy shape, prompt digest, paths, and collector. Its writes use the compatibility
bridge into current files, with the original baseline fence. Existing pending
legacy candidates remain resolvable. Neither old task workspaces nor arbitrary
external instruction roots are imported as agent directories.
Stock-agent and plugin resets update their declared files while retaining unrelated
personal files and formerly configured entries. Automatic stock upgrades first
record baseline hashes in the existing resource binding, then apply and finalize
under the agent lock. A failed file write or database commit retries against
those hashes and already-applied bytes. Removed, unchanged stock files are
removed; intervening personal edits stop the retry. This pending operation
metadata is cleared on success and does not retain file revisions.
External bundles retain their existing behavior. Their migration to managed
storage is an explicit configuration action. Historical task cwd, provider-home,
checkpoint, and workspace restoration formats are not rewritten.
Backups must include the persistent instance filesystem as well as the database.
New current-file bytes are not database revision rows. Old instruction-only
candidates are retained solely for upgrade compatibility.
Crash recovery can collect a stopped working copy without starting a model.
Missing stop proof or lost remote bytes produce a visible diagnostic, never a
save receipt. An interrupted apply can replay its changed files with the same
last-sync-wins rule. Cleanup resumes for terminal runs; no copy is retained as
an archive after cleanup succeeds.
## Verification
`agent-directory-working-copies.test.ts` exercises nested/binary files, directory
isolation, last-sync-wins edits and deletions, terminal cleanup, link rejection, old-head
adoption, and stable prompt digests. The legacy working-copy and native-tool
suites exercise compatibility. Workspace merge tests exercise preflight and
interrupted replay.
The explicit Product E2E `instruction-persistence` suite creates a file through
the browser editor, runs an agent that changes instructions and supporting files,
checks exact binary bytes via the public download route, restarts the server,
and asks a fresh task to prove restored contents using an independent nonce. A
third task edits its entry while the browser saves that same file; the later
run sync wins while a separate browser-created file survives, with no conflict
candidate or manual resolution. Three more tasks save a sparse file at its
256 MiB boundary, exceed that boundary with a nonfatal save rejection, then
remove it and save a new small file. All tasks must succeed, with warnings
visible in run details while full and cleared after cleanup.
Run results, including unavailable credentials, must be reported separately from
unit or matcher results; a passing matcher does not prove a live run.
+15
View File
@@ -0,0 +1,15 @@
# Legacy instruction revision compatibility
The revision-based design in the first version of PR #14325 has been superseded
by [persistent agent files](agent-files.md). Current managed files are authoritative;
new edits do not create revision or head rows.
Published migrations 0285 and 0286 and existing revision records are retained so
preview installations and saved sessions can upgrade safely. Old heads are
adopted once. Legacy collectors and UUID-based APIs compare their baseline bytes
against current files before writing. Read-only history and restore endpoints
remain for pre-upgrade records; they are not advertised as new agent capabilities
or shown as a history feature in the current file editor.
See the current document for directory layout, synchronization, conflicts,
authorization, recovery, and verification.
@@ -142,6 +142,7 @@ conditions before model disposition:
| Runner reports a task-wide blocker | `blocked` | Persist blocker owner and unblock action |
| Runner reports a current-track blocker | `in_progress` | Enqueue another productive track |
| Runner reports `yielded` with a valid continuation | `in_progress` | Enqueue the declared continuation |
| Ordinary task reports `response_wake` and blocking remaining work, without a real wait | `in_progress` | Use the bounded incomplete-work continuation, then a visible recovery error if repair was already used |
| Completion evidence is incomplete and continuation is forbidden | Preserve | Record a finalization error and named next action |
| Completion evidence is otherwise incomplete | `in_progress` | Enqueue a bounded, idempotent continuation |
@@ -155,6 +156,21 @@ The output is a `NativeStatusDecision` containing:
The pure arbiter performs no database or network writes.
A response to a Board comment does not by itself justify parking unfinished
work. When a `yielded` / `response_wake` result contains
`remainingWork[].blocksCompletion: true`, `paperclip_finish` rejects the report
unless a real wait owns the next action. The agent can correct it in the same
turn. Finalization also checks accepted reports and uses the existing
`native-completion-incomplete` one-follow-up budget. Model-selected retry keys
cannot reset that budget. Pending interactions, approvals, dependencies, pause
holds, and conversation lifecycles retain their authority.
The current Board source is rechecked in the status transaction before a repair
is queued. Edited, deleted, or superseded comments do not authorize replay of an
old response. Recovery no longer treats an old committed passive wait as healthy
when its report admits blocking work and its Board source is still current.
Existing recovery gates continue to protect pauses, budgets, and ownership.
## Transactional decision commit
`commitNativeStatusDecision()` applies the decision against authoritative issue
@@ -0,0 +1,592 @@
# Rich ACP integration and qualification report
Updated: 2026-09-29. Base: `c65fc9e3c81c41aafe421aa90a00514b84343285`.
Mainline integration: `3ca196b0a642aa21b8feba1fcd89edb53d1c622e`.
Status: implementation is split into a shared foundation and three provider PRs.
Provider review and qualification remain separate from foundation acceptance.
All three new profiles remain **pending qualification**. Cursor passed all five
local semantic Product E2E cases. Copilot passed completion, file validation,
plan approval and controller restart; its question case failed the required final
marker. Pi passed completion, file validation and semantic question continuation,
plus real native denial, active steering and queued follow-up probes on v2/v3/v4.
Pi completion/file/question passes retain their v2/v3 identities. The latest
Pi candidate is profile v5 with a patched dependency closure; its paid native
control probe passed, but its Product qualification remains blocked. Copilot
passed real native denial and detached-command settlement probes. Failed attempts remain in the evidence;
no Daytona resources have been started. This report does not certify a provider
from its ACP listing or a partial run.
The [harness priorities report](https://pages.paperclip.ing/2026-09-25-harness-priorities/)
recommends Cursor and Copilot, followed by Pi, using the existing qualified ACPX
path. Codex app-server is the richness benchmark. The legacy Cursor and Pi
adapters are outside this change.
## Branches and evidence ownership
| Branch | Deliverable |
| --- | --- |
| [`codex/runner-rich-acp` / #14430](https://github.com/paperclipai/paperclip/pull/14430) | Shared ACPX extension boundary, durable permissions, canonical display events, provider pack infrastructure, configuration and UI |
| [`codex/runner-cursor-acp` / #14435](https://github.com/paperclipai/paperclip/pull/14435) | Cursor native distribution, questions/plans, child activity, policy admission, wire fixtures |
| [`codex/runner-copilot-acp` / #14434](https://github.com/paperclipai/paperclip/pull/14434) | Copilot native distribution, event inventory/projections, permission and settlement probes |
| [`codex/runner-pi-acp` / #14436](https://github.com/paperclipai/paperclip/pull/14436) | Patched wrapper, owned extension, MCP/tools, permissions/input, portable dependency closure |
| [`codex/rich-acp-extended-harness-evals` / evals #29](https://github.com/paperclipai/paperclip-evals/pull/29) | Explicit 21-cell Runner Eval campaign, semantic assertions, provenance and fail-closed budget accounting |
The provider branches were implemented in parallel from the foundation. Final
shared registration and packaging conflicts are resolved in dependency order:
foundation → Cursor → Copilot → Pi. They remain four separate worktrees and PR
review units; the later PR bases include their prerequisite providers. Foundation
acceptance requires Apex review and CI to pass. Provider PRs remain unmerged
pending qualification. Source reports on the provider branches are
`doc/architecture/runner-cursor-capabilities.md`,
`doc/architecture/runner-copilot-capabilities.md`, and
`doc/architecture/runner-pi-capabilities.md`. Those reports retain versioned
source references, per-field dispositions, fixture paths, and narrower claims.
The Copilot inventory enumerates all 150 pinned native event types. Read the
[Cursor inventory](https://github.com/paperclipai/paperclip/blob/codex/runner-cursor-acp/doc/architecture/runner-cursor-capabilities.md),
[Copilot inventory](https://github.com/paperclipai/paperclip/blob/codex/runner-copilot-acp/doc/architecture/runner-copilot-capabilities.md),
and [Pi inventory](https://github.com/paperclipai/paperclip/blob/codex/runner-pi-acp/doc/architecture/runner-pi-capabilities.md)
for the complete per-provider source audit.
[Retained browser evidence](../../ui/storybook/fixtures/evidence/rich-acp-browser-proof.darwin-arm64.json)
records the production renderer's full native plan, accept/reject/cancel,
single/multiple selection, typed input and activity-details checks. Its Cursor
transport is a canonical fixture, not a paid provider session. The browser
renders a 99,724-character plan and verifies its final paragraph before approval.
The JSON records screenshot hashes; screenshots remain outside the source tree.
## Capability matrix
“Candidate” means implemented or observed in deterministic tests. It does not
mean the required paid local and Daytona product cases have passed. “Not exposed”
means the pinned interface was inspected; “unverified” is a separate finding.
Codex's row is the existing app-server integration, not the Codex ACP bridge.
The provider reports above are the method/event and field inventories. In the
table below, a method that is absent from a pinned implementation is distinct
from an exposed method with no Paperclip control. Shared surfaces and their
deterministic evidence are mapped separately after the comparison.
| Capability | Codex app-server benchmark | Cursor ACP candidate | Copilot ACP candidate | Pi ACP candidate |
| --- | --- | --- | --- | --- |
| Exact model | Selected and reported model | Explicit ID required; exact echo, paid semantic protocol and five local semantic product cases passed | Explicit ID required; exact `gpt-5.6-luna` echo, paid semantic protocol and local completion/file/plan/restart passed | Exact `openrouter/deepseek/deepseek-v4-flash-0731`; model echo and v2/v3 paid Product completion/file/question cases passed; v5 native controls passed |
| Text and tools | Typed thread/turn/item events | Standard ACP updates; child activity kept separate | Standard ACP plus opt-in native session events | Wrapper text/tool updates and owned tool gates |
| Active steering | Dedicated `turn/steer` | Concurrent prompt replaces/cancels, so it is not steering | Concurrent prompt replaces/cancels, so it is not steering | Owned `pi/steer` requires handshake, exact active turn and acknowledgment; real active-turn probe passed |
| Queued follow-up | Product continuation controls | Controller can schedule a later prompt; native queue not established | Native pending-message activity exists; no qualified ACP queue responder | Owned `pi/follow_up`, separately named and ordered; real queued marker followed the steered current response |
| Cancellation | Typed interrupt and process lifecycle | ACP cancel; paid command cleanup pending | ACP cancel; bounded live detached-command settlement and cleanup passed | Native abort; wrapper waits for `agent_settled` and treats provider errors as failure |
| Session continuity | Read/load/history/fork and durable identity | Session load/list observed; paid semantic warm continuation passed; native history replay unverified and fork absent in tested methods | Session load plus native history events; semantic pending-question controller recovery passed | Private Pi JSONL mapping/load; native RPC `fork`, `clone`, `get_fork_messages` are not mapped through this ACP wrapper; unresolved UI promises cannot survive provider death |
| Questions | Typed input requests and response correlation | `cursor/ask_question`, option identity and multiple selection preserved | Native ask-user capability exists, but pinned ACP does not wire its responder; do not display a false answerable form | `select`, `confirm`, `input`, `editor` through typed form elicitation |
| Permissions | Durable typed approvals | Standard ACP permission options; denied shell write had no observed side effects; separately labeled exact-correlation assessment | Standard ACP; real native reject_once prevented marker creation; session decision scope inspected | Native pre-tool gate; allow once, exact-operation session grant, deny; paths rechecked after wait |
| Plans | Typed plan and collaboration mode | `cursor/create_plan` includes full plan and revision-bound accept/reject/cancel; todo activity separate | Native plan events displayed; native plan-decision callback not exposed in ACP | No native structured plan event; authenticated Paperclip planning tools available |
| Authenticated tools | Runner bridge and governed operations | ACP HTTP MCP binding; paid context/history reads passed | ACP HTTP MCP binding; paid context read passed | Owned extension registers exact bound MCP tools; four authenticated semantic reads succeeded in a paid partial run; no ambient servers |
| Delegation | Typed agent roles and lifecycle | Opt-in subagent lifecycle, nested ownership and bounded child activity; never parent transcript flattening | Native delegation/session events projected with role/model/agent provenance | No built-in ACP delegation protocol; arbitrary extensions are excluded |
| Files/diffs | Typed file changes and artifact references | Standard tool changes plus validated image references | File/workspace events retained; provider session files are not silently treated as task files | Wrapper-retained read/write/edit diffs; common typed/UI projection remains partial; semantic artifact tools |
| Images/artifacts | Typed references and registered work products | Existing contained files only, provenance, `registered:false` | Contained task references only; external/session-store paths become descriptive notices | Image/resource tool blocks preserved by wrapper; dedicated artifact channel absent |
| Image/attachment input | Typed input conversion | ACP advertises images, but the runner turn converter currently forwards text only | ACP advertises images/embedded context, but the runner turn converter currently forwards text only | Native Pi/ACP image input exists, but the runner turn converter currently forwards text only |
| Usage | Per-request receipt and model context | Pinned ACP omitted receipts on denied and successful turns; account UI confirms included usage separately | ACP/native tokens retained; account UI confirms included credits separately, without a per-run USD receipt | Assistant-message and compaction token receipts; dollar cost is a catalog pricing estimate, never authoritative billing |
| Config/model changes | Typed configurable controls | Known modes/model interfaces researched; runtime policy cannot be changed by a display event | Model/reasoning/mode options exist; runtime policy remains authoritative | Exact pinned candidate model; arbitrary slash commands/config/extensions disabled |
| Reconnect/restart | Durable controller replay and qualified provider restoration | Semantic pending question survived a real controller restart; exact native callback restoration remains unverified | Semantic pending question survived controller restart with the same interaction and provider session; native callback restoration remains unverified | Wrapper explicitly advertises live-process-only pending-input recovery |
| Shared capability | User-visible surface | Inspectable implementation / deterministic evidence |
| --- | --- | --- |
| Text, tools and child activity | Task transcript; bounded activity details preserve provider/session/tool attribution | `src/drivers/acpx/codex-runtime-adapter.test.ts`, `runner/crates/runner-core/tests/acpx_rich_events.rs`, `ui/src/components/task-chat/TaskChatProtocolActivityRow.test.tsx` |
| Native questions and permissions | Existing question/confirmation cards; only offered decisions can be submitted | `src/protocol/permission-request.test.ts`, `src/drivers/acpx/acp-permission-adapter.test.ts`, `server/src/services/native-runtime/native-question-bridge.test.ts`, `ui/src/components/task-chat/TaskChatProtocolCard.test.tsx` |
| Native plan decisions | Full plan description followed by a revision-bound decision; no implicit plan acceptance | Cursor provider fixtures; `ui/storybook/fixtures/evidence/rich-acp-browser-proof.darwin-arm64.json` records the real renderer with a canonical fixture |
| Active steering and queued follow-up | Existing active-turn control where a bound method is negotiated; no native Pi queue selector yet | `src/drivers/acpx/turn-controls.test.ts`; provider reports state which wire method is absent or not surfaced |
| Files, diffs and images | Workspace file/artifact cards plus contained provider reference notices; raw provider diffs are still partial | `src/drivers/acpx/profile-extensions.test.ts`; provider field audits; Product `file-edit-validate` uses an independent exact-byte oracle |
| Usage and model identity | Exact configured model, per-provider token/accounting fields, explicit incomplete cost coverage | `src/drivers/acpx/usage-accounting.test.ts`, `src/cli/eval-session-contract.test.ts`, `server/src/services/native-runtime/native-session-executor.test.ts` |
| Durable input, reconnect and provider death | Pending interaction cards survive controller recovery; unsafe replacement expires unresolved requests; delivered settlement survives a crash before journaling | `src/control-plane/durable-prp-control-plane.test.ts`, `runner/crates/runner-core/tests/acpx_provider_resolutions.rs`, `runner/crates/runner-core/tests/native_provider_backend.rs`, `src/live/runnerd-codex-transport.test.ts` |
| Session list/fork, generic configuration and commands | No added operator surface; exact owned session recovery and configured model remain available | Provider inventories identify native-only, ACP-exposed, confirmed-absent and unverified methods with follow-ups |
Paths starting with `src/` or `runner/` in this evidence table are relative to
`packages/paperclip-runner/`; other paths are repository-relative. Deterministic
fixtures establish contract behavior, not successful paid provider execution.
## Shared event and interaction contract
The active connection, wire session, normalized turn and original request identity
bind every extension callback. The allowlist is per provider. An omitted wire
session can only acquire the verified active connection's session; an explicit
mismatch is rejected. Retired streams and inactive turns cannot emit new activity.
Requests enter durable runtime state before the UI presents them. The response
must match an outstanding request and an offered action or valid typed answer.
The direct driver and sidecar await a receipt for the exact JSON-RPC pipe write before the runtime settles its durable record.
The successful resolution enters the retained event outbox in the same atomic
state save that removes the pending request. A restart before journal delivery
retains that resolution for normal event replay and acknowledgment. A restart
before this state save instead expires the unresolved request; neither path sends
the provider response again.
If persistence fails, the current executor stops accepting commands and exposing
or acknowledging retained events. Cleanup still terminates its owned provider,
but leaves the uncertain snapshot untouched. A fresh executor reads the complete
atomic snapshot that survived; it cannot publish an in-memory resolution that
conflicts with a later recovery expiry.
Standard ACP does not acknowledge application of a permission reply; a lost
transport acknowledgment is not proof of exactly-once external effects. A
replacement provider process cannot inherit an old approval promise. A bounded durable ledger expires pending requests after provider loss or unsafe restart, including requests whose creation events were already acknowledged. No tool
mutation or approval is automatically replayed into a replacement.
Full plan documents have a bounded 100,000-character description and a 196 KiB
question-set envelope. Oversized plans fail rather than approve an unseen suffix.
Display redaction remains visible. Decision descriptions render image references as
inert text and Mermaid diagrams as source, so reviewing a plan does not fetch
provider-selected media. Automatic issue-reference linking is disabled for these
descriptions, so a long provider plan cannot start an issue-detail query for every
identifier. The rich event channel has exact canonical
schemas and a bounded envelope. It cannot create terminal outcomes, dispatch a
semantic tool, register an artifact, synchronize a durable plan, or supply source
authority. Notices retain useful bounded fields and provenance in expandable UI
details. Provider references remain unregistered until a control-plane operation
registers them.
Permission labels are derived from offered option kinds. An unknown or duplicate
option is rejected. A provider's “always” decision is not relabeled “this session”
unless the pinned implementation proves that scope. Restrictive execution policy
is separate from automatic approval and company governance. Fresh permission
configuration defaults to full auto; it never overrides read-only task policy.
## Distribution and isolation
Cursor pins `2026.09.26-dd393fe`; Copilot pins `1.0.88`; Pi pins wrapper `0.0.33`,
runtime `0.84.2`, portable Node and its full npm lock. Native distribution hashes
cover macOS ARM64, macOS x64 and Linux x64. Source-owned closure pins remain
separate from profile declaration digests. Native admission reads held files,
verifies every admitted byte, creates a private immutable snapshot and retains
the existing process guardian. A manifest cannot supply its own trusted pin.
Candidate credentials are only read from explicitly bound run environments:
Cursor `CURSOR_API_KEY`/`CURSOR_AUTH_TOKEN`, Copilot `COPILOT_GITHUB_TOKEN`, Pi
`OPENROUTER_API_KEY`. Ambient GitHub login variables, provider configuration,
extensions and MCP discovery do not establish authority. Homes/config/cache are
private. Updates are disabled. Pi launches only its owned extension and assigned
skills, with a startup sentinel before a prompt can run.
Build candidate packs explicitly with `--candidate-providers=<name>` on that
provider's branch. Local and remote pack verification includes the complete
candidate asset tree. The corresponding Daytona build argument is documented in
`docker/daytona-runner/README.md`. Candidate packaging never promotes a profile.
ACP is not an OS sandbox. Cursor still reads project MCP/hooks beyond its
`--disable-project-configs` flag; the adapter rejects known ambient execution
configuration. Mutations during a run and remote team hooks still require
qualification. Pi's tool policy supplements the execution boundary; arbitrary
shell commands and filesystem races require the host boundary. These are
qualification gates, not claims that a JavaScript path check confines a shell.
## Explicit gaps and follow-ups
| Priority | Exposed but unused, partial, or unverified | Reason and next proof |
| --- | --- | --- |
| P0 | Remaining paid product cases on local and Daytona | Credentials are explicitly bound. Cursor's five local semantic cases passed. Copilot completion/file/plan/restart cases and Pi completion/file/question passed. Copilot question continuation reached the correct UI and warm session but failed its final marker. Copilot question qualification, remaining Pi cases and all remote cases remain open. The 30-cell Product E2E and private 21-cell Runner Eval extended suites are explicit-only. Daytona billing access still requires user email verification. |
| P0 | Pi fresh-profile Product verification blocked by host resources | The next paid plan case failed during embedded PostgreSQL initialization before any provider prompt. A disposable reproduction found 16 free SysV semaphores when Postgres required a set of 17. Resume the unchanged canonical case only after task-owned cleanup or a supported isolated test host restores capacity; do not remove unrelated IPC objects or substitute an unqualified database fixture. |
| P0 | Copilot native ask-user and plan-decision callbacks | Pinned ACP does not install native responders. Prove no blocking request is exposed, or add a qualified responder/wrapper; never swallow the request. |
| P0 | Broader Copilot denial and background settlement qualification | Two bounded real-service probes passed on pinned 1.0.88: native reject_once prevented a requested file write, and an observed detach:true command completed before end_turn. These establish the selected cases only; retain broader local/Daytona qualification before promotion. |
| P0 | Cursor native question availability | A real default-mode prompt with no semantic MCP tools reported that native AskQuestion was unavailable. No native request arrived. The pinned client implements the RPC and mode controls, but inspected local guards do not explain the negotiated tool availability. Inspect the actual catalog/flags before another live probe; do not infer permanent harness absence or native success from semantic question tests. |
| P0 | Typed Cursor entitlement failure | Exact models and successful inference are observed on the selected paid accounts. Cursor's first-account entitlement denial became ordinary text and normal completion. Preserve this failure and qualify typed failure handling; never infer a successful task from terminal status alone. |
| P0 | Cursor project and remote hooks; native shell boundaries | Configuration flags do not cover every native source. Demonstrate policy cannot be bypassed before production qualification. |
| P1 | Native Pi queue selection in the product UI | The runner API exposes negotiated `follow_up` separately from active steering. The current composer has no native queue selector; add one without confusing it with controller-scheduled later turns. |
| P1 | Pi `queue_update` contents and delivery state | RPC acknowledgment proves acceptance, not model consumption. Add bounded queued/delivered events and durable message correlation with explicit retention rules for user content; preserve this distinction on reconnect. The selected live marker-order probe proves its own consumed messages only. |
| P1 | Pi structured retry and compaction progress | Current text projection drops some `attempt`, `maxAttempts`, `delayMs`, `errorMessage`, `success`, `finalError`, `reason` and `willRetry` fields. Add bounded redacted provider notices with source-event provenance and outcome regressions; keep terminal failure authority separate. |
| P1 | Native Cursor/Copilot active steering and queues | ACP prompt replacement is not steering; native SDK capabilities may be richer. Require a dedicated bound method plus acknowledgment before advertising. |
| P1 | Child tool media/diff/raw payloads | Bounded delegation summaries preserve lifecycle and identity. Large nested payloads need a child-owned canonical item model; current omission is a visible notice and provider report entry. |
| P1 | Standard ACP parent tool `content` diffs/images, secondary locations and raw input bodies | The existing common normalizer projects bounded output text, input-presence, and the first safe relative location. Raw argument bodies can contain secrets; richer content needs bounded typed blocks and a separately validated workspace binding for each file. Provider-specific image/file notices do not close this standard-tool gap. |
| P1 | User attachments and image prompting | `AcpxRuntimeTurnInput` and the common runtime adapter currently forward text only, despite underlying image-input support. Implement validated attachment-to-ACP content conversion and model-specific capability admission, then qualify real local/Daytona image prompts. This is an implementation gap as well as a live-verification gap. |
| P1 | Copilot session-store files and export/artifact URIs | Provider paths are not task-workspace paths. Add a separately authorized export flow with validated bytes and provenance; do not resolve arbitrary URLs or auto-register. |
| P1 | Pi native fork/history/export interfaces | Pinned Pi 0.84.2 native RPC exposes `fork(entryId)`, `clone`, `get_fork_messages` and `export_html`. The wrapper does not map them to runner controls. Add durable branch lineage for fork/clone and an authorized, contained artifact flow for HTML export before exposing them; do not label these native capabilities absent. |
| P1 | Complete usage/billing provenance | Missing cache fields remain unknown. Pi price estimates are displayed separately. Budget qualification requires actual spend coverage, not an estimate presented as a bill. |
| P1 | Fork/history/model/mode controls not exposed by Paperclip | Research documents the native and ACP methods separately. Add governance-aware controls and durable lineage before enabling them. |
| P1 | Exact pending-request restoration after process death | Session transcript restoration does not restore callbacks. Expire unresolved requests unless a provider proves exact restoration. |
| P1 | Persistent agent-directory access through ACPX | Mainline `3ca196b0a` supplies `AGENT_HOME`, but its existing ACPX environment allowlist does not forward it. Pi also confines native writes to the task workspace, while the local persistent agent directory lives outside it. The disabled candidates do not claim this new capability. Bind and validate the company/agent/run-owned directory explicitly through launch and native-tool policy, including warm-run rebinding and cleanup-before-collection tests; do not widen ambient environment or filesystem access. |
| P2 | Remaining Copilot native diagnostic/config/account events | The provider inventory records every event and field, its projection or reason for omission. Preserve bounded useful context; avoid credentials, raw environment or unbounded blobs. |
| P2 | Pi native extension surfaces and unsupported slash commands | Arbitrary extensions/templates/themes may execute ambient code. Only reviewed runner-owned capabilities are admitted; structured native plan/goals are not fabricated. Native fork/clone/export are separate unmapped capabilities above. |
| P2 | Pi status/widget/title/editor and session/configuration notifications | `setStatus`, `setWidget`, `setTitle` and `set_editor_text` have no UI projection and are unused by the owned extension. Native session-name and thinking-level events also lack a dedicated projection. Add reviewed bounded notice schemas and governed configuration controls before exposing these fields; `notify` and interactive input already have separate bridges. |
| P2 | Pi invocation and history provenance metadata | The v4 wrapper retains `nativeToolCallId`, `modelIteration`, `historyMessageIndex` and `identityScope` in private ACP-wire metadata. Closed common tool and permission projections omit them from the UI. Normalized IDs still correlate live tool, MCP and permission events. Add bounded, redacted display-only provenance fields and parity tests before surfacing the native metadata. |
| P2 | Conditional native-plan follow-up fields | Cursor's optional rejection-reason field also appears for accept/cancel. The current question renderer has no conditional fields; add conditional presentation without changing the revision-bound decision receipt. |
| P2 | Cursor command exit code projection | The paid file case preserved native `exitCode: 0` inside output text, while the canonical command field remained null. Normalize a typed, correlated exit code without parsing arbitrary prose; current independent file assertions do not prove this field. |
## Qualification ledger
Combined ceiling: **$100**, including retries and infrastructure. Cursor allocation:
$25; Copilot: $25; Pi: $25; coordinated diagnosis reserve: $25.
Initial reservations are $2 per provider. Measured cumulative OpenRouter key-usage
delta through Pi's native denial, both file attempts, question, three plan attempts and native controls: **$0.028858372**, including failures.
The first model-backed Pi attempt cost **$0.005748807**. Cursor's first account was
not entitled; its dashboard was unchanged at the displayed precision, with no
per-request receipt. Copilot's first session-start failure left its dashboard at
0/1,500 included AI credits and $0 incremental charges. After its protocol and
Product question cases, GitHub displayed 3/1,500 included credits with additional
usage disabled and a $0 budget. It still displayed 3 immediately after the native
denial and detached-command probes, then 5 after the two-turn plan batch. These
are delayed aggregate snapshots; exact allocation among calls is unverified.
After its denial probe, the selected Cursor account displayed 482.5K included
tokens across twelve qualification requests and zero on-demand tokens. The restart
rows showed 23.8K and 41.7K included tokens; denial used 24.4K. Displayed credit precision
and delayed accounting do not establish
a per-run zero receipt. These measurements are
partial, not a final all-provider total. No Daytona leases have been started.
Registry downloads, fake-model fixtures and metadata-only authenticated discovery
are separate from model inference.
The first Pi canonical `get-task-context` attempt used source `788105248a3ba594b30b5bcec3fa266d8a51d8d4`
and provider-pack digest `sha256:4de47c31a8131424495366741bd491ff5fa10723ab9a3918c07a3e42982dbe3c`.
It observed successful `get_task_context`, `get_task_history`, `list_documents`
and `read_document` calls, then hit its 120-second turn deadline without a
terminal receipt. This is a retained failed attempt, not semantic qualification.
Earlier launch/interpreter failures are retained separately. The initial Cursor
failure exposed a native wrapper gap: a typed entitlement error becomes an
ordinary message and normal completion. The runner does not infer authorization
from that message or fabricate a zero-cost usage receipt.
The selected paid Cursor account passed canonical `get-task-context` at source
`01959b8a602683f13706807983f02c3cba9d36a0`, pack
`sha256:f0b622e9151c1c0886e6220c48ea71993b65887baa88b1600b27462074748ddb`,
using exact `gpt-5.6-luna[context=272k,reasoning=medium,fast=false]`.
Its context/history reads and all four semantic checks passed in 29.291 seconds.
The account usage row attributes 56K tokens to this run, included in its existing
Pro+ subscription; incremental cash is zero. ACP supplied no token or USD receipt.
A conservative list-price bound of $0.07 is an estimate, not an invoice.
Copilot's first real completed attempt used source `92fcaf0c`, the same immutable
runnerd SHA-256 `986060810ba7377c6ddd64a5d89e322d0a434e1a9c80400e6d4acf5934bfc421`,
and exact `gpt-5.6-luna`. One context read and all four canonical semantic checks
passed. The original post-run package/provenance failure is retained; offline
scoring recovered the same artifact with zero additional provider calls. Its
24,258 input, 11,781 cached-input and 441 output tokens yield a $0.00326022 catalog
estimate. GitHub still displayed 0/1,500 included credits and additional billing
disabled with a $0 budget after the run. UI delay/rounding leaves the exact credit
delta unverified; this is not a provider USD receipt. Neither protocol case proves
Product E2E, restrictive permissions, restart recovery, or Daytona qualification.
Separate paid Product E2E evidence now records:
| Provider / local case | Exact source revision | Observed result |
| --- | --- | --- |
| Cursor / completion | `edf538e61e712dddb6b4d59045c3dcfd445686c7` | 6/6 assertions; committed finalization, one completion marker, cleanup passed |
| Cursor / file edit and validation | `fe132224c2b30a8d9ce7b46cea38b8760af233fc` | 7/7 assertions; independent final file bytes, visible downloadable workspace artifact, cleanup passed |
| Copilot / completion | `bcc9c638a25b91b84065f12633f083bd4f7a689f` | 6/6 assertions and cleanup passed; original accounting projection failed independently |
| Cursor / question continuation | `a7e01a0cec397dd5048f5d5b5825658dc6e91450` | UI answer Cobalt, continuation and 6/6 terminal assertions passed; two expected provider runs, no retry |
| Cursor / semantic plan approval | `a7e01a0cec397dd5048f5d5b5825658dc6e91450` | Displayed plan revision matched the confirmation target; accept/continue and 6/6 terminal assertions passed |
| Copilot / file edit and validation | `ee9536001fbe733b2386dd3379730a4e0be59488` | 7/7 assertions and cleanup passed; independent bytes validated; GitHub biller and unpriced receipt verified |
| Copilot / question continuation | `ee9536001fbe733b2386dd3379730a4e0be59488` | Question/answer and warm session reuse worked; 4/6 terminal assertions passed because the provider returned a literal placeholder instead of the required marker; failed attempt retained |
| Cursor / controller restart | `d35b83074a018537f5475568d7410e3b1d676789` | 6/6 assertions in 57.865 seconds; pending semantic question survived server restart, Cobalt answer continued to Done, cleanup passed; earlier failed attempts retained |
| Pi / completion | `dd78df1ef8b279c30c710c9b7a7f9fda22e321d6` | 6/6 assertions and cleanup passed in 32.668 seconds; one authenticated `paperclip_finish`, `turn.completed` and `run.terminal`; exclusive-key delta $0.000654767 |
| Copilot / semantic plan approval | `c06fc5fccc88f5816450434493451b9d2d339125` | 6/6 assertions in 45.625 seconds; accepted decision bound to displayed revision 1, exact marker and Done; both paused/completed receipts are GitHub/unpriced; cleanup passed |
| Copilot / controller restart | `19ca0f558` (final runtime remains `8aa867b64d5fc2fd62cff110bd000addf5dc54de`) | 6/6 assertions in 50.875 seconds; same pending interaction survived restart and same persisted provider session continued; exact marker once, Done and cleanup passed |
| Pi / file edit and validation | Runtime `dd78df1ef8b279c30c710c9b7a7f9fda22e321d6` | Failed at the 120-second active deadline after five rejected `paperclip_finish` calls; cleanup passed. The extension discarded validation details. Final bytes and completion arguments cannot be reconstructed from the retained projection. |
| Pi / file edit and validation, v3 | Pack source `7710736ca3924c655c5b0efd172cfd3c0173766a`; execution fix `06cf356a8bc94f709fcd15606fe05e17933fe3b2` | 7/7 assertions in 49.427 seconds, exact file bytes and cleanup passed. One rejected completion exposed the missing registered deliverable; Pi corrected it, registered the artifact and finished. Exclusive-key delta $0.003324737. |
| Pi / semantic question, v3 | Pack source `7710736ca3924c655c5b0efd172cfd3c0173766a` | 6/6 assertions in 158.388 seconds across two bounded turns; exact question/option IDs, Cobalt answer, warm continuation, final marker and cleanup passed. First waiting run has no usage receipt; exclusive-key delta covers both runs ($0.001413697). |
| Pi / semantic plan, v3 first attempt | Pack source `7710736ca3924c655c5b0efd172cfd3c0173766a` | Failed the native write-boundary UI assertion in 119.553 seconds. The saved two-step plan, matching revision and confirmation controls were visible, but all retained DOM snapshots showed the fallback Plan card. Rust lost the MCP display name/namespace used for placement. Cleanup passed; exclusive-key delta $0.003090504. Matcher remains unchanged. |
| Pi / semantic plan, v3 second attempt | Combined pack source `7ab463697037c9456a4ad2b83ea0e9c28b353f8a`; daemon source `e35b11db21b8da8527fa0b6c6f5fe186bdced111` | Displayed Plan placement and revision-bound acceptance passed. The resumed turn exceeded the unchanged 120-second active deadline; total case duration 260.246 seconds, cleanup passed. Pi reused native `call_0` for different tool executions, so the bridge correctly rejected conflicting identities. No terminal usage receipt; exclusive-key delta $0.002440082. |
| Pi / semantic plan, v4 first attempt | Frozen combined runtime `f556110d588a9de9fefe676a9a62bf09e98afb85`; fresh pack `sha256:4df7e9fa164929c7ae7d8e8711f0bf7d587d9fa6a246d0a8340f318f57168855` | Infrastructure failure during embedded PostgreSQL initialization in 6.679 seconds; no provider process or prompt. Disposable reproduction confirms host SysV semaphore exhaustion (16 free, 17 required). Cleanup passed, exclusive-key delta $0, and all original graders/deadlines remain unchanged. |
Pi's immediately preceding `hello05` consumed $0.000351509 and failed evidence
packaging after a confirmed 888-second host Maintenance Sleep. It reached
authenticated prompt acceptance but retained no semantic-tool or terminal event.
An incomplete Playwright archive could not be inspected (`unzip` exit 9), so the
existing scanner failed closed with its `secret_leak` classification; no credential
match was observed. That canonical failure remains unchanged. The deliberate
`hello06` repeat used the same runtime source, a task-owned idle-sleep hold, and
both wall-clock and monotonic outer deadlines. Canonical Pi cost remains unpriced;
the exclusive-key billing delta is separate evidence, not a fabricated receipt.
Separate real native probes used final immutable packs without a Product database:
| Provider / probe | Runtime source | Observed result |
| --- | --- | --- |
| Cursor / native AskQuestion | `25fb1b5b317e52a8ad50208d7681a1ee34bd939c` | Failed: zero native input events, normal terminal and cleanup; the provider reported that the tool was unavailable. This does not qualify the native question bridge. |
| Copilot / denied write | `8aa867b64d5fc2fd62cff110bd000addf5dc54de` | Passed: actual native permission request ID 0, original `reject_once`, forbidden marker absent in all 79 observations through process cleanup. |
| Copilot / detached command | `8aa867b64d5fc2fd62cff110bd000addf5dc54de` | Passed: actual `mode:async` and `detach:true`, original `allow_once`; command completion at 10.083 seconds and marker at 10.098 preceded `end_turn` at 10.675; five-second late-effect check and process cleanup passed. |
| Pi / denied write | `dd78df1ef8b279c30c710c9b7a7f9fda22e321d6` | Passed in 20.438 seconds: actual native permission ID 0 persisted before original reject_once reply, pipe delivery acknowledged, correlated tool failed, prompt settled end_turn, forbidden file absent and cleanup passed. |
| Cursor / denied shell write | `25fb1b5b317e52a8ad50208d7681a1ee34bd939c` | Original grader failed because permission omitted rawInput. Separate offline assessment passed from the preceding exact-command tool_call bound to the same toolCallId, original reject-once reply and 98 independent absent-file observations through terminal and cleanup; no repeat prompt. |
| Pi / active steering and queued follow-up, v3 | Pack source `7710736ca3924c655c5b0efd172cfd3c0173766a` | Passed in 21.342 seconds. Both active-session operations acknowledged while a persisted native write permission waited; original reject_once denied the tool, visible STEERED_CURRENT preceded QUEUED_NEXT, forbidden file stayed absent, end_turn settled, stale steer was rejected and cleanup passed. |
| Pi / active steering and queued follow-up, v4 | Frozen combined runtime `f556110d588a9de9fefe676a9a62bf09e98afb85`; verified pack `sha256:4df7e9fa164929c7ae7d8e8711f0bf7d587d9fa6a246d0a8340f318f57168855` | Passed in 20.255 seconds: normalized invocation identity matched the denied tool update; both controls acknowledged before the original denial; exact STEERED_CURRENT then QUEUED_NEXT, stale steering rejected, end_turn and cleanup passed, forbidden file absent. Settled exclusive-key delta $0.000546502; separate Pi estimate $0.000803936. |
| Pi / active steering and queued follow-up, v5 | Runtime `aec26ad83f1d082f0d0a5eaffbd615a9e2e26155`; pack `sha256:cf7d0998bbc2bed7b893c726c2b6455ba8a683d9cff7d92afedbff2d5900d500` | Passed in 19.199 seconds: one real normalized write invocation, original reject_once persisted before reply, both controls acknowledged before denial, exact STEERED_CURRENT then QUEUED_NEXT, stale steering rejected, end_turn, forbidden file absent and cleanup passed. Settled exclusive-key delta $0.000151739; its separate Pi estimate is $0.000611156. |
These narrow native probes do not replace durable Product interaction, restart,
or Daytona coverage. Their private wire evidence remains separate from sanitized
public summaries. A private durable retention manifest also records hashes for the
logs, screenshots, wire evidence and failed attempts; no credentials are published. The semantic Cursor question and plan cases above exercise
Paperclip tools, not the native `cursor/ask_question` or `cursor/create_plan` RPCs.
The failed Pi file case exposed two wrapper losses. Profile version 3 at
`06cf356a8bc94f709fcd15606fe05e17933fe3b2` preserves bounded, redacted MCP validation
errors and bounded Bash arguments/output; oversized values have an explicit
omission marker. Version 1 and 2 warm snapshots are rejected. Installed-wrapper
regressions pass. A fresh v3 pack then passed the paid file case: the model received
the actual missing-deliverable receipt error, registered its deliverable and
completed. The original v2 failure remains retained.
Canonical command exit codes and typed generic diff/media projection remain partial.
The Pi denial/file batch cost $0.010995043 by exclusive-key delta. Delayed billing
prevents exact per-call attribution, so the aggregate remains separate from Pi's
catalog estimates and its missing terminal receipt on the file case.
The Cursor file case proves the workspace artifact surface, not complete native
file/diff projection. The Copilot result incorrectly projected missing native cost
as USD zero and attributed its biller to OpenAI. The original result is retained;
the shared fix identifies GitHub, Cursor and OpenRouter correctly and keeps absent
candidate USD receipts unpriced. The separate Copilot file case verifies that fix.
The question case exposed a second ledger edge: zero normalized token counters
were treated as a reported cost despite no cost field. Ledger classification now
requires an explicit finite nonnegative cost; an explicit zero remains reported.
Protocol evals now fail their cost gate when spend is unknown, preserving completed
behavior and semantic evidence in a separate accounting-failure result. The
maintained campaign stops subsequent cells on unknown accounting and never turns
an unavailable receipt into a zero-dollar measurement.
Initial Product attempts exposed local PostgreSQL postinstall hydration and a
server candidate-admission gap before any model prompt. Both failed attempts are
retained. The package's own hydration repairs local installation; exact host
qualification now applies consistently at agent creation, runtime selection,
native input and process construction. Agent configuration cannot grant itself
qualification authority. The obsolete unconditional Pi executor rejection is
replaced by the same closed host authorization. Candidate active turns are bounded
to 120 seconds and automatic infrastructure retries remain disabled.
Pi's next local startup attempt timed out before any prompt, with zero exclusive
key usage delta. A credential-free reproduction isolated a 37.423-second immutable
copy of its 13,827-file, 234,019,683-byte distribution, after 5.168 seconds of
verification, against the 30-second session-open deadline. Bounded parallel copying
reduced that same copy to 5.851 seconds without changing a timeout. Ten tests cover
the eight-file / 32 MiB batch bounds, unchanged per-file integrity checks, stable
digest order, mutation rejection, and draining pending copies before cleanup.
The later `hello06` paid Product case above proves successful inference and
settlement after this startup fix. Its final screenshot shows one readable answer,
a visible Done status and a usable composer, with no duplicate response or error.
The maintained Product E2E `extended-harnesses` suite covers local and Daytona
completion, question/answer, semantic plan approval, pending-input restart and
file edit/validation. It has no automatic retries and does not enable candidates
outside exact operator-authorized provider/model pairs. The private Runner Eval
campaign is complementary: seven semantic protocol cases per provider. Neither
suite's membership is a qualification claim.
Before each paid batch, record source SHA, executable and closure/profile digests,
exact model, OS/architecture or Daytona image, selected cases, prior spend,
maximum batch spend and authoritative billing coverage. Stop before the shared
ceiling. Missing spend coverage blocks a run rather than treating unknown cost as
zero. Retain screenshots and wire evidence without credentials. Never convert a
provider to supported solely because a test suite or packaging check passed.
Verification commands and final results are recorded with the prerequisite and
provider PRs. The full handoff requires runner checks, token gates, recursive
typecheck, `pnpm test:run`, and `pnpm build`. Until that evidence is recorded, this
report is an implementation report rather than a PR-ready certification.
At foundation `5aeebb20c`, recursive typecheck, build, token gates, full Rust runner
checks, conformance/replay parity and API-authority checks passed. Approval
verification includes 18 real database integration tests, 15 projector cases,
87 transcript/UI cases, 82 route/websocket cases and eight provider receipt cases.
The local full root test attempt initially failed because embedded Postgres's
install-time library links were missing. Its official package postinstall restored
them; all 38 affected suites (743 tests) then passed. The workspace streaming stress test initially exceeded macOS path limits
(`ENAMETOOLONG`); the later portable fixture correction is recorded below. The full runner TypeScript repeat passed 2,164 tests in 156 files, with ten
skipped tests. The full UI and CLI suites passed 6,772 and 502 tests. Remaining
source workspace checks passed 2,882 tests; two macOS path-alias fixture failures
were corrected with an explicit injection assertion (all 89 sandbox tests pass),
and a database timeout passed in an isolated repeat. Failed attempts and the
latest CI state remain recorded in the PR. The decision-media review fix passes
87 focused tests, UI typecheck, token gates and the UI build.
At execution source `f063fbf2b`, recursive typecheck, full build, token gates,
13 accounting tests, 10 immutable-distribution tests and seven image-contract tests
pass. Both full UI/CLI shard partitions pass (6,848 UI and 502 CLI tests).
The remaining local test partitions run sequentially after paid Product cells to
avoid the machine's observed PostgreSQL semaphore exhaustion. Earlier failures,
including the unchanged macOS path-length stress case, remain retained. Foundation
CI at this source passed typecheck, build, Rust, both runner test lanes, all twelve
general server shards and all eight browser shards. Six other jobs received a
coordinated runner-shutdown signal; their cancellation is not a passing result.
Greptile reviewed this source at 5/5. Later documentation/build-pin updates still
require their own final check status.
Initial macOS ARM64 candidate packs were independently built and launched through
the generic installation registry: Cursor source `1055c13f8`, Copilot `5d8829add`,
Pi `f58cfa1cb`. Review fixes that change execution bytes require fresh packs and
launch proofs; the latest source SHA, manifest/profile/closure digests and
sanitized wire evidence are retained in each provider PR. Earlier proofs retain
their original source identity. These probes send no model prompt.
Final native Linux packaging probes also run with no network, no credentials and
no model prompt. Cursor source `25fb1b5b317e52a8ad50208d7681a1ee34bd939c` initialized
from image `sha256:5fa7951d1d6dd99305555fe00a5baf2bf5b834d16f021053737301975b93986f`;
its native EOF does not settle within five seconds, so explicit process-group
cleanup is required and verified. Copilot source
`8aa867b64d5fc2fd62cff110bd000addf5dc54de` initialized as version 1.0.88 from image
`sha256:5457769683fd310223d3b0d4f1ed9a6cf341bdb16514746b3aaeabca2e888fee` and exited
zero on EOF. Its metadata-only fixture received zero requests. The provider PRs
retain complete pack/executable digests and sanitized initialization responses.
Pi v3 source `06cf356a8bc94f709fcd15606fe05e17933fe3b2` also initializes and
exits zero from image `sha256:c5fa7976bba92a186a2f70dc8b8ddc58ab86606b80a819c89bf550d2d057c832`,
with profile digest `sha256:72cb225288376f733b9ed3afa5e13565eb4152f0de509bc1181382fa44bee472`.
The earlier Pi v2 proof remains historical.
These are packaging proofs, not paid Daytona or model-availability evidence.
At `e8dee462e2fd09cf858be0a02368805346ec4f3b`, versioned profile contract tests
pass 36 cases and full runner TypeScript checking passes. Nine additional source
workspace suites pass 2,757 tests with 19 skips. An isolated repeat of the unchanged
HTTP redaction suite passes all 59 tests after its earlier concurrent-run timeout.
The feedback-route mock fix retains both company-boundary assertions and passes
two independent fresh-environment repeats (4/4 each). Current source and any
remaining partition failures are recorded in the final prerequisite PR evidence.
The portable real-Git stress fixture passes on macOS in 234.43 seconds, retaining
40,000 files, four independently checked 34,988,890-byte filename lanes, real
staging/deletion and cleanup. Its timeout is 300 seconds on macOS and remains
180 seconds elsewhere. Both earlier failures are retained. All seven remaining
serialized root partitions pass; database-project and previously skipped-file
remediation later passed all ten partitions, including 1,042 chat integration tests.
The combined provider stack passes 616 ACP/profile/backend tests with eight
skips, 34 packaging and actual-wrapper tests, and a TypeScript build. The
Copilot registry regression now verifies dispatch to separate native factories;
the pack test no longer assumes another implemented provider is absent.
The complete database project passes 161 tests in 44 files after serialization;
all ten remediation partitions completed without failure, including the full
1,042-test chat integration lane.
The first Pi plan attempt exposed a shared Rust display-mapping gap: prefixed MCP
tool names were retained as builtin names, preventing the saved Plan from anchoring
to its write event. All four retained browser snapshots showed the fallback Plan
card inside a settled turn, so the existing Product matcher remains unchanged.
Foundation `5aa02662b34fc70f5fe9ffd5ac8f7ae81b5fe3bd` parses both ACP MCP name forms,
preserves bounded/redacted namespace and name, and leaves semantic authorization,
operation and read-only classification unchanged. The original Rust regression
fails before the fix; 55 Rust, 28 TypeScript provider and 158 UI tests pass afterward.
The UI regression distinguishes the proper write-boundary card from the original
fallback. A rebuilt daemon is required for the retained paid retry.
The rebuilt daemon passed the Plan placement and revision-acceptance boundary in
the second paid attempt. Its continuation exposed a separate Pi identity defect:
the owned extension reused native `call_0` as the MCP request ID for different
executions. A rejected finish was followed by a corrected finish and context read,
both correctly rejected by shared duplicate-call protection. Further paid cases
stopped. Pi v4 at `f556110d588a9de9fefe676a9a62bf09e98afb85` binds live identities
to a private launch namespace and a lifetime model-iteration ordinal. Exact
request retries retain their identity; conflicting or duplicate native invocations
within one iteration fail closed. History uses a separate display-only identity.
Bridge idempotence and durable call tombstones stay intact. Raw native IDs remain
private-wire provenance, with the UI omission recorded above.
The corrective-call regression failed before the fix and passes against the real
authenticated loopback tool bridge afterward. The pinned native SDK test covers
six iterations across two warm prompts and four executions that reuse `call_0`;
its native turn index resets, while the owned ordinal remains monotonic. This
check uses an in-memory model stream and no inference. Independent review and
profile-digest verification pass. At that v4 checkpoint, the profile digest was:
`sha256:2324d9b47650c12b16f8e2c44dc33637d52f1b22ba8e914623eac4049e7e1991`.
Earlier v1-v3 snapshots could not establish that runtime identity. The extended
Runner Eval configuration then pinned v4 and rejected those historical identities;
all 132 eval tests and seven-cell Pi campaign validation passed without provider
calls at that checkpoint. The later v5 identity is recorded below.
Recursive typecheck and the full build both pass at shared repair source
`5aa02662b34fc70f5fe9ffd5ac8f7ae81b5fe3bd` and again at version-admission source
`1545c7692`. The latter adds closed profile-version-4 admission, with 22 contract
tests and full runner TypeScript checks passing. Exact profile digest matching
remains mandatory. Recursive typecheck and the full repository build also pass
on the frozen combined provider source `f556110d588a9de9fefe676a9a62bf09e98afb85`,
including Pi v4. The combined macOS ARM64 provider pack
at `7ab463697037c9456a4ad2b83ea0e9c28b353f8a` independently verifies all three
complete candidate trees (446 Cursor files, one Copilot executable and 13,827 Pi
files), manifest digest
`sha256:0800f10114e3d8e2e981ea27f0ab47f1da9349dbcc22bf55a76ebbc4af00601d`.
All three initialize through the verified registry without credentials or model
prompts. The combined Linux image at source
`e35b11db21b8da8527fa0b6c6f5fe186bdced111` also passes all three initialization
probes under network-none and a read-only root. Image digest:
`sha256:17c228d3b9744d6bf375ead57918c755d0cc44fc3db44d238cc898317cea25ce`;
pack digest: `sha256:4e0ddb39a34e7f55d7520a8a2d9074009589f3f95f4c10bb837e54ec58d70ba1`.
These combined proofs contain Pi v3 and retain that historical identity when the
invocation-identity repair produces a new profile. They are not paid Daytona proof.
All candidates remain pending; verification and packaging do not grant support.
The fresh v4 macOS ARM64 pack at the frozen combined source has digest
`sha256:4df7e9fa164929c7ae7d8e8711f0bf7d587d9fa6a246d0a8340f318f57168855`.
All 13,827 Pi runtime files independently match the pinned closure. Native
initialization under denied networking exits cleanly; the complete Runner path
returns the expected typed missing-key rejection without inference. The rebuilt
daemon has SHA-256 `373848d2d7287b2c47b2cee422cb7bd25073a594a620a9ad574f2d3d51cd1670`.
The next canonical paid plan attempt stopped in PostgreSQL initialization before
provider startup. Its unchanged billing and exact disposable initdb failure are
retained. The diagnostic created no server, removed its temporary directory, and
left IPC totals unchanged. Unrelated database processes and IPC objects were not
modified. Canonical Product E2E does not currently admit an external database
substitute; introducing one requires its own ownership, isolation and cleanup
proof rather than bypassing the existing guard.
The v4 native probe above does not need a database and passed independently of
the host PostgreSQL blocker. A read-only IPC attribution audit found no semaphore
set with sufficient task provenance for safe cleanup; no unrelated set was
removed. The paid Product and Daytona qualification gaps remain open.
Dependency review subsequently identified Pi's pinned `undici@8.9.0` in
[GHSA-3wwx-pv8p-q78v](https://github.com/advisories/GHSA-3wwx-pv8p-q78v),
indexed in GitHub's advisory database on September 28. Pi's owned Node 24.19.0
also embeds affected Undici 7.29.0. The repair requires both the exact npm override
and the provider-local patched Node distribution; it cannot be represented by a
waived check or a claim that updating npm replaces Node's builtin implementation.
Shared contract source `e6163e16a` admits closed profile version 5, with 23 contract
cases and full runner TypeScript checking passing; unknown versions remain
rejected and exact command digests remain mandatory.
The combined v4 Linux image at frozen source `f556110d588a9de9fefe676a9a62bf09e98afb85`
was built and all three providers passed credential-free initialization under
network-none with a read-only root. Image digest:
`sha256:4c4e2fb7eb8ef3681b14c3e21dd28eace5ef920d6d08d572fb9c9c7e9a5f290a`;
pack digest: `sha256:8f263ed5c309b47cdc74a37d8217bb49e57129fca393498f54f6dd461657a9b9`.
The daemon SHA-256 is `5914ed1ad0235aaee9af8731af2cbf28d7b16b0fda231a13963f1d86c3220daa`.
Cursor still needs explicit cleanup after EOF; Copilot and Pi exit zero. No model
prompt or credential was supplied. This remains a historical packaging proof
when the dependency repair changes Pi's profile and executable closure.
Recursive `pnpm -r typecheck` and `pnpm build` also pass at shared version-5
admission source `e6163e16a`; no database or model request is used by those checks.
Pi v5 repair source `aec26ad83f1d082f0d0a5eaffbd615a9e2e26155` pins private
Node 24.21.0 (bundled Undici 7.29.1) and nested npm Undici 8.10.2. Because npm
retains the vulnerable shrinkwrapped package despite overrides, materialization
verifies the exact published old tuple, replaces only that package from a bounded,
integrity-checked fixed archive, and then verifies the complete trusted closure.
The upstream Pi package and shrinkwrap bytes remain unchanged; the exception is
explicit and closed. Profile digest:
`sha256:020d96ccbd3c45c3f62680814776394ed5a56d9572a1a4dccda56a74d16c7803`.
Old versions 1–4 are rejected. Forty-three focused TypeScript tests, 19 package
checks, eight materializer checks and 13 Rust tests pass. Independent review verified all 13,827 installed
files, both patched dependency copies, eight materializer and twelve profile/
recovery cases. Recursive typecheck and the full build pass at the same source.
Shared Docker pin source `3a5637732` uses the official Linux x64
`node:24.21.0-bookworm@sha256:5a750d3be5e5c80275f8c9a5367c3aed99c2875656590c8d0701c7ee687f5f0a`
for the outer provider pack. A network-disabled launch independently reports
Node 24.21.0 and Undici 7.29.1, matching the
[official Node release](https://nodejs.org/en/blog/release/v24.21.0). All seven
image-contract tests pass. The first local test invocation failed because the
root has no `tsx` executable; the retained retry used the maintained Vitest
configuration. No provider call was made by either command. Pack authority
includes the outer Node bytes and is bound into the persisted launch profile;
changes invalidate incompatible warm recovery independently of Pi's profile bump.
The fresh v5 macOS ARM64 combined pack has manifest digest
`sha256:cf7d0998bbc2bed7b893c726c2b6455ba8a683d9cff7d92afedbff2d5900d500`
and source `aec26ad83f1d082f0d0a5eaffbd615a9e2e26155`. Independent initialization
through the verified registry passes for Cursor, Copilot and Pi with no provider
credentials or prompts. The outer interpreter reports Node 24.21.0 and Undici
7.29.1. Cursor still requires explicit process-group cleanup after EOF; Copilot
and Pi exit zero. All owned processes and private supervisor directories are
cleaned. Cursor/Copilot profile and closure identities match their prior pack.
The proof index has SHA-256
`a07394e3215eb0a153c4a9443762a5d84ddfe0e70b92e1d47ae8d574239e104c`.
The private Runner Eval campaign now pins v5 at eval source
`18f06b9391838d637c1588f372c7f1397ec6f9a7`. All 133 Python tests and the seven-cell
Pi validation pass without provider calls. Explicit tests reject each historical
v1–v4 version, digest and paired identity. Cases, model, scoring, budgets and
deadlines remain unchanged.
Current Linux image identities and exact-head CI/review status are recorded in
the [Pi PR verification](https://github.com/paperclipai/paperclip/pull/14436) and
[foundation PR verification](https://github.com/paperclipai/paperclip/pull/14430).
Their immutable source and dependency identities remain distinct from the
historical packaging proofs above. Image initialization does not certify paid
Daytona execution. The unresolved local database and remote account-verification
blockers prevent support promotion.
+32 -1
View File
@@ -99,6 +99,34 @@ Sending `environmentId: null` tests a change back to the instance default.
## Runtime isolation
Provider authentication failures, including `acpx_auth_required`, adapter login
requirements, and expired/invalidated refresh tokens, create an AI connection
card as the failed run is finalized. The card names the provider and uses the
same inline connection/reconnect controls as missing-account setup. Pending
cards are deduplicated. Creating a repair card persists a blocked run classification
that suppresses immediate and periodic generic retries until the responsible user
repairs the connection. Unsupported providers and failures that could not create
a card retain their existing recovery path. Tool permission errors and provider quota failures do not
request model authentication.
An attributed managed credential is marked as needing reauthorization only if
its stored generation still matches the failed run. Late failures cannot
invalidate a refreshed or reconnected credential. Repair preserves the selected
account and its permissions when the same sign-in method is selected. The card
also lets the user switch between API key and subscription authentication for
providers that support both. Switching creates a separate account, then selects
it as the user's provider default or validates and updates the agent's explicit
account binding. The original account is retained. Accepting the card resumes with a fresh session
through the existing durable continuation delivery.
For compatible legacy agents, the card offers the responsible person's provider
connection without changing authentication automatically. After connecting,
**Use connection and continue** checks agent-update permissions and validates in
the agent's execution environment before committing the agent binding, connection
install, audit, and card completion in one transaction. A failed validation or
completion leaves the request pending and the old agent configuration and access
intact. Unsupported harness/provider routes are not guessed.
Codex ACP terminal failures with category `limit` and explicit usage-exhaustion
wording enter provider-quota recovery. A supported reset clock uses the existing
Codex parser; when none is available, recovery uses its existing quota backoff.
@@ -324,7 +352,10 @@ Native Codex and ACPX/Claude provider selections follow the same compatibility r
Hiring may succeed before that personal account exists or while it needs repair,
including hires awaiting board approval. The first assigned task then shows an AI
connection card. First-time setup presents the provider's subscription/API controls
inside the task. Connecting installs access for that agent and resumes the pending
inside the task. The inline form omits the connection name field and names new
accounts from the user's display name, provider, and selected authentication
method (for example, `dotta's Claude API account`). Reconnecting preserves the
existing account name. Connecting installs access for that agent and resumes the pending
work automatically. Explicit incompatible bindings and shared-account permission
denials still fail; hiring never expands a restricted shared account's audience.
+387
View File
@@ -0,0 +1,387 @@
# Browser Use Cloud
This connection delegates website work to Browser Use's hosted agent through its
v4 REST API. The task's Browser tab embeds the provider's interactive live viewer.
The connection's API key stays in Paperclip's vault and is never exported to the
agent, local browser tools, or the board UI.
## Connect and configure
Choose **Browser Use Cloud** (`browser-use-cloud`) in Apps, select agent access, enter a project API key from
[Browser Use settings](https://cloud.browser-use.com/settings), and review
Permissions. No OAuth registration, callback, Paperclip ID, or external connection
broker is required. Cloud and self-hosted instances use the same server-side path.
The instance needs outbound HTTPS to `api.browser-use.com`; browsers need access
to `live.browser-use.com`. An operator-supplied CSP must permit that viewer origin
in `frame-src`.
The setup probe reads profiles without creating paid work. Existing personal and
organization grants, audience restrictions, installations, policies, approvals,
and invocation audit remain authoritative. Browser settings on Permissions are
per credential: the owner (or a shared connection manager) can set a maximum cost per run and explicitly allow
existing saved profiles. No selected profiles means fresh browsers. Profiles can
contain authenticated website sessions, so selecting one grants meaningful access.
Paperclip does not create or import profiles.
## Reviewed actions
| Action | Risk | Scope and behavior |
| --- | --- | --- |
| `browser_start` | Destructive-capable | Delegate a task; optional allowed profile and lower cost cap. |
| `browser_status` | Read | Current run status, sanitized result, progress and recorded run cost. |
| `browser_continue` | Destructive-capable | Start a new turn in an idle owned conversation. Busy sessions reject it. |
| `browser_cancel` | Destructive-capable | Cancel hosted agent work; keep the browser for the idle window. |
| `browser_end` | Destructive-capable | Cancel work and stop every browser registered to the conversation. |
| `browser_sessions` | Read | List conversations for the current task, agent and selected grant only. |
| `browser_profiles` | Read | List existing profiles explicitly allowed for the selected grant. |
Start, continue and control are conservative because a natural-language browser
task can change external systems. They use the normal Allowed / Ask first / Off
policy, rate limits and signed approval path. A new action or schema change goes
through the existing catalog review/quarantine configuration. Arbitrary HTTP
endpoints, CDP, provider run IDs and browser IDs are not agent inputs.
Hosted execution requires a bound task and agent run. The Apps Test surface can
use `browser_profiles`; paid starts there are rejected with task guidance. For a
hosted test, use a real task so ownership, budgets and cleanup are exercised.
## Runtime and lifecycle
The gateway exposes reviewed REST tools to existing native and CLI tool bridges.
Authorized task/run access contributes the `browser-use-cloud` skill, bundled
with the connector in `server/src/services/connectors/browser-use-cloud/skill.ts`.
It is not in the universal `skills/` directory and is not offered to every agent.
The existing connector assignment resolver requires accessible connection tools
and a current credential grant. No assignment, disabled tools/connections, or
revoked access means no skill. The run-scoped bundle exposes its short description
for discovery and contains the detailed `SKILL.md` for using this connection.
Adapters that cannot isolate skill files receive the same authorized instructions
in their run context. Nothing is added to an agent's saved skill preferences.
Retired `paperclipai/paperclip/browser-use` selections are stripped from runtime
overlays; unrelated browser skills keep their own names and behavior.
The idempotent migration updates pre-release app/connection and financial keys
to `browser-use-cloud` without replacing credentials, grants, or browser records.
The parent agent starts
a hosted run, polls its status and stays alive until it finishes. Approved starts
can execute after the requesting turn ends: a ten-minute handoff window lets the
resumed agent attach by reading status. An expired handoff stops unfinished work.
Four company-scoped tables separate grant settings, conversation ownership, hosted
runs and browser instances. Each hosted run records its invocation, parent run,
event cursor and cost checkpoint. A leased reconciler polls minimal status,
drains bounded event pages, discovers browser instances by conversation, and
checks each instance's authoritative conversation association. Restarting the
server resumes reconciliation. A lost paid-create response becomes an explicit
unknown outcome and is never submitted again automatically; the operator must
inspect Browser Use before starting replacement work.
Each new ready browser session opens once per task/account in the right panel. A task
card and the panel launcher reopen it. Switching tabs preserves the iframe.
Closing the tab hides it; **Close browser** in the footer menu stops the provider browser. **Stop browsing**
requests hosted-run cancellation. Visible viewers keep completed browsers open;
leaving the viewer starts a ten-minute idle window.
**Keep browser open** in the footer menu renews that window, bounded by provider expiry.
Only the final five minutes show a seconds countdown and a direct **Keep browsing**
action. **Reconnect view** reloads the live display without restarting browser work.
The footer has no cost display. **Browser size** defaults to **Fit to pane**.
A ResizeObserver measures the iframe's actual CSS size and, after 250 ms without
another meaningful change, applies the remote viewport through CDP. Only the
newest pending dimensions are retained; requests are serialized. The iframe is
not reloaded. Fixed presets disable automatic fitting until Fit to pane is chosen.
Only the visible panel in a visible document resizes. One viewer holds a 20-second
in-memory lease, renewed every eight seconds and released when hidden/unmounted.
Other viewers show **Size follows another viewer** and can explicitly choose
**Fit to this pane instead**. Automatic updates never take over a live lease or
override a newer fixed selection. A resize failure pauses fitting until a size
is explicitly selected again. Socket/server restart resets the mode to fitting.
Browser Use's current viewer has a 30-pixel tab strip and 40-pixel navigation bar.
The Paperclip token `--browser-use-viewer-chrome-height` records that measured
70-pixel allowance; Paperclip's footer is excluded by observing the iframe itself.
The provider's published viewer bundle exposes no sizing postMessage contract.
This keeps navigation available but requires rechecking the allowance if the
provider changes its chrome; it is not a guaranteed cross-origin measurement.
Remote dimensions are bounded to 6144 × 3456, scaling proportionally at the limits.
One-pixel jitter is ignored. These dimensions are browser settings, not UI tokens.
The same menu also offers Phone (390 × 844), Tablet (768 × 1024), Laptop (1280 × 800), Desktop (1440 × 900),
and **Browser default**. Presets fix the remote page viewport; changing panel size
scales that viewport. Failed resizes retain the previous selection and show an
inline error. Follow-up work uses a
normal task message and `browser_continue`, with the same conversation ID.
Cancellation/failure of the owning run, task cancellation/reassignment,
agent pause/termination, task pause, budget blocks, grant/access removal and idle
expiry request cleanup. Completing a task stops unfinished hosted work; a completed
hosted run retains its idle grace period. Stop is not considered complete until the provider confirms
it. Terminal event pages and accounting finish before the session closes. Removing
the app disables its access first, then confirms shutdown before deleting secrets.
If shutdown is pending, removal returns a retryable conflict; only cleanup retains
credential access, and retrying removal finishes secret deletion.
## Credential and budget boundaries
The board list and stored tab state contain internal IDs and safe metadata only.
An authenticated, active, non-viewer company member with access to the exact grant
can request a viewer URL through a `no-store` endpoint. Agent keys cannot use that
endpoint. Only the exact HTTPS `live.browser-use.com` origin is accepted. The URL
lives in component memory, uses `no-referrer`, and is revalidated while hidden.
Provider events/results are sanitized before persistence or gateway output; live,
CDP, recording and other URL fields are excluded. Do not put a viewer link in a
comment, log, tool response or artifact.
Each dispatch uses the minimum of the requested cap, configured grant cap and
applicable remaining hard company/agent/project budgets. Unconfigured limits do
not invent a cap. Provider run `totalCostUsd` is recorded through a locked,
idempotent cumulative checkpoint and feeds normal Paperclip budget enforcement.
Each positive accounting delta also creates a linked, task-scoped `finance_events`
row in the same transaction as its `cost_events` row. Reconciliation retries do
not duplicate either entry. Costs remain available for reporting without appearing
in the browser footer. These are recorded run charges, not a live invoice. Browser hosting and
proxy fields are not added separately because their relationship to run totals
has not been verified; avoid double counting. Concurrent runs are not a shared
provider-side reservation. Recording is explicitly off. Artifact/file import,
recordings and arbitrary provider queue interruption are not part of this connection.
CDP is limited to board-authorized viewport presets and bounded pane dimensions; arbitrary commands and
credential-bearing CDP addresses are never exposed to the UI or agent tools.
## Validation and live proof
Deterministic tests use the real vault, connection setup, policy/approval gateway,
Postgres tables, reconciler and routes with a controlled v4 provider fixture.
They cover paid-create idempotency/uncertainty, ownership, delayed browser readiness,
viewer authorization and no-store headers, durable event pagination, cost dedup,
approval handoff, revoked access and shutdown before secret removal. Panel tests
check iframe preservation and tab closure. Storybook provides the production panel
with explicitly simulated interactive, idle, closed, failed and revoked states.
For live acceptance, connect a dedicated test credential in the isolated instance:
1. Confirm setup/discovery performs no paid start and the agent receives the tools.
2. Delegate a harmless public-page task with a small cap; inspect the actual viewer
while the run is active and type/click in it.
3. Switch tabs, close/reopen, stop the hosted agent, keep an idle browser open, and
continue through a task message. Verify a second agent or different grant cannot
use the conversation or fetch its viewer.
4. Exercise Ask first, denied access, restart during work, provider rate limiting,
idle expiry and connection removal. Confirm provider shutdown and one cost entry
per cumulative billed increment without any viewer credential in audit.
The live test-drive used a fresh
database with one company, one agent and one task; it contains no cloned inbox or
task history. The connection was configured through the normal setup wizard and
given a $1 per-run limit with fresh browsers. Browser Use completed a real v4 run
that explored `paperclip.ing`, visited About and returned to the homepage. The
recorded run cost was $0.01. The completed browser was visible in Paperclip's
Browser tab, and manual address-bar navigation and Back worked inside the iframe.
Keep open renewed the idle countdown. The initial blank viewer recovered after a
page reload; provider results alone were not treated as proof of the viewer.
An earlier run exposed a runtime transport filter that excluded
Browser Use REST tools even though the skill was installed. Both runtime tool
delivery and the native assignment snapshot now include this reviewed REST
connection, with a regression test covering all seven tools. Live acceptance of
revocation, approvals, rate limiting and restart recovery remains outstanding;
those cases currently have deterministic fixture coverage.
## Verification record — 2026-09-29
- Footer redesign: 59 Storybook states across panel, footer, task activity and
credential settings. UI typecheck/build, Storybook build, token gates and 34
existing panel/state tests pass. Browser walkthrough verified the 5:01/5:00
countdown boundary, narrow layout, timer extension, preserved page state after
stopping work, reconnect/close controls and saving settings with offline fixtures.
- Fresh live test: setup/discovery, actual hosted task, completed result, $0.01
accounting, interactive iframe navigation, and Keep open verified. The runtime
delivery regression suite passes 14 tests; server TypeScript check passes.
- `pnpm -r typecheck` and `pnpm build`: passed. The final lifecycle changes also
passed server typecheck/build and UI typecheck.
- Browser Use backend: 18 passing tests; task-panel/state/brand tests: 43 passing;
shared app-definition tests: 24 passing; gallery route assertion: passing.
Existing gateway/connection/removal regression suites: 429 passing tests.
- `pnpm check:token-gates`, brand asset validation, module boundary validation
and `git diff --check`: passed.
- Browser inspection of the production panel with a simulated provider: typed
input survived Properties/Browser tab switches; Stop agent, Keep open and End
session behaved correctly; controls remained usable at a 390-pixel viewport.
- The full `pnpm test:run` attempt was stopped after known failures. Its gallery
count assertion was updated for the added app and passed on retry. Its unrelated
Slack callback-ordering test passed in isolation. There is no clean full-suite
result from this worktree.
- The optional `pnpm check:tokens` content check finds existing names in unrelated
Storybook fixtures. None of the new Browser Use files introduced those matches.
## Provider research
Reviewed on 2026-09-29:
- [v4 overview](https://docs.browser-use.com/cloud/api-v4-overview): current REST
base URL and API key header; minimal status polling and terminal summary.
- [v4 OpenAPI](https://docs.browser-use.com/cloud/openapi/v4.json): run/session/browser
identities, continuation by session, events, cancellation, profiles and cost cap.
- [Live preview and recording](https://docs.browser-use.com/cloud/browser/live-preview):
browser-ready event, interactive iframe, credential treatment and recording flags.
- Official artwork: [Browser Use logo](https://browser-use.com/logo-primary.svg),
copied as a sanitized local SVG; provenance is in the app brand manifest.
The connector uses v4 directly. It does not mix v3 MCP task/session methods with
v4 runs, and it does not substitute Paperclip's existing execution agent adapter.
## Viewer size and Storybook coverage
The iframe fills the available panel height and width without a Paperclip-imposed
aspect ratio. The provider streams an existing remote viewport, so scaling its
viewer does not make the website switch responsive breakpoints.
The published v4 `RunBrowserSettings` schema exposes `screenWidth` (320–6144) and
`screenHeight` (320–3456) at browser creation. Explicit settings on a follow-up
apply only when provisioning a new browser. The schema does not list
`allowResizing`, although a live test on 2026-09-29 established that sending
`browserSettings: { allowResizing: true }` when creating an agent run enables
runtime viewport resizing. Treat this as verified provider behavior with an
OpenAPI documentation gap, not a documented contract. The standalone browser
creation API documents the same opt-in and notes its stealth tradeoff.
Live test results, using the same browser and page throughout each test:
| Browser configuration | Requested viewport | Measured page viewport | Result |
| --- | --- | --- | --- |
| Agent run, default resizing policy | 480 × 800, then 900 × 700 | Stayed 1280 × 588 | CDP commands succeeded but had no effect; native window resizing also had no effect |
| Standalone browser, `allowResizing: true` | 480 × 800, then 900 × 700 | 480 × 800, then 900 × 700 | Responsive layout and screenshot dimensions changed |
| Agent run, `browserSettings.allowResizing: true` | 480 × 800, then 900 × 700 | 480 × 800, then 900 × 700 | Both changes occurred while run status was `running`; live viewer followed the change |
The successful command was server-side CDP `Emulation.setDeviceMetricsOverride`
with `width`, `height`, `screenWidth`, `screenHeight`, `deviceScaleFactor: 1`, and
`mobile: false`. Verification checked `window.innerWidth/innerHeight`, layout
metrics, PNG screenshot dimensions, and a 600-pixel CSS media query. The media
query changed with the page width, proving reflow rather than viewer scaling.
The opted-in agent's 900 × 700 viewport persisted on a check 41 seconds later,
still during the run; the run subsequently completed. All three disposable cloud
browsers were confirmed stopped after testing.
The initial configured screen dimensions were not the actual page viewport;
browser chrome and device scaling affected the latter.
REST `PATCH /api/v4/browsers/{id}` still rejected both a dimensions-only body and
`action: "resize"` with HTTP 422. That endpoint documents only `action: "stop"`.
The live-preview docs do not specify a parent-frame resize message. Browser Use
does document [CDP attachment to a live v4 agent browser](https://docs.browser-use.com/cloud/agent/network-capture).
Automatic panel-size synchronization is implemented through
`POST /api/issues/:issueId/browsers/:browserId/viewport`. Fit requests carry bounded
width/height and a viewer UUID; fixed choices carry a validated preset. All use
the same company, task, grant and board checks as the viewer. New runs request
the resizing opt-in. A bounded CDP session verifies actual viewport dimensions,
reapplies the selection to new page targets, and serializes updates. Browser
default clears the override while retaining the selected mode. Mode and ownership
are shared by viewers on the same server process and reset on server/CDP restart.
Unsupported older browsers retain their existing size and show an actionable error.
The iframe stays mounted across resizing. Only the visible pane observes and
renews ownership; it debounces changes for 250 ms and coalesces pending updates.
A 20-second lease prevents concurrent viewers from fighting over dimensions;
the menu allows explicit takeover. See the interaction details above for viewer
chrome calibration and the fixed-size escape hatch. Resizing during an agent
action can invalidate screenshot coordinates; the live test covered a running
agent observing the page, not concurrent click safety.
Automatic fitting verification on 2026-09-29:
- 60 focused tests passed across viewport scheduling, CDP ownership and validation,
connection routes, Browser Use service behavior and task-panel state. UI/server
typechecks, UI/Storybook builds and token gates passed.
- In the embedded Storybook, the page reflowed at 358 × 527 and 798 × 327 while
retaining typed input. Phone stayed 390 × 844 when the pane changed. Two viewers
correctly handed ownership over through the menu.
- The fresh test-drive browser received a 478 × 754 Fit request and the backend verified
those actual page dimensions. A subsequent real pane maximize/restore changed
the remote viewport from 624 × 520 to 1040 × 520 and back through the automatic
observer path. Its hosted viewer stayed at `about:blank` in the
embedded browser, including after clearing the viewport override and reconnecting.
Thus live remote sizing is verified, but a successful visual end-to-end fit in
that test-drive remains unverified. The Storybook uses an offline fixture.
Storybook now groups the production panel and its lifecycle states, isolated footer
and expiry boundaries, task activity/reopening, and credential settings with
loading/error/access states. Offline interactions exercise keep-open, stop/close,
reconnect, profile selection, saving and changing/resetting viewport presets.
The offline viewer simulates reflow at the chosen preset and scales it to fit its
panel. It is not evidence of provider viewport resizing; that was tested separately.
## Follow-up opening and viewer presence
Browser sessions appear as compact entries in the chronological task feed at
their creation time, between the request and reply. Status updates stay on the
same session entry; **Open browser** opens its side-panel tab and **View session**
shows a closed session. There is no browser list in the task header. Older
sessions outside the loaded comment window appear when that history is loaded.
Storybook's **Task activity / Inline History** demonstrates successive sessions.
Each new live session opens its Browser tab once per task and browser tab, including
follow-up requests after older browsers have closed. Repeated polling does not
steal focus back after the user switches away. Open requests are acknowledged only
after the new tab selection has been committed and persisted.
A visible, selected Browser tab sends an authorized presence POST every 30 seconds.
The server renews the existing idle window (at most once a minute), respecting
provider expiry and the reconciler's shutdown lease. Hidden tabs and collapsed
panes stop renewing; closing a viewer does not immediately destroy its browser.
The ten-minute idle window applies after the last visible presence. Explicit
Close browser, revoked access, pause/budget gates and provider expiry still win.
No paid run is started by presence. Each renewal is activity-logged.
The iframe shows a loading state and reconnects once after 15 seconds if its
document never loads, then shows a recoverable error if the retry also times out.
Inactive browser tabs do not consume that load window. A viewer that did not
load cannot own automatic sizing. When a task has several sessions, the tabs use
the same browser numbers as task activity. A closed tab offers **Open active
browser** if another live session exists, without starting another paid run.
On 2026-09-29 the same provider iframe rendered successfully in Chrome, while
Codex's embedded browser left both the provider and a plain example.com iframe
at about:blank. After reconnecting to the updated test-drive, the embedded viewer
also loaded and displayed paperclip.ing. The new-session selection and automatic
presence renewal were verified there with the same hosted session; no replacement
paid run was needed.
Follow-up verification: 54 focused tests passed (15 service/route lifecycle tests
and 39 UI tests), plus UI/server typechecks, UI build and token gates. The original
hosted session stayed idle and available after its original deadline; viewing
renewed the deadline automatically. Closing/reopening the tab selected and rendered
that same session. Proof was captured in the isolated test-drive.
Second hands-on pass reproduced a stale closed tab and a viewer that required
manual reconnect. A fresh composer request then opened paperclip.ing automatically
and stayed visible after completion, switching tabs and reloading the task. The
original stalled-navigation cause remains unproven; the bounded automatic retry
and closed-tab recovery cover those observed failure paths. Recovery was exercised
in the actual test-drive; 41 focused UI tests, UI typecheck/build and token gates
passed. The automatic timeout retry is regression-tested with a non-loading iframe.
## Passkey limitation
The provider's current v4 authentication and live-viewer documentation does not
document WebAuthn/passkey forwarding from the viewer's device to the cloud
browser. Do not promise that a local Touch ID or iCloud passkey will work in the
embedded viewer. The documented alternatives are saved login profiles, passwords
and TOTP codes, and human entry of verification codes. Profile sync transfers
login cookies and browser state, not a password manager; sites can reject a
transferred login or require another verification step. See [profile sync](https://docs.browser-use.com/cloud/guides/profile-sync)
and [1Password integration](https://docs.browser-use.com/cloud/guides/1password).
## Uncertain starts and rejected continuations
Each paid task includes an opaque invocation marker. The v4 API has no documented
create idempotency key. If a response is lost or the process stops before saving
provider identifiers, Paperclip never repeats the paid POST. The reconciler scans
the documented run list for that exact marker, with a durable pagination cursor.
Once found, it cancels the run, discovers and stops its browsers, and records cost.
A missing list entry is not proof that no work exists: cleanup remains pending
and retains the credential while recovery continues. Each sweep reads at most
five pages. Continuations restrict recovery to their known provider session.
Definite request rejections (such as HTTP 409 or 429) atomically record a failed
local run and return its existing browser to idle. That browser remains visible,
gets a fresh idle grace period, and can still be closed. A crash before commit
leaves the run eligible for recovery. Provider backoff also applies to cleanup.
New-browser arrivals are queued in order and are acknowledged only after the
panel has selected and persisted the tab. Polling cannot claim an unseen arrival.
+64
View File
@@ -0,0 +1,64 @@
# Connection search
`connections_search` accepts a service name or a natural-language query up to
4,000 characters. It ranks names before capabilities, tolerates extra words,
split names such as “Agent Mail,” and single-character spelling errors or
transpositions. Short partial names work too. Capability searches return
overlapping matches without requiring every query word to occur in the catalog.
Results are bounded to 40 entries. Exact app names outrank typo matches for
different apps (for example, Motion is not replaced by Notion). Query preparation
is shared across the catalog scan.
Discovery includes tool, channel/email, and AI methods. Channel methods follow
the instance's Chat connectors experimental setting. Each catalog method names
its `purpose`. Channel and AI methods include a company-scoped `setupPath` to
their existing setup flow. `connection_request` still creates tool setup cards;
channel discovery does not turn an email inbox into an executable MCP tool or
add channel setup to that card.
The agent chooses the relevant match using descriptions and method purposes.
It should clarify only when the task remains ambiguous. A search match does not
prove that an account is authorized, that a tool is installed, or that a provider
can perform every requested action.
## Aggregator discovery
The local support index combines reviewed Arcade/Zapier claims with the public
[Composio toolkit catalog](https://docs.composio.dev/toolkits). The Composio
snapshot includes names and toolkit identifiers, a source URL, and a verification
date. Each result links to its official toolkit page. This covers, for example,
[Circleback MCP](https://docs.composio.dev/toolkits/circleback_mcp), including
queries such as “help me find tools for circle back.”
Refresh the snapshot from the official public page with:
```sh
node scripts/update-composio-search-catalog.mjs
```
The script also accepts a saved HTML file as its first argument for reproducible
extraction. Review the resulting diff before committing it. Searches use this
local snapshot and authorized indexed tool namespaces; they do not send user
queries to a provider. Broad provider descriptions are not evidence of support.
Configured connection metadata remains scoped to the current company and
identity. Archived and unauthorized catalogs cannot establish an aggregator route.
Built-in connections remain preferred for the same app. Existing AI access is
checked through the agent's AI binding and credential selection. An aggregator route still requires a
saved provider choice or a verified explicit user request. Fuzzy retrieval never
grants consent. A provider connection is not proof that its underlying app is
authorized. Multiple matching aggregator apps are returned as candidates; the
agent searches the selected `aggregator.targetService` to get its provider question.
## Regression coverage
```sh
pnpm exec vitest run packages/shared/src/connection-search.test.ts packages/shared/src/connection-routing.test.ts packages/shared/src/validators/connection-intent.test.ts server/src/__tests__/connection-aggregator-fallback.test.ts server/src/__tests__/connection-intents-service.test.ts
```
The database suite covers the original AgentMail sentence, split names, typos,
capability overlap, multiple services, AI discovery, experimental gating,
Circleback/Attio/ClickUp, indexed Executor tools, and provider consent. The native
tool authority test searches a paragraph longer than the old 200-character limit
and then creates a real connection interaction. Existing tests retain checks for
private metadata, administrative denials, stale identities, and saved declines.
@@ -0,0 +1,160 @@
# Tool connection permission audit — 2026-09-30
This review covers 117 tool methods (84 OAuth methods). The machine-readable
[source of reviewed defaults](./tool-method-permission-reviews.json) lists each
method's exact requested scopes, supported actions, restrictions, sources and
verification limits. AI runtime authentication and chat/channel setup are
separate contracts and were not changed.
## Shared credential failure
Zapier's secret URL exposed a shared ownership/resolution problem. The same
invariants apply to personal pasted-key and custom-header methods, including
Bitly, Cloudflare, Coda, Fireflies, GitHub PATs, Kernel, O'Reilly, PagerDuty,
PostHog, Postman, Razorpay, Sanity, Similarweb, Stripe, Supabase and You.com.
Generic MCP setup uses this path too, so “Connect your own” was not a reliable
workaround. This is a code-path finding, not a claim that every provider was
tested with a live account.
Personal invocation secrets now belong to the grant's user. Setup, health,
discovery and the run gateway share ownership checks and canonical credential
paths. Public unauthenticated URLs need no credential repair. OAuth client
registration secrets remain company-owned and separate from invocation tokens.
Existing connections with incorrectly owned credentials require the owner to
reconnect with a fresh key or secret URL. There is no automatic ownership backfill.
## Changes
- Airtable now requests its seven documented record, schema, comment and
workspace/base scopes, including writes.
- Explicit scope sets were added for providers that advertise a scoped MCP
resource, including Beehiiv, Netlify, Miro, Sentry, Supabase, Todoist and
TickTick. Read and write remain subject to provider roles and resource selection.
- Hugging Face adds repository read/contribution and jobs to `read-mcp`.
Optional tools still need enabling in HF's MCP settings.
- Slack's reviewed set now covers the documented message, channel, reaction,
canvas, file and list actions. It replaces the obsolete generic search scope
with the documented search scopes. The fixed app/approval gate remains.
- Google write/draft methods take priority over managed read-only methods when
available. Existing Google profiles, preview verification and availability
gates remain. People and Workspace Search stay read-only.
- The hosted Fireflies server also exposes meeting sharing, renaming, moving and
soundbite creation. Its identity scopes are retained; meeting ownership and
team roles decide which mutations succeed.
- Provider read-only choices stay available under Advanced. Existing OAuth
tokens and action restrictions are untouched. Permissions offers reconnect
when provider consent needs changing.
## Provider-default exceptions
These are deliberate exceptions to supplying `scopesHint`, not claims that an
old grant can write. Several servers put resource/permission selection in their
own consent screen; Box uses registered app scopes, and managed GitHub uses
installation permissions. An authorization server's general scope catalog is
not a safe substitute for the MCP permission contract.
| Provider / method | Reason and limits |
| --- | --- |
| bitly / `mcp-oauth` | The official MCP quickstart uses browser authorization without client-selected scopes; neither protected-resource nor authorization-server metadata advertises a scope list. Bitly account permissions govern link actions. [Evidence](https://dev.bitly.com/bitly-mcp/overview/quickstart/) |
| box / `mcp-own-oauth` | Box requires scopes (including Manage AI) on the registered Box app in the Admin Console. Its MCP authorization metadata has no request-scope list; consent and the app registration control file operations. [Evidence](https://support.box.com/hc/en-us/articles/43847256139923-Managing-Box-MCP-Servers) |
| egnyte / `mcp-oauth` | The hosted authorization server selects the Egnyte tenant/account permissions. Neither MCP nor authorization-server metadata advertises request scopes. Documentation rendered no readable body during this review; tenant-bound write proof remains required. [Evidence](https://developers.egnyte.com/docs/Remote_MCP_Server) |
| github / `managed` | Managed github.code uses a GitHub App installation and selected repositories. Installation permissions, not OAuth scope strings, grant code and pull-request writes; retain the managed profile. [Evidence](https://api.githubcopilot.com/.well-known/oauth-protected-resource/mcp/) |
| make / `mcp-oauth` | Make documents selecting scopes and scenarios during its hosted connection flow. Management tools require a paid plan. Do not replace that resource selection with invented MCP client scopes. [Evidence](https://developers.make.com/mcp-server) |
| oauth-generic / `oauth` | Operator-defined OAuth endpoints have no provider-specific reviewed scope list. Request only the scopes supplied by the operator; this template is not a curated provider allowlist. [Evidence](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization) |
| planetscale / `mcp-oauth` | PlanetScale documents choosing organizations, databases and read/write permission at authorization. Its general authorization server advertises unrelated organization administration too; retain the provider consent selection, and the separate insights-only endpoint. [Evidence](https://planetscale.com/docs/connect/mcp) |
| planetscale / `mcp-insights-only` | PlanetScale documents choosing organizations, databases and read/write permission at authorization. Its general authorization server advertises unrelated organization administration too; retain the provider consent selection, and the separate insights-only endpoint. [Evidence](https://planetscale.com/docs/connect/mcp) |
| posthog / `mcp-oauth` | PostHog documents the no-scope MCP setup as read/write, with optional readonly and feature filters. The authorization server advertises account administration unrelated to many tools; retain its MCP consent defaults and the existing explicit filters. [Evidence](https://posthog.com/docs/model-context-protocol) |
| postman / `mcp-oauth-minimal` | The official remote MCP setup uses browser sign-in and endpoint-selected minimal/code/full tool catalogs. Its protected-resource and authorization-server metadata publish no scope set; account/workspace rights govern writes. [Evidence](https://learning.postman.com/latest-v-12/docs/reference/postman-api/postman-mcp-server/postman-mcp-remote-server) |
| postman / `mcp-oauth-code` | The official remote MCP setup uses browser sign-in and endpoint-selected minimal/code/full tool catalogs. Its protected-resource and authorization-server metadata publish no scope set; account/workspace rights govern writes. [Evidence](https://learning.postman.com/latest-v-12/docs/reference/postman-api/postman-mcp-server/postman-mcp-remote-server) |
| postman / `mcp-oauth-full` | The official remote MCP setup uses browser sign-in and endpoint-selected minimal/code/full tool catalogs. Its protected-resource and authorization-server metadata publish no scope set; account/workspace rights govern writes. [Evidence](https://learning.postman.com/latest-v-12/docs/reference/postman-api/postman-mcp-server/postman-mcp-remote-server) |
| razorpay / `mcp-oauth` | Neither protected-resource nor authorization-server metadata advertises scopes. Keep hosted provider consent; the previous official OAuth documentation URL returned 404 during review. Account-bound writes need live confirmation. [Evidence](https://razorpay.com/docs/mcp-server/oauth/) |
| ticket-tailor / `mcp-oauth` | Hosted authorization controls box-office API-key permissions; do not treat OAuth sign-in as increasing the underlying key permissions. This method requires account-bound proof. [Evidence](https://developers.tickettailor.com/docs/mcp/authentication/) |
| webflow / `mcp-oauth` | The official MCP setup installs the Bridge App and asks users to authorize sites. Neither protected-resource nor authorization-server metadata publishes a request-scope list; the installed app and site selection govern writes. [Evidence](https://developers.webflow.com/mcp/reference/getting-started) |
## Read-only and provider-controlled methods
Candid, Context7, O'Reilly, Similarweb and You.com expose retrieval/research
capabilities rather than document/account mutation. PlanetScale's insights-only
endpoint deliberately excludes query execution. Xero's current published MCP
scope set includes invoice/report reads and settings access, without invoice
write permission. Do not widen these using unrelated general API scopes.
Scopes such as `openid`, `global`, `mcp:all` or `workspace:member` do not have to
contain the word “write” to authorize work. The provider's MCP tool contract,
role and consent selection determine their meaning. In particular, Cloudflare,
Vercel, Wix, Kernel and Supermemory retain their provider-side permission gates.
## Evidence and verification boundaries
Official docs and unauthenticated protected-resource/authorization metadata were
retrieved on 2026-09-30. The review records source URLs per method. Some pages
(especially Egnyte and Razorpay's old OAuth URL) were unavailable or unreadable;
those entries explicitly retain that limitation. Embat publishes identity
metadata but no verified write contract. No account-bound write guarantee is
made for these entries.
No provider account was used for live read/write verification in this task.
In particular, public scope metadata is **not** proof that an existing token has
those permissions or that an account has the required paid plan, resource ACLs,
admin approval, or Google preview enrollment. Use test accounts/resources for
that proof before claiming provider-level acceptance.
Backend regressions execute catalog Zapier, generic secret URLs, personal
bearer/custom headers, organization credentials and public URLs through the
run-scoped gateway using a fixture transport. They assert canonical persisted
ownership/declarations and run read and write calls. Reconnect regressions cover
replacing legacy company credentials with user-owned values and declarations.
OAuth fixtures assert exact authorization scopes and rejection of an unrelated
advertised admin scope; they do not contact provider accounts.
The managed worktree has a separate instance configuration and database path.
Local CLI provisioning was attempted with both minimal and full seed modes;
both failed while applying the copied database's migrations because
`tool_connections_transport_check` was missing. The development instance was
not started from that clone. Clean fixture databases were used for the backend and Apps browser
checks; their success does not establish successful seeding of that local copy.
### Embedded-browser acceptance
On 2026-09-30, a hands-on walkthrough used the PR checkout's built UI and actual
server, a fresh isolated database created through CLI onboarding, and a local
HTTP MCP fixture. Starting from the sidebar's Connectors page, the operator
selected “Just me,” entered a bearer key, and created and read back a disposable
widget through the Permissions screen's agent test controls.
After seeding the legacy ownership mismatch in that disposable connection,
invocation and catalog refresh rejected it with `grant_credential_invalid`.
The catalog showed “Needs attention” and offered Reconnect. Replacing the key
through that UI preserved the connection and personal grant, created a fresh
user-owned secret and canonical declaration, and restored writes. The legacy
company secret retained its ownership. Separate HTTP calls through a session
bound to a fixture agent run also completed a write and read-back.
The walkthrough exposed and verified fixes for a false “Still not working”
message after successful reconnect, incorrect action-input advice for ownership
errors, and Cancel attempting to save an invalid Zapier URL. The browser
reconnect regression now performs the replacement through the form and checks
that the stale warning disappears.
A second personal connection used a secret URL. After reproducing its legacy
ownership failure, the browser could replace the URL and create/read back a
widget. Generic reconnect now derives URL/header fields from the stored
credential placement instead of assuming a bearer key, and rejects replacement
URLs for a different public endpoint. A new public, organization-wide connection
also appeared immediately when returning to Browse, without a page reload.
The walkthrough fixed setup cache invalidation and a cramped reconnect banner.
Zapier's URL validation and cancellation were exercised, but a live Zapier
connection was not completed. Gmail stopped at instance enrollment. Neither
journey establishes provider-account consent or live provider read/write proof.
### Automated verification
- Run `pnpm -r typecheck`, `pnpm build`, and `pnpm check:token-gates`.
- The affected Apps browser suites passed 10 tests, with one existing restart
case skipped. These used clean, isolated fixture databases.
- Run `pnpm test:run` for the full suite. Current-head CI and local results are
recorded in the pull request; fixture coverage is distinct from provider proof.
- New gateway fixtures execute both read and write calls through real run-scoped
gateway sessions. They also verify that invalid ownership requires reconnect
and that the owner's fresh credentials restore access without changing identity.
+70
View File
@@ -61,6 +61,59 @@ execution. Extend the provider's existing catalog entry with typed AI methods;
reuse the existing login controllers. See [AI Connections](./AI-CONNECTIONS.md)
for compatibility, personal defaults, resolver isolation, and legacy adoption.
## Read/write defaults and credential ownership
New tool connections request the provider-documented permissions needed for their
supported read **and write** actions. Prefer an available write/draft capability
before a managed read-only method. Read-only capability choices and provider
read-only switches belong under **Advanced**. Preserve an explicit method on
resume/reconnect. Keep the Access → Connect flow and manage action restrictions
on Permissions; changing OAuth configuration never changes existing consent or
turns an Off/Ask-first action into Allowed.
Every tool method must have a review in
[`tool-method-permission-reviews.json`](./tool-method-permission-reviews.json).
It records requested scopes, supported actions, provider restrictions, official
evidence, and live-proof status. The ingestion script uses its explicit scope
lists as `scopesHint`; the catalog regression rejects missing reviews, drift,
and undocumented omitted OAuth scopes. An omission requires a provider-default
exception explaining how consent/registration grants access. Never automatically
request all scopes advertised by an authorization server. See the
[permission audit](./CONNECTOR-PERMISSION-AUDIT.md) for review findings and limits.
API-key fields must explain required provider permissions; Paperclip cannot
increase an already-issued key's permissions. Reconnect with fresh consent/key
when access is insufficient. A provider's explicit `insufficient_scope` response
becomes an actionable `oauth_insufficient_scope` error; provider response text
and credentials are not echoed.
All invocation credential writes use `writeConnectionCredential` (setup,
replacement, reconnect, OAuth completion and rotation). A personal grant requires
a **user-scoped** secret owned by its subject and a user-secret definition and
declaration for the connection. Organization/dedicated-agent credentials use
company secrets and bindings. OAuth **client-registration** secrets remain
company-owned and are resolved separately from action credentials. Declaration
paths are canonical (`credentials.authorization`, `headers.X-Api-Key`,
`remote.url`, `oauth.access_token`), never double-prefixed. Health, discovery,
board tests, invocation, and version tracking must use the selected grant's
refs and the same ownership checks. A personal resolver must not fall back to a
company credential. Ownership mismatch is `grant_credential_invalid` and tells
the owner to reconnect.
Existing personal connections with company-scoped invocation credentials require
their owner to reconnect and enter a fresh key or secret URL. Reconnect creates
a user-owned credential and updates the grant and declarations while preserving
the connection identity and action policies. Credential ownership is never
automatically reassigned at startup. Health, discovery and invocation reject an
invalid ownership layout with an actionable reconnect error.
Verification must assert stored ownership and declarations, then execute a read
and a write through a real run-scoped gateway. Cover generic and curated URL
credentials, bearer/custom headers, shared identities, public endpoints, rotation,
removal, failed-setup cleanup, owner reconnect of legacy credentials, another user,
and another company. Fixture-backed MCP calls prove Paperclip behavior; they do
not prove provider consent or account entitlements. Record account-bound live
read/write proof separately and never describe metadata discovery as live proof.
## Contents
- [Mental model and support matrix](#mental-model-five-independent-axes)
@@ -497,6 +550,13 @@ registry in `server/src/services/connector-runtime.ts`; AgentMail is the first
consumer. This registry describes bundled server implementations, not executable
code or skill URLs supplied by a credential or external message.
Optional connector instructions must not be placed in the universal `skills/`
directory, which adapters can enumerate for every agent. A trusted contribution
can provide `skillMarkdown` from its connector module; the server then materializes
`SKILL.md` only for authorized assignments. Browser Use Cloud uses this path with
the app key and skill name `browser-use-cloud`, leaving generic browser skill
names available to other integrations.
For each contribution, declare its connector key, bundled skill, namespaced tool
definitions, resource-assignment resolver, and execution handler. Use names such
as `agentmail_send` rather than extending core tools with provider-specific
@@ -2191,6 +2251,16 @@ Collect end-to-end evidence using the production validation matrix above:
- Audit rows prove actor, run/issue context, connection, tool, decision,
reason code, and outcome.
## Reviewed REST browser connection
[Browser Use Cloud](./BROWSER-USE.md) uses the v4 REST API through the same
connection grants, catalog, policies, approvals and audit gateway. Its
`provider_rest` execution path is limited to the reviewed Browser Use adapter;
adding `rest_api` to a catalog entry does not enable arbitrary HTTP execution.
The task Browser panel is a human-only credential viewer, separate from agent
tool results. Follow that guide for lifecycle, profile scope, cost accounting,
cleanup and the required live acceptance pass.
## Slack task tools
For Slack bot tool contributions, use [Slack task tools](SLACK-TASK-TOOLS.md). It
+17
View File
@@ -62,6 +62,23 @@ back that step while retaining the established draft identity.
Later health checks still require the user's authorization and return an
actionable `422` error when it is missing.
### Personal apps on shared agents
Installing a personal app on an agent does not require every user who runs that
agent to connect the app. Runs start without probing optional apps or warning
about another user's missing credentials. The installed app's cached tools stay
available even when its shared health check needs attention.
Authorization happens when the agent calls an app tool. Paperclip uses the run's
responsible user, never another user's personal grant. If that user has not
connected the app, the tool returns `user_authorization_required` and adds an
inline connection request. Unrelated work can continue without using the app.
Disabled or uninstalled apps remain unavailable.
Optional assigned apps do not emit run-start connection warnings, including
unavailable shared apps. Their health state and reconnect controls remain in
Apps. An unrelated run does not need to act on that state.
### Slack app access
If Slack reports that MCP access is disabled for the app, ask the Slack app
+34
View File
@@ -159,6 +159,40 @@ operations. Write profiles add only the reviewed write operations for their app;
destructive or unreviewed tools do not become available merely because Google
adds them upstream.
### Service-specific minimum scopes (2026-09-30)
Docs, Sheets, and Slides request only their service's read-only scope in the
read profile and its read/write scope in the write profile. They do not also
request `drive.readonly` or `drive.file`: those are authorization alternatives,
not additional requirements. Drive and Workspace Search retain their separate
Drive permissions. Calendar write requests `calendar.calendarlist.readonly`
and `calendar.events`; the latter also authorizes `suggest_time`. Calendar read
retains `calendar.events.freebusy` alongside list/event read-only access.
References: Google's [Docs read](https://developers.google.com/workspace/docs/api/reference/mcp/tools_list/read_doc)
and [update](https://developers.google.com/workspace/docs/api/reference/mcp/tools_list/update_doc),
[Sheets read](https://developers.google.com/workspace/sheets/api/reference/mcp/tools_list/get_spreadsheet)
and [update](https://developers.google.com/workspace/sheets/api/reference/mcp/tools_list/update_values),
[Slides read](https://developers.google.com/workspace/slides/api/reference/mcp/tools_list/read_presentation)
and [update](https://developers.google.com/workspace/slides/api/reference/mcp/tools_list/update_presentation),
and [Calendar suggest_time](https://developers.google.com/workspace/calendar/api/v3/reference/mcp/tools_list/suggest_time).
The write scopes support editing existing accessible files by ID. `drive.file`
is a valid narrower alternative for app-authorized files, but would require a
different per-file authorization workflow. Read-only profiles remain separate;
the project-wide union is not the scope set requested by every connection.
Deploy with the matching Cloud broker registry and test fresh grants in staging
before production. Signed authorization and refresh requests use exact scope
sets, so mixed versions fail closed. The broker rejects old broader grants and
refresh responses without explicit scope evidence for these reduced profiles.
Reconnect affected connections; do not relabel or globally revoke existing
tokens. Customer-owned methods request the same reduced sets on new consent;
previously issued grants are not retroactively narrowed. The 21-scope integration
union is unchanged by these profile-level reductions. Console must still keep
Drive/free-busy entries required by other profiles and separately used identity
scopes. Fresh provider proof is a release requirement, not implied by unit tests.
### Google Chat scope reduction (2026-09-22)
`chat.read` requests only `chat.spaces.readonly` and `chat.messages.readonly`.
File diff suppressed because it is too large. Load diff
+20
View File
@@ -6,6 +6,13 @@ Run scope: `ui/src/components/` and `ui/src/pages/` on branch `design/token-extr
## Counts
### Connection recovery — 2026-09-29
`CloudAccessGate.tsx` contains the startup `CloudAccessError` composition, using
the existing Button and typography tokens. It covers automatic reconnection,
an in-flight check, and manual retry after access-check failures. See
`/design-guide` and Storybook **App / Connection recovery**.
### Independent MCP connection setup — 2026-09-21
`ui/src/features/connections/remote-mcp/RemoteMcpConnectionSetup.tsx` is the controlled
@@ -444,3 +451,16 @@ Independently addressable examples live under `Connections/In-task connections`
`ui/src/components/SetupWizard.tsx` extracts the Slack setup navigation into reusable numbered steps, a portal sidebar, an optional section-menu takeover outlet, and a single-row footer. `SetupWizardSidebarProvider` owns the portal target and takeover lifecycle. Chat exports retain their existing names and defaults for compatibility. The Design Guide demonstrates the components. The production routine trigger wizard and its Storybook previews share the sidebar takeover, navigation, and footer. `routine-triggers/TriggerWizard.tsx`, `TriggerCard.tsx`, and `WebhookFields.tsx` provide the shared trigger setup, compact editable cards, copyable credentials, and agent instructions.
`routine-triggers/WebhookUrlWarning.tsx` uses `InlineBanner` for non-blocking localhost, private-network, Tailscale, and HTTP guidance. Setup and saved webhook editors share it; the Design Guide shows each warning. URL classification is heuristic, not a public reachability test.
## Browser Use surfaces — 2026-09-29
| Component | Responsibility | Storybook |
| --- | --- | --- |
| `TaskBrowserPanel` | Live iframe, debounced fitting with viewer ownership, connecting/closed/error/access states | Tasks / Browser Use / Panel |
| `TaskBrowserFooter` | Fit to pane and fixed viewport presets, ownership feedback, last-five-minute countdown, contextual stop/extend, explained session menu | Tasks / Browser Use / Footer |
| `TaskBrowserActivity` | Compact single-session row, interleaved at browser creation time in the task feed; opens the matching panel tab | Tasks / Browser Use / Task activity, including Inline History |
| `BrowserUseSettingsPanel` | Per-credential cost caps and saved-profile access | Connections / Browser Use / Settings |
The footer also appears on `/design-guide`. All stories use offline fixtures;
they do not create provider sessions. The viewer fixture intentionally scales a
fixed desktop viewport, preserving the current provider embed's sizing limitation.
+93 -1
View File
@@ -19,7 +19,7 @@ The names describe the system under test; “headless” is an execution option,
not an eval category.
The explicit Product E2E `completion-updates` suite compares onboarding and
idle Agent Chat handoffs on native Claude/Codex. It separates mechanical
idle, busy, multiple-task, and restart Agent Chat handoffs on native Claude/Codex. It separates mechanical
completion delivery/result access from semantic review of the retained answer;
see the [probe contract](../tests/runner-e2e/README.md#completion-update-probes-explicit-only).
@@ -90,6 +90,10 @@ authentication respectively. Keep their results separate; the subscription
fixture seeds an explicitly supplied login and does not qualify interactive
login. See the [Grok fixture contract](../tests/runner-e2e/README.md#grok-build-qualification).
The explicit [Direct blocker guidance suite](../tests/runner-e2e/README.md#direct-blocker-guidance)
checks the legacy coordination skill against human authority, missing hiring
permission, and requester scope decisions through saved browser interactions.
## Validation ladder
Start with credential-free checks and a catalog listing. For Product E2E:
@@ -192,6 +196,78 @@ For a repeatable workflow, use the matching skill: [paperclip-evals](../.agents/
[add-runner-eval](../.agents/skills/add-runner-eval/SKILL.md), or
[add-product-e2e-eval](../.agents/skills/add-product-e2e-eval/SKILL.md).
## Diagnose failures before buying another campaign
Use this loop to turn eval failures into product improvements. The unit of work
is a broken user outcome or invariant, not an individual red cell.
1. **Freeze the evidence.** Record the inspected application revision and each
campaign's evaluated revision, definition/grader fingerprint, model/profile,
environment, exact selected IDs, attempts, and usage coverage. Read the
history feed and retained attempt records before launching models. A report
refresh, skipped workflow, passing unit suite, or old-definition green cell
is not a new live measurement. Inventory explicit-only suites separately
from `--all`.
2. **Reconstruct the failed boundary.** Read durable task state, interactions,
event chronology, source/worker identity, delivered output, and the failing
assertion. State whether the test reached the boundary it claims to test.
Separate observed failure, machine class, analytical cause, and confidence.
A cancelled queued wake does not by itself prove lost work; a failed decline
assertion does not prove unauthorized execution. A saved artifact does not
prove the user received a correct completion update.
3. **Group by cause and product contract.** Join cells only when their evidence
supports the same mechanism. Check for already-merged fixes and definition
corrections before proposing new work. Keep product defects, provider/model
behavior, grading defects, infrastructure, and unexercised boundaries
distinct. Preserve the original grades when attribution changes.
4. **Design the smallest general correction.** Name the desired user behavior,
the authoritative state/transaction or provider boundary that owns it, the
affected callers, and the existing guarantees that must survive. Check it
against `PRODUCT.md` and `SPEC-implementation.md`. A change to an intentional
product rule is a contract change, not an excuse to delete its guard. Avoid
case-name branches, phrase-specific prompts, unconditional retries, or
weakening approval, ownership, cancellation, and budget gates to get green.
5. **Prove the mechanism cheaply.** Calibrate a grader against correct and
plausible wrong retained evidence. Reproduce a product race with scripted
providers or service tests. Pair every proposed fix with a regression that
exercises the opposite boundary (for example, eligible delivery versus
revoked access, benign obsolete wake versus interrupted active work).
Presentation-only changes use the existing report refresh path and make no
provider calls; do not claim replay can prove changed runtime behavior.
6. **Select a bounded live confirmation.** Write the exact failed representative
IDs and only the passing controls affected by the change. Pin the source,
definitions, models, and remote image. Record an attempt cap, provider and
compute budget, wall-clock deadline, and concurrency before dispatch under
the existing live-run authorization. Missing pricing means unknown, not free.
Do not rerun unaffected green cells during diagnosis or retry usable behavior
failures until they happen to pass. Preserve every attempt. Expand to a
compatible qualification campaign only after the causal fix passes.
7. **Close with evidence and remaining scope.** Report original versus new
measurements, exact selected coverage, regression results, cost coverage,
and unresolved boundaries. A partial verification may close one defect; it
cannot turn the full catalog green or establish reliability from one attempt.
Keep one triage record per cause with: affected cell IDs; evidence links;
observed failure; supported cause and confidence; existing fix/revision; proposed
product contract; invariant regressions; next exact live selection; budget and
stop condition; owner; and disposition. Useful dispositions include confirmed
product defect, model behavior, grader correction, infrastructure repair,
fixed-but-not-remeasured, historical pass, and unqualified coverage.
Parallelize independent artifact inventory, deterministic checks, and isolated
cells. Keep a cell's dependent turns ordered. When delegating, use inexpensive
agents for bounded extraction, catalog reconciliation, and test execution; keep
causal attribution, product design, and final review with the lead. Begin local
browser campaigns at the documented conservative concurrency and increase only
with measured host headroom. Hosted fanout must respect the workflow's provider
and fleet caps; more simultaneous timeouts do not improve wall-clock efficiency.
Current tools support exact-ID selection and retained-evidence report refresh,
but not an automatic cause-aware "rerun unresolved failures" planner. Build an
explicit selection manifest rather than treating `--all` as that planner. Review
the launcher's automatic retry policy when budgeting; Product E2E may create one
fresh attempt for a retryable failure.
## Install the authoring skills
The reviewable sources live in this repository's `.agents/skills`. For a
@@ -276,6 +352,22 @@ results and follow-up coverage are recorded in that suite's guide.
Continuation accounting has an explicit-only eight-cell Product E2E [baseline suite](../tests/runner-e2e/CONTINUATION-ACCOUNTING.md), complementing the deterministic lifecycle inventory.
The explicit Product E2E `instruction-persistence` suite verifies private file
edits, nested and binary agent files, stopped-provider directory saves, server restart, and a fresh task's
downloaded proof on local native/legacy Codex and native Daytona. See the
[Product E2E runbook](../tests/runner-e2e/README.md).
The explicit-only Product E2E `api-response-reading` suite verifies retrieval of
large saved API responses on local and Daytona native Codex runs. See the
[Runner E2E guide](../tests/runner-e2e/README.md#bounded-api-response-reading).
The explicit-only Product E2E `extended-harnesses` suite covers pending Cursor,
Copilot and Pi ACP profiles on local and Daytona. See the
[fixture admission, credentials and budget contract](../tests/runner-e2e/README.md#extended-acp-harnesses-explicit-only).
The private Runner Evals campaign of the same name provides complementary
semantic protocol cases; catalog membership is not live qualification.
The explicit-only Product E2E `confirmation-replies` suite tests conversational
approval and rejection, persisted message provenance, approval before execution,
ambiguous proposals, and the existing card-click path with native Claude/Codex.
See the [suite contract](../tests/runner-e2e/README.md#conversational-confirmation-replies-explicit-only).
+13 -10
View File
@@ -394,7 +394,6 @@ Issue document comments, document annotation comments, and document review comme
Document-scoped activity may still route work when it is converted into an explicit action-path primitive. Valid routing exceptions include:
- an issue mention or structured agent mention that intentionally wakes or assigns a named participant
- a document-review assignment that names a reviewer or assignee for the review state
- a response to an issue-thread interaction, such as `request_confirmation`, `ask_user_questions`, or `suggest_tasks`
- intentional board routing that assigns or reassigns the issue, opens a first-class blocker, creates delegated follow-up work, or queues a typed wake
@@ -423,16 +422,9 @@ A wake is the delivery path for a selected agent owner. If an interrupting updat
If the committed update assigns the issue to a user, clears the agent assignee, or leaves the issue without an agent owner, Paperclip must not imply that an agent handoff happened. The issue is then waiting on the human owner or on a future explicit assignment, blocker, approval, interaction, monitor, or recovery action.
Plain text is not assignment. Writing an agent's name, role, or team label in a comment does not change ownership and does not create an agent wake. Agent routing from comment text requires a structured agent mention that resolves inside the company, an explicit `assigneeAgentId` mutation, or an existing current agent assignee receiving normal issue-thread feedback.
Agent names and structured @-mentions are context, not assignment or wake requests. Mentions remain on the original comment and do not start an agent, authorize checkout, or copy feedback onto a referenced child task. This applies equally to board and agent comments, standalone comments and issue updates, and open and terminal tasks. New work for another agent requires explicit assignment, a bounded delegated task, or an explicit review request.
A delegation comment from the current assignee's run on this parent must not start competing parent work when the named worker already owns the referenced child. This applies to issue updates with a comment and standalone comments. Verify the source run's company, agent, and parent-task context. Then verify that the comment references the child's identifier, the child's `parentId` names this parent, and the child belongs to the same company and is assigned to the mentioned worker. Apply child-aware routing only in either of these states:
- The parent is `blocked` and the child is `in_progress`. The child must have a blocker edge to the parent. The child's execution or checkout run must still be `running`, belong to that worker and company, and name that child in its run context. Verify comment and mutation access to the child, then retain the parent comment and append a linked copy on the child. Preserve the full comment, author, source run, responsible-user attribution, and source trust. Target the normal mention wake at the child and its new comment ID, with explicit resume and follow-up intent. This keeps new feedback available to the worker and lets the existing queue serialize a child continuation behind its current execution.
- The parent and child are both `done`. The assignee's closing comment must not start another worker run for the completed delegation. A blocker edge is not required after completion: a fast child can finish before the lead needs to record a wait. New agent work must use explicit `resume: true`, a status change, or a new assigned task. Explicit resume moves the parent out of `done` before this rule runs; the comment's prose alone does not restart completed work.
The completed-delegation comment remains on the parent without a worker wake. Neither path changes ownership. Board-user comments and unrelated mentions retain their normal wake behavior. If multiple referenced children qualify for the same worker, the child or run no longer meets these conditions, child comment or mutation access is denied, or a lookup or copy fails, use the normal parent mention path. Do not parse mentions again while copying a comment, which would create another routing loop. Completion of the child still uses the existing blocker-resolution wake for the parent's assignee.
The parent may receive a closing comment before its assignee changes the status to `done`. Recheck the completed-delegation rule when releasing that parent execution, before promoting a deferred mention. On the same transaction, verify the final parent state, finishing run, and every original queued or deferred comment ID. Each comment must belong to this parent and company, come from its assignee's finishing run, and reference exactly one completed direct child assigned to the mentioned worker. A link to the parent itself is allowed; any other extra issue reference keeps the normal mention path, including an unknown or foreign reference. Mixed human, other-run, unrelated, or ambiguous input retains its normal wake path. Explicit continuation and interaction requests also retain their normal path.
Normal issue-thread feedback can still wake the current assignee according to the comment policy; self-comments and closed-task comments retain their existing suppression rules. Explicit assignment, review, interaction, and blocker-resolution wakes retain their own routing. New mention wake requests are ignored at admission, including calls from legacy producers. Already accepted queue entries retain their existing execution rules: an old entry can combine an assignment or other feedback with a later mention, so its last wake reason does not prove that the entry is mention-only.
Accepted agent feedback must survive a child changing to `done` before its active run exits. Deferred wake promotion may reopen that completed child only for its current assignee, with explicit agent resume intent and live tracked comments from another author. Claim promotion before reopening. Cancelled tasks, deleted comments, self-authored comments, empty continuations, and agent continuations without explicit intent keep their existing suppression rules. Normal pause, ownership, authorization, and budget gates still apply.
@@ -936,6 +928,12 @@ Questions must be created as durable interactions before the agent claims to be
waiting. A direct Board comment reopening completed work has the same passive
response-wait semantics as a comment on an open task, subject to the same source,
identity, and governance checks. An automatic continuation is not a user reply.
For ordinary tasks, a response wait cannot park reported blocking remaining
work without a recorded wait condition. The finish tool rejects this combination;
finalization routes already-accepted reports through one bounded corrective
continuation and then a visible recovery error. Real questions, approvals,
dependencies, pauses, and conversation lifecycles keep their existing behavior.
A superseded Board comment never grants permission to replay the old response.
A run must receive queued human direction before it creates a new task question.
When the saved run context contains an explicit delivered-comment list, the
@@ -1258,6 +1256,11 @@ turn finishes during shutdown, its release checkpoints the session before
returning instead of leaving a new idle owner behind. If checkpointing fails,
the retained state continues to block unverified reuse.
Local durable control-plane state is bounded at 256 MiB in the server,
runnerd recovery, and durable control-plane readers. These paths synchronously
read and parse the full JSON file, so memory use and parse time grow with file
size. Remote checkpoint archive and expanded-size limits remain 64 MiB.
### Warm sandbox continuity
For the native runner, warm mode requests a reusable sandbox lease **before**
+79 -1
View File
@@ -22,7 +22,8 @@ alternative peer dependencies — install exactly **one**, matching
`OTEL_EXPORTER_OTLP_PROTOCOL`.
When `OTEL_EXPORTER_OTLP_ENDPOINT` is unset, none of the `@opentelemetry/*` SDK
packages are imported and there is zero runtime overhead.
packages are imported and there is no SDK or exporter overhead. Local request
timings still use the performance clock.
`server/package.json` declares each optional package at the exact version the
server tests against; install that exact version. Our Dependabot cannot bump
@@ -34,6 +35,33 @@ maintains as a dependency; once you install the packages below, they become
normal dependencies of **your own** project, and your own Dependabot updates
them.
## Task detail loading
`GET /api/issues/:id` returns `Server-Timing` with `paperclip_issue` for the
whole handler and `issue_<phase>` for its reads. The total includes the final
execution-blocker read after recovery revalidation. Concurrent phases overlap;
do not sum them to obtain request duration.
With the existing operator-configured OTLP endpoint, the same reads produce
`issue.read.<phase>` child spans through the `paperclip.issue-read` tracer.
There are no custom attributes, identifiers, content, or exception messages.
The closed phase names in `server/src/services/issue-read-timing.ts` are:
`lookup`, `authorization`, `project_goal`, `ancestors`, `mentions`, `documents`,
`relations`, `blockers`, `review`, `references`, `handoff`, `retry`, `recovery`,
`cases`, `inbox`, `channel`, `workspace`, `work_products`, `execution_blocker`,
`relation_recovery`, `revalidate_recovery`, and `mentioned_projects`.
Without an OTLP endpoint, spans remain no-ops. This adds no first-party
Telemetry events or run-log events.
The browser's `issue-detail:navigate→content-paint` User Timing measure ends
after the redesigned conversation is revealed and painted, rather than when
the comments request completes while the conversation is still hidden. Durable
messages can paint before run history and plan enrichment. Runtime-only threads
wait for initial output; late history retains the existing scroll-anchor behavior.
Use Chrome's request waterfall and this mark together to distinguish API wait,
request dependencies, and rendering time. A hard-load trace also includes auth,
company selection, and JavaScript startup before the task navigation mark.
## Enabling tracing
### 1. Install the OTel peer dependencies
@@ -406,6 +434,56 @@ They do not change the ambient Sentry scope, whose isolation is unavailable
without an OpenTelemetry context manager. Later, unrelated exceptions must
not inherit a previous run's identity or fingerprint.
The `run_failure` context also includes the recorded process `exitCode` and
`signal`, so a generic adapter error can still distinguish a nonzero exit from
a signal termination. Exit codes must fit the database's signed 32-bit integer;
missing or malformed values become `null`. Signals must match the reporting
host's Node signal constants; missing values become `null` and unrecognized
values become `unknown`. A signal such as `SIGKILL` does not establish who sent
it or prove an out-of-memory kill. These fields do not change error grouping
or run outcomes, and do not include process output or adapter result payloads.
An unconfirmed adapter Stop timeout has an event-local `adapter_stop` context:
the run UUID, built-in adapter type, native/legacy runtime mode, configured
wait duration, and whether abort was requested. Invalid identities become
`null`, and unknown adapter/runtime values become `unknown`. The report does
not include stop reasons, prompts, process output, provider responses, or
credentials. It preserves default error grouping and does not acknowledge
termination, remove the live execution control, or change the timeout. The
context is sent only through the existing opt-in Sentry gate and never leaks
into unrelated captures.
The shared reporter also attaches bounded diagnostic contexts for both legacy
and native runs:
- `run_execution`: runtime mode, execution stage/native phase, driver and version,
duration, failure phase, stop reason, error family, and timeout settings when available.
- `adapter_failure`: selected adapter error fields such as phase, category,
protocol code, retryability, cause message, stack preview, HTTP status, and request ID.
- `provider_failure`: the saved provider failure category, title, and details.
- `run_exception_0` through `run_exception_3`: exception names, codes, HTTP
statuses, and request IDs for a caught exception and up to three causes.
The execution and setup catch paths pass the original exception to the reporter.
It snapshots and rebuilds only these selected fields and the original message and
stack. Sentry receives the sanitized cause chain rather than a stack created at
the reporting call. Saved adapter results use an available adapter stack preview;
without one, the report has no synthetic reporting stack.
Credential patterns, the current user's home path, registered run-secret values,
and known host/runtime environment credentials are removed before truncation.
Declared environment secret bindings are included even when their key has no
credential-like name. Runtime values are used only for redaction and are never
copied into an event. Stack fields are limited to 8,192 characters,
exception messages to 2,048, provider titles to 4,096, and provider details to
12,288. Other diagnostic strings are limited to 200 characters (adapter cause
messages: 2,048). Truncation is marked in the text and in
`run_execution.truncatedFields`; cyclic and deeper cause chains are marked too.
Request/response objects, headers, environment/configuration, prompts, stdout,
stderr, and arbitrary adapter result fields are not copied. The existing DSN
opt-in gate and error-code/adapter fingerprint remain unchanged. These reports
cannot recover diagnostic data that a provider or adapter discarded upstream.
### Browser data
The browser sends no page URL, no referrer, no user agent, and no
@@ -0,0 +1,93 @@
# Return completed work to its originating chat
## Summary
When an agent hands off work from Agent Chat, Paperclip will bring the finished result back to that conversation automatically. The original agent will write the update using the task’s recorded status and saved output.
Report tasks as they finish. Combine completions already waiting for the same reply. A `/new` reset suppresses updates from the previous conversation session.
The merged completion-update evals are the baseline. This change should turn the missing-update cases green and address the stale onboarding replies.
This behavior applies to Agent Chat handoffs and the onboarding completion-update fix below, not all agent handoffs in general. It uses the existing Agent Chat availability setting and adds no separate experimental flag.
## Implementation
**Record the handoff automatically.**
- Add a durable, company-scoped handoff record linking the created task to the source conversation, its session generation, and its agent.
- Capture it inside task creation using the authenticated creating run. Both native `create_task` and HTTP task creation already supply that context. Do not rely on the model to provide a source ID.
- Keep execution tasks as ordinary project tasks. Do not turn the conversation into their parent or block it on their completion.
- Preserve existing creation idempotency: replaying task creation must not create another handoff.
**Queue completion durably.**
- Create a completion-delivery record in the same transaction that moves a linked task into Done. Cover both ordinary issue updates and native status-decision commits.
- Identify each event by the handoff and committed completion transition. Repeating the same transition must not create duplicate deliveries.
- Use a small database-backed outbox, following the existing delivery-service pattern. Process it immediately after commit and through startup and periodic recovery.
- Reopening a task before delivery supersedes its pending completion. Completing it again creates a new event.
- Start with newly created handoffs. Do not backfill historical tasks or emit old completion notices.
**Run an agent-authored follow-up.**
- Add a dedicated completion wake reason in `server/src/services/agent-conversations.ts` and `server/src/services/heartbeat.ts`.
- Wake an idle conversation. If a reply is underway, queue a subsequent turn; never treat an update to the running turn’s stored context as delivery.
- At turn start, collect pending completions for that conversation session. Completions arriving after that point wait for another turn.
- Supply current task status, task links, saved deliverable references, and bounded result content. Keep task output separate from trusted instructions.
- Tell the agent to explain what finished and provide access to the result. Do not prescribe canned wording or authorize additional execution.
- Use the existing conversation response path. Record delivery against the persisted agent reply, not merely an enqueued or successful run.
**Handle recovery and onboarding.**
- Carry delivery IDs through wakeups, retries, and response persistence. Before retrying, check for an already-published reply; make publication and delivery acknowledgement atomic.
- Use leased claims and bounded retries following existing delivery conventions. Retain exhausted failures for diagnosis.
- Check the conversation’s session generation before scheduling and publishing. Suppress old-session deliveries after `/new`.
- Add `issue_children_completed` to the existing follow-up scheduling rule so onboarding completion cannot disappear into an active parent run. Refresh completion facts when that follow-up starts. Apply this scheduling change to onboarding flows; preserve existing scheduling for other handoffs.
- Preserve company access, agent permissions, budgets, and pause controls. Result delivery must not broaden general access to other tasks.
No new user-facing tool, notification settings, onboarding option, or UI component is required. Add the database migration and synchronized internal types; preserve existing public task-creation inputs.
## Tests and evals
**Deterministic service and integration tests**
Cover both task-creation paths and both completion paths, including:
- Idle versus actively replying conversations.
- Multiple pending completions combined without losing task identities.
- Duplicate events, concurrent dispatchers, and restart recovery.
- Crashes before dispatch and after reply persistence.
- `/new`, task reopening, cross-company inputs, and paused agents.
- Onboarding child completion during an active parent turn.
- Existing Agent Chat availability controls and unaffected behavior for other agent handoffs.
**Product E2E evals**
Extend the explicit `completion-updates` suite for native Claude and Codex:
- Retain the existing onboarding and idle-handoff stories.
- Add completion during an active chat reply, multiple-task completion, and interrupted completion-reply recovery.
- Establish each timing boundary with observable run state and bounded fixture gates. Use real browser/API actions; do not inject completion messages or edit database state.
- Require durable Done status, saved output, an unsolicited source reply, working result access, and no duplicate completion update.
Add a separate completion-accuracy grader using the existing pinned judge infrastructure. It must cite recorded evidence and reject stale promises such as “the work will run next” after completion. Calibrate it against accurate replies, stale replies, unsupported claims, and later corrections. Missing judge evidence leaves accuracy unqualified; it cannot override a mechanical failure.
Run the selected cells in GitHub Actions. Preserve source revisions, grader versions, attempts, timing, cleanup, and cost coverage. Require two consecutive complete campaigns to pass delivery, access, and accuracy before calling this scope qualified.
## Rollout and defaults
- Enable completion follow-ups wherever Agent Chat is already enabled; leave native-runner onboarding selection unchanged.
- Report Done transitions only. Progress, blocked, failed, and cancelled notifications remain outside this change.
- Report each task as it finishes, combining already-pending completions without an artificial batching delay.
- Suppress old-session updates after `/new`; keep tasks and results intact.
- Record pending, delivered, superseded, and exhausted events in instance-local diagnostics.
- Submit a separate product PR with unit/integration coverage, live eval reports, and passing merge checks. The previously merged eval baseline remains the red reference.
## Implementation security refinement
Completion input carries server-recorded task identifiers, Done status, timestamps,
and result links. Worker-authored titles, document bodies, and comments are not
copied into the source agent's prompt; the result remains on the linked task.
Tool authority and provider permissions retain their normal configured defaults.
The original agent writes the completion reply from these facts and its existing
conversation context. Evals independently read the saved output and grade the
reply's truthfulness and result access.
+30
View File
@@ -0,0 +1,30 @@
# Cursor, Copilot and Pi through rich ACP
Approved implementation plan, 2026-09-28. Shared base:
`c65fc9e3c81c41aafe421aa90a00514b84343285`.
Keep the existing ACPX runner transport and Codex app-server richness benchmark.
Implement a foundation prerequisite, then three separate provider branches and
PRs. Do not merge automatically. Preserve legacy Cursor/Pi adapters.
1. Investigate each pinned harness, its native API, ACP adapter, observed wire,
runner normalization and UI surface. Inventory every meaningful event field.
2. Extend shared profile declarations, durable permissions/input, extension
dispatch, steering/queue distinctions, artifact provenance and bounded display
activity. Preserve existing replay and exact warm-session identities.
3. Pin complete provider distributions for macOS ARM64/x64 and Linux x64. Use
verified launches, isolated credentials/configuration and build-time assets.
4. Implement Cursor native ACP and its question/plan/subagent extensions;
Copilot ACP authentication/events/permission and settlement handling; Pi
wrapper patch and owned extension with authenticated tools and input policy.
5. Run deterministic, recovery, isolation and richness conformance. Qualify each
provider locally and in Daytona with product E2E evidence before advertising
support. Use a combined $100 ceiling: $25 each plus $25 diagnosis reserve.
6. Run required full checks, prepare one foundation and three dependent PRs,
retain evidence and deliver the comparative capability report. List every
unused/partial capability with reason and prioritized follow-up.
Implementation and qualification status is maintained in
[the capability report](../architecture/runner-rich-acp-capabilities.md).
Missing credentials or spend verification leave qualification pending; they do
not authorize a weaker supported profile or an implicit model fallback.
@@ -0,0 +1,39 @@
# Browser Use Cloud connector — 2026-09-29
Approved implementation: connect Browser Use Cloud using its v4 REST API and the
normal Access → Connect → Permissions flow. API keys remain in the vault; actions
use the existing governed gateway. Hosted browser tasks are destructive-capable.
2026-09-30 clarification: the app key and runtime skill name are
`browser-use-cloud`. Bundle instructions with the connector implementation,
outside universal `skills/`, and contribute them only with authorized connection
tools in the agent's task run. Remove them when access is revoked. Preserve
pre-release connection and cost records when migrating the old provider key.
Persist company/task/agent/credential-grant ownership, conversation sessions,
individual runs with durable event cursors and accounting checkpoints, and browser
instances separately. Observe browser.ready while work proceeds. Reconcile after
restart; never replay an uncertain paid create. Continue idle conversations with
POST /runs and sessionId. Cancel run and stop browser are separate operations.
Show an interactive Browser tab beside Properties and Artifacts. Auto-open the
arrival of each new session once, respect subsequent navigation and closes, preserve the iframe
across tab switches, and allow reopening through the launcher. Closing a tab only
hides it. Viewer credentials are fetched on demand by authorized humans, never
returned to agents, persisted in tab state, or included in logs or live events.
Completed browsers stay open while viewed, then have a ten-minute idle grace period with Keep open. Task/run
cancellation, reassignment, budget stops, and credential revocation trigger durable
cleanup. Provider expiry wins. Followups use normal task conversations and tools.
Existing profiles are optional, explicitly allowed per credential. Fresh browsers
are the default. Recording and automatic artifact import are deferred.
Enforce v4 maxCostUsd from applicable remaining budgets and configured limits;
account cumulative provider usage idempotently. Verify deterministic transport,
ownership, policy, lifecycle, event recovery, viewer authorization, and UI states;
then verify with a live credential if available. Never claim fixture evidence as
live provider proof.
Sources: https://docs.browser-use.com/cloud/api-v4-overview,
https://docs.browser-use.com/cloud/openapi/v4.json,
https://docs.browser-use.com/cloud/browser/live-preview.
+92
View File
@@ -34,6 +34,47 @@ credential material are never written to the run log.
These records remain run-log events. They do not create an OpenTelemetry or
Paperclip Telemetry export, and legacy adapters do not use this writer.
## Semantic Settlement Diagnostics and Retry Logs
Before interrupting a Codex turn, the runner durably records a
`harness.diagnostic` event with code `provider_interrupt_requested`, the stop
reason, provider turn ID, and pending tool call IDs. A harmless identical result
replay records `semantic_tool_result_duplicate` with warning severity, call ID,
operation ID, and result digest. It does not fail the task or create a user
attention request. A true conflict includes the call and operation IDs and both
result digests in its error; full arguments belong to the canonical semantic
input record, not the error message.
Incomplete close emits the bounded `native_session_settlement_incomplete`
runner diagnostic. Its evidence includes runner suspension, provider drain,
final provider state, pending call/operation/source-event IDs and input digests,
and incomplete result-delivery command IDs and statuses. If execution and
cleanup both fail, execution retains its original error identity and cleanup is
attached as `cleanupError`.
Instruction writes also commit an `agent.instruction_write_attempted` activity
row and a run-scoped `instructionToolAttempts` entry before permitting the
filesystem effect. They retain the call ID, operation ID, and input digest, not
instruction text. A later transaction rollback cannot erase this attempt proof.
A missing success or definite pre-write failure receipt means the outcome is
unknown, even if the current file contains the requested text. Known validation
and stale-base failures are saved under the attempt's `failure` entry and replay
their original status, message, and details without a new write. Unknown
outcomes remain blocked until reconciled; a
committed receipt with a lost acknowledgement can replay its exact result.
The NDJSON output log appends across repeated `begin` calls for one run. Each
new handle adds an `attemptId` to its lines. If the local file is absent and a
durable S3 mirror exists, `begin` restores that prefix before appending. A
failed restore must not replace the mirror with an empty or partial attempt.
Publishing a restored prefix uses an atomic create-if-absent operation, so a
concurrent restore cannot overwrite lines another attempt has already appended.
Earlier attempts therefore remain available for incident diagnosis. Existing
records without `attemptId` remain readable.
These records use the instance run log and its configured storage. They add no
Paperclip Telemetry or OpenTelemetry export.
## Omitted Unsafe Workspace Export
`workspace_export_omitted` is an informational system event in the local run log.
@@ -155,6 +196,40 @@ The payload never carries a command, an argument, a path, an environment value,
or a raw identifier. The event rides the `ctx.onEvent` run-event bridge and is
run-log-only. It needs no OTLP endpoint.
## ACP terminal failure diagnostics
The shared ACP adapter engine preserves typed terminal session failures in the
run error, the `acpx.error` transcript record, and
`heartbeat_runs.result_json.terminalSessionFailure`. The structured diagnostic
contains the provider category, title, and details. It works when raw provider
tracing is disabled. The existing UI and CLI render the diagnostic as an error,
not as assistant output or an automatic task response.
Issue continuation summaries and session-compaction handoffs retain only the
generic failure category; provider diagnostic prose is not copied into prompts.
Both pinned ACPX patches pass complete title and detail strings to the in-memory
callback. The engine redacts configured environment values (including resolved
secrets with arbitrary variable names), launch environment values outside a
closed allowlist of public process settings, known boolean flags, and run identifiers,
connection URL passwords, the run API key, and common credential forms before
truncation. It removes control characters,
retains line breaks for JSON and stack traces, and preserves up to 4,096 title
characters and 24,576 detail characters. These bounds also keep the escaped
transcript JSON below the server's 64 KiB chunk limit. Longer fields end with an explicit
omission count and appear in `truncatedFields`. The error message includes the
same sanitized text. Ordinary run retrieval preserves the bounded structured
diagnostic even when multibyte text or other result fields exceed the result
byte budget. In that reduced response, title and details have 1 KiB and 8 KiB
byte budgets, including truncation markers. `retrievalTruncated` directs callers
to the full adapter-bounded text in the run error or transcript. Other provider
metadata and action payloads are not copied.
Recovery still uses the typed failure category and the adapter's existing
classifier. Provider warnings do not become failures, and timeouts or lost
control channels keep their authoritative failure messages. These diagnostics
stay in the instance's run records and configured run-log storage. They add no
Paperclip Telemetry or OpenTelemetry export.
## Related instrumentation
The sandbox duplex transport also writes one run-log event as one of its three
@@ -243,3 +318,20 @@ The message distinguishes an automatic retry from work that is no longer eligibl
This pre-provider wait records `ai_connection_busy` on the cancelled run and does
not consume the provider-failure retry allowance. The event contains no credentials
and creates no Telemetry or OpenTelemetry export.
## Managed Agent File Save Receipts
The server writes `instruction_save` after managed file collection or a warm
turn checkpoint. The payload includes the save state, instruction entry path,
storage warning, and error code/message. Agent-directory receipts identify
`contract: "agent_files"` and the applied candidate hash. Legacy instruction
receipts instead identify the saved revision.
A validated warm checkpoint reports `saved` or `unchanged`, even though its
working directory remains owned by the live session. An unstable checkpoint
reports `pending_collection` until stopped collection produces a final receipt.
Successful checkpoints can include `checkpointStats`: `scannedEntries`,
`hashedBytes`, `copiedFiles`, and `copiedBytes`. These counts describe that
capture, not cumulative traffic or an atomic snapshot of background writers.
They contain no file contents. The receipt remains in the instance run log;
it adds no Paperclip Telemetry or OpenTelemetry export.
+6 -3
View File
@@ -9,10 +9,10 @@ WORKDIR /workspace/packages/paperclip-runner/runner
COPY packages/paperclip-runner/runner/Cargo.toml packages/paperclip-runner/runner/Cargo.lock ./
COPY packages/paperclip-runner/runner/crates ./crates
COPY packages/paperclip-runner/protocol ../protocol
RUN cargo build --locked --release -p paperclip-runner-core --bin paperclip-runnerd \
RUN cargo build --locked --release -j 2 -p paperclip-runner-core --bin paperclip-runnerd \
&& strip /workspace/packages/paperclip-runner/runner/target/release/paperclip-runnerd
FROM node:24-bookworm@sha256:9137a20e25879e0b557227b57e3ee4e9af4bde29eb3db66134cd1723e84f830b AS provider-pack-build
FROM node:24.21.0-bookworm@sha256:5a750d3be5e5c80275f8c9a5367c3aed99c2875656590c8d0701c7ee687f5f0a AS provider-pack-build
RUN corepack enable && corepack prepare pnpm@9.15.4 --activate
WORKDIR /workspace
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc tsconfig.base.json ./
@@ -28,15 +28,18 @@ COPY cli/package.json ./cli/package.json
# The complete resolved lock (including transitive integrity hashes) is reviewed.
# Reject registry-time drift BEFORE installing packages or running lifecycle code.
# Refresh this digest together with source/provider dependency changes.
ARG PAPERCLIP_RUNNER_LOCK_SHA256=0c06ab38c1d4b6b58e1c9f5ee85eb79f97cecd83e751b5d27c3b262faf5ab265
ARG PAPERCLIP_RUNNER_LOCK_SHA256=2c46a68811ba1d504a41b6f4d3f642bc93f4a1c615097754c9c6486881dba8e6
RUN printf '%s pnpm-lock.yaml\n' "${PAPERCLIP_RUNNER_LOCK_SHA256}" > /tmp/provider-lock.sha256 \
&& sha256sum -c /tmp/provider-lock.sha256 \
&& pnpm install --frozen-lockfile --filter '@paperclipai/paperclip-runner...'
ARG PAPERCLIP_RUNNER_SOURCE_REVISION
# Optional qualification assets; this never promotes candidate profiles.
ARG PAPERCLIP_RUNNER_CANDIDATE_PROVIDERS=
RUN test -n "${PAPERCLIP_RUNNER_SOURCE_REVISION}"
RUN pnpm --filter @paperclipai/paperclip-runner build:typescript \
&& PAPERCLIP_RUNNER_SOURCE_REVISION="${PAPERCLIP_RUNNER_SOURCE_REVISION}" \
node packages/paperclip-runner/scripts/build-provider-pack.mjs /provider-pack \
"--candidate-providers=${PAPERCLIP_RUNNER_CANDIDATE_PROVIDERS}" \
&& chmod -R a+rX /provider-pack
# Fleet sandbox base image. Keep this section aligned with
+37
View File
@@ -94,6 +94,12 @@ full Git SHA as `PAPERCLIP_RUNNER_SOURCE_REVISION`.
Do not bake provider credentials, Paperclip bootstrap tickets, or Daytona
preview tokens into this image. They remain per-run secret material.
The provider-pack build pins the official Linux x64 Node 24.21.0 image by
manifest digest. Its bundled Undici is 7.29.1, which fixes
[GHSA-3wwx-pv8p-q78v](https://github.com/advisories/GHSA-3wwx-pv8p-q78v).
Pi separately verifies its private Node executable and nested npm dependency;
changing the outer interpreter does not replace either provider-owned pin.
Provider CLI updates are manifest-only changes: repository CI owns the root
lockfile. Resolve the complete workspace manifest graph in the build context
before invoking Docker, matching CI when a source commit precedes the lockfile
@@ -106,6 +112,37 @@ verifies the downloaded artifact, then passes that artifact's SHA-256 as the
`PAPERCLIP_RUNNER_LOCK_SHA256` build argument. The Dockerfile checks the resolved
lock against this value before installation. The fixed Dockerfile default is
for standalone builds; it must not replace a campaign's verified lock digest.
Refresh the default from the clean tracked lockfile using the exact
`pnpm install --resolution-only --ignore-scripts --no-frozen-lockfile` command,
and verify a second resolution preserves the digest. A lockfile left by a
filtered or incremental install can retain stale importer patch identities.
Refresh exact runtime versions and qualification digests together; do not
download dependencies when a task starts.
## Candidate ACP qualification assets
Provider branches can build their pinned assets with
`node packages/paperclip-runner/scripts/build-provider-pack.mjs /absolute/pack --candidate-providers=cursor`
(or `copilot` or `pi`). The source revision must include the named provider's
builder. Assets are installed at build time under `provider-assets/<provider>/<platform>-<architecture>`.
The pack manifest binds each complete asset tree. Runtime admission separately
checks the provider's source-owned closure pins and copies a verified launch snapshot.
A pack with candidate assets does not qualify or enable that provider.
For an isolated Linux x64 Daytona qualification image, pass
`--build-arg PAPERCLIP_RUNNER_CANDIDATE_PROVIDERS=cursor` with the normal build arguments.
Compute its content ID with the same selection:
`pnpm --silent test:e2e:runner:image-id --candidate-providers=cursor`.
Candidate assets and the default pack have distinct image identities. Never reuse
the default image's content ID for a candidate build.
Use each provider branch's recorded version and explicit model. Keep credentials
out of images. Paid qualification requires bound provider and Daytona credentials,
inspectable spend, and the shared $100 ceiling recorded in the capability report.
The pack builder tests its copied Node interpreter after relocation. Use a
standalone Node distribution if the host interpreter depends on a package manager's
private shared libraries. Pi additionally pins its complete portable interpreter
and npm dependency graph.
Refresh exact runtime versions and qualification digests together; never download
dependencies when a task starts. Grok's additive native ACP profile keeps its
qualified 1.0.13 executable at the verified sandbox prerequisite path. It does not replace the
+9
View File
@@ -137,3 +137,12 @@ Rough tiers, richest first:
## UI Parser Contract
External adapters can ship a self-contained UI parser that tells the Paperclip web UI how to render their stdout. Without it, the UI uses a generic shell parser. See the [UI Parser Contract](/adapters/adapter-ui-parser) for details.
### Cursor failure details
The Cursor CLI adapter uses structured error output first, then the first
nonempty diagnostic line. It skips the informational `cursor-retrieval: tracing
to ...` file-location notice. If the CLI exits unsuccessfully with only that
notice, the run shows the exit code. The original stdout and stderr remain in
the local run result and log for troubleshooting. Environment probes use the
same diagnostic selection. This does not change retry or success decisions.
+1 -1
View File
@@ -175,7 +175,7 @@ POST /api/issues/{issueId}/comments
{ "body": "Progress update in markdown..." }
```
@-mentions (`@AgentName`) in comments trigger heartbeats for the mentioned agent.
Agent @-mentions are context only and do not trigger heartbeats. Normal comment feedback can still wake the current assignee. Use explicit assignment or a review request to ask another agent to act.
## Issue-Thread Interactions
+1 -1
View File
@@ -93,7 +93,7 @@ Daytona snapshot for future leases.
their management endpoints with `403 settings_operator_managed`; hiding
`instance.experimental` floors every experimental toggle write.
- Any Instance → General section: `instance.general.censorUsernameInLogs`,
`instance.general.keyboardShortcuts`, `instance.general.backupRetention`,
`instance.general.backupRetention`,
`instance.general.feedbackDataSharingPreference` (each also rejects
value-changing writes via `PATCH /api/instance/settings/general`), plus the
UI-only `instance.general.deploymentStatus` and `instance.general.signOut`.
@@ -41,22 +41,16 @@ Submitted CTO hire request and linked it for board review.
## @-Mentions
Mention another agent by name using `@AgentName` in a comment to wake them:
Use a structured agent link to identify someone relevant to the task:
```
POST /api/issues/{issueId}/comments
{ "body": "@EngineeringLead I need a review on this implementation." }
{ "body": "[@Engineering Lead](agent://agent-id) has relevant context on this implementation." }
```
The name must match the agent's `name` field exactly (case-insensitive). This triggers a heartbeat for the mentioned agent.
Resolve the agent ID from the company’s agent list. Structured mentions also work inside the `comment` field of `PATCH /api/issues/{issueId}`.
@-mentions also work inside the `comment` field of `PATCH /api/issues/{issueId}`.
## @-Mention Rules
- **Don't overuse mentions** — each mention triggers a budget-consuming heartbeat
- **Don't use mentions for assignment** — create/assign a task instead
- **Mention handoff exception** — if an agent is explicitly @-mentioned with a clear directive to take a task, they may self-assign via checkout
Mentions are context only. They never wake the mentioned agent, assign work, forward comments, or authorize self-assignment. Normal feedback can still wake the current assignee. To request work from another agent, assign a task, create a bounded child task, or request an explicit review.
## Structured Decisions
+129 -8
View File
@@ -63,10 +63,10 @@ its contents require. The level is **derived from the files**, not self-declared
| `assets` | Markdown plus images/PDFs/other static files | No executable code |
| `scripts_executables` | Any script (`.sh`, `.js`, `.py`, `.ts`, …) | Highest scrutiny |
Trust level gates what can be imported. A skill that carries executable scripts **cannot
be imported from an external source** (GitHub, `skills.sh`, or a raw URL) — only
first-party bundled catalog skills are allowed to ship scripts. This keeps untrusted
remote code out of your agents' hands.
GitHub sources support scripts after the existing content audit passes. Importing
never executes scripts, hooks, dependency installers, or builds. Raw URL and
`skills.sh` imports retain their existing script restrictions. Unsafe content,
secrets, invalid paths, and oversized files are still rejected.
## Where skills come from (source types)
@@ -81,10 +81,9 @@ A skill in your company library records where it originated. The Store shows thi
| `url` | URL | Imported from a raw markdown URL |
| `local_path` | Local | Created in-app or scanned from a project workspace on disk |
External imports (`github`, `skills_sh`, `url`) are held to two rules: they must be
`markdown_only` or `assets` (no scripts), and Git-backed sources **must resolve to a
pinned 40-character commit SHA** before import, so a moving branch can never silently
change what your agents run.
Git-backed sources **must resolve to a pinned 40-character commit SHA** before
import. A moving branch cannot change what agents run until you refresh its source.
GitHub originals are read-only; use **Make a copy** to edit an independent local skill.
### Thin wrappers for external live playbooks
@@ -119,6 +118,128 @@ upstream catalog skill has changed.
- Re-installing an already-installed catalog skill updates it in place rather than
creating a duplicate.
### Sync skills from GitHub
Open **Skills → Sources → Import from GitHub** (also in the **New** menu), choose a connected repository or
click **… or add public repo by URL** to paste a GitHub.com repository URL, then choose **Find skills**. The searchable list
combines repositories from all GitHub connections you can access, with duplicates
removed. Paperclip automatically uses an authorized connection for the chosen
repository. Pasted URLs also use your authorized connections when available,
including public repositories outside the list. Public repositories can use
anonymous access when no connection succeeds; private repositories require access.
A repository URL uses the default branch. Paste a branch URL such as
`https://github.com/owner/repo/tree/feature/new-skills` to track another branch,
including branches whose names contain slashes. **Add repos**
opens the standard setup in Apps. Completing or cancelling setup returns you to
the importer with your draft retained; the repository list refreshes automatically.
Use the refresh button beside the repository count to reload accessible repositories.
The searchable tree starts with every discovered skill package checked, including
hidden and deeply nested directories. Each package shows its name and file count;
expand it to see the files included with it. Folder checkboxes select descendant
packages. A package checkbox selects that whole directory, stopping at nested
`SKILL.md` boundaries; nested skills remain independently selectable. Supporting
files cannot be deselected individually.
Use the inspection button or click an included file to open a read-only preview.
The inspector shows the repository path, scanned commit, byte sizes, executable
flags, and any `compatibility` requirements declared by the author. Text previews
show up to 64 KiB; binary assets show metadata and a link to GitHub for viewing or
download. Preview reads use the current caller's GitHub access and re-audit the
package at that exact commit. Preview failures do not alter selection or installed
content. Existing sources gain inspection metadata on their next successful refresh.
**Check references** flags detectable Markdown links and inline-code relative
resource paths that are missing or outside the package, including files belonging to
nested skills. This is a best-effort check, not a complete dependency analysis;
references do not cause external files to be imported automatically. Fix the source
or leave the package unchecked if it needs those files. Declared requirements are
shown separately from these warnings and from content-audit notices.
Validation errors appear beside affected skills; eligible selections import and
skipped skills are reported. These skills become available in the current company's
library and agent skill picker. Files are limited to 1 MiB each and a scan to 100 MiB
of expanded skill content, counting repeated file copies. A scan supports up to
1,000 skill packages and 10,000 package files. The repository path index includes
directories and is limited to 100,000 paths, with paths up to 4,096 characters and
64 levels deep. Discovery retains audited manifests without retaining every
package's contents. Importing never runs scripts, hooks, dependency installation,
or builds.
Repeated scans resolve the branch's current commit and reuse the caller's cached
snapshot when it is unchanged. Each server process allows one active scan per
caller and two per company. Per minute, callers can open 30 snapshots and download
6 repositories; companies can open 60 snapshots and download 12 repositories.
Cached scans do not consume the download quota. A limit returns a retry message;
installed skills remain available.
**Skills → Sources** shows repositories, installed skill links, and connection errors.
Click a repository title to open it on GitHub. Its three-dot menu contains
**Refresh**, **Select skills**, and **Disconnect source**. **Refresh** applies valid
updates immediately. New skills wait for
**Select skills → Save selection** before import. Previously declined skills stay
unchecked, including additions beneath excluded folders.
Unchecking an installed skill stops future syncing and keeps its content and agent
assignments. **Disconnect source** does the same for the whole repository. Remove
an installed skill separately if you want it gone from the library. Upstream deletion
shows **Removed from source** and retains the installed version; a moved path is a
new skill, not an inferred rename.
Installed content is stored locally, so viewing, testing, and agent execution work
without GitHub access. Updates preserve skill identity, organization, assignments,
and history. New versions are created only when package bytes or executable modes
change; active runs and pinned versions keep their existing content. Skill Studio
can view and test originals. **Make a copy** creates an independent editable skill.
Older imports with no saved tracking branch resolve the repository’s default branch
on their first successful refresh. If an explicit source already tracks that branch,
the adopted source keeps a **Default branch** alias so both sets of installed skill
identities and selections remain intact. Explicit branch and commit pins are preserved.
Recognizable older GitHub imports are adopted into Sources without refetching or
changing content; the first successful refresh completes their local snapshots.
Bundled/catalog, project/local, unsupported-host, and `skills.sh` imports keep their
existing behavior. Sync is manual and GitHub.com-only; upstream editing and pull
requests are not part of this milestone.
While finding skills, the dialog shows the scan stages, the current file, and
recently checked skills as their local packages are validated. Large repositories show real
package and file counts; you can cancel and retry without importing partial
results. Selection opens only after the full scan completes. Importing displays
an animated saving state while complete local packages are prepared. Animations
respect reduced-motion preferences.
Paperclip fetches one shallow Git snapshot, preserving committed bytes and executable
permissions without checking out or running repository code. Download progress shows
Git's measured receiving and preparing percentages before discovery begins. The server
requires Git. Downloads are limited to 128 MiB and three minutes; at most two downloads
run concurrently. Up to four temporary snapshots (including active downloads) are kept
for ten minutes, scoped to the company and caller's authorization. Preview and import
can reuse the exact scanned commit after checking access again. Installed skills remain
local and independent of this temporary cache. GitHub's API is still used for repository
identity and the connection picker, so metadata requests can still encounter rate limits.
The discovery API keeps its normal JSON response. Clients requesting
`Accept: application/x-ndjson` receive `progress` and `candidate` events followed
by `complete` (containing the discovery response), or a terminal `error`. A stream
ending without `complete` is unsuccessful; partial candidates are not importable.
Progress contains metadata only, never package file contents.
Source APIs live beneath `/api/companies/:companyId/skill-sources`:
| Method/path | Purpose |
|---|---|
| `GET /` and `GET /:sourceId` | List sources and entries |
| `GET /repositories` | Browse repositories through existing GitHub grants |
| `POST /discover` | Discover and validate packages at one commit |
| `POST /preview` | Preview an audited package file at an immutable commit using current caller access |
| `POST /` | Import selected packages at the discovered commit |
| `PATCH /:sourceId` | Save selection/exclusions and optional connection, with revision check |
| `POST /:sourceId/refresh` | Refresh selected skills; discover new ones for review |
| `DELETE /:sourceId` | Disconnect while retaining installed skills |
### Import from an external source
Paste a source and Paperclip fetches and imports it. Accepted forms include:
+1 -1
View File
@@ -72,7 +72,7 @@ paperclip/
When a heartbeat fires:
1. **Trigger** — Scheduler, manual invoke, or event (assignment, mention) triggers a heartbeat
1. **Trigger** — Scheduler, manual invoke, or event (assignment, assignee feedback) triggers a heartbeat
2. **Adapter invocation** — Server calls the configured adapter's `execute()` function
3. **Agent process** — Adapter spawns the agent (e.g. Claude Code CLI) with Paperclip env vars and a prompt
4. **Agent work** — The agent calls Paperclip's REST API to check assignments, checkout tasks, do work, and update status
@@ -39,6 +39,13 @@ import {
import { captureLocalProcess, capturedProcessExited, killCapturedLocalProcess } from "./local-process-control.js";
import type { DuplexLossReason } from "../duplex-observability.js";
import { DUPLEX_CHANNEL_LOST_ERROR_CODE } from "../bridge-transport-contract.js";
import {
formatTerminalSessionFailure,
sanitizeTerminalSessionFailure,
type AcpxTerminalSessionFailure,
type AcpxTerminalSessionFailureDiagnostic,
} from "./terminal-session-failure.js";
export type { AcpxTerminalSessionFailure } from "./terminal-session-failure.js";
import type { WorkspaceRestoreFailureCode, WorkspaceRestoreOutcome } from "../workspace-restore-merge.js";
import {
classifyWorkspaceRestoreFailure,
@@ -353,12 +360,6 @@ export interface AcpxRemoteManagedHomeResult {
disposeStaged?: () => Promise<void>;
}
export interface AcpxTerminalSessionFailure {
category: string;
title?: string;
details?: string;
}
export type AcpxTerminalFailureClassification = Pick<
AdapterExecutionResult,
"errorCode" | "errorFamily" | "retryNotBefore"
@@ -2058,7 +2059,11 @@ async function buildRuntime(input: {
paperclipClaudeSettings = await writePaperclipClaudeSettings({
cwd,
stateDir,
agentHome,
// A run's writable AGENT_HOME moves, but the existing permission root
// must not invalidate its resumable conversation. Run copies are covered
// by the company root already granted in these settings; the process env
// still receives this run's actual AGENT_HOME.
agentHome: asString(workspaceContext.agentHomeForPermissions, agentHome),
companyId: agent.companyId,
});
skillCommandNotes.push(
@@ -4094,6 +4099,7 @@ export function createAcpxEngineExecutor(deps: AcpxEngineExecutorOptions = {}) {
// (the cancel-before-close order). The turn wrapper assigns it in `turnStart`.
let activeTurn: AcpRuntimeTurn | null = null;
let terminalFailureClassification: AcpxTerminalFailureClassification | null = null;
let terminalSessionFailure: AcpxTerminalSessionFailureDiagnostic | null = null;
// How the settlement `endSession` step must release the runtime for the path
// this run took. Each exit path that acquired the runtime records it before it
// returns; a build or create-runtime failure never registers the runtime, so
@@ -4775,14 +4781,14 @@ export function createAcpxEngineExecutor(deps: AcpxEngineExecutorOptions = {}) {
timeoutMs: startTimeoutMs,
signal,
// The callback belongs to this turn, including when a runtime is reused.
// Raw provider text must never enter the result or the run log.
...(deps.classifyTerminalSessionFailure
? {
onTerminalSessionFailure: (failure: AcpxTerminalSessionFailure) => {
terminalFailureClassification = deps.classifyTerminalSessionFailure!(failure, new Date(now()));
},
}
: {}),
// Diagnostics are retained even when an adapter has no recovery
// classifier. Redact before bounding so partial secrets cannot leak.
onTerminalSessionFailure: (failure: AcpxTerminalSessionFailure) => {
terminalSessionFailure = sanitizeTerminalSessionFailure(
failure, prepared.env, ctx.authToken, parseObject(ctx.config.env),
);
terminalFailureClassification = deps.classifyTerminalSessionFailure?.(failure, new Date(now())) ?? null;
},
});
activeTurn = turn;
// A latched sandbox duplex-channel loss otherwise has no way to reach
@@ -4995,11 +5001,14 @@ export function createAcpxEngineExecutor(deps: AcpxEngineExecutorOptions = {}) {
skipRemoteClose: channelLost,
};
const failureDiagnostic = !timedOut && !channelLost && terminal.status === "failed"
? terminalSessionFailure
: null;
const errorMessage = timedOut
? formatAdapterExecutionTimeoutErrorMessage(prepared.timeoutResolution)
: channelLost
? channelLostMessage
: resultErrorMessage(terminal);
: formatTerminalSessionFailure(resultErrorMessage(terminal), failureDiagnostic);
const terminalStopReason = terminal.status === "failed" ? terminal.error.message : terminal.stopReason;
const classifiedFailure = !timedOut && !channelLost && terminal.status === "failed"
? terminalFailureClassification
@@ -5038,6 +5047,7 @@ export function createAcpxEngineExecutor(deps: AcpxEngineExecutorOptions = {}) {
costUsd: turnUsage.costUsd,
resultJson: {
status: channelLost ? "failed" : terminal.status,
...(failureDiagnostic ? { terminalSessionFailure: failureDiagnostic } : {}),
...(classifiedFailure?.errorFamily ? { errorFamily: classifiedFailure.errorFamily } : {}),
...(classifiedFailure?.retryNotBefore
? {
@@ -16,6 +16,12 @@ const fixturePath = path.join(
);
const tempRoots: string[] = [];
async function writeFailureFile(root: string, title: string): Promise<string> {
const file = path.join(root, "failure.json");
await fs.writeFile(file, JSON.stringify({ title, category: "request" }));
return file;
}
afterEach(async () => {
await Promise.all(
tempRoots
@@ -61,7 +67,7 @@ it("spawns a real Node ACP agent with per-session env on this platform", async (
expect(stderr).toContain("paperclip-acp-echo-agent started");
});
it("fails closed on a typed ACP session failure without exposing its provider text", async () => {
it("retains a typed ACP failure as diagnostics without making it assistant output", async () => {
const root = await fs.mkdtemp(
path.join(os.tmpdir(), "paperclip-acpx-typed-failure-"),
);
@@ -80,7 +86,7 @@ it("fails closed on a typed ACP session failure without exposing its provider te
mode: "oneshot",
stateDir: path.join(root, "state"),
cwd: repoRoot,
env: { PAPERCLIP_ACPX_TYPED_FAILURE_CANARY: providerText },
env: { PAPERCLIP_ACPX_TYPED_FAILURE_FILE: await writeFailureFile(root, providerText) },
},
context: {},
onLog: async (_stream: string, text: string) => logs.push(text),
@@ -89,8 +95,10 @@ it("fails closed on a typed ACP session failure without exposing its provider te
expect(result.exitCode).toBe(1);
expect(result.errorCode).toBe("acpx_turn_failed");
expect(JSON.stringify(result)).not.toContain(providerText);
expect(logs.join("\n")).not.toContain(providerText);
expect(result.errorMessage).toContain(providerText);
expect(result.resultJson?.terminalSessionFailure).toMatchObject({ title: providerText });
expect(result.summary).not.toContain(providerText);
expect(logs.join("\n")).toContain(providerText);
expect(result.summary).toContain("terminal request failure");
});
@@ -114,7 +122,7 @@ it("fails closed on a typed ACP session failure in persistent mode", async () =>
warmHandleIdleMs: 0,
stateDir: path.join(root, "state"),
cwd: repoRoot,
env: { PAPERCLIP_ACPX_TYPED_FAILURE_CANARY: providerText },
env: { PAPERCLIP_ACPX_TYPED_FAILURE_FILE: await writeFailureFile(root, providerText) },
},
context: {},
onLog: async (_stream: string, text: string) => logs.push(text),
@@ -123,8 +131,10 @@ it("fails closed on a typed ACP session failure in persistent mode", async () =>
expect(result.exitCode).toBe(1);
expect(result.errorCode).toBe("acpx_turn_failed");
expect(JSON.stringify(result)).not.toContain(providerText);
expect(logs.join("\n")).not.toContain(providerText);
expect(result.errorMessage).toContain(providerText);
expect(result.resultJson?.terminalSessionFailure).toMatchObject({ title: providerText });
expect(result.summary).not.toContain(providerText);
expect(logs.join("\n")).toContain(providerText);
expect(result.summary).toContain("terminal request failure");
});
@@ -0,0 +1,134 @@
import { describe, expect, it } from "vitest";
import { formatTerminalSessionFailure, sanitizeTerminalSessionFailure } from "./terminal-session-failure.js";
describe("terminal session failure diagnostics", () => {
it("keeps the provider category, message, request id and stack", () => {
const failure = {
category: "service",
title: "HTTP 529: overloaded_error",
details: 'request_id=req_diagnostic_123\n{"error":{"message":"Service unavailable"}}\n at prompt (agent.js:42:7)',
};
const diagnostic = sanitizeTerminalSessionFailure(failure, {});
expect(diagnostic).toEqual(failure);
expect(formatTerminalSessionFailure("ACP turn failed.", diagnostic)).toBe(
`ACP turn failed.\n${failure.title}\n${failure.details}`,
);
});
it("redacts known credentials and common secret forms without dropping useful context", () => {
const secret = 'opaque / credential "value"';
const runKey = "run-credential-canary";
const diagnostic = sanitizeTerminalSessionFailure({
category: "access",
title: `Request rejected: ${secret}`,
details: [
`request_id=req_123 ${runKey}`,
JSON.stringify({ message: secret }),
`https://example.test/?value=${encodeURIComponent(secret)}`,
'Authorization: Bearer bearer-canary',
'{"api_key":"json-canary"}',
'sk-ant-example-provider-key-canary',
'TOKEN=assignment-canary',
].join("\n"),
}, { PROVIDER_SECRET: secret }, runKey);
const serialized = JSON.stringify(diagnostic);
for (const value of ["opaque", runKey, "bearer-canary", "json-canary", "sk-ant-example", "assignment-canary"]) {
expect(serialized).not.toContain(value);
}
expect(diagnostic.details).toContain("request_id=req_123");
expect(diagnostic.title).toBe("Request rejected: ***REDACTED***");
});
it("redacts before truncation and reports every omitted field", () => {
const credential = "opaque-credential-crossing-the-limit";
const diagnostic = sanitizeTerminalSessionFailure({
category: "service",
title: "t".repeat(5000),
details: `${"d".repeat(24568)}${credential}${"x".repeat(1000)}`,
}, { API_KEY: credential });
expect(diagnostic.truncatedFields).toEqual(["title", "details"]);
expect(diagnostic.title).toContain("[truncated: 904 characters omitted]");
expect(diagnostic.details).not.toContain("opaque");
expect(diagnostic.details).toContain("[truncated:");
expect(diagnostic.details!.length).toBeLessThan(24700);
});
it("redacts configured values with arbitrary names and connection URL passwords", () => {
const databaseUrl = "postgres://user:database%20canary@db.test/database";
const opaque = "arbitrarily-named-secret";
const diagnostic = sanitizeTerminalSessionFailure({
category: "service",
details: `request_id=req_123 ${databaseUrl}\npassword echoed: database canary\n${opaque}`,
}, { DATABASE_URL: databaseUrl, PROVIDER_SETTING: opaque }, undefined, {
DATABASE_URL: databaseUrl, PROVIDER_SETTING: opaque,
});
expect(diagnostic.details).toBe(
"request_id=req_123 ***REDACTED***\npassword echoed: ***REDACTED***\n***REDACTED***",
);
});
it("redacts unknown inherited launch values while preserving public process context", () => {
const diagnostic = sanitizeTerminalSessionFailure({
category: "service",
details: "request_id=req_123 inherited-canary A /workspace",
}, { UNEXPECTED_VARIABLE: "inherited-canary", ANOTHER_VARIABLE: "A", HOME: "/workspace" });
expect(diagnostic.details).toBe("request_id=req_123 ***REDACTED*** ***REDACTED*** /workspace");
});
it("preserves known boolean settings, HTTP codes and request IDs", () => {
const details = "HTTP 401 request_id=req_123 req-1 /1/path retries=1";
const diagnostic = sanitizeTerminalSessionFailure({ category: "access", details }, {
OPENCODE_ALLOW_ALL_MODELS: "1",
}, undefined, { OPENCODE_ALLOW_ALL_MODELS: "1" });
expect(diagnostic.details).toBe(details);
});
it("redacts short credentials even inside words, paths and punctuated strings", () => {
const diagnostic = sanitizeTerminalSessionFailure({
category: "access", details: "upstream abc-def /abc/path xabcx abc.value abc_other",
}, { CUSTOM_SETTING: "abc" }, undefined, { CUSTOM_SETTING: "abc" });
expect(diagnostic.details).not.toContain("abc");
expect(diagnostic.details).toBe(
"upstream ***REDACTED***-def /***REDACTED***/path x***REDACTED***x ***REDACTED***.value ***REDACTED***_other",
);
});
it("fits the persisted transcript chunk limit even with escaped provider text", () => {
const diagnostic = sanitizeTerminalSessionFailure({
category: "service",
title: '"'.repeat(5000),
details: "\\".repeat(40000),
}, {});
const log = JSON.stringify({
type: "acpx.error", summary: "failed", stopReason: "adapter_failed",
message: formatTerminalSessionFailure("ACP agent reported a terminal service failure.", diagnostic),
});
expect(log.length).toBeLessThan(64 * 1024);
expect(JSON.parse(log).message).toContain("[truncated:");
});
it("handles empty, malformed and control-character fields", () => {
expect(sanitizeTerminalSessionFailure({
category: "untrusted-category",
title: " \n",
details: 42 as unknown as string,
}, {})).toEqual({ category: "unknown" });
expect(sanitizeTerminalSessionFailure({
category: "service",
title: "\x1b[31mFailure\x1b[0m\0",
details: "line 1\n\tline 2",
}, {})).toEqual({ category: "service", title: "Failure", details: "line 1\n\tline 2" });
expect(formatTerminalSessionFailure("original error", null)).toBe("original error");
});
it("keeps truncated Unicode valid for JSONB storage", () => {
const diagnostic = sanitizeTerminalSessionFailure({
category: "service",
title: `${"x".repeat(4095)}🚨failure`,
details: "malformed \ud800 detail",
}, {});
expect(diagnostic.title).not.toMatch(/[\ud800-\udfff]/u);
expect(diagnostic.details).not.toMatch(/[\ud800-\udfff]/u);
expect(diagnostic.truncatedFields).toEqual(["title"]);
});
});
@@ -0,0 +1,104 @@
import { redactDiagnosticText, REDACTED_COMMAND_TEXT_VALUE } from "../command-redaction.js";
export interface AcpxTerminalSessionFailure {
category: string;
title?: string;
details?: string;
}
export interface AcpxTerminalSessionFailureDiagnostic extends AcpxTerminalSessionFailure {
truncatedFields?: Array<"title" | "details">;
}
const CATEGORIES = new Set(["connection", "access", "limit", "service", "request", "unknown"]);
// Leave room under the server's 64 KiB run-log chunk limit even when every
// retained character needs JSON escaping. The transcript stores the text once.
const FIELD_LIMITS = { title: 4096, details: 24576 } as const;
// Only conventional public process settings may survive by default. Provider,
// proxy, bridge, and future launch contributions may carry secrets under any
// name. Explicitly configured values are still redacted even for these keys.
const PUBLIC_ENV_KEYS = new Set([
"PATH", "PATHEXT", "SYSTEMROOT", "WINDIR", "COMSPEC", "HOME", "USERPROFILE",
"HOMEDRIVE", "HOMEPATH", "USER", "USERNAME", "LOGNAME", "SHELL", "LANG",
"LANGUAGE", "LC_ALL", "LC_CTYPE", "TZ", "TMPDIR", "TEMP", "TMP", "NODE_ENV",
"XDG_CONFIG_HOME", "XDG_CACHE_HOME", "XDG_DATA_HOME",
"PAPERCLIP_AGENT_ID", "PAPERCLIP_COMPANY_ID", "PAPERCLIP_RUN_ID", "PAPERCLIP_TASK_ID",
]);
const PUBLIC_BOOLEAN_ENV_KEYS = new Set([
"OPENCODE_ALLOW_ALL_MODELS", "CLAUDE_CODE_USE_BEDROCK", "GOOGLE_GENAI_USE_GCA",
"CI", "NO_COLOR", "FORCE_COLOR",
]);
function isPublicBooleanSetting(key: string, value: string): boolean {
return PUBLIC_BOOLEAN_ENV_KEYS.has(key.toUpperCase()) && /^(?:0|1|true|false)$/i.test(value);
}
/** Keep provider diagnostics in the run, after redaction and before truncation. */
export function sanitizeTerminalSessionFailure(
failure: AcpxTerminalSessionFailure,
env: Record<string, string>,
authToken?: string,
configuredEnv: Record<string, unknown> = {},
): AcpxTerminalSessionFailureDiagnostic {
const secrets = Object.entries(env)
.filter(([key, value]) => value && !PUBLIC_ENV_KEYS.has(key.toUpperCase()) && !isPublicBooleanSetting(key, value))
.map(([, value]) => value);
// Configured values can be resolved secret_refs under arbitrary names (for
// example DATABASE_URL). Key-name heuristics cannot establish they are public.
for (const [key, value] of Object.entries(configuredEnv)) {
if (typeof value === "string" && value && !isPublicBooleanSetting(key, value)) secrets.push(value);
}
// A provider may echo just the password from a configured connection URL.
for (const value of Object.values(env)) {
try {
const url = new URL(value);
if (url.password) {
secrets.push(value, url.password, decodeURIComponent(url.password));
}
} catch { /* ordinary environment values are not URLs */ }
}
if (authToken) secrets.push(authToken);
const secretForms = [...new Set(secrets.flatMap((value) => {
const forms = [value, JSON.stringify(value).slice(1, -1)];
// A malformed Unicode credential must not discard the entire diagnostic.
try { forms.push(encodeURIComponent(value)); } catch { /* retain literal forms */ }
return forms;
}))].sort((a, b) => b.length - a.length);
// Replace in one pass so a short value cannot modify a redaction marker
// inserted for a longer value or cause repeated marker expansion.
const secretPattern = secretForms.length > 0
? new RegExp(secretForms.map((value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"), "gu")
: null;
const diagnostic: AcpxTerminalSessionFailureDiagnostic = {
category: CATEGORIES.has(failure.category) ? failure.category : "unknown",
};
for (const field of ["title", "details"] as const) {
const raw = failure[field];
if (typeof raw !== "string" || !raw.trim()) continue;
let text = secretPattern ? raw.replace(secretPattern, () => REDACTED_COMMAND_TEXT_VALUE) : raw;
text = redactDiagnosticText(text)
// Keep line breaks and tabs for provider JSON and stack traces, but strip
// terminal control sequences and characters PostgreSQL cannot store.
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "")
.replace(/[\ud800-\udfff]/gu, "\ufffd")
.trim();
const limit = FIELD_LIMITS[field];
if (text.length > limit) {
(diagnostic.truncatedFields ??= []).push(field);
// Do not split a UTF-16 surrogate pair into invalid JSONB text.
const end = (text.codePointAt(limit - 1) ?? 0) > 0xffff ? limit - 1 : limit;
text = `${text.slice(0, end)}\n[truncated: ${text.length - end} characters omitted]`;
}
if (text) diagnostic[field] = text;
}
return diagnostic;
}
export function formatTerminalSessionFailure(
message: string | null,
diagnostic: AcpxTerminalSessionFailureDiagnostic | null,
): string | null {
if (!diagnostic) return message;
return [...new Set([message, diagnostic.title, diagnostic.details].filter(Boolean))].join("\n");
}
@@ -227,7 +227,7 @@ describe("ACPX engine turn characterization", () => {
runtimeSessionName: "runtime-session",
};
it("passes exactly the six turn inputs to startTurn", async () => {
it("passes the turn inputs and terminal failure callback to startTurn", async () => {
const root = await makeTempRoot();
const stateDir = path.join(root, "state");
let captured: Record<string, unknown> | null = null;
@@ -276,9 +276,10 @@ describe("ACPX engine turn characterization", () => {
const signal = input.signal as AbortSignal;
expect(signal).toBeInstanceOf(AbortSignal);
expect(signal.aborted).toBe(false);
// Exactly the six documented keys are threaded.
// Every adapter receives diagnostics, even without a failure classifier.
expect(input.onTerminalSessionFailure).toBeTypeOf("function");
expect(Object.keys(input).sort()).toEqual(
["handle", "mode", "requestId", "signal", "text", "timeoutMs"].sort(),
["handle", "mode", "onTerminalSessionFailure", "requestId", "signal", "text", "timeoutMs"].sort(),
);
});
@@ -544,6 +544,8 @@ export async function prepareCommandManagedRuntime(input: {
workspaceBaseline?: DirectorySnapshot;
workspaceGitSnapshot?: GitWorkspaceSnapshot | null;
workspaceExclude?: string[];
/** Plain persistent directories include all files, independent of Git and task cache exclusions. */
workspaceFileMode?: "all";
preserveAbsentOnRestore?: string[];
assets?: CommandManagedRuntimeAsset[];
/** Referenced (additional) projects to stage into the sandbox as plain, read-only trees. */
@@ -609,6 +611,7 @@ export async function prepareCommandManagedRuntime(input: {
workspaceBaseline: input.workspaceBaseline,
workspaceGitSnapshot: input.workspaceGitSnapshot,
workspaceExclude: mergeRuntimeExcludes(input.workspaceExclude),
workspaceFileMode: input.workspaceFileMode,
preserveAbsentOnRestore: input.preserveAbsentOnRestore,
assets: input.assets,
additionalSources: input.additionalSources,
@@ -652,6 +655,7 @@ export async function prepareCommandManagedRuntime(input: {
workspaceBaseline: input.workspaceBaseline,
workspaceGitSnapshot: input.workspaceGitSnapshot,
workspaceExclude: mergeRuntimeExcludes(input.workspaceExclude),
workspaceFileMode: input.workspaceFileMode,
preserveAbsentOnRestore: input.preserveAbsentOnRestore,
assets: input.assets,
additionalSources: input.additionalSources,
@@ -1437,6 +1437,8 @@ export async function prepareAdapterExecutionTargetRuntime(input: {
workspaceBaseline?: DirectorySnapshot;
workspaceGitSnapshot?: GitWorkspaceSnapshot | null;
workspaceExclude?: string[];
/** Plain persistent directories include all files, independent of Git and task cache exclusions. */
workspaceFileMode?: "all";
preserveAbsentOnRestore?: string[];
assets?: AdapterManagedRuntimeAsset[];
/** Referenced (additional) projects to stage into the sandbox as plain, read-only trees. */
@@ -1478,6 +1480,8 @@ export async function prepareAdapterExecutionTargetRuntime(input: {
workspaceLocalDir: input.workspaceLocalDir,
workspaceRemoteDir: input.workspaceRemoteDir,
syncWorkspace: input.syncWorkspace,
workspaceFileMode: input.workspaceFileMode,
workspaceExclude: input.workspaceExclude,
assets: input.assets,
additionalSources: input.additionalSources,
onProgress: input.onProgress,
@@ -1517,6 +1521,7 @@ export async function prepareAdapterExecutionTargetRuntime(input: {
workspaceBaseline: input.workspaceBaseline,
workspaceGitSnapshot: input.workspaceGitSnapshot,
workspaceExclude: input.workspaceExclude,
workspaceFileMode: input.workspaceFileMode,
preserveAbsentOnRestore: input.preserveAbsentOnRestore,
assets: input.assets,
additionalSources: input.additionalSources,
+1
View File
@@ -117,6 +117,7 @@ export {
PAPERCLIP_RUNNER_IDLE_TIMEOUT_MAX_MS,
PAPERCLIP_RUNNER_DEFAULT_MODELS,
PAPERCLIP_RUNNER_PERMISSION_CAPABILITIES,
PAPERCLIP_RUNNER_ACPX_PROFILES,
isPaperclipRunnerProvider,
resolvePaperclipRunnerIdleTimeoutMs,
resolvePaperclipRunnerModel,
@@ -125,7 +125,7 @@ export const PAPERCLIP_RUNNER_PERMISSION_CAPABILITIES = {
value: "approve-reads",
label: "Allow Paperclip reads",
description:
"Automatically allow assigned Paperclip read tools. Other operations stop with an approval-required message because this runner has no interactive approval handler.",
"Automatically allow assigned Paperclip read tools. Other operations request a supported permission decision. Company permissions and execution boundaries still apply.",
},
{
value: "deny-all",
@@ -223,3 +223,12 @@ export function normalizeLegacyRunnerProvider(
} = config;
return { ...rest, provider: "codex", codexPermissionMode: "never" };
}
/** Qualification is a release property, never an operator-configurable bypass. */
export const PAPERCLIP_RUNNER_ACPX_PROFILES = Object.freeze([
{ value: "grok", label: "Grok Build", qualified: true, credentialEnvironment: ["XAI_API_KEY"] },
{ value: "claude", label: "Claude", qualified: true, credentialEnvironment: ["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"] },
{ value: "cursor", label: "Cursor", qualified: false, credentialEnvironment: ["CURSOR_API_KEY", "CURSOR_AUTH_TOKEN"] },
{ value: "copilot", label: "GitHub Copilot", qualified: false, credentialEnvironment: ["COPILOT_GITHUB_TOKEN"] },
{ value: "pi", label: "Pi", qualified: false, credentialEnvironment: ["OPENROUTER_API_KEY"] },
] as const);
@@ -32,6 +32,33 @@ import { setExpensiveWorkspaceGitExecutor } from "./git-workspace-sync.js";
describe("remote managed runtime", () => {
const cleanupDirs: string[] = [];
it("stages all files over SSH without Git or cache exclusions and restores the same baseline", async () => {
const root = await mkdtemp(path.join(os.tmpdir(), "paperclip-ssh-plain-"));
cleanupDirs.push(root);
await mkdir(path.join(root, "node_modules"));
await writeFile(path.join(root, ".gitignore"), "node_modules/\n");
await writeFile(path.join(root, "node_modules", "personal.bin"), Buffer.from([0, 255, 1]));
const prepared = await prepareRemoteManagedRuntime({
spec: { host: "127.0.0.1", port: 2222, username: "fixture", remoteWorkspacePath: "/app", remoteCwd: "/app",
privateKey: "PRIVATE KEY", knownHosts: "KNOWN HOSTS", strictHostKeyChecking: true },
runId: "plain", adapterKey: "test", workspaceLocalDir: root,
workspaceFileMode: "all", workspaceExclude: ["explicitly-excluded"],
});
expect(prepareWorkspaceForSshExecution).toHaveBeenCalledWith(expect.objectContaining({
localDir: root, remoteDir: prepared.workspaceRemoteDir, workspaceFileMode: "all", workspaceExclude: ["explicitly-excluded"],
}));
await prepared.restoreWorkspace();
expect(restoreWorkspaceFromSshExecution).toHaveBeenCalledWith(expect.objectContaining({
restoreGitHistory: false, baselineSnapshot: expect.objectContaining({
exclude: [".paperclip-runtime", "explicitly-excluded"],
entries: expect.any(Map),
}),
}));
const args = vi.mocked(restoreWorkspaceFromSshExecution).mock.calls[0] as unknown as [{ baselineSnapshot: { entries: Map<string, unknown> } }];
expect(args[0].baselineSnapshot.entries.has("node_modules/personal.bin")).toBe(true);
});
afterEach(async () => {
vi.clearAllMocks();
while (cleanupDirs.length > 0) {
@@ -113,6 +113,8 @@ export async function prepareRemoteManagedRuntime(input: {
workspaceLocalDir: string;
workspaceRemoteDir?: string;
syncWorkspace?: boolean;
workspaceFileMode?: "all";
workspaceExclude?: string[];
assets?: RemoteManagedRuntimeAsset[];
/** Referenced (additional) projects to stage as plain, read-only trees. */
additionalSources?: SandboxAdditionalSource[];
@@ -139,13 +141,15 @@ export async function prepareRemoteManagedRuntime(input: {
localDir: input.workspaceLocalDir,
remoteDir: workspaceRemoteDir,
onProgress: input.onProgress,
workspaceFileMode: input.workspaceFileMode,
workspaceExclude: input.workspaceExclude,
})
: null;
const baselineSnapshot = preparedWorkspace
? await captureDirectorySnapshot(input.workspaceLocalDir, {
exclude: preparedWorkspace.gitBacked
? [...GIT_ARCHIVE_EXCLUDES, ".paperclip-runtime"]
: [".paperclip-runtime"],
: [".paperclip-runtime", ...(input.workspaceFileMode === "all" ? input.workspaceExclude ?? [] : [])],
})
: null;
@@ -1212,7 +1212,9 @@ describe("sandbox managed runtime", () => {
});
it.each(["symlink", "root_symlink", "root_alias", "root_alias_retarget", "case_alias", "EACCES", "EIO", "ENOENT"])("handles an overlay source changed after the snapshot: %s", async (change) => {
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-overlay-source-"));
// The runtime pins a realpath root before staging. Match that spelling so
// injected I/O errors also reach the selected file on macOS's /var alias.
const rootDir = await fsPromises.realpath(await mkdtemp(path.join(os.tmpdir(), "paperclip-sandbox-overlay-source-")));
cleanupDirs.push(rootDir);
const workspaceLocalDir = path.join(rootDir, "workspace");
await initGitRepo(workspaceLocalDir);
@@ -1301,6 +1303,7 @@ describe("sandbox managed runtime", () => {
expect(syncIn).toHaveBeenCalledOnce();
} else {
await expect(preparing).rejects.toThrow(change.endsWith("symlink") ? /overlay.*directory/i : failure.message);
if (statSpy) expect(statSpy).toHaveBeenCalledWith(selectedPath);
expect(syncIn).not.toHaveBeenCalled();
}
} finally {
@@ -1102,6 +1102,8 @@ export async function prepareSandboxManagedRuntime(input: {
workspaceBaseline?: DirectorySnapshot;
workspaceGitSnapshot?: GitWorkspaceSnapshot | null;
workspaceExclude?: string[];
/** Plain persistent directories include all files, independent of Git and task cache exclusions. */
workspaceFileMode?: "all";
preserveAbsentOnRestore?: string[];
assets?: SandboxManagedRuntimeAsset[];
/**
@@ -1179,7 +1181,7 @@ export async function prepareSandboxManagedRuntime(input: {
// The git enumeration (`git status --ignored`, the HEAD diffs, `ls-files`).
// It reads git's own bookkeeping to decide what to include/exclude, so it is
// usually fast, but on a large working tree the `--ignored` walk is not free.
const gitSnapshot = syncWorkspace
const gitSnapshot = syncWorkspace && input.workspaceFileMode !== "all"
? input.workspaceGitSnapshot !== undefined
? input.workspaceGitSnapshot
: await runStepSpan("snapshot.git", () =>
@@ -1195,7 +1197,7 @@ export async function prepareSandboxManagedRuntime(input: {
// A selected subfolder has no cloneable Git snapshot, but its parent
// repository's ignore rules still govern which files may leave the host.
// Use the same bounded, path-relative resolver as referenced project trees.
const directoryIgnore = syncWorkspace && !gitSnapshot
const directoryIgnore = syncWorkspace && !gitSnapshot && input.workspaceFileMode !== "all"
? await resolveReferencedSourceIgnore(input.workspaceLocalDir)
: null;
if (directoryIgnore?.kind === "failed") {
@@ -1203,14 +1205,14 @@ export async function prepareSandboxManagedRuntime(input: {
}
const gitIgnoredExcludes = directoryIgnore?.kind === "git" ? directoryIgnore.ignoredPaths : undefined;
const workspaceArchiveExclude = mergeExcludes(
SANDBOX_WORKSPACE_HEAVY_DIR_EXCLUDES,
[...GIT_ARCHIVE_EXCLUDES],
input.workspaceFileMode === "all" ? [] : SANDBOX_WORKSPACE_HEAVY_DIR_EXCLUDES,
input.workspaceFileMode === "all" ? [] : [...GIT_ARCHIVE_EXCLUDES],
input.workspaceExclude,
gitIgnoredExcludes,
);
const restoreExclude = mergeExcludes(
SANDBOX_WORKSPACE_HEAVY_DIR_EXCLUDES,
[...GIT_ARCHIVE_EXCLUDES],
input.workspaceFileMode === "all" ? [] : SANDBOX_WORKSPACE_HEAVY_DIR_EXCLUDES,
input.workspaceFileMode === "all" ? [] : [...GIT_ARCHIVE_EXCLUDES],
[".paperclip-runtime"],
input.preserveAbsentOnRestore,
input.workspaceExclude,
@@ -712,6 +712,28 @@ describe("ssh env-lab fixture", () => {
expect(result.stdout).toContain("{\"token\":\"secret\"}");
}, SSH_FIXTURE_TEST_TIMEOUT_MS);
it("clears stale files when plain SSH preparation is retried", async () => {
const rootDir = await createFixtureRootDir();
const localDir = path.join(rootDir, "plain-local");
await mkdir(path.join(localDir, "node_modules"), { recursive: true });
await git(localDir, ["init"]);
await writeFile(path.join(localDir, ".gitignore"), "node_modules/\n");
await writeFile(path.join(localDir, "removed.txt"), "remove on retry");
const binary = Buffer.from([0, 255, 1]);
await writeFile(path.join(localDir, "node_modules", "personal.bin"), binary);
const started = await startSshEnvLabFixtureOrSkip(path.join(rootDir, "state.json"), "SSH plain retry");
if (!started) return;
const config = await buildSshEnvLabFixtureConfig(started);
const input = { spec: { ...config, remoteCwd: started.workspaceDir }, localDir,
remoteDir: started.workspaceDir, workspaceFileMode: "all" as const };
expect(await prepareWorkspaceForSshExecution(input)).toEqual({ gitBacked: false });
expect(await readFile(path.join(started.workspaceDir, "node_modules", "personal.bin"))).toEqual(binary);
await rm(path.join(localDir, "removed.txt"));
await prepareWorkspaceForSshExecution(input);
await expect(stat(path.join(started.workspaceDir, "removed.txt"))).rejects.toMatchObject({ code: "ENOENT" });
expect(await readFile(path.join(started.workspaceDir, "node_modules", "personal.bin"))).toEqual(binary);
}, SSH_FIXTURE_TEST_TIMEOUT_MS);
it("round-trips a git workspace through the SSH fixture", async () => {
const rootDir = await createFixtureRootDir();
const statePath = path.join(rootDir, "state.json");
+4 -2
View File
@@ -1558,9 +1558,11 @@ export async function prepareWorkspaceForSshExecution(input: {
localDir: string;
remoteDir?: string;
onProgress?: RuntimeProgressSink;
workspaceFileMode?: "all";
workspaceExclude?: string[];
}): Promise<{ gitBacked: boolean }> {
const remoteDir = input.remoteDir ?? input.spec.remoteCwd;
const gitSnapshot = await readLocalGitWorkspaceSnapshot(input.localDir);
const gitSnapshot = input.workspaceFileMode === "all" ? null : await readLocalGitWorkspaceSnapshot(input.localDir);
if (gitSnapshot) {
await importGitWorkspaceToSsh({
@@ -1595,7 +1597,7 @@ export async function prepareWorkspaceForSshExecution(input: {
spec: input.spec,
localDir: input.localDir,
remoteDir,
exclude: [".paperclip-runtime"],
exclude: [".paperclip-runtime", ...(input.workspaceFileMode === "all" ? input.workspaceExclude ?? [] : [])],
onProgress: input.onProgress,
progressLabel: "workspace",
});
@@ -605,3 +605,56 @@ describe("workspace restore merge", () => {
});
});
});
describe("conflict-preserving directory restore", () => {
it("preflights competing edits before applying any other change and deduplicates replay", async () => {
const root = await fsPromises.realpath(await mkdtemp(path.join(os.tmpdir(), "directory-cas-")));
const source = path.join(root, "source"), target = path.join(root, "target");
try {
await mkdir(target);
await writeFile(path.join(target, "conflict"), "baseline");
const baseline = await captureDirectorySnapshot(target);
await fsPromises.cp(target, source, { recursive: true });
await writeFile(path.join(source, "conflict"), "incoming");
await writeFile(path.join(source, "independent"), "also incoming");
await writeFile(path.join(target, "conflict"), "board");
await expect(mergeDirectoryWithBaseline({ baseline, sourceDir: source, targetDir: target, conflictPolicy: "reject" })).rejects.toMatchObject({ code: "DIRECTORY_MERGE_CONFLICT", paths: ["conflict"] });
await expect(stat(path.join(target, "independent"))).rejects.toMatchObject({ code: "ENOENT" });
await writeFile(path.join(target, "conflict"), "baseline");
await expect(mergeDirectoryWithBaseline({ baseline, sourceDir: source, targetDir: target, conflictPolicy: "reject", afterApply: async () => { throw new Error("receipt interrupted"); } })).rejects.toThrow("receipt interrupted");
await mergeDirectoryWithBaseline({ baseline, sourceDir: source, targetDir: target, conflictPolicy: "reject" });
expect(await readFile(path.join(target, "independent"), "utf8")).toBe("also incoming");
} finally { await rm(root, { recursive: true, force: true }); }
});
it("preserves a newly added child when another run removes or replaces its parent", async () => {
const root = await fsPromises.realpath(await mkdtemp(path.join(os.tmpdir(), "directory-delete-cas-")));
const source = path.join(root, "source"), target = path.join(root, "target");
try {
await mkdir(path.join(target, "folder"), { recursive: true });
const baseline = await captureDirectorySnapshot(target);
await mkdir(source);
await writeFile(path.join(target, "folder", "new"), "concurrent");
await expect(mergeDirectoryWithBaseline({ baseline, sourceDir: source, targetDir: target, conflictPolicy: "reject" })).rejects.toMatchObject({ paths: ["folder/new"] });
expect(await readFile(path.join(target, "folder", "new"), "utf8")).toBe("concurrent");
} finally { await rm(root, { recursive: true, force: true }); }
});
});
it("strict preflight preserves excluded descendants when a directory becomes a file", async () => {
const root = await fsPromises.realpath(await mkdtemp(path.join(os.tmpdir(), "directory-excluded-cas-")));
const target = path.join(root, "target"), source = path.join(root, "source");
await mkdir(path.join(target, "folder", "node_modules"), { recursive: true });
await writeFile(path.join(target, "folder", "node_modules", "keep"), "excluded contents");
const baseline = await captureDirectorySnapshot(target, { exclude: ["*/node_modules"], diskBacked: true });
try {
await mkdir(source);
await writeFile(path.join(source, "folder"), "replacement");
await writeFile(path.join(source, "independent"), "must not partially apply");
await expect(mergeDirectoryWithBaseline({ baseline, sourceDir: source, targetDir: target, conflictPolicy: "reject" }))
.rejects.toMatchObject({ code: "DIRECTORY_MERGE_CONFLICT", paths: expect.arrayContaining(["folder/node_modules/keep"]) });
expect(await readFile(path.join(target, "folder", "node_modules", "keep"), "utf8")).toBe("excluded contents");
await expect(stat(path.join(target, "independent"))).rejects.toMatchObject({ code: "ENOENT" });
} finally { await disposeDirectorySnapshot(baseline); await rm(root, { recursive: true, force: true }); }
});
@@ -1,4 +1,4 @@
import { createHash } from "node:crypto";
import { createHash, randomUUID } from "node:crypto";
import { createReadStream } from "node:fs";
import { constants as fsConstants, promises as fs } from "node:fs";
import path from "node:path";
@@ -442,16 +442,26 @@ async function copySnapshotEntry(sourceDir: string, targetDir: string, relative:
}
await fs.mkdir(path.dirname(targetPath), { recursive: true });
await fs.rm(targetPath, { recursive: true, force: true }).catch(() => undefined);
if (entry.kind === "symlink") {
await fs.rm(targetPath, { recursive: true, force: true });
await fs.symlink(entry.target, targetPath);
return;
}
// An interrupted restore must not leave a truncated current file. Keep the
// incoming tree until its owner records success; exact retries deduplicate.
const temporary = path.join(path.dirname(targetPath), `.paperclip-merge-${randomUUID()}`);
try {
await fs.copyFile(sourcePath, temporary, fsConstants.COPYFILE_FICLONE).catch(async () => {
await fs.copyFile(sourcePath, temporary);
});
await fs.chmod(temporary, entry.mode);
const file = await fs.open(temporary, "r");
try { await file.sync(); } finally { await file.close(); }
const existing = await fs.lstat(targetPath).catch(() => null);
if (existing?.isDirectory()) await fs.rm(targetPath, { recursive: true, force: true });
await fs.rename(temporary, targetPath);
} finally { await fs.rm(temporary, { force: true }); }
await fs.copyFile(sourcePath, targetPath, fsConstants.COPYFILE_FICLONE).catch(async () => {
await fs.copyFile(sourcePath, targetPath);
});
await fs.chmod(targetPath, entry.mode);
}
export async function captureDirectorySnapshot(
@@ -511,20 +521,74 @@ function orderedEntries(snapshot: DirectorySnapshot, reverse = false): Iterable<
return [...snapshot.entries].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0) * (reverse ? -1 : 1));
}
export class DirectoryMergeConflict extends Error {
readonly code = "DIRECTORY_MERGE_CONFLICT";
constructor(readonly paths: string[]) {
super("Directory contents changed concurrently");
}
}
/** Preflight the entire delta before writing. Identical replays are safe after
* an interrupted apply; unrelated edits are left alone. No history is retained. */
export function directoryMergeConflicts(baseline: DirectorySnapshot, source: DirectorySnapshot, current: DirectorySnapshot): string[] {
const same = (a: SnapshotEntry | undefined, b: SnapshotEntry | undefined) =>
(!a && !b) || entriesMatch(a, b);
const conflicts = new Set<string>();
function* changedPaths() {
for (const [name] of baseline.entries) yield name;
for (const [name] of source.entries) if (!baseline.entries.has(name)) yield name;
}
for (const relative of changedPaths()) {
const before = baseline.entries.get(relative);
const incoming = source.entries.get(relative);
const present = current.entries.get(relative);
if (same(before, incoming) || same(incoming, present)) continue;
if (!same(before, present)) conflicts.add(relative);
// A parent removed/replaced by another writer must never be traversed.
for (let parent = path.posix.dirname(relative); parent !== "."; parent = path.posix.dirname(parent)) {
if (current.entries.get(parent)?.kind !== "dir" &&
!same(current.entries.get(parent), baseline.entries.get(parent))) conflicts.add(parent);
}
}
// Stream each current entry once. A replacement must not remove children
// omitted from the baseline, including excluded or newly created files.
for (const [child, entry] of current.entries) {
if (same(entry, baseline.entries.get(child)) || same(entry, source.entries.get(child))) continue;
for (let parent = path.posix.dirname(child); parent !== "."; parent = path.posix.dirname(parent)) {
if (baseline.entries.get(parent)?.kind === "dir" && source.entries.get(parent)?.kind !== "dir") {
conflicts.add(child);
break;
}
}
}
return [...conflicts].sort();
}
export async function mergeDirectoryWithBaseline(input: {
baseline: DirectorySnapshot;
sourceDir: string;
targetDir: string;
conflictPolicy?: "reject";
beforeApply?: () => Promise<void>;
afterApply?: () => Promise<void>;
/** Caller holds the target's writer lock and validated an immutable sparse
* source. Unchanged entries need no payload and are never copied. */
snapshots?: { source: DirectorySnapshot; current: DirectorySnapshot };
}): Promise<void> {
const options = { exclude: input.baseline.exclude, ignoredPaths: input.baseline.ignoredPaths, diskBacked: true };
const source = await captureDirectorySnapshot(input.sourceDir, options);
const source = input.snapshots?.source ?? await captureDirectorySnapshot(input.sourceDir, options);
try {
await withDirectoryMergeLock(input.targetDir, async (canonicalTargetDir) => {
await input.beforeApply?.();
const current = await captureDirectorySnapshot(canonicalTargetDir, options);
// Strict preflight must see excluded children before a directory is
// replaced. The merge still applies only the filtered source/baseline.
const current = input.snapshots?.current ?? await captureDirectorySnapshot(canonicalTargetDir,
input.conflictPolicy === "reject" ? { exclude: [], diskBacked: true } : options);
try {
if (input.conflictPolicy === "reject") {
const conflicts = directoryMergeConflicts(input.baseline, source, current);
if (conflicts.length) throw new DirectoryMergeConflict(conflicts);
}
for (const [relative, baselineEntry] of orderedEntries(input.baseline)) {
if (baselineEntry.kind === "dir" || source.entries.has(relative)) continue;
if (!entriesMatch(current.entries.get(relative), baselineEntry)) continue;
@@ -537,7 +601,8 @@ export async function mergeDirectoryWithBaseline(input: {
});
}
for (const [relative, entry] of orderedEntries(source)) {
if (!entriesMatch(input.baseline.entries.get(relative), entry)) await copySnapshotEntry(input.sourceDir, canonicalTargetDir, relative, entry);
if (!entriesMatch(input.baseline.entries.get(relative), entry) &&
!(input.conflictPolicy === "reject" && entriesMatch(current.entries.get(relative), entry))) await copySnapshotEntry(input.sourceDir, canonicalTargetDir, relative, entry);
}
await input.afterApply?.();
} finally { await disposeDirectorySnapshot(current); }
@@ -3,9 +3,10 @@ import os from "node:os";
import path from "node:path";
import { createRequire } from "node:module";
import { fileURLToPath } from "node:url";
import { afterEach, expect, it } from "vitest";
import { afterEach, expect, it, vi } from "vitest";
import { classifyClaudeTerminalSessionFailure, createClaudeAcpExecutor } from "./acp.js";
import type { AcpxEngineExecutorOptions } from "@paperclipai/adapter-utils/acpx-engine/execute";
import { parseAcpxStdoutLine } from "@paperclipai/adapter-utils/acpx-engine/ui";
const repoRoot = fileURLToPath(new URL("../../../../..", import.meta.url));
const fixture = path.join(repoRoot, "scripts/mcp-fixtures/servers/acp-echo-agent.mjs");
@@ -16,6 +17,7 @@ const runnerRequire = createRequire(path.join(repoRoot, "packages/paperclip-runn
const runnerAcpx = await import(runnerRequire.resolve("acpx/runtime"));
afterEach(async () => {
vi.unstubAllEnvs();
await Promise.all(roots.splice(0).map((root) => fs.rm(root, { recursive: true, force: true })));
});
@@ -24,9 +26,13 @@ async function executeFailure(
category = "limit",
mode = "oneshot",
createRuntime?: AcpxEngineExecutorOptions["createRuntime"],
extraEnv: Record<string, string> = {},
details?: string,
) {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-claude-acp-quota-"));
roots.push(root);
const failureFile = path.join(root, "failure.json");
await fs.writeFile(failureFile, JSON.stringify({ title, category, details }));
const logs: string[] = [];
const execute = createClaudeAcpExecutor({ now: () => now.getTime(), createRuntime });
const result = await execute({
@@ -40,8 +46,8 @@ async function executeFailure(
cwd: repoRoot,
stateDir: path.join(root, "state"),
env: {
PAPERCLIP_ACPX_TYPED_FAILURE_CANARY: title,
PAPERCLIP_ACPX_TYPED_FAILURE_CATEGORY: category,
...extraEnv,
PAPERCLIP_ACPX_TYPED_FAILURE_FILE: failureFile,
},
},
context: {},
@@ -56,7 +62,7 @@ it.each([
["0.12.0", "persistent"],
["0.13.1", "oneshot"],
["0.13.1", "persistent"],
])("waits for a typed Claude quota reset with ACPX %s in %s mode without exposing provider text", async (version, mode) => {
])("waits for a typed Claude quota reset with ACPX %s in %s mode with provider diagnostics", async (version, mode) => {
const title = "You've hit your session limit · resets 4:30pm (America/Chicago)";
const { result, logs } = await executeFailure(
title, "limit", mode, version === "0.13.1" ? runnerAcpx.createAcpRuntime : undefined,
@@ -72,8 +78,49 @@ it.each([
providerQuotaRetryNotBefore: "2026-07-15T21:30:00.000Z",
},
});
expect(JSON.stringify(result)).not.toContain(title);
expect(logs).not.toContain(title);
expect(result.errorMessage).toContain(title);
expect(result.resultJson?.terminalSessionFailure).toMatchObject({ title });
expect(result.summary).not.toContain(title);
expect(logs).toContain(title);
});
it.each([
["0.12.0", "oneshot"],
["0.12.0", "persistent"],
["0.13.1", "oneshot"],
["0.13.1", "persistent"],
])("retains redacted service diagnostics beyond 4 KiB with ACPX %s in %s mode", async (version, mode) => {
const secret = "opaque-provider-credential-canary";
const inheritedSecret = "opaque-inherited-proxy-canary";
// HTTP_PROXY is inherited by the real ACP launch but has no secret-name hint.
vi.stubEnv("HTTP_PROXY", inheritedSecret);
const title = "HTTP 529: overloaded_error";
const details = `${"provider context\n".repeat(300)}request_id=req_service_123\nCredential echoed: ${secret}\nInherited credential: ${inheritedSecret}\n at prompt (agent.js:42:7)`;
const { result, logs } = await executeFailure(
title, "service", mode, version === "0.13.1" ? runnerAcpx.createAcpRuntime : undefined,
{ PROVIDER_SETTING: secret }, details,
);
expect(result).toMatchObject({
exitCode: 1,
errorCode: "acpx_turn_failed",
resultJson: {
terminalSessionFailure: {
category: "service",
title,
details: details.replace(secret, "***REDACTED***").replace(inheritedSecret, "***REDACTED***"),
},
},
});
expect(result.errorMessage).toContain("request_id=req_service_123");
expect(result.errorMessage).toContain("at prompt (agent.js:42:7)");
expect(result.summary).not.toContain(title);
expect(JSON.stringify(result)).not.toContain(secret);
expect(logs).not.toContain(secret);
expect(JSON.stringify(result)).not.toContain(inheritedSecret);
expect(logs).not.toContain(inheritedSecret);
const transcript = logs.split("\n").flatMap((line) => parseAcpxStdoutLine(line, "2026-07-15T20:00:00Z"));
expect(transcript).toContainEqual(expect.objectContaining({ kind: "stderr", text: result.errorMessage }));
expect(transcript.some((entry) => entry.kind === "assistant")).toBe(false);
});
it("classifies quota without a reset time for the existing recovery backoff", async () => {
@@ -95,14 +142,16 @@ it.each([
);
expect(result).toMatchObject({
exitCode: 1,
errorMessage: "ACP agent reported a terminal limit failure.",
errorMessage: `ACP agent reported a terminal limit failure.\n${title}`,
errorCode: "provider_quota",
errorFamily: "provider_quota",
resultJson: { errorFamily: "provider_quota" },
});
expect(result.retryNotBefore).toBeUndefined();
expect(JSON.stringify(result)).not.toContain(title);
expect(logs).not.toContain(title);
expect(result.errorMessage).toContain(title);
expect(result.resultJson?.terminalSessionFailure).toMatchObject({ category: "limit", title });
expect(result.summary).not.toContain(title);
expect(logs).toContain(title);
});
it.each([
@@ -119,8 +168,10 @@ it.each([
expect(result).toMatchObject({ exitCode: 1, errorCode: "acpx_turn_failed" });
expect(result.errorFamily).not.toBe("provider_quota");
expect(result.retryNotBefore).toBeUndefined();
expect(JSON.stringify(result)).not.toContain(title);
expect(logs).not.toContain(title);
expect(result.errorMessage).toContain(title);
expect(result.resultJson?.terminalSessionFailure).toMatchObject({ category, title });
expect(result.summary).not.toContain(title);
expect(logs).toContain(title);
});
it("does not infer quota from the historical generic terminal-limit error", () => {
@@ -1290,23 +1290,22 @@ describe("stageCodexHomeForSync", () => {
}
});
// Mode normalization: nested skill files must be staged 0600 regardless of
// their source mode (0644 documents, 0755 scripts, etc.).
it("writes nested skill files with mode 0600 regardless of source mode", async () => {
// Skill staging retains executable permission with owner-only access.
it("retains skill executable bits without group or world permissions", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-stage-skill-mode-"));
let staged: string | null = null;
try {
const home = path.join(root, "codex-home");
await fs.mkdir(path.join(home, "skills", "my-skill"), { recursive: true });
// Typical source modes: readable doc (0644) and executable script (0755);
// both must land 0600 in the staged dir.
// documents land 0600, scripts 0700 in the staged dir.
await fs.writeFile(path.join(home, "skills", "my-skill", "SKILL.md"), "# skill\n", { mode: 0o644 });
await fs.writeFile(path.join(home, "skills", "my-skill", "run.sh"), "#!/bin/sh\n", { mode: 0o755 });
staged = await stageCodexHomeForSync(home, { runId: "run-skill-mode" });
for (const rel of ["my-skill/SKILL.md", "my-skill/run.sh"]) {
const mode = (await fs.stat(path.join(staged, "skills", rel))).mode & 0o777;
expect(mode, `skills/${rel} should be staged 0600`).toBe(0o600);
expect(mode, `skills/${rel} mode`).toBe(rel.endsWith(".sh") ? 0o700 : 0o600);
}
} finally {
if (staged) await fs.rm(staged, { recursive: true, force: true });
@@ -383,7 +383,8 @@ function isResolvedPathInside(candidate: string, root: string): boolean {
* Recursively copies one skill subtree — rooted at its real directory
* `containmentRoot` — into `targetDir`, dereferencing symlinks to bytes (so the
* sandbox receives real file content, not host-relative links) and normalizing
* every copied regular file to mode `0600`. Created directories get mode `0700`.
* regular files to owner-only access (`0700` for executables, `0600` otherwise).
* Created directories get mode `0700`.
*
* Two containment guards protect the staged upload:
*
@@ -438,8 +439,9 @@ async function stageContainedSubtree(
activePath.delete(resolved);
} else if (entryStat.isFile()) {
const bytes = await fs.readFile(resolved);
await fs.writeFile(entryTarget, bytes, { mode: 0o600 });
await fs.chmod(entryTarget, 0o600);
const mode = entryStat.mode & 0o111 ? 0o700 : 0o600;
await fs.writeFile(entryTarget, bytes, { mode });
await fs.chmod(entryTarget, mode);
}
// Other types (sockets, devices) are silently skipped.
}
@@ -448,11 +450,12 @@ async function stageContainedSubtree(
/**
* Recursively copies `sourceDir` (a directory allowlist entry — currently only
* `skills/`) into `targetDir`, dereferencing symlinks to bytes and normalizing
* every copied regular file to mode `0600`. Created directories get mode `0700`.
* regular files to owner-only access (`0700` for executables, `0600` otherwise).
* Created directories get mode `0700`.
*
* This replaces `fs.cp({ dereference: true })` which preserves source file modes,
* leaving `0644` documents and `0755` scripts group/other-readable in the staged
* asset; here all regular files are normalized to `0600` regardless of source mode.
* asset; here skill files retain only their executable flag, with owner-only access.
*
* `sourceDir`'s *direct* children are the Paperclip-injected skill symlinks that
* intentionally point into a shared skill store *outside* `CODEX_HOME/skills/`,
@@ -463,7 +466,7 @@ async function stageContainedSubtree(
* cycles (finding 1). A direct child that resolves to `sourceDir` itself or to
* an ancestor of it (a degenerate `-> .` / `-> ..` link at the top level) is
* skipped rather than used as a root, so it can never drag the wider home into
* the staged skills asset. The `0700` staged directory and per-file `0600` mode
* the staged skills asset. The `0700` staged directory and per-file owner-only mode
* together ensure even externally-sourced skill content is not group/world-readable.
*/
async function stageDirectorySecure(
@@ -498,8 +501,9 @@ async function stageDirectorySecure(
await stageContainedSubtree(resolved, entryTarget, resolved, new Set([resolved]));
} else if (entryStat.isFile()) {
const bytes = await fs.readFile(resolved);
await fs.writeFile(entryTarget, bytes, { mode: 0o600 });
await fs.chmod(entryTarget, 0o600);
const mode = entryStat.mode & 0o111 ? 0o700 : 0o600;
await fs.writeFile(entryTarget, bytes, { mode });
await fs.chmod(entryTarget, mode);
}
// Other types (sockets, devices) are silently skipped.
}
@@ -138,6 +138,43 @@ function createFreshLeaseSandboxRunner(options: {
}
describe("cursor execute", () => {
it.each([
{ detail: "Authentication failed", structured: "", expected: "Authentication failed" },
{ detail: "", structured: "", expected: "Cursor exited with code 7" },
{ detail: "stderr fallback", structured: '{"type":"error","message":"Structured failure"}', expected: "Structured failure" },
])("keeps the actual failure after a retrieval trace announcement: $expected", async ({ detail, structured, expected }) => {
setPrepareCursorSandboxCommand.mockReset();
setPrepareCursorSandboxCommand.mockImplementation(async (input) => ({
command: input.command, env: input.env, remoteSystemHomeDir: null,
addedPathEntry: null, preferredCommandPath: null,
}));
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-cursor-diagnostic-"));
const command = path.join(root, "agent.sh");
const trace = "cursor-retrieval: tracing to '/tmp/fixture-cursor-retrieval.log'";
// Values are fixed test fixtures, passed through env rather than shell code.
await fs.writeFile(command, `#!/bin/sh
cat >/dev/null
printf '%s\\n' "$FIXTURE_TRACE" "$FIXTURE_DETAIL" >&2
printf '%s\\n' "$FIXTURE_STRUCTURED"
exit 7
`, { mode: 0o755 });
try {
const result = await execute({
runId: "run-diagnostic-1",
agent: { id: "agent-1", companyId: "company-1", name: "Cursor", adapterType: "cursor", adapterConfig: {} },
runtime: { sessionId: null, sessionParams: null, sessionDisplayId: null, taskKey: null },
config: { command, cwd: root, env: { FIXTURE_TRACE: trace, FIXTURE_DETAIL: detail, FIXTURE_STRUCTURED: structured } },
context: createPromptContextFixture(), authToken: "fixture-run-token", onLog: async () => {},
});
expect(result.exitCode).toBe(7);
expect(result.errorMessage).toBe(expected);
expect(result.resultJson?.stderr).toContain(trace);
expect(result.resultJson?.stderr).toContain(detail);
} finally {
await fs.rm(root, { recursive: true, force: true });
}
});
it("installs the default agent command on a fresh sandbox lease before execution", async () => {
setPrepareCursorSandboxCommand.mockReset();
setPrepareCursorSandboxCommand.mockImplementation(async (input) => {
@@ -52,7 +52,7 @@ import {
joinPromptSections,
} from "@paperclipai/adapter-utils/server-utils";
import { DEFAULT_CURSOR_LOCAL_MODEL, SANDBOX_INSTALL_COMMAND } from "../index.js";
import { parseCursorJsonl, isCursorUnknownSessionError } from "./parse.js";
import { firstCursorDiagnosticLine, parseCursorJsonl, isCursorUnknownSessionError } from "./parse.js";
import { prepareCursorSandboxCommand } from "./remote-command.js";
import { normalizeCursorStreamLine } from "../shared/stream.js";
import { hasCursorTrustBypassArg } from "../shared/trust.js";
@@ -60,15 +60,6 @@ import { resolveCursorSkillsHome } from "./skills.js";
const __moduleDir = path.dirname(fileURLToPath(import.meta.url));
function firstNonEmptyLine(text: string): string {
return (
text
.split(/\r?\n/)
.map((line) => line.trim())
.find(Boolean) ?? ""
);
}
function hasNonEmptyEnvValue(env: Record<string, string>, key: string): boolean {
const raw = env[key];
return typeof raw === "string" && raw.trim().length > 0;
@@ -726,7 +717,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
} as Record<string, unknown>)
: null;
const parsedError = typeof attempt.parsed.errorMessage === "string" ? attempt.parsed.errorMessage.trim() : "";
const stderrLine = firstNonEmptyLine(attempt.proc.stderr);
const stderrLine = firstCursorDiagnosticLine(attempt.proc.stderr);
const fallbackErrorMessage =
parsedError ||
stderrLine ||
@@ -0,0 +1,22 @@
import { describe, expect, it } from "vitest";
import { firstCursorDiagnosticLine } from "./parse.js";
describe("Cursor diagnostic selection", () => {
it("skips trace announcements with CRLF, whitespace and ANSI color", () => {
expect(firstCursorDiagnosticLine("\r\n\x1b[90m cursor-retrieval: tracing to '/tmp/fixture.log' \x1b[0m\r\ncursor-retrieval: tracing to \"/tmp/second.log\"\r\nAuthentication failed\r\nmore detail"))
.toBe("Authentication failed");
});
it("keeps retrieval failures and unknown diagnostic formats", () => {
for (const line of [
"cursor-retrieval: failed to open trace file",
"cursor-retrieval: tracing to '/tmp/fixture.log' failed: permission denied",
"unknown warning: preserve this diagnostic",
]) expect(firstCursorDiagnosticLine(line)).toBe(line);
});
it("leaves no diagnostic when only a trace location was printed", () => {
expect(firstCursorDiagnosticLine("cursor-retrieval: tracing to '/tmp/fixture.log'\n")).toBe("");
expect(firstCursorDiagnosticLine("\n\r\n")).toBe("");
});
});
@@ -1,6 +1,17 @@
import { stripVTControlCharacters } from "node:util";
import { asString, asNumber, parseObject, parseJson } from "@paperclipai/adapter-utils/server-utils";
import { normalizeCursorStreamLine } from "../shared/stream.js";
/** Select diagnostics without mistaking Cursor's trace-file notice for an error. */
export function firstCursorDiagnosticLine(text: string): string {
for (const raw of text.split(/\r?\n/)) {
const line = stripVTControlCharacters(raw).trim();
if (!line || /^cursor-retrieval: tracing to (?:'[^']*'|"[^"]*")$/.test(line)) continue;
return line;
}
return "";
}
function asErrorText(value: unknown): string {
if (typeof value === "string") return value;
const rec = parseObject(value);
@@ -70,6 +70,30 @@ function createSandboxRunner(options: { homeDir: string; installCommandPath: str
}
describe("cursor testEnvironment", () => {
it("shows the probe failure after an informational retrieval trace notice", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-cursor-probe-diagnostic-"));
const command = path.join(root, "agent");
await fs.writeFile(command, `#!/bin/sh
if [ "$1" = "--version" ]; then
printf '%s\\n' 'Cursor Agent 1.2.3'
exit 0
fi
printf '%s\\n' "cursor-retrieval: tracing to '/tmp/fixture.log'" 'Provider connection failed' >&2
exit 7
`, { mode: 0o755 });
try {
const result = await testEnvironment({
companyId: "company-1", adapterType: "cursor",
config: { command: "agent", cwd: root, env: { PATH: `${root}${path.delimiter}${process.env.PATH ?? ""}`, CURSOR_API_KEY: "fixture-cursor-key" } },
});
expect(result.checks).toEqual(expect.arrayContaining([
expect.objectContaining({ code: "cursor_hello_probe_failed", detail: "Provider connection failed" }),
]));
} finally {
await fs.rm(root, { recursive: true, force: true });
}
});
it("re-resolves the installed agent under ~/.cursor/bin and verifies --version before the hello probe", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-cursor-envtest-"));
const homeDir = path.join(root, "home");
@@ -22,7 +22,7 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { DEFAULT_CURSOR_LOCAL_MODEL, SANDBOX_INSTALL_COMMAND } from "../index.js";
import { parseCursorJsonl } from "./parse.js";
import { firstCursorDiagnosticLine, parseCursorJsonl } from "./parse.js";
import { isDefaultCursorCommand, prepareCursorSandboxCommand } from "./remote-command.js";
import { hasCursorTrustBypassArg } from "../shared/trust.js";
@@ -36,17 +36,8 @@ function isNonEmpty(value: unknown): value is string {
return typeof value === "string" && value.trim().length > 0;
}
function firstNonEmptyLine(text: string): string {
return (
text
.split(/\r?\n/)
.map((line) => line.trim())
.find(Boolean) ?? ""
);
}
function summarizeProbeDetail(stdout: string, stderr: string, parsedError: string | null): string | null {
const raw = parsedError?.trim() || firstNonEmptyLine(stderr) || firstNonEmptyLine(stdout);
const raw = parsedError?.trim() || firstCursorDiagnosticLine(stderr) || firstCursorDiagnosticLine(stdout);
if (!raw) return null;
const clean = raw.replace(/\s+/g, " ").trim();
const max = 240;
@@ -0,0 +1,141 @@
import { createHash, randomUUID } from "node:crypto";
import { readFile } from "node:fs/promises";
import postgres from "postgres";
import { describe, expect, it } from "vitest";
import { applyPendingMigrations, inspectMigrations } from "./client.js";
import { EMBEDDED_POSTGRES_TEST_TIMEOUT_MS, getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase } from "./test-embedded-postgres.js";
// Exact SQL from PR #14325 before master assigned 0285/0286 to API response storage.
// Exercise real preview tables/receipts, not a second application of the new schema.
const previewMigrations = [
{ when: 1790474441205, sql: `CREATE TABLE "agent_instruction_heads" (
"company_id" uuid NOT NULL,
"agent_id" uuid NOT NULL,
"entry_file" text NOT NULL,
"revision_id" uuid NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "agent_instruction_heads_identity_uq" UNIQUE("company_id","agent_id","entry_file")
);
--> statement-breakpoint
CREATE TABLE "agent_instruction_revisions" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"company_id" uuid NOT NULL,
"agent_id" uuid NOT NULL,
"entry_file" text NOT NULL,
"content_base64" text NOT NULL,
"content_hash" text NOT NULL,
"byte_length" integer NOT NULL,
"parent_revision_id" uuid,
"base_revision_id" uuid,
"restored_from_revision_id" uuid,
"actor_agent_id" uuid,
"actor_user_id" text,
"responsible_user_id" text,
"source_run_id" uuid,
"source" text NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "agent_instruction_revisions_identity_uq" UNIQUE("company_id","agent_id","entry_file","id")
);
--> statement-breakpoint
ALTER TABLE "agent_instruction_heads" ADD CONSTRAINT "agent_instruction_heads_company_id_agent_id_entry_file_revision_id_agent_instruction_revisions_company_id_agent_id_entry_file_id_fk" FOREIGN KEY ("company_id","agent_id","entry_file","revision_id") REFERENCES "public"."agent_instruction_revisions"("company_id","agent_id","entry_file","id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "agent_instruction_revisions" ADD CONSTRAINT "agent_instruction_revisions_company_id_agent_id_agents_company_id_id_fk" FOREIGN KEY ("company_id","agent_id") REFERENCES "public"."agents"("company_id","id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "agent_instruction_revisions_history_idx" ON "agent_instruction_revisions" USING btree ("company_id","agent_id","entry_file","created_at","id");` },
{ when: 1790543348340, sql: `CREATE TABLE "agent_instruction_working_copies" (
"run_id" uuid PRIMARY KEY NOT NULL,
"company_id" uuid NOT NULL,
"agent_id" uuid NOT NULL,
"responsible_user_id" text NOT NULL,
"entry_file" text NOT NULL,
"base_revision_id" uuid,
"base_hash" text NOT NULL,
"local_root" text NOT NULL,
"execution_root" text NOT NULL,
"location" text NOT NULL,
"state" text DEFAULT 'prepared' NOT NULL,
"candidate_base64" text,
"candidate_hash" text,
"error_code" text,
"error_message" text,
"receipt" jsonb,
"process_stopped_at" timestamp with time zone,
"attempts" integer DEFAULT 0 NOT NULL,
"next_attempt_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "agent_instruction_working_copies" ADD CONSTRAINT "agent_instruction_working_copies_run_id_heartbeat_runs_id_fk" FOREIGN KEY ("run_id") REFERENCES "public"."heartbeat_runs"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "agent_instruction_working_copies" ADD CONSTRAINT "agent_instruction_working_copies_company_id_agent_id_agents_company_id_id_fk" FOREIGN KEY ("company_id","agent_id") REFERENCES "public"."agents"("company_id","id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "agent_instruction_copies_pending_idx" ON "agent_instruction_working_copies" USING btree ("state","next_attempt_at");--> statement-breakpoint
CREATE INDEX "agent_instruction_copies_agent_idx" ON "agent_instruction_working_copies" USING btree ("company_id","agent_id","created_at");` },
];
const migration = await readFile(new URL("./migrations/0287_serious_tinkerer.sql", import.meta.url), "utf8");
const migrationHash = createHash("sha256").update(migration).digest("hex");
const support = await getEmbeddedPostgresTestSupport();
const describePostgres = support.supported ? describe : describe.skip;
describePostgres("instruction revision migrations", () => {
it("keeps applied migration receipts, revisions, and pending copies intact on repeated upgrades", async () => {
const database = await startEmbeddedPostgresTestDatabase("instruction-migration-upgrade-");
const sql = postgres(database.connectionString, { max: 1, onnotice: () => {} });
try {
await sql`DROP TABLE agent_instruction_heads, agent_instruction_working_copies, agent_instruction_revisions`;
await sql`DELETE FROM drizzle.__drizzle_migrations WHERE hash = ${migrationHash}`;
for (const preview of previewMigrations) {
for (const statement of preview.sql.split("--> statement-breakpoint")) await sql.unsafe(statement);
await sql`INSERT INTO drizzle.__drizzle_migrations (hash, created_at)
VALUES (${createHash("sha256").update(preview.sql).digest("hex")}, ${preview.when})`;
}
const companyId = randomUUID(), otherCompanyId = randomUUID(), agentId = randomUUID();
const runId = randomUUID(), revisionId = randomUUID();
const bytes = Buffer.from("\uFEFF# Saved\r\nExact bytes ☃\n");
const contentBase64 = bytes.toString("base64");
const contentHash = createHash("sha256").update(bytes).digest("hex");
await sql`INSERT INTO companies (id, name, issue_prefix) VALUES
(${companyId}, 'Instructions', 'INS'), (${otherCompanyId}, 'Other', 'OTH')`;
await sql`INSERT INTO agents (id, company_id, name) VALUES (${agentId}, ${companyId}, 'Writer')`;
await sql`INSERT INTO heartbeat_runs (id, company_id, agent_id) VALUES (${runId}, ${companyId}, ${agentId})`;
await sql`INSERT INTO agent_instruction_revisions
(id, company_id, agent_id, entry_file, content_base64, content_hash, byte_length, source, source_run_id)
VALUES (${revisionId}, ${companyId}, ${agentId}, 'AGENTS.md', ${contentBase64}, ${contentHash}, ${bytes.length}, 'cleanup', ${runId})`;
await sql`INSERT INTO agent_instruction_heads (company_id, agent_id, entry_file, revision_id)
VALUES (${companyId}, ${agentId}, 'AGENTS.md', ${revisionId})`;
await sql`INSERT INTO agent_instruction_working_copies
(run_id, company_id, agent_id, responsible_user_id, entry_file, base_revision_id, base_hash, local_root, execution_root, location, state, candidate_base64, candidate_hash)
VALUES (${runId}, ${companyId}, ${agentId}, 'editor', 'AGENTS.md', ${revisionId}, ${contentHash}, '/private/copy', '/private/copy', 'local', 'pending_commit', ${contentBase64}, ${contentHash})`;
const snapshot = async () => ({
journal: await sql`SELECT * FROM drizzle.__drizzle_migrations ORDER BY id`,
relations: await sql`SELECT oid::text, relname FROM pg_class WHERE relname IN
('agent_instruction_revisions', 'agent_instruction_heads', 'agent_instruction_working_copies') ORDER BY relname`,
constraints: await sql`SELECT oid::text, conname, pg_get_constraintdef(oid) AS definition FROM pg_constraint
WHERE conrelid IN ('agent_instruction_revisions'::regclass, 'agent_instruction_heads'::regclass,
'agent_instruction_working_copies'::regclass) ORDER BY conname`,
revisions: await sql`SELECT * FROM agent_instruction_revisions`,
heads: await sql`SELECT * FROM agent_instruction_heads`,
copies: await sql`SELECT * FROM agent_instruction_working_copies`,
});
const before = await snapshot();
expect(before.relations).toHaveLength(3);
expect(await inspectMigrations(database.connectionString)).toMatchObject({
status: "needsMigrations", pendingMigrations: ["0287_serious_tinkerer.sql"],
});
await applyPendingMigrations(database.connectionString);
const upgraded = await snapshot();
expect(upgraded).toEqual({ ...before, journal: expect.arrayContaining(before.journal) });
expect(upgraded.journal).toHaveLength(before.journal.length + 1);
for (let attempt = 0; attempt < 2; attempt += 1) {
expect(await inspectMigrations(database.connectionString)).toMatchObject({ status: "upToDate" });
await applyPendingMigrations(database.connectionString);
expect(await snapshot()).toEqual(upgraded);
}
await expect(sql`UPDATE agent_instruction_heads SET company_id = ${otherCompanyId} WHERE revision_id = ${revisionId}`)
.rejects.toMatchObject({ code: "23503" });
await expect(sql`UPDATE agent_instruction_working_copies SET company_id = ${otherCompanyId} WHERE run_id = ${runId}`)
.rejects.toMatchObject({ code: "23503" });
} finally {
await sql.end();
await database.cleanup();
}
}, EMBEDDED_POSTGRES_TEST_TIMEOUT_MS);
});
@@ -42,6 +42,11 @@ describeEmbeddedPostgres("connections v3 schema core migration", () => {
await sql`DROP TABLE IF EXISTS "ai_connection_defaults"`;
await sql`DROP TABLE IF EXISTS "connection_grant_delegations"`;
await sql`DROP TABLE IF EXISTS "connection_grant_members"`;
// Browser Use is a later grant consumer; rewind its empty fixture tables too.
await sql`DROP TABLE IF EXISTS "browser_use_browsers"`;
await sql`DROP TABLE IF EXISTS "browser_use_runs"`;
await sql`DROP TABLE IF EXISTS "browser_use_sessions"`;
await sql`DROP TABLE IF EXISTS "browser_use_settings"`;
await sql`DROP TABLE IF EXISTS "connection_grants"`;
await sql`DROP INDEX IF EXISTS "tool_connections_company_uid_uq"`;
await sql`ALTER TABLE "tool_connections" DROP CONSTRAINT IF EXISTS "tool_connections_company_id_uq"`;
@@ -0,0 +1,61 @@
-- Retain preview instruction rows and pending captures when upgrading PR #14325.
CREATE TABLE IF NOT EXISTS "agent_instruction_heads" (
"company_id" uuid NOT NULL,
"agent_id" uuid NOT NULL,
"entry_file" text NOT NULL,
"revision_id" uuid NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "agent_instruction_heads_identity_uq" UNIQUE("company_id","agent_id","entry_file")
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "agent_instruction_revisions" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"company_id" uuid NOT NULL,
"agent_id" uuid NOT NULL,
"entry_file" text NOT NULL,
"content_base64" text NOT NULL,
"content_hash" text NOT NULL,
"byte_length" integer NOT NULL,
"parent_revision_id" uuid,
"base_revision_id" uuid,
"restored_from_revision_id" uuid,
"actor_agent_id" uuid,
"actor_user_id" text,
"responsible_user_id" text,
"source_run_id" uuid,
"source" text NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "agent_instruction_revisions_identity_uq" UNIQUE("company_id","agent_id","entry_file","id")
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "agent_instruction_working_copies" (
"run_id" uuid PRIMARY KEY NOT NULL,
"company_id" uuid NOT NULL,
"agent_id" uuid NOT NULL,
"responsible_user_id" text NOT NULL,
"entry_file" text NOT NULL,
"base_revision_id" uuid,
"base_hash" text NOT NULL,
"local_root" text NOT NULL,
"execution_root" text NOT NULL,
"location" text NOT NULL,
"state" text DEFAULT 'prepared' NOT NULL,
"candidate_base64" text,
"candidate_hash" text,
"error_code" text,
"error_message" text,
"receipt" jsonb,
"process_stopped_at" timestamp with time zone,
"attempts" integer DEFAULT 0 NOT NULL,
"next_attempt_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
DO $$ BEGIN ALTER TABLE "agent_instruction_heads" ADD CONSTRAINT "agent_instruction_heads_company_id_agent_id_entry_file_revision_id_agent_instruction_revisions_company_id_agent_id_entry_file_id_fk" FOREIGN KEY ("company_id","agent_id","entry_file","revision_id") REFERENCES "public"."agent_instruction_revisions"("company_id","agent_id","entry_file","id") ON DELETE cascade ON UPDATE no action; EXCEPTION WHEN duplicate_object THEN NULL; END $$;--> statement-breakpoint
DO $$ BEGIN ALTER TABLE "agent_instruction_revisions" ADD CONSTRAINT "agent_instruction_revisions_company_id_agent_id_agents_company_id_id_fk" FOREIGN KEY ("company_id","agent_id") REFERENCES "public"."agents"("company_id","id") ON DELETE cascade ON UPDATE no action; EXCEPTION WHEN duplicate_object THEN NULL; END $$;--> statement-breakpoint
DO $$ BEGIN ALTER TABLE "agent_instruction_working_copies" ADD CONSTRAINT "agent_instruction_working_copies_run_id_heartbeat_runs_id_fk" FOREIGN KEY ("run_id") REFERENCES "public"."heartbeat_runs"("id") ON DELETE cascade ON UPDATE no action; EXCEPTION WHEN duplicate_object THEN NULL; END $$;--> statement-breakpoint
DO $$ BEGIN ALTER TABLE "agent_instruction_working_copies" ADD CONSTRAINT "agent_instruction_working_copies_company_id_agent_id_agents_company_id_id_fk" FOREIGN KEY ("company_id","agent_id") REFERENCES "public"."agents"("company_id","id") ON DELETE cascade ON UPDATE no action; EXCEPTION WHEN duplicate_object THEN NULL; END $$;--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "agent_instruction_revisions_history_idx" ON "agent_instruction_revisions" USING btree ("company_id","agent_id","entry_file","created_at","id");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "agent_instruction_copies_pending_idx" ON "agent_instruction_working_copies" USING btree ("state","next_attempt_at");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "agent_instruction_copies_agent_idx" ON "agent_instruction_working_copies" USING btree ("company_id","agent_id","created_at");
@@ -0,0 +1,36 @@
CREATE TABLE IF NOT EXISTS "chat_completion_deliveries" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"company_id" uuid NOT NULL,
"task_id" uuid NOT NULL,
"status_version" integer NOT NULL,
"status" text DEFAULT 'pending' NOT NULL,
"attempts" integer DEFAULT 0 NOT NULL,
"next_attempt_at" timestamp with time zone DEFAULT now() NOT NULL,
"target_run_id" uuid,
"response_comment_id" uuid,
"error" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "chat_task_handoffs" (
"task_id" uuid PRIMARY KEY NOT NULL,
"company_id" uuid NOT NULL,
"conversation_id" uuid NOT NULL,
"agent_id" uuid NOT NULL,
"session_generation" integer NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
DO $$ BEGIN ALTER TABLE "chat_completion_deliveries" ADD CONSTRAINT "chat_completion_deliveries_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE cascade ON UPDATE no action; EXCEPTION WHEN duplicate_object THEN NULL; END $$;--> statement-breakpoint
DO $$ BEGIN ALTER TABLE "chat_completion_deliveries" ADD CONSTRAINT "chat_completion_deliveries_task_id_chat_task_handoffs_task_id_fk" FOREIGN KEY ("task_id") REFERENCES "public"."chat_task_handoffs"("task_id") ON DELETE cascade ON UPDATE no action; EXCEPTION WHEN duplicate_object THEN NULL; END $$;--> statement-breakpoint
DO $$ BEGIN ALTER TABLE "chat_completion_deliveries" ADD CONSTRAINT "chat_completion_deliveries_target_run_id_heartbeat_runs_id_fk" FOREIGN KEY ("target_run_id") REFERENCES "public"."heartbeat_runs"("id") ON DELETE set null ON UPDATE no action; EXCEPTION WHEN duplicate_object THEN NULL; END $$;--> statement-breakpoint
DO $$ BEGIN ALTER TABLE "chat_completion_deliveries" ADD CONSTRAINT "chat_completion_deliveries_response_comment_id_issue_comments_id_fk" FOREIGN KEY ("response_comment_id") REFERENCES "public"."issue_comments"("id") ON DELETE set null ON UPDATE no action; EXCEPTION WHEN duplicate_object THEN NULL; END $$;--> statement-breakpoint
DO $$ BEGIN ALTER TABLE "chat_task_handoffs" ADD CONSTRAINT "chat_task_handoffs_task_id_issues_id_fk" FOREIGN KEY ("task_id") REFERENCES "public"."issues"("id") ON DELETE cascade ON UPDATE no action; EXCEPTION WHEN duplicate_object THEN NULL; END $$;--> statement-breakpoint
DO $$ BEGIN ALTER TABLE "chat_task_handoffs" ADD CONSTRAINT "chat_task_handoffs_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE cascade ON UPDATE no action; EXCEPTION WHEN duplicate_object THEN NULL; END $$;--> statement-breakpoint
DO $$ BEGIN ALTER TABLE "chat_task_handoffs" ADD CONSTRAINT "chat_task_handoffs_conversation_id_issues_id_fk" FOREIGN KEY ("conversation_id") REFERENCES "public"."issues"("id") ON DELETE cascade ON UPDATE no action; EXCEPTION WHEN duplicate_object THEN NULL; END $$;--> statement-breakpoint
DO $$ BEGIN ALTER TABLE "chat_task_handoffs" ADD CONSTRAINT "chat_task_handoffs_agent_id_agents_id_fk" FOREIGN KEY ("agent_id") REFERENCES "public"."agents"("id") ON DELETE cascade ON UPDATE no action; EXCEPTION WHEN duplicate_object THEN NULL; END $$;--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "chat_completion_deliveries_transition_uq" ON "chat_completion_deliveries" USING btree ("task_id","status_version");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "chat_completion_deliveries_pending_idx" ON "chat_completion_deliveries" USING btree ("status","next_attempt_at");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "chat_task_handoffs_source_idx" ON "chat_task_handoffs" USING btree ("company_id","conversation_id");--> statement-breakpoint
-- paperclip:migration-safety-ignore large-create-index-not-concurrently: Drizzle migrations are transactional, so CONCURRENTLY is unavailable. This new key namespace has no existing matches; the partial unique index is required for atomic completion wake deduplication. The one-time table scan takes a write lock.
CREATE UNIQUE INDEX IF NOT EXISTS "agent_wakeup_requests_chat_completion_uq" ON "agent_wakeup_requests" USING btree ("company_id","idempotency_key") WHERE "agent_wakeup_requests"."idempotency_key" LIKE 'chat-completion:%';
@@ -0,0 +1,5 @@
-- Drop the per-user keyboard shortcut preference. Keyboard shortcuts are now
-- always enabled for every signed-in user, so the column lost its last reader
-- when the Profile settings toggle and the /api/auth/preferences routes were
-- removed.
ALTER TABLE "user" DROP COLUMN IF EXISTS "keyboard_shortcuts";
@@ -0,0 +1,155 @@
CREATE TABLE IF NOT EXISTS "browser_use_browsers" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"company_id" uuid NOT NULL,
"session_id" uuid NOT NULL,
"provider_browser_id" uuid NOT NULL,
"status" text DEFAULT 'active' NOT NULL,
"expires_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "browser_use_runs" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"company_id" uuid NOT NULL,
"session_id" uuid NOT NULL,
"heartbeat_run_id" uuid NOT NULL,
"invocation_id" uuid NOT NULL,
"provider_run_id" uuid,
"status" text DEFAULT 'creating' NOT NULL,
"detached_until" timestamp with time zone,
"recovery_cursor" text,
"event_cursor" integer DEFAULT 0 NOT NULL,
"events_drained" integer DEFAULT 0 NOT NULL,
"accounted_cents" integer DEFAULT 0 NOT NULL,
"progress" text,
"result" jsonb,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "browser_use_sessions" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"company_id" uuid NOT NULL,
"issue_id" uuid NOT NULL,
"agent_id" uuid NOT NULL,
"connection_id" uuid NOT NULL,
"grant_id" uuid NOT NULL,
"provider_session_id" uuid,
"status" text DEFAULT 'starting' NOT NULL,
"idle_deadline" timestamp with time zone,
"stop_requested" text,
"error" text,
"lease_token" uuid,
"lease_until" timestamp with time zone,
"next_poll_at" timestamp with time zone DEFAULT now() NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "browser_use_settings" (
"grant_id" uuid PRIMARY KEY NOT NULL,
"company_id" uuid NOT NULL,
"allowed_profile_ids" jsonb DEFAULT '[]'::jsonb NOT NULL,
"max_cost_usd" text
);
--> statement-breakpoint
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'browser_use_browsers_company_id_companies_id_fk' AND conrelid = 'public.browser_use_browsers'::regclass) THEN
ALTER TABLE "browser_use_browsers" ADD CONSTRAINT "browser_use_browsers_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;
END IF;
END $$;--> statement-breakpoint
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'browser_use_browsers_session_id_browser_use_sessions_id_fk' AND conrelid = 'public.browser_use_browsers'::regclass) THEN
ALTER TABLE "browser_use_browsers" ADD CONSTRAINT "browser_use_browsers_session_id_browser_use_sessions_id_fk" FOREIGN KEY ("session_id") REFERENCES "public"."browser_use_sessions"("id") ON DELETE no action ON UPDATE no action;
END IF;
END $$;--> statement-breakpoint
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'browser_use_runs_company_id_companies_id_fk' AND conrelid = 'public.browser_use_runs'::regclass) THEN
ALTER TABLE "browser_use_runs" ADD CONSTRAINT "browser_use_runs_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;
END IF;
END $$;--> statement-breakpoint
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'browser_use_runs_session_id_browser_use_sessions_id_fk' AND conrelid = 'public.browser_use_runs'::regclass) THEN
ALTER TABLE "browser_use_runs" ADD CONSTRAINT "browser_use_runs_session_id_browser_use_sessions_id_fk" FOREIGN KEY ("session_id") REFERENCES "public"."browser_use_sessions"("id") ON DELETE no action ON UPDATE no action;
END IF;
END $$;--> statement-breakpoint
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'browser_use_runs_heartbeat_run_id_heartbeat_runs_id_fk' AND conrelid = 'public.browser_use_runs'::regclass) THEN
ALTER TABLE "browser_use_runs" ADD CONSTRAINT "browser_use_runs_heartbeat_run_id_heartbeat_runs_id_fk" FOREIGN KEY ("heartbeat_run_id") REFERENCES "public"."heartbeat_runs"("id") ON DELETE no action ON UPDATE no action;
END IF;
END $$;--> statement-breakpoint
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'browser_use_sessions_company_id_companies_id_fk' AND conrelid = 'public.browser_use_sessions'::regclass) THEN
ALTER TABLE "browser_use_sessions" ADD CONSTRAINT "browser_use_sessions_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;
END IF;
END $$;--> statement-breakpoint
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'browser_use_sessions_issue_id_issues_id_fk' AND conrelid = 'public.browser_use_sessions'::regclass) THEN
ALTER TABLE "browser_use_sessions" ADD CONSTRAINT "browser_use_sessions_issue_id_issues_id_fk" FOREIGN KEY ("issue_id") REFERENCES "public"."issues"("id") ON DELETE no action ON UPDATE no action;
END IF;
END $$;--> statement-breakpoint
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'browser_use_sessions_agent_id_agents_id_fk' AND conrelid = 'public.browser_use_sessions'::regclass) THEN
ALTER TABLE "browser_use_sessions" ADD CONSTRAINT "browser_use_sessions_agent_id_agents_id_fk" FOREIGN KEY ("agent_id") REFERENCES "public"."agents"("id") ON DELETE no action ON UPDATE no action;
END IF;
END $$;--> statement-breakpoint
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'browser_use_sessions_connection_id_tool_connections_id_fk' AND conrelid = 'public.browser_use_sessions'::regclass) THEN
ALTER TABLE "browser_use_sessions" ADD CONSTRAINT "browser_use_sessions_connection_id_tool_connections_id_fk" FOREIGN KEY ("connection_id") REFERENCES "public"."tool_connections"("id") ON DELETE no action ON UPDATE no action;
END IF;
END $$;--> statement-breakpoint
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'browser_use_sessions_grant_id_connection_grants_id_fk' AND conrelid = 'public.browser_use_sessions'::regclass) THEN
ALTER TABLE "browser_use_sessions" ADD CONSTRAINT "browser_use_sessions_grant_id_connection_grants_id_fk" FOREIGN KEY ("grant_id") REFERENCES "public"."connection_grants"("id") ON DELETE no action ON UPDATE no action;
END IF;
END $$;--> statement-breakpoint
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'browser_use_settings_grant_id_connection_grants_id_fk' AND conrelid = 'public.browser_use_settings'::regclass) THEN
ALTER TABLE "browser_use_settings" ADD CONSTRAINT "browser_use_settings_grant_id_connection_grants_id_fk" FOREIGN KEY ("grant_id") REFERENCES "public"."connection_grants"("id") ON DELETE no action ON UPDATE no action;
END IF;
END $$;--> statement-breakpoint
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'browser_use_settings_company_id_companies_id_fk' AND conrelid = 'public.browser_use_settings'::regclass) THEN
ALTER TABLE "browser_use_settings" ADD CONSTRAINT "browser_use_settings_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;
END IF;
END $$;--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "browser_use_browsers_provider_uq" ON "browser_use_browsers" USING btree ("provider_browser_id");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "browser_use_browsers_session_idx" ON "browser_use_browsers" USING btree ("session_id");--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "browser_use_runs_invocation_uq" ON "browser_use_runs" USING btree ("invocation_id");--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "browser_use_runs_provider_uq" ON "browser_use_runs" USING btree ("provider_run_id");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "browser_use_runs_session_idx" ON "browser_use_runs" USING btree ("session_id");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "browser_use_sessions_task_idx" ON "browser_use_sessions" USING btree ("company_id","issue_id");--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "browser_use_sessions_provider_uq" ON "browser_use_sessions" USING btree ("provider_session_id");
--> statement-breakpoint
-- Support development databases that applied the initial browser schema.
ALTER TABLE "browser_use_runs" ADD COLUMN IF NOT EXISTS "detached_until" timestamp with time zone;
--> statement-breakpoint
ALTER TABLE "browser_use_runs" ADD COLUMN IF NOT EXISTS "recovery_cursor" text;
--> statement-breakpoint
-- Preserve pre-release connections, grants and browser history under the Cloud identity.
UPDATE "tool_connections"
SET "config" = jsonb_set("config", '{sourceTemplateKey}', '"browser-use-cloud"'),
"transport_config" = CASE WHEN "transport_config"->>'sourceTemplateKey' = 'browser-use'
THEN jsonb_set("transport_config", '{sourceTemplateKey}', '"browser-use-cloud"') ELSE "transport_config" END
WHERE "transport" = 'rest_api' AND "config"->>'sourceTemplateKey' = 'browser-use';
--> statement-breakpoint
UPDATE "tool_applications"
SET "metadata" = "metadata" || '{"sourceTemplateKey":"browser-use-cloud","galleryKey":"browser-use-cloud"}'::jsonb,
"application_key" = regexp_replace("application_key", '^app-gallery:browser-use:', 'app-gallery:browser-use-cloud:')
WHERE "type" = 'rest_api' AND "metadata"->>'sourceTemplateKey' = 'browser-use';
--> statement-breakpoint
UPDATE "cost_events"
SET "provider" = 'browser-use-cloud',
"biller" = CASE WHEN "biller" = 'browser-use' THEN 'browser-use-cloud' ELSE "biller" END,
"billing_code" = regexp_replace("billing_code", '^browser-use:', 'browser-use-cloud:')
WHERE "provider" = 'browser-use' AND "billing_code" IN (
SELECT 'browser-use:' || "id"::text FROM "browser_use_runs" WHERE "company_id" = "cost_events"."company_id"
);
--> statement-breakpoint
UPDATE "finance_events"
SET "provider" = 'browser-use-cloud',
"biller" = CASE WHEN "biller" = 'browser-use' THEN 'browser-use-cloud' ELSE "biller" END,
"billing_code" = regexp_replace("billing_code", '^browser-use:', 'browser-use-cloud:')
WHERE "provider" = 'browser-use' AND "billing_code" IN (
SELECT 'browser-use:' || "id"::text FROM "browser_use_runs" WHERE "company_id" = "finance_events"."company_id"
);
@@ -0,0 +1,119 @@
CREATE TABLE IF NOT EXISTS "company_skill_source_entries" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"company_id" uuid NOT NULL,
"source_id" uuid NOT NULL,
"path" text NOT NULL,
"name" text NOT NULL,
"description" text,
"skill_id" uuid,
"selection" text DEFAULT 'new' NOT NULL,
"present" boolean DEFAULT true NOT NULL,
"error" text
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "company_skill_sources" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"company_id" uuid NOT NULL,
"repository_id" text,
"repository_url" text NOT NULL,
"full_name" text NOT NULL,
"tracking_ref" text NOT NULL,
"connection_id" uuid,
"excluded_folders" jsonb DEFAULT '[]'::jsonb NOT NULL,
"enabled" boolean DEFAULT true NOT NULL,
"revision" integer DEFAULT 0 NOT NULL,
"last_attempt_at" timestamp with time zone,
"last_success_at" timestamp with time zone,
"last_scan_commit" text,
"last_error" text,
"lease_token" uuid,
"lease_expires_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'company_skill_source_entries_company_id_companies_id_fk' AND conrelid = 'company_skill_source_entries'::regclass) THEN
ALTER TABLE "company_skill_source_entries" ADD CONSTRAINT "company_skill_source_entries_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE cascade ON UPDATE no action;
END IF;
END $$;--> statement-breakpoint
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'company_skill_source_entries_source_id_company_skill_sources_id_fk' AND conrelid = 'company_skill_source_entries'::regclass) THEN
ALTER TABLE "company_skill_source_entries" ADD CONSTRAINT "company_skill_source_entries_source_id_company_skill_sources_id_fk" FOREIGN KEY ("source_id") REFERENCES "public"."company_skill_sources"("id") ON DELETE cascade ON UPDATE no action;
END IF;
END $$;--> statement-breakpoint
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'company_skill_source_entries_skill_id_company_skills_id_fk' AND conrelid = 'company_skill_source_entries'::regclass) THEN
ALTER TABLE "company_skill_source_entries" ADD CONSTRAINT "company_skill_source_entries_skill_id_company_skills_id_fk" FOREIGN KEY ("skill_id") REFERENCES "public"."company_skills"("id") ON DELETE set null ON UPDATE no action;
END IF;
END $$;--> statement-breakpoint
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'company_skill_sources_company_id_companies_id_fk' AND conrelid = 'company_skill_sources'::regclass) THEN
ALTER TABLE "company_skill_sources" ADD CONSTRAINT "company_skill_sources_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE cascade ON UPDATE no action;
END IF;
END $$;--> statement-breakpoint
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'company_skill_sources_connection_id_tool_connections_id_fk' AND conrelid = 'company_skill_sources'::regclass) THEN
ALTER TABLE "company_skill_sources" ADD CONSTRAINT "company_skill_sources_connection_id_tool_connections_id_fk" FOREIGN KEY ("connection_id") REFERENCES "public"."tool_connections"("id") ON DELETE set null ON UPDATE no action;
END IF;
END $$;--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "company_skill_source_entries_source_path_idx" ON "company_skill_source_entries" USING btree ("source_id","path");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "company_skill_source_entries_company_skill_idx" ON "company_skill_source_entries" USING btree ("company_id","skill_id");--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "company_skill_sources_repository_ref_idx" ON "company_skill_sources" USING btree ("company_id","repository_url","tracking_ref");--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "company_skill_sources_identity_ref_idx" ON "company_skill_sources" USING btree ("company_id","repository_id","tracking_ref");
--> statement-breakpoint
-- Adopt only actual GitHub imports. Keyset batches use company_skills' primary
-- key; only source provenance is added, with no provider requests, content rewrites, or credential assumptions.
DO $$
DECLARE
skill record;
last_id uuid := '00000000-0000-0000-0000-000000000000';
adopted_source_id uuid;
repo_url text;
repo_name text;
adopted_tracking_ref text;
skill_path text;
batch_count integer;
BEGIN
LOOP
batch_count := 0;
FOR skill IN
SELECT id, company_id, name, description, slug, metadata, source_ref
FROM company_skills
WHERE id > last_id AND source_type = 'github'
AND coalesce(metadata->>'hostname', 'github.com') = 'github.com'
AND coalesce(metadata->>'sourceKind', '') NOT IN ('paperclip_bundled', 'catalog')
AND metadata->>'catalogId' IS NULL
AND metadata->>'owner' ~ '^[A-Za-z0-9_.-]+$'
AND metadata->>'repo' ~ '^[A-Za-z0-9_.-]+$'
ORDER BY id LIMIT 500
LOOP
last_id := skill.id;
batch_count := batch_count + 1;
repo_name := (skill.metadata->>'owner') || '/' || (skill.metadata->>'repo');
repo_url := 'https://github.com/' || lower(repo_name);
-- Older imports recorded only the installed commit. Keep explicit tracking
-- refs (including intentional commit pins); resolve unknown refs on first refresh.
adopted_tracking_ref := coalesce(
nullif(skill.metadata->>'trackingRef', ''),
CASE WHEN skill.metadata->>'ref' !~* '^[0-9a-f]{40}$' THEN nullif(skill.metadata->>'ref', '') END,
CASE WHEN skill.source_ref !~* '^[0-9a-f]{40}$' THEN nullif(skill.source_ref, '') END,
'HEAD'
);
skill_path := trim(both '/' from coalesce(skill.metadata->>'repoSkillDir', skill.slug));
IF skill_path = '.' THEN skill_path := ''; END IF;
skill_path := CASE WHEN skill_path = '' THEN 'SKILL.md' ELSE skill_path || '/SKILL.md' END;
INSERT INTO company_skill_sources (company_id, repository_url, full_name, tracking_ref)
VALUES (skill.company_id, repo_url, repo_name, adopted_tracking_ref)
ON CONFLICT (company_id, repository_url, tracking_ref) DO NOTHING;
SELECT id INTO adopted_source_id FROM company_skill_sources
WHERE company_id = skill.company_id AND repository_url = repo_url AND company_skill_sources.tracking_ref = adopted_tracking_ref;
INSERT INTO company_skill_source_entries (company_id, source_id, path, name, description, skill_id, selection)
VALUES (skill.company_id, adopted_source_id, skill_path, skill.name, skill.description, skill.id, 'selected')
ON CONFLICT (source_id, path) DO NOTHING;
UPDATE company_skills SET metadata = coalesce(metadata, '{}'::jsonb)
|| jsonb_build_object('legacySkillSourceId', adopted_source_id, 'skillSourcePath', skill_path)
WHERE id = skill.id AND company_id = skill.company_id;
END LOOP;
EXIT WHEN batch_count < 500;
END LOOP;
END $$;
@@ -0,0 +1 @@
ALTER TABLE "company_skill_source_entries" ADD COLUMN IF NOT EXISTS "inspection" jsonb;
File diff suppressed because it is too large. Load diff
@@ -1,6 +1,6 @@
{
"id": "7db2ceec-ed62-4968-90c9-4c79206cfe34",
"prevId": "786cd4a9-366b-4934-be90-014e1ee15b51",
"id": "770b6d59-6346-4c92-8e49-0b71cf4819ab",
"prevId": "26beb23c-50c3-42f9-8a1c-814d0c8acfbe",
"version": "7",
"dialect": "postgresql",
"tables": {
@@ -829,6 +829,479 @@
"checkConstraints": {},
"isRLSEnabled": false
},
"public.agent_instruction_heads": {
"name": "agent_instruction_heads",
"schema": "",
"columns": {
"company_id": {
"name": "company_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"agent_id": {
"name": "agent_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"entry_file": {
"name": "entry_file",
"type": "text",
"primaryKey": false,
"notNull": true
},
"revision_id": {
"name": "revision_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"agent_instruction_heads_company_id_agent_id_entry_file_revision_id_agent_instruction_revisions_company_id_agent_id_entry_file_id_fk": {
"name": "agent_instruction_heads_company_id_agent_id_entry_file_revision_id_agent_instruction_revisions_company_id_agent_id_entry_file_id_fk",
"tableFrom": "agent_instruction_heads",
"tableTo": "agent_instruction_revisions",
"columnsFrom": [
"company_id",
"agent_id",
"entry_file",
"revision_id"
],
"columnsTo": [
"company_id",
"agent_id",
"entry_file",
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"agent_instruction_heads_identity_uq": {
"name": "agent_instruction_heads_identity_uq",
"nullsNotDistinct": false,
"columns": [
"company_id",
"agent_id",
"entry_file"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.agent_instruction_revisions": {
"name": "agent_instruction_revisions",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"company_id": {
"name": "company_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"agent_id": {
"name": "agent_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"entry_file": {
"name": "entry_file",
"type": "text",
"primaryKey": false,
"notNull": true
},
"content_base64": {
"name": "content_base64",
"type": "text",
"primaryKey": false,
"notNull": true
},
"content_hash": {
"name": "content_hash",
"type": "text",
"primaryKey": false,
"notNull": true
},
"byte_length": {
"name": "byte_length",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"parent_revision_id": {
"name": "parent_revision_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"base_revision_id": {
"name": "base_revision_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"restored_from_revision_id": {
"name": "restored_from_revision_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"actor_agent_id": {
"name": "actor_agent_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"actor_user_id": {
"name": "actor_user_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"responsible_user_id": {
"name": "responsible_user_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"source_run_id": {
"name": "source_run_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"source": {
"name": "source",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"agent_instruction_revisions_history_idx": {
"name": "agent_instruction_revisions_history_idx",
"columns": [
{
"expression": "company_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "agent_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "entry_file",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "created_at",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"agent_instruction_revisions_company_id_agent_id_agents_company_id_id_fk": {
"name": "agent_instruction_revisions_company_id_agent_id_agents_company_id_id_fk",
"tableFrom": "agent_instruction_revisions",
"tableTo": "agents",
"columnsFrom": [
"company_id",
"agent_id"
],
"columnsTo": [
"company_id",
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"agent_instruction_revisions_identity_uq": {
"name": "agent_instruction_revisions_identity_uq",
"nullsNotDistinct": false,
"columns": [
"company_id",
"agent_id",
"entry_file",
"id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.agent_instruction_working_copies": {
"name": "agent_instruction_working_copies",
"schema": "",
"columns": {
"run_id": {
"name": "run_id",
"type": "uuid",
"primaryKey": true,
"notNull": true
},
"company_id": {
"name": "company_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"agent_id": {
"name": "agent_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"responsible_user_id": {
"name": "responsible_user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"entry_file": {
"name": "entry_file",
"type": "text",
"primaryKey": false,
"notNull": true
},
"base_revision_id": {
"name": "base_revision_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"base_hash": {
"name": "base_hash",
"type": "text",
"primaryKey": false,
"notNull": true
},
"local_root": {
"name": "local_root",
"type": "text",
"primaryKey": false,
"notNull": true
},
"execution_root": {
"name": "execution_root",
"type": "text",
"primaryKey": false,
"notNull": true
},
"location": {
"name": "location",
"type": "text",
"primaryKey": false,
"notNull": true
},
"state": {
"name": "state",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'prepared'"
},
"candidate_base64": {
"name": "candidate_base64",
"type": "text",
"primaryKey": false,
"notNull": false
},
"candidate_hash": {
"name": "candidate_hash",
"type": "text",
"primaryKey": false,
"notNull": false
},
"error_code": {
"name": "error_code",
"type": "text",
"primaryKey": false,
"notNull": false
},
"error_message": {
"name": "error_message",
"type": "text",
"primaryKey": false,
"notNull": false
},
"receipt": {
"name": "receipt",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"process_stopped_at": {
"name": "process_stopped_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"attempts": {
"name": "attempts",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"next_attempt_at": {
"name": "next_attempt_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"agent_instruction_copies_pending_idx": {
"name": "agent_instruction_copies_pending_idx",
"columns": [
{
"expression": "state",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "next_attempt_at",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"agent_instruction_copies_agent_idx": {
"name": "agent_instruction_copies_agent_idx",
"columns": [
{
"expression": "company_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "agent_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "created_at",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"agent_instruction_working_copies_run_id_heartbeat_runs_id_fk": {
"name": "agent_instruction_working_copies_run_id_heartbeat_runs_id_fk",
"tableFrom": "agent_instruction_working_copies",
"tableTo": "heartbeat_runs",
"columnsFrom": [
"run_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"agent_instruction_working_copies_company_id_agent_id_agents_company_id_id_fk": {
"name": "agent_instruction_working_copies_company_id_agent_id_agents_company_id_id_fk",
"tableFrom": "agent_instruction_working_copies",
"tableTo": "agents",
"columnsFrom": [
"company_id",
"agent_id"
],
"columnsTo": [
"company_id",
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.agent_memberships": {
"name": "agent_memberships",
"schema": "",
@@ -1942,6 +2415,28 @@
"method": "btree",
"with": {}
},
"agent_wakeup_requests_chat_completion_uq": {
"name": "agent_wakeup_requests_chat_completion_uq",
"columns": [
{
"expression": "company_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "idempotency_key",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"where": "\"agent_wakeup_requests\".\"idempotency_key\" LIKE 'chat-completion:%'",
"concurrently": false,
"method": "btree",
"with": {}
},
"agent_wakeup_requests_company_payload_issue_idx": {
"name": "agent_wakeup_requests_company_payload_issue_idx",
"columns": [
@@ -2917,7 +3412,7 @@
},
"byte_size": {
"name": "byte_size",
"type": "integer",
"type": "bigint",
"primaryKey": false,
"notNull": true
},
@@ -8067,6 +8562,311 @@
"checkConstraints": {},
"isRLSEnabled": false
},
"public.chat_completion_deliveries": {
"name": "chat_completion_deliveries",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"company_id": {
"name": "company_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"task_id": {
"name": "task_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"status_version": {
"name": "status_version",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'pending'"
},
"attempts": {
"name": "attempts",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"next_attempt_at": {
"name": "next_attempt_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"target_run_id": {
"name": "target_run_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"response_comment_id": {
"name": "response_comment_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"error": {
"name": "error",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"chat_completion_deliveries_transition_uq": {
"name": "chat_completion_deliveries_transition_uq",
"columns": [
{
"expression": "task_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "status_version",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
},
"chat_completion_deliveries_pending_idx": {
"name": "chat_completion_deliveries_pending_idx",
"columns": [
{
"expression": "status",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "next_attempt_at",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"chat_completion_deliveries_company_id_companies_id_fk": {
"name": "chat_completion_deliveries_company_id_companies_id_fk",
"tableFrom": "chat_completion_deliveries",
"tableTo": "companies",
"columnsFrom": [
"company_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"chat_completion_deliveries_task_id_chat_task_handoffs_task_id_fk": {
"name": "chat_completion_deliveries_task_id_chat_task_handoffs_task_id_fk",
"tableFrom": "chat_completion_deliveries",
"tableTo": "chat_task_handoffs",
"columnsFrom": [
"task_id"
],
"columnsTo": [
"task_id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"chat_completion_deliveries_target_run_id_heartbeat_runs_id_fk": {
"name": "chat_completion_deliveries_target_run_id_heartbeat_runs_id_fk",
"tableFrom": "chat_completion_deliveries",
"tableTo": "heartbeat_runs",
"columnsFrom": [
"target_run_id"
],
"columnsTo": [
"id"
],
"onDelete": "set null",
"onUpdate": "no action"
},
"chat_completion_deliveries_response_comment_id_issue_comments_id_fk": {
"name": "chat_completion_deliveries_response_comment_id_issue_comments_id_fk",
"tableFrom": "chat_completion_deliveries",
"tableTo": "issue_comments",
"columnsFrom": [
"response_comment_id"
],
"columnsTo": [
"id"
],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.chat_task_handoffs": {
"name": "chat_task_handoffs",
"schema": "",
"columns": {
"task_id": {
"name": "task_id",
"type": "uuid",
"primaryKey": true,
"notNull": true
},
"company_id": {
"name": "company_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"conversation_id": {
"name": "conversation_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"agent_id": {
"name": "agent_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"session_generation": {
"name": "session_generation",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"chat_task_handoffs_source_idx": {
"name": "chat_task_handoffs_source_idx",
"columns": [
{
"expression": "company_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "conversation_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"chat_task_handoffs_task_id_issues_id_fk": {
"name": "chat_task_handoffs_task_id_issues_id_fk",
"tableFrom": "chat_task_handoffs",
"tableTo": "issues",
"columnsFrom": [
"task_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"chat_task_handoffs_company_id_companies_id_fk": {
"name": "chat_task_handoffs_company_id_companies_id_fk",
"tableFrom": "chat_task_handoffs",
"tableTo": "companies",
"columnsFrom": [
"company_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"chat_task_handoffs_conversation_id_issues_id_fk": {
"name": "chat_task_handoffs_conversation_id_issues_id_fk",
"tableFrom": "chat_task_handoffs",
"tableTo": "issues",
"columnsFrom": [
"conversation_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"chat_task_handoffs_agent_id_agents_id_fk": {
"name": "chat_task_handoffs_agent_id_agents_id_fk",
"tableFrom": "chat_task_handoffs",
"tableTo": "agents",
"columnsFrom": [
"agent_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.chat_discord_command_owners": {
"name": "chat_discord_command_owners",
"schema": "",
@@ -38939,6 +39739,113 @@
"checkConstraints": {},
"isRLSEnabled": false
},
"public.runner_api_response_reservations": {
"name": "runner_api_response_reservations",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"company_id": {
"name": "company_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"run_id": {
"name": "run_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"asset_id": {
"name": "asset_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"reserved_bytes": {
"name": "reserved_bytes",
"type": "bigint",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"runner_api_response_reservations_company_idx": {
"name": "runner_api_response_reservations_company_idx",
"columns": [
{
"expression": "company_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"runner_api_response_reservations_company_id_companies_id_fk": {
"name": "runner_api_response_reservations_company_id_companies_id_fk",
"tableFrom": "runner_api_response_reservations",
"tableTo": "companies",
"columnsFrom": [
"company_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"runner_api_response_reservations_run_id_heartbeat_runs_id_fk": {
"name": "runner_api_response_reservations_run_id_heartbeat_runs_id_fk",
"tableFrom": "runner_api_response_reservations",
"tableTo": "heartbeat_runs",
"columnsFrom": [
"run_id"
],
"columnsTo": [
"id"
],
"onDelete": "set null",
"onUpdate": "no action"
},
"runner_api_response_reservations_asset_id_assets_id_fk": {
"name": "runner_api_response_reservations_asset_id_assets_id_fk",
"tableFrom": "runner_api_response_reservations",
"tableTo": "assets",
"columnsFrom": [
"asset_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.secret_access_events": {
"name": "secret_access_events",
"schema": "",
@@ -1,6 +1,6 @@
{
"id": "33120bf3-9cac-4909-ae94-d514ddc1e9e2",
"prevId": "c27cbbec-6948-4b2b-8db8-36d0bf3830fd",
"id": "3960ae6b-5bf1-4fd7-91db-6a18fc7eaa19",
"prevId": "770b6d59-6346-4c92-8e49-0b71cf4819ab",
"version": "7",
"dialect": "postgresql",
"tables": {
@@ -829,6 +829,479 @@
"checkConstraints": {},
"isRLSEnabled": false
},
"public.agent_instruction_heads": {
"name": "agent_instruction_heads",
"schema": "",
"columns": {
"company_id": {
"name": "company_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"agent_id": {
"name": "agent_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"entry_file": {
"name": "entry_file",
"type": "text",
"primaryKey": false,
"notNull": true
},
"revision_id": {
"name": "revision_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"agent_instruction_heads_company_id_agent_id_entry_file_revision_id_agent_instruction_revisions_company_id_agent_id_entry_file_id_fk": {
"name": "agent_instruction_heads_company_id_agent_id_entry_file_revision_id_agent_instruction_revisions_company_id_agent_id_entry_file_id_fk",
"tableFrom": "agent_instruction_heads",
"tableTo": "agent_instruction_revisions",
"columnsFrom": [
"company_id",
"agent_id",
"entry_file",
"revision_id"
],
"columnsTo": [
"company_id",
"agent_id",
"entry_file",
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"agent_instruction_heads_identity_uq": {
"name": "agent_instruction_heads_identity_uq",
"nullsNotDistinct": false,
"columns": [
"company_id",
"agent_id",
"entry_file"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.agent_instruction_revisions": {
"name": "agent_instruction_revisions",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"company_id": {
"name": "company_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"agent_id": {
"name": "agent_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"entry_file": {
"name": "entry_file",
"type": "text",
"primaryKey": false,
"notNull": true
},
"content_base64": {
"name": "content_base64",
"type": "text",
"primaryKey": false,
"notNull": true
},
"content_hash": {
"name": "content_hash",
"type": "text",
"primaryKey": false,
"notNull": true
},
"byte_length": {
"name": "byte_length",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"parent_revision_id": {
"name": "parent_revision_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"base_revision_id": {
"name": "base_revision_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"restored_from_revision_id": {
"name": "restored_from_revision_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"actor_agent_id": {
"name": "actor_agent_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"actor_user_id": {
"name": "actor_user_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"responsible_user_id": {
"name": "responsible_user_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"source_run_id": {
"name": "source_run_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"source": {
"name": "source",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"agent_instruction_revisions_history_idx": {
"name": "agent_instruction_revisions_history_idx",
"columns": [
{
"expression": "company_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "agent_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "entry_file",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "created_at",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"agent_instruction_revisions_company_id_agent_id_agents_company_id_id_fk": {
"name": "agent_instruction_revisions_company_id_agent_id_agents_company_id_id_fk",
"tableFrom": "agent_instruction_revisions",
"tableTo": "agents",
"columnsFrom": [
"company_id",
"agent_id"
],
"columnsTo": [
"company_id",
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"agent_instruction_revisions_identity_uq": {
"name": "agent_instruction_revisions_identity_uq",
"nullsNotDistinct": false,
"columns": [
"company_id",
"agent_id",
"entry_file",
"id"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.agent_instruction_working_copies": {
"name": "agent_instruction_working_copies",
"schema": "",
"columns": {
"run_id": {
"name": "run_id",
"type": "uuid",
"primaryKey": true,
"notNull": true
},
"company_id": {
"name": "company_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"agent_id": {
"name": "agent_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"responsible_user_id": {
"name": "responsible_user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"entry_file": {
"name": "entry_file",
"type": "text",
"primaryKey": false,
"notNull": true
},
"base_revision_id": {
"name": "base_revision_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"base_hash": {
"name": "base_hash",
"type": "text",
"primaryKey": false,
"notNull": true
},
"local_root": {
"name": "local_root",
"type": "text",
"primaryKey": false,
"notNull": true
},
"execution_root": {
"name": "execution_root",
"type": "text",
"primaryKey": false,
"notNull": true
},
"location": {
"name": "location",
"type": "text",
"primaryKey": false,
"notNull": true
},
"state": {
"name": "state",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'prepared'"
},
"candidate_base64": {
"name": "candidate_base64",
"type": "text",
"primaryKey": false,
"notNull": false
},
"candidate_hash": {
"name": "candidate_hash",
"type": "text",
"primaryKey": false,
"notNull": false
},
"error_code": {
"name": "error_code",
"type": "text",
"primaryKey": false,
"notNull": false
},
"error_message": {
"name": "error_message",
"type": "text",
"primaryKey": false,
"notNull": false
},
"receipt": {
"name": "receipt",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"process_stopped_at": {
"name": "process_stopped_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"attempts": {
"name": "attempts",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"next_attempt_at": {
"name": "next_attempt_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"agent_instruction_copies_pending_idx": {
"name": "agent_instruction_copies_pending_idx",
"columns": [
{
"expression": "state",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "next_attempt_at",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"agent_instruction_copies_agent_idx": {
"name": "agent_instruction_copies_agent_idx",
"columns": [
{
"expression": "company_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "agent_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "created_at",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"agent_instruction_working_copies_run_id_heartbeat_runs_id_fk": {
"name": "agent_instruction_working_copies_run_id_heartbeat_runs_id_fk",
"tableFrom": "agent_instruction_working_copies",
"tableTo": "heartbeat_runs",
"columnsFrom": [
"run_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"agent_instruction_working_copies_company_id_agent_id_agents_company_id_id_fk": {
"name": "agent_instruction_working_copies_company_id_agent_id_agents_company_id_id_fk",
"tableFrom": "agent_instruction_working_copies",
"tableTo": "agents",
"columnsFrom": [
"company_id",
"agent_id"
],
"columnsTo": [
"company_id",
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.agent_memberships": {
"name": "agent_memberships",
"schema": "",
@@ -1942,6 +2415,28 @@
"method": "btree",
"with": {}
},
"agent_wakeup_requests_chat_completion_uq": {
"name": "agent_wakeup_requests_chat_completion_uq",
"columns": [
{
"expression": "company_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "idempotency_key",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"where": "\"agent_wakeup_requests\".\"idempotency_key\" LIKE 'chat-completion:%'",
"concurrently": false,
"method": "btree",
"with": {}
},
"agent_wakeup_requests_company_payload_issue_idx": {
"name": "agent_wakeup_requests_company_payload_issue_idx",
"columns": [
@@ -3302,13 +3797,6 @@
"primaryKey": false,
"notNull": false
},
"keyboard_shortcuts": {
"name": "keyboard_shortcuts",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
@@ -8067,6 +8555,311 @@
"checkConstraints": {},
"isRLSEnabled": false
},
"public.chat_completion_deliveries": {
"name": "chat_completion_deliveries",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"company_id": {
"name": "company_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"task_id": {
"name": "task_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"status_version": {
"name": "status_version",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'pending'"
},
"attempts": {
"name": "attempts",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 0
},
"next_attempt_at": {
"name": "next_attempt_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"target_run_id": {
"name": "target_run_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"response_comment_id": {
"name": "response_comment_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"error": {
"name": "error",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"chat_completion_deliveries_transition_uq": {
"name": "chat_completion_deliveries_transition_uq",
"columns": [
{
"expression": "task_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "status_version",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
},
"chat_completion_deliveries_pending_idx": {
"name": "chat_completion_deliveries_pending_idx",
"columns": [
{
"expression": "status",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "next_attempt_at",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"chat_completion_deliveries_company_id_companies_id_fk": {
"name": "chat_completion_deliveries_company_id_companies_id_fk",
"tableFrom": "chat_completion_deliveries",
"tableTo": "companies",
"columnsFrom": [
"company_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"chat_completion_deliveries_task_id_chat_task_handoffs_task_id_fk": {
"name": "chat_completion_deliveries_task_id_chat_task_handoffs_task_id_fk",
"tableFrom": "chat_completion_deliveries",
"tableTo": "chat_task_handoffs",
"columnsFrom": [
"task_id"
],
"columnsTo": [
"task_id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"chat_completion_deliveries_target_run_id_heartbeat_runs_id_fk": {
"name": "chat_completion_deliveries_target_run_id_heartbeat_runs_id_fk",
"tableFrom": "chat_completion_deliveries",
"tableTo": "heartbeat_runs",
"columnsFrom": [
"target_run_id"
],
"columnsTo": [
"id"
],
"onDelete": "set null",
"onUpdate": "no action"
},
"chat_completion_deliveries_response_comment_id_issue_comments_id_fk": {
"name": "chat_completion_deliveries_response_comment_id_issue_comments_id_fk",
"tableFrom": "chat_completion_deliveries",
"tableTo": "issue_comments",
"columnsFrom": [
"response_comment_id"
],
"columnsTo": [
"id"
],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.chat_task_handoffs": {
"name": "chat_task_handoffs",
"schema": "",
"columns": {
"task_id": {
"name": "task_id",
"type": "uuid",
"primaryKey": true,
"notNull": true
},
"company_id": {
"name": "company_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"conversation_id": {
"name": "conversation_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"agent_id": {
"name": "agent_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"session_generation": {
"name": "session_generation",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {
"chat_task_handoffs_source_idx": {
"name": "chat_task_handoffs_source_idx",
"columns": [
{
"expression": "company_id",
"isExpression": false,
"asc": true,
"nulls": "last"
},
{
"expression": "conversation_id",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"chat_task_handoffs_task_id_issues_id_fk": {
"name": "chat_task_handoffs_task_id_issues_id_fk",
"tableFrom": "chat_task_handoffs",
"tableTo": "issues",
"columnsFrom": [
"task_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"chat_task_handoffs_company_id_companies_id_fk": {
"name": "chat_task_handoffs_company_id_companies_id_fk",
"tableFrom": "chat_task_handoffs",
"tableTo": "companies",
"columnsFrom": [
"company_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"chat_task_handoffs_conversation_id_issues_id_fk": {
"name": "chat_task_handoffs_conversation_id_issues_id_fk",
"tableFrom": "chat_task_handoffs",
"tableTo": "issues",
"columnsFrom": [
"conversation_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"chat_task_handoffs_agent_id_agents_id_fk": {
"name": "chat_task_handoffs_agent_id_agents_id_fk",
"tableFrom": "chat_task_handoffs",
"tableTo": "agents",
"columnsFrom": [
"agent_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.chat_discord_command_owners": {
"name": "chat_discord_command_owners",
"schema": "",
File diff suppressed because it is too large. Load diff
+42
View File
@@ -1996,6 +1996,48 @@
"when": 1790607525758,
"tag": "0286_complete_alice",
"breakpoints": true
},
{
"idx": 287,
"version": "7",
"when": 1790611116286,
"tag": "0287_serious_tinkerer",
"breakpoints": true
},
{
"idx": 288,
"version": "7",
"when": 1790690265758,
"tag": "0288_glorious_jamie_braddock",
"breakpoints": true
},
{
"idx": 289,
"version": "7",
"when": 1790727655506,
"tag": "0289_drop_user_keyboard_shortcuts",
"breakpoints": true
},
{
"idx": 290,
"version": "7",
"when": 1790768845200,
"tag": "0290_browser_use_cloud",
"breakpoints": true
},
{
"idx": 291,
"version": "7",
"when": 1790771783697,
"tag": "0291_conscious_secret_warriors",
"breakpoints": true
},
{
"idx": 292,
"version": "7",
"when": 1790776905226,
"tag": "0292_powerful_devos",
"breakpoints": true
}
]
}
@@ -0,0 +1,119 @@
import {
pgTable,
uuid,
text,
integer,
timestamp,
index,
unique,
foreignKey,
jsonb,
} from "drizzle-orm/pg-core";
import { agents } from "./agents.js";
import { heartbeatRuns } from "./heartbeat_runs.js";
/** Append-only content. Base64 preserves every UTF-8 byte, including NUL and BOM. */
export const agentInstructionRevisions = pgTable(
"agent_instruction_revisions",
{
id: uuid("id").primaryKey().defaultRandom(),
companyId: uuid("company_id").notNull(),
agentId: uuid("agent_id").notNull(),
entryFile: text("entry_file").notNull(),
contentBase64: text("content_base64").notNull(),
contentHash: text("content_hash").notNull(),
byteLength: integer("byte_length").notNull(),
parentRevisionId: uuid("parent_revision_id"),
baseRevisionId: uuid("base_revision_id"),
restoredFromRevisionId: uuid("restored_from_revision_id"),
actorAgentId: uuid("actor_agent_id"),
actorUserId: text("actor_user_id"),
responsibleUserId: text("responsible_user_id"),
sourceRunId: uuid("source_run_id"),
source: text("source").notNull(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
},
(t) => ({
owner: foreignKey({
columns: [t.companyId, t.agentId],
foreignColumns: [agents.companyId, agents.id],
}).onDelete("cascade"),
identity: unique("agent_instruction_revisions_identity_uq").on(
t.companyId,
t.agentId,
t.entryFile,
t.id,
),
history: index("agent_instruction_revisions_history_idx").on(
t.companyId,
t.agentId,
t.entryFile,
t.createdAt,
t.id,
),
}),
);
export const agentInstructionHeads = pgTable(
"agent_instruction_heads",
{
companyId: uuid("company_id").notNull(),
agentId: uuid("agent_id").notNull(),
entryFile: text("entry_file").notNull(),
revisionId: uuid("revision_id").notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true })
.notNull()
.defaultNow(),
},
(t) => ({
identity: unique("agent_instruction_heads_identity_uq").on(
t.companyId,
t.agentId,
t.entryFile,
),
revision: foreignKey({
columns: [t.companyId, t.agentId, t.entryFile, t.revisionId],
foreignColumns: [
agentInstructionRevisions.companyId,
agentInstructionRevisions.agentId,
agentInstructionRevisions.entryFile,
agentInstructionRevisions.id,
],
}).onDelete("cascade"),
}),
);
/** A server-owned collection receipt, retained even when canonical CAS rejects an edit. */
export const agentInstructionWorkingCopies = pgTable(
"agent_instruction_working_copies",
{
runId: uuid("run_id").primaryKey().references(() => heartbeatRuns.id, { onDelete: "cascade" }),
companyId: uuid("company_id").notNull(),
agentId: uuid("agent_id").notNull(),
responsibleUserId: text("responsible_user_id").notNull(),
entryFile: text("entry_file").notNull(),
baseRevisionId: uuid("base_revision_id"),
baseHash: text("base_hash").notNull(),
localRoot: text("local_root").notNull(),
executionRoot: text("execution_root").notNull(),
location: text("location").notNull(),
state: text("state").notNull().default("prepared"),
candidateBase64: text("candidate_base64"),
candidateHash: text("candidate_hash"),
errorCode: text("error_code"),
errorMessage: text("error_message"),
receipt: jsonb("receipt").$type<Record<string, unknown>>(),
processStoppedAt: timestamp("process_stopped_at", { withTimezone: true }),
attempts: integer("attempts").notNull().default(0),
nextAttemptAt: timestamp("next_attempt_at", { withTimezone: true }),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
},
(t) => ({
owner: foreignKey({ columns: [t.companyId, t.agentId], foreignColumns: [agents.companyId, agents.id] }).onDelete("cascade"),
pending: index("agent_instruction_copies_pending_idx").on(t.state, t.nextAttemptAt),
history: index("agent_instruction_copies_agent_idx").on(t.companyId, t.agentId, t.createdAt),
}),
);
@@ -81,6 +81,9 @@ export const agentWakeupRequests = pgTable(
toolActionDeliveryIdempotencyUq: uniqueIndex("agent_wakeup_requests_tool_action_delivery_uq")
.on(table.companyId, table.idempotencyKey)
.where(sql`${table.idempotencyKey} LIKE 'tool-action-response:%' AND ${table.status} NOT IN ('skipped', 'failed', 'cancelled')`),
chatCompletionIdempotencyUq: uniqueIndex("agent_wakeup_requests_chat_completion_uq")
.on(table.companyId, table.idempotencyKey)
.where(sql`${table.idempotencyKey} LIKE 'chat-completion:%'`),
companyPayloadIssueIdx: index("agent_wakeup_requests_company_payload_issue_idx").on(
table.companyId,
sql`(${table.payload} ->> 'issueId')`,
-1
View File
@@ -6,7 +6,6 @@ export const authUsers = pgTable("user", {
email: text("email").notNull(),
emailVerified: boolean("email_verified").notNull().default(false),
image: text("image"),
keyboardShortcuts: boolean("keyboard_shortcuts").notNull().default(false),
createdAt: timestamp("created_at", { withTimezone: true }).notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull(),
});
+125
View File
@@ -0,0 +1,125 @@
import {
pgTable,
uuid,
text,
timestamp,
integer,
jsonb,
index,
uniqueIndex,
} from "drizzle-orm/pg-core";
import { companies } from "./companies.js";
import { issues } from "./issues.js";
import { agents } from "./agents.js";
import { heartbeatRuns } from "./heartbeat_runs.js";
import { toolConnections, connectionGrants } from "./tool_access.js";
export const browserUseSettings = pgTable("browser_use_settings", {
grantId: uuid("grant_id")
.primaryKey()
.references(() => connectionGrants.id),
companyId: uuid("company_id")
.notNull()
.references(() => companies.id),
allowedProfileIds: jsonb("allowed_profile_ids")
.$type<string[]>()
.notNull()
.default([]),
maxCostUsd: text("max_cost_usd"),
});
export const browserUseSessions = pgTable(
"browser_use_sessions",
{
id: uuid("id").primaryKey().defaultRandom(),
companyId: uuid("company_id")
.notNull()
.references(() => companies.id),
issueId: uuid("issue_id")
.notNull()
.references(() => issues.id),
agentId: uuid("agent_id")
.notNull()
.references(() => agents.id),
connectionId: uuid("connection_id")
.notNull()
.references(() => toolConnections.id),
grantId: uuid("grant_id")
.notNull()
.references(() => connectionGrants.id),
providerSessionId: uuid("provider_session_id"),
status: text("status").notNull().default("starting"),
idleDeadline: timestamp("idle_deadline", { withTimezone: true }),
stopRequested: text("stop_requested"),
error: text("error"),
leaseToken: uuid("lease_token"),
leaseUntil: timestamp("lease_until", { withTimezone: true }),
nextPollAt: timestamp("next_poll_at", { withTimezone: true })
.notNull()
.defaultNow(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true })
.notNull()
.defaultNow(),
},
(t) => [
index("browser_use_sessions_task_idx").on(t.companyId, t.issueId),
uniqueIndex("browser_use_sessions_provider_uq").on(t.providerSessionId),
],
);
export const browserUseRuns = pgTable(
"browser_use_runs",
{
id: uuid("id").primaryKey().defaultRandom(),
companyId: uuid("company_id")
.notNull()
.references(() => companies.id),
sessionId: uuid("session_id")
.notNull()
.references(() => browserUseSessions.id),
heartbeatRunId: uuid("heartbeat_run_id")
.notNull()
.references(() => heartbeatRuns.id),
invocationId: uuid("invocation_id").notNull(),
providerRunId: uuid("provider_run_id"),
recoveryCursor: text("recovery_cursor"),
status: text("status").notNull().default("creating"),
detachedUntil: timestamp("detached_until", { withTimezone: true }),
eventCursor: integer("event_cursor").notNull().default(0),
eventsDrained: integer("events_drained").notNull().default(0),
accountedCents: integer("accounted_cents").notNull().default(0),
progress: text("progress"),
result: jsonb("result"),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
},
(t) => [
uniqueIndex("browser_use_runs_invocation_uq").on(t.invocationId),
uniqueIndex("browser_use_runs_provider_uq").on(t.providerRunId),
index("browser_use_runs_session_idx").on(t.sessionId),
],
);
export const browserUseBrowsers = pgTable(
"browser_use_browsers",
{
id: uuid("id").primaryKey().defaultRandom(),
companyId: uuid("company_id")
.notNull()
.references(() => companies.id),
sessionId: uuid("session_id")
.notNull()
.references(() => browserUseSessions.id),
providerBrowserId: uuid("provider_browser_id").notNull(),
status: text("status").notNull().default("active"),
expiresAt: timestamp("expires_at", { withTimezone: true }),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
},
(t) => [
uniqueIndex("browser_use_browsers_provider_uq").on(t.providerBrowserId),
index("browser_use_browsers_session_idx").on(t.sessionId),
],
);
@@ -0,0 +1,34 @@
import { index, integer, pgTable, text, timestamp, uniqueIndex, uuid } from "drizzle-orm/pg-core";
import { companies } from "./companies.js";
import { issues } from "./issues.js";
import { agents } from "./agents.js";
import { heartbeatRuns } from "./heartbeat_runs.js";
import { issueComments } from "./issue_comments.js";
/** Server-derived audience captured when a conversation creates an execution task. */
export const chatTaskHandoffs = pgTable("chat_task_handoffs", {
taskId: uuid("task_id").primaryKey().references(() => issues.id, { onDelete: "cascade" }),
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
conversationId: uuid("conversation_id").notNull().references(() => issues.id, { onDelete: "cascade" }),
agentId: uuid("agent_id").notNull().references(() => agents.id, { onDelete: "cascade" }),
sessionGeneration: integer("session_generation").notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
}, t => ({ sourceIdx: index("chat_task_handoffs_source_idx").on(t.companyId, t.conversationId) }));
/** Content-free outbox; results remain on the source task and its documents. */
export const chatCompletionDeliveries = pgTable("chat_completion_deliveries", {
id: uuid("id").primaryKey().defaultRandom(),
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
taskId: uuid("task_id").notNull().references(() => chatTaskHandoffs.taskId, { onDelete: "cascade" }),
statusVersion: integer("status_version").notNull(),
status: text("status").$type<"pending" | "queued" | "delivered" | "superseded" | "exhausted">().notNull().default("pending"),
attempts: integer("attempts").notNull().default(0),
nextAttemptAt: timestamp("next_attempt_at", { withTimezone: true }).notNull().defaultNow(),
targetRunId: uuid("target_run_id").references(() => heartbeatRuns.id, { onDelete: "set null" }),
responseCommentId: uuid("response_comment_id").references(() => issueComments.id, { onDelete: "set null" }),
error: text("error"),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
}, t => ({
transitionUq: uniqueIndex("chat_completion_deliveries_transition_uq").on(t.taskId, t.statusVersion),
pendingIdx: index("chat_completion_deliveries_pending_idx").on(t.status, t.nextAttemptAt),
}));
@@ -0,0 +1,44 @@
import { pgTable, uuid, text, timestamp, jsonb, integer, boolean, uniqueIndex, index } from 'drizzle-orm/pg-core';
import type { SkillPackageInspection } from '@paperclipai/shared';
import { companies } from './companies.js';
import { companySkills } from './company_skills.js';
import { toolConnections } from './tool_access.js';
export const companySkillSources = pgTable('company_skill_sources', {
id: uuid('id').primaryKey().defaultRandom(),
companyId: uuid('company_id').notNull().references(() => companies.id, { onDelete: 'cascade' }),
repositoryId: text('repository_id'),
repositoryUrl: text('repository_url').notNull(),
fullName: text('full_name').notNull(),
trackingRef: text('tracking_ref').notNull(),
connectionId: uuid('connection_id').references(() => toolConnections.id, { onDelete: 'set null' }),
excludedFolders: jsonb('excluded_folders').$type<string[]>().notNull().default([]),
enabled: boolean('enabled').notNull().default(true),
revision: integer('revision').notNull().default(0),
lastAttemptAt: timestamp('last_attempt_at', { withTimezone: true }),
lastSuccessAt: timestamp('last_success_at', { withTimezone: true }),
lastScanCommit: text('last_scan_commit'),
lastError: text('last_error'),
leaseToken: uuid('lease_token'),
leaseExpiresAt: timestamp('lease_expires_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
}, table => ({
repositoryRef: uniqueIndex('company_skill_sources_repository_ref_idx').on(table.companyId, table.repositoryUrl, table.trackingRef),
repositoryIdentity: uniqueIndex('company_skill_sources_identity_ref_idx').on(table.companyId, table.repositoryId, table.trackingRef),
}));
export const companySkillSourceEntries = pgTable('company_skill_source_entries', {
id: uuid('id').primaryKey().defaultRandom(),
companyId: uuid('company_id').notNull().references(() => companies.id, { onDelete: 'cascade' }),
sourceId: uuid('source_id').notNull().references(() => companySkillSources.id, { onDelete: 'cascade' }),
path: text('path').notNull(),
name: text('name').notNull(),
description: text('description'),
inspection: jsonb('inspection').$type<SkillPackageInspection>(),
skillId: uuid('skill_id').references(() => companySkills.id, { onDelete: 'set null' }),
selection: text('selection').$type<'selected' | 'excluded' | 'new'>().notNull().default('new'),
present: boolean('present').notNull().default(true),
error: text('error'),
}, table => ({
sourcePath: uniqueIndex('company_skill_source_entries_source_path_idx').on(table.sourceId, table.path),
companySkill: index('company_skill_source_entries_company_skill_idx').on(table.companyId, table.skillId),
}));
+2
View File
@@ -64,6 +64,8 @@ export const companySkills = pgTable(
export type CompanySkillVersionFileInventoryEntry = CompanySkillFileInventoryEntry & {
content: string;
encoding?: "utf8" | "base64";
executable?: boolean;
};
export const companySkillVersions = pgTable(
+7
View File
@@ -211,3 +211,10 @@ export { aiConnectionDefaults } from "./ai_connection_defaults.js";
export { aiProviderDefaults } from "./ai_provider_defaults.js";
export * from "./email.js";
export { announcementDismissals, announcementPublications } from "./announcement_dismissals.js";
export { chatTaskHandoffs, chatCompletionDeliveries } from "./chat_completion_deliveries.js";
export { agentInstructionRevisions, agentInstructionHeads, agentInstructionWorkingCopies } from "./agent_instruction_revisions.js";
export { browserUseSettings, browserUseSessions, browserUseRuns, browserUseBrowsers } from "./browser_use.js";
export * from "./company_skill_sources.js";
+24 -13
View File
@@ -54,7 +54,11 @@ leave the board showing an active run after the provider turn has already ended.
The package also builds `paperclip-runner-acpx-sidecar`. This bounded v2
stdin/stdout bridge admits the pinned Claude and Codex ACPX profiles. It
validates the exact model, session identity, tool catalog, structured input,
and terminal settlement at the process boundary. Pi remains unavailable.
and terminal settlement at the process boundary. Cursor, Copilot and Pi have
separate candidate branches and remain unavailable in production until local
and Daytona qualification passes. Their verified distributions are build-owned;
no candidate accepts an arbitrary executable. See
[the rich ACP capability report](../../doc/architecture/runner-rich-acp-capabilities.md).
Remote Codex sessions relay assigned app tools through the server's configured
gateway. Small catalogs are sent directly. When a catalog would exceed the
@@ -96,19 +100,22 @@ do not change workspace isolation or grant credentials or connection access.
task tools; `approve-reads` allows assigned reads; `deny-all` rejects requests.
None of these restrictive modes is the default.
Restrictive profiles route supported permission decisions through durable runtime
requests and the existing task interaction controls. Requests are persisted
before presentation; answers are checked against the offered decisions and
acknowledged by the sidecar before settlement. Unknown, stale and duplicate
responses fail. Missing provider decision support remains a blocked disposition,
not implicit approval. Company access checks still run for each Paperclip tool.
Provider death expires pending promises; approvals are never replayed into a
replacement process.
Automatic Paperclip/read allowances currently require the Claude SDK dispatch
boundary. Grok preserves these restricted settings, but its ACP requests lack
independently bound tool authority. They therefore stop with
`approval_required`, including Paperclip tool requests. Use an explicitly
selected `approve-all` policy for unattended Grok work in an assigned sandbox;
Paperclip authorization and governed approvals still apply.
This runtime has no interactive permission handler. An operation that still
requires approval stops the turn with `approval_required`. The server marks the
task blocked, exposes the permission action to the operator, and disables
automatic retry. The operator must review the operation and the agent's
permission setting before retrying. Company access checks still run when each
Paperclip tool executes.
independently bound tool authority. Those operations require a supported operator
permission decision; a missing interactive responder stops with
`approval_required`. An explicitly selected `approve-all` policy permits unattended
Grok work in an assigned sandbox. Paperclip authorization and governed approvals
still apply.
Runnerd selects only qualified provider profiles. Claude Managed and AWS
AgentCore receive immutable company-profile snapshots with explicit retention,
@@ -285,7 +292,11 @@ pnpm --filter @paperclipai/paperclip-runner report:runner-chaos-evals
`report:runner-live-evals` is a paid, provider-backed command. Native Codex
requires `OPENAI_API_KEY`; ACPX Claude requires
`ANTHROPIC_API_KEY`; OpenCode candidates require `OPENROUTER_API_KEY`. The live
matrix admits no Pi profile and does not persist credential values. Set
matrix remains qualified-only and does not persist credential values. Candidate
qualification uses `eval-session --candidate-profile <pi|cursor|copilot>` with an
explicit model and a separately materialized pinned candidate pack. This option
is a constructor-bound diagnostic opt-in; session JSON cannot enable a candidate.
Missing credentials or unverifiable spend block paid qualification. Set
`PAPERCLIP_EVAL_MAX_CAMPAIGN_COST_USD` to a positive finite number to bound
additional scheduling after the observed campaign total reaches that value:
@@ -172,6 +172,7 @@ function ProviderActivity({ item, callbacks }: {
{children.length > 0 ? <ul className="pit-provider-tree">{children.map((child, index) => <li key={String(child.childId ?? index)}><strong>{String(child.role ?? "Child agent")}</strong><span>{String(child.status ?? "unknown")}</span><small>{String(child.summary ?? "")}</small></li>)}</ul> : null}
{sources.length > 0 ? <ul className="pit-provider-sources">{sources.map((source, index) => { const url = typeof source.url === "string" && /^https?:\/\//.test(source.url) ? source.url : null; return <li key={String(source.sourceId ?? index)}>{url === null ? <span>{String(source.title ?? "Unavailable source")}</span> : <a href={url} target="_blank" rel="noreferrer">{String(source.title ?? url)}</a>}<small>Provider-reported</small></li>; })}</ul> : null}
{output.length > 0 ? <pre className="pit-provider-output" aria-label="Command output">{output}</pre> : null}
{item.family === "provider_notice" && Array.isArray(payload.details) ? <dl className="pit-provider-fields">{payload.details.slice(0, 64).map((value, index) => { const detail = valueRecord(value); return <div key={index}><dt>{String(detail.name ?? "Detail")}</dt><dd>{String(detail.value ?? "")}</dd></div>; })}</dl> : null}
{item.family === "model_identity" ? <dl className="pit-provider-fields"><dt>Requested</dt><dd>{String(payload.requestedModel ?? "—")}</dd><dt>Effective</dt><dd>{String(payload.effectiveModel ?? "—")}</dd></dl> : null}
{item.family === "artifact" && typeof payload.reference === "string" ? <button type="button" className="pit-link-button">Open {payload.reference}</button> : null}
<button type="button" className="pit-link-button" onClick={() => callbacks.onOpenEvidence(item.evidenceRef.section, item.evidenceRef.recordId)}>View in Evidence</button>
@@ -8,9 +8,9 @@ The skill/reference inventory and eval cases are the only normative behavior sou
## Baseline Counts
- Skill/reference headings: 155
- Skill/reference headings: 157
- Eval cases: 106 across 16 groups
- Total normative rows: 261
- Total normative rows: 263
- Legacy MCP aliases folded into normative rows: 42
| Eval group | Cases |
@@ -47,30 +47,31 @@ The skill/reference inventory and eval cases are the only normative behavior sou
| skill:skills/paperclip/SKILL.md:conversation-tasks:30 | optional_agent_tool | skills/paperclip/SKILL.md:30 |
| skill:skills/paperclip/SKILL.md:server-verified-external-chat-turns:47 | control_plane_owned | skills/paperclip/SKILL.md:47 |
| skill:skills/paperclip/SKILL.md:the-heartbeat-procedure:87 | optional_agent_tool | skills/paperclip/SKILL.md:87 |
| skill:skills/paperclip/SKILL.md:generated-artifacts-and-work-products:159 | always_agent_tool | skills/paperclip/SKILL.md:159 |
| skill:skills/paperclip/SKILL.md:status-quick-guide:207 | control_plane_owned | skills/paperclip/SKILL.md:207 |
| skill:skills/paperclip/SKILL.md:monitors-and-watchers-say-only-what-you-actually-scheduled:217 | optional_agent_tool | skills/paperclip/SKILL.md:217 |
| skill:skills/paperclip/SKILL.md:delegating-review-tasks:230 | always_agent_tool | skills/paperclip/SKILL.md:230 |
| skill:skills/paperclip/SKILL.md:managing-a-user-s-inbox:241 | control_plane_owned | skills/paperclip/SKILL.md:241 |
| skill:skills/paperclip/SKILL.md:issue-dependencies-blockers:249 | control_plane_owned | skills/paperclip/SKILL.md:249 |
| skill:skills/paperclip/SKILL.md:requesting-board-approval:274 | optional_agent_tool | skills/paperclip/SKILL.md:274 |
| skill:skills/paperclip/SKILL.md:issue-thread-interactions:295 | optional_agent_tool | skills/paperclip/SKILL.md:295 |
| skill:skills/paperclip/SKILL.md:standalone-decisions:324 | optional_agent_tool | skills/paperclip/SKILL.md:324 |
| skill:skills/paperclip/SKILL.md:mcp-tool-approval-gates:428 | optional_agent_tool | skills/paperclip/SKILL.md:428 |
| skill:skills/paperclip/SKILL.md:niche-workflow-pointers:470 | optional_agent_tool | skills/paperclip/SKILL.md:470 |
| skill:skills/paperclip/SKILL.md:cases:480 | optional_agent_tool | skills/paperclip/SKILL.md:480 |
| skill:skills/paperclip/SKILL.md:company-skills-workflow:485 | optional_agent_tool | skills/paperclip/SKILL.md:485 |
| skill:skills/paperclip/SKILL.md:routines:496 | optional_agent_tool | skills/paperclip/SKILL.md:496 |
| skill:skills/paperclip/SKILL.md:issue-workspace-runtime-controls:507 | optional_agent_tool | skills/paperclip/SKILL.md:507 |
| skill:skills/paperclip/SKILL.md:proposing-credentials-safely:514 | optional_agent_tool | skills/paperclip/SKILL.md:514 |
| skill:skills/paperclip/SKILL.md:reading-granted-secrets:521 | optional_agent_tool | skills/paperclip/SKILL.md:521 |
| skill:skills/paperclip/SKILL.md:critical-rules:547 | optional_agent_tool | skills/paperclip/SKILL.md:547 |
| skill:skills/paperclip/SKILL.md:comment-style-required:567 | always_agent_tool | skills/paperclip/SKILL.md:567 |
| skill:skills/paperclip/SKILL.md:update:599 | optional_agent_tool | skills/paperclip/SKILL.md:599 |
| skill:skills/paperclip/SKILL.md:planning-required-when-planning-requested:609 | optional_agent_tool | skills/paperclip/SKILL.md:609 |
| skill:skills/paperclip/SKILL.md:key-endpoints-hot-routes:642 | optional_agent_tool | skills/paperclip/SKILL.md:642 |
| skill:skills/paperclip/SKILL.md:searching-issues:671 | optional_agent_tool | skills/paperclip/SKILL.md:671 |
| skill:skills/paperclip/SKILL.md:full-reference:681 | optional_agent_tool | skills/paperclip/SKILL.md:681 |
| skill:skills/paperclip/SKILL.md:generated-artifacts-and-work-products:195 | always_agent_tool | skills/paperclip/SKILL.md:195 |
| skill:skills/paperclip/SKILL.md:status-quick-guide:243 | control_plane_owned | skills/paperclip/SKILL.md:243 |
| skill:skills/paperclip/SKILL.md:monitors-and-watchers-say-only-what-you-actually-scheduled:253 | optional_agent_tool | skills/paperclip/SKILL.md:253 |
| skill:skills/paperclip/SKILL.md:delegating-review-tasks:266 | always_agent_tool | skills/paperclip/SKILL.md:266 |
| skill:skills/paperclip/SKILL.md:managing-a-user-s-inbox:277 | control_plane_owned | skills/paperclip/SKILL.md:277 |
| skill:skills/paperclip/SKILL.md:issue-dependencies-blockers:285 | control_plane_owned | skills/paperclip/SKILL.md:285 |
| skill:skills/paperclip/SKILL.md:requesting-board-approval:310 | optional_agent_tool | skills/paperclip/SKILL.md:310 |
| skill:skills/paperclip/SKILL.md:issue-thread-interactions:331 | optional_agent_tool | skills/paperclip/SKILL.md:331 |
| skill:skills/paperclip/SKILL.md:standalone-decisions:360 | optional_agent_tool | skills/paperclip/SKILL.md:360 |
| skill:skills/paperclip/SKILL.md:mcp-tool-approval-gates:464 | optional_agent_tool | skills/paperclip/SKILL.md:464 |
| skill:skills/paperclip/SKILL.md:niche-workflow-pointers:506 | optional_agent_tool | skills/paperclip/SKILL.md:506 |
| skill:skills/paperclip/SKILL.md:cases:516 | optional_agent_tool | skills/paperclip/SKILL.md:516 |
| skill:skills/paperclip/SKILL.md:company-skills-workflow:521 | optional_agent_tool | skills/paperclip/SKILL.md:521 |
| skill:skills/paperclip/SKILL.md:routines:532 | optional_agent_tool | skills/paperclip/SKILL.md:532 |
| skill:skills/paperclip/SKILL.md:issue-workspace-runtime-controls:543 | optional_agent_tool | skills/paperclip/SKILL.md:543 |
| skill:skills/paperclip/SKILL.md:proposing-credentials-safely:550 | optional_agent_tool | skills/paperclip/SKILL.md:550 |
| skill:skills/paperclip/SKILL.md:reading-granted-secrets:557 | optional_agent_tool | skills/paperclip/SKILL.md:557 |
| skill:skills/paperclip/SKILL.md:critical-rules:583 | optional_agent_tool | skills/paperclip/SKILL.md:583 |
| skill:skills/paperclip/SKILL.md:comment-style-required:604 | always_agent_tool | skills/paperclip/SKILL.md:604 |
| skill:skills/paperclip/SKILL.md:update:636 | optional_agent_tool | skills/paperclip/SKILL.md:636 |
| skill:skills/paperclip/SKILL.md:planning-required-when-planning-requested:646 | optional_agent_tool | skills/paperclip/SKILL.md:646 |
| skill:skills/paperclip/SKILL.md:key-endpoints-hot-routes:679 | optional_agent_tool | skills/paperclip/SKILL.md:679 |
| skill:skills/paperclip/SKILL.md:searching-issues:708 | optional_agent_tool | skills/paperclip/SKILL.md:708 |
| skill:skills/paperclip/SKILL.md:full-reference:718 | optional_agent_tool | skills/paperclip/SKILL.md:718 |
| skill:skills/paperclip/SKILL.md:conversational-confirmation-answers:722 | always_agent_tool | skills/paperclip/SKILL.md:722 |
| skill:skills/paperclip/references/artifacts.md:generated-artifacts-and-work-products:1 | always_agent_tool | skills/paperclip/references/artifacts.md:1 |
| skill:skills/paperclip/references/artifacts.md:workspace-only-file-references:15 | optional_agent_tool | skills/paperclip/references/artifacts.md:15 |
| skill:skills/paperclip/references/cases.md:cases:1 | optional_agent_tool | skills/paperclip/references/cases.md:1 |
@@ -165,21 +166,22 @@ The skill/reference inventory and eval cases are the only normative behavior sou
| skill:skills/paperclip/references/api-reference.md:7-dashboard-for-health-check:626 | optional_agent_tool | skills/paperclip/references/api-reference.md:626 |
| skill:skills/paperclip/references/api-reference.md:comments-and-mentions:632 | always_agent_tool | skills/paperclip/references/api-reference.md:632 |
| skill:skills/paperclip/references/api-reference.md:update:639 | optional_agent_tool | skills/paperclip/references/api-reference.md:639 |
| skill:skills/paperclip/references/api-reference.md:cross-team-work-and-delegation:677 | optional_agent_tool | skills/paperclip/references/api-reference.md:677 |
| skill:skills/paperclip/references/api-reference.md:receiving-cross-team-work:681 | optional_agent_tool | skills/paperclip/references/api-reference.md:681 |
| skill:skills/paperclip/references/api-reference.md:questions-and-dependencies:691 | always_agent_tool | skills/paperclip/references/api-reference.md:691 |
| skill:skills/paperclip/references/api-reference.md:company-context:703 | optional_agent_tool | skills/paperclip/references/api-reference.md:703 |
| skill:skills/paperclip/references/api-reference.md:company-branding-ceo-board:715 | optional_agent_tool | skills/paperclip/references/api-reference.md:715 |
| skill:skills/paperclip/references/api-reference.md:openclaw-invite-prompt-ceo:735 | optional_agent_tool | skills/paperclip/references/api-reference.md:735 |
| skill:skills/paperclip/references/api-reference.md:setting-agent-instructions-path:754 | optional_agent_tool | skills/paperclip/references/api-reference.md:754 |
| skill:skills/paperclip/references/api-reference.md:project-setup-create-workspace:787 | optional_agent_tool | skills/paperclip/references/api-reference.md:787 |
| skill:skills/paperclip/references/api-reference.md:option-a-one-call-create-with-workspace:811 | optional_agent_tool | skills/paperclip/references/api-reference.md:811 |
| skill:skills/paperclip/references/api-reference.md:option-b-two-calls-project-first-then-workspace:830 | optional_agent_tool | skills/paperclip/references/api-reference.md:830 |
| skill:skills/paperclip/references/api-reference.md:governance-and-approvals:859 | optional_agent_tool | skills/paperclip/references/api-reference.md:859 |
| skill:skills/paperclip/references/api-reference.md:requesting-a-hire-management-only:863 | optional_agent_tool | skills/paperclip/references/api-reference.md:863 |
| skill:skills/paperclip/references/api-reference.md:ceo-strategy-approval:928 | optional_agent_tool | skills/paperclip/references/api-reference.md:928 |
| skill:skills/paperclip/references/api-reference.md:questions-and-waiting-for-human-input:937 | always_agent_tool | skills/paperclip/references/api-reference.md:937 |
| skill:skills/paperclip/references/api-reference.md:issue-thread-confirmations:1034 | always_agent_tool | skills/paperclip/references/api-reference.md:1034 |
| skill:skills/paperclip/references/api-reference.md:cross-team-work-and-delegation:661 | optional_agent_tool | skills/paperclip/references/api-reference.md:661 |
| skill:skills/paperclip/references/api-reference.md:receiving-cross-team-work:665 | optional_agent_tool | skills/paperclip/references/api-reference.md:665 |
| skill:skills/paperclip/references/api-reference.md:questions-and-dependencies:675 | always_agent_tool | skills/paperclip/references/api-reference.md:675 |
| skill:skills/paperclip/references/api-reference.md:company-context:687 | optional_agent_tool | skills/paperclip/references/api-reference.md:687 |
| skill:skills/paperclip/references/api-reference.md:company-branding-ceo-board:699 | optional_agent_tool | skills/paperclip/references/api-reference.md:699 |
| skill:skills/paperclip/references/api-reference.md:openclaw-invite-prompt-ceo:719 | optional_agent_tool | skills/paperclip/references/api-reference.md:719 |
| skill:skills/paperclip/references/api-reference.md:setting-agent-instructions-path:738 | optional_agent_tool | skills/paperclip/references/api-reference.md:738 |
| skill:skills/paperclip/references/api-reference.md:project-setup-create-workspace:771 | optional_agent_tool | skills/paperclip/references/api-reference.md:771 |
| skill:skills/paperclip/references/api-reference.md:option-a-one-call-create-with-workspace:795 | optional_agent_tool | skills/paperclip/references/api-reference.md:795 |
| skill:skills/paperclip/references/api-reference.md:option-b-two-calls-project-first-then-workspace:814 | optional_agent_tool | skills/paperclip/references/api-reference.md:814 |
| skill:skills/paperclip/references/api-reference.md:governance-and-approvals:843 | optional_agent_tool | skills/paperclip/references/api-reference.md:843 |
| skill:skills/paperclip/references/api-reference.md:requesting-a-hire-management-only:847 | optional_agent_tool | skills/paperclip/references/api-reference.md:847 |
| skill:skills/paperclip/references/api-reference.md:ceo-strategy-approval:912 | optional_agent_tool | skills/paperclip/references/api-reference.md:912 |
| skill:skills/paperclip/references/api-reference.md:questions-and-waiting-for-human-input:921 | always_agent_tool | skills/paperclip/references/api-reference.md:921 |
| skill:skills/paperclip/references/api-reference.md:issue-thread-confirmations:1028 | always_agent_tool | skills/paperclip/references/api-reference.md:1028 |
| skill:skills/paperclip/references/api-reference.md:conversational-confirmation-answers:1086 | always_agent_tool | skills/paperclip/references/api-reference.md:1086 |
| skill:skills/paperclip/references/api-reference.md:checkbox-confirmations:1092 | always_agent_tool | skills/paperclip/references/api-reference.md:1092 |
| skill:skills/paperclip/references/api-reference.md:item-verdict-requests:1207 | optional_agent_tool | skills/paperclip/references/api-reference.md:1207 |
| skill:skills/paperclip/references/api-reference.md:checking-approval-status:1317 | optional_agent_tool | skills/paperclip/references/api-reference.md:1317 |
@@ -189,13 +191,13 @@ The skill/reference inventory and eval cases are the only normative behavior sou
| skill:skills/paperclip/references/api-reference.md:full-api-reference:1385 | optional_agent_tool | skills/paperclip/references/api-reference.md:1385 |
| skill:skills/paperclip/references/api-reference.md:agents:1387 | optional_agent_tool | skills/paperclip/references/api-reference.md:1387 |
| skill:skills/paperclip/references/api-reference.md:issues-tasks:1408 | optional_agent_tool | skills/paperclip/references/api-reference.md:1408 |
| skill:skills/paperclip/references/api-reference.md:companies-projects-goals:1448 | optional_agent_tool | skills/paperclip/references/api-reference.md:1448 |
| skill:skills/paperclip/references/api-reference.md:routines:1472 | optional_agent_tool | skills/paperclip/references/api-reference.md:1472 |
| skill:skills/paperclip/references/api-reference.md:approvals-costs-activity-dashboard:1488 | optional_agent_tool | skills/paperclip/references/api-reference.md:1488 |
| skill:skills/paperclip/references/api-reference.md:secrets:1510 | optional_agent_tool | skills/paperclip/references/api-reference.md:1510 |
| skill:skills/paperclip/references/api-reference.md:agent-secret-proposals:1523 | optional_agent_tool | skills/paperclip/references/api-reference.md:1523 |
| skill:skills/paperclip/references/api-reference.md:agent-secret-access:1623 | optional_agent_tool | skills/paperclip/references/api-reference.md:1623 |
| skill:skills/paperclip/references/api-reference.md:common-mistakes:1663 | optional_agent_tool | skills/paperclip/references/api-reference.md:1663 |
| skill:skills/paperclip/references/api-reference.md:companies-projects-goals:1449 | optional_agent_tool | skills/paperclip/references/api-reference.md:1449 |
| skill:skills/paperclip/references/api-reference.md:routines:1473 | optional_agent_tool | skills/paperclip/references/api-reference.md:1473 |
| skill:skills/paperclip/references/api-reference.md:approvals-costs-activity-dashboard:1489 | optional_agent_tool | skills/paperclip/references/api-reference.md:1489 |
| skill:skills/paperclip/references/api-reference.md:secrets:1511 | optional_agent_tool | skills/paperclip/references/api-reference.md:1511 |
| skill:skills/paperclip/references/api-reference.md:agent-secret-proposals:1524 | optional_agent_tool | skills/paperclip/references/api-reference.md:1524 |
| skill:skills/paperclip/references/api-reference.md:agent-secret-access:1624 | optional_agent_tool | skills/paperclip/references/api-reference.md:1624 |
| skill:skills/paperclip/references/api-reference.md:common-mistakes:1664 | optional_agent_tool | skills/paperclip/references/api-reference.md:1664 |
## Legacy MCP Alias Index
@@ -185,6 +185,105 @@ The trace records `logicalEffectCount`. Every completed command has exactly one
logical effect. A policy rejection, such as a new turn during drain or storage
pressure, has zero effects.
## Semantic operations that outlive a turn
An execution session includes the provider process, the runner's durable call
receipts and event outbox, and the controller's durable command queue. Ending a
provider turn closes admission of new work. It does not establish whether a
semantic operation already dispatched to the server succeeded or failed.
The tool bridge retains pending call IDs, operation IDs, and exact arguments
across that boundary. It accepts the authority's late result without delivering
it to an obsolete provider turn. An identical result is an idempotent replay;
a different result for that call remains a conflict. Codex records harmless
result replay as `semantic_tool_result_duplicate` in a diagnostic event, with
no task failure. Conflict errors identify the call, operation, and both result
digests without including the result bodies.
The controller checkpoints a reusable session only after every admitted tool
has a matching completed result-delivery command and the provider has drained.
A callback rejected before business dispatch can report
`semantic_tool_not_dispatched`. A callback rejected after dispatch has an
unknown outcome; the controller must not invent a failure or execute it again.
After controller restart, an already committed input without a result remains
unsettled. It requires authoritative reconciliation, not an automatic retry.
A different case is a runner crash after the controller saved the result but
before its delivery command completed. Codex/OpenCode explicitly opt in to
reconciling that exact `semantic_tool.result` command through their durable tool
receipt. The full command fingerprint must match. This delivers the saved
answer without executing the business operation. Unsupported providers and all
other indeterminate commands remain non-reexecutable. This change does not
repair legacy journals already containing contradictory or failed receipts.
`update_agent_instructions` and `restore_agent_instructions` use a durable,
company/run-scoped mutation receipt keyed by call ID. Before attempting a file
write, the authority commits an `instructionToolAttempts` record and an
`agent.instruction_write_attempted` activity row with the call ID, operation ID,
and input digest. This evidence survives a later filesystem or database failure.
Completed calls and retries after authority restart return the exact original receipt.
Changing the arguments under that ID is an idempotency conflict. Receipt replay
still checks current instruction-write authorization. Concurrent callers in the
same process wait for the first handler before checking the receipt again.
The filesystem and PostgreSQL are not one atomic store. If a write is visible
but its receipt transaction rolls back, the attempt record remains durable and
the result is unknown. An identical call with an attempt but no committed result
raises `paperclip_runner_instruction_outcome_unknown`; it does not write again
or pretend to have succeeded. This also applies to another process reaching a
reserved call before its effect transaction starts. The controller retains an
unsettled operation for authoritative reconciliation. If only the commit
acknowledgement was lost, the committed receipt is replayed normally.
The native tool wrapper propagates `SemanticToolOutcomeUnknownError` instead
of turning it into a completed failed tool response. Ordinary validation,
authorization, and stale-base errors still return normal tool errors. Before
returning a definite pre-write failure, the authority saves its status, message,
and details under the attempt's `failure` receipt. Replays return that original
error without executing again, even if the old CAS base becomes current again.
Failure-receipt storage errors retain an unsettled operation instead of exposing
an unrecorded final answer.
### Lossless arguments and bounded transport
Execution arguments do not use the diagnostic preview formatter. Accepted
input remains exact, including Unicode, line endings, and long document tails.
The credential policy rejects input it would have to alter before dispatch.
Diagnostic copies remain redacted and bounded. The semantic input limit is
480 KiB of encoded JSON, leaving room for the encrypted frame's hex expansion
and envelope inside the 1 MiB wire limit. Oversized input fails before a write;
it is never truncated into an apparently successful write.
A result command retains a SHA-256 `inputDigest` instead of duplicating the
full input alongside the full result. The canonical input event retains the
arguments. Settlement validates the digest and all run, event, call, operation,
and correlation identities. Legacy result commands carrying the full input
remain supported. Large results still have to fit the command transport limit;
exceeding it leaves the outcome unsettled rather than retrying an effect.
### Deterministic fault coverage
These tests use explicit barriers, durable reloads, real runner processes with
scripted providers, and a real PostgreSQL database. They do not require a paid
model or rely on an LLM choosing the desired timing.
| Fault | Required assertion | Regression suite |
| --- | --- | --- |
| Stop before/after server result; restart before late delivery | No invented failure; pending identity survives; late result accepted | Rust `provider_bridge`, `codex_provider` |
| Provider terminal before tool completion | New turn blocked until actual result; safe replay and later reuse | Rust `acpx_provider_turns`, `acpx_provider_state` |
| Crash before/after applying a saved delivery receipt, before command completion | Exact result delivery recovers only for opted-in providers; changed commands and ordinary operations never replay | Rust durable runner tests |
| Lost result acknowledgement; conflicting redelivery | Identical result accepted; changed result rejected with IDs and digests | Rust `codex_provider`, `provider_bridge` |
| Concurrent writes held at filesystem commit; authority restart | One write, one audit record, same receipt; revoked authorization rejected | Server `agent-instruction-tools.integration.test.ts` |
| Failure after file rename or rollback while saving the receipt | Durable attempt and audit evidence survive; no success claim and no second write | Server `agent-instruction-tools.integration.test.ts` |
| Receipt commits but its database acknowledgement is lost | Exact original result replays; one file-update audit and one attempt record | Server `agent-instruction-tools.integration.test.ts` |
| Tool handler throws an unknown-outcome error versus a validation error | Unknown propagates without a completed tool event; validation returns an ordinary failed response | `codex-app-server-driver.test.ts` |
| Invalid arguments, oversized input, or stale base; restart and replay | Original failure replays with zero writes, even after the base becomes valid | Server `agent-instruction-tools.integration.test.ts` |
| Failure receipt cannot be stored | No unrecorded final failure; attempt remains unsettled and never reexecutes | Server `agent-instruction-tools.integration.test.ts` |
| Long Unicode input and result; corrupted input digest | Exact bytes across persistence/wire; no copied input in result; bad proof blocks settlement | Rust durable-state tests; `durable-prp-control-plane.test.ts` |
| Handler finishes during close, after close budget, or persistence fails | Checkpoint marked settled only with completed delivery and drained provider | `runnerd-codex-transport.test.ts` |
| Controller restarts with committed input but no result | No second dispatch and no fabricated outcome | `durable-prp-control-plane.test.ts` |
| Execution and cleanup both fail | Original execution error retained; cleanup error attached | `native-session-runtime.test.ts` |
| Retry in same process or after local log loss | Earlier attempts survive locally and in the durable mirror | Server `run-log-store.test.ts` |
| Two restores overlap and one appends before the other publishes | Complete prefix is published without replacement; both attempts survive | Server `run-log-store.test.ts` |
## Residual local trust and revocation window
The authenticated session removes trust in whichever process wins the configured
@@ -56,221 +56,230 @@
"expectedMockState": "runtime_decision_record"
},
{
"id": "skill:skills/paperclip/SKILL.md:159",
"id": "skill:skills/paperclip/SKILL.md:195",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/SKILL.md#L159:generated-artifacts-and-work-products",
"sourceAnchor": "skills/paperclip/SKILL.md#L195:generated-artifacts-and-work-products",
"heading": "Generated Artifacts and Work Products",
"primaryDisposition": "always_agent_tool",
"semanticOperation": "register_deliverable",
"expectedMockState": "operation_result"
},
{
"id": "skill:skills/paperclip/SKILL.md:207",
"id": "skill:skills/paperclip/SKILL.md:243",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/SKILL.md#L207:status-quick-guide",
"sourceAnchor": "skills/paperclip/SKILL.md#L243:status-quick-guide",
"heading": "Status Quick Guide",
"primaryDisposition": "control_plane_owned",
"semanticOperation": "runtime_reconciliation",
"expectedMockState": "runtime_decision_record"
},
{
"id": "skill:skills/paperclip/SKILL.md:217",
"id": "skill:skills/paperclip/SKILL.md:253",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/SKILL.md#L217:monitors-and-watchers-say-only-what-you-actually-scheduled",
"sourceAnchor": "skills/paperclip/SKILL.md#L253:monitors-and-watchers-say-only-what-you-actually-scheduled",
"heading": "Monitors and Watchers (say only what you actually scheduled)",
"primaryDisposition": "control_plane_owned",
"semanticOperation": "runtime_reconciliation",
"expectedMockState": "runtime_decision_record"
},
{
"id": "skill:skills/paperclip/SKILL.md:230",
"id": "skill:skills/paperclip/SKILL.md:266",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/SKILL.md#L230:delegating-review-tasks",
"sourceAnchor": "skills/paperclip/SKILL.md#L266:delegating-review-tasks",
"heading": "Delegating review tasks",
"primaryDisposition": "control_plane_owned",
"semanticOperation": "runtime_reconciliation",
"expectedMockState": "runtime_decision_record"
},
{
"id": "skill:skills/paperclip/SKILL.md:241",
"id": "skill:skills/paperclip/SKILL.md:277",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/SKILL.md#L241:managing-a-user-s-inbox",
"sourceAnchor": "skills/paperclip/SKILL.md#L277:managing-a-user-s-inbox",
"heading": "Managing A User's Inbox",
"primaryDisposition": "control_plane_owned",
"semanticOperation": "runtime_reconciliation",
"expectedMockState": "runtime_decision_record"
},
{
"id": "skill:skills/paperclip/SKILL.md:249",
"id": "skill:skills/paperclip/SKILL.md:285",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/SKILL.md#L249:issue-dependencies-blockers",
"sourceAnchor": "skills/paperclip/SKILL.md#L285:issue-dependencies-blockers",
"heading": "Issue Dependencies (Blockers)",
"primaryDisposition": "control_plane_owned",
"semanticOperation": "runtime_reconciliation",
"expectedMockState": "runtime_decision_record"
},
{
"id": "skill:skills/paperclip/SKILL.md:274",
"id": "skill:skills/paperclip/SKILL.md:310",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/SKILL.md#L274:requesting-board-approval",
"sourceAnchor": "skills/paperclip/SKILL.md#L310:requesting-board-approval",
"heading": "Requesting Board Approval",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "scoped_discovery",
"expectedMockState": "operation_result"
},
{
"id": "skill:skills/paperclip/SKILL.md:295",
"id": "skill:skills/paperclip/SKILL.md:331",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/SKILL.md#L295:issue-thread-interactions",
"sourceAnchor": "skills/paperclip/SKILL.md#L331:issue-thread-interactions",
"heading": "Issue-Thread Interactions",
"primaryDisposition": "always_agent_tool",
"semanticOperation": "request_human_input",
"expectedMockState": "operation_result"
},
{
"id": "skill:skills/paperclip/SKILL.md:324",
"id": "skill:skills/paperclip/SKILL.md:360",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/SKILL.md#L324:standalone-decisions",
"sourceAnchor": "skills/paperclip/SKILL.md#L360:standalone-decisions",
"heading": "Standalone Decisions",
"primaryDisposition": "control_plane_owned",
"semanticOperation": "runtime_reconciliation",
"expectedMockState": "runtime_decision_record"
},
{
"id": "skill:skills/paperclip/SKILL.md:428",
"id": "skill:skills/paperclip/SKILL.md:464",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/SKILL.md#L428:mcp-tool-approval-gates",
"sourceAnchor": "skills/paperclip/SKILL.md#L464:mcp-tool-approval-gates",
"heading": "MCP Tool Approval Gates",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "scoped_discovery",
"expectedMockState": "operation_result"
},
{
"id": "skill:skills/paperclip/SKILL.md:470",
"id": "skill:skills/paperclip/SKILL.md:506",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/SKILL.md#L470:niche-workflow-pointers",
"sourceAnchor": "skills/paperclip/SKILL.md#L506:niche-workflow-pointers",
"heading": "Niche Workflow Pointers",
"primaryDisposition": "control_plane_owned",
"semanticOperation": "runtime_reconciliation",
"expectedMockState": "runtime_decision_record"
},
{
"id": "skill:skills/paperclip/SKILL.md:480",
"id": "skill:skills/paperclip/SKILL.md:516",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/SKILL.md#L480:cases",
"sourceAnchor": "skills/paperclip/SKILL.md#L516:cases",
"heading": "Cases",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "scoped_discovery",
"expectedMockState": "operation_result"
},
{
"id": "skill:skills/paperclip/SKILL.md:485",
"id": "skill:skills/paperclip/SKILL.md:521",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/SKILL.md#L485:company-skills-workflow",
"sourceAnchor": "skills/paperclip/SKILL.md#L521:company-skills-workflow",
"heading": "Company Skills Workflow",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "scoped_discovery",
"expectedMockState": "operation_result"
},
{
"id": "skill:skills/paperclip/SKILL.md:496",
"id": "skill:skills/paperclip/SKILL.md:532",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/SKILL.md#L496:routines",
"sourceAnchor": "skills/paperclip/SKILL.md#L532:routines",
"heading": "Routines",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "scoped_discovery",
"expectedMockState": "operation_result"
},
{
"id": "skill:skills/paperclip/SKILL.md:507",
"id": "skill:skills/paperclip/SKILL.md:543",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/SKILL.md#L507:issue-workspace-runtime-controls",
"sourceAnchor": "skills/paperclip/SKILL.md#L543:issue-workspace-runtime-controls",
"heading": "Issue Workspace Runtime Controls",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "scoped_discovery",
"expectedMockState": "operation_result"
},
{
"id": "skill:skills/paperclip/SKILL.md:514",
"id": "skill:skills/paperclip/SKILL.md:550",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/SKILL.md#L514:proposing-credentials-safely",
"sourceAnchor": "skills/paperclip/SKILL.md#L550:proposing-credentials-safely",
"heading": "Proposing Credentials Safely",
"primaryDisposition": "control_plane_owned",
"semanticOperation": "runtime_reconciliation",
"expectedMockState": "runtime_decision_record"
},
{
"id": "skill:skills/paperclip/SKILL.md:521",
"id": "skill:skills/paperclip/SKILL.md:557",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/SKILL.md#L521:reading-granted-secrets",
"sourceAnchor": "skills/paperclip/SKILL.md#L557:reading-granted-secrets",
"heading": "Reading Granted Secrets",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "scoped_discovery",
"expectedMockState": "operation_result"
},
{
"id": "skill:skills/paperclip/SKILL.md:547",
"id": "skill:skills/paperclip/SKILL.md:583",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/SKILL.md#L547:critical-rules",
"sourceAnchor": "skills/paperclip/SKILL.md#L583:critical-rules",
"heading": "Critical Rules",
"primaryDisposition": "control_plane_owned",
"semanticOperation": "runtime_reconciliation",
"expectedMockState": "runtime_decision_record"
},
{
"id": "skill:skills/paperclip/SKILL.md:567",
"id": "skill:skills/paperclip/SKILL.md:604",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/SKILL.md#L567:comment-style-required",
"sourceAnchor": "skills/paperclip/SKILL.md#L604:comment-style-required",
"heading": "Comment Style (Required)",
"primaryDisposition": "always_agent_tool",
"semanticOperation": "report_progress",
"expectedMockState": "operation_result"
},
{
"id": "skill:skills/paperclip/SKILL.md:599",
"id": "skill:skills/paperclip/SKILL.md:636",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/SKILL.md#L599:update",
"sourceAnchor": "skills/paperclip/SKILL.md#L636:update",
"heading": "Update",
"primaryDisposition": "control_plane_owned",
"semanticOperation": "runtime_reconciliation",
"expectedMockState": "runtime_decision_record"
},
{
"id": "skill:skills/paperclip/SKILL.md:609",
"id": "skill:skills/paperclip/SKILL.md:646",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/SKILL.md#L609:planning-required-when-planning-requested",
"sourceAnchor": "skills/paperclip/SKILL.md#L646:planning-required-when-planning-requested",
"heading": "Planning (Required when planning requested)",
"primaryDisposition": "always_agent_tool",
"semanticOperation": "write_document",
"expectedMockState": "operation_result"
},
{
"id": "skill:skills/paperclip/SKILL.md:642",
"id": "skill:skills/paperclip/SKILL.md:679",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/SKILL.md#L642:key-endpoints-hot-routes",
"sourceAnchor": "skills/paperclip/SKILL.md#L679:key-endpoints-hot-routes",
"heading": "Key Endpoints (Hot Routes)",
"primaryDisposition": "control_plane_owned",
"semanticOperation": "runtime_reconciliation",
"expectedMockState": "runtime_decision_record"
},
{
"id": "skill:skills/paperclip/SKILL.md:671",
"id": "skill:skills/paperclip/SKILL.md:708",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/SKILL.md#L671:searching-issues",
"sourceAnchor": "skills/paperclip/SKILL.md#L708:searching-issues",
"heading": "Searching Issues",
"primaryDisposition": "control_plane_owned",
"semanticOperation": "runtime_reconciliation",
"expectedMockState": "runtime_decision_record"
},
{
"id": "skill:skills/paperclip/SKILL.md:681",
"id": "skill:skills/paperclip/SKILL.md:718",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/SKILL.md#L681:full-reference",
"sourceAnchor": "skills/paperclip/SKILL.md#L718:full-reference",
"heading": "Full Reference",
"primaryDisposition": "control_plane_owned",
"semanticOperation": "runtime_reconciliation",
"expectedMockState": "runtime_decision_record"
},
{
"id": "skill:skills/paperclip/SKILL.md:722",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/SKILL.md#L722:conversational-confirmation-answers",
"heading": "Conversational confirmation answers",
"primaryDisposition": "always_agent_tool",
"semanticOperation": "call_api",
"expectedMockState": "operation_result"
},
{
"id": "skill:skills/paperclip/references/api-reference.md:1",
"kind": "skill_heading",
@@ -614,140 +623,149 @@
"expectedMockState": "operation_result"
},
{
"id": "skill:skills/paperclip/references/api-reference.md:677",
"id": "skill:skills/paperclip/references/api-reference.md:661",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L677:cross-team-work-and-delegation",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L661:cross-team-work-and-delegation",
"heading": "Cross-Team Work and Delegation",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "scoped_discovery",
"expectedMockState": "operation_result"
},
{
"id": "skill:skills/paperclip/references/api-reference.md:681",
"id": "skill:skills/paperclip/references/api-reference.md:665",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L681:receiving-cross-team-work",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L665:receiving-cross-team-work",
"heading": "Receiving cross-team work",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "scoped_discovery",
"expectedMockState": "operation_result"
},
{
"id": "skill:skills/paperclip/references/api-reference.md:691",
"id": "skill:skills/paperclip/references/api-reference.md:675",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L691:questions-and-dependencies",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L675:questions-and-dependencies",
"heading": "Questions and dependencies",
"primaryDisposition": "always_agent_tool",
"semanticOperation": "request_human_input",
"expectedMockState": "operation_result"
},
{
"id": "skill:skills/paperclip/references/api-reference.md:703",
"id": "skill:skills/paperclip/references/api-reference.md:687",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L703:company-context",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L687:company-context",
"heading": "Company Context",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "scoped_discovery",
"expectedMockState": "operation_result"
},
{
"id": "skill:skills/paperclip/references/api-reference.md:715",
"id": "skill:skills/paperclip/references/api-reference.md:699",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L715:company-branding-ceo-board",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L699:company-branding-ceo-board",
"heading": "Company Branding (CEO / Board)",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "scoped_discovery",
"expectedMockState": "operation_result"
},
{
"id": "skill:skills/paperclip/references/api-reference.md:735",
"id": "skill:skills/paperclip/references/api-reference.md:719",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L735:openclaw-invite-prompt-ceo",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L719:openclaw-invite-prompt-ceo",
"heading": "OpenClaw Invite Prompt (CEO)",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "scoped_discovery",
"expectedMockState": "operation_result"
},
{
"id": "skill:skills/paperclip/references/api-reference.md:754",
"id": "skill:skills/paperclip/references/api-reference.md:738",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L754:setting-agent-instructions-path",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L738:setting-agent-instructions-path",
"heading": "Setting Agent Instructions Path",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "scoped_discovery",
"expectedMockState": "operation_result"
},
{
"id": "skill:skills/paperclip/references/api-reference.md:787",
"id": "skill:skills/paperclip/references/api-reference.md:771",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L787:project-setup-create-workspace",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L771:project-setup-create-workspace",
"heading": "Project Setup (Create + Workspace)",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "scoped_discovery",
"expectedMockState": "operation_result"
},
{
"id": "skill:skills/paperclip/references/api-reference.md:811",
"id": "skill:skills/paperclip/references/api-reference.md:795",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L811:option-a-one-call-create-with-workspace",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L795:option-a-one-call-create-with-workspace",
"heading": "Option A: One-call create with workspace",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "scoped_discovery",
"expectedMockState": "operation_result"
},
{
"id": "skill:skills/paperclip/references/api-reference.md:830",
"id": "skill:skills/paperclip/references/api-reference.md:814",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L830:option-b-two-calls-project-first-then-workspace",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L814:option-b-two-calls-project-first-then-workspace",
"heading": "Option B: Two calls (project first, then workspace)",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "scoped_discovery",
"expectedMockState": "operation_result"
},
{
"id": "skill:skills/paperclip/references/api-reference.md:859",
"id": "skill:skills/paperclip/references/api-reference.md:843",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L859:governance-and-approvals",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L843:governance-and-approvals",
"heading": "Governance and Approvals",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "scoped_discovery",
"expectedMockState": "operation_result"
},
{
"id": "skill:skills/paperclip/references/api-reference.md:863",
"id": "skill:skills/paperclip/references/api-reference.md:847",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L863:requesting-a-hire-management-only",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L847:requesting-a-hire-management-only",
"heading": "Requesting a hire (management only)",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "scoped_discovery",
"expectedMockState": "operation_result"
},
{
"id": "skill:skills/paperclip/references/api-reference.md:928",
"id": "skill:skills/paperclip/references/api-reference.md:912",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L928:ceo-strategy-approval",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L912:ceo-strategy-approval",
"heading": "CEO strategy approval",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "scoped_discovery",
"expectedMockState": "operation_result"
},
{
"id": "skill:skills/paperclip/references/api-reference.md:937",
"id": "skill:skills/paperclip/references/api-reference.md:921",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L937:questions-and-waiting-for-human-input",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L921:questions-and-waiting-for-human-input",
"heading": "Questions and waiting for human input",
"primaryDisposition": "always_agent_tool",
"semanticOperation": "request_human_input",
"expectedMockState": "operation_result"
},
{
"id": "skill:skills/paperclip/references/api-reference.md:1034",
"id": "skill:skills/paperclip/references/api-reference.md:1028",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1034:issue-thread-confirmations",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1028:issue-thread-confirmations",
"heading": "Issue-thread confirmations",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "scoped_discovery",
"expectedMockState": "operation_result"
},
{
"id": "skill:skills/paperclip/references/api-reference.md:1086",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1086:conversational-confirmation-answers",
"heading": "Conversational confirmation answers",
"primaryDisposition": "always_agent_tool",
"semanticOperation": "call_api",
"expectedMockState": "operation_result"
},
{
"id": "skill:skills/paperclip/references/api-reference.md:1092",
"kind": "skill_heading",
@@ -830,72 +848,72 @@
"expectedMockState": "operation_result"
},
{
"id": "skill:skills/paperclip/references/api-reference.md:1448",
"id": "skill:skills/paperclip/references/api-reference.md:1449",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1448:companies-projects-goals",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1449:companies-projects-goals",
"heading": "Companies, Projects, Goals",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "scoped_discovery",
"expectedMockState": "operation_result"
},
{
"id": "skill:skills/paperclip/references/api-reference.md:1472",
"id": "skill:skills/paperclip/references/api-reference.md:1473",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1472:routines",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1473:routines",
"heading": "Routines",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "scoped_discovery",
"expectedMockState": "operation_result"
},
{
"id": "skill:skills/paperclip/references/api-reference.md:1488",
"id": "skill:skills/paperclip/references/api-reference.md:1489",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1488:approvals-costs-activity-dashboard",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1489:approvals-costs-activity-dashboard",
"heading": "Approvals, Costs, Activity, Dashboard",
"primaryDisposition": "control_plane_owned",
"semanticOperation": "runtime_reconciliation",
"expectedMockState": "runtime_decision_record"
},
{
"id": "skill:skills/paperclip/references/api-reference.md:1510",
"id": "skill:skills/paperclip/references/api-reference.md:1511",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1510:secrets",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1511:secrets",
"heading": "Secrets",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "scoped_discovery",
"expectedMockState": "operation_result"
},
{
"id": "skill:skills/paperclip/references/api-reference.md:1523",
"id": "skill:skills/paperclip/references/api-reference.md:1524",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1523:agent-secret-proposals",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1524:agent-secret-proposals",
"heading": "Agent secret proposals",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "scoped_discovery",
"expectedMockState": "operation_result"
},
{
"id": "skill:skills/paperclip/references/api-reference.md:1576",
"id": "skill:skills/paperclip/references/api-reference.md:1577",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1576:re-bind-an-existing-secret-under-a-new-path-no-secret-id",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1577:re-bind-an-existing-secret-under-a-new-path-no-secret-id",
"heading": "Re-bind an existing secret under a new path (no secret ID)",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "scoped_discovery",
"expectedMockState": "operation_result"
},
{
"id": "skill:skills/paperclip/references/api-reference.md:1623",
"id": "skill:skills/paperclip/references/api-reference.md:1624",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1623:agent-secret-access",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1624:agent-secret-access",
"heading": "Agent secret access",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "scoped_discovery",
"expectedMockState": "operation_result"
},
{
"id": "skill:skills/paperclip/references/api-reference.md:1663",
"id": "skill:skills/paperclip/references/api-reference.md:1664",
"kind": "skill_heading",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1663:common-mistakes",
"sourceAnchor": "skills/paperclip/references/api-reference.md#L1664:common-mistakes",
"heading": "Common Mistakes",
"primaryDisposition": "optional_agent_tool",
"semanticOperation": "scoped_discovery",
@@ -2,9 +2,9 @@
Generated by `scripts/generate-capability-contract.mjs`; do not edit generated files.
- Skill/reference headings: 156
- Skill/reference headings: 158
- Legacy MCP tools: 42
- Eval cases: 106 across 16 groups
- Deterministic content SHA-256: `3a651e198c543b747fb4abb78e6a2b4ece3082bfc9941de5095cccd707d04b13`
- Deterministic content SHA-256: `60255e688c2b172a02979b4bd305ec089c9ad93b6d4eb4ffae59051a60170d7e`
Every row has exactly one primary disposition, a source anchor, a semantic operation, and a mock-state expectation.
@@ -1 +1 @@
[{"annotations":{"exposure":"always","operationId":"get_task_context","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read the active task and actor, including the exact approved Markdown revision when this issue has an accepted plan.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"get_task_context","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"get_task_history","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read bounded comments on the active task.","inputSchema":{"additionalProperties":false,"properties":{"limit":{"default":50,"maximum":200,"minimum":1,"type":"integer"}},"required":[],"type":"object"},"name":"get_task_history","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"list_documents","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"List revisioned documents on the active task.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"list_documents","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"read_document","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read the current revision of one active-task document.","inputSchema":{"additionalProperties":false,"properties":{"key":{"description":"Stable issue-document key.","maxLength":120,"minLength":1,"type":"string"}},"required":["key"],"type":"object"},"name":"read_document","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"list_document_revisions","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read bounded revision history for one active-task document.","inputSchema":{"additionalProperties":false,"properties":{"key":{"description":"Stable issue-document key.","maxLength":120,"minLength":1,"type":"string"},"limit":{"default":50,"maximum":200,"minimum":1,"type":"integer"}},"required":["key"],"type":"object"},"name":"list_document_revisions","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"report_progress","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Append a durable progress comment to the active task.","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Multiline progress update.","maxLength":20000,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","body"],"type":"object"},"name":"report_progress","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"answer_status_question","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Append the answer to a status-only wake without changing task disposition.","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Concise status answer.","maxLength":20000,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","body"],"type":"object"},"name":"answer_status_question","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"write_document","requiredClaimLine truncated
[{"annotations":{"exposure":"always","operationId":"get_task_context","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read the active task and actor, including the exact approved Markdown revision when this issue has an accepted plan.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"get_task_context","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"get_task_history","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read bounded comments on the active task.","inputSchema":{"additionalProperties":false,"properties":{"limit":{"default":50,"maximum":200,"minimum":1,"type":"integer"}},"required":[],"type":"object"},"name":"get_task_history","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"list_documents","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"List revisioned documents on the active task.","inputSchema":{"additionalProperties":false,"properties":{},"required":[],"type":"object"},"name":"list_documents","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"read_document","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read the current revision of one active-task document.","inputSchema":{"additionalProperties":false,"properties":{"key":{"description":"Stable issue-document key.","maxLength":120,"minLength":1,"type":"string"}},"required":["key"],"type":"object"},"name":"read_document","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"list_document_revisions","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Read bounded revision history for one active-task document.","inputSchema":{"additionalProperties":false,"properties":{"key":{"description":"Stable issue-document key.","maxLength":120,"minLength":1,"type":"string"},"limit":{"default":50,"maximum":200,"minimum":1,"type":"integer"}},"required":["key"],"type":"object"},"name":"list_document_revisions","outputSchema":{"additionalProperties":true,"type":"object"}},{"annotations":{"exposure":"always","operationId":"report_progress","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Append a durable progress comment to the active task.","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Multiline progress update.","maxLength":20000,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","body"],"type":"object"},"name":"report_progress","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"answer_status_question","requiredClaims":[],"semanticContract":"paperclip.semantic-tool.v1","version":1},"description":"Append the answer to a status-only wake without changing task disposition.","inputSchema":{"additionalProperties":false,"properties":{"body":{"description":"Concise status answer.","maxLength":20000,"minLength":1,"type":"string"},"idempotencyKey":{"description":"Caller-stable retry key.","maxLength":240,"minLength":1,"type":"string"}},"required":["idempotencyKey","body"],"type":"object"},"name":"answer_status_question","outputSchema":{"additionalProperties":false,"properties":{"commandId":{"description":"Stable command identifier.","maxLength":200,"minLength":1,"type":"string"},"disposition":{"enum":["applied","duplicate"]},"entityRefs":{"description":"Entities affected by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"scheduledWakeIds":{"description":"Wake identifiers scheduled by the operation.","items":{"minLength":1,"type":"string"},"maxItems":200,"type":"array","uniqueItems":true},"stateRevision":{"minimum":0,"type":"integer"}},"required":["commandId","disposition","stateRevision","entityRefs","scheduledWakeIds"],"type":"object"}},{"annotations":{"exposure":"always","operationId":"write_document","requiredClaimLine truncated
+1 -1
View File
@@ -74,7 +74,7 @@
"typecheck:browser": "tsc -p tsconfig.browser.json --noEmit",
"test": "pnpm run test:typescript && pnpm run test:rust",
"test:typescript": "pnpm run test:typescript:prep && vitest run",
"test:typescript:prep": "pnpm run ensure:eval-build-deps && pnpm run build:rust && node --test test/protocol-contract.test.mjs test/acpx-sidecar-contract.test.mjs test/acpx-codex-package-contract.test.mjs scripts/aws-agentcore-provisioning.test.mjs scripts/build-verified-provider-entrypoints.test.mjs scripts/local-provider-smoke-environment.test.mjs scripts/materialize-opencode-binary.test.mjs",
"test:typescript:prep": "pnpm run ensure:eval-build-deps && pnpm run build:rust && node --test test/protocol-contract.test.mjs test/acpx-sidecar-contract.test.mjs test/acpx-codex-package-contract.test.mjs test/acpx-rich-extensions-package-contract.test.mjs test/acpx-pi-receipt-package-contract.test.mjs test/acpx-response-delivery-package-contract.test.mjs scripts/candidate-provider-pack.test.mjs scripts/aws-agentcore-provisioning.test.mjs scripts/build-verified-provider-entrypoints.test.mjs scripts/local-provider-smoke-environment.test.mjs scripts/materialize-opencode-binary.test.mjs",
"test:typescript:vitest": "node ./scripts/run-pr-vitest-lane.mjs",
"test:rust": "cargo test --release --manifest-path runner/Cargo.toml --locked --workspace",
"test:codex": "cargo test --manifest-path runner/Cargo.toml --locked -p paperclip-runner-core --test codex_provider",
@@ -24,7 +24,7 @@
"prpVersion": 1,
"nativeExecutionVersion": 1,
"catalogVersion": 1,
"catalogSha256": "sha256:a19534304480dfd3d357cc902e785af05158ce1496f4d324184112732ff94c9c",
"catalogSha256": "sha256:994cf391ef851c37a4815db38368aa97bd3830def50218b2ffe9428966fc994f",
"driverContractVersion": 1,
"driverKind": "paperclip-deterministic",
"driverVersion": "1.0.0"
@@ -40,7 +40,7 @@
{
"path": "schemas/event-v2.schema.json",
"id": "https://paperclip.dev/schemas/prp/v2/event.schema.json",
"sha256": "d7528ee2a46ad20de831fd4ee4a0a51e8325a9855fed5a9b8c428dcebc39cc11"
"sha256": "9dcf149bdb8e10041ca2212037231e7ceff11be08a0b8ee5dbd0f21974daa294"
},
{
"path": "schemas/event.schema.json",
@@ -65,12 +65,12 @@
{
"path": "schemas/provider-descriptor.schema.json",
"id": "https://paperclip.dev/schemas/prp/v1/provider-descriptor.schema.json",
"sha256": "a56c0c8501171c34bb72f6fba08d3872839a8b45bbc10daa93620afc9dc2b669"
"sha256": "f5badf70748d6a761a3487f001ce8282582b161cb5b80fabb6bb5f683d28c512"
},
{
"path": "schemas/provider-event.schema.json",
"id": "https://paperclip.dev/schemas/prp/v1/provider-event.schema.json",
"sha256": "0b138a41bd3bd0b1a115f5536a2d7349537e79e36ede3b51d003f9f21ce73b50"
"sha256": "14143d53f28969fa3f940def7331a45ee0eed560dd343506ac41b6a3dfe59985"
},
{
"path": "schemas/question-adapter-fixture.schema.json",
@@ -85,12 +85,12 @@
{
"path": "schemas/question-set.schema.json",
"id": "https://paperclip.dev/schemas/prp/v1/question-set.schema.json",
"sha256": "42b5441a3d388851dacb6e4500dfd4a17d878eded2e724228078b647e7440d3f"
"sha256": "6aeb2b880a60356cb48acfe26605acd9412a1df65f5308397d3ec521db5ea70f"
},
{
"path": "schemas/request.schema.json",
"id": "https://paperclip.dev/schemas/prp/v1/request.schema.json",
"sha256": "c8d00e10150c90963f19641c075f50da7f67617dd817f41b356287ad85c53f2b"
"sha256": "985a3b0a6ae36ae5ad4cf7d708dc401db7c108f683a81cd44a03093fc4ca7862"
},
{
"path": "schemas/result.schema.json",
@@ -160,7 +160,7 @@
},
{
"path": "fixtures/evals/native-execution-seeded.json",
"sha256": "c6367c7c7e3dd01fed341b7a83b0008c9d5d1efdfa85bc70e1013bc304a008c7",
"sha256": "076427778f1ecced927c0680ab3d59e52e06d3e680a610bee3af6fd0795b6a8c",
"expectation": "accept",
"compatibilityCase": "canonical"
},
Loaded 100 of 786 files, more files were not shown because too many files have changed in this diff. Show more