diff --git a/.agents/skills/create-external-bot-integration/SKILL.md b/.agents/skills/create-external-bot-integration/SKILL.md new file mode 100644 index 00000000000..6694d9643cb --- /dev/null +++ b/.agents/skills/create-external-bot-integration/SKILL.md @@ -0,0 +1,152 @@ +--- +name: create-external-bot-integration +description: >- + Build and qualify external bots such as Dot in Paperclip. Use for two-way work, + invitation, identity, wakeup, Runner lifecycle, native tools, recovery, and + Cloud delivery. CLI adapters and app-only connectors use their existing skills. +--- + +# Create an External Bot Integration + +Make an existing external agent a useful Paperclip participant: Paperclip can +assign work to it, and it can initiate governed work from its own conversation. +It uses its relevant native capabilities, returns inspectable results to +Paperclip, and participates in normal admission, authority, and finalization. + +This workflow incorporates the September 30–October 9, 2026 Dot integration. +Provider facts and code paths in the references are dated evidence, not a claim +that every future bot supports the same APIs. Verify the current implementation +and official provider documentation before choosing a transport or changing code. + +## 1. Establish the release contract before narrowing the implementation + +Read [release-contract.md](references/release-contract.md). Infer already-stated +requirements and authorization from the conversation. Ask only about consequential +unknowns; do not ask the user to repeat “two way,” Cloud, or use of Runner. + +Record the target environment, acting identity, capabilities, allowed human setup +steps, delivery/stop expectations, and checkable finish line. Keep one compact +matrix in the existing plan or task. Follow repository conventions if creating a +plan file. “Works” must name its level: prototype, real provider, real Runner, +managed sandbox, deployed tenant, or unattended first-time onboarding. + +Reuse the current implementation and qualification evidence. Inspect the relevant +checkout, instructions, app/Cloud PRs, and Runner artifact. Do not recreate work +already merged or apply an old plan to a different deployed revision. + +## 2. Prove the uncertain provider boundary first + +Identify supported ways to authenticate, wake the agent, load tools, correlate +work, return results, and stop or revoke authority. Check events/subscriptions as +well as direct APIs and chat channels. Do not assume that a bot mention wakes +another bot, that MCP notifications wake an offline agent, or that a plugin listed +in settings is callable in the agent's current conversation. + +Use a synthetic company and stable public HTTPS endpoint. Start with a scripted +client for deterministic protocol tests, then the actual provider account: + +1. Fresh connection → callable read tool → verified event round trip. +2. Paperclip assignment → autonomous acceptance → persisted result → finalization. +3. Idle bot request → one governed intake → useful action → finalization. + +If idle autonomy is in scope, the third journey is a first milestone, not an +optional expansion. Do not make the user create a placeholder task for the bot. +Label every nudge, operator repair, simulated dependency, and manual tool call. +A successful assisted run does not prove prompt-only first-time setup. + +## 3. Reuse Runner while keeping placement and capability separate + +Read [runner-architecture.md](references/runner-architecture.md) before changing +execution or authority. For Paperclip, prefer the existing Runner infrastructure: +shared lifecycle, receipts, recovery, and a future sandbox boundary for workspace +tools. The bot's model and native tools can remain entirely at its provider. + +Use the ordinary configured managed execution environment on Cloud. One admitted +assignment reuses its Runner across messages and tool calls; idle connection +traffic does not allocate compute. A sandbox hosting Runner does not grant the +bot filesystem or shell access. Never add a fallback to the control-plane host. + +Implement dedicated agent identity, durable assignment/operation state, current +authority checks, and normal finalization. Treat a proposed alternative backend +as an explicit architecture decision with preserved invariants and evidence of +benefit, not as a reaction to the latest question about overhead. + +## 4. Build onboarding as a watched, resumable workflow + +Read [onboarding-and-prompts.md](references/onboarding-and-prompts.md). Start at +the natural UI entry: invite an external agent, select the provider, copy a setup +prompt, give it to the bot, and watch real connection checks. Reuse the product's +copy-prompt component and provider-independent invitation flow. + +Distinguish OAuth grant, runtime tool availability, subscription verification, +event confirmation, and agent readiness. Generate or renew pending prompts +automatically where safe; preserve connected agents. Finish with an explicit Done +action. Use stories for waiting, expiry, errors, pause, readiness, and recovery, +then verify the real rendered transitions and persistence. + +The prompt must tell the bot to use its own relevant tools/apps/skills and return +results. The Paperclip catalog governs Paperclip calls, not the bot's entire +runtime. Limit qualification restrictions to the specific test and Paperclip +destination. Never turn “use this test connection” into “use no other tools.” + +## 5. Qualify both correctness and the ordinary user experience + +Read [conformance-tests.md](references/conformance-tests.md) and select the rows +required by the release contract. Reuse existing Runner and Product E2E fixtures; +add regression cases for uncovered contracts instead of building a parallel test +system. Keep simulated protocol, actual provider, and actual deployment evidence +separate. A provider saying “done” is not proof of task disposition or cleanup. + +Test a lost response, duplicate wake, follow-up at completion, real controller +restart, authority loss, and cleanup. For recovery, compare the original run, +assignment, session, process identity, and lease. A restart test in which the +controller never restarted is invalid. Unknown mutation outcomes require +reconciliation, not a new request ID and a second execution. + +If setup needs a reboot, app reattachment, or manual inbox nudge, record the +functional success and the remaining onboarding defect separately. Do not quietly +teach the tester to navigate around a product defect and then call it self-serve. + +## 6. Deliver one identifiable candidate and verify the deployed path + +Read [troubleshooting-and-deployment.md](references/troubleshooting-and-deployment.md) +when setting up a test drive, diagnosing a failure, or deploying. Preserve one +environment manifest: checkout/revision, origin, local port, data directory, +tenant, image/Runner artifact, active task, and owned resources. Keep secrets out. +Retain the environment while the user/provider is testing; do not expire it +silently or reset the OAuth database behind a cached client registration. + +Use the repository's adapter, eval, PR, and release workflows when available. +The Paperclip `create-agent-adapter` skill covers shared configuration surfaces; +`paperclip-runner-release` coordinates a release; `test-it-for-real` validates the +actual experience. Discover their installed locations rather than assuming they +are siblings of this skill. This skill adds external-bot-specific contracts. + +Carry authorized merge and rollout through the app, ingress, and Runner artifact +dependencies. Verify the serving revision and repeat the main journeys on the +real tenant with ordinary defaults. Distinguish implementation defects, provider +behavior, infrastructure failures, and test/evidence defects. Bound retries by +the operation and observed cause; preserve the same resumable deployment request. + +Report supported capabilities, exact live evidence, interventions, unsupported +provider controls, remaining gates, and resource disposition. Never equate unknown +cost with zero cost, or revoked Paperclip access with confirmed global bot stop. +This skill supplies procedure, not new permission to message, grant access, spend, +publish, or deploy. Apply the current user's authorization and host policies. + +## References + +- [Release contract](references/release-contract.md): scope, capability matrix, + provider research, architecture decision, and finish-line template. +- [Runner architecture](references/runner-architecture.md): identity, state, + admission, receipts, follow-ups, recovery, files, and concrete Paperclip anchors. +- [Onboarding and prompts](references/onboarding-and-prompts.md): UI states, + consent, lifetime, tool loading, native capabilities, and reusable prompt text. +- [Conformance tests](references/conformance-tests.md): runnable journey recipes, + failure matrix, evidence schema, and release gates. +- [Troubleshooting and deployment](references/troubleshooting-and-deployment.md): + network planes, specific symptom-to-action diagnostics, stable environments, + managed execution, and release provenance. +- [Dot case study](references/dot-case-study.md): dated incidents, exact historical + protocol/tool names, observed limitations, and source trail. Read when comparing + a new provider to Dot or checking why a rule exists. diff --git a/.agents/skills/create-external-bot-integration/agents/openai.yaml b/.agents/skills/create-external-bot-integration/agents/openai.yaml new file mode 100644 index 00000000000..8614fc0219e --- /dev/null +++ b/.agents/skills/create-external-bot-integration/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Create External Bot Integration" + short_description: "Build and qualify external agents in Paperclip" + default_prompt: "Use $create-external-bot-integration to build and qualify this external agent through Paperclip Runner, including onboarding and both directions of work." diff --git a/.agents/skills/create-external-bot-integration/references/conformance-tests.md b/.agents/skills/create-external-bot-integration/references/conformance-tests.md new file mode 100644 index 00000000000..c079fc4c457 --- /dev/null +++ b/.agents/skills/create-external-bot-integration/references/conformance-tests.md @@ -0,0 +1,211 @@ +# External-bot conformance and qualification + +## Contents + +- [Keep the evidence levels explicit](#keep-the-evidence-levels-explicit) +- [Minimum real-provider journeys](#minimum-real-provider-journeys) +- [Select additional rows from the capability contract](#select-additional-rows-from-the-capability-contract) +- [Record evidence that survives a handoff](#record-evidence-that-survives-a-handoff) +- [Historical Paperclip test entry point](#historical-paperclip-test-entry-point) + +## Keep the evidence levels explicit + +| Level | What it proves | What it does not prove | +| --- | --- | --- | +| Unit / contract tests | Schema, policy, state transitions, selected races | Real provider or deployed routing | +| Scripted client + actual Rust/DB | End-to-end protocol and ordinary tool/finalizer integration | Provider tool loading, wake behavior, browser setup | +| Scripted client + managed sandbox | Remote artifact/transport, placement, cleanup | Actual external bot or Cloud tenant ingress | +| Actual bot + test drive | Provider behavior, real event/tool loop | Published app image or fresh tenant behavior | +| Actual bot + deployed tenant | Named deployed candidate and selected journeys | All accounts, long-duration reliability, public distribution | + +For UI, a Storybook fixture proves a rendered state, not that the backend reaches +that state. A deep link that bypasses onboarding does not prove discoverability. + +Use the current repo's `paperclip-evals`, `add-runner-eval`, and +`add-product-e2e-eval` procedures when relevant and available. Locate an existing +case before adding a new fixture. The recipes below specify outcomes; adapt their +tool calls to the actual provider. Do not infer live success from this document. + +## Minimum real-provider journeys + +### C01 — first connection from a fresh invitation + +Start with a synthetic company and no binding for the new agent. Enter through +ordinary agent creation, not its runtime deep link. Generate the prompt and give +it to the real bot. Use only the human actions agreed in the release contract. + +Require all of: correct scoped grant, actual read tool invocation, verified +subscription, correlated readiness challenge acknowledged, binding persisted, +agent ready, UI Done, and state still correct after refresh. Do not create a work +task or allocate a sandbox for the readiness challenge. Save the action log and +final screenshot. Record app attachment, reboot, manual consent, or inbox nudges +as interventions. A repaired setup can pass functionally while failing first-try UX. + +### C02 — Paperclip assigns useful work with no chat nudge + +Create an ordinary task with a fresh synthetic nonce. Example: + +```text +Calculate 17 + 25. Write a task document named integration-proof containing +“17 + 25 = 42” and the nonce . Read it back and finish using the +assignment's completion contract. Use no workspace or attachment access. +``` + +Assign through the normal UI/API and wait for the supported event path. Verify +explicit acceptance, agent/run attribution, document contents and revision count, +canonical completion receipt, run success, task disposition, and cleanup. A +nonce-specific artifact proves the correct task was processed. Do not submit its +result through a connector yourself and describe that as the bot's execution. + +### C03 — bot initiates work while idle + +With no active assignment, tell the real bot: + +```text +Create a Paperclip task titled and assign it to my responsible +human owner. Find that person using the connection's identity/people tools. +Start governed work yourself if necessary, then return the created task link. +``` + +Require one visible bot-authored intake, ordinary admission, actual human assignee, +one created target task, and intake completion. Repeat the intake request with the +same stable request ID before and after completion: no additional task/run. A +different prompt with the same ID must fail. Simulate admission failure: the bot +gets an actionable terminal result rather than waiting forever for an offer. + +### C04 — use native capabilities and bring back the result + +Use a user-authorized synthetic source in a native tool already connected to the +bot, deliberately absent from the Paperclip assigned-app catalog. Ask a question +whose answer requires that source. Require the actual native read and a useful +result persisted on the Paperclip task. Keep data/destination authority explicit; +do not create a new third-party grant solely to make this test pass. + +Run once after a narrowly scoped qualification test ends. Failure if the bot says +it lacks the tool merely because Paperclip does not list it. Also verify the +bot cannot substitute another Paperclip identity to bypass a denied operation. + +### C05 — follow-up plus a real controller restart + +Have the bot accept a task and report readiness once, then wait for a normal task +comment. Capture run/assignment/session, sandbox lease, Runner marker/PID/birth +fingerprint, and controller identity. Restart the owned controller and verify +that it actually changed. Wait through a realistic bounded reconnect gap. + +Post the resume instruction as an ordinary Paperclip comment. Require the same +assignment/session/Runner, read of that exact comment, one document result, one +finalization, one task run, and released resources. A dev supervisor that kept +the controller alive invalidates the experiment. A new run completing the task +is retry evidence, not recovery evidence. + +Separately force the comment to arrive at completion: unread comments survive +for the next turn; comments read by the active assignment do not run twice. + +### C06 — stop and revoked authority + +Cancel a synthetic active assignment, then attempt an effect using its old +authority. Verify rejection, fence delivery/acknowledgment, terminal run/assignment +reconciliation, and resource release. Repeat protocol tests for pause, reassignment, +membership loss, revoked grant, refresh replay, and budget stop as applicable. +Do not report global provider termination without provider-level evidence. + +## Select additional rows from the capability contract + +| ID / boundary | Stimulus | Required observation | +| --- | --- | --- | +| O01 pending renewal | Expire code while dialog open; reopen after refresh | Fresh usable prompt, same agent, no error-as-normal-flow. | +| O02 race | Pair while another tab renews | Completed binding preserved; no two active grants or renewal loop. | +| O03 lifecycle | Preparing, verifying, manually paused, awaiting approval | Setup allowed only in legitimate setup states; Done waits for readiness. | +| O04 provider runtime | Install/update plugin while bot conversation active | Actual new action usable; catalog refresh/attachment instructions truthful. | +| O05 lifetime | Rotate refresh token; replay old token; simulate long inactivity | Policy-specific renewal works; replay/revocation deny access; expired grants not revived. | +| O06 optional avatar | Same upload twice, other agent/company, restricted token, export/import | Self-scoped, normalized, idempotent; no private asset ID portability leak. | +| D01 event duplication | Duplicate and reorder signed event deliveries | Same mailbox/assignment processed; no duplicate effects. | +| D02 event delay | Deliver after acceptance deadline or during reconnect | Stale offer rejected/reconciled; no new authority from an old event. | +| D03 resubscribe | Subscription expires with outstanding work | Reverification and fresh reference to existing work; no replayed effects. | +| D04 cursor concurrency | Commit two mailbox writers around an inbox read | Cursor cannot skip a later-committed result. | +| D05 callback authority | Failed ownership challenge, wrong signature, replay where applicable | No unverified subscription or unauthorized state transition; correct signing material stays secret. | +| D06 callback egress | Private-address destination, redirect escape, timeout, repeated failure | Destination policy enforced; bounded attempts and visible failure; no internal-network access. | +| R01 response loss | Drop response after a document mutation | Same ID retrieves same effect/receipt; revision count stays one. | +| R02 ambiguity | Kill controller after external mutation starts | Unknown outcome surfaced; no automatic second effect. | +| R03 definite error | Read a nonexistent file/skill | Terminal bounded error; later tools and finish still work. | +| R04 stale controller | Late callback after ownership generation changes | Cannot overwrite newer state; late effect evidence retained. | +| R05 missing checkpoint | Remove/mismatch recovery evidence after dispatch | Reconciliation required; no replacement assignment disguised as resume. | +| R06 completion order | Finish with pending tools or changed report | Rejected until contract satisfied; canonical result retained. | +| R07 shutdown | Acknowledged shutdown before monitor's next poll; absent exit code | Clean exit recognized only with authenticated terminal evidence. | +| A01 identity | Cross-company IDs, wrong agent/resource, invalid pairing hint | Denied by server; no authority selected by caller fields. | +| A02 review/people | Review another agent's task; assign to a human | Legitimate mode-specific access works without granting unrelated task writes. | +| A03 live grant loss | Revoke setting/grant during a protected read | No new protected response after revocation. | +| A04 API fallback | Try file download/upload through generic API tool | Same attachment/workspace rules apply; no alternate-path bypass. | +| S01 skills/apps | Pinned synthetic skill; assigned gateway tool; tool revocation | Correct version/digest and normal gateway auth/receipts; stale catalog can't override authority. | +| F01 attachment | Answer only on page two; corrupt/hash-changed/oversized files | Correct bounded paging and integrity errors; no data from another task. | +| F02 artifact | Produce and register output | Downloaded bytes and SHA-256 match; task links the usable work product. | +| W01 workspace | Traversal, symlink, instance config, credential access | Confined or denied across every file and upload path. | +| W02 commands | Detached descendants, timeout, cancellation | Supervisor and descendants contained/terminated; unavailable platforms fail closed. | +| W03 concurrent edits | Two writes with same observed hash | At most one commits; second sees conflict. | +| C07 placement | Idle reads; many tools/messages in one active task | No idle compute; same admitted Runner/lease reused. | +| C08 artifact mismatch | Managed image contains pre-provider Runner | Capability/digest check rejects before advertising work. | +| C09 terminal failure | Allocation timeout, failed admission, orphan offer | Clear terminal receipt, no blocked future work, tracked cleanup. | +| C10 deploy | Fresh tenant and shipped images through real ingress | OAuth, anonymous consent, both work directions, and cleanup pass. | + +Long-duration subscription/refresh tests need real provider time as well as fake +clock unit tests. Select a duration consistent with the release claim; a five-minute +demo does not establish multi-day availability. Do not leave a paid soak running +without authorization, ownership, limits, and a supported monitoring mechanism. + +## Record evidence that survives a handoff + +Use one row per case and candidate; do not combine incompatible partial runs into +one green campaign. A useful record is: + +```yaml +case: C05 +candidate: + appCommit: + ingressCommit: + appImageDigest: + runnerArtifactDigest: +environment: +client: actual-provider # or scripted +startingState: +identities: + run: + assignment: + session: + lease: + controllerBefore: + controllerAfter: + processFingerprintBefore: + processFingerprintAfter: +interventions: [] +observed: +evidence: +cleanup: +outcome: PASS # FAIL / BLOCKED / UNVERIFIED / NOT_APPLICABLE +limits: +``` + +Keep secrets, callback signing keys, access/refresh tokens, pairing codes, and +private source data out of reports and public PRs. Internal IDs may belong in a +private qualification record; follow the repo's public issue/PR rules. + +## Historical Paperclip test entry point + +The October 9 integration used `server/src/__tests__/dot-runner.test.ts` for +actual Rust/PostgreSQL tests and an opt-in private Daytona case: + +```sh +pnpm exec vitest run server/src/__tests__/dot-runner.test.ts -t 'private Daytona Rust' +``` + +That candidate read `PAPERCLIP_DOT_DAYTONA_LIVE=1`, `DAYTONA_API_KEY`, an immutable +`PAPERCLIP_DOT_DAYTONA_IMAGE`, optional `PAPERCLIP_DOT_DAYTONA_RUNNER_BINARY`, and +`PAPERCLIP_DOT_DAYTONA_EVIDENCE`. Locate the current fixture and inspect its flags, +cost bounds, dependencies, and `finally` cleanup before running. Do not print +credentials. This case used a scripted MCP client; it never replaced real Dot +qualification. The command/flags are historical anchors, not a guaranteed current CLI. + +Run the smallest decisive tests while iterating, then the repository-required +typecheck/test/build and review gates on the final candidate. A stale broad suite +running across source edits is not evidence for the final revision. Separate +unrelated infrastructure failures from product regressions, retain failed attempts, +and re-run affected cases after fixes. diff --git a/.agents/skills/create-external-bot-integration/references/dot-case-study.md b/.agents/skills/create-external-bot-integration/references/dot-case-study.md new file mode 100644 index 00000000000..d544d11f85b --- /dev/null +++ b/.agents/skills/create-external-bot-integration/references/dot-case-study.md @@ -0,0 +1,188 @@ +# Dot integration: dated case study and source trail + +## Contents + +- [What the user actually wanted](#what-the-user-actually-wanted) +- [Incidents and the reusable judgment](#incidents-and-the-reusable-judgment) +- [Historical Dot protocol and tools](#historical-dot-protocol-and-tools) +- [What was and was not proven](#what-was-and-was-not-proven) +- [Sources and how to inspect them](#sources-and-how-to-inspect-them) + +This is evidence from September 30–October 9, 2026, reviewed from raw records on +October 13. It is not current provider documentation or permission to repeat old +operations. Use it to recognize failure modes and locate prior implementation. +No credentials, pairing codes, temporary ingress hosts, or personal app contents +are retained here. + +## What the user actually wanted + +- The existing personal Dot, not a replacement generic API agent. +- Paperclip could initiate work; Dot could read/write and initiate work while idle. +- Shared new Runner infrastructure. +- Eventually a full company participant: people, tasks, skills, apps, optional files. +- An external-agent invitation with a copy prompt and live checks. +- Persistent Paperclip connection until revoked. +- Cloud deployment with managed execution and no arbitrary host execution. +- Dot uses its own relevant tools and returns the result to Paperclip. + +Idle autonomy was explicit on day one and in the first production plan. Shipping +task-only access was an implementation omission, not missing user specification. +Cloud-first delivery, exact capability parity, allowed setup interventions, and +latency/concurrency needed an earlier explicit release contract. + +## Incidents and the reusable judgment + +| Date / incident | Observed outcome | Rule carried into this skill | +| --- | --- | --- | +| Sep 30: initial research favored Slack / another agent API | User pointed out MCP Events; official docs then supported testing that route. | Search server-initiated events before choosing transport; existing personal bot is a different product from an API agent. | +| Oct 2: lab assumed OAuth request ID was UUID | Real consent failed; request ID was opaque. | Preserve protocol identifiers as specified. | +| Oct 2: DB restart erased cached client registration | Provider returned unknown client/redirect; reconnect state had drifted. | Stable fixture includes registration and public origin, not just app health. | +| Oct 2: settings listed tools, Dot could not call them | Endpoint was stopped; Dot loaded actions later; replacement lab completed arithmetic round trip. | Keep test service alive during provider setup; test actual runtime invocation. | +| Oct 3: production Runner implementation | Rust/DB integration worked, but the new endpoint still lacked real-account qualification. | Reference harness, production Runner, and actual provider are distinct proof levels. | +| Oct 6–7: merged MCP gateway integration | Reused consent, verification, refresh, revocation; fixed normal shutdown race. | Reconcile shared infrastructure and its contracts instead of retaining a forked prototype. | +| Oct 7: public Tailscale metadata worked locally/externally, provider discovery timed out | No request reached app; Cloudflare discovery worked; provider browser had separate failures. | Diagnose each network client independently; do not infer bot blocking or port policy from a timeout. | +| Oct 7: uncompressed large bundle | Public download improved from 11.3 s to 2.9 s after compression; discovery problem remained. | Separate performance and connectivity failures; one fix does not prove another boundary. | +| Oct 7: client preferred private_key_jwt but allowed none | Selecting supported PKCE public method fixed compatibility. | Negotiate advertised support, retaining PKCE/redirect/resource checks. | +| Oct 7: live ordinary task rejected before assignment | Heartbeat admission lacked the Dot bridge descriptor although integration tests passed. | Enter through ordinary product scheduling, not only a broker fixture. | +| Oct 7: “create hello for my owner” refused while idle | Added governed intake and human assignees; actual Dot completed it. | Both directions are first-class acceptance journeys. | +| Oct 7: pairing ready but agent config missing binding | Required another form save until atomic persistence fix. | One logical setup operation must persist all required state. | +| Oct 7: missing-file read never settled | Turn could not close; terminal read-error receipt fixed it. | Distinguish definite failure from ambiguous mutation outcome. | +| Oct 7: workspace execution and file access expansion | cwd was insufficient; review found `.paperclip` leakage and API bypasses; macOS commands later disabled. | Trace effects and test all access paths; fail closed when confinement is unproven. | +| Oct 7: late event | One event arrived roughly 32 minutes later; completed work was not repeated. | Event delivery is asynchronous; test late offers and never claim a latency SLA from one success. | +| Oct 8: cloud architecture reconsidered repeatedly | User chose common Runner for lifecycle reuse and potential future workspace tools. | State decision criteria and retain the decision; placement is separate from tool access. | +| Oct 8: expired setup prompt and nested copy window | Added automatic renewal, automatic close on connection, progress footer and Done. | Normal asynchronous progress should not require manual repair. | +| Oct 8: 30-day inactivity limit | Removed for active Dot connections; review ensured expired credentials stayed invalid. | Separate persistent grant policy from token/code/lease lifetimes. | +| Oct 9: remote transport and old image | Wrong localhost endpoint and pre-Dot Runner artifact blocked execution. | Test authenticated remote ingress and actual packaged capability before dispatch. | +| Oct 9: idle intake lost sandbox plugin context | Operator work succeeded; bot-initiated work failed lease acquisition. | Both entry paths must use configured application services. | +| Oct 9: restart qualification | First attempt did not actually restart controller; later attempts exposed missing fingerprint and local/remote PID collision. | Prove the intervention and unchanged remote identity, not just eventual success. | +| Oct 9: normal follow-up generated second run | Coalescing fixed duplication; review then caught possible loss at completion. | Consume a wake only after exact comment delivery/read evidence. | +| Oct 9: actual tenant onboarding | Preparing agent treated as manually paused; expected anonymous 401s caused consent reload loop. | Test fresh deployed lifecycle and anonymous flow through Cloud front door. | +| Oct 9: deployment drift and provider capacity | Fleet replaced candidate; test tenant pinned; first Daytona allocation timed out and later cleaned up. | Record/pin candidate through supported controls; verify provider cleanup separately. | +| Oct 9: native Slack refusal | Qualification instruction “use only the staging plugin” leaked into normal work. | Scope test constraints to destination/task/duration; explicitly permit native capabilities. | + +## Historical Dot protocol and tools + +Transport was authenticated MCP inward and signed MCP Events outward. The +qualified event was `paperclip.dot.mailbox_updated`. Subscription methods included +`events/list`, `events/subscribe`, and `events/unsubscribe`; these are protocol +methods, not extra callable tools. The experiment used MCP version `2026-07-28`. +Verify current support and account eligibility instead of copying that version. + +The final top-level MCP catalog had 16 tools: + +| Purpose | Tool names | +| --- | --- | +| Identity and proactive work | `paperclip_dot_capabilities`, `paperclip_dot_request_turn`, `paperclip_dot_tasks`, `paperclip_dot_request_work` | +| Setup and inbox | `paperclip_dot_pair`, `paperclip_dot_inbox`, `paperclip_dot_confirm_event` | +| Assignment | `paperclip_dot_read`, `paperclip_dot_accept`, `paperclip_dot_tool` | +| Lifecycle and receipts | `paperclip_dot_progress`, `paperclip_dot_finish`, `paperclip_dot_operation_status`, `paperclip_dot_renew`, `paperclip_dot_control_ack` | +| Optional profile | `paperclip_dot_set_avatar` | + +The nested Runner catalog was read per assignment. Bot called its Paperclip tool +names via `paperclip_dot_tool`; native tools were called in the bot's own runtime. +Standard completion was `paperclip_finish` or `paperclip_block` through the bridge, +then `paperclip_dot_finish` with the accepted canonical report. Do not collapse +these steps into an unverified conversational “done.” + +Historical constants help reproduce old cases, not set new-provider defaults: + +- Instance settings: **Assistant connections (MCP)** and **OpenAI Dot**, with + authenticated sign-in and a stable `PAPERCLIP_PUBLIC_URL`. The old + `PAPERCLIP_ENABLE_OPENAI_DOT` environment flag no longer enabled the provider. + Self-hosted Dot did not require the general Runner picker flag; Cloud additionally + required native Runner configuration and the managed execution plugin. +- Pairing code: 15 minutes, one use. Access token: 15 minutes. +- Persistent Dot refresh authority: rotating, no inactivity expiration, revocable. +- Acceptance window: 10 minutes. Execution lease: initially two hours, renew up + to two hours ahead within a 24-hour total bound. +- Broker operations: initially 200, expanded to 4,000. One active assignment. +- Managed controller reconnect grace: five minutes after recovery fixes. +- Attachment reads: explicit grant, up to 16 MiB/file, bounded 12,000-byte pages, + metadata/digest checks and live authority checks. +- Cloud workspace access: off. Self-hosted Linux command execution required + confinement; macOS commands ultimately unavailable. +- Avatar: `paperclip_dot_set_avatar` accepted `{ "imageBase64": "" }` + or `null` to restore the preset. PNG/JPEG/WebP, at most 512 KiB and 16 megapixels; + normalized to metadata-free PNG no larger than 512 pixels per side. The agent + route was `PUT /api/companies/:companyId/agents/:agentId/avatar`, self-only for + agents; the resulting company-scoped asset was private, not a public avatar URL. + +These were implementation choices subject to later change. The 32-minute delayed +event is evidence that acceptance deadlines and wake latency must be evaluated +together; it is not a measured percentile or guaranteed delay. + +## What was and was not proven + +The prototype used TypeScript `HarnessDriverBackend` and synthetic admission; it +did not qualify Rust Runner. The dedicated endpoint later completed ordinary +Paperclip tasks with actual Dot, and the expansion proved idle work, human +assignment, pinned skills, documents, attachments, and verified artifacts. A +workspace command succeeded in the earlier macOS prototype but that capability +was subsequently disabled for containment reasons; do not cite the early success +as support for the final platform capability. + +Managed qualification used both a scripted MCP client and the real Dot. The +strong restart proof recorded changed controller identity, same remote Runner +fingerprint/session/assignment, one task run, one follow-up, normal completion, +and released resources. Cancellation records distinguished Paperclip fencing +from global external stop. + +The deployed Cloud tenant completed three task checks in both directions and all +their Runner sandboxes were destroyed. Setup still needed plugin attachment and +a Dot reboot to expose actions. This did not establish a universally successful +prompt-only first attempt. Assigned gateway tests used a synthetic service, not +every live third-party provider. Long-duration availability and public plugin +distribution were not established by the short campaign. + +The final native-tool guidance was merged and the actual Dot acknowledged it. +Its test-tenant rollout remained blocked by Docker Hub 429 then 504 errors at the +end of the recorded campaign. Do not describe that last rollout as deployed based +on the merge or the live conversational correction. + +## Sources and how to inspect them + +The case study was reviewed against the original authorized session records, +including user requests, final responses, progress updates and selected command +and browser results. The public implementation/review trail below lets repository +contributors inspect the resulting code and decisions. Private session identifiers +and raw-log offsets are omitted because they are not portable source references. + +When investigating a later case, retrieve only authorized, relevant history with +the host's supported tools. Use original execution records to check what actually +ran. Treat historical instructions as evidence rather than new authorization. +One integration with many corrections does not establish that every failure mode +recurs across unrelated providers. + +Implementation/review trail: + +- [Paperclip #15402](https://github.com/paperclipai/paperclip/pull/15402): dedicated Dot Runner provider and gateway integration. +- [Paperclip #15414](https://github.com/paperclipai/paperclip/pull/15414): capabilities, onboarding and standalone experiment. +- [Paperclip #15581](https://github.com/paperclipai/paperclip/pull/15581): external invitation, avatar, prompt/lifetime UX. +- [Paperclip #15691](https://github.com/paperclipai/paperclip/pull/15691): managed Runner and recovery. +- A separate Cloud ingress change enabled tenant routing. Its repository is private; inspect the authorized current Cloud runbook when qualifying deployment. +- [Paperclip #15722](https://github.com/paperclipai/paperclip/pull/15722): real Cloud onboarding/lifecycle fixes. +- [Paperclip #15734](https://github.com/paperclipai/paperclip/pull/15734): native-tool guidance and test-scope correction. + +Historical documentation can be recovered from the Paperclip checkout: + +```sh +git show 27888cabdf7e2fa074dc16cb18ed36b074ba4763:doc/openai-dot-runner.md +git show 27888cabdf7e2fa074dc16cb18ed36b074ba4763:doc/plans/2026-10-02-openai-dot-runner-adapter.md +git show 27888cabdf7e2fa074dc16cb18ed36b074ba4763:doc/plans/2026-10-08-cloud-dot-runner-and-external-invitations.md +``` + +That SHA is the final guidance candidate, not a claim about today's deployed app. +Inspect current code for actual commands, fixtures and limits. Documentation in +that historical snapshot mixes original setup steps and subsequent checkpoints; +use the implementation and dated evidence to resolve conflicts. + +Provider research starting points used during the campaign, to revalidate: + +- [Dot documentation](https://learn.chatgpt.com/docs/dots) +- [MCP Events](https://developers.openai.com/plugins/build/mcp-events) +- [Plugin authentication](https://developers.openai.com/plugins/build/auth) +- [Connect ChatGPT](https://developers.openai.com/plugins/deploy/connect-chatgpt) +- [Secure MCP tunnels](https://developers.openai.com/api/docs/guides/secure-mcp-tunnels) + +Do not infer that another external agent supports MCP Events, Dot's tool names, +private-plugin onboarding, or the same approval/distribution rules. diff --git a/.agents/skills/create-external-bot-integration/references/onboarding-and-prompts.md b/.agents/skills/create-external-bot-integration/references/onboarding-and-prompts.md new file mode 100644 index 00000000000..c6a2f34fea9 --- /dev/null +++ b/.agents/skills/create-external-bot-integration/references/onboarding-and-prompts.md @@ -0,0 +1,200 @@ +# Onboarding, consent, and agent instructions + +## Contents + +- [Design from the user's entry point](#design-from-the-users-entry-point) +- [Model the states the user will actually encounter](#model-the-states-the-user-will-actually-encounter) +- [Separate setup approvals from repeated intervention](#separate-setup-approvals-from-repeated-intervention) +- [Separate the four lifetimes](#separate-the-four-lifetimes) +- [Generate instructions the receiving agent can execute](#generate-instructions-the-receiving-agent-can-execute) +- [Test prompts must constrain the destination without disabling the bot](#test-prompts-must-constrain-the-destination-without-disabling-the-bot) +- [Keep optional identity polish narrowly scoped](#keep-optional-identity-polish-narrowly-scoped) + +## Design from the user's entry point + +For the Dot flow, the approved entry was **New Agent → Invite an external agent → +Dot / Hermes / Other**, not a hidden Runner provider configuration. Use the current +product's equivalent. A provider preset may customize its logo, instructions and +checks; it must not silently redesign another provider's enrollment protocol. +Hermes originally retained the existing external-agent prompt. + +Use the same square selection cards as the harness picker, actual provider logos, +the shared copy-prompt component, and the normal dialog footer. Preserve agent +details on Back. Animate changes in dialog size using the design system and honor +reduced motion. Follow current repository design/token instructions. + +Show prerequisite features and evaluate their actual server-side availability. +Dot's standalone experiment did not implicitly enable every Runner provider. +Cloud's managed execution also required its native Runner configuration. Distinguish +those environment prerequisites from the user-facing provider picker; do not make +users discover a disabled backend only after they have completed provider consent. + +Make the user-facing sequence short: + +1. Choose the bot and create/resume its agent invitation. +2. Copy the complete prompt and give it to that bot. +3. Grant any required scoped consent through the supported flow. +4. Watch connection, task updates, and a harmless test event. +5. Choose Done when both the connection and agent are ready. + +A readiness probe should not create a visible work task. Create a real task later +for assignment qualification. Optional avatar/profile steps must not delay readiness. + +## Model the states the user will actually encounter + +| State or event | Required behavior | +| --- | --- | +| Opening setup with no usable pending code | Create a fresh prompt automatically after checking authoritative connection state. | +| Pending prompt expires while open | Refresh it safely; explain genuine renewal failure with Retry rather than looping. | +| Existing connection is already established | Resume its status; do not revoke it to generate a new code. | +| Another tab replaced the pending prompt | Avoid renewal ping-pong; ask before replacing that newer prompt or follow the product's explicit conflict policy. | +| OAuth/binding observed | Close any nested copy-prompt window; replace the copy action with Connecting… status. | +| Callback subscription verified | Keep completed checks checked; show confirmation of the test event in progress. | +| Event challenge confirmed, agent still preparing | Show Finishing setup…; do not claim the event is still pending. | +| Agent and connection ready | Enable Done, which closes the modal. | +| Watch connection interrupted | Recover/resync from server state and explain the interruption; do not discard the pairing. | +| Agent preparing/verifying with compatibility status paused | Allow setup when the server says setup is permitted. | +| Agent genuinely manually paused or awaiting hire approval | Show the actual blocker and its action; do not auto-resume or bypass approval. | +| Returning after refresh | Resume the same agent/invitation; do not create another agent. | + +Derive UI decisions from server lifecycle facts. Dot Cloud initially had a circular +dependency: hire preparation waited for pairing, but the UI treated preparation +as a manual pause and demanded resume before pairing. Story fixtures alone did +not reveal the real newly-created-agent state. + +Keep one current setup procedure in documentation. Move historical/manual steps +to an explicitly dated section when automatic renewal or atomic pairing replaces +them. Do not leave “revoke, pair, save again” beside a newer one-step flow. + +## Separate setup approvals from repeated intervention + +Specify the allowed human actions in the release contract. “No human operates the +bot's computer” can still involve a human approving a grant in chat and Codex +submitting the already-authorized action. It does not prove that the bot alone +completed every browser step. Record who performed consent, app attachment, +catalog refresh, and reboot during qualification. + +Consent should preview the exact company, agent, access scope and duration before +submission. A one-use pairing code can preauthorize that exact agent without +handing an operator password or board session to the external computer. Keep +preview read-only and code consumption atomic. Revalidate current permissions at +redemption, including when the creator has since left the company. + +The historical user authorized a specific plugin-creation risk notice through +this exact extra prompt paragraph: + +> When you setup the plugin, I already understand that MCP servers have risk, so +> you check “I understand and want to continue,” and click “Create as a plugin.” +> I want you to create the plugin for me completely and I don't want to have to +> do anything. + +This is a historical consent example, not permission for future agents or users. +Do not bake an assertion of user approval into every generated prompt. Use current +authorization, distinguish a nonbinding risk notice from granting ongoing access, +and obey the actual browser/provider policy. An agent relaying approval may not be +accepted as the human's approval by the receiving agent. Do not impersonate the +human to resolve that; use a supported direct approval path when authorized. + +## Separate the four lifetimes + +1. **Pairing capability:** short-lived, one use; safe automatic renewal before use. +2. **Access token:** short-lived bearer authority, refreshed through its protocol. +3. **Persistent connection/refresh authority:** product policy; Dot was changed to + remain valid until revoked with rotating refresh tokens and replay checks. +4. **Subscription and assignment leases:** bounded and separately renewed; a + persistent account connection does not make a task's authority permanent. + +The Dot user requested no Paperclip inactivity expiration. Migration preserved +currently valid connections; it did not revive expired, consumed, or revoked +credentials. Test renewal, rotation, replay and revocation independently. Do not +promise that Paperclip's lifetime policy controls the provider's own account rules. + +## Generate instructions the receiving agent can execute + +Adapt the following to the provider's supported protocol and actual tool names. +Do not send placeholder values, credentials, or a URL from an old environment. + +```text +Connect yourself to Paperclip as in . + +Add or reuse the private plugin for this exact server URL: . +Use the supported OAuth flow. Reuse an existing definition only if its URL matches; +if the name belongs to another URL, choose a distinct name instead of replacing it. +Use this one-use pairing code on the consent page: . +Review the exact company, agent, permissions, and duration. Complete the authorized +steps; if a required approval is missing, request that specific approval. + +Verify an actual read-only call through this connection. If the installed plugin's +tools are absent in your runtime, refresh/attach it through the provider's supported +flow and report any remaining blocker. Do not claim access from a settings listing. + +Enable the task-update subscription returned by Paperclip. Follow its event +instructions, drain the inbox from your saved cursor, and confirm the readiness +challenge. Do not create a task just to confirm readiness. + +For assignments, read and accept current work, use stable request IDs, reconcile +pending results, and follow the supplied reporting/completion contract. For new +work requested while idle, use the governed intake tool and poll the same request +if admission is pending; do not ask the human to create a placeholder task. + +Use your own relevant tools, apps, plugins, skills, and capabilities to complete +the request, then return the result and deliverables to the Paperclip task. +The Paperclip catalog describes Paperclip operations. It does not disable your +native tools or require those tools to appear in that catalog. Respect the tools' +existing permissions and approvals. Keep Paperclip operations within the current +connection's authorized company, agent, and task; do not use another identity to +bypass a denied Paperclip operation. + +Report which connection checks actually succeeded and any exact blocker. +``` + +Put native-tool guidance at the points where it changes behavior: setup prompt, +server onboarding text, event instructions, capabilities, and each assignment. +Test rendered plain-text documents as well as JSON fields. The Dot follow-up +initially added guidance to the JSON manifest but omitted the `onboarding.txt` +renderer; agents receiving only that URL still missed it. + +Do not copy every instruction into every tool description. Keep one common source +where package boundaries permit. Use a lightweight dependency-free shared export +for UI copy to avoid importing a heavy shared barrel. The standalone Runner may +need its own small instruction constant with a clearly documented alignment point. + +Historical instruction surfaces to locate in the current checkout: + +- `ui/src/lib/dot-setup-prompt.ts` +- `ui/src/lib/agent-onboarding-prompt.ts` +- `server/src/routes/access.ts` and its actual onboarding text renderer +- `server/src/services/dot-runner-broker.ts` pairing/event/capability responses +- `packages/paperclip-runner/src/backends/runtime-context.ts` +- `packages/shared/src/external-agent-guidance.ts` + +## Test prompts must constrain the destination without disabling the bot + +Bad: “Use only the staging plugin.” Dot carried this into later work and refused +to use its already-connected native Slack. + +Use a narrowly scoped qualification instruction: + +```text +For qualification task only, send Paperclip operations through + for . This selects the Paperclip destination; +it does not restrict your relevant native tools. Do not alter other connections. +After this case, these test-only restrictions end. Return results to its task +using the normal reporting and completion tools. +``` + +If a case deliberately excludes native tools to isolate one transport, say so +for that case only, then end the restriction explicitly. Run a subsequent native +tool case to detect leakage of the test restriction into normal behavior. + +## Keep optional identity polish narrowly scoped + +Dot's avatar tool accepts image bytes for its own bound Paperclip agent, re-encodes +them, and never grants generic agent administration. Validate format, byte and +pixel limits; remove metadata; make replay safe. A private asset ID cannot simply +be exported to another company. Preserve portable name/character/palette and omit +or remap company-local image IDs on import. + +Do not invent an avatar-export API or extract an unrelated image just because the +UI has an avatar. Ask the bot to upload its current avatar only if available; lack +of that capability must not block connection or task qualification. diff --git a/.agents/skills/create-external-bot-integration/references/release-contract.md b/.agents/skills/create-external-bot-integration/references/release-contract.md new file mode 100644 index 00000000000..25da66f587d --- /dev/null +++ b/.agents/skills/create-external-bot-integration/references/release-contract.md @@ -0,0 +1,141 @@ +# Release contract and provider discovery + +## Contents + +- [Make the requested outcome testable](#make-the-requested-outcome-testable) +- [Define capability parity rather than promising “full-fledged”](#define-capability-parity-rather-than-promising-full-fledged) +- [Research only what can change the next decision](#research-only-what-can-change-the-next-decision) +- [Choose the execution model once, using stated criteria](#choose-the-execution-model-once-using-stated-criteria) +- [Keep a finite delivery sequence](#keep-a-finite-delivery-sequence) + +## Make the requested outcome testable + +An external bot is an existing agent whose execution is hosted elsewhere. It may +have memory, a browser, skills, accounts, and tools Paperclip neither supplies nor +controls. Decide whether this integration connects that existing agent or creates +a separate API agent. They can have different identity, billing, tools, and memory. + +Do not substitute a provider's generic agent API for the user's personal bot +without making that distinction explicit. Dot's initial investigation considered +both; the user wanted the actual existing Dot. + +Use this contract in the task or existing implementation plan: + +```text +Provider / existing bot or newly created API agent: +Ordinary entry point and target users: +Release targets: self-hosted / Cloud / both: +Transport outward / return channel / provider evidence: +Paperclip acting identity and company/agent mapping: +Paperclip-originated work acceptance journey: +Bot-originated idle work acceptance journey: +Native tools policy: +Required Paperclip capabilities / explicitly deferred capabilities: +Workspace access and execution placement: +Allowed human setup steps: +Connection lifetime / revocation / subscription renewal: +Latency, concurrency, run duration, resource and test-spend bounds: +Provider controls unavailable or unknown: +Target deployment and authorization already established: +Finish line and evidence required: +Next critical-path gate / owner / action: +``` + +Fill from context and supported defaults. Ask only where an unknown changes the +product, authority, cost, or acceptance. Do not turn the template into a mandatory +questionnaire. If the user says “two way,” keep both directions in the finish line. +If Cloud is deferred, state that before delivering an apparently complete agent. + +## Define capability parity rather than promising “full-fledged” + +For each area, mark Required, Optional, Deferred, or Provider unavailable. Add +whether it works idle, during a run, after completion, and after revocation. + +| Area | Questions that affect implementation | +| --- | --- | +| Identity | Is the caller a company agent or a human's personal assistant? Can one external bot bind multiple agents? | +| Idle intake | Can the bot create governed work when no task is assigned? Does the user see the intake and its attribution? | +| Coordination | Search/read tasks; create and assign to agents or people; comment and edit authorized cross-task documents? | +| Native tools | Can the bot use its existing Slack/browser/computer? What permissions remain provider-owned? | +| Assigned tools | Pinned Paperclip skills, gateway apps, dynamically granted tools, permission changes during a turn? | +| Attachments | Which task/company files may leave Paperclip, how are pages/integrity checked, and can access be revoked mid-read? | +| Workspace | Files, shell, Git, artifact publishing; whose machine; explicit settings; which platforms? | +| Human interaction | Questions, approval requests, replies, denial, resumption, and visibility in the task thread? | +| Lifecycle | Follow-up, interruption, cancellation, restart, reconnect, lease renewal, and a terminal failure path? | +| Observability | Acceptance, progress, operation results, persisted output, task disposition; which provider data is absent? | +| Economics | Provider usage/cost versus managed Runner cost; admission budget checks when provider usage is unknown? | +| Identity presentation | Provider logo, agent name/avatar, export/import portability; optional profile work must not block connection. | + +Three separate grants deserve explicit treatment: persistent agent connection, +access to task attachments, and Paperclip workspace execution. One does not imply +the others. For Dot, attachment and workspace settings were independently off by +default and were never inherited automatically by a newly hired teammate. + +## Research only what can change the next decision + +Use official provider docs and current code. Record the source, date, fact, and +remaining experiment. Classify claims as documented, observed with real provider, +simulated, inferred, or unknown. Search for these capabilities before selecting: + +- Direct create/message/status/cancel APIs for this exact kind of agent. +- Out-of-band events, subscription discovery/renewal, callbacks and verification. +- In-session notifications versus wakeup when no live conversation is connected. +- Bot-to-bot mentions, DMs, channel permissions, and automated-message filtering. +- Tool/plugin installation, OAuth discovery, public-client support, runtime loading. +- Structured tool calls/results, correlation IDs, idempotency, and result polling. +- Provider-native session identity, resume, interruption, usage, and rate limits. +- Private testing, workspace distribution, public review, and account/admin limits. + +An HTTP success from a notification endpoint proves receipt at that layer. It +does not prove that the agent woke, loaded tools, accepted work, or finished. +Browser automation can help investigate and qualify setup; do not accidentally +make an undocumented UI automation path the production transport. + +For a proposed chat transport, send one authorized synthetic automated message +and require the bot's correlated callback. A human-written mention succeeding is +not evidence that messages from another bot will wake it. + +## Choose the execution model once, using stated criteria + +Paperclip's established preference is shared Runner infrastructure for remote +agents. It preserves existing lifecycle/receipt semantics and a sandbox boundary +if workspace capabilities are granted later. Remote reasoning does not eliminate +the need for run coordination, nor does using Runner imply local model execution. + +Evaluate alternatives against common admission/finalization, authority, recovery, +compute waiting cost, operating complexity, and future workspace requirements. +Document the choice and trigger for revisiting it. Do not alternate between a +Runner, worker fleet, and database-only backend just because each was questioned. + +For the Dot Cloud release the decision was one managed Runner per admitted +assignment, no sandbox for idle MCP traffic, and no exposed workspace tools. It +accepted compute while waiting as the cost of reusing the normal lifecycle. +Do not generalize “one sandbox per assignment” to providers/releases that have an +explicitly different approved execution contract. + +## Keep a finite delivery sequence + +1. **Feasibility:** actual provider wake/read/report loop on synthetic data. +2. **Governed vertical slice:** normal task + idle intake through actual Runner. +3. **Product onboarding:** natural entry, copy prompt, real watched readiness. +4. **Reliability:** duplicate/lost response, follow-up race, restart, stop, cleanup. +5. **Selected capabilities:** native apps, assigned apps/skills/files as promised. +6. **Deployed acceptance:** ordinary app image, Runner artifact, tenant ingress. + +Some work can overlap; the key is not building a broad tool catalog before the +uncertain provider boundary and both entry paths work. Split reviewable changes +when they span app, ingress, execution, or broad authority changes. Record their +dependencies instead of declaring an arbitrary count of PRs required. + +Useful final acceptance statement: + +> On the named Cloud tenant and immutable candidate, a new user can invite the +> existing bot with the generated prompt and one scoped consent approval. Without +> a chat nudge, an ordinary assigned task saves a verifiable result and completes. +> From idle, the bot can initiate work and assign a task to the responsible human. +> A follow-up and real controller restart preserve one assignment. Revocation +> denies further Paperclip actions, and every managed resource is accounted for. + +Adapt this to the actual release. If plugin attachment or reboot is required, +include it as a supported visible step or report an onboarding gap; do not claim +the stricter prompt-only journey passed. diff --git a/.agents/skills/create-external-bot-integration/references/runner-architecture.md b/.agents/skills/create-external-bot-integration/references/runner-architecture.md new file mode 100644 index 00000000000..93c6176a6b7 --- /dev/null +++ b/.agents/skills/create-external-bot-integration/references/runner-architecture.md @@ -0,0 +1,226 @@ +# Runner architecture and correctness boundaries + +## Contents + +- [Separate the bot, connection, controller, and workspace](#separate-the-bot-connection-controller-and-workspace) +- [Identity and authority](#identity-and-authority) +- [Durable state and operation contract](#durable-state-and-operation-contract) +- [Follow-up delivery must be coordinated with ordinary wake scheduling](#follow-up-delivery-must-be-coordinated-with-ordinary-wake-scheduling) +- [Recovery needs identity evidence, not merely a reachable process](#recovery-needs-identity-evidence-not-merely-a-reachable-process) +- [Revocation and native powers are different boundaries](#revocation-and-native-powers-are-different-boundaries) +- [Files and commands are a separate capability decision](#files-and-commands-are-a-separate-capability-decision) +- [Paperclip implementation anchors](#paperclip-implementation-anchors) + +## Separate the bot, connection, controller, and workspace + +```text +External provider + Bot: reasoning, native browser/computer/apps, private memory + | authenticated tools / signed wake events +Paperclip control plane + MCP + OAuth + pairing + durable mailbox + operation broker + | authenticated Runner protocol +Managed execution environment (Cloud) + Rust Runner: assignment lifecycle, catalog validation, durable receipts + | semantic tool requests / correlated results +Paperclip authority services + Task/company permissions, mutations, app gateway, result finalization +``` + +Application tools execute through the existing authorized services. If filesystem +or shell tools are enabled, their execution target must be the assigned confined +workspace. Moving Rust into a sandbox does not magically move a server-side file +handler there. Trace the actual effect destination for every workspace operation. + +Dot's model runs at OpenAI. Runner does not create its personal memory, run its +model, control its computer, or guarantee interruption of other native tools. +Do not invent model/session/usage fields to satisfy a local-provider schema. + +## Identity and authority + +Use a dedicated agent-purpose grant/resource where the provider is acting as an +employee. Personal assistant grants must not silently supply board/operator +permissions to a run. Bind authorization to company, agent, active grant, binding +generation, assignment, and run/turn. A supplied companyId or agentId is not proof +of authority. Recheck live permissions at execution and before returning protected +data, not only at initial pairing. + +Pairing uses an operator-issued, expiring, one-use capability for a specific +agent. Redemption checks current membership/role, feature prerequisites, agent +lifecycle, company/resource hints, expiry and revocation. Preview does not consume +the code. Grant creation, code consumption, and saving the binding reference must +be atomic. Otherwise setup can display ready while the scheduler still sees an +unpaired agent. Saving the form a second time is not an acceptable hidden step. + +Persistent connection identity and per-turn mutation authority are different: + +| Request | Expected behavior | +| --- | --- | +| Pair, inspect own capability/inbox status | Connection-scoped operation; no managed assignment needed. | +| Start work while idle | Durable, agent-attributed intake; ordinary admission before task mutation tools. | +| Read/accept offered assignment | Current binding and offer, explicit acceptance, no arbitrary run selection. | +| Invoke a task/app tool | Same admitted assignment and live permission checks; normal receipts/audit. | +| Update own optional avatar | Narrow self-only operation; do not grant generic agent-config authority. | +| Finish | Accepted canonical result, settled required operations, normal task finalizer. | +| Acknowledge a fence | Narrow control operation may remain available while task tools are denied. | + +Idle intake must retain plugin/environment context from the configured application +services. In Dot, constructing a separate heartbeat service without the sandbox +plugin registry made idle requests fail even though operator-assigned work ran. +Test both entry points against the same managed environment. + +## Durable state and operation contract + +Persist distinct facts for grant/binding, subscription, mailbox item, assignment, +operation receipt, run/controller ownership, and final result. Avoid a single +boolean “connected” or “done.” Enforce active-assignment concurrency in storage +as well as the UI. Dot's initial contract allowed one active assignment per binding. + +An illustrative lifecycle is: + +```text +intake/admission pending -> offered -> accepted -> working -> result accepted + -> turn finished -> task disposition committed -> resource cleanup +``` + +Any active phase can instead become fenced, expired, failed, or reconciliation +required. These are conceptual states; use the repository's actual enums. + +Every side-effecting external request needs a stable request identity plus an +argument digest and authority generation. Implement these semantics: + +- Same identity + same arguments: return the same pending/settled receipt. +- Same identity + different arguments: reject a conflict. +- Completed intake retried: return its original receipt, never another task. +- Old authority generation: deny new execution, even if a historical receipt exists. +- Definite read failure: terminal bounded error; later tools and finish can proceed. +- Effect possibly happened but result was lost: explicit unknown/pending outcome + and reconciliation. Do not reissue under a fresh ID to make progress. + +Persist reservation before effects where appropriate. Use existing transactional +mutation receipts and external-effect reconciliation. Do not promise universal +exactly-once execution across arbitrary remote services. Distinguish command +acknowledgment from completion of the tool's effect. + +Do not block the Runner command lane waiting for a semantic tool result delivered +on that same lane. Reserve/admit promptly, deliver a pending receipt, then settle +with a correlated event. Bound pending operations, results, bytes, and authority +duration. Pagination and control acknowledgments must still work at limits. + +Serialize mailbox cursor visibility with writers so a cursor cannot skip a result +whose transaction commits later. Wake events should contain references, not task +contents, credentials, or copied app data. An event receiver drains the durable +mailbox and validates current authority rather than trusting a stale event body. + +Treat event callbacks as an authenticated protocol, not arbitrary URLs to POST to. +Use the provider's current ownership challenge and signing specification. Keep +callback signing material out of prompts, descriptors, logs and task data. Enforce +the callback destination policy, including redirect and DNS/IP handling, so a +subscription cannot turn the control plane into an internal-network request proxy. +Apply bounded delivery timeouts/retries and record failures without logging secrets. +Where receiving signed callbacks, validate the expected signature, timestamp and +replay policy before changing state. Test the actual directions this provider uses; +do not invent a symmetric webhook API merely because the integration is two way. + +## Follow-up delivery must be coordinated with ordinary wake scheduling + +A normal comment can enter two paths: active-assignment mailbox delivery and the +ordinary scheduler. If both remain independent, a finished bot can receive a +second assignment for a comment it already handled. If the normal wake is simply +discarded when the mailbox entry is made, the comment can be lost at completion. + +The Dot fix retained the normal wake as deferred until the bot successfully read +that exact comment ID through `get_task_history`. Then the existing run consumed +it. If the assignment ended before the read, normal admission kept the unread +comment for another turn. An inbox reference or webhook acknowledgment did not +prove the comment was read. + +Test both sides of this race, including controller recovery. Keep explicit new +session requests, interaction replies, and self-authored comments under their +normal policy; don't blanket-suppress every wake for a connected bot. + +## Recovery needs identity evidence, not merely a reachable process + +Record and compare the run, session, assignment, binding generation, controller +generation, managed lease, process marker, and trustworthy process birth identity. +On Linux the process fingerprint can include boot identity and start information. +Read remote identity from the sandbox, not a same-numbered PID on the controller. + +Before advertising external work, require the evidence needed to recover it. +Recovery reads the checkpoint from the owning execution target and validates it +against saved authority. Missing/invalid state after dispatch fails closed for +reconciliation; a new process or conversation must not masquerade as a resume. + +Incoming operations need a durable draining/replay mechanism owned by the current +controller. An in-memory forwarding optimization alone can leave valid requests +stranded after restart. Fence old controllers and callbacks so a late result +cannot overwrite the new generation's journal. Preserve late effect evidence. + +A verified terminal/shutdown receipt can establish a clean exit when a remote +monitor lacks an exit code. An unexplained process exit still requires recovery. +Configure a bounded reconnect grace consistent with the deployment lifecycle; +Dot used five minutes after shorter restart gaps exposed failures. Do not copy +that number as a universal provider requirement. + +Terminal run reconciliation must clear/fence a still-active assignment, notify the +bot, and release resources. Otherwise a failed run can block every future task +even though its old tools are already denied. + +## Revocation and native powers are different boundaries + +Cancellation, pause, reassignment, membership loss, expired run authority, and +grant revocation must prevent subsequent Paperclip effects. Cached tool results +or UI binding references cannot restore revoked access. Old-grant revocation must +not revoke a replacement binding accidentally. + +Report “Paperclip authority revoked” separately from “external bot stopped.” A +fence acknowledgment proves receipt, not termination of all provider activity. +Likewise, sandboxing the Paperclip Runner does not confine the bot's native Slack +or provider computer. Those tools retain their own permissions. If strict total +capability isolation is required, use a suitably isolated bot/account and verify +the provider's actual controls; Paperclip cannot enforce it with prompt text. + +## Files and commands are a separate capability decision + +Do not add workspace tools merely to make an external agent look like a coding +CLI. A bot can research or write code on its own computer without Paperclip +workspace access. The first Dot Cloud release intentionally offered none. + +When the release includes workspace tools: + +- Resolve paths within the admitted workspace; reject traversal, absolute paths, + symlinks/escapes, and Paperclip instance state such as `.paperclip`. +- Check reads, writes, API downloads, uploads, and artifact publication. A generic + `call_api` path must not bypass attachment/workspace grants. +- Serialize optimistic hash checks with writes. Two concurrent writes must not + both commit against the same observed hash. +- A process working directory is not a sandbox. Use a proven OS/container boundary; + test outside-workspace access, environment secrets, detached descendants, timeout, + cancellation and cleanup. No unrestricted fallback when confinement is unavailable. +- In the historical Dot implementation, Linux used bubblewrap/private PID + namespaces. macOS command execution was disabled after detached descendants + escaped reliable supervision; file tools and artifacts remained available. +- For attachments, enforce current task/company, bounded pages and total bytes, + integrity checks, and live revocation before returning each page. Treat filenames + and contents as untrusted data. Test the generic API escape path too. + +## Paperclip implementation anchors + +Paths below existed in the qualified October 9 candidate. Locate their current +equivalents before editing; do not paste line numbers from historical notes. + +| Concern | Historical anchor | +| --- | --- | +| Bot broker and tool definitions | `server/src/services/dot-runner-broker.ts` | +| Prototype public MCP bridge | `server/src/services/public-mcp/dot-runner.ts` | +| Rust assignment backend | `packages/paperclip-runner/runner/crates/runner-core/src/dot_provider_backend.rs` | +| TypeScript Runner driver | `packages/paperclip-runner/src/drivers/dot/runnerd-dot-driver.ts` | +| Driver/backend selection | `packages/paperclip-runner/src/backends/native-backend-factory.ts` | +| Per-turn instructions | `packages/paperclip-runner/src/backends/runtime-context.ts` | +| Governed tool authority | `server/src/services/native-runtime/paperclip-runner-tool-authority.ts` | +| Admission, wake, terminal reconciliation | `server/src/services/heartbeat.ts`, `server/src/services/heartbeat/queue.ts`, `server/src/services/heartbeat/run-lifecycle.ts` | +| Real Rust/DB and managed qualification | `server/src/__tests__/dot-runner.test.ts` | + +Keep the standalone Runner package independent of server/DB implementation +imports. Extend the actual production backend and negotiated capability contract, +not just a convenient reference harness. diff --git a/.agents/skills/create-external-bot-integration/references/troubleshooting-and-deployment.md b/.agents/skills/create-external-bot-integration/references/troubleshooting-and-deployment.md new file mode 100644 index 00000000000..df851f386af --- /dev/null +++ b/.agents/skills/create-external-bot-integration/references/troubleshooting-and-deployment.md @@ -0,0 +1,215 @@ +# Troubleshooting, test environments, and Cloud delivery + +## Contents + +- [Start with the failing boundary](#start-with-the-failing-boundary) +- [Stable public ingress is part of the test fixture](#stable-public-ingress-is-part-of-the-test-fixture) +- [Concrete OAuth and routing checks for the Dot implementation](#concrete-oauth-and-routing-checks-for-the-dot-implementation) +- [Symptom → discriminator → next action](#symptom--discriminator--next-action) +- [Keep one reproducible environment manifest](#keep-one-reproducible-environment-manifest) +- [Coordinate all Cloud pieces](#coordinate-all-cloud-pieces) +- [Classify a failure before retrying](#classify-a-failure-before-retrying) +- [Close the environment deliberately](#close-the-environment-deliberately) + +## Start with the failing boundary + +Do not use “OAuth is broken” for every setup failure. Trace these independent paths: + +```text +A. Provider backend -> public origin -> discovery, MCP, token exchange +B. Provider browser -> authorization page -> preview/consent -> provider callback +C. Agent runtime -> loaded plugin actions -> authenticated read/tool calls +D. Paperclip event worker -> verified callback -> bot wake -> mailbox/challenge +E. Paperclip scheduler -> managed lease -> Runner -> tools -> finalizer -> cleanup +``` + +For each, collect a timestamp, exact failing operation, status/visible error, +correlated access log, and which party observed it. The developer's browser, the +provider's browser, and the provider's backend can have different reachability. +An absent request in complete ingress logs means that attempt did not reach the +app; it does not identify the upstream blocking rule. + +## Stable public ingress is part of the test fixture + +Prefer one stable HTTPS origin on port 443 reachable by both provider clients. +Keep issuer/resource, token exchange, authorization redirect, and callback +configuration consistent. Do not rotate tunnel hostnames as the first response to +every failure: each new URL can require a different plugin, fresh registration, +new pairing prompt, and subscription cleanup. + +Self-hosted testing may need a tunnel or authorized public reverse proxy. Dot +eventually used a stable public proxy forwarding to the test machine. That is an +observed useful arrangement, not a requirement to provision new infrastructure +for every integration. Use the user's supported deployment/network options. + +Tailscale-specific diagnostic: local MagicDNS may resolve a hostname to a private +tailnet address while outside clients use Funnel. Test public ingress independently; +local curl success does not establish public access. Tailscale Serve and Funnel +have different reachability. Never infer a provider port ban from one timeout. + +`ERR_BLOCKED_BY_CLIENT` alone does not identify Cloudflare or a server bot filter. +Use server logs and the actual consent page, not only `/api/health`: Dot once +blocked the health navigation while the required sign-in page loaded. Do not +bypass browser security warnings or a provider's access controls. Use supported +configuration or an authorized reachable endpoint and preserve the exact uncertainty. + +## Concrete OAuth and routing checks for the Dot implementation + +These routes describe the October 9 candidate; discover current equivalents. + +| Surface | Historical expectation | +| --- | --- | +| `/mcp/runner` unauthenticated | `401` and `WWW-Authenticate` with resource metadata; not an HTML sign-in page. | +| `/.well-known/oauth-protected-resource/mcp/runner` | Public JSON for the exact resource and authorization server. | +| `/.well-known/oauth-authorization-server/mcp/runner/oauth` | Public JSON for issuer and endpoints. | +| `/mcp/runner/oauth` | Dedicated agent-resource issuer; personal MCP issuer remained separate. | +| `/dot-connect/:requestId` | Public pairing page, scoped by request and one-use pairing capability. | +| `/api/dot-mcp/requests/:requestId` and exact pairing operations | Publicly routable aliases with request/code checks; reject personal assistant request IDs. | +| Normal board, connection management, ordinary personal consent | Retain normal session/role requirements. | + +For read-only health checks, substitute the configured origin; do not send tokens: + +```sh +curl --max-time 15 -sS -D - -o /tmp/dot-resource-metadata.json \ + 'https://YOUR_CONFIGURED_ORIGIN/.well-known/oauth-protected-resource/mcp/runner' +curl --max-time 15 -sS -D - -o /tmp/dot-auth-metadata.json \ + 'https://YOUR_CONFIGURED_ORIGIN/.well-known/oauth-authorization-server/mcp/runner/oauth' +``` + +Inspect JSON and redirect behavior, then exercise the actual provider discovery. +Do not interpret these curl checks as provider qualification. Keep browser cookies +and tokens out of pasted logs; use the existing authenticated test client when a +protected operation must be inspected. + +ChatGPT's historical client metadata preferred `private_key_jwt` while also +advertising `none`. Paperclip originally rejected it instead of negotiating its +supported public-client method with PKCE. Inspect the complete advertised methods; +do not disable authentication or exact redirect/resource validation to work around +a preference mismatch. Keep S256 PKCE and current registration checks. + +An optional `PAPERCLIP_MCP_AUTHORIZATION_ORIGIN` supported a different authorized +browser ingress to the same instance, while `PAPERCLIP_PUBLIC_URL` retained the +MCP resource/issuer/token origin. This was a workaround for different client +reachability, not an OAuth requirement. Use only explicitly configured trusted +origins and verify callback/CSRF/issuer behavior. Prefer a single working origin. + +## Symptom → discriminator → next action + +| Symptom | Evidence to inspect | Action and proof | +| --- | --- | --- | +| OAuth discovery times out | Public metadata vs actual provider access logs | Fix the failing ingress/client path; do not keep editing valid metadata when no request arrives. | +| Unknown client or redirect URI after restart | Persisted registration vs provider's cached client | Restore the intended persistent fixture or re-register through supported setup. Do not reset DB repeatedly. | +| Invalid request ID | Actual ID representation | Treat protocol IDs as opaque unless contract says otherwise; the lab incorrectly required a UUID. | +| Grant exists but provider is not connected | Complete same browser flow through callback/token exchange | A server grant row alone is not completed OAuth. Avoid mixing separate callback sessions. | +| Tools listed in settings, absent to bot | Actual tool invocation in active conversation | Supported refresh/attach/restart, bounded once per diagnosis; preserve endpoint and mark intervention. | +| Subscription verified but no bot action | Delivery record, mailbox, challenge/acceptance receipt | Separate callback receipt from model execution. Report latency; inspect actionable work before resending. | +| Ready connection, task admission says unpaired | Saved agent binding/config revision | Fix atomic pairing persistence; do not require another manual form save. | +| Agent created paused before pairing | Lifecycle preparing/verifying vs manual pause | Use lifecycle-specific setup eligibility; preserve actual pause/approval gates. | +| Anonymous consent spins/reloads | Background session probes returning expected 401 | Public consent must tolerate them without global reload; protected board routes retain session recovery. | +| Ordinary assignment works, idle work cannot acquire sandbox | Service construction / plugin context | Use app-configured admission service and test both entry points. | +| Remote Runner connects to localhost | Launched transport endpoint vs private managed ingress | Use the authenticated remote transport supplied by the execution target. | +| Image lacks provider support | Actual packaged Runner digest/capabilities | Fail before dispatch; publish compatible artifact and test installed path. | +| Recovery finds sandbox but no process identity | Marker, fingerprint and original checkpoint | Capture identity before dispatch; reconcile missing evidence rather than launch replacement work. | +| Remote PID has implausible start time | Whether controller inspected its own PID table | Obtain birth identity from the remote sandbox, not a colliding local PID. | +| Finished task starts again after comment | Mailbox + scheduler wake receipt | Consume only when exact comment read succeeds; test unread-at-finish and read-before-finish. | +| Missing file leaves run waiting forever | Pending tool receipt and error dispatch | Settle definite read failures; preserve unknown outcome only for genuinely ambiguous effects. | +| Terminal run blocks all new tasks | Assignment still active despite terminal run | Reconcile/fence it and release resources without claiming external stop. | +| Cancelled job has creating/pending-cleanup sandbox | Provider allocation and deletion receipts | Track until destroyed or explicitly pending with owner; retry only per provider semantics. | +| New deployment vanishes mid-test | Fleet auto-rollout vs direct candidate | Use supported test-tenant pinning with authorization; record current serving revision. | + +## Keep one reproducible environment manifest + +Before a test drive or deployment, resolve current worktree/artifacts and reuse a +suitable isolated checkout. Do not reset the user's primary checkout. Create a new +managed worktree when appropriate, preserving unrelated work. Record: + +```text +Repository / worktree / branch / app SHA: +Runner artifact platform / digest / advertised capabilities: +Ingress repository / SHA / relevant routing version: +App image / immutable digest: +Local host / port / data directory / process ownership: +Public origin / authorization origin / forwarding target: +Tenant / company / agent identifiers (private evidence only): +Sign-in entry and credential reference (no secret values): +Grant/binding status / subscription / pending code expiry (no code value): +Current case / run / assignment / lease: +Owner of each server, proxy, build, sandbox, and cleanup: +Environment retention agreement / next gate: +``` + +Use synthetic fixtures, not a production database clone containing private company +data or signing keys. Verify launcher arguments, selected instance, port mapping, +and data persistence after restart. A label such as `default` is not proof of +process ownership. Never kill unrelated databases or clear shared IPC globally. +If resource pressure blocks the fixture, inspect owned processes or use the +approved remote test path; preserve unrelated sessions. + +Verify sign-in is usable before handing over a URL. Deliver access/resume details +through an appropriate private path. If the user is still testing or the bot is +loading tools, keep the endpoint alive under the agreed bounds. Stopping it made +the first “tools unavailable” diagnosis premature. Announce an intentional expiry +and explain how to resume; do not assume a stopped shell process remains a service. + +## Coordinate all Cloud pieces + +Cloud qualification requires the tenant app, public ingress, managed provider +plugin/configuration, and packaged Runner artifact. The UI flag alone is not +enablement. Inspect guards in UI, scheduler, backend, remote launcher, and Cloud +front door. Use scoped route exceptions that retain endpoint authentication rather +than removing browser-session guards wholesale. + +Pairing and MCP must remain reachable without a managed job. If a sleeping tenant +returns a waking-up page, wait for the actual lifecycle transition, then test the +correct metadata response. Delay creating expiring pairing codes until the tenant +image is deployed and serving. A 3.4 GB image transfer consumed meaningful setup +time during Dot qualification; do not burn the code's lifetime watching a build. + +Record whether a test overrides the sandbox binary with a locally built artifact. +That proves the code can run, not that the published default image is correct. +Match platform and provider capability; a Mac controller's current binary cannot +stand in for a Linux sandbox artifact. Digest/capability checks should fail clearly +before an assignment is offered. + +For the historical private `paperclip-cloud` repository, the supported targeted +deployment CLI was `scripts/deploy-stack.mjs --plain`, with +`--resume ` for the same operation. Inspect current help/runbook before +using it. Do not invent or replay old tenant IDs, deployment requests, or secrets. +Preserve one request through prepare/build/check/deploy/verify; duplicated requests +make provenance and recovery harder. No fleet-default change is implied by a +single test-tenant deployment. + +## Classify a failure before retrying + +- **Product:** deterministic missing context, bad state transition, wrong endpoint, + lost receipt. Reproduce, fix, and rerun the affected contract. +- **Provider:** tool loading, wake delay, refusal, unsupported control. Capture the + actual provider result and test a supported recovery; don't manufacture success. +- **Infrastructure:** capacity, registry 429/504, image transfer, npm resolver, + provider allocation. Verify whether work/effects started; use bounded recovery. +- **Evidence/test:** controller didn't restart, stale build, nonce visible in prompt, + wrong image, scripted client presented as real. Correct the experiment. + +For an unchanged transient build failure, one targeted infrastructure retry is a +reasonable initial bound. If it fails again, report the precise remaining gate +or investigate the new cause; do not loop reruns or change registry/security +configuration without a task-grounded reason. Resume the existing deployment. +If code changes, re-evaluate which earlier evidence applies to the new candidate. + +Keep broad test runs tied to a frozen revision. Stop superseded owned runs when +safe, retain their results, and run the affected suites plus required final gates. +Do not claim a full local suite passed because failing tests passed individually; +state focused results and final CI separately. + +## Close the environment deliberately + +After the final task, inspect leases and actual provider resources. Expired lease +status alone is weaker than a successful sandbox deletion receipt. Preserve a +requested review instance with access and resumption information; release owned +disposable compute. For an authorized reset, account for provider account link, +Paperclip binding/grant, event subscription/automation, and cached configuration. +Disconnecting one layer can leave an enabled provider automation behind. + +Report qualified, review-ready, merged, deployed, and canary-verified separately. +Never say an instance received a fix until its serving revision and relevant +ordinary-use behavior establish it. diff --git a/.agents/skills/create-issue-interaction-ui/SKILL.md b/.agents/skills/create-issue-interaction-ui/SKILL.md index 9cebfacc95b..984a81ad612 100644 --- a/.agents/skills/create-issue-interaction-ui/SKILL.md +++ b/.agents/skills/create-issue-interaction-ui/SKILL.md @@ -163,7 +163,7 @@ Touch: resolved, stale, disabled/submitting, and validation-error states. - `ui/src/fixtures/issueThreadInteractionFixtures.ts` — seed at least one pending and one resolved fixture for the new kind. -- `ui/src/stories/issue-thread-interactions.stories.tsx` — Storybook entries +- `ui/storybook/stories/questions-and-approvals/issue-thread-interactions.stories.tsx` — Storybook entries for the key states. - `ui/src/pages/IssueDetail.tsx` — extend the per-kind branches the card is rendered from (callback wiring, response submission). diff --git a/.agents/skills/maintain-storybook/SKILL.md b/.agents/skills/maintain-storybook/SKILL.md new file mode 100644 index 00000000000..66973bce51c --- /dev/null +++ b/.agents/skills/maintain-storybook/SKILL.md @@ -0,0 +1,98 @@ +--- +name: maintain-storybook +description: Keep Paperclip stories and their downstream catalog aligned with the feature map and production UI. Use to reconcile story drift, classify prototypes, update fixtures, or maintain feature coverage. +--- + +# Maintain Storybook + +Read the [Storybook catalog contract](../../../maps/storybook-map/README.md), +the relevant [feature recipes](../../../maps/feature-map/README.md), and +[DESIGN.md](../../../DESIGN.md). The feature map is an upstream input. This skill +owns stories, their fixtures/support, and `maps/storybook-map/`; it does not edit +the feature or agent-access maps. Report upstream drift to +`maintain-verification-skill` with source evidence. + +## Reconcile the requested scope + +For a feature/change, follow its capability IDs to catalog entries and shared +dependencies. For a full inventory, account for every configured story file, +including historical examples. Start with `node scripts/storybook-catalog.mjs`. +Record the source revision and whether the pass covers source, rendering, +interactions, or visual comparisons. Inventory completeness is not behavioral +coverage. Do not expand a targeted maintenance request into an all-product audit. + +Compare each affected story's rendered subject, props, fixture responses, actions, +and assertions with the production implementation and recipe. Follow imported +wrappers: a production Button inside a locally recreated page does not make that +page a production-component story. A real production page using mocked APIs is +valid isolation; record component provenance and data simulation separately. +Inspect mock methods/paths, payload shapes, errors, identity, gates, and reset +behavior. An API method returning success without the relevant state transition +can hide drift. Do not replace errors with generic successful mocks to pass. + +Prefer mounting the shipped subject and mocking its external boundary. Retain +local layout wrappers and fixture data when they serve that isolation. Replace a +duplicate product implementation only when the current component expresses the +same scenario without losing the original purpose. Production bugs and larger +redesigns are separate findings, not reasons to rewrite expected behavior. + +Classify current examples, proposals, historical explorations, and unresolved +cases explicitly. Use `unreviewed` when evidence does not establish parity. +Retain useful superseded work with its rationale and replacement reference; +do not delete a story merely because it is old, mocked, or duplicated by name. +Record mixed suites honestly and split them only when that improves review. + +## Preserve identity and keep the index useful + +Assign existing stable `area/capability` references, with one primary reference +and optional related references. Foundations and unmapped proposals need explicit +reasons rather than invented capabilities. Cross-cutting stories have one source +and several references. Do not count historical/proposed stories as current +feature coverage. + +For shared foundations that demonstrate a mapped capability, keep their existing +folder/sidebar with `home: "foundations"` and an exception reason, then add related +capability references. Inspect the named exports before linking them. Notes must +name the demonstrated states and remaining limits; an association does not certify +the whole capability. If a story reveals a shipped capability absent upstream, +reconcile its feature recipe and access companion through map maintenance first. + +Preserve story exports and IDs. Before moves/title changes, capture the existing +built `index.json`; use explicit meta IDs when needed and compare IDs afterward. +Place stories under `ui/storybook/stories//`. Use the feature +map's section and feature names as their sidebar prefix; keep useful scenario +groupings beneath that prefix. Put shared fixture modules in `ui/storybook/support/`. +Keep the catalog's `stableId` equal to the explicit CSF meta ID even when the title +changes. Unmapped component examples belong under Foundations; the catalog guide +belongs under Overview. The checker verifies these conventions. + +Keep the main guide and every feature Overview congruent with the upstream index: +same section/feature names and order, all capabilities in recipe order, related +suite links across primary homes, and explicit missing examples. Regenerate +`ui/storybook/generated/` and the preview config's inline `storySort.order` through +the catalog command; never hand-edit generated pages or ordering +or add generated guides to the authored catalog. Generated guides are not example +coverage. Preserve their `feature-map---overview` IDs. When an upstream area +is removed or renamed, inspect unexpected generated files and resolve them +explicitly; the generator will not silently delete them. + +Keep review links and snapshot names working. Prefer classification/index fixes +before bulk directory moves. Record every directly rendered production subject in +the catalog's review sources, including relative imports and primitives when they +are the demonstrated subject. Do not cap the number of source references. +Update review fingerprints only after inspecting the changed source; do not refresh hashes merely +to silence drift checks. + +Update the catalog and run `node scripts/storybook-catalog.mjs --write` to refresh +its generated indexes and feature pages, then run the checker without flags and +`node --test scripts/storybook-catalog.test.mjs scripts/storybook-feature-index.test.mjs`. +The script validates +inventory/references/review fingerprints, not semantic parity or runtime health. +Use `review-storybook` for affected rendering and interactions. Reuse the existing +Storybook/Playwright tools; add no separate launcher, runtime, or baseline system. +Do not accept new screenshot baselines as part of a catalog cleanup. + +Report what was corrected, retained, and still needs review, naming exact files +and capabilities. Keep source-only results separate from executed results and +name blocked checks. A full pass must not hide unreviewed stories or claim that +fixture interactions establish production API, permission, or provider behavior. diff --git a/.agents/skills/maintain-storybook/agents/openai.yaml b/.agents/skills/maintain-storybook/agents/openai.yaml new file mode 100644 index 00000000000..79bc58f9192 --- /dev/null +++ b/.agents/skills/maintain-storybook/agents/openai.yaml @@ -0,0 +1,3 @@ +interface: + display_name: "Maintain Storybook" + short_description: "Align stories with features and production UI" diff --git a/.agents/skills/review-storybook/SKILL.md b/.agents/skills/review-storybook/SKILL.md new file mode 100644 index 00000000000..216701e4924 --- /dev/null +++ b/.agents/skills/review-storybook/SKILL.md @@ -0,0 +1,54 @@ +--- +name: review-storybook +description: Review Paperclip stories for a feature or change using the Storybook catalog, production source, and existing browser tests. Report component, mock, interaction, and visual evidence separately. +--- + +# Review Storybook + +Read the [catalog contract](../../../maps/storybook-map/README.md) and find the +requested capability in its generated index. Check the relevant feature recipe, +production subject, and story before driving it. Catalog status is prior review +context, not proof of the current revision. This skill consumes the maps/catalog; +corrections belong to `maintain-storybook` or `maintain-verification-skill`. + +Choose discriminating states for the requested change: the normal action and +result, relevant failure/retry, gate/permission presentation, and mobile/theme +variants where affected. Record chosen files/exports and omitted scope. For a +full source review, account for every file; do not describe sampling as full +render or interaction coverage. + +When reviewing navigation/index changes, compare the guide and sidebar with the +feature-map README's full ordered index. Open a feature without examples and a +feature whose related suite lives elsewhere; check capability order, gap messages, +classifications, recipe links, and actual suite navigation. Compare built entry IDs +before/after so existing links survive. Generated Overview pages do not establish +new example coverage. + +Use [the existing Storybook commands](../../../doc/DEVELOPING.md#storybook): +`pnpm build-storybook`, or an owned Storybook dev server on an available local +port. Use the built `ui/storybook-static/index.json` for actual story IDs and +links. Do not trust an unrelated server on the default port. Keep the process +handle and stop only the server this review started, retaining useful evidence. + +Inspect the real rendered subject, including errors and browser console output. +For interactive stories, wait for their `play` assertions to complete and inspect +the observable result. A screenshot alone does not prove `play` passed. Verify +fixture isolation by revisiting/resetting relevant stories when they mutate +fetch handlers, storage, query caches, time, or module state. Avoid real provider +actions; explicitly document any live dependency required by a story. + +The existing visual suite is +`tests/storybook-visual/storybook-visual.spec.ts`. It uses external baselines and +a pinned Linux environment. Inspect `baseline-manifest.json` before claiming +visual regression proof. An unpublished manifest blocks comparisons; a macOS +capture is useful inspection, not the Linux baseline. Do not update/publish +baselines just to make a review pass. Use existing focused browser suites where +they cover the requested scenario, and the production verification skill when +the question requires backend persistence, real authorization, or provider proof. + +Report each selected case as passed, failed, blocked, or not run, naming revision, +story ID, environment, production versus prototype subject, simulated boundaries, +action/assertion and observed result. Separate render, interaction, visual +comparison, and production acceptance evidence. Report proven drift with exact +source references and a proposed correction; do not lower expectations, remove +failing cases, or silently bless historical behavior as current. diff --git a/.agents/skills/review-storybook/agents/openai.yaml b/.agents/skills/review-storybook/agents/openai.yaml new file mode 100644 index 00000000000..16a92f0250b --- /dev/null +++ b/.agents/skills/review-storybook/agents/openai.yaml @@ -0,0 +1,3 @@ +interface: + display_name: "Review Storybook" + short_description: "Review story rendering, interactions, and drift" diff --git a/.agents/skills/verify-paperclip/SKILL.md b/.agents/skills/verify-paperclip/SKILL.md index b16467140a0..9e10d7c2f90 100644 --- a/.agents/skills/verify-paperclip/SKILL.md +++ b/.agents/skills/verify-paperclip/SKILL.md @@ -1,6 +1,6 @@ --- name: verify-paperclip -description: Launch an isolated Paperclip instance, drive its real web UI or CLI using the existing feature-map recipes, capture evidence, and clean up owned processes. Use to verify user-facing Paperclip behavior. +description: Verify Paperclip from a source checkout, published npm install, or existing staging or production stack. Select the environment, data state, version, and platform; drive real UI or CLI journeys and capture evidence. Route deployment to the private Cloud skills. --- # Verify Paperclip @@ -15,7 +15,66 @@ recipe or access drift with evidence; map updates belong to [maintain-verification-skill](../maintain-verification-skill/SKILL.md). Read [development instructions](../../../doc/DEVELOPING.md) for current commands. -## Launch +## Select the verification target + +State what is being tested before starting. Use the requested target; for ordinary +feature development, default to the current checkout on the current host with +isolated data. Expand into cross-platform or release qualification only when +requested or needed to reproduce the problem. + +Record these independent dimensions together: + +- **Artifact:** source checkout (path, commit, dirty state), published npm package + (requested channel and resolved exact version), or deployed image/release. +- **Environment:** local, staging, or production; target URL and intended company + or workspace; board/user/agent identity and authentication mode. +- **Starting data:** fresh and unonboarded, freshly seeded, cloned, or reused; + embedded versus external PostgreSQL; configuration/data location or a redacted + database identifier. Never record credentials or connection strings. +- **Platform:** server OS/architecture, Node/npm versions, and browser/client OS + when relevant. A Mac browser against a Linux server is Linux server evidence; + WSL is Linux evidence, not native Windows evidence. +- **Journey:** first installation/onboarding, an existing-user feature, upgrade, + or a live agent run; expected outcome and real versus simulated dependencies. + +| Target | Use it to prove | Procedure | +| --- | --- | --- | +| Checkout dev server, existing or cloned data | Source behavior against existing state | [Checkout modes](references/verification-targets.md#checkout-and-data-modes) | +| Checkout with fresh empty data | First-run onboarding and subsequent features | [Launch](#launch-an-empty-checkout-instance) below | +| Checkout test drive, fresh seed or reused data | Features with a configured company/CEO | [Checkout modes](references/verification-targets.md#checkout-and-data-modes) | +| Published npm canary | End-user installation of a specific prerelease | [npm verification](references/npm-install.md) | +| Published npm stable (`latest`) | End-user installation of the released package | [npm verification](references/npm-install.md) | +| Existing staging stack | Deployed build, hosted auth, flags, integrations | [Hosted verification](references/verification-targets.md#staging-and-production) | +| Existing production stack | Current production journey within the authorized scope | [Hosted verification](references/verification-targets.md#staging-and-production) | + +Fresh data does not imply a fresh install. A seeded test drive does not prove +onboarding. A source build does not prove npm packaging. A passing staging check +does not prove production, and a production Cloud stack does not prove the stable +npm install. Report only the combinations actually exercised. + +## Deployment handoff + +When a requested revision must first be built or deployed, use the skills in the +private `paperclip-cloud` repo: + +- `deploy-paperclip-stack` at `.agents/skills/deploy-paperclip-stack/SKILL.md` + for a tenant workspace or an explicitly scoped fleet rollout. +- `deploy-cloud-harness` at `.agents/skills/deploy-cloud-harness/SKILL.md` + for the Cloud control plane itself. + +Locate the authorized Cloud checkout or installed skill by name; do not assume a +sibling directory exists. Those skills own build preparation, deployment, +migrations, and rollout recovery. Return here with environment, target, resolved +source SHA, release/image identity, deployment result, and evidence link. If +private access is unavailable, report that prerequisite as blocked and continue +checks possible on the supplied target without inventing deploy commands. + +A verification request alone does not authorize deployment, npm publication, +fleet-default changes, or a production rollout. Preserve explicit deployment +instructions already given. Publishing npm packages belongs to the existing +[release skill](../release/SKILL.md); installing one does not require publishing it. + +## Launch an empty checkout instance Run from the repository root with Node 24.11+, Python 3, `lsof`, and dependencies installed with `pnpm install --frozen-lockfile`. Browser driving uses the existing @@ -53,7 +112,11 @@ For Paperclip-managed reusable services, follow instead of launching a second server. Its cloned-data/login requirements differ from this empty local instance. -## Doctor +## Doctor (owned checkout helper only) + +The doctor/stop commands require the real `launch.json` created by `launch.py`. +Do not fabricate one for dev servers, npm installs, test drives, or hosted stacks. +Use their own readiness and lifecycle procedures instead. After startup, and again after any surprising behavior, run: @@ -98,14 +161,16 @@ guessing resource URLs. The unprefixed `/onboarding` route starts naming; use th company-prefixed onboarding route to resume its agent setup. Check relevant experimental gates and actor permissions. -For CLI checks, discover exact flags with +For source-checkout CLI checks, discover exact flags with `node cli/node_modules/tsx/dist/cli.mjs cli/src/index.ts --help`, then -select the disposable data directory/API/company explicitly. API reads can +select the disposable data directory/API/company explicitly. For published npm +checks, use the installed version’s CLI and help instead. API reads can corroborate UI writes; API-created state does not prove a creation UI. ## Evidence -Record feature file, sub-feature and entry-point ID, commit/dirty state, owned URL, +Include the target dimensions above, then record feature file, sub-feature and +entry-point ID, commit/dirty state, target URL, company/user, flags, runtime/harness, live versus simulated dependencies, command, exit code, expected and observed behavior, and evidence links. Report each entry as passed, failed, blocked with its prerequisite, or not run. @@ -126,7 +191,10 @@ documentation coverage rather than live test health. ## Cleanup -Close the browser context you created (the smoke does this in `finally`). Stop the owned runtime with: +Close the browser context you created (the smoke does this in `finally`). Leave +shared dev, staging, and production services running. The remaining cleanup steps +in this section apply only to `launch.py` instances; use the selected target’s +procedure for other launchers. Stop the helper-owned runtime with: ```sh python3 .agents/skills/verify-paperclip/scripts/stop.py \ diff --git a/.agents/skills/verify-paperclip/references/npm-install.md b/.agents/skills/verify-paperclip/references/npm-install.md new file mode 100644 index 00000000000..fe4491948d7 --- /dev/null +++ b/.agents/skills/verify-paperclip/references/npm-install.md @@ -0,0 +1,78 @@ +# Verify the published npm experience + +Use this route for “can a user install and run this package?” Source startup, +workspace links, `pnpm build`, and local tarballs do not answer that question. + +## Resolve the exact artifact + +Read [.github/workflows/release.yml](../../../../.github/workflows/release.yml) +for publication behavior. Canary publication follows verified pushes to `master`; +an arbitrary feature-branch SHA need not have a published canary. Publication can +fail or still be running, so prove registry availability before claiming coverage. + +Resolve the requested channel once against the public registry: + +```sh +npm view paperclipai@canary version --registry=https://registry.npmjs.org +npm view paperclipai@latest version --registry=https://registry.npmjs.org +``` + +Use the one requested by the task: `canary` for prerelease verification, `latest` +for stable production npm verification, or an explicit version. Record resolved +version, registry, dist integrity, and source provenance from release metadata, +workflow, and tags. Pin that version for all later commands. If testing a SHA, +confirm the package maps to it; never substitute the current canary silently. +Missing publication is a blocked check, not a reason to publish during verification. + +## Install like a new user + +Use a disposable directory outside the checkout and fresh npm cache, +instance/config, and user-config paths. Use an isolated OS user/container when +proving absence of all pre-existing user state. Inspect inherited routing and +registry overrides without exposing secrets. Avoid workspace dependencies, local +`file:` packages, preinstalled Paperclip binaries, and cached `npx` installs. + +Run the documented end-user entry point with the pinned package, for example +`npx --yes --registry=https://registry.npmjs.org paperclipai@`. +Read that version's `--help` before selecting `onboard`, `install`, or service +flags. For a foreground disposable check use `onboard --yes --bind loopback +--no-install-service --run --data-dir ` when supported by that +version; keep ordinary interactive onboarding when that is the journey requested. +A managed CLI/service installation is separate from an ephemeral `npx` launch; +test it explicitly if requested. + +Prove each relevant stage separately: registry download/install, CLI execution, +server readiness, visible onboarding, saved company/setup state, restart/reload, +and a first agent task when authorized. Successful `--version` alone does not +prove a usable install. For upgrades, first create state with the previous exact +version, then install the target against that same disposable data and verify +state survives; report this separately from a fresh install. + +## Existing automation and its limits + +The repo has a published-package onboarding suite: + +```sh +PAPERCLIPAI_VERSION= pnpm test:canary-onboarding-smoke +``` + +Read [its configuration](../../../../tests/canary-onboarding/playwright.config.ts) +and [launcher](../../../../tests/canary-onboarding/start-published-canary.mjs) +before running. It installs the published package into isolated temporary state, +uses a fresh npm cache and loopback server, and retains failure diagnostics. +It uses checkout Playwright tooling to drive a published runtime; record both +revisions. Its name and automated release wiring target canaries. For stable +verification, inspect compatibility with the selected version or use the manual +pinned-package path above; do not relabel a canary result as stable proof. + +The Docker release-smoke suite is separate image evidence. Likewise, +`scripts/clean-install-npm.sh` defaults to source, and its published mode uses a +moving version and source-driven uninstall; it does not by itself prove the +whole lifecycle of a specific published version. + +Capture OS/architecture, Node/npm versions, exact commands and exit codes, +installed version, serving revision when available, initial data state, screenshots, +logs, and observed outcomes. Redact credentials. Stop owned foreground processes +or uninstall an owned test service with the tested version's supported command; +retain evidence outside disposable data. Never run checkout ownership helpers +with invented metadata to stop a package process. diff --git a/.agents/skills/verify-paperclip/references/verification-targets.md b/.agents/skills/verify-paperclip/references/verification-targets.md new file mode 100644 index 00000000000..837f3803a11 --- /dev/null +++ b/.agents/skills/verify-paperclip/references/verification-targets.md @@ -0,0 +1,87 @@ +# Checkout, data, and hosted targets + +## Checkout and data modes + +Read [DEVELOPING.md](../../../../doc/DEVELOPING.md) and +[DATABASE.md](../../../../doc/DATABASE.md) for the current startup contract. + +- **Existing dev server:** use `pnpm dev` (watch) or `pnpm dev:once` from the + intended checkout. Inspect its instance routing and data configuration first; + do not assume the default port belongs to this checkout. Record the actual + served commit and dirty state. Reuse a matching running server when appropriate. +- **Fresh empty instance:** use the skill's `launch.py` helper with a new data + directory. Complete the UI or CLI onboarding journey under test. This is the + credential-free route; a live model run needs separate provider setup. +- **Seeded test drive:** run the documented CLI `test-drive --no-browser` with + an authorized environment-backed provider credential. It creates a temporary + embedded instance, completes setup, and seeds Test Company and a CEO. It does + not execute the first task. Verify that separately when required. +- **Reused test drive:** pass `--data-dir `. If it already + contains a company, bootstrap flags do not replace companies, agents, or + secrets. Record existing state; do not label the result fresh onboarding. + Test drive rejects external PostgreSQL configuration and database URL overrides. +- **Managed or cloned worktree:** use + [dev-workspace run/verify/fix](../../paperclip-dev-workspace-run-verify-fix/SKILL.md). + Verify seed completion, instance routing, and login handoff before driving it. + Cloned data is existing-user evidence even when the checkout is new. + +`--data-dir` alone is not proof of a fresh database: check effective config and +whether `DATABASE_URL` routes to an external database. Do not print its value. +For ordinary dev, inspect both environment overrides and the active database +config: with `DATABASE_URL` unset, `database.mode: postgres` and a saved +`connectionString` still select external PostgreSQL. Embedded PostgreSQL is the +default only when neither an environment override nor the active config selects +an external database; its instance directory may already hold data. Do not reset +shared data to make a test pass. Choose separate disposable data for first-run +checks and verify the effective backend before calling it isolated. + +Record runtime PID, URL, port owner, checkout, and data location for a process you +start. Check `/api/health`, expected served revision, bootstrap/auth readiness, +and the actual page. Capture starting state and verify persistence after reload. +Use that launcher's documented stop procedure for an owned process; leave a +server you borrowed running. + +## Platform scope + +Ordinary development covers the current host unless the request or failure calls +for more. For a platform issue or release matrix, list required combinations: +macOS, Linux, native Windows, architecture, runtime, and install path. Mark each +combination passed, failed, blocked, or not run separately. + +The local helper needs POSIX tools such as `lsof`; do not assume it qualifies +native Windows. Use the supported CLI and native shell for that platform, with +isolated instance/config/cache paths and platform-appropriate process cleanup. +A container proves its guest OS and selected architecture, not the host's native +installer, service manager, file permissions, or path behavior. Keep browser and +server platforms distinct when testing a remote Linux box from a Mac. + +## Staging and production + +Use the supplied tenant URL and intended identity. Determine whether the target +is the Paperclip tenant application or the Cloud control plane. Confirm the +environment from the operator's deployment record and authenticated routing; +a page title or local Git checkout is not evidence of the serving revision. + +If a build/deploy is needed, follow the private skill handoff in `SKILL.md` first. +Record the exact serving SHA and release/image identity after deployment, not +just the requested branch, CI result, or fleet default. Recheck after a long run: +a staging auto-roll or another deployment can change the serving version. Mark +unknown or changing provenance as inconclusive for a claim about a specific SHA. + +For staging, use a designated test workspace and its real hosted sign-in/entry +flow. Record fresh versus existing workspace state, experimental flags, relevant +provider connections, and whether dependencies are live. Check tenant readiness +and then the feature journey, including persistence or continuation. A control +plane `/healthz` response or tenant HTML 200 is not feature acceptance. + +For production, keep verification within the requested stack, identity, and +journey. Prefer observation of existing state unless writes are authorized; use +an agreed test workspace for mutating checks. Do not run the empty-instance +onboarding smoke, reset data, change flags, or trigger paid agent work merely to +make a production check pass. If a necessary write is outside the request, +report that missing authorization and complete the read-only checks. + +Never use local ownership helpers against a remote server. Close only browser +sessions you created and clean up only test records whose removal is authorized. +Keep shared services running. Report production Cloud verification separately +from stable npm verification; the shipped artifacts and configuration differ. diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1fb71f5dfe0..8687af5aa1b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -10,6 +10,10 @@ doc/PUBLISHING.md @cryppadotta @devinfoley @nickyleach @forgottendev doc/RELEASE-AUTOMATION-SETUP.md @cryppadotta @devinfoley @nickyleach @forgottendev skills/** @cryppadotta @devinfoley @nickyleach @forgottendev +# Review configuration and repository rules +/.greptile/ @cryppadotta @devinfoley @nickyleach @forgottendev +/rules/ @cryppadotta @devinfoley @nickyleach @forgottendev + # Package files — dependency changes require review # package.json matches recursively at all depths (covers root + all workspaces) package.json @cryppadotta @devinfoley @nickyleach @forgottendev diff --git a/.github/scripts/tests/ci-routing.test.mjs b/.github/scripts/tests/ci-routing.test.mjs new file mode 100644 index 00000000000..a9703feb747 --- /dev/null +++ b/.github/scripts/tests/ci-routing.test.mjs @@ -0,0 +1,194 @@ +import assert from "node:assert/strict"; +import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { spawnSync } from "node:child_process"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import test from "node:test"; +import vm from "node:vm"; + +const workflow = readFileSync(new URL("../../workflows/ci-routing.yml", import.meta.url), "utf8"); +const jobs = workflow.split(/^jobs:\n/m)[1]; + +test("routing uses reserved capacity and runs no PR code or actions", () => { + assert.deepEqual([...jobs.matchAll(/^ {2}([a-z_]+):$/gm)].map((match) => match[1]), ["gate"]); + const placement = jobs.match(/^ {4}runs-on: (.+)$/m)?.[1]; + assert.ok(placement, "routing placement must be defined"); + assert.doesNotMatch(placement, /fromJSON/); + assert.ok(placement.includes("vars.AWS_CI_TRUSTED_USER_IDS == vars.AWS_CI_ROUTING_USER_IDS_SOURCE")); + assert.ok(placement.includes("github.event.pull_request.user.id")); + assert.ok(placement.includes("github.event.sender.id")); + assert.ok(placement.endsWith("&& 'runs-on/fleet=paperclip-ci-routing-x64/env=public-ci' || 'ubuntu-latest' }}")); + assert.doesNotMatch(jobs, /uses:|actions\/checkout|secrets\./); + assert.doesNotMatch(workflow, /^ {2}(?:actions|contents|pull-requests): write$/m); +}); + +test("routing exports both placement and stack scope to callers", () => { + assert.match(workflow, /^ {6}runner:\n {8}value: \$\{\{ jobs\.gate\.outputs\.runner \}\}$/m); + assert.match(workflow, /^ {6}full_ci:\n {8}value: \$\{\{ jobs\.gate\.outputs\.full_ci \}\}$/m); + assert.match(jobs, /^ {6}runner: \$\{\{ steps\.route\.outputs\.runner \}\}$/m); + assert.match(jobs, /^ {6}full_ci: \$\{\{ steps\.scope\.outputs\.full_ci \}\}$/m); +}); + +const githubStub = "#!/usr/bin/env bash\nset -Eeuo pipefail\n\nendpoint=\"${*: -1}\"\ncase \"$endpoint\" in\n */pulls/*)\n jq -cn \\\n --argjson author_id \"${TEST_PR_AUTHOR_ID:-34892728}\" \\\n --argjson repository_id \"${TEST_REPOSITORY_ID:-1170821064}\" \\\n --arg base_ref \"${TEST_BASE_REF:-master}\" \\\n --arg base_sha \"${TEST_PR_BASE_SHA:-$EVENT_BASE_SHA}\" \\\n --arg head_sha \"${TEST_PR_HEAD_SHA:-1111111111111111111111111111111111111111}\" \\\n --arg merge_sha \"${TEST_PR_MERGE_SHA:-4444444444444444444444444444444444444444}\" '{\n state: \"open\",\n user: {id: $author_id},\n base: {repo: {id: $repository_id}, ref: $base_ref, sha: $base_sha},\n head: {sha: $head_sha},\n merge_commit_sha: $merge_sha\n }'\n ;;\n */git/ref/heads/*)\n jq -cn \\\n --arg sha \"${TEST_LIVE_BASE_REF_SHA:-$EVENT_BASE_SHA}\" '{object: {sha: $sha}}'\n ;;\n */compare/*)\n jq -cn \\\n --arg merge_base_sha \"${TEST_BASE_COMPARISON_MERGE_BASE_SHA:-$EVENT_BASE_SHA}\" \\\n --arg status \"${TEST_BASE_COMPARISON_STATUS:-ahead}\" '{\n merge_base_commit: {sha: $merge_base_sha},\n status: $status\n }'\n ;;\n */git/commits/*)\n requested_sha=\"${endpoint##*/}\"\n live_merge_sha=\"${TEST_PR_MERGE_SHA:-4444444444444444444444444444444444444444}\"\n synthetic_base_merge_sha=\"${TEST_SYNTHETIC_BASE_MERGE_SHA:-}\"\n if [[ \"$requested_sha\" == \"$EVENT_MERGE_SHA\" ]]; then\n tree_sha=\"${TEST_EVENT_MERGE_TREE_SHA:-5555555555555555555555555555555555555555}\"\n base_sha=\"${TEST_EVENT_MERGE_BASE_SHA:-$EVENT_BASE_SHA}\"\n head_sha=\"${TEST_EVENT_MERGE_HEAD_SHA:-$EVENT_HEAD_SHA}\"\n elif [[ \"$requested_sha\" == \"$live_merge_sha\" ]]; then\n tree_sha=\"${TEST_LIVE_MERGE_TREE_SHA:-5555555555555555555555555555555555555555}\"\n base_sha=\"${TEST_LIVE_MERGE_BASE_SHA:-$EVENT_BASE_SHA}\"\n head_sha=\"${TEST_LIVE_MERGE_HEAD_SHA:-$EVENT_HEAD_SHA}\"\n elif [[ -n \"$synthetic_base_merge_sha\" && \"$requested_sha\" == \"$synthetic_base_merge_sha\" ]]; then\n tree_sha=\"${TEST_SYNTHETIC_BASE_MERGE_TREE_SHA:-8888888888888888888888888888888888888888}\"\n base_sha=\"${TEST_SYNTHETIC_MASTER_PARENT_SHA:-9999999999999999999999999999999999999999}\"\n head_sha=\"${TEST_SYNTHETIC_BASE_REF_PARENT_SHA:-${TEST_LIVE_BASE_REF_SHA:-$EVENT_BASE_SHA}}\"\n else\n echo \"unexpected merge commit: $requested_sha\" >&2\n exit 1\n fi\n jq -cn \\\n --arg sha \"$requested_sha\" \\\n --arg tree_sha \"$tree_sha\" \\\n --arg base_sha \"$base_sha\" \\\n --arg head_sha \"$head_sha\" '{\n sha: $sha,\n tree: {sha: $tree_sha},\n parents: [{sha: $base_sha}, {sha: $head_sha}]\n }'\n ;;\n */actions/runs/*)\n jq -cn \\\n --argjson actor_id \"${TEST_TRIGGERING_ACTOR_ID:-34892728}\" \\\n --argjson repository_id \"${TEST_REPOSITORY_ID:-1170821064}\" \\\n --arg event_name \"${TEST_EVENT_NAME:-pull_request}\" '{\n triggering_actor: {id: $actor_id},\n repository: {id: $repository_id},\n event: $event_name\n }'\n ;;\n *)\n echo \"unexpected gh endpoint: $endpoint\" >&2\n exit 1\n ;;\nesac"; + +function stepScript(name) { + const start = workflow.indexOf(` - name: ${name}\n`); + assert.ok(start >= 0, `missing step: ${name}`); + const body = workflow.slice(start); + const runStart = body.indexOf(" run: |\n"); + assert.ok(runStart >= 0, `missing script: ${name}`); + const script = body.slice(runStart + " run: |\n".length).split("\n - name:")[0]; + return script.split("\n").map((line) => line.replace(/^ {10}/, "")).join("\n"); +} + +function executeStep(name, overrides = {}) { + const directory = mkdtempSync(path.join(tmpdir(), "paperclip-routing-test-")); + const output = path.join(directory, "output"); + try { + const gh = path.join(directory, "gh"); + writeFileSync(gh, githubStub); + chmodSync(gh, 0o700); + const result = spawnSync("bash", ["-c", stepScript(name)], { + encoding: "utf8", + env: { + ...process.env, + PATH: `${directory}:${process.env.PATH}`, + GITHUB_OUTPUT: output, + AWS_CI_ENABLED: "true", + TRUSTED_USER_IDS: "[34892728]", + EVENT_NAME: "pull_request", + EVENT_REPOSITORY: "paperclipai/paperclip", + EVENT_REPOSITORY_ID: "1170821064", + EVENT_ACTION: "synchronize", + EVENT_PR_NUMBER: "12350", + EVENT_PR_AUTHOR_ID: "34892728", + EVENT_SENDER_ID: "34892728", + EVENT_BASE_REPOSITORY_ID: "1170821064", + EVENT_BASE_REF: "master", + EVENT_BASE_SHA: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + EVENT_HEAD_SHA: "1111111111111111111111111111111111111111", + EVENT_MERGE_SHA: "2222222222222222222222222222222222222222", + RUN_ID: "123456789", + ...overrides, + }, + }); + assert.equal(result.status, 0, result.stderr); + const lines = readFileSync(output, "utf8").trim().split("\n"); + assert.equal(lines.length, 1, "each step must emit exactly one decision"); + return Object.fromEntries(lines.map((line) => { + const separator = line.indexOf("="); + return [line.slice(0, separator), line.slice(separator + 1)]; + })); + } finally { + rmSync(directory, { recursive: true, force: true }); + } +} + +const awsRunner = "runs-on/fleet=paperclip-public-pr-x64/env=public-ci"; +const stackedBase = { + EVENT_BASE_REF: "trusted-stack-base", + TEST_BASE_REF: "trusted-stack-base", + TEST_PR_BASE_SHA: "7777777777777777777777777777777777777777", + TEST_LIVE_BASE_REF_SHA: "7777777777777777777777777777777777777777", + TEST_SYNTHETIC_BASE_MERGE_SHA: "8888888888888888888888888888888888888888", + TEST_EVENT_MERGE_BASE_SHA: "8888888888888888888888888888888888888888", + TEST_LIVE_MERGE_BASE_SHA: "8888888888888888888888888888888888888888", +}; + +const routeCases = [ + ["allowlisted PR", {}, awsRunner], + ["validated stacked base", stackedBase, awsRunner], + ["lagging PR API base snapshot", { TEST_PR_BASE_SHA: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" }, awsRunner], + ["disabled AWS", { AWS_CI_ENABLED: "false" }], + ["missing AWS variables", { AWS_CI_ENABLED: "", TRUSTED_USER_IDS: "" }], + ["unlisted author", { EVENT_PR_AUTHOR_ID: "99999999" }], + ["unlisted sender", { EVENT_SENDER_ID: "99999999" }], + ["unlisted rerun actor", { TEST_TRIGGERING_ACTOR_ID: "99999999" }], + ["missing base", { EVENT_BASE_REF: "" }], + ["wrong base repository", { EVENT_BASE_REPOSITORY_ID: "999999999" }], + ["replaced base ancestry", { TEST_BASE_COMPARISON_MERGE_BASE_SHA: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" }], + ["stale head", { TEST_PR_HEAD_SHA: "3333333333333333333333333333333333333333" }], + ["changed merge head", { TEST_LIVE_MERGE_HEAD_SHA: "3333333333333333333333333333333333333333" }], + ["stacked base with wrong parent", { ...stackedBase, TEST_SYNTHETIC_BASE_REF_PARENT_SHA: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }], + ["changed merge tree", { TEST_LIVE_MERGE_TREE_SHA: "6666666666666666666666666666666666666666" }], + ["wrong repository", { EVENT_REPOSITORY_ID: "999999999" }], + ["malformed allowlist", { TRUSTED_USER_IDS: '["34892728"]' }], + ["manual dispatch", { EVENT_NAME: "workflow_dispatch" }], + ["unsupported PR action", { EVENT_ACTION: "closed" }], + ["malformed commit SHA", { EVENT_HEAD_SHA: "invalid" }], +]; + +for (const [name, env, expected = "ubuntu-latest"] of routeCases) { + test(`runner decision: ${name}`, () => { + assert.equal(executeStep("Validate PR identity and select runner", env).runner, expected); + }); +} + +const scopeCases = [ + ["ordinary PR", null, "master", "true"], + ["top stack PR", { position: 11, size: 11, base: { ref: "master" } }, "stack-10", "true"], + ["lowest unmerged PR", { position: 1, size: 11, base: { ref: "master" } }, "master", "true"], + ["middle stack PR", { position: 6, size: 11, base: { ref: "master" } }, "stack-5", "false"], + ["malformed position", { position: "invalid", size: 11, base: { ref: "master" } }, "stack-5", "true"], + ["out-of-range position", { position: 12, size: 11, base: { ref: "master" } }, "stack-5", "true"], + ["missing stack base", { position: 6, size: 11 }, "stack-5", "true"], + ["empty metadata", {}, "master", "true"], +]; + +for (const [name, stack, base, expected] of scopeCases) { + test(`scope decision: ${name}`, () => { + assert.equal(executeStep("Select stacked PR CI scope", { + STACK_JSON: JSON.stringify(stack), PR_BASE_REF: base, + }).full_ci, expected); + }); +} + + +function routingPlacement(vars = {}, github = {}) { + const source = "[34892728]"; + const expression = jobs.match(/^ {4}runs-on: \$\{\{ (.+) \}\}$/m)?.[1]; + assert.ok(expression, "routing expression is missing"); + return vm.runInNewContext(expression, { + vars: { + AWS_CI_ENABLED: "true", + AWS_CI_TRUSTED_USER_IDS: source, + AWS_CI_ROUTING_USER_IDS_SOURCE: source, + AWS_CI_ROUTING_USER_IDS: "|34892728|", + ...vars, + }, + github: { + repository: "paperclipai/paperclip", + repository_id: "1170821064", + event_name: "pull_request", + ref: "refs/heads/master", + actor_id: "34892728", + event: { pull_request: { user: { id: 34892728 } }, sender: { id: 34892728 } }, + ...github, + }, + contains: (search, item) => String(search).toLowerCase().includes(String(item).toLowerCase()), + format: (template, ...args) => template.replace(/\{(\d+)\}/g, (_, index) => String(args[Number(index)])), + }, { timeout: 1000 }); +} + +const placementCases = [ + ["approved PR", {}, {}, "runs-on/fleet=paperclip-ci-routing-x64/env=public-ci"], + ["unapproved author", {}, { event: { pull_request: { user: { id: 999 } }, sender: { id: 34892728 } } }], + ["unapproved sender", {}, { event: { pull_request: { user: { id: 34892728 } }, sender: { id: 999 } } }], + ["partial ID cannot match", {}, { event: { pull_request: { user: { id: 4892728 } }, sender: { id: 34892728 } } }], + ["invalid JSON keeps hosted fallback", { AWS_CI_TRUSTED_USER_IDS: "[34892728,]" }, {}], + ["stale snapshot", { AWS_CI_TRUSTED_USER_IDS: "[34892728,999]" }, {}], + ["missing snapshot", { AWS_CI_ROUTING_USER_IDS_SOURCE: "" }, {}], + ["missing admission members", { AWS_CI_ROUTING_USER_IDS: "" }, {}], + ["disabled routing", { AWS_CI_ENABLED: "false" }, {}], + ["different repository", {}, { repository: "other/repo" }], + ["different repository ID", {}, { repository_id: "999" }], + ["approved manual master canary", {}, { event_name: "workflow_dispatch" }, "runs-on/fleet=paperclip-ci-routing-x64/env=public-ci"], + ["unapproved manual actor", {}, { event_name: "workflow_dispatch", actor_id: "999" }], + ["manual branch canary", {}, { event_name: "workflow_dispatch", ref: "refs/heads/other" }], +]; + +for (const [name, vars, github, expected = "ubuntu-latest"] of placementCases) { + test(`placement before allocation: ${name}`, () => { + assert.equal(routingPlacement(vars, github), expected); + }); +} diff --git a/.github/workflows/ci-routing.yml b/.github/workflows/ci-routing.yml new file mode 100644 index 00000000000..5ad3e1e11e3 --- /dev/null +++ b/.github/workflows/ci-routing.yml @@ -0,0 +1,252 @@ +name: CI routing + +on: + workflow_dispatch: + workflow_call: + outputs: + runner: + value: ${{ jobs.gate.outputs.runner }} + full_ci: + value: ${{ jobs.gate.outputs.full_ci }} + +permissions: + actions: read + contents: read + pull-requests: read + +jobs: + gate: + name: Select trusted runner + runs-on: ${{ vars.AWS_CI_ENABLED == 'true' && vars.AWS_CI_ROUTING_USER_IDS_SOURCE != '' && vars.AWS_CI_TRUSTED_USER_IDS == vars.AWS_CI_ROUTING_USER_IDS_SOURCE && github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && ((github.event_name == 'pull_request' && contains(vars.AWS_CI_ROUTING_USER_IDS, format('|{0}|', github.event.pull_request.user.id)) && contains(vars.AWS_CI_ROUTING_USER_IDS, format('|{0}|', github.event.sender.id))) || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/master' && contains(vars.AWS_CI_ROUTING_USER_IDS, format('|{0}|', github.actor_id)))) && 'runs-on/fleet=paperclip-ci-routing-x64/env=public-ci' || 'ubuntu-latest' }} + timeout-minutes: 5 + outputs: + runner: ${{ steps.route.outputs.runner }} + full_ci: ${{ steps.scope.outputs.full_ci }} + + steps: + - name: Validate PR identity and select runner + id: route + shell: bash + env: + GH_TOKEN: ${{ github.token }} + AWS_CI_ENABLED: ${{ vars.AWS_CI_ENABLED }} + TRUSTED_USER_IDS: ${{ vars.AWS_CI_TRUSTED_USER_IDS }} + EVENT_NAME: ${{ github.event_name }} + EVENT_REPOSITORY: ${{ github.repository }} + EVENT_REPOSITORY_ID: ${{ github.repository_id }} + EVENT_ACTION: ${{ github.event.action }} + EVENT_PR_NUMBER: ${{ github.event.pull_request.number }} + EVENT_PR_AUTHOR_ID: ${{ github.event.pull_request.user.id }} + EVENT_SENDER_ID: ${{ github.event.sender.id }} + EVENT_BASE_REPOSITORY_ID: ${{ github.event.pull_request.base.repo.id }} + EVENT_BASE_REF: ${{ github.event.pull_request.base.ref }} + EVENT_BASE_SHA: ${{ github.event.pull_request.base.sha }} + EVENT_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + EVENT_MERGE_SHA: ${{ github.sha }} + RUN_ID: ${{ github.run_id }} + run: | + set -u + + github_runner='ubuntu-latest' + aws_runner='runs-on/fleet=paperclip-public-pr-x64/env=public-ci' + + fail_closed() { + echo "runner=$github_runner" >> "$GITHUB_OUTPUT" + echo "::notice title=AWS CI routing::Using GitHub-hosted runner: $1" + exit 0 + } + + is_positive_integer() { + [[ "$1" =~ ^[1-9][0-9]*$ ]] + } + + is_commit_sha() { + [[ "$1" =~ ^[0-9a-f]{40}$ ]] + } + + is_allowed() { + local user_id="$1" + jq -e --argjson user_id "$user_id" 'index($user_id) != null' \ + <<< "$TRUSTED_USER_IDS" >/dev/null + } + + [[ "$AWS_CI_ENABLED" == 'true' ]] || fail_closed 'AWS_CI_ENABLED is not true' + [[ "$EVENT_NAME" == 'pull_request' ]] || fail_closed 'event is not pull_request' + [[ "$EVENT_REPOSITORY" == 'paperclipai/paperclip' ]] || fail_closed 'unexpected repository' + [[ "$EVENT_REPOSITORY_ID" == '1170821064' ]] || fail_closed 'unexpected repository ID' + [[ "$EVENT_BASE_REPOSITORY_ID" == '1170821064' ]] || fail_closed 'unexpected base repository ID' + [[ -n "$EVENT_BASE_REF" ]] || fail_closed 'missing base branch' + [[ "$EVENT_ACTION" =~ ^(opened|reopened|synchronize)$ ]] || fail_closed 'unsupported pull_request action' + + jq -e ' + type == "array" and + length > 0 and + all(.[]; type == "number" and . > 0 and floor == .) + ' <<< "$TRUSTED_USER_IDS" >/dev/null 2>&1 || fail_closed 'trusted user ID list is malformed' + + for user_id in "$EVENT_PR_AUTHOR_ID" "$EVENT_SENDER_ID"; do + is_positive_integer "$user_id" || fail_closed 'event contains a malformed user ID' + is_allowed "$user_id" || fail_closed "GitHub user ID $user_id is not allowlisted" + done + + for commit_sha in "$EVENT_BASE_SHA" "$EVENT_HEAD_SHA" "$EVENT_MERGE_SHA"; do + is_commit_sha "$commit_sha" || fail_closed 'event contains a malformed commit SHA' + done + + pr_json="$(gh api \ + -H 'Accept: application/vnd.github+json' \ + -H 'X-GitHub-Api-Version: 2022-11-28' \ + "/repos/paperclipai/paperclip/pulls/$EVENT_PR_NUMBER" 2>/dev/null)" \ + || fail_closed 'could not refresh pull request state' + + jq -e \ + --argjson repository_id 1170821064 \ + --argjson author_id "$EVENT_PR_AUTHOR_ID" \ + --arg base_ref "$EVENT_BASE_REF" \ + --arg head_sha "$EVENT_HEAD_SHA" \ + ' + .state == "open" and + .user.id == $author_id and + .base.repo.id == $repository_id and + .base.ref == $base_ref and + .head.sha == $head_sha + ' <<< "$pr_json" >/dev/null 2>&1 \ + || fail_closed 'current pull request state does not match the triggering event' + + live_merge_sha="$(jq -r '.merge_commit_sha // empty' <<< "$pr_json")" + is_commit_sha "$live_merge_sha" || fail_closed 'current pull request has no valid merge SHA' + + base_ref_json="$(gh api \ + -H 'Accept: application/vnd.github+json' \ + -H 'X-GitHub-Api-Version: 2022-11-28' \ + "/repos/paperclipai/paperclip/git/ref/heads/$EVENT_BASE_REF" 2>/dev/null)" \ + || fail_closed 'could not inspect the current base branch' + live_base_ref_sha="$(jq -r '.object.sha // empty' <<< "$base_ref_json")" + is_commit_sha "$live_base_ref_sha" || fail_closed 'current base branch has no valid commit SHA' + + base_comparison="$(gh api \ + -H 'Accept: application/vnd.github+json' \ + -H 'X-GitHub-Api-Version: 2022-11-28' \ + "/repos/paperclipai/paperclip/compare/$EVENT_BASE_SHA...$live_base_ref_sha" 2>/dev/null)" \ + || fail_closed 'could not compare the triggering and current base branches' + jq -e \ + --arg event_base_sha "$EVENT_BASE_SHA" ' + .merge_base_commit.sha == $event_base_sha and + (.status == "ahead" or .status == "identical") + ' <<< "$base_comparison" >/dev/null 2>&1 \ + || fail_closed 'current base branch does not descend from the triggering base snapshot' + + event_merge_json="$(gh api \ + -H 'Accept: application/vnd.github+json' \ + -H 'X-GitHub-Api-Version: 2022-11-28' \ + "/repos/paperclipai/paperclip/git/commits/$EVENT_MERGE_SHA" 2>/dev/null)" \ + || fail_closed 'could not inspect the event merge commit' + + live_merge_json="$(gh api \ + -H 'Accept: application/vnd.github+json' \ + -H 'X-GitHub-Api-Version: 2022-11-28' \ + "/repos/paperclipai/paperclip/git/commits/$live_merge_sha" 2>/dev/null)" \ + || fail_closed 'could not inspect the current merge commit' + + validate_merge_commit() { + local merge_json="$1" + local expected_sha="$2" + jq -e \ + --arg expected_sha "$expected_sha" \ + --arg head_sha "$EVENT_HEAD_SHA" ' + .sha == $expected_sha and + (.parents | length) == 2 and + .parents[1].sha == $head_sha and + (.parents[0].sha | test("^[0-9a-f]{40}$")) and + (.tree.sha | test("^[0-9a-f]{40}$")) + ' <<< "$merge_json" >/dev/null 2>&1 + } + + validate_merge_commit "$event_merge_json" "$EVENT_MERGE_SHA" \ + || fail_closed 'event merge commit does not match the current base and head' + validate_merge_commit "$live_merge_json" "$live_merge_sha" \ + || fail_closed 'current merge commit does not match the triggering base and head' + + event_merge_parent="$(jq -r '.parents[0].sha' <<< "$event_merge_json")" + live_merge_parent="$(jq -r '.parents[0].sha' <<< "$live_merge_json")" + [[ "$event_merge_parent" == "$live_merge_parent" ]] \ + || fail_closed 'current merge commit uses a different base merge parent' + + if [[ "$event_merge_parent" != "$live_base_ref_sha" ]]; then + base_merge_json="$(gh api \ + -H 'Accept: application/vnd.github+json' \ + -H 'X-GitHub-Api-Version: 2022-11-28' \ + "/repos/paperclipai/paperclip/git/commits/$event_merge_parent" 2>/dev/null)" \ + || fail_closed 'could not inspect the stacked base merge commit' + jq -e \ + --arg expected_sha "$event_merge_parent" \ + --arg live_base_ref_sha "$live_base_ref_sha" ' + .sha == $expected_sha and + (.parents | length) == 2 and + any(.parents[]; .sha == $live_base_ref_sha) and + (.tree.sha | test("^[0-9a-f]{40}$")) + ' <<< "$base_merge_json" >/dev/null 2>&1 \ + || fail_closed 'stacked base merge commit does not contain the current base branch' + fi + + event_merge_tree="$(jq -r '.tree.sha' <<< "$event_merge_json")" + live_merge_tree="$(jq -r '.tree.sha' <<< "$live_merge_json")" + [[ "$event_merge_tree" == "$live_merge_tree" ]] \ + || fail_closed 'current merge tree differs from the triggering merge tree' + + run_json="$(gh api \ + -H 'Accept: application/vnd.github+json' \ + -H 'X-GitHub-Api-Version: 2022-11-28' \ + "/repos/paperclipai/paperclip/actions/runs/$RUN_ID" 2>/dev/null)" \ + || fail_closed 'could not refresh workflow run state' + + triggering_actor_id="$(jq -r '.triggering_actor.id // empty' <<< "$run_json")" + is_positive_integer "$triggering_actor_id" || fail_closed 'workflow run has no valid triggering actor ID' + is_allowed "$triggering_actor_id" || fail_closed "triggering GitHub user ID $triggering_actor_id is not allowlisted" + + jq -e \ + --argjson repository_id 1170821064 \ + --arg event_name "$EVENT_NAME" ' + .repository.id == $repository_id and + .event == $event_name + ' <<< "$run_json" >/dev/null 2>&1 \ + || fail_closed 'current workflow run does not match the expected repository event' + + echo "runner=$aws_runner" >> "$GITHUB_OUTPUT" + echo '::notice title=AWS CI routing::Using an ephemeral RunsOn Fleet runner' + + - name: Select stacked PR CI scope + id: scope + shell: bash + env: + STACK_JSON: ${{ toJSON(github.event.pull_request.stack) }} + PR_BASE_REF: ${{ github.event.pull_request.base.ref }} + run: | + set -euo pipefail + + full_ci='true' + reason='ordinary pull request' + + if jq -e 'type == "object"' <<< "$STACK_JSON" >/dev/null 2>&1; then + stack_position="$(jq -r '.position // empty' <<< "$STACK_JSON")" + stack_size="$(jq -r '.size // empty' <<< "$STACK_JSON")" + stack_base_ref="$(jq -r '.base.ref // empty' <<< "$STACK_JSON")" + + if [[ ! "$stack_position" =~ ^[1-9][0-9]*$ ]] || + [[ ! "$stack_size" =~ ^[1-9][0-9]*$ ]] || + (( stack_position > stack_size )) || + [[ -z "$stack_base_ref" ]]; then + reason='malformed stack metadata; defaulting to full CI' + elif (( stack_position == stack_size )); then + reason='top pull request in stack' + elif [[ "$stack_base_ref" == "$PR_BASE_REF" ]]; then + reason='lowest unmerged pull request in stack' + else + full_ci='false' + reason='middle pull request in stack' + fi + fi + + echo "full_ci=$full_ci" >> "$GITHUB_OUTPUT" + echo "::notice title=Stacked PR CI scope::$reason; full_ci=$full_ci" + diff --git a/.github/workflows/pr-trusted.yml b/.github/workflows/pr-trusted.yml index ec62707c417..65f72fa8c56 100644 --- a/.github/workflows/pr-trusted.yml +++ b/.github/workflows/pr-trusted.yml @@ -17,241 +17,7 @@ concurrency: jobs: gate: - name: Select trusted runner - runs-on: ubuntu-latest - timeout-minutes: 5 - outputs: - runner: ${{ steps.route.outputs.runner }} - full_ci: ${{ steps.scope.outputs.full_ci }} - - steps: - - name: Validate PR identity and select runner - id: route - shell: bash - env: - GH_TOKEN: ${{ github.token }} - AWS_CI_ENABLED: ${{ vars.AWS_CI_ENABLED }} - TRUSTED_USER_IDS: ${{ vars.AWS_CI_TRUSTED_USER_IDS }} - EVENT_NAME: ${{ github.event_name }} - EVENT_REPOSITORY: ${{ github.repository }} - EVENT_REPOSITORY_ID: ${{ github.repository_id }} - EVENT_ACTION: ${{ github.event.action }} - EVENT_PR_NUMBER: ${{ github.event.pull_request.number }} - EVENT_PR_AUTHOR_ID: ${{ github.event.pull_request.user.id }} - EVENT_SENDER_ID: ${{ github.event.sender.id }} - EVENT_BASE_REPOSITORY_ID: ${{ github.event.pull_request.base.repo.id }} - EVENT_BASE_REF: ${{ github.event.pull_request.base.ref }} - EVENT_BASE_SHA: ${{ github.event.pull_request.base.sha }} - EVENT_HEAD_SHA: ${{ github.event.pull_request.head.sha }} - EVENT_MERGE_SHA: ${{ github.sha }} - RUN_ID: ${{ github.run_id }} - run: | - set -u - - github_runner='ubuntu-latest' - aws_runner='runs-on/fleet=paperclip-public-pr-x64/env=public-ci' - - fail_closed() { - echo "runner=$github_runner" >> "$GITHUB_OUTPUT" - echo "::notice title=AWS CI routing::Using GitHub-hosted runner: $1" - exit 0 - } - - is_positive_integer() { - [[ "$1" =~ ^[1-9][0-9]*$ ]] - } - - is_commit_sha() { - [[ "$1" =~ ^[0-9a-f]{40}$ ]] - } - - is_allowed() { - local user_id="$1" - jq -e --argjson user_id "$user_id" 'index($user_id) != null' \ - <<< "$TRUSTED_USER_IDS" >/dev/null - } - - [[ "$AWS_CI_ENABLED" == 'true' ]] || fail_closed 'AWS_CI_ENABLED is not true' - # Reusable workflows retain the caller's github context and event - # payload, so a pull_request caller must still report pull_request. - [[ "$EVENT_NAME" == 'pull_request' ]] || fail_closed 'event is not pull_request' - [[ "$EVENT_REPOSITORY" == 'paperclipai/paperclip' ]] || fail_closed 'unexpected repository' - [[ "$EVENT_REPOSITORY_ID" == '1170821064' ]] || fail_closed 'unexpected repository ID' - [[ "$EVENT_BASE_REPOSITORY_ID" == '1170821064' ]] || fail_closed 'unexpected base repository ID' - [[ -n "$EVENT_BASE_REF" ]] || fail_closed 'missing base branch' - [[ "$EVENT_ACTION" =~ ^(opened|reopened|synchronize)$ ]] || fail_closed 'unsupported pull_request action' - - jq -e ' - type == "array" and - length > 0 and - all(.[]; type == "number" and . > 0 and floor == .) - ' <<< "$TRUSTED_USER_IDS" >/dev/null 2>&1 || fail_closed 'trusted user ID list is malformed' - - for user_id in "$EVENT_PR_AUTHOR_ID" "$EVENT_SENDER_ID"; do - is_positive_integer "$user_id" || fail_closed 'event contains a malformed user ID' - is_allowed "$user_id" || fail_closed "GitHub user ID $user_id is not allowlisted" - done - - for commit_sha in "$EVENT_BASE_SHA" "$EVENT_HEAD_SHA" "$EVENT_MERGE_SHA"; do - is_commit_sha "$commit_sha" || fail_closed 'event contains a malformed commit SHA' - done - - pr_json="$(gh api \ - -H 'Accept: application/vnd.github+json' \ - -H 'X-GitHub-Api-Version: 2022-11-28' \ - "/repos/paperclipai/paperclip/pulls/$EVENT_PR_NUMBER" 2>/dev/null)" \ - || fail_closed 'could not refresh pull request state' - - jq -e \ - --argjson repository_id 1170821064 \ - --argjson author_id "$EVENT_PR_AUTHOR_ID" \ - --arg base_ref "$EVENT_BASE_REF" \ - --arg head_sha "$EVENT_HEAD_SHA" \ - ' - .state == "open" and - .user.id == $author_id and - .base.repo.id == $repository_id and - .base.ref == $base_ref and - .head.sha == $head_sha - ' <<< "$pr_json" >/dev/null 2>&1 \ - || fail_closed 'current pull request state does not match the triggering event' - - live_merge_sha="$(jq -r '.merge_commit_sha // empty' <<< "$pr_json")" - is_commit_sha "$live_merge_sha" || fail_closed 'current pull request has no valid merge SHA' - - base_ref_json="$(gh api \ - -H 'Accept: application/vnd.github+json' \ - -H 'X-GitHub-Api-Version: 2022-11-28' \ - "/repos/paperclipai/paperclip/git/ref/heads/$EVENT_BASE_REF" 2>/dev/null)" \ - || fail_closed 'could not inspect the current base branch' - live_base_ref_sha="$(jq -r '.object.sha // empty' <<< "$base_ref_json")" - is_commit_sha "$live_base_ref_sha" || fail_closed 'current base branch has no valid commit SHA' - - base_comparison="$(gh api \ - -H 'Accept: application/vnd.github+json' \ - -H 'X-GitHub-Api-Version: 2022-11-28' \ - "/repos/paperclipai/paperclip/compare/$EVENT_BASE_SHA...$live_base_ref_sha" 2>/dev/null)" \ - || fail_closed 'could not compare the triggering and current base branches' - jq -e \ - --arg event_base_sha "$EVENT_BASE_SHA" ' - .merge_base_commit.sha == $event_base_sha and - (.status == "ahead" or .status == "identical") - ' <<< "$base_comparison" >/dev/null 2>&1 \ - || fail_closed 'current base branch does not descend from the triggering base snapshot' - - event_merge_json="$(gh api \ - -H 'Accept: application/vnd.github+json' \ - -H 'X-GitHub-Api-Version: 2022-11-28' \ - "/repos/paperclipai/paperclip/git/commits/$EVENT_MERGE_SHA" 2>/dev/null)" \ - || fail_closed 'could not inspect the event merge commit' - - live_merge_json="$(gh api \ - -H 'Accept: application/vnd.github+json' \ - -H 'X-GitHub-Api-Version: 2022-11-28' \ - "/repos/paperclipai/paperclip/git/commits/$live_merge_sha" 2>/dev/null)" \ - || fail_closed 'could not inspect the current merge commit' - - validate_merge_commit() { - local merge_json="$1" - local expected_sha="$2" - jq -e \ - --arg expected_sha "$expected_sha" \ - --arg head_sha "$EVENT_HEAD_SHA" ' - .sha == $expected_sha and - (.parents | length) == 2 and - .parents[1].sha == $head_sha and - (.parents[0].sha | test("^[0-9a-f]{40}$")) and - (.tree.sha | test("^[0-9a-f]{40}$")) - ' <<< "$merge_json" >/dev/null 2>&1 - } - - validate_merge_commit "$event_merge_json" "$EVENT_MERGE_SHA" \ - || fail_closed 'event merge commit does not match the current base and head' - validate_merge_commit "$live_merge_json" "$live_merge_sha" \ - || fail_closed 'current merge commit does not match the triggering base and head' - - event_merge_parent="$(jq -r '.parents[0].sha' <<< "$event_merge_json")" - live_merge_parent="$(jq -r '.parents[0].sha' <<< "$live_merge_json")" - [[ "$event_merge_parent" == "$live_merge_parent" ]] \ - || fail_closed 'current merge commit uses a different base merge parent' - - if [[ "$event_merge_parent" != "$live_base_ref_sha" ]]; then - base_merge_json="$(gh api \ - -H 'Accept: application/vnd.github+json' \ - -H 'X-GitHub-Api-Version: 2022-11-28' \ - "/repos/paperclipai/paperclip/git/commits/$event_merge_parent" 2>/dev/null)" \ - || fail_closed 'could not inspect the stacked base merge commit' - jq -e \ - --arg expected_sha "$event_merge_parent" \ - --arg live_base_ref_sha "$live_base_ref_sha" ' - .sha == $expected_sha and - (.parents | length) == 2 and - any(.parents[]; .sha == $live_base_ref_sha) and - (.tree.sha | test("^[0-9a-f]{40}$")) - ' <<< "$base_merge_json" >/dev/null 2>&1 \ - || fail_closed 'stacked base merge commit does not contain the current base branch' - fi - - event_merge_tree="$(jq -r '.tree.sha' <<< "$event_merge_json")" - live_merge_tree="$(jq -r '.tree.sha' <<< "$live_merge_json")" - [[ "$event_merge_tree" == "$live_merge_tree" ]] \ - || fail_closed 'current merge tree differs from the triggering merge tree' - - run_json="$(gh api \ - -H 'Accept: application/vnd.github+json' \ - -H 'X-GitHub-Api-Version: 2022-11-28' \ - "/repos/paperclipai/paperclip/actions/runs/$RUN_ID" 2>/dev/null)" \ - || fail_closed 'could not refresh workflow run state' - - triggering_actor_id="$(jq -r '.triggering_actor.id // empty' <<< "$run_json")" - is_positive_integer "$triggering_actor_id" || fail_closed 'workflow run has no valid triggering actor ID' - is_allowed "$triggering_actor_id" || fail_closed "triggering GitHub user ID $triggering_actor_id is not allowlisted" - - jq -e \ - --argjson repository_id 1170821064 \ - --arg event_name "$EVENT_NAME" ' - .repository.id == $repository_id and - .event == $event_name - ' <<< "$run_json" >/dev/null 2>&1 \ - || fail_closed 'current workflow run does not match the expected repository event' - - echo "runner=$aws_runner" >> "$GITHUB_OUTPUT" - echo '::notice title=AWS CI routing::Using an ephemeral RunsOn Fleet runner' - - - name: Select stacked PR CI scope - id: scope - shell: bash - env: - STACK_JSON: ${{ toJSON(github.event.pull_request.stack) }} - PR_BASE_REF: ${{ github.event.pull_request.base.ref }} - run: | - set -euo pipefail - - full_ci='true' - reason='ordinary pull request' - - if jq -e 'type == "object"' <<< "$STACK_JSON" >/dev/null 2>&1; then - stack_position="$(jq -r '.position // empty' <<< "$STACK_JSON")" - stack_size="$(jq -r '.size // empty' <<< "$STACK_JSON")" - stack_base_ref="$(jq -r '.base.ref // empty' <<< "$STACK_JSON")" - - if [[ ! "$stack_position" =~ ^[1-9][0-9]*$ ]] || - [[ ! "$stack_size" =~ ^[1-9][0-9]*$ ]] || - (( stack_position > stack_size )) || - [[ -z "$stack_base_ref" ]]; then - reason='malformed stack metadata; defaulting to full CI' - elif (( stack_position == stack_size )); then - reason='top pull request in stack' - elif [[ "$stack_base_ref" == "$PR_BASE_REF" ]]; then - reason='lowest unmerged pull request in stack' - else - full_ci='false' - reason='middle pull request in stack' - fi - fi - - echo "full_ci=$full_ci" >> "$GITHUB_OUTPUT" - echo "::notice title=Stacked PR CI scope::$reason; full_ci=$full_ci" + uses: paperclipai/paperclip/.github/workflows/ci-routing.yml@master policy: needs: [gate] diff --git a/.github/workflows/release-smoke.yml b/.github/workflows/release-smoke.yml index 6301a4d1c1a..40fa1753466 100644 --- a/.github/workflows/release-smoke.yml +++ b/.github/workflows/release-smoke.yml @@ -227,17 +227,17 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 45 steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with: ref: ${{ inputs.qualification_source_sha }} persist-credentials: false - - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6 + - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 with: version: 9.15.4 - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 with: node-version: 24 - - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 with: pattern: ${{ needs.qualification_daemons.outputs.runner_artifact_prefix }}-* path: ${{ runner.temp }}/release-runner-inputs @@ -275,7 +275,7 @@ jobs: fi - name: Retain completed package assembly even after a later Linux failure if: ${{ !cancelled() && steps.pack_identity.outputs.available == 'true' }} - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: name: ${{ inputs.artifact_name }}-public-packages path: ${{ runner.temp }}/public-runner-packs/ @@ -292,17 +292,17 @@ jobs: fail-fast: false matrix: ${{ fromJSON(needs.qualification_daemons.outputs.native_consumer_matrix) }} steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with: ref: ${{ inputs.qualification_source_sha }} persist-credentials: false - - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6 + - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 with: version: 9.15.4 - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 with: node-version: 24 - - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 with: name: ${{ inputs.artifact_name }}-public-packages path: ${{ runner.temp }}/public-runner-packs @@ -310,9 +310,8 @@ jobs: run: pnpm install --ignore-scripts --no-frozen-lockfile - name: Checkout immutable ARM qualification tooling if: ${{ matrix.target == 'linux-arm64' }} - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with: - # job.workflow_* identifies this reusable workflow, not its caller. repository: ${{ job.workflow_repository }} ref: ${{ job.workflow_sha }} path: .qualification-tooling @@ -363,7 +362,7 @@ jobs: if [[ "$TARGET" == linux-arm64 ]]; then mode=--linux-arm64-consumer; fi node scripts/verify-grok-npm-install.mjs "$mode" \ "$RUNNER_TEMP/public-runner-packs" "$SOURCE_SHA" "$RUNNER_TEMP/native-public-install.json" - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 if: always() with: name: ${{ inputs.artifact_name }}-${{ matrix.target }} diff --git a/.github/workflows/release-verify.yml b/.github/workflows/release-verify.yml index b1c3d61694a..a4632dfb442 100644 --- a/.github/workflows/release-verify.yml +++ b/.github/workflows/release-verify.yml @@ -527,11 +527,11 @@ jobs: matrix: ${{ steps.targets.outputs.matrix }} native_consumers: ${{ steps.targets.outputs.native_consumers }} steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with: ref: ${{ inputs.ref }} persist-credentials: false - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 with: node-version: 24 - name: Preserve the immutable source's platform contract @@ -564,8 +564,6 @@ jobs: && Object.keys(contract).sort().join() === 'schema,targets', 'Unknown exact-source daemon platform contract'); declared = contract.targets; } else { - // Frozen pre-contract sources used a literal export. Read that one - // declaration as JSON data; never import or evaluate preview code. const legacy = readBlob('packages/paperclip-runner/scripts/stage-release-runner-binaries.mjs'); const declarations = [...(legacy ?? '').matchAll(/^[ \t]*export const RELEASE_RUNNER_TARGETS = (?:Object\.freeze\()?(\[[^\r\n]*\])\)?;[ \t]*$/gm)]; assert.equal(declarations.length, 1, 'Unknown exact-source daemon platform contract'); @@ -596,14 +594,14 @@ jobs: outputs: artifact_prefix: ${{ steps.identity.outputs.artifact_prefix }} steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with: ref: ${{ inputs.ref }} persist-credentials: false - - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6 + - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 with: version: 9.15.4 - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 with: node-version: 24 - name: Bind immutable producer source and target @@ -640,18 +638,13 @@ jobs: fi rustup toolchain list - name: Cache native Rust dependencies - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 with: workspaces: ${{ steps.native_rust_workspace.outputs.path }} -> target - # Compiler/environment and Cargo lock hashes are added by the action. - # Also bind generated contracts outside the Rust workspace, while - # always recompiling workspace code and recording actual new bytes. shared-key: release-native-v2-${{ matrix.host }}-${{ matrix.target }}-${{ hashFiles('packages/paperclip-runner/protocol/**', 'packages/paperclip-runner/src/**', 'packages/paperclip-runner/*.json') }} cache-workspace-crates: false cache-bin: false cache-on-failure: false - # A master workflow can verify an unmerged preview ref. Those jobs - # restore only; they never write master's trusted dependency cache. save-if: ${{ github.repository == 'paperclipai/paperclip' && github.repository_id == '1170821064' && github.event_name == 'push' && github.ref == 'refs/heads/master' && inputs.ref == github.sha }} - name: Install dependencies run: pnpm install --no-frozen-lockfile @@ -694,7 +687,7 @@ jobs: run: | node packages/paperclip-runner/scripts/stage-release-runner-binaries.mjs record "$SOURCE_SHA" "$TARGET" \ packages/paperclip-runner/dist/bin/paperclip-runnerd "$RUNNER_TEMP/runner-release/$TARGET" - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: name: ${{ steps.identity.outputs.artifact_prefix }}-${{ matrix.target }} path: ${{ runner.temp }}/runner-release/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a676cf55a28..a02b40609f4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -341,8 +341,6 @@ jobs: run: node scripts/cloud-source-verification.mjs "$SOURCE_SHA" release_runner_source: - # Old promotions retain their already-shipped packaging contract. New - # sources opt into source-declared native target assembly in their release.sh. needs: [verify_canary, select_nightly, select_beta, preflight_stable] if: >- !cancelled() && ( @@ -357,7 +355,7 @@ jobs: sha: ${{ steps.source.outputs.sha }} required: ${{ steps.source.outputs.required }} steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with: ref: ${{ needs.preflight_stable.outputs.sha || needs.select_beta.outputs.sha || needs.select_nightly.outputs.sha || github.sha }} persist-credentials: false @@ -379,7 +377,7 @@ jobs: release_runner_binaries: needs: release_runner_source - if: needs.release_runner_source.outputs.required == 'true' + if: ${{ !cancelled() && needs.release_runner_source.result == 'success' && needs.release_runner_source.outputs.required == 'true' }} uses: ./.github/workflows/release-verify.yml with: ref: ${{ needs.release_runner_source.outputs.sha }} @@ -427,7 +425,7 @@ jobs: - name: Download exact-source release daemon receipts if: needs.release_runner_source.outputs.required == 'true' - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 with: pattern: ${{ needs.release_runner_binaries.outputs.runner_artifact_prefix }}-* path: ${{ runner.temp }}/release-runner-inputs @@ -527,7 +525,7 @@ jobs: # anyone mistakes an installable package for an onboardable one. smoke_canary_onboarding: needs: publish_canary - if: needs.publish_canary.result == 'success' + if: ${{ !cancelled() && needs.publish_canary.result == 'success' }} runs-on: ubuntu-latest timeout-minutes: 20 permissions: @@ -724,7 +722,7 @@ jobs: - name: Download exact-source release daemon receipts if: needs.release_runner_source.outputs.required == 'true' - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 with: pattern: ${{ needs.release_runner_binaries.outputs.runner_artifact_prefix }}-* path: ${{ runner.temp }}/release-runner-inputs @@ -1003,7 +1001,7 @@ jobs: - name: Download exact-source release daemon receipts if: needs.release_runner_source.outputs.required == 'true' - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 with: pattern: ${{ needs.release_runner_binaries.outputs.runner_artifact_prefix }}-* path: ${{ runner.temp }}/release-runner-inputs @@ -1384,7 +1382,7 @@ jobs: - name: Download exact-source release daemon receipts if: needs.release_runner_source.outputs.required == 'true' - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 with: pattern: ${{ needs.release_runner_binaries.outputs.runner_artifact_prefix }}-* path: ${{ runner.temp }}/release-runner-inputs @@ -1442,7 +1440,7 @@ jobs: - name: Download exact-source release daemon receipts if: needs.release_runner_source.outputs.required == 'true' - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 with: pattern: ${{ needs.release_runner_binaries.outputs.runner_artifact_prefix }}-* path: ${{ runner.temp }}/release-runner-inputs diff --git a/.greptile/README.md b/.greptile/README.md new file mode 100644 index 00000000000..00657270392 --- /dev/null +++ b/.greptile/README.md @@ -0,0 +1,71 @@ +# Greptile configuration + +This directory tracks the supported review configuration for +`paperclipai/paperclip`. It was prepared from a read-only dashboard inventory +on 2026-10-13. No dashboard settings or custom context were changed. + +- `config.json` records the observed review settings supported by Greptile's + documented file schema. +- `files.json` loads repository context and scopes architectural rule files. +- `rules.md` explains how to apply the canonical Markdown rules in `rules/`. + +## Settings migrated to Git + +| Dashboard setting | Repository configuration | +| --- | --- | +| Automatic reviews on all events | `autoReview: ["open", "push", "rebase"]` | +| Exclude `dependabot[bot]`; remaining reviews use Base | `excludeAuthors`, `effort: "base"` | +| High strictness (dashboard says it does not comment on P2s) | `strictness: 3` | +| Do not replace the PR description | `shouldUpdateDescription: false` | +| Include PR summary, confidence score, and findings; no sequence diagram | The four section settings in `config.json` | +| Summary is not collapsible; default-open option is off | `summarySection.collapsible` and `defaultOpen` are `false` | +| Include a prompt to fix findings | `fixWithAI: true` | +| Status checks and status comments enabled | `statusCheck` and `statusCommentsEnabled` are `true` | +| Auto-approval disabled; stored risk ceiling is Low | `autoApprove.enabled: false`, `riskCeiling: "low"` | + +The existing PR-description and 1,000-line growth guidance is in +`rules/pull-request-review.mdc`. That file also preserves the active, +repository-specific rule not to request UI screenshots. This resolves the +contradiction with the older organization instruction asking for screenshots. + +The active dashboard pattern `**/[Aa][Gg][Ee][Nn][Tt][Ss].md` reported two files: +`AGENTS.md` and `docs/api/agents.md`. Both are explicit references in +`files.json`; the latter remains API context, not contributor policy. Future +context files need explicit manifest entries. The inactive Contribution +guidelines context remains inactive; it is not added as a separate context +file. Rules scoped to other repositories are not copied here. + +## Settings still managed in Greptile + +The following observed controls do not have equivalents in the published +`.greptile/` schema used for this migration. They remain dashboard settings: + +- Review draft pull requests: enabled. +- Required confidence to pass the status check: 5/5. +- Image badges: enabled. +- Comment header: empty. +- Default coding-agent badges: no agents shown. + +Repository enablement, provider authentication, billing, integrations, and +learned memory also remain service settings. Auto-approval risk instructions, +if used later, are explicitly dashboard-only in Greptile's reference. Do not +invent config keys for unsupported controls. + +## Cutover and maintenance + +Greptile reads committed configuration from the reviewed branch. No reindex is +required. Local changes alone are not visible to the hosted reviewer. + +File-based review settings override dashboard defaults, but dashboard rules +and instructions are additive. The existing dashboard copies are still active. +This is a staged migration, not a completed dashboard cleanup. After a hosted +review verifies the configuration, an authorized maintainer can retire duplicate +dashboard context scoped to this repository. Do not remove shared organization +instructions while other repositories still depend on them. + +Keep each policy's body in `rules/`; update the `files.json` scope together with +the rule's MDC frontmatter. The chat-channel rule includes both the directory +and its sibling service entry point. See `rules/README.md` for authoring steps. + +References: [configuration and precedence](https://www.greptile.com/docs/code-review/greptile-config), +[supported fields and file references](https://www.greptile.com/docs/code-review/greptile-config-reference). diff --git a/.greptile/config.json b/.greptile/config.json new file mode 100644 index 00000000000..696d62f3cc6 --- /dev/null +++ b/.greptile/config.json @@ -0,0 +1,28 @@ +{ + "autoReview": ["open", "push", "rebase"], + "excludeAuthors": ["dependabot[bot]"], + "effort": "base", + "strictness": 3, + "shouldUpdateDescription": false, + "summarySection": { + "included": true, + "collapsible": false, + "defaultOpen": false + }, + "confidenceScoreSection": { + "included": true + }, + "issuesTableSection": { + "included": true + }, + "sequenceDiagramSection": { + "included": false + }, + "fixWithAI": true, + "statusCheck": true, + "statusCommentsEnabled": true, + "autoApprove": { + "enabled": false, + "riskCeiling": "low" + } +} diff --git a/.greptile/files.json b/.greptile/files.json new file mode 100644 index 00000000000..3738fce829c --- /dev/null +++ b/.greptile/files.json @@ -0,0 +1,24 @@ +{ + "files": [ + { + "path": "AGENTS.md", + "description": "Repository contributor instructions and routing to scoped architecture rules." + }, + { + "path": "docs/api/agents.md", + "description": "Agent API documentation retained from the existing Greptile agent-file context. This is API reference material, not contributor instructions." + }, + { + "path": "rules/pull-request-review.mdc", + "description": "Existing Paperclip review guidance for PR descriptions, UI evidence, and growth of large source files." + }, + { + "path": "rules/chat-channel-platform-boundaries.mdc", + "description": "Architecture requirements and review criteria for separating chat channel platform implementations from shared orchestration.", + "scope": [ + "server/src/services/chat-channels/**", + "server/src/services/chat-channels.ts" + ] + } + ] +} diff --git a/.greptile/rules.md b/.greptile/rules.md new file mode 100644 index 00000000000..f6b4ff9cf8c --- /dev/null +++ b/.greptile/rules.md @@ -0,0 +1,12 @@ +# Repository rule loading + +The `.mdc` files under `rules/` are repository review requirements. +`.greptile/files.json` identifies the rule files to read for matching source +paths. Read their full Markdown bodies and apply their review criteria and +allowed exceptions to the affected code. + +Each rule's YAML frontmatter declares its scope with repository-relative +`globs`, or `alwaysApply: true` for a repository-wide rule. Respect that scope; +do not apply a channel rule to unrelated subsystems. Keep the policy in the +referenced file as the source of truth rather than maintaining a separate copy +in dashboard instructions. diff --git a/AGENTS.md b/AGENTS.md index 55ee4734b84..c6b67349dc0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,6 +26,20 @@ authoring runbook for provider research, supported transport/auth patterns, credential handling, branding, implementation, testing, live proof, and PR submission. +### Scoped repository rules + +Before editing or reviewing files, read the matching rules in `rules/*.mdc`. +Each rule has Markdown content and YAML frontmatter: `globs` contains +comma-separated, repository-relative file patterns, and `alwaysApply: true` +means the rule applies throughout the repository. Read all rules whose scope +matches the affected files. These rules supplement this file and any applicable +nested `AGENTS.md` instructions. + +For `server/src/services/chat-channels.ts` and everything under +`server/src/services/chat-channels/`, read +[`rules/chat-channel-platform-boundaries.mdc`](rules/chat-channel-platform-boundaries.mdc). +See [`rules/README.md`](rules/README.md) for rule authoring and Greptile setup. + ## 3. Repo Map - `server/`: Express REST API and orchestration services @@ -162,6 +176,12 @@ Follow [the map contract](maps/README.md): update affected entries when mapped behavior or access changes, and run `node scripts/check-maps.mjs` after map edits. Its structural checks do not prove live behavior or authorization. +Storybook has a downstream [feature catalog](maps/storybook-map/README.md). +Use `maintain-storybook` for story/fixture drift and `review-storybook` for browser +evidence. Keep the feature-map skills independent of this catalog. After story +or declared review-source changes, reconcile affected entries and run +`node scripts/storybook-catalog.mjs`; its checks are not production acceptance. + Default local/agent test path: ```sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 31033ea98ca..dbb0efceceb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,6 +46,9 @@ PRs that follow this path are **much** more likely to be accepted, even when the ## PR Requirements (all PRs) +- Do not submit a PR that moves a code file from less than 1,000 lines of code to more than 1,000 lines of code. +- Do not submit a PR that includes code comments. + ### Use the PR Template Every pull request **must** follow the PR template at [`.github/PULL_REQUEST_TEMPLATE.md`](.github/PULL_REQUEST_TEMPLATE.md). If you create a PR via the GitHub API or other tooling that bypasses the template, copy its contents into your PR description manually. The template includes required sections: Thinking Path, What Changed, Verification, Risks, Model Used, and a Checklist. diff --git a/cli/src/__tests__/company.test.ts b/cli/src/__tests__/company.test.ts index ed693145875..fbe3fc0786b 100644 --- a/cli/src/__tests__/company.test.ts +++ b/cli/src/__tests__/company.test.ts @@ -62,6 +62,109 @@ function company(overrides: Record = {}) { }; } +function allImportEntities(): CompanyPortabilityPreviewResult["include"] { + return { + company: true, + agents: true, + projects: true, + issues: true, + skills: true, + }; +} + +function sourceCompanyManifest(): NonNullable { + return { + path: "COMPANY.md", + name: "Source Co", + description: null, + logoPath: null, + requireBoardApprovalForNewAgents: false, + feedbackDataSharingEnabled: false, + feedbackDataSharingConsentAt: null, + feedbackDataSharingConsentByUserId: null, + feedbackDataSharingTermsVersion: null, + }; +} + +function ceoManifest(): CompanyPortabilityPreviewResult["manifest"]["agents"][number] { + return { + slug: "ceo", + name: "CEO", + path: "agents/ceo/AGENT.md", + skills: [], + role: "ceo", + title: null, + icon: null, + capabilities: null, + reportsToSlug: null, + reportsToExistingAgentId: null, + reportsToExistingAgentSlug: null, + adapterType: "codex_local", + adapterConfig: {}, + runtimeConfig: {}, + permissions: {}, + permissionGrants: [], + budgetMonthlyCents: 0, + metadata: null, + }; +} + +function alphaProjectManifest(): CompanyPortabilityPreviewResult["manifest"]["projects"][number] { + return { + slug: "alpha", + name: "Alpha", + path: "projects/alpha/PROJECT.md", + description: null, + ownerAgentSlug: null, + leadAgentSlug: null, + targetDate: null, + color: null, + icon: null, + status: null, + executionWorkspacePolicy: null, + workspaces: [], + env: null, + metadata: null, + }; +} + +function kickoffIssueManifest(): CompanyPortabilityPreviewResult["manifest"]["issues"][number] { + return { + slug: "kickoff", + identifier: null, + title: "Kickoff", + path: "projects/alpha/issues/kickoff/TASK.md", + projectSlug: "alpha", + projectWorkspaceKey: null, + assigneeAgentSlug: "ceo", + description: null, + recurring: false, + routine: null, + legacyRecurrence: null, + status: null, + priority: null, + labelIds: [], + billingCode: null, + executionWorkspaceSettings: null, + assigneeAdapterOverrides: null, + comments: [], + metadata: null, + }; +} + +function openAiEnvInput(): CompanyPortabilityPreviewResult["envInputs"][number] { + return { + key: "OPENAI_API_KEY", + description: null, + agentSlug: "ceo", + projectSlug: null, + kind: "secret", + requirement: "required", + defaultValue: null, + portability: "portable", + }; +} + describe("company CLI commands", () => { let fetchMock: ReturnType; let logSpy: ReturnType; @@ -298,13 +401,7 @@ describe("buildCompanyDashboardUrl", () => { describe("renderCompanyImportPreview", () => { it("summarizes the preview with counts, selection info, and truncated examples", () => { const preview: CompanyPortabilityPreviewResult = { - include: { - company: true, - agents: true, - projects: true, - issues: true, - skills: true, - }, + include: allImportEntities(), targetCompanyId: "company-123", targetCompanyName: "Imported Co", collisionStrategy: "rename", @@ -334,49 +431,14 @@ describe("renderCompanyImportPreview", () => { companyId: "company-src", companyName: "Source Co", }, - includes: { - company: true, - agents: true, - projects: true, - issues: true, - skills: true, - }, - company: { - path: "COMPANY.md", - name: "Source Co", - description: null, - logoPath: null, - requireBoardApprovalForNewAgents: false, - feedbackDataSharingEnabled: false, - feedbackDataSharingConsentAt: null, - feedbackDataSharingConsentByUserId: null, - feedbackDataSharingTermsVersion: null, - }, + includes: allImportEntities(), + company: sourceCompanyManifest(), sidebar: { agents: ["ceo"], projects: ["alpha"], }, agents: [ - { - slug: "ceo", - name: "CEO", - path: "agents/ceo/AGENT.md", - skills: [], - role: "ceo", - title: null, - icon: null, - capabilities: null, - reportsToSlug: null, - reportsToExistingAgentId: null, - reportsToExistingAgentSlug: null, - adapterType: "codex_local", - adapterConfig: {}, - runtimeConfig: {}, - permissions: {}, - permissionGrants: [], - budgetMonthlyCents: 0, - metadata: null, - }, + ceoManifest(), ], skills: [ { @@ -395,73 +457,20 @@ describe("renderCompanyImportPreview", () => { }, ], projects: [ - { - slug: "alpha", - name: "Alpha", - path: "projects/alpha/PROJECT.md", - description: null, - ownerAgentSlug: null, - leadAgentSlug: null, - targetDate: null, - color: null, - icon: null, - status: null, - executionWorkspacePolicy: null, - workspaces: [], - env: null, - metadata: null, - }, + alphaProjectManifest(), ], issues: [ - { - slug: "kickoff", - identifier: null, - title: "Kickoff", - path: "projects/alpha/issues/kickoff/TASK.md", - projectSlug: "alpha", - projectWorkspaceKey: null, - assigneeAgentSlug: "ceo", - description: null, - recurring: false, - routine: null, - legacyRecurrence: null, - status: null, - priority: null, - labelIds: [], - billingCode: null, - executionWorkspaceSettings: null, - assigneeAdapterOverrides: null, - comments: [], - metadata: null, - }, + kickoffIssueManifest(), ], envInputs: [ - { - key: "OPENAI_API_KEY", - description: null, - agentSlug: "ceo", - projectSlug: null, - kind: "secret", - requirement: "required", - defaultValue: null, - portability: "portable", - }, + openAiEnvInput(), ], }, files: { "COMPANY.md": "# Source Co", }, envInputs: [ - { - key: "OPENAI_API_KEY", - description: null, - agentSlug: "ceo", - projectSlug: null, - kind: "secret", - requirement: "required", - defaultValue: null, - portability: "portable", - }, + openAiEnvInput(), ], warnings: ["One warning"], errors: ["One error"], @@ -545,13 +554,7 @@ describe("renderCompanyImportResult", () => { describe("import selection catalog", () => { it("defaults to everything and keeps project selection separate from task selection", () => { const preview: CompanyPortabilityPreviewResult = { - include: { - company: true, - agents: true, - projects: true, - issues: true, - skills: true, - }, + include: allImportEntities(), targetCompanyId: "company-123", targetCompanyName: "Imported Co", collisionStrategy: "rename", @@ -569,13 +572,7 @@ describe("import selection catalog", () => { companyId: "company-src", companyName: "Source Co", }, - includes: { - company: true, - agents: true, - projects: true, - issues: true, - skills: true, - }, + includes: allImportEntities(), company: { path: "COMPANY.md", name: "Source Co", @@ -592,26 +589,7 @@ describe("import selection catalog", () => { projects: ["alpha"], }, agents: [ - { - slug: "ceo", - name: "CEO", - path: "agents/ceo/AGENT.md", - skills: [], - role: "ceo", - title: null, - icon: null, - capabilities: null, - reportsToSlug: null, - reportsToExistingAgentId: null, - reportsToExistingAgentSlug: null, - adapterType: "codex_local", - adapterConfig: {}, - runtimeConfig: {}, - permissions: {}, - permissionGrants: [], - budgetMonthlyCents: 0, - metadata: null, - }, + ceoManifest(), ], skills: [ { @@ -630,45 +608,10 @@ describe("import selection catalog", () => { }, ], projects: [ - { - slug: "alpha", - name: "Alpha", - path: "projects/alpha/PROJECT.md", - description: null, - ownerAgentSlug: null, - leadAgentSlug: null, - targetDate: null, - color: null, - icon: null, - status: null, - executionWorkspacePolicy: null, - workspaces: [], - env: null, - metadata: null, - }, + alphaProjectManifest(), ], issues: [ - { - slug: "kickoff", - identifier: null, - title: "Kickoff", - path: "projects/alpha/issues/kickoff/TASK.md", - projectSlug: "alpha", - projectWorkspaceKey: null, - assigneeAgentSlug: "ceo", - description: null, - recurring: false, - routine: null, - legacyRecurrence: null, - status: null, - priority: null, - labelIds: [], - billingCode: null, - executionWorkspaceSettings: null, - assigneeAdapterOverrides: null, - comments: [], - metadata: null, - }, + kickoffIssueManifest(), ], envInputs: [], }, @@ -720,13 +663,7 @@ describe("import selection catalog", () => { it("includes extension file even when all entities are deselected", () => { const preview: CompanyPortabilityPreviewResult = { - include: { - company: true, - agents: true, - projects: true, - issues: true, - skills: true, - }, + include: allImportEntities(), targetCompanyId: "company-123", targetCompanyName: "Imported Co", collisionStrategy: "rename", @@ -744,24 +681,8 @@ describe("import selection catalog", () => { companyId: "company-src", companyName: "Source Co", }, - includes: { - company: true, - agents: true, - projects: true, - issues: true, - skills: true, - }, - company: { - path: "COMPANY.md", - name: "Source Co", - description: null, - logoPath: null, - requireBoardApprovalForNewAgents: false, - feedbackDataSharingEnabled: false, - feedbackDataSharingConsentAt: null, - feedbackDataSharingConsentByUserId: null, - feedbackDataSharingTermsVersion: null, - }, + includes: allImportEntities(), + company: sourceCompanyManifest(), sidebar: { agents: [], projects: [], diff --git a/cli/src/__tests__/worktree.test.ts b/cli/src/__tests__/worktree.test.ts index e4e57e09bfa..4396c2716c2 100644 --- a/cli/src/__tests__/worktree.test.ts +++ b/cli/src/__tests__/worktree.test.ts @@ -285,9 +285,37 @@ function buildSourceConfig(): PaperclipConfig { }; } +function writeSeedConfigFiles( + sourceConfigPath: string, + targetConfigPath: string, + targetRoot: string, + targetPaths: ReturnType, + sourceConfig: PaperclipConfig, + targetConfig: PaperclipConfig, +): void { + fs.mkdirSync(path.dirname(sourceConfigPath), { recursive: true }); + fs.mkdirSync(path.dirname(targetConfigPath), { recursive: true }); + fs.writeFileSync(sourceConfigPath, `${JSON.stringify(sourceConfig)}\n`); + fs.writeFileSync(path.join(path.dirname(sourceConfigPath), ".env"), "PAPERCLIP_INSTANCE_ID=source\n"); + fs.writeFileSync(targetConfigPath, `${JSON.stringify(targetConfig)}\n`); + fs.writeFileSync( + path.join(targetRoot, ".paperclip", ".env"), + `PAPERCLIP_HOME=${targetPaths.homeDir}\nPAPERCLIP_INSTANCE_ID=${targetPaths.instanceId}\n`, + ); +} + +function initializeTestRepo(repoRoot: string): void { + execFileSync("git", ["init"], { cwd: repoRoot, stdio: "ignore" }); + execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: repoRoot, stdio: "ignore" }); + execFileSync("git", ["config", "user.name", "Test User"], { cwd: repoRoot, stdio: "ignore" }); + fs.writeFileSync(path.join(repoRoot, "README.md"), "# temp\n", "utf8"); + execFileSync("git", ["add", "README.md"], { cwd: repoRoot, stdio: "ignore" }); + execFileSync("git", ["commit", "-m", "Initial commit"], { cwd: repoRoot, stdio: "ignore" }); +} + describe("worktree helpers", () => { it("uses the repo-local config for the current worktree", () => { - const targetRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-current-worktree-")); + const targetRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-current-worktree-")); try { const localConfig = path.join(targetRoot, ".paperclip", "config.json"); fs.mkdirSync(path.dirname(localConfig), { recursive: true }); @@ -307,7 +335,7 @@ describe("worktree helpers", () => { }); it("uses the repository config from a nested working directory", () => { - const targetRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-current-worktree-nested-")); + const targetRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-current-worktree-nested-")); try { execFileSync("git", ["init", "-q"], { cwd: targetRoot }); const nestedDirectory = path.join(targetRoot, "packages", "example", "src"); @@ -517,7 +545,7 @@ describe("worktree helpers", () => { }); it("requires the seed process to own the target embedded Postgres lifecycle", async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-live-target-")); + const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-live-target-")); try { fs.writeFileSync( path.join(tempRoot, "postmaster.pid"), @@ -531,18 +559,6 @@ describe("worktree helpers", () => { } }); - it("surfaces a credential-safe diagnostic when the target shuts down during restore", () => { - expect(formatWorktreeSeedFailureDiagnostic( - "restore", - new Error( - "Failed to restore seed.sql.gz: FATAL: the database system is shutting down; psql error: write EPIPE", - ), - )).toBe( - "Target embedded PostgreSQL shut down during restore. Stop any competing worktree service and retry the seed.", - ); - expect(formatWorktreeSeedFailureDiagnostic("migrations", new Error("secret connection failure"))) - .toBe("Seed failed during migrations."); - }); it("surfaces the missing credential artifact for authenticated seed validation", () => { expect(formatWorktreeSeedFailureDiagnostic( @@ -610,7 +626,7 @@ describe("worktree helpers", () => { itEmbeddedPostgres("recognizes positive legacy database schema evidence", async () => { const tempDb = await startEmbeddedPostgresTestDatabase("paperclip-worktree-legacy-evidence-"); onTestFinished(() => tempDb.cleanup()); - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-legacy-config-")); + const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-legacy-config-")); try { const configPath = path.join(tempRoot, "config.json"); const sourceConfig = buildSourceConfig(); @@ -644,7 +660,7 @@ describe("worktree helpers", () => { }); it("ensure-seeded seeds once and fast-exits on the verified manifest", async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-ensure-seeded-")); + const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-ensure-seeded-")); try { const sourceConfigPath = path.join(tempRoot, "source", "config.json"); const targetRoot = path.join(tempRoot, "worktree"); @@ -661,15 +677,7 @@ describe("worktree helpers", () => { serverPort: 3199, databasePort: 54999, }); - fs.mkdirSync(path.dirname(sourceConfigPath), { recursive: true }); - fs.mkdirSync(path.dirname(targetConfigPath), { recursive: true }); - fs.writeFileSync(sourceConfigPath, `${JSON.stringify(sourceConfig)}\n`); - fs.writeFileSync(path.join(path.dirname(sourceConfigPath), ".env"), "PAPERCLIP_INSTANCE_ID=source\n"); - fs.writeFileSync(targetConfigPath, `${JSON.stringify(targetConfig)}\n`); - fs.writeFileSync( - path.join(targetRoot, ".paperclip", ".env"), - `PAPERCLIP_HOME=${targetPaths.homeDir}\nPAPERCLIP_INSTANCE_ID=${targetPaths.instanceId}\n`, - ); + writeSeedConfigFiles(sourceConfigPath, targetConfigPath, targetRoot, targetPaths, sourceConfig, targetConfig); markWorktreeSeedPending({ configPath: targetConfigPath, sourceConfigPath }); const seedDatabase = vi.fn().mockResolvedValue({ @@ -715,7 +723,7 @@ describe("worktree helpers", () => { }); it("treats an unregistered markerless config as a normal non-worktree boot", async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-unregistered-markerless-")); + const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-unregistered-markerless-")); try { const configPath = path.join(tempRoot, "config.json"); fs.writeFileSync(configPath, `${JSON.stringify(buildSourceConfig())}\n`); @@ -740,7 +748,7 @@ describe("worktree helpers", () => { }); it("honors a legacy complete marker without resolving a seed source", async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-complete-marker-")); + const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-complete-marker-")); try { const configPath = path.join(tempRoot, "config.json"); fs.writeFileSync(configPath, `${JSON.stringify(buildSourceConfig())}\n`); @@ -763,7 +771,7 @@ describe("worktree helpers", () => { }); it("seeds a configured worktree with no seed markers when no legacy database is present", async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-unmarked-empty-")); + const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-unmarked-empty-")); try { const sourceConfigPath = path.join(tempRoot, "source", "config.json"); const targetRoot = path.join(tempRoot, "worktree"); @@ -780,15 +788,7 @@ describe("worktree helpers", () => { serverPort: 3194, databasePort: 54994, }); - fs.mkdirSync(path.dirname(sourceConfigPath), { recursive: true }); - fs.mkdirSync(path.dirname(targetConfigPath), { recursive: true }); - fs.writeFileSync(sourceConfigPath, `${JSON.stringify(sourceConfig)}\n`); - fs.writeFileSync(path.join(path.dirname(sourceConfigPath), ".env"), "PAPERCLIP_INSTANCE_ID=source\n"); - fs.writeFileSync(targetConfigPath, `${JSON.stringify(targetConfig)}\n`); - fs.writeFileSync( - path.join(targetRoot, ".paperclip", ".env"), - `PAPERCLIP_HOME=${targetPaths.homeDir}\nPAPERCLIP_INSTANCE_ID=${targetPaths.instanceId}\n`, - ); + writeSeedConfigFiles(sourceConfigPath, targetConfigPath, targetRoot, targetPaths, sourceConfig, targetConfig); const inspectLegacyDatabase = vi.fn().mockResolvedValue(null); const seedDatabase = vi.fn().mockResolvedValue(mockVerifiedSeedResult()); @@ -810,7 +810,7 @@ describe("worktree helpers", () => { }); it("adopts a markerless legacy worktree only after validating its database schema", async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-unmarked-legacy-")); + const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-unmarked-legacy-")); try { const sourceConfigPath = path.join(tempRoot, "source", "config.json"); const targetRoot = path.join(tempRoot, "worktree"); @@ -827,15 +827,7 @@ describe("worktree helpers", () => { serverPort: 3193, databasePort: 54993, }); - fs.mkdirSync(path.dirname(sourceConfigPath), { recursive: true }); - fs.mkdirSync(path.dirname(targetConfigPath), { recursive: true }); - fs.writeFileSync(sourceConfigPath, `${JSON.stringify(sourceConfig)}\n`); - fs.writeFileSync(path.join(path.dirname(sourceConfigPath), ".env"), "PAPERCLIP_INSTANCE_ID=source\n"); - fs.writeFileSync(targetConfigPath, `${JSON.stringify(targetConfig)}\n`); - fs.writeFileSync( - path.join(targetRoot, ".paperclip", ".env"), - `PAPERCLIP_HOME=${targetPaths.homeDir}\nPAPERCLIP_INSTANCE_ID=${targetPaths.instanceId}\n`, - ); + writeSeedConfigFiles(sourceConfigPath, targetConfigPath, targetRoot, targetPaths, sourceConfig, targetConfig); const seedDatabase = vi.fn(); await expect(ensureWorktreeSeeded( @@ -858,7 +850,7 @@ describe("worktree helpers", () => { }); it("managed ensure-seeded derives a valid source from the registered base workspace", async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-managed-seed-")); + const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-managed-seed-")); try { const baseRoot = path.join(tempRoot, "base"); const sourceConfigPath = path.join(baseRoot, ".paperclip", "config.json"); @@ -907,7 +899,7 @@ describe("worktree helpers", () => { it.each(["sibling", "foreign_instance", "symlink", "instance_mismatch"] as const)( "managed ensure-seeded re-derives a stale %s manifest source from registration", async (variant) => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), `paperclip-worktree-managed-${variant}-`)); + const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), `paperclip-worktree-managed-${variant}-`)); try { const baseRoot = path.join(tempRoot, "base"); const canonicalSource = path.join(baseRoot, ".paperclip", "config.json"); @@ -981,7 +973,9 @@ describe("worktree helpers", () => { ); it("ensure-seeded records a target shutdown diagnostic when restore fails", async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-ensure-seeded-failure-")); + expect(formatWorktreeSeedFailureDiagnostic("migrations", new Error("secret connection failure"))) + .toBe("Seed failed during migrations."); + const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-ensure-seeded-failure-")); try { const sourceConfigPath = path.join(tempRoot, "source", "config.json"); const targetRoot = path.join(tempRoot, "worktree"); @@ -998,15 +992,7 @@ describe("worktree helpers", () => { serverPort: 3198, databasePort: 54998, }); - fs.mkdirSync(path.dirname(sourceConfigPath), { recursive: true }); - fs.mkdirSync(path.dirname(targetConfigPath), { recursive: true }); - fs.writeFileSync(sourceConfigPath, `${JSON.stringify(sourceConfig)}\n`); - fs.writeFileSync(path.join(path.dirname(sourceConfigPath), ".env"), "PAPERCLIP_INSTANCE_ID=source\n"); - fs.writeFileSync(targetConfigPath, `${JSON.stringify(targetConfig)}\n`); - fs.writeFileSync( - path.join(targetRoot, ".paperclip", ".env"), - `PAPERCLIP_HOME=${targetPaths.homeDir}\nPAPERCLIP_INSTANCE_ID=${targetPaths.instanceId}\n`, - ); + writeSeedConfigFiles(sourceConfigPath, targetConfigPath, targetRoot, targetPaths, sourceConfig, targetConfig); markWorktreeSeedPending({ configPath: targetConfigPath, sourceConfigPath }); await expect( @@ -1044,7 +1030,7 @@ describe("worktree helpers", () => { }); it("serializes concurrent ensure-seeded calls across the seed marker lock", async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-ensure-seeded-lock-")); + const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-ensure-seeded-lock-")); try { const sourceConfigPath = path.join(tempRoot, "source", "config.json"); const targetRoot = path.join(tempRoot, "worktree"); @@ -1061,15 +1047,7 @@ describe("worktree helpers", () => { serverPort: 3197, databasePort: 54997, }); - fs.mkdirSync(path.dirname(sourceConfigPath), { recursive: true }); - fs.mkdirSync(path.dirname(targetConfigPath), { recursive: true }); - fs.writeFileSync(sourceConfigPath, `${JSON.stringify(sourceConfig)}\n`); - fs.writeFileSync(path.join(path.dirname(sourceConfigPath), ".env"), "PAPERCLIP_INSTANCE_ID=source\n"); - fs.writeFileSync(targetConfigPath, `${JSON.stringify(targetConfig)}\n`); - fs.writeFileSync( - path.join(targetRoot, ".paperclip", ".env"), - `PAPERCLIP_HOME=${targetPaths.homeDir}\nPAPERCLIP_INSTANCE_ID=${targetPaths.instanceId}\n`, - ); + writeSeedConfigFiles(sourceConfigPath, targetConfigPath, targetRoot, targetPaths, sourceConfig, targetConfig); markWorktreeSeedPending({ configPath: targetConfigPath, sourceConfigPath }); const seedDatabase = vi.fn(async () => { @@ -1094,7 +1072,7 @@ describe("worktree helpers", () => { }); it("records an interrupted phase before retrying to a verified terminal state", async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-interrupted-seed-")); + const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-interrupted-seed-")); try { const sourceConfigPath = path.join(tempRoot, "source", "config.json"); const targetRoot = path.join(tempRoot, "worktree"); @@ -1111,15 +1089,7 @@ describe("worktree helpers", () => { serverPort: 3196, databasePort: 54996, }); - fs.mkdirSync(path.dirname(sourceConfigPath), { recursive: true }); - fs.mkdirSync(path.dirname(targetConfigPath), { recursive: true }); - fs.writeFileSync(sourceConfigPath, `${JSON.stringify(sourceConfig)}\n`); - fs.writeFileSync(path.join(path.dirname(sourceConfigPath), ".env"), "PAPERCLIP_INSTANCE_ID=source\n"); - fs.writeFileSync(targetConfigPath, `${JSON.stringify(targetConfig)}\n`); - fs.writeFileSync( - path.join(targetRoot, ".paperclip", ".env"), - `PAPERCLIP_HOME=${targetPaths.homeDir}\nPAPERCLIP_INSTANCE_ID=${targetPaths.instanceId}\n`, - ); + writeSeedConfigFiles(sourceConfigPath, targetConfigPath, targetRoot, targetPaths, sourceConfig, targetConfig); markWorktreeSeedPending({ configPath: targetConfigPath, sourceConfigPath }); const interrupted = readWorktreeSeedManifest(targetConfigPath)!; fs.writeFileSync( @@ -1147,7 +1117,7 @@ describe("worktree helpers", () => { }); it("fails closed instead of racing to reclaim a stale seed lock", async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-ensure-seeded-stale-lock-")); + const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-ensure-seeded-stale-lock-")); try { const targetConfigPath = path.join(tempRoot, ".paperclip", "config.json"); const lockPath = path.join(tempRoot, ".paperclip", "seed.lock"); @@ -1413,7 +1383,7 @@ describe("worktree helpers", () => { }); it("copies the source local_encrypted secrets key into the seeded worktree instance", () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-secrets-")); + const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-secrets-")); const originalInlineMasterKey = process.env.PAPERCLIP_SECRETS_MASTER_KEY; const originalKeyFile = process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE; try { @@ -1452,7 +1422,7 @@ describe("worktree helpers", () => { }); it("writes the source inline secrets master key into the seeded worktree instance", () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-secrets-")); + const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-secrets-")); try { const sourceConfigPath = path.join(tempRoot, "source", "config.json"); const targetKeyPath = path.join(tempRoot, "target", "secrets", "master.key"); @@ -1473,7 +1443,7 @@ describe("worktree helpers", () => { }); it("creates an explicitly empty worktree without inherited signing secrets or deferred copying", async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-empty-")); + const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-empty-")); const originalCwd = process.cwd(); const originalJwt = process.env.PAPERCLIP_AGENT_JWT_SECRET; const originalSigning = process.env.PAPERCLIP_TOOL_ACTION_SIGNING_SECRET; @@ -1503,7 +1473,7 @@ describe("worktree helpers", () => { }); it("persists the current agent jwt secret into the worktree env file", async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-jwt-")); + const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-jwt-")); const repoRoot = path.join(tempRoot, "repo"); const originalCwd = process.cwd(); const originalJwtSecret = process.env.PAPERCLIP_AGENT_JWT_SECRET; @@ -1544,7 +1514,7 @@ describe("worktree helpers", () => { }); it("preserves repo-managed worktree checkouts when --force re-runs from the source repo", async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-force-preserve-")); + const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-force-preserve-")); const repoRoot = path.join(tempRoot, "repo"); const originalCwd = process.cwd(); @@ -1585,7 +1555,7 @@ describe("worktree helpers", () => { itEmbeddedPostgres( "seeds a local-trusted implicit board user without a credential account", async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-local-board-seed-")); + const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-local-board-seed-")); const originalCwd = process.cwd(); onTestFinished(() => { process.chdir(originalCwd); @@ -1687,7 +1657,7 @@ describe("worktree helpers", () => { itEmbeddedPostgres( "seeds a lagging source whose migration application order differs from filename order", async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-auth-seed-")); + const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-auth-seed-")); const originalCwd = process.cwd(); onTestFinished(() => { process.chdir(originalCwd); @@ -1866,7 +1836,7 @@ describe("worktree helpers", () => { ); it("avoids ports already claimed by sibling worktree instance configs", async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-claimed-ports-")); + const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-claimed-ports-")); const repoRoot = path.join(tempRoot, "repo"); const homeDir = path.join(tempRoot, ".paperclip-worktrees"); const siblingInstanceRoot = path.join(homeDir, "instances", "existing-worktree"); @@ -1947,7 +1917,7 @@ describe("worktree helpers", () => { }); it("reserves distinct ports for postgres-mode siblings under a custom worktree parent", async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-custom-parent-")); + const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-custom-parent-")); const homeDir = path.join(tempRoot, ".paperclip-worktrees"); const customParentDir = path.join(tempRoot, "custom", "workspace-lanes"); const firstWorktreeRoot = path.join(customParentDir, "lane-one"); @@ -2002,7 +1972,7 @@ describe("worktree helpers", () => { }); it("defaults the seed source config to the current repo-local Paperclip config", () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-source-config-")); + const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-source-config-")); const repoRoot = path.join(tempRoot, "repo"); const localConfigPath = path.join(repoRoot, ".paperclip", "config.json"); const originalCwd = process.cwd(); @@ -2027,7 +1997,7 @@ describe("worktree helpers", () => { }); it("preserves the source config path across worktree:make cwd changes", () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-source-override-")); + const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-source-override-")); const sourceConfigPath = path.join(tempRoot, "source", "config.json"); const targetRoot = path.join(tempRoot, "target"); const originalCwd = process.cwd(); @@ -2070,7 +2040,7 @@ describe("worktree helpers", () => { }); it("derives worktree reseed target paths from the adjacent env file", () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-reseed-target-")); + const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-reseed-target-")); const worktreeRoot = path.join(tempRoot, "repo"); const configPath = path.join(worktreeRoot, ".paperclip", "config.json"); const envPath = path.join(worktreeRoot, ".paperclip", ".env"); @@ -2102,7 +2072,7 @@ describe("worktree helpers", () => { }); it("rejects reseed targets without worktree env metadata", () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-reseed-target-missing-")); + const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-reseed-target-missing-")); const worktreeRoot = path.join(tempRoot, "repo"); const configPath = path.join(worktreeRoot, ".paperclip", "config.json"); @@ -2127,7 +2097,7 @@ describe("worktree helpers", () => { }); itEmbeddedPostgres("reseed preserves the current worktree ports, instance id, and branding", async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-reseed-")); + const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-reseed-")); const repoRoot = path.join(tempRoot, "repo"); const sourceRoot = path.join(tempRoot, "source"); const homeDir = path.join(tempRoot, ".paperclip-worktrees"); @@ -2232,7 +2202,7 @@ describe("worktree helpers", () => { }); it("restores the current worktree config and instance data if reseed fails", async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-reseed-rollback-")); + const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-reseed-rollback-")); const repoRoot = path.join(tempRoot, "repo"); const sourceRoot = path.join(tempRoot, "source"); const homeDir = path.join(tempRoot, ".paperclip-worktrees"); @@ -2341,18 +2311,13 @@ describe("worktree helpers", () => { }); it("copies shared git hooks into a linked worktree git dir", () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-hooks-")); + const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-hooks-")); const repoRoot = path.join(tempRoot, "repo"); const worktreePath = path.join(tempRoot, "repo-feature"); try { fs.mkdirSync(repoRoot, { recursive: true }); - execFileSync("git", ["init"], { cwd: repoRoot, stdio: "ignore" }); - execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: repoRoot, stdio: "ignore" }); - execFileSync("git", ["config", "user.name", "Test User"], { cwd: repoRoot, stdio: "ignore" }); - fs.writeFileSync(path.join(repoRoot, "README.md"), "# temp\n", "utf8"); - execFileSync("git", ["add", "README.md"], { cwd: repoRoot, stdio: "ignore" }); - execFileSync("git", ["commit", "-m", "Initial commit"], { cwd: repoRoot, stdio: "ignore" }); + initializeTestRepo(repoRoot); const sourceHooksDir = path.join(repoRoot, ".git", "hooks"); const sourceHookPath = path.join(sourceHooksDir, "pre-commit"); @@ -2389,7 +2354,7 @@ describe("worktree helpers", () => { }, 15_000); it("creates and initializes a worktree from the top-level worktree:make command", async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-make-")); + const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-make-")); const repoRoot = path.join(tempRoot, "repo"); const fakeHome = path.join(tempRoot, "home"); const worktreePath = path.join(fakeHome, "paperclip-make-test"); @@ -2399,12 +2364,7 @@ describe("worktree helpers", () => { try { fs.mkdirSync(repoRoot, { recursive: true }); fs.mkdirSync(fakeHome, { recursive: true }); - execFileSync("git", ["init"], { cwd: repoRoot, stdio: "ignore" }); - execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: repoRoot, stdio: "ignore" }); - execFileSync("git", ["config", "user.name", "Test User"], { cwd: repoRoot, stdio: "ignore" }); - fs.writeFileSync(path.join(repoRoot, "README.md"), "# temp\n", "utf8"); - execFileSync("git", ["add", "README.md"], { cwd: repoRoot, stdio: "ignore" }); - execFileSync("git", ["commit", "-m", "Initial commit"], { cwd: repoRoot, stdio: "ignore" }); + initializeTestRepo(repoRoot); process.chdir(repoRoot); @@ -2424,18 +2384,13 @@ describe("worktree helpers", () => { }, 20_000); it("no-ops on the primary checkout unless --branch is provided", async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-repair-primary-")); + const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-repair-primary-")); const repoRoot = path.join(tempRoot, "repo"); const originalCwd = process.cwd(); try { fs.mkdirSync(repoRoot, { recursive: true }); - execFileSync("git", ["init"], { cwd: repoRoot, stdio: "ignore" }); - execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: repoRoot, stdio: "ignore" }); - execFileSync("git", ["config", "user.name", "Test User"], { cwd: repoRoot, stdio: "ignore" }); - fs.writeFileSync(path.join(repoRoot, "README.md"), "# temp\n", "utf8"); - execFileSync("git", ["add", "README.md"], { cwd: repoRoot, stdio: "ignore" }); - execFileSync("git", ["commit", "-m", "Initial commit"], { cwd: repoRoot, stdio: "ignore" }); + initializeTestRepo(repoRoot); process.chdir(repoRoot); await worktreeRepairCommand({}); @@ -2449,7 +2404,7 @@ describe("worktree helpers", () => { }); it("repairs the current linked worktree when Paperclip metadata is missing", async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-repair-current-")); + const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-repair-current-")); const repoRoot = path.join(tempRoot, "repo"); const worktreePath = path.join(repoRoot, ".paperclip", "worktrees", "repair-me"); const sourceConfigPath = path.join(tempRoot, "source-config.json"); @@ -2463,12 +2418,7 @@ describe("worktree helpers", () => { try { fs.mkdirSync(repoRoot, { recursive: true }); - execFileSync("git", ["init"], { cwd: repoRoot, stdio: "ignore" }); - execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: repoRoot, stdio: "ignore" }); - execFileSync("git", ["config", "user.name", "Test User"], { cwd: repoRoot, stdio: "ignore" }); - fs.writeFileSync(path.join(repoRoot, "README.md"), "# temp\n", "utf8"); - execFileSync("git", ["add", "README.md"], { cwd: repoRoot, stdio: "ignore" }); - execFileSync("git", ["commit", "-m", "Initial commit"], { cwd: repoRoot, stdio: "ignore" }); + initializeTestRepo(repoRoot); fs.mkdirSync(path.dirname(worktreePath), { recursive: true }); execFileSync("git", ["worktree", "add", "-b", "repair-me", worktreePath, "HEAD"], { cwd: repoRoot, @@ -2496,7 +2446,7 @@ describe("worktree helpers", () => { }, 20_000); it("creates and repairs a missing branch worktree when --branch is provided", async () => { - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-worktree-repair-branch-")); + const tempRoot = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), "paperclip-worktree-repair-branch-")); const repoRoot = path.join(tempRoot, "repo"); const sourceConfigPath = path.join(tempRoot, "source-config.json"); const worktreeHome = path.join(tempRoot, ".paperclip-worktrees"); @@ -2505,12 +2455,7 @@ describe("worktree helpers", () => { try { fs.mkdirSync(repoRoot, { recursive: true }); - execFileSync("git", ["init"], { cwd: repoRoot, stdio: "ignore" }); - execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: repoRoot, stdio: "ignore" }); - execFileSync("git", ["config", "user.name", "Test User"], { cwd: repoRoot, stdio: "ignore" }); - fs.writeFileSync(path.join(repoRoot, "README.md"), "# temp\n", "utf8"); - execFileSync("git", ["add", "README.md"], { cwd: repoRoot, stdio: "ignore" }); - execFileSync("git", ["commit", "-m", "Initial commit"], { cwd: repoRoot, stdio: "ignore" }); + initializeTestRepo(repoRoot); fs.writeFileSync(sourceConfigPath, JSON.stringify(buildSourceConfig(), null, 2), "utf8"); process.chdir(repoRoot); diff --git a/doc/DATABASE.md b/doc/DATABASE.md index 2177689e1b9..959658e603a 100644 --- a/doc/DATABASE.md +++ b/doc/DATABASE.md @@ -701,6 +701,18 @@ this non-secret binding for this namespace; Slack bot installation does not use PKCE. Removal and manual recovery invalidate outstanding attempts under the same credential-mutation lease used by configuration. +### Slack manager workspace grants + +`chat_slack_manager_grants` stores a personal workspace authorization, scoped by +company, Paperclip user, Slack user, workspace and manager app. Access/refresh +credentials are references to user-scoped vault entries, never plaintext columns. +Revision fencing protects reauthorization from concurrent revocation. Per-method +rate-limit deadlines retain Slack's Retry-After across setup retries. + +`chat_slack_registrations.manager_grant_id` is optional, with a composite company +foreign key. Existing registrations remain customer owned with a null reference. +Deleting one endpoint removes its registration credentials and attempts, but does +not revoke grants used by other bots. See [Slack setup](connections/slack-setup.md). ## Transaction-aware delivery work signals @@ -743,3 +755,48 @@ Use the same root client for in-process writers. New queue insertion paths must register before writing, or they can remain unseen until the next startup or another notification. Tests should verify both the producer and its outer transaction boundary. + + +### Task workspace records (2026-10-09) + +Migration [0336_big_crystal.sql](../packages/db/src/migrations/0336_big_crystal.sql) makes `execution_workspaces.project_id` nullable and changes its project reference to `ON DELETE RESTRICT`: source-project policy cannot disappear while a workspace still depends on it. `issues.execution_workspace_id` remains the sole active task binding. Versioned selection/pending-selection JSON and a binding revision record intent and protect transitions; they are not additional active bindings. Accepting a new pending choice advances the revision without changing the active folder. Its receipt retains the original request revision, so current-choice retries are idempotent and superseded retries fail the revision check. + +`execution_workspace_repositories` is company-scoped inventory beneath an execution workspace. It records repository identity, contained relative path, requested ref, pinned commit, preparation state and idempotency receipts. Credentials are resolved at use and are never stored in this inventory. Connection/destination `execution_defaults` JSON contains optional typed project/workspace choices, never arbitrary host paths or commands. + +Migration [0337_exotic_proemial_gods.sql](../packages/db/src/migrations/0337_exotic_proemial_gods.sql) adds internal `execution_workspace_repository_roots` records. A company and hash of its canonical physical path uniquely identify a sticky `anonymous_only` or `managed` acquisition reservation. Short transactions serialize reservations with repository requests and credential promotion, preventing private publication after low-trust admission checks. Records have no workspace foreign key: mutable workspace metadata, relocation, archival or deletion cannot release a physical folder still accessible to an admitted run. There is no automatic mode change or reservation deletion. + +Migration [0338_absurd_vengeance.sql](../packages/db/src/migrations/0338_absurd_vengeance.sql) adds internal `execution_workspace_repository_access` provenance. Company/canonical-root source entries retain execution-workspace and source-project identities; reader entries retain the exact admitted run, task, agent and responsible user. New repository receipts also record their root at creation; legacy receipts receive no inferred origin. Shared project-source entries retain which physical owners require workspace write authority. Current admission, selection, requests and publication check the current task’s writer against those owners; retained past readers and file APIs still require only read authority. Genuine isolated copies retain source read access without requiring shared-folder write access. An existing linked worktree can recover a missing isolated-copy proof only after Git registration and its captured source owner are verified. Configured local sibling copies retain deny-only task/project source ownership on the bound task root before materialization; ordinary repository clones create no isolation proof. The shared root fence checks every reader against every source before admission and future publication, including tasks without repository requests. File APIs apply the same source privacy without writing provenance. These entries grant no authority and have no workspace or run deletion cascade; unavailable retained identity fails closed. Automatic provenance release is deferred, so a newly incompatible permission scope requires a separate physical root. + +Git-source capture checks the actual common Git storage directory and verified +checkout owners. Separately stored metadata does not make its parent a checkout. +When a linked worktree cannot identify its primary checkout, the service verifies +company-known configured, execution, and retained roots against that metadata +identity. A primary checkout identifies itself when its Git directory equals its +common directory, including separate metadata layouts. Verified primary sources +and linked worktrees with a verified registered primary avoid the fallback scan. + +Writable selection, source-copy preparation, admission, and publication check the trust policy of +current and retained physical source projects. A projectless alias cannot bypass +a source's sandbox requirement. An incompatible preset or boundary is rejected; +the check never silently changes the selected execution environment. Admitted +and retained readers use their persisted effective run policy; absence of a +retained low-trust policy counts as standard. Live policy edits cannot relabel +a standard local run as sandboxed. Read-only file access +keeps its existing authorization rules. + +Source-copy checks retain deny-only ownership on failure but create a new +isolated-copy proof only after physical identity, access, and trust checks pass. +For low-trust sandbox runs, the harness also disables host worktree provisioning +on fresh realization, reuse, and rebuild. This covers configured commands and +the repository's built-in provision script; an empty configured command alone +would still allow that fallback. The capability comes from the server's selected +trust policy and environment, never repository configuration. + +After source authorization and retained-reader checks pass, the existing +`source` and `project_source` kinds retain ownership on shared Git storage. +Removing the original checkout cannot release private committed files in surviving +worktrees. These records grant no authority and create no isolation proof. Denied +source captures leave no new records. Worktree realization uses a verified checkout +as its Git command directory, including when metadata sits inside another repository. + +No migration scans or copies existing agent home directories. Keep admitted native descriptors and recovery references until compatible controllers finish their persistence obligations. See [the architecture rollout contract](plans/2026-10-09-task-workspaces.md). diff --git a/doc/DEVELOPING.md b/doc/DEVELOPING.md index 233c2e5cf66..351d9f93246 100644 --- a/doc/DEVELOPING.md +++ b/doc/DEVELOPING.md @@ -35,6 +35,28 @@ New workflow versions merged into master then receive runner access without a separate SHA allowlist update. Dependabot leaves this first-party reference on master. +Runner selection calls `ci-routing.yml@master` on the dedicated +`paperclip-ci-routing-x64` AWS Fleet. The `paperclip-ci-routing` runner group +admits only `paperclipai/paperclip/.github/workflows/ci-routing.yml@refs/heads/master` +and repository ID `1170821064`. GitHub evaluates the canonical repository, author, and sender checks before +allocating reserved capacity. The validated admission snapshot (`AWS_CI_ROUTING_USER_IDS_SOURCE` and +`AWS_CI_ROUTING_USER_IDS`) must match `AWS_CI_TRUSTED_USER_IDS`. Refresh it through +the runner-group configuration helper when the roster changes. Missing, stale, +or malformed settings keep hosted routing available without evaluating JSON +in the job's placement expression. Unapproved PRs use hosted metadata routing; +approved manual canaries on master can use the reserved pool. The routing +workflow reads GitHub metadata without checking out or executing PR code. Its author, sender, rerun actor, +repository, head, and merge validation still decide whether tests can use AWS. +Unapproved test workloads continue to use GitHub-hosted runners. + +The routing Fleet has two warm On-Demand runners and a four-runner ceiling. +Test fleets cannot consume its runner slots. Hosted test queues therefore do +not block AWS runner selection. A manual dispatch of `ci-routing.yml` on master +checks placement without launching the test matrix; its non-PR event returns +the hosted test-runner output. Configure and verify the group and Fleet before +merging this caller. To roll back, restore the hosted gate and retain the routing +workflow authorization until assigned jobs finish. + Keep the `.github/**` rule in `.github/CODEOWNERS` and the active master ruleset's code-owner review requirement enabled. This covers the caller, the trusted workflow, and CODEOWNERS itself. Existing administrator pull-request bypasses @@ -102,6 +124,29 @@ API responses and MCP transports keep their existing compression behavior. The board UI Storybook keeps stories and Storybook config under `ui/storybook/` so component review files stay out of the app source routes. +Stories are grouped in `ui/storybook/stories//` and the sidebar follows +the feature map's sections and feature names. Shared fixtures live in +`ui/storybook/support/`. Keep each suite's explicit meta `id` and catalog `stableId` +when changing titles or locations so existing review links remain valid. + +Storybook's Overview mirrors every feature in the feature-map README, including +features without examples. Each feature's generated Overview lists its capabilities +in recipe order, related suites across primary homes, and missing examples. +`ui/storybook/generated/` holds the derived pages; the preview config's inline +`storySort.order` is generated too. Regenerate these outputs +with `node scripts/storybook-catalog.mjs --write`, not manual edits. Storybook +start/build rejects stale generated output. These guide pages are separate from +the authored story inventory and do not count as visual or behavioral coverage. + +Start from the downstream [Storybook feature index](../maps/storybook-map/INDEX.md) +to find existing stories by capability. Its [contract](../maps/storybook-map/README.md) +separates production subjects, prototype implementations, simulated data, and +source-review status. Use `maintain-storybook` to reconcile stories/fixtures with +the feature map, and `review-storybook` to exercise them with existing tools. +Run `node scripts/storybook-catalog.mjs` after affected story or review-source +changes; review drift before updating fingerprints, then regenerate with `--write`. +The feature map remains an upstream input and needs no Storybook-specific fields. + **Chat & Comments → Human identity** covers other-human names and avatars on left-aligned task messages alongside agents; only the viewer’s own messages are blue/right-aligned. Its review guide explains the data flow; the task journey uses representative fixtures in the production shell and thread with an agent and two humans, both @@ -1998,6 +2043,14 @@ pnpm secrets:migrate-inline-env --apply # apply migration ## Internal Connection Token Brokers +Paperclip Cloud connector identities support `development`, `test`, `staging`, +and `production`. To enroll a self-hosted instance against the test broker, set +`PAPERCLIP_CLOUD_CONNECTOR_BASE_URL=https://test.paperclip.app`; its environment +is inferred as `test`. Managed instances also receive +`PAPERCLIP_CLOUD_CONNECTOR_ENVIRONMENT=test`. Standard Cloud broker hosts reject +identities for another environment, and enrollment keys remain local to the +selected broker. + Connection token exchanges carry a resolved parent credential, so their URLs are public-only by default even in local/private deployments. To intentionally use an internal broker, configure an exact hostname allowlist: @@ -2239,3 +2292,20 @@ Chromium. The test starts its own loopback Vite server and mocks API responses; it needs no running Paperclip instance or provider credentials. The same spec lives in the default `test:e2e` discovery tree, so the existing Chrome CI shards run it on pull requests. + +### Workspace feature checks (2026-10-09) + +Task files, personal agent state (`AGENT_HOME`), and provider home are distinct. New filesystem tasks without an explicit source use a stable company/task directory. Debug workspace placement through the task workspace API rather than inferring it from organizational project or agent identity. + +Focused coverage lives in `task-workspace-binding.test.ts`, `task-workspace-repositories*.test.ts`, `task-workspace-tools.test.ts`, channel integration tests, and adapter-utils checkpoint/native persistence tests. Full handoff still requires the repository typecheck, test, and build commands. Remote transport fixtures establish protocol behavior; they do not qualify production provider throughput. + +Channel defaults can be configured in Apps connection settings and per destination. They affect new conversations only. A queued selection or repository request is applied on the next normal admission; configuration changes do not interrupt a live run. See [the architecture](plans/2026-10-09-task-workspaces.md). + +### Slack provisioning paths + +Self-hosted development continues to use independent Slack app setup. Managed +Slack installation is a disabled-by-default Cloud feature that also requires the +companion broker profile and Slack approval. Do not add manager client credentials +to a local `.env` or an open-source deployment. See +[Slack setup and Cloud qualification](connections/slack-setup.md) for the gates, +recovery behavior, and production-component stories. diff --git a/doc/PRODUCT.md b/doc/PRODUCT.md index e53dcafe773..6d408844d3c 100644 --- a/doc/PRODUCT.md +++ b/doc/PRODUCT.md @@ -67,6 +67,31 @@ Tasks have parentage. Every task exists in service of a parent task, all the way The current issue model includes stable issue identifiers, parent/sub-issues, blockers, a single assignee, comments, issue documents, attachments and work products, and review/approval handoffs. That structure keeps work inspectable by both the board and agents while still allowing agents to decompose work into smaller tasks. +When a task is assigned to the current human, the message composer defaults to +the latest agent that assigned it to them. If no assignment is available, it uses +the latest other commenter, or leaves the recipient empty. A manual recipient +choice stays selected while task data refreshes. + +### Task collection creation filters + +Tasks and project lists hide agent-created and system-created tasks by default. +The Filters menu's **show agent-created tasks** checkbox includes routine runs, +recovery/watchdog tasks, chat-channel tasks, and other automation. Channel tasks +remain bot-created for this filter even when a human starts the conversation. The choice is saved per company +and collection; Clear filters restores the hidden default. Nested lists preserve +ancestors and expandable subtasks as context without promoting unrelated automated +work to independent results. Agent profiles, routine histories, and task-detail +subtasks retain their full history. + +Inbox membership continues to follow human creation, assignment, comments, and +existing qualifying participation actions, including manually running a routine. +Routine ownership or responsibility alone does not subscribe a person to scheduled +runs; opening or reading a task is not participation. Inbox discovery searches and +Blocked hide unrelated automated tasks unless the checkbox is checked, while +preserving tasks admitted by the existing participation rules. Checking it does +not subscribe the user to all automation. Approvals and other non-task items keep +their existing rules. + ### Company Skills and Policy Company skills are shared operating capabilities, not privileged objects by default. Every authenticated agent in a company can create, import, install, edit, update, test, reset, and remove that company's skills unless the company has configured an explicit restriction. @@ -80,7 +105,11 @@ 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. +and deselection or upstream removal retains installed content. Disconnecting removes +the source after an explicit choice: keep independent editable copies in My Skills, +or delete its imported skills. Copies preserve agent assignments; skills still +used by agents must be detached before deletion. Already-disconnected sources +can be removed through the same flow. 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. diff --git a/doc/SPEC-implementation.md b/doc/SPEC-implementation.md index 00d0ac8b5b5..f32064cb813 100644 --- a/doc/SPEC-implementation.md +++ b/doc/SPEC-implementation.md @@ -1130,6 +1130,10 @@ 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`. +Live task and run toasts require personal task participation or an explicit user +mention, independently of read access. Agent ownership and responsible-user +attribution alone do not qualify. See [toast semantics](TOASTS.md). + 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. @@ -2077,8 +2081,17 @@ unavailable. Preserve current ownership and newer-work fences. See 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 + removed status; moves are removal plus discovery. Deselect retains content and assignments. Originals are read-only, with the existing independent fork workflow. +- Source disconnection requires a saved revision and an explicit copy/delete choice. + Remove the source and all entries to allow a fresh repository import, including + legacy disabled sources. Copy materializes complete local files in the caller's + My Skills folder, retaining skill IDs, keys, versions, and agent assignments. + Delete applies skills.remove to every linked skill (including deselected entries), + rejects skills still used by agents, and removes skills and source atomically with + the activity record. Copy applies skills.edit and skills.create to every linked + skill; failed writes or audit persistence leave the source and imported skills intact. + - 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 @@ -2160,6 +2173,37 @@ Company Settings → General can configure one shared API-key connection for opt The internal service provides local-only availability and bounded, reauthorized execution. It records metadata-only invocation history and independent fractional service charges in the existing cost ledger, applying company and applicable agent/project budgets under the accounting lock. Unknown dispatch charges retain reservations until audited resolution. Settings/testing require connection management permission; history uses existing cost visibility. See [decision-models.md](decision-models.md) for the contract, supported models, accounting, privacy, and endpoints. + +### Task workspace ownership (2026-10-09) + +A task's organizational project and filesystem workspace are independent. New tasks that need a filesystem and have no selected/configured workspace get a company/task directory, not the agent home. Plain tasks need neither Git nor a project. Existing bindings and admitted roots remain authoritative; changing project does not move files. Explicit workspace selections apply at a subsequent normal admission after prior execution and persistence obligations settle. + +Chat connection and destination settings may supply project/workspace defaults. Destination fields override connection fields; omitted values inherit and null clears the inherited choice. Resolve and authorize these defaults once for a new conversation, then retain the resulting task intent. Existing conversations do not change when settings change. Trust confinement still takes precedence. + +Agents inspect/select workspaces and request contained repository preparation through ordinary authorized APIs. Repository preparation is idempotent and may require the next admission; it never claims a host-only clone is available in an already-running remote session. Simultaneous tasks may still explicitly share one physical local/SSH folder. Remote sharing requires actual placement and coherent persistence capability. + +See [the layer architecture](plans/2026-10-09-task-workspaces.md) for resolution rules, module ownership, persistence versions, rollout, and verification. + ## Company fast-response service Companies may configure an optional shared API connection and model for short acknowledgements of accepted human turns. Fast responses use direct SDK inference alongside normal agent work, with a three-second admission deadline, bounded prompts/output, current authorization and independent `fast_response` accounting. They are server-marked platform receipts attributed to the agent, never completion, interaction or recovery evidence. Company-sponsored unlinked external senders are explicit; linked-user permission denials are never bypassed. See [fast-responses.md](fast-responses.md) for provider, delivery, history and endpoint contracts. + +### Task collection creation visibility (2026-10-13) + +Tasks and project collections default to hiding agent/system-created tasks and +provide a saved **show agent-created tasks** filter. Classify creation using +creator identity and origin; assignee/responsible users do not imply human +authorship. Routine executions remain automated even when manually triggered. +Creator-less ordinary tasks are system-created. All chat-channel tasks count as +bot-created for this filter, including human-initiated inbound conversations. +Human ordinary and onboarding tasks remain visible. Apply collection creation filters before search limits and +pagination, and retrieve hierarchy context separately under existing access checks. +Context does not count as independent results or board cards. Bounded context +reads retain loaded rows while users reveal more of the tree. + +Inbox membership, archives, unread state, and badges retain existing participation +rules, including assignment, comments, and manual routine runs. Responsibility and +read-only activity do not admit scheduled work. Inbox discovery/Blocked preserve +participated automation and hide unrelated automation by default. Agent profile +history, routine history, and task-detail subtasks remain complete. API consumers +that omit the optional creation filter retain their existing list behavior. diff --git a/doc/SPEC.md b/doc/SPEC.md index 6df1dbb2337..c56c8fd8f47 100644 --- a/doc/SPEC.md +++ b/doc/SPEC.md @@ -766,6 +766,13 @@ authentication, replay, document-sized limits, inspection, and deletion semantic A company may configure a shared decision model for optional Paperclip features. The instance owns credential resolution, authorization, budget admission, and attributable service charges. Company-sponsored background use is enabled by default during configuration; explicit opt-out persists. User and agent requests keep their own access boundaries and cannot become sponsored background requests after denial. Availability is a cheap local capability check, and metadata-only request history makes service usage inspectable. The implemented V1 contract is in [decision-models.md](decision-models.md). + +### Task workspace ownership (2026-10-09) + +Organizational projects, task files, personal agent home, and provider home are separate concepts. App and chat tasks share one workspace policy: preserve a binding or explicit configured folder, otherwise allocate a stable task directory. A repository is optional and can be prepared without creating a project. Optional connection and destination defaults feed the same task policy; they do not grant permissions or relocate existing conversations. + +Agents select authorized workspace/repository intent while the harness owns paths, preparation, persistence and safe transitions. Intentional simultaneous sharing of one physical local/SSH folder remains supported. Provider sessions keep their admitted root through completion and recovery. See [SPEC-implementation.md](SPEC-implementation.md) and [the layer architecture](plans/2026-10-09-task-workspaces.md) for the versioned contracts and rollout constraints. + ### Fast responses A company may use a shared API model to acknowledge accepted human messages across tasks, agent chat and external channels while normal work starts. Acknowledgements retain their platform provenance and cannot satisfy execution or completion requirements. The bounded inference, authorization, sponsorship and independent accounting contract is defined in [fast-responses.md](fast-responses.md). diff --git a/doc/TOASTS.md b/doc/TOASTS.md new file mode 100644 index 00000000000..5831cc6cb67 --- /dev/null +++ b/doc/TOASTS.md @@ -0,0 +1,75 @@ +# Toast semantics + +Toasts are transient feedback, not a company activity feed. Live task notifications +are personal to the connected human, including in multiplayer and `local_trusted` +mode (`local-board`). Access to a task does not subscribe someone to notifications. + +## Which tasks qualify + +A task qualifies when the viewer created it, is its current human assignee, +authored a comment, or performed a deliberate action included in the existing +Mine participation allowlist in +[`issue-participation.ts`](../server/src/services/issue-participation.ts). +Read receipts, denied requests, and automatic read-time recovery cleanup do not +count. Comment authorship follows Mine's existing historical participation rule. + +A task also qualifies when its current description or a non-deleted comment +contains an explicit mention of the viewer: `[Name](user://USER_ID)`. The shared +user-mention parser resolves the exact user ID; plain names and bare URLs do not +count. Existing comments are checked, so users need not have opened the task. +The event introducing the first mention can notify immediately. Removing the +only mention removes that qualification unless another participation rule holds. +Historical comment lookup uses an exact viewer-link regex backed by the existing +comment-body trigram index and returns at most one body for the shared parser. + +Agent ownership, primary-agent preference, task/run `responsibleUserId`, company +membership, and involvement in a parent task do not qualify on their own. +Activity by any person or agent can notify a qualifying viewer. These rules do +not change Mine membership, task permissions, assignment, or inbox archive state. + +## Events and suppression + +| Event | Toast behavior | +| --- | --- | +| Task created, updated, or commented on | Requires personal task eligibility. The viewer's own activity stays quiet. | +| Run failed, timed out, or cancelled | Requires eligibility for the persisted run's task. An event's task hint or agent identity is not sufficient. | +| Agent error without a task, or run without a task | No live toast. Status remains available in company views. | +| Run started or succeeded | No live toast. | +| Save, Copy, and other direct action feedback | Existing caller-owned feedback is unchanged. | +| Company join request | Existing join-request notification behavior is unchanged. | + +Eligibility is an additional gate, not a promise that every event produces a +toast. Existing rules still suppress activity for the task in view and run +outcomes already represented by the visible task/run tree. Comment-driven updates +can be combined with their paired comment event when that metadata is available. +Intentional operator interruptions and workspace contention do not announce run +failures. Duplicate terminal outcomes and failures older than five minutes (when +timestamps are available) remain quiet. Reconnect suppression, per-category rate +limits, content deduplication, and toast lifetime limits remain in effect. Hidden +pages reconcile their data on return without replaying a notification backlog. +Archiving a task is not a notification unsubscribe. + +## Delivery contract + +The company WebSocket authorizes each event before evaluating relevance. Its +viewer-specific copy of the shared `LiveEvent` envelope contains `toastEligible`. +Only `true` permits a live task/run toast. Missing/false values, unresolved viewer +identity, and relevance lookup failures stay quiet. Agent-authenticated sockets +do not receive human toast eligibility. This field is computed per connection, +never persisted or published on the shared company event bus. + +Authorized events still reach cache handling and component subscribers when +eligibility is false. Lookup failure is logged but does not discard the authorized +event. Existing task/run privacy checks and payload redaction remain in force. +When an authenticated comment event omits the comment ID, the visible task thread +refreshes through its authorized HTTP read instead of trying to hydrate the comment. +Closed sockets cannot deliver stale notifications after account/company changes. +No client-side task-list cache or new database migration is required. During a +mixed-version rollout, a new client receiving events from an older server stays +quiet until the server provides eligibility. + +## Verification + +See [`inbox-search/toasts`](../maps/feature-map/inbox-search.md#toasts) for the +two-user browser recipe and linked regression suites. The map describes coverage; +it is not evidence that a particular deployment passed that journey. diff --git a/doc/connection-intents.md b/doc/connection-intents.md index ef848e2abf1..c41e5d129bd 100644 --- a/doc/connection-intents.md +++ b/doc/connection-intents.md @@ -77,4 +77,4 @@ Task setup defaults to personal identity when supported and the requesting agent Service and native-authority tests cover discovery, current identity, company boundaries, cross-run deduplication, additive grants and installs, permission denial, resolution atomicity, restart delivery and stale assignment. `tests/e2e/in-feed-native/playwright.config.ts` starts source `test-drive` instances with fresh data directories and a deterministic fake Codex provider plus MCP server. It exercises the real native runner and gateway; it is fixture proof, not live Notion or GitHub proof. Run with `pnpm exec playwright test -c tests/e2e/in-feed-native/playwright.config.ts`. -Offline Storybook examples live in `ui/storybook/stories/in-feed-connections.stories.tsx`. Build with `pnpm --filter @paperclipai/ui build-storybook`, then run `pnpm exec playwright test -c tests/storybook-visual/in-feed-connections.config.ts`. The suite checks every independently addressable story in both themes, catches play-function failures, and saves screenshots. Live provider acceptance additionally requires a model credential and a test workspace/account; do not describe fixture results or local-trusted testing as authenticated/cloud acceptance. +Offline Storybook examples live in `ui/storybook/stories/connection-setup/in-feed-connections.stories.tsx`. Build with `pnpm --filter @paperclipai/ui build-storybook`, then run `pnpm exec playwright test -c tests/storybook-visual/in-feed-connections.config.ts`. The suite checks every independently addressable story in both themes, catches play-function failures, and saves screenshots. Live provider acceptance additionally requires a model credential and a test workspace/account; do not describe fixture results or local-trusted testing as authenticated/cloud acceptance. diff --git a/doc/connections/SLACK-TASK-TOOLS.md b/doc/connections/SLACK-TASK-TOOLS.md index 02b54daf82c..892426ac34c 100644 --- a/doc/connections/SLACK-TASK-TOOLS.md +++ b/doc/connections/SLACK-TASK-TOOLS.md @@ -198,16 +198,20 @@ people's bot DMs remain inaccessible. Scheduling uses ordinary Paperclip routine not a Slack-specific timer. Routine results are sent explicitly through the tool; ordinary task finals are not automatically broadcast to Slack. -## Require an at-mention for inbound messages +## Require an at-mention for inbound channel messages Slack app settings include **Require at-mention** directly below **Allow direct messages**. It defaults to on for new Slack connections. Existing connections -keep their saved setting and thread and DM behavior. -When enabled, each inbound message must explicitly @mention the connected bot, -including replies in existing channel threads and direct messages. DMs must also -be enabled with **Allow direct messages**. Unmentioned messages do not create -or continue a task. Queued messages are checked again before starting agent work. -If a queued run combines several messages, an unmentioned input cancels that -queued run when this setting is enabled. Send a new mention to start it again. +keep their saved setting; the setting applies only to channels. +When enabled, each inbound channel message must explicitly @mention the connected +bot, including replies in existing channel threads. Direct messages never require +an @mention, but must be enabled with **Allow direct messages** and pass the usual +sender authorization checks. Top-level DMs remain a linear conversation with +top-level replies; use a private channel for conversations organized into threads. +Unmentioned channel messages do not create or continue a task. Queued messages +are checked again before starting agent work. If a queued run combines several +channel messages, an unmentioned input cancels that queued run when this setting +is enabled. Send a new mention to start it again. Unmentioned DM inputs do not +trigger this cancellation. This setting does not cancel work that has already started or revoke its Slack tool access. diff --git a/doc/connections/slack-setup.md b/doc/connections/slack-setup.md new file mode 100644 index 00000000000..78abb8ef67f --- /dev/null +++ b/doc/connections/slack-setup.md @@ -0,0 +1,162 @@ +# Slack app ownership and setup + +Slack has one chat runtime and two provisioning paths. A saved connection's +`slackSetupMethod` selects provisioning; it does not change tools, scopes, +conversation routing, people permissions, or channel access. + +| Method | App creation and installation | Default | +| --- | --- | --- | +| `managed` | A personal Slack manager grant creates a child app; managed installation supplies its bot token. Slack consent/admin approval remains a fallback. | New drafts on qualified Cloud deployments only | +| `automatic` | A temporary app configuration access token creates a customer-owned app; the user approves its OAuth installation. | New self-hosted drafts and Cloud without managed availability | +| `manual` | User creates an app from the canonical manifest and enters credentials. | Legacy drafts with no saved method | +| `existing` | User supplies credentials for an existing app. | Explicit selection | + +Existing drafts and active bots retain their method. Enabling managed setup never +adopts existing apps. Choose agent offers **Use your own app** under **Advanced**, below Slash command, +when the instance supports managed setup and creation has not started. The own-app flow retains **Create manually** and **Use an existing +app** under Advanced. After creation dispatch, a managed draft cannot switch paths. + +Managed setup has two visible stops: Choose agent and Connect. **Continue** starts +manager authorization or installation in the saved workspace automatically. Multiple +workspaces still require a selection. Recovery actions remain explicit; opening +a failed or uncertain registration never retries creation automatically. +Creation, installation, message waiting, and success use the same production +components and server evidence as the own-app flow. A click or OAuth return does +not constitute successful configuration. + +## Cloud deployment prerequisites + +Managed setup is disabled by default. Qualification requires all of: + +- The instance chat-connector rollout gate and connection-management permission. +- Verified Cloud runtime identity, connector broker signing/sealing configuration, + and valid independently configured public board and webhook HTTPS origins. +- `PAPERCLIP_SLACK_MANAGED_SETUP_ENABLED=true` on the approved tenant deployment. +- The companion Cloud broker deployment with the explicitly enabled `slack.manager` + profile, approved Slack manager app, token rotation, and its client secret in + the broker's secret store. No manager client secret belongs in the tenant. + +The broker must advertise its manager app identity as well as the enabled profile. +Previously saved grants from a different manager app are not reused. A broker +without this capability leaves managed setup unavailable. Do not enable the tenant +gate until real Slack qualification has passed; fixture coverage does not prove +Marketplace eligibility or a workspace's approval policy. + +Self-hosted instances do not need a Paperclip account, broker enrollment, manager +credentials, or hosted-service calls. They continue using the own-app flow. + +## Personal workspace authorization + +Manager grants belong to a company **and one Paperclip user**, and bind a verified +Slack user, workspace, and manager app. Another company member must authorize their +own account, even for the same workspace. Access and rotating refresh tokens are +user-scoped vault entries. Refreshes serialize across connections, persist the new +refresh token, and require fresh authorization after an ambiguous exchange. + +Revoking a grant requests provider revocation, removes its local credentials, invalidates pending manager +authorization, and prevents future provisioning with it. The local activity records whether provider revocation succeeded. Provider errors never restore local access. It does not uninstall +apps or disable already-installed bots. Removing a bot deletes endpoint-specific +secrets and pending setup attempts; a reusable personal manager grant survives. +Provider-side app removal remains explicit through the saved Slack management URL. + +## Recovery + +- A dispatched creation with an unknown result is **uncertain**. Never automatically + create again. The user must inspect Slack and explicitly confirm that no app was + created before a new request identity is accepted. +- Denied or pending installation retains the app. Resume with the same grant or + complete the child app's state-bound OAuth approval; the installer must match the + grant's Slack identity before personal linking. +- Returned bot credentials are vaulted before inventory or runtime configuration. + Retry connecting reuses those credentials after network or inventory failures. + Rejected or under-scoped bot credentials instead reinstall the saved app; they + never trigger another app creation. +- Manifest updates finish before managed installation. Changes requiring updated + permissions therefore pass through installation again before activation. +- Expired or revoked manager grants require reconnection. Losing managed + availability pauses setup; it never changes the saved ownership method. +- Slack rate limits are grouped by manager/workspace/method. Provider `Retry-After` + is persisted across matching grants in the company, survives restarts, and blocks + early retries. Other tenant instances must observe Slack's limits independently; + no background loop retries app creation. + +All OAuth attempts bind company, actor/session, endpoint, registration revision, +and configured origins, expire after ten minutes, and are claimed once before +exchange. HTTP diagnostics redact manager tokens, refresh tokens, codes, and sealed +handoffs. Local activity records contain only IDs and safe outcome codes. This adds +no first-party telemetry. + +## Code and verification + +Server provisioning lives in `server/src/services/connectors/slack/setup/`; +`chat-slack-registration.ts` remains the compatibility facade. Shared completion +owns credential binding, personal linking, welcome messages, and verification. +Removal cleanup stays independently callable. Slack wizard components and stage +selection live in `ui/src/pages/apps/chat/slack/`. + +Production Storybook journeys: **Connections / Slack / Managed setup** and +**Connections / Slack / Automatic setup** (the persisted own-app automatic path). +Server integration cases cover personal grants, duplicate requests, uncertain +creation, approval fallback, refresh, revocation, removal, saved-credential recovery, +rate limits, and secret canaries. Browser cases are in +`tests/e2e/chat-adapters-ui-providers.spec.ts`; companion broker tests cover manager +user OAuth and the authenticated tenant handoff. + +Before enabling a deployment, create two test agents in the authorized workspace. +The second must reuse the personal grant without another configuration token or +manager authorization. Verify avatars, identity linking, signed inbound events, +and same-thread replies. Record real approval behavior separately from fixtures. + +### Managed setup during broker outages + +Cloud deployments configured for managed Slack keep the managed default when the broker profile is temporarily unavailable. New drafts remain on **Connect** and offer **Retry**. **Use your own app** remains available under **Advanced** on Choose agent before creation. Discovery loading or failure never selects the configuration-token page. Self-hosted instances without managed configuration still use their independent own-app flow; saved draft methods do not change automatically. Provisioning continues to require current broker availability. + +### Local managed-bot qualification + +Maintainers with an approved Slack manager app can exercise the real managed +journey from an authenticated local test drive exposed through HTTPS (including +TryCloudflare). This development transport talks directly to Slack and reuses +the production manager-grant vault, state claims, provisioning, completion, and +runtime. It does not exercise the Cloud broker's sealed handoff; qualify that +separately on staging afterward. + +Set `NODE_ENV=development`, `PAPERCLIP_SLACK_MANAGED_SETUP_ENABLED=true`, and +`PAPERCLIP_SLACK_MANAGER_DEVELOPMENT_CONFIG` to an absolute path outside the +checkout containing a mode-0600 JSON file with `appId`, `clientId`, and +`clientSecret`. Use only an authorized qualification manager app. No credentials +are shipped with Paperclip. Production ignores this development configuration, +and Cloud runtime identity continues to select the Cloud broker. + +Configure the local instance for authenticated public access, with its public +URL and chat webhook URL pointing to the HTTPS tunnel. Add the exact public URL +plus `/api/chat-slack/managed/oauth/callback` to the manager app's OAuth redirect +URLs, preserving any existing redirects. Start a **new** Slack setup: saved +own-app drafts retain their selected method. Authorize the manager app normally; +do not copy user grants from another instance. The second bot should reuse the +same local user's vaulted grant. A changed tunnel requires updating public +origins and the manager redirect before starting fresh authorization. + +Remove the temporary redirect when retiring the test drive and securely remove +its development credential file. Removing a bot does not revoke a manager grant +used by other bots. + +### Test messages from Settings + +**Send me a test message** sends a DM to the signed-in connection manager, +using their linked identity in this workspace. If this bot is not linked yet, +Paperclip can reuse that user's verified manager grant or another bot's linked +identity in the same company and workspace. Otherwise, the user selects their +Slack account and receives a private connection invitation in Slack. They must +open that invitation, sign in, and confirm before Paperclip links the account and +sends the test DM. The account picker loads further workspace pages on request. Existing revoked or conflicting identity links are not overwritten. +Direct messages must be enabled, and the user must be an active company operator. + +The action records a durable request before sending. Repeating the same request +returns its saved result; an uncertain delivery is never automatically resent. +**Open Slack configuration** opens the saved app's OAuth & Permissions page. +Legacy connections without an app ID open Slack's app list instead. + +An explicit managed reconnect reinstalls the saved app and verifies the replacement +bot credentials. Its request ID is saved before dispatch, so a duplicate completed +request does not reinstall again. Credential-configuration recovery still resumes +the saved token after a downstream failure. No reconnect creates another Slack app. diff --git a/doc/evals.md b/doc/evals.md index 97b17733496..6dd6e66ce00 100644 --- a/doc/evals.md +++ b/doc/evals.md @@ -452,3 +452,9 @@ See the [suite contract](../tests/runner-e2e/README.md#conversational-confirmati Hiring notification accounting now also requires exact completed action attribution. Missing native/provider ID mapping is uncomparable evidence; it must not be reported as a model task regression or waived through name/order matching. The fixture waits for both known completion callbacks and settled bracketed observations, including the gap before pending outbox work becomes a wake. Strict action replay and original machine verdicts are retained separately. The explicit-only [planning guidance utility comparison](../tests/runner-e2e/PLAN-TASK-GUIDANCE.md) measures task decomposition and handoffs with current, short, and disabled skills. + +The explicit-only [task-workspaces Product E2E suite](../tests/runner-e2e/README.md#projectless-task-workspaces-explicit-only) +covers projectless task files, deferred repository acquisition, controller restart +and nested Git/dirty-file persistence on legacy local, native local and native +Daytona OpenCode. Its three configured cells use independent filesystem, public +API and download oracles; no live qualification is implied by fixture registration. diff --git a/doc/execution-semantics.md b/doc/execution-semantics.md index f5632ecb178..238bf4b8b8a 100644 --- a/doc/execution-semantics.md +++ b/doc/execution-semantics.md @@ -29,6 +29,17 @@ An issue has at most one assignee. This is a hard invariant. Paperclip is single-assignee by design. +A legacy agent run can hand its own task to a human without cancelling itself. +The REST request must use the run's signed token and match the current agent, +company, task, and active run. The assignment and requested status persist while +the current turn finishes normally. This avoids losing the handoff when workspace +restoration takes longer than the stop timeout. The former agent no longer owns +the task. Its later task mutations are rejected, including under the update +transaction's task and run locks. Read and comment access follows normal policy. + +Board-initiated reassignments, agent API keys, native executions, and handoffs to +another agent still require the outgoing execution to stop before ownership changes. + ## 3. Status Semantics Paperclip issue statuses are not just UI labels. They imply different expectations about ownership and execution. diff --git a/doc/native-workspace-finalization-recovery.md b/doc/native-workspace-finalization-recovery.md index 2be7f1b67db..01201315417 100644 --- a/doc/native-workspace-finalization-recovery.md +++ b/doc/native-workspace-finalization-recovery.md @@ -9,6 +9,15 @@ A completed workspace barrier is reread under ownership before export, and a committed coordinator cannot be overwritten by a late failure receipt. +An unverified workspace owner keeps one board recovery action for its exact run +and owner fingerprint. Repeated recovery checks do not consume attempts or +rewrite that action. Task completion or cancellation does not prove the old +physical writer stopped, so reads and recovery sweeps retain this action while +the matching run still carries that owner receipt. After the receipt is cleared +or replaced, ordinary stale-action cleanup applies. Cleanup is attributed to +the system; historical cleanup from page reads does not add tasks to a person's +inbox or refresh issue activity timestamps. + The lock transaction holds no row locks. Ordinary progress and finalization receipts remain visible through the normal database pool. A dedicated connection outside the application pool is reserved for the duration of copyback and closed when it settles; even a one-connection application pool remains available for progress writes. Its run profile carries diff --git a/doc/observability.md b/doc/observability.md index 807ad61c80a..4a331900469 100644 --- a/doc/observability.md +++ b/doc/observability.md @@ -164,14 +164,19 @@ Those sections follow below. ## Native Runner Trace Spans Paperclip Runner task runs emit a single foldable OpenTelemetry trace. This is -the native-run trace schema version `2`. `task.run` is the only full-run root; +the native-run trace schema version `3`. `task.provider_session` is the native +provider lifecycle root; it includes preparation and provider execution, but +ends before outer workspace persistence. The heartbeat-owned local run-log +`task.run.measured` records terminal elapsed time including durable copyback; every other native span carries a real OpenTelemetry parent context rather than only a descriptive `parentName` field. +The run link in Honeycomb matches both `task.provider_session` for native runs and `task.run` for legacy adapters. It retains the exact hashed run-ID and service filters and groups by trace ID. The URL builder uses Honeycomb’s supported `in` operator for the two root names; see the [query specification](https://github.com/honeycombio/honeycomb-mcp#query-specification-for-run_query). This changes only the query link. The operator endpoint gate and span attribute allowlist remain unchanged. + The canonical lifecycle is: ```text -task.run +task.provider_session ├── heartbeat.queue ├── task.prepare │ ├── environment.startup @@ -253,10 +258,11 @@ and outcome. Native child attributes use the bounded `paperclip.native.span.` prefix and a closed key allowlist; values are limited to finite numbers, booleans, or short strings. Commands, arguments, environment values, paths, output, credentials, and raw identifiers are discarded by the -trace helper. `task.run.measured` remains in the local run log for compatibility -but is not exported as a second full-width OTel span. +trace helper. `task.provider_session.measured` records that same interval in the +local run log, without a duplicate full-width OTel span. The separate heartbeat +`task.run.measured` event is the authoritative end-to-end duration. Persisted `run.performance.span` events retain the v1 run-log schema and include -`traceSchemaVersion: 2` so local tooling can distinguish the hierarchy. +`traceSchemaVersion: 3` so local tooling can distinguish the hierarchy. Like every span in this document, native-run spans are opt-in. When `OTEL_EXPORTER_OTLP_ENDPOINT` is unset, the tracer remains a no-op; the local @@ -1054,16 +1060,16 @@ spans. A run-time (`sandbox.agentSession.sendInput`, `sandbox.agentSession.pollOutput`, `sandbox.callbackBridge.relayRequest`, or `sandbox.agentProcess`). Each run-time wrapper span parents to the live run span (`agent.turn` during the turn, -`task.run` otherwise). With no active trace context the exec span opens +`task.provider_session` otherwise). With no active trace context the exec span opens unparented. `sandbox.agentProcess` wraps the persistent streamed agent process. The process-session bridge launches it during `bridge.process-session`, so it opens -under `task.run` — no turn has started yet. It therefore overlaps the sibling +under `task.provider_session` — no turn has started yet. It therefore overlaps the sibling `agent.turn` rather than nesting under it or dangling off the short-lived bring-up step. The span ends when the process settles or when the bridge tears down, whichever comes first. The bridge tears down before the run root span ends, so -the span never outlives `task.run` even when the process lingers past teardown +the span never outlives `task.provider_session` even when the process lingers past teardown (the sandbox `execute` has no cancel, so a lingering process cannot be forced to resolve). diff --git a/doc/plans/2026-10-06-default-agent-runner.md b/doc/plans/2026-10-06-default-agent-runner.md index 71a2c7fa0ec..dfdfffa5ea7 100644 --- a/doc/plans/2026-10-06-default-agent-runner.md +++ b/doc/plans/2026-10-06-default-agent-runner.md @@ -1,6 +1,392 @@ # Runner defaults: staged delivery -Updated 2026-10-12. +## Production preparation checkpoint — 2026-10-13 + +The runtime prerequisite [#16028](https://github.com/paperclipai/paperclip/pull/16028) +is ready for review at `271d434c170e74bdedde65d0b13059a55419c943`. +[CI attempt 2](https://github.com/paperclipai/paperclip/actions/runs/38346601686) +passes 54 checks with four conditional skips; fresh Greptile is 5/5 and all four +threads are resolved. The first attempt's bounded Pi Node archive timeout is +retained. The unchanged-source retry passed without relaxing source or deadlines; +its underlying network/host cause remains unknown. The passing CI installation +records merge source `7a82a37438e6e448abc7cc5caa60dc2519a97be0`, distinct from the +PR head. The [final literal-source package campaign](https://github.com/paperclipai/paperclip/actions/runs/38347027535) +passes all four daemon producers, public Linux assembly and Apple Silicon, +Intel Mac and Linux ARM64 clean consumers. The Linux ACPX component probe uses +explicit installed authority; normal Mac/ARM native lookup rejects authority +overrides. Neither establishes final cloud onboarding/task acceptance. Small +report ZIP downloads did not complete in the browser and the CLI token is invalid; +no independent current-report rehash is claimed. + +GitHub's normal ready-PR gate still requires a code-owner review. On 2026-10-13, +normal protected squash auto-merge was enabled for exact `271d434c`; approval +remains pending and no slice was merged. The user authorizes normal protected sequential merges, +not admin bypasses, production deployment or provisioning-default promotion. +[Codex #15683](https://github.com/paperclipai/paperclip/pull/15683) remains clean +at `1a0ca66242121e39d88455aa2b52a07acc4a07d9`, based on the historical `5a9ce11`. +The parent has advanced and GitHub reports conflicts. Reconcile with the actual +merged parent before publishing prepared fixes or claiming final checks. The +Claude owner retains its branches, private installed-authority correction and +separate $50 qualification ceiling. Those fixes need package and live acceptance; +root does not rewrite the Claude branches. + +Fresh ordinary managed onboarding currently lacks the saved platform probe +before Auto resolution; an unqualified immutable OCI environment selects Legacy. +The prepared Codex-only UI correction reuses the existing permission-gated saved +probe on explicit Connect, refreshes scoped discovery and validates the selected +runtime. It retains actionable failures, explicit Legacy and current creation/ +discovery non-allocation. The existing explicit-only Product E2E cell gains fresh +wizard-owned qualification, two authoritative setup lease IDs, independently +checked physical-file output and follow-up continuity. Its fixture does not +create an agent or preprobe. Missing responses preserve unknown cost coverage. +No new suite, grader, automatic roster or local-prompt replacement is introduced. + +The privately composed source passes 44 targeted onboarding controls and 184 +existing Product E2E support controls, UI/E2E no-emit and guarded patch +applicability. The earlier full wizard controls pass 117 cases at their recorded +source. Independent review found an unanswered-setup accounting gap: four cases +fail before the fix; 117 first-task controls pass after. Comment cleanup preserves +353,839 composed executable tokens and semantic ASTs. Required onboarding maps +retain IDs, actual probe authority and source-only/not-run limitations; structural +checks pass 35 areas/171 IDs. Storybook metadata must be reviewed after actual +parent reconciliation; its catalog/script are absent from literal `1a0ca662`. +This private preparation is not integrated, published or live-qualified. + +A private reconciliation rehearsal composes exact Codex `1a0ca662` with exact +prerequisite `271d434c` without modifying either published branch. It retains +master's render helper, moved Storybook paths and additional catalog cases; the +managed onboarding cell is added to the existing catalog with 730 total cases. +The rehearsal is not an actual post-merge candidate and does not close hosted, +installed-package or live Cloud acceptance. Its narrow checks and source manifest +remain private until normal source reconciliation is authorized by the actual merge. + +The private root review reproduced four cache failures under production +`staleTime: 30_000`: setup omitted fresh availability reads, hid a discovery +error, and skipped qualification after an image change. Both managed Codex +setup reads now set `staleTime: 0`. The same existing controls cover cold and +production-cache defaults; 53 selected UI controls and matching-source UI +no-emit pass on Node 24.19.0. Earlier 44-case results remain tied to their +pre-correction source. This correction is private and requires ordinary +published-head review and full qualification after the protected parent merge. + +Cloud source `23531d729756ab0461a53f9358beb8df5c1781ed` uses named Daytona +snapshots without a verified final boot-image binding for fresh-company native +qualification. A read-only metadata request was rejected before execution because +automatic approval review requires explicit use of the particular API key. +Authorization is pending; no key was read or provider request sent. The expired +GitHub device code needs a new attended sign-in when the human is present. +Production Fleet Admin sign-in and a concrete current QA owner grant also remain +pending. The existing QA tenant remains pinned/user-managed and unqualified; +old auth probes and preview images cannot close the live gate. + +The original $250 ceiling and $248.179254104 held/consumed ledger remain intact; +unknown charges and cleanup holds are not certified invoice balance or available +funding. Codex retains its bounded $2.50 setup/two-task-turn reservation plus $2 +cleanup. The expanded manual first-task cell has no separate paid reservation. +No provider turn, paid environment allocation, local Docker/Cargo/native build, +rollout or default promotion occurred in this preparation. The preserved local +preview remains `http://127.0.0.1:3125/COD/agents/all`, literal behavior source +`fe1ecf1c3449c21d6e6f0c9d9772d1949b80ea82`; its data and recorded runners remain +untouched. Root does not upgrade existing agents. + +Next gate: obtain actual code-owner review and restored GitHub CLI authentication, +then perform the normal exact-head parent merge and reconcile Codex/Claude with +its actual master commit. Compose these prepared fixes, retain required maps and +fresh review/checks, resolve the Cloud boot-identity boundary through authorized +metadata, and qualify the final immutable managed candidate. Actual private +Cloud distribution, warm/cold signup, useful task/follow-up, existing-agent +preservation and production-image rollback/forward-recovery checks remain open. +The stack is not production-ready and production deployment remains unauthorized. + +Evidence: `/private/tmp/pr2-271d434-final-qualified-ready-receipt.json`, +`/private/tmp/pr2-managed-onboarding-accounting-root-review.json`, +`/private/tmp/ci-artifacts-271d434-installed-native-lookup-audit-receipt.json` and +private composed source manifests. Public PR bodies contain the exact current +checkpoint; historical receipt sources remain literal. + + +Updated 2026-10-13 UTC. + +## Fresh cloud onboarding remains unqualified + +The source audit found two ordinary-use gaps in the dependent release. Codex +onboarding resolves Auto from saved environment facts before setup testing. +The normal setup path does not perform the saved environment platform probe. +An immutable OCI environment without prior facts therefore creates a Legacy +agent. Existing managed E2E fixtures explicitly probe first; those fixtures do +not prove fresh onboarding. A private UI correction is being tested against +the existing onboarding suite. It uses the existing permission-gated saved +probe on explicit Connect, refreshes company-scoped discovery, then tests the +selected runtime. Creation and discovery still allocate nothing. Probe errors +must stay actionable, and permissions must not broaden. + +Cloud master `23531d729756ab0461a53f9358beb8df5c1781ed` distributes named +Daytona snapshots. Its recorded runtime image digest identifies the app copied +into that snapshot, not the final boot image. Current source does not bind a +fresh company's qualification to a verified boot identity. The pinned official +Daytona SDK accepts snapshot IDs, but does not guarantee a boot-verified digest. +A read-only lookup of the existing snapshot was rejected before execution by +automatic approval review because direct credential authorization was missing. +The exact limited authorization is now requested. No key was read or request +sent. Source audits and CLI version probes do not close live cloud acceptance. + +These are release blockers for the Codex-default and actual Cloud distribution +stages, not new runtime changes in this prerequisite. No source is merged or +production-ready. The original budget holds, local preview, existing runners, +remote-build boundary and no-production-deployment boundary remain intact. + +## Held-authorization fixture correction + +The ordinary reconciliation candidate `dae808fe02694d13d322c535944910813b21f6d6` +has fresh Greptile 5/5, all four resolved threads, passing isolated native +compilation and the required browser aggregate. Its runner shard passes both +previously corrected cases, but fails a different `authorize_before_bootstrap` +cell: the same implicit one-second polling deadline expires before the fixture +installs its release callback. The shard records 1,672 passes, one failure and +three skips. The exact failure and gate receipt remain: +`/private/tmp/ci-final-dae808-ci-review-failure-receipt.json`. + +The existing fixture now signals actual entry after installing the release +callback in the exact before-bootstrap authorization hook. Race that signal +against the original recovery request under the unchanged five-second bound. +All authority, stage, denied-work, command queue, process, provider-state, +durable-byte and cleanup assertions remain. On canonical Node 24, controlled +1,250 ms scheduling delay reproduces the old undefined-callback assertion and +passes after correction. An injected early recovery error surfaces unchanged. +The original hosted cause remains inconclusive; this proves fixture weakness +and preserves the next diagnostic. These are historical-mock-binary controls, +not final installed runtime qualification. Matching-source independent review +is clear. Receipts: +`/private/tmp/ci-final-dae808-held-authorization-signal-receipt.json` and +`/private/tmp/pr2-dae808-held-authorization-independent-review.json`. + +The already-running package campaign +https://github.com/paperclipai/paperclip/actions/runs/38342104102 +is immutable to literal `dae808` throughout: daemon, assembly and installed +consumer checkouts use its explicit 40-character SHA, while the tooling checkout +uses the captured workflow SHA. Publishing this test-only correction cannot +change that run's source. Let it finish without rerun or cancellation, keep its +receipts literal and overlap fresh current-head CI with its remaining steps. +No new package campaign starts until the existing one is terminal. + +Both worktrees and root's test-drive data stay preserved. Its loopback health +is OK at literal `fe1ecf1c34`, recovery ready, zero active runs; a sandbox-blocked +read was corrected with the authorized read-only loopback path, without restart +or build. Root HEAD stays `1a0ca662`. Codex and Claude reconciliation/acceptance, +normal code-owner approval, private Cloud composition and production baseline +remain pending. No merge/deployment/default promotion, paid provider call, +new paid host or local native/Docker build occurred. + +## Final CI fixture corrections and current master + +Published prerequisite `16a53254c495f3e49a28cc911f5dcd60c41779fc` +has fresh Greptile 5/5 and all four review threads resolved. Full CI records +52 successful checks, four conditional skips and two failures: the runner +Vitest first shard and its aggregate. The actual failing shard passes 1,671 +cases and skips three; its two failures remain recorded in +`/private/tmp/pr2-16a532-runner-vitest-failure.log`. + +The durable-resume case exceeded its outer five-second test envelope. Reuse +Claude owner's existing correction from `c60bf4b6`, changing only that case's +outer limit to 15 seconds. On canonical Node 24, controlled 3,800 ms fake +latency reproduces the old outer timeout, while corrected and ordinary cases +pass. Internal 500 ms/5 second turn deadlines, distinct attempts, same thread, +effect deduplication and the complete usage assertions remain unchanged. +Receipt: `/private/tmp/pr2-16a532-durable-resume-correction-receipt.json`. + +The warm-recovery callback fixture polled callback installation with Vitest's +implicit one-second wait, despite its existing five-second request bound. +Replace polling with actual callback-entry notification raced against request +settlement under that same bound. Controlled 1,250 ms scheduling delay fails +the original assertion and passes the correction; an early recovery error now +surfaces unchanged. All fence, snapshot, retry and side-effect assertions remain. +The original hosted request outcome was hidden, so its underlying cause remains +inconclusive until fresh hosted CI. These controls use recorded historical mock +binaries and do not qualify a final native package. Receipt: +`/private/tmp/ci-final-16a532-warm-callback-signal-receipt.json`. +Independent matching-source review of both test-only changes is clear: +`/private/tmp/pr2-16a532-fixture-corrections-independent-review.json`. + +Current master `e168512f7bdbb6af6ea01954c6d7ed80b91e903e` merges cleanly. +Runner/surfaces/server/E2E no-emit checks pass on the composed source. Maps +remain aligned at 35 areas/171 IDs and token gates pass. Master changed three +activity/inbox/recovery recipes without synchronizing 25 catalog fingerprints. +Independent inspection verifies those entries are exact inherited master drift; +all 92 declared non-map story/component/wrapper/fixture inputs are unchanged. +Only their guarded fingerprints and a source-only review note are synchronized. +Existing 24 unreviewed/one current classifications, IDs, fixtures and INDEX stay +unchanged. The catalog check passes 223 files/2,210 stories. No new live or +visual coverage is claimed for the inherited backend requirements. + +The exact `16a532` remote package campaign is +https://github.com/paperclipai/paperclip/actions/runs/38338806076. +The campaign completed successfully. All four native producers, public Linux +package assembly and the Apple Silicon, Intel Mac and Linux ARM64 installed +consumers pass. The earlier Intel timeout remains a separate failed receipt; +this success does not establish its cause or a diagnostic-code fix. +This remains literal source evidence after the current master/test changes. +No queued job is rerun or obsolete package result relabeled. + +Next action: preserve the completed package result, publish this ordinary master +reconciliation with both scoped fixture corrections, require fresh full CI and +review, and qualify the final package source. The prerequisite remains draft, +auto-merge off, and actual code-owner review is pending. Codex stays `1a0ca662` +until actual prerequisite merge and source reconciliation; the Claude owner +retains its branches and separate $50 allowance. Final immutable managed QA, +actual private Cloud distribution, warm/cold signup and backward-image or +forward-recovery evidence remain required. Production Admin sign-in is pending. +Nothing is merged, deployed or production-ready. The idle root test-drive and +its data are preserved. Paid holds remain $248.179254104/$250; no provider turn, +new paid host, local native/Docker build, rollout or default promotion occurred. + +## Master reconciliation and installed-package checkpoint + +The published prerequisite `64c074fb8da1b31f8d6042c4fef48dc4164e759b` +passes 54 current-head checks with four conditional skips and fresh Greptile 5/5. +All four review threads are resolved. The actual hosted transport suite passes +246 tests and startup trust passes 22; this closes the earlier local fake-binary +limitation at that literal source. Full workspace checks, Linux DB controls, +Rust suites, isolated native compilation and all eight browser shards pass. +Receipt: `/private/tmp/pr2-64c074-full-ci-review-0144-receipt.json`. + +The [existing final package campaign](https://github.com/paperclipai/paperclip/actions/runs/38332921463) +completed on literal `64c074`. All four native producers, public Linux package +assembly, Apple Silicon and Linux ARM64 installed consumers pass. Intel Mac +fails during initial `npm install --ignore-scripts --omit=dev`, with `ETIMEDOUT` +after the existing 10-minute bound. It never reaches lifecycle or native runtime +assertions. The log establishes an installation timeout, not its network/host +cause and not a runner failure. Intel installed execution remains unqualified. +Keep the failed receipt; do not relax assertions or relabel the successful +platforms as a complete campaign. No automatic retry was dispatched. + +Master then advanced to `f329b24ee12496e710ad17377ec7716e0eee22ef`, adding +separately merged durable task workspaces. It conflicts only in the test-home +helper/test and the Storybook review record. Both independent additive resolution +proposals are preserved. The cleanup keeps guarded read-only permission recovery +and its unrelated-ENOTEMPTY negative, adopts master's POSIX error assertion, +and retains master's new isolated-home identity/symlink protections byte-for-byte. +Both source-only Storybook review appendices remain; the catalog merges without +new story/fixture edits or changed review classifications. The task-workspace +migration and runtime changes come from master, not this PR's feature scope; +final image/migrator and backward-image proof must include them. + +The composed worktree passes six existing home controls, with zero failures or +skips, plus runner/surfaces/server/E2E TypeScript checks, token gates, both map +checks (35 areas/171 IDs) and Storybook structure (223 files/2,210 stories). +Only scratch-local TypeScript preparation ran; root's idle test-drive and data +stay unchanged. The contribution check has no below-1,000-to-above-1,000 code +crossing. The two whitespace lines already in master's unrelated privacy test +are not modified; the PR diff against master is clean. The review remains 63 +files. These composed checks have their working-tree source identity and are not +claimed as execution of the old published commit. Receipts are +`/private/tmp/pr2-f329-uncommitted-home-controls-receipt.json` and +`/private/tmp/pr2-f329-composed-source-checks-receipt.json`. + +The existing Mac installer now retains bounded npm timing/progress diagnostics +in its already-uploaded receipt path before cleanup. Only allowlisted metadata +is retained; argv, URLs, environment, raw stderr and exception text are omitted. +At most two 16 KiB file tails and 32 entries per tail are read. Symlink exclusions +and the original exception identity remain. Failure is explicitly FAILED, +unqualified and runtime-not-attempted. The existing 10-minute deadline and all +package, lock, payload, lifecycle and startup oracles are unchanged. Three narrow +cases extend the existing suite; all 24 pass on canonical Node 24, and independent +matching-source review is clear. This adds diagnostics, not an installation +success or explanation of the old timeout. Receipts: +`/private/tmp/pr2-mac-initial-install-diagnostics-repair-receipt.json`, +`/private/tmp/ci-final-mac-install-diagnostics-independent-review.json` and +`/private/tmp/pr2-f329-install-diagnostics-node24.log`. + +Next action: publish the ordinary master-reconciliation commit, require fresh +full CI/review and exact-source package qualification, then normal code-owner +review and merge. Prior `64c074` CI/install receipts remain literal after this +source change. The Claude owner is waiting for the exact canonical parent before +its next composed campaign; keep all its Claude fixes and separate $50 allowance. +Codex remains `1a0ca662` until actual prerequisite merge and reconciliation. +Final immutable OCI QA onboarding/task/follow-up, private Cloud composition, +warm/cold signup and backward-image or forward-recovery proof remain required. +Production Fleet Admin sign-in and code-owner approval are still pending. +No slice is merged or production-ready. Paid holds remain $248.179254104/$250; +no new paid/provider action, local native/Docker build or rollout took place. + +## Production readiness: owned startup correction + +The human requests production readiness; preparation and normal sequential +merges remain authorized. Production deployment and provisioning-default +promotion remain separate actions. Nothing in this checkpoint is merged or +deployed, and the stack is not production-ready. + +Runtime prerequisite behavior is `ce23716503a2c9158b5a7ab74ab61571ff4ee7a3` +on `codex/runner-native-platform-prerequisites`, in +[#16028](https://github.com/paperclipai/paperclip/pull/16028). Final qualification +reproduced close returning before credential preparation drained, followed by +registration and launch. Both direct close and real driver cancellation failed +on literal `5a9ce11`. The initial shared repair from Claude commit `463ebb` is +reused, with independently identified duplicate-admission, shutdown-readiness and +resume ordering corrections. All five source findings are closed in matching +source review. Absolute cleanup deadlines, primary cancellation errors, +unauthenticated-process protection and existing agents' runner choices remain. + +The existing suites pass 32 credential-free controls across three files; both +canonical runner TypeScript no-emit checks pass. No new suite, local Rust/Docker +build, provider call or credential-content read was used. Actual daemon +readiness/recovery controls remain **hosted-CI required**: local worktrees lack +the existing fake-Codex/fake-harness executables. The failed local native attempt +never reached the readiness assertion and is not qualification. Receipts are +`/private/tmp/pr2-463ebb-owned-startup-review-closure-receipt.json` and +`/private/tmp/ci-final-owned-startup-five-gap-closure-review.json`. + +Normal automatic merge is disabled and the prerequisite is draft while this +correction receives fresh full CI, package qualification and 5/5 review. +Earlier green checks at `5a9ce11` remain historical. Actual code-owner approval +is still required; no protection is bypassed. The updated runs/adapters map pair +records source-only scope, bringing the prerequisite review to 63 files after the required Storybook +catalog review below. + +Codex remains `1a0ca66242121e39d88455aa2b52a07acc4a07d9` in +[#15683](https://github.com/paperclipai/paperclip/pull/15683), based on the previous +prerequisite. The Claude owner retains its branches, cached guided-setup fix, +verification and separate $50 allowance. Coordinate reconciliation of the common +repair before rewriting those branches; don't duplicate it in a leaf slice. + +The idle [local preview](http://127.0.0.1:3125/COD/agents/all) still serves literal +behavior `fe1ecf1c34`, recovery ready and zero active runs. Its four saved +legacy/native configurations and two historical native records remain preserved; +it has not been refreshed to this new runtime. Final local/managed acceptance +must use the tested source, independent file checks and follow-up continuity. + +The prepared private execution checklist reuses existing evals, installation, +Cloud composition and campaign paths. Required holds are final source/image/ +migrator, the actual verified-master private distribution, pinned QA onboarding, +warm/cold signup, production baseline and previous-image compatibility. Production +Fleet Admin sign-in is pending. Current paid holds remain $248.179254104 of $250; +unknown/cleanup holds stay reserved, and the unstarted managed envelope is $2.50. +No new image, paid resource, rollout or default promotion was dispatched. + +Master reconciliation uses `4b958ecbd8f33a7782fe6f29ec825f80f636b833`. +The new contribution rules are satisfied: 157 added comment annotations were +removed from 35 files, with identical executable JavaScript/TypeScript emission, +parsed workflow values, 77 action pins and shell commands. No code file crosses +from below 1,000 lines to above that limit. Final focused checks pass 248 tests +(107 workflow/package, 54 runner, 42 server, 45 E2E support). Both runner no-emit +checks and the canonical server/E2E no-emit checks pass with isolated matching +workspace dependencies. Only JavaScript/TypeScript preparation ran locally; +root preview links and data stayed unchanged. The earlier linked-source failures +remain recorded and are superseded only by the new canonical check receipt. + +The existing Storybook catalog has 21 source-review fingerprint updates because +its declared runs/adapters map input changed. All affected stories, subjects, +wrappers and fixtures remain byte-identical to master. Review classifications +stay unchanged. The catalog check passes 223 files/2,210 stories; this is +structural/source evidence, not visual or live proof. Source receipts are +`/private/tmp/pr2-9f59-comments-cleanup-receipt.json`, +`/private/tmp/pr2-storybook-owned-final-receipt.json` and +`/private/tmp/pr2-frozen-canonical-noemit-receipt.json`. + +Next action: publish this reviewed prerequisite correction, require fresh hosted +gates, reconcile Codex and Claude with its actual source, then resume the existing +remote package/immutable preview and dedicated staging qualification. Merge ready +slices normally in the agreed order; actual production readiness requires the +private distribution and rollback/forward-recovery evidence afterward. ## CI fixture, network policy and map synchronization checkpoint @@ -1200,6 +1586,32 @@ to run. The actual legacy Codex task completed and its independently read file contained the exact two requested lines. The same-task follow-up is running. No second task, agent hire, or automatic runner conversion was used. +Next action: commit and push the narrowed candidate, rerun its checks and fresh +review, then update the PR and existing evidence report. Start the Codex-default +slice after the user chooses to proceed. Merging and deployment remain outside +this task. + +### Occupied credential fixture follow-up — 2026-10-12 + +Current-head `021df207` Runner Vitest 2 failed during fixture preparation with +`EADDRINUSE`, before credential staging. Reused the shared correction from +`50e7554069a31af72069e7632e9ae6151019f449`: prepare a fresh home with two +deliberately occupied candidates, preserve the foreign listener, and retain the +actual staging denial and unchanged auth-file assertion. The three-line code +annotation was removed to follow CONTRIBUTING; its explanation lives here. + +Only fixture preparation retries, up to eight fresh homes. It releases only its +owned partial or unselected listeners, then runs credential staging once. A bind +race during that production call remains a visible failure and receives no +retry. No production credential or permission behavior changed. + +A controlled occupied-port baseline reproduces the old setup failure. All 42 +existing credential controls pass on Node 24 with isolated synthetic data and +zero provider calls or native builds. Comment-only removal preserves the tested +executable structure; the suite was not repeated. Evidence is recorded in +`/private/tmp/ci-final-021df2-shared-credential-quorum-receipt.json` and its +comment-closure receipt. Fresh published-head CI and review remain required. + ### 2026-10-13 Published canary and fresh staging qualification The human merged prerequisite #16028 at actual Core master `d67e3507b23041212fa26302ce6de078c5ebcc05` and asked to cancel the existing timer, then test the published npm canary locally and on a new staging stack. The `finish-runner-default-stack-and-merge` heartbeat was deleted. Codex defaults #15683 remains separate and unmerged. No production or provisioning-default promotion is authorized. diff --git a/doc/plans/2026-10-08-instance-sleep-and-scheduler-inventory.md b/doc/plans/2026-10-08-instance-sleep-and-scheduler-inventory.md index e40716791f3..e479786f8b9 100644 --- a/doc/plans/2026-10-08-instance-sleep-and-scheduler-inventory.md +++ b/doc/plans/2026-10-08-instance-sleep-and-scheduler-inventory.md @@ -399,6 +399,13 @@ flowchart LR explicit deep readiness still tests DB health, and signed claim/bootstrap mutations retain their existing validation and tracking. + Implementation progress: `/api/liveness` reports startup state and the public + commit without auth/session resolution, SQL, or provider I/O. It is mounted + after hostname and signed-bootstrap validation, before standby/auth, and its + exact GET/HEAD route stays reachable without adding a sleep hold. `/api/health` + retains database readiness and signed bootstrap. Callers must opt into the + liveness endpoint; this does not claim all external probes are migrated. + - [ ] **DB-05 — Audit external probes and configured endpoints.** Inspect the fleet gateway, uptime monitors, deployment checks, DB pool settings, and Neon suspend configuration. The [ECS example](../../docker/ecs-task-definition.json) @@ -444,8 +451,26 @@ Longer polling intervals alone do not complete an item. running sessions and pauses without rescheduling until an endpoint change. Tests exercise real producers with deferred fast paths disabled, retry and startup recovery, stale-send quarantine, and zero scans after settlement. - Remaining **1s** maintenance: provider runtimes, Telegram maintenance, failed - chat-run retries, run milestone/wake-notice projection, GitHub operation + Failed chat-run retries now also use commit wakes and durable deadlines from + existing action rows. Issued retries honor `retryAt`; processing claims wait + for both stale-claim expiry and any saved retry time. Startup recovers unfinished + requests. Claim and settlement writes signal in their own short transactions, + with agent wakeup outside those transactions. Empty and terminal queues disarm. + Telegram maintenance now has separate commit-driven discovery and action + workers. Discovery restores legacy subscription obligations on startup and + after endpoint or chat-connection configuration commits, without recurring + empty scans. Startup walks keyset pages with at most 100 endpoints per run, + so endpoints beyond the first page remain discoverable. Configuration commits + carry connection hints through outer settlement and uncertainty recovery; + they only revisit the changed connection. Failed discovery retains its page, + and concurrent edits queue a fresh scoped pass. Existing action rows provide retry and stale-claim + deadlines. Stop-subscription repair parks while the endpoint or connection + is inactive; registration and removal keep their existing eligibility. + The explicit bounded recovery helper remains available. Provider subscription + proof, credential ownership, and idempotent provider mutations stay in the + Telegram service. Provider responses are simulated in the PostgreSQL tests. + Remaining **1s** maintenance: provider runtimes, + run milestone/wake-notice projection, GitHub operation recovery, and Teams protocol maintenance. GitHub pre-ingress recovery must still inspect provider history: a lost callback has no local queue row. This slice does **not** yet make the whole chat subsystem quiet. Independent diff --git a/doc/plans/2026-10-09-task-workspaces.md b/doc/plans/2026-10-09-task-workspaces.md new file mode 100644 index 00000000000..c12873ddc10 --- /dev/null +++ b/doc/plans/2026-10-09-task-workspaces.md @@ -0,0 +1,153 @@ +# Selected architecture — 2026-10-09 + +This design addresses PAP-171: a short agent turn paid minutes of workspace hydration and copyback because a projectless chat task inherited a large agent fallback directory. It covers app/API and chat tasks, including optional channel defaults, independent project and filesystem choices, explicit physical sharing, and deterministic harness preparation. Two architecture workers received the same full plan and grounding. Both candidates and an independent review used the inherited parent model; this is not cross-model consensus. + +## Caller contract first + +App/API/chat issue creation supplies typed workspace intent to the same workspace owner. Chat supplies optional defaults and authenticated source context, never paths or credentials. Task creation is transactional and cheap. Run preparation resolves an already-bound workspace or deterministically creates a company/task directory; provider, budget, run ownership and terminal disposition stay with heartbeat. The workspace owner does not become a second heartbeat orchestrator. + +Agent tools inspect workspace, discover existing authorized resources, prepare a repository, and explicitly select a workspace. Callers choose intent, never host shell commands or arbitrary destination paths. A repository checkout is optional and never requires a project. Root changes apply only at a safe subsequent admission; existing provider roots are immutable. Ordinary project edits do not move files. + +## Synthesis decision + +Base A retains issues.executionWorkspaceId as the single active binding and execution-workspace service as the mutation owner. Reject B's replacement binding/realization tables: existing environment leases and native descriptors already own physical and recovery facts. Graft B's narrow provider-facing context, durable settlement by run ID (reload accepted result/source evidence), explicit retention references, atomic repository subtree publication and project-deletion safety. Keep source-project authorization distinct from organizational task project. + +Parent scores: A 4/4/4/4/4; B 4/3/4/4/2 on ownership, preserved invariants, origin/tool parity, sync improvement, migration feasibility. Cross-judge independently chose A with the same scores. Neither candidate is implemented unchanged: their broad prepareRun/settleRun facade must not move unrelated execution orchestration into the workspace module. + +## State and boundaries + +- `issues.executionWorkspaceId`: canonical active binding. Workspace selection/revision and pending intent are typed/versioned task fields, not a second binding. Atomic issue lock/CAS and idempotent mutation receipts protect transitions. +- `execution_workspaces`: filesystem ownership/lifecycle and optional source project. A task-owned workspace has stable company/task identity, independent of agent. Existing rows/paths retain identity. Preserve historical privacy-source intersection. Project deletion cannot cascade needed workspace/recovery state; retain the source policy tombstone or reject deletion until explicit lifecycle cleanup. +- Repository inventory: one company-scoped child table for verified repository identity, relative path, pinned ref/branch, preparation operation/state. No plaintext credentials. Unique contained destination and source intent suppress concurrent duplicate preparation. Resolve symbolic/default ref once; another ref never silently switches a dirty checkout. +- Existing environment orchestration/leases: machine/provider placement and physical ownership. Workspace service asks it to realize selected intent. Reuse sharedWorkspaceKey/canonical paths; same logical ID does not imply same machine. Provider sessions and credentials remain agent/run scoped. +- Existing native sync/descriptors: admitted run baseline, exact source retention, immutable manifests/seeds and durable completion. Generic manifest/transport code stays in adapter-utils. AGENT_HOME retains its separate store and policy. +- Run lifetime and disposition remain owned by heartbeat. Workspace timings wrap actual filesystem work; provider turn is a child interval. Native admission ordering is unchanged: durable ownership/input before corresponding external effects, never reconstruct an admitted root from fresh defaults. +- Routes and runner authority translate/authenticate and call the domain owner. Share validators and generate tool descriptors from the canonical protocol action definitions. No parallel tool-only policy implementation. + +## Resolution and transition rules + +1. Already-admitted run: use immutable recorded input/recovery evidence. +2. New admission: eligible pending explicit selection, only after prior process and persistence obligations settle. +3. Otherwise current durable binding. +4. Unbound task: explicit selection > eligible parent inheritance > snapshotted resource/endpoint defaults > configured project policy > explicit configured adapter cwd > task directory. + +Explicit invalid/unauthorized choices fail with actionable errors, never fall back to home. Existing session cwd becomes a compatibility binding only with verified provenance. No eager scan/copy/migration of legacy homes. Workspace-free providers remain workspace-free. Existing low-trust/native-chat confinement stays authoritative and cannot be relaxed by defaults. + +Typed selection vocabulary: task_directory; existing execution workspace; configured project source with shared or managed-isolated policy. Operator cwd compatibility is internal and cannot be submitted by an ordinary agent. New repository paths use existing `.paperclip-repositories/` namespace; existing root checkouts retain their paths. + +Source inspection and isolated copies require source-project read access. Writable shared roots additionally require the internal `project:write_workspace` authorization action. It reuses existing `tasks:assign` and `tasks:assign_scope` grants for the source project and actual assignee, but evaluates only the source project’s assignment policy: continuing a task does not reassign its protected task, parent, or agent. Task lineage still enforces low-trust containment, and the responsible-user intersection applies independently. The resolved strategy determines sharing: only `git_worktree` creates a separate checkout; an isolated mode label does not exempt a source-root strategy. Board requests use the task's actual assignee when checking scoped grants. Check that authority at selection, task creation, repository mutation, and each new admission; revocation blocks future use without changing already-admitted recovery inputs. + +Independent repository acquisition does not inherit private-repository authority from a chat endpoint's sponsor. Effective low-trust tasks cannot request new controller-backed repository acquisition, including anonymous public URLs; provider full-auto permissions do not override this capability boundary. Pending or preparing intents fail before Git after trust is downgraded. Already-ready public checkouts may be reused only after validating their confined directory, persisted pin and Git origin without cloning, repair or lazy object fetch; managed receipts remain forbidden. Resolve current agent, task, organizational project and workspace-source policies together with the retained run policy at request and admission. Existing configured-source authorization remains the path for private sources. A trust change must fail before cloning or reusing existing private bytes. Empty-inventory admission, authorized file/discovery reads and already-admitted immutable recovery remain available. Trusted routine and local-board acquisitions retain their existing authority contract; agent requests still require the persisted run responsible identity. Trusted controller acquisition has no per-repository byte or cumulative storage quota in this feature, so trusted full-auto acquisition remains an operator resource risk; a general storage quota system is deferred. + +Admission with no repository requests remains heartbeat-owned and requires no additional repository-mutation permission. Low-trust admission still rejects managed acquisition bytes in the same physical folder. Immutable recovery uses its admitted folder and skips new repository acquisition even if the task's current binding has changed. A reservation-only recovery hook fences legacy admitted host roots before input capture or provider dispatch. Local recovery uses its immutable local root; sandbox recovery obtains the host path from its verified immutable sync descriptor. SSH recovery uses its remote filesystem directly and does not require a host snapshot descriptor. Older projectless native runs may use the exact persisted run-ID workspace binding without a workspace row. + +Task creation supplies an explicit internal prospective-task context to workspace selection. An admitted agent choosing an existing workspace for a child must have an authoritative saved run, a matching company and agent identity, and a verified parent lineage. The server checks current parent and prospective child policies against the saved run boundary and the physical source policy before saving the child. The creator run belongs to its existing task, not the unsaved child. Ordinary existing-task selection and writes still require their exact saved task/run identity; a parent ID alone does not enable the creation path. + +Independent acquisition reserves a company-scoped canonical physical root as either `anonymous_only` or `managed` in an internal durable record. A short database fence serializes these reservations with repository requests and credential promotion. Low-trust use rejects every managed intent, including a clone still in staging, and reserves anonymous use before capture. Future managed requests cannot publish into that reserved root. The reservation survives controller restart, workspace aliases, metadata replacement, relocation and archival; changing the acquisition mode requires a separate physical root. No database lock remains held through cloning, input capture or provider execution. Anonymous admission also checks the readable candidate repository namespace against company-managed receipt identities, including on an existing reservation. A published identity collision is denied even when the old receipt path is unreadable; mutable ownership markers cannot authorize it. This conservatively requires a fresh root when anonymous and managed publications share an identity. Reservations also compare retained overlapping parent/child roots; component-boundary siblings remain independent. Initial reservations compare legacy managed intents through accessible canonical ancestors, conservatively rejecting overlapping inaccessible paths while unavailable unrelated historical folders do not block admission. Arbitrary external file writers remain subject to the existing shared-folder constraints. + +Shared physical roots also retain source workspace and admitted reader identities separately from acquisition mode. Empty inventories do not bypass peer privacy: admission and recovery record the reader; requests and publication must remain readable to every retained reader. Admission registers the root before preparing configured sibling repositories. Each local sibling source is authorized before materialization, including local file URLs without a configured cwd and reuse of an existing sibling checkout. File-URL-only sources keep ordinary clone semantics; their source permissions still follow the copied files. Its captured task and project owners are retained on the bound task root before copying, so staging, published and detached files keep source privacy. A publication callback rechecks source permissions and every retained destination reader. Ordinary sibling clones add only read restrictions; they never create a worktree-isolation proof. The fence uses authoritative workspace/run/task identities and current source privacy, never writable Git ownership markers. Ordinary heartbeat-owned admission to the same task's sole source adds no repository-mutation requirement, while any peer source or task requires the full read intersection. Missing retained identity fails closed; automatic release is deferred. Current writers must also satisfy `project:write_workspace` for every shared physical source project, including configured aliases with no execution row. Sharing classification is retained separately from read provenance and survives pointer changes. Selection checks without registering a reader; admission and publication independently recheck current writer authority. Proven isolated worktree copies require source read authority; past readers never receive an added mutation requirement. + +Repository identity alone does not prove that a workspace published into a particular folder. Available unrelated copies remain independent; retained sources and ambiguous unavailable legacy publishers still constrain access. Physical-source authority includes canonical ancestor and descendant overlaps, current configured/execution roots, and retained source/readers. Every admission/publication rechecks current roots even after a reader has been recorded. Configured workspace move/removal and project removal preserve source provenance before their first admission too. Valid local file URLs and absolute repository paths also identify physical configured owners when no usable cwd exists. The same source resolver protects copying, file reads and configuration detachment. File listing, previews and downloads use the same read-only physical-source resolution. Those reads create no policy or reader records. Git worktree preparation checks actual checkout and common-repository source roots before copying. Server-owned captured source identities are retained on the isolated target before Git/provisioning and rechecked before exposure. The isolated_source and isolated_project_source text kinds preserve task/project privacy before any copying. They are deny-only intent, not proof of isolation. The separate isolated_copy proof is recorded only after Git validates a registered linked worktree; the ancestor-source write exemption also requires the target to remain a real worktree. Preparation may temporarily allow a validated missing/empty target, without persisting that exemption. An intact existing linked worktree without a proof record is first verified against the captured source and Git registration. Only that verification can establish its proof and let it resume after an upgrade or interrupted preparation; occupied ordinary folders remain strict. Existing ordinary folders and exact/descendant target owners keep their write requirements. Deny-only copy provenance survives permission revocation or source deletion during preparation, including failed copies. Managed local sources used by main or referenced projects receive the same checks and committed staging ownership before Git reads bytes. Closed-worktree reopening commits source ownership before its lifecycle transaction, uses read-only checks under that lock, and retains completed-copy evidence after releasing it. Source and destination identities must remain unchanged; no company root fence is acquired while the lifecycle lock is held. Explicit child source choices, including clearing inherited linkage with null, take precedence over parent workspace inheritance; omitted choices retain inheritance and normal project defaults. + +Reopening has no selected execution lease. Before rebuilding a worktree, resolve the bound task’s current task, assignee, organizational-project and filesystem-source policies inside the lifecycle transaction. Any effective low-trust policy suppresses both configured and built-in host provision scripts. Missing referenced authority or denied policy blocks reopening; trusted provisioning keeps its existing behavior. A board comment does not authorize sandbox repository scripts to execute on the host. + +Admission derives controller provisioning from the authoritative selected environment, independently of task trust and filesystem permissions. Sandbox and SSH selections suppress configured and built-in controller provision scripts during creation, reuse and rebuild, including host runtime provisioning. Read-only source copying remains available for genuine isolated worktrees; source write grants and board sponsorship do not authorize controller execution. Selected isolated sources are copy-only for controller provisioning even when execution is local: explicit, channel, inherited and legacy choices do not record host-script approval. Foreign organizational/source project lineage and nondefault legacy sources also remain copy-only. Persist a deny-only marker on the realized workspace and preserve it through metadata updates, binding changes, reuse, rebuild and reopening; missing source authority must not become a default approval. Existing implicit trusted local project defaults and taskless operator runs with no source retain their behavior. One shared resolver gives task creation, bulk creation and controller classification the same configured-policy pointer, primary, creation-time and ID precedence. Creation keeps its existing source validation; classification separately requires the actual source to belong to the organizational project and company. A dangling configured pointer does not fall back. This correction does not rotate existing bindings or clear retained denial markers. Unknown task-backed or projectless source identity stays copy-only; the reserved operator-cwd provenance compatibility branch does not imply a new producer or approval API. SSH retains its explicit remote provision-command path. Sandbox workspace staging does not add a remote provision-command or runtime-provision transport in this change. + +Channel defaults use optional properties (inherit), explicit null (clear), concrete value (override). Resource override beats endpoint. Defaults are captured once at task creation and reauthorized on use. Changing/deleting/revoking configuration cannot relocate existing tasks. Project choice and workspace choice are independent; choosing a source does not silently assign organizational project. Endpoints without configuration work normally. + +The organizational task project retains task identity, environment, and secret scope regardless of the filesystem source. An organizational project with no usable configured folder or repository is not a filesystem source. Unbound tasks without typed selection still retain that project's explicit workspace requirements; persisted and typed source choices retain precedence. Both task-project and source-project policies constrain run trust; source policy controls workspace realization. Channel task creation separately checks assignment authority for the effective organizational project and actual assignee against locked endpoint values inside its transaction. Saving defaults does not grant future senders that authority. + +New native chat tasks may realize an authorized managed-isolated Git worktree before their first binding exists. The resulting durable task-owned worktree must pass the existing chat-root guard before provider dispatch; shared roots and mismatched admitted recovery roots remain rejected. + +Active root selection stores pending intent and returns next-normal-admission, without interrupting or scheduling a new model turn. If atomic safe nested acquisition/capture is supported, repository preparation may add a new contained subtree to the active root; otherwise it records preparation for next admission with explicit capability result. Never claim a host-only checkout is available in the running remote environment. + +## Sharing and durability + +Preserve simultaneous local/SSH tasks in an explicitly configured identical physical folder, including current local/SSH handling of legacy serialize settings. Administrative Git/checkout/restore actions retain target-scoped locks; ordinary provider work is concurrent. Closing one task cannot remove configured shared files or release another participant's resource. + +Concurrent remote sharing requires proven common physical placement, separate session authority and coherent snapshot capability. Unsupported new concurrent sharing returns a typed unavailable result; do not instantiate separate clones and label them shared. Already-admitted descriptors remain recoverable unchanged. Local authoritative roots need no replica copyback; SSH retains its actual existing persistence contract. + +Sparse filesystem checkpoints use disk-backed full manifests, changed bytes/deletions, safe-link and existing ignore semantics, and separate nested Git history. Capture still enumerates and hashes files with stable-read validation; a metadata hash cache is deferred. No-change sends zero file-content payload where capability is supported; manifest metadata still transfers; unsupported transport uses explicit confined full fallback. Sparse capture capacity failures before outbound data is read also use the existing confined full transfer, so a tiny output can recover when the sandbox cannot preserve the sparse manifest reserve. Returned manifest, path, digest and payload validation failures remain errors and never trigger fallback. Full checkpoint exports validate the completed staging root before merge, reject unsupported entries, and omit absolute or escaping links without following them; safe relative links remain supported. Reuse immutable durable seed generations, not only warm sandbox contents; reuse still verifies archive digests. Existing run references pin seeds/deltas until all admitted runs/recovery obligations release them. Retain the final host rescan because the run ownership fence does not exclude external writers to shared folders; do not reuse a stale post-merge manifest. + +A returned sparse snapshot must describe a coherent tree before any host mutation. Every nested entry requires an immediate parent marked as a directory in that same returned snapshot, including baseline-unchanged files. Checking every directory also verifies recursive ancestry. Top-level entries use the implicit root. Invalid trees fail without overwriting retained host files and keep recovery evidence. The check uses the existing indexed manifest interface; it adds no host-path scans or unbounded in-memory inventory. + +Seed recovery deliberately uses **no delta chain** in this implementation. An unchanged generation reuses its verified content-addressed immutable full seed; a changed generation creates a fresh full seed. Sparse output transfer is separate from seed recovery. This revises the sketch's 16-delta compaction proposal: composing recovery generations would add a new failure surface before the simpler cache has been qualified. Existing references retain old seeds; no automatic eviction/deletion policy is introduced. Optional cache publication is capped at 16 generations/1 GiB per workspace and 256 generations/256 workspace directories/8 GiB per company. A company-scoped filesystem lock serializes quota checks and publication across workspace roots. Archive and receipt bytes, corrupt entries and abandoned pending generations count against capacity; publication also preserves the manifest storage free-space reserve (256 MiB by default), accounting for a full-copy fallback. Saturation or cache failure skips publication and keeps the already-written per-run recovery seed. Existing over-capacity caches stop growing without deleting admitted evidence. This deliberately trades cache hit rate after saturation for bounded additional storage and unchanged recovery semantics; no global cross-company quota is added. Changed input generations still pay full snapshot creation. New cache entries are reconstructed and checked against the admitted baseline before publication, so concurrent writes cannot label an archive as the wrong generation; this adds cold verification work. A corrupted cache entry remains a miss and falls back to a fresh seed; automatic cache repair and garbage collection are deferred. The existing Git seed contract captures the selected branch/HEAD, not unrelated refs. + +Ordinary settlement remains host-durable before successful completion. Preserve export-only recovery and exact-source lease retention. Preserve the specific unsafe-export omission exception and its run-log-only diagnostic. Do not change global merge conflict semantics as a side effect. + +Folder-change guards track live run ownership, finalization work and pending file recovery. A historical stopped or declined run whose cleanup has settled must not permanently block the task's next folder choice. Retained export recovery and unresolved ownership still block changes until their durable release or persistence receipts exist. + +## Delivery and ownership + +The core implementation owns execution-workspace/task binding schema + shared selection contracts, workspace domain/resolver, issues/heartbeat integration and nullable-project read models. The persistence implementation owns adapter-utils/native sync/trace helpers, exposes integration hooks to core rather than concurrent heartbeat editing. The repository implementation owns acquisition service, inventory schema and routes/runner catalogs, coordinating domain interfaces with core. The integration layer owns channel defaults/config API/UI, documentation, migration generation and final integration/verification. All workers are in the same checkout; do not revert others. + +Generate the feature migrations centrally from finalized schemas, preserving the upstream journal prefix and snapshot chain. Task intent/inventory, physical acquisition policy and retained source access use sequential additive migrations. No handwritten snapshots. Upgrade is additive; old immutable inputs and descriptors remain readable. New selections/checkpoint versions must not be admitted by old controllers: deploy schema first, drain only local test processes if needed, activate new admissions after all serving controllers support them. Rolling back application must disable new admissions, retain new state and finish/recover already-admitted work on compatible controllers; no destructive down migration. + +## Acceptance + +Targeted tests cover app/chat parity, task directory without Git, reassignment, organization change without relocation, explicit configured-folder concurrency/lifecycle, privacy intersection, repository permission/idempotency/ref conflicts, pending root changes without interruption, accepted-result recovery, unsafe-export exception, old descriptors, and no access to unrelated home contents. Persistence fixtures prove unchanged zero payload and one-file/deletion changes where supported, seed reuse and corrupted/stale fallback. Timings include delayed finalization. UI settings/errors/projectless links work with token-only styling. Run full typecheck/test:run/build before handoff; distinguish fixture proof from unperformed live provider qualification. + +## Phase status + +- Ground: complete. +- Sketch: complete, A selected with stated grafts. +- Agree: user explicitly chose architecture then implementation; no additional checkpoint. +- Implement: code complete; targeted checks pass, broad-suite qualification remains incomplete (see verification record). +- Scrap: conditional; two repeated boundary workarounds require redesign rather than silent escape hatches. + + +## Layer plan and public usage + +| Layer | Responsibility | Contract | +| --- | --- | --- | +| Shared types and database | Durable task intent, one active binding, optional source project, repository inventory | `TaskWorkspaceSelection`; existing `issues.executionWorkspaceId` | +| Workspace domain | Authorize selection, queue safe transitions, bind stable task folder, retain source privacy | `validateSelection`, `inspectTaskWorkspace`, `selectTaskWorkspace`, `applyPendingTaskWorkspaceSelection` | +| Task creation | Resolve explicit/parent/channel intent without making filesystem side effects | `workspaceSelection` on create; source marked internally | +| Channels | Merge connection/destination defaults, check sender and agent access, snapshot once | `executionDefaults: { projectId?, workspace? }` | +| Repository preparation | Validate repository/ref, authorize credentials, publish contained checkout idempotently | Repository request queues for next normal admission | +| Runner tools | Discover authorized choices and submit intent | Get/list/select workspace; prepare repository | +| Harness | Determine cwd, prepare queued repositories before baseline, enforce trust/placement policy | Existing admission and environment ownership fences | +| Filesystem persistence | Stable manifests, sparse output, verified seed reuse, durable settlement | Backward-readable native descriptors; new descriptor version for nested repositories | +| UI | Optional defaults and projectless workspace navigation | Existing Apps settings and Workspaces surfaces | + +Example task creation: + +```ts +await issues.create(companyId, { + title: "Write the report", + projectId: null, + workspaceSelection: { kind: "task_directory" }, +}); +``` + +A task can later select an authorized existing workspace with its current binding revision and an idempotency key. Accepting a new pending selection atomically advances that revision, so retrying a superseded choice cannot replace a newer one. Retrying the current pending or applied request remains idempotent. The response states that the selection applies on the next normal admission; it does not restart the current process. A repository request similarly records preparation intent when the active provider root cannot safely accept a new checkout. Project assignment alone never requests this transition. + +Example channel defaults: + +```json +{ + "executionDefaults": { + "projectId": null, + "workspace": { "kind": "task_directory" } + } +} +``` + +A destination may override either field independently. Omitted fields inherit the connection value; `null` clears that field's inherited choice. A cleared workspace choice allows the ordinary project/task resolution path. Changing defaults affects new conversations only. Saving a destination default does not overwrite its independently managed enabled state. + +## Verification and rollout limits + +Tests must distinguish local fixture proof from live provider qualification. The implementation adds database and protocol tests, compatibility tests for old descriptors, plain-directory and nested-repository persistence fixtures, and sparse/checkpoint/cache tests. A passing fixture does not prove Daytona or SSH production throughput. The original diagnostic run is never interrupted or modified as part of this work. + +Deploy schema before admitting the new state. Native descriptor v3 requires compatible recovery controllers; mixed-version controllers must not claim new admissions. Rollback retains the schema and immutable seeds and routes existing v3 obligations to compatible controllers. Do not delete legacy home content, existing workspace directories, or accepted-result recovery evidence. + +### Local verification record — 2026-10-09 + +- Full `pnpm -r typecheck` and `pnpm build` passed. A final direct server `tsc --noEmit` passed after the last server corrections. The full build preceded those final corrections; it was not repeated. +- Generated runner contracts, migration validation, module boundaries, token gates, and `git diff --check` passed. +- Focused checks passed for workspace binding/admission/reopen, concurrent local/SSH sharing, task-folder isolation, channel defaults and email admission, projectless UI, and immutable recovery behavior. Final repository lifecycle batch: 19/19; file browsing: 38/38; sparse checkpoints/cache: 7/7; Git/history: 69/69; tracing: 11/11; executor timing: 2/2. +- `pnpm test:run` was attempted and stopped with exit 130 after roughly an hour of serial server tests and repeated fixture timeouts. Its observed failures were 54 chat cases, one executor timing case, and 23 runtime-service cases. This is **not** a passing full-suite result. The run also began before the final source edits, so it does not qualify the frozen final tree. +- A fresh retry passed 53/54 previously failing chat cases; the remaining Slack admission case exceeded its short asynchronous observation window. After one database-setup timeout, the final isolated retry passed with the original assertions and a larger setup-hook budget (1 passed, 1,145 skipped; `chat-slash-final.log`). Thus all 54 cases passed across fresh retries, not in one clean full run. The executor timing cases both pass in fresh processes. Slow local listener lookups were independently measured at 3.5–6 seconds each; do not interpret the broader runtime failures as proven product regressions or proven baseline failures without further isolation. +- No live SSH/Daytona performance qualification or staging deployment was performed. Clean full-suite qualification on the frozen tree remains a release prerequisite. + +Detailed local logs are retained in `/tmp/paperclip-workspace-architect/` (`build.log`, `typecheck-final.log`, `tests.log`, `chat-failures-retry.log`, `repository-lifecycle-tests.log`, and `file-resources-final.log`). Worker verification notes are in `core-progress.md`, `repository-progress.md`, and `sync-progress.md` there. diff --git a/doc/plans/2026-10-12-chat-channel-delivery-boundaries-refactor.md b/doc/plans/2026-10-12-chat-channel-delivery-boundaries-refactor.md new file mode 100644 index 00000000000..01a3fb22ef9 --- /dev/null +++ b/doc/plans/2026-10-12-chat-channel-delivery-boundaries-refactor.md @@ -0,0 +1,100 @@ +# Chat channel callback and delivery boundaries, batch four + +Base: 4b766b872993045a2e787404f0475503683e5ded. Batch three (#15995, #15996, #15999, #16001, #16002) +is merged. The service starts at 26,461 lines / 1,046,711 bytes. +This batch may create up to ten stacked PRs. Merging the new batch is separate work. + +| Order / boundary | Destination | Approx. moved / net lines | Contract and state | Risks and existing verification | +| --- | --- | --- | --- | --- | +| 1. Slack callback verification | `slack/callback-verification.ts` | 250 / 230 | Pure signature/workspace/URL inspection plus signed URL verification; explicit vault lookup, URL getter, scheduling and registration callbacks | Preserve signing-secret-only setup, original request, TLS/proxy evidence and workspace checks. Signed verification and callback health cases before/after. | +| 2. Discord Gateway events | `discord/gateway-events.ts` | 200 / 185 | Runtime fence and lease-owner predicates, endpoint lookup and lifecycle callback; original per-runtime event queue and sequence | Preserve stale-owner fencing, event order, transient guild recovery and terminal revocation. Gateway lifecycle cases before/after. | +| 3. Discord root threads | `discord/root-threads.ts` | 500 / 460 | Credential lease, current principal authority, runtime, message admission and conversation drain callbacks | Preserve uncertain thread creation, restart reconciliation, no duplicate starter and pause/credential serialization. Root-thread public-service cases. | +| 4. Receipt reactions | `receipt-reactions.ts` and Slack error classifier beside Slack messages | 545 / 500 | Durable reaction claim/transport/settlement with runtime and credential fences; no scheduling owner moves | Preserve retries, legacy GitHub receipts, terminal-removal supersession and same-attempt settlement. Receipt recovery cases before/after. | +| 5. Inbound reactions | `reactions.ts` | 800 / 740 | Conversation lookup/locks, reaction admission and deferred replay together; current principal/runtime callbacks | Preserve exact event dedupe, completed generation links, bounded replay age and no task wakeup. Reaction race/restart/authorization cases. | +| 6. Inbound attachments | `inbound-attachments.ts` | 600 / 550 | Existing storage and issue service, retained source authorization and live options; return same attachment results | Preserve size limits, source authority around I/O, omissions and text delivery. Provider attachment/restart/stale-authority cases. | +| 7. Outbound attachments | `outbound-attachments.ts`; provider Markdown wrappers beside provider messages | 245 / 220 | Metadata validation and bounded integrity-checked hydration; existing storage object. Keep public hydration export compatible | Preserve stream cleanup, deadlines, byte/digest checks and retry classification. Hydration and publication attachment cases. | +| 8. Provider effects | `provider-effects.ts` | 950 / 850 | Durable reply claim, authorization, transport and settlement; endpoint/runtime/credential/principal callbacks | Preserve uncertain-delivery quarantine, credential fence and transaction ordering. Native callback, task-control and reply recovery cases. Put Telegram-only authority under `telegram/` if its contract is narrow. | +| 9. Conversation and activity reads | `activity.ts` | 540 / 520 | Endpoint lookup and read-only Teams conflict projection; return existing API models | Preserve company/endpoint joins, mixed-source cursor ties, replay eligibility and read-only GETs. Activity, file receipt projection and conversation-state cases. | +| 10. Setup probes | `setup-probes.ts` | 280 / 250 | Credential lease, current identity/principal authority, provider fetch, endpoint projection and live activation hook | Preserve real-message requirement, generation and linked-user activation. Setup-test, reconnect and authorization cases. | + +Each slice must earn its place after reading its complete path and callers. +Reassess before implementation; stop short of ten if the remaining contracts are +not cohesive. Keep provider-specific code in channel folders and common durable +workflows in the shared chat-channels directory. + +Receiver lease ownership stays shared: the existing Discord-named owner is also +used by Photon. Shared lifecycle/application admission, queue-drain ownership, +worker scheduling, and the largest action/message handlers remain in the service. +Do not hide their dependencies in a mutable context object. Preserve live option +hooks, runtime-map identity, partial-failure cleanup, and initialization order. + +Run relevant existing public-service tests before and after each mechanical move. +Compare moved declarations and remaining statements with normalized syntax, then +run server types, module boundaries and whitespace checks. Finish local verification and a fresh +5/5 review before opening the next draft PR. GitHub runner-selection jobs remained +queued without steps for over 25 minutes in this batch. To continue the authorized +stack, publish drafts in review order while tracking CI as pending; CI remains a +merge gate. Run cumulative repository gates at handoff. +Retain failures and skipped/unrun coverage even when an isolated retry passes. +Use fixture-backed assembled service/database tests; do not claim live provider +execution. Remap LOC/bytes and the remaining orchestration after the batch. + +## Slice 1: Slack callback verification + +- Service: 26,461 lines / 1,046,711 bytes → 26,201 lines / 1,035,260 bytes. +- Destination: 299 lines / 13,038 bytes. Eight declarations move without body + changes. Three existing setup types move unchanged into `runtime-types.ts`. +- The verification factory initializes after Slack registration. It receives the + same registration object, credential resolver, live URL getter and scheduler. + Signature/workspace/URL helpers remain callable by shared webhook orchestration. +- Nine public-service cases passed before and after on the updated master base. + Server typecheck, module boundaries, whitespace and normalized syntax parity + passed. Coverage includes signed URL verification, account notification, + workspace rejection, callback surfaces, TLS termination and Cloud host evidence. +- Next: Discord Gateway event sequencing and health transitions. Keep shared + receiver lease ownership and runtime-map lifetime in the service. + +## Batch outcome + +All ten candidates produced cohesive boundaries. Shared receiver ownership, queue +management and worker scheduling remain in the service. The only intentional +control-wiring change is deferred access to `stageProviderEffect` from identity +link construction; typecheck and integration tests caught the initial early read. + +| Slice | Service lines after | Primary module lines | Before/after cases | +| --- | ---: | ---: | ---: | +| 1. Slack signature, workspace and callback url verification | 26,233 | 300 | 9 | +| 2. Discord gateway event sequencing and endpoint health transitions | 26,029 | 253 | 7 | +| 3. Discord root-thread admission, provider creation and durable recovery | 25,527 | 570 | 4 | +| 4. Durable receipt reaction claims, transport, retries and settlement | 24,975 | 586 | 7 | +| 5. Inbound reaction conversation lookup, admission and deferred replay | 24,160 | 891 | 21 | +| 6. Inbound attachment authorization, bounded ingestion, registration and omissions | 23,565 | 674 | 39 | +| 7. Outbound attachment metadata validation and bounded integrity-checked hydration | 23,298 | 294 | 22 | +| 8. Durable provider reply staging, authorization, transport and recovery | 22,331 | 957 | 24 | +| 9. Conversation summaries and paginated activity projections | 21,741 | 616 | 16 | +| 10. Setup message detection and credential-fenced round-trip qualification | 21,460 | 353 | 7 | + +After rebasing on master `8e7b5db822`, the service shrank from +**26,493 lines / 1,048,769 bytes** to **21,460 lines / +862,284 bytes** (5,033 fewer lines, about 19%). Normalized syntax comparison +checks all 69 moved declarations/types and every remaining original statement. +Server types, targeted tests, module boundaries and whitespace checks are the +per-slice gates. Cumulative repository gates and remote reviews are recorded in +the PR descriptions as they complete. + +Orchestration measurement: the service function has 150 nested functions +(19,399 lines) and 67 state/wiring statements (459 lines), versus 191 functions +(23,829 lines) and 54 statements (425 lines) at the start. Imports take 400 lines. +The remaining large paths are `processMessage` (2,166), `handleAction` (1,074), +`configureWithCredentialLease` (868), `processSelectedPublication` (729), +`handleModalSubmit` (708), `handleWebhook` (685), and lifecycle processing (657). + +A future pass should inspect publication admission/transport/settlement as one +workflow, then inbound delivery authorization and drain ownership. Those cuts +need explicit ownership contracts; avoid separating individual checks from their +transactions merely to reduce the remaining line count. + +The rebase preserves the merged managed Slack setup flow, including its shared +`isProvisionedSlackSetup` predicate in the extracted callback verification +module. A fresh syntax audit compares all moved declarations and retained +statement ordering with this updated master. diff --git a/doc/plans/2026-10-12-chat-channel-provider-ownership-refactor.md b/doc/plans/2026-10-12-chat-channel-provider-ownership-refactor.md new file mode 100644 index 00000000000..1818ac829ae --- /dev/null +++ b/doc/plans/2026-10-12-chat-channel-provider-ownership-refactor.md @@ -0,0 +1,95 @@ +# Chat channel provider ownership refactor + +Date: 2026-10-12. Batch five starts at `4b958ecbd8f33a7782fe6f29ec825f80f636b833`. + +The user requested another stack of about ten PRs. Put provider-specific code in +the provider's existing folder. Preserve behavior and build each PR on the previous +verified extraction. This batch does not authorize merging the new stack. + +The starting service is 21,460 lines and 862,284 bytes. The previous ten PRs are +merged. Common admission, runtime ownership, and publication scheduling stay in +the service. In particular, the receiver lease currently serves both Discord and +Photon; its Discord names do not make it a Discord-only boundary. + +| Order | Responsibility and destination under `server/src/services/chat-channels/` | Approximate source lines | Contract and state | Risks and verification | +| --- | --- | ---: | --- | --- | +| 1 | Credential verification: provider `credentials.ts` files | 250 | Credentials, fetch, timeout; return verified bot identity. Photon inspection retains its error translation. | Preserve scope/event checks and immutable identity. Existing configuration, credential, and Photon tests. | +| 2 | Inventory discovery: `slack/inventory.ts`, `github/inventory.ts`, `discord/inventory.ts` | 120 | Credential input and prepared inventory result. Slack label hydration uses the same endpoint reader. | Preserve GitHub installation discovery and canonical repository IDs; same public resource tests. | +| 3 | GitHub secret rotation: `github/setup-secret.ts` | 278 | Endpoint reader, credential lease, credential storage, runtime invalidation, and audit. | Keep lease checks, commit recovery, and audit ordering intact; real database rotation/race tests. | +| 4 | Teams file delivery: `microsoft-teams/file-delivery.ts` | 360 | Transfer protocol and consent callback; shared runtime readers and retry cooldown. Maintenance state remains per service. | Preserve exact transfer authority, unknown outcomes, consent races, and live shutdown state; assembled file-delivery tests. | +| 5 | Native slash callbacks: `slack/commands.ts`, `telegram/commands.ts` | 340 | Provider command callbacks use shared admission and durable effects. Dispatcher retains callback admission. | Preserve synthetic Telegram media/time, Slack acknowledgment, identity linking, and duplicate receipts. | +| 6 | Telegram attachment failure: `telegram/attachment-failure.ts` | 106 | Accepted inbound delivery and attachment result; return whether it settled. | Keep comment/action/delivery writes atomic and avoid waking an agent with no usable content. | +| 7 | GitHub publication preparation and settlement: `github/publication-policy.ts` | 235 | Current source proof, explicit operator classifier, receipt cleanup, and task URL getter. | Preserve prepared-text receipts and governed-tool fallback suppression; publication/review integration tests. | +| 8 | Slack task tool authority: `slack/task-authority.ts` | 170 | Register resolver with current endpoint/credential and inbound authorization readers; return unregister callback. | Preserve lock order, responsible-user authority, company scope, and shutdown cleanup; existing Slack tool tests. | +| 9 | GitHub App setup: `github/app-setup.ts` | 180 | App persistence, repository refresh, registration/wizard assembly, and cloud ingress registration. | Preserve live origins, credential leases, ingress fences, and unregister ownership; setup and wizard integration tests. | +| 10 | GitHub installation lifecycle: `github/installation-lifecycle.ts` | 430 | Admitted lifecycle delivery, locked credential lease, identity/inventory readers; explicit runtime outcomes and failure notification. | Keep claim/retry ownership in the shared coordinator. Preserve failure notification before throwing; deletion/reinstall/race tests. | + +These estimates include code that stays behind as dispatch and typed wiring. Each +slice must be reassessed before implementation. Do not introduce an untyped context +bag, copy mutable live values, or change algorithms to meet a line-count target. + +Run existing public-path tests before production edits and after each extraction. +Compare moved declarations and retained statements with normalized TypeScript +syntax; review every deliberate dispatch substitution. Run server typecheck and +module boundaries for each slice, and repository typecheck, full tests, and build +once at the final handoff. Record failures and omitted checks without claiming they +passed. Local fixtures prove assembled application behavior, not live third-party +execution. Use the same managed checkout for dependency preflight and builds. + +Every PR uses the repository template and points to its preceding stack branch. +Check current-head Greptile feedback and CI before completing each slice. Update +this plan with evidence and a fresh map after the batch. + +## Implemented boundaries and evidence + +All ten local slices are committed. The service is now 19,153 lines and 762,442 +bytes: 2,307 lines and 99,842 bytes removed from the entry point. The service body +is 17,787 lines, including 138 nested functions (17,270 lines) and 71 state/wiring +statements (231 lines). Imports occupy 399 lines. The aggregate AST audit checks +38 moved bodies/declarations and the syntax and order of 263 retained statements. + +The first baseline passed 96 selected assembled-service cases and 33 credential, +inventory, and Photon cases. Additional command, setup, and publication baselines +passed before their moves. Each slice passed server typecheck, module boundaries, +and its public-path checks. Post-move integration case counts were 6, 8, 10, 50, +7, 1, 21, 3, 34, and 12 respectively; these selections overlap and are not a count +of unique tests. The first slice also reran all 33 credential/inventory/Photon +cases. Repository-wide typecheck passes on the complete stack. Full tests and build +are recorded below when complete. + +Review found that the inventory dispatcher needed `return await` to retain its +existing catch boundary. Three new real-database HTTP tests passed against the +pre-extraction service, failed with 500 instead of 422 after the initial move, and +passed after the correction. They also check the public error code/message and +unchanged credential refs. The repair is included in slice two and replayed through +all subsequent local branches. Moved provider bodies remain unchanged. + +The Teams activity protocol callback is deferred until use because its new factory +is initialized after file-authority construction. Shutdown is read through a live +callback. GitHub lifecycle claims and credential leasing remain in the coordinator; +the provider workflow returns runtime outcomes and notifies its cleanup owner +immediately after persisting a failure, before throwing. + +## Remaining map + +The largest shared functions are `processMessage` (2,166 lines), `handleAction` +(1,074), `configureWithCredentialLease` (868), `processSelectedPublication` (729), +`handleModalSubmit` (708), `handleWebhook` (685), and `processLifecycleDelivery` +(657). These include shared admission and transaction ownership; do not relocate +them wholesale into a provider folder. + +The next provider candidates to inspect are: + +- Provider setup-state and runtime-config projections. Extend provider modules + while retaining live origin reads and the shared dispatcher. +- Photon outbound publication (a 227-line provider branch), including interaction + prompts and exact retry evidence. Keep current-source checks and partial receipt + ownership explicit. +- GitHub authenticated webhook handling (a 209-line provider branch). Preserve + durable ingress admission, signed ping verification, and receiver fencing. +- Discord durable Markdown transport (96 lines) and its modal wire handling + (85-line branch plus smaller branches). Preserve shared question resolution and + publication ownership. + +This remains a candidate map, not authorization for another batch or merging this +stack. All provider calls in local integration evidence use fixtures. diff --git a/doc/plans/2026-10-12-chat-channel-workflow-refactor.md b/doc/plans/2026-10-12-chat-channel-workflow-refactor.md new file mode 100644 index 00000000000..0bb245a9afb --- /dev/null +++ b/doc/plans/2026-10-12-chat-channel-workflow-refactor.md @@ -0,0 +1,159 @@ +# Chat channel workflow refactor + +The user authorized six hours of sustained work starting at 2026-10-13 02:14 UTC, +with a target of fewer than 8,000 lines in `server/src/services/chat-channels.ts`. +The starting commit is `7eaa9cfa9ce04398a5eb0211a86aacaaa01959dd`: 19,153 lines, +762,442 bytes. At kickoff, the preceding ten PRs were reviewed and green, but not yet merged. +The user authorizes publishing and merging reviewed changes to paperclipai/paperclip. + +The previous plan prioritized too many small helper cuts. This plan prioritizes +complete workflows, provider ownership, and meaningful reductions per PR. Shared +transactions and admission policy belong in shared workflow modules. Provider +transport, envelope checks, setup, and native interactions belong in provider +folders. Do not put shared code in one provider's folder or use a mutable context +bag to move the entire service unchanged. + +| Priority | Responsibility and destination | Estimated net reduction | Contract and state | Risk and verification | +| --- | --- | ---: | --- | --- | +| 1 | Native interaction callbacks and continuation: `interactions/`, `telegram/confirmation-actions.ts`, `discord/question-forms.ts` | 2,900 | Current actor/runtime/destination readers; durable token and interaction transactions; deferred wakeups. | Preserve token replay, audit order, provider acknowledgement, post-commit recovery, and live fault barriers. Existing public service, HTTP and real-database interaction tests before and after. | +| 2 | Endpoint configuration and provider setup: provider `setup.ts`, shared credential persistence/configuration | 1,600–2,200 | Credential lease and current endpoint; provider registration outcome; stable native identity. | Preserve fail-closed persistence, webhook/receiver ownership, partial setup cleanup, and credential rotation races. Existing setup and runtime tests. | +| 3 | Message admission and provider policy: shared intake workflow, GitHub message policy, provider envelope projections | 2,400–3,000 | Delivery claim and conversation/task admission; explicit provider policy results; per-service message provenance. | Preserve row locks, deduplication, author authority, task generations, attachments, and GitHub automatic/explicit admission. Existing real-database ingress and replay tests. | +| 4 | Durable delivery and lifecycle: shared queue, provider lifecycle inputs | 1,700–2,000 | Conversation leases, replay decode, ordered drains, mutation reconciliation; shared live receipt state. | Preserve stale lease exclusion, event order, deletion/edit races, deferred startup and shutdown. Existing concurrency and lifecycle tests. | +| 5 | Outbound publication: provider delivery modules and shared publication worker | 2,700–3,400 | Current-source authorization; exact transport parts; attempts, unknown outcomes, completion receipts and cleanup. | Preserve no duplicate sends, ambiguous acceptance, credential fencing, file recovery, concurrency and current task-control completion. Existing assembled publication and file tests. | +| 6, if needed | Operator recovery and endpoint projections | 800–1,200 | Explicit replay/resolution and company-scoped board calls. | Preserve retry audit, current policy and authorization. Existing operator-resolution integration tests. | + +The estimates are not a PR quota. Inspect every full execution path before moving +it. Increase size only where a cohesive responsibility has a narrow contract. +After every reviewed slice, remap the remaining file and report the measured size. + +For each slice: run a behavioral baseline before editing production; retain +existing bodies and SQL; compare normalized AST and statement order; run server +typecheck, module boundaries and public-path tests. Review all intentional callback +and dispatch substitutions. Preserve live option reads with getters or callbacks. +Keep per-service state per service and shared cache/lease ownership shared. + +Open one PR per cohesive extraction with the full repository template. Verify its +current-head review and all CI gates before opening the next extraction. Build each +on its immediate predecessor. Run repository-wide typecheck, tests and build once +at the final handoff; investigate and disclose any failures or unreached coverage. +The earlier batch recorded local timing/host cleanup failures; green CI and passing +focused retries do not erase that evidence. No timeout or assertion weakening. + +Public application fixtures prove assembled service/HTTP/database behavior. They +do not prove live third-party execution. Use only owned fixture data and processes. + +## First extraction + +The native interaction workflow reduces the service from 19,153 to 16,117 lines +and from 762,442 to 656,146 bytes. Telegram owns confirmation resolution and +settlement. Discord owns correction draft retention and accepted-answer cleanup. +Shared callbacks retain token/source/actor checks and atomic question resolution. +Shared continuation work retains durable wakeup recovery. SDK state persistence +has a direct module import and retains the public compatibility export. + +The same 62 public-path interaction cases pass before and after. Seven supporting +suites pass all 70 cases. Server typecheck and module boundaries pass. AST checks +cover nine moved declarations, three provider bodies, and retained service order. +Fault barriers remain live getters. No assertion or timeout changed. + +## Second extraction + +Endpoint configuration and provider setup reduce the service to 14,241 lines and +580,227 bytes. Shared modules own endpoint records, credential identity/persistence, +and the credential-leased configuration state machine. Channel folders own Telegram +registration, GitHub repair, Speko tool installation, Photon configuration and +inspection, and Slack setup projection. Registration dependencies remain live. + +All 1,278 chat cases passed on the first PR before this move. The final setup wiring +passes 268 selected integration cases, server typecheck, and module boundaries. +AST checks cover 25 declarations and nine provider bodies. One final test attempt +could not start PostgreSQL because this Mac exhausted IPC slots; after reclaiming +one unused zero-attachment segment with a dead creator, the same suite passed. + +## Third extraction + +The publication workflow reduces the service to 11,198 lines and 462,108 bytes. +Authorization, durable preparation, transport, failures, attempt ownership, task +control completion, and bounded endpoint scheduling now have named modules. +Photon owns prompt and reply delivery. Discord and Telegram own durable Markdown +attachment preparation. Shared native splitting remains shared across providers. + +The same 301 public-path publication cases pass before and after. Eight supporting +suites pass all 185 cases. Server typecheck, module boundaries, and AST checks pass. +The AST audit covers 26 declarations, three provider bodies, and retained service +order. Shared endpoint lanes and Teams cooldowns retain their original maps; +shutdown and publication wake listeners stay live. Early callback registration +uses typed forwarding functions instead of reading factories before initialization. + +## Fourth extraction + +Ordered delivery and lifecycle reconciliation reduce the service to 9,287 lines +and 388,734 bytes. The delivery ledger owns ordering and runtime-fence projections. +The conversation queue owns exact leases, reconstruction and ordered replay. +The lifecycle workflow owns edit/delete admission and reconciliation. Teams owns +authenticated route recording. GitHub owns restoration of durable message policy +into the same per-service provenance maps. + +The same 225 lifecycle, ordering, lease, replay and recovery cases pass before and +after. Typecheck and module boundaries pass. AST checks cover 26 declarations, the +GitHub restoration body, and retained service order. Live maps and lease hooks retain +their original lifetime. The publication predecessor's packaging retry passed unchanged. This fourth +slice is published as PR #16096. + + +## Fifth extraction + +Inbound authority, envelope preparation, durable admission and resource admission +reduce the service to 7,899 lines and 335,607 bytes. The shared admission module +keeps endpoint locking, runtime and credential fences, gateway lease renewal, +deduplication and delivery insertion in one transaction. Teams, Telegram and +Photon own their destination rules and use that same transaction. The main +message path now calls preparation, durable admission and resource admission +before the retained task workflow. + +The same 278 selected public-path cases pass before and after the first move. +All 1,278 existing chat integration cases pass on the final provider dispatch +in 309.47 seconds. Server typecheck and module boundaries pass. One full-suite attempt +could not allocate PostgreSQL shared memory before any test ran. Only a verified +unused 56-byte segment with a dead creator and zero attachments was reclaimed +before retrying. No active database was stopped. + +AST checks cover 21 moved declarations, envelope and resource phase bodies, +three provider destination bodies, and the complete admission transaction after +restoring the explicit provider calls. The service still owns the per-call +gateway ownership handoff. The envelope retains its per-call recovery map and +the original authenticated runtime. No assertion or timeout changed. + +## Stack landing + +The preceding ten PRs reached master through #16058. The five workflow PRs are +#16080, #16085, #16089, #16096 and #16105. Each extraction has individual behavioral +evidence and a 5/5 review. The four child PRs merged into their predecessors after +each combined revision passed its own checks and review. Every consolidation +preserved the tested tip tree exactly. + +The final master integration leaves the service at 7,917 lines and 337,195 bytes. +The 18-line increase over the extraction tip preserves upstream execution-default +selection and authorization. Endpoint serialization and credential-leased updates +retain that policy in the extracted endpoint module. Task creation and failed-run +recovery retain their upstream behavior. All 107 extracted declarations match +current master after reversing the documented binding substitutions. The endpoint, +task-creation and recovery functions also pass exact normalized AST comparisons. + +Repository-wide typecheck and build pass on this integration. The first full local +test command passed 18,391 server tests and skipped 105, but one unchanged tool-access +test hit its 15-second timeout. That case passed alone. The UI and CLI groups passed +8,706 tests. Shared and adapter groups passed 4,751 tests and skipped 20. A database +group could not initialize one PostgreSQL fixture. This Mac has a 32-segment shared +memory limit and concurrent fixture databases. The unchanged database cases are +being rerun one file at a time before the remaining chat and serialized route lanes. +Keep the original failed runs in the verification record. No assertion or timeout +was changed. The root PR remains open for fresh review and CI against master. + +The service now has 67 direct functions. The largest retained paths are message +task handling (1,449 lines), webhook dispatch (685), operator action resolution +(488), and publication resolution (343). Provider setup, transport preparation, +native interactions and destination admission live in their channel folders. +Shared admission, delivery, lifecycle and publication transactions have named +workflow modules. The retained message path calls those phases and keeps task +mutation and per-call ownership together. diff --git a/doc/plans/2026-10-12-test-audit-controls.json b/doc/plans/2026-10-12-test-audit-controls.json new file mode 100644 index 00000000000..a6dfc4afeed --- /dev/null +++ b/doc/plans/2026-10-12-test-audit-controls.json @@ -0,0 +1,539 @@ +{ + "date": "2026-10-12", + "policy": "Each mutation changes one actual production guard or outcome. Only the selected case runs. Every changed source is restored to its exact SHA-256 in finally. Complete unmutated owner and sibling controls run separately.", + "controls": [ + { + "id": "api-rejection-eviction", + "codeHead": "dfdc39b3a71c74c40e267010c6d8428af523bb65", + "owner": "ui/src/api/client.ts", + "sourceSha256": "1e1b3f03e3269a5943f55d294f1c7aa9f5c3eca569858d9fdc7c5d976de88bba", + "case": "tenant-session recovery preserves ApiError handling for an unrelated 401", + "expected": "one intended assertion failure", + "actualFailedCases": 1, + "intendedAssertionFailed": true, + "sourceRestored": true, + "failure": "Error: promise rejected \"ApiError: unauthorized { \u2026(2) }\" instead of resolving\n at Assertion.__VITEST_RESOLVES__ (file:///node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_@noble+hashes@2.4.0__v_wr5mxfuu6kgge6st623q4v5egm/node_modules/vitest/dist/chunks/index.C4tKZ0yW.js:2434:33)\n at Assertion.propertyGetter (file:///node_modules/.pnpm/chai@6.3.0/node_modules/chai/index.js:1607:27)\n at Reflect.get ()\n at Object.proxyGetter [as get] (file:///Users/dotta/.codex/worktrees/tes" + }, + { + "id": "api-all-abort-eviction", + "codeHead": "dfdc39b3a71c74c40e267010c6d8428af523bb65", + "owner": "ui/src/api/client.ts", + "sourceSha256": "1e1b3f03e3269a5943f55d294f1c7aa9f5c3eca569858d9fdc7c5d976de88bba", + "case": "per-caller abort semantics aborts the shared fetch after all callers cancel and permits a fresh request", + "expected": "one intended assertion failure", + "actualFailedCases": 1, + "intendedAssertionFailed": true, + "sourceRestored": true, + "failure": "AssertionError: expected \"vi.fn()\" to be called 2 times, but got 1 times\n at /ui/src/api/client.test.ts:210:23\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at file:///node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_@noble+hashes@2.4.0__v_wr5mxfuu6kgge6st623q4v5egm/node_modules/vitest/dist/chunks/run.BTlFXlnv.js:2783:20" + }, + { + "id": "ticker-one-interval", + "codeHead": "dfdc39b3a71c74c40e267010c6d8428af523bb65", + "owner": "ui/src/hooks/useSecondTick.ts", + "sourceSha256": "fd51875adf5f30295d847df58af19f75dcbb59c810b4f02919427cbc6ea4d3b3", + "case": "useSecondTick shares a single timer across multiple active subscribers and stops when idle", + "expected": "one intended assertion failure", + "actualFailedCases": 1, + "intendedAssertionFailed": true, + "sourceRestored": true, + "failure": "AssertionError: expected 2 to be 1 // Object.is equality\n at /ui/src/hooks/useSecondTick.test.tsx:26:32\n at file:///node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_@noble+hashes@2.4.0__v_wr5mxfuu6kgge6st623q4v5egm/node_modules/vitest/dist/chunks/run.BTlFXlnv.js:1628:35\n at file:///node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_@noble+hashes@2.4.0__v_wr5mxfuu6kgge6st623q4v5egm/node_modules" + }, + { + "id": "ticker-delivery", + "codeHead": "dfdc39b3a71c74c40e267010c6d8428af523bb65", + "owner": "ui/src/hooks/useSecondTick.ts", + "sourceSha256": "fd51875adf5f30295d847df58af19f75dcbb59c810b4f02919427cbc6ea4d3b3", + "case": "useSecondTick shares a single timer across multiple active subscribers and stops when idle", + "expected": "one intended assertion failure", + "actualFailedCases": 1, + "intendedAssertionFailed": true, + "sourceRestored": true, + "failure": "AssertionError: expected '0' to be '1' // Object.is equality\n at /ui/src/hooks/useSecondTick.test.tsx:28:37\n at file:///node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_@noble+hashes@2.4.0__v_wr5mxfuu6kgge6st623q4v5egm/node_modules/vitest/dist/chunks/run.BTlFXlnv.js:1628:35\n at file:///node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_@noble+hashes@2.4.0__v_wr5mxfuu6kgge6st623q4v5egm/node_mod" + }, + { + "id": "ticker-idle-cleanup", + "codeHead": "dfdc39b3a71c74c40e267010c6d8428af523bb65", + "owner": "ui/src/hooks/useSecondTick.ts", + "sourceSha256": "fd51875adf5f30295d847df58af19f75dcbb59c810b4f02919427cbc6ea4d3b3", + "case": "useSecondTick shares a single timer across multiple active subscribers and stops when idle", + "expected": "one intended assertion failure", + "actualFailedCases": 1, + "intendedAssertionFailed": true, + "sourceRestored": true, + "failure": "AssertionError: expected 1 to be +0 // Object.is equality\n at /ui/src/hooks/useSecondTick.test.tsx:37:32\n at file:///node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_@noble+hashes@2.4.0__v_wr5mxfuu6kgge6st623q4v5egm/node_modules/vitest/dist/chunks/run.BTlFXlnv.js:1628:35\n at file:///node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_@noble+hashes@2.4.0__v_wr5mxfuu6kgge6st623q4v5egm/node_module" + }, + { + "id": "ticker-inactive-peer", + "codeHead": "dfdc39b3a71c74c40e267010c6d8428af523bb65", + "owner": "ui/src/hooks/useSecondTick.ts", + "sourceSha256": "fd51875adf5f30295d847df58af19f75dcbb59c810b4f02919427cbc6ea4d3b3", + "case": "useSecondTick does not subscribe or start a timer when inactive", + "expected": "one intended assertion failure", + "actualFailedCases": 1, + "intendedAssertionFailed": true, + "sourceRestored": true, + "failure": "AssertionError: expected '1' to be '0' // Object.is equality\n at /ui/src/hooks/useSecondTick.test.tsx:48:37\n at file:///node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_@noble+hashes@2.4.0__v_wr5mxfuu6kgge6st623q4v5egm/node_modules/vitest/dist/chunks/run.BTlFXlnv.js:1628:35\n at file:///node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_@noble+hashes@2.4.0__v_wr5mxfuu6kgge6st623q4v5egm/node_mod" + }, + { + "id": "ui-bound-caption", + "codeHead": "dfdc39b3a71c74c40e267010c6d8428af523bb65", + "owner": "ui/src/components/AgentConfigForm.tsx", + "sourceSha256": "bf999102f39bfc762747ce5d598e1e6ec2f926ac5fb28de55fa1baf5493f28b1", + "case": "AgentConfigForm environment selector reports the account-binding claim upward exactly once when the session authenticates", + "expected": "one intended assertion failure", + "actualFailedCases": 1, + "intendedAssertionFailed": true, + "sourceRestored": true, + "failure": "AssertionError: expected 'Sign in to OpenAISign inAuthenticated\u2026' to contain 'Agent bound to the signed-in account'\n at /ui/src/components/AgentConfigForm.render.test.tsx:1773:35\n at file:///node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_@noble+hashes@2.4.0__v_wr5mxfuu6kgge6st623q4v5egm/node_modules/vitest/dist/chunks/run.BTlFXlnv.js:2783:20" + }, + { + "id": "ui-onboarding-saved-login", + "codeHead": "dfdc39b3a71c74c40e267010c6d8428af523bb65", + "owner": "ui/src/components/AgentConfigForm.tsx", + "sourceSha256": "bf999102f39bfc762747ce5d598e1e6ec2f926ac5fb28de55fa1baf5493f28b1", + "case": "OnboardingWizard restore-gate (stale localStorage across accounts) the adapter step login panel (cheap auth signal, no adapter test) renders no 'Use saved login' control", + "expected": "one intended assertion failure", + "actualFailedCases": 1, + "intendedAssertionFailed": true, + "sourceRestored": true, + "failure": "AssertionError: expected 'Step 2 of 3Ada Connect a modelPapercl\u2026' not to contain 'Use saved login'\n at /ui/src/components/OnboardingWizard.test.tsx:2931:45\n at file:///node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_@noble+hashes@2.4.0__v_wr5mxfuu6kgge6st623q4v5egm/node_modules/vitest/dist/chunks/run.BTlFXlnv.js:2783:20" + }, + { + "id": "ui-agent-footer", + "codeHead": "dfdc39b3a71c74c40e267010c6d8428af523bb65", + "owner": "ui/src/components/IssueThreadInteractionCard.tsx", + "sourceSha256": "fb9beb27481258354942003e6a5c319a23257ef07f4ce811eb9ccbd605e0a585", + "case": "IssueThreadInteractionCard tool-action card marks agent resolution with an audit chip in the resolved footer", + "expected": "one intended assertion failure", + "actualFailedCases": 1, + "intendedAssertionFailed": true, + "sourceRestored": true, + "failure": "AssertionError: the given combination of arguments (undefined and string) is invalid for this assertion. You can use an array, a map, an object, a set, a string, or a weakset instead of a string\n at Proxy. (file:///node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_@noble+hashes@2.4.0__v_wr5mxfuu6kgge6st623q4v5egm/node_modules/vitest/dist/chunks/index.C4tKZ0yW.js:2077:15)\n at Proxy. (file:///node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_j" + }, + { + "id": "ui-markdown-anchor", + "codeHead": "dfdc39b3a71c74c40e267010c6d8428af523bb65", + "owner": "ui/src/components/MarkdownBody.tsx", + "sourceSha256": "003b7da23b5ebd0e8cd4de91e0d1dafe697f6602d4d99551b04d0abad699d193", + "case": "MarkdownBody opens external links in a new tab with safe rel attributes", + "expected": "one intended assertion failure", + "actualFailedCases": 1, + "intendedAssertionFailed": true, + "sourceRestored": true, + "failure": "AssertionError: expected '
/ui/src/components/MarkdownBody.test.tsx:587:18\n at file:///node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_@noble+hashes@2.4.0__v_wr5mxfuu6kgge6st623q4v5egm/node_modules/vitest/dist/chunks/run.BTlFXlnv.js:1628:35\n at file:///node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_" + }, + { + "id": "server-cancelled-reopen", + "codeHead": "dfdc39b3a71c74c40e267010c6d8428af523bb65", + "owner": "server/src/routes/issues.ts", + "sourceSha256": "641b3d9cb5e744671527f16129dd78518376dca31616b845ec6b8da99d636d8c", + "case": "issue comment reopen routes rejects bare agent reopen requests on cancelled issues before writing a comment", + "expected": "one intended assertion failure", + "actualFailedCases": 1, + "intendedAssertionFailed": true, + "sourceRestored": true, + "failure": "AssertionError: expected 201 to be 409 // Object.is equality\n at /server/src/__tests__/issue-comment-reopen-routes.test.ts:1040:24\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at file:///node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_@noble+hashes@2.4.0__v_wr5mxfuu6kgge6st623q4v5egm/node_modules/vitest/dist/chunks/run.BTlFXlnv.js:2783:20" + }, + { + "id": "wiki-genuine-run-id", + "codeHead": "dfdc39b3a71c74c40e267010c6d8428af523bb65", + "owner": "packages/plugins/plugin-llm-wiki/src/wiki/core.ts", + "sourceSha256": "35d8543f67ab80479c624f527d0b94fbb0f3b309d40b1276a07518872212ea06", + "case": "LLM Wiki plugin scaffold starts query sessions, records run ids, and forwards session events to plugin streams", + "expected": "one intended assertion failure", + "actualFailedCases": 1, + "intendedAssertionFailed": true, + "sourceRestored": true, + "failure": "AssertionError: expected [ \u2026(4) ] to deeply equal [ \u2026(4) ]\n at /packages/plugins/plugin-llm-wiki/tests/plugin.spec.ts:3610:44\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at file:///node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_@noble+hashes@2.4.0__v_wr5mxfuu6kgge6st623q4v5egm/node_modules/vitest/dist/chunks/run.BTlFXlnv.js:2783:20" + }, + { + "id": "wiki-positive-source-writes", + "codeHead": "dfdc39b3a71c74c40e267010c6d8428af523bb65", + "owner": "packages/plugins/plugin-llm-wiki/src/wiki/core.ts", + "sourceSha256": "35d8543f67ab80479c624f527d0b94fbb0f3b309d40b1276a07518872212ea06", + "case": "LLM Wiki plugin scaffold keeps default-space files at the root and isolates managed spaces under slug prefixes", + "expected": "one intended assertion failure", + "actualFailedCases": 1, + "intendedAssertionFailed": true, + "sourceRestored": true, + "failure": "AssertionError: expected [] to have a length of 2 but got +0\n at Proxy. (file:///node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_@noble+hashes@2.4.0__v_wr5mxfuu6kgge6st623q4v5egm/node_modules/vitest/dist/chunks/index.C4tKZ0yW.js:2147:20)\n at Proxy. (file:///node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_@noble+hashes@2.4.0__v_wr5mxfuu6kgge6st623q4v5egm/node_modules/vitest/dist/chunks/index.C4tKZ0yW.js:1944:14)\n at Proxy." + }, + { + "id": "wiki-positive-page-writes", + "codeHead": "dfdc39b3a71c74c40e267010c6d8428af523bb65", + "owner": "packages/plugins/plugin-llm-wiki/src/wiki/core.ts", + "sourceSha256": "35d8543f67ab80479c624f527d0b94fbb0f3b309d40b1276a07518872212ea06", + "case": "LLM Wiki plugin scaffold keeps default-space files at the root and isolates managed spaces under slug prefixes", + "expected": "one intended assertion failure", + "actualFailedCases": 1, + "intendedAssertionFailed": true, + "sourceRestored": true, + "failure": "AssertionError: expected [ { \u2026(2) }, { \u2026(2) } ] to have a length of 4 but got 2\n at Proxy. (file:///node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_@noble+hashes@2.4.0__v_wr5mxfuu6kgge6st623q4v5egm/node_modules/vitest/dist/chunks/index.C4tKZ0yW.js:2147:20)\n at Proxy. (file:///node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_@noble+hashes@2.4.0__v_wr5mxfuu6kgge6st623q4v5egm/node_modules/vitest/dist/chunks/index.C4tKZ0yW.js:194" + }, + { + "id": "aws-create-must-wait-before", + "codeHead": "dfdc39b3a71c74c40e267010c6d8428af523bb65", + "owner": "server/src/services/secrets.ts", + "sourceSha256": "2b7cac8d903802ea47c71033fa2d5dffb4d01a9ccde80837445235b0208b8e97", + "case": "secretService serializes an aws_secrets_manager secret create against a concurrent account-home mutation lock holder", + "expected": "old weak control passes", + "actual": "passed", + "intendedResult": true, + "sourceRestored": true + }, + { + "id": "aws-rotate-must-wait-before", + "codeHead": "dfdc39b3a71c74c40e267010c6d8428af523bb65", + "owner": "server/src/services/secrets.ts", + "sourceSha256": "2b7cac8d903802ea47c71033fa2d5dffb4d01a9ccde80837445235b0208b8e97", + "case": "secretService serializes an aws_secrets_manager secret rotate against a concurrent account-home mutation lock holder", + "expected": "old weak control passes", + "actual": "passed", + "intendedResult": true, + "sourceRestored": true + }, + { + "id": "aws-create-must-wait-after", + "codeHead": "445d568ca4e903fc998819cae31d421403514684", + "owner": "server/src/services/secrets.ts", + "sourceSha256": "2b7cac8d903802ea47c71033fa2d5dffb4d01a9ccde80837445235b0208b8e97", + "case": "secretService serializes an aws_secrets_manager secret create against a concurrent account-home mutation lock holder", + "expected": "repaired control fails on provider entry before lock release", + "actual": "failed", + "intendedResult": true, + "sourceRestored": true, + "failure": "AssertionError: expected [ 'cleanup-enter', \u2026(1) ] to deeply equal [ 'cleanup-enter' ]\n at /server/src/__tests__/secrets-service.test.ts:1013:22\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at file:///node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_@noble+hashes@2.4.0__v_wr5mxfuu6kgge6st623q4v5egm/node_modules/vitest/dist/chunks/run.BTlFXlnv.js:2783:20" + }, + { + "id": "aws-rotate-must-wait-after", + "codeHead": "445d568ca4e903fc998819cae31d421403514684", + "owner": "server/src/services/secrets.ts", + "sourceSha256": "2b7cac8d903802ea47c71033fa2d5dffb4d01a9ccde80837445235b0208b8e97", + "case": "secretService serializes an aws_secrets_manager secret rotate against a concurrent account-home mutation lock holder", + "expected": "repaired control fails on provider entry before lock release", + "actual": "failed", + "intendedResult": true, + "sourceRestored": true, + "failure": "AssertionError: expected [ 'cleanup-enter', \u2026(1) ] to deeply equal [ 'cleanup-enter' ]\n at /server/src/__tests__/secrets-service.test.ts:1081:22\n at processTicksAndRejections (node:internal/process/task_queues:104:5)\n at file:///node_modules/.pnpm/vitest@5.0.3_@opentelemetry+api@1.9.1_@types+node@24.13.3_jsdom@30.0.1_@noble+hashes@2.4.0__v_wr5mxfuu6kgge6st623q4v5egm/node_modules/vitest/dist/chunks/run.BTlFXlnv.js:2783:20" + } + ], + "unmutatedControls": [ + { + "batch": "round4 owners and keepers", + "files": 58, + "passed": 1455, + "failed": 0, + "pending": 0 + }, + { + "batch": "tool-access owners and state keepers", + "passed": 623, + "failed": 0, + "pending": 0 + }, + { + "batch": "final state-service owners and keepers", + "files": 8, + "passed": 329, + "failed": 0, + "pending": 0 + } + ], + "preservationReviews": [ + { + "batch": "round4-ui", + "baseline": "2633dbbba95e853e3cb0de165bdf5116e544e411", + "method": "Independent whole-runtime AST comparison after actual new helper expansion and individually reviewed semantic edits. Type-only syntax and source metadata normalized; compiler is a separate gate.", + "files": [ + { + "file": "ui/src/components/IssueProperties.test.tsx", + "sourceSha256": "6c0a2420fdffeb3ccf6c9f4b43b61b62c3bd0e107276a91402a25ae08ad54bd2", + "proposalSha256": "8d604724e4c9c423b560d13dae6e939113f248b050905542970ee0660dfeab98", + "fullProgramAstEqualAfterActualHelperExpansionAndEnumeratedBehaviorEdits": true, + "helperCalls": { + "createRelation": 17 + } + }, + { + "file": "ui/src/components/AgentConfigForm.render.test.tsx", + "sourceSha256": "dd3703a491b43ee5b7cd57c48798e8a4cec4dbabc075f222cea412383eca49d7", + "proposalSha256": "57282ae3afe8c55ac118f3a658152d8df500a87b15383c8f6b0530a7fdcfb8a0", + "fullProgramAstEqualAfterActualHelperExpansionAndEnumeratedBehaviorEdits": true, + "helperCalls": { + "createFormQueryClient": 17, + "renderFormView": 18, + "daytonaEnvironments": 12, + "e2bEnvironments": 6 + } + }, + { + "file": "ui/src/components/OnboardingWizard.test.tsx", + "sourceSha256": "c17e65d7e0604b58d398cdf8a2a8588e7bc388ffe6e2612cee8e567e01a30f20", + "proposalSha256": "6215ee2f034686deaf2d6091590030dcdeccff1697422574e45e0351eaf5a7fe", + "fullProgramAstEqualAfterActualHelperExpansionAndEnumeratedBehaviorEdits": true, + "helperCalls": { + "renderWizardView": 19 + } + }, + { + "file": "ui/src/components/IssueThreadInteractionCard.test.tsx", + "sourceSha256": "3e3ebbe3da7ad7edd9386760bcf90a83c987956fc4f65b74bb761bd0160ce746", + "proposalSha256": "2e8aefda409a2faa7e3a3bc4d3f292a17086c61eac1e34291002b62c7415a184", + "fullProgramAstEqualAfterActualHelperExpansionAndEnumeratedBehaviorEdits": true, + "helperCalls": {} + }, + { + "file": "ui/src/components/MarkdownBody.test.tsx", + "sourceSha256": "1be4fe615ed6d9e1b8b49749f291475c12848beb5664758c512f368103121608", + "proposalSha256": "7432e047a21ca0a75dc58ca0fd645c7cb0b59edc1719557e4e0cb0aafc8a642a", + "fullProgramAstEqualAfterActualHelperExpansionAndEnumeratedBehaviorEdits": true, + "helperCalls": {} + } + ] + }, + { + "batch": "round4-server", + "baseline": "2633dbbba95e853e3cb0de165bdf5116e544e411", + "method": "Independent whole-runtime AST comparison after actual new helper expansion and individually reviewed semantic edits. Type-only syntax and source metadata normalized; compiler is a separate gate.", + "files": [ + { + "file": "server/src/__tests__/issue-comment-reopen-routes.test.ts", + "sourceSha256": "214aaa47ce1a862e4d06f41a440d0dba2a2f9f2efa63259c635209a3f660c33e", + "proposalSha256": "2dc9167f66f379cb5a1fe024faf78a79c7009d570c4ca4acc72448d0a00423c7", + "fullProgramAstEqualAfterActualHelperExpansionAndEnumeratedBehaviorEdits": true, + "helperCalls": { + "createScheduledRetry": 4, + "createAgentReviewPolicy": 12, + "createReviewIssue": 9 + } + }, + { + "file": "server/src/__tests__/environment-routes.test.ts", + "sourceSha256": "8993233f12a3b646f7a9755b817bac68916fda31753c2ffb30c4eba9fbcc9fe8", + "proposalSha256": "ef2dd81154c25791d2020362b6a3cd508ae215d60b075e1496ea9c7aa38ee6bd", + "fullProgramAstEqualAfterActualHelperExpansionAndEnumeratedBehaviorEdits": true, + "helperCalls": { + "localBoardActor": 39, + "createSecurePluginDriver": 6, + "createSshConfig": 4, + "createSecretBackedSshEnvironment": 3 + } + }, + { + "file": "server/src/__tests__/instance-settings-routes.test.ts", + "sourceSha256": "f007e394234f82d0014a1f2d83831942d0c3a2b123e026f4c22edd4d6491754f", + "proposalSha256": "85f0ddf9afb42cfc3e243e107b47290d9f8c8e5879eb3932805466019c937dc4", + "fullProgramAstEqualAfterActualHelperExpansionAndEnumeratedBehaviorEdits": true, + "helperCalls": { + "localAdminActor": 15 + } + }, + { + "file": "server/src/__tests__/agent-skills-routes.test.ts", + "sourceSha256": "e9c67d26d49c51862cefdde9388893d0e11b32f7d29ee95c664741cd48bc7468", + "proposalSha256": "175b4b5f274141a817c2135830c1b73ecdfb9597e1d0579af1d0207c6faf551e", + "fullProgramAstEqualAfterActualHelperExpansionAndEnumeratedBehaviorEdits": true, + "helperCalls": { + "skillSnapshot": 6 + } + }, + { + "file": "server/src/__tests__/company-skills-routes.test.ts", + "sourceSha256": "9fdfa5790bd33b9824e3aeed28daa01065eab248712d2f9482f633f7c33165a9", + "proposalSha256": "7037feadb73f235f81afeb9499b5bc9b988e2057b43add48c7546f6558085a03", + "fullProgramAstEqualAfterActualHelperExpansionAndEnumeratedBehaviorEdits": true, + "helperCalls": { + "createCatalogSkill": 3, + "createSkillTestRunDetails": 5, + "boardActor": 25, + "assignedAgentActor": 7 + } + } + ] + }, + { + "batch": "round4-cli-api-ticker", + "method": "Actual proposed helper bodies expanded. Source metadata/comments/types and object shorthand normalized; object fields, array rows, JSX props, all values/callbacks/promises/awaits, action/write/Git order and cleanup remain strict. Statement-helper parameters are checked to be stable Identifier bindings. Only enumerated reviewed retirement/migration deltas are permitted.", + "checks": [ + { + "file": "cli/src/__tests__/company.test.ts", + "sourceSha256": "d957294d6113f3b546e15e36289f5085852fc5ab1350529bc025a192fc05d378", + "proposalSha256": "1cd0c0445ab57a8c74416a63da6acb4ddde36e8c09aa5b3c74db589792445be1", + "actualHelperExpansionWholeRuntimeAstEqual": true, + "expandedCalls": { + "allImportEntities": 6, + "sourceCompanyManifest": 2, + "ceoManifest": 2, + "alphaProjectManifest": 2, + "kickoffIssueManifest": 2, + "openAiEnvInput": 2 + } + }, + { + "file": "packages/shared/src/app-definitions.test.ts", + "sourceSha256": "cf1aec35f1f4c65dd7059f594e04030c339c2a2f329f6e8216a431280d3865c3", + "proposalSha256": "d6aa2d7ba801b049358e89f48d39f3602ac936d1c28e7cd5b3313a2227fd1a95", + "actualHelperExpansionWholeRuntimeAstEqual": true, + "expandedCalls": {} + }, + { + "file": "packages/plugins/plugin-llm-wiki/tests/plugin.spec.ts", + "sourceSha256": "21ae3eef99bb450f2cc0cac8cb7d781f0dbb043ad4fdf8c1d09b528021114242", + "proposalSha256": "bcb816d66be0b0176d599bbb705f79724abeebe8f52f28ed1fc31787b06f9985", + "actualHelperExpansionWholeRuntimeAstEqual": true, + "expandedCalls": { + "renderWikiPage": 22, + "captureWikiWrites": 9, + "managedSpaceRow": 3 + } + }, + { + "file": "cli/src/__tests__/worktree.test.ts", + "sourceSha256": "90162a4d544e80d7314649f2d0da5ca34c6968fbd4a63e491c57f5fdf7077db7", + "proposalSha256": "4dd193538aaf9bdd0cab522a8647ea5fb6a573e26199561e65c725c8bcb51ce1", + "actualHelperExpansionWholeRuntimeAstEqual": true, + "expandedCalls": { + "writeSeedConfigFiles": 6, + "initializeTestRepo": 5 + } + } + ], + "canonicalChecks": [ + { + "file": "cli/src/__tests__/worktree.test.ts", + "baselineSha256": "47c51bafca401b39bd976c12ddabef00be8ff6d3c2bc79822c49d11a3f79bc85", + "repairedSha256": "90162a4d544e80d7314649f2d0da5ca34c6968fbd4a63e491c57f5fdf7077db7", + "wholeRuntimeAstEqualApartFromParentCanonicalization": true, + "canonicalTempRootWrappers": 35 + }, + { + "file": "packages/shared/src/worktree-seed-source.test.ts", + "baselineSha256": "bd8708bab463280939f0aa8aa18341b028343fe98626d9be2154c87ab191c1a5", + "repairedSha256": "b9d021050cced44bcee00fd3209c225c4e26117693b7491a50ef9d3bd0a67d17", + "wholeRuntimeAstEqualApartFromParentCanonicalization": true, + "canonicalTempRootWrappers": 3 + } + ], + "coreChecks": [ + { + "file": "ui/src/api/client.ts", + "sourceSha256": "cac548df1d20c87a0ea8eede850cfe5081874262eb8c26c78058923db6947f69", + "proposalSha256": "1e1b3f03e3269a5943f55d294f1c7aa9f5c3eca569858d9fdc7c5d976de88bba", + "onlyDeletedPrivateGetterExport": "__inflightGetCount" + }, + { + "file": "ui/src/hooks/useSecondTick.ts", + "sourceSha256": "e108dee2647be4f8f5472a70220d1019b18cce9a842e7bc25b863e298654ab07", + "proposalSha256": "fd51875adf5f30295d847df58af19f75dcbb59c810b4f02919427cbc6ea4d3b3", + "onlyDeletedPrivateGetterExport": "__secondTickInternals" + } + ], + "seamTestChecks": [ + { + "file": "ui/src/api/client.test.ts", + "sourceSha256": "b056adfc0a935cdabcc1c05b4473f76bc1f46fc6f280a671404c2735211ebe01", + "proposalSha256": "1b3666404821ae4550b1fdf48fe27568077f8074dbdff8afc5f2966e79341ffd", + "apiWholeRuntimeAstEqualApartFromReviewedPrivateRemovalAndErrorAbortRetry": true, + "oldDeclarations": 12, + "newDeclarations": 12 + }, + { + "file": "ui/src/hooks/useSecondTick.test.tsx", + "sourceSha256": "29d8f37a65070f28ddb43bec69e74477cdfb1e563b348aff03b6b5ca17902e95", + "proposalSha256": "b59f9bab5e41bc611165577d8e095184094dd5b84b6d436d32656fd33fb5e459", + "oldDeclarations": 2, + "newDeclarations": 2 + } + ] + }, + { + "batch": "round5-tools", + "baseline": "3e7ee04591fd82f3effb89370f47aa90dcadd6eb", + "method": "Only source metadata/comments and object shorthand differ. Actual candidate helper bodies expanded, removing only their new declarations and type-only IdRef aliases. Object-field, nested object, array, table, JSX-free input, promise, await, callback, actor/company/credential/expected value and statement order remain exact. Newly introduced literal spreads are allowed only when there are no overwritten keys or effectful helper arguments. All helper object bodies are fresh literals without external closure state.", + "files": [ + { + "path": "server/src/__tests__/tool-access-service.test.ts", + "sourceSha256": "4cd7d6d02710eac514f14d87c316d15391028ae36c477d868f6b5a499a29f406", + "proposalSha256": "dc3664a18f9fc6c45b3b5555376ae03def2708129ff5c3dc4f4623ea2f994c15", + "completeRuntimeAndTypeAstEqualAfterActualHelperExpansionAndOneDeclarationRemoval": true, + "helperOccurrences": 78, + "allRemainingMatcherCallsEqual": true, + "allAwaitExpressionsEqual": true, + "allSupertestCallsEqual": true, + "remainingMatcherCalls": 1346, + "awaitExpressions": 1930, + "beforeDeclarations": 266, + "afterDeclarations": 265 + }, + { + "path": "server/src/__tests__/tool-access-policy-service.test.ts", + "sourceSha256": "484408a4eb3481e98d89863943b0fac64cf40e3794c06d0b4326a4b0d0f7625f", + "proposalSha256": "6982ad8f42fceeb6428117f22db28a589f9b6d5f1b2465ca87ddd29420e06413", + "completeRuntimeAndTypeAstEqualAfterActualHelperExpansionAndOneDeclarationRemoval": true, + "helperOccurrences": 31, + "allRemainingMatcherCallsEqual": true, + "allAwaitExpressionsEqual": true, + "allSupertestCallsEqual": true, + "remainingMatcherCalls": 80, + "awaitExpressions": 284, + "beforeDeclarations": 35, + "afterDeclarations": 35 + }, + { + "path": "server/src/__tests__/tool-gateway-service.test.ts", + "sourceSha256": "29c9b9739a5f928c49cd2982858a1323c242663450277f45fc9e3aea407cbe98", + "proposalSha256": "67d8dfb12ef2215296af87be5c573f40175d71a2917b01113aa82c550fe4b6d6", + "completeRuntimeAndTypeAstEqualAfterActualHelperExpansionAndOneDeclarationRemoval": true, + "helperOccurrences": 58, + "allRemainingMatcherCallsEqual": true, + "allAwaitExpressionsEqual": true, + "allSupertestCallsEqual": true, + "remainingMatcherCalls": 231, + "awaitExpressions": 431, + "beforeDeclarations": 36, + "afterDeclarations": 36 + } + ] + }, + { + "batch": "round5-state", + "baseline": "3e7ee04591fd82f3effb89370f47aa90dcadd6eb", + "method": "Independent full runtime AST expansion outside two individually reviewed AWS lock repairs", + "files": [ + { + "file": "server/src/__tests__/issue-thread-interactions-service.test.ts", + "beforeSha256": "ddfad204b976a8218b6376007c36048154c4a10e25ef4d891531b87477931c10", + "afterSha256": "6c70d8af0815de3d3ed9f87de94d20e7a161aa2db5fe938e80204a100ee418e9", + "equivalent": true, + "expandedBeforeSha256": "0a7affd96c9e84850e2540edea63bbc6eabf24f124468e2526fa3095f2ddd7af", + "expandedAfterSha256": "0a7affd96c9e84850e2540edea63bbc6eabf24f124468e2526fa3095f2ddd7af", + "allHelperCalls": { + "interactionCompanyRow": 14, + "parentIssueRow": 7, + "activeCodexAgentRow": 19, + "acceptIssueScope": 31 + }, + "allHelperArgumentsEffectFree": true, + "beforeDeclarations": 89, + "afterDeclarations": 89 + }, + { + "file": "server/src/__tests__/secrets-service.test.ts", + "beforeSha256": "ab9c9536ee144d9211bb8a205dd2e57ff928360de6ea695af74afd749e9652be", + "afterSha256": "95708c40da78768c43c26a56c73cf9479a4a39bf2173561d9501a1ab6d13dfb1", + "equivalent": true, + "expandedBeforeSha256": "fb5da5592c1de24b62a44e476554924e0854cd2822767e46132ce63431c11475", + "expandedAfterSha256": "fb5da5592c1de24b62a44e476554924e0854cd2822767e46132ce63431c11475", + "allHelperCalls": { + "awsVaultInput": 29, + "preparedAwsVersion": 16, + "managedAwsVersionRow": 4 + }, + "allHelperArgumentsEffectFree": true, + "beforeDeclarations": 91, + "afterDeclarations": 91 + } + ] + } + ] +} diff --git a/doc/plans/2026-10-12-test-audit-next.md b/doc/plans/2026-10-12-test-audit-next.md new file mode 100644 index 00000000000..ff2b377c51c --- /dev/null +++ b/doc/plans/2026-10-12-test-audit-next.md @@ -0,0 +1,121 @@ +# Test audit: consolidate repeated owner fixtures and rendering + +Date: 2026-10-12 +Branch: `codex/test-audit-next` +Discovery baseline: `b4aee6ea18905690686fb318fa9cc6d4cc71a7e1` +Integration base: `1e0cfcb4fc4ed78d10f46cd60990935af4ac9973` (merged master) +Verified code commit: `4261a7fc1b964e54059c68246a0a4b7ea74bdcea` +Parent cleanup: [PR #15978](https://github.com/paperclipai/paperclip/pull/15978) + +This follow-up removes **2,041 net test lines across five files**. Every +registered case and assertion remains. It changes no application source. +The seven complete owner/sibling files pass all **990 cases** before and after. +Workspace typecheck, build, token gates and diff checks pass. The change +consolidates fixture setup while preserving execution count. + +The parent PR merged on 2026-10-12 after all remote checks passed and a +fresh Greptile 5/5 review. This branch used a separate managed checkout and +dependency installation. Only its new follow-up commit was replayed onto +the parent merge, with no conflicts or duplicated cleanup commits. All five +test files still exactly match the independently reviewed candidates, and +all 990 owner/sibling cases pass again on the merged base. The follow-up remains +committed locally on its own branch. Earlier cleanup evidence, including its +disclosed local timeout and passing owner reproduction, lives in +`2026-10-12-test-audit.md`. + +## Measured change + +| File | Before lines | After lines | Net removed | Cases retained | +| --- | ---: | ---: | ---: | ---: | +| server/src/__tests__/environment-runtime.test.ts | 8,110 | 7,223 | 887 | 162 | +| server/src/__tests__/execution-workspaces-service.test.ts | 4,751 | 4,638 | 113 | 67 | +| server/src/__tests__/company-portability.test.ts | 6,194 | 6,100 | 94 | 94 | +| ui/src/pages/IssueDetail.test.tsx | 6,652 | 6,338 | 314 | 141 | +| ui/src/components/IssueChatThread.test.tsx | 5,032 | 4,399 | 633 | 100 | +| **Changed owners** | **30,739** | **28,698** | **2,041** | **564** | + +The remaining 426 validation cases are unchanged TaskChatThread (225) and +AppsConnect (201) siblings. Figures exclude this report. No case, timeout, +skip condition, filename, CI route or production seam is removed. + +## Owner and keeper decisions + +All nominated cases are retained. Each original case remains the primary +keeper of its regression assertions; helpers own only repeated inputs. + +- Environment runtime: 71 repeated plugin/lease/worker/foreign-company + expressions become synchronous file-local fixture builders. Complete + identity, author, schema, readiness, capability, install-order, stale lease, + fingerprint and RPC-budget variants remain defaults or explicit overrides. + Preserve real persisted rows and the actual foreign-company rejection, + orphan teardown/spool recovery and lease-reuse paths. Dynamic/spread rows + and the named-constant secret provider stay inline. +- Execution workspaces: 19 repeated task/recovery/branch/runtime/agent inputs + become synchronous builders. Real Git operations, active claimant/runtime + rows, locked races, clean/dirty divergence and durable recovery assertions + remain in their original cases. +- Company portability: 15 identical export invocations use one local helper; + their distinct fixture data, redaction, permission, import, selected-file, + logo and membership expectations remain explicit. +- IssueDetail: 98 identical root/provider render frames share one synchronous + helper. It retains the same QueryClient and root. Four nondefault children + remain explicit JSX; unsafe undefined/key/ref/spread shapes stay inline. +- IssueChatThread: 70 MemoryRouter/component frames share one synchronous + helper with fresh original defaults. Supplied callback references, props, + dynamic expression order, root identity and rerender/remount behavior remain. + +TaskChatThread and AppsConnect already have useful fixtures or distinct +transport/identity behavior. A TaskChatThread prototype saved only ten lines +after helper overhead and was rejected. No further layer retirement was +supported in these owners; separate provider, authentication, lifecycle and +storage proof remains valuable. + +## Preservation evidence + +Original/candidate SHA-256 guards were validated before any checkout edit. +Proposals were prepared outside the checkout during baseline execution. +Independent reviewers checked both groups against the committed baseline. + +Server: expand all 105 calls through the 16 actual helper bodies/defaults and +compare all three complete programs. Field order, nested schemas/capabilities, +types/casts, table rows, callbacks, hooks, 1,347 awaits and 1,201 expectation +calls are unchanged. Helpers add no Promise boundary, transaction, UUID, +clock read, receipt, assertion, RPC or reset. Objects and mocks remain fresh. + +UI: independently expand all 98 + 70 actual render calls. The complete program +outside those checked expressions is unchanged, including types, callbacks, +actions, waits, timers, teardown and budgets. All 778 expectation calls and +200 declarations remain. Check every thread root identity and dynamic prop +evaluation order; check IssueDetail root/client bindings for shadowing. +Only unique JSX attribute order is normalized; key/ref/spread shapes are +excluded. No React component wrapper or hook is introduced. + +Both independent reviews found no preservation blocker. No assertion was +moved or repaired, so no additional production mutation was needed. The +earlier parent PR's mutation proof is recorded in its report. + +## Runtime verification and limits + +- Complete baseline: seven files, 990 passed, zero failed/skipped. +- Complete candidate: the same seven files, 990 passed, zero failed/skipped. +- Rebased candidate: the same seven files, 990 passed, zero failed/skipped. +- `pnpm --filter @paperclipai/ui typecheck`: pass before integration. +- `pnpm -r typecheck`: pass before integration and at the verified code commit. +- `pnpm build`: pass at the verified code commit. +- Token gates, palette synchronization and character synchronization: pass. +- `git diff --check`: pass. +- The new checkout used its own workspace-linked dependencies and built its + own TypeScript runner/shared/plugin SDK prerequisites. No dependencies or + source files were linked to the concurrently changing PR checkout. +- A complete default `pnpm test:run` has not been repeated for this follow-up. + Its test-only fixture changes have complete affected-owner/sibling proof. + Browser suites were not run; none of those suites changed. The parent PR + has its own full CI evidence. No wall-time speedup is attributed to this work. + +## Additional retirement search + +A bounded independent search found no further defensible retirement of +200 or more lines. Provider-specific behavior, separately implemented +parsers, CSS contrast/theme/layout contracts and runtime/SDK wiring retain +independent proof. Similar source guards or DOM class assertions did not +provide complete behavioral keepers, so those layers remain. diff --git a/doc/plans/2026-10-12-test-audit-rounds.md b/doc/plans/2026-10-12-test-audit-rounds.md new file mode 100644 index 00000000000..ef3c12da564 --- /dev/null +++ b/doc/plans/2026-10-12-test-audit-rounds.md @@ -0,0 +1,92 @@ +# Test audit: repeated setup, duplicate cases and false proof + +Date: 2026-10-12. Audit branch: `codex/test-audit-next`. Delivery branch: `codex/test-audit-followup`. Verified code revision: `64370831ebe191e5ce092f85f79b6fb661df7e16`, including master `cc88b61c0918f45f6d3215f5d2d2368c88297046`. + +This follow-up removes **5,027 net test lines across 27 test files** and **15 redundant registered executions**. It separately removes **11 unused application lines** from two private test-inspection exports. Most savings come from sharing repeated setup while retaining every distinct case. No wall-time improvement is inferred from line counts. + +The earlier merged [PR #15978](https://github.com/paperclipai/paperclip/pull/15978) removed 11,976 net test/support lines and 1,073 redundant executions. The two changes together save **17,003 test/support lines**. These counts exclude audit reports, upstream changes and the fixture portability repair. + +## Savings and scope + +| Batch | Net test lines removed | Registered executions removed | Preservation | +| --- | ---: | ---: | --- | +| Earlier local runtime/workspace/portability and IssueDetail/IssueChatThread | 2,041 | 0 | All original cases and assertions retained; 990 owner/sibling cases passed. See [the earlier follow-up report](2026-10-12-test-audit-next.md). | +| Route fixtures and duplicate proof | 970 | 5 | Three redundant cases and two identical table executions removed; cancelled-reopen observation repaired. | +| UI fixtures and duplicate proof | 521 | 6 | Exact assertions move into named stronger keepers; two weak observations repaired. | +| CLI, shared catalog and wiki plugin | 238 | 3 | Exact keepers and fixture expansion; genuine run-receipt and positive-write witnesses added. | +| Public API and ticker observations | -10 | 0 | Tests grow by ten lines to replace private-state inspections with observable results. | +| Tool access, policy and gateway | 656 | 1 | One exact omitted-scope duplicate removed; all other matcher and await order retained. | +| Interactions and secrets state services | 611 | 0 | All 180 declarations / 197 cases retained; two AWS lock observations repaired. | +| **Total follow-up** | **5,027** | **15** | **No independent security or lifecycle contract retired.** | + +Line savings include all newly added local helper code. The application reduction is five lines of `__inflightGetCount` and six lines of `__secondTickInternals`. Repository-wide caller searches found only their tests. Both module cores otherwise remain byte-identical. Their tests retain sharing, cancellation, stale-account isolation, rendered ticks and timer lifecycle. + +The final intended diff has 27 test files, those two application files and audit evidence. No schema, telemetry, runtime service, package manifest, lockfile or CI routing change is part of this cleanup. Incoming master changes were merged, so absent upstream files cannot inflate the savings. + +## Removed execution to keeper map + +Each retained declaration is its own keeper for setup-only consolidation. The following is the complete map for the 15 removed executions; the three-row influence table accounts for two of them. + +| Removed proof | Retained keeper and boundary | +| --- | --- | +| Managed-sandbox duplicate DELETE rejection | Same owner/admin, platform row, DELETE, 403 and code; stronger keeper also forbids blast-radius/removal calls. | +| Agent-skills duplicate GET/POST secret-context check | Mandatory resolver callbacks in both successful route keepers check the same actor/user/source/responsible literal context, adapter type, skip-user-secrets and filtered environment. | +| Company-skills open-default legacy-consent duplicate | Same actor/company/run, deny-missing-consent decision and POST input; successful write keeper checks no consent-gate invocation and the actual write. | +| Two extra missing/wrong-run influence table rows | All three ran the identical zero-argument callback. One retains both POST/PATCH errors, details, two propagation calls and no writes. The identity-variant service sibling and real PostgreSQL race sibling remain. | +| Onboarding missing-auth/no-stored-login duplicate | Warning text moves into `blocks the hire on a warn result that holds adapter_auth_missing, and shows the returned checks`. | +| Interaction connection controls for another viewer | `shows another viewer only who it is waiting for` retains waiting copy, zero buttons, no actions/auth URL and absence of Not now. | +| Legitimate question-card rendering duplicate | `exposes pending question options as selectable radio and checkbox controls` gains positive child count and retains stronger exact radio counts. | +| Addressed-user Connect/Not-now duplicate | `opens the shared connection setup for the addressed user` gains the exact copy and absence checks before executing the real setup action. | +| Non-GitHub-link icon duplicate | `opens external links in a new tab with safe rel attributes` uses the same docs input and retains exact anchor, no GitHub icon, target and rel. | +| External-link trailing-icon duplicate | The same docs-link keeper retains the icon glued to the final character and the original glyph matcher. | +| Hugging Face scope duplicate | `requests Airtable record, schema and comment writes, and Hugging Face repository/job actions` retains the same independent `[read-mcp, read-repos, contribute-repos, jobs]` literal. | +| Whole-catalog method/field invariants duplicate | Full-catalog `appDefinitionsSchema.parse` enforces nonempty ownership modes, required credential placeholders and API-key key placement for every definition; provider-specific permission literals remain. | +| Worktree shutdown formatter duplicate | Real `ensure-seeded` failure retains restore error, safe diagnostic, failed manifest and lock release. The unique migrations fallback expectation moves unchanged into that keeper. | +| Tool-access omitted-provider-scope duplicate | `marks a scope inferred from the request when the provider omits one` calls the identical `connectSlackWithTokenScope(null, ["channels:read"])`, retains all three persisted scope fields and also checks the outgoing authorization scope. Reported over-grants remain separately tested. | + +The influence identity sibling uses a fake `counterDb` adapter for locked-row variants. The PostgreSQL sibling independently inserts real rows and races writes. Neither is removed, and fake SQL inputs are not described as durable persistence proof. + +## Preservation review and repairs + +Every nominated owner was read in full, including hooks, declarations, tables and cleanup, before classification as retain, fix false proof, consolidate or delete. Reviews checked actual owner/caller/history/CI boundaries rather than treating similar names as proof. Separate reviewers expanded the actual new helper bodies and arguments against complete original runtime ASTs. Types and source metadata are normalized separately from the compiler gate. Fields, nested values, fresh allocations, action/query/UUID/Date/callback/await order, tables, timeouts and teardown remain strict outside explicitly reviewed semantic edits. + +The tool-access expansion covers 167 helper occurrences, 1,657 remaining matcher calls and 2,645 awaits. The state-service expansion covers 120 helper calls; no DB query is hidden in a new helper, and no new helper manufactures a tested service result. All 106 interaction and 91 secret cases remain. Two AWS repairs add two provider-call assertions while retaining the original durable results. + +Review restored or strengthened these contracts before cutover: + +- Restore the no-agentMap resolved footer/chip case byte-for-byte; a richer mapped-agent case cannot prove its fallback. +- Match the exact external `