mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-24 18:19:05 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Managed worktree services run isolated Paperclip instances with cloned databases. > - A reachable service was reported as ready even when its database, runtime identity, or login path was not usable. > - The first candidate added verified database seeding and managed repair in #11665. > - This pull request consolidates that candidate with signed login handoff and a complete readiness contract. > - Post-QA fixes close five defects in repair identity, repair responses, UI retry, seed journal handling, and seed-source trust. > - The benefit is a workspace that either opens safely or reports one accurate recovery action. ## Linked Issues or Issue Description No public GitHub issue exists for this work, so the problem is described here. **What happened** Managed workspace URLs could return HTTP 200 and report ready while login failed. QA also found cases where repair used the wrong instance identity, returned a generic error, left the UI stuck, rejected a safe journal lag, or trusted a mutable workspace manifest. **Expected behavior** Opening a ready workspace signs the board user in to the correct isolated instance. Provisioning and repair use a registered source and report a structured recovery state. **Actual behavior** Entry depended on a password copied into the clone. Several failure paths could publish stale readiness, hide the repair precondition, or trust state that the workspace could modify. **Additional context** This pull request includes the commits first published in #11665. That pull request keeps the original base head for review history. This consolidated pull request is the merge candidate. Related open readiness work includes #11575 and #11621. ## What Changed - Adds a short-lived, signed, single-use login ticket. It binds the user, workspace, instance, and runtime origin. - Exchanges the ticket through Better Auth. It creates the session and cookie through the supported adapter path. - Adds protected workspace readiness fields for the database, clone data, login handoff, seed phase, and runtime identity. - Fails readiness closed when the guest has no company or execution-workspace binding. - Binds ticket issuance to the exact cloned user and active company membership selected for the handoff. - Verifies every current active board identity through the exact-user handoff before publication or reuse. - Gates managed runtime publication on the readiness contract and the recorded worktree instance identity. - Refreshes runtime work products from the live runtime row after a port change. - Adds one workspace access card with ready, degraded, repairing, and failed states. - Uses the runtime response identity for repair. It returns structured repair precondition errors. - Lets a valid source journal lag converge during provisioning. - Binds seed and repair manifests to a source registered outside the agent-writable worktree. - Clears recovered UI errors so a successful retry can open the workspace. - Makes runtime tests register canonical sources and avoid ports owned by live host listeners. - Keeps Vitest on source suites when compiled `dist` trees exist. - Isolates CLI and adapter tests from ambient AWS and runtime API environment variables. - Preserves a 404 response for cross-company workspace ID lookups before runtime authorization. - Makes concurrent single-flight coverage independent of path-canonicalization scheduling order. ## Verification The following checks passed on the integrated head: ```sh pnpm -r typecheck pnpm build pnpm check:token-gates pnpm --filter @paperclipai/db check:migrations ``` - The server source lane passed 420 files and 4,953 tests. Five tests were skipped. - The CLI lane passed 57 files and 385 tests. - The database lane passed 26 files and 97 tests. - The shared package passed 58 files and 506 tests. - The adapter utility lane passed 640 tests. Four tests were skipped. - The Claude adapter passed 220 tests. One test was skipped. - The Codex adapter passed 323 tests. - The OpenClaw adapter passed 13 tests. - The OpenCode adapter passed 42 tests. - The plugin SDK passed 45 tests. - The workspace runtime suite passed 124 tests. - The caller-scoped readiness and handoff suite passed 52 tests. - The workspace provisioning shell suite passed 7 tests. - The runtime exposure suite passed 17 tests while live host mappings occupied fixed test ports. - `git diff --check` passed and the worktree is clean. The serialized route lane will run in GitHub CI with its normal shards. No deployment or active-workspace migration was performed. ## Risks - This is a medium-risk authentication and runtime-readiness change. - The login ticket uses exact origin, workspace, instance, and user binding. It has a short expiry and a one-time nonce. - Runtime publication is stricter. A real readiness, identity, per-user handoff, or control-plane database disagreement now blocks publication. - This pull request supersedes #11665 as the merge candidate. Close #11665 after this pull request merges. - No new database migration is included. The lockfile and workflow files are unchanged. - Deployment and active-workspace migration are intentionally outside this pull request. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used Claude Opus 5 (`claude-opus-5[1m]`), 1M context, extended thinking, tool use, and code execution produced the main candidate. OpenAI GPT-5 (`gpt-5`) through Codex, with agentic reasoning, tool use, and code execution, integrated the post-QA fixes and hardened the test gates. The Codex context-window size was not exposed. ## 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>
116 lines
3.3 KiB
TypeScript
116 lines
3.3 KiB
TypeScript
// Redaction for HTTP log payloads.
|
|
//
|
|
// `customProps` in logger.ts copies `req.body` / `req.params` / `req.query`
|
|
// verbatim into the 4xx/5xx log lines so operators can diagnose. That means
|
|
// Better Auth's `POST /api/auth/sign-in/email` body (which has the user's
|
|
// plaintext password) and similar payloads (sign-up, reset-password, API
|
|
// keys via Authorization header equivalents) end up on disk.
|
|
//
|
|
// This walker returns a shallow copy of the input with values for sensitive
|
|
// keys replaced with the literal string "[REDACTED]". Recurses into nested
|
|
// objects/arrays. Caps depth so a hostile or accidental cycle can't pin
|
|
// the logger.
|
|
|
|
const SENSITIVE_KEYS = new Set<string>([
|
|
"password",
|
|
"currentpassword",
|
|
"newpassword",
|
|
"passwordconfirmation",
|
|
"password_confirmation",
|
|
"passwordconfirm",
|
|
"password_confirm",
|
|
"confirmpassword",
|
|
"confirm_password",
|
|
"secret",
|
|
"client_secret",
|
|
"clientsecret",
|
|
"access_token",
|
|
"accesstoken",
|
|
"refresh_token",
|
|
"refreshtoken",
|
|
"id_token",
|
|
"idtoken",
|
|
"api_key",
|
|
"apikey",
|
|
"authorization",
|
|
"auth_token",
|
|
"authtoken",
|
|
"session_token",
|
|
"sessiontoken",
|
|
"private_key",
|
|
"privatekey",
|
|
// The Claude setup-token login fields. `browserCode` carries the one-time
|
|
// sign-in code and `authorization_code` carries the OAuth code; neither may
|
|
// reach a log line.
|
|
"browsercode",
|
|
"authorization_code",
|
|
"authorizationcode",
|
|
// The workspace login handoff ticket (PAP-17572). It is a signed bearer
|
|
// credential carried as a query parameter, so it must never reach a log line
|
|
// even though the exchange itself answers 302.
|
|
"ticket",
|
|
]);
|
|
|
|
const MAX_DEPTH = 6;
|
|
const REDACTED = "[REDACTED]";
|
|
const URLISH_KEYS = new Set<string>([
|
|
"href",
|
|
"locator",
|
|
"source",
|
|
"source_locator",
|
|
"sourcelocator",
|
|
"source_url",
|
|
"sourceurl",
|
|
"uri",
|
|
"url",
|
|
// The Claude setup-token login URL. A structured `loginUrl` that reaches a log
|
|
// sink keeps its origin and path only; the OAuth query, fragment, and any
|
|
// credentials are stripped (SR-5 backstop).
|
|
"loginurl",
|
|
"login_url",
|
|
]);
|
|
|
|
function isSensitiveKey(key: string): boolean {
|
|
return SENSITIVE_KEYS.has(key.toLowerCase());
|
|
}
|
|
|
|
function isUrlishKey(key: string): boolean {
|
|
return URLISH_KEYS.has(key.toLowerCase());
|
|
}
|
|
|
|
function stripSecretBearingUrlParts(value: string): string {
|
|
try {
|
|
const url = new URL(value);
|
|
if (!url.username && !url.password && !url.search && !url.hash) return value;
|
|
url.username = "";
|
|
url.password = "";
|
|
url.search = "";
|
|
url.hash = "";
|
|
return url.toString();
|
|
} catch {
|
|
return value;
|
|
}
|
|
}
|
|
|
|
export function redactSensitive(value: unknown, depth = 0): unknown {
|
|
if (depth > MAX_DEPTH) return undefined;
|
|
if (value === null || typeof value !== "object") return value;
|
|
if (Array.isArray(value)) {
|
|
if (depth + 1 > MAX_DEPTH) return undefined;
|
|
return value.map((entry) => redactSensitive(entry, depth + 1));
|
|
}
|
|
const out: Record<string, unknown> = {};
|
|
for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
|
|
if (isSensitiveKey(key)) {
|
|
out[key] = REDACTED;
|
|
continue;
|
|
}
|
|
if (typeof entry === "string" && isUrlishKey(key)) {
|
|
out[key] = stripSecretBearingUrlParts(entry);
|
|
continue;
|
|
}
|
|
out[key] = redactSensitive(entry, depth + 1);
|
|
}
|
|
return out;
|
|
}
|