feat(runner): add hidden server PRP coordinator (#12176)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The Paperclip Runner needs a narrow server trust boundary before an
adapter can start it.
> - The package has durable runner transport, but the server does not
host or authorize that transport.
> - Native persistence exists, but no writer connects PRP events to
those records.
> - A direct adapter must not enter this path by accident.
> - This pull request adds a hidden, run-bound PRP server coordinator.
> - The benefit is a recoverable server boundary that remains
unavailable to normal execution.

## Linked Issues or Issue Description

Refs #11962

Refs #12129

Refs #12169

**Subsystem affected**

Cross-cutting. The change affects the runner package and server
orchestration.

**Problem or motivation**

The server cannot authenticate runnerd, commit PRP events before ACK,
authorize semantic tools, or enter native finalization from a durable
runner result. The application must have this hidden boundary before a
guarded adapter can use the runner.

**Proposed solution**

Add an authenticated PRP WebSocket authority and register it only for
one exact persisted native Codex run. Bind each connection and event to
the company, issue, agent, run, runner, session, turn, item, and
verified runner identity. Commit each event before its cumulative ACK.
Project only authorized same-task read tools. Rebuild the accepted
result and finalization record from durable result and terminal events.

**Alternatives considered**

The server could expose a broad runner API key or route semantic calls
through existing adapter endpoints. Those options grant too much
authority and weaken replay recovery. The server could also add the
user-facing adapter in this pull request. That option would mix rollout
selection with the transport trust boundary and make legacy
compatibility harder to review.

**Roadmap alignment**

This work supports the shipped enforced-outcomes, governed-tool, and
self-healing-run milestones. It does not add a new roadmap surface.

## What Changed

- Add the durable PRP server authority with one-use bootstrap tickets,
reconnect leases, encrypted frames, bounded state, cumulative ACKs, and
idempotent commands.
- Add `/api/runner/v1/connect/:runId`. Derive its `ws://` or `wss://`
URL from the configured Paperclip API URL.
- Register one authority only after the coordinator verifies the
complete native Codex run binding.
- Commit validated PRP events to `heartbeat_run_events` before ACK.
Reject source gaps and conflicting replays.
- Rebuild accepted results and finalization records from durable result
and terminal events. Enforce finalization owner leases and retry times.
- Project five same-task read operations. Recheck run, agent, task, and
company authority for each call.
- Keep the route hidden. No adapter selects this coordinator, and no
code starts runnerd.
- Vendor the compiled runner TypeScript runtime into the server package
while keeping the workspace package development-only for the server.
- Document the package, database writer, run-log payload, and credential
exclusions.

## Verification

- Run `pnpm --filter @paperclipai/paperclip-runner check:all`. All
TypeScript protocol checks and 69 Vitest tests pass, including
commit-before-ACK crash recovery. All 43 Rust unit tests and 13 Rust
integration tests pass. Conformance and replay parity pass.
- Run the focused server WebSocket, coordinator, package-build, and
startup-wiring suites. All 26 tests pass, including a clean-checkout
reproduction with the runner `dist` directory absent.
- Run `pnpm -r typecheck`.
- Run `pnpm test:run`.
- Run `pnpm build`.
- Confirm that the diff contains 19 files. Confirm that it contains no
workflow or `pnpm-lock.yaml` change.

## Risks

- The server installs the WebSocket route at startup. An unregistered or
malformed run path fails closed and creates no native record.
- Bootstrap tickets are one use. The private state directory uses mode
`0700`, and the state file uses mode `0600`. The file stores derived
authentication verifiers and never stores raw tickets or lease tokens.
- The journal has explicit frame, command, event-window, and file-size
bounds. A bound violation closes the runner connection or rejects the
command.
- A runner event reaches the database before its ACK. A crash between
event commit and ACK causes a byte-equivalent replay, not a second
logical effect.
- The coordinator accepts only an existing queued or running native
Codex row with exact company, task, agent, runner, session, and
completion-contract ownership.
- Existing direct adapters do not call this service. They keep their
current execution, transcript, result, and finalization paths.
- The server has no production dependency on the private runner package.
Its build copies the compiled runtime into `server/dist`; the workspace
link is development-only. This adds no external package and does not
change the lockfile.

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

## Model Used

- OpenAI Codex with GPT-5. The exact deployment ID and context-window
size are not exposed. The model used agentic reasoning, repository
tools, code execution, and test execution.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] 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
This commit is contained in:
Dotta authored and GitHub committed 2026-08-25 14:17:14 -05:00
1 parent 86fe9339e1
commit 9964b034bb
19 files changed
+4129 -15

No files matched your search

