mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-05 21:02:34 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Low-trust agents may read only the projects in their allowed scope. > - Workspace setup correctly stops a task outside that scope. > - The task previously showed a generic failure, and agent settings did not offer an additive repair. > - This pull request explains the denial and lets authorized operators add projects while preserving other restrictions. > - The saved task then offers an explicit retry. Saving access does not start work. ## Linked Issues or Issue Description **What happened?** A project access denial appeared as an ordinary failed attempt. Operators could not add a second allowed project from agent settings. **Expected behavior** The task should explain the denied access and link to a working permission editor. The editor should preserve existing scope and restrictions, enforce operator authority, and retain drafts after a failed save. **Steps to reproduce** 1. Assign a low-trust agent to a task whose project is outside its allowed projects. 2. Let workspace setup deny project access. 3. Open the task, follow Manage project access, add the project, save, reload, and return to the task. 4. Confirm that the saved message remains and Retry task is a separate action. ## What Changed - Record a typed, non-retryable project denial before provider dispatch. Project denials do not enter automatic continuation or provider comment policy. - Project current and historical setup denials into a safe task notice. Keep unrelated failures, prior assignees, and approval/pause/budget gates separate. Recheck current project authority, including standard trust and root-issue grants. Accepted retries keep their working state. - Add board-only project-access reads and additive saves under the agent row lock. Enforce company scope, agent-management authority, pending-approval restrictions, and an audit record for new additions. - Add the production notice and searchable multi-project editor, including read-only, saving, cancellation, and failed-save states. Preserve the return-to-task query through canonical agent redirects. - Register board-only project-access routes in OpenAPI with a strict request schema. - Add production Storybook examples and API/persistence/concurrency regressions. Retain the approved design snapshot and update the behavior maps and implementation contract. ## Verification - Passed: `pnpm -r typecheck`, `pnpm build`, `pnpm build-storybook`, `pnpm check:token-gates`, `node scripts/check-maps.mjs`, and `node scripts/storybook-catalog.mjs`. - Passed: 11 permission/projection tests and 30 run-completion tests against an isolated PostgreSQL database. These cover historical failures, concurrent additions, idempotent audit, preserved restrictions, foreign projects, read-only users, agent self-grants, and no automatic wakeup. - Passed: all 27 OpenAPI route tests, including route coverage and the new board-only request/response contract. - Browser: the real app displayed a historical denial; Manage project access opened the editor; saving retained the original project and survived reload; Return to task showed the ready notice. Saving created one audit event and zero runs. Explicit Retry returned 202 and reached the separate sandbox requirement, which remained enforced. No external provider was used. - All five production Storybook interaction checks pass: blocked task, save, cancellation, failed-save recovery, and read-only settings. These use fixture callbacks; durable authorization is covered separately above. - `pnpm test:run` was attempted locally but stopped after repeated embedded PostgreSQL startup failures caused by macOS semaphore exhaustion. Full-suite CI is required before merge. The separate UI suite passed all 9,095 tests. The 41 focused server regressions also passed after rebasing onto the latest master. A further 26 recovery-classification tests pass, including a red-to-green regression for automatic continuation after a project denial. ## Risks - No schema migration. The new endpoint only adds same-company project IDs and preserves the rest of the stored policy. - The historical notice recognizes one exact setup denial message. It does not display arbitrary run errors or disclose a different source project's identity. - The retry still passes all current admission and isolation checks. The notice does not promise that other execution requirements are satisfied. - Related open work: #13309 covers inherited secret restrictions; #11234 covers the broader repository catalog. This change does not replace either policy surface. ## Model Used OpenAI Codex (GPT-6), with code execution and browser tools. The precise runtime model ID and context-window size were 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 #` 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 (focused server and full UI suites; full-suite infrastructure limit above) - [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>
418 lines
20 KiB
TypeScript
418 lines
20 KiB
TypeScript
import type { AgentPublicIdentity } from "@paperclipai/shared";
|
|
import type { AgentProjectAccess } from "@paperclipai/shared";
|
|
import type {
|
|
Agent,
|
|
EnvironmentProbeResult,
|
|
ConnectionIntentInteraction,
|
|
AgentDesiredSkillEntry,
|
|
AgentSkillAssignmentMode,
|
|
AgentPermissions,
|
|
AgentDetail,
|
|
AgentInstructionsBundle,
|
|
AgentInstructionHistory,
|
|
AgentInstructionCandidate,
|
|
ResolveAgentInstructionCandidate,
|
|
AgentInstructionDiff,
|
|
AgentInstructionSnapshot,
|
|
AgentInstructionsFileDetail,
|
|
AgentSkillSnapshot,
|
|
AdapterEnvironmentTestResult,
|
|
AdapterAuthSignalResponse,
|
|
AdapterAuthSessionResponse,
|
|
AdapterAuthSessionOwnerResponse,
|
|
ClaudeSetupTokenSessionResponse,
|
|
ClaudeSetupTokenSessionOwnerResponse,
|
|
ClaudeSetupTokenSessionPrompt,
|
|
ClaudeSetupTokenCompletionResponse,
|
|
ClaudeSetupTokenOverwrite,
|
|
ClaudeOAuthTokenStatusResponse,
|
|
SubmitBrowserCodeRequest,
|
|
AgentKeyCreated,
|
|
AgentRuntimeState,
|
|
AgentTaskSession,
|
|
AgentWakeupResponse,
|
|
ChatFailedRunRetryResponse,
|
|
HeartbeatRun,
|
|
Approval,
|
|
AgentConfigRevision,
|
|
ClearAgentErrorResponse,
|
|
AgentApiKeyScope,
|
|
} from "@paperclipai/shared";
|
|
import { isUuidLike, normalizeAgentUrlKey } from "@paperclipai/shared";
|
|
import { ApiError, api } from "./client";
|
|
|
|
export interface AgentKey {
|
|
id: string;
|
|
name: string;
|
|
scope: AgentApiKeyScope;
|
|
createdAt: Date;
|
|
revokedAt: Date | null;
|
|
}
|
|
|
|
export interface AdapterModel {
|
|
id: string;
|
|
label: string;
|
|
}
|
|
|
|
export interface DetectedAdapterModel {
|
|
model: string;
|
|
provider: string;
|
|
source: string;
|
|
candidates?: string[];
|
|
}
|
|
|
|
export interface ClaudeLoginResult {
|
|
exitCode: number | null;
|
|
signal: string | null;
|
|
timedOut: boolean;
|
|
loginUrl: string | null;
|
|
stdout: string;
|
|
stderr: string;
|
|
}
|
|
|
|
export interface OrgNode {
|
|
id: string;
|
|
name: string;
|
|
role: string;
|
|
status: string;
|
|
reports: OrgNode[];
|
|
}
|
|
|
|
export interface AgentHireResponse {
|
|
agent: Agent;
|
|
approval: Approval | null;
|
|
}
|
|
|
|
export interface AgentPermissionUpdate {
|
|
canCreateAgents: boolean;
|
|
canCreateSkills: boolean;
|
|
canAssignTasks: boolean;
|
|
trustPreset?: AgentPermissions["trustPreset"];
|
|
authorizationPolicy?: AgentPermissions["authorizationPolicy"];
|
|
}
|
|
|
|
export interface AgentWakeRequest {
|
|
source?: "timer" | "assignment" | "on_demand" | "automation";
|
|
triggerDetail?: "manual" | "ping" | "callback" | "system";
|
|
reason?: string | null;
|
|
payload?: Record<string, unknown> | null;
|
|
idempotencyKey?: string | null;
|
|
forceFreshSession?: boolean;
|
|
debug?: { providerTrace: "raw" };
|
|
}
|
|
|
|
function withCompanyScope(path: string, companyId?: string) {
|
|
if (!companyId) return path;
|
|
const separator = path.includes("?") ? "&" : "?";
|
|
return `${path}${separator}companyId=${encodeURIComponent(companyId)}`;
|
|
}
|
|
|
|
function agentPath(id: string, companyId?: string, suffix = "") {
|
|
return withCompanyScope(`/agents/${encodeURIComponent(id)}${suffix}`, companyId);
|
|
}
|
|
|
|
export const agentsApi = {
|
|
projectAccess: (id: string) => api.get<AgentProjectAccess>(`/agents/${id}/project-access`),
|
|
addProjectAccess: (id: string, projectIds: string[]) => api.post<AgentProjectAccess>(`/agents/${id}/project-access`, { projectIds }),
|
|
qualifyRunner: (id: string, expectedEnvironmentId: string, companyId?: string) =>
|
|
api.post<EnvironmentProbeResult>(agentPath(id, companyId, "/qualify-runner"), { expectedEnvironmentId }),
|
|
setAvatar: (companyId: string, agentId: string, imageBase64: string | null) =>
|
|
api.put<{ agentId: string; appearance: import("@paperclipai/shared").AgentAppearance; avatarUrl: string }>(`/companies/${companyId}/agents/${agentId}/avatar`, { imageBase64 }),
|
|
getIdentity: (id: string, companyId?: string) =>
|
|
api.get<AgentPublicIdentity | null>(agentPath(id, companyId, "/identity")),
|
|
adoptAiConnection: (agentId: string, interactionId: string, connectionId: string, companyId: string) =>
|
|
api.post<ConnectionIntentInteraction>(withCompanyScope(`/agents/${agentId}/connection-intents/${interactionId}/adopt`, companyId), { connectionId }),
|
|
|
|
list: (companyId: string) => api.get<Agent[]>(`/companies/${companyId}/agents`),
|
|
org: (companyId: string) => api.get<OrgNode[]>(`/companies/${companyId}/org`),
|
|
listConfigurations: (companyId: string) =>
|
|
api.get<Record<string, unknown>[]>(`/companies/${companyId}/agent-configurations`),
|
|
get: async (id: string, companyId?: string) => {
|
|
try {
|
|
return await api.get<AgentDetail>(agentPath(id, companyId));
|
|
} catch (error) {
|
|
// Backward-compat fallback: if backend shortname lookup reports ambiguity,
|
|
// resolve using company agent list while ignoring terminated agents.
|
|
if (
|
|
!(error instanceof ApiError) ||
|
|
error.status !== 409 ||
|
|
!companyId ||
|
|
isUuidLike(id)
|
|
) {
|
|
throw error;
|
|
}
|
|
|
|
const urlKey = normalizeAgentUrlKey(id);
|
|
if (!urlKey) throw error;
|
|
|
|
const agents = await api.get<Agent[]>(`/companies/${companyId}/agents`);
|
|
const matches = agents.filter(
|
|
(agent) => agent.status !== "terminated" && normalizeAgentUrlKey(agent.urlKey) === urlKey,
|
|
);
|
|
if (matches.length !== 1) throw error;
|
|
return api.get<AgentDetail>(agentPath(matches[0]!.id, companyId));
|
|
}
|
|
},
|
|
getConfiguration: (id: string, companyId?: string) =>
|
|
api.get<Record<string, unknown>>(agentPath(id, companyId, "/configuration")),
|
|
listConfigRevisions: (id: string, companyId?: string) =>
|
|
api.get<AgentConfigRevision[]>(agentPath(id, companyId, "/config-revisions")),
|
|
getConfigRevision: (id: string, revisionId: string, companyId?: string) =>
|
|
api.get<AgentConfigRevision>(agentPath(id, companyId, `/config-revisions/${revisionId}`)),
|
|
rollbackConfigRevision: (id: string, revisionId: string, companyId?: string) =>
|
|
api.post<Agent>(agentPath(id, companyId, `/config-revisions/${revisionId}/rollback`), {}),
|
|
create: (companyId: string, data: Record<string, unknown>) =>
|
|
api.post<Agent>(`/companies/${companyId}/agents`, data),
|
|
hire: (companyId: string, data: Record<string, unknown>) =>
|
|
api.post<AgentHireResponse>(`/companies/${companyId}/agent-hires`, data),
|
|
updateRuntimeSettings: (id: string, runtimeSettings: import("@paperclipai/shared").AgentRuntimeSettingsPatch, companyId: string) =>
|
|
api.patch<Agent>(agentPath(id, companyId), { runtimeSettings }),
|
|
update: (id: string, data: Record<string, unknown>, companyId?: string) =>
|
|
api.patch<Agent>(agentPath(id, companyId), data),
|
|
updatePermissions: (id: string, data: AgentPermissionUpdate, companyId?: string) =>
|
|
api.patch<AgentDetail>(agentPath(id, companyId, "/permissions"), data),
|
|
instructionsBundle: (id: string, companyId?: string) =>
|
|
api.get<AgentInstructionsBundle>(agentPath(id, companyId, "/instructions-bundle")),
|
|
updateInstructionsBundle: (
|
|
id: string,
|
|
data: {
|
|
mode?: "managed" | "external";
|
|
rootPath?: string | null;
|
|
entryFile?: string;
|
|
clearLegacyPromptTemplate?: boolean;
|
|
},
|
|
companyId?: string,
|
|
) => api.patch<AgentInstructionsBundle>(agentPath(id, companyId, "/instructions-bundle"), data),
|
|
instructionsFile: (id: string, relativePath: string, companyId?: string) =>
|
|
api.get<AgentInstructionsFileDetail>(
|
|
agentPath(id, companyId, `/instructions-bundle/file?path=${encodeURIComponent(relativePath)}`),
|
|
),
|
|
saveInstructionsFile: (
|
|
id: string,
|
|
data: { path: string; content: string; baseRevisionId?: string | null; baseHash?: string | null; clearLegacyPromptTemplate?: boolean },
|
|
companyId?: string,
|
|
) => api.put<AgentInstructionsFileDetail>(agentPath(id, companyId, "/instructions-bundle/file"), data),
|
|
instructionCandidates: (id: string, companyId?: string) =>
|
|
api.get<AgentInstructionCandidate[]>(agentPath(id, companyId, "/instructions-bundle/candidates")),
|
|
resolveInstructionCandidate: (id: string, runId: string, data: ResolveAgentInstructionCandidate, companyId?: string) =>
|
|
api.post<AgentInstructionsFileDetail>(agentPath(id, companyId, `/instructions-bundle/candidates/${runId}/resolve`), data),
|
|
instructionHistory: (id: string, path: string, companyId?: string, cursor?: string) =>
|
|
api.get<AgentInstructionHistory>(agentPath(id, companyId, `/instructions-bundle/history?path=${encodeURIComponent(path)}${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}`)),
|
|
instructionRevision: (id: string, path: string, revisionId: string, companyId?: string) =>
|
|
api.get<AgentInstructionSnapshot>(agentPath(id, companyId, `/instructions-bundle/revision/${revisionId}?path=${encodeURIComponent(path)}`)),
|
|
instructionDiff: (id: string, path: string, from: string, to: string, companyId?: string) =>
|
|
api.get<AgentInstructionDiff>(agentPath(id, companyId, `/instructions-bundle/diff?path=${encodeURIComponent(path)}&from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`)),
|
|
restoreInstructions: (id: string, data: { path: string; revisionId: string; baseRevisionId: string }, companyId?: string) =>
|
|
api.post<AgentInstructionsFileDetail>(agentPath(id, companyId, "/instructions-bundle/restore"), data),
|
|
downloadInstructionsFile: (id: string, relativePath: string, companyId?: string) =>
|
|
`/api${agentPath(id, companyId, `/instructions-bundle/file?path=${encodeURIComponent(relativePath)}&download=true`)}`,
|
|
deleteInstructionsFile: (id: string, relativePath: string, companyId?: string, baseHash?: string) =>
|
|
api.delete<AgentInstructionsBundle>(
|
|
agentPath(id, companyId, `/instructions-bundle/file?path=${encodeURIComponent(relativePath)}${baseHash ? `&baseHash=${baseHash}` : ""}`),
|
|
),
|
|
retryLifecycle: (id: string, companyId?: string) => api.post<Agent>(agentPath(id, companyId, "/lifecycle/retry"), {}),
|
|
pause: (id: string, companyId?: string) => api.post<Agent>(agentPath(id, companyId, "/pause"), {}),
|
|
resume: (id: string, companyId?: string) => api.post<Agent>(agentPath(id, companyId, "/resume"), {}),
|
|
clearError: (id: string, companyId?: string) =>
|
|
api.post<ClearAgentErrorResponse>(agentPath(id, companyId, "/clear-error"), {}),
|
|
approve: (id: string, companyId?: string) => api.post<Agent>(agentPath(id, companyId, "/approve"), {}),
|
|
terminate: (id: string, companyId?: string) => api.post<Agent>(agentPath(id, companyId, "/terminate"), {}),
|
|
remove: (id: string, companyId?: string) => api.delete<{ ok: true }>(agentPath(id, companyId)),
|
|
listKeys: (id: string, companyId?: string) => api.get<AgentKey[]>(agentPath(id, companyId, "/keys")),
|
|
skills: (id: string, companyId?: string) =>
|
|
api.get<AgentSkillSnapshot>(agentPath(id, companyId, "/skills")),
|
|
syncSkills: (
|
|
id: string,
|
|
desiredSkills: Array<string | AgentDesiredSkillEntry>,
|
|
mode: AgentSkillAssignmentMode,
|
|
companyId?: string,
|
|
) => api.post<AgentSkillSnapshot>(agentPath(id, companyId, "/skills/sync"), { desiredSkills, mode }),
|
|
createKey: (id: string, name: string, companyId?: string, scope?: AgentApiKeyScope) =>
|
|
api.post<AgentKeyCreated>(agentPath(id, companyId, "/keys"), { name, ...(scope ? { scope } : {}) }),
|
|
revokeKey: (agentId: string, keyId: string, companyId?: string) =>
|
|
api.delete<{ ok: true }>(agentPath(agentId, companyId, `/keys/${encodeURIComponent(keyId)}`)),
|
|
runtimeState: (id: string, companyId?: string) =>
|
|
api.get<AgentRuntimeState>(agentPath(id, companyId, "/runtime-state")),
|
|
taskSessions: (id: string, companyId?: string) =>
|
|
api.get<AgentTaskSession[]>(agentPath(id, companyId, "/task-sessions")),
|
|
resetSession: (id: string, taskKey?: string | null, companyId?: string) =>
|
|
api.post<void>(agentPath(id, companyId, "/runtime-state/reset-session"), { taskKey: taskKey ?? null }),
|
|
adapterModels: (
|
|
companyId: string,
|
|
type: string,
|
|
options?: { refresh?: boolean; environmentId?: string | null; provider?: string; acpxAgent?: string; aiConnection?: import("@paperclipai/shared").AiConnectionBinding; agentId?: string; poolId?: string },
|
|
) => {
|
|
const params = new URLSearchParams();
|
|
if (options?.refresh) params.set("refresh", "1");
|
|
if (options?.provider) params.set("provider", options.provider);
|
|
if (options?.acpxAgent) params.set("acpxAgent", options.acpxAgent);
|
|
if (options?.aiConnection) params.set("aiConnection", JSON.stringify(options.aiConnection));
|
|
if (options?.agentId) params.set("agentId", options.agentId);
|
|
if (options?.poolId) params.set("poolId", options.poolId);
|
|
if (options?.environmentId) params.set("environmentId", options.environmentId);
|
|
const query = params.size > 0 ? `?${params.toString()}` : "";
|
|
return api.get<AdapterModel[]>(
|
|
`/companies/${encodeURIComponent(companyId)}/adapters/${encodeURIComponent(type)}/models${query}`,
|
|
);
|
|
},
|
|
detectModel: (companyId: string, type: string) =>
|
|
api.get<DetectedAdapterModel | null>(
|
|
`/companies/${encodeURIComponent(companyId)}/adapters/${encodeURIComponent(type)}/detect-model`,
|
|
),
|
|
testEnvironment: (
|
|
companyId: string,
|
|
type: string,
|
|
data: {
|
|
adapterConfig: Record<string, unknown>;
|
|
runner?: import("@paperclipai/shared").AgentRunnerChoice;
|
|
aiConnection?: import("@paperclipai/shared").AiRuntimeConnectionBinding;
|
|
agentId?: string;
|
|
testCredentials?: Record<string, string>;
|
|
environmentId?: string | null;
|
|
},
|
|
) =>
|
|
api.post<AdapterEnvironmentTestResult>(
|
|
`/companies/${companyId}/adapters/${type}/test-environment`,
|
|
data,
|
|
),
|
|
getAdapterAuthSignal: (companyId: string, type: string, environmentId?: string | null) => {
|
|
const query = environmentId ? `?environmentId=${encodeURIComponent(environmentId)}` : "";
|
|
return api.get<AdapterAuthSignalResponse>(
|
|
`/companies/${encodeURIComponent(companyId)}/adapters/${encodeURIComponent(type)}/auth-signal${query}`,
|
|
);
|
|
},
|
|
invoke: (id: string, companyId?: string, data: AgentWakeRequest = {}) =>
|
|
api.post<HeartbeatRun>(agentPath(id, companyId, "/heartbeat/invoke"), data),
|
|
wakeup: (
|
|
id: string,
|
|
data: AgentWakeRequest,
|
|
companyId?: string,
|
|
) => api.post<AgentWakeupResponse>(agentPath(id, companyId, "/wakeup"), data),
|
|
retryFailedRun: async (
|
|
id: string,
|
|
failedRunId: string,
|
|
companyId: string,
|
|
) => {
|
|
const result = await api.post<
|
|
AgentWakeupResponse | ChatFailedRunRetryResponse
|
|
>(agentPath(id, companyId, "/wakeup"), {
|
|
source: "on_demand",
|
|
triggerDetail: "manual",
|
|
reason: "retry_failed_run",
|
|
failedRunId,
|
|
});
|
|
if ("id" in result) {
|
|
// Exact retries are idempotent: a repeated click can return a successor
|
|
// that has already stopped. It does not mean a new attempt was queued.
|
|
if (["failed", "timed_out", "cancelled", "interrupted"].includes(result.status)) {
|
|
throw new Error(
|
|
"The previous retry has already stopped. Refresh the task and retry its latest failed run.",
|
|
);
|
|
}
|
|
return { runId: result.id, issueId: null };
|
|
}
|
|
if ("actionId" in result) {
|
|
if (result.status === "failed" || result.status === "cancelled") {
|
|
throw new Error(
|
|
"This retry could not start. Open the task to review its current access and recovery state.",
|
|
);
|
|
}
|
|
return { runId: result.runId, issueId: result.issueId };
|
|
}
|
|
throw new Error(result.message ?? "Retry was skipped.");
|
|
},
|
|
loginWithClaude: (id: string, companyId?: string) =>
|
|
api.post<ClaudeLoginResult>(agentPath(id, companyId, "/claude-login"), {}),
|
|
startAdapterAuthLogin: (
|
|
companyId: string,
|
|
type: string,
|
|
data: { environmentId: string; ttlSeconds?: number; aiConnection?: import("@paperclipai/shared").AiConnectionLoginIntent },
|
|
) =>
|
|
api.post<AdapterAuthSessionResponse>(
|
|
`/companies/${encodeURIComponent(companyId)}/adapters/${encodeURIComponent(type)}/login-sessions`,
|
|
data,
|
|
),
|
|
// The owner response repeats the live prompt on every read while the
|
|
// session holds an active public status, so this polling call carries the
|
|
// same no-store request option as the active-session read below.
|
|
getAdapterAuthLoginStatus: (companyId: string, type: string, sessionId: string) =>
|
|
api.get<AdapterAuthSessionOwnerResponse>(
|
|
`/companies/${encodeURIComponent(companyId)}/adapters/${encodeURIComponent(type)}/login-sessions/${encodeURIComponent(sessionId)}`,
|
|
{ cache: "no-store" },
|
|
),
|
|
// Reads the caller's active login session for one company and adapter, with
|
|
// no session id, so the browser rediscovers its own session after a reload
|
|
// with no local state. A 404 means no active session for the caller.
|
|
getActiveAdapterAuthLoginSession: (companyId: string, type: string) =>
|
|
api.get<AdapterAuthSessionOwnerResponse>(
|
|
`/companies/${encodeURIComponent(companyId)}/adapters/${encodeURIComponent(type)}/login-sessions/active`,
|
|
{ cache: "no-store" },
|
|
),
|
|
cancelAdapterAuthLogin: (companyId: string, type: string, sessionId: string) =>
|
|
api.post<AdapterAuthSessionOwnerResponse>(
|
|
`/companies/${encodeURIComponent(companyId)}/adapters/${encodeURIComponent(type)}/login-sessions/${encodeURIComponent(sessionId)}/cancel`,
|
|
{},
|
|
),
|
|
// The Claude submitted-browser-code login uses the company-and-environment
|
|
// setup-token routes. The route fixes the `claude_local` adapter. The start
|
|
// response carries the panel mode; the authorization URL rides only through the
|
|
// guarded prompt read. The completion response carries a non-secret
|
|
// `storedSessionId` claim and no token.
|
|
// Reads the stored Claude OAuth token status for the authenticated owner. A
|
|
// 200 carries only the secret id and the latest version; a 404 means the owner
|
|
// has no stored value (indistinguishable from a foreign value). The client
|
|
// applies the stored token first and captures the version for a later
|
|
// version-checked overwrite.
|
|
getClaudeOAuthTokenStatus: (companyId: string) =>
|
|
api.get<ClaudeOAuthTokenStatusResponse>(
|
|
`/companies/${encodeURIComponent(companyId)}/claude-oauth-token-status`,
|
|
),
|
|
startClaudeSetupTokenLogin: (
|
|
companyId: string,
|
|
data: { environmentId: string; overwrite?: ClaudeSetupTokenOverwrite; aiConnection?: import("@paperclipai/shared").AiConnectionLoginIntent },
|
|
) =>
|
|
api.post<ClaudeSetupTokenSessionOwnerResponse>(
|
|
`/companies/${encodeURIComponent(companyId)}/setup-token-login-sessions`,
|
|
{ adapterType: "claude_local", ...data },
|
|
),
|
|
getClaudeSetupTokenLoginStatus: (companyId: string, sessionId: string) =>
|
|
api.get<ClaudeSetupTokenSessionResponse>(
|
|
`/companies/${encodeURIComponent(companyId)}/setup-token-login-sessions/${encodeURIComponent(sessionId)}`,
|
|
),
|
|
// Reads the caller's active Claude setup-token login session, with no
|
|
// session id, so the browser rediscovers its own session after a reload with
|
|
// no local state. A 404 means no active session for the caller.
|
|
getActiveClaudeSetupTokenLoginSession: (companyId: string) =>
|
|
api.get<ClaudeSetupTokenSessionOwnerResponse>(
|
|
`/companies/${encodeURIComponent(companyId)}/setup-token-login-sessions/active`,
|
|
{ cache: "no-store" },
|
|
),
|
|
getClaudeSetupTokenLoginPrompt: (companyId: string, sessionId: string) =>
|
|
api.get<ClaudeSetupTokenSessionPrompt>(
|
|
`/companies/${encodeURIComponent(companyId)}/setup-token-login-sessions/${encodeURIComponent(sessionId)}/prompt`,
|
|
),
|
|
submitClaudeSetupTokenBrowserCode: (companyId: string, sessionId: string, browserCode: string) =>
|
|
api.post<ClaudeSetupTokenSessionResponse>(
|
|
`/companies/${encodeURIComponent(companyId)}/setup-token-login-sessions/${encodeURIComponent(sessionId)}/code`,
|
|
{ browserCode } satisfies SubmitBrowserCodeRequest,
|
|
),
|
|
completeClaudeSetupTokenLogin: (companyId: string, sessionId: string) =>
|
|
api.post<ClaudeSetupTokenCompletionResponse>(
|
|
`/companies/${encodeURIComponent(companyId)}/setup-token-login-sessions/${encodeURIComponent(sessionId)}/completion`,
|
|
{},
|
|
),
|
|
cancelClaudeSetupTokenLogin: (companyId: string, sessionId: string) =>
|
|
api.post<void>(
|
|
`/companies/${encodeURIComponent(companyId)}/setup-token-login-sessions/${encodeURIComponent(sessionId)}/cancel`,
|
|
{},
|
|
),
|
|
availableSkills: () =>
|
|
api.get<{ skills: AvailableSkill[] }>("/skills/available"),
|
|
};
|
|
|
|
export interface AvailableSkill {
|
|
name: string;
|
|
description: string;
|
|
isPaperclipManaged: boolean;
|
|
}
|