mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-10 12:37:55 +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>
177 lines
11 KiB
JavaScript
177 lines
11 KiB
JavaScript
import { createHash } from 'node:crypto';
|
|
import { catalogTargetSha, fullCatalogCases, fullCatalogPlanName } from './chat-full-catalog-testops-manifest.mjs';
|
|
|
|
const positive = value => Number.isSafeInteger(value) && value > 0;
|
|
const fail = code => { throw new Error(`chat_full_catalog_testops_${code}`); };
|
|
const equal = (left, right) => JSON.stringify(left) === JSON.stringify(right);
|
|
const metadataFields = ['name', 'automated', 'description', 'precondition', 'expectedResult'];
|
|
const marker = (operation, key) => `${operation}_${createHash('sha256').update(key).digest('hex').replace(/[0-9a-f]/g, value => String.fromCharCode(97 + parseInt(value, 16)))}`;
|
|
|
|
function manifestIndex() {
|
|
if (!Array.isArray(fullCatalogCases) || fullCatalogCases.length !== 244
|
|
|| !/^[a-f0-9]{40}$/.test(catalogTargetSha) || !fullCatalogPlanName) fail('manifest');
|
|
const names = new Map(), keys = new Set();
|
|
for (const row of fullCatalogCases) {
|
|
if (!row.key || keys.has(row.key) || row.key !== row.canonicalScenario || !row.fullName
|
|
|| !Array.isArray(row.aliases) || !row.name || !row.originalQuery || !row.originalContext || !row.expectedOutcome
|
|
|| !Array.isArray(row.capabilities) || !Array.isArray(row.surface)
|
|
|| !Array.isArray(row.coverageGaps) || !Array.isArray(row.requiredCredentials)) fail('manifest');
|
|
keys.add(row.key);
|
|
for (const name of [row.fullName, ...row.aliases]) {
|
|
if (typeof name !== 'string' || !name || names.has(name)) fail('manifest_alias');
|
|
names.set(name, row);
|
|
}
|
|
}
|
|
return names;
|
|
}
|
|
|
|
function desiredMetadata(row) {
|
|
const gaps = row.coverageGaps.length ? row.coverageGaps.join('\n') : 'No coverage gap is declared in the source metadata; live qualification is separate.';
|
|
const requirements = [
|
|
`Full-catalog lane targets candidate ${catalogTargetSha}; other lanes keep their own actual source; target is recorded per result. This registration does not qualify current master or record a PASS.`,
|
|
`Assumed context:\n${row.originalContext}`,
|
|
`Required credential names: ${row.requiredCredentials.join(', ') || 'None declared for this case; the controller profile credential is separate.'}`,
|
|
...(row.optionalCompanySecretBindingApproval ? ['Company secret binding uses two base human inputs plus at most one source-bound, accepted UI approval (maximum three actual human inputs). Native runs: minimum two, maximum three; original 480000ms attempt remains.'] : []),
|
|
`External input contract: ${row.externalInput == null ? 'None declared.' : JSON.stringify(row.externalInput)}`,
|
|
`Installed-source requirement: ${row.installedSource ?? 'None declared.'}`,
|
|
`Source evidence kind: ${row.evidenceKind}`,
|
|
`Source screenshot carrier: ${row.screenshot ?? 'None declared.'}`,
|
|
`Source evidence readiness: ${row.evidenceReadiness}`,
|
|
`Coverage gaps (the original oracle remains unchanged):\n${gaps}`,
|
|
];
|
|
return {
|
|
name: row.name,
|
|
automated: true,
|
|
description: [
|
|
`Canonical chat scenario: ${row.canonicalScenario}`,
|
|
`Full-catalog lane targets candidate ${catalogTargetSha}; other lanes keep their own actual source; target is recorded per result. Executable registration is not a result or a qualification claim.`,
|
|
`Human request:\n${row.originalQuery}`,
|
|
`Capabilities: ${row.capabilities.join(', ') || 'None declared.'}`,
|
|
`Surfaces: ${row.surface.join(', ') || 'None declared.'}`,
|
|
`Native OpenCode / OpenRouter DeepSeek1017. Original source bounds and evidence gates apply.`,
|
|
`Accounted work runs: minimum ${row.minimumRuns ?? 'unknown'}, maximum ${row.maximumRuns ?? 'unknown'}. Turn limit: ${row.turnTimeoutMs} ms. Attempt limit: ${row.attemptTimeoutMs} ms.`,
|
|
`Coverage gaps:\n${gaps}`,
|
|
].join('\n\n'),
|
|
precondition: requirements.join('\n\n'),
|
|
expectedResult: row.expectedOutcome,
|
|
};
|
|
}
|
|
|
|
function caseMetadata(actual) {
|
|
return Object.fromEntries(metadataFields.map(field => [field, actual[field]]));
|
|
}
|
|
|
|
function assertCase(actual, row, caseId, registeredFullName) {
|
|
if (actual?.id !== caseId || actual.projectId !== 1 || actual.deleted === true
|
|
|| typeof actual.automated !== 'boolean' || actual.fullName !== registeredFullName
|
|
|| ![row.fullName, ...row.aliases].includes(actual.fullName)) fail('case_identity');
|
|
}
|
|
|
|
function assertSelection(plan, allowedIds, exact = false) {
|
|
const selection = plan.treeSelection;
|
|
if (!positive(plan.id) || plan.projectId !== 1 || plan.name !== fullCatalogPlanName
|
|
|| !selection || selection.inverted !== false || !Array.isArray(selection.leafsInclude)
|
|
|| selection.leafsInclude.some(value => !positive(value) || !allowedIds.has(value))
|
|
|| new Set(selection.leafsInclude).size !== selection.leafsInclude.length
|
|
|| !Number.isSafeInteger(plan.testCasesCount) || plan.testCasesCount !== selection.leafsInclude.length
|
|
|| ['groupsInclude', 'groupsExclude', 'leafsExclude', 'path'].some(key => selection[key] != null
|
|
&& (!Array.isArray(selection[key]) || selection[key].length !== 0))
|
|
|| plan.baseRql != null && plan.baseRql !== ''
|
|
|| exact && selection.leafsInclude.length !== allowedIds.size) fail('plan_scope');
|
|
}
|
|
|
|
export async function inspectFullCatalog(api) {
|
|
const names = manifestIndex(), seen = new Set(), found = new Map();
|
|
let totalPages, totalElements;
|
|
for (let page = 0; page < 10; page++) {
|
|
const listed = await api(`/api/testcase?${new URLSearchParams({ projectId: '1', page: String(page), size: '100', sort: 'id,asc' })}`);
|
|
if (!Array.isArray(listed?.content) || listed.content.length > 100 || listed.number !== page
|
|
|| !Number.isSafeInteger(listed.totalPages) || listed.totalPages < 0 || listed.totalPages > 10
|
|
|| !Number.isSafeInteger(listed.totalElements) || listed.totalElements < 0 || listed.totalElements > 1000
|
|
|| listed.totalPages !== Math.ceil(listed.totalElements / 100)
|
|
|| listed.content.length !== Math.min(100, Math.max(0, listed.totalElements - page * 100))
|
|
|| totalPages !== undefined && (totalPages !== listed.totalPages || totalElements !== listed.totalElements)) fail('catalog_page');
|
|
totalPages = listed.totalPages; totalElements = listed.totalElements;
|
|
const ids = listed.content.map(value => {
|
|
if (!positive(value.id) || seen.has(value.id)) fail('catalog_identity');
|
|
seen.add(value.id); return value.id;
|
|
});
|
|
for (let offset = 0; offset < ids.length; offset += 4) {
|
|
const batch = ids.slice(offset, offset + 4);
|
|
const actuals = await Promise.all(batch.map(caseId => api(`/api/testcase/${caseId}`)));
|
|
for (const [index, actual] of actuals.entries()) {
|
|
if (actual?.id !== batch[index] || actual.projectId !== 1
|
|
|| actual.fullName != null && typeof actual.fullName !== 'string') fail('catalog_case');
|
|
const row = names.get(actual.fullName);
|
|
if (!row) continue;
|
|
assertCase(actual, row, actual.id, actual.fullName);
|
|
if (found.has(row.key)) fail('case_alias_conflict');
|
|
found.set(row.key, { ...row, caseId: actual.id, registeredFullName: actual.fullName, currentMetadata: caseMetadata(actual) });
|
|
}
|
|
}
|
|
if (page + 1 >= totalPages) break;
|
|
}
|
|
if (seen.size !== totalElements) fail('catalog_incomplete');
|
|
const listedPlans = await api(`/api/testplan?${new URLSearchParams({ projectId: '1', name: fullCatalogPlanName, page: '0', size: '100' })}`);
|
|
if (!Array.isArray(listedPlans?.content) || listedPlans.content.length > 100
|
|
|| !Number.isSafeInteger(listedPlans.totalPages) || listedPlans.totalPages < 0 || listedPlans.totalPages > 1
|
|
|| !Number.isSafeInteger(listedPlans.totalElements) || listedPlans.totalElements !== listedPlans.content.length) fail('plan_page');
|
|
const plans = listedPlans.content.filter(plan => plan.name === fullCatalogPlanName);
|
|
if (plans.length > 1) fail('plan_conflict');
|
|
const cases = fullCatalogCases.flatMap(row => found.has(row.key) ? [found.get(row.key)] : []);
|
|
if (plans.length) {
|
|
if (!positive(plans[0].id) || plans[0].projectId !== 1) fail('plan_identity');
|
|
const actual = await api(`/api/testplan/${plans[0].id}`);
|
|
if (actual?.id !== plans[0].id) fail('plan_identity');
|
|
assertSelection(actual, new Set(cases.map(row => row.caseId)));
|
|
plans[0] = actual;
|
|
}
|
|
return { projectId: 1, catalogCount: totalElements, cases, plans };
|
|
}
|
|
|
|
export async function reconcileFullCatalog(api, markOnce) {
|
|
if (typeof markOnce !== 'function') fail('mutation_marker');
|
|
const inspected = await inspectFullCatalog(api), existing = new Map(inspected.cases.map(row => [row.key, row]));
|
|
const reconciled = [];
|
|
for (const row of fullCatalogCases) {
|
|
const wanted = desiredMetadata(row);
|
|
let bound = existing.get(row.key);
|
|
if (!bound) {
|
|
await markOnce(marker('create_case', row.key));
|
|
const created = await api('/api/testcase', 'POST', { projectId: 1, fullName: row.fullName, ...wanted });
|
|
if (!positive(created?.id) || reconciled.some(value => value.caseId === created.id)) fail('created_case');
|
|
const actual = await api(`/api/testcase/${created.id}`);
|
|
assertCase(actual, row, created.id, row.fullName);
|
|
if (!equal(caseMetadata(actual), wanted)) fail('case_metadata');
|
|
bound = { ...row, caseId: actual.id, registeredFullName: actual.fullName };
|
|
} else if (!equal(bound.currentMetadata, wanted)) {
|
|
await markOnce(marker('patch_case', row.key));
|
|
await api(`/api/testcase/${bound.caseId}`, 'PATCH', wanted);
|
|
const actual = await api(`/api/testcase/${bound.caseId}`);
|
|
assertCase(actual, row, bound.caseId, bound.registeredFullName);
|
|
if (!equal(caseMetadata(actual), wanted)) fail('case_metadata');
|
|
}
|
|
reconciled.push({ ...row, caseId: bound.caseId, registeredFullName: bound.registeredFullName });
|
|
}
|
|
const checked = await inspectFullCatalog(api);
|
|
if (checked.cases.length !== fullCatalogCases.length
|
|
|| checked.cases.some((row, index) => row.caseId !== reconciled[index].caseId
|
|
|| row.registeredFullName !== reconciled[index].registeredFullName
|
|
|| !equal(row.currentMetadata, desiredMetadata(fullCatalogCases[index])))) fail('reconciliation_changed');
|
|
const selection = { inverted: false, leafsInclude: reconciled.map(row => row.caseId), groupsInclude: [], groupsExclude: [], leafsExclude: [] };
|
|
let plan = checked.plans[0];
|
|
if (!plan) {
|
|
await markOnce('create_plan');
|
|
const created = await api('/api/testplan', 'POST', { projectId: 1, name: fullCatalogPlanName, treeSelection: selection });
|
|
if (!positive(created?.id)) fail('created_plan');
|
|
plan = await api(`/api/testplan/${created.id}`);
|
|
if (plan?.id !== created.id) fail('created_plan');
|
|
} else if (!equal([...plan.treeSelection.leafsInclude].sort((a, b) => a - b), [...selection.leafsInclude].sort((a, b) => a - b))) {
|
|
await markOnce('patch_plan');
|
|
await api(`/api/testplan/${plan.id}`, 'PATCH', { treeSelection: selection });
|
|
plan = await api(`/api/testplan/${plan.id}`);
|
|
}
|
|
assertSelection(plan, new Set(selection.leafsInclude), true);
|
|
return { planId: plan.id, cases: reconciled };
|
|
}
|