mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-03 09:37:19 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Agent runs need credentials for a specific provider and sign-in method. > - Connections already owns accounts, grants, and access permissions. > - AI authentication should use those same boundaries. > - This pull request adds the storage, API, adoption, and runtime foundation. > - Legacy agents keep their authentication until they explicitly adopt a managed connection. ## Linked Issues or Issue Description **Problem or motivation** AI credentials are configured separately from Connections. Agents cannot consistently reuse a responsible user's account or a permitted shared account. **Proposed solution** Manage AI accounts with the existing Connections grants and permissions. Keep model and harness selection independent from credential selection. Preserve legacy authentication until validated adoption. **Alternatives considered** A separate credential registry would duplicate ownership and access policy. Automatic fallback would risk using the wrong account. **Roadmap alignment** This extends the shipped Apps, multi-user, secrets, and agent-runtime capabilities. The maintainer requested the feature and reviewed the UI. Related groundwork: #11899 (connection permissions), #10910 (connection wizard), #11692 (Claude subscription profiles), and #11854 (Codex account rotation). ## What Changed - Add AI-purpose/runtime-auth contracts and an additive, idempotent migration. - Add Claude, OpenAI, OpenRouter, and Grok provider capabilities and catalog entries. - Store credentials on grants. Resolve responsible-user defaults or explicit permitted grants. - Isolate managed credentials and provider sessions across accounts. Block missing credentials without ambient fallback. - Keep imported legacy secrets unchanged during reconnect. Use independent local Codex/Grok sign-in attempts for rotating credentials. - Add authorization, migration, concurrent refresh, retry, cancellation, and legacy-compatibility tests. This is part 1 of a two-PR stack. The app UI follows in #13248. Merge the foundation first. ## Verification - Updated against master `04e364236`, preserving upstream provider login and connector workflows. - Full workspace typecheck, production build, Storybook build, and token gates passed on the integrated branch. Final local-login changes passed 59 focused tests; new-agent and inbox regression suites passed 63 tests. - Browser checks verified automatic local Claude account detection, resumable Codex login commands, retry, focus restoration, and desktop/phone layouts. Commands create their isolated directory before invoking the CLI. - All current-head CI checks passed on `2a996560a`, including all server/workspace tests, browser shards, runner verification, typecheck, build, and canary dry run. Greptile reviewed that commit at 5/5 with no unresolved threads. Earlier local full-suite attempts hit the Mac PostgreSQL shared-memory limit; the complete suites passed in CI. - Renumbered the additive AI migration to `0276` after upstream migrations and regenerated its snapshot. Existing legacy agents retain their configuration. - Added local login status checks, owner-scoped retry, managed OpenCode remote homes, credential-aware model discovery, and task connection-repair delivery. ## Risks - Managed credential failures intentionally block execution. They do not restore legacy fallback. - Preview-era copied Codex/Grok subscriptions require independent reconnect. - The integrated branch has live provider acceptance coverage. This update verifies local Claude detection and Codex API-key task repair; it does not add a new subscription authorization/refresh or Daytona stress pass. - Runtime-auth connections must stay excluded from tool and channel handling. ## Model Used OpenAI GPT-6 through Codex, with reasoning, repository tools, code execution, and browser testing. The exact runtime model identifier and context-window size are not exposed in this session. ## 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>
127 lines
4.7 KiB
TypeScript
127 lines
4.7 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,
|
|
resolveManagedCodexHomeDir,
|
|
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,
|
|
type PromoteDeviceLoginCredentialResult,
|
|
} from "./adapter-auth-promotion.js";
|
|
export {
|
|
withCodexAccountHomePromotionLock,
|
|
withAccountHomeSecretMutationLock,
|
|
assertAccountHomeCacheDirStillValid,
|
|
resolveCodexAuthCacheDir,
|
|
isCodexAuthCachePath,
|
|
readSubscriptionAccountId,
|
|
} from "./codex-auth-cache.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
|
|
);
|
|
},
|
|
};
|
|
|
|
export { decideCodexAuthMerge } from "./codex-auth-merge-decision.js";
|
|
|
|
export { copyBackCodexAuth } from "./codex-auth-copyback.js";
|