Expose Pi question field types at the tool schema root

Keep method-specific validation and immutable profile admission. Record the real core-path passes and retain the original failed question evidence.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-08 10:18:31 -05:00
1 parent ee3b094d35
commit a690e5ab8f
13 files changed
+152 -30

No files matched your search

+1
View File
@@ -48,6 +48,7 @@ packages/paperclip-runner/test-fixtures/**
!packages/paperclip-runner/test-fixtures/pi-acp
packages/paperclip-runner/test-fixtures/pi-acp/**
!packages/paperclip-runner/test-fixtures/pi-acp/profile-v18-identity.json
!packages/paperclip-runner/test-fixtures/pi-acp/profile-v19-identity.json
!packages/paperclip-runner/docs/capability-contract.md
# check:runner-workflow-traceability access()es every regression test the
# stress-traceability spec names — those are src/**/*.test.ts files, so
@@ -14,8 +14,9 @@ Done and a succeeded native run; three follow-up turns with continuity; browser
questions and answers; plan approval; same-turn steering and denial; Stop; and
controller recovery with the original native question and run. Use the retained
`b696e131ae32a82418e570b32f4727b5adb14e49` packages and cloud image, the approved
Sonnet 4.6 fixture and low thinking. No local Docker, provider expansion, image
rebuild, or accounting change is needed for this scope.
Sonnet 4.6 fixture and low thinking. No local Docker, provider expansion, or
accounting change is in scope. Rebuild the cloud runtime only when a confirmed
core-path defect requires a production change.
The first current-cloud file attempt, `pi-core-0-1791470050`, edited, validated,
published and finished through the real product. Its strict definition-4 grade
@@ -29,6 +30,41 @@ canonical files (91,468,056 bytes) are retained with archive SHA-256
independent process and scratch cleanup pass. Definition 5 fences the unchanged
command for literal Markdown paste; the one-command grader stays unchanged.
The following current-cloud paths pass with runtime `b696e131`, harness
`ee3b094d35719e4dd7cbc791d1924205c4fa474f` and the retained immutable image:
| Core path | Canonical campaign | Retained evidence |
| --- | --- | --- |
| File edit, one exact successful command, authenticated public download, registered deliverable, finish, Done and finalized copy-back | `pi-core-0-1791470879` | 23 files; archive SHA256 `9a1c41e2ccad75f89a069ff571a3aa9f8cb94ae80c1bd397f7c886338b9a09bd`; definition 5 |
| Three browser follow-up turns retain native session/process/workspace identity and append the earlier work | `pi-core-1-1791471072` | 23 files; archive SHA256 `d10310a2720a0d6c0636ce389fc6cc5faa887cc5c95bd697cdf0172619973609`; definition 2 |
| Human plan approval resumes work and completion through the public product | `pi-core-3-1791472300` | 22 files; archive SHA256 `8c069ff176cb94166429901957f80e8b22c36ddf0f2047193889c4b7059aedf3`; definition 5 |
Every listed canonical result and independent cleanup check passes. All retained
files are hash-verified. These are exact-source passes; a changed runtime must
be checked again before they qualify that runtime.
Native-question campaign `pi-core-2-1791471416` remains failed. Its original
machine classification is `transient_infrastructure`; the run times out after
120 seconds. The evidence shows three rejected native-question calls: empty
arguments, then two `select` calls with `options` encoded as a JSON string. The
model subsequently asks confirmation while the browser waits for color. All
45 canonical files (177,803,509 bytes) are retained with archive SHA256
`66c801596d81b909ac2ad8004d885ddf69b7478d9ab9a0cce11a52ebfe8a6e28`;
independent retirement and scratch cleanup pass. Do not regrade this failure or
retry the unchanged runtime.
The production question schema has method-specific branches but no root field
definitions. Profile 19 exposes the same field types at the root and keeps every
method-specific constraint and runtime validator. Root-only gateway tool
rendering is the inferred compatibility cause; a fresh real question pass is
still required. Stringified options, absent arguments and cross-method fields
remain invalid. The existing 38 extension tests pass on the pinned Node 24.21.0.
All three prior closure hashes are independently reproduced from the retained
full manifest and reviewed Node pins; only `extensions/paperclip.js` changes.
No model allowlist, model fallback, credential policy or question-answer coercion
is added. The corrected runtime requires the normal hosted cloud build and
public-install checks before another paid question attempt.
## Correct cloud memory-read authority — 2026-10-08
Frozen runtime/image `b696e131ae32a82418e570b32f4727b5adb14e49` passes all
+2 -2
View File
@@ -14,12 +14,12 @@
"requiresProviderPolicy": false
},
"pi": {
"agentProfileVersion": 18,
"agentProfileVersion": 19,
"agentServerPackage": "pi-acp",
"agentServerVersion": "0.0.33",
"agentRuntimePackage": "@earendil-works/pi-coding-agent",
"agentRuntimeVersion": "1.0.0",
"commandDigest": "sha256:9d3e7d8269f1a0af94616552dfc69671932688b81dbbd5bab9ef356b3a9cbccb",
"commandDigest": "sha256:b7647ebf97f802ca053ec3384c912bf0e8d18eba308d27397bb1d95a37220825",
"requiresProviderPolicy": true
},
"cursor": {
@@ -3184,7 +3184,7 @@ mod tests {
#[test]
fn pi_profile_matches_published_identity_and_rejects_prior_profiles() {
let published: Value = serde_json::from_str(include_str!(
"../../../../test-fixtures/pi-acp/profile-v18-identity.json"
"../../../../test-fixtures/pi-acp/profile-v19-identity.json"
))
.unwrap();
let mut value = descriptor("codex");
@@ -3204,6 +3204,7 @@ mod tests {
include_str!("../../../../test-fixtures/pi-acp/profile-v15-identity.json"),
include_str!("../../../../test-fixtures/pi-acp/profile-v16-identity.json"),
include_str!("../../../../test-fixtures/pi-acp/profile-v17-identity.json"),
include_str!("../../../../test-fixtures/pi-acp/profile-v18-identity.json"),
] {
let prior: Value = serde_json::from_str(prior).unwrap();
value["commandDigest"] = prior["commandDigest"].clone();
@@ -29,7 +29,7 @@ pub(crate) fn acpx_release_profile(agent: &str) -> Option<AcpxReleaseProfile> {
agent_runtime_package: Some("@earendil-works/pi-coding-agent"),
agent_runtime_version: Some("1.0.0"),
command_digest:
"sha256:9d3e7d8269f1a0af94616552dfc69671932688b81dbbd5bab9ef356b3a9cbccb",
"sha256:b7647ebf97f802ca053ec3384c912bf0e8d18eba308d27397bb1d95a37220825",
requires_provider_policy: true,
},
"cursor" => AcpxReleaseProfile {
@@ -39,7 +39,7 @@ const { commandDigest, ...attestation } = contract;
assert.equal(commandDigest, `sha256:${createHash("sha256").update(canonicalJson(attestation)).digest("hex")}`);
assert.equal(contract.acpxPatchSha256, createHash("sha256")
.update(await readFile(new URL("../../patches/acpx@0.13.1.patch", root))).digest("hex"));
for (const [agent, path] of [["pi", "test-fixtures/pi-acp/profile-v18-identity.json"], ["copilot", "test/fixtures/copilot-profile-v16-identity.json"]]) {
for (const [agent, path] of [["pi", "test-fixtures/pi-acp/profile-v19-identity.json"], ["copilot", "test/fixtures/copilot-profile-v16-identity.json"]]) {
const identity = await readJson(path);
assert.equal(manifest.profiles[agent].agentProfileVersion, identity.declaration.agentProfileVersion);
assert.equal(manifest.profiles[agent].commandDigest, identity.commandDigest);
@@ -22,12 +22,12 @@ export const QUALIFIED_ACPX_PROFILE_DATA = {
"protocolVersion": 1,
"acpxVersion": "0.13.1",
"agent": "pi",
"agentProfileVersion": 18,
"agentProfileVersion": 19,
"agentServerPackage": "pi-acp",
"agentServerVersion": "0.0.33",
"agentRuntimePackage": "@earendil-works/pi-coding-agent",
"agentRuntimeVersion": "1.0.0",
"commandDigest": "sha256:9d3e7d8269f1a0af94616552dfc69671932688b81dbbd5bab9ef356b3a9cbccb",
"commandDigest": "sha256:b7647ebf97f802ca053ec3384c912bf0e8d18eba308d27397bb1d95a37220825",
"permissionPolicy": "interactive"
},
"cursor": {
@@ -7,7 +7,7 @@
* three pins. Never accept a digest supplied only by an installed manifest.
*/
export const PI_DISTRIBUTION_CLOSURE_SHA256 = Object.freeze({
"darwin-arm64": "8b85caad950fc720eabd80d7b67146b5b2106c66aecf1797993b90a3568ddc7f",
"darwin-x64": "c66bfff68705627b3c5589e038080b284a04027a0a0a3371a54a657a80787789",
"linux-x64": "a1b2797c5ca8245b441b0cc0e18f09463d9d896bd52631874320d77a09a4736d",
"darwin-arm64": "e076276c674dfffccbb488883e18571d77d7225d801fabf5c8391773ddbbfc6c",
"darwin-x64": "eafd44e672dec4966ef6f038620f003e5d492c889d475886cd66be2e9c2bff1d",
"linux-x64": "f493df174cfecba3c31c524aa486ae37663cd68f8fcc0d9556e3f615c4a40ce9",
});
@@ -56,11 +56,19 @@ describe("Pi installation factory", () => {
expect(current.commandDigest).not.toBe(prior.commandDigest);
});
it("preserves profile 18 and binds root question field types in profile 19", async () => {
const prior = JSON.parse(await readFile(new URL("../../../test-fixtures/pi-acp/profile-v18-identity.json", import.meta.url), "utf8"));
const current = JSON.parse(await readFile(new URL("../../../test-fixtures/pi-acp/profile-v19-identity.json", import.meta.url), "utf8"));
expect(Object.keys(current.declaration).filter(key => JSON.stringify(current.declaration[key]) !== JSON.stringify(prior.declaration[key])).sort())
.toEqual(["agentProfileVersion", "closure", "extensionSha256", "nativeQuestions"]);
expect(current.commandDigest).not.toBe(prior.commandDigest);
});
it("binds the profile declaration to the reviewed patch and platform closure pins", async () => {
const hash = (bytes: string | Uint8Array) => createHash("sha256").update(bytes).digest("hex");
const canonical = (value: any): string => Array.isArray(value) ? `[${value.map(canonical).join(",")}]`
: value && typeof value === "object" ? `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}` : JSON.stringify(value);
const identity = JSON.parse(await readFile(new URL("../../../test-fixtures/pi-acp/profile-v18-identity.json", import.meta.url), "utf8"));
const identity = JSON.parse(await readFile(new URL("../../../test-fixtures/pi-acp/profile-v19-identity.json", import.meta.url), "utf8"));
const declaration = identity.declaration;
expect(declaration.sharedRuntimeContract).toBe("paperclip.acpx-runtime-contract.v1");
expect(identity.commandDigest).toBe(`sha256:${hash(canonical(declaration))}`);
@@ -90,21 +98,21 @@ describe("Pi installation factory", () => {
it("rejects legacy profiles and tampered executable identities", () => {
expect(() => assertPiInstallationProfile(candidate())).not.toThrow();
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 1 })).toThrow("version 18");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 2 })).toThrow("version 18");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 3 })).toThrow("version 18");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 4 })).toThrow("version 18");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 5 })).toThrow("version 18");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 6 })).toThrow("version 18");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 7 })).toThrow("version 18");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 8 })).toThrow("version 18");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 9 })).toThrow("version 18");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 10 })).toThrow("version 18");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 11 })).toThrow("version 18");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 12 })).toThrow("version 18");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 13 })).toThrow("version 18");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 14 })).toThrow("version 18");
for (const priorVersion of [15, 16, 17]) expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: priorVersion })).toThrow("version 18");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 1 })).toThrow("version 19");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 2 })).toThrow("version 19");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 3 })).toThrow("version 19");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 4 })).toThrow("version 19");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 5 })).toThrow("version 19");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 6 })).toThrow("version 19");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 7 })).toThrow("version 19");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 8 })).toThrow("version 19");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 9 })).toThrow("version 19");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 10 })).toThrow("version 19");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 11 })).toThrow("version 19");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 12 })).toThrow("version 19");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 13 })).toThrow("version 19");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 14 })).toThrow("version 19");
for (const priorVersion of [15, 16, 17, 18]) expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: priorVersion })).toThrow("version 19");
for (const changed of [{ agentServerVersion: "latest" }, { commandDigest: `sha256:${"0".repeat(64)}` }, { agentRuntimePackage: "ambient-pi" }]) {
expect(() => assertPiInstallationProfile({ ...candidate(), ...changed })).toThrow("trusted declaration");
}
@@ -43,6 +43,8 @@ describe("owned Pi runtime extension", () => {
];
for (const question of questions) expect(validate(question)).toBe(true);
for (const question of [
{},
{ ...questions[0], options: JSON.stringify(questions[0]!.options) },
{ ...questions[3], placeholder: "Name" },
{ ...questions[2], prefill: "Old draft" },
{ ...questions[1], options: [] },
@@ -51,6 +53,21 @@ describe("owned Pi runtime extension", () => {
]) expect(validate(question)).toBe(false);
});
it("exposes native question field types to gateways that describe root properties", async () => {
const { config } = await workspace(); const h = harness();
await installPiRuntimeExtension(h.api, config);
const schema = h.nativeTools[0]!.parameters;
expect(schema.required).toEqual(["method", "title"]);
expect(schema.properties).toMatchObject({
method: { type: "string", enum: ["select", "confirm", "input", "editor"] },
title: { type: "string" },
options: { type: "array", items: { type: "object", required: ["id", "label"] } },
message: { type: "string" },
placeholder: { type: "string" },
prefill: { type: "string" },
});
});
it("stops Pi 1 cache warming even when native economics recommend a paid refresh", async () => {
const { config } = await workspace(); const h = harness();
await installPiRuntimeExtension(h.api, config);
@@ -374,7 +374,17 @@ export async function installPiRuntimeExtension(
pi.registerTool({
name: PI_NATIVE_QUESTION_TOOL, label: "Ask a native question",
description: "Ask the human a native Pi select, confirm, input, or editor question. Select is single-choice and returns the supplied stable option ID. This cannot approve tools or a Paperclip Plan. For durable task questions or Plan approval use the assigned Paperclip semantic tools. Cancellation is not an answer; confirm false means No or dismissal.",
parameters: { type: "object", anyOf: [
// Model gateways may render only the root properties when describing a
// tool. Expose the field types there as well as in the strict method
// branches, so an array of options cannot be mistaken for JSON text.
parameters: { type: "object", additionalProperties: false, required: ["method", "title"], properties: {
method: { type: "string", enum: ["select", "confirm", "input", "editor"] },
title: { type: "string", maxLength: PI_QUESTION_LABEL_MAX_LENGTH },
options: { type: "array", minItems: 1, maxItems: 128, items: { type: "object", additionalProperties: false, required: ["id", "label"], properties: { id: { type: "string", pattern: "^[A-Za-z0-9_-]{1,128}$" }, label: { type: "string", maxLength: PI_QUESTION_LABEL_MAX_LENGTH } } } },
message: { type: "string", maxLength: 16384 },
placeholder: { type: "string", maxLength: 16384 },
prefill: { type: "string", maxLength: 16384 },
}, anyOf: [
{ type: "object", additionalProperties: false, required: ["method", "title", "options"], properties: {
method: { const: "select" }, title: { type: "string", maxLength: PI_QUESTION_LABEL_MAX_LENGTH },
options: { type: "array", minItems: 1, maxItems: 128, items: { type: "object", additionalProperties: false, required: ["id", "label"], properties: { id: { type: "string", pattern: "^[A-Za-z0-9_-]{1,128}$" }, label: { type: "string", maxLength: PI_QUESTION_LABEL_MAX_LENGTH } } } },
@@ -0,0 +1,49 @@
{
"commandDigest": "sha256:b7647ebf97f802ca053ec3384c912bf0e8d18eba308d27397bb1d95a37220825",
"declaration": {
"schema": "paperclip.acpx_profile_declaration.v1",
"agent": "pi",
"agentProfileVersion": 19,
"acpxVersion": "0.13.1",
"agentServerVersion": "0.0.33",
"agentRuntimeVersion": "1.0.0",
"nodeVersion": "24.21.0",
"closure": {
"darwin-arm64": "e076276c674dfffccbb488883e18571d77d7225d801fabf5c8391773ddbbfc6c",
"darwin-x64": "eafd44e672dec4966ef6f038620f003e5d492c889d475886cd66be2e9c2bff1d",
"linux-x64": "f493df174cfecba3c31c524aa486ae37663cd68f8fcc0d9556e3f615c4a40ce9"
},
"wrapperSha256": "9d129b3d38772e93e97080aa6c4e574ac5df4e47ce5bc331a484a9fbf8188b36",
"helperSha256": "41e0490b617da0d60e0c8ec58ef311236f945129d99f816cff6897f78da7f91d",
"extensionSha256": "f5818573a355702388072b28926897db546a5cf346b5171f646582ce5f9102a4",
"agentFilesBinding": "registered-runtime-context-v1",
"nativeQuestions": "select-confirm-input-editor-v4-root-field-types",
"pathPolicy": "sdk-normalization-v1",
"mcpToolNames": "provider-safe-exact-source-v1",
"acpxPatchSha256": "00d6af17216cb0cad902b48633eb19bf0ec6b05837a984cb24661fca593f09c1",
"nativeAssistantMessages": "pi1-rpc-deltas-v3-bound-message-and-tool-receipts",
"nativeNotices": "session-extension-v2-bounded-runtime-failure",
"messageProjectionSha256": "17d1e91b948dfa6a56e1905645add52b502041ad843c8d50a13b07956f22aadd",
"noticeProjectionSha256": "014485adc690998e395d334ea3ae67f72be6060a2fc78a1185df7b8d1da28fbf",
"sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1",
"nativeInputDisposition": "queued-only-v1",
"cacheWarming": "disabled-owned-decision-v1",
"thinkingLevel": {
"supported": [
"off",
"low",
"high",
"max"
],
"newConfigurationDefault": "low",
"admission": "explicit-native-effective-before-prompt-v1",
"recovery": "mode-bound-v1"
},
"modelSelection": "caller-selected-native-verified-v1",
"providerConfigurationSourceSha256": "2735ec4b46929f4f59c26cea42e623dfc81016aa7da7bd8399a24caa68a75d6b",
"credentialEnvironmentSourceSha256": "4ee0e9ac56fab8889a4d06b98634bb1797aeda64fcb31e0a404829018c7a12cb",
"runtimeSandboxSourceSha256": "21e1ed2d132886260cf17f956dc21824948bd428b8a0aa05518c508017e66213",
"recoveryIdentitySourceSha256": "f333a24b19ef1b5b5d23ea9778e753f7051690e835ef9e4476a962d8a231c375",
"nativeCancellation": "acknowledged-cancelled-terminal-preserves-usage-v1"
}
}
+1 -1
View File
@@ -90,7 +90,7 @@ describe("runner E2E Daytona image contract", () => {
expect(dockerignore).toContain("packages/paperclip-runner/runner/target");
expect(await readFile(path.join(repositoryRoot, ".github/docker-context-checks.Dockerfile"), "utf8")).toContain("node packages/paperclip-runner/scripts/generate-acpx-profiles.mjs --check");
for (const declaration of [
"test-fixtures/pi-acp/profile-v18-identity.json",
"test-fixtures/pi-acp/profile-v19-identity.json",
"test/fixtures/copilot-profile-v16-identity.json",
]) {
expect(dockerignore).toContain(`!packages/paperclip-runner/${declaration}`);