mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-12 22:56:58 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Plugin environment drivers with `supportsTasks` run agents as provider tasks, and warm runs keep a parked task for the next run > - A follow-up run can find that its parked task already stopped. Core then retires that task before it acquires a new one > - Core sent `stop`, `status`, and `complete` in the settle step. Then the plugin release hook sent `stop`, `status`, and `complete` again for the same task. Every call was on the critical path of the new run > - This pull request removes the repeated calls and keeps every call that does work no other call does > - The benefit is a faster start for a run whose parked task already stopped ## Linked Issues or Issue Description No issue exists. This follows the enhancement template. **What existing behavior does this improve?** The release of a plugin task lease that has already stopped. This occurs when a follow-up run finds that its parked warm task stopped. **Subsystem affected** Server: plugin environment drivers and the plugin Runner task lifecycle. **Current behavior** Core sends `stop`, `status`, and `complete` in the settle step. Then the plugin release hook sends `stop`, `status`, and `complete` again for the same task. Core also sends `stop` after resume already saw that the task stopped. On a hosted test deployment, 8 of 40 follow-up runs took this path. From the resume to the new acquire, these runs spent P50 1014 ms and P90 1080 ms. These 8 runs are the full overall P90 (1275 ms). **Proposed behavior** Core does not call the release hook when settle confirmed the stop and completed the same task. After a resume with no lease, Core sends `status` first, and sends `stop` only when `status` does not confirm the stop. **Reason and benefit** The repeated calls do no new work. Removing them saves about 355 ms, plus the `stop` share of about 187 ms, for each such run. These numbers are modeled from trace timings. **Breaking changes** None for the plugin API. A task driver must release task-scoped resources in its `status` or `complete` handling. The doc now says this. **Additional context** The settle `status` stays. Providers release task storage there, and it records the stop that gates the credential writeback and `complete`. ## What Changed - `settleDispatchedEnvironmentTask` now returns `"completed"` when it confirmed the stop and completed the task in this call, `"stopped"` when the lease was already settled, and `false` otherwise. - The plugin driver release does not call `environmentReleaseLease` when settle returned `"completed"`. In all other cases it calls the hook as before. - When `environmentResumeLease` returns no provider lease, the release sends `status` first. It sends `stop` (and a second `status`) only when that `status` does not confirm `executionStopped: true`. Startup and startup-cleanup leases keep the old `stop` first order. - The cleanup sweep (`retryPendingSandboxTeardown`) and `destroyRunLease` do not change. A settle failure still leaves the lease in `pending_cleanup`, and the sweep runs `stop`, `status`, `complete`, and the release hook. - `doc/plugins/PLUGIN_RUNNER_EXECUTION.md` tells drivers about the new order and that task-scoped resources must be released in `status` or `complete`. ### Dispatch count for a stopped parked task (resume to new acquire) | | Calls | | --- | --- | | Before | resume, stop, status, (credentials), complete, release hook (stop, status, complete), acquire: 9 provider calls (10 with Codex writeback) | | After | resume, status, (credentials), complete, acquire: 4 provider calls (5 with Codex writeback) | The modeled saving is about 540 ms for each such run: about 355 ms for the repeated release, and about 187 ms for the settle `stop` and `status`. This pull request keeps the settle `status`, so the real saving is about 355 ms plus the `stop` share of the 187 ms. These numbers are modeled from trace timings. They are not measured on this branch. ### What covers each removed call - Release hook `stop`, `status`, `complete`: settle already sent `stop` and `status` and checked `executionStopped: true` for the same task id (`lease.providerLeaseId`), then sent `complete`. The skip uses settle's success as its condition, not the code path. - Settle `stop` after a resume with no lease: the next `status` must report `executionStopped: true`. If it does not, Core sends `stop` and `status` as before. ### What each kept call protects - Settle `status`: providers release task storage and cancel preparations here. It also writes `runnerTaskExecutionStopped`, which the credential writeback and the `complete` bookkeeping need. - Credential writeback: Codex can rotate its token. The writeback must land before the new task installs credentials. It still runs inside `complete`, before the new acquire. - Settle `complete`: it retires every run lease that owned the task and clears the dispatch marker. - Release hook when settle did not complete a dispatched task in this call: the provider may still hold the lease. ## Verification - `pnpm --filter @paperclipai/shared --filter @paperclipai/plugin-sdk build` - `pnpm --filter @paperclipai/server typecheck` - `npx vitest run src/__tests__/plugin-warm-tasks.integration.test.ts src/__tests__/plugin-task-credential-writeback.test.ts src/__tests__/environment-task-runtime.test.ts` - Related suites: `environment-runtime`, `plugin-runner-task.integration`, `task-creation-recovery.integration`, `heartbeat-plugin-environment`, `native-startup-cleanup-recovery`, `plugin-environment-driver-ready-recovery`, `environment-task-authority.integration`, `native-workspace-warm-reuse`, `remote-workspaces.integration`, `legacy-workspace-restore-recovery`, `plugin-task-source-recovery.integration`, `managed-plugin-environments`. Locally, all of these pass except `heartbeat-plugin-environment` "inherits the instance default environment across companies…". That test also fails locally with the `origin/master` service files, so this change does not cause it. The existing `plugin-runner-task.integration` same-run replacement test now expects no release hook after a completed settle. New tests: - The stopped parked path sends exactly resume, `status`, `complete`, acquire. The `status` goes to the old task. - A resume with no lease for a task that still runs sends `status`, `stop`, `status`, `complete`. - A failure at `status`, `complete`, or `stop` leaves the lease in `pending_cleanup`, and the sweep then sends `stop`, `status`, `complete`, and the release hook. - With an observed stop, the Codex writeback runs before `complete`. A writeback failure leaves the lease for cleanup, and the retry writes back and completes. - A startup-cleanup lease still sends `stop` first. ## Risks - A third-party task driver that releases resources only in `onEnvironmentReleaseLease` would not get that call after a completed settle. The doc now states the requirement. The cleanup sweep still calls the hook after any failure. - A driver that returns no lease from resume while the task still runs costs one extra `status`. The behavior stays correct. - No migration. No API change for plugins. ## Model Used - Claude Opus 5.5 (`claude-opus-5-5`), via Claude Code with tool use and code execution. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
426 lines
25 KiB
Markdown
426 lines
25 KiB
Markdown
# Plugin Runner execution
|
|
|
|
Core can run `paperclip_runner` tasks in a plugin environment.
|
|
The plugin owns the execution host and its files.
|
|
Core owns run identity, selected policy, assigned tools, and the PRP journal.
|
|
|
|
## Managed AI connection required
|
|
|
|
A task-capable environment runs only agents with a managed AI connection.
|
|
Core does not use an account installed on the plugin's execution host.
|
|
Before it acquires a lease or calls the plugin, Core fails a new run that has
|
|
no managed AI connection. A `homeMode: "preconfigured"` Runner home does not use
|
|
the agent's AI connection, so Core refuses it in the same way. The run fails as
|
|
`configuration_incomplete` with reason `ai_connection_required`. The task is
|
|
blocked with a notice that names the fix: choose an AI connection, or switch to
|
|
an isolated Runner home. Agent verification applies the same rule before it
|
|
resolves the probe target, so it acquires no lease. The agent Test route also
|
|
checks before it resolves secrets or allocates a test target. A setup probe without a
|
|
managed AI connection, or with a preconfigured home, fails the
|
|
`ai_connection_required` check and calls no task operation. Verification then
|
|
waits for the configuration to change.
|
|
A resumed native run keeps the contract it was admitted under.
|
|
|
|
## Required hooks
|
|
|
|
Set `supportsTasks: true` on the environment driver.
|
|
Declare the `environment.drivers.register` capability.
|
|
Implement these hooks:
|
|
|
|
- `onEnvironmentAcquireLease`
|
|
- `onEnvironmentTask`
|
|
- `onEnvironmentReleaseLease`
|
|
- `onEnvironmentDestroyLease`
|
|
|
|
Task execution does not require `onEnvironmentExecute` or
|
|
`onEnvironmentRealizeWorkspace`. The task API has no shell command or file installation
|
|
operation. Use a separate workflow to install agent files. Providers that
|
|
advertise `workspaceFileReadVersion: 1` can return task output files through the
|
|
bounded `read_file` operation for attachments and deliverables.
|
|
|
|
Core supports per-turn workspace sessions with Codex, OpenCode, and ACPX.
|
|
OpenCode and ACPX require the existing qualified provider profiles.
|
|
Workspace-free providers are not supported here. Drivers opt into warm retention through `warmTaskVersion: 1`. Core prefers warm execution for new task runs on capable drivers, including ACPX; existing provider admission still determines which harnesses the driver supports.
|
|
The plugin owns project workspace preparation. Core does not prepare or reuse
|
|
a Core Git worktree for a task-only environment.
|
|
Selecting a plain task directory does not add redundant default configuration.
|
|
Retaining that directory does not itself change the run configuration.
|
|
For plugin-backed project storage, Core uses the workspace provider contract.
|
|
The storage plugin must also provide the selected execution environment.
|
|
Its declared workspace modes and strategies remain in effect.
|
|
Core passes the pinned workspace binding with each task operation.
|
|
Core rejects configured workspace services for task-only environments.
|
|
Fresh retries that import retained files are not supported for plugin storage.
|
|
An agent-owned workspace cannot move to another agent or environment.
|
|
Core also rejects reuse of a local workspace in a task-only environment.
|
|
Keep its original owner, or use the workspace storage provider contract.
|
|
|
|
## Prepare and launch
|
|
|
|
1. Return an opaque task ID from lease acquisition as `providerLeaseId`.
|
|
2. Return the installed Runner version and SHA-256 digest from `describe`.
|
|
3. Return `launchPreparationVersion: 1` from `describe`.
|
|
4. Handle `prepare` with the [launch preparation contract](../runner-launch-preparation.md).
|
|
5. Save the result under its configuration ID.
|
|
6. Handle `submit` with that ID in `preparedLaunchId`.
|
|
7. Return an authenticated WSS endpoint from `connection`.
|
|
|
|
Preparation must not start a process. It must preserve Core's provider, model,
|
|
policy, instructions, and resource assignment identities. Resolve assigned
|
|
resources to execution-host paths. Use `bindRunnerLaunchResources` and
|
|
`createRunnerProviderLaunch` when the built-in descriptor builder is suitable.
|
|
|
|
Core checks the returned requirements digest and descriptor before it registers
|
|
PRP or submits the task. A failed check does not enable a fallback launch.
|
|
Recovery within the same run must return the exact saved configuration.
|
|
|
|
The plugin supplies executable paths and the provider workspace in the
|
|
descriptor. It owns the process environment and account installation.
|
|
For example, a persistent computer can use these locations:
|
|
|
|
```text
|
|
HOME=/home/agent
|
|
AGENT_HOME=/home/agent/.agents
|
|
CODEX_HOME=/home/agent/.codex
|
|
instructions=/home/agent/.agents/AGENTS.md
|
|
skills=/home/agent/.agents/skills/<name>/SKILL.md
|
|
```
|
|
|
|
These are execution-host locations. Core does not copy the home for each run.
|
|
The plugin must declare `projectAuthCheckVersion: 1` for all native tasks.
|
|
Core requires an allowed `check_project_auth` result, or a prepared result with
|
|
`projectAuthCheckAtSubmit: true`. The latter delegates the check to the trusted
|
|
plugin's submit handler before credential installation or task start. The plugin
|
|
must reject conflicting project or parent authentication settings. It must keep
|
|
the checked configuration fixed until submission. See
|
|
[launch authentication validation](PLUGIN_ENVIRONMENT_TASKS.md#launch-authentication-validation).
|
|
|
|
Core sends authorized project IDs, the PRP identity, launch limits, and the
|
|
bootstrap ticket through `submit`. It does not send a process command.
|
|
The plugin must submit each task at most once. A lost reply requires status
|
|
reconciliation for the same task.
|
|
|
|
## Credentials and tools
|
|
|
|
Core can transfer a selected Codex account or a direct Claude credential.
|
|
Other managed routes fail closed until they have a typed credential contract.
|
|
Do not put credentials in task metadata, journals, logs, or errors.
|
|
|
|
Assigned MCP tools run through the Core gateway over PRP.
|
|
The gateway credentials stay on Core.
|
|
The plugin must preserve the MCP binding identity in the prepared context.
|
|
|
|
Core records credential writeback authority before dispatch.
|
|
After process termination, Core saves refreshed credentials before it completes
|
|
the task. A failed save retains the task grants for cleanup.
|
|
|
|
## Stop and recovery
|
|
|
|
A terminal phase does not prove process termination.
|
|
Return `executionStopped: true` only after the task and its descendants stop.
|
|
Return `sessionStateAvailable: true` only when the suspended state remains usable.
|
|
Keep retained state when Core completes the task.
|
|
Core checks managed plugin workspaces through their storage provider before
|
|
completion, including after a restart. Core does not inspect these folders locally.
|
|
If this check fails, Core defers the run and continues recovery for other runs.
|
|
Core keeps known branch and owner error codes with the repair instructions.
|
|
|
|
The task contract exposes two checkpoint operations:
|
|
|
|
- `read_state` returns the exact Runner durable state for the supplied identity.
|
|
- `archive_state` moves a suspended Runner authority checkpoint under the supplied
|
|
archive key. It returns the same state on retry. It keeps provider session files.
|
|
|
|
These operations do not accept paths. Core checks the active lease and retained
|
|
source task before it calls the plugin. The existing Runner transport validates
|
|
state identity and suspension before it rotates authority.
|
|
|
|
For controller replacement, Core restores its encrypted PRP journal from Postgres
|
|
and reuses the original active task lease. The task owner checks running status
|
|
and exact Runner identity, then reacquires the connection. It does not call
|
|
`prepare`, `submit`, or `stop`. The running artifact remains pinned even if
|
|
`describe` reports a newer installed version. Launch preparation files on the
|
|
old controller are not required to adopt an already running task.
|
|
The five-minute outage contract and its prerequisites are described in the
|
|
[continuity recipe](../testing-native-remote-continuity.md#replacing-the-main-server).
|
|
|
|
Core checks company, agent, environment, workspace, driver, and termination
|
|
receipts before it supplies `resumeFrom`. Historical controller recovery without
|
|
the durable journal first stops and releases the old task. A failed stop prevents replacement.
|
|
A new run prepares its current assignments. Provider compatibility checks remain
|
|
in the existing backends. Missing or incompatible retained state fails closed.
|
|
|
|
## Setup verification
|
|
|
|
Core creates a private probe lease with an expiry.
|
|
It runs the same describe, prepare, submit, and WSS/PRP path.
|
|
The probe checks provider authentication and the selected model with tools denied.
|
|
Core bounds Runner lifetime to the remaining probe time.
|
|
It then requires suspension, confirmed termination, and task cleanup.
|
|
For a new agent-owned computer, the saved-agent lifecycle runs verification
|
|
after it saves the agent. Core does not require a computer before this save.
|
|
|
|
## Verification
|
|
|
|
- [Lease and SDK RPC integration](../../server/src/__tests__/plugin-runner-task.integration.test.ts)
|
|
- [Real Runner setup and recovery](../../server/src/services/native-runtime/plugin-runner-probe.test.ts)
|
|
- [Task ownership and cancellation](../../server/src/services/native-runtime/plugin-runner-task.test.ts)
|
|
- [Credential transfer](../../server/src/services/native-runtime/plugin-runner-credentials.test.ts)
|
|
|
|
The Runner fixture uses a synthetic Codex provider and local sockets.
|
|
It does not qualify a deployed plugin, live account, or production WSS proxy.
|
|
|
|
## Workspace completion and maintenance
|
|
|
|
A storage provider can declare `workspaceProvider.finalizationVersion: 1`.
|
|
Core then sends `workspaceOperation` with `operation.kind: "finalize"`, the retained
|
|
binding, lifecycle generation, and originating `runId` and `agentId`. The operation
|
|
must verify workspace identity and make the run's retained writes durable before
|
|
returning `{ kind: "finalized", headSha, branchName }`. Git workspaces must still
|
|
report the assigned branch. Non-Git workspaces return null Git fields.
|
|
|
|
Completion permits dirty, untracked, ignored, and unpublished work. It does not
|
|
prove remote recoverability or authorize deletion. The provider chooses how to
|
|
meet this contract; Core does not prescribe a VM, filesystem, or Git layout.
|
|
Core gives completion a 30-second deadline. An unavailable execution host or an
|
|
unconfirmed durability barrier must fail retryably, never return success.
|
|
Recovery receives the original run and agent context.
|
|
|
|
The existing agent-independent `inspect` and `remove` operations continue to
|
|
protect storage maintenance. Providers that do not declare finalization version 1
|
|
retain the inspection-based completion path. No stored binding migration is required.
|
|
|
|
A provider whose inspection cannot prove merge ancestry may declare
|
|
`workspaceProvider.supportsMergeAncestry: false`. Omission preserves the existing
|
|
ancestry inspection behavior. For an opted-out provider, automatic cleanup uses
|
|
its latest successful finalization HEAD as a hint for destination-aware GitHub
|
|
checks. It discovers associated PRs without requiring a recorded work product.
|
|
A direct ancestry proof or a merged PR with the exact final HEAD, intended base
|
|
repository/branch, and reachable integration commit must precede storage
|
|
inspection. A fresh matching HEAD and branch, remote recoverability, no local
|
|
data, terminal tasks, and the existing lifecycle fences still govern deletion.
|
|
Open or unknown delivery for a known commit retains a bounded remote-only retry.
|
|
Missing candidate identity parks until a relevant write or startup recovery.
|
|
Explicit close-readiness and pending lifecycle recovery retain their checks.
|
|
|
|
A provider may declare `workspaceProvider.recoveryPullRequestVersion: 1`. Core
|
|
can then include `recoveryPullRequest: { number, headSha }` in `inspect` and
|
|
`remove`. This is a narrow recovery hint, not permission to delete. The provider
|
|
must use its trusted source repository, validate that the specific PR recovery
|
|
reference still identifies `headSha`, and independently check the actual workspace
|
|
and all owned data. Arbitrary remote URLs and reference names are not accepted.
|
|
Absent capability means Core does not send the extra field. Before dispatch,
|
|
Core persists the negotiated removal payload and its operation ID. Retries replay
|
|
both unchanged, even if capabilities change, while still enforcing company,
|
|
provider enablement, binding and generation checks. A downgraded provider can
|
|
reject an unsupported payload; Core does not silently change its identity.
|
|
Legacy intents without a recovery hint retain their deterministic ID. A hinted
|
|
intent without a recorded ID is ambiguous and fails closed. None of these
|
|
receipts replaces the provider's checks.
|
|
|
|
A reservation is a workspace whose `prepare` never returned a binding. When its
|
|
source task is done or cancelled, no task points at it, and no run for the task
|
|
is queued or running, Core archives it and sends `remove` with `binding: null`
|
|
and `expectedHeadSha: null` instead of reconciling it with an agent-free
|
|
`prepare`. The provider must resolve its own record for that execution
|
|
workspace, discard anything the unfinished preparation created, and return
|
|
`{ kind: "removed" }`, including when nothing was created. The removal is
|
|
persisted and replayed with the same operation ID like any other removal. A
|
|
provider that refuses it fails the removal, which backs off like any other
|
|
cleanup failure. A reservation that a task still points at, or whose task is
|
|
open, keeps the `prepare` reconciliation.
|
|
|
|
When terminal cleanup inspection or removal fails, Core records
|
|
`metadata.terminalCleanupRetry` on the workspace with the failure count, the
|
|
next attempt time, and the failure `kind`, `reason`, and Core's message. Retries
|
|
back off from 5 minutes, doubling up to 4 hours. A successful inspection or
|
|
removal clears the record, and a reopen, which raises the lifecycle generation,
|
|
discards it. Other wakes, including provider upgrades, wait for the next attempt
|
|
time. Core keeps the record through workspace metadata updates.
|
|
|
|
The optional `prepared.destinationRef` records the resolved delivery branch as
|
|
`refs/heads/<branch>`, up to 266 characters including the prefix. Providers should
|
|
return null when preparation cannot establish an unambiguous branch destination
|
|
or its canonical ref exceeds this receipt limit; an optional receipt must not
|
|
fail an otherwise successful preparation. Core stores this alongside the source
|
|
snapshot; callers cannot overwrite it through workspace metadata updates. Older
|
|
providers omit it. Core only falls back to their captured explicit source refs,
|
|
not a mutable project configuration or a newly chosen default branch. A source
|
|
SHA or tag may support direct ancestry, but does not imply a PR base branch.
|
|
Provider upgrades, activation, company re-enablement, and finalization settlement
|
|
wake cleanup transactionally. No table, independent timer, or background scan of
|
|
all remote PR refs is required.
|
|
|
|
### Workspace operation failures
|
|
|
|
Core separates a failed `workspaceOperation` call by what it knows about the
|
|
outcome:
|
|
|
|
| Failure | Run error | Core behavior |
|
|
| --- | --- | --- |
|
|
| The worker was not running, so no request was sent | `Workspace storage provider is not running; retry the same workspace` | A run that has not started provider work is retried as `plugin_worker_unavailable`. |
|
|
| The call timed out, or the worker exited with the call pending | A fixed timeout or stopped-provider message that says to retry the same workspace | The operation may have run, so the same workspace and operation identity must be retried. |
|
|
| The provider threw an error with a known reason | The fixed message Core keeps for that reason | Only transient reasons tell the user to retry. |
|
|
| The provider threw any other error | `Workspace storage provider reported a failure; retry the same workspace` | Unchanged setup failure. |
|
|
|
|
To report a reason, throw `PluginWorkspaceOperationError` from
|
|
`onWorkspaceOperation`, or any error with an own `workspaceFailureReason`
|
|
property. The reason must come from `WORKSPACE_OPERATION_FAILURE_REASONS`:
|
|
|
|
| Reason | Retrying the same workspace can help |
|
|
| --- | --- |
|
|
| `unsupported_operation`, `configuration_unavailable`, `credential_unavailable`, `storage_grant_unavailable`, `binding_mismatch`, `runtime_connection_invalid` | No |
|
|
| `storage_pending`, `runtime_unavailable`, `remote_incomplete`, `context_changed`, `deadline_exceeded` | Yes |
|
|
|
|
```ts
|
|
import { PluginWorkspaceOperationError } from "@paperclipai/plugin-sdk";
|
|
|
|
throw new PluginWorkspaceOperationError("storage_grant_unavailable", "Project mount grants unavailable");
|
|
```
|
|
|
|
The SDK sends only the reason. Core never reads the provider's message, so a
|
|
message cannot reach the run, the API, or Core logs. The run keeps its
|
|
`setup_failed` error code and records `resultJson.workspaceProviderFailure` with
|
|
the failure `kind`, `reason`, and `retryable` flag. The
|
|
`heartbeat execution setup failed` log carries the same fields. A provider may
|
|
also record a `plugin.workspace.failure` span whose `plugin.failure_reason` is a
|
|
reason from this set or `unknown`.
|
|
|
|
## Warm task retention
|
|
|
|
A driver declares `warmTaskVersion: 1`, implements `onEnvironmentResumeLease`,
|
|
and returns `warmTaskVersion: 1` from `describe` only when its execution host
|
|
supports authorized run attachment. Newly hired agents targeting such a driver
|
|
are configured for warm execution. Existing agents keep their explicit policy;
|
|
plugin runs without an explicit policy prefer warm execution. There is no
|
|
provider-specific warm gate or additional setting. Existing runs retain their
|
|
admitted lifecycle policy during restart recovery. The existing
|
|
`runnerIdleTimeoutMs` controls bounded retention. Fresh warm admission requires
|
|
a Core-managed credential identity before lease acquisition. The saved warm
|
|
preference does not bypass account ownership checks. Drivers without warm
|
|
capability and setup probes remain ephemeral.
|
|
|
|
Plugin task execution uses the public, run-scoped GitHub credential broker
|
|
when the provider advertises its versioned contract. Warm preparation updates
|
|
the command capability before admitting the new run. Restart recovery retains
|
|
the original run binding; new work must not reactivate an older capability.
|
|
Project authorization is still checked for each new run.
|
|
|
|
A Launcher task owns one Runner process, independently of Core heartbeat runs.
|
|
Successful warm run completion retains the task and its mounts. The reuse
|
|
policy is saved with the selected native runtime, independently of workspace
|
|
copy-back records. Recovery uses that policy when no workspace-sync record exists,
|
|
so it does not stop a Runner that foreground completion retains. Failed and
|
|
cancelled runs still stop instead of staying warm. The reuse
|
|
fingerprint includes the authorized project set; changing it retires the previous
|
|
task before checkpoint recovery. Adoption and each warm handoff require a current,
|
|
configuration-bound project-auth check against the exact retained mount scope. Core atomically
|
|
hands its retained provider identity to one new run lease, revalidates current
|
|
project authentication, prepares the new run's resources, and uses PRP
|
|
`run.attach`. Preparation includes the exact `priorIdentity` and stable
|
|
`accountIdentity`. The provider journals that transition idempotently before
|
|
allowing the new authority. The original ingress address remains unchanged.
|
|
Runner accepts `connection: { mode: "external" }` on warm attachment to preserve
|
|
the process owner's listener and TLS settings while rotating the run identity.
|
|
|
|
Core permits connection refresh for the saved successful run while its warm task
|
|
lease is retained, unexpired, and not retiring or stopped. PRP authentication
|
|
still applies. A superseded lease cannot acquire a connection. Retention does
|
|
not authorize preparation or submission of new work through the completed lease.
|
|
|
|
Idle retirement suspends and stops Runner, saves its exact checkpoint, writes
|
|
credentials through the durable lease handoff, then completes the provider task.
|
|
Core permits read-only suspension proof for the exact saved identity of a
|
|
successful warm run after its reuse lease is retained or superseded. This does
|
|
not authorize new work or file access through that lease.
|
|
Temporary credential homes are not needed after the originating run ends.
|
|
An explicit reset to a different normalized session can retire a warm owner
|
|
whose close failed only because it could not prove checkpoint suspension. Core
|
|
requires the exact lease-bound task's `executionStopped: true` receipt, a
|
|
successful `complete` handoff, and settled Core tool results first. It then
|
|
collects stopped instruction files before it releases the in-memory owner. The old checkpoint remains evidence;
|
|
it is not reused by the new session. A failed handoff, failed file collection,
|
|
unsettled Core tool result, other close error, or continuation of the same
|
|
session still blocks replacement.
|
|
A controller restart is not required for this stopped-owner reset path.
|
|
|
|
Core ends a dispatched task lease with `stop`, `status`, and `complete`. When
|
|
that sequence confirms the stop and completes the same task, Core does not also
|
|
call `onEnvironmentReleaseLease`. Core still calls it when the release did not
|
|
complete a dispatched task, and the cleanup retry calls it after any failure.
|
|
When `onEnvironmentResumeLease` returns no provider lease, Core sends `status`
|
|
first. It sends `stop` only when that `status` does not report
|
|
`executionStopped: true`. A driver must release task-scoped resources in its
|
|
`status` or `complete` handling, not only in `onEnvironmentReleaseLease`.
|
|
|
|
Termination receipts cover all run leases that owned the retired physical task.
|
|
A stopped task gets a new provider task ID for checkpoint resumption. Inspection
|
|
precedes in-memory session selection so a replacement cannot inherit the stopped
|
|
session. Core saves the fresh provider expiry and forwards and enforces any
|
|
caller-imposed deadline. A missing
|
|
or unreachable computer never authorizes a replacement. Controller recovery can
|
|
adopt a verified surviving idle process through its authenticated PRP connection;
|
|
ambiguous transitions remain fenced by the existing durable recovery checks.
|
|
|
|
Changes to account, credential generation, workspace, runtime or process policy
|
|
retire the old owner before recovery. A new authority epoch archives the stopped
|
|
checkpoint under its stable archive key and preserves provider session files.
|
|
Warm status monitoring relies on PRP after startup and does not poll an idle
|
|
Launcher. ACPX uses this task-provider contract; other execution transports retain
|
|
their provider-specific lifecycle policies.
|
|
|
|
### Operation-time GitHub credentials
|
|
|
|
A task provider can advertise `githubCredentialBrokerVersion: 1` in its
|
|
`describe` result. Core then supplies an optional `githubBroker: { url, token }`
|
|
in each `prepare` operation. This is a runtime capability, not a GitHub token.
|
|
No controller path, executable, script, or command implementation is part of
|
|
this contract. Plugin authors own how their command environment uses it.
|
|
|
|
At the start of each authenticated operation, send `POST` with body `{}` to
|
|
`<url without trailing /api>/runtime-tools/github/credentials`, with
|
|
`Authorization: Bearer <token>`. The response is either
|
|
`{ status: "available", env: { ... } }` or
|
|
`{ status: "unavailable", reason: "..." }`. Core rechecks the live run, accepted
|
|
instruction identity, grants, and trust policy on each request. Use the returned
|
|
environment only in the command and its descendants. Never retain raw GitHub
|
|
credentials in plugin state, launch descriptors, logs, or files.
|
|
|
|
A warm task must replace its broker capability before admitting the next run's
|
|
commands. An absent broker clears the old capability and allows anonymous/local
|
|
operations only. Isolate ambient host credentials and Git/GitHub configuration;
|
|
an unavailable broker must never select a host account. Capture once per
|
|
operation so steering does not split a command's Git descendants across people.
|
|
The runtime capability may be staged privately for command-time access; it must
|
|
not enter the returned launch descriptor or entity records. Core's managed
|
|
launchers are an optional reference implementation. Compatible native helpers,
|
|
credential helpers, or command proxies can implement the same semantics.
|
|
|
|
Deploy the updated Core and plugin together. A configured
|
|
GitHub connection on an older task provider returns an explicit upgrade error.
|
|
Unconfigured providers continue ordinary work without authenticated GitHub access.
|
|
|
|
Managed GitHub command providers must retain the qualified provider executable
|
|
and arguments. Supply helper discovery through the task environment instead of
|
|
wrapping that executable. Claude ACPX preserves explicit `BASH_ENV`, `ZDOTDIR`,
|
|
`GH_CONFIG_DIR`, and `PAPERCLIP_GITHUB_LAUNCHER_DIR` selectors across the sidecar
|
|
boundary. It does not forward GitHub tokens or broker capabilities to the provider.
|
|
The helper reads the current task capability at invocation, including after warm
|
|
reuse. These selectors must come from the trusted environment owner.
|
|
Reference helpers mark only their child operation and reject re-entry before resolving
|
|
another executable or capturing credentials. A PATH shim that calls a helper's
|
|
absolute path fails with `launcher_recursion_rejected` (exit 126), preventing
|
|
recursive process growth. Offline Git fixtures must resolve the native binary
|
|
outside managed helper paths and exclude runtime shell startup hooks.
|
|
|
|
|
|
The pure launch factory exports `RUNNER_GITHUB_LAUNCHER_VERSION = 1` for this
|
|
contract. A provider must check it as well as the host broker schema and its
|
|
Launcher capability before advertising managed GitHub commands. The installed
|
|
Runner must match the factory's source revision.
|
|
|
|
The helper projection is marked by the controller-owned
|
|
`PAPERCLIP_GITHUB_LAUNCHER_DIR`. Task overrides cannot supply that marker or the
|
|
shell selectors. Run preparation strips stored bindings before secret resolution,
|
|
and configured task-environment projections reject the shell selectors too.
|