mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-09 16:02:36 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - The chat catalog uses a fixed source and explicit execution conditions. > - A correct timeline response failed because its completed edit and attached command result were not recognized. > - Company credential setup can require a real human approval after the secure save. > - The controller must declare that optional approval and its run bounds before it admits the source. > - This pull request pins the reviewed source and caps native catalog overlap at two. > - The next run can test these conditions without treating local controls as native acceptance. ## Linked Issues or Issue Description Refs: #19357, #19086, #18971, #19093. This follows the prior fixed-source catalog pin. It changes only controller metadata, admission, tests, and load policy. The complete catalog and separate core feature PRs remain unmerged. ## What Changed - Pin immutable source `fe547be84826b4611d3ba9ee1a7d7aa0bd0a2843`, tree `a23723ea5244e6ce8cee11e9c4b476acb6978fcd`. - Advance only timeline v10 to v11 and Company v11 to v12. Add the declared Company proposal helper to its source closure. - Declare Company minimum two and maximum three native runs. Retain two base human inputs and permit at most one independently verified UI approval. Retain its original 480000ms attempt. - Carry the literal Company-only approval marker from the descriptor into the manifest. Reject foreign markers, changed bounds, and a mismatch with static source task metadata. - Cap native catalog execution at two workers. Keep the external lane serial and retain zero retries. - Keep all244 requests, contexts, expected outcomes, model, clocks, credentials and other case bounds unchanged. ## Verification - Source:421 focused controls across seven suites pass. Full runner E2E TypeScript check passes. - Explicit model-free approval UI canary passes one test with three local scenarios. It checks accepted approval, wrong human, and failed response. The first sandbox attempt stopped at Chrome launch; the allowed execution passed. - Controller:40 manifest, adapter and reconciliation controls pass. - SDK506 inputs remain byte-identical to the accepted registry `efa70621e9fb42aa185f7ee616a1a726aa3eccb367e67e28817cedf26ef2ade8`. - Launcher and trusted private evidence wire remain byte-identical. Source contract/flow accounting changes are explicit. - Source descriptor SHA256: `50e90d8c02ad57f444e27b522efe694db9734d47bdbc3a058475af77bac8b2ca`. - Controller manifest SHA256: `0447315601ed21ba15d6ea0ecd7fb89bc79ad6262db2594cd12ac774aa8d6c21`. - Review the two changed rows. The next planned experiment selects timeline and Company once at cap2 before a full ten-case run. No native run on this source has passed yet. - Exact controller head `d49dfc59ab7fddfe1a1de7e81bdb9c4e13400f30`: normal CI has56 successful checks and three configured skips. Protected `ci / verify` and `ci / e2e` both pass. Greptile5/5 and zero open threads are verified for this head. ## Risks - Original timeline5056 and Company5057 remain FAILED. Timeline5056 supplied the correct account; the local revised check passes its diagnostic only. Company5057 saved the secret but left the binding approval pending. - Company native3 completion within480s is unproved. This change does not raise its clock or claim to fix latency. - Two-worker execution is a changed load condition. It does not prove a concurrency cause, reliability, or eight-worker qualification. - Secret journeys require independent semantic safety review before pass credit. The documented closing-comma warning and arbitrary metadata-prefill classifier limitations remain follow-ups in the functional secret PR; a native failure is not waived. - Candidate core PRs remain unmerged. The full-catalog196/244 admission gate remains fixed. Local controls do not qualify all244 journeys. - Local checks use Node26.4.0. They do not replace actual AWS runtime qualification. - The separate Paperclip Review app remains queued while its assigned task waits for maintenance. It has no completed assessment. GitHub reports `UNSTABLE` and `MERGEABLE`; this is not an all-checks PASS claim. No review retry or staging mutation was performed. ## Model Used OpenAI Codex, GPT-6.1 Sol (`gpt-6.1-sol`), with reasoning, repository tools and code execution. The host does not expose an exact context-window size. No model or paid provider was called for local controls. ## 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>
209 lines
15 KiB
JavaScript
209 lines
15 KiB
JavaScript
import { execFile } from 'node:child_process';
|
|
import { promisify } from 'node:util';
|
|
import { appendFile, lstat, mkdir, open, readFile, realpath, writeFile } from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import { pathToFileURL } from 'node:url';
|
|
import { admitDispatch, hash, reject, validateSelection, evidencePolicy, catalogDefinition } from './chat-completion-testops-contract.mjs';
|
|
import { testopsClient, bootstrapPilotPlan, completePilotRegistration, validatePilotSelection, launchPilotPlan, verifyPilotUpload } from './chat-completion-testops-api.mjs';
|
|
import { parseOfficialBootstrapContext } from './testops-chat-reporter.mjs';
|
|
import { exportPilotAllure, readPilotExport } from './chat-completion-testops-results.mjs';
|
|
import { prepareCaseBundle, publishCaseBundle, publicationSet, gatherCaseProjections } from './chat-catalog-testops-streaming.mjs';
|
|
|
|
const execute = promisify(execFile);
|
|
const fullCatalogFailureCodes = new Set([
|
|
'case_alias_conflict', 'case_identity', 'case_metadata', 'catalog_case', 'catalog_identity',
|
|
'catalog_incomplete', 'catalog_page', 'created_case', 'created_plan', 'manifest', 'manifest_alias',
|
|
'mutation_marker', 'plan_conflict', 'plan_identity', 'plan_page', 'plan_scope', 'reconciliation_changed',
|
|
].map(code => `chat_full_catalog_testops_${code}`));
|
|
function compactCase(row) {
|
|
return Object.fromEntries(['key', 'scenario', 'executionId', 'fullName', 'registeredFullName', 'caseId'].filter(key => row[key] !== undefined).map(key => [key, row[key]]));
|
|
}
|
|
export function executionMatrix(selection, context, jobRunId) {
|
|
const selected = validateSelection(selection, context, jobRunId);
|
|
const matrix = { include: selected.map((row, index) => ({ executionId: row.executionId, key: row.key,
|
|
...(context.catalog === 'full' ? { index, required_credentials: row.requiredCredentials,
|
|
external: row.externalInput != null, timeout_minutes: Math.ceil(row.attemptTimeoutMs / 60_000) + 15,
|
|
private_ids: JSON.stringify([row.executionId]),
|
|
private_policy: JSON.stringify(evidencePolicy({ ...selection, cases: [row] }, context, jobRunId)) } : {}) })) };
|
|
if (JSON.stringify(matrix).length * 2 > 768_000) reject('matrix_size');
|
|
return matrix;
|
|
}
|
|
export function executionPartition(selection, context, jobRunId) {
|
|
const matrix = executionMatrix(selection, context, jobRunId);
|
|
if (context.catalog !== 'full') reject('catalog');
|
|
const small = matrix.include.length <= 3;
|
|
const native = { include: matrix.include.filter(row => !row.external) };
|
|
const external = { include: matrix.include.filter(row => row.external) };
|
|
const size = [matrix, native, external, selection.cases.map(row => row.executionId)]
|
|
.reduce((sum, value) => sum + JSON.stringify(value).length * 2, 0);
|
|
if (size > 1_000_000) reject('matrix_size');
|
|
return { matrix, native, external, parallelism: small && external.include.length ? '1' : '2' };
|
|
}
|
|
async function readOwned(file, root, limit) {
|
|
const absolute = path.resolve(file), base = await realpath(root);
|
|
if (!absolute.startsWith(`${base}${path.sep}`)) reject('file_path');
|
|
let current = base; const parts = path.relative(base, absolute).split(path.sep);
|
|
for (const [index, part] of parts.entries()) {
|
|
if (!/^[A-Za-z0-9._-]+$/.test(part) || ['.', '..'].includes(part)) reject('file_path');
|
|
current = path.join(current, part); const stat = await lstat(current);
|
|
if (stat.isSymbolicLink() || (index < parts.length - 1 ? !stat.isDirectory() : !stat.isFile() || stat.nlink !== 1 || stat.size > limit)) reject('file_path');
|
|
}
|
|
return readFile(absolute);
|
|
}
|
|
export async function actualContext(env, exec = execute) {
|
|
if (!env.GITHUB_EVENT_PATH || !env.GH_TOKEN || !/^[1-9][0-9]{0,15}$/.test(env.GITHUB_RUN_ID ?? '')) reject('context');
|
|
const stat = await lstat(env.GITHUB_EVENT_PATH);
|
|
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size > 65_536) reject('event');
|
|
const event = JSON.parse(await readFile(env.GITHUB_EVENT_PATH, 'utf8'));
|
|
const { stdout } = await exec('gh', ['api', `repos/paperclipai/paperclip/actions/runs/${env.GITHUB_RUN_ID}`],
|
|
{ env, shell: false, timeout: 30_000, maxBuffer: 512_000, encoding: 'utf8' });
|
|
const run = JSON.parse(stdout), context = admitDispatch(env, event, run.triggering_actor?.id);
|
|
if (run.id !== Number(context.runId) || run.run_attempt !== 1 || run.head_sha !== context.workflowSha || run.head_branch !== 'master'
|
|
|| run.event !== 'workflow_dispatch' || run.path !== catalogDefinition(context).workflow || run.repository?.full_name !== 'paperclipai/paperclip' || run.repository.private !== false
|
|
|| run.actor?.id !== context.actorId || run.run_number !== Number(env.GITHUB_RUN_NUMBER)
|
|
|| !Number.isSafeInteger(run.workflow_id) || run.workflow_id <= 0) reject('actual_run');
|
|
return { ...context, workflowId: run.workflow_id };
|
|
}
|
|
async function readSelection(file, env, context) {
|
|
if (!/^[a-f0-9]{64}$/.test(env.EXPECTED_SELECTION_SHA256 ?? '')) reject('selection_digest');
|
|
const bytes = await readOwned(file, env.GITHUB_WORKSPACE, context.catalog === 'full' ? 262_144 : 32_768);
|
|
if (hash(bytes) !== env.EXPECTED_SELECTION_SHA256) reject('selection_digest');
|
|
const selected = JSON.parse(bytes); validateSelection(selected, context, env.ALLURE_JOB_RUN_ID); return selected;
|
|
}
|
|
async function output(env, key, value) {
|
|
if (!env.GITHUB_OUTPUT || /[\r\n]/.test(value)) reject('output');
|
|
await appendFile(env.GITHUB_OUTPUT, `${key}=${value}\n`);
|
|
}
|
|
async function stateDirectory(env, command, existing = false) {
|
|
const root = path.join(await realpath(env.RUNNER_TEMP), `chat-completion-testops-${command}`);
|
|
if (!existing) await mkdir(root, { mode: 0o700 });
|
|
const stat = await lstat(root);
|
|
if (!stat.isDirectory() || stat.isSymbolicLink() || stat.mode & 0o077 || process.getuid && stat.uid !== process.getuid()) reject('state_directory');
|
|
return root;
|
|
}
|
|
const writeReceipt = (root, name, value) => writeFile(path.join(root, name), JSON.stringify(value, null, 2), { flag: 'wx', mode: 0o600 });
|
|
function mutationMarker(root) {
|
|
return async stage => {
|
|
if (!/^[a-z_]{1,80}$/.test(stage)) reject('mutation_marker');
|
|
const handle = await open(path.join(root, `once-${stage}.json`), 'wx', 0o600);
|
|
await handle.writeFile(JSON.stringify({ stage, status: 'STARTED_NO_AUTOMATIC_RESEND' })); await handle.close();
|
|
};
|
|
}
|
|
|
|
export async function run(command, argument, env = process.env, { fetchImpl = fetch, exec = execute } = {}) {
|
|
let state;
|
|
try {
|
|
if (!['guard', 'bootstrap', 'bootstrap-context', 'selection', 'launch-plan', 'verify-selection', 'upload', 'verify-upload',
|
|
'prepare-case-upload', 'upload-case', 'verify-stream', 'gather-case-projections'].includes(command)) reject('operation');
|
|
const context = await actualContext(env, exec);
|
|
if (command === 'guard') {
|
|
await output(env, 'operation', context.operation); await output(env, 'target_sha', context.targetSha);
|
|
await output(env, 'workflow_id', String(context.workflowId)); return 0;
|
|
}
|
|
if (command === 'verify-selection') {
|
|
if (Object.hasOwn(env, 'ALLURE_TOKEN')) reject('worker_credential');
|
|
await readSelection(argument, env, context); return 0;
|
|
}
|
|
if (['bootstrap', 'bootstrap-context'].includes(command) && context.operation !== 'bootstrap'
|
|
|| command === 'launch-plan' && context.operation !== 'launch-plan'
|
|
|| ['selection', 'upload', 'verify-upload', 'prepare-case-upload', 'upload-case', 'verify-stream', 'gather-case-projections'].includes(command) && context.operation !== 'evaluate') reject('operation');
|
|
state = await stateDirectory(env, command === 'bootstrap-context' ? 'bootstrap' : command, command === 'bootstrap-context');
|
|
if (command === 'prepare-case-upload') {
|
|
if (Object.hasOwn(env, 'ALLURE_TOKEN')) reject('worker_credential');
|
|
const selected = await readSelection(path.join(env.GITHUB_WORKSPACE, 'chat-completion-testops-selection', 'testops-pilot-selection.json'), env, context);
|
|
const prepared = await prepareCaseBundle(path.resolve(argument), env.ALLURE_RESULTS, context, selected,
|
|
env.CHAT_TESTOPS_EXECUTION_ID, env.EXPECTED_SELECTION_SHA256);
|
|
await writeReceipt(state, 'prepared.json', prepared); return 0;
|
|
}
|
|
if (command === 'gather-case-projections') {
|
|
if (Object.hasOwn(env, 'ALLURE_TOKEN')) reject('worker_credential');
|
|
const selected = await readSelection(path.join(env.GITHUB_WORKSPACE, 'chat-completion-testops-selection', 'testops-pilot-selection.json'), env, context);
|
|
await gatherCaseProjections(env.CHAT_TESTOPS_BUNDLES, path.resolve(argument), context, selected, env.EXPECTED_SELECTION_SHA256);
|
|
return 0;
|
|
}
|
|
const api = await testopsClient(env.ALLURE_TOKEN, fetchImpl);
|
|
if (command === 'bootstrap') {
|
|
const prepared = await bootstrapPilotPlan(api, Number(env.CHAT_TESTOPS_INTEGRATION_ID), mutationMarker(state), context);
|
|
await writeReceipt(state, 'prepared.json', context.catalog === 'full' ? { ...prepared, cases: prepared.cases.map(compactCase) } : prepared); return 0;
|
|
}
|
|
if (command === 'bootstrap-context') {
|
|
const official = parseOfficialBootstrapContext((await exec('allurectl', ['job-run', 'env'],
|
|
{ env, shell: false, timeout: 30_000, maxBuffer: 16_384, encoding: 'utf8' })).stdout);
|
|
const prepared = JSON.parse(await readOwned(path.join(state, 'prepared.json'), env.RUNNER_TEMP, context.catalog === 'full' ? 262_144 : 32_768));
|
|
const receipt = await completePilotRegistration(api, prepared, official, context, context.workflowId, mutationMarker(state),
|
|
observation => writeReceipt(state, 'registration-observed.json', observation));
|
|
await writeReceipt(state, 'registration.json', receipt); return 0;
|
|
}
|
|
if (command === 'launch-plan') {
|
|
const receipt = await launchPilotPlan(api, env, context, context.workflowId, mutationMarker(state));
|
|
await writeReceipt(state, 'launch.json', receipt); return 0;
|
|
}
|
|
if (command === 'selection') {
|
|
const selection = await validatePilotSelection(api, env, context, context.workflowId);
|
|
if (context.catalog === 'full') selection.cases = selection.cases.map(compactCase);
|
|
await writeReceipt(state, 'selection.json', selection);
|
|
const bytes = Buffer.from(JSON.stringify(selection, null, 2));
|
|
await writeFile(path.join(env.GITHUB_WORKSPACE, 'testops-pilot-selection.json'), bytes, { flag: 'wx', mode: 0o600 });
|
|
await output(env, 'selection_sha256', hash(bytes));
|
|
await output(env, 'execution_ids', JSON.stringify(selection.cases.map(row => row.executionId)));
|
|
const matrix = executionMatrix(selection, context, env.ALLURE_JOB_RUN_ID);
|
|
await output(env, 'matrix', JSON.stringify(matrix));
|
|
if (context.catalog === 'full') {
|
|
const partition = executionPartition(selection, context, env.ALLURE_JOB_RUN_ID);
|
|
for (const name of ['native', 'external']) {
|
|
const { include } = partition[name];
|
|
await output(env, `${name}_matrix`, JSON.stringify({ include })); await output(env, `${name}_count`, String(include.length));
|
|
}
|
|
await output(env, 'parallelism', partition.parallelism);
|
|
}
|
|
await output(env, 'policy', JSON.stringify(context.catalog === 'full' ? null : evidencePolicy(selection, context, env.ALLURE_JOB_RUN_ID))); return 0;
|
|
}
|
|
const selected = await readSelection(path.join(env.GITHUB_WORKSPACE, 'chat-completion-testops-selection', 'testops-pilot-selection.json'), env, context);
|
|
if (command === 'upload-case') {
|
|
const prepared = JSON.parse(await readOwned(env.CHAT_TESTOPS_PREPARED_FILE, env.RUNNER_TEMP, 65_536));
|
|
const publication = await publishCaseBundle(api, path.resolve(argument), env.ALLURE_RESULTS, prepared, context, selected,
|
|
env.CHAT_TESTOPS_EXECUTION_ID, env.EXPECTED_SELECTION_SHA256, () => mutationMarker(state)('upload_case'));
|
|
await writeReceipt(state, 'publication.json', publication);
|
|
return publication.exported.status === 'passed' ? 0 : 1;
|
|
}
|
|
if (command === 'verify-stream') {
|
|
const exported = await publicationSet(env.CHAT_TESTOPS_BUNDLES, env.CHAT_TESTOPS_PUBLICATIONS, context, selected, env.EXPECTED_SELECTION_SHA256);
|
|
const { campaign } = await readPilotExport(path.resolve(argument), context, selected);
|
|
if (exported.some(row => {
|
|
const original = campaign.results.find(value => value.executionId === row.executionId);
|
|
return !original || row.status !== (original.status === 'passed' && original.cleanup === 'passed' ? 'passed' : 'failed') || row.cleanup !== original.cleanup;
|
|
})) reject('upload_receipt');
|
|
const verification = await verifyPilotUpload(api, selected, context, exported);
|
|
await writeReceipt(state, 'verification.json', verification);
|
|
return exported.some(row => row.status !== 'passed') ? 1 : 0;
|
|
}
|
|
if (command === 'upload') {
|
|
const exported = await exportPilotAllure(path.resolve(argument), env.ALLURE_RESULTS, context, selected);
|
|
await writeReceipt(state, 'exported.json', exported); return exported.some(row => row.status !== 'passed') ? 1 : 0;
|
|
}
|
|
const prior = path.join(env.RUNNER_TEMP, 'chat-completion-testops-upload', 'exported.json');
|
|
const exported = JSON.parse(await readOwned(prior, env.RUNNER_TEMP, context.catalog === 'full' ? 262_144 : 32_768));
|
|
const { campaign } = await readPilotExport(path.resolve(argument), context, selected);
|
|
if (exported.some(row => {
|
|
const original = campaign.results.find(value => value.executionId === row.executionId);
|
|
return !original || row.status !== (original.status === 'passed' && original.cleanup === 'passed' ? 'passed' : 'failed') || row.cleanup !== original.cleanup;
|
|
})) reject('upload_receipt');
|
|
const verification = await verifyPilotUpload(api, selected, context, exported);
|
|
await writeReceipt(state, 'verification.json', verification); return 0;
|
|
} catch (error) {
|
|
const code = fullCatalogFailureCodes.has(error?.message)
|
|
|| /^(chat_completion_testops_[a-z_]{1,80}|testops_(?:api|auth)_http(?:_[1-5][0-9]{2})?)$/.test(error?.message ?? '') ? error.message : 'chat_completion_testops_unknown';
|
|
const receipt = { command, code, httpStatus: Number.isInteger(error?.status) ? error.status : null };
|
|
if (code === 'chat_completion_testops_stream_upload_uncertain'
|
|
&& ['testops_api_http', 'testops_response_invalid', 'testops_response_too_large', 'transport_or_response_failure'].includes(error.uploadFailure?.kind)) {
|
|
receipt.uploadFailure = { kind: error.uploadFailure.kind,
|
|
httpStatus: Number.isInteger(error.uploadFailure.httpStatus) ? error.uploadFailure.httpStatus : null };
|
|
}
|
|
if (state) await writeReceipt(state, 'failure.json', receipt).catch(() => {});
|
|
process.stderr.write(`Chat completion TestOps stopped: ${JSON.stringify(receipt)}\n`); return 1;
|
|
}
|
|
}
|
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
run(process.argv[2], process.argv[3]).then(code => { process.exitCode = code; });
|
|
}
|