mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-23 09:07:46 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Sandbox agents need a safe login path for each supported adapter > - Codex device login and Claude setup-token login used separate session stores and route logic > - Separate stores made session lookup, expiry, and login capability checks harder to keep consistent > - This pull request unifies both flows on one session table and one capability contract > - The benefit is one company-scoped login model with public session identifiers and shared lifecycle rules ## Linked Issues or Issue Description **Subsystem affected** Cross-cutting (multiple of the above) **Problem or motivation** Codex and Claude sandbox login used separate session stores and different route paths. This split increased the risk of inconsistent company scoping, session lookup, and cleanup. **Proposed solution** Use `adapter_auth_sessions` for both login flows. Use public session identifiers for API access. Select login behavior from projected adapter capability data. Share the route spine, lease arguments, runner lifecycle, and reaper rules. **Alternatives considered** Keep two session tables and add matching fixes to both routes. This keeps duplicate logic and does not provide one capability contract, so this pull request uses shared infrastructure. **Roadmap alignment** This change supports the shipped Cloud / Sandbox agents milestone in `ROADMAP.md`. ## What Changed - Unify Codex device login and Claude setup-token login on `adapter_auth_sessions`. - Return and look up sessions with company-scoped public session identifiers. - Enforce one active session for each company, owner, and adapter. - Share the login route spine, sandbox lease arguments, runner lifecycle, and missing-auth check. - Add a standalone setup-token reaper with adapter-specific row selection. - Add optional login capability projection for adapters and drive route and UI selection from that data. - Rename the provider flag to `supportsLoginPty` and validate its deprecated alias. - Remove the old Claude setup-token session table and add the required migrations. ## Verification - Server typecheck passed with `tsc`. - Database typecheck passed. - UI typecheck passed with `tsc -b`. - Codex login service and route suites passed. - Setup-token session, route, and reaper suites passed. - Adapter session schema, plugin validator, capability projection, UI render, and Daytona suites passed. - GitHub Actions must confirm the complete CI gate after pull request creation. ## Risks - The migrations remove short-lived in-flight login rows during deployment. A login that spans the migration can continue until its provider lease expires. - The Codex credential store remains company-scoped. A cross-owner credential race remains a documented, board-accepted risk. - API clients that use internal session row identifiers no longer work. The API accepts only public session identifiers. ## Model Used Codex, GPT-5, exact runtime model ID not exposed in this handoff, large context window, reasoning, and repository tool use. The implementing engineer produced the code with AI assistance. ## 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>
113 lines
4.3 KiB
TypeScript
113 lines
4.3 KiB
TypeScript
export { execute, ensureCodexSkillsInjected } from "./execute.js";
|
|
export {
|
|
resolveCodexAuthPrecedence,
|
|
CODEX_SANDBOX_AUTH_PRECEDENCE_WARNING,
|
|
CODEX_SANDBOX_AUTH_PRECEDENCE_WARNING_LOG_LINE,
|
|
CODEX_SANDBOX_AUTH_EXISTS_COMMAND,
|
|
type CodexAuthPrecedenceInput,
|
|
type CodexAuthPrecedenceResolution,
|
|
type CodexAuthPrecedenceWinner,
|
|
} from "./auth-precedence.js";
|
|
export * from "./acp.js";
|
|
export { getConfigSchema } from "./config-schema.js";
|
|
export {
|
|
reconcileManagedCodexHome,
|
|
isManagedCodexHomePath,
|
|
evaluateCodexCredentialReadiness,
|
|
type ReconcileManagedCodexHomeInput,
|
|
type ReconcileManagedCodexHomeResult,
|
|
type ReconcileManagedCodexHomeStatus,
|
|
type CodexCredentialReadiness,
|
|
type CodexCredentialReadinessInput,
|
|
type CodexCredentialAuthMode,
|
|
} from "./codex-home.js";
|
|
export { listCodexSkills, syncCodexSkills } from "./skills.js";
|
|
export { testEnvironment } from "./test.js";
|
|
export {
|
|
runDeviceLogin,
|
|
CODEX_DEVICE_LOGIN_COMMAND,
|
|
type SandboxLoginDriver,
|
|
type DeviceLoginPromptSink,
|
|
type DeviceLoginOutcome,
|
|
type DeviceLoginResult,
|
|
type RunDeviceLoginOptions,
|
|
} from "./device-login-runner.js";
|
|
export { DEVICE_LOGIN_URL, parseDeviceLoginPrompt, type DeviceLoginPrompt } from "./device-login-parse.js";
|
|
export {
|
|
promoteDeviceLoginCredential,
|
|
checkStagedCredentialReadiness,
|
|
DeviceLoginReadinessError,
|
|
type CredentialReadinessResult,
|
|
type PromoteDeviceLoginCredentialInput,
|
|
type PromoteDeviceLoginCredentialOutcome,
|
|
} from "./adapter-auth-promotion.js";
|
|
export { parseCodexJsonl, isCodexHarnessCrash, isCodexProviderQuotaError, isCodexTransientUpstreamError, isCodexUnknownSessionError } from "./parse.js";
|
|
export {
|
|
getQuotaWindows,
|
|
readCodexAuthInfo,
|
|
readCodexToken,
|
|
fetchCodexQuota,
|
|
fetchCodexRpcQuota,
|
|
mapCodexRpcQuota,
|
|
secondsToWindowLabel,
|
|
fetchWithTimeout,
|
|
codexHomeDir,
|
|
} from "./quota.js";
|
|
import type { AdapterSessionCodec } from "@paperclipai/adapter-utils";
|
|
import { sessionCodec as acpxSessionCodec } from "@paperclipai/adapter-utils/acpx-engine/session-codec";
|
|
|
|
function readNonEmptyString(value: unknown): string | null {
|
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
}
|
|
|
|
export const sessionCodec: AdapterSessionCodec = {
|
|
deserialize(raw: unknown) {
|
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
|
|
const record = raw as Record<string, unknown>;
|
|
const sessionId = readNonEmptyString(record.sessionId) ?? readNonEmptyString(record.session_id);
|
|
if (!sessionId) return acpxSessionCodec.deserialize(raw);
|
|
const cwd =
|
|
readNonEmptyString(record.cwd) ??
|
|
readNonEmptyString(record.workdir) ??
|
|
readNonEmptyString(record.folder);
|
|
const workspaceId = readNonEmptyString(record.workspaceId) ?? readNonEmptyString(record.workspace_id);
|
|
const repoUrl = readNonEmptyString(record.repoUrl) ?? readNonEmptyString(record.repo_url);
|
|
const repoRef = readNonEmptyString(record.repoRef) ?? readNonEmptyString(record.repo_ref);
|
|
return {
|
|
sessionId,
|
|
...(cwd ? { cwd } : {}),
|
|
...(workspaceId ? { workspaceId } : {}),
|
|
...(repoUrl ? { repoUrl } : {}),
|
|
...(repoRef ? { repoRef } : {}),
|
|
};
|
|
},
|
|
serialize(params: Record<string, unknown> | null) {
|
|
if (!params) return null;
|
|
const sessionId = readNonEmptyString(params.sessionId) ?? readNonEmptyString(params.session_id);
|
|
if (!sessionId) return acpxSessionCodec.serialize(params);
|
|
const cwd =
|
|
readNonEmptyString(params.cwd) ??
|
|
readNonEmptyString(params.workdir) ??
|
|
readNonEmptyString(params.folder);
|
|
const workspaceId = readNonEmptyString(params.workspaceId) ?? readNonEmptyString(params.workspace_id);
|
|
const repoUrl = readNonEmptyString(params.repoUrl) ?? readNonEmptyString(params.repo_url);
|
|
const repoRef = readNonEmptyString(params.repoRef) ?? readNonEmptyString(params.repo_ref);
|
|
return {
|
|
sessionId,
|
|
...(cwd ? { cwd } : {}),
|
|
...(workspaceId ? { workspaceId } : {}),
|
|
...(repoUrl ? { repoUrl } : {}),
|
|
...(repoRef ? { repoRef } : {}),
|
|
};
|
|
},
|
|
getDisplayId(params: Record<string, unknown> | null) {
|
|
if (!params) return null;
|
|
return (
|
|
readNonEmptyString(params.sessionId) ??
|
|
readNonEmptyString(params.session_id) ??
|
|
acpxSessionCodec.getDisplayId?.(params) ??
|
|
null
|
|
);
|
|
},
|
|
};
|