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. > - Projects give tasks a common source repository and execution context. > - The current project form asks for a raw URL and unrelated metadata. > - Teams need to select several repos from GitHub connections they can use. > - This pull request implements the reviewed project form and repository editor. > - The server checks credential ownership and shared audiences before discovery. > - Existing workspace URLs and runtime identity rules remain compatible. ## Linked Issues or Issue Description **Problem or motivation** Project creation accepts one raw repository URL. It does not help users select repos from their usable GitHub connections or attach several repos together. **Proposed solution** Add a shared GitHub repository picker to project creation and Configuration. Support multiple selections, transactional persistence, and the existing GitHub setup flow. Simplify the project form and Configuration tab as reviewed. **Alternatives considered** Keep a raw URL field or add a separate repository table. The existing workspace collection already supports several repositories and keeps legacy URLs compatible. **Roadmap alignment** This builds on the shipped MCP Tool Gateway and Apps capability. It does not change runtime credential delegation. Related work: #11662 addresses the existing dialog's viewport limits. #4552 addresses generic Git URLs; this change preserves those URLs in existing workspaces. ## What Changed - Add company-scoped repository discovery from usable personal and shared GitHub grants, with provider-ID deduplication, PAT pagination, and partial failure handling. - Document the repository endpoints and board access requirements in OpenAPI. - Validate new selections and save projects with multiple repository workspaces in one transaction. Preserve legacy URLs and existing selections whose access was lost. - Implement the reviewed Create project dialog, shared repository editor, scrolling, and mobile layout. - Move repositories above environment variables, remove Status and Goals controls and env help paragraphs, move Created to the bottom, and redirect Overview to Configuration. - Reuse GitHub setup in dialogs, preserve project drafts, and verify popup completion through the API. - Replace the configuration story's DOM adapter with explicit production composition. Keep the reviewed mobile and short-viewport stories. ## Verification - Passed: `pnpm build`, `pnpm -r typecheck`, `pnpm build-storybook`, and `pnpm check:token-gates`. - Passed: focused repository access, database persistence, configuration, and connection setup tests. - Passed: `pnpm exec playwright test --config tests/e2e/playwright.config.ts tests/e2e/project-repositories.spec.ts`. - The browser tests use a real temporary server/database. They cover create, forty persisted repos, mobile scrolling, save/reload, legacy URL editing, and rejection without a partial project. - GitHub responses and popup completion use deterministic fixtures. No real GitHub account was authorized by the test suite. - All CI general, serialized server, and browser test shards pass on the final commit. - The local full-suite run overlapped review edits and was stopped; fresh repository, OpenAPI, UI/CLI, and connection tests pass. Unrelated local worker, built-in-agent, and routine timing/socket failures passed isolated reruns. - Final commit `1b3308dca`: all CI gates pass, including build, runner verification, typecheck, canary dry run, and security checks. Greptile is 5/5 with no unresolved review threads. - Storybook visual regression is opt-in and was skipped by CI; the Storybook build passed locally. ## Risks - Repository discovery depends on provider availability. Failed connections are reported while successful results stay usable. - Selections identify source workspaces; they do not grant agents new credentials. The existing primary-workspace and responsible-user identity rules still apply. - No database migration is needed. Existing API status, goals, dates, and manual workspace URLs remain supported. ## Model Used OpenAI Codex, based on GPT-6, with repository inspection, code execution, and browser tools. The runtime does not expose a more specific model deployment ID or 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 (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>
8438 lines
272 KiB
TypeScript
8438 lines
272 KiB
TypeScript
import { experimentalApiMetadata } from "./experimental-api-metadata.js";
|
|
import { experimentalApiPaths, experimentalApiQueries } from "./experimental-api-paths.js";
|
|
import { Router } from "express";
|
|
import { z } from "zod";
|
|
import {
|
|
// Agent
|
|
createAgentSchema,
|
|
createAgentHireSchema,
|
|
updateAgentSchema,
|
|
updateAgentPermissionsSchema,
|
|
updateAgentInstructionsPathSchema,
|
|
updateAgentInstructionsBundleSchema,
|
|
upsertAgentInstructionsFileSchema,
|
|
createAgentKeySchema,
|
|
builtInAgentEmptyMutationSchema,
|
|
builtInAgentProvisionSchema,
|
|
generateSummarySlotSchema,
|
|
writeSummarySlotSchema,
|
|
createStatusCardSchema,
|
|
patchStatusCardSchema,
|
|
refreshStatusCardSchema,
|
|
writeStatusCardQuerySchema,
|
|
writeStatusCardSummarySchema,
|
|
wakeAgentSchema,
|
|
resetAgentSessionSchema,
|
|
agentSkillSyncSchema,
|
|
testAdapterEnvironmentSchema,
|
|
// Issue
|
|
createIssueSchema,
|
|
updateIssueSchema,
|
|
stalledReviewDecisionSchema,
|
|
createIssueLabelSchema,
|
|
addIssueCommentSchema,
|
|
checkoutIssueSchema,
|
|
linkIssueApprovalSchema,
|
|
createIssueWorkProductSchema,
|
|
updateIssueWorkProductSchema,
|
|
upsertIssueDocumentSchema,
|
|
restoreIssueDocumentRevisionSchema,
|
|
upsertIssueFeedbackVoteSchema,
|
|
upsertIssueWatchdogSchema,
|
|
// Project
|
|
createProjectSchema,
|
|
updateProjectSchema,
|
|
createProjectWorkspaceSchema,
|
|
updateProjectWorkspaceSchema,
|
|
// Company
|
|
createCompanySchema,
|
|
updateCompanySchema,
|
|
updateCompanyBrandingSchema,
|
|
companyArtifactsQuerySchema,
|
|
companyArtifactsResponseSchema,
|
|
// Decisions
|
|
addDecisionQueueItemSchema,
|
|
createDecisionQueueSchema,
|
|
createDecisionArchiveProposalSchema,
|
|
decisionAttentionSourceKindSchema,
|
|
decisionInputsSchema,
|
|
decisionOptionsSchema,
|
|
removeDecisionQueueItemSchema,
|
|
updateDecisionQueueSchema,
|
|
updateDecisionTriageSchema,
|
|
updateDecisionRetentionSchema,
|
|
// Routine
|
|
createRoutineSchema,
|
|
updateRoutineSchema,
|
|
createRoutineTriggerSchema,
|
|
updateRoutineTriggerSchema,
|
|
rotateRoutineTriggerSecretSchema,
|
|
runRoutineSchema,
|
|
// Folders
|
|
createFolderSchema,
|
|
ensureMySkillFolderSchema,
|
|
folderKindSchema,
|
|
moveFolderItemSchema,
|
|
moveFolderSchema,
|
|
updateFolderSchema,
|
|
// Goal
|
|
createGoalSchema,
|
|
updateGoalSchema,
|
|
// Secret
|
|
createSecretSchema,
|
|
updateSecretSchema,
|
|
rotateSecretSchema,
|
|
rotateUserSecretValueSchema,
|
|
createUserSecretDefinitionSchema,
|
|
updateUserSecretDefinitionSchema,
|
|
createUserSecretValueSchema,
|
|
updateUserSecretValueSchema,
|
|
// Approval
|
|
createApprovalSchema,
|
|
resolveApprovalSchema,
|
|
requestApprovalRevisionSchema,
|
|
resubmitApprovalSchema,
|
|
addApprovalCommentSchema,
|
|
// Cost / budget
|
|
createCostEventSchema,
|
|
createFinanceEventSchema,
|
|
updateBudgetSchema,
|
|
upsertBudgetPolicySchema,
|
|
resolveBudgetIncidentSchema,
|
|
// Sidebar
|
|
upsertSidebarOrderPreferenceSchema,
|
|
// Execution workspaces
|
|
reconcileExecutionWorkspaceBranchSchema,
|
|
updateExecutionWorkspaceSchema,
|
|
workspaceOverviewQuerySchema,
|
|
workspaceRuntimeControlTargetSchema,
|
|
// Environments
|
|
createEnvironmentSchema,
|
|
cancelEnvironmentCustomImageSetupSessionSchema,
|
|
createEnvironmentCustomImageTerminalSessionTokenSchema,
|
|
environmentCustomImageSetupSessionSchema,
|
|
environmentCustomImageTerminalSessionTokenSchema,
|
|
environmentCustomImageTemplateSchema,
|
|
finishEnvironmentCustomImageSetupSessionSchema,
|
|
relinkEnvironmentCustomImageTemplateSchema,
|
|
updateEnvironmentSchema,
|
|
probeEnvironmentConfigSchema,
|
|
startEnvironmentCustomImageSetupSessionSchema,
|
|
// Company skills
|
|
companySkillCreateSchema,
|
|
companySkillFileDeleteSchema,
|
|
companySkillFileUpdateSchema,
|
|
companySkillImportSchema,
|
|
companySkillProjectBrowseRequestSchema,
|
|
companySkillProjectBrowseResultSchema,
|
|
companySkillProjectScanRequestSchema,
|
|
companySkillProjectScanResultSchema,
|
|
companySkillRenameResultSchema,
|
|
companySkillRenameSchema,
|
|
companySkillTestInputCreateSchema,
|
|
companySkillTestInputUpdateSchema,
|
|
companySkillTestRunCreateSchema,
|
|
companySkillTestRunListQuerySchema,
|
|
companySkillTestRunTemplateCreateSchema,
|
|
companySkillTestRunTemplateUpdateSchema,
|
|
evaluateSkillPolicySchema,
|
|
replaceSkillPolicySchema,
|
|
updateInboxAgentPolicySchema,
|
|
// Issue tree
|
|
createIssueTreeHoldSchema,
|
|
previewIssueTreeControlSchema,
|
|
releaseIssueTreeHoldSchema,
|
|
// Issue interactions
|
|
createIssueThreadInteractionSchema,
|
|
createChildIssueSchema,
|
|
acceptIssueThreadInteractionSchema,
|
|
rejectIssueThreadInteractionSchema,
|
|
respondIssueThreadInteractionSchema,
|
|
skipIssueThreadInteractionSchema,
|
|
submitIssueThreadInteractionVerdictsSchema,
|
|
withdrawIssueThreadInteractionSchema,
|
|
// Auth / profile
|
|
updateCurrentUserProfileSchema,
|
|
// Company portability (legacy routes)
|
|
companyPortabilityExportSchema,
|
|
companyPortabilityPreviewSchema,
|
|
companyPortabilityImportSchema,
|
|
// Access / membership
|
|
acceptInviteSchema,
|
|
createCompanyInviteSchema,
|
|
createOpenClawInvitePromptSchema,
|
|
claimJoinRequestApiKeySchema,
|
|
createCliAuthChallengeSchema,
|
|
resolveCliAuthChallengeSchema,
|
|
createBoardApiKeySchema,
|
|
updateCompanyMemberSchema,
|
|
updateCompanyMemberWithPermissionsSchema,
|
|
archiveCompanyMemberSchema,
|
|
updateMemberPermissionsSchema,
|
|
updateUserCompanyAccessSchema,
|
|
// Instance settings
|
|
patchInstanceGeneralSettingsSchema,
|
|
patchInstanceExperimentalSettingsSchema,
|
|
patchInstanceSettingsSchema,
|
|
startTaskDrainRequestSchema,
|
|
// Resource memberships
|
|
updateDocumentResourceMembershipSchema,
|
|
updateResourceMembershipSchema,
|
|
// Document annotations
|
|
createDocumentAnnotationCommentSchema,
|
|
createDocumentAnnotationThreadSchema,
|
|
updateDocumentAnnotationThreadSchema,
|
|
// Issue recovery and decomposition
|
|
createAcceptedPlanDecompositionSchema,
|
|
resolveIssueRecoveryActionSchema,
|
|
cancelIssueThreadInteractionSchema,
|
|
// Secret provider configs and remote import
|
|
createSecretProviderConfigSchema,
|
|
updateSecretProviderConfigSchema,
|
|
secretProviderConfigDiscoveryPreviewSchema,
|
|
remoteSecretImportPreviewSchema,
|
|
remoteSecretImportSchema,
|
|
workspaceFileAvailabilityRequestSchema,
|
|
workspaceFileAvailabilityResponseSchema,
|
|
workspaceFileListQuerySchema,
|
|
workspaceFileResourceQuerySchema,
|
|
// Tool access
|
|
connectToolAppSchema,
|
|
createToolApplicationSchema,
|
|
updateToolApplicationSchema,
|
|
createToolConnectionSchema,
|
|
createConnectionGrantDelegationSchema,
|
|
connectionTokenRequestSchema,
|
|
startConnectionAuthorizationSchema,
|
|
createToolStdioCommandTemplateSchema,
|
|
disableToolStdioCommandTemplateSchema,
|
|
finalizeOAuthAccessSchema,
|
|
finishToolAppSchema,
|
|
reconnectToolAppSchema,
|
|
startToolOAuthSchema,
|
|
updateToolConnectionSchema,
|
|
putToolConnectionInstallsSchema,
|
|
toolConnectionTestCallSchema,
|
|
createToolPolicySchema,
|
|
duplicateToolPolicySchema,
|
|
createToolProfileBindingForProfileSchema,
|
|
createToolProfileEntryForProfileSchema,
|
|
createToolProfileWithEntriesSchema,
|
|
deleteToolProfileSchema,
|
|
duplicateToolProfileSchema,
|
|
reorderToolPoliciesSchema,
|
|
reviewToolProfileNewToolsSchema,
|
|
updateToolPolicySchema,
|
|
updateToolProfileEntrySchema,
|
|
updateToolProfileWithEntriesSchema,
|
|
createToolTrustRuleFromActionRequestSchema,
|
|
revokeToolTrustRuleSchema,
|
|
unbindToolProfileBindingSchema,
|
|
importMcpJsonSchema,
|
|
toolPolicyTestRequestSchema,
|
|
createToolMcpGatewaySchema,
|
|
completeConnectionIntentSchema,
|
|
connectionRequestInputSchema,
|
|
connectionsSearchInputSchema,
|
|
declineConnectionIntentSchema,
|
|
startClaudeSetupTokenSessionRequestSchema,
|
|
submitBrowserCodeRequestSchema,
|
|
claudeSetupTokenSessionResponseSchema,
|
|
claudeSetupTokenSessionPromptSchema,
|
|
claudeSetupTokenSessionOwnerResponseSchema,
|
|
claudeSetupTokenCompletionResponseSchema,
|
|
claudeOAuthTokenStatusResponseSchema,
|
|
startAdapterAuthSessionRequestSchema,
|
|
} from "@paperclipai/shared";
|
|
import {
|
|
COMPANY_IMPORT_TRANSFERS_API_PATH,
|
|
companyImportTransferDeclarationSchema,
|
|
} from "@paperclipai/shared/company-import-transfer";
|
|
|
|
type JsonSchema = Record<string, unknown>;
|
|
type OpenApiResponse = Record<string, unknown>;
|
|
type OpenApiPathRegistration = {
|
|
method: string;
|
|
path: string;
|
|
request?: {
|
|
params?: z.ZodTypeAny;
|
|
query?: z.ZodTypeAny;
|
|
body?: {
|
|
content: Record<string, { schema: unknown }>;
|
|
required?: boolean;
|
|
};
|
|
};
|
|
responses?: Record<string, OpenApiResponse>;
|
|
[key: string]: unknown;
|
|
};
|
|
|
|
// Zod 4 stores each schema definition on `_def` with a lowercase `type`
|
|
// discriminator and moves the wrapped members onto that def. This loose view
|
|
// lets the converter read those members, because Zod 4 does not export a
|
|
// public type for every internal def shape.
|
|
type ZodDefAny = Record<string, unknown> & { type: string };
|
|
|
|
const zodDef = (schema: z.ZodTypeAny): ZodDefAny => schema._def as unknown as ZodDefAny;
|
|
const zodTypeName = (schema: z.ZodTypeAny): string => zodDef(schema).type;
|
|
|
|
function unwrapSchema(schema: z.ZodTypeAny): z.ZodTypeAny {
|
|
const def = zodDef(schema);
|
|
if (def.type === "optional" || def.type === "default" || def.type === "catch") {
|
|
return unwrapSchema(def.innerType as z.ZodTypeAny);
|
|
}
|
|
// A `.transform()` or `.pipe()` becomes a pipe. Read the input schema so the
|
|
// published contract describes the value a client sends.
|
|
if (def.type === "pipe") {
|
|
return unwrapSchema(def.in as z.ZodTypeAny);
|
|
}
|
|
return schema;
|
|
}
|
|
|
|
function isOptionalSchema(schema: z.ZodTypeAny): boolean {
|
|
const def = zodDef(schema);
|
|
if (def.type === "optional" || def.type === "default" || def.type === "catch") {
|
|
return true;
|
|
}
|
|
if (def.type === "pipe") {
|
|
return isOptionalSchema(def.in as z.ZodTypeAny);
|
|
}
|
|
if (def.type === "nullable") {
|
|
return isOptionalSchema(def.innerType as z.ZodTypeAny);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Zod 4 stores each check as an object with a `_zod.def` that carries a `check`
|
|
// name and the check members. Read that def to describe the constraint.
|
|
function checkDef(check: unknown): Record<string, unknown> | undefined {
|
|
return (check as { _zod?: { def?: Record<string, unknown> } })._zod?.def;
|
|
}
|
|
|
|
function applyStringChecks(jsonSchema: JsonSchema, checks: ReadonlyArray<unknown>) {
|
|
for (const check of checks) {
|
|
const def = checkDef(check);
|
|
if (!def) continue;
|
|
if (def.check === "min_length") jsonSchema.minLength = def.minimum;
|
|
else if (def.check === "max_length") jsonSchema.maxLength = def.maximum;
|
|
else if (def.check === "string_format") {
|
|
if (def.format === "email") jsonSchema.format = "email";
|
|
else if (def.format === "url") jsonSchema.format = "uri";
|
|
// Zod 3 `.uuid()` maps to `.guid()` in zod 4 to keep the loose UUID
|
|
// format. Publish both as the OpenAPI `uuid` format so the spec does not
|
|
// change.
|
|
else if (def.format === "uuid" || def.format === "guid") jsonSchema.format = "uuid";
|
|
else if (def.format === "datetime") jsonSchema.format = "date-time";
|
|
// Zod 4 stores a `.regex()` pattern as a `RegExp`; publish its source.
|
|
else if (def.format === "regex") {
|
|
if (def.pattern instanceof RegExp) jsonSchema.pattern = def.pattern.source;
|
|
else if (typeof def.pattern === "string") jsonSchema.pattern = def.pattern;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function applyNumberChecks(jsonSchema: JsonSchema, checks: ReadonlyArray<unknown>) {
|
|
for (const check of checks) {
|
|
const def = checkDef(check);
|
|
if (!def) continue;
|
|
// `.int()` records a number-format check such as `safeint`.
|
|
if (def.check === "number_format") {
|
|
if (typeof def.format === "string" && def.format.includes("int")) {
|
|
jsonSchema.type = "integer";
|
|
}
|
|
} else if (def.check === "greater_than") {
|
|
jsonSchema.minimum = def.value;
|
|
if (!def.inclusive) jsonSchema.exclusiveMinimum = true;
|
|
} else if (def.check === "less_than") {
|
|
jsonSchema.maximum = def.value;
|
|
if (!def.inclusive) jsonSchema.exclusiveMaximum = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
function zodToOpenApiSchema(schema: z.ZodTypeAny): JsonSchema {
|
|
const unwrapped = unwrapSchema(schema);
|
|
const def = zodDef(unwrapped);
|
|
const typeName = def.type;
|
|
|
|
if (typeName === "string") {
|
|
const jsonSchema: JsonSchema = { type: "string" };
|
|
applyStringChecks(jsonSchema, (def.checks as unknown[]) ?? []);
|
|
return jsonSchema;
|
|
}
|
|
|
|
if (typeName === "number") {
|
|
const jsonSchema: JsonSchema = { type: "number" };
|
|
applyNumberChecks(jsonSchema, (def.checks as unknown[]) ?? []);
|
|
return jsonSchema;
|
|
}
|
|
|
|
if (typeName === "boolean") return { type: "boolean" };
|
|
if (typeName === "date") return { type: "string", format: "date-time" };
|
|
if (typeName === "any" || typeName === "unknown") return {};
|
|
|
|
if (typeName === "literal") {
|
|
const values = def.values as unknown[];
|
|
return { type: typeof values[0], enum: values };
|
|
}
|
|
|
|
// Zod 4 merges string enums and native enums into one `enum` type and stores
|
|
// the members on `entries`. A pure string enum keeps `type: "string"`; a
|
|
// native enum can hold numbers, so it publishes the values without a type.
|
|
if (typeName === "enum") {
|
|
const values = Array.from(
|
|
new Set(
|
|
Object.values(def.entries as Record<string, unknown>).filter(
|
|
(value) => typeof value === "string" || typeof value === "number",
|
|
),
|
|
),
|
|
);
|
|
if (values.every((value) => typeof value === "string")) {
|
|
return { type: "string", enum: values };
|
|
}
|
|
return { enum: values };
|
|
}
|
|
|
|
if (typeName === "array") {
|
|
return { type: "array", items: zodToOpenApiSchema(def.element as z.ZodTypeAny) };
|
|
}
|
|
|
|
if (typeName === "record") {
|
|
return {
|
|
type: "object",
|
|
additionalProperties: zodToOpenApiSchema(def.valueType as z.ZodTypeAny),
|
|
};
|
|
}
|
|
|
|
if (typeName === "nullable") {
|
|
return { ...zodToOpenApiSchema(def.innerType as z.ZodTypeAny), nullable: true };
|
|
}
|
|
|
|
// Zod 4 represents a plain union and a discriminated union as one `union`
|
|
// type with the members on `options`.
|
|
if (typeName === "union") {
|
|
return {
|
|
oneOf: (def.options as z.ZodTypeAny[]).map((option) => zodToOpenApiSchema(option)),
|
|
};
|
|
}
|
|
|
|
if (typeName === "intersection") {
|
|
return {
|
|
allOf: [
|
|
zodToOpenApiSchema(def.left as z.ZodTypeAny),
|
|
zodToOpenApiSchema(def.right as z.ZodTypeAny),
|
|
],
|
|
};
|
|
}
|
|
|
|
if (typeName === "object") {
|
|
const shape = def.shape as Record<string, z.ZodTypeAny>;
|
|
const properties: Record<string, JsonSchema> = {};
|
|
const required: string[] = [];
|
|
for (const [key, value] of Object.entries(shape)) {
|
|
properties[key] = zodToOpenApiSchema(value);
|
|
if (!isOptionalSchema(value)) required.push(key);
|
|
}
|
|
const jsonSchema: JsonSchema = { type: "object", properties };
|
|
if (required.length > 0) jsonSchema.required = required;
|
|
// A `.strict()` Zod object forbids an unknown key. Zod 4 records that as a
|
|
// `never` catchall. Publish the constraint as `additionalProperties: false`,
|
|
// so a client, a gateway, or a handler that treats the contract as
|
|
// authoritative rejects an extra property too.
|
|
const catchall = def.catchall as z.ZodTypeAny | undefined;
|
|
if (catchall && zodDef(catchall).type === "never") {
|
|
jsonSchema.additionalProperties = false;
|
|
}
|
|
return jsonSchema;
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
function normalizeContent(content: Record<string, { schema: unknown }>) {
|
|
return Object.fromEntries(
|
|
Object.entries(content).map(([contentType, media]) => [
|
|
contentType,
|
|
{
|
|
...media,
|
|
schema: isZodSchema(media.schema)
|
|
? zodToOpenApiSchema(media.schema)
|
|
: media.schema,
|
|
},
|
|
]),
|
|
);
|
|
}
|
|
|
|
function isZodSchema(value: unknown): value is z.ZodTypeAny {
|
|
return Boolean(
|
|
value &&
|
|
typeof value === "object" &&
|
|
"_def" in value &&
|
|
typeof (value as z.ZodTypeAny).safeParse === "function",
|
|
);
|
|
}
|
|
|
|
function normalizeResponses(responses: Record<string, OpenApiResponse> = {}) {
|
|
return Object.fromEntries(
|
|
Object.entries(responses).map(([status, response]) => {
|
|
const content = response.content as Record<string, { schema: unknown }> | undefined;
|
|
return [
|
|
status,
|
|
content
|
|
? {
|
|
...response,
|
|
content: normalizeContent(content),
|
|
}
|
|
: response,
|
|
];
|
|
}),
|
|
);
|
|
}
|
|
|
|
function parametersFromSchema(schema: z.ZodTypeAny, location: "path" | "query") {
|
|
const objectSchema = unwrapSchema(schema);
|
|
if (zodTypeName(objectSchema) !== "object") return [];
|
|
const shape = zodDef(objectSchema).shape as Record<string, z.ZodTypeAny>;
|
|
return Object.entries(shape).map(([name, value]) => ({
|
|
name,
|
|
in: location,
|
|
required: location === "path" ? true : !isOptionalSchema(value),
|
|
schema: zodToOpenApiSchema(value),
|
|
}));
|
|
}
|
|
|
|
class OpenAPIRegistry {
|
|
private readonly schemas: Record<string, JsonSchema> = {};
|
|
private readonly paths: Array<OpenApiPathRegistration> = [];
|
|
|
|
register(name: string, schema: z.ZodTypeAny) {
|
|
this.schemas[name] = zodToOpenApiSchema(schema);
|
|
return { $ref: `#/components/schemas/${name}` };
|
|
}
|
|
|
|
registerPath(pathRegistration: OpenApiPathRegistration) {
|
|
this.paths.push(pathRegistration);
|
|
}
|
|
|
|
buildPaths() {
|
|
const paths: Record<string, Record<string, unknown>> = {};
|
|
for (const { method, path, request, responses, ...operation } of this.paths) {
|
|
const normalizedOperation: Record<string, unknown> = {
|
|
...operation,
|
|
responses: normalizeResponses(responses),
|
|
};
|
|
if (request?.params) {
|
|
normalizedOperation.parameters = parametersFromSchema(request.params, "path");
|
|
}
|
|
if (request?.query) {
|
|
normalizedOperation.parameters = [
|
|
...((normalizedOperation.parameters as unknown[]) ?? []),
|
|
...parametersFromSchema(request.query, "query"),
|
|
];
|
|
}
|
|
if (request?.body) {
|
|
normalizedOperation.requestBody = {
|
|
...request.body,
|
|
content: normalizeContent(request.body.content),
|
|
};
|
|
}
|
|
paths[path] ??= {};
|
|
paths[path][method] = normalizedOperation;
|
|
}
|
|
return paths;
|
|
}
|
|
|
|
buildComponents() {
|
|
return { schemas: this.schemas };
|
|
}
|
|
}
|
|
|
|
const registry = new OpenAPIRegistry();
|
|
|
|
// ─── Common schemas ──────────────────────────────────────────────────────────
|
|
|
|
const ErrorSchema = registry.register(
|
|
"Error",
|
|
z.object({ error: z.string() }),
|
|
);
|
|
|
|
const responses = {
|
|
ok: (schema: z.ZodTypeAny = z.record(z.string(), z.unknown())) => ({
|
|
description: "Success",
|
|
content: { "application/json": { schema } },
|
|
}),
|
|
noContent: { description: "No content" },
|
|
badRequest: {
|
|
description: "Bad request",
|
|
content: { "application/json": { schema: ErrorSchema } },
|
|
},
|
|
unauthorized: {
|
|
description: "Unauthorized",
|
|
content: { "application/json": { schema: ErrorSchema } },
|
|
},
|
|
forbidden: {
|
|
description: "Forbidden",
|
|
content: { "application/json": { schema: ErrorSchema } },
|
|
},
|
|
notFound: {
|
|
description: "Not found",
|
|
content: { "application/json": { schema: ErrorSchema } },
|
|
},
|
|
conflict: {
|
|
description: "Conflict",
|
|
content: { "application/json": { schema: ErrorSchema } },
|
|
},
|
|
payloadTooLarge: {
|
|
description: "Payload too large",
|
|
content: { "application/json": { schema: ErrorSchema } },
|
|
},
|
|
unsupportedMediaType: {
|
|
description: "Unsupported media type",
|
|
content: { "application/json": { schema: ErrorSchema } },
|
|
},
|
|
unprocessable: {
|
|
description: "Unprocessable entity",
|
|
content: { "application/json": { schema: ErrorSchema } },
|
|
},
|
|
serverError: {
|
|
description: "Internal server error",
|
|
content: { "application/json": { schema: ErrorSchema } },
|
|
},
|
|
tooManyRequests: {
|
|
description: "Too many requests",
|
|
content: { "application/json": { schema: ErrorSchema } },
|
|
},
|
|
};
|
|
|
|
const jsonBody = (schema: z.ZodTypeAny) => ({
|
|
content: { "application/json": { schema } },
|
|
required: true as const,
|
|
});
|
|
|
|
// The company import + preview routes accept the inline JSON body or the raw
|
|
// company package as a compressed zip upload. Document both content types: the
|
|
// JSON variant keeps its zod schema; the multipart variant carries the zip in a
|
|
// `package` file field plus the other import fields as a JSON `meta` field.
|
|
const importRequestBody = (schema: z.ZodTypeAny) => ({
|
|
content: {
|
|
"application/json": { schema },
|
|
"multipart/form-data": {
|
|
schema: {
|
|
type: "object",
|
|
properties: {
|
|
package: {
|
|
type: "string",
|
|
format: "binary",
|
|
description: "The company package as a compressed .zip.",
|
|
},
|
|
meta: {
|
|
type: "string",
|
|
description:
|
|
"JSON-encoded import fields (include, target, collisionStrategy, and, for " +
|
|
"the apply route, nameOverrides / selectedFiles / adapterOverrides / " +
|
|
"pauseAutomations). A `source` here is ignored — the source is the zip.",
|
|
},
|
|
},
|
|
required: ["package"],
|
|
},
|
|
},
|
|
"application/zip": {
|
|
schema: { type: "string", format: "binary" },
|
|
},
|
|
},
|
|
required: true as const,
|
|
});
|
|
|
|
const r = responses;
|
|
|
|
const externalObjectSummariesBodySchema = z.object({
|
|
issueIds: z.array(z.string().guid()).max(1000),
|
|
}).strict();
|
|
|
|
const refreshExternalObjectsBodySchema = z.object({
|
|
objectIds: z.array(z.string().guid()).max(50).optional(),
|
|
}).strict();
|
|
|
|
// The route enforces the shared strict request schema. The route spine
|
|
// injects the adapter type from the path, so the client body never carries
|
|
// it; derive the documented body from the shared schema and omit that field,
|
|
// so the documented body cannot drift from the route again.
|
|
const startAdapterLoginSessionSchema = startAdapterAuthSessionRequestSchema.omit({
|
|
adapterType: true,
|
|
});
|
|
|
|
const environmentCustomImageCompanyQuerySchema = z.object({
|
|
companyId: z.string().optional(),
|
|
}).strict();
|
|
|
|
const disableEnvironmentCustomImageTemplateQuerySchema =
|
|
environmentCustomImageCompanyQuerySchema.extend({
|
|
deleteProviderTemplate: z.enum(["true", "false"]).optional(),
|
|
});
|
|
|
|
const environmentCustomImageOverviewSchema = z.object({
|
|
activeTemplate: environmentCustomImageTemplateSchema.nullable(),
|
|
activeSession: environmentCustomImageSetupSessionSchema.nullable(),
|
|
latestSession: environmentCustomImageSetupSessionSchema.nullable(),
|
|
}).strict();
|
|
|
|
const environmentCustomImageSetupSessionResultSchema = z.object({
|
|
session: environmentCustomImageSetupSessionSchema,
|
|
connectionPayload: z.record(z.string(), z.unknown()).nullable(),
|
|
}).strict();
|
|
|
|
const environmentCustomImageSetupSessionFinishResultSchema =
|
|
environmentCustomImageSetupSessionResultSchema.extend({
|
|
template: environmentCustomImageTemplateSchema,
|
|
});
|
|
|
|
const environmentCustomImageTemplateRollbackResultSchema = z.object({
|
|
activeTemplate: environmentCustomImageTemplateSchema,
|
|
supersededTemplate: environmentCustomImageTemplateSchema,
|
|
}).strict();
|
|
|
|
const environmentCustomImageTemplateRelinkResultSchema = z.object({
|
|
template: environmentCustomImageTemplateSchema,
|
|
classification: z.enum(["knob_only", "boot_source_drift", "unclassified"]),
|
|
}).strict();
|
|
|
|
const workTimelineQuerySchema = z.object({
|
|
from: z.string().optional(),
|
|
to: z.string().optional(),
|
|
userId: z.string().optional(),
|
|
goalId: z.string().guid().optional(),
|
|
projectId: z.string().guid().optional(),
|
|
issueId: z.string().guid().optional(),
|
|
limit: z.string().optional(),
|
|
offset: z.string().optional(),
|
|
}).strict();
|
|
|
|
const workTimelineResponseSchema = z.object({
|
|
actors: z.array(z.object({
|
|
id: z.string(),
|
|
type: z.enum(["agent", "user", "system", "plugin"]),
|
|
name: z.string(),
|
|
avatar: z.string().nullable().optional(),
|
|
}).strict()),
|
|
spans: z.array(z.object({
|
|
actorId: z.string(),
|
|
laneHint: z.string().nullable(),
|
|
runId: z.string(),
|
|
issueId: z.string(),
|
|
issueIdentifier: z.string().nullable(),
|
|
start: z.string(),
|
|
end: z.string().nullable(),
|
|
status: z.string(),
|
|
retryOfRunId: z.string().nullable().optional(),
|
|
continuationAttempt: z.number().optional(),
|
|
invocationSource: z.string().nullable().optional(),
|
|
}).strict()),
|
|
events: z.array(z.object({
|
|
actorId: z.string(),
|
|
kind: z.enum(["created", "commented", "approved", "delegated", "assigned"]),
|
|
issueId: z.string(),
|
|
at: z.string(),
|
|
}).strict()),
|
|
edges: z.array(z.object({
|
|
fromActorId: z.string(),
|
|
toActorId: z.string(),
|
|
issueId: z.string(),
|
|
at: z.string(),
|
|
kind: z.enum(["delegation", "assignment", "mention"]),
|
|
}).strict()),
|
|
pagination: z.object({
|
|
limit: z.number().int().positive(),
|
|
offset: z.number().int().nonnegative(),
|
|
totalIssues: z.number().int().nonnegative(),
|
|
hasMore: z.boolean(),
|
|
}).strict(),
|
|
window: z.object({
|
|
from: z.string(),
|
|
to: z.string(),
|
|
capped: z.boolean(),
|
|
}).strict(),
|
|
}).strict();
|
|
|
|
function paramsSchemaFromPath(routePath: string): z.ZodObject<z.ZodRawShape> | undefined {
|
|
const names = [...routePath.matchAll(/\{([A-Za-z0-9_]+)\}/g)].map((match) => match[1]);
|
|
if (names.length === 0) return undefined;
|
|
const shape: Record<string, z.ZodTypeAny> = {};
|
|
for (const name of names) {
|
|
shape[name] = z.string();
|
|
}
|
|
return z.object(shape);
|
|
}
|
|
|
|
function registerCurrentRoute(input: {
|
|
method: string;
|
|
path: string;
|
|
tags: string[];
|
|
summary: string;
|
|
query?: z.ZodTypeAny;
|
|
body?: z.ZodTypeAny;
|
|
responses?: Record<string, OpenApiResponse>;
|
|
}) {
|
|
const params = paramsSchemaFromPath(input.path);
|
|
const request = params || input.query || input.body
|
|
? {
|
|
...(params ? { params } : {}),
|
|
...(input.query ? { query: input.query } : {}),
|
|
...(input.body ? { body: jsonBody(input.body) } : {}),
|
|
}
|
|
: undefined;
|
|
registry.registerPath({
|
|
method: input.method,
|
|
path: input.path,
|
|
tags: input.tags,
|
|
summary: input.summary,
|
|
...(request ? { request } : {}),
|
|
responses: input.responses ?? { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
}
|
|
|
|
type OpenApiAuthLevel =
|
|
| "public"
|
|
| "runtime_tools"
|
|
| "authenticated"
|
|
| "board"
|
|
| "instance_admin";
|
|
|
|
const BOARD_SESSION_AUTH_SCHEME = "BoardSessionAuth";
|
|
const BOARD_API_KEY_AUTH_SCHEME = "BoardApiKeyAuth";
|
|
const AGENT_BEARER_AUTH_SCHEME = "AgentBearerAuth";
|
|
const RUNTIME_TOOLS_BEARER_AUTH_SCHEME = "RuntimeToolsBearerAuth";
|
|
|
|
function securityRequirement(name: string): Record<string, string[]> {
|
|
return { [name]: [] };
|
|
}
|
|
|
|
const BOARD_SECURITY: Array<Record<string, string[]>> = [
|
|
securityRequirement(BOARD_SESSION_AUTH_SCHEME),
|
|
securityRequirement(BOARD_API_KEY_AUTH_SCHEME),
|
|
];
|
|
|
|
const AUTHENTICATED_SECURITY: Array<Record<string, string[]>> = [
|
|
...BOARD_SECURITY,
|
|
securityRequirement(AGENT_BEARER_AUTH_SCHEME),
|
|
];
|
|
|
|
const RUNTIME_TOOLS_SECURITY: Array<Record<string, string[]>> = [
|
|
securityRequirement(RUNTIME_TOOLS_BEARER_AUTH_SCHEME),
|
|
];
|
|
|
|
const RUNTIME_TOOLS_OPERATIONS = new Set([
|
|
"POST /runtime-tools/github/credentials",
|
|
"GET /mcp/runtime-tools",
|
|
"POST /mcp/runtime-tools",
|
|
"POST /runtime-tools/connections/search",
|
|
"POST /runtime-tools/connections/request",
|
|
]);
|
|
|
|
const PUBLIC_OPERATIONS = new Set([
|
|
"GET /api/health",
|
|
"GET /api/openapi.json",
|
|
"GET /api/board-claim/{token}",
|
|
"POST /api/cli-auth/challenges",
|
|
"GET /api/cli-auth/challenges/{id}",
|
|
"POST /api/cli-auth/challenges/{id}/cancel",
|
|
"GET /api/invites/{token}",
|
|
"GET /api/invites/{token}/logo",
|
|
"GET /api/invites/{token}/onboarding",
|
|
"GET /api/invites/{token}/onboarding.txt",
|
|
"GET /api/invites/{token}/skills/index",
|
|
"GET /api/invites/{token}/skills/{skillName}",
|
|
"GET /api/invites/{token}/test-resolution",
|
|
"POST /api/invites/{token}/accept",
|
|
"POST /api/join-requests/{requestId}/claim-api-key",
|
|
"GET /mcp/gateways/{gatewayPublicId}",
|
|
"POST /mcp/gateways/{gatewayPublicId}",
|
|
"GET /api/tool-gateway/gateways/{gatewayId}/mcp",
|
|
"POST /api/tool-gateway/gateways/{gatewayId}/mcp",
|
|
]);
|
|
|
|
const BOARD_ONLY_PREFIXES = [
|
|
"/api/auth/",
|
|
"/api/admin/",
|
|
"/api/plugins",
|
|
"/api/instance/",
|
|
];
|
|
|
|
const BOARD_ONLY_OPERATIONS = new Set([
|
|
"GET /api/companies/{companyId}/project-repositories",
|
|
"PUT /api/projects/{id}/repositories",
|
|
"DELETE /api/issues/{id}/documents/{key}",
|
|
"GET /api/companies/{companyId}/decisions",
|
|
"GET /api/cloud/stacks",
|
|
"GET /api/companies",
|
|
"POST /api/companies",
|
|
"GET /api/companies/stats",
|
|
"GET /api/companies/issues",
|
|
"GET /api/companies/import/jobs/{jobId}",
|
|
"POST /api/board-claim/{token}/claim",
|
|
"GET /api/cli-auth/me",
|
|
"POST /api/companies/{companyId}/invites",
|
|
"GET /api/companies/{companyId}/invites",
|
|
"POST /api/companies/{companyId}/openclaw/invite-prompt",
|
|
"GET /api/companies/{companyId}/join-requests",
|
|
"POST /api/companies/{companyId}/join-requests/{requestId}/approve",
|
|
"POST /api/companies/{companyId}/join-requests/{requestId}/reject",
|
|
"GET /api/companies/{companyId}/members",
|
|
"PATCH /api/companies/{companyId}/members/{memberId}",
|
|
"PATCH /api/companies/{companyId}/members/{memberId}/role-and-grants",
|
|
"POST /api/companies/{companyId}/members/{memberId}/archive",
|
|
"PATCH /api/companies/{companyId}/members/{memberId}/permissions",
|
|
"GET /api/companies/{companyId}/user-directory",
|
|
"GET /api/companies/{companyId}/managed-agent-profiles",
|
|
"POST /api/companies/{companyId}/managed-agent-profiles",
|
|
"GET /api/companies/{companyId}/remote-agent-profiles",
|
|
"POST /api/companies/{companyId}/remote-agent-profiles",
|
|
"POST /api/execution-workspaces/{id}/reconcile-branch",
|
|
"POST /api/execution-workspaces/{id}/login-handoff",
|
|
"GET /api/board-api-keys",
|
|
"POST /api/board-api-keys",
|
|
"DELETE /api/board-api-keys/{keyId}",
|
|
"POST /api/bootstrap/claim",
|
|
"GET /api/companies/{companyId}/resource-memberships/me",
|
|
"PUT /api/companies/{companyId}/resource-memberships/me/agents/{agentId}",
|
|
"PUT /api/companies/{companyId}/resource-memberships/me/documents/{documentId}",
|
|
"PUT /api/companies/{companyId}/resource-memberships/me/projects/{projectId}",
|
|
"GET /api/companies/{companyId}/secret-provider-configs",
|
|
"POST /api/companies/{companyId}/secret-provider-configs",
|
|
"GET /api/companies/{companyId}/secret-providers/health",
|
|
"POST /api/companies/{companyId}/secret-provider-configs/discovery/preview",
|
|
"GET /api/secret-provider-configs/{id}",
|
|
"PATCH /api/secret-provider-configs/{id}",
|
|
"DELETE /api/secret-provider-configs/{id}",
|
|
"POST /api/secret-provider-configs/{id}/default",
|
|
"POST /api/secret-provider-configs/{id}/health",
|
|
"GET /api/companies/{companyId}/user-secret-definitions",
|
|
"POST /api/companies/{companyId}/user-secret-definitions",
|
|
"PATCH /api/companies/{companyId}/user-secret-definitions/{definitionId}",
|
|
"DELETE /api/companies/{companyId}/user-secret-definitions/{definitionId}",
|
|
"GET /api/companies/{companyId}/user-secret-definitions/{definitionId}/coverage",
|
|
"GET /api/companies/{companyId}/me/user-secrets",
|
|
"POST /api/companies/{companyId}/me/user-secrets",
|
|
"PATCH /api/companies/{companyId}/me/user-secrets/{secretId}",
|
|
"POST /api/companies/{companyId}/me/user-secrets/{secretId}/rotate",
|
|
"DELETE /api/companies/{companyId}/me/user-secrets/{secretId}",
|
|
"POST /api/companies/{companyId}/secrets/remote-import",
|
|
"POST /api/companies/{companyId}/secrets/remote-import/preview",
|
|
"GET /api/secrets/{id}/usage",
|
|
"GET /api/secrets/{id}/access-events",
|
|
"POST /api/health/dev-server/restart",
|
|
"POST /api/issues/{issueId}/file-resources/availability",
|
|
"GET /api/issues/{issueId}/file-resources/content",
|
|
"GET /api/issues/{issueId}/file-resources/list",
|
|
"GET /api/issues/{issueId}/file-resources/resolve",
|
|
"POST /api/issues/{id}/interactions/{interactionId}/accept",
|
|
"POST /api/issues/{id}/interactions/{interactionId}/reject",
|
|
"POST /api/issues/{id}/interactions/{interactionId}/respond",
|
|
"POST /api/issues/{id}/interactions/{interactionId}/skip",
|
|
"POST /api/issues/{id}/interactions/{interactionId}/withdraw",
|
|
"GET /api/companies/{companyId}/tools/gallery",
|
|
"GET /api/companies/{companyId}/tools/apps/{galleryKey}/preflight",
|
|
"POST /api/companies/{companyId}/tools/apps/connect",
|
|
"POST /api/companies/{companyId}/tools/apps/{connectionId}/finalize-oauth-access",
|
|
"POST /api/companies/{companyId}/tools/apps/{connectionId}/finish",
|
|
"GET /api/companies/{companyId}/tools/apps/attention",
|
|
"GET /api/companies/{companyId}/tools/action-requests",
|
|
"GET /api/companies/{companyId}/tools/examples",
|
|
"POST /api/companies/{companyId}/tools/examples/{id}/install",
|
|
"POST /api/companies/{companyId}/tools/examples/{id}/smoke",
|
|
"GET /api/companies/{companyId}/tools/applications",
|
|
"POST /api/companies/{companyId}/tools/applications",
|
|
"PATCH /api/tool-applications/{applicationId}",
|
|
"DELETE /api/tool-applications/{applicationId}",
|
|
"GET /api/companies/{companyId}/tools/connections",
|
|
"POST /api/companies/{companyId}/tools/connections",
|
|
"POST /api/companies/{companyId}/tools/connections/{connectionId}/start-authorization",
|
|
"GET /api/tool-connections/{connectionId}",
|
|
"GET /api/tool-connections/{connectionId}/grants",
|
|
"POST /api/tool-connections/{connectionId}/grants/installations",
|
|
"POST /api/tool-connections/{connectionId}/grants/{grantId}/delegations",
|
|
"DELETE /api/tool-connections/{connectionId}/grants/{grantId}/delegations/{delegationId}",
|
|
"DELETE /api/tool-connections/{connectionId}/grants/{grantId}",
|
|
"GET /api/tool-connections/{connectionId}/usage",
|
|
"PATCH /api/tool-connections/{connectionId}",
|
|
"DELETE /api/tool-connections/{connectionId}",
|
|
"POST /api/tool-connections/{connectionId}/health-check",
|
|
"POST /api/tool-connections/{connectionId}/reconnect",
|
|
"POST /api/tool-connections/{connectionId}/catalog/refresh",
|
|
"GET /api/tool-connections/{connectionId}/catalog",
|
|
"GET /api/tool-connections/{connectionId}/activity",
|
|
"GET /api/tool-connections/{connectionId}/test-agents",
|
|
"GET /api/tool-connections/{connectionId}/test-agents/{agentId}/access",
|
|
"POST /api/tool-connections/{connectionId}/test-calls",
|
|
"GET /api/tool-connections/{connectionId}/test-calls/{actionRequestId}",
|
|
"POST /api/agents/me/connections/{connectionId}/start-authorization",
|
|
"POST /api/agents/me/connections/{connectionId}/token",
|
|
"POST /api/tools/oauth/{connectionId}/start",
|
|
"GET /api/tools/oauth/callback",
|
|
"GET /api/tools/vercel-connect/callback",
|
|
"GET /api/connection-intents/{interactionId}/setup-options",
|
|
"POST /api/connection-intents/{interactionId}/phase",
|
|
"POST /api/connection-intents/{interactionId}/complete",
|
|
"POST /api/connection-intents/{interactionId}/decline",
|
|
"GET /api/companies/{companyId}/tools/profiles",
|
|
"POST /api/companies/{companyId}/tools/profiles",
|
|
"GET /api/companies/{companyId}/tools/profiles/effective/agents/{agentId}",
|
|
"GET /api/tool-profiles/{profileId}/new-tools",
|
|
"PATCH /api/tool-profiles/{profileId}",
|
|
"POST /api/tool-profiles/{profileId}/duplicate",
|
|
"DELETE /api/tool-profiles/{profileId}",
|
|
"POST /api/tool-profiles/{profileId}/new-tools/review",
|
|
"POST /api/tool-profiles/{profileId}/entries",
|
|
"PATCH /api/tool-profile-entries/{entryId}",
|
|
"DELETE /api/tool-profile-entries/{entryId}",
|
|
"POST /api/companies/{companyId}/tools/profiles/{profileId}/bind",
|
|
"POST /api/companies/{companyId}/tools/profiles/{profileId}/unbind",
|
|
"GET /api/companies/{companyId}/tools/runtime-slots",
|
|
"POST /api/companies/{companyId}/tools/runtime-slots/{id}/stop",
|
|
"POST /api/companies/{companyId}/tools/runtime-slots/{id}/restart",
|
|
"GET /api/companies/{companyId}/tools/runtime-health",
|
|
"GET /api/companies/{companyId}/tools/runs/{runId}/decisions",
|
|
"GET /api/companies/{companyId}/tools/trust-rules",
|
|
"GET /api/companies/{companyId}/tools/policies",
|
|
"POST /api/companies/{companyId}/tools/policies/reorder",
|
|
"POST /api/companies/{companyId}/tools/policies",
|
|
"POST /api/companies/{companyId}/tools/policies/{policyId}/duplicate",
|
|
"PATCH /api/companies/{companyId}/tools/policies/{policyId}",
|
|
"DELETE /api/companies/{companyId}/tools/policies/{policyId}",
|
|
"POST /api/companies/{companyId}/tools/action-requests/{actionRequestId}/trust-rule",
|
|
"POST /api/companies/{companyId}/tools/trust-rules/{policyId}/revoke",
|
|
"GET /api/companies/{companyId}/tools/stdio-templates",
|
|
"POST /api/companies/{companyId}/tools/stdio-templates",
|
|
"POST /api/companies/{companyId}/tools/stdio-templates/{templateId}/disable",
|
|
"POST /api/companies/{companyId}/tools/mcp/import-json",
|
|
"POST /api/companies/{companyId}/tools/policy/test",
|
|
"GET /api/companies/{companyId}/tools/gateways",
|
|
"POST /api/companies/{companyId}/tools/gateways",
|
|
"PATCH /api/tool-gateway/gateways/{gatewayId}",
|
|
"POST /api/tool-gateway/gateways/{gatewayId}/tokens",
|
|
"POST /api/tool-gateway/gateway-tokens/{tokenId}/revoke",
|
|
"POST /api/tool-gateway/action-requests/{id}/approve",
|
|
"POST /api/tool-gateway/action-requests/{id}/decline",
|
|
]);
|
|
|
|
const INSTANCE_ADMIN_OPERATIONS = new Set([
|
|
"POST /api/companies",
|
|
"POST /api/plugins/install",
|
|
"POST /api/instance/database-backups",
|
|
"POST /api/admin/users/{userId}/promote-instance-admin",
|
|
"POST /api/admin/users/{userId}/demote-instance-admin",
|
|
"PUT /api/admin/users/{userId}/company-access",
|
|
]);
|
|
|
|
const CREATED_OPERATIONS = new Set([
|
|
"POST /api/adapters/install",
|
|
"POST /api/companies/{companyId}/agent-hires",
|
|
"POST /api/companies/{companyId}/agents",
|
|
"POST /api/agents/{id}/keys",
|
|
"POST /api/companies/{companyId}/approvals",
|
|
"POST /api/approvals/{id}/comments",
|
|
"POST /api/companies/{companyId}/assets/images",
|
|
"POST /api/companies/{companyId}/logo",
|
|
"POST /api/companies/{companyId}/onboarding-seed",
|
|
"POST /api/cli-auth/challenges",
|
|
"POST /api/board-api-keys",
|
|
"POST /api/companies",
|
|
"POST /api/companies/{companyId}/invites",
|
|
"POST /api/companies/{companyId}/openclaw/invite-prompt",
|
|
"POST /api/companies/{companyId}/cost-events",
|
|
"POST /api/companies/{companyId}/finance-events",
|
|
"POST /api/companies/{companyId}/secret-provider-configs",
|
|
"POST /api/companies/{companyId}/environments",
|
|
"POST /api/environments/{environmentId}/custom-image-setup-sessions",
|
|
"POST /api/companies/{companyId}/goals",
|
|
"POST /api/companies/{companyId}/labels",
|
|
"POST /api/issues/{id}/documents/{key}/annotations",
|
|
"POST /api/issues/{id}/documents/{key}/annotations/{threadId}/comments",
|
|
"POST /api/routines/{id}/description/annotations",
|
|
"POST /api/routines/{id}/description/annotations/{threadId}/comments",
|
|
"POST /api/issues/{id}/work-products",
|
|
"POST /api/issues/{id}/low-trust/promotions",
|
|
"POST /api/issues/{id}/approvals",
|
|
"POST /api/companies/{companyId}/issues",
|
|
"POST /api/issues/{id}/children",
|
|
"POST /api/issues/{id}/interactions",
|
|
"POST /api/issues/{id}/comments",
|
|
"POST /api/companies/{companyId}/issues/{issueId}/attachments",
|
|
"POST /api/companies/{companyId}/projects",
|
|
"POST /api/projects/{id}/workspaces",
|
|
"POST /api/companies/{companyId}/routines",
|
|
"POST /api/companies/{companyId}/folders",
|
|
"POST /api/companies/{companyId}/folders/ensure-my",
|
|
"POST /api/routines/{id}/triggers",
|
|
"POST /api/companies/{companyId}/secrets",
|
|
"POST /api/companies/{companyId}/user-secret-definitions",
|
|
"POST /api/companies/{companyId}/me/user-secrets",
|
|
"POST /api/companies/{companyId}/skills",
|
|
"POST /api/companies/{companyId}/skills/import",
|
|
"POST /api/join-requests/{requestId}/claim-api-key",
|
|
"POST /api/admin/users/{userId}/promote-instance-admin",
|
|
"POST /api/plugins/install",
|
|
"POST /api/instance/database-backups",
|
|
"POST /api/companies/{companyId}/tools/applications",
|
|
"POST /api/companies/{companyId}/tools/connections",
|
|
"POST /api/companies/{companyId}/tools/action-requests/{actionRequestId}/trust-rule",
|
|
"POST /api/companies/{companyId}/tools/gateways",
|
|
"POST /api/tool-gateway/gateways/{gatewayId}/tokens",
|
|
"POST /api/tool-gateway/sessions",
|
|
]);
|
|
|
|
const ACCEPTED_OPERATIONS = new Set([
|
|
"POST /api/companies/import",
|
|
"POST /api/health/dev-server/restart",
|
|
"POST /api/invites/{token}/accept",
|
|
]);
|
|
|
|
const FORBIDDEN_RESPONSE = {
|
|
description: "Forbidden",
|
|
content: {
|
|
"application/json": {
|
|
schema: { $ref: "#/components/schemas/Error" },
|
|
},
|
|
},
|
|
};
|
|
|
|
function operationKey(method: string, path: string) {
|
|
return `${method.toUpperCase()} ${path}`;
|
|
}
|
|
|
|
function isBoardOnlyOperation(method: string, path: string) {
|
|
const key = operationKey(method, path);
|
|
if (BOARD_ONLY_OPERATIONS.has(key)) return true;
|
|
return BOARD_ONLY_PREFIXES.some((prefix) => path.startsWith(prefix));
|
|
}
|
|
|
|
function resolveOperationAuthLevel(method: string, path: string): OpenApiAuthLevel {
|
|
const key = operationKey(method, path);
|
|
if (PUBLIC_OPERATIONS.has(key)) return "public";
|
|
if (RUNTIME_TOOLS_OPERATIONS.has(key)) return "runtime_tools";
|
|
if (INSTANCE_ADMIN_OPERATIONS.has(key)) return "instance_admin";
|
|
if (isBoardOnlyOperation(method, path) || experimentalApiMetadata[`${method.toUpperCase()} ${path}`]?.boardOnly) return "board";
|
|
return "authenticated";
|
|
}
|
|
|
|
function applyOperationStatusOverride(
|
|
operation: Record<string, unknown>,
|
|
fromStatus: string,
|
|
toStatus: string,
|
|
) {
|
|
const responses = operation.responses as Record<string, unknown> | undefined;
|
|
if (!responses || !responses[fromStatus] || responses[toStatus]) return;
|
|
responses[toStatus] = responses[fromStatus];
|
|
delete responses[fromStatus];
|
|
}
|
|
|
|
function applyDocumentFixups(document: any): any {
|
|
document.components ??= {};
|
|
document.components.securitySchemes = {
|
|
[BOARD_SESSION_AUTH_SCHEME]: {
|
|
type: "apiKey",
|
|
in: "cookie",
|
|
name: "paperclip_session",
|
|
description:
|
|
"Board session cookie in authenticated mode. Paperclip uses Better Auth; cookie transport may vary by deployment.",
|
|
},
|
|
[BOARD_API_KEY_AUTH_SCHEME]: {
|
|
type: "http",
|
|
scheme: "bearer",
|
|
bearerFormat: "Board API Key",
|
|
description: "Board API key presented in the Authorization bearer header.",
|
|
},
|
|
[AGENT_BEARER_AUTH_SCHEME]: {
|
|
type: "http",
|
|
scheme: "bearer",
|
|
bearerFormat: "Agent API Key or Agent JWT",
|
|
description:
|
|
"Agent API key or Paperclip-issued local agent JWT presented in the Authorization bearer header.",
|
|
},
|
|
[RUNTIME_TOOLS_BEARER_AUTH_SCHEME]: {
|
|
type: "http",
|
|
scheme: "bearer",
|
|
bearerFormat: "Heartbeat-bound runtime tools token",
|
|
description:
|
|
"Scoped token bound to an active heartbeat run and presented in the Authorization bearer header. The GitHub credential endpoint requires the distinct github_credentials scope.",
|
|
},
|
|
};
|
|
document.security = AUTHENTICATED_SECURITY;
|
|
|
|
for (const [path, pathItem] of Object.entries(document.paths ?? {})) {
|
|
for (const [method, operation] of Object.entries(pathItem as Record<string, any>)) {
|
|
const authLevel = resolveOperationAuthLevel(method, path);
|
|
if (authLevel === "public") {
|
|
operation.security = [];
|
|
} else if (authLevel === "runtime_tools") {
|
|
operation.security = RUNTIME_TOOLS_SECURITY;
|
|
} else if (authLevel === "authenticated") {
|
|
operation.security = AUTHENTICATED_SECURITY;
|
|
} else {
|
|
operation.security = BOARD_SECURITY;
|
|
}
|
|
|
|
operation["x-paperclip-authorization"] =
|
|
authLevel === "instance_admin"
|
|
? { actor: "board", instanceAdmin: true }
|
|
: authLevel === "board"
|
|
? { actor: "board" }
|
|
: authLevel === "runtime_tools"
|
|
? { actor: "runtime_tools", heartbeatBound: true }
|
|
: authLevel === "authenticated"
|
|
? { actor: "board_or_agent" }
|
|
: { actor: "public" };
|
|
|
|
const key = operationKey(method, path);
|
|
if (authLevel !== "public") {
|
|
const responses = (operation.responses ??= {}) as Record<string, unknown>;
|
|
if (!responses["403"]) {
|
|
responses["403"] = FORBIDDEN_RESPONSE;
|
|
}
|
|
}
|
|
if (CREATED_OPERATIONS.has(key)) {
|
|
applyOperationStatusOverride(operation, "200", "201");
|
|
}
|
|
if (ACCEPTED_OPERATIONS.has(key)) {
|
|
applyOperationStatusOverride(operation, "200", "202");
|
|
}
|
|
}
|
|
}
|
|
|
|
return document;
|
|
}
|
|
|
|
// ─── Health ──────────────────────────────────────────────────────────────────
|
|
|
|
// Shared by the healthy and database-unreachable responses: full details
|
|
// (including serverInfo) ride only on board/agent-actor responses.
|
|
const healthServerInfoSchema = z.object({
|
|
processStartedAt: z.string().datetime(),
|
|
git: z.union([
|
|
z.object({
|
|
available: z.literal(true),
|
|
fullSha: z.string(),
|
|
shortSha: z.string(),
|
|
branchName: z.string().nullable(),
|
|
subject: z.string(),
|
|
committedAt: z.string().datetime().nullable(),
|
|
localChanges: z.union([
|
|
z.object({
|
|
available: z.literal(true),
|
|
hasLocalChanges: z.boolean(),
|
|
stagedFileCount: z.number().int().nonnegative(),
|
|
unstagedFileCount: z.number().int().nonnegative(),
|
|
untrackedFileCount: z.number().int().nonnegative(),
|
|
}).strict(),
|
|
z.object({
|
|
available: z.literal(false),
|
|
unavailableReason: z.enum(["git_status_unavailable"]),
|
|
}).strict(),
|
|
]),
|
|
}).strict(),
|
|
z.object({
|
|
available: z.literal(false),
|
|
unavailableReason: z.enum(["git_unavailable", "invalid_git_metadata"]),
|
|
}).strict(),
|
|
]),
|
|
}).strict();
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/health",
|
|
tags: ["health"],
|
|
summary: "Health check",
|
|
responses: {
|
|
200: r.ok(z.object({
|
|
status: z.enum(["ok", "unhealthy"]),
|
|
version: z.string().optional(),
|
|
// Running build commit (full git SHA), or null when git metadata is
|
|
// unavailable. Present on every response shape, including redacted ones.
|
|
commit: z.string().nullable(),
|
|
deploymentMode: z.string().optional(),
|
|
cloud: z.object({
|
|
managed: z.literal(true),
|
|
managedBy: z.literal("paperclip-cloud"),
|
|
stackSlug: z.string().nullable(),
|
|
stackDisplayName: z.string().optional(),
|
|
cloudBaseUrl: z.string().nullable(),
|
|
}).strict().optional(),
|
|
bootstrapStatus: z.enum(["ready", "bootstrap_pending"]).optional(),
|
|
bootstrapInviteActive: z.boolean().optional(),
|
|
databaseBackup: z.object({
|
|
enabled: z.boolean(),
|
|
status: z.enum(["ok", "warning"]),
|
|
backupDir: z.string().optional(),
|
|
maxAgeHours: z.number().optional(),
|
|
latestBackup: z.object({
|
|
name: z.string(),
|
|
path: z.string(),
|
|
mtime: z.string().datetime(),
|
|
ageHours: z.number(),
|
|
sizeBytes: z.number(),
|
|
}).nullable().optional(),
|
|
lastFailure: z.object({
|
|
path: z.string(),
|
|
mtime: z.string().datetime(),
|
|
message: z.string(),
|
|
}).nullable().optional(),
|
|
warnings: z.array(z.object({
|
|
code: z.enum([
|
|
"database_backup_check_failed",
|
|
"database_backup_last_failure",
|
|
"database_backup_missing",
|
|
"database_backup_stale",
|
|
]),
|
|
message: z.string(),
|
|
})),
|
|
}).optional(),
|
|
warnings: z.array(z.object({
|
|
code: z.string(),
|
|
message: z.string(),
|
|
})).optional(),
|
|
serverInfo: healthServerInfoSchema.optional(),
|
|
})),
|
|
// The database-unreachable body still carries version and commit so
|
|
// deployment tooling can verify the running build during an outage;
|
|
// serverInfo rides only on full-details (board/agent) responses.
|
|
503: {
|
|
description: "Service unavailable",
|
|
content: {
|
|
"application/json": {
|
|
schema: z.object({
|
|
status: z.literal("unhealthy"),
|
|
version: z.string(),
|
|
serverVersion: z.string(),
|
|
commit: z.string().nullable(),
|
|
error: z.literal("database_unreachable"),
|
|
serverInfo: healthServerInfoSchema.optional(),
|
|
}),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/openapi.json",
|
|
tags: ["health"],
|
|
summary: "Get the generated OpenAPI document",
|
|
responses: { 200: r.ok() },
|
|
});
|
|
|
|
// ─── Companies ───────────────────────────────────────────────────────────────
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/cloud/stacks",
|
|
tags: ["cloud"],
|
|
summary: "List the current Cloud tenant user's stacks",
|
|
responses: {
|
|
200: r.ok(),
|
|
403: r.forbidden,
|
|
404: r.notFound,
|
|
502: r.serverError,
|
|
503: r.serverError,
|
|
500: r.serverError,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies",
|
|
tags: ["companies"],
|
|
summary: "List companies",
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies",
|
|
tags: ["companies"],
|
|
summary: "Create a company",
|
|
request: { body: jsonBody(createCompanySchema) },
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/stats",
|
|
tags: ["companies"],
|
|
summary: "Company stats",
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}",
|
|
tags: ["companies"],
|
|
summary: "Get a company",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/artifacts",
|
|
tags: ["companies"],
|
|
summary: "List company artifacts",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
query: companyArtifactsQuerySchema,
|
|
},
|
|
responses: {
|
|
200: {
|
|
description: "Company artifact projection",
|
|
content: {
|
|
"application/json": {
|
|
schema: companyArtifactsResponseSchema,
|
|
},
|
|
},
|
|
},
|
|
401: r.unauthorized,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/timeline",
|
|
tags: ["companies"],
|
|
summary: "Get company work timeline",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
query: workTimelineQuerySchema,
|
|
},
|
|
responses: {
|
|
200: r.ok(workTimelineResponseSchema),
|
|
400: r.badRequest,
|
|
401: r.unauthorized,
|
|
403: r.forbidden,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "patch",
|
|
path: "/api/companies/{companyId}",
|
|
tags: ["companies"],
|
|
summary: "Update a company",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
body: jsonBody(updateCompanySchema.partial()),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "patch",
|
|
path: "/api/companies/{companyId}/branding",
|
|
tags: ["companies"],
|
|
summary: "Update company branding",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
body: jsonBody(updateCompanyBrandingSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/archive",
|
|
tags: ["companies"],
|
|
summary: "Archive a company",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "delete",
|
|
path: "/api/companies/{companyId}",
|
|
tags: ["companies"],
|
|
summary: "Delete a company",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/feedback-traces",
|
|
tags: ["companies"],
|
|
summary: "List company feedback traces",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/exports",
|
|
tags: ["companies"],
|
|
summary: "Export company data",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/exports/preview",
|
|
tags: ["companies"],
|
|
summary: "Preview company export",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/imports/preview",
|
|
tags: ["companies"],
|
|
summary: "Preview company import",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/imports/apply",
|
|
tags: ["companies"],
|
|
summary: "Apply company import",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
// ─── Teams Catalog ──────────────────────────────────────────────────────────
|
|
|
|
for (const route of [
|
|
["get", "/api/teams/catalog", "List catalog teams"],
|
|
["get", "/api/teams/catalog/{catalogId}/files", "Get catalog team file"],
|
|
["get", "/api/teams/catalog/{catalogId}", "Get catalog team"],
|
|
["get", "/api/companies/{companyId}/teams/catalog/installed", "List installed catalog teams"],
|
|
["post", "/api/companies/{companyId}/teams/catalog/{catalogId}/preview", "Preview catalog team install"],
|
|
["post", "/api/companies/{companyId}/teams/catalog/{catalogId}/install", "Install catalog team"],
|
|
] as const) {
|
|
registerCurrentRoute({
|
|
method: route[0],
|
|
path: route[1],
|
|
tags: ["teams"],
|
|
summary: route[2],
|
|
});
|
|
}
|
|
|
|
// ─── Agents ──────────────────────────────────────────────────────────────────
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/built-in-agents",
|
|
tags: ["agents"],
|
|
summary: "List built-in agent provisioning state",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/built-in-agents/{key}/status",
|
|
tags: ["agents"],
|
|
summary: "Get built-in agent bundle status",
|
|
request: { params: z.object({ companyId: z.string(), key: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/built-in-agents/{key}/reconcile",
|
|
tags: ["agents"],
|
|
summary: "Reconcile built-in agent managed resources",
|
|
request: {
|
|
params: z.object({ companyId: z.string(), key: z.string() }),
|
|
body: jsonBody(builtInAgentEmptyMutationSchema),
|
|
},
|
|
responses: {
|
|
200: r.ok(),
|
|
400: r.badRequest,
|
|
401: r.unauthorized,
|
|
403: r.forbidden,
|
|
404: r.notFound,
|
|
409: r.conflict,
|
|
422: r.unprocessable,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/built-in-agents/{key}/provision",
|
|
tags: ["agents"],
|
|
summary: "Provision a built-in agent",
|
|
request: {
|
|
params: z.object({ companyId: z.string(), key: z.string() }),
|
|
body: jsonBody(builtInAgentProvisionSchema),
|
|
},
|
|
responses: {
|
|
200: r.ok(),
|
|
202: r.ok(),
|
|
400: r.badRequest,
|
|
401: r.unauthorized,
|
|
403: r.forbidden,
|
|
404: r.notFound,
|
|
409: r.conflict,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/built-in-agents/{key}/reset",
|
|
tags: ["agents"],
|
|
summary: "Reset a built-in agent",
|
|
request: { params: z.object({ companyId: z.string(), key: z.string() }) },
|
|
responses: {
|
|
200: r.ok(),
|
|
401: r.unauthorized,
|
|
403: r.forbidden,
|
|
404: r.notFound,
|
|
409: r.conflict,
|
|
},
|
|
});
|
|
|
|
for (const route of [
|
|
["enable", "Enable a built-in routine schedule", 200],
|
|
["disable", "Disable a built-in routine schedule", 200],
|
|
["run", "Run a built-in routine once", 202],
|
|
] as const) {
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: `/api/companies/{companyId}/built-in-agents/{key}/routines/{routineKey}/${route[0]}`,
|
|
tags: ["agents"],
|
|
summary: route[1],
|
|
request: {
|
|
params: z.object({ companyId: z.string(), key: z.string(), routineKey: z.string() }),
|
|
body: jsonBody(builtInAgentEmptyMutationSchema),
|
|
},
|
|
responses: {
|
|
[route[2]]: r.ok(),
|
|
400: r.badRequest,
|
|
401: r.unauthorized,
|
|
403: r.forbidden,
|
|
404: r.notFound,
|
|
409: r.conflict,
|
|
422: r.unprocessable,
|
|
},
|
|
});
|
|
}
|
|
|
|
const summarySlotParams = z.object({
|
|
companyId: z.string(),
|
|
scopeKind: z.string(),
|
|
slotKey: z.string(),
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/summary-slots/{scopeKind}/{slotKey}",
|
|
tags: ["summaries"],
|
|
summary: "Get a summary slot with its latest document and generation state",
|
|
request: { params: summarySlotParams },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden, 404: r.notFound, 422: r.unprocessable },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/summary-slots/{scopeKind}/{slotKey}/revisions",
|
|
tags: ["summaries"],
|
|
summary: "List dated revisions for a summary slot",
|
|
request: { params: summarySlotParams },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden, 404: r.notFound, 422: r.unprocessable },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/summary-slots/{scopeKind}/{slotKey}/generate",
|
|
tags: ["summaries"],
|
|
summary: "Manually generate (or refresh) a summary slot",
|
|
request: { params: summarySlotParams, body: jsonBody(generateSummarySlotSchema) },
|
|
responses: {
|
|
200: r.ok(),
|
|
202: r.ok(),
|
|
400: r.badRequest,
|
|
401: r.unauthorized,
|
|
403: r.forbidden,
|
|
404: r.notFound,
|
|
409: r.conflict,
|
|
422: r.unprocessable,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "put",
|
|
path: "/api/companies/{companyId}/summary-slots/{scopeKind}/{slotKey}",
|
|
tags: ["summaries"],
|
|
summary: "Write a summary revision (Summarizer built-in agent only)",
|
|
request: { params: summarySlotParams, body: jsonBody(writeSummarySlotSchema) },
|
|
responses: {
|
|
200: r.ok(),
|
|
400: r.badRequest,
|
|
401: r.unauthorized,
|
|
403: r.forbidden,
|
|
404: r.notFound,
|
|
409: r.conflict,
|
|
422: r.unprocessable,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/status-cards",
|
|
tags: ["status-cards"],
|
|
summary: "List status cards",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/status-cards",
|
|
tags: ["status-cards"],
|
|
summary: "Create a status card",
|
|
request: { params: z.object({ companyId: z.string() }), body: jsonBody(createStatusCardSchema) },
|
|
responses: { 201: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
|
|
});
|
|
|
|
for (const route of [
|
|
["get", "/api/status-cards/{id}", "Get a status card"],
|
|
["delete", "/api/status-cards/{id}", "Delete a status card"],
|
|
["post", "/api/status-cards/{id}/recompile", "Recompile a status card query"],
|
|
["get", "/api/status-cards/{id}/dry-run", "Execute stored status card queries without an LLM"],
|
|
["get", "/api/status-cards/{id}/updates", "List status card updates"],
|
|
["get", "/api/status-cards/{id}/summary-revisions", "List status card summary revisions"],
|
|
] as const) {
|
|
registerCurrentRoute({ method: route[0], path: route[1], tags: ["status-cards"], summary: route[2] });
|
|
}
|
|
|
|
registerCurrentRoute({
|
|
method: "patch",
|
|
path: "/api/status-cards/{id}",
|
|
tags: ["status-cards"],
|
|
summary: "Update, archive, or restore a status card",
|
|
body: patchStatusCardSchema,
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/status-cards/{id}/refresh",
|
|
tags: ["status-cards"],
|
|
summary: "Refresh a status card",
|
|
body: refreshStatusCardSchema,
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "put",
|
|
path: "/api/status-cards/{id}/query",
|
|
tags: ["status-cards"],
|
|
summary: "Write a compiled status card query",
|
|
body: writeStatusCardQuerySchema,
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "put",
|
|
path: "/api/status-cards/{id}/summary",
|
|
tags: ["status-cards"],
|
|
summary: "Write a generated status card summary",
|
|
body: writeStatusCardSummarySchema,
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/agents",
|
|
tags: ["agents"],
|
|
summary: "List agents in a company",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/agents",
|
|
tags: ["agents"],
|
|
summary: "Create an agent",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
body: jsonBody(createAgentSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/agent-hires",
|
|
tags: ["agents"],
|
|
summary: "Hire an agent",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
body: jsonBody(createAgentHireSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/agent-configurations",
|
|
tags: ["agents"],
|
|
summary: "List agent configurations for a company",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/org",
|
|
tags: ["agents"],
|
|
summary: "Get org chart data",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/agents/me",
|
|
tags: ["agents"],
|
|
summary: "Get the current agent",
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/agents/me/inbox-lite",
|
|
tags: ["agents"],
|
|
summary: "Get current agent inbox (lite)",
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
const AgentSecretListResponseSchema = z.object({
|
|
secrets: z.array(z.object({
|
|
secretRef: z.string().guid(),
|
|
key: z.string(),
|
|
name: z.string(),
|
|
description: z.string().nullable(),
|
|
delivery: z.enum(["env", "api", "both"]),
|
|
projectionClass: z.string(),
|
|
latestVersion: z.number().int().nonnegative(),
|
|
versionSelector: z.union([z.literal("latest"), z.number().int().positive()]),
|
|
resolvedVersion: z.number().int().positive(),
|
|
})),
|
|
});
|
|
|
|
const createAgentSecretProposalSchema = z.discriminatedUnion("kind", [
|
|
z.object({
|
|
kind: z.literal("secret"),
|
|
name: z.string().min(1),
|
|
description: z.string().optional().nullable(),
|
|
value: z.string().min(1),
|
|
justification: z.string().min(1),
|
|
}),
|
|
z.object({
|
|
kind: z.literal("binding"),
|
|
secretId: z.string().guid().optional(),
|
|
sourceConfigPath: z.string().min(1).optional(),
|
|
secretProposalId: z.string().guid().optional(),
|
|
targetAgentId: z.string().guid().optional(),
|
|
configPath: z.string().min(1),
|
|
justification: z.string().min(1),
|
|
}),
|
|
]).superRefine((value, ctx) => {
|
|
if (
|
|
value.kind === "binding"
|
|
&& [value.secretId, value.sourceConfigPath, value.secretProposalId]
|
|
.filter((reference) => Boolean(reference)).length !== 1
|
|
) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: "Provide exactly one of secretId, sourceConfigPath, or secretProposalId",
|
|
});
|
|
}
|
|
});
|
|
|
|
const approveSecretProposalSchema = z.object({
|
|
cascade: z.boolean().optional(),
|
|
overrides: z.object({
|
|
name: z.string().min(1).optional(),
|
|
description: z.string().optional().nullable(),
|
|
providerConfigId: z.string().guid().optional().nullable(),
|
|
}).optional(),
|
|
});
|
|
|
|
const rejectSecretProposalSchema = z.object({ reason: z.string().min(1) });
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/agents/me/secret-proposals",
|
|
tags: ["secrets"],
|
|
summary: "Propose a company secret or agent secret binding",
|
|
request: { body: jsonBody(createAgentSecretProposalSchema) },
|
|
responses: { 201: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 422: r.unprocessable },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/agents/me/secret-proposals",
|
|
tags: ["secrets"],
|
|
summary: "List secret proposals visible to the current agent run",
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "delete",
|
|
path: "/api/agents/me/secret-proposals/{id}",
|
|
tags: ["secrets"],
|
|
summary: "Withdraw a pending secret proposal",
|
|
request: { params: z.object({ id: z.string().guid() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden, 404: r.notFound, 409: r.conflict },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/agents/me/secrets",
|
|
tags: ["secrets"],
|
|
summary: "List secrets accessible to the current agent run",
|
|
responses: {
|
|
200: { description: "Accessible secret metadata", content: { "application/json": { schema: AgentSecretListResponseSchema } } },
|
|
401: r.unauthorized,
|
|
403: r.forbidden,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/agents/me/secrets/{key}/value",
|
|
tags: ["secrets"],
|
|
summary: "Fetch one secret value for the current agent run",
|
|
request: { params: z.object({ key: z.string() }) },
|
|
responses: {
|
|
200: {
|
|
description: "Decrypted secret value",
|
|
content: { "application/json": { schema: z.object({ key: z.string(), value: z.string(), version: z.number().int().positive() }) } },
|
|
},
|
|
401: r.unauthorized,
|
|
403: r.forbidden,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/agents/me/connections/{connectionId}/token",
|
|
tags: ["tools"],
|
|
summary: "Mint a short-lived token for an agent connection",
|
|
request: {
|
|
params: z.object({ connectionId: z.string() }),
|
|
body: jsonBody(connectionTokenRequestSchema),
|
|
},
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden, 409: r.conflict, 429: r.tooManyRequests },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/agents/me/inbox/mine",
|
|
tags: ["agents"],
|
|
summary: "Get current agent assigned inbox items",
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/agents/{id}",
|
|
tags: ["agents"],
|
|
summary: "Get an agent",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "patch",
|
|
path: "/api/agents/{id}",
|
|
tags: ["agents"],
|
|
summary: "Update an agent",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(updateAgentSchema.omit({ permissions: true })),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "delete",
|
|
path: "/api/agents/{id}",
|
|
tags: ["agents"],
|
|
summary: "Delete an agent",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "patch",
|
|
path: "/api/agents/{id}/permissions",
|
|
tags: ["agents"],
|
|
summary: "Update agent permissions",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(updateAgentPermissionsSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "patch",
|
|
path: "/api/agents/{id}/instructions-path",
|
|
tags: ["agents"],
|
|
summary: "Update agent instructions path",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(updateAgentInstructionsPathSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/agents/{id}/instructions-bundle",
|
|
tags: ["agents"],
|
|
summary: "Get agent instructions bundle",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "patch",
|
|
path: "/api/agents/{id}/instructions-bundle",
|
|
tags: ["agents"],
|
|
summary: "Update agent instructions bundle",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(updateAgentInstructionsBundleSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/agents/{id}/instructions-bundle/file",
|
|
tags: ["agents"],
|
|
summary: "Get agent instructions file",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "put",
|
|
path: "/api/agents/{id}/instructions-bundle/file",
|
|
tags: ["agents"],
|
|
summary: "Upsert agent instructions file",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(upsertAgentInstructionsFileSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "delete",
|
|
path: "/api/agents/{id}/instructions-bundle/file",
|
|
tags: ["agents"],
|
|
summary: "Delete agent instructions file",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/agents/{id}/configuration",
|
|
tags: ["agents"],
|
|
summary: "Get agent configuration",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/agents/{id}/config-revisions",
|
|
tags: ["agents"],
|
|
summary: "List agent config revisions",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/agents/{id}/config-revisions/{revisionId}",
|
|
tags: ["agents"],
|
|
summary: "Get an agent config revision",
|
|
request: { params: z.object({ id: z.string(), revisionId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/agents/{id}/config-revisions/{revisionId}/rollback",
|
|
tags: ["agents"],
|
|
summary: "Roll back to a config revision",
|
|
request: { params: z.object({ id: z.string(), revisionId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/agents/{id}/runtime-state",
|
|
tags: ["agents"],
|
|
summary: "Get agent runtime state",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/agents/{id}/runtime-state/reset-session",
|
|
tags: ["agents"],
|
|
summary: "Reset agent session",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(resetAgentSessionSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/agents/{id}/task-sessions",
|
|
tags: ["agents"],
|
|
summary: "List agent task sessions",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/agents/{id}/skills",
|
|
tags: ["agents"],
|
|
summary: "List agent skills",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/agents/{id}/skills/sync",
|
|
tags: ["agents"],
|
|
summary: "Sync desired skills onto an agent configuration",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(agentSkillSyncSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound, 422: r.unprocessable },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/agents/{id}/keys",
|
|
tags: ["agents"],
|
|
summary: "List agent API keys",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/agents/{id}/keys",
|
|
tags: ["agents"],
|
|
summary: "Create an agent API key",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(createAgentKeySchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "delete",
|
|
path: "/api/agents/{id}/keys/{keyId}",
|
|
tags: ["agents"],
|
|
summary: "Delete an agent API key",
|
|
request: { params: z.object({ id: z.string(), keyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/agents/{id}/wakeup",
|
|
tags: ["agents"],
|
|
summary: "Wake up an agent",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(wakeAgentSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/agents/{id}/pause",
|
|
tags: ["agents"],
|
|
summary: "Pause an agent",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/agents/{id}/resume",
|
|
tags: ["agents"],
|
|
summary: "Resume an agent",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/agents/{id}/clear-error",
|
|
tags: ["agents"],
|
|
summary: "Clear an agent error",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden, 404: r.notFound, 409: r.conflict },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/agents/{id}/terminate",
|
|
tags: ["agents"],
|
|
summary: "Terminate an agent",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/instance/scheduler-heartbeats",
|
|
tags: ["agents"],
|
|
summary: "List scheduler heartbeats",
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
// ─── Adapters ────────────────────────────────────────────────────────────────
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/adapters/{type}/models",
|
|
tags: ["adapters"],
|
|
summary: "List models for an adapter type",
|
|
request: { params: z.object({ companyId: z.string(), type: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/adapters/{type}/detect-model",
|
|
tags: ["adapters"],
|
|
summary: "Detect active model for an adapter",
|
|
request: { params: z.object({ companyId: z.string(), type: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/adapters/{type}/test-environment",
|
|
tags: ["adapters"],
|
|
summary: "Validate adapter environment access for a company",
|
|
request: {
|
|
params: z.object({ companyId: z.string(), type: z.string() }),
|
|
body: jsonBody(testAdapterEnvironmentSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/adapters/{type}/auth-signal",
|
|
tags: ["adapters"],
|
|
summary: "Read the cheap host-local authentication signal for an adapter type",
|
|
request: {
|
|
params: z.object({ companyId: z.string(), type: z.string() }),
|
|
query: z.object({ environmentId: z.string().optional() }),
|
|
},
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/adapters/{type}/login-sessions",
|
|
tags: ["adapters"],
|
|
summary: "Start a company-scoped adapter device login",
|
|
request: {
|
|
params: z.object({ companyId: z.string(), type: z.string() }),
|
|
body: jsonBody(startAdapterLoginSessionSchema),
|
|
},
|
|
responses: {
|
|
201: r.ok(),
|
|
400: r.badRequest,
|
|
401: r.unauthorized,
|
|
403: r.forbidden,
|
|
409: r.conflict,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/adapters/{type}/login-sessions/active",
|
|
tags: ["adapters"],
|
|
summary: "Read the caller's active adapter device login session",
|
|
request: {
|
|
params: z.object({ companyId: z.string(), type: z.string() }),
|
|
},
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/adapters/{type}/login-sessions/{sessionId}",
|
|
tags: ["adapters"],
|
|
summary: "Read an adapter device login session",
|
|
request: {
|
|
params: z.object({ companyId: z.string(), type: z.string(), sessionId: z.string() }),
|
|
},
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/adapters/{type}/login-sessions/{sessionId}/cancel",
|
|
tags: ["adapters"],
|
|
summary: "Cancel an adapter device login session",
|
|
request: {
|
|
params: z.object({ companyId: z.string(), type: z.string(), sessionId: z.string() }),
|
|
},
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
|
|
});
|
|
|
|
// ─── Issues ──────────────────────────────────────────────────────────────────
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/issues",
|
|
tags: ["issues"],
|
|
summary: "List issues in a company",
|
|
description: "Use `view=compact` for the board issue-list row contract. The default response remains the broad compatibility contract.",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
query: z.object({ view: z.enum(["compact"]).optional() }).passthrough(),
|
|
},
|
|
responses: { 200: r.ok(), 304: { description: "Not Modified" }, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/issues",
|
|
tags: ["issues"],
|
|
summary: "Create an issue",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
body: jsonBody(createIssueSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 422: r.unprocessable },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/issues/{id}",
|
|
tags: ["issues"],
|
|
summary: "Get an issue",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "patch",
|
|
path: "/api/issues/{id}",
|
|
tags: ["issues"],
|
|
summary: "Update an issue",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(updateIssueSchema.partial()),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound, 422: r.unprocessable },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/issues/{id}/stalled-review-decision",
|
|
tags: ["issues"],
|
|
summary: "Resolve a stalled issue review",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(stalledReviewDecisionSchema),
|
|
},
|
|
responses: {
|
|
200: r.ok(),
|
|
400: r.badRequest,
|
|
401: r.unauthorized,
|
|
403: r.forbidden,
|
|
404: r.notFound,
|
|
409: r.conflict,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "delete",
|
|
path: "/api/issues/{id}",
|
|
tags: ["issues"],
|
|
summary: "Delete an issue",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/issues/{id}/heartbeat-context",
|
|
tags: ["issues"],
|
|
summary: "Get issue heartbeat context",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/issues/{id}/watchdog",
|
|
tags: ["issues"],
|
|
summary: "Get active issue watchdog",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "put",
|
|
path: "/api/issues/{id}/watchdog",
|
|
tags: ["issues"],
|
|
summary: "Create or update an issue watchdog",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(upsertIssueWatchdogSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "delete",
|
|
path: "/api/issues/{id}/watchdog",
|
|
tags: ["issues"],
|
|
summary: "Disable an issue watchdog",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/issues/{id}/work-products",
|
|
tags: ["issues"],
|
|
summary: "List issue work products",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
query: z.object({ refreshPullRequests: z.enum(["true"]).optional() }),
|
|
},
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/issues/{id}/work-products",
|
|
tags: ["issues"],
|
|
summary: "Create an issue work product",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(createIssueWorkProductSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/issues/{id}/work-products/{workProductId}/review-document",
|
|
tags: ["issues"],
|
|
summary: "Ensure the review document for a Markdown work product",
|
|
request: { params: z.object({ id: z.string(), workProductId: z.string() }) },
|
|
responses: {
|
|
200: r.ok(),
|
|
201: r.ok(),
|
|
401: r.unauthorized,
|
|
403: r.forbidden,
|
|
404: r.notFound,
|
|
409: r.conflict,
|
|
413: r.payloadTooLarge,
|
|
415: r.unsupportedMediaType,
|
|
422: r.unprocessable,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "patch",
|
|
path: "/api/work-products/{id}",
|
|
tags: ["issues"],
|
|
summary: "Update a work product",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(updateIssueWorkProductSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "delete",
|
|
path: "/api/work-products/{id}",
|
|
tags: ["issues"],
|
|
summary: "Delete a work product",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/issues/{id}/documents",
|
|
tags: ["issues"],
|
|
summary: "List issue documents",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/issues/{id}/documents/{key}",
|
|
tags: ["issues"],
|
|
summary: "Get an issue document",
|
|
request: { params: z.object({ id: z.string(), key: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "put",
|
|
path: "/api/issues/{id}/documents/{key}",
|
|
tags: ["issues"],
|
|
summary: "Upsert an issue document",
|
|
request: {
|
|
params: z.object({ id: z.string(), key: z.string() }),
|
|
body: jsonBody(upsertIssueDocumentSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "delete",
|
|
path: "/api/issues/{id}/documents/{key}",
|
|
tags: ["issues"],
|
|
summary: "Delete an issue document",
|
|
request: { params: z.object({ id: z.string(), key: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/issues/{id}/documents/{key}/revisions",
|
|
tags: ["issues"],
|
|
summary: "List issue document revisions",
|
|
request: { params: z.object({ id: z.string(), key: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/issues/{id}/documents/{key}/revisions/{revisionId}/restore",
|
|
tags: ["issues"],
|
|
summary: "Restore a document revision",
|
|
request: {
|
|
params: z.object({ id: z.string(), key: z.string(), revisionId: z.string() }),
|
|
body: jsonBody(restoreIssueDocumentRevisionSchema),
|
|
},
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/issues/{id}/comments",
|
|
tags: ["issues"],
|
|
summary: "List issue comments",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/issues/{id}/comments",
|
|
tags: ["issues"],
|
|
summary: "Add a comment to an issue",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(addIssueCommentSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "delete",
|
|
path: "/api/issues/{id}/comments/{commentId}",
|
|
tags: ["issues"],
|
|
summary: "Delete an issue comment",
|
|
request: { params: z.object({ id: z.string(), commentId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/issues/{id}/approvals",
|
|
tags: ["issues"],
|
|
summary: "List issue approvals",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/issues/{id}/approvals",
|
|
tags: ["issues"],
|
|
summary: "Link an approval to an issue",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(linkIssueApprovalSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "delete",
|
|
path: "/api/issues/{id}/approvals/{approvalId}",
|
|
tags: ["issues"],
|
|
summary: "Unlink an approval from an issue",
|
|
request: { params: z.object({ id: z.string(), approvalId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/issues/{id}/checkout",
|
|
tags: ["issues"],
|
|
summary: "Check out an issue",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(checkoutIssueSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/issues/{id}/release",
|
|
tags: ["issues"],
|
|
summary: "Release an issue",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/issues/{id}/read",
|
|
tags: ["issues"],
|
|
summary: "Mark an issue as read",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "delete",
|
|
path: "/api/issues/{id}/read",
|
|
tags: ["issues"],
|
|
summary: "Mark an issue as unread",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/issues/{id}/inbox-archive",
|
|
tags: ["issues"],
|
|
summary: "Archive issue from inbox",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(z.object({ userId: z.string().min(1).optional() })),
|
|
},
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "delete",
|
|
path: "/api/issues/{id}/inbox-archive",
|
|
tags: ["issues"],
|
|
summary: "Un-archive issue from inbox",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(z.object({ userId: z.string().min(1).optional() })),
|
|
},
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/issues/{id}/feedback-votes",
|
|
tags: ["issues"],
|
|
summary: "List issue feedback votes",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/issues/{id}/feedback-votes",
|
|
tags: ["issues"],
|
|
summary: "Upsert a feedback vote",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(upsertIssueFeedbackVoteSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/issues/{id}/feedback-traces",
|
|
tags: ["issues"],
|
|
summary: "List issue feedback traces",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/feedback-traces/{traceId}",
|
|
tags: ["issues"],
|
|
summary: "Get a feedback trace",
|
|
request: { params: z.object({ traceId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/feedback-traces/{traceId}/bundle",
|
|
tags: ["issues"],
|
|
summary: "Get a feedback trace bundle",
|
|
request: { params: z.object({ traceId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/issues/{issueId}/file-resources/availability",
|
|
tags: ["issues"],
|
|
summary: "Check whether issue workspace files can be opened",
|
|
request: {
|
|
params: z.object({ issueId: z.string() }),
|
|
body: {
|
|
required: true,
|
|
content: { "application/json": { schema: workspaceFileAvailabilityRequestSchema } },
|
|
},
|
|
},
|
|
responses: {
|
|
200: r.ok(workspaceFileAvailabilityResponseSchema),
|
|
400: r.badRequest,
|
|
401: r.unauthorized,
|
|
403: r.forbidden,
|
|
404: r.notFound,
|
|
429: r.tooManyRequests,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/issues/{issueId}/file-resources/list",
|
|
tags: ["issues"],
|
|
summary: "List workspace files for an issue",
|
|
request: {
|
|
params: z.object({ issueId: z.string() }),
|
|
query: workspaceFileListQuerySchema,
|
|
},
|
|
responses: {
|
|
200: r.ok(),
|
|
401: r.unauthorized,
|
|
404: r.notFound,
|
|
422: r.unprocessable,
|
|
429: r.tooManyRequests,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/issues/{issueId}/file-resources/resolve",
|
|
tags: ["issues"],
|
|
summary: "Resolve an issue workspace file",
|
|
request: {
|
|
params: z.object({ issueId: z.string() }),
|
|
query: workspaceFileResourceQuerySchema,
|
|
},
|
|
responses: {
|
|
200: r.ok(),
|
|
401: r.unauthorized,
|
|
404: r.notFound,
|
|
422: r.unprocessable,
|
|
429: r.tooManyRequests,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/issues/{issueId}/file-resources/content",
|
|
tags: ["issues"],
|
|
summary: "Read issue workspace file content",
|
|
request: {
|
|
params: z.object({ issueId: z.string() }),
|
|
query: workspaceFileResourceQuerySchema,
|
|
},
|
|
responses: {
|
|
200: r.ok(),
|
|
401: r.unauthorized,
|
|
404: r.notFound,
|
|
422: r.unprocessable,
|
|
429: r.tooManyRequests,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/issues/{id}/attachments",
|
|
tags: ["issues"],
|
|
summary: "List issue attachments",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/labels",
|
|
tags: ["issues"],
|
|
summary: "List labels in a company",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/labels",
|
|
tags: ["issues"],
|
|
summary: "Create a label",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
body: jsonBody(createIssueLabelSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "delete",
|
|
path: "/api/labels/{labelId}",
|
|
tags: ["issues"],
|
|
summary: "Delete a label",
|
|
request: { params: z.object({ labelId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
// ─── Projects ────────────────────────────────────────────────────────────────
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/project-repositories",
|
|
tags: ["projects"],
|
|
summary: "Discover GitHub repositories available to the current board user",
|
|
description: "Deduplicates repositories across usable personal and company-shared GitHub connections. Failed connections are reported without discarding successful results.",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: {
|
|
200: r.ok(z.object({
|
|
repositories: z.array(z.object({ id: z.string(), fullName: z.string(), url: z.string(), private: z.boolean().optional(), connections: z.array(z.string()) })),
|
|
connectionCount: z.number().int().nonnegative(),
|
|
failedConnectionCount: z.number().int().nonnegative(),
|
|
})),
|
|
401: r.unauthorized, 403: r.forbidden,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "put",
|
|
path: "/api/projects/{id}/repositories",
|
|
tags: ["projects"],
|
|
summary: "Replace selected GitHub source repositories",
|
|
description: "Saves provider IDs transactionally, refreshes canonical names and URLs, and preserves legacy workspace URLs. Unavailable existing selections can remain; new selections must be available to the caller.",
|
|
request: { params: z.object({ id: z.string() }), body: jsonBody(createProjectSchema.pick({ repositoryIds: true }).required()) },
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound, 422: r.unprocessable },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/projects",
|
|
tags: ["projects"],
|
|
summary: "List projects in a company",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/projects",
|
|
tags: ["projects"],
|
|
summary: "Create a project",
|
|
description: "The optional repositoryIds field selects GitHub source repositories and requires a board caller. It cannot be combined with workspace. All selections are validated before the project and repository workspaces are created atomically.",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
body: jsonBody(createProjectSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 422: r.unprocessable },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/projects/{id}",
|
|
tags: ["projects"],
|
|
summary: "Get a project",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "patch",
|
|
path: "/api/projects/{id}",
|
|
tags: ["projects"],
|
|
summary: "Update a project",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(updateProjectSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "delete",
|
|
path: "/api/projects/{id}",
|
|
tags: ["projects"],
|
|
summary: "Delete a project",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/projects/{id}/workspaces",
|
|
tags: ["projects"],
|
|
summary: "List project workspaces",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/projects/{id}/workspaces",
|
|
tags: ["projects"],
|
|
summary: "Create a project workspace",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(createProjectWorkspaceSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "patch",
|
|
path: "/api/projects/{id}/workspaces/{workspaceId}",
|
|
tags: ["projects"],
|
|
summary: "Update a project workspace",
|
|
request: {
|
|
params: z.object({ id: z.string(), workspaceId: z.string() }),
|
|
body: jsonBody(updateProjectWorkspaceSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "delete",
|
|
path: "/api/projects/{id}/workspaces/{workspaceId}",
|
|
tags: ["projects"],
|
|
summary: "Delete a project workspace",
|
|
request: { params: z.object({ id: z.string(), workspaceId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
// ─── Routines ────────────────────────────────────────────────────────────────
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/routines",
|
|
tags: ["routines"],
|
|
summary: "List routines in a company",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/routines",
|
|
tags: ["routines"],
|
|
summary: "Create a routine",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
body: jsonBody(createRoutineSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/routines/{id}",
|
|
tags: ["routines"],
|
|
summary: "Get a routine",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "patch",
|
|
path: "/api/routines/{id}",
|
|
tags: ["routines"],
|
|
summary: "Update a routine",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(updateRoutineSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/routines/{id}/runs",
|
|
tags: ["routines"],
|
|
summary: "List runs for a routine",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/routines/{id}/run",
|
|
tags: ["routines"],
|
|
summary: "Manually run a routine",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(runRoutineSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 422: r.unprocessable },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/routines/{id}/triggers",
|
|
tags: ["routines"],
|
|
summary: "Create a routine trigger",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(createRoutineTriggerSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "patch",
|
|
path: "/api/routine-triggers/{id}",
|
|
tags: ["routines"],
|
|
summary: "Update a routine trigger",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(updateRoutineTriggerSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "delete",
|
|
path: "/api/routine-triggers/{id}",
|
|
tags: ["routines"],
|
|
summary: "Delete a routine trigger",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/routine-triggers/{id}/rotate-secret",
|
|
tags: ["routines"],
|
|
summary: "Rotate a routine trigger secret",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(rotateRoutineTriggerSecretSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/routine-triggers/public/{publicId}/fire",
|
|
tags: ["routines"],
|
|
summary: "Fire a public routine trigger",
|
|
request: { params: z.object({ publicId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
// ─── Goals ───────────────────────────────────────────────────────────────────
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/goals",
|
|
tags: ["goals"],
|
|
summary: "List goals in a company",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/goals",
|
|
tags: ["goals"],
|
|
summary: "Create a goal",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
body: jsonBody(createGoalSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/goals/{id}",
|
|
tags: ["goals"],
|
|
summary: "Get a goal",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "patch",
|
|
path: "/api/goals/{id}",
|
|
tags: ["goals"],
|
|
summary: "Update a goal",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(updateGoalSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "delete",
|
|
path: "/api/goals/{id}",
|
|
tags: ["goals"],
|
|
summary: "Delete a goal",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
// ─── Secrets ─────────────────────────────────────────────────────────────────
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/secret-providers",
|
|
tags: ["secrets"],
|
|
summary: "List secret providers",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/secrets/catalog",
|
|
tags: ["secrets"],
|
|
summary: "List secret metadata (id, name, key, status) — accessible to agents",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/secrets",
|
|
tags: ["secrets"],
|
|
summary: "List secrets in a company",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/secrets",
|
|
tags: ["secrets"],
|
|
summary: "Create a secret",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
body: jsonBody(createSecretSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/secret-proposals",
|
|
tags: ["secrets"],
|
|
summary: "List company secret proposals for board review",
|
|
request: {
|
|
params: z.object({ companyId: z.string().guid() }),
|
|
query: z.object({ status: z.enum(["pending", "approved", "rejected", "withdrawn", "expired"]).optional() }),
|
|
},
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/secret-proposals/{id}/approve",
|
|
tags: ["secrets"],
|
|
summary: "Approve and execute a secret proposal as the approving board user",
|
|
request: {
|
|
params: z.object({ companyId: z.string().guid(), id: z.string().guid() }),
|
|
body: jsonBody(approveSecretProposalSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound, 409: r.conflict, 422: r.unprocessable },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/secret-proposals/{id}/reject",
|
|
tags: ["secrets"],
|
|
summary: "Reject a pending secret proposal and dependent bindings",
|
|
request: {
|
|
params: z.object({ companyId: z.string().guid(), id: z.string().guid() }),
|
|
body: jsonBody(rejectSecretProposalSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound, 409: r.conflict, 422: r.unprocessable },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "patch",
|
|
path: "/api/secrets/{id}",
|
|
tags: ["secrets"],
|
|
summary: "Update a secret",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(updateSecretSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/secrets/{id}/rotate",
|
|
tags: ["secrets"],
|
|
summary: "Rotate a secret",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(rotateSecretSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "delete",
|
|
path: "/api/secrets/{id}",
|
|
tags: ["secrets"],
|
|
summary: "Delete a secret",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/user-secret-definitions",
|
|
tags: ["secrets"],
|
|
summary: "List user secret definitions",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/user-secret-definitions",
|
|
tags: ["secrets"],
|
|
summary: "Create a user secret definition",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
body: jsonBody(createUserSecretDefinitionSchema),
|
|
},
|
|
responses: { 201: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "patch",
|
|
path: "/api/companies/{companyId}/user-secret-definitions/{definitionId}",
|
|
tags: ["secrets"],
|
|
summary: "Update a user secret definition",
|
|
request: {
|
|
params: z.object({ companyId: z.string(), definitionId: z.string() }),
|
|
body: jsonBody(updateUserSecretDefinitionSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "delete",
|
|
path: "/api/companies/{companyId}/user-secret-definitions/{definitionId}",
|
|
tags: ["secrets"],
|
|
summary: "Delete a user secret definition",
|
|
request: { params: z.object({ companyId: z.string(), definitionId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/user-secret-definitions/{definitionId}/coverage",
|
|
tags: ["secrets"],
|
|
summary: "Get user secret definition coverage",
|
|
request: { params: z.object({ companyId: z.string(), definitionId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/me/user-secrets",
|
|
tags: ["secrets"],
|
|
summary: "List my user secret values",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/me/user-secrets",
|
|
tags: ["secrets"],
|
|
summary: "Create my user secret value",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
body: jsonBody(createUserSecretValueSchema),
|
|
},
|
|
responses: { 201: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "patch",
|
|
path: "/api/companies/{companyId}/me/user-secrets/{secretId}",
|
|
tags: ["secrets"],
|
|
summary: "Update my user secret value",
|
|
request: {
|
|
params: z.object({ companyId: z.string(), secretId: z.string() }),
|
|
body: jsonBody(updateUserSecretValueSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/me/user-secrets/{secretId}/rotate",
|
|
tags: ["secrets"],
|
|
summary: "Rotate my user secret value",
|
|
request: {
|
|
params: z.object({ companyId: z.string(), secretId: z.string() }),
|
|
body: jsonBody(rotateUserSecretValueSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "delete",
|
|
path: "/api/companies/{companyId}/me/user-secrets/{secretId}",
|
|
tags: ["secrets"],
|
|
summary: "Delete my user secret value",
|
|
request: { params: z.object({ companyId: z.string(), secretId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
|
|
});
|
|
|
|
// ─── Approvals ───────────────────────────────────────────────────────────────
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/approvals",
|
|
tags: ["approvals"],
|
|
summary: "List approvals in a company",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/approvals",
|
|
tags: ["approvals"],
|
|
summary: "Create an approval",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
body: jsonBody(createApprovalSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/approvals/{id}",
|
|
tags: ["approvals"],
|
|
summary: "Get an approval",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/approvals/{id}/issues",
|
|
tags: ["approvals"],
|
|
summary: "List issues linked to an approval",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/approvals/{id}/approve",
|
|
tags: ["approvals"],
|
|
summary: "Approve an approval",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(resolveApprovalSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/approvals/{id}/reject",
|
|
tags: ["approvals"],
|
|
summary: "Reject an approval",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(resolveApprovalSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/approvals/{id}/request-revision",
|
|
tags: ["approvals"],
|
|
summary: "Request revision on an approval",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(requestApprovalRevisionSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/approvals/{id}/resubmit",
|
|
tags: ["approvals"],
|
|
summary: "Resubmit an approval",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(resubmitApprovalSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/approvals/{id}/comments",
|
|
tags: ["approvals"],
|
|
summary: "List approval comments",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/approvals/{id}/comments",
|
|
tags: ["approvals"],
|
|
summary: "Add a comment to an approval",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(addApprovalCommentSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
// ─── Costs ───────────────────────────────────────────────────────────────────
|
|
|
|
const costSummaryPaths = [
|
|
"summary", "by-agent", "by-agent-model", "by-provider",
|
|
"by-biller", "by-project", "finance-summary", "finance-by-biller",
|
|
"finance-by-kind", "finance-events", "window-spend", "quota-windows",
|
|
] as const;
|
|
|
|
for (const segment of costSummaryPaths) {
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: `/api/companies/{companyId}/costs/${segment}`,
|
|
tags: ["costs"],
|
|
summary: `Cost report: ${segment}`,
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
}
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/cost-events",
|
|
tags: ["costs"],
|
|
summary: "Record a cost event",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
body: jsonBody(createCostEventSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/finance-events",
|
|
tags: ["costs"],
|
|
summary: "Record a finance event",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
body: jsonBody(createFinanceEventSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/budgets/policies",
|
|
tags: ["costs"],
|
|
summary: "Create or update a budget policy",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
body: jsonBody(upsertBudgetPolicySchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/budget-incidents/{incidentId}/resolve",
|
|
tags: ["costs"],
|
|
summary: "Resolve a budget incident",
|
|
request: {
|
|
params: z.object({ companyId: z.string(), incidentId: z.string() }),
|
|
body: jsonBody(resolveBudgetIncidentSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/budgets/overview",
|
|
tags: ["costs"],
|
|
summary: "Get budget overview",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "patch",
|
|
path: "/api/companies/{companyId}/budgets",
|
|
tags: ["costs"],
|
|
summary: "Update company budget",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
body: jsonBody(updateBudgetSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "patch",
|
|
path: "/api/agents/{agentId}/budgets",
|
|
tags: ["costs"],
|
|
summary: "Update agent budget",
|
|
request: {
|
|
params: z.object({ agentId: z.string() }),
|
|
body: jsonBody(updateBudgetSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
// ─── Activity ────────────────────────────────────────────────────────────────
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/activity",
|
|
tags: ["activity"],
|
|
summary: "List company activity",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/audit/agent-actions",
|
|
tags: ["activity"],
|
|
summary: "List agent action audit entries",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
query: z.object({
|
|
agentId: z.string().guid().optional(),
|
|
responsibleUserId: z.string().min(1).optional(),
|
|
runId: z.string().guid().optional(),
|
|
entityType: z.string().min(1).optional(),
|
|
entityId: z.string().min(1).optional(),
|
|
action: z.string().min(1).optional(),
|
|
actorType: z.enum(["agent", "user", "system", "plugin"]).optional(),
|
|
from: z.string().datetime().optional(),
|
|
to: z.string().datetime().optional(),
|
|
cursor: z.string().min(1).optional(),
|
|
limit: z.coerce.number().int().min(1).max(200).optional(),
|
|
}),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/audit/agent-actions.csv",
|
|
tags: ["activity"],
|
|
summary: "Export agent action audit entries as CSV",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
query: z.object({
|
|
agentId: z.string().guid().optional(),
|
|
responsibleUserId: z.string().min(1).optional(),
|
|
runId: z.string().guid().optional(),
|
|
entityType: z.string().min(1).optional(),
|
|
entityId: z.string().min(1).optional(),
|
|
action: z.string().min(1).optional(),
|
|
actorType: z.enum(["agent", "user", "system", "plugin"]).optional(),
|
|
from: z.string().datetime().optional(),
|
|
to: z.string().datetime().optional(),
|
|
cursor: z.string().min(1).optional(),
|
|
limit: z.coerce.number().int().min(1).max(200).optional(),
|
|
}),
|
|
},
|
|
responses: {
|
|
200: {
|
|
description: "Agent action audit export",
|
|
content: { "text/csv": { schema: z.string() } },
|
|
},
|
|
400: r.badRequest,
|
|
401: r.unauthorized,
|
|
403: r.forbidden,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/activity",
|
|
tags: ["activity"],
|
|
summary: "Create an activity entry",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
body: jsonBody(z.object({
|
|
actorType: z.enum(["agent", "user", "system", "plugin"]).optional(),
|
|
actorId: z.string().min(1),
|
|
action: z.string().min(1),
|
|
entityType: z.string().min(1),
|
|
entityId: z.string().min(1),
|
|
agentId: z.string().guid().optional().nullable(),
|
|
details: z.record(z.string(), z.unknown()).optional().nullable(),
|
|
})),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/issues/{id}/activity",
|
|
tags: ["activity"],
|
|
summary: "List activity for an issue",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/issues/{id}/runs",
|
|
tags: ["activity"],
|
|
summary: "List runs for an issue",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/heartbeat-runs/{runId}/issues",
|
|
tags: ["activity"],
|
|
summary: "List issues for a heartbeat run",
|
|
request: { params: z.object({ runId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
// ─── Dashboard ───────────────────────────────────────────────────────────────
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/dashboard",
|
|
tags: ["dashboard"],
|
|
summary: "Get dashboard data",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/recovery-observability",
|
|
tags: ["dashboard"],
|
|
summary: "Get recovery observability report",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
query: z.object({
|
|
weeks: z.string().optional(),
|
|
threshold: z.string().optional(),
|
|
}),
|
|
},
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
// ─── Sidebar ─────────────────────────────────────────────────────────────────
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/sidebar-badges",
|
|
tags: ["sidebar"],
|
|
summary: "Get sidebar badge counts",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/attention",
|
|
tags: ["inbox"],
|
|
summary: "List decision-only attention feed items",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
query: z.object({
|
|
includeDismissed: z.enum(["true", "false"]).optional(),
|
|
archived: z.enum(["true", "false"]).optional(),
|
|
all: z.enum(["true", "false"]).optional(),
|
|
activitySince: z.string().datetime().optional(),
|
|
activityUntil: z.string().datetime().optional(),
|
|
queue: z.string().min(1).optional(),
|
|
sort: z.enum(["activity", "decide"]).optional(),
|
|
cursor: z.string().min(1).optional(),
|
|
limit: z.coerce.number().int().min(1).max(100).optional(),
|
|
}),
|
|
},
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden },
|
|
});
|
|
|
|
// ─── Decisions ──────────────────────────────────────────────────────────────
|
|
|
|
// Decision queues and triage
|
|
|
|
const decisionQueueSeedRuleSchema = z.object({
|
|
key: z.string(),
|
|
description: z.string(),
|
|
signal: z.enum([
|
|
"issue_has_pull_request_work_product",
|
|
"plan_document_confirmation",
|
|
"ask_user_questions",
|
|
]),
|
|
}).strict();
|
|
|
|
const decisionQueueSchema = z.object({
|
|
id: z.string(),
|
|
companyId: z.string(),
|
|
key: z.string(),
|
|
title: z.string(),
|
|
description: z.string().nullable(),
|
|
createdByType: z.enum(["agent", "user", "system"]),
|
|
createdByAgentId: z.string().nullable(),
|
|
createdByUserId: z.string().nullable(),
|
|
createdByRunId: z.string().nullable(),
|
|
retentionDays: z.number().int().nullable(),
|
|
seedRules: z.array(decisionQueueSeedRuleSchema),
|
|
seedRulesEnabled: z.boolean(),
|
|
itemCount: z.number().int().nonnegative(),
|
|
createdAt: z.string().datetime(),
|
|
updatedAt: z.string().datetime(),
|
|
}).strict();
|
|
|
|
const decisionQueueItemSchema = z.object({
|
|
id: z.string(),
|
|
companyId: z.string(),
|
|
queueId: z.string(),
|
|
sourceKind: decisionAttentionSourceKindSchema,
|
|
sourceId: z.string(),
|
|
addedByType: z.enum(["agent", "user", "system"]),
|
|
addedByAgentId: z.string().nullable(),
|
|
addedByUserId: z.string().nullable(),
|
|
addedByRunId: z.string().nullable(),
|
|
responsibleUserId: z.string().nullable(),
|
|
createdAt: z.string().datetime(),
|
|
}).strict();
|
|
|
|
const decisionTriageSchema = z.object({
|
|
id: z.string(),
|
|
companyId: z.string(),
|
|
sourceKind: decisionAttentionSourceKindSchema,
|
|
sourceId: z.string(),
|
|
decideBy: z.string().nullable(),
|
|
snoozedUntil: z.string().datetime().nullable(),
|
|
setByType: z.enum(["agent", "user"]),
|
|
setByAgentId: z.string().nullable(),
|
|
setByUserId: z.string().nullable(),
|
|
setByRunId: z.string().nullable(),
|
|
responsibleUserId: z.string().nullable(),
|
|
version: z.number().int().positive(),
|
|
createdAt: z.string().datetime(),
|
|
updatedAt: z.string().datetime(),
|
|
}).strict();
|
|
|
|
const decisionRetentionSchema = z.object({
|
|
id: z.string(),
|
|
companyId: z.string(),
|
|
sourceKind: decisionAttentionSourceKindSchema,
|
|
sourceId: z.string(),
|
|
sourceActivityAt: z.string().datetime(),
|
|
keep: z.boolean(),
|
|
archivedAt: z.string().datetime().nullable(),
|
|
archivedReason: z.string().nullable(),
|
|
archivedByType: z.enum(["agent", "user", "system"]).nullable(),
|
|
archivedByAgentId: z.string().nullable(),
|
|
archivedByUserId: z.string().nullable(),
|
|
archivedByRunId: z.string().nullable(),
|
|
version: z.number().int().positive(),
|
|
archiveVersion: z.number().int().nonnegative(),
|
|
createdAt: z.string().datetime(),
|
|
updatedAt: z.string().datetime(),
|
|
}).strict();
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/decision-queue-seed-rules",
|
|
tags: ["decision-queues"],
|
|
summary: "List built-in decision queue seed rules",
|
|
responses: { 200: r.ok(z.array(decisionQueueSeedRuleSchema)), 401: r.unauthorized, 403: r.forbidden },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/decision-queues",
|
|
tags: ["decision-queues"],
|
|
summary: "List decision queues",
|
|
responses: { 200: r.ok(z.array(decisionQueueSchema)), 401: r.unauthorized, 403: r.forbidden },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/decision-queues",
|
|
tags: ["decision-queues"],
|
|
summary: "Create a decision queue",
|
|
body: createDecisionQueueSchema,
|
|
responses: {
|
|
200: r.ok(decisionQueueSchema),
|
|
201: r.ok(decisionQueueSchema),
|
|
400: r.badRequest,
|
|
401: r.unauthorized,
|
|
403: r.forbidden,
|
|
},
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "patch",
|
|
path: "/api/companies/{companyId}/decision-queues/{key}",
|
|
tags: ["decision-queues"],
|
|
summary: "Update a decision queue",
|
|
body: updateDecisionQueueSchema,
|
|
responses: {
|
|
200: r.ok(decisionQueueSchema),
|
|
400: r.badRequest,
|
|
401: r.unauthorized,
|
|
403: r.forbidden,
|
|
404: r.notFound,
|
|
},
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/decision-queues/{key}/items",
|
|
tags: ["decision-queues"],
|
|
summary: "List visible items in a decision queue",
|
|
responses: {
|
|
200: r.ok(z.array(decisionQueueItemSchema)),
|
|
401: r.unauthorized,
|
|
403: r.forbidden,
|
|
404: r.notFound,
|
|
},
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/decision-queues/{key}/items",
|
|
tags: ["decision-queues"],
|
|
summary: "Add an item to a decision queue",
|
|
body: addDecisionQueueItemSchema,
|
|
responses: {
|
|
200: r.ok(decisionQueueItemSchema),
|
|
201: r.ok(decisionQueueItemSchema),
|
|
400: r.badRequest,
|
|
401: r.unauthorized,
|
|
403: r.forbidden,
|
|
404: r.notFound,
|
|
},
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "delete",
|
|
path: "/api/companies/{companyId}/decision-queues/{key}/items/{sourceKind}/{sourceId}",
|
|
tags: ["decision-queues"],
|
|
summary: "Remove an item from a decision queue",
|
|
body: removeDecisionQueueItemSchema,
|
|
responses: {
|
|
200: r.ok(decisionQueueItemSchema),
|
|
400: r.badRequest,
|
|
401: r.unauthorized,
|
|
403: r.forbidden,
|
|
404: r.notFound,
|
|
},
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/decision-triage/{sourceKind}/{sourceId}",
|
|
tags: ["decision-queues"],
|
|
summary: "Get decision triage for an attention source",
|
|
responses: {
|
|
200: r.ok(decisionTriageSchema.nullable()),
|
|
400: r.badRequest,
|
|
401: r.unauthorized,
|
|
403: r.forbidden,
|
|
404: r.notFound,
|
|
},
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "put",
|
|
path: "/api/companies/{companyId}/decision-triage/{sourceKind}/{sourceId}",
|
|
tags: ["decision-queues"],
|
|
summary: "Set decision triage for an attention source",
|
|
body: updateDecisionTriageSchema,
|
|
responses: {
|
|
200: r.ok(decisionTriageSchema),
|
|
400: r.badRequest,
|
|
401: r.unauthorized,
|
|
403: r.forbidden,
|
|
404: r.notFound,
|
|
422: r.unprocessable,
|
|
},
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "patch",
|
|
path: "/api/companies/{companyId}/decision-retention/{sourceKind}/{sourceId}",
|
|
tags: ["decision-queues"],
|
|
summary: "Set Keep for an attention source",
|
|
body: updateDecisionRetentionSchema,
|
|
responses: { 200: r.ok(decisionRetentionSchema), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
|
|
});
|
|
|
|
for (const action of ["archive", "revive"] as const) {
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: `/api/companies/{companyId}/decision-retention/{sourceKind}/{sourceId}/${action}`,
|
|
tags: ["decision-queues"],
|
|
summary: action === "archive" ? "Archive an attention source" : "Revive an archived attention source",
|
|
responses: { 200: r.ok(decisionRetentionSchema), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
|
|
});
|
|
}
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/decision-archive-proposals",
|
|
tags: ["decisions"],
|
|
summary: "Propose one signed bulk archive decision",
|
|
body: createDecisionArchiveProposalSchema,
|
|
responses: { 201: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 409: r.conflict, 422: r.unprocessable },
|
|
});
|
|
|
|
// Decisions
|
|
|
|
const createDecisionBodySchema = z.object({
|
|
title: z.string().trim().min(1).max(500),
|
|
body: z.string().max(100_000),
|
|
ruleKey: z.string().trim().max(240).nullable().optional(),
|
|
options: decisionOptionsSchema,
|
|
inputs: decisionInputsSchema.nullable().optional(),
|
|
expiresAt: z.string().datetime().optional(),
|
|
idempotencyKey: z.string().trim().min(1).max(500).nullable().optional(),
|
|
continuationPolicy: z.enum(["none", "wake_origin_agent"]).optional(),
|
|
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
}).strict();
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/decisions",
|
|
tags: ["decisions"],
|
|
summary: "Propose a decision",
|
|
body: createDecisionBodySchema,
|
|
responses: { 201: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 409: r.conflict },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/decision-bundles",
|
|
tags: ["decisions"],
|
|
summary: "Propose a decision bundle",
|
|
body: z.object({
|
|
title: z.string().trim().min(1).max(500),
|
|
summary: z.string().max(100_000),
|
|
decisions: z.array(createDecisionBodySchema).min(1).max(50),
|
|
}).strict(),
|
|
responses: { 201: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 409: r.conflict },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/decisions",
|
|
tags: ["decisions"],
|
|
summary: "List decisions",
|
|
query: z.object({
|
|
status: z.enum(["open", "decided", "expired", "cancelled"]).optional(),
|
|
bundleId: z.string().guid().optional(),
|
|
targetIssueId: z.string().guid().optional(),
|
|
originAgentId: z.string().guid().optional(),
|
|
limit: z.coerce.number().int().positive().max(100).optional(),
|
|
}),
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/decisions/stats",
|
|
tags: ["decisions"],
|
|
summary: "Get decision telemetry grouped by rule key",
|
|
query: z.object({
|
|
groupBy: z.literal("ruleKey"),
|
|
originAgentId: z.string().guid().optional(),
|
|
since: z.string().datetime().optional(),
|
|
}),
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/decisions/{id}",
|
|
tags: ["decisions"],
|
|
summary: "Get a decision outcome",
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/decisions/{id}/decide",
|
|
tags: ["decisions"],
|
|
summary: "Resolve a decision",
|
|
body: z.object({
|
|
optionId: z.string().trim().min(1).max(120),
|
|
inputValues: z.record(z.string(), z.string().max(20_000)).optional(),
|
|
idempotencyKey: z.string().trim().min(1).max(500).nullable().optional(),
|
|
}).strict(),
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound, 409: r.conflict },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/decisions/{id}/dismiss",
|
|
tags: ["decisions"],
|
|
summary: "Dismiss a decision",
|
|
body: z.object({ reason: z.string().max(20_000).nullable().optional() }).strict(),
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound, 409: r.conflict },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/decisions/{id}/cancel",
|
|
tags: ["decisions"],
|
|
summary: "Cancel a decision",
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden, 404: r.notFound, 409: r.conflict },
|
|
});
|
|
|
|
// ─── Decision training ──────────────────────────────────────────────────────
|
|
|
|
const decisionTrainingSourceKindSchema = z.enum(["interaction", "approval", "execution_decision"]);
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/decision-training",
|
|
tags: ["decision-training"],
|
|
summary: "Capture a decision training example",
|
|
body: z.object({
|
|
sourceKind: decisionTrainingSourceKindSchema,
|
|
sourceId: z.string().guid(),
|
|
issueId: z.string().guid(),
|
|
notes: z.string().max(100_000).default(""),
|
|
}).strict(),
|
|
responses: { 201: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound, 409: r.conflict },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/decision-training/preview",
|
|
tags: ["decision-training"],
|
|
summary: "Preview a decision training snapshot",
|
|
body: z.object({
|
|
sourceKind: decisionTrainingSourceKindSchema,
|
|
sourceId: z.string().guid(),
|
|
issueId: z.string().guid(),
|
|
}).strict(),
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound, 409: r.conflict },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/decision-training",
|
|
tags: ["decision-training"],
|
|
summary: "List decision training examples",
|
|
query: z.object({
|
|
project: z.string().guid().optional(),
|
|
kind: decisionTrainingSourceKindSchema.optional(),
|
|
author: z.string().optional(),
|
|
q: z.string().max(500).optional(),
|
|
}),
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/decision-training/export.jsonl",
|
|
tags: ["decision-training"],
|
|
summary: "Export decision training examples as JSONL",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/decision-training/{id}",
|
|
tags: ["decision-training"],
|
|
summary: "Get a decision training example",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "patch",
|
|
path: "/api/decision-training/{id}",
|
|
tags: ["decision-training"],
|
|
summary: "Update decision training notes",
|
|
body: z.object({ notes: z.string().max(100_000) }).strict(),
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "delete",
|
|
path: "/api/decision-training/{id}",
|
|
tags: ["decision-training"],
|
|
summary: "Delete a decision training example",
|
|
responses: { 204: r.ok(), 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/sidebar-preferences/me",
|
|
tags: ["sidebar"],
|
|
summary: "Get current user sidebar preferences",
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "put",
|
|
path: "/api/sidebar-preferences/me",
|
|
tags: ["sidebar"],
|
|
summary: "Update current user sidebar preferences",
|
|
request: { body: jsonBody(upsertSidebarOrderPreferenceSchema) },
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/sidebar-preferences/me",
|
|
tags: ["sidebar"],
|
|
summary: "Get sidebar preferences for company",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "put",
|
|
path: "/api/companies/{companyId}/sidebar-preferences/me",
|
|
tags: ["sidebar"],
|
|
summary: "Update sidebar preferences for company",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
body: jsonBody(upsertSidebarOrderPreferenceSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
// ─── Inbox dismissals ────────────────────────────────────────────────────────
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/inbox-dismissals",
|
|
tags: ["inbox"],
|
|
summary: "List inbox dismissals",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/inbox-dismissals",
|
|
tags: ["inbox"],
|
|
summary: "Create an inbox dismissal or snooze",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
body: jsonBody(z.object({
|
|
itemKey: z.string().trim().min(1).regex(/^(approval|join|run|attention):.+$/, "Unsupported inbox item key"),
|
|
kind: z.enum(["dismiss", "snooze"]).optional(),
|
|
snoozedUntil: z.string().datetime().optional(),
|
|
})),
|
|
},
|
|
responses: { 201: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "delete",
|
|
path: "/api/companies/{companyId}/inbox-dismissals/{itemKey}",
|
|
tags: ["inbox"],
|
|
summary: "Restore an inbox dismissal or snooze",
|
|
request: { params: z.object({ companyId: z.string(), itemKey: z.string() }) },
|
|
responses: { 204: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
// ─── Instance settings ────────────────────────────────────────────────────────
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/instance/settings",
|
|
tags: ["instance"],
|
|
summary: "Get instance settings",
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "patch",
|
|
path: "/api/instance/settings",
|
|
tags: ["instance"],
|
|
summary: "Update instance settings",
|
|
request: { body: jsonBody(patchInstanceSettingsSchema) },
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/instance/settings/general",
|
|
tags: ["instance"],
|
|
summary: "Get general instance settings",
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "patch",
|
|
path: "/api/instance/settings/general",
|
|
tags: ["instance"],
|
|
summary: "Update general instance settings",
|
|
request: { body: jsonBody(patchInstanceGeneralSettingsSchema) },
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/instance/settings/experimental",
|
|
tags: ["instance"],
|
|
summary: "Get experimental instance settings",
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "patch",
|
|
path: "/api/instance/settings/experimental",
|
|
tags: ["instance"],
|
|
summary: "Update experimental instance settings",
|
|
request: { body: jsonBody(patchInstanceExperimentalSettingsSchema) },
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/instance/task-drain",
|
|
tags: ["instance"],
|
|
summary: "Get the task-drain status for this process only; quiescent counts in-process work, and a process restart clears it even when the database still holds running rows",
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/instance/task-drain",
|
|
tags: ["instance"],
|
|
summary: "Start a task drain, so new run admission holds until active runs finish",
|
|
request: { body: jsonBody(startTaskDrainRequestSchema) },
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "delete",
|
|
path: "/api/instance/task-drain",
|
|
tags: ["instance"],
|
|
summary: "End a task drain and restore run admission",
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden },
|
|
});
|
|
|
|
// ─── Board chat (Conference Room Chat, experimental) ──────────────────────────
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/board/chat/stream",
|
|
tags: ["instance"],
|
|
summary: "Stream a board-level chat response (requires enableConferenceRoomChat)",
|
|
request: {
|
|
body: jsonBody(
|
|
z.object({
|
|
companyId: z.string(),
|
|
message: z.string(),
|
|
taskId: z.string().optional(),
|
|
}),
|
|
),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden },
|
|
});
|
|
|
|
// ─── Access / invites / members ───────────────────────────────────────────────
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/invites",
|
|
tags: ["access"],
|
|
summary: "List company invites",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/invites",
|
|
tags: ["access"],
|
|
summary: "Create a company invite",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
body: jsonBody(createCompanyInviteSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/join-requests",
|
|
tags: ["access"],
|
|
summary: "List company join requests",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/join-requests/{requestId}/approve",
|
|
tags: ["access"],
|
|
summary: "Approve a company join request",
|
|
request: { params: z.object({ companyId: z.string(), requestId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/join-requests/{requestId}/reject",
|
|
tags: ["access"],
|
|
summary: "Reject a company join request",
|
|
request: { params: z.object({ companyId: z.string(), requestId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/invites/{inviteId}/revoke",
|
|
tags: ["access"],
|
|
summary: "Revoke an invite",
|
|
request: { params: z.object({ inviteId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/invites/{token}",
|
|
tags: ["access"],
|
|
summary: "Get an invite by token",
|
|
request: { params: z.object({ token: z.string() }) },
|
|
responses: { 200: r.ok(), 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/invites/{token}/accept",
|
|
tags: ["access"],
|
|
summary: "Accept an invite and create or replay a join request",
|
|
request: {
|
|
params: z.object({ token: z.string() }),
|
|
body: jsonBody(acceptInviteSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/members",
|
|
tags: ["access"],
|
|
summary: "List company members",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "patch",
|
|
path: "/api/companies/{companyId}/members/{memberId}",
|
|
tags: ["access"],
|
|
summary: "Update a company member status or role",
|
|
request: {
|
|
params: z.object({ companyId: z.string(), memberId: z.string() }),
|
|
body: jsonBody(updateCompanyMemberSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "patch",
|
|
path: "/api/companies/{companyId}/members/{memberId}/role-and-grants",
|
|
tags: ["access"],
|
|
summary: "Update a company member role and explicit grants",
|
|
request: {
|
|
params: z.object({ companyId: z.string(), memberId: z.string() }),
|
|
body: jsonBody(updateCompanyMemberWithPermissionsSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/members/{memberId}/archive",
|
|
tags: ["access"],
|
|
summary: "Archive a company member",
|
|
request: {
|
|
params: z.object({ companyId: z.string(), memberId: z.string() }),
|
|
body: jsonBody(archiveCompanyMemberSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "patch",
|
|
path: "/api/companies/{companyId}/members/{memberId}/permissions",
|
|
tags: ["access"],
|
|
summary: "Update explicit company member permissions",
|
|
request: {
|
|
params: z.object({ companyId: z.string(), memberId: z.string() }),
|
|
body: jsonBody(updateMemberPermissionsSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/user-directory",
|
|
tags: ["access"],
|
|
summary: "Get company user directory",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/cli-auth/me",
|
|
tags: ["access"],
|
|
summary: "Get current CLI auth session",
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/openclaw/invite-prompt",
|
|
tags: ["access"],
|
|
summary: "Create an OpenClaw invite prompt bundle",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
body: jsonBody(createOpenClawInvitePromptSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/cli-auth/challenges",
|
|
tags: ["access"],
|
|
summary: "Create a CLI auth challenge",
|
|
request: { body: jsonBody(createCliAuthChallengeSchema) },
|
|
responses: { 200: r.ok(), 400: r.badRequest },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/cli-auth/challenges/{id}/approve",
|
|
tags: ["access"],
|
|
summary: "Approve a CLI auth challenge",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(resolveCliAuthChallengeSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/cli-auth/challenges/{id}/cancel",
|
|
tags: ["access"],
|
|
summary: "Cancel a CLI auth challenge",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(resolveCliAuthChallengeSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/cli-auth/revoke-current",
|
|
tags: ["access"],
|
|
summary: "Revoke current CLI auth session",
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/skills/available",
|
|
tags: ["access"],
|
|
summary: "List available skills",
|
|
responses: { 200: r.ok() },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/skills/index",
|
|
tags: ["access"],
|
|
summary: "Get skills index",
|
|
responses: { 200: r.ok() },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/skills/{skillName}",
|
|
tags: ["access"],
|
|
summary: "Get a skill by name",
|
|
request: { params: z.object({ skillName: z.string() }) },
|
|
responses: { 200: r.ok(), 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/join-requests/{requestId}/claim-api-key",
|
|
tags: ["access"],
|
|
summary: "Claim the initial API key for an approved agent join request",
|
|
request: {
|
|
params: z.object({ requestId: z.string() }),
|
|
body: jsonBody(claimJoinRequestApiKeySchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 403: r.forbidden, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/admin/users",
|
|
tags: ["admin"],
|
|
summary: "List all users (admin)",
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden },
|
|
});
|
|
|
|
// ─── Auth / profile ──────────────────────────────────────────────────────────
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/auth/get-session",
|
|
tags: ["auth"],
|
|
summary: "Get current session",
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/auth/profile",
|
|
tags: ["auth"],
|
|
summary: "Get current user profile",
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "patch",
|
|
path: "/api/auth/profile",
|
|
tags: ["auth"],
|
|
summary: "Update current user profile",
|
|
request: { body: jsonBody(updateCurrentUserProfileSchema) },
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/users/{userSlug}/profile",
|
|
tags: ["auth"],
|
|
summary: "Get a user profile within a company",
|
|
request: { params: z.object({ companyId: z.string(), userSlug: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/managed-agent-profiles",
|
|
tags: ["agents"],
|
|
summary: "List Claude Managed Agent profiles for a company",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/managed-agent-profiles",
|
|
tags: ["agents"],
|
|
summary: "Create or operator-attest a Claude Managed Agent profile",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
body: jsonBody(z.object({
|
|
profileKey: z.string(),
|
|
displayName: z.string(),
|
|
anthropicAgentId: z.string(),
|
|
agentVersion: z.string(),
|
|
environmentId: z.string(),
|
|
defaultModel: z.literal("claude-sonnet-5").optional(),
|
|
defaultMaxListCostUsd: z.number().positive().optional(),
|
|
apiKeySecretId: z.string(),
|
|
enabled: z.boolean().optional(),
|
|
retentionAcknowledged: z.boolean().optional(),
|
|
qualification: z.object({
|
|
probedAt: z.string().datetime(),
|
|
betaVersion: z.literal("managed-agents-2026-04-01"),
|
|
environmentPolicy: z.literal("limited_no_hosts_no_packages"),
|
|
agentCapabilities: z.literal("no_tools_no_mcp_no_skills_no_multiagent"),
|
|
}).strict().optional(),
|
|
})),
|
|
},
|
|
responses: {
|
|
201: r.ok(),
|
|
401: r.unauthorized,
|
|
403: r.forbidden,
|
|
409: r.conflict,
|
|
422: r.unprocessable,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/remote-agent-profiles",
|
|
tags: ["agents"],
|
|
summary: "List remote AgentCore profiles for a company",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
query: z.object({
|
|
service: z.literal("aws_bedrock_agentcore_harness").optional(),
|
|
}),
|
|
},
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden, 422: r.unprocessable },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/remote-agent-profiles",
|
|
tags: ["agents"],
|
|
summary: "Create or operator-attest a remote AgentCore profile",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
body: jsonBody(z.object({
|
|
profileKey: z.string(),
|
|
displayName: z.string(),
|
|
service: z.literal("aws_bedrock_agentcore_harness"),
|
|
configuration: z.record(z.string(), z.unknown()),
|
|
enabled: z.boolean().optional(),
|
|
retentionAcknowledged: z.boolean().optional(),
|
|
qualification: z.object({ suite: z.literal("aws-agentcore-harness-context-v2") }).strict().optional(),
|
|
})),
|
|
},
|
|
responses: {
|
|
201: r.ok(),
|
|
401: r.unauthorized,
|
|
403: r.forbidden,
|
|
409: r.conflict,
|
|
422: r.unprocessable,
|
|
},
|
|
});
|
|
|
|
// ─── Heartbeat runs ──────────────────────────────────────────────────────────
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/heartbeat-runs",
|
|
tags: ["runs"],
|
|
summary: "List heartbeat runs for a company",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/provider-traces",
|
|
tags: ["runs"],
|
|
summary: "List provider trace metadata for selected runs",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/live-runs",
|
|
tags: ["runs"],
|
|
summary: "List live runs for a company",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/issues/{issueId}/live-runs",
|
|
tags: ["runs"],
|
|
summary: "List live runs for an issue",
|
|
request: { params: z.object({ issueId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/issues/{issueId}/active-run",
|
|
tags: ["runs"],
|
|
summary: "Get active run for an issue",
|
|
request: { params: z.object({ issueId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/heartbeat-runs/{runId}",
|
|
tags: ["runs"],
|
|
summary: "Get a heartbeat run",
|
|
request: { params: z.object({ runId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/heartbeat-runs/{runId}/cancel",
|
|
tags: ["runs"],
|
|
summary: "Cancel a heartbeat run",
|
|
request: { params: z.object({ runId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/heartbeat-runs/{runId}/provider-trace",
|
|
tags: ["runs"],
|
|
summary: "Inspect a redacted provider trace",
|
|
request: { params: z.object({ runId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/heartbeat-runs/{runId}/provider-trace/reproject-workspace-diffs",
|
|
tags: ["runs"],
|
|
summary: "Reproject retained Codex workspace diffs into run events",
|
|
request: { params: z.object({ runId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/heartbeat-runs/{runId}/provider-trace/frames/{frameId}/reveal",
|
|
tags: ["runs"],
|
|
summary: "Reveal one exact provider trace frame",
|
|
request: { params: z.object({ runId: z.string(), frameId: z.coerce.number().int().positive() }) },
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/heartbeat-runs/{runId}/provider-trace/download",
|
|
tags: ["runs"],
|
|
summary: "Download an exact provider trace as NDJSON",
|
|
request: { params: z.object({ runId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "delete",
|
|
path: "/api/heartbeat-runs/{runId}/provider-trace",
|
|
tags: ["runs"],
|
|
summary: "Permanently delete a provider trace",
|
|
request: { params: z.object({ runId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/issues/{id}/queued-comments",
|
|
tags: ["issues"],
|
|
summary: "List queued comments for an issue",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "patch",
|
|
path: "/api/issues/{id}/queued-comments/{commentId}",
|
|
tags: ["issues"],
|
|
summary: "Edit a queued issue comment",
|
|
request: {
|
|
params: z.object({ id: z.string(), commentId: z.string() }),
|
|
body: jsonBody(z.object({
|
|
queueId: z.string().min(1),
|
|
revision: z.string().min(1),
|
|
body: z.string().min(1).max(200_000),
|
|
})),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound, 409: r.conflict },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "put",
|
|
path: "/api/issues/{id}/queued-comments/order",
|
|
tags: ["issues"],
|
|
summary: "Reorder queued issue comments",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(z.object({
|
|
queueId: z.string().min(1),
|
|
revision: z.string().min(1),
|
|
orderedCommentIds: z.array(z.string().min(1)).max(500),
|
|
})),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound, 409: r.conflict },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "delete",
|
|
path: "/api/issues/{id}/queued-comments/{commentId}",
|
|
tags: ["issues"],
|
|
summary: "Delete a queued issue comment",
|
|
request: {
|
|
params: z.object({ id: z.string(), commentId: z.string() }),
|
|
body: jsonBody(z.object({
|
|
queueId: z.string().min(1),
|
|
revision: z.string().min(1),
|
|
})),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound, 409: r.conflict },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/issues/{id}/queued-comments/{commentId}/steer",
|
|
tags: ["issues"],
|
|
summary: "Steer a queued issue comment into the active native run",
|
|
request: {
|
|
params: z.object({ id: z.string(), commentId: z.string() }),
|
|
body: jsonBody(z.object({
|
|
queueId: z.string().min(1),
|
|
revision: z.string().min(1),
|
|
targetRunId: z.string().min(1),
|
|
})),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound, 409: r.conflict },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/heartbeat-runs/{runId}/runtime-requests/{requestId}/resolve",
|
|
tags: ["runs"],
|
|
summary: "Resolve a pending Paperclip runner runtime request",
|
|
request: {
|
|
params: z.object({ runId: z.string(), requestId: z.string() }),
|
|
body: jsonBody(z.object({
|
|
turnId: z.string().min(1).max(160),
|
|
requestKind: z.enum(["command_approval", "file_approval", "permission_approval", "user_input", "elicitation"]),
|
|
resolution: z.union([
|
|
z.object({ action: z.enum(["accept", "accept_for_session", "decline", "cancel"]) }),
|
|
z.object({ action: z.literal("submit"), answers: z.record(z.string(), z.object({ answers: z.array(z.string()) })) }),
|
|
z.object({ action: z.literal("submit"), content: z.record(z.string(), z.unknown()) }),
|
|
]),
|
|
})),
|
|
},
|
|
responses: { 202: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound, 409: r.conflict },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/heartbeat-runs/{runId}/watchdog-decisions",
|
|
tags: ["runs"],
|
|
summary: "Submit watchdog decisions for a run",
|
|
request: {
|
|
params: z.object({ runId: z.string() }),
|
|
body: jsonBody(z.object({
|
|
decision: z.enum(["snooze", "continue", "dismissed_false_positive"]),
|
|
evaluationIssueId: z.string().optional().nullable(),
|
|
reason: z.string().optional().nullable(),
|
|
snoozedUntil: z.string().datetime().optional().nullable(),
|
|
})),
|
|
},
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/heartbeat-runs/{runId}/events",
|
|
tags: ["runs"],
|
|
summary: "Get events for a heartbeat run",
|
|
request: { params: z.object({ runId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/heartbeat-runs/{runId}/log",
|
|
tags: ["runs"],
|
|
summary: "Get log for a heartbeat run",
|
|
request: { params: z.object({ runId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/heartbeat-runs/{runId}/workspace-operations",
|
|
tags: ["runs"],
|
|
summary: "List workspace operations for a run",
|
|
request: { params: z.object({ runId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/workspace-operations/{operationId}/log",
|
|
tags: ["runs"],
|
|
summary: "Get log for a workspace operation",
|
|
request: { params: z.object({ operationId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
// ─── Agent runs & heartbeat ───────────────────────────────────────────────────
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/agents/{id}/approve",
|
|
tags: ["agents"],
|
|
summary: "Approve a pending agent action",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/agents/{id}/heartbeat/invoke",
|
|
tags: ["agents"],
|
|
summary: "Invoke agent heartbeat",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/agents/{id}/claude-login",
|
|
tags: ["agents"],
|
|
summary: "Trigger Claude login for agent",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
// Company-and-environment Claude setup-token login session routes. The owner
|
|
// user starts one session for one adapter in one environment. The scope carries
|
|
// no agent id, so a hire flow with no agent still starts one session. The owner
|
|
// reads the login prompt, submits the browser code from the browser, and reads
|
|
// the completion claim. The prompt, code, and completion responses require a
|
|
// confidential transport; the guard returns 403 when the transport is not
|
|
// confidential. No response carries a token; the completion returns the
|
|
// non-secret `storedSessionId` claim.
|
|
//
|
|
// The stored-token status read returns only the secret id and the latest version
|
|
// of the owner value; it returns no token. The server derives the owner from the
|
|
// authenticated actor. A missing or a foreign value returns the same fixed 404,
|
|
// so the read discloses no existence distinction. The response is `no-store`.
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/claude-oauth-token-status",
|
|
tags: ["companies"],
|
|
summary: "Read the stored Claude OAuth token status for the authenticated owner",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: {
|
|
200: r.ok(claudeOAuthTokenStatusResponseSchema),
|
|
401: r.unauthorized,
|
|
403: r.forbidden,
|
|
404: r.notFound,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/setup-token-login-sessions",
|
|
tags: ["companies"],
|
|
summary: "Start a company-and-environment Claude setup-token login session",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
body: jsonBody(startClaudeSetupTokenSessionRequestSchema),
|
|
},
|
|
responses: {
|
|
201: r.ok(claudeSetupTokenSessionOwnerResponseSchema),
|
|
400: r.badRequest,
|
|
401: r.unauthorized,
|
|
403: r.forbidden,
|
|
404: r.notFound,
|
|
503: r.serverError,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/setup-token-login-sessions/active",
|
|
tags: ["companies"],
|
|
summary: "Read the caller's active Claude setup-token login session",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: {
|
|
200: r.ok(claudeSetupTokenSessionOwnerResponseSchema),
|
|
401: r.unauthorized,
|
|
403: r.forbidden,
|
|
404: r.notFound,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/setup-token-login-sessions/{sessionId}",
|
|
tags: ["companies"],
|
|
summary: "Read the status of a Claude setup-token login session",
|
|
request: { params: z.object({ companyId: z.string(), sessionId: z.string() }) },
|
|
responses: {
|
|
200: r.ok(claudeSetupTokenSessionResponseSchema),
|
|
401: r.unauthorized,
|
|
403: r.forbidden,
|
|
404: r.notFound,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/setup-token-login-sessions/{sessionId}/prompt",
|
|
tags: ["companies"],
|
|
summary: "Read the login prompt for a Claude setup-token login session",
|
|
request: { params: z.object({ companyId: z.string(), sessionId: z.string() }) },
|
|
responses: {
|
|
200: r.ok(claudeSetupTokenSessionPromptSchema),
|
|
400: r.badRequest,
|
|
401: r.unauthorized,
|
|
403: r.forbidden,
|
|
404: r.notFound,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/setup-token-login-sessions/{sessionId}/code",
|
|
tags: ["companies"],
|
|
summary: "Submit the browser code for a Claude setup-token login session",
|
|
request: {
|
|
params: z.object({ companyId: z.string(), sessionId: z.string() }),
|
|
body: jsonBody(submitBrowserCodeRequestSchema),
|
|
},
|
|
responses: {
|
|
200: r.ok(claudeSetupTokenSessionResponseSchema),
|
|
400: r.badRequest,
|
|
401: r.unauthorized,
|
|
403: r.forbidden,
|
|
404: r.notFound,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/setup-token-login-sessions/{sessionId}/completion",
|
|
tags: ["companies"],
|
|
summary: "Read the completion claim of a Claude setup-token login session",
|
|
request: { params: z.object({ companyId: z.string(), sessionId: z.string() }) },
|
|
responses: {
|
|
200: r.ok(claudeSetupTokenCompletionResponseSchema),
|
|
400: r.badRequest,
|
|
401: r.unauthorized,
|
|
403: r.forbidden,
|
|
404: r.notFound,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/setup-token-login-sessions/{sessionId}/cancel",
|
|
tags: ["companies"],
|
|
summary: "Cancel a Claude setup-token login session",
|
|
// The 404 is reserved for the pre-scope non-member gate. The company-access
|
|
// gate runs before the cancel logic and returns a fixed 404 for a non-member.
|
|
// Cancel itself is idempotent and stays uniform: a same-company owner-scoped
|
|
// missing, terminal, or foreign session id all return 200. So the route never
|
|
// confirms a session exists for the owner.
|
|
request: { params: z.object({ companyId: z.string(), sessionId: z.string() }) },
|
|
responses: {
|
|
200: r.ok(),
|
|
401: r.unauthorized,
|
|
403: r.forbidden,
|
|
404: r.notFound,
|
|
},
|
|
});
|
|
|
|
// ─── Issue interactions & tree ───────────────────────────────────────────────
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/issues/{id}/interactions",
|
|
tags: ["issues"],
|
|
summary: "List issue thread interactions",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/issues/{id}/interactions",
|
|
tags: ["issues"],
|
|
summary: "Create an issue thread interaction",
|
|
description:
|
|
"Resolver policy defaults to canonical `anyone` for every interaction kind. `not_creator` and `human_only` are opt-in restrictions; deprecated `board_or_agents` and `board_only` inputs are accepted as compatibility aliases.",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(createIssueThreadInteractionSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/issues/{id}/interactions/{interactionId}/accept",
|
|
tags: ["issues"],
|
|
summary: "Accept an issue thread interaction",
|
|
request: {
|
|
params: z.object({ id: z.string(), interactionId: z.string() }),
|
|
body: jsonBody(acceptIssueThreadInteractionSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/issues/{id}/interactions/{interactionId}/reject",
|
|
tags: ["issues"],
|
|
summary: "Reject an issue thread interaction",
|
|
request: {
|
|
params: z.object({ id: z.string(), interactionId: z.string() }),
|
|
body: jsonBody(rejectIssueThreadInteractionSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/issues/{id}/interactions/{interactionId}/respond",
|
|
tags: ["issues"],
|
|
summary: "Answer questions on an issue thread interaction",
|
|
request: {
|
|
params: z.object({ id: z.string(), interactionId: z.string() }),
|
|
body: jsonBody(respondIssueThreadInteractionSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/issues/{id}/interactions/{interactionId}/verdicts",
|
|
tags: ["issues"],
|
|
summary: "Submit item verdicts on an issue thread interaction",
|
|
request: {
|
|
params: z.object({ id: z.string(), interactionId: z.string() }),
|
|
body: jsonBody(submitIssueThreadInteractionVerdictsSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/issues/{id}/interactions/{interactionId}/withdraw",
|
|
tags: ["issues"],
|
|
summary: "Withdraw a pending issue thread interaction",
|
|
request: {
|
|
params: z.object({ id: z.string(), interactionId: z.string() }),
|
|
body: jsonBody(withdrawIssueThreadInteractionSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/issues/{id}/children",
|
|
tags: ["issues"],
|
|
summary: "Create child issues",
|
|
request: { params: z.object({ id: z.string() }), body: jsonBody(createChildIssueSchema) },
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 422: r.unprocessable },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/issues/{id}/admin/force-release",
|
|
tags: ["issues"],
|
|
summary: "Force-release an issue (admin)",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/issues/{id}/tree-control/state",
|
|
tags: ["issues"],
|
|
summary: "Get issue tree control state",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/issues/{id}/tree-control/preview",
|
|
tags: ["issues"],
|
|
summary: "Preview issue tree control changes",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(previewIssueTreeControlSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/issues/{id}/tree-holds",
|
|
tags: ["issues"],
|
|
summary: "List issue tree holds",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/issues/{id}/tree-holds",
|
|
tags: ["issues"],
|
|
summary: "Create an issue tree hold",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(createIssueTreeHoldSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/issues/{id}/tree-holds/{holdId}",
|
|
tags: ["issues"],
|
|
summary: "Get an issue tree hold",
|
|
request: { params: z.object({ id: z.string(), holdId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/issues/{id}/tree-holds/{holdId}/release",
|
|
tags: ["issues"],
|
|
summary: "Release an issue tree hold",
|
|
request: {
|
|
params: z.object({ id: z.string(), holdId: z.string() }),
|
|
body: jsonBody(releaseIssueTreeHoldSchema),
|
|
},
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
// ─── Attachments ──────────────────────────────────────────────────────────────
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/issues/{issueId}/attachments",
|
|
tags: ["assets"],
|
|
summary: "Upload an attachment to an issue",
|
|
request: { params: z.object({ companyId: z.string(), issueId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/attachments/{attachmentId}/content",
|
|
tags: ["assets"],
|
|
summary: "Download attachment content",
|
|
request: { params: z.object({ attachmentId: z.string() }) },
|
|
responses: { 200: { description: "File content" }, 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "delete",
|
|
path: "/api/attachments/{attachmentId}",
|
|
tags: ["assets"],
|
|
summary: "Delete an attachment",
|
|
request: { params: z.object({ attachmentId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
// ─── Assets ──────────────────────────────────────────────────────────────────
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/assets/images",
|
|
tags: ["assets"],
|
|
summary: "Upload an image asset",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/logo",
|
|
tags: ["assets"],
|
|
summary: "Upload company logo",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/onboarding-seed",
|
|
tags: ["companies"],
|
|
summary: "Apply the onboarding seed Paperclip Cloud collected at signup",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 422: r.unprocessable },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/assets/{assetId}/content",
|
|
tags: ["assets"],
|
|
summary: "Download asset content",
|
|
request: { params: z.object({ assetId: z.string() }) },
|
|
responses: { 200: { description: "File content" }, 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
// ─── Company skills ───────────────────────────────────────────────────────────
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/skills",
|
|
tags: ["skills"],
|
|
summary: "List skills for a company",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/skills/{skillId}",
|
|
tags: ["skills"],
|
|
summary: "Get a company skill",
|
|
request: { params: z.object({ companyId: z.string(), skillId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/skills/{skillId}/update-status",
|
|
tags: ["skills"],
|
|
summary: "Get skill update status",
|
|
request: { params: z.object({ companyId: z.string(), skillId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/skills/{skillId}/files",
|
|
tags: ["skills"],
|
|
summary: "List skill files",
|
|
request: { params: z.object({ companyId: z.string(), skillId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/skills/{skillId}/rename",
|
|
tags: ["skills"],
|
|
summary: "Rename a managed company skill",
|
|
request: {
|
|
params: z.object({ companyId: z.string(), skillId: z.string() }),
|
|
body: jsonBody(companySkillRenameSchema),
|
|
},
|
|
responses: {
|
|
200: r.ok(companySkillRenameResultSchema),
|
|
400: r.badRequest,
|
|
401: r.unauthorized,
|
|
403: r.forbidden,
|
|
404: r.notFound,
|
|
409: r.conflict,
|
|
422: r.unprocessable,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/skills",
|
|
tags: ["skills"],
|
|
summary: "Create a company skill",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
body: jsonBody(companySkillCreateSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "patch",
|
|
path: "/api/companies/{companyId}/skills/{skillId}/files",
|
|
tags: ["skills"],
|
|
summary: "Update a skill file",
|
|
request: {
|
|
params: z.object({ companyId: z.string(), skillId: z.string() }),
|
|
body: jsonBody(companySkillFileUpdateSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "delete",
|
|
path: "/api/companies/{companyId}/skills/{skillId}/files",
|
|
tags: ["skills"],
|
|
summary: "Delete a skill file or folder",
|
|
request: {
|
|
params: z.object({ companyId: z.string(), skillId: z.string() }),
|
|
body: jsonBody(companySkillFileDeleteSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/skills/{skillId}/test-inputs",
|
|
tags: ["skills"],
|
|
summary: "List skill test inputs",
|
|
request: { params: z.object({ companyId: z.string(), skillId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/skills/{skillId}/test-inputs",
|
|
tags: ["skills"],
|
|
summary: "Create a skill test input",
|
|
request: {
|
|
params: z.object({ companyId: z.string(), skillId: z.string() }),
|
|
body: jsonBody(companySkillTestInputCreateSchema),
|
|
},
|
|
responses: { 201: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "patch",
|
|
path: "/api/companies/{companyId}/skills/{skillId}/test-inputs/{inputId}",
|
|
tags: ["skills"],
|
|
summary: "Update a skill test input",
|
|
request: {
|
|
params: z.object({ companyId: z.string(), skillId: z.string(), inputId: z.string() }),
|
|
body: jsonBody(companySkillTestInputUpdateSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "delete",
|
|
path: "/api/companies/{companyId}/skills/{skillId}/test-inputs/{inputId}",
|
|
tags: ["skills"],
|
|
summary: "Delete a skill test input",
|
|
request: { params: z.object({ companyId: z.string(), skillId: z.string(), inputId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/skill-test-run-templates",
|
|
tags: ["skills"],
|
|
summary: "List skill test-run templates",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/skill-test-run-templates",
|
|
tags: ["skills"],
|
|
summary: "Create a skill test-run template",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
body: jsonBody(companySkillTestRunTemplateCreateSchema),
|
|
},
|
|
responses: { 201: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "patch",
|
|
path: "/api/companies/{companyId}/skill-test-run-templates/{templateId}",
|
|
tags: ["skills"],
|
|
summary: "Update a skill test-run template",
|
|
request: {
|
|
params: z.object({ companyId: z.string(), templateId: z.string() }),
|
|
body: jsonBody(companySkillTestRunTemplateUpdateSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "delete",
|
|
path: "/api/companies/{companyId}/skill-test-run-templates/{templateId}",
|
|
tags: ["skills"],
|
|
summary: "Delete a skill test-run template",
|
|
request: { params: z.object({ companyId: z.string(), templateId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/skills/{skillId}/test-runs",
|
|
tags: ["skills"],
|
|
summary: "List skill test runs",
|
|
request: {
|
|
params: z.object({ companyId: z.string(), skillId: z.string() }),
|
|
query: companySkillTestRunListQuerySchema,
|
|
},
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 422: r.unprocessable },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/skills/{skillId}/test-runs/{runId}",
|
|
tags: ["skills"],
|
|
summary: "Get a skill test run",
|
|
request: { params: z.object({ companyId: z.string(), skillId: z.string(), runId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/skills/{skillId}/test-runs",
|
|
tags: ["skills"],
|
|
summary: "Create a skill test run",
|
|
request: {
|
|
params: z.object({ companyId: z.string(), skillId: z.string() }),
|
|
body: jsonBody(companySkillTestRunCreateSchema),
|
|
},
|
|
responses: { 201: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/skills/{skillId}/test-runs/{runId}/cancel",
|
|
tags: ["skills"],
|
|
summary: "Cancel a skill test run",
|
|
request: { params: z.object({ companyId: z.string(), skillId: z.string(), runId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "delete",
|
|
path: "/api/companies/{companyId}/skills/{skillId}/test-runs/{runId}",
|
|
tags: ["skills"],
|
|
summary: "Delete a skill test run",
|
|
request: { params: z.object({ companyId: z.string(), skillId: z.string(), runId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/skills/import",
|
|
tags: ["skills"],
|
|
summary: "Import a skill",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
body: jsonBody(companySkillImportSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/skills/browse-project",
|
|
tags: ["skills"],
|
|
summary: "Browse a project workspace for skills",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
body: jsonBody(companySkillProjectBrowseRequestSchema),
|
|
},
|
|
responses: { 200: r.ok(companySkillProjectBrowseResultSchema), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/skills/scan-projects",
|
|
tags: ["skills"],
|
|
summary: "Scan project for skills",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
body: jsonBody(companySkillProjectScanRequestSchema),
|
|
},
|
|
responses: { 200: r.ok(companySkillProjectScanResultSchema), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/skills/{skillId}/install-update",
|
|
tags: ["skills"],
|
|
summary: "Install a skill update",
|
|
request: { params: z.object({ companyId: z.string(), skillId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "delete",
|
|
path: "/api/companies/{companyId}/skills/{skillId}",
|
|
tags: ["skills"],
|
|
summary: "Delete a company skill",
|
|
request: { params: z.object({ companyId: z.string(), skillId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/skill-policy",
|
|
tags: ["skills"],
|
|
summary: "Get the effective company skill policy",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "put",
|
|
path: "/api/companies/{companyId}/skill-policy",
|
|
tags: ["skills"],
|
|
summary: "Replace the company skill policy",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
body: jsonBody(replaceSkillPolicySchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 409: r.conflict },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "delete",
|
|
path: "/api/companies/{companyId}/skill-policy",
|
|
tags: ["skills"],
|
|
summary: "Reset the company skill policy to the open default",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/skill-policy/evaluate",
|
|
tags: ["skills"],
|
|
summary: "Evaluate a company skill policy decision",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
body: jsonBody(evaluateSkillPolicySchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/users/me/inbox-agent-policy",
|
|
tags: ["companies"],
|
|
summary: "Get the current user's inbox agent policy",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "put",
|
|
path: "/api/companies/{companyId}/users/me/inbox-agent-policy",
|
|
tags: ["companies"],
|
|
summary: "Update the current user's inbox agent policy",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
body: jsonBody(updateInboxAgentPolicySchema),
|
|
},
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden, 422: r.unprocessable },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/users/{userId}/inbox-agent-policy",
|
|
tags: ["companies"],
|
|
summary: "Get a company user's inbox agent policy",
|
|
request: { params: z.object({ companyId: z.string(), userId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "put",
|
|
path: "/api/companies/{companyId}/users/{userId}/inbox-agent-policy",
|
|
tags: ["companies"],
|
|
summary: "Update a company user's inbox agent policy",
|
|
request: {
|
|
params: z.object({ companyId: z.string(), userId: z.string() }),
|
|
body: jsonBody(updateInboxAgentPolicySchema),
|
|
},
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden, 422: r.unprocessable },
|
|
});
|
|
|
|
// ─── Execution workspaces ─────────────────────────────────────────────────────
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/execution-workspaces",
|
|
tags: ["execution-workspaces"],
|
|
summary: "List execution workspaces for a company",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/workspace-overview",
|
|
tags: ["execution-workspaces"],
|
|
summary: "List bounded execution workspace overview rows for a company",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
query: workspaceOverviewQuerySchema,
|
|
},
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 422: r.unprocessable },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/execution-workspaces/{id}",
|
|
tags: ["execution-workspaces"],
|
|
summary: "Get an execution workspace",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/execution-workspaces/{id}/close-readiness",
|
|
tags: ["execution-workspaces"],
|
|
summary: "Check close-readiness of a workspace",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/execution-workspaces/{id}/workspace-operations",
|
|
tags: ["execution-workspaces"],
|
|
summary: "List workspace operations",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "patch",
|
|
path: "/api/execution-workspaces/{id}",
|
|
tags: ["execution-workspaces"],
|
|
summary: "Update an execution workspace",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(updateExecutionWorkspaceSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/execution-workspaces/{id}/reconcile-branch",
|
|
tags: ["execution-workspaces"],
|
|
summary: "Reconcile an execution workspace branch record",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(reconcileExecutionWorkspaceBranchSchema),
|
|
},
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden, 422: r.unprocessable },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/execution-workspaces/{id}/login-handoff",
|
|
tags: ["execution-workspaces"],
|
|
summary: "Issue a single-use workspace login handoff",
|
|
description:
|
|
"Mints a short-lived, single-use ticket the isolated workspace exchanges for its own "
|
|
+ "instance-scoped session, so opening a managed workspace does not depend on a cloned "
|
|
+ "password. Board actors only. The response `url` must be navigated to, not stored: the "
|
|
+ "workspace answers it with a redirect so the ticket never enters browser history. A refusal "
|
|
+ "carries a machine `reason` and, where the control plane probed it, the workspace's own "
|
|
+ "readiness.",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(
|
|
z.object({
|
|
next: z.string().optional().describe("Same-origin path to land on; anything else collapses to `/`."),
|
|
}),
|
|
),
|
|
},
|
|
responses: {
|
|
201: r.ok(),
|
|
401: r.unauthorized,
|
|
403: r.forbidden,
|
|
404: r.notFound,
|
|
409: r.conflict,
|
|
501: r.serverError,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/execution-workspaces/{id}/runtime-services/{action}",
|
|
tags: ["execution-workspaces"],
|
|
summary: "Control a runtime service in a workspace",
|
|
request: {
|
|
params: z.object({ id: z.string(), action: z.string() }),
|
|
body: jsonBody(workspaceRuntimeControlTargetSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/execution-workspaces/{id}/runtime-commands/{action}",
|
|
tags: ["execution-workspaces"],
|
|
summary: "Run a runtime command in a workspace",
|
|
request: {
|
|
params: z.object({ id: z.string(), action: z.string() }),
|
|
body: jsonBody(workspaceRuntimeControlTargetSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
// ─── Environments ─────────────────────────────────────────────────────────────
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/environments",
|
|
tags: ["environments"],
|
|
summary: "List environments for a company",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/environments/capabilities",
|
|
tags: ["environments"],
|
|
summary: "Get environment capabilities",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/environments",
|
|
tags: ["environments"],
|
|
summary: "Create an environment",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
body: jsonBody(createEnvironmentSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/environments/{id}",
|
|
tags: ["environments"],
|
|
summary: "Get an environment",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/environments/{id}/delete-blast-radius",
|
|
tags: ["environments"],
|
|
summary: "Get environment delete blast radius",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/environments/{id}/secret-refs",
|
|
tags: ["environments"],
|
|
summary: "Describe an environment's config secret refs (name, status, owning company — never values)",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/environments/{id}/leases",
|
|
tags: ["environments"],
|
|
summary: "List leases for an environment",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/environment-leases/{leaseId}",
|
|
tags: ["environments"],
|
|
summary: "Get an environment lease",
|
|
request: { params: z.object({ leaseId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "patch",
|
|
path: "/api/environments/{id}",
|
|
tags: ["environments"],
|
|
summary: "Update an environment",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(updateEnvironmentSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "delete",
|
|
path: "/api/environments/{id}",
|
|
tags: ["environments"],
|
|
summary: "Delete an environment",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden, 404: r.notFound, 409: r.conflict },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/environments/{id}/probe",
|
|
tags: ["environments"],
|
|
summary: "Probe an environment",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/environments/probe-config",
|
|
tags: ["environments"],
|
|
summary: "Probe environment config",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
body: jsonBody(probeEnvironmentConfigSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/environments/{environmentId}/custom-image-template",
|
|
tags: ["environments"],
|
|
summary: "Get the active customImage template and setup status for an environment",
|
|
request: {
|
|
params: z.object({ environmentId: z.string() }),
|
|
query: environmentCustomImageCompanyQuerySchema,
|
|
},
|
|
responses: { 200: r.ok(environmentCustomImageOverviewSchema), 401: r.unauthorized, 403: r.forbidden },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/environments/{environmentId}/custom-image-setup-sessions",
|
|
tags: ["environments"],
|
|
summary: "Start an interactive environment customImage setup session",
|
|
request: {
|
|
params: z.object({ environmentId: z.string() }),
|
|
query: environmentCustomImageCompanyQuerySchema,
|
|
body: jsonBody(startEnvironmentCustomImageSetupSessionSchema),
|
|
},
|
|
responses: {
|
|
201: r.ok(environmentCustomImageSetupSessionResultSchema),
|
|
400: r.badRequest,
|
|
401: r.unauthorized,
|
|
403: r.forbidden,
|
|
409: r.conflict,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/environment-custom-image-setup-sessions/{sessionId}",
|
|
tags: ["environments"],
|
|
summary: "Get and refresh an environment customImage setup session",
|
|
request: { params: z.object({ sessionId: z.string() }) },
|
|
responses: {
|
|
200: r.ok(environmentCustomImageSetupSessionResultSchema),
|
|
401: r.unauthorized,
|
|
403: r.forbidden,
|
|
404: r.notFound,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/environment-custom-image-setup-sessions/{sessionId}/terminal-session-token",
|
|
tags: ["environments"],
|
|
summary: "Mint a short-lived terminal websocket token for a customImage SSH setup session",
|
|
request: {
|
|
params: z.object({ sessionId: z.string() }),
|
|
body: jsonBody(createEnvironmentCustomImageTerminalSessionTokenSchema),
|
|
},
|
|
responses: {
|
|
201: r.ok(environmentCustomImageTerminalSessionTokenSchema),
|
|
400: r.badRequest,
|
|
401: r.unauthorized,
|
|
403: r.forbidden,
|
|
404: r.notFound,
|
|
409: r.conflict,
|
|
422: r.unprocessable,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/environment-custom-image-setup-sessions/{sessionId}/finish",
|
|
tags: ["environments"],
|
|
summary: "Capture and promote an environment customImage setup session",
|
|
request: {
|
|
params: z.object({ sessionId: z.string() }),
|
|
body: jsonBody(finishEnvironmentCustomImageSetupSessionSchema),
|
|
},
|
|
responses: {
|
|
200: r.ok(environmentCustomImageSetupSessionFinishResultSchema),
|
|
400: r.badRequest,
|
|
401: r.unauthorized,
|
|
403: r.forbidden,
|
|
404: r.notFound,
|
|
409: r.conflict,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/environment-custom-image-setup-sessions/{sessionId}/cancel",
|
|
tags: ["environments"],
|
|
summary: "Cancel an environment customImage setup session",
|
|
request: {
|
|
params: z.object({ sessionId: z.string() }),
|
|
body: jsonBody(cancelEnvironmentCustomImageSetupSessionSchema),
|
|
},
|
|
responses: {
|
|
200: r.ok(environmentCustomImageSetupSessionSchema),
|
|
400: r.badRequest,
|
|
401: r.unauthorized,
|
|
403: r.forbidden,
|
|
404: r.notFound,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/environments/{environmentId}/custom-image-template/rollback",
|
|
tags: ["environments"],
|
|
summary: "Roll back an environment customImage template to the previous captured template",
|
|
request: {
|
|
params: z.object({ environmentId: z.string() }),
|
|
query: environmentCustomImageCompanyQuerySchema,
|
|
},
|
|
responses: {
|
|
200: r.ok(environmentCustomImageTemplateRollbackResultSchema),
|
|
401: r.unauthorized,
|
|
403: r.forbidden,
|
|
404: r.notFound,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/environments/{environmentId}/custom-image-template/relink",
|
|
tags: ["environments"],
|
|
summary: "Relink a detached environment customImage template to the current config",
|
|
request: {
|
|
params: z.object({ environmentId: z.string() }),
|
|
query: environmentCustomImageCompanyQuerySchema,
|
|
body: jsonBody(relinkEnvironmentCustomImageTemplateSchema),
|
|
},
|
|
responses: {
|
|
200: r.ok(environmentCustomImageTemplateRelinkResultSchema),
|
|
400: r.badRequest,
|
|
401: r.unauthorized,
|
|
403: r.forbidden,
|
|
404: r.notFound,
|
|
409: r.conflict,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "delete",
|
|
path: "/api/environments/{environmentId}/custom-image-template",
|
|
tags: ["environments"],
|
|
summary: "Disable the active environment customImage template",
|
|
request: {
|
|
params: z.object({ environmentId: z.string() }),
|
|
query: disableEnvironmentCustomImageTemplateQuerySchema,
|
|
},
|
|
responses: {
|
|
200: r.ok(environmentCustomImageTemplateSchema),
|
|
401: r.unauthorized,
|
|
403: r.forbidden,
|
|
404: r.notFound,
|
|
},
|
|
});
|
|
|
|
// ─── Adapters (full) ──────────────────────────────────────────────────────────
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/adapters",
|
|
tags: ["adapters"],
|
|
summary: "List all adapters",
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/adapters/install",
|
|
tags: ["adapters"],
|
|
summary: "Install an adapter",
|
|
request: {
|
|
body: jsonBody(z.object({
|
|
packageName: z.string(),
|
|
isLocalPath: z.boolean().optional(),
|
|
version: z.string().optional(),
|
|
})),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "patch",
|
|
path: "/api/adapters/{type}",
|
|
tags: ["adapters"],
|
|
summary: "Enable or disable an adapter",
|
|
request: {
|
|
params: z.object({ type: z.string() }),
|
|
body: jsonBody(z.object({ disabled: z.boolean() })),
|
|
},
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "patch",
|
|
path: "/api/adapters/{type}/override",
|
|
tags: ["adapters"],
|
|
summary: "Pause or resume an adapter's override of a builtin",
|
|
request: {
|
|
params: z.object({ type: z.string() }),
|
|
body: jsonBody(z.object({ paused: z.boolean() })),
|
|
},
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "delete",
|
|
path: "/api/adapters/{type}",
|
|
tags: ["adapters"],
|
|
summary: "Delete an adapter",
|
|
request: { params: z.object({ type: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/adapters/{type}/reload",
|
|
tags: ["adapters"],
|
|
summary: "Reload an adapter",
|
|
request: { params: z.object({ type: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/adapters/{type}/reinstall",
|
|
tags: ["adapters"],
|
|
summary: "Reinstall an adapter",
|
|
request: { params: z.object({ type: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/adapters/{type}/config-schema",
|
|
tags: ["adapters"],
|
|
summary: "Get adapter config schema",
|
|
request: { params: z.object({ type: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
// ─── Plugins ──────────────────────────────────────────────────────────────────
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/plugins",
|
|
tags: ["plugins"],
|
|
summary: "List installed plugins",
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/plugins/examples",
|
|
tags: ["plugins"],
|
|
summary: "List example plugins",
|
|
responses: { 200: r.ok() },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/plugins/ui-contributions",
|
|
tags: ["plugins"],
|
|
summary: "List plugin UI contributions",
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/plugins/tools",
|
|
tags: ["plugins"],
|
|
summary: "List plugin tools",
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/plugins/tools/execute",
|
|
tags: ["plugins"],
|
|
summary: "Execute a plugin tool",
|
|
request: {
|
|
body: jsonBody(z.object({
|
|
tool: z.string(),
|
|
parameters: z.record(z.string(), z.unknown()).optional(),
|
|
runContext: z.object({
|
|
agentId: z.string(),
|
|
runId: z.string(),
|
|
companyId: z.string(),
|
|
projectId: z.string(),
|
|
}),
|
|
})),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/plugins/install",
|
|
tags: ["plugins"],
|
|
summary: "Install a plugin",
|
|
request: {
|
|
body: jsonBody(z.object({
|
|
packageName: z.string(),
|
|
version: z.string().optional(),
|
|
isLocalPath: z.boolean().optional(),
|
|
})),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/plugins/{pluginId}",
|
|
tags: ["plugins"],
|
|
summary: "Get a plugin",
|
|
request: { params: z.object({ pluginId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "delete",
|
|
path: "/api/plugins/{pluginId}",
|
|
tags: ["plugins"],
|
|
summary: "Delete a plugin",
|
|
request: { params: z.object({ pluginId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/plugins/{pluginId}/enable",
|
|
tags: ["plugins"],
|
|
summary: "Enable a plugin",
|
|
request: { params: z.object({ pluginId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/plugins/{pluginId}/disable",
|
|
tags: ["plugins"],
|
|
summary: "Disable a plugin",
|
|
request: { params: z.object({ pluginId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/plugins/{pluginId}/health",
|
|
tags: ["plugins"],
|
|
summary: "Get plugin health",
|
|
request: { params: z.object({ pluginId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/plugins/{pluginId}/logs",
|
|
tags: ["plugins"],
|
|
summary: "Get plugin logs",
|
|
request: { params: z.object({ pluginId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/plugins/{pluginId}/upgrade",
|
|
tags: ["plugins"],
|
|
summary: "Upgrade a plugin",
|
|
request: { params: z.object({ pluginId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/plugins/{pluginId}/config",
|
|
tags: ["plugins"],
|
|
summary: "Get company-scoped plugin config",
|
|
request: {
|
|
params: z.object({ pluginId: z.string() }),
|
|
query: z.object({ companyId: z.string() }),
|
|
},
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/plugins/{pluginId}/config",
|
|
tags: ["plugins"],
|
|
summary: "Set company-scoped plugin config",
|
|
request: {
|
|
params: z.object({ pluginId: z.string() }),
|
|
body: jsonBody(z.object({ companyId: z.string(), configJson: z.record(z.string(), z.unknown()) })),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/plugins/{pluginId}/config/test",
|
|
tags: ["plugins"],
|
|
summary: "Test company-scoped plugin config",
|
|
request: {
|
|
params: z.object({ pluginId: z.string() }),
|
|
body: jsonBody(z.object({ companyId: z.string(), configJson: z.record(z.string(), z.unknown()) })),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/plugins/{pluginId}/jobs",
|
|
tags: ["plugins"],
|
|
summary: "List plugin jobs",
|
|
request: { params: z.object({ pluginId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/plugins/{pluginId}/jobs/{jobId}/runs",
|
|
tags: ["plugins"],
|
|
summary: "List runs for a plugin job",
|
|
request: { params: z.object({ pluginId: z.string(), jobId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/plugins/{pluginId}/jobs/{jobId}/trigger",
|
|
tags: ["plugins"],
|
|
summary: "Trigger a plugin job",
|
|
request: { params: z.object({ pluginId: z.string(), jobId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/plugins/{pluginId}/webhooks/{endpointKey}",
|
|
tags: ["plugins"],
|
|
summary: "Deliver an external webhook payload to a plugin",
|
|
request: {
|
|
params: z.object({ pluginId: z.string(), endpointKey: z.string() }),
|
|
},
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/plugins/{pluginId}/dashboard",
|
|
tags: ["plugins"],
|
|
summary: "Get plugin dashboard data",
|
|
request: { params: z.object({ pluginId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/plugins/{pluginId}/bridge/data",
|
|
tags: ["plugins"],
|
|
summary: "Send data via plugin bridge",
|
|
request: {
|
|
params: z.object({ pluginId: z.string() }),
|
|
body: jsonBody(z.object({
|
|
key: z.string(),
|
|
companyId: z.string().optional(),
|
|
params: z.record(z.string(), z.unknown()).optional(),
|
|
})),
|
|
},
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/plugins/{pluginId}/bridge/action",
|
|
tags: ["plugins"],
|
|
summary: "Send action via plugin bridge",
|
|
request: {
|
|
params: z.object({ pluginId: z.string() }),
|
|
body: jsonBody(z.object({
|
|
key: z.string(),
|
|
companyId: z.string().optional(),
|
|
params: z.record(z.string(), z.unknown()).optional(),
|
|
})),
|
|
},
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/plugins/{pluginId}/data/{key}",
|
|
tags: ["plugins"],
|
|
summary: "Get plugin data by key (URL-keyed bridge)",
|
|
request: {
|
|
params: z.object({ pluginId: z.string(), key: z.string() }),
|
|
body: jsonBody(z.object({
|
|
companyId: z.string().optional(),
|
|
params: z.record(z.string(), z.unknown()).optional(),
|
|
})),
|
|
},
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/plugins/{pluginId}/actions/{key}",
|
|
tags: ["plugins"],
|
|
summary: "Invoke a plugin action (URL-keyed bridge)",
|
|
request: {
|
|
params: z.object({ pluginId: z.string(), key: z.string() }),
|
|
body: jsonBody(z.object({
|
|
companyId: z.string().optional(),
|
|
params: z.record(z.string(), z.unknown()).optional(),
|
|
})),
|
|
},
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
// ─── Instance database backups ────────────────────────────────────────────────
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/instance/database-backups",
|
|
tags: ["instance"],
|
|
summary: "Trigger a database backup",
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden },
|
|
});
|
|
|
|
// ─── LLM text endpoints ───────────────────────────────────────────────────────
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/llms/agent-configuration.txt",
|
|
tags: ["llms"],
|
|
summary: "Get agent configuration as plain text (for LLM context)",
|
|
responses: { 200: { description: "Plain text agent configuration" }, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/llms/agent-configuration/{adapterType}.txt",
|
|
tags: ["llms"],
|
|
summary: "Get agent configuration for a specific adapter type",
|
|
request: { params: z.object({ adapterType: z.string() }) },
|
|
responses: { 200: { description: "Plain text agent configuration" }, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/llms/agent-icons.txt",
|
|
tags: ["llms"],
|
|
summary: "Get agent icon names as plain text",
|
|
responses: { 200: { description: "Plain text icon list" }, 401: r.unauthorized },
|
|
});
|
|
|
|
// ─── Issues (legacy / misc) ───────────────────────────────────────────────────
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/issues",
|
|
tags: ["issues"],
|
|
summary: "Legacy — returns error directing to /api/companies/{companyId}/issues",
|
|
responses: { 400: r.badRequest },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/issues/{id}/comments/{commentId}",
|
|
tags: ["issues"],
|
|
summary: "Get a single issue comment",
|
|
request: { params: z.object({ id: z.string(), commentId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/issues/{id}/external-objects",
|
|
tags: ["issues"],
|
|
summary: "List external objects mentioned by an issue",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/issues/{id}/external-object-summary",
|
|
tags: ["issues"],
|
|
summary: "Get external object status summary for an issue",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/issues/external-object-summaries",
|
|
tags: ["issues"],
|
|
summary: "Get external object status summaries for issues",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
body: jsonBody(externalObjectSummariesBodySchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/issues/{id}/external-objects/refresh",
|
|
tags: ["issues"],
|
|
summary: "Refresh external objects mentioned by an issue",
|
|
request: {
|
|
params: z.object({ id: z.string() }),
|
|
body: jsonBody(refreshExternalObjectsBodySchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/projects/{id}/external-object-summary",
|
|
tags: ["projects"],
|
|
summary: "Get external object status summary for a project",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
|
|
});
|
|
|
|
// ─── Org chart images ─────────────────────────────────────────────────────────
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/org.svg",
|
|
tags: ["companies"],
|
|
summary: "Get org chart as SVG",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: { description: "SVG image" }, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/org.png",
|
|
tags: ["companies"],
|
|
summary: "Get org chart as PNG",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: { description: "PNG image" }, 401: r.unauthorized },
|
|
});
|
|
|
|
// ─── Company portability (legacy routes) ─────────────────────────────────────
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/issues",
|
|
tags: ["companies"],
|
|
summary: "Legacy — returns error directing to correct issues path",
|
|
responses: { 400: r.badRequest },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/export",
|
|
tags: ["companies"],
|
|
summary: "Export a company (legacy singular form)",
|
|
request: {
|
|
params: z.object({ companyId: z.string() }),
|
|
body: jsonBody(companyPortabilityExportSchema),
|
|
},
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/export/fidelity",
|
|
tags: ["companies"],
|
|
summary: "Report company data that an export bundle does not include",
|
|
request: { params: z.object({ companyId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/import/preview",
|
|
tags: ["companies"],
|
|
summary: "Preview a company import (legacy route)",
|
|
description:
|
|
"Accepts either the inline JSON body (`application/json`) or the raw company " +
|
|
"package uploaded as a compressed zip (`multipart/form-data` with a `package` " +
|
|
"file field plus a JSON `meta` field carrying the other import fields, or a bare " +
|
|
"`application/zip` body with the `meta` JSON in the `meta` query parameter). The " +
|
|
"zip is unzipped server-side into the same `{ source: { type: \"inline\", ... } }` " +
|
|
"bundle the JSON body carries.",
|
|
request: { body: importRequestBody(companyPortabilityPreviewSchema) },
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 422: r.unprocessable },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/companies/import",
|
|
tags: ["companies"],
|
|
summary: "Apply a company import (legacy route)",
|
|
description:
|
|
"Accepts either the inline JSON body (`application/json`) or the raw company package " +
|
|
"uploaded as a compressed zip (`multipart/form-data` with a `package` file field plus a " +
|
|
"JSON `meta` field, or a bare `application/zip` body with the `meta` JSON in the `meta` " +
|
|
"query parameter); the zip is unzipped server-side into the same import bundle. " +
|
|
"Callers can opt into asynchronous processing: trusted Cloud tenants set the " +
|
|
"`x-paperclip-cloud-async-import: 1` header (browsers cannot — the Cloud harness proxy " +
|
|
"strips inbound `x-paperclip-cloud-*` headers), while board sessions use the proxy-safe " +
|
|
"`?async=1` query parameter. Either way the server responds 202 with a job id and status " +
|
|
"URL instead of holding the connection open for the whole import. While a board actor " +
|
|
"already has an async job running, a resubmit returns 409 carrying the running job's id " +
|
|
"and status URL. Jobs are held in memory and are lost on restart.",
|
|
request: {
|
|
query: z.object({ async: z.enum(["1"]).optional() }),
|
|
body: importRequestBody(companyPortabilityImportSchema),
|
|
},
|
|
responses: {
|
|
200: r.ok(),
|
|
202: { description: "Async import job accepted" },
|
|
400: r.badRequest,
|
|
401: r.unauthorized,
|
|
409: { description: "An async import job is already running for this actor" },
|
|
422: r.unprocessable,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: COMPANY_IMPORT_TRANSFERS_API_PATH,
|
|
tags: ["companies"],
|
|
summary: "Declare a chunked company import transfer",
|
|
description:
|
|
"Declares the caller's existing company package .zip as content-addressed byte-range parts " +
|
|
"(whole-file plus per-part sha256). Re-declaring the same zip resumes the prior transfer " +
|
|
"with its uploaded parts intact; the response carries the transfer id and the part indexes " +
|
|
"still missing.",
|
|
request: { body: jsonBody(companyImportTransferDeclarationSchema) },
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 422: r.unprocessable },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "put",
|
|
path: `${COMPANY_IMPORT_TRANSFERS_API_PATH}/{transferId}/parts/{partIndex}`,
|
|
tags: ["companies"],
|
|
summary: "Upload one declared part of a company import transfer",
|
|
description:
|
|
"Raw part bytes as the request body. The upload is verified against the declared byte size " +
|
|
"and sha256 before it is spooled; re-uploading an already completed part is a no-op success. " +
|
|
"Uploading against a transfer the abandoned-spool sweep has expired returns 410 — the client " +
|
|
"re-creates the transfer.",
|
|
request: {
|
|
params: z.object({ transferId: z.string(), partIndex: z.string() }),
|
|
body: {
|
|
content: {
|
|
"application/octet-stream": {
|
|
schema: { type: "string", format: "binary", description: "The raw part bytes." },
|
|
},
|
|
},
|
|
required: true as const,
|
|
},
|
|
},
|
|
responses: {
|
|
200: r.ok(),
|
|
401: r.unauthorized,
|
|
404: r.notFound,
|
|
409: { description: "The transfer has already been applied" },
|
|
410: { description: "The transfer expired and its spooled parts were deleted" },
|
|
422: r.unprocessable,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: `${COMPANY_IMPORT_TRANSFERS_API_PATH}/{transferId}`,
|
|
tags: ["companies"],
|
|
summary: "Get company import transfer progress",
|
|
description:
|
|
"Resume polling for a chunked import transfer: the transfer status plus which declared " +
|
|
"parts are completed and which are still missing.",
|
|
request: { params: z.object({ transferId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: `${COMPANY_IMPORT_TRANSFERS_API_PATH}/{transferId}/preview`,
|
|
tags: ["companies"],
|
|
summary: "Preview a completed company import transfer",
|
|
description:
|
|
"Runs the import preview against the assembled spool without consuming the transfer: the " +
|
|
"ledger run stays open and the parts stay spooled, so the subsequent apply reuses them " +
|
|
"instead of re-uploading. The JSON body carries the same fields as the multipart preview " +
|
|
"route's `meta` field (include, target, collisionStrategy, ...).",
|
|
request: {
|
|
params: z.object({ transferId: z.string() }),
|
|
body: jsonBody(companyPortabilityPreviewSchema.omit({ source: true })),
|
|
},
|
|
responses: {
|
|
200: r.ok(),
|
|
400: r.badRequest,
|
|
401: r.unauthorized,
|
|
404: r.notFound,
|
|
409: {
|
|
description:
|
|
"Parts are still missing, an apply is in progress, or the transfer was already applied",
|
|
},
|
|
410: { description: "The transfer expired and its spooled parts were deleted" },
|
|
422: r.unprocessable,
|
|
},
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: `${COMPANY_IMPORT_TRANSFERS_API_PATH}/{transferId}/apply`,
|
|
tags: ["companies"],
|
|
summary: "Apply a completed company import transfer",
|
|
description:
|
|
"Assembles the spooled parts back into the original zip, verifies the whole file against " +
|
|
"the declared hash fail-closed, and runs it through the same import pipeline as the " +
|
|
"single-shot upload — including the async import job machinery via the proxy-safe " +
|
|
"`?async=1` query parameter. The JSON body carries the same import fields as the multipart " +
|
|
"route's `meta` field (include, target, collisionStrategy, ...). Overlapping applies of " +
|
|
"the same transfer are serialized: exactly one proceeds, the rest get 409.",
|
|
request: {
|
|
params: z.object({ transferId: z.string() }),
|
|
query: z.object({ async: z.enum(["1"]).optional() }),
|
|
body: jsonBody(companyPortabilityImportSchema.omit({ source: true })),
|
|
},
|
|
responses: {
|
|
200: r.ok(),
|
|
202: { description: "Async import job accepted" },
|
|
400: r.badRequest,
|
|
401: r.unauthorized,
|
|
404: r.notFound,
|
|
409: {
|
|
description:
|
|
"Parts are still missing, an apply is already in progress, or the transfer was already applied",
|
|
},
|
|
410: { description: "The transfer expired and its spooled parts were deleted" },
|
|
422: r.unprocessable,
|
|
},
|
|
});
|
|
|
|
// ─── Board claim & CLI auth ───────────────────────────────────────────────────
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/board-claim/{token}",
|
|
tags: ["access"],
|
|
summary: "Get board claim details by token",
|
|
request: { params: z.object({ token: z.string() }) },
|
|
responses: { 200: r.ok(), 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/board-claim/{token}/claim",
|
|
tags: ["access"],
|
|
summary: "Claim a board token",
|
|
request: { params: z.object({ token: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/cli-auth/challenges/{id}",
|
|
tags: ["access"],
|
|
summary: "Get a CLI auth challenge",
|
|
request: { params: z.object({ id: z.string() }) },
|
|
responses: { 200: r.ok(), 404: r.notFound },
|
|
});
|
|
|
|
// ─── Invite onboarding ────────────────────────────────────────────────────────
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/invites/{token}/logo",
|
|
tags: ["access"],
|
|
summary: "Get company logo for an invite",
|
|
request: { params: z.object({ token: z.string() }) },
|
|
responses: { 200: { description: "Image file" }, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/invites/{token}/onboarding",
|
|
tags: ["access"],
|
|
summary: "Get onboarding data for an invite",
|
|
request: { params: z.object({ token: z.string() }) },
|
|
responses: { 200: r.ok(), 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/invites/{token}/onboarding.txt",
|
|
tags: ["access"],
|
|
summary: "Get onboarding instructions as plain text",
|
|
request: { params: z.object({ token: z.string() }) },
|
|
responses: { 200: { description: "Plain text onboarding instructions" }, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/invites/{token}/skills/index",
|
|
tags: ["access"],
|
|
summary: "Get skills index for an invite",
|
|
request: { params: z.object({ token: z.string() }) },
|
|
responses: { 200: r.ok(), 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/invites/{token}/skills/{skillName}",
|
|
tags: ["access"],
|
|
summary: "Get a skill by name for an invite",
|
|
request: { params: z.object({ token: z.string(), skillName: z.string() }) },
|
|
responses: { 200: r.ok(), 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/invites/{token}/test-resolution",
|
|
tags: ["access"],
|
|
summary: "Test invite token resolution",
|
|
request: { params: z.object({ token: z.string() }) },
|
|
responses: { 200: r.ok(), 404: r.notFound },
|
|
});
|
|
|
|
// ─── Admin ────────────────────────────────────────────────────────────────────
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/admin/users/{userId}/company-access",
|
|
tags: ["admin"],
|
|
summary: "Get company access for a user (admin)",
|
|
request: { params: z.object({ userId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "put",
|
|
path: "/api/admin/users/{userId}/company-access",
|
|
tags: ["admin"],
|
|
summary: "Set company access for a user (admin)",
|
|
request: {
|
|
params: z.object({ userId: z.string() }),
|
|
body: jsonBody(updateUserCompanyAccessSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/admin/users/{userId}/promote-instance-admin",
|
|
tags: ["admin"],
|
|
summary: "Promote a user to instance admin",
|
|
request: { params: z.object({ userId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/admin/users/{userId}/demote-instance-admin",
|
|
tags: ["admin"],
|
|
summary: "Demote a user from instance admin",
|
|
request: { params: z.object({ userId: z.string() }) },
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
|
|
});
|
|
|
|
// ─── Project workspace runtime ────────────────────────────────────────────────
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/projects/{id}/workspaces/{workspaceId}/runtime-services/{action}",
|
|
tags: ["projects"],
|
|
summary: "Control a runtime service in a project workspace",
|
|
request: {
|
|
params: z.object({ id: z.string(), workspaceId: z.string(), action: z.string() }),
|
|
body: jsonBody(workspaceRuntimeControlTargetSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "post",
|
|
path: "/api/projects/{id}/workspaces/{workspaceId}/runtime-commands/{action}",
|
|
tags: ["projects"],
|
|
summary: "Run a runtime command in a project workspace",
|
|
request: {
|
|
params: z.object({ id: z.string(), workspaceId: z.string(), action: z.string() }),
|
|
body: jsonBody(workspaceRuntimeControlTargetSchema),
|
|
},
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
// ─── Plugin bridge stream ─────────────────────────────────────────────────────
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/plugins/{pluginId}/bridge/stream/{channel}",
|
|
tags: ["plugins"],
|
|
summary: "Subscribe to a plugin bridge SSE stream",
|
|
request: { params: z.object({ pluginId: z.string(), channel: z.string() }) },
|
|
responses: {
|
|
200: { description: "Server-sent event stream (text/event-stream)" },
|
|
401: r.unauthorized,
|
|
},
|
|
});
|
|
|
|
// ─── Plugin UI static ─────────────────────────────────────────────────────────
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/_plugins/{pluginId}/ui/{filePath}",
|
|
tags: ["plugins"],
|
|
summary: "Serve plugin UI static file",
|
|
request: { params: z.object({ pluginId: z.string(), filePath: z.string() }) },
|
|
responses: { 200: { description: "Static file content" }, 404: r.notFound },
|
|
});
|
|
|
|
// ─── Adapter UI parser ────────────────────────────────────────────────────────
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/adapters/{type}/ui-parser.js",
|
|
tags: ["adapters"],
|
|
summary: "Get adapter UI parser script",
|
|
request: { params: z.object({ type: z.string() }) },
|
|
responses: { 200: { description: "JavaScript file" }, 404: r.notFound },
|
|
});
|
|
|
|
// ─── Current route coverage ─────────────────────────────────────────────────
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/adapters/{type}",
|
|
tags: ["adapters"],
|
|
summary: "Get adapter registration details",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/health/dev-server/restart",
|
|
tags: ["health"],
|
|
summary: "Request a managed dev-server restart",
|
|
responses: { 202: r.ok(), 403: r.forbidden, 404: r.notFound, 409: { description: "Restart is not required" } },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/bootstrap/claim",
|
|
tags: ["access"],
|
|
summary: "Claim first instance admin from a browser session",
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 404: r.notFound, 409: { description: "Instance admin already claimed" } },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/board-api-keys",
|
|
tags: ["access"],
|
|
summary: "List board API keys",
|
|
responses: { 200: r.ok(), 401: r.unauthorized },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/board-api-keys",
|
|
tags: ["access"],
|
|
summary: "Create a named board API key",
|
|
body: createBoardApiKeySchema,
|
|
responses: { 201: r.ok(), 400: r.badRequest, 401: r.unauthorized },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "delete",
|
|
path: "/api/board-api-keys/{keyId}",
|
|
tags: ["access"],
|
|
summary: "Revoke a board API key",
|
|
});
|
|
|
|
registry.registerPath({
|
|
method: "get",
|
|
path: "/api/companies/import/jobs/{jobId}",
|
|
tags: ["companies"],
|
|
summary: "Get company import job status",
|
|
description:
|
|
"A job is readable only by the actor that created it — the board user or the trusted Cloud " +
|
|
"tenant identity from the async import submission. Any other caller gets the same 404 as an " +
|
|
"unknown id. Jobs are held in memory: they are dropped a few minutes after finishing and do " +
|
|
"not survive a server restart.",
|
|
request: { params: z.object({ jobId: z.string() }) },
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
for (const route of [
|
|
["get", "/api/companies/{companyId}/search", "Search company data"],
|
|
["get", "/api/companies/{companyId}/search/extract", "Extract company search matches"],
|
|
["get", "/api/companies/{companyId}/issues/count", "Count issues in a company"],
|
|
] as const) {
|
|
registerCurrentRoute({
|
|
method: route[0],
|
|
path: route[1],
|
|
tags: ["companies"],
|
|
summary: route[2],
|
|
});
|
|
}
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/folders",
|
|
tags: ["folders"],
|
|
summary: "List folders for a company item kind",
|
|
query: z.object({ kind: folderKindSchema }),
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/folders",
|
|
tags: ["folders"],
|
|
summary: "Create a folder",
|
|
body: createFolderSchema,
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/folders/ensure-my",
|
|
tags: ["folders"],
|
|
summary: "Ensure the current user's personal skill folder exists",
|
|
body: ensureMySkillFolderSchema,
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "patch",
|
|
path: "/api/companies/{companyId}/folders/{folderId}",
|
|
tags: ["folders"],
|
|
summary: "Update a folder",
|
|
body: updateFolderSchema,
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/folders/items/move",
|
|
tags: ["folders"],
|
|
summary: "Move an item into or out of a folder",
|
|
body: moveFolderItemSchema,
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/folders/{folderId}/move",
|
|
tags: ["folders"],
|
|
summary: "Move or reorder a folder",
|
|
body: moveFolderSchema,
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "delete",
|
|
path: "/api/companies/{companyId}/folders/{folderId}",
|
|
tags: ["folders"],
|
|
summary: "Delete a folder",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/issues/{id}/cost-summary",
|
|
tags: ["costs"],
|
|
summary: "Get issue cost summary",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "put",
|
|
path: "/api/companies/{companyId}/resource-memberships/me/documents/{documentId}",
|
|
tags: ["resource-memberships"],
|
|
summary: "Star or unstar a document resource",
|
|
body: updateDocumentResourceMembershipSchema,
|
|
});
|
|
|
|
for (const route of [
|
|
["get", "/api/companies/{companyId}/resource-memberships/me", "List current user's resource memberships"],
|
|
["put", "/api/companies/{companyId}/resource-memberships/me/agents/{agentId}", "Join or leave an agent resource"],
|
|
["put", "/api/companies/{companyId}/resource-memberships/me/projects/{projectId}", "Join or leave a project resource"],
|
|
] as const) {
|
|
registerCurrentRoute({
|
|
method: route[0],
|
|
path: route[1],
|
|
tags: ["resource-memberships"],
|
|
summary: route[2],
|
|
...(route[0] === "put" ? { body: updateResourceMembershipSchema } : {}),
|
|
});
|
|
}
|
|
|
|
for (const route of [
|
|
["get", "/api/companies/{companyId}/secret-providers/health", "Check configured secret providers"],
|
|
["get", "/api/companies/{companyId}/secret-provider-configs", "List secret provider configurations"],
|
|
["get", "/api/secret-provider-configs/{id}", "Get a secret provider configuration"],
|
|
["delete", "/api/secret-provider-configs/{id}", "Delete a secret provider configuration"],
|
|
["post", "/api/secret-provider-configs/{id}/default", "Set the default secret provider configuration"],
|
|
["post", "/api/secret-provider-configs/{id}/health", "Check a secret provider configuration"],
|
|
["get", "/api/secrets/{id}/usage", "Get secret usage"],
|
|
["get", "/api/secrets/{id}/access-events", "List secret access events"],
|
|
] as const) {
|
|
registerCurrentRoute({
|
|
method: route[0],
|
|
path: route[1],
|
|
tags: ["secrets"],
|
|
summary: route[2],
|
|
});
|
|
}
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/secret-provider-configs",
|
|
tags: ["secrets"],
|
|
summary: "Create a secret provider configuration",
|
|
body: createSecretProviderConfigSchema,
|
|
responses: { 201: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "patch",
|
|
path: "/api/secret-provider-configs/{id}",
|
|
tags: ["secrets"],
|
|
summary: "Update a secret provider configuration",
|
|
body: updateSecretProviderConfigSchema,
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/secret-provider-configs/discovery/preview",
|
|
tags: ["secrets"],
|
|
summary: "Preview secret provider discovery",
|
|
body: secretProviderConfigDiscoveryPreviewSchema,
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/secrets/remote-import/preview",
|
|
tags: ["secrets"],
|
|
summary: "Preview remote secret import",
|
|
body: remoteSecretImportPreviewSchema,
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/secrets/remote-import",
|
|
tags: ["secrets"],
|
|
summary: "Import remote secrets",
|
|
body: remoteSecretImportSchema,
|
|
});
|
|
|
|
for (const route of [
|
|
["get", "/api/skills/catalog", "List catalog skills"],
|
|
["get", "/api/skills/catalog/{catalogId}", "Get a catalog skill"],
|
|
["get", "/api/skills/catalog/{catalogId}/files", "List catalog skill files"],
|
|
["post", "/api/companies/{companyId}/skills/install-catalog", "Install a catalog skill"],
|
|
["get", "/api/companies/{companyId}/skills/categories", "List company skill categories"],
|
|
["post", "/api/companies/{companyId}/skills/{skillId}/audit", "Audit a company skill"],
|
|
["patch", "/api/companies/{companyId}/skills/{skillId}", "Update a company skill"],
|
|
["get", "/api/companies/{companyId}/skills/{skillId}/versions", "List skill versions"],
|
|
["post", "/api/companies/{companyId}/skills/{skillId}/versions", "Create a skill version"],
|
|
["get", "/api/companies/{companyId}/skills/{skillId}/versions/{versionId}", "Get a skill version"],
|
|
["post", "/api/companies/{companyId}/skills/{skillId}/star", "Star a company skill"],
|
|
["delete", "/api/companies/{companyId}/skills/{skillId}/star", "Unstar a company skill"],
|
|
["get", "/api/companies/{companyId}/skills/{skillId}/fork-precheck", "Preview company skill fork impact"],
|
|
["post", "/api/companies/{companyId}/skills/{skillId}/fork", "Fork a company skill"],
|
|
["get", "/api/companies/{companyId}/skills/{skillId}/comments", "List skill comments"],
|
|
["post", "/api/companies/{companyId}/skills/{skillId}/comments", "Create a skill comment"],
|
|
["patch", "/api/companies/{companyId}/skills/{skillId}/comments/{commentId}", "Update a skill comment"],
|
|
["delete", "/api/companies/{companyId}/skills/{skillId}/comments/{commentId}", "Delete a skill comment"],
|
|
["post", "/api/companies/{companyId}/skills/{skillId}/reset", "Reset a company skill"],
|
|
] as const) {
|
|
registerCurrentRoute({
|
|
method: route[0],
|
|
path: route[1],
|
|
tags: ["skills"],
|
|
summary: route[2],
|
|
...(route[0] === "post" ? { body: z.record(z.string(), z.unknown()).optional() } : {}),
|
|
});
|
|
}
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/issues/{id}/accepted-plan-decompositions",
|
|
tags: ["issues"],
|
|
summary: "List accepted plan decompositions",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/issues/{id}/accepted-plan-decompositions",
|
|
tags: ["issues"],
|
|
summary: "Create accepted plan decomposition child issues",
|
|
body: createAcceptedPlanDecompositionSchema,
|
|
});
|
|
|
|
for (const route of [
|
|
["get", "/api/issues/{id}/documents/{key}/annotations", "List document annotation threads"],
|
|
["get", "/api/issues/{id}/documents/{key}/annotations/{threadId}", "Get a document annotation thread"],
|
|
["post", "/api/issues/{id}/documents/{key}/lock", "Lock an issue document"],
|
|
["post", "/api/issues/{id}/documents/{key}/unlock", "Unlock an issue document"],
|
|
] as const) {
|
|
registerCurrentRoute({
|
|
method: route[0],
|
|
path: route[1],
|
|
tags: ["issues"],
|
|
summary: route[2],
|
|
});
|
|
}
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/issues/{id}/documents/{key}/annotations",
|
|
tags: ["issues"],
|
|
summary: "Create a document annotation thread",
|
|
body: createDocumentAnnotationThreadSchema,
|
|
responses: { 201: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/issues/{id}/documents/{key}/annotations/{threadId}/comments",
|
|
tags: ["issues"],
|
|
summary: "Add a document annotation comment",
|
|
body: createDocumentAnnotationCommentSchema,
|
|
responses: { 201: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/issues/{id}/low-trust/promotions",
|
|
tags: ["issues"],
|
|
summary: "Promote quarantined low-trust output",
|
|
body: z.object({
|
|
sourceArtifactKind: z.enum(["comment", "document", "work_product", "issue"]),
|
|
sourceArtifactId: z.string().guid(),
|
|
title: z.string().trim().min(1).max(200),
|
|
summary: z.string().trim().min(1).max(8_000),
|
|
}),
|
|
responses: { 201: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound, 422: r.unprocessable },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "patch",
|
|
path: "/api/issues/{id}/documents/{key}/annotations/{threadId}",
|
|
tags: ["issues"],
|
|
summary: "Update a document annotation thread",
|
|
body: updateDocumentAnnotationThreadSchema,
|
|
});
|
|
|
|
for (const route of [
|
|
["get", "/api/routines/{id}/description/annotations", "List routine description annotation threads"],
|
|
["get", "/api/routines/{id}/description/annotations/{threadId}", "Get a routine description annotation thread"],
|
|
] as const) {
|
|
registerCurrentRoute({
|
|
method: route[0],
|
|
path: route[1],
|
|
tags: ["routines"],
|
|
summary: route[2],
|
|
});
|
|
}
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/routines/{id}/description/annotations",
|
|
tags: ["routines"],
|
|
summary: "Create a routine description annotation thread",
|
|
body: createDocumentAnnotationThreadSchema,
|
|
responses: { 201: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/routines/{id}/description/annotations/{threadId}/comments",
|
|
tags: ["routines"],
|
|
summary: "Add a routine description annotation comment",
|
|
body: createDocumentAnnotationCommentSchema,
|
|
responses: { 201: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "patch",
|
|
path: "/api/routines/{id}/description/annotations/{threadId}",
|
|
tags: ["routines"],
|
|
summary: "Update a routine description annotation thread",
|
|
body: updateDocumentAnnotationThreadSchema,
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/issues/{id}/diagnostics/blockers",
|
|
tags: ["issues"],
|
|
summary: "Get blocker diagnostics for an issue",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/issues/{id}/diagnostics/wakes",
|
|
tags: ["issues"],
|
|
summary: "Get wake diagnostics for an issue",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/issues/{id}/diagnostics/subtree",
|
|
tags: ["issues"],
|
|
summary: "Get bounded subtree wake and blocker diagnostics for an issue",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/issues/{id}/recovery-actions",
|
|
tags: ["issues"],
|
|
summary: "List issue recovery actions",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/issues/{id}/recovery-actions/resolve",
|
|
tags: ["issues"],
|
|
summary: "Resolve an issue recovery action",
|
|
body: resolveIssueRecoveryActionSchema,
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/issues/{id}/scheduled-retry/retry-now",
|
|
tags: ["issues"],
|
|
summary: "Retry a scheduled issue run now",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/issues/{id}/monitor/check-now",
|
|
tags: ["issues"],
|
|
summary: "Run an issue monitor check now",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/issues/{id}/interactions/{interactionId}/cancel",
|
|
tags: ["issues"],
|
|
summary: "Cancel an issue question interaction",
|
|
body: cancelIssueThreadInteractionSchema,
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/issues/{id}/interactions/{interactionId}/skip",
|
|
tags: ["issues"],
|
|
summary: "Skip a pending issue thread interaction",
|
|
body: skipIssueThreadInteractionSchema,
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/issues/{id}/interactions/{interactionId}/withdraw",
|
|
tags: ["issues"],
|
|
summary: "Withdraw a pending issue thread interaction",
|
|
body: withdrawIssueThreadInteractionSchema,
|
|
});
|
|
|
|
for (const route of [
|
|
["get", "/api/routines/{id}/revisions", "List routine revisions"],
|
|
["post", "/api/routines/{id}/revisions/{revisionId}/restore", "Restore a routine revision"],
|
|
["get", "/api/routines/{id}/description/annotations", "List routine description annotation threads"],
|
|
["get", "/api/routines/{id}/description/annotations/{threadId}", "Get a routine description annotation thread"],
|
|
] as const) {
|
|
registerCurrentRoute({
|
|
method: route[0],
|
|
path: route[1],
|
|
tags: ["routines"],
|
|
summary: route[2],
|
|
});
|
|
}
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/routines/{id}/description/annotations",
|
|
tags: ["routines"],
|
|
summary: "Create a routine description annotation thread",
|
|
body: createDocumentAnnotationThreadSchema,
|
|
responses: { 201: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/routines/{id}/description/annotations/{threadId}/comments",
|
|
tags: ["routines"],
|
|
summary: "Add a routine description annotation comment",
|
|
body: createDocumentAnnotationCommentSchema,
|
|
responses: { 201: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "patch",
|
|
path: "/api/routines/{id}/description/annotations/{threadId}",
|
|
tags: ["routines"],
|
|
summary: "Update a routine description annotation thread",
|
|
body: updateDocumentAnnotationThreadSchema,
|
|
});
|
|
|
|
const pluginLocalFolderRequestSchema = z.object({
|
|
path: z.string().min(1),
|
|
access: z.enum(["read", "readWrite"]).optional(),
|
|
requiredDirectories: z.array(z.string()).optional(),
|
|
requiredFiles: z.array(z.string()).optional(),
|
|
});
|
|
|
|
for (const route of [
|
|
["get", "/api/plugins/{pluginId}/companies/{companyId}/local-folders", "List plugin local folders"],
|
|
["get", "/api/plugins/{pluginId}/companies/{companyId}/local-folders/{folderKey}/status", "Get plugin local folder status"],
|
|
["post", "/api/plugins/{pluginId}/companies/{companyId}/local-folders/{folderKey}/validate", "Validate a plugin local folder"],
|
|
["put", "/api/plugins/{pluginId}/companies/{companyId}/local-folders/{folderKey}", "Save a plugin local folder"],
|
|
] as const) {
|
|
registerCurrentRoute({
|
|
method: route[0],
|
|
path: route[1],
|
|
tags: ["plugins"],
|
|
summary: route[2],
|
|
...(route[0] === "post" || route[0] === "put" ? { body: pluginLocalFolderRequestSchema } : {}),
|
|
});
|
|
}
|
|
|
|
// --- Connection intents ------------------------------------------------------
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/runtime-tools/github/credentials",
|
|
tags: ["connection-intents"],
|
|
summary: "Resolve operation credentials using a run capability with github_credentials scope; browser sessions are rejected",
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden, 409: r.conflict },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/mcp/runtime-tools",
|
|
tags: ["connection-intents"],
|
|
summary: "Inspect the heartbeat-bound runtime tools MCP endpoint",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/mcp/runtime-tools",
|
|
tags: ["connection-intents"],
|
|
summary: "Call the heartbeat-bound runtime tools MCP endpoint",
|
|
responses: {
|
|
200: r.ok(),
|
|
202: r.ok(),
|
|
400: r.badRequest,
|
|
401: r.unauthorized,
|
|
404: r.notFound,
|
|
},
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/runtime-tools/connections/search",
|
|
tags: ["connection-intents"],
|
|
summary: "Search connections available to the active heartbeat run",
|
|
body: connectionsSearchInputSchema,
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/runtime-tools/connections/request",
|
|
tags: ["connection-intents"],
|
|
summary: "Request a connection for the active heartbeat run",
|
|
body: connectionRequestInputSchema,
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/connection-intents/{interactionId}/setup-options",
|
|
tags: ["connection-intents"],
|
|
summary: "Get setup options for an addressed connection request",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/connection-intents/{interactionId}/phase",
|
|
tags: ["connection-intents"],
|
|
summary: "Update the setup phase for an addressed connection request",
|
|
body: z.object({
|
|
phase: z.enum(["requested", "authorizing", "needs_retry"]),
|
|
}).strict(),
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound, 422: r.unprocessable },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/connection-intents/{interactionId}/complete",
|
|
tags: ["connection-intents"],
|
|
summary: "Complete an addressed connection request",
|
|
body: completeConnectionIntentSchema,
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/connection-intents/{interactionId}/decline",
|
|
tags: ["connection-intents"],
|
|
summary: "Decline an addressed connection request",
|
|
body: declineConnectionIntentSchema,
|
|
});
|
|
|
|
// --- Tool access -------------------------------------------------------------
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/tools/gallery",
|
|
tags: ["tool-access"],
|
|
summary: "List tool app gallery entries",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/tools/apps/{galleryKey}/preflight",
|
|
tags: ["tool-access"],
|
|
summary: "Inspect a curated app's public MCP and OAuth metadata without credentials or registration",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/tools/apps/connect",
|
|
tags: ["tool-access"],
|
|
summary: "Create a draft app connection from gallery input",
|
|
body: connectToolAppSchema,
|
|
responses: { 200: r.ok(), 201: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 422: r.unprocessable },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/tools/apps/{connectionId}/finish",
|
|
tags: ["tool-access"],
|
|
summary: "Finish a gallery app connection and profile setup",
|
|
body: finishToolAppSchema,
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound, 422: r.unprocessable },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/tools/apps/{connectionId}/finalize-oauth-access",
|
|
tags: ["tool-access"],
|
|
summary: "Choose personal or company-wide access after OAuth sign-in",
|
|
body: finalizeOAuthAccessSchema,
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound, 422: r.unprocessable },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/tools/apps/attention",
|
|
tags: ["tool-access"],
|
|
summary: "List tool apps needing attention",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/tools/action-requests",
|
|
tags: ["tool-access"],
|
|
summary: "List pending tool action requests",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/tools/examples",
|
|
tags: ["tool-access"],
|
|
summary: "List installable tool examples",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/tools/examples/{id}/install",
|
|
tags: ["tool-access"],
|
|
summary: "Install a safe tool example",
|
|
responses: { 200: r.ok(), 201: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/tools/examples/{id}/smoke",
|
|
tags: ["tool-access"],
|
|
summary: "Run tool example governance smoke checks",
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/tools/applications",
|
|
tags: ["tool-access"],
|
|
summary: "List tool applications",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/tools/applications",
|
|
tags: ["tool-access"],
|
|
summary: "Create a tool application",
|
|
body: createToolApplicationSchema,
|
|
responses: { 201: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "patch",
|
|
path: "/api/tool-applications/{applicationId}",
|
|
tags: ["tool-access"],
|
|
summary: "Update a tool application",
|
|
body: updateToolApplicationSchema,
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "delete",
|
|
path: "/api/tool-applications/{applicationId}",
|
|
tags: ["tool-access"],
|
|
summary: "Delete a tool application",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/tools/connections",
|
|
tags: ["tool-access"],
|
|
summary: "List tool connections",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/tools/connections",
|
|
tags: ["tool-access"],
|
|
summary: "Create a tool connection",
|
|
body: createToolConnectionSchema,
|
|
responses: { 201: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/tool-connections/{connectionId}",
|
|
tags: ["tool-access"],
|
|
summary: "Get a tool connection",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/tools/connections/{connectionId}/start-authorization",
|
|
tags: ["tool-access"],
|
|
summary: "Start user authorization for a tool connection",
|
|
body: startConnectionAuthorizationSchema,
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/agents/me/connections/{connectionId}/start-authorization",
|
|
tags: ["tool-access"],
|
|
summary: "Start user authorization for an agent tool connection",
|
|
body: startConnectionAuthorizationSchema,
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/tool-connections/{connectionId}/grants",
|
|
tags: ["tool-access"],
|
|
summary: "List tool connection grants",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/tool-connections/{connectionId}/grants/installations",
|
|
tags: ["tool-access"],
|
|
summary: "Add an installation grant to a tool connection",
|
|
responses: { 201: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/tool-connections/{connectionId}/grants/{grantId}/delegations",
|
|
tags: ["tool-access"],
|
|
summary: "Delegate a personal tool connection grant to an agent",
|
|
body: createConnectionGrantDelegationSchema,
|
|
responses: { 201: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "delete",
|
|
path: "/api/tool-connections/{connectionId}/grants/{grantId}/delegations/{delegationId}",
|
|
tags: ["tool-access"],
|
|
summary: "Revoke a personal tool connection grant delegation",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "delete",
|
|
path: "/api/tool-connections/{connectionId}/grants/{grantId}",
|
|
tags: ["tool-access"],
|
|
summary: "Revoke a tool connection grant",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/tool-connections/{connectionId}/usage",
|
|
tags: ["tool-access"],
|
|
summary: "Get tool connection usage",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "put",
|
|
path: "/api/tool-connections/{connectionId}/grants/{grantId}/members",
|
|
tags: ["tool-access"],
|
|
summary: "Replace the member audience of a tool connection grant",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/tool-connections/{connectionId}/services",
|
|
tags: ["tool-access"],
|
|
summary: "List the broker services behind a tool connection",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/tool-connections/{connectionId}/services/{toolkitSlug}/connect",
|
|
tags: ["tool-access"],
|
|
summary: "Start a broker service connection for a toolkit",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/tool-connections/{connectionId}/services/{toolkitSlug}/status",
|
|
tags: ["tool-access"],
|
|
summary: "Poll the connection status of a broker service",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "delete",
|
|
path: "/api/tool-connections/{connectionId}/services/{toolkitSlug}",
|
|
tags: ["tool-access"],
|
|
summary: "Disconnect a broker service from a tool connection",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/tool-connections/{connectionId}/installs",
|
|
tags: ["tool-access"],
|
|
summary: "List tool connection installs",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "put",
|
|
path: "/api/tool-connections/{connectionId}/installs",
|
|
tags: ["tool-access"],
|
|
summary: "Sync tool connection installs",
|
|
body: putToolConnectionInstallsSchema,
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "patch",
|
|
path: "/api/tool-connections/{connectionId}",
|
|
tags: ["tool-access"],
|
|
summary: "Update a tool connection",
|
|
body: updateToolConnectionSchema,
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "delete",
|
|
path: "/api/tool-connections/{connectionId}",
|
|
tags: ["tool-access"],
|
|
summary: "Archive a tool connection",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/tool-connections/{connectionId}/health-check",
|
|
tags: ["tool-access"],
|
|
summary: "Run a tool connection health check",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/tool-connections/{connectionId}/reconnect",
|
|
tags: ["tool-access"],
|
|
summary: "Reconnect a tool app with replacement credentials",
|
|
body: reconnectToolAppSchema,
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 422: r.unprocessable },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/tool-connections/{connectionId}/catalog/refresh",
|
|
tags: ["tool-access"],
|
|
summary: "Refresh a tool connection catalog",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/tool-connections/{connectionId}/catalog",
|
|
tags: ["tool-access"],
|
|
summary: "List a tool connection catalog",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/tool-connections/{connectionId}/activity",
|
|
tags: ["tool-access"],
|
|
summary: "List tool connection activity",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/tool-connections/{connectionId}/test-agents",
|
|
tags: ["tool-access"],
|
|
summary: "List agents available for tool connection test calls",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/tool-connections/{connectionId}/test-agents/{agentId}/access",
|
|
tags: ["tool-access"],
|
|
summary: "Summarize one agent's effective access to a tool connection",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/tool-connections/{connectionId}/test-calls",
|
|
tags: ["tool-access"],
|
|
summary: "Run a tool connection test call",
|
|
body: toolConnectionTestCallSchema,
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound, 422: r.unprocessable, 501: r.ok() },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/tool-connections/{connectionId}/test-calls/{actionRequestId}",
|
|
tags: ["tool-access"],
|
|
summary: "Get a tool connection test call status",
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound, 501: r.ok() },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/tools/oauth/{connectionId}/start",
|
|
tags: ["tool-access"],
|
|
summary: "Start OAuth sign-in for a tool connection",
|
|
body: startToolOAuthSchema,
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/tools/oauth/callback",
|
|
tags: ["tool-access"],
|
|
summary: "Handle a tool app OAuth callback",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/tools/oauth/cloud-connector/callback",
|
|
tags: ["tool-access"],
|
|
summary: "Handle a brokered Paperclip Cloud OAuth callback",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/tools/oauth/paperclip-id/callback",
|
|
tags: ["tool-access"],
|
|
summary: "Handle a legacy brokered Paperclip ID OAuth callback",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/tools/oauth/cloud-connector/enrollment",
|
|
tags: ["tool-access"],
|
|
summary: "Get Paperclip Cloud connector enrollment status",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/tools/oauth/cloud-connector/enrollment",
|
|
tags: ["tool-access"],
|
|
summary: "Start Paperclip Cloud connector enrollment",
|
|
body: z.object({ companyId: z.string().min(1), label: z.string().optional() }).strict(),
|
|
responses: { 201: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 422: r.unprocessable },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/tools/oauth/cloud-connector/enrollment-callback",
|
|
tags: ["tool-access"],
|
|
summary: "Complete Paperclip Cloud connector enrollment",
|
|
query: z.object({
|
|
enrollment_id: z.string().min(1),
|
|
approval_code: z.string().min(1),
|
|
state: z.string().min(1),
|
|
}).strict(),
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/tools/vercel-connect/callback",
|
|
tags: ["tool-access"],
|
|
summary: "Handle a managed Vercel Connect OAuth callback",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/tools/profiles",
|
|
tags: ["tool-access"],
|
|
summary: "List tool access profiles with entries and bindings",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/tools/profiles",
|
|
tags: ["tool-access"],
|
|
summary: "Create a tool access profile",
|
|
body: createToolProfileWithEntriesSchema,
|
|
responses: { 201: r.ok(), 400: r.badRequest, 401: r.unauthorized, 409: r.conflict },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/tools/profiles/effective/agents/{agentId}",
|
|
tags: ["tool-access"],
|
|
summary: "Resolve effective tool access profiles for an agent",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/tool-profiles/{profileId}/new-tools",
|
|
tags: ["tool-access"],
|
|
summary: "List new catalog tools pending profile review",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "patch",
|
|
path: "/api/tool-profiles/{profileId}",
|
|
tags: ["tool-access"],
|
|
summary: "Update a tool access profile",
|
|
body: updateToolProfileWithEntriesSchema,
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/tool-profiles/{profileId}/duplicate",
|
|
tags: ["tool-access"],
|
|
summary: "Duplicate a tool access profile",
|
|
body: duplicateToolProfileSchema,
|
|
responses: { 201: r.ok(), 400: r.badRequest, 401: r.unauthorized, 409: r.conflict },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "delete",
|
|
path: "/api/tool-profiles/{profileId}",
|
|
tags: ["tool-access"],
|
|
summary: "Delete a tool access profile",
|
|
body: deleteToolProfileSchema,
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 422: r.unprocessable },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/tool-profiles/{profileId}/new-tools/review",
|
|
tags: ["tool-access"],
|
|
summary: "Review new catalog tools for a profile",
|
|
body: reviewToolProfileNewToolsSchema,
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 422: r.unprocessable },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/tool-profiles/{profileId}/entries",
|
|
tags: ["tool-access"],
|
|
summary: "Create a tool access profile entry",
|
|
body: createToolProfileEntryForProfileSchema,
|
|
responses: { 201: r.ok(), 400: r.badRequest, 401: r.unauthorized, 422: r.unprocessable },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "patch",
|
|
path: "/api/tool-profile-entries/{entryId}",
|
|
tags: ["tool-access"],
|
|
summary: "Update a tool access profile entry",
|
|
body: updateToolProfileEntrySchema,
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "delete",
|
|
path: "/api/tool-profile-entries/{entryId}",
|
|
tags: ["tool-access"],
|
|
summary: "Delete a tool access profile entry",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/tools/profiles/{profileId}/bind",
|
|
tags: ["tool-access"],
|
|
summary: "Bind a tool access profile to a company, agent, project, routine, or issue",
|
|
body: createToolProfileBindingForProfileSchema,
|
|
responses: { 201: r.ok(), 400: r.badRequest, 401: r.unauthorized, 409: r.conflict, 422: r.unprocessable },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/tools/profiles/{profileId}/unbind",
|
|
tags: ["tool-access"],
|
|
summary: "Unbind a tool access profile from a company, agent, project, routine, or issue",
|
|
body: unbindToolProfileBindingSchema,
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/tools/runtime-slots",
|
|
tags: ["tool-access"],
|
|
summary: "List MCP runtime slots",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/tools/runtime-slots/{id}/stop",
|
|
tags: ["tool-access"],
|
|
summary: "Stop a local stdio MCP runtime slot",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/tools/runtime-slots/{id}/restart",
|
|
tags: ["tool-access"],
|
|
summary: "Restart a local stdio MCP runtime slot",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/tools/runtime-health",
|
|
tags: ["tool-access"],
|
|
summary: "Summarize MCP runtime health and alert recommendations",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/tools/runs/{runId}/decisions",
|
|
tags: ["tool-access"],
|
|
summary: "Get governed tool decisions for a run transcript",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/tools/trust-rules",
|
|
tags: ["tool-access"],
|
|
summary: "List tool trust rules",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/tools/policies",
|
|
tags: ["tool-access"],
|
|
summary: "List tool policies",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/tools/policies/reorder",
|
|
tags: ["tool-access"],
|
|
summary: "Reorder tool policies",
|
|
body: reorderToolPoliciesSchema,
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/tools/policies",
|
|
tags: ["tool-access"],
|
|
summary: "Create a tool policy",
|
|
body: createToolPolicySchema,
|
|
responses: { 201: r.ok(), 400: r.badRequest, 401: r.unauthorized, 409: r.conflict },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/tools/policies/{policyId}/duplicate",
|
|
tags: ["tool-access"],
|
|
summary: "Duplicate a tool policy",
|
|
body: duplicateToolPolicySchema,
|
|
responses: { 201: r.ok(), 400: r.badRequest, 401: r.unauthorized, 409: r.conflict },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "patch",
|
|
path: "/api/companies/{companyId}/tools/policies/{policyId}",
|
|
tags: ["tool-access"],
|
|
summary: "Update a tool policy",
|
|
body: updateToolPolicySchema,
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "delete",
|
|
path: "/api/companies/{companyId}/tools/policies/{policyId}",
|
|
tags: ["tool-access"],
|
|
summary: "Delete a tool policy",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/tools/action-requests/{actionRequestId}/trust-rule",
|
|
tags: ["tool-access"],
|
|
summary: "Create a tool trust rule from an action request",
|
|
body: createToolTrustRuleFromActionRequestSchema,
|
|
responses: { 201: r.ok(), 400: r.badRequest, 401: r.unauthorized, 404: r.notFound },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/tools/trust-rules/{policyId}/revoke",
|
|
tags: ["tool-access"],
|
|
summary: "Revoke a tool trust rule",
|
|
body: revokeToolTrustRuleSchema,
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/tools/stdio-templates",
|
|
tags: ["tool-access"],
|
|
summary: "List approved stdio MCP templates",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/tools/stdio-templates",
|
|
tags: ["tool-access"],
|
|
summary: "Create an approved stdio MCP template",
|
|
body: createToolStdioCommandTemplateSchema,
|
|
responses: { 201: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 409: r.conflict },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/tools/stdio-templates/{templateId}/disable",
|
|
tags: ["tool-access"],
|
|
summary: "Disable an approved stdio MCP template",
|
|
body: disableToolStdioCommandTemplateSchema,
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/tools/mcp/import-json",
|
|
tags: ["tool-access"],
|
|
summary: "Preview MCP JSON import",
|
|
body: importMcpJsonSchema,
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/tools/policy/test",
|
|
tags: ["tool-access"],
|
|
summary: "Test tool policy decision",
|
|
body: toolPolicyTestRequestSchema,
|
|
});
|
|
|
|
// --- Tool gateway ------------------------------------------------------------
|
|
|
|
const toolGatewaySessionSchema = z.object({
|
|
companyId: z.string().optional(),
|
|
agentId: z.string().optional(),
|
|
runId: z.string().optional(),
|
|
issueId: z.string().nullable().optional(),
|
|
projectId: z.string().nullable().optional(),
|
|
ttlMs: z.number().int().positive().optional(),
|
|
});
|
|
|
|
const toolGatewayCallSchema = z.object({
|
|
tool: z.string(),
|
|
parameters: z.record(z.string(), z.unknown()).optional(),
|
|
timeoutMs: z.number().int().positive().optional(),
|
|
approvedActionRequestId: z.string().optional(),
|
|
idempotencyKey: z.string().optional(),
|
|
});
|
|
|
|
const toolGatewayCompanyQuerySchema = z.object({
|
|
companyId: z.string().optional(),
|
|
});
|
|
const toolGatewayCompanyBodySchema = z.object({
|
|
companyId: z.string(),
|
|
}).passthrough();
|
|
|
|
const mcpGatewayProtocolSchema = z.record(z.string(), z.unknown());
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/mcp/gateways/{gatewayPublicId}",
|
|
tags: ["tool-gateway"],
|
|
summary: "Describe a public MCP gateway endpoint",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/mcp/gateways/{gatewayPublicId}",
|
|
tags: ["tool-gateway"],
|
|
summary: "Handle MCP gateway protocol requests by public id",
|
|
body: mcpGatewayProtocolSchema,
|
|
responses: { 200: r.ok(), 202: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound, 429: r.ok() },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/companies/{companyId}/tools/gateways",
|
|
tags: ["tool-gateway"],
|
|
summary: "List named MCP gateways",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/companies/{companyId}/tools/gateways",
|
|
tags: ["tool-gateway"],
|
|
summary: "Create a named MCP gateway",
|
|
body: createToolMcpGatewaySchema,
|
|
responses: { 201: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 422: r.unprocessable },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "patch",
|
|
path: "/api/tool-gateway/gateways/{gatewayId}",
|
|
tags: ["tool-gateway"],
|
|
summary: "Update a named MCP gateway",
|
|
body: toolGatewayCompanyBodySchema,
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound, 422: r.unprocessable },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/tool-gateway/gateways/{gatewayId}/tokens",
|
|
tags: ["tool-gateway"],
|
|
summary: "Create a named MCP gateway token",
|
|
body: toolGatewayCompanyBodySchema,
|
|
responses: { 201: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound, 422: r.unprocessable },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/tool-gateway/gateway-tokens/{tokenId}/revoke",
|
|
tags: ["tool-gateway"],
|
|
summary: "Revoke a named MCP gateway token",
|
|
body: toolGatewayCompanyQuerySchema.required({ companyId: true }),
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/tool-gateway/gateways/{gatewayId}/mcp",
|
|
tags: ["tool-gateway"],
|
|
summary: "Describe a named MCP gateway endpoint",
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/tool-gateway/gateways/{gatewayId}/mcp",
|
|
tags: ["tool-gateway"],
|
|
summary: "Handle named MCP gateway protocol requests",
|
|
body: mcpGatewayProtocolSchema,
|
|
responses: { 200: r.ok(), 202: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound, 429: r.ok() },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/tool-gateway/sessions",
|
|
tags: ["tool-gateway"],
|
|
summary: "Create a tool gateway session",
|
|
body: toolGatewaySessionSchema,
|
|
responses: { 201: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/tool-gateway/sessions/{sessionId}/revoke",
|
|
tags: ["tool-gateway"],
|
|
summary: "Revoke a tool gateway session",
|
|
body: toolGatewayCompanyQuerySchema,
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/tool-gateway/tools",
|
|
tags: ["tool-gateway"],
|
|
summary: "List tools available to a gateway session",
|
|
responses: { 200: r.ok(), 401: r.unauthorized, 403: r.forbidden },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/tool-gateway/tools/call",
|
|
tags: ["tool-gateway"],
|
|
summary: "Execute a tool through the gateway",
|
|
body: toolGatewayCallSchema,
|
|
responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden },
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/tool-gateway/action-requests/{id}/approve",
|
|
tags: ["tool-gateway"],
|
|
summary: "Approve a deferred tool gateway action request",
|
|
query: toolGatewayCompanyQuerySchema,
|
|
body: z.object({ companyId: z.string().optional() }),
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/tool-gateway/action-requests/{id}/decline",
|
|
tags: ["tool-gateway"],
|
|
summary: "Decline a deferred tool gateway action request",
|
|
query: toolGatewayCompanyQuerySchema,
|
|
body: z.object({ companyId: z.string().optional() }),
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/tool-gateway/runtime-slots",
|
|
tags: ["tool-gateway"],
|
|
summary: "List gateway runtime slots",
|
|
query: toolGatewayCompanyQuerySchema,
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/tool-gateway/runtime-slots/{slotId}/stop",
|
|
tags: ["tool-gateway"],
|
|
summary: "Stop a gateway runtime slot",
|
|
query: toolGatewayCompanyQuerySchema,
|
|
body: z.object({ companyId: z.string().optional() }),
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "post",
|
|
path: "/api/tool-gateway/runtime-slots/{slotId}/restart",
|
|
tags: ["tool-gateway"],
|
|
summary: "Restart a gateway runtime slot",
|
|
query: toolGatewayCompanyQuerySchema,
|
|
body: z.object({ companyId: z.string().optional() }),
|
|
});
|
|
|
|
registerCurrentRoute({
|
|
method: "get",
|
|
path: "/api/tool-gateway/audit",
|
|
tags: ["tool-gateway"],
|
|
summary: "List tool gateway audit events",
|
|
query: z.object({
|
|
companyId: z.string().optional(),
|
|
limit: z.number().int().positive().optional(),
|
|
app: z.string().optional(),
|
|
agent: z.string().optional(),
|
|
outcome: z.string().optional(),
|
|
window: z.enum(["1h", "24h", "7d", "30d"]).optional(),
|
|
search: z.string().optional(),
|
|
cursor: z.string().optional(),
|
|
}),
|
|
});
|
|
|
|
// Every experimental REST route remains discoverable while its runtime feature
|
|
// flag and actor checks stay authoritative. Shared validators prevent drift.
|
|
for (const [method, path, body] of experimentalApiPaths) {
|
|
const query = experimentalApiQueries[`${method.toUpperCase()} ${path}`];
|
|
const metadata = experimentalApiMetadata[`${method.toUpperCase()} ${path}`];
|
|
registry.registerPath({
|
|
method, path, tags: ["Experimental"],
|
|
summary: `${method.toUpperCase()} ${path.replace(/\{[^}]+\}/g, "").replace(/\/api\//, "").replaceAll("/", " ")}`,
|
|
description: "Experimental API; the corresponding instance feature must be enabled. Existing route authorization applies." + (path.startsWith("/api/cases/{caseId}") ? " Pipeline case resource. On overlapping /cases routes the server selects the handler by resource identity; use a pipeline case ID." : path.startsWith("/api/cases/{id}") ? " Cases resource (not a pipeline case). Overlapping /cases routes select their handler by resource identity." : ""),
|
|
request: {
|
|
params: z.object(Object.fromEntries([...path.matchAll(/\{([^}]+)\}/g)].map((match) => [match[1], z.string()]))),
|
|
...(query ? { query } : {}),
|
|
...(path === "/api/cases/{id}/attachments" ? { body: { required: true, content: { "multipart/form-data": { schema: { type: "object", required: ["file"], properties: { file: { type: "string", format: "binary" } } } } } } } : body ? { body: { required: true, content: { "application/json": { schema: body } } } } : {}),
|
|
},
|
|
responses: { ...Object.fromEntries((metadata?.successStatuses ?? [200]).map(status => [status, responses.ok()])), 400: responses.badRequest, 403: responses.forbidden, 404: responses.notFound },
|
|
});
|
|
}
|
|
|
|
// ─── Spec builder ─────────────────────────────────────────────────────────────
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
export function buildOpenApiDocument(): any {
|
|
return applyDocumentFixups({
|
|
openapi: "3.0.0",
|
|
info: {
|
|
title: "Paperclip API",
|
|
version: "1.0.0",
|
|
description: "REST API for the Paperclip AI agent management platform",
|
|
},
|
|
servers: [{ url: "/" }],
|
|
components: registry.buildComponents(),
|
|
paths: registry.buildPaths(),
|
|
});
|
|
}
|
|
|
|
export const buildOpenApiSpec = buildOpenApiDocument;
|
|
|
|
export function openApiRoutes() {
|
|
const router = Router();
|
|
router.get("/openapi.json", (_req, res) => {
|
|
res.json(buildOpenApiDocument());
|
|
});
|
|
return router;
|
|
}
|