mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 11:01:16 +01:00
## Thinking Path > - Paperclip manages AI agents and their work. > - The private chat catalog tests real user requests through TestOps and AWS. > - Each test must bind its immutable candidate and private evidence owner before it receives model credentials. > - The previous source pin changed the candidate launcher but kept the old launcher digest in the full-catalog evidence policy. > - The private guard rejected the 23-case launch before any original test or model-key step began. > - This pull request binds the full-catalog launcher to its reviewed candidate and selects the next reviewed repair source. > - The result is an explicit source contract for new measurements, with the old grades and privacy gates preserved. ## Linked Issues or Issue Description Refs #18721. Related: #18710 and #18717. The core guidance PR #18717 remains unmerged. A duplicate search found no open PR for this launcher binding defect. **What happened?** The full-catalog evidence policy used the legacy pilot launcher digest after the candidate launcher changed. All first eight case jobs failed the private binding step. Their original Execute and model-key steps were skipped. The operator cancelled the remaining jobs once. This was an operational failure, not a feature result. **Expected behavior** Every singleton policy must use the launcher digest from the same reviewed manifest as the immutable full-catalog target. A final source check must compare the digest and private protocol files with the exact Git target before submission. **Steps to reproduce** Compare the old evidencePolicy digest with launch.ts from target36dff2e. The old owning regression fails. The corrected full-lane policy emits the digest of launch.ts from target43c44ccc. The legacy pilot digest remains unchanged. ## What Changed - Pin the full catalog to candidate `43c44ccc823a8c972b247fa7159281887e6f5255` and its exact launcher digest. Keep the strict private prepare guard and pilot digest unchanged. - Bind the actual 242-row candidate descriptor snapshot and the explicit changed definition fields. Preserve all 244 case IDs, aliases and original definition provenance. - Select the reviewed source repairs for skill import guidance, immutable skill versions, app review navigation, authorized app testing, adapter prerequisites, the isolated announcement fixture and bounded diagnostics. These are new candidate measurements. - Record the one native-accounting change: Studio144 has a 2..3 envelope and requires three genuine settled source-bound turns. Its query, model, human count and 540-second case limit stay fixed. All other native bounds and all original case clocks stay fixed. - Keep the SDK scanner, compiler/parser identities and canary certificates unchanged. The public SDK source-input registry is synchronized; this does not certify the runtime or release held SDK cases. - Update the current-target documentation and preserve dated source history. ## Verification - The old full-catalog policy fails the new regression with the stale launcher digest. The corrected policy passes for all 244 singleton declarations. - Current CI tests hash the checked exact candidate launcher source bytes and bind their target and Git blob OID. The previous test accepted an advanced target with a stale launcher; the corrected test rejects that mutation. The candidate remains unchanged. - 62 focused Node controls pass across the manifest, API adapter, reconciliation and trusted contract owners. - The source-bound final-GO helper reads the exact Git target launcher and all three private protocol files. It verifies every singleton policy against those bytes. All 244 pass. - The unchanged actual private prepare command passes with the exact candidate bytes. Mutated launcher bytes, a missing launcher and a wrong-target policy fail before outputs. No service or provider credentials are passed in these controls. - Candidate verification: 294 pure controls, eight Git/FS updater controls, actual 242 factory declarations with zero fixture callbacks, and 42 admission/legacy controls pass. SDK registry check reports no change on the final candidate. - Prior head6bda80f passed PR CI41577494646 on attempt1:48 jobs passed and1 intentionally skipped. Both required checks and Greptile5/5 passed. Two late review findings then required declaration synchronization and removal of an unused exported helper. Final head09719566 then passed PR CI41579310890 on attempt1:48 jobs passed and1 intentionally skipped. Both required checks and exact-head Greptile5/5 pass. The whole ten-file diff received independent review after both late findings were fixed. - One actual TypeScript declaration consumer reproduces missing-export/property errors on the prior declarations and passes after synchronization (installed TypeScript6.0.3; isolated consumer only). The finalGO keeps direct exact-source comparisons; no unused guard export remains. - Local full graph typecheck/build and native browser/model execution were not run for this trusted metadata change. Workspace dependency output prevents a clean local full typecheck claim. Latest-head CI is required. No new feature PASS is claimed from these controls. ## Risks - This advances a private evaluation candidate. It does not merge the candidate's core guidance changes to master. Existing result history and the catalog qualification gate remain unchanged. - The new Studio144 accounting and app492 context/outcome are explicit changed conditions. They must not be credited as passes under the original definitions. - SDK input synchronization preserves all scanner guards. Runtime canary certificates still need real qualification; the other five SDK cases remain held. - The isolated announcement fixture contains one public synthetic JSON object. Production announcement feeds and the global index are unchanged. - The cancelled 23-case launch must be reported as operational BROKEN rows with unknown original grades. It must not be retried under the unchanged source. ## Model Used OpenAI GPT-6.1 Sol (`gpt-6.1-sol`) assisted with source integration, code, tests and review. The model used tool execution and repository inspection. The host did not report the context-window size. ## 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>
92 lines
6.9 KiB
JavaScript
92 lines
6.9 KiB
JavaScript
import { createHash } from 'node:crypto';
|
|
import { fullCatalogCases, fullCatalogPlanName, catalogTargetSha, catalogLauncherSha256 } from './chat-full-catalog-testops-manifest.mjs';
|
|
|
|
// The catalog remains on its preservation/integration branch. Updating this
|
|
// immutable source pin requires a reviewed source change.
|
|
export const targetSha = 'de0941835cc8f0d01c982574ee258e110babdbeb';
|
|
export const launchSha256 = 'f2e69957c350b220b06edca49194a4bd07d70bc882f80838eeb7203184c0bd0d';
|
|
export const workflow = '.github/workflows/chat-completion-testops.yml';
|
|
export const planName = 'Chat completion pilot — OpenCode 1017';
|
|
export const model = 'openrouter/deepseek/deepseek-v4.1-flash-1017';
|
|
export const catalogWorkflow = '.github/workflows/chat-catalog-testops.yml';
|
|
export const cases = Object.freeze([
|
|
{ key: 'control', scenario: 'instance-operations.restart-service', name: 'Restart an instance and retain the conversation', checks: 9, minimumRuns: 1, maximumRuns: 1 },
|
|
{ key: 'defaults', scenario: 'projects.project-defaults', name: 'Save project defaults for future work', checks: 11, minimumRuns: 3, maximumRuns: 3 },
|
|
{ key: 'work', scenario: 'projects.project-work', name: 'Create and delegate project work through chat', checks: 15, minimumRuns: 1, maximumRuns: 3 },
|
|
].map(row => Object.freeze({ ...row, executionId: `chat-features.runner-opencode.local.${row.scenario}`,
|
|
fullName: `paperclip.Product E2E.chat-features.${row.scenario}` })));
|
|
export const positive = value => Number.isSafeInteger(value) && value > 0;
|
|
export function catalogDefinition(context = {}) {
|
|
if (context.catalog === 'full') return { cases: fullCatalogCases, planName: fullCatalogPlanName,
|
|
targetSha: catalogTargetSha, launchSha256: catalogLauncherSha256, workflow: catalogWorkflow, bindingKey: 'CHAT_FULL_CATALOG_TESTOPS_BINDING' };
|
|
if (context.catalog !== undefined) reject('catalog');
|
|
return { cases, planName, targetSha, workflow, bindingKey: 'CHAT_COMPLETION_TESTOPS_BINDING' };
|
|
}
|
|
export const hash = value => createHash('sha256').update(value).digest('hex');
|
|
// These bounds are copied from the pinned source, not inferred from one
|
|
// measurement. The real-source control checks them before provider access.
|
|
export const validRunCount = (ids, descriptor) => Array.isArray(ids) && ids.length >= descriptor.minimumRuns
|
|
&& ids.length <= descriptor.maximumRuns && new Set(ids).size === ids.length;
|
|
const numeric = value => /^[1-9][0-9]{0,15}$/.test(value ?? '') && positive(Number(value));
|
|
export function reject(code) { throw new Error(`chat_completion_testops_${code}`); }
|
|
|
|
export function admitDispatch(env, event, triggeringActorId) {
|
|
let allowed;
|
|
try { allowed = JSON.parse(env.TESTOPS_ALLOWED_ACTOR_IDS); } catch { reject('actor'); }
|
|
const branch = event.repository?.default_branch;
|
|
const operation = event.inputs?.operation ?? 'evaluate';
|
|
const full = env.GITHUB_WORKFLOW_REF === `paperclipai/paperclip/${catalogWorkflow}@refs/heads/master`;
|
|
const definition = catalogDefinition(full ? { catalog: 'full' } : {});
|
|
if (env.GITHUB_ACTIONS !== 'true' || env.GITHUB_EVENT_NAME !== 'workflow_dispatch'
|
|
|| env.GITHUB_REPOSITORY !== 'paperclipai/paperclip' || event.repository?.full_name !== 'paperclipai/paperclip'
|
|
|| event.repository?.private !== false || branch !== 'master' || env.GITHUB_REF !== 'refs/heads/master'
|
|
|| event.ref !== env.GITHUB_REF || env.GITHUB_WORKFLOW_REF !== `paperclipai/paperclip/${definition.workflow}@refs/heads/master`
|
|
|| !/^[a-f0-9]{40}$/.test(env.GITHUB_SHA ?? '') || env.GITHUB_RUN_ATTEMPT !== '1'
|
|
|| !numeric(env.GITHUB_RUN_ID) || !numeric(env.GITHUB_ACTOR_ID) || event.sender?.id !== Number(env.GITHUB_ACTOR_ID)
|
|
|| !Array.isArray(allowed) || !allowed.length || allowed.some(v => !positive(v))
|
|
|| !allowed.includes(Number(env.GITHUB_ACTOR_ID)) || !allowed.includes(triggeringActorId)
|
|
|| !event.inputs || Object.keys(event.inputs).some(k => !['operation', 'ALLURE_JOB_RUN_ID', 'ALLURE_USERNAME'].includes(k))
|
|
|| !['bootstrap', 'evaluate', 'launch-plan'].includes(operation)
|
|
|| full && operation === 'launch-plan'
|
|
|| (operation === 'evaluate' ? !numeric(event.inputs.ALLURE_JOB_RUN_ID) : (event.inputs.ALLURE_JOB_RUN_ID ?? '') !== '')) reject('dispatch');
|
|
return Object.freeze({ runId: env.GITHUB_RUN_ID, attempt: '1', workflowSha: env.GITHUB_SHA, targetSha: definition.targetSha, operation,
|
|
...(full ? { catalog: 'full' } : {}),
|
|
actorId: Number(env.GITHUB_ACTOR_ID), triggeringActorId });
|
|
}
|
|
|
|
export function selectedCases(rows, context = {}) {
|
|
const { cases } = catalogDefinition(context);
|
|
if (!Array.isArray(rows) || !rows.length || rows.length > cases.length || new Set(rows.map(row => row.executionId)).size !== rows.length) reject('selection');
|
|
const selected = cases.filter(row => rows.some(value => value.executionId === row.executionId));
|
|
if (selected.length !== rows.length) reject('selection');
|
|
for (const row of rows) {
|
|
const expected = selected.find(value => value.executionId === row.executionId);
|
|
if (!positive(row.caseId) || row.fullName !== expected.fullName || row.key !== expected.key || row.scenario !== expected.scenario
|
|
|| row.registeredFullName !== undefined && ![expected.fullName, ...(expected.aliases ?? [])].includes(row.registeredFullName)) reject('selection');
|
|
}
|
|
if (new Set(rows.map(row => row.caseId)).size !== rows.length) reject('selection');
|
|
return selected.map(expected => {
|
|
const actual = rows.find(row => row.executionId === expected.executionId);
|
|
return { ...expected, caseId: actual.caseId, ...(actual.registeredFullName === undefined ? {} : { registeredFullName: actual.registeredFullName }) };
|
|
});
|
|
}
|
|
|
|
export function validateSelection(selection, context, jobRunId) {
|
|
const definition = catalogDefinition(context);
|
|
if (selection.schema !== 'paperclip.chat-completion-testops-selection/v1' || selection.projectId !== 1
|
|
|| selection.runId !== context.runId || selection.attempt !== '1' || selection.workflowSha !== context.workflowSha
|
|
|| selection.targetSha !== definition.targetSha || selection.catalog !== context.catalog || selection.allureJobRunId !== jobRunId || !numeric(jobRunId)
|
|
|| ['planId', 'jobId', 'launchId'].some(key => !positive(selection[key]))) reject('selection_binding');
|
|
return selectedCases(selection.cases, context);
|
|
}
|
|
|
|
// This authority is emitted only by the trusted main-only selector after an
|
|
// authenticated TestOps selection. The existing evidence validator, protocol,
|
|
// scanners, byte limits and exact-key S3 policies remain the enforcement owner.
|
|
export function evidencePolicy(selection, context, jobRunId) {
|
|
const selected = validateSelection(selection, context, jobRunId);
|
|
return { version: 1, approvals: [{ workflowSha: context.workflowSha, targetSha: catalogDefinition(context).targetSha,
|
|
runId: context.runId, attempt: '1', executionIds: selected.map(row => row.executionId),
|
|
protocol: 'paperclip.manual-private-evidence/v1', launchSha256: context.catalog === 'full' ? catalogDefinition(context).launchSha256 : launchSha256 }] };
|
|
}
|