mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-03 17:51:58 +01:00
## Thinking Path > - Paperclip manages AI agents and their access to saved connections. > - The Permissions page loads account details and permitted agent choices. > - Company-wide AI accounts repeat retained installation checks for every candidate agent. > - This makes target checks grow with the square of the roster size. > - The browser waits without a deadline, and connection names do not identify the provider login. > - This pull request batches direct AI checks and bounds loading with recovery controls. > - Account details load separately and show a provider email or an explicit profile-permission limit. ## Linked Issues or Issue Description **What happened?** Opening a saved AI account can leave Permissions on “Loading AI account…” for minutes. The account itself can be connected. The existing capability batch excludes AI connections, so a company-wide install repeats checks for every installed agent for every candidate. Saved names and Paperclip ownership also do not establish which provider login a credential uses. **Expected behavior** Direct AI accounts reuse retained-target checks within one descriptive request. The page shows loaded account controls or a bounded failure with Retry. Permissions shows the provider email and organization when the selected credential permits profile access. An unavailable profile must not block maintenance or disconnect a working inference credential. Writes still authorize current permissions. **Steps to reproduce** 1. Save an AI account in a company with several agents. 2. Install the account company-wide. 3. Open its Permissions page. 4. Delay the account-list or grants response to exercise the loading failure. The maintained benchmark reproduces database work with synthetic local data. Refs #17945, which added batching for non-AI connections. Searches found no open PR for this AI-account loading fix. ## What Changed - Include direct AI accounts in the existing board capability batch. Referenced pool members, delegated actors, complex selectors, and mutation authorization keep their existing evaluator. - Add opt-in per-caller GET deadlines to the API client. A timed-out caller detaches the stuck coalesced request so Retry starts a fresh fetch. Other callers retain their own cancellation and results. - Apply a 15-second deadline to AI account lists and grants reads. The permissions view retries transient failures once and reports other failures immediately. - Share the parent Permissions grants query with AI account details. Keep loaded controls visible after a background error and offer Retry. - Add a grant-scoped identity GET and OpenAPI contract. The service checks current membership and credential audience before it reads the selected credential. It projects only the provider email and organization; provider IDs, tokens, and raw errors stay private. - Show account details independently of maintenance controls. Anthropic profile denial has an explicit message and preserves connected status. Transient profile failures have a separate Retry action. Observations are cached for five minutes under the company, connection, grant, and grant revision. - Extend real PostgreSQL and authenticated HTTP coverage, client/component regressions, map guidance, and the existing benchmark's AI mode. - Address the Apex file-size finding by moving gallery identity lookup into the existing app display helper module. `AppDetail.tsx` is 984 lines; lookup behavior is unchanged. ## Verification Local source checkout on macOS, Node 25.6.1, pnpm 9.15.4, and disposable PostgreSQL 18.1. Provider calls use fixtures. No hosted deployment or provider qualification is claimed. - Passed `pnpm -r typecheck` and `pnpm build`. - Passed all 87 tests in `connection-management.integration.test.ts` and all 33 tests in `connection-install-capabilities.test.ts`. - Passed 196 AI connection, identity-provider, and OpenAPI tests: `ai-connections.test.ts`, `ai-account-identity.test.ts`, and `openapi-routes.test.ts`. - Passed 107 focused UI/client tests: `client.test.ts`, `ManagedAiConnectionDetails.test.tsx`, `AiConnectionAccountDetails.test.tsx`, and `AppDetail.test.tsx`. - After the Apex extraction, passed UI typecheck/build and 106 component tests across `AppDetail.test.tsx`, `AppNotConnected.test.tsx`, `ManagedAiConnectionDetails.test.tsx`, and `AiConnectionAccountDetails.test.tsx`. - The authenticated HTTP tests compare complete grants responses across human roles, for tool and AI connections. They verify a real install mutation succeeds before revocation and fails afterward. - A company-wide personal AI fixture checks 20 targets instead of 400. Pool-member permission loss still denies the complete pool description. - UI states covered: shared cached grants, stuck account/grants fetches, one automatic retry, terminal failure, manual recovery, authorization failure, background failure with retained controls, and unmount cancellation. Client tests also cover stalled response bodies and independent coalesced callers. - Identity coverage includes actual bearer-key/JWT reads, private-user and cross-company denial before provider access, revoked credentials, provider 401/403/429/500, and grant-change isolation. A real local HTTP fixture stalls the response body, verifies the shared 15-second deadline, and keeps unrelated HTTP requests responsive. The test shortens the native deadline to 100 ms. Profile permission denial leaves the saved account connected. - Passed `pnpm check:token-gates`, `node scripts/check-maps.mjs`, `node scripts/check-module-boundaries.mjs`, and `git diff --check`. - The full local `pnpm test:run` started with the loading-only change. All six chat shards passed. It recorded one failure in the unchanged `heartbeat-retry-scheduling.test.ts` quota-rejection case. That exact case passed a focused rerun on the final head. The initial failure cause remains unconfirmed and its local log is retained; this is not a full local-suite pass. The redundant local run was stopped with SIGINT after final-head CI completed successfully. - [Final-head CI](https://github.com/paperclipai/paperclip/actions/runs/41515556725) passed on `ead8d276d4a2023614fff54b3e9a858f28c3178b`: typecheck, build, all 12 server shards, all 3 chat shards, workspace suites, all 9 serialized suites, all 8 end-to-end shards, Runner checks, and the aggregate verify gate. Security checks passed. Optional Storybook checks were skipped by workflow policy. GitHub reports the branch mergeable without conflicts. - Apex reviewed head `ead8d276d4a2023614fff54b3e9a858f28c3178b` at 5/5, confirmed the extraction resolves its earlier file-size finding, and has no unresolved review threads. The existing benchmark ran for the loading-only change now recorded in commit `5f725b36fa52002545241a5a8b7a10602f0cc0ce`, with `--purpose=ai --baseline-ref=026fd9e3452`. The identity follow-up leaves that batch source unchanged. Two warmups and five alternating samples exercise each of eight fixture shapes. Complete positional capabilities, reasons, and operations match the baseline in every sample. | AI fixture | SQL calls before → after | Target reads before → after | | --- | ---: | ---: | | 1 agent, company install | 9 → 9 | 1 → 1 | | 32 agents, company install | 5,248 → 164 | 1,024 → 32 | | 100 agents, company install | 50,400 → 504 | 10,000 → 100 | | 100 agents, partial install | 13,175 → 503 | 2,575 → 100 | These counts measure the real service over local PostgreSQL. They do not predict hosted page latency. To reproduce the standalone benchmark, use a checkout of commit `5f725b36fa52002545241a5a8b7a10602f0cc0ce` and run the command below. Its isolation check intentionally rejects the later identity follow-up's additional runtime changes. ```sh node --import ./server/node_modules/tsx/dist/esm/index.mjs scripts/benchmark-connection-install-capabilities.ts --purpose=ai --baseline-ref=026fd9e3452 ``` Run the final-head regression suites from the repository root: ```sh cd server pnpm exec vitest run src/__tests__/connection-management.integration.test.ts src/__tests__/connection-install-capabilities.test.ts src/__tests__/ai-connections.test.ts src/services/ai-account-identity.test.ts src/__tests__/openapi-routes.test.ts cd ../ui pnpm exec vitest run src/api/client.test.ts src/components/ai-connections/ManagedAiConnectionDetails.test.tsx src/components/ai-connections/AiConnectionAccountDetails.test.tsx src/pages/apps/AppDetail.test.tsx ``` ## Risks Descriptive permissions reuse retained facts within one request. A concurrent change can leave a displayed choice stale; the next request and every write still authorize fresh. Pools with referenced members retain their existing cost. The account and grants clients now bound each caller's wait. A browser abort does not cancel an authorization read already admitted by the server. The independent profile request uses a fixed Anthropic origin, a bounded body, no redirects, and a 15-second deadline. Inference-only credentials can return no email; that is reported without guessing from a Paperclip owner or changing connection health. Profiles are fetched only for the caller's authorized selected grant. No ambient CLI identity, persistent identity cache, background polling, or billing-identity change is added. Other API reads keep their existing behavior. No migration is required. Staging has not been redeployed. ## Model Used OpenAI `gpt-6.1-sol` through the Codex harness in T3 Code, with xhigh reasoning, code editing, execution, and test tools. The host does not expose the context-window size. ## 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 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>
611 lines
27 KiB
TypeScript
611 lines
27 KiB
TypeScript
import type { AggregatorAppsResponse, ArcadeDiscoverySetupInput } from "@paperclipai/shared/aggregator-apps";
|
|
import type { ComposioAppAccountInput, ComposioAppSetupInput, ComposioAppSetupResult, ComposioAppSnapshot, ComposioAppsResponse } from "@paperclipai/shared";
|
|
import type {
|
|
ToolApplication,
|
|
ConfigureRailwaySsh,
|
|
RailwaySshSetup,
|
|
ToolConnection,
|
|
ToolConnectionInstall,
|
|
ToolConnectionInstallSnapshot,
|
|
ToolConnectionRemovalSummary,
|
|
ConnectToolAppResult,
|
|
ConnectToolApp,
|
|
FinishToolAppResult,
|
|
ToolCatalogEntry,
|
|
ToolRuntimeSlot,
|
|
ToolPolicy,
|
|
ToolConnectionHealthCheckResult,
|
|
ToolCatalogRefreshResult,
|
|
ToolAccessDecision,
|
|
ToolAccessDecisionInput,
|
|
CreateToolPolicy,
|
|
DuplicateToolPolicy,
|
|
McpJsonImportPreview,
|
|
ToolRuntimeHealthSummary,
|
|
ToolRunDecisionLookup,
|
|
ToolOAuthStartResult,
|
|
ToolStdioCommandTemplate,
|
|
ToolProfileBinding,
|
|
ToolProfileBindingTargetType,
|
|
ToolProfileDefaultAction,
|
|
ToolProfileEffectiveSummary,
|
|
ToolProfileEntry,
|
|
ToolProfileEntryEffect,
|
|
ToolProfileEntrySelectorType,
|
|
ToolProfileStatus,
|
|
ToolProfileWithDetails,
|
|
ToolProfileNewToolReviewDecision,
|
|
ToolProfileNewToolsReview,
|
|
ToolProfileNewToolsReviewResult,
|
|
ToolRiskLevel,
|
|
UpdateToolPolicy,
|
|
ReorderToolPolicies,
|
|
AppDefinition,
|
|
ToolAppsAttentionResponse,
|
|
ToolConnectionActivityResponse,
|
|
ToolConnectionLifecycleEventType,
|
|
ToolConnectionTestAgentsResponse,
|
|
ToolConnectionTestAgentAccessResponse,
|
|
ToolConnectionTestCallResult,
|
|
ToolConnectionTestCallStatus,
|
|
ToolActionRequest,
|
|
ToolActionRequestStatus,
|
|
ToolActionRequestsResponse,
|
|
ToolMcpGatewayWithTokens,
|
|
ToolMcpGatewayTokenCreated,
|
|
ToolMcpGatewayToken,
|
|
CreateToolMcpGateway,
|
|
CreateToolMcpGatewayToken,
|
|
UpdateToolMcpGateway,
|
|
CreateToolTrustRuleFromActionRequest,
|
|
ToolRedactedValueSummary,
|
|
ConnectionGrant,
|
|
ConnectionGrantKind,
|
|
ConnectionGrantDelegation,
|
|
ConnectionGrantsResponse,
|
|
ToolConnectionCreateCapabilities,
|
|
ToolAppMetadataPreflightResult,
|
|
} from "@paperclipai/shared";
|
|
import { api, detachInflightGet, type RequestOptions } from "./client";
|
|
|
|
/**
|
|
* Tools & Access API client (Phase 6, PAP-10389).
|
|
*
|
|
* Mirrors the governed MCP/tool-access contracts shipped by Phases 2-5
|
|
* (`server/src/routes/tool-access.ts` and `tool-gateway.ts`). The UI consumes
|
|
* server-side enforcement contracts directly instead of faking tool access in
|
|
* the browser.
|
|
*/
|
|
|
|
export type ToolApplicationsResponse = { applications: ToolApplication[] };
|
|
export type ToolConnectionsResponse = { connections: ToolConnection[] };
|
|
export type ToolCatalogResponse = { catalog: ToolCatalogEntry[] };
|
|
export type ToolRuntimeSlotsResponse = { runtimeSlots: ToolRuntimeSlot[] };
|
|
export type ToolRuntimeHealthResponse = ToolRuntimeHealthSummary;
|
|
export type ToolTrustRulesResponse = { trustRules: ToolPolicy[] };
|
|
export type ToolPoliciesResponse = { policies: ToolPolicy[] };
|
|
export type ToolProfilesResponse = { profiles: ToolProfileWithDetails[] };
|
|
export type ToolGalleryResponse = {
|
|
apps: AppDefinition[];
|
|
/** Server-authoritative OAuth callback URI, omitted when this deployment has no supported browser origin. */
|
|
oauthCallbackUrl?: string;
|
|
capabilities: ToolConnectionCreateCapabilities;
|
|
credentialSources: {
|
|
vercelConnect: {
|
|
available: boolean;
|
|
enabled: boolean;
|
|
authentication: "workload_oidc" | "access_token" | null;
|
|
manageUrl: string;
|
|
reason: string | null;
|
|
};
|
|
};
|
|
};
|
|
export type ToolMcpGatewaysResponse = { gateways: ToolMcpGatewayWithTokens[] };
|
|
export type CloudConnectorEnrollmentStatus = {
|
|
configured: boolean;
|
|
status: "not_configured" | "unenrolled" | "pending" | "active" | "suspended" | "unverified";
|
|
brokerBaseUrl: string;
|
|
instanceId: string | null;
|
|
environment: "development" | "test" | "staging" | "production";
|
|
origins: string[];
|
|
verificationUrl?: string;
|
|
expiresAt?: string;
|
|
};
|
|
export type CreateGatewayTokenInput = Omit<CreateToolMcpGatewayToken, "expiresAt"> & {
|
|
expiresAt?: string | Date | null;
|
|
};
|
|
/** Gateway update payload — `companyId` is injected by the client method. */
|
|
export type UpdateGatewayInput = Omit<UpdateToolMcpGateway, "companyId">;
|
|
export type ReviewNewToolsInput = {
|
|
decisions: Array<{ catalogEntryId: string; decision: ToolProfileNewToolReviewDecision }>;
|
|
};
|
|
|
|
export type StdioTemplateSummary = ToolStdioCommandTemplate;
|
|
export type StdioTemplatesResponse = { templates: StdioTemplateSummary[] };
|
|
|
|
/** Admin "run your own" command-template create input (M8b, PAP-10862). */
|
|
export interface CreateStdioTemplateInput {
|
|
templateId: string;
|
|
name: string;
|
|
description?: string | null;
|
|
command: string;
|
|
args?: string[];
|
|
envKeys?: string[];
|
|
}
|
|
|
|
export interface CreateToolApplicationInput {
|
|
name: string;
|
|
description?: string | null;
|
|
type: ToolApplication["type"];
|
|
pluginId?: string | null;
|
|
metadata?: Record<string, unknown> | null;
|
|
}
|
|
|
|
export interface UpdateToolApplicationInput {
|
|
name?: string;
|
|
description?: string | null;
|
|
status?: ToolApplication["status"];
|
|
metadata?: Record<string, unknown> | null;
|
|
}
|
|
|
|
export interface CreateToolConnectionInput {
|
|
applicationId?: string;
|
|
applicationName?: string;
|
|
name: string;
|
|
transport: NonNullable<ToolConnection["transport"]>;
|
|
status?: ToolConnection["status"];
|
|
config?: Record<string, unknown>;
|
|
credentialRefs?: ToolConnection["credentialRefs"];
|
|
enabled?: boolean;
|
|
}
|
|
|
|
export interface UpdateToolConnectionInput {
|
|
agentInstructions?: import("@paperclipai/shared").ConnectionAgentInstructions | null;
|
|
name?: string;
|
|
status?: ToolConnection["status"];
|
|
config?: Record<string, unknown>;
|
|
transportConfig?: Record<string, unknown>;
|
|
credentialRefs?: ToolConnection["credentialRefs"];
|
|
enabled?: boolean;
|
|
}
|
|
|
|
export interface ToolProfileEntryInput {
|
|
selectorType: ToolProfileEntrySelectorType;
|
|
effect?: ToolProfileEntryEffect;
|
|
applicationId?: string | null;
|
|
connectionId?: string | null;
|
|
catalogEntryId?: string | null;
|
|
toolName?: string | null;
|
|
riskLevel?: ToolRiskLevel | null;
|
|
conditions?: Record<string, unknown> | null;
|
|
}
|
|
|
|
export interface CreateToolProfileInput {
|
|
profileKey: string;
|
|
name: string;
|
|
description?: string | null;
|
|
status?: ToolProfileStatus;
|
|
defaultAction?: ToolProfileDefaultAction;
|
|
metadata?: Record<string, unknown> | null;
|
|
entries?: ToolProfileEntryInput[];
|
|
}
|
|
|
|
export interface UpdateToolProfileInput {
|
|
profileKey?: string;
|
|
name?: string;
|
|
description?: string | null;
|
|
status?: ToolProfileStatus;
|
|
defaultAction?: ToolProfileDefaultAction;
|
|
metadata?: Record<string, unknown> | null;
|
|
entries?: ToolProfileEntryInput[];
|
|
}
|
|
|
|
export interface ToolProfileBindingInput {
|
|
targetType: ToolProfileBindingTargetType;
|
|
targetId: string;
|
|
priority?: number;
|
|
metadata?: Record<string, unknown> | null;
|
|
}
|
|
|
|
export type UnbindToolProfileInput = Pick<ToolProfileBindingInput, "targetType" | "targetId">;
|
|
|
|
/** Redacted tool-gateway audit row (subset of `activity_log`). */
|
|
export interface ToolGatewayAuditRow {
|
|
id: string;
|
|
companyId: string;
|
|
action: string;
|
|
actorType: string | null;
|
|
actorId: string | null;
|
|
entityType: string | null;
|
|
entityId: string | null;
|
|
details: Record<string, unknown> | null;
|
|
createdAt: string;
|
|
}
|
|
|
|
/** Normalized outcome for the humanized Activity feed. */
|
|
export type ToolAuditOutcome =
|
|
| "allowed"
|
|
| "blocked"
|
|
| "asked_first"
|
|
| "waiting"
|
|
| "failed"
|
|
| "unknown";
|
|
|
|
/**
|
|
* Audit row enriched server-side with humanized display names and a normalized
|
|
* outcome — the shape returned by `GET /tool-gateway/audit`.
|
|
*/
|
|
export interface ToolGatewayActivityEvent extends ToolGatewayAuditRow {
|
|
agentId: string | null;
|
|
runId: string | null;
|
|
applicationId: string | null;
|
|
connectionId: string | null;
|
|
agentDisplayName: string | null;
|
|
actorDisplayName?: string | null;
|
|
appDisplayName: string | null;
|
|
applicationDisplayName: string | null;
|
|
connectionDisplayName: string | null;
|
|
toolDisplayName: string | null;
|
|
lifecycleType?: ToolConnectionLifecycleEventType | null;
|
|
normalizedOutcome: ToolAuditOutcome;
|
|
invocation: {
|
|
id: string;
|
|
toolName: string;
|
|
status: string;
|
|
policyDecision: string | null;
|
|
approvalState: string;
|
|
argumentsSummary: ToolRedactedValueSummary | null;
|
|
resultSummary: ToolRedactedValueSummary | null;
|
|
resultSizeBytes: number | null;
|
|
errorCode: string | null;
|
|
errorMessage: string | null;
|
|
startedAt: string | null;
|
|
completedAt: string | null;
|
|
} | null;
|
|
}
|
|
|
|
export type ToolGatewayActivityResponse = {
|
|
events: ToolGatewayActivityEvent[];
|
|
nextCursor: string | null;
|
|
};
|
|
|
|
export type ToolAuditWindow = "1h" | "24h" | "7d" | "30d" | "all";
|
|
|
|
export interface ListActivityParams {
|
|
gateway?: string | null;
|
|
app?: string | null;
|
|
agent?: string | null;
|
|
outcome?: string | null;
|
|
window?: ToolAuditWindow;
|
|
search?: string | null;
|
|
limit?: number;
|
|
cursor?: string | null;
|
|
}
|
|
|
|
export type ToolPolicyTestResponse = {
|
|
decision: ToolAccessDecision;
|
|
auditEvent: unknown | null;
|
|
};
|
|
|
|
export const toolsApi = {
|
|
getCloudConnectorEnrollment: () =>
|
|
api.get<CloudConnectorEnrollmentStatus>("/tools/oauth/cloud-connector/enrollment"),
|
|
startCloudConnectorEnrollment: (companyId: string, label?: string, returnTo?: string) =>
|
|
api.post<CloudConnectorEnrollmentStatus>("/tools/oauth/cloud-connector/enrollment", { companyId, label, returnTo }),
|
|
// --- Applications ---
|
|
listGallery: (companyId: string) =>
|
|
api.get<ToolGalleryResponse>(`/companies/${companyId}/tools/gallery`),
|
|
preflightAppMetadata: (companyId: string, galleryKey: string, methodKey?: string | null) => {
|
|
const query = methodKey ? `?methodKey=${encodeURIComponent(methodKey)}` : "";
|
|
return api.get<ToolAppMetadataPreflightResult>(
|
|
`/companies/${companyId}/tools/apps/${encodeURIComponent(galleryKey)}/preflight${query}`,
|
|
);
|
|
},
|
|
connectApp: (companyId: string, input: ConnectToolApp) =>
|
|
api.post<ConnectToolAppResult>(`/companies/${companyId}/tools/apps/connect`, input),
|
|
startOAuth: (
|
|
connectionId: string,
|
|
input: {
|
|
asCurrentUser?: boolean;
|
|
asAgentId?: string;
|
|
interactionId?: string;
|
|
workspaceServices?: import("@paperclipai/shared").GoogleWorkspaceServiceSelection;
|
|
} = {},
|
|
) =>
|
|
api.post<ToolOAuthStartResult>(`/tools/oauth/${connectionId}/start`, input),
|
|
finalizeOAuthAccess: (
|
|
companyId: string,
|
|
connectionId: string,
|
|
input: { grantKind: ConnectionGrantKind },
|
|
) =>
|
|
api.post<FinishToolAppResult>(
|
|
`/companies/${companyId}/tools/apps/${connectionId}/finalize-oauth-access`,
|
|
input,
|
|
),
|
|
finishApp: (companyId: string, connectionId: string, input: {
|
|
enabledCatalogEntryIds: string[];
|
|
askFirstCatalogEntryIds: string[];
|
|
reviewedCatalogEntryIds?: string[];
|
|
agentInstructions?: import("@paperclipai/shared").ConnectionAgentInstructions | null;
|
|
access: "all_agents" | { agentIds: string[] };
|
|
}) =>
|
|
api.post<FinishToolAppResult>(
|
|
`/companies/${companyId}/tools/apps/${connectionId}/finish`,
|
|
input,
|
|
),
|
|
listAppsAttention: (companyId: string) =>
|
|
api.get<ToolAppsAttentionResponse>(`/companies/${companyId}/tools/apps/attention`),
|
|
listApplications: (companyId: string) =>
|
|
api.get<ToolApplicationsResponse>(`/companies/${companyId}/tools/applications`),
|
|
createApplication: (companyId: string, input: CreateToolApplicationInput) =>
|
|
api.post<ToolApplication>(`/companies/${companyId}/tools/applications`, input),
|
|
updateApplication: (applicationId: string, input: UpdateToolApplicationInput) =>
|
|
api.patch<ToolApplication>(`/tool-applications/${applicationId}`, input),
|
|
deleteApplication: (applicationId: string) =>
|
|
api.delete<ToolApplication>(`/tool-applications/${applicationId}`),
|
|
|
|
// --- Connections ---
|
|
listConnections: (companyId: string) =>
|
|
api.get<ToolConnectionsResponse>(`/companies/${companyId}/tools/connections`),
|
|
getConnection: (connectionId: string) =>
|
|
api.get<ToolConnection>(`/tool-connections/${connectionId}`),
|
|
// --- Installs (Phase 3b, PAP-13618): which agents carry this connection's
|
|
// tools in their runtime context. `installed ⊆ permitted`; the server
|
|
// auto-extends access (adds profile bindings) for any newly-installed target.
|
|
getConnectionInstalls: (connectionId: string) =>
|
|
api.get<{ connectionId: string; installs: ToolConnectionInstall[] }>(
|
|
`/tool-connections/${connectionId}/installs`,
|
|
),
|
|
putConnectionInstalls: (
|
|
connectionId: string,
|
|
installs: Array<{ targetType: "company" | "agent"; targetId: string }>,
|
|
) =>
|
|
api.put<ToolConnectionInstallSnapshot>(
|
|
`/tool-connections/${connectionId}/installs`,
|
|
{ installs },
|
|
),
|
|
// --- Identity grants (PAP-17835): who a connection acts as. The response
|
|
// carries server-computed capabilities and the audience member directory, so
|
|
// the UI never rebuilds the permission matrix from `membershipRole`.
|
|
listConnectionGrants: (connectionId: string, options?: RequestOptions) =>
|
|
api.get<ConnectionGrantsResponse>(`/tool-connections/${connectionId}/grants`, { timeoutMs: 15_000, ...options }),
|
|
createConnectionGrantDelegation: (connectionId: string, grantId: string, agentId: string) =>
|
|
api.post<ConnectionGrantDelegation>(
|
|
`/tool-connections/${connectionId}/grants/${grantId}/delegations`,
|
|
{ agentId },
|
|
),
|
|
revokeConnectionGrantDelegation: (
|
|
connectionId: string,
|
|
grantId: string,
|
|
delegationId: string,
|
|
) => api.delete<ConnectionGrantDelegation>(
|
|
`/tool-connections/${connectionId}/grants/${grantId}/delegations/${delegationId}`,
|
|
),
|
|
revokeConnectionGrant: (connectionId: string, grantId: string) =>
|
|
api.delete<ConnectionGrant>(`/tool-connections/${connectionId}/grants/${grantId}`),
|
|
// An empty `memberUserIds` is the canonical "all organization members".
|
|
// Replacement is atomic server-side, so this never partially widens access.
|
|
replaceConnectionGrantMembers: (connectionId: string, grantId: string, memberUserIds: string[]) =>
|
|
api.put<ConnectionGrant>(
|
|
`/tool-connections/${connectionId}/grants/${grantId}/members`,
|
|
{ memberUserIds },
|
|
),
|
|
/**
|
|
* Start the signed-in user's own personal authorization. `subjectUserId` must
|
|
* be the caller: the server refuses any other subject, so there is no way to
|
|
* initiate consent on someone else's behalf.
|
|
*/
|
|
startPersonalAuthorization: (
|
|
companyId: string,
|
|
connectionId: string,
|
|
input: { subjectUserId: string; scopes?: string[]; returnTo?: string },
|
|
) =>
|
|
api.post<{ url: string; handoff?: ToolOAuthStartResult["handoff"] }>(
|
|
`/companies/${companyId}/tools/connections/${connectionId}/start-authorization`,
|
|
input,
|
|
),
|
|
createConnection: (companyId: string, input: CreateToolConnectionInput) =>
|
|
api.post<ToolConnection>(`/companies/${companyId}/tools/connections`, input),
|
|
updateConnection: (connectionId: string, input: UpdateToolConnectionInput) =>
|
|
api.patch<ToolConnection>(`/tool-connections/${connectionId}`, input),
|
|
configureRailwaySsh: (connectionId: string, input: ConfigureRailwaySsh) =>
|
|
api.post<RailwaySshSetup | null>(`/tool-connections/${connectionId}/railway/ssh`, input),
|
|
// Removal is a credential-revoking teardown (PAP-17119), so the response
|
|
// carries the cleanup receipt alongside the archived connection.
|
|
archiveConnection: (connectionId: string) =>
|
|
api.delete<ToolConnection & { removal: ToolConnectionRemovalSummary }>(
|
|
`/tool-connections/${connectionId}`,
|
|
),
|
|
checkConnectionHealth: (connectionId: string) =>
|
|
api.post<ToolConnectionHealthCheckResult>(`/tool-connections/${connectionId}/health-check`, {}),
|
|
reconnectConnection: (connectionId: string, credentialValues: Record<string, string>) =>
|
|
api.post<ToolConnectionHealthCheckResult>(`/tool-connections/${connectionId}/reconnect`, {
|
|
credentialValues,
|
|
}),
|
|
refreshCatalog: (connectionId: string) =>
|
|
api.post<ToolCatalogRefreshResult>(`/tool-connections/${connectionId}/catalog/refresh`, {}),
|
|
listCatalog: (connectionId: string) =>
|
|
api.get<ToolCatalogResponse>(`/tool-connections/${connectionId}/catalog`),
|
|
listConnectionActivity: (connectionId: string, limit = 20) =>
|
|
api.get<ToolConnectionActivityResponse>(
|
|
`/tool-connections/${connectionId}/activity?limit=${limit}`,
|
|
),
|
|
listTestAgents: (connectionId: string) =>
|
|
api.get<ToolConnectionTestAgentsResponse>(
|
|
`/tool-connections/${connectionId}/test-agents`,
|
|
),
|
|
getTestAgentAccess: (connectionId: string, agentId: string) =>
|
|
api.get<ToolConnectionTestAgentAccessResponse>(
|
|
`/tool-connections/${connectionId}/test-agents/${agentId}/access`,
|
|
),
|
|
setupComposioApp: (connectionId: string, toolkit: string, input: ComposioAppSetupInput) =>
|
|
api.post<ComposioAppSetupResult>(`/tool-connections/${connectionId}/composio/apps/${encodeURIComponent(toolkit)}/setup`, input),
|
|
listAggregatorApps: (connectionId: string) => {
|
|
const path = `/tool-connections/${connectionId}/aggregator/apps`;
|
|
// React Query already deduplicates within a viewing-user key. Path-only
|
|
// request coalescing could hand a previous user's in-flight response to a new account.
|
|
detachInflightGet(path);
|
|
return api.get<AggregatorAppsResponse>(path, { cache: "no-store" });
|
|
},
|
|
syncAggregatorApps: (connectionId: string, force = false) => api.post<AggregatorAppsResponse>(`/tool-connections/${connectionId}/aggregator/apps/sync`, { force }),
|
|
refreshAggregatorApps: (connectionId: string, toolkits: string[] = []) => api.post<AggregatorAppsResponse>(`/tool-connections/${connectionId}/aggregator/apps/refresh`, { toolkits }),
|
|
configureArcadeDiscovery: (connectionId: string, input: ArcadeDiscoverySetupInput) => api.put<AggregatorAppsResponse>(`/tool-connections/${connectionId}/aggregator/discovery`, input),
|
|
listComposioApps: (connectionId: string) => api.get<ComposioAppsResponse>(`/tool-connections/${connectionId}/composio/apps`),
|
|
syncComposioApps: (connectionId: string, force = false) => api.post<ComposioAppsResponse>(`/tool-connections/${connectionId}/composio/apps/sync`, { force }),
|
|
refreshComposioApps: (connectionId: string, toolkits: string[]) => api.post<ComposioAppsResponse>(`/tool-connections/${connectionId}/composio/apps/refresh`, { toolkits }),
|
|
manageComposioAppAccount: (connectionId: string, toolkit: string, input: ComposioAppAccountInput) =>
|
|
api.post<ComposioAppSetupResult & { apps: ComposioAppSnapshot[] }>(`/tool-connections/${connectionId}/composio/apps/${encodeURIComponent(toolkit)}/accounts`, input),
|
|
runTestCall: (
|
|
connectionId: string,
|
|
input: { agentId: string; toolName: string; parameters?: Record<string, unknown> },
|
|
) =>
|
|
api.post<ToolConnectionTestCallResult>(
|
|
`/tool-connections/${connectionId}/test-calls`,
|
|
input,
|
|
),
|
|
getTestCallStatus: (connectionId: string, actionRequestId: string) =>
|
|
api.get<ToolConnectionTestCallStatus>(
|
|
`/tool-connections/${connectionId}/test-calls/${actionRequestId}`,
|
|
),
|
|
importMcpJson: (companyId: string, body: { mcpJson: unknown }) =>
|
|
api.post<McpJsonImportPreview>(`/companies/${companyId}/tools/mcp/import-json`, body),
|
|
listStdioTemplates: (companyId: string) =>
|
|
api.get<StdioTemplatesResponse>(`/companies/${companyId}/tools/stdio-templates`),
|
|
createStdioTemplate: (companyId: string, input: CreateStdioTemplateInput) =>
|
|
api.post<StdioTemplateSummary>(`/companies/${companyId}/tools/stdio-templates`, input),
|
|
disableStdioTemplate: (companyId: string, templateId: string, reason?: string | null) =>
|
|
api.post<StdioTemplateSummary>(
|
|
`/companies/${companyId}/tools/stdio-templates/${encodeURIComponent(templateId)}/disable`,
|
|
{ reason: reason ?? null },
|
|
),
|
|
|
|
// --- Profiles ---
|
|
listProfiles: (companyId: string) =>
|
|
api.get<ToolProfilesResponse>(`/companies/${companyId}/tools/profiles`),
|
|
getProfileNewTools: (profileId: string) =>
|
|
api.get<ToolProfileNewToolsReview>(`/tool-profiles/${profileId}/new-tools`),
|
|
reviewProfileNewTools: (profileId: string, input: ReviewNewToolsInput) =>
|
|
api.post<ToolProfileNewToolsReviewResult>(`/tool-profiles/${profileId}/new-tools/review`, input),
|
|
createProfile: (companyId: string, input: CreateToolProfileInput) =>
|
|
api.post<ToolProfileWithDetails>(`/companies/${companyId}/tools/profiles`, input),
|
|
updateProfile: (profileId: string, input: UpdateToolProfileInput) =>
|
|
api.patch<ToolProfileWithDetails>(`/tool-profiles/${profileId}`, input),
|
|
duplicateProfile: (
|
|
profileId: string,
|
|
input: { name: string; includeAssignments?: boolean },
|
|
) => api.post<ToolProfileWithDetails>(`/tool-profiles/${profileId}/duplicate`, input),
|
|
deleteProfile: (
|
|
profileId: string,
|
|
input: { force?: boolean; reassignToProfileId?: string } = {},
|
|
) => api.delete<{ deleted: true }>(`/tool-profiles/${profileId}`, input),
|
|
addProfileEntry: (profileId: string, input: ToolProfileEntryInput) =>
|
|
api.post<ToolProfileEntry>(`/tool-profiles/${profileId}/entries`, input),
|
|
updateProfileEntry: (entryId: string, input: Partial<ToolProfileEntryInput>) =>
|
|
api.patch<ToolProfileEntry>(`/tool-profile-entries/${entryId}`, input),
|
|
deleteProfileEntry: (entryId: string) =>
|
|
api.delete<ToolProfileEntry>(`/tool-profile-entries/${entryId}`),
|
|
bindProfile: (companyId: string, profileId: string, input: ToolProfileBindingInput) =>
|
|
api.post<ToolProfileBinding>(`/companies/${companyId}/tools/profiles/${profileId}/bind`, input),
|
|
unbindProfile: (companyId: string, profileId: string, input: UnbindToolProfileInput) =>
|
|
api.post<{ unbound: number }>(`/companies/${companyId}/tools/profiles/${profileId}/unbind`, input),
|
|
getEffectiveProfilesForAgent: (companyId: string, agentId: string) =>
|
|
api.get<ToolProfileEffectiveSummary>(
|
|
`/companies/${companyId}/tools/profiles/effective/agents/${encodeURIComponent(agentId)}`,
|
|
),
|
|
|
|
// --- Runtime ---
|
|
listRuntimeSlots: (companyId: string) =>
|
|
api.get<ToolRuntimeSlotsResponse>(`/companies/${companyId}/tools/runtime-slots`),
|
|
stopRuntimeSlot: (companyId: string, slotId: string) =>
|
|
api.post<ToolRuntimeSlot>(`/companies/${companyId}/tools/runtime-slots/${slotId}/stop`, {}),
|
|
restartRuntimeSlot: (companyId: string, slotId: string) =>
|
|
api.post<ToolRuntimeSlot>(`/companies/${companyId}/tools/runtime-slots/${slotId}/restart`, {}),
|
|
getRuntimeHealth: (companyId: string) =>
|
|
api.get<ToolRuntimeHealthResponse>(`/companies/${companyId}/tools/runtime-health`),
|
|
getRunDecisionLookup: (companyId: string, runId: string) =>
|
|
api.get<ToolRunDecisionLookup>(`/companies/${companyId}/tools/runs/${runId}/decisions`),
|
|
listLiveRuntimeSlots: (companyId: string) =>
|
|
api.get<ToolRuntimeSlot[]>(`/tool-gateway/runtime-slots?companyId=${encodeURIComponent(companyId)}`),
|
|
|
|
// --- Policies (trust rules + decision simulator) ---
|
|
listPolicies: (companyId: string) =>
|
|
api.get<ToolPoliciesResponse>(`/companies/${companyId}/tools/policies`),
|
|
createPolicy: (companyId: string, input: CreateToolPolicy) =>
|
|
api.post<ToolPolicy>(`/companies/${companyId}/tools/policies`, input),
|
|
reorderPolicies: (companyId: string, input: ReorderToolPolicies) =>
|
|
api.post<ToolPoliciesResponse>(`/companies/${companyId}/tools/policies/reorder`, input),
|
|
duplicatePolicy: (companyId: string, policyId: string, input: DuplicateToolPolicy = {}) =>
|
|
api.post<ToolPolicy>(`/companies/${companyId}/tools/policies/${policyId}/duplicate`, input),
|
|
updatePolicy: (companyId: string, policyId: string, input: UpdateToolPolicy) =>
|
|
api.patch<ToolPolicy>(`/companies/${companyId}/tools/policies/${policyId}`, input),
|
|
deletePolicy: (companyId: string, policyId: string) =>
|
|
api.delete<ToolPolicy>(`/companies/${companyId}/tools/policies/${policyId}`),
|
|
listTrustRules: (companyId: string) =>
|
|
api.get<ToolTrustRulesResponse>(`/companies/${companyId}/tools/trust-rules`),
|
|
revokeTrustRule: (companyId: string, policyId: string, reason?: string | null) =>
|
|
api.post<ToolPolicy>(`/companies/${companyId}/tools/trust-rules/${policyId}/revoke`, {
|
|
reason: reason ?? null,
|
|
}),
|
|
testPolicy: (companyId: string, input: Omit<ToolAccessDecisionInput, "companyId">) =>
|
|
api.post<ToolPolicyTestResponse>(`/companies/${companyId}/tools/policy/test`, input),
|
|
|
|
// --- Review queue (Ask first) ---
|
|
listActionRequests: (companyId: string, status: ToolActionRequestStatus = "pending") =>
|
|
api.get<ToolActionRequestsResponse>(
|
|
`/companies/${companyId}/tools/action-requests?status=${encodeURIComponent(status)}`,
|
|
),
|
|
approveActionRequest: (companyId: string, actionRequestId: string, rememberAction = false) =>
|
|
api.post<ToolActionRequest>(`/tool-gateway/action-requests/${actionRequestId}/approve`, { companyId, rememberAction }),
|
|
declineActionRequest: (companyId: string, actionRequestId: string, reason?: string) =>
|
|
api.post<ToolActionRequest>(`/tool-gateway/action-requests/${actionRequestId}/decline`, { companyId, reason }),
|
|
createTrustRuleFromActionRequest: (
|
|
companyId: string,
|
|
actionRequestId: string,
|
|
input: CreateToolTrustRuleFromActionRequest = {},
|
|
) =>
|
|
api.post<ToolPolicy>(
|
|
`/companies/${companyId}/tools/action-requests/${actionRequestId}/trust-rule`,
|
|
input,
|
|
),
|
|
|
|
// --- Named MCP gateways ---
|
|
listGateways: (companyId: string) =>
|
|
api.get<ToolMcpGatewaysResponse>(`/companies/${companyId}/tools/gateways`),
|
|
createGateway: (companyId: string, input: CreateToolMcpGateway) =>
|
|
api.post<ToolMcpGatewayWithTokens>(`/companies/${companyId}/tools/gateways`, input),
|
|
updateGateway: (companyId: string, gatewayId: string, input: UpdateGatewayInput) =>
|
|
api.patch<ToolMcpGatewayWithTokens>(`/tool-gateway/gateways/${gatewayId}`, { ...input, companyId }),
|
|
createGatewayToken: (companyId: string, gatewayId: string, input: CreateGatewayTokenInput) =>
|
|
api.post<ToolMcpGatewayTokenCreated>(`/tool-gateway/gateways/${gatewayId}/tokens`, { ...input, companyId }),
|
|
revokeGatewayToken: (companyId: string, tokenId: string) =>
|
|
api.post<ToolMcpGatewayToken>(`/tool-gateway/gateway-tokens/${tokenId}/revoke`, { companyId }),
|
|
|
|
// --- Audit / Activity ---
|
|
/**
|
|
* Humanized Activity feed with server-side filters and cursor pagination.
|
|
* Returns `{ events, nextCursor }`.
|
|
*/
|
|
listActivity: (companyId: string, params: ListActivityParams = {}) => {
|
|
const search = new URLSearchParams({ companyId });
|
|
if (params.gateway) search.set("gateway", params.gateway);
|
|
if (params.app) search.set("app", params.app);
|
|
if (params.agent) search.set("agent", params.agent);
|
|
if (params.outcome) search.set("outcome", params.outcome);
|
|
// Omitting the window is the API's canonical all-time request. This also
|
|
// keeps the page usable during a rolling restart against an older server
|
|
// that does not recognize the newer explicit `all` value.
|
|
if (params.window && params.window !== "all") search.set("window", params.window);
|
|
if (params.search) search.set("search", params.search);
|
|
if (params.cursor) search.set("cursor", params.cursor);
|
|
search.set("limit", String(params.limit ?? 50));
|
|
return api.get<ToolGatewayActivityResponse>(`/tool-gateway/audit?${search.toString()}`);
|
|
},
|
|
/** Flat audit sample (no pagination) — used by derived counters/banners. */
|
|
listAudit: (companyId: string, limit = 100) =>
|
|
api
|
|
.get<ToolGatewayActivityResponse>(
|
|
`/tool-gateway/audit?companyId=${encodeURIComponent(companyId)}&window=30d&limit=${Math.min(limit, 100)}`,
|
|
)
|
|
.then((res) => res.events),
|
|
};
|