mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-09 07:10:34 +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>
92 lines
6.9 KiB
JavaScript
92 lines
6.9 KiB
JavaScript
import { execFileSync } from 'node:child_process';
|
|
import { mkdtemp, readFile, realpath, rm } from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import { pathToFileURL } from 'node:url';
|
|
import { parse } from 'yaml';
|
|
import { validateCompanySecretBindingApprovalSource } from './chat-full-catalog-testops-manifest.mjs';
|
|
import { model, catalogDefinition, catalogWorkflow } from './chat-completion-testops-contract.mjs';
|
|
|
|
export function paidWorker(definition, called) {
|
|
const path = './.github/workflows/chat-catalog-testops-case.yml';
|
|
for (const name of ['chat', 'chat_external']) {
|
|
const job = definition.jobs?.[name];
|
|
if (job?.uses !== path || job.with?.protocol !== 'streaming-v1'
|
|
|| job.with?.target_sha !== '${{ needs.authorize.outputs.target_sha }}'
|
|
|| job.with?.selection_artifact_id !== '${{ needs.select.outputs.artifact_id }}'
|
|
|| job.with?.selection_sha256 !== '${{ needs.select.outputs.selection_sha256 }}'
|
|
|| JSON.stringify(job.secrets) !== JSON.stringify({ OPENROUTER_API_KEY: '${{ secrets.OPENROUTER_API_KEY }}',
|
|
ANTHROPIC_API_KEY: '${{ secrets.ANTHROPIC_API_KEY }}', ALLURE_TOKEN: '${{ secrets.ALLURE_TOKEN }}' })) {
|
|
throw new Error('Workflow caller boundary mismatch');
|
|
}
|
|
}
|
|
if (!called?.on?.workflow_call || Object.keys(called.on).length !== 1
|
|
|| called.jobs?.chat?.environment !== 'runner-e2e-private-paid'
|
|
|| called.jobs?.report?.environment !== 'runner-e2e-private-history'
|
|
|| called.jobs?.upload?.environment !== 'runner-e2e-testops-reporting') throw new Error('Workflow environment boundary mismatch');
|
|
return called.jobs.chat;
|
|
}
|
|
|
|
/** A credential-free caller/source control with synthetic IMDS responses.
|
|
* It does not qualify an actual AWS host or make a provider call.
|
|
*/
|
|
export async function verifyHostContract(workspace, temporaryRoot, env = process.env) {
|
|
if (env.ALLURE_TOKEN || env.OPENROUTER_API_KEY || !path.isAbsolute(workspace)) throw new Error('Credential-free source control required');
|
|
const full = env.GITHUB_WORKFLOW_REF === `paperclipai/paperclip/${catalogWorkflow}@refs/heads/master`;
|
|
const { cases, targetSha, workflow } = catalogDefinition(full ? { catalog: 'full' } : {});
|
|
const source = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: workspace, encoding: 'utf8' }).trim();
|
|
if (source !== targetSha) throw new Error('Source control pin mismatch');
|
|
const definition = parse(await readFile(new URL(`../../${workflow}`, import.meta.url), 'utf8'));
|
|
// Only the reviewed local callee at the same trusted checkout is read. A
|
|
// workflow input or another repository/ref cannot supply the worker body.
|
|
const worker = full ? paidWorker(definition, parse(await readFile(new URL('../../.github/workflows/chat-catalog-testops-case.yml', import.meta.url), 'utf8'))) : definition.jobs.chat;
|
|
const paid = worker.steps.find(step => step.env?.OPENROUTER_API_KEY);
|
|
if (paid.env.PAPERCLIP_CHAT_COMPLETION_TESTOPS !== 'v1' || paid.env.ALLURE_JOB_RUN_ID !== '${{ inputs.ALLURE_JOB_RUN_ID }}'
|
|
|| !paid.run.includes('--max-automatic-retries 0') || definition.jobs.chat.strategy['max-parallel'] !== (full ? '${{ fromJSON(needs.select.outputs.parallelism) }}' : 1)) throw new Error('Workflow host contract mismatch');
|
|
const { runnerMatrix } = await import(pathToFileURL(path.join(workspace, 'tests/runner-e2e/catalog.ts')).href);
|
|
const { prepareChatRemoteHost } = await import(pathToFileURL(path.join(workspace, 'tests/runner-e2e/chat-remote-host.ts')).href);
|
|
const root = await realpath(await mkdtemp(path.join(temporaryRoot, 'chat-completion-host-control-')));
|
|
try {
|
|
for (const descriptor of cases) {
|
|
const execution = runnerMatrix.find(row => row.id === descriptor.executionId);
|
|
if (!execution || execution.task.id !== descriptor.scenario || execution.profile.model !== model
|
|
|| execution.profile.id !== 'runner-opencode' || execution.environment.id !== 'local'
|
|
|| execution.task.expectedRunCount !== descriptor.maximumRuns
|
|
|| (execution.task.minimumExpectedRunCount ?? execution.task.expectedRunCount) !== descriptor.minimumRuns
|
|
|| full && (execution.task.turnTimeoutMs !== descriptor.turnTimeoutMs
|
|
|| execution.task.attemptTimeoutMs.local !== descriptor.attemptTimeoutMs
|
|
|| execution.suite.definitionMetadata?.companyAndAgentBudgetCents !== 500
|
|
|| execution.suite.definitionMetadata?.maximumAttemptsPerCell !== 1)) throw new Error('Pinned source selection mismatch');
|
|
if (full) validateCompanySecretBindingApprovalSource(descriptor, execution.task);
|
|
const controlEnv = { GITHUB_ACTIONS: 'true', GITHUB_REPOSITORY: 'paperclipai/paperclip', GITHUB_REF: 'refs/heads/master',
|
|
GITHUB_WORKFLOW_REF: `paperclipai/paperclip/${workflow}@refs/heads/master`, GITHUB_RUN_ID: '1', GITHUB_RUN_ATTEMPT: '1',
|
|
PAPERCLIP_RUNNER_E2E_SOURCE_SHA: targetSha, PAPERCLIP_CHAT_COMPLETION_TESTOPS: paid.env.PAPERCLIP_CHAT_COMPLETION_TESTOPS,
|
|
ALLURE_JOB_RUN_ID: '1' };
|
|
let calls = 0;
|
|
const dependencies = { platform: 'linux', arch: 'x64', sourceRevision: async () => source,
|
|
fetch: async url => {
|
|
calls++;
|
|
if (String(url) === 'http://169.254.169.254/latest/api/token') return new Response('synthetic-imds-control');
|
|
if (String(url) === 'http://169.254.169.254/latest/meta-data/instance-life-cycle') return new Response('spot');
|
|
if (String(url) === 'http://169.254.169.254/latest/dynamic/instance-identity/document') return Response.json({
|
|
instanceId: 'i-1234567890abcdef0', imageId: 'ami-1234567890abcdef0', region: 'us-east-1', architecture: 'x86_64' });
|
|
throw new Error('Unexpected control request');
|
|
} };
|
|
const input = { executions: [execution], env: controlEnv, repositoryRoot: workspace, outputDirectory: root, dependencies };
|
|
await prepareChatRemoteHost(input);
|
|
const receipt = JSON.parse(await readFile(path.join(root, 'chat-remote-host.json'), 'utf8'));
|
|
if (calls !== 3 || receipt.workflowRef !== controlEnv.GITHUB_WORKFLOW_REF || receipt.testOps?.allureJobRunId !== '1'
|
|
|| receipt.testOps.admission !== 'v1' || receipt.evaluationAdmitted !== false) throw new Error('Host audit contract mismatch');
|
|
let rejected = false;
|
|
try { await prepareChatRemoteHost({ ...input, executions: [{ ...execution, task: { ...execution.task, id: 'foreign' } }] }); }
|
|
catch { rejected = true; }
|
|
if (!rejected || calls !== 3) throw new Error('Host task identity contract mismatch');
|
|
}
|
|
return { fixtureOnly: true, liveHostQualified: false, providerCalls: 0, source, cases: cases.length };
|
|
} finally { await rm(root, { recursive: true, force: true }); }
|
|
}
|
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
verifyHostContract(process.env.GITHUB_WORKSPACE, process.env.RUNNER_TEMP)
|
|
.then(value => process.stdout.write(`${JSON.stringify(value)}\n`))
|
|
.catch(() => { process.stderr.write('Workflow/source host control failed before provider access\n'); process.exitCode = 1; });
|
|
}
|