@@ -0,0 +1,460 @@
import { createHash } from "node:crypto";
import { and, desc, eq, lt } from "drizzle-orm";
import type { Db } from "@paperclipai/db";
import {
heartbeatRunEvents,
heartbeatRuns,
nativeRunFinalizations,
nativeRunResults,
} from "@paperclipai/db";
import {
type PrpEvent,
type PrpStructuredRunResult,
type PrpTerminalState,
validatePrpEvent,
validatePrpStructuredRunResult,
} from "../../vendor/paperclip-runner/index.js";
export interface NativeRunStoreBinding {
readonly companyId: string;
readonly issueId: string;
readonly runId: string;
readonly agentId: string;
readonly normalizedSessionId: string;
readonly runnerSourceInstanceId: string;
readonly completionContractId: string;
readonly completionContractSha256: string;
}
export interface CompleteNativeRunInput {
readonly result: PrpStructuredRunResult;
readonly terminal: PrpTerminalState;
readonly turnId?: string;
readonly callerResultId?: string;
readonly callerDedupeKey?: string;
}
function canonicalJson(value: unknown): string {
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
if (typeof value === "object" && value !== null) {
const record = value as Record<string, unknown>;
return `{${Object.keys(record)
.sort()
.map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`)
.join(",")}}`;
}
return JSON.stringify(value) ?? "null";
}
function sha256(value: unknown): string {
return `sha256:${createHash("sha256").update(canonicalJson(value)).digest("hex")}`;
}
function assertTerminal(value: PrpTerminalState): void {
if (
value.schema !== "paperclip.prp.terminal.v1" ||
!["completed", "failed", "interrupted", "cancelled"].includes(
value.turnTerminalState,
) ||
!["succeeded", "failed", "cancelled"].includes(value.runTerminalState) ||
!["done", "blocked", "needs_review", "yielded"].includes(
value.reportedWorkDisposition,
)
) {
throw new Error("native_terminal_schema_invalid");
}
}
/** Durable DB boundary used by the hidden PRP coordinator. */
export class NativeRunCoordinatorStore {
readonly #db: Db;
readonly #binding: NativeRunStoreBinding;
constructor(db: Db, binding: NativeRunStoreBinding) {
this.#db = db;
this.#binding = structuredClone(binding);
}
async appendEvent(value: PrpEvent): Promise<{
readonly disposition: "committed" | "duplicate";
readonly cursor: number;
readonly highestContiguousSourceSeq: number;
}> {
const validated = validatePrpEvent(value);
if (!validated.ok) throw new Error("native_event_schema_invalid");
const event = validated.event;
if (
event.runId !== this.#binding.runId ||
event.normalizedSessionId !== this.#binding.normalizedSessionId ||
event.sourceKind !== "runner" ||
event.sourceInstanceId !== this.#binding.runnerSourceInstanceId
) {
throw new Error("native_event_binding_mismatch");
}
const canonicalPayload = event as unknown as Record<string, unknown>;
const payloadDigest = sha256(canonicalPayload);
return this.#db.transaction(async (tx) => {
const [run] = await tx
.select()
.from(heartbeatRuns)
.where(
and(
eq(heartbeatRuns.id, this.#binding.runId),
eq(heartbeatRuns.companyId, this.#binding.companyId),
eq(heartbeatRuns.agentId, this.#binding.agentId),
eq(heartbeatRuns.nativeIssueId, this.#binding.issueId),
eq(
heartbeatRuns.nativeSessionId,
this.#binding.normalizedSessionId,
),
eq(heartbeatRuns.runtimeMode, "native"),
),
)
.for("update")
.limit(1);
if (!run) throw new Error("native_event_run_not_authorized");
const [existing] = await tx
.select()
.from(heartbeatRunEvents)
.where(
and(
eq(heartbeatRunEvents.runId, this.#binding.runId),
eq(heartbeatRunEvents.sourceEventId, event.sourceEventId),
),
)
.limit(1);
if (existing) {
if (
existing.sourcePayloadSha256 !== payloadDigest ||
existing.sourceInstanceId !== event.sourceInstanceId ||
existing.sourceSeq !== event.sourceSeq
) {
throw new Error("native_event_replay_conflict");
}
const [latest] = await tx
.select({ sourceSeq: heartbeatRunEvents.sourceSeq })
.from(heartbeatRunEvents)
.where(
and(
eq(heartbeatRunEvents.runId, this.#binding.runId),
eq(heartbeatRunEvents.sourceInstanceId, event.sourceInstanceId),
),
)
.orderBy(desc(heartbeatRunEvents.sourceSeq))
.limit(1);
return {
disposition: "duplicate" as const,
cursor: existing.seq,
highestContiguousSourceSeq: latest?.sourceSeq ?? event.sourceSeq,
};
}
const [previous] = await tx
.select({ sourceSeq: heartbeatRunEvents.sourceSeq })
.from(heartbeatRunEvents)
.where(
and(
eq(heartbeatRunEvents.runId, this.#binding.runId),
eq(heartbeatRunEvents.sourceInstanceId, event.sourceInstanceId),
),
)
.orderBy(desc(heartbeatRunEvents.sourceSeq))
.limit(1);
const expectedSourceSeq = (previous?.sourceSeq ?? 0) + 1;
if (event.sourceSeq !== expectedSourceSeq) {
throw new Error("native_event_source_gap");
}
const cursor = run.nextEventSeq;
const [inserted] = await tx
.insert(heartbeatRunEvents)
.values({
companyId: this.#binding.companyId,
runId: this.#binding.runId,
agentId: this.#binding.agentId,
seq: cursor,
eventType: event.eventType,
stream: "system",
level: event.eventType.includes("failed") ? "error" : "info",
payload: { prpEvent: canonicalPayload },
sourceInstanceId: event.sourceInstanceId,
sourceEventId: event.sourceEventId,
sourceSeq: event.sourceSeq,
sourcePayloadSha256: payloadDigest,
protocolSchemaVersion: event.schemaVersion,
})
.returning({ seq: heartbeatRunEvents.seq });
if (!inserted) throw new Error("native_event_not_persisted");
await tx
.update(heartbeatRuns)
.set({ nextEventSeq: cursor + 1, updatedAt: new Date() })
.where(eq(heartbeatRuns.id, this.#binding.runId));
return {
disposition: "committed" as const,
cursor: inserted.seq,
highestContiguousSourceSeq: event.sourceSeq,
};
});
}
async completeRun(input: CompleteNativeRunInput): Promise<{
readonly disposition: "committed" | "duplicate";
readonly resultId: string;
}> {
const validated = validatePrpStructuredRunResult(input.result);
if (!validated.ok) throw new Error("native_result_schema_invalid");
assertTerminal(input.terminal);
if (
validated.result.reportedWorkDisposition !==
input.terminal.reportedWorkDisposition
) {
throw new Error("native_result_terminal_disposition_mismatch");
}
const canonical = {
result: validated.result,
terminal: input.terminal,
turnId: input.turnId ?? null,
};
const canonicalSha256 = sha256(canonical);
const serverFingerprint = sha256({
runId: this.#binding.runId,
completionContractSha256: this.#binding.completionContractSha256,
canonicalSha256,
});
return this.#db.transaction(async (tx) => {
const [run] = await tx
.select()
.from(heartbeatRuns)
.where(eq(heartbeatRuns.id, this.#binding.runId))
.for("update")
.limit(1);
if (
!run ||
run.companyId !== this.#binding.companyId ||
run.agentId !== this.#binding.agentId ||
run.nativeIssueId !== this.#binding.issueId ||
run.runtimeMode !== "native" ||
run.completionContractId !== this.#binding.completionContractId ||
run.completionContractSha256 !== this.#binding.completionContractSha256
) {
throw new Error("native_result_run_not_authorized");
}
const [existing] = await tx
.select()
.from(nativeRunResults)
.where(
and(
eq(nativeRunResults.runId, this.#binding.runId),
eq(nativeRunResults.schemaStatus, "accepted"),
),
)
.limit(1);
if (existing) {
if (existing.canonicalSha256 !== canonicalSha256) {
throw new Error("native_result_replay_conflict");
}
return { disposition: "duplicate" as const, resultId: existing.id };
}
const [inserted] = await tx
.insert(nativeRunResults)
.values({
companyId: this.#binding.companyId,
issueId: this.#binding.issueId,
runId: this.#binding.runId,
turnId: input.turnId ?? null,
completionContractId: this.#binding.completionContractId,
callerResultId: input.callerResultId ?? null,
callerDedupeKey: input.callerDedupeKey ?? null,
serverFingerprint,
schemaStatus: "accepted",
resultJson: canonical as unknown as Record<string, unknown>,
canonicalSha256,
})
.returning({ id: nativeRunResults.id });
if (!inserted) throw new Error("native_result_not_persisted");
await tx
.insert(nativeRunFinalizations)
.values({
runId: this.#binding.runId,
companyId: this.#binding.companyId,
issueId: this.#binding.issueId,
phase: "workspace_finalizing",
resultId: inserted.id,
})
.onConflictDoUpdate({
target: nativeRunFinalizations.runId,
set: {
phase: "workspace_finalizing",
resultId: inserted.id,
failureCode: null,
failureDetail: null,
nextAttemptAt: null,
updatedAt: new Date(),
},
});
await tx
.update(heartbeatRuns)
.set({
nativePhase: "workspace_finalizing",
nativePhaseUpdatedAt: new Date(),
updatedAt: new Date(),
})
.where(eq(heartbeatRuns.id, this.#binding.runId));
return { disposition: "committed" as const, resultId: inserted.id };
});
}
/** Rebuild the accepted result/finalization record from durable PRP events. */
async reconcileTerminalEvent(event: PrpEvent): Promise<{
readonly disposition: "committed" | "duplicate";
readonly resultId: string;
} | null> {
if (event.eventType !== "run.terminal") return null;
if (
event.runId !== this.#binding.runId ||
event.normalizedSessionId !== this.#binding.normalizedSessionId ||
event.sourceInstanceId !== this.#binding.runnerSourceInstanceId
) {
throw new Error("native_terminal_binding_mismatch");
}
const terminal = event.payload as PrpTerminalState;
assertTerminal(terminal);
const [proposed] = await this.#db
.select({ payload: heartbeatRunEvents.payload })
.from(heartbeatRunEvents)
.where(
and(
eq(heartbeatRunEvents.runId, this.#binding.runId),
eq(
heartbeatRunEvents.sourceInstanceId,
this.#binding.runnerSourceInstanceId,
),
eq(heartbeatRunEvents.eventType, "run.result.proposed"),
lt(heartbeatRunEvents.sourceSeq, event.sourceSeq),
),
)
.orderBy(desc(heartbeatRunEvents.sourceSeq))
.limit(1);
const proposedEnvelope = (
proposed?.payload as Record<string, unknown> | undefined
)?.prpEvent as Record<string, unknown> | undefined;
if (proposedEnvelope?.payload === undefined) {
throw new Error("native_terminal_result_missing");
}
return this.completeRun({
result: proposedEnvelope.payload as PrpStructuredRunResult,
terminal,
turnId: event.turnId,
callerDedupeKey: `prp-terminal:${event.sourceEventId}`,
});
}
async claimFinalization(input: {
readonly leaseOwner: string;
readonly leaseTtlMs?: number;
}): Promise<{ readonly attempt: number; readonly resultId: string }> {
const leaseTtlMs = input.leaseTtlMs ?? 30_000;
if (
!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,239}$/.test(input.leaseOwner) ||
!Number.isInteger(leaseTtlMs) ||
leaseTtlMs < 1_000 ||
leaseTtlMs > 300_000
) {
throw new Error("native_finalization_lease_invalid");
}
return this.#db.transaction(async (tx) => {
const [row] = await tx
.select()
.from(nativeRunFinalizations)
.where(
and(
eq(nativeRunFinalizations.runId, this.#binding.runId),
eq(nativeRunFinalizations.companyId, this.#binding.companyId),
eq(nativeRunFinalizations.issueId, this.#binding.issueId),
),
)
.for("update")
.limit(1);
if (!row?.resultId || ["completed", "failed"].includes(row.phase)) {
throw new Error("native_finalization_not_claimable");
}
if (row.nextAttemptAt && row.nextAttemptAt.getTime() > Date.now()) {
throw new Error("native_finalization_retry_not_due");
}
if (
row.leaseOwner &&
row.leaseOwner !== input.leaseOwner &&
row.leaseExpiresAt &&
row.leaseExpiresAt.getTime() > Date.now()
) {
throw new Error("native_finalization_lease_conflict");
}
if (
row.leaseOwner === input.leaseOwner &&
row.leaseExpiresAt &&
row.leaseExpiresAt.getTime() > Date.now()
) {
await tx
.update(nativeRunFinalizations)
.set({
leaseExpiresAt: new Date(Date.now() + leaseTtlMs),
updatedAt: new Date(),
})
.where(eq(nativeRunFinalizations.runId, this.#binding.runId));
return { attempt: row.attempt, resultId: row.resultId };
}
const attempt = row.attempt + 1;
await tx
.update(nativeRunFinalizations)
.set({
attempt,
leaseOwner: input.leaseOwner,
leaseExpiresAt: new Date(Date.now() + leaseTtlMs),
updatedAt: new Date(),
})
.where(eq(nativeRunFinalizations.runId, this.#binding.runId));
return { attempt, resultId: row.resultId };
});
}
async markFinalizationRetry(input: {
readonly leaseOwner: string;
readonly failureCode: string;
readonly retryAfterMs: number;
}): Promise<void> {
if (
!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,239}$/.test(input.failureCode) ||
!Number.isInteger(input.retryAfterMs) ||
input.retryAfterMs < 1_000 ||
input.retryAfterMs > 24 * 60 * 60 * 1_000
) {
throw new Error("native_finalization_retry_invalid");
}
const [updated] = await this.#db
.update(nativeRunFinalizations)
.set({
leaseOwner: null,
leaseExpiresAt: null,
failureCode: input.failureCode,
nextAttemptAt: new Date(Date.now() + input.retryAfterMs),
updatedAt: new Date(),
})
.where(
and(
eq(nativeRunFinalizations.runId, this.#binding.runId),
eq(nativeRunFinalizations.companyId, this.#binding.companyId),
eq(nativeRunFinalizations.issueId, this.#binding.issueId),
eq(nativeRunFinalizations.leaseOwner, input.leaseOwner),
),
)
.returning({ runId: nativeRunFinalizations.runId });
if (!updated) throw new Error("native_finalization_lease_lost");
}
}
@@ -0,0 +1,421 @@
import { randomUUID } from "node:crypto";
import { mkdtempSync, rmSync } from "node:fs";
import { createServer } from "node:http";
import { tmpdir } from "node:os";
import { resolve } from "node:path";
import { eq } from "drizzle-orm";
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
import {
agents,
companies,
completionContracts,
createDb,
heartbeatRunEvents,
heartbeatRuns,
issues,
nativeRunFinalizations,
nativeRunResults,
} from "@paperclipai/db";
import type {
PrpEvent,
PrpStructuredRunResult,
PrpTerminalState,
} from "@paperclipai/paperclip-runner";
import {
getEmbeddedPostgresTestSupport,
startEmbeddedPostgresTestDatabase,
} from "../../__tests__/helpers/embedded-postgres.js";
import {
runnerPrpWebSocketInternals,
setupRunnerPrpWebSocketServer,
} from "../../realtime/runner-prp-ws.js";
import { NativeRunCoordinatorStore } from "./native-run-coordinator-store.js";
import { runnerPrpCoordinator } from "./runner-prp-coordinator.js";
import { PaperclipRunnerSemanticAuthority } from "./runner-semantic-authority.js";
const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport();
const describeEmbeddedPostgres = embeddedPostgresSupport.supported
? describe
: describe.skip;
if (!embeddedPostgresSupport.supported) {
console.warn(
`Skipping runner coordinator tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`,
);
}
interface SeededNativeRun {
companyId: string;
issueId: string;
agentId: string;
runId: string;
runnerInstanceId: string;
sessionId: string;
completionContractId: string;
completionContractSha256: string;
}
const result: PrpStructuredRunResult = {
schema: "paperclip.run_result.v1",
reportedWorkDisposition: "done",
summary: "The hidden runner completed the bounded task.",
completionClaim: {
contractRevision: "contract-v1",
objectiveSatisfied: true,
criteria: [],
remainingWork: [],
},
evidence: [],
verification: [{ commandOrCheck: "coordinator test", status: "passed" }],
attentionRequests: [],
artifacts: [],
};
const terminal: PrpTerminalState = {
schema: "paperclip.prp.terminal.v1",
turnTerminalState: "completed",
runTerminalState: "succeeded",
reportedWorkDisposition: "done",
};
function runnerEvent(seed: SeededNativeRun, sourceSeq = 1): PrpEvent {
return {
schema: "paperclip.prp.event.v1",
sourceEventId: `event-${sourceSeq}`,
sourceSeq,
sourceInstanceId: seed.runnerInstanceId,
sourceKind: "runner",
runId: seed.runId,
normalizedSessionId: seed.sessionId,
turnId: "turn-1",
itemId: "item-1",
eventType: "turn.started",
schemaVersion: 1,
priority: 1,
emittedAt: "2026-08-25T18:00:00.000Z",
payload: {},
};
}
describeEmbeddedPostgres("hidden runner PRP coordinator", () => {
let db!: ReturnType<typeof createDb>;
let tempDb: Awaited<
ReturnType<typeof startEmbeddedPostgresTestDatabase>
> | null = null;
const scratchDirectories: string[] = [];
beforeAll(async () => {
tempDb = await startEmbeddedPostgresTestDatabase(
"paperclip-runner-coordinator-",
);
db = createDb(tempDb.connectionString);
}, 20_000);
afterEach(async () => {
runnerPrpWebSocketInternals.resetForTests();
await db.delete(nativeRunFinalizations);
await db.delete(nativeRunResults);
await db.delete(heartbeatRunEvents);
await db.update(issues).set({ executionRunId: null });
await db.delete(heartbeatRuns);
await db.delete(completionContracts);
await db.delete(issues);
await db.delete(agents);
await db.delete(companies);
for (const directory of scratchDirectories.splice(0)) {
rmSync(directory, { recursive: true, force: true });
}
});
afterAll(async () => {
await tempDb?.cleanup();
});
async function seedNativeRun(): Promise<SeededNativeRun> {
const companyId = randomUUID();
const issueId = randomUUID();
const agentId = randomUUID();
const runId = randomUUID();
const runnerInstanceId = randomUUID();
const sessionId = randomUUID();
const completionContractId = randomUUID();
const completionContractSha256 = `sha256:${"c".repeat(64)}`;
await db.insert(companies).values({
id: companyId,
name: "Runner Test Company",
issuePrefix: `R${companyId.replaceAll("-", "").slice(0, 6).toUpperCase()}`,
requireBoardApprovalForNewAgents: false,
});
await db.insert(agents).values({
id: agentId,
companyId,
name: "Codex runner",
role: "engineer",
status: "running",
adapterType: "codex_local",
adapterConfig: {},
runtimeConfig: {},
});
await db.insert(issues).values({
id: issueId,
companyId,
identifier: `RUN-${runId.slice(0, 8)}`,
title: "Exercise the hidden native coordinator",
description: "Verify transport and durable server boundaries.",
status: "in_progress",
priority: "medium",
workMode: "standard",
assigneeAgentId: agentId,
});
await db.insert(completionContracts).values({
id: completionContractId,
companyId,
issueId,
revision: 1,
schemaVersion: "paperclip.completion-contract.v1",
policyVersion: "policy-v1",
risk: "low",
completionAuthority: "runner",
incompleteCriteriaPolicy: "fail_closed",
contractJson: { criteria: [] },
canonicalSha256: completionContractSha256,
createdByActorType: "system",
createdByActorId: "test",
});
await db.insert(heartbeatRuns).values({
id: runId,
companyId,
agentId,
status: "running",
runtimeMode: "native",
nativeIssueId: issueId,
runnerInstanceId,
nativeSessionId: sessionId,
driverKind: "codex",
driverVersion: "0.3.0",
completionContractId,
completionContractSha256,
nativePhase: "observed",
});
await db
.update(issues)
.set({ executionRunId: runId })
.where(eq(issues.id, issueId));
return {
companyId,
issueId,
agentId,
runId,
runnerInstanceId,
sessionId,
completionContractId,
completionContractSha256,
};
}
function store(seed: SeededNativeRun): NativeRunCoordinatorStore {
return new NativeRunCoordinatorStore(db, {
companyId: seed.companyId,
issueId: seed.issueId,
runId: seed.runId,
agentId: seed.agentId,
normalizedSessionId: seed.sessionId,
runnerSourceInstanceId: seed.runnerInstanceId,
completionContractId: seed.completionContractId,
completionContractSha256: seed.completionContractSha256,
});
}
it("registers only an exact Codex native binding and exposes read-only tools", async () => {
const seed = await seedNativeRun();
const server = createServer();
setupRunnerPrpWebSocketServer(server, { apiUrl: "http://127.0.0.1:3213" });
const stateRoot = mkdtempSync(resolve(tmpdir(), "paperclip-runner-state-"));
scratchDirectories.push(stateRoot);
const coordinator = runnerPrpCoordinator(db, { stateRoot });
await expect(
coordinator.prepare({
...seed,
companyId: randomUUID(),
normalizedSessionId: seed.sessionId,
environmentLeaseId: "environment-lease-1",
turnId: "turn-1",
itemId: "item-1",
runnerVersion: "0.3.0",
runnerDigest: `sha256:${"a".repeat(64)}`,
}),
).rejects.toThrow("runner_prp_run_not_authorized");
const prepared = await coordinator.prepare({
...seed,
normalizedSessionId: seed.sessionId,
environmentLeaseId: "environment-lease-1",
turnId: "turn-1",
itemId: "item-1",
runnerVersion: "0.3.0",
runnerDigest: `sha256:${"a".repeat(64)}`,
});
expect(prepared.connectUrl).toBe(
`ws://127.0.0.1:3213/api/runner/v1/connect/${seed.runId}`,
);
expect(prepared.bootstrapTicket).toMatch(/^bootstrap_/);
expect(prepared.semanticTools.map((tool) => tool.name)).toEqual([
"get_task_context",
"get_task_history",
"list_documents",
"read_document",
"list_document_revisions",
]);
expect(
runnerPrpWebSocketInternals.activeRegistration({
companyId: seed.companyId,
runId: seed.runId,
}),
).toBe(true);
await expect(
coordinator.prepare({
...seed,
normalizedSessionId: seed.sessionId,
environmentLeaseId: "environment-lease-1",
turnId: "turn-1",
itemId: "item-1",
runnerVersion: "0.3.0",
runnerDigest: `sha256:${"a".repeat(64)}`,
}),
).rejects.toThrow("runner_prp_authority_already_registered");
await prepared.release();
expect(
runnerPrpWebSocketInternals.activeRegistration({
companyId: seed.companyId,
runId: seed.runId,
}),
).toBe(false);
server.close();
});
it("rechecks task ownership and returns semantic receipts", async () => {
const seed = await seedNativeRun();
const authority = new PaperclipRunnerSemanticAuthority(db, {
companyId: seed.companyId,
issueId: seed.issueId,
runId: seed.runId,
agentId: seed.agentId,
});
const call = {
callId: "call-1",
operationId: "get_task_context",
correlation: {
runId: seed.runId,
normalizedSessionId: seed.sessionId,
turnId: "turn-1",
itemId: "item-1",
},
input: {},
};
const allowed = await authority.dispatch(call);
expect(allowed).toMatchObject({
ok: true,
operationId: "get_task_context",
value: { activeTask: { id: seed.issueId }, run: { id: seed.runId } },
inputReceipt: { phase: "input" },
resultReceipt: { phase: "result" },
});
await db
.update(issues)
.set({ assigneeAgentId: null })
.where(eq(issues.id, seed.issueId));
const denied = await authority.dispatch({ ...call, callId: "call-2" });
expect(denied).toMatchObject({
ok: false,
error: { code: "task_ownership_denied", retryable: false },
resultReceipt: { phase: "result" },
});
});
it("persists events and results idempotently and leases finalization", async () => {
const seed = await seedNativeRun();
const nativeStore = store(seed);
const event = runnerEvent(seed);
await expect(nativeStore.appendEvent(event)).resolves.toMatchObject({
disposition: "committed",
cursor: 1,
highestContiguousSourceSeq: 1,
});
await expect(nativeStore.appendEvent(event)).resolves.toMatchObject({
disposition: "duplicate",
cursor: 1,
});
await expect(
nativeStore.appendEvent({ ...event, priority: 2 }),
).rejects.toThrow("native_event_replay_conflict");
await expect(nativeStore.appendEvent(runnerEvent(seed, 3))).rejects.toThrow(
"native_event_source_gap",
);
const resultEvent = {
...runnerEvent(seed, 2),
eventType: "run.result.proposed",
payload: result,
} as PrpEvent;
const terminalEvent = {
...runnerEvent(seed, 3),
eventType: "run.terminal",
payload: terminal,
} as PrpEvent;
await nativeStore.appendEvent(resultEvent);
await nativeStore.appendEvent(terminalEvent);
const firstResult = await nativeStore.reconcileTerminalEvent(terminalEvent);
if (!firstResult)
throw new Error("terminal reconciliation returned no result");
expect(firstResult.disposition).toBe("committed");
await expect(nativeStore.appendEvent(event)).resolves.toMatchObject({
disposition: "duplicate",
highestContiguousSourceSeq: 3,
});
await expect(
nativeStore.completeRun({
result,
terminal,
turnId: "turn-1",
callerDedupeKey: "result-1",
}),
).resolves.toEqual({ ...firstResult, disposition: "duplicate" });
await expect(
nativeStore.completeRun({
result: { ...result, summary: "Conflicting retry" },
terminal,
turnId: "turn-1",
callerDedupeKey: "result-1",
}),
).rejects.toThrow("native_result_replay_conflict");
await expect(
nativeStore.claimFinalization({ leaseOwner: "server-1" }),
).resolves.toMatchObject({ attempt: 1, resultId: firstResult.resultId });
await expect(
nativeStore.claimFinalization({ leaseOwner: "server-1" }),
).resolves.toMatchObject({ attempt: 1, resultId: firstResult.resultId });
await expect(
nativeStore.claimFinalization({ leaseOwner: "server-2" }),
).rejects.toThrow("native_finalization_lease_conflict");
await nativeStore.markFinalizationRetry({
leaseOwner: "server-1",
failureCode: "workspace_busy",
retryAfterMs: 1_000,
});
await expect(
nativeStore.claimFinalization({ leaseOwner: "server-2" }),
).rejects.toThrow("native_finalization_retry_not_due");
await db
.update(nativeRunFinalizations)
.set({ nextAttemptAt: new Date(0) })
.where(eq(nativeRunFinalizations.runId, seed.runId));
await expect(
nativeStore.claimFinalization({ leaseOwner: "server-2" }),
).resolves.toMatchObject({ attempt: 2, resultId: firstResult.resultId });
});
});
@@ -0,0 +1,261 @@
import { resolve } from "node:path";
import { and, eq } from "drizzle-orm";
import type { Db } from "@paperclipai/db";
import { agents, heartbeatRuns, issues } from "@paperclipai/db";
import {
DurablePrpControlPlane,
type PaperclipSemanticToolDefinition,
type PrpStructuredRunResult,
type PrpTerminalState,
} from "../../vendor/paperclip-runner/index.js";
import { registerRunnerPrpAuthority } from "../../realtime/runner-prp-ws.js";
import { NativeRunCoordinatorStore } from "./native-run-coordinator-store.js";
import { PaperclipRunnerSemanticAuthority } from "./runner-semantic-authority.js";
const UUID_PATTERN =
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
const STABLE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,239}$/;
const RUNNER_DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/;
export interface PrepareRunnerPrpSessionInput {
readonly companyId: string;
readonly issueId: string;
readonly runId: string;
readonly agentId: string;
readonly runnerInstanceId: string;
readonly environmentLeaseId: string;
readonly normalizedSessionId: string;
readonly turnId: string;
readonly itemId: string;
readonly runnerVersion: string;
readonly runnerDigest: string;
readonly bootstrapTtlMs?: number;
readonly connectionLeaseTtlMs?: number;
}
export interface PreparedRunnerPrpSession {
readonly connectUrl: string;
/** One-use secret. Pass it only through the runner's protected bootstrap channel. */
readonly bootstrapTicket: string;
readonly semanticTools: readonly PaperclipSemanticToolDefinition[];
queueCommand(
type: string,
payload?: Record<string, unknown>,
commandId?: string,
): { readonly commandId: string; readonly controllerSeq: number };
completeRun(input: {
readonly result: PrpStructuredRunResult;
readonly terminal: PrpTerminalState;
readonly turnId?: string;
readonly callerResultId?: string;
readonly callerDedupeKey?: string;
}): Promise<{
readonly disposition: "committed" | "duplicate";
readonly resultId: string;
}>;
release(): Promise<void>;
}
function clampDuration(
value: number | undefined,
fallback: number,
minimum: number,
maximum: number,
): number {
if (value === undefined) return fallback;
if (!Number.isInteger(value) || value < minimum || value > maximum) {
throw new Error("runner_prp_credential_ttl_invalid");
}
return value;
}
function validateInput(input: PrepareRunnerPrpSessionInput): void {
for (const id of [
input.companyId,
input.issueId,
input.runId,
input.agentId,
input.runnerInstanceId,
input.normalizedSessionId,
]) {
if (!UUID_PATTERN.test(id)) throw new Error("runner_prp_binding_invalid");
}
for (const id of [input.environmentLeaseId, input.turnId, input.itemId]) {
if (!STABLE_ID_PATTERN.test(id))
throw new Error("runner_prp_binding_invalid");
}
if (
!STABLE_ID_PATTERN.test(input.runnerVersion) ||
!RUNNER_DIGEST_PATTERN.test(input.runnerDigest)
) {
throw new Error("runner_prp_runner_identity_invalid");
}
}
/**
* Creates the hidden, run-bound PRP authority. This module does not select a
* runtime or start runnerd. The flagged adapter owns those actions later.
*/
export function runnerPrpCoordinator(
db: Db,
options: {
readonly stateRoot: string;
},
) {
const stateRoot = resolve(options.stateRoot);
return {
prepare: async (
input: PrepareRunnerPrpSessionInput,
): Promise<PreparedRunnerPrpSession> => {
validateInput(input);
const bootstrapTtlMs = clampDuration(
input.bootstrapTtlMs,
30_000,
1_000,
60_000,
);
const connectionLeaseTtlMs = clampDuration(
input.connectionLeaseTtlMs,
60 * 60 * 1_000,
60_000,
24 * 60 * 60 * 1_000,
);
const [binding] = await db
.select({ run: heartbeatRuns, issue: issues, agent: agents })
.from(heartbeatRuns)
.innerJoin(
issues,
and(
eq(issues.id, heartbeatRuns.nativeIssueId),
eq(issues.companyId, heartbeatRuns.companyId),
),
)
.innerJoin(
agents,
and(
eq(agents.id, heartbeatRuns.agentId),
eq(agents.companyId, heartbeatRuns.companyId),
),
)
.where(
and(
eq(heartbeatRuns.id, input.runId),
eq(heartbeatRuns.companyId, input.companyId),
eq(heartbeatRuns.agentId, input.agentId),
eq(heartbeatRuns.nativeIssueId, input.issueId),
eq(heartbeatRuns.runnerInstanceId, input.runnerInstanceId),
eq(heartbeatRuns.nativeSessionId, input.normalizedSessionId),
eq(heartbeatRuns.runtimeMode, "native"),
),
)
.limit(1);
if (
!binding ||
!["queued", "running"].includes(binding.run.status) ||
binding.run.driverKind !== "codex" ||
!binding.run.completionContractId ||
!binding.run.completionContractSha256 ||
binding.issue.assigneeAgentId !== input.agentId ||
binding.issue.executionRunId !== input.runId ||
["paused", "terminated", "pending_approval", "error"].includes(
binding.agent.status,
)
) {
throw new Error("runner_prp_run_not_authorized");
}
const semanticAuthority = new PaperclipRunnerSemanticAuthority(db, {
companyId: input.companyId,
issueId: input.issueId,
runId: input.runId,
agentId: input.agentId,
});
const semanticTools = await semanticAuthority.listAlwaysAvailableTools();
const nativeStore = new NativeRunCoordinatorStore(db, {
companyId: input.companyId,
issueId: input.issueId,
runId: input.runId,
agentId: input.agentId,
normalizedSessionId: input.normalizedSessionId,
runnerSourceInstanceId: input.runnerInstanceId,
completionContractId: binding.run.completionContractId,
completionContractSha256: binding.run.completionContractSha256,
});
const authority = new DurablePrpControlPlane({
stateDirectory: resolve(stateRoot, input.runId),
identity: {
runnerInstanceId: input.runnerInstanceId,
environmentLeaseId: input.environmentLeaseId,
runId: input.runId,
normalizedSessionId: input.normalizedSessionId,
turnId: input.turnId,
itemId: input.itemId,
},
expectedRunnerVersion: input.runnerVersion,
expectedRunnerDigest: input.runnerDigest,
connectionLeaseTtlMs,
onCommittedEvent: async (event) => {
await nativeStore.appendEvent(event);
await nativeStore.reconcileTerminalEvent(event);
},
onSemanticToolInput: async (call) => {
const result = await semanticAuthority.dispatch({
callId: call.callId,
operationId: call.operationId,
correlation: call.correlation,
input: call.input,
});
return { result, isError: !result.ok };
},
});
const registration = await registerRunnerPrpAuthority({
companyId: input.companyId,
runId: input.runId,
authority,
});
let bootstrapTicket: string;
try {
bootstrapTicket = authority.issueBootstrapTicket(bootstrapTtlMs);
} catch (error) {
authority.disconnectActiveRunner();
await registration.release();
throw error;
}
let released = false;
return {
connectUrl: registration.connectUrl,
bootstrapTicket,
semanticTools,
queueCommand: (type, payload = {}, commandId) => {
if (released) throw new Error("runner_prp_session_released");
const command = authority.queueCommand(
type,
payload,
commandId,
true,
);
return {
commandId: command.commandId,
controllerSeq: command.controllerSeq,
};
},
completeRun: (completeInput) => {
if (released) throw new Error("runner_prp_session_released");
return nativeStore.completeRun(completeInput);
},
release: async () => {
if (released) return;
released = true;
authority.disconnectActiveRunner();
await registration.release();
},
};
},
};
}
@@ -0,0 +1,351 @@
import { and, desc, eq, isNull } from "drizzle-orm";
import type { Db } from "@paperclipai/db";
import {
agents,
documentRevisions,
documents,
heartbeatRuns,
issueComments,
issueDocuments,
issues,
} from "@paperclipai/db";
import {
PaperclipSemanticDispatcher,
type PaperclipJsonValue,
type PaperclipSemanticActionBinding,
type PaperclipSemanticActionId,
type PaperclipSemanticAuthorizationRecord,
type PaperclipSemanticRunContext,
type PaperclipSemanticToolCall,
type PaperclipSemanticToolDefinition,
type PaperclipSemanticToolResult,
} from "../../vendor/paperclip-runner/index.js";
export interface PaperclipRunnerSemanticBinding {
readonly companyId: string;
readonly issueId: string;
readonly runId: string;
readonly agentId: string;
}
const READ_OPERATION_IDS = [
"get_task_context",
"get_task_history",
"list_documents",
"read_document",
"list_document_revisions",
] as const satisfies readonly PaperclipSemanticActionId[];
type BoundContext = {
readonly run: typeof heartbeatRuns.$inferSelect;
readonly issue: typeof issues.$inferSelect;
readonly agent: typeof agents.$inferSelect;
};
function boundedLimit(value: unknown): number {
return typeof value === "number" && Number.isInteger(value)
? Math.max(1, Math.min(value, 200))
: 50;
}
function requiredString(value: unknown): string {
if (typeof value !== "string" || value.length === 0 || value.length > 240) {
throw new Error("paperclip_runner_semantic_input_invalid");
}
return value;
}
function jsonValue(value: unknown): PaperclipJsonValue {
return JSON.parse(JSON.stringify(value)) as PaperclipJsonValue;
}
function activeAgentStatus(status: string): "active" | "inactive" {
return ["paused", "terminated", "pending_approval", "error"].includes(status)
? "inactive"
: "active";
}
/**
* Run-scoped semantic authority for the hidden native coordinator.
* This first server slice binds only same-task read operations. A catalog
* entry remains undiscoverable until a later PR adds its guarded binding.
*/
export class PaperclipRunnerSemanticAuthority {
readonly #db: Db;
readonly #binding: PaperclipRunnerSemanticBinding;
readonly #dispatcher: PaperclipSemanticDispatcher;
constructor(db: Db, binding: PaperclipRunnerSemanticBinding) {
this.#db = db;
this.#binding = structuredClone(binding);
this.#dispatcher = new PaperclipSemanticDispatcher({
contextProvider: (runId) => this.#context(runId),
bindings: READ_OPERATION_IDS.map((operationId) =>
this.#readBinding(operationId),
),
});
}
listAlwaysAvailableTools(): Promise<
readonly PaperclipSemanticToolDefinition[]
> {
return this.#dispatcher.listAlwaysAvailableTools(this.#binding.runId);
}
dispatch(
call: Omit<PaperclipSemanticToolCall, "runId">,
): Promise<PaperclipSemanticToolResult> {
return this.#dispatcher.dispatch({ ...call, runId: this.#binding.runId });
}
authorizationRecords(): readonly PaperclipSemanticAuthorizationRecord[] {
return this.#dispatcher.authorizationRecords();
}
#readBinding(
operationId: (typeof READ_OPERATION_IDS)[number],
): PaperclipSemanticActionBinding {
return {
operationId,
execute: async (invocation) => {
const context = await this.#loadBoundContext();
this.#assertActiveContext(context, true);
const input = invocation.input;
switch (operationId) {
case "get_task_context":
return {
value: jsonValue({
company: { id: this.#binding.companyId },
actor: {
id: context.agent.id,
name: context.agent.name,
role: context.agent.role,
title: context.agent.title,
capabilities: context.agent.capabilities,
},
activeTask: {
id: context.issue.id,
identifier: context.issue.identifier,
title: context.issue.title,
description: context.issue.description,
status: context.issue.status,
statusVersion: context.issue.statusVersion,
priority: context.issue.priority,
workMode: context.issue.workMode,
parentId: context.issue.parentId,
projectId: context.issue.projectId,
goalId: context.issue.goalId,
},
run: {
id: context.run.id,
status: context.run.status,
invocationSource: context.run.invocationSource,
},
}),
references: [{ kind: "task", id: context.issue.id }],
};
case "get_task_history": {
const rows = await this.#db
.select({
id: issueComments.id,
body: issueComments.body,
authorAgentId: issueComments.authorAgentId,
authorUserId: issueComments.authorUserId,
createdAt: issueComments.createdAt,
})
.from(issueComments)
.where(
and(
eq(issueComments.companyId, this.#binding.companyId),
eq(issueComments.issueId, this.#binding.issueId),
isNull(issueComments.deletedAt),
),
)
.orderBy(desc(issueComments.createdAt))
.limit(boundedLimit(input.limit));
return {
value: jsonValue({ comments: rows.reverse() }),
references: [{ kind: "task", id: context.issue.id }],
};
}
case "list_documents": {
const rows = await this.#db
.select({
key: issueDocuments.key,
id: documents.id,
title: documents.title,
format: documents.format,
latestRevisionId: documents.latestRevisionId,
latestRevisionNumber: documents.latestRevisionNumber,
lockedAt: documents.lockedAt,
updatedAt: documents.updatedAt,
})
.from(issueDocuments)
.innerJoin(documents, eq(documents.id, issueDocuments.documentId))
.where(
and(
eq(issueDocuments.companyId, this.#binding.companyId),
eq(issueDocuments.issueId, this.#binding.issueId),
eq(documents.companyId, this.#binding.companyId),
),
);
return {
value: jsonValue({ documents: rows }),
references: rows.map((row) => ({
kind: "document_revision" as const,
id: row.latestRevisionId ?? row.id,
})),
};
}
case "read_document": {
const key = requiredString(input.key);
const [row] = await this.#db
.select({
key: issueDocuments.key,
id: documents.id,
title: documents.title,
format: documents.format,
body: documents.latestBody,
latestRevisionId: documents.latestRevisionId,
latestRevisionNumber: documents.latestRevisionNumber,
lockedAt: documents.lockedAt,
updatedAt: documents.updatedAt,
})
.from(issueDocuments)
.innerJoin(documents, eq(documents.id, issueDocuments.documentId))
.where(
and(
eq(issueDocuments.companyId, this.#binding.companyId),
eq(issueDocuments.issueId, this.#binding.issueId),
eq(issueDocuments.key, key),
eq(documents.companyId, this.#binding.companyId),
),
)
.limit(1);
if (!row) throw new Error("paperclip_runner_document_not_found");
return {
value: jsonValue({ document: row }),
references: [
{
kind: "document_revision",
id: row.latestRevisionId ?? row.id,
},
],
};
}
case "list_document_revisions": {
const key = requiredString(input.key);
const rows = await this.#db
.select({
id: documentRevisions.id,
revisionNumber: documentRevisions.revisionNumber,
title: documentRevisions.title,
format: documentRevisions.format,
body: documentRevisions.body,
changeSummary: documentRevisions.changeSummary,
createdByAgentId: documentRevisions.createdByAgentId,
createdByUserId: documentRevisions.createdByUserId,
createdAt: documentRevisions.createdAt,
})
.from(issueDocuments)
.innerJoin(
documentRevisions,
eq(documentRevisions.documentId, issueDocuments.documentId),
)
.where(
and(
eq(issueDocuments.companyId, this.#binding.companyId),
eq(issueDocuments.issueId, this.#binding.issueId),
eq(issueDocuments.key, key),
eq(documentRevisions.companyId, this.#binding.companyId),
),
)
.orderBy(desc(documentRevisions.revisionNumber))
.limit(boundedLimit(input.limit));
return {
value: jsonValue({ revisions: rows }),
references: rows.map((row) => ({
kind: "document_revision" as const,
id: row.id,
})),
};
}
}
},
};
}
async #context(requestedRunId: string): Promise<PaperclipSemanticRunContext> {
if (requestedRunId !== this.#binding.runId) {
throw new Error("paperclip_runner_semantic_run_mismatch");
}
const context = await this.#loadBoundContext();
this.#assertActiveContext(context, false);
return {
runId: context.run.id,
companyId: context.run.companyId,
actor: {
id: context.agent.id,
companyId: context.agent.companyId,
status: activeAgentStatus(context.agent.status),
role: context.agent.role,
claims: [],
},
activeTask: {
id: context.issue.id,
companyId: context.issue.companyId,
assigneeActorId: context.issue.assigneeAgentId,
executionRunId: context.issue.executionRunId,
status: context.issue.status,
workMode: context.issue
.workMode as PaperclipSemanticRunContext["activeTask"]["workMode"],
},
delegatedClaims: [],
};
}
async #loadBoundContext(): Promise<BoundContext> {
const [row] = await this.#db
.select({ run: heartbeatRuns, issue: issues, agent: agents })
.from(heartbeatRuns)
.innerJoin(
issues,
and(
eq(issues.id, heartbeatRuns.nativeIssueId),
eq(issues.companyId, heartbeatRuns.companyId),
),
)
.innerJoin(
agents,
and(
eq(agents.id, heartbeatRuns.agentId),
eq(agents.companyId, heartbeatRuns.companyId),
),
)
.where(
and(
eq(heartbeatRuns.id, this.#binding.runId),
eq(heartbeatRuns.companyId, this.#binding.companyId),
eq(heartbeatRuns.agentId, this.#binding.agentId),
eq(heartbeatRuns.nativeIssueId, this.#binding.issueId),
eq(heartbeatRuns.runtimeMode, "native"),
),
)
.limit(1);
if (!row) throw new Error("paperclip_runner_semantic_binding_not_found");
return row;
}
#assertActiveContext(context: BoundContext, requireOwnership: boolean): void {
if (
!["queued", "running"].includes(context.run.status) ||
activeAgentStatus(context.agent.status) !== "active" ||
(requireOwnership &&
(context.issue.assigneeAgentId !== this.#binding.agentId ||
context.issue.executionRunId !== this.#binding.runId))
) {
throw new Error("paperclip_runner_semantic_binding_inactive");
}
}
}