mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 19:35:13 +01:00
## Thinking Path > - Paperclip manages agents and their work through normal company controls. > - Product E2E journeys check these controls through the browser and public API. > - Cloud onboarding uses one Connect action to validate the environment and hire the first agent. > - A failed Connect action previously left only a general journey error in the report. > - This pull request retains a closed diagnostic receipt for that action. > - The report can show the observed failure boundary without retaining private response text. ## Linked Issues or Issue Description **What existing behavior does this improve?** Cloud onboarding Product E2E failure evidence and report detail. **Current behavior** A validation or hire failure can reach the journey as a general operation failure. The report does not retain which Connect response failed. **Proposed behavior** Observe only the same-origin, same-company validation and hire POST requests made during one Connect action. Retain a closed receipt with the checkpoint, response state, bounded HTTP status, and allowlisted adapter, outcome, check code, level, and server code. The Get started control and saved-agent readback remain required. **Reason and benefit** The next failure can be attributed to the exact observed browser/API boundary. This change does not claim the cause of a historical failure. **Breaking changes** The new receipt is optional. Historical results remain valid. Explicit validation and hire rejections now stop the journey with a precise closed error code. This changes only E2E source; it does not change product behavior or qualification gates. ## What Changed - Add a bounded Connect observer with exact request matching and listener cleanup. - Retain known server codes from `details.code` or `code` without response messages, hints, headers, request bodies, provider text, or credentials. - Validate the receipt as a closed schema and render its allowlisted fields in existing report detail. - Include the new helper in the journey definition hash. - Add route, privacy, malformed-body, deadline, late-response, delayed-failure, success, and historical compatibility tests. Document the evidence boundary. ## Verification - Focused Product E2E tests passed: 3 files, 58 tests on the review-fix head. - E2E typecheck, workspace typecheck, and full build passed on the preceding implementation head. The review fix changes only report labels, checkpoint timing, and the matching tests. - The local full `pnpm test:run` command stopped progressing at the general-server start header. Its owned session was interrupted. This is not recorded as a pass. - All 58 remote checks completed on the final review-fix head: 55 passed, one neutral, and two expected skips. Both Greptile and Paperclip reviews are 5/5 with no unresolved review threads. - No live campaign was run with this patch. The observed historical failure is preserved. ## Risks - Unknown or missing responses remain explicit diagnostic states. They do not establish a product root cause. - Response body reads stop after five seconds or the action deadline and accept at most 64 KiB of JSON. Checks are capped at 32 retained entries. - New server or check codes appear as `unrecognized` until the closed allowlist is updated. - The production launch classifier, native gateway qualification, and Get started requirement are unchanged. ## Model Used OpenAI Codex (GPT-6 family), with code execution and tool use. The host does not expose the exact model ID or context window to this run; these values are unavailable. ## 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 --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
113 lines
9.2 KiB
TypeScript
113 lines
9.2 KiB
TypeScript
import type { RunnerE2EResult } from "./types.js";
|
|
import { isBlockedConnectionResult } from "./connection-evidence.js";
|
|
import { validateOnboardingConnectEvidence } from "./product-journeys/onboarding-connect.js";
|
|
const html = (value: unknown) =>
|
|
String(value ?? "").replace(
|
|
/[&<>"']/g,
|
|
(c) =>
|
|
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[
|
|
c
|
|
]!,
|
|
);
|
|
|
|
function renderOnboardingConnect(result: RunnerE2EResult): string {
|
|
const receipt = result.productJourney?.onboardingConnect;
|
|
if (receipt === undefined) return "";
|
|
try {
|
|
validateOnboardingConnectEvidence(receipt);
|
|
} catch {
|
|
return "<p>Onboarding Connect diagnostics unavailable: invalid receipt.</p>";
|
|
}
|
|
return `<details class="case-context"><summary>Onboarding Connect diagnostics</summary>
|
|
<p>Last checkpoint: <code>${html(receipt.checkpoint)}</code></p>
|
|
${([["validation", "Native validation"], ["hire", "Agent hire"]] as const).map(([key, label]) => {
|
|
const response = receipt[key];
|
|
return `<h4>${label}</h4><dl>${[
|
|
["Response state", response.state], ["HTTP status", response.httpStatus],
|
|
["Test endpoint adapter", response.requestedAdapter], ["Returned adapter", response.adapter],
|
|
["Validation outcome", response.outcome], ["Server code", response.serverCode],
|
|
].map(([name, value]) => `<div><dt>${name}</dt><dd>${html(value ?? "Unavailable")}</dd></div>`).join("")}</dl>
|
|
${response.checks.length ? `<p>Validation checks:</p><ul>${response.checks.map(check => `<li><code>${html(check.code)}</code> · ${html(check.level)}</li>`).join("")}</ul>` : ""}
|
|
${response.truncatedChecks ? "<p>Only the first 32 validation checks were retained.</p>" : ""}`;
|
|
}).join("")}</details>`;
|
|
}
|
|
/** An interrupted first-task journey is not evidence of failed downstream behavior. */
|
|
export function isIncompleteFirstTaskResult(
|
|
result: RunnerE2EResult,
|
|
errors: readonly string[] = [],
|
|
) {
|
|
if (result.productJourney && ["blocked", "unsupported"].includes(result.productJourney.outcome)) return result.cleanup === "passed" && errors.length === 0;
|
|
if (isBlockedConnectionResult(result)) return result.cleanup === "passed" && result.failureClass !== "secret_leak" && errors.length === 0;
|
|
const checks = result.firstTask?.checks ?? [];
|
|
return result.status === "failed" &&
|
|
result.failureClass === "candidate_failure" &&
|
|
result.cleanup === "passed" && errors.length === 0 &&
|
|
checks.some((c) => c.notReached) &&
|
|
checks.every((c) => c.passed || c.notReached);
|
|
}
|
|
|
|
export function renderCaseOutcome(
|
|
result: RunnerE2EResult,
|
|
valid: boolean,
|
|
errors: readonly string[],
|
|
) {
|
|
const checks =
|
|
(result.productJourney?.checks.map(c => ({ ...c, detail: c.id.replaceAll("-", " "), notReached: undefined as string | undefined })) ?? (result.firstTask?.checks.length ? result.firstTask.checks : undefined)) ??
|
|
(result.matcherResults ?? []).map((m) => ({
|
|
id: m.matcher.kind === "json_path" ? m.matcher.path : m.matcher.kind,
|
|
passed: m.passed,
|
|
notReached: isBlockedConnectionResult(result) && !m.passed ? "Blocked before this checkpoint" : undefined as string | undefined,
|
|
detail: m.detail,
|
|
}));
|
|
const failures = checks.filter((c) => !c.passed && !c.notReached);
|
|
const notReached = checks.filter((c) => c.notReached);
|
|
const evaluated = checks.length - notReached.length;
|
|
const passed = checks.filter((c) => c.passed).length;
|
|
const incomplete = isIncompleteFirstTaskResult(result, errors);
|
|
const failed = !valid || result.status === "failed";
|
|
const reason =
|
|
result.failureClass === "secret_leak"
|
|
? /persisted Paperclip home/.test(result.error ?? "")
|
|
? "Credential-persistence check failed"
|
|
: "Secret / evidence safety check failed"
|
|
: (result.failureClass?.replaceAll("_", " ") ??
|
|
(result.cleanup === "failed"
|
|
? "Cleanup failed"
|
|
: "Run or evidence validation failed"));
|
|
const messages = [
|
|
...new Set(
|
|
[result.error, ...errors].filter((s): s is string => Boolean(s)),
|
|
),
|
|
];
|
|
return `<section class="case-outcome ${failed ? "outcome-failed" : "outcome-passed"}" aria-label="Case result explanation">
|
|
<strong>${result.productJourney ? html(result.productJourney.outcome) : isBlockedConnectionResult(result) ? `Blocked: ${html(result.providerConnection!.outcome.replaceAll("_", " "))}` : incomplete ? "Incomplete journey" : failed ? "Overall failed" : "Overall passed"} · ${checks.length ? `${passed}/${evaluated} behavioral checks passed${notReached.length ? ` · ${notReached.length} not reached` : ""}` : "No behavioral checks recorded"}</strong>
|
|
${result.productJourney ? `<p>Deployment: ${html(result.productJourney.deployment?.kind ?? "unavailable")} · ${html(result.productJourney.deployment?.targetId ?? "unavailable")} · ${html(result.productJourney.deployment?.commit ?? "revision unavailable")} · starting state: ${html(result.productJourney.startingState)} · resources: ${html(result.productJourney.resourceDisposition)}</p><ul>${result.productJourney.phases.map(p => `<li>${html(p.phase)}: ${html(p.outcome)}${p.code ? ` (${html(p.code)})` : ""}</li>`).join("")}</ul>` : ""}
|
|
${result.productJourney ? `<details class="case-context"><summary>Deployment and setup evidence</summary><dl>${[
|
|
["Campaign", result.productJourney.campaignId], ["Attempt", result.productJourney.attemptId],
|
|
["Origin", result.productJourney.deployment?.origin], ["Cloud release", result.productJourney.deployment?.cloudRelease],
|
|
["Runtime image", result.productJourney.deployment?.runtimeImage], ["Effective configuration hash", result.productJourney.deployment?.configurationHash],
|
|
["Harness revision", result.source?.sha], ["Journey definition hash", result.suiteDefinitionHash],
|
|
].map(([label, value]) => `<div><dt>${html(label)}</dt><dd>${html(value ?? "Unavailable")}</dd></div>`).join("")}</dl>
|
|
${result.productJourney.deployment?.executionEnvironment ? `<p>Managed environment: ${html(result.productJourney.deployment.executionEnvironment.id)} · ${html(result.productJourney.deployment.executionEnvironment.driver)} · ${html(result.productJourney.deployment.executionEnvironment.status)}</p>` : ""}
|
|
${result.productJourney.deployment?.effectiveFeatures ? `<p>Effective features:</p><dl>${Object.entries(result.productJourney.deployment.effectiveFeatures).map(([key, value]) => `<div><dt>${html(key)}</dt><dd>${html(value)}</dd></div>`).join("")}</dl>` : ""}</details>` : ""}
|
|
${result.productJourney?.operations ? `<p>Eval operations: ${html(result.productJourney.operations.campaignId)} · ${result.productJourney.operations.requestCount} requests · model cost $${html(result.productJourney.operations.modelCostUsd ?? "unknown")} (setup $${html(result.productJourney.operations.setupCostUsd ?? "unknown")}, counted once). Feedback: ${html(result.productJourney.operations.feedback.collection)} · ${result.productJourney.operations.feedback.complaints} complaints · ${result.productJourney.operations.feedback.suggestions} suggestions.</p>` : ""}
|
|
${result.productJourney?.operations?.traceIds.length ? `<details><summary>Trace correlation IDs</summary><ul>${result.productJourney.operations.traceIds.map(id => `<li><code>${html(id)}</code></li>`).join("")}</ul></details>` : ""}
|
|
${renderOnboardingConnect(result)}
|
|
${result.providerConnection ? `<p>Target: ${html(result.providerConnection.target.origin)} · ${html(result.providerConnection.target.commit ?? "revision unavailable")} · ${result.providerConnection.assisted ? "Assisted login" : "Unassisted"} · ${html(result.providerConnection.authFreshness)} browser · ${html(result.providerConnection.entry)} / ${html(result.providerConnection.method)} · stopped at ${html(result.providerConnection.phase)}</p>` : ""}
|
|
${failures.length ? `<p>Failed checks:</p><ul>${failures.map((c) => `<li><strong>${html(c.id)}</strong> — ${html(c.detail)}</li>`).join("")}</ul>` : checks.length ? `<p>No behavioral matcher failed.${notReached.length ? " The journey is incomplete; unexercised checks are not passes." : failed ? " The overall failure came from a separate run, cleanup, or evidence check." : ""}</p>` : ""}
|
|
${notReached.length ? `<p>Not reached:</p><ul>${notReached.map((c) => `<li><strong>${html(c.id)}</strong> — ${html(c.notReached)}</li>`).join("")}</ul>` : ""}
|
|
${checks.length ? `<details><summary>See all behavioral checks (${checks.length})</summary><table class="matchers"><thead><tr><th>Result</th><th>Check</th><th>Detail</th></tr></thead><tbody>${checks.map((c) => `<tr class="matcher-${c.notReached ? "not-reached" : c.passed ? "passed" : "failed"}"><td>${c.notReached ? "Not reached" : c.passed ? "Pass" : "Fail"}</td><td><code>${html(c.id)}</code></td><td>${html(c.detail)}${c.notReached ? ` — ${html(c.notReached)}` : ""}</td></tr>`).join("")}</tbody></table></details>` : ""}
|
|
${
|
|
failed
|
|
? `<p><strong>${html(incomplete ? "Recording stopped before the journey finished" : reason)}</strong></p>${messages
|
|
.map((m) =>
|
|
m.length > 1200
|
|
? `<details><summary>Full failure details (${m.length.toLocaleString("en-US")} characters)</summary><div class="failure-reason">${html(m)}</div></details>`
|
|
: `<div class="failure-reason">${html(m)}</div>`,
|
|
)
|
|
.join("")}`
|
|
: ""
|
|
}
|
|
</section>`;
|
|
}
|