mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-06 21:05:21 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Paperclip supports direct adapters and an experimental native Runner path. > - These paths need one stable compatibility matrix. > - The matrix must not launch providers or load credentials during normal tests. > - Result handling must reject incomplete output and sensitive values. > - This pull request adds a credential-free acceptance foundation. > - The benefit is a reviewable contract for later end-to-end executors. ## Linked Issues or Issue Description **What existing behavior does this improve?** This improves verification for direct adapters and Paperclip Runner providers. **Subsystem affected** Cross-cutting test infrastructure for adapters, the server runtime, and the task thread. **Current behavior** The repository has subsystem tests. It does not have one declarative matrix for direct and native compatibility. **Proposed behavior** Add a pure acceptance catalog, result validator, redaction helpers, and failure classification. Keep all execution authority outside this change. **Reason and benefit** The matrix makes legacy isolation and native recovery requirements explicit. The helpers let later executors report safe and complete results. **Breaking changes** None. This change does not alter production runtime selection or start any provider. ## What Changed - Add a catalog for built-in direct adapters and qualified native provider profiles. - Add compatibility cases for runtime selection, task threads, questions, and flag-change recovery. - Add pure redaction and transient-failure classification helpers. - Add fail-closed Markdown and JUnit report aggregation. - Add isolated test and type-check commands. - Document the credential-free boundary and deferred live execution work. ## Verification GitHub Actions must run: - `pnpm test:runner-acceptance` - `pnpm test:runner-acceptance:typecheck` - The repository test, type-check, build, policy, and security gates. No local test command was run. The repository owner requested GitHub-only verification. ## Risks Low production risk. The change adds test-only files and root scripts. The catalog can drift when a built-in adapter changes. Its validation fails closed on that drift. ## Model Used OpenAI Codex with the GPT-5 agent model. The work used high reasoning, repository inspection, tool use, and parallel code review. ## 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 - [ ] 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 - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge
119 lines
3.4 KiB
TypeScript
119 lines
3.4 KiB
TypeScript
import { redactDiagnosticText } from "../../packages/adapter-utils/src/command-redaction.js";
|
|
|
|
const SECRET_SHAPES = [
|
|
/\bsk-ant-[A-Za-z0-9_-]{16,}\b/g,
|
|
/\bsk-(?:proj-)?[A-Za-z0-9_-]{16,}\b/g,
|
|
/\bopenrouter[-_]?(?:api)?[-_]?key["'=:\s]+[A-Za-z0-9._-]{12,}\b/gi,
|
|
] as const;
|
|
const SENSITIVE_KEY = /(?:authorization|api.?key|token|password|secret|credential|private.?key)/i;
|
|
|
|
export function normalizedSensitiveValues(
|
|
values: readonly (string | undefined)[],
|
|
) {
|
|
return [...new Set(
|
|
values
|
|
.map((value) => value?.trim())
|
|
.filter((value): value is string => Boolean(value)),
|
|
)].sort((left, right) => right.length - left.length);
|
|
}
|
|
|
|
function redactKnownValuesAndShapes(
|
|
value: string,
|
|
sensitiveValues: readonly string[],
|
|
) {
|
|
let redacted = value;
|
|
for (const sensitiveValue of normalizedSensitiveValues(sensitiveValues)) {
|
|
redacted = redacted.split(sensitiveValue).join("[REDACTED]");
|
|
}
|
|
for (const pattern of SECRET_SHAPES) {
|
|
pattern.lastIndex = 0;
|
|
redacted = redacted.replace(pattern, "[REDACTED_SECRET_SHAPE]");
|
|
}
|
|
return redacted;
|
|
}
|
|
|
|
export function redactText(
|
|
value: string,
|
|
sensitiveValues: readonly string[] = [],
|
|
) {
|
|
return redactKnownValuesAndShapes(
|
|
redactDiagnosticText(value, "[REDACTED]"),
|
|
sensitiveValues,
|
|
);
|
|
}
|
|
|
|
export function findSensitiveValue(
|
|
value: string | Buffer,
|
|
sensitiveValues: readonly string[] = [],
|
|
): string | null {
|
|
const text = Buffer.isBuffer(value) ? value.toString("utf8") : value;
|
|
for (const sensitiveValue of normalizedSensitiveValues(sensitiveValues)) {
|
|
if (text.includes(sensitiveValue)) return "exact sensitive value";
|
|
}
|
|
for (const pattern of SECRET_SHAPES) {
|
|
pattern.lastIndex = 0;
|
|
if (pattern.test(text)) return "secret-shaped value";
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function findSensitiveJsonValue(
|
|
value: unknown,
|
|
sensitiveValues: readonly string[] = [],
|
|
): string | null {
|
|
if (typeof value === "string") return findSensitiveValue(value, sensitiveValues);
|
|
if (Array.isArray(value)) {
|
|
for (const entry of value) {
|
|
const leak = findSensitiveJsonValue(entry, sensitiveValues);
|
|
if (leak) return leak;
|
|
}
|
|
return null;
|
|
}
|
|
if (value && typeof value === "object") {
|
|
for (const [key, entry] of Object.entries(value)) {
|
|
if (
|
|
SENSITIVE_KEY.test(key)
|
|
&& entry !== null
|
|
&& entry !== undefined
|
|
&& entry !== "[REDACTED]"
|
|
&& entry !== "[REDACTED_SECRET_SHAPE]"
|
|
) {
|
|
return `sensitive field ${key}`;
|
|
}
|
|
const leak = findSensitiveJsonValue(entry, sensitiveValues);
|
|
if (leak) return leak;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function sanitizeJson(
|
|
value: unknown,
|
|
sensitiveValues: readonly string[] = [],
|
|
): unknown {
|
|
if (typeof value === "string") return redactText(value, sensitiveValues);
|
|
if (Array.isArray(value)) {
|
|
return value.map((entry) => sanitizeJson(entry, sensitiveValues));
|
|
}
|
|
if (value && typeof value === "object") {
|
|
return Object.fromEntries(
|
|
Object.entries(value).map(([key, entry]) => [
|
|
key,
|
|
SENSITIVE_KEY.test(key) && entry !== null && entry !== undefined
|
|
? "[REDACTED]"
|
|
: sanitizeJson(entry, sensitiveValues),
|
|
]),
|
|
);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
export function assertSensitiveValueFree(
|
|
value: string | Buffer,
|
|
sensitiveValues: readonly string[],
|
|
label: string,
|
|
) {
|
|
const leak = findSensitiveValue(value, sensitiveValues);
|
|
if (leak) throw new Error(`Secret leak in ${label}: ${leak}`);
|
|
}
|