Bind Pi steering to the submitted browser comment

Preserve exact queue authority through editor Markdown serialization and bound installer scratch capacity for Pi assembly.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-02 14:10:56 -05:00
1 parent 03dd6ef933
commit 2a6107c049
8 files changed
+62 -13

No files matched your search

@@ -19,11 +19,11 @@ qualification. The existing draft stack must be reviewed in dependency order.
| Gate | Acceptance evidence | Current result |
| --- | --- | --- |
| Runtime identity and admission | Exact runtime/profile/model; verified effective thinking; startup below the 60-second admission limit; unsupported configuration fails before a prompt | Deterministic checks and ARM installed startup pass. Recheck final source on all supported hosts. |
| Live lifecycle | Pending native question survives controller restart in the original process; one creation and resolution; stale answer after provider death rejected; Stop retires owned processes; three warm turns keep process identity | Installed local restart and three-turn warm continuity pass at `f5f57e380`. Provider-death and Stop remain required. |
| Live lifecycle | Pending native question survives controller restart in the original process; one creation and resolution; stale answer after provider death rejected; Stop retires owned processes; three warm turns keep process identity | Installed local restart and three-turn warm continuity pass at `f5f57e380`. Pending-permission Stop and stale response rejection pass at `03dd6ef93`. Provider-death evidence remains required. |
| Product workflows | All 26 explicit Pi Product E2E cells (13 local, 13 Daytona); screenshots, public state, independent artifacts, terminal and cleanup evidence | Historical partial passes exist. Full final-source campaign remains required. |
| Runner protocol | Pi roster passes through the native packaged runner and authenticated mock control plane; lifecycle/denial/control cases remain distinct from Product tests | Historical get-task-context pass. Final-source roster remains required. |
| Runner protocol | Pi roster passes through the native packaged runner and authenticated mock control plane; lifecycle/denial/control cases remain distinct from Product tests | Installed get-task-context passes at `f5f57e380`; context-before-action times out. Five cells remain unexecuted. |
| Installed distribution | Public CLI/server tars on ARM Mac, Intel Mac and Linux; normal Pi setup; exact Linux companion and immutable Daytona image imported without binary override | Fresh ARM public installation and normal packaged no-key probe pass. The Linux fleet job is queued. Final-source Intel/Linux/Daytona receipts remain required. |
| Governance and spend | Company isolation, human-only permission, duplicate/stale answers, Stop and budget hard stop; pricing estimates never become claimed bills | 58 focused governance/cost/session invariants pass at `f5f57e380`. Live Stop remains required. The qualification key cap is not proof of Paperclip budget enforcement. |
| Governance and spend | Company isolation, human-only permission, duplicate/stale answers, Stop and budget hard stop; pricing estimates never become claimed bills | 58 focused governance/cost/session invariants pass at `f5f57e380`. Live pending-permission Stop passes at `03dd6ef93`. The qualification key cap is not proof of Paperclip budget enforcement. |
| Integration and rollout | Review each prerequisite; final-head typecheck, tests, build and CI pass; no unresolved review; exact artifacts; rollback recorded | Draft #14956 is stacked on #14924. Prerequisite PRs #14921–#14924 remain open. No merge or release. |
## Bounded execution
@@ -78,6 +78,20 @@ prices are estimates. Unpriced usage must stay unpriced in the ledger and UI.
credentials, prompts, binary override or borrowed workspace package.
- Draft #14956 at `2b3d7ff0a` has a fresh Greptile 5/5, the Linux finding is
resolved, and its CI checks pass. Subsequent changes require a fresh review.
- At `03dd6ef93`, fresh pending-permission Stop passes: the original callback is
cancelled, a stale decline is rejected, the owned processes retire and the
continuous watcher records no file effect. The subsequent steering attempt
retains its failed grade. Its browser successfully posts one queued comment,
but the oracle compares plain input with the editor's Markdown-escaped body
and never clicks Steer. The correction binds to the exact browser POST body;
67 calibrations include escaped content and rejection of an altered queue.
- The `03dd6ef93` full build passes. The broad unit run reports 14,984 passes
and one HTTP socket failure; all 12 tests in that unchanged suite pass in
isolation. Fresh review is 5/5. CI's public installer reaches Pi setup but
exhausts its 256 MiB scratch mount. Pi assembly now gets at most 2048 MiB,
retaining the same unprivileged, read-only sandbox and 3 GiB memory limit.
All seven sandbox checks pass. CI's separate legacy signoff browser failure
received one diagnostic shard rerun; it is not counted as passing yet.
- Earlier evidence in `doc/architecture/runner-pi-capabilities.md` is historical
and must not be counted as qualification of a new source revision.
@@ -38,6 +38,18 @@ test('only the scripts-disabled dependency download gets network access', () =>
assert.ok(values(args, '--env').includes('npm_config_ignore_scripts=true'));
});
test('Pi assembly gets bounded scratch capacity while preserving sandbox restrictions', () => {
const args = grokConsumerDockerArgs({ ...paths, download: true, temporarySizeMiB: 2048, command: ['node', '/consumer/node_modules/paperclipai/dist/index.js', 'runtime', 'setup', 'pi'] });
assert.deepEqual(values(args, '--tmpfs'), ['/tmp:rw,nosuid,nodev,size=2048m,mode=1777']);
assert.deepEqual(values(args, '--memory'), ['3g']);
assert.ok(args.includes('--read-only'));
assert.deepEqual(values(args, '--cap-drop'), ['ALL']);
assert.deepEqual(values(args, '--security-opt'), ['no-new-privileges']);
for (const temporarySizeMiB of [0, 255, 2049, Infinity, '2048']) {
assert.throws(() => grokConsumerDockerArgs({ ...paths, temporarySizeMiB, command: ['node'] }), /bounded/);
}
});
test('the separately provisioned executable is exposed read-only to the offline probe', () => {
const prerequisite = '/private/staging/native/grok';
const args = grokConsumerDockerArgs({ ...paths, prerequisite, command: ['node', '/packages/probe.mjs', 'present'] });
+5 -2
View File
@@ -7,17 +7,20 @@ export const GROK_PUBLIC_INSTALL_LIFECYCLE = [
'npm', 'rebuild', '--offline', '--ignore-scripts=false', '--dangerously-allow-all-scripts',
];
export function grokConsumerDockerArgs({ assets, consumer, cache, command, uid, gid, download = false, prerequisite }) {
export function grokConsumerDockerArgs({ assets, consumer, cache, command, uid, gid, download = false, prerequisite, temporarySizeMiB = 256 }) {
if (!Number.isSafeInteger(uid) || uid <= 0 || !Number.isSafeInteger(gid) || gid <= 0) {
throw new Error('Public-install verification requires an unprivileged host user');
}
if (!Number.isSafeInteger(temporarySizeMiB) || temporarySizeMiB < 256 || temporarySizeMiB > 2048) {
throw new Error('Public-install temporary storage must be bounded between 256 and 2048 MiB');
}
return [
'run', '--rm', '--platform', 'linux/amd64',
'--user', `${uid}:${gid}`, '--read-only',
'--cap-drop', 'ALL', '--security-opt', 'no-new-privileges',
'--pids-limit', '256', '--memory', '3g',
'--network', download ? 'bridge' : 'none',
'--tmpfs', '/tmp:rw,nosuid,nodev,size=256m,mode=1777',
'--tmpfs', `/tmp:rw,nosuid,nodev,size=${temporarySizeMiB}m,mode=1777`,
'--env', 'HOME=/tmp', '--env', 'npm_config_cache=/cache',
'--env', 'npm_config_nodedir=/usr/local',
'--env', 'npm_config_audit=false', '--env', 'npm_config_fund=false',
+4 -1
View File
@@ -121,7 +121,10 @@ try {
// isolated; the actual admission probe runs without a network or credentials.
const piProbe = join(assets, 'pi-public-install-probe.mjs');
cpSync(join(repo, 'scripts/pi-public-install-probe.mjs'), piProbe); chmodSync(piProbe, 0o644);
const setup = isolated(['node', '/consumer/node_modules/paperclipai/dist/index.js', 'runtime', 'setup', 'pi'], { download: true }).toString();
// Pi assembles a bundled runtime in scratch space before atomic publication.
// Keep the existing sandbox and memory bound; only this download needs more
// temporary capacity than the smaller offline lifecycle probes.
const setup = isolated(['node', '/consumer/node_modules/paperclipai/dist/index.js', 'runtime', 'setup', 'pi'], { download: true, temporarySizeMiB: 2048 }).toString();
const receipt = JSON.parse(setup.trim());
assert.equal(receipt.status, 'installed_verified');
assert.equal(receipt.target, 'linux-x64');
+2 -1
View File
@@ -437,7 +437,8 @@ Only after cancellation does it attempt a stale **decline**, which must return
cancelled run and no automatic continuation.
Steering submits a random marker only in a browser comment after the permission
is pending, then clicks that comment's production Steer button. It records the
is pending, binds the queued comment to the exact body submitted by the
production Markdown editor, then clicks that comment's production Steer button. It records the
exact public POST's queue/revision/run binding and requires the saved run
acknowledgment plus the Product facade's same-turn acknowledgment item. The raw
Rust `acpx-control-*` transport echo is suppressed by the facade;
+2 -1
View File
@@ -1090,8 +1090,9 @@ export const runnerSuites: readonly RunnerSuiteFixture[] = [
description: "Pending native-write Stop and browser-originated same-turn steering, with exact control receipts and independent retirement/no-effect evidence.",
groups: ["native"], profiles: extendedHarnessProfiles.filter(profile => profile.qualificationCandidate === "pi"),
environments: runnerEnvironments, tasks: piControlTasks, expectedMatrixSize: 4,
definitionMetadata: { version: 2, qualification: "pending", scheduling: "explicit-only", profileVersion: QUALIFIED_ACPX_PROFILES.pi.agentProfileVersion,
definitionMetadata: { version: 3, qualification: "pending", scheduling: "explicit-only", profileVersion: QUALIFIED_ACPX_PROFILES.pi.agentProfileVersion,
nativeArguments: "streamed-until-exact-target",
steeringComment: "exact-browser-submitted-markdown",
pending: "paperclip.e2e.pi-control-pending.v1", stop: "paperclip.e2e.pi-stop-settlement.v1", steering: "paperclip.e2e.pi-steering-settlement.v1",
permissionPolicy: "approve-reads", lifecycle: "per_turn", normalCompletionProvesStop: false, nativeFollowUp: "not-covered", providerDeath: "not-covered",
remoteObservationCoverage: "continuous-through-owned-process-retirement", filesystemAfterRemoteRetirementObserved: false },
+12 -3
View File
@@ -11,7 +11,7 @@ import { readPiSteeringSettlement } from "./pi-controls-evidence.js";
const harness = vi.hoisted(() => ({ target: "", prompt: "", message: "", mutation: false, incomplete: false }));
vi.mock("./user-actions.js", () => ({
createTaskThroughUi: async (input: { prompt: string }) => { harness.prompt = input.prompt; },
submitTaskReply: async (_page: unknown, body: string) => { harness.message = body; return Date.now(); },
submitTaskReply: async (page: { submitReply(body: string): void }, body: string) => { page.submitReply(body); return Date.now(); },
}));
vi.mock("./copilot-local-fixtures.js", async importOriginal => {
const actual = await importOriginal<typeof import("./copilot-local-fixtures.js")>();
@@ -26,7 +26,7 @@ vi.mock("@playwright/test", () => ({ expect: (actual: any, message?: string) =>
toHaveCount: async (value: number) => { if (typeof actual.count === "function") expect(actual.count()).toBe(value); },
}) }));
async function exercise(taskId: string, remote: boolean, failure?: "mutation" | "incomplete" | "missing-ack" | "missing-comment" | "foreign-comment") {
async function exercise(taskId: string, remote: boolean, failure?: "mutation" | "incomplete" | "missing-ack" | "missing-comment" | "foreign-comment" | "foreign-queued-body") {
harness.target = ""; harness.prompt = ""; harness.message = ""; harness.mutation = failure === "mutation"; harness.incomplete = failure === "incomplete";
const task = piControlTasks.find(t => t.id === taskId)!, stopCase = taskId === "pending-permission-stop";
const workspacePath = await mkdtemp(join(tmpdir(), "pi-controls-fixture-"));
@@ -39,7 +39,7 @@ async function exercise(taskId: string, remote: boolean, failure?: "mutation" |
const target = remote ? "pi-control-fixture.txt" : harness.target;
f.scope.target = target; f.tool.target = target; f.issue.title = task.buildTitle("fixture");
};
const queue = () => ({ queueId: "queue", targetRunId: "run", revision: "revision", protocol: "paperclip_runner_v1", steeringDisposition: "available", entries: harness.message ? [{ comment: { id: "comment", body: harness.message } }] : [] });
const queue = () => ({ queueId: "queue", targetRunId: "run", revision: "revision", protocol: "paperclip_runner_v1", steeringDisposition: "available", entries: harness.message ? [{ comment: { id: "comment", body: harness.message + (failure === "foreign-queued-body" ? " altered" : "") } }] : [] });
const api = {
post: async (path: string, body: any) => {
if (path.endsWith("/projects")) return { name: "Pi controls fixture" };
@@ -102,6 +102,13 @@ async function exercise(taskId: string, remote: boolean, failure?: "mutation" |
},
});
const page = { goto: async () => {}, reload: async () => {}, getByText: () => locator(),
submitReply: (body: string) => {
// Match the production editor's retained Markdown escaping. Matching the
// plain input would never observe this otherwise valid queued comment.
harness.message = body.replaceAll("_", "\\\\_").replaceAll("[]", "\\\\[]");
expect(harness.message).not.toBe(body);
postBrowser("/api/issues/issue/comments", { body: harness.message });
},
getByTestId: (id: string) => locator(id === "task-chat-runtime-request" ? "card" : id.startsWith("task-chat-queued-steer-") ? "steer" : id === "task-chat-history-loading" ? "loading" : "other"),
waitForRequest: (predicate: (request: any) => boolean) => new Promise(resolve => waiters.push({ predicate, resolve })) };
// The real shared remote oracle is exercised. Poisoned local copyback cannot
@@ -127,6 +134,7 @@ async function exercise(taskId: string, remote: boolean, failure?: "mutation" |
observe: () => {}, capture: async () => {}, evidence: async (name: string, value: unknown) => saved.set(name, structuredClone(value)), remoteBootstrap,
registerCleanupAssertion: (fn: () => Promise<any>) => localCleanup.push(fn), registerBeforeEnvironmentTeardownAssertion: (fn: () => Promise<any>) => remoteCleanup.push(fn) } as any);
if (failure === "missing-ack") { await expect(call).rejects.toThrow("acknowledgement"); expect(browserDeclines).toBe(0); }
else if (failure === "foreign-queued-body") { await expect(call).rejects.toThrow("browser comment queued"); expect(browserSteers).toBe(0); expect(browserDeclines).toBe(0); }
else if (remote && (failure === "mutation" || failure === "incomplete")) await expect(call).rejects.toThrow();
else {
const result = await call; expect(result.checks.every(c => c.passed)).toBe(true); expect(saved.has("api-state.json")).toBe(true);
@@ -165,4 +173,5 @@ for (const task of ["pending-permission-stop", "same-turn-steering"]) {
it.each(["mutation", "incomplete"] as const)("fails local cleanup on %s despite an absent final target", failure => exercise("pending-permission-stop", false, failure));
it.each(["mutation", "incomplete"] as const)("fails remote lifetime proof on %s despite an absent target", failure => exercise("same-turn-steering", true, failure));
it("never denies the native write before the steering acknowledgement exists", () => exercise("same-turn-steering", false, "missing-ack"));
it("rejects a queued body that differs from the exact browser submission", () => exercise("same-turn-steering", false, "foreign-queued-body"));
it.each(["missing-comment", "foreign-comment"] as const)("retains and rejects %s during cleanup without fabricating persisted output", failure => exercise("same-turn-steering", false, failure));
+8 -2
View File
@@ -172,10 +172,16 @@ export async function runPiControlsFlow(input: {
await input.evidence("pi-control-pending.json", pending);
const marker = `PI-STEER-${randomBytes(16).toString("hex")}`;
const message = `After the actual native denial, complete this negative test. Do not write or invoke any other native tool. ${piNativeFinish(marker)}`;
const commentRoutes = new Set([issue.id, issue.identifier].filter(Boolean).map(id => `/api/issues/${id}/comments`));
const submitted = page.waitForRequest(request => commentRoutes.has(new URL(request.url()).pathname) && request.method() === "POST");
await submitTaskReply(page, message);
// The production rich-text editor serializes plain input as Markdown.
// Bind the queued comment to the exact body actually sent by the browser.
const submittedBody = (await submitted).postDataJSON()?.body;
check("browser-steering-content", typeof submittedBody === "string" && submittedBody.startsWith("After the actual native denial, complete this negative test. Do not write or invoke any other native tool. ") && submittedBody.includes(marker), "Browser submitted the hidden instruction while permission remains pending");
const queue = await pollUntil({ label: "Pi browser comment queued for steering", deadlineAt: input.deadlineAt, intervalMs: 200,
load: () => api.get<Row>(`/api/issues/${issue.id}/queued-comments`), accept: q => q.steeringDisposition === "available" && q.entries?.some((entry: Row) => entry.comment.body === message) });
const entries = queue.entries.filter((entry: Row) => entry.comment.body === message);
load: () => api.get<Row>(`/api/issues/${issue.id}/queued-comments`), accept: q => q.steeringDisposition === "available" && q.entries?.some((entry: Row) => entry.comment.body === submittedBody) });
const entries = queue.entries.filter((entry: Row) => entry.comment.body === submittedBody);
check("one-browser-steering-message", entries.length === 1 && queue.entries.length === 1 && queue.targetRunId === pending.scope.runId, "One browser-originated comment targets the pending run");
const commentId = entries[0].comment.id, queueId = queue.queueId;
assertSamePiPending(pending, observePiControlPending({ ...await load(), scope: scope() }));