mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-23 17:33:51 +02:00
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:
commit
673b03496b
786 files changed
+107799
-53083
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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
@@ -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.
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
-47935
File diff suppressed because it is too large.
Load diff
+910
-3
@@ -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": "",
|
||||
+802
-9
@@ -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": "",
|
||||
+1502
-9
File diff suppressed because it is too large.
Load diff
+1864
-3
File diff suppressed because it is too large.
Load diff
+50315
File diff suppressed because it is too large.
Load diff
+42
@@ -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')`,
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
@@ -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),
|
||||
}));
|
||||
@@ -64,6 +64,8 @@ export const companySkills = pgTable(
|
||||
|
||||
export type CompanySkillVersionFileInventoryEntry = CompanySkillFileInventoryEntry & {
|
||||
content: string;
|
||||
encoding?: "utf8" | "base64";
|
||||
executable?: boolean;
|
||||
};
|
||||
|
||||
export const companySkillVersions = pgTable(
|
||||
|
||||
@@ -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";
|
||||
@@ -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
|
||||
@@ -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
Reference in new issue
Block a user