openapi: 3.1.0 info: title: ItWorx Knowledge Hub API version: 0.1.0-draft description: | Versioned API for the ItWorx Knowledge Hub web client and scoped automation. This is not an MCP transport. The future MCP service will consume this API. servers: - url: /api/v1 tags: - name: System - name: Setup - name: Auth - name: Admin - name: Search - name: Rag - name: Pages - name: Drafts - name: Templates - name: Attachments - name: Imports - name: Source batch purge - name: Projects - name: Decisions - name: Runbooks - name: Service accounts - name: Backups - name: Infrastructure - name: Activity - name: Workspace - name: Ingestion proposals security: - cookieAuth: [] - bearerAuth: [] paths: /ingestion-proposals: get: tags: [Ingestion proposals] operationId: listIngestionProposals description: Human review queue only. Page/pageSize and all filters are evaluated server-side; cursor/limit remain supported for existing clients. Project and risk are accepted but return no matches until this version persists those attributes, rather than fabricating filter results. parameters: - name: status in: query schema: { type: string } - $ref: "#/components/parameters/Cursor" - $ref: "#/components/parameters/Limit" - name: page in: query schema: { type: integer, minimum: 1, maximum: 10000, default: 1 } - name: pageSize in: query schema: { type: integer, minimum: 1, maximum: 100, default: 50 } - name: projectId in: query schema: { type: string, format: uuid } - name: submitterId in: query schema: { type: string, format: uuid } - name: minAgeDays in: query description: Minimum proposal age, calculated from the first immutable proposal revision. schema: { type: integer, minimum: 0, maximum: 3650 } - name: risk in: query schema: { type: string, enum: [low, medium, high] } - name: documentType in: query schema: { type: string, maxLength: 64 } responses: "200": { description: Review queue, content: { application/json: { schema: { $ref: "#/components/schemas/IngestionProposalListResponse" } } } } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } post: tags: [Ingestion proposals] operationId: createIngestionProposal description: >- Requires an Idempotency-Key and an editor cookie or an ingestion:write service token. Repeating the same request with the same caller and key replays the original 201 response. Reusing that key with a different request body returns a 409 problem with type https://itworx.dev/problems/idempotency-key-conflict. parameters: - $ref: "#/components/parameters/RequiredIdempotencyKey" requestBody: required: true content: { application/json: { schema: { $ref: "#/components/schemas/CreateIngestionProposalRequest" } } } responses: "201": { description: Proposal created, headers: { ETag: { $ref: "#/components/headers/ETag" } }, content: { application/json: { schema: { $ref: "#/components/schemas/IngestionProposalDetail" } } } } "400": { description: Idempotency-Key is missing or invalid, content: { application/problem+json: { schema: { $ref: "#/components/schemas/ProblemDetails" } } } } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "409": description: Idempotency-Key was already used with a different request body. content: application/problem+json: schema: { $ref: "#/components/schemas/ProblemDetails" } examples: idempotencyKeyConflict: value: type: https://itworx.dev/problems/idempotency-key-conflict title: Idempotency-Key hergebruikt met een andere aanvraag. status: 409 correlationId: pilot-contract detail: Deze Idempotency-Key is eerder gebruikt voor een andere aanvraag-inhoud. "422": { $ref: "#/components/responses/BadRequest" } /ingestion-proposals/{proposalId}: parameters: - name: proposalId in: path required: true schema: { type: string, format: uuid } get: tags: [Ingestion proposals] operationId: getIngestionProposal description: A human reviewer may read any workspace proposal. An ingestion:write token may read only the proposal it submitted. responses: "200": { description: Complete metadata-only review/status dossier, headers: { ETag: { $ref: "#/components/headers/ETag" } }, content: { application/json: { schema: { $ref: "#/components/schemas/IngestionProposalDetail" } } } } "404": { $ref: "#/components/responses/NotFound" } patch: tags: [Ingestion proposals] operationId: patchIngestionProposal description: >- Requires If-Match. Human editors may change manifest/rationale; an edit to an Approved proposal clears its approval and returns it to ReadyForReview. Only the submitting ingestion:write account may submit analysis, which advances the proposal to ReadyForReview. A stale proposal ETag returns 412 with type https://itworx.local/problems/ingestion-concurrency-conflict. parameters: - $ref: "#/components/parameters/IfMatch" requestBody: required: true content: { application/json: { schema: { $ref: "#/components/schemas/PatchIngestionProposalRequest" } } } responses: "200": { description: Updated proposal, headers: { ETag: { $ref: "#/components/headers/ETag" } }, content: { application/json: { schema: { $ref: "#/components/schemas/IngestionProposalDetail" } } } } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "412": description: Proposal ETag does not match the current version. content: application/problem+json: schema: { $ref: "#/components/schemas/ProblemDetails" } examples: ingestionConcurrencyConflict: value: type: https://itworx.local/problems/ingestion-concurrency-conflict title: Versieconflict. status: 412 correlationId: pilot-contract /ingestion-proposals/{proposalId}/sources: post: tags: [Ingestion proposals] operationId: stageIngestionProposalSource description: Requires If-Match and Idempotency-Key. Upload is quarantined, signature-validated and stored only as immutable metadata plus a content-addressed attachment reference; neither bytes nor storage paths are returned. parameters: - name: proposalId in: path required: true schema: { type: string, format: uuid } - $ref: "#/components/parameters/IfMatch" - $ref: "#/components/parameters/RequiredIdempotencyKey" requestBody: required: true content: multipart/form-data: schema: type: object required: [file] properties: { file: { type: string, format: binary } } responses: "200": { description: Source staged, headers: { ETag: { $ref: "#/components/headers/ETag" } }, content: { application/json: { schema: { $ref: "#/components/schemas/IngestionProposalDetail" } } } } "413": { description: Source exceeds the configured size limit } "422": { $ref: "#/components/responses/BadRequest" } /ingestion-proposals/{proposalId}/submit: post: tags: [Ingestion proposals] operationId: submitManualIngestionProposal description: >- Human submitter only. Moves a proposal with publication details and at least one staged source into ReadyForReview without claiming automated analysis. Requires If-Match. A separate human review and approval remain mandatory. parameters: - name: proposalId in: path required: true schema: { type: string, format: uuid } - $ref: "#/components/parameters/IfMatch" responses: "200": { description: Submitted for review, headers: { ETag: { $ref: "#/components/headers/ETag" } }, content: { application/json: { schema: { $ref: "#/components/schemas/IngestionProposalDetail" } } } } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "409": { $ref: "#/components/responses/Conflict" } "412": { $ref: "#/components/responses/PreconditionFailed" } "428": { $ref: "#/components/responses/PreconditionRequired" } /ingestion-proposals/{proposalId}/request-changes: post: tags: [Ingestion proposals] operationId: requestIngestionProposalChanges description: >- Human reviewer only. Requires If-Match and returns the updated metadata-only dossier. Stale ETags return 412 with type https://itworx.local/problems/ingestion-concurrency-conflict. parameters: - name: proposalId in: path required: true schema: { type: string, format: uuid } - $ref: "#/components/parameters/IfMatch" requestBody: required: true content: { application/json: { schema: { $ref: "#/components/schemas/IngestionProposalDecisionRequest" } } } responses: "200": { description: Change request recorded, headers: { ETag: { $ref: "#/components/headers/ETag" } }, content: { application/json: { schema: { $ref: "#/components/schemas/IngestionProposalDetail" } } } } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "409": { $ref: "#/components/responses/Conflict" } "412": { $ref: "#/components/responses/PreconditionFailed" } "428": { $ref: "#/components/responses/PreconditionRequired" } /ingestion-proposals/{proposalId}/approve: post: tags: [Ingestion proposals] operationId: approveIngestionProposal description: >- Human reviewer only. Service accounts cannot approve. Requires If-Match and approves a ReadyForReview proposal without publishing it. Stale ETags return 412 with type https://itworx.local/problems/ingestion-concurrency-conflict. parameters: - name: proposalId in: path required: true schema: { type: string, format: uuid } - $ref: "#/components/parameters/IfMatch" requestBody: required: true content: { application/json: { schema: { $ref: "#/components/schemas/IngestionProposalDecisionRequest" } } } responses: "200": { description: Proposal approved for later publication, headers: { ETag: { $ref: "#/components/headers/ETag" } }, content: { application/json: { schema: { $ref: "#/components/schemas/IngestionProposalDetail" } } } } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "409": { $ref: "#/components/responses/Conflict" } "412": { $ref: "#/components/responses/PreconditionFailed" } "428": { $ref: "#/components/responses/PreconditionRequired" } /ingestion-proposals/{proposalId}/reject: post: tags: [Ingestion proposals] operationId: rejectIngestionProposal description: >- Human reviewer only. Service accounts cannot reject. Requires If-Match and records the rejection comment on the metadata-only proposal dossier. Stale ETags return 412 with type https://itworx.local/problems/ingestion-concurrency-conflict. parameters: - name: proposalId in: path required: true schema: { type: string, format: uuid } - $ref: "#/components/parameters/IfMatch" requestBody: required: true content: { application/json: { schema: { $ref: "#/components/schemas/IngestionProposalDecisionRequest" } } } responses: "200": { description: Proposal rejected, headers: { ETag: { $ref: "#/components/headers/ETag" } }, content: { application/json: { schema: { $ref: "#/components/schemas/IngestionProposalDetail" } } } } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "409": { $ref: "#/components/responses/Conflict" } "412": { $ref: "#/components/responses/PreconditionFailed" } "428": { $ref: "#/components/responses/PreconditionRequired" } /ingestion-proposals/{proposalId}/retry: post: tags: [Ingestion proposals] operationId: retryIngestionProposal description: Requires If-Match. An ingestion:write caller can retry only its own eligible failed proposal; a human editor may retry an eligible proposal. An optional Idempotency-Key safely replays a successful retry response. parameters: - name: proposalId in: path required: true schema: { type: string, format: uuid } - $ref: "#/components/parameters/IfMatch" - $ref: "#/components/parameters/IdempotencyKey" responses: "200": { description: Eligible proposal reset for re-analysis, headers: { ETag: { $ref: "#/components/headers/ETag" } }, content: { application/json: { schema: { $ref: "#/components/schemas/IngestionProposalDetail" } } } } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "409": { $ref: "#/components/responses/Conflict" } "412": { $ref: "#/components/responses/PreconditionFailed" } "428": { $ref: "#/components/responses/PreconditionRequired" } /ingestion-proposals/{proposalId}/publish: post: tags: [Ingestion proposals] operationId: publishIngestionProposal description: >- Human review only. Requires a human editor cookie session, If-Match and Idempotency-Key; service accounts are forbidden from publishing. A replay with the same key and request body returns the original success response even when it carries the original now-stale If-Match value, and does not create a second page revision. Reusing the key with a different body returns a typed idempotency conflict. A stale target page ETag returns 409 with type https://itworx.local/problems/ingestion-target-stale and moves the proposal back to ReadyForReview. parameters: - name: proposalId in: path required: true schema: { type: string, format: uuid } - $ref: "#/components/parameters/IfMatch" - $ref: "#/components/parameters/RequiredIdempotencyKey" responses: "200": { description: Approved proposal published, headers: { ETag: { $ref: "#/components/headers/ETag" } }, content: { application/json: { schema: { $ref: "#/components/schemas/IngestionProposalDetail" } } } } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "409": description: >- Invalid state transition, stale target, or idempotency-key conflict. Typed problem types are https://itworx.local/problems/ingestion-invalid-transition, https://itworx.local/problems/ingestion-target-stale, and https://itworx.dev/problems/idempotency-key-conflict. content: application/problem+json: schema: { $ref: "#/components/schemas/ProblemDetails" } examples: staleTarget: value: type: https://itworx.local/problems/ingestion-target-stale title: Publicatiedoel is gewijzigd. status: 409 correlationId: pilot-contract detail: Het voorstel is teruggezet naar de reviewwachtrij. invalidTransition: value: type: https://itworx.local/problems/ingestion-invalid-transition title: Conflict. status: 409 correlationId: pilot-contract idempotencyKeyConflict: value: type: https://itworx.dev/problems/idempotency-key-conflict title: Idempotency-Key hergebruikt met een andere aanvraag. status: 409 correlationId: pilot-contract detail: Deze Idempotency-Key is eerder gebruikt voor een andere aanvraag-inhoud. "412": description: Proposal ETag is stale. content: application/problem+json: schema: { $ref: "#/components/schemas/ProblemDetails" } examples: ingestionConcurrencyConflict: value: type: https://itworx.local/problems/ingestion-concurrency-conflict title: Versieconflict. status: 412 correlationId: pilot-contract "428": { $ref: "#/components/responses/PreconditionRequired" } /system/info: get: tags: [System] operationId: getSystemInfo responses: "200": description: Permission-filtered system information content: application/json: schema: $ref: "#/components/schemas/SystemInfo" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" /setup/status: get: tags: [Setup] operationId: getSetupStatus security: [] description: >- Reveals only whether first-run setup is still required. Never reveals any other installation detail to an unauthenticated caller. responses: "200": description: Setup status content: application/json: schema: $ref: "#/components/schemas/SetupStatus" /setup/complete: post: tags: [Setup] operationId: completeSetup security: [] description: >- Single-use first-run owner setup. Requires CSRF protection and is rate limited. Permanently fails once setup has completed. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CompleteSetupRequest" responses: "200": description: Installation completed; caller is signed in content: application/json: schema: $ref: "#/components/schemas/CompleteSetupResponse" "409": description: Setup already completed content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "422": description: Validation failed (email/password/workspace name) content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "429": $ref: "#/components/responses/RateLimited" /auth/csrf: get: tags: [Auth] operationId: getCsrfToken security: [] description: Issues the double-submit anti-forgery token required on cookie-authenticated mutations. responses: "200": description: Anti-forgery token content: application/json: schema: $ref: "#/components/schemas/CsrfToken" /auth/session: get: tags: [Auth] operationId: getSession security: [] description: >- Always returns 200. An unauthenticated caller only ever sees `authenticated: false` plus whether setup is required — never whether a specific account exists. responses: "200": description: Current session state content: application/json: schema: $ref: "#/components/schemas/SessionResponse" /auth/login: post: tags: [Auth] operationId: login security: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/LoginRequest" responses: "200": description: Signed in content: application/json: schema: $ref: "#/components/schemas/SessionResponse" "401": description: Invalid credentials (also returned for an unknown account and a disabled account) content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "429": $ref: "#/components/responses/RateLimited" /auth/logout: post: tags: [Auth] operationId: logout responses: "200": description: Signed out "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" /auth/recovery/redeem: post: tags: [Auth] operationId: redeemRecoveryToken security: [] description: Consumes a single-use operator-issued recovery token and sets a new password. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RecoveryRedeemRequest" responses: "200": description: Password reset; all prior sessions for this account are invalidated "400": description: Invalid or expired token content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "422": description: New password rejected content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "429": $ref: "#/components/responses/RateLimited" /admin/users: get: tags: [Admin] operationId: listUsers description: Administrator only. responses: "200": description: Workspace members content: application/json: schema: type: array items: $ref: "#/components/schemas/UserSummary" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" post: tags: [Admin] operationId: createUser description: >- Administrator only. There is no invite email; the administrator sets an initial password and communicates it out of band. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateUserRequest" responses: "200": description: User created content: application/json: schema: $ref: "#/components/schemas/UserSummary" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "422": $ref: "#/components/responses/BadRequest" /admin/users/{userId}: patch: tags: [Admin] operationId: updateUser description: >- Administrator only. Changes role and/or enabled state. Refuses a change that would leave zero enabled administrators. parameters: - $ref: "#/components/parameters/UserId" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UpdateUserRequest" responses: "200": description: Updated "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": description: Would remove the last enabled administrator content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" /admin/users/{userId}/recovery-tokens: post: tags: [Admin] operationId: issueRecoveryToken description: >- Administrator only. Issues a single-use, one-hour password-recovery token for another account. The raw token is returned exactly once, in this response. parameters: - $ref: "#/components/parameters/UserId" responses: "200": description: Token issued content: application/json: schema: $ref: "#/components/schemas/IssueRecoveryTokenResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" /admin/trash/purge: post: tags: [Admin] operationId: purgeTrash description: >- Administrator only. The explicit administrative purge (docs/09-data-safety: never a permanent delete through a normal UI flow). Permanently removes every trashed page in the workspace whose retention window has already elapsed; never a page still within retention, never a page that was never trashed. responses: "200": description: Purge completed content: application/json: schema: $ref: "#/components/schemas/TrashPurgeResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" /admin/staging-cleanup/status: get: tags: [Admin] operationId: getStagingCleanupStatus description: >- Administrator only (docs/adr/ADR-029-v2-r5-final-hardening.md). Live counts of currently-open and expired-pending-cleanup page-creation staging sessions, plus the most recent cleanup run (scheduled or manual) and its result. The "last run" fields are in-memory only and reset on application restart — an accepted, documented limitation; the cleanup guarantee itself does not depend on this history. responses: "200": description: Current staging-cleanup status content: application/json: schema: $ref: "#/components/schemas/StagingCleanupStatusResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" /admin/staging-cleanup/run: post: tags: [Admin] operationId: runStagingCleanup description: >- Administrator only, CSRF-protected (docs/adr/ADR-029-v2-r5-final-hardening.md). Runs the exact same reclamation logic as the periodic background worker, once, synchronously. `dryRun: true` only counts currently-expired, claimable sessions — it claims nothing, deletes nothing and is safe to call as often as wanted; never expose a destructive "run now" action without this dry-run mode. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RunStagingCleanupRequest" responses: "200": description: Cleanup run (or dry-run preview) result content: application/json: schema: $ref: "#/components/schemas/RunStagingCleanupResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" /activity: get: tags: [Activity] operationId: listActivity description: >- Requires a reader cookie session or an `audit:read` service-token scope. The most recent audit/activity events across the workspace (page/project/decision mutations, imports, backups, staging-session cleanup, etc.), newest first — the same durable `audit_events` trail every mutation writes to (docs/12-auth-security.md "Logging and privacy": never a password, raw token or session identifier). parameters: - name: limit in: query required: false description: Defaults to 100. schema: type: integer minimum: 1 maximum: 500 responses: "200": description: Recent activity, newest first content: application/json: schema: $ref: "#/components/schemas/ActivityListResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" /workspace/members: get: tags: [Workspace] operationId: searchWorkspaceMembers description: >- Requires a reader cookie session — deliberately not reachable by a service-account bearer token (no automation use case has been named for this yet). Backs the accessible owner picker (docs/adr/ADR-029-v2-r5-final-hardening.md, replacing manual `OwnerId`/GUID entry): only enabled members of the current workspace, matched case-insensitively against display name or email, capped at 25 results. Never discloses accounts outside the workspace or disabled accounts. An empty/omitted `q` returns the first 25 members alphabetically (the picker's initial open state, before typing). parameters: - name: q in: query required: false schema: type: string responses: "200": description: Matching workspace members content: application/json: schema: $ref: "#/components/schemas/WorkspaceMemberListResponse" "401": $ref: "#/components/responses/Unauthorized" /search: get: tags: [Search] operationId: searchKnowledge description: >- PostgreSQL full-text (weighted title/tags/body) plus trigram typo-tolerant title matching. Authorization is entirely SQL-level: always scoped to the caller's workspace, always excludes trashed pages (docs/10-search-and-indexing.md). An empty or absent `q` returns all permitted, non-trashed pages (browsing by tag/type filter alone). parameters: - $ref: "#/components/parameters/CorrelationId" - name: q in: query schema: type: string maxLength: 500 - name: type in: query description: A single DocumentType value. schema: $ref: "#/components/schemas/DocumentType" - name: tag in: query description: A single exact tag match. schema: type: string - $ref: "#/components/parameters/Cursor" - $ref: "#/components/parameters/Limit" responses: "200": description: Permission-filtered search results content: application/json: schema: $ref: "#/components/schemas/SearchResponse" "401": $ref: "#/components/responses/Unauthorized" /rag/publication-manifest: get: tags: [Rag] operationId: getRagPublicationManifest description: >- A narrow, read-only view of every page tagged `rag:publish` (docs/32-rag-publication-convention.md), built for RAGcore's n8n sync — the one reliable way to discover the full curated publication set, since this API has no `GET /pages` enumeration (only `/pages/roots` plus a tree walk). Reuses the existing `pages:read` scope; no new scope was added. A page that still carries `rag:publish` but is trashed, still a draft, awaiting review, archived, or deprecated without the `rag:allow-deprecated` tag comes back with `retired: true` and a `retiredReason`, and its `markdown`/`checksum` are `null` — so a sync consumer can safely withdraw previously ingested content instead of only ever learning about new content. parameters: - $ref: "#/components/parameters/CorrelationId" - name: space in: query description: >- A single `rag:space:` tag suffix. Case-sensitive lowercase slug; an invalid shape is rejected with 422 rather than passed into the array- containment filter. schema: type: string maxLength: 64 - $ref: "#/components/parameters/Cursor" - name: limit in: query required: false schema: type: integer minimum: 1 maximum: 200 default: 100 responses: "200": description: Publication manifest page content: application/json: schema: $ref: "#/components/schemas/RagPublicationManifestResponse" "401": $ref: "#/components/responses/Unauthorized" "422": description: Invalid `space` parameter content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" /pages/roots: get: tags: [Pages] operationId: listRootPages description: Top-level pages (no parent), cursor-paginated. parameters: - $ref: "#/components/parameters/Cursor" - $ref: "#/components/parameters/Limit" responses: "200": description: Root pages content: application/json: schema: $ref: "#/components/schemas/PageChildrenResponse" "401": $ref: "#/components/responses/Unauthorized" /pages/preview: post: tags: [Pages] operationId: previewMarkdown description: >- Requires a reader cookie session or a `pages:read` service-token scope. Renders arbitrary Markdown through the exact same server-side pipeline (Markdig with `.DisableHtml()` plus the allowlist `HtmlSanitizer` pass) used for a published page revision — safe to call with unsaved editor content, never persists anything. The same safe pipeline is reused for project free-text Markdown fields (`currentStateMarkdown`, `summaryMarkdown`, etc. — docs/adr/ADR-029-v2-r5-final-hardening.md). requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/PreviewMarkdownRequest" responses: "200": description: Rendered, sanitized HTML content: application/json: schema: $ref: "#/components/schemas/PreviewMarkdownResponse" "401": $ref: "#/components/responses/Unauthorized" /pages: post: tags: [Pages] operationId: createPage parameters: - $ref: "#/components/parameters/CorrelationId" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreatePageRequest" responses: "200": description: Page created and published as revision 1 headers: ETag: $ref: "#/components/headers/ETag" content: application/json: schema: $ref: "#/components/schemas/PageDetail" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "422": description: Validation failed, or an unknown templateKey was supplied content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" /pages/{pageId}: parameters: - $ref: "#/components/parameters/PageId" - $ref: "#/components/parameters/CorrelationId" get: tags: [Pages] operationId: getPage responses: "200": description: Current page, with server-sanitized rendered HTML headers: ETag: $ref: "#/components/headers/ETag" content: application/json: schema: $ref: "#/components/schemas/PageDetail" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" put: tags: [Pages] operationId: updatePage description: Publishes a new revision. Requires If-Match with the page's current ETag. parameters: - $ref: "#/components/parameters/IfMatch" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UpdatePageRequest" responses: "200": description: Updated page headers: ETag: $ref: "#/components/headers/ETag" content: application/json: schema: $ref: "#/components/schemas/PageDetail" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": description: Page is trashed and must be restored before it can be edited content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "412": $ref: "#/components/responses/PreconditionFailed" "422": $ref: "#/components/responses/BadRequest" "428": $ref: "#/components/responses/PreconditionRequired" /pages/{pageId}/children: get: tags: [Pages] operationId: listPageChildren parameters: - $ref: "#/components/parameters/PageId" - $ref: "#/components/parameters/Cursor" - $ref: "#/components/parameters/Limit" responses: "200": description: Direct children, cursor-paginated (proven at 10,000-child scale, docs/27-acceptance-matrix.md M2-A07) content: application/json: schema: $ref: "#/components/schemas/PageChildrenResponse" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" /pages/{pageId}/move: post: tags: [Pages] operationId: movePage description: >- Reparents and/or reorders a page among its siblings. Rejects a move that would make a page its own descendant. Requires If-Match with the page's current ETag. parameters: - $ref: "#/components/parameters/PageId" - $ref: "#/components/parameters/CorrelationId" - $ref: "#/components/parameters/IfMatch" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/MovePageRequest" responses: "200": description: Moved page headers: ETag: $ref: "#/components/headers/ETag" content: application/json: schema: $ref: "#/components/schemas/PageDetail" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": description: The requested move would make the page its own descendant (a cycle) content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "412": $ref: "#/components/responses/PreconditionFailed" "428": $ref: "#/components/responses/PreconditionRequired" /pages/{pageId}/trash: post: tags: [Pages] operationId: trashPage description: >- Moves a page to the trash/retention model. Never a permanent delete (docs/09-data-safety). Requires If-Match with the page's current ETag and accepts either an editor cookie session or a `pages:write` service token, enabling bounded import rollback without permanent deletion. parameters: - $ref: "#/components/parameters/PageId" - $ref: "#/components/parameters/CorrelationId" - $ref: "#/components/parameters/IfMatch" responses: "200": description: Page trashed "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "412": $ref: "#/components/responses/PreconditionFailed" "428": $ref: "#/components/responses/PreconditionRequired" /pages/{pageId}/restore: post: tags: [Pages] operationId: restorePage description: Restores a trashed page. Requires If-Match with the page's current ETag. parameters: - $ref: "#/components/parameters/PageId" - $ref: "#/components/parameters/CorrelationId" - $ref: "#/components/parameters/IfMatch" responses: "200": description: Page restored "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "412": $ref: "#/components/responses/PreconditionFailed" "428": $ref: "#/components/responses/PreconditionRequired" /pages/{pageId}/revisions: get: tags: [Pages] operationId: listPageRevisions description: Immutable revision history, most recent first (M3-A01/M3-A02). parameters: - $ref: "#/components/parameters/PageId" - $ref: "#/components/parameters/CorrelationId" responses: "200": description: Revisions content: application/json: schema: type: array items: $ref: "#/components/schemas/PageRevisionSummary" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" /pages/{pageId}/revisions/{revisionNumber}: get: tags: [Pages] operationId: getPageRevision parameters: - $ref: "#/components/parameters/PageId" - $ref: "#/components/parameters/RevisionNumber" - $ref: "#/components/parameters/CorrelationId" responses: "200": description: One revision's full content, server-rendered content: application/json: schema: $ref: "#/components/schemas/PageRevisionDetailResponse" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" /pages/{pageId}/revisions/{revisionNumber}/restore: post: tags: [Pages] operationId: restorePageRevision description: >- Publishes a new current revision whose content matches the chosen past snapshot. Never rewrites history: the state right before this call is itself preserved as its own revision. Requires If-Match with the page's current ETag. parameters: - $ref: "#/components/parameters/PageId" - $ref: "#/components/parameters/RevisionNumber" - $ref: "#/components/parameters/CorrelationId" - $ref: "#/components/parameters/IfMatch" responses: "200": description: Restored page headers: ETag: $ref: "#/components/headers/ETag" content: application/json: schema: $ref: "#/components/schemas/PageDetail" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": description: Page is trashed and must be restored before it can be edited content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "412": $ref: "#/components/responses/PreconditionFailed" "428": $ref: "#/components/responses/PreconditionRequired" /pages/{pageId}/backlinks: get: tags: [Pages] operationId: getPageBacklinks description: >- "What links here": pages whose most recently published content contains a resolved internal link to this page (M3-A03). parameters: - $ref: "#/components/parameters/PageId" - $ref: "#/components/parameters/CorrelationId" responses: "200": description: Backlinks content: application/json: schema: $ref: "#/components/schemas/BacklinksResponse" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" /drafts/{draftKey}: parameters: - $ref: "#/components/parameters/DraftKey" - $ref: "#/components/parameters/CorrelationId" get: tags: [Drafts] operationId: getDraft description: >- Private per-user autosaved draft. A draft never overwrites the published revision (docs/27-acceptance-matrix.md, M2-A04). responses: "200": description: Draft content: application/json: schema: $ref: "#/components/schemas/DraftResponse" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" put: tags: [Drafts] operationId: saveDraft requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/SaveDraftRequest" responses: "200": description: >- Draft saved. `baseRevisionIsStale` is true when the page has been published again since this draft's base revision, without blocking the save. content: application/json: schema: $ref: "#/components/schemas/DraftResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" delete: tags: [Drafts] operationId: discardDraft responses: "200": description: Draft discarded "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" /templates: get: tags: [Templates] operationId: listTemplates description: Starter document templates available when creating a new page. responses: "200": description: Available templates content: application/json: schema: type: array items: $ref: "#/components/schemas/TemplateSummary" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" /pages/{pageId}/attachments: parameters: - $ref: "#/components/parameters/PageId" - $ref: "#/components/parameters/CorrelationId" get: tags: [Attachments] operationId: listPageAttachments description: Requires a reader cookie session or an `attachments:read` service-token scope. responses: "200": description: Attachments referenced by this page content: application/json: schema: $ref: "#/components/schemas/AttachmentListResponse" "401": $ref: "#/components/responses/Unauthorized" post: tags: [Attachments] operationId: uploadAttachment description: >- Requires an editor cookie session or an `attachments:write` service-token scope. The upload pipeline from docs/13-attachments.md: streams once to quarantine while hashing, verifies size, detects the real signature (never trusting the declared type/extension alone), applies delivery policy, then commits. Identical content deduplicates to one physical blob while still creating an independent logical reference. requestBody: required: true content: multipart/form-data: schema: type: object required: [file] properties: file: type: string format: binary responses: "200": description: Uploaded content: application/json: schema: $ref: "#/components/schemas/AttachmentSummary" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "413": description: File exceeds the configured maximum upload size content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "422": description: Rejected content (unsafe signature) or an invalid filename content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" /pages/staged: post: tags: [Pages] operationId: beginPageCreationSession description: >- Opens a staging session for creating a new page that may need attachments inserted before the page is ever visible to a reader (docs/adr/ADR-025-v2-r1-data-integrity-remediation.md). Requires an editor cookie session or a `pages:write` service-token scope. parameters: - $ref: "#/components/parameters/CorrelationId" - $ref: "#/components/parameters/IdempotencyKey" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/BeginPageCreationSessionRequest" responses: "200": description: Session opened content: application/json: schema: $ref: "#/components/schemas/PageCreationSessionResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "422": description: The supplied parentPageId does not exist content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" /pages/staged/{sessionId}/attachments: parameters: - $ref: "#/components/parameters/SessionId" - $ref: "#/components/parameters/CorrelationId" post: tags: [Attachments] operationId: uploadStagedAttachment description: >- The same real, immediately-addressable attachment as `POST /pages/{pageId}/attachments` — owned by an open page-creation session instead of an existing page until that session's commit re-points ownership atomically. Requires an editor cookie session or an `attachments:write` service-token scope. requestBody: required: true content: multipart/form-data: schema: type: object required: [file] properties: file: type: string format: binary responses: "200": description: Uploaded content: application/json: schema: $ref: "#/components/schemas/AttachmentSummary" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": description: The session does not exist, is no longer open, or has expired content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "413": description: File exceeds the configured maximum upload size content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "422": description: Rejected content (unsafe signature) or an invalid filename content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" /pages/staged/{sessionId}/commit: parameters: - $ref: "#/components/parameters/SessionId" - $ref: "#/components/parameters/CorrelationId" post: tags: [Pages] operationId: commitPageCreationSession description: >- The single explicit publication action: atomically creates the page and re-points every attachment already uploaded into this session to it, in one transaction — no half-published page with broken attachment links can ever become visible (docs/adr/ADR-025-v2-r1-data-integrity-remediation.md). Requires an editor cookie session or a `pages:write` service-token scope. parameters: - $ref: "#/components/parameters/IdempotencyKey" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CommitPageCreationSessionRequest" responses: "200": description: Page created and published, with every staged attachment now owned by it headers: ETag: $ref: "#/components/headers/ETag" content: application/json: schema: $ref: "#/components/schemas/PageDetail" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": description: The session does not exist content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "409": description: The session is no longer open (already committed, discarded, or expired) content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "422": $ref: "#/components/responses/BadRequest" /pages/staged/{sessionId}/discard: parameters: - $ref: "#/components/parameters/SessionId" - $ref: "#/components/parameters/CorrelationId" post: tags: [Pages] operationId: discardPageCreationSession description: >- Explicit compensation for an abandoned session: removes every attachment uploaded into it that is not used elsewhere, then marks the session discarded. Idempotent — discarding an already-discarded session is a safe no-op. Requires an editor cookie session or a `pages:write` service-token scope. parameters: - $ref: "#/components/parameters/IdempotencyKey" responses: "200": description: Session discarded (or already was) content: application/json: schema: $ref: "#/components/schemas/DiscardPageCreationSessionResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": description: The session does not exist content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "409": description: The session was already committed and can no longer be discarded content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" /infrastructure/overview: get: tags: [Infrastructure] operationId: getInfrastructureOverview description: >- Requires a reader cookie session or an `infrastructure:read` service-token scope. The dashboard summary: the most recent inventory snapshot (if any), every known host, and aggregate service counts (total, proposed/unreviewed, missing, unhealthy, unprotected-by-backup). Returns an all-zero/empty response rather than an error when no workspace exists yet. responses: "200": description: Infrastructure overview content: application/json: schema: $ref: "#/components/schemas/InfrastructureOverviewResponse" "401": $ref: "#/components/responses/Unauthorized" /infrastructure/services: get: tags: [Infrastructure] operationId: listInfrastructureServices description: >- Requires a reader cookie session or an `infrastructure:read` service-token scope. Cursor-paginated services catalog (docs/adr/ADR-026-v2-r2-infrastructure-scale-and-sync-durability.md) — proven at real production scale (InfrastructureCatalogReleaseScaleTests.cs, 50,000 services) with an index-order scan, never a full in-memory load. `projectId`, `search` and `status` are applied server-side on the same paginated query; filter results never depend on pages already loaded by the client. parameters: - name: includeHidden in: query required: false description: Defaults to false — hidden/archived services are excluded from the default catalog view. schema: type: boolean - name: projectId in: query required: false description: Only services whose editorial profile links this project. schema: type: string format: uuid - name: search in: query required: false description: Case-insensitive match against detected name, image reference, and editorial display name/category. schema: type: string - name: status in: query required: false description: Server-side operational status filter. schema: type: string enum: [attention, healthy, missing] - $ref: "#/components/parameters/Cursor" - $ref: "#/components/parameters/Limit" responses: "200": description: A page of services, newest-`(DetectedName, Id)`-ordered content: application/json: schema: $ref: "#/components/schemas/InfrastructureServiceListResponse" "401": $ref: "#/components/responses/Unauthorized" /infrastructure/storage: get: tags: [Infrastructure] operationId: listInfrastructureStorage description: >- Requires a reader cookie session or an `infrastructure:read` service-token scope. Cursor-paginated storage listing (V2-R5, docs/adr/ADR-029-v2-r5-final-hardening.md) — every known volume across every service, each row carrying its owning service's ID/name for display. parameters: - name: search in: query required: false description: Case-insensitive match against the owning service's detected name and the volume's own name/mount path, applied server-side. schema: type: string - $ref: "#/components/parameters/Cursor" - $ref: "#/components/parameters/Limit" responses: "200": description: A page of storage volumes content: application/json: schema: $ref: "#/components/schemas/InfrastructureStorageListResponse" "401": $ref: "#/components/responses/Unauthorized" /infrastructure/services/{serviceId}: get: tags: [Infrastructure] operationId: getInfrastructureService description: >- Requires a reader cookie session or an `infrastructure:read` service-token scope. The full fact graph for one service: editorial profile, manual overrides, components, endpoints, networks, volumes, databases, dependencies, health observations, deployment references and backup coverage, each fact carrying its own provenance (source key, confidence, last-seen snapshot, observed-at). The `ETag` response header is the editorial profile's optimistic-concurrency token, required as `If-Match` on `PUT .../editorial-profile`. parameters: - name: serviceId in: path required: true schema: type: string format: uuid responses: "200": description: Full service detail headers: ETag: schema: type: string content: application/json: schema: $ref: "#/components/schemas/InfrastructureServiceDetail" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" /infrastructure/services/{serviceId}/editorial-profile: put: tags: [Infrastructure] operationId: updateInfrastructureEditorialProfile description: >- Editor role or `infrastructure:write` service-token scope required, CSRF- and `If-Match`-protected. Updates the human-authored editorial layer for a service (display name, summary, category, purpose, owning project, procedures, tags, favorite/hidden/archived flags) — automatically-detected facts are never touched by this endpoint. A stale `If-Match` is a 412, never a silent overwrite. parameters: - name: serviceId in: path required: true schema: type: string format: uuid - $ref: "#/components/parameters/IfMatch" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UpdateEditorialProfileRequest" responses: "200": description: Updated service detail headers: ETag: schema: type: string content: application/json: schema: $ref: "#/components/schemas/InfrastructureServiceDetail" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "412": description: If-Match did not match the current editorial profile ETag content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "422": $ref: "#/components/responses/ValidationFailed" "428": description: If-Match header is required and was missing content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" /infrastructure/services/{serviceId}/overrides: post: tags: [Infrastructure] operationId: setManualOverride description: >- Editor role or `infrastructure:write` service-token scope required, CSRF-protected. Sets (creates or replaces) one field-level manual correction over an automatically-detected fact, validated against the exact allowlist `GET /infrastructure/overrides/catalog` returns for this object kind — an inventory sync never overwrites or removes an existing override (docs/adr/ADR-025-v2-r1-data-integrity-remediation.md, audit finding H2). parameters: - name: serviceId in: path required: true schema: type: string format: uuid requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/SetManualOverrideRequest" responses: "200": description: The created/updated override's ID content: application/json: schema: type: object properties: id: type: string format: uuid required: [id] "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "422": $ref: "#/components/responses/ValidationFailed" /infrastructure/overrides: post: tags: [Infrastructure] operationId: setInfrastructureOverride description: >- Editor role or `infrastructure:write` service-token scope required, CSRF-protected. The generic sibling of `POST /infrastructure/services/{serviceId}/overrides` (V2-R5, docs/adr/ADR-029-v2-r5-final-hardening.md): sets a field-level manual correction for any `InfrastructureObjectKind` (Host, Network, Volume, Database, Endpoint, Component, Deployment, Backup), not only a Service reached through its own nested route — validated against the exact allowlist `GET /infrastructure/overrides/catalog?targetKind=...` returns for that kind. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/SetInfrastructureOverrideRequest" responses: "200": description: The created/updated override's ID content: application/json: schema: type: object properties: id: type: string format: uuid required: [id] "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "422": $ref: "#/components/responses/ValidationFailed" /infrastructure/overrides/{overrideId}: delete: tags: [Infrastructure] operationId: removeManualOverride description: >- Editor role or `infrastructure:write` service-token scope required, CSRF-protected. Removes one manual override, reverting that field to its automatically-detected value (if any). parameters: - name: overrideId in: path required: true schema: type: string format: uuid responses: "204": description: Override removed "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" /infrastructure/services/{serviceId}/review: post: tags: [Infrastructure] operationId: reviewInfrastructureService description: >- Administrator only, CSRF-protected. Accepts or rejects a `Proposed` (newly detected, unreviewed) service — the deliberate human-in-the-loop gate before an automatically-detected container becomes part of the confirmed catalog. A name match alone is never treated as a high-confidence relationship (docs/adr/ADR-029-v2-r5-final-hardening.md). parameters: - name: serviceId in: path required: true schema: type: string format: uuid requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ReviewInfrastructureServiceRequest" responses: "200": description: The service's resulting presence state content: application/json: schema: $ref: "#/components/schemas/ReviewInfrastructureServiceResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "422": $ref: "#/components/responses/ValidationFailed" /infrastructure/inventory/snapshots: post: tags: [Infrastructure] operationId: synchronizeInfrastructureInventory description: >- Administrator only, CSRF- and `Idempotency-Key`-protected. Imports one inventory snapshot (hosts + services + every nested fact) captured by the external, read-only inventory tooling — never destructive: existing editorial profiles and manual overrides are always preserved, a previously-seen service that is missing from this snapshot is marked missing (not deleted), and replaying the same `sourceKey`/`sourceHash` is a safe no-op (`alreadyImported: true`), not a duplicate import. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/SynchronizeInventoryRequest" responses: "200": description: Synchronization result content: application/json: schema: $ref: "#/components/schemas/InventorySyncResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "409": description: Idempotency-Key reused with a different request body content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "422": $ref: "#/components/responses/ValidationFailed" /infrastructure/inventory/curated-package: post: tags: [Infrastructure] operationId: importCuratedInventoryPackage description: >- Administrator only, CSRF- and `Idempotency-Key`-protected. A single bounded call that both synchronizes a raw inventory snapshot and applies a curated set of editorial profiles/review decisions on top of it in one step — the mechanism the curated-infrastructure-wiki publication used (docs/deployments/2026-08-02-curated-infrastructure-wiki.md). Never overwrites an editorial profile a human has since hand-edited. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ImportCuratedInventoryRequest" responses: "200": description: Combined synchronization + curation result content: application/json: schema: $ref: "#/components/schemas/ImportCuratedInventoryResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "409": description: Idempotency-Key reused with a different request body content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "422": $ref: "#/components/responses/ValidationFailed" /infrastructure/changes: get: tags: [Infrastructure] operationId: listInfrastructureChanges description: >- Requires a reader cookie session or an `infrastructure:read` service-token scope. The most recent inventory-sync change log entries (new/changed/missing objects), newest first, each naming its target kind/ID, source key, change kind and the specific fields that changed. parameters: - name: limit in: query required: false description: Defaults to 100. schema: type: integer minimum: 1 maximum: 500 responses: "200": description: Recent inventory changes, newest first content: application/json: schema: type: array items: $ref: "#/components/schemas/InventoryChangeResponse" "401": $ref: "#/components/responses/Unauthorized" /infrastructure/overrides/catalog: get: tags: [Infrastructure] operationId: getManualOverrideFieldCatalog description: >- The exact, server-authoritative set of fields a ManualOverride may target for the given InfrastructureObjectKind, with each field's JSON value kind, max length and (for enum fields) allowed values (docs/adr/ADR-025-v2-r1-data-integrity-remediation.md, audit finding H3). The frontend fetches this instead of keeping its own list, so the two can never independently drift. Requires a reader cookie session or an `infrastructure:read` service-token scope. parameters: - name: targetKind in: query required: false description: Defaults to Service, the only kind reachable through the current UI/API. schema: type: string enum: [Host, Service, Component, Endpoint, Network, Volume, Database, Deployment, Backup] responses: "200": description: Field catalog for the requested target kind content: application/json: schema: $ref: "#/components/schemas/ManualOverrideFieldCatalogResponse" "401": $ref: "#/components/responses/Unauthorized" "422": description: Unknown targetKind content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" /attachments/{attachmentId}/content: get: tags: [Attachments] operationId: getAttachmentContent description: >- Requires a reader cookie session or an `attachments:read` service-token scope. Every request re-authorizes the logical reference (docs/13-attachments.md) — a content hash alone is never sufficient permission to retrieve a blob. Served with a safe Content-Disposition (inline only for a known-safe raster image, attachment otherwise) and X-Content-Type-Options: nosniff. parameters: - name: attachmentId in: path required: true schema: type: string format: uuid responses: "200": description: The raw file content content: application/octet-stream: {} "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" /admin/imports/preflight: post: tags: [Imports] operationId: preflightImport description: >- Administrator only. Discovers and fingerprints a read-only source beneath the operator-mounted imports directory, selects an adapter, and builds a plan — no target mutation occurs (docs/17-leafwiki-migration.md). requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/PreflightImportRequest" responses: "200": description: Plan ready (or an idempotent repeat of an already-preflighted request) content: application/json: schema: $ref: "#/components/schemas/ImportPreflightResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "422": description: Invalid source path, or no adapter can interpret this source content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" /admin/imports/{jobId}: get: tags: [Imports] operationId: getImportJob parameters: - $ref: "#/components/parameters/ImportJobId" responses: "200": description: Import job status content: application/json: schema: $ref: "#/components/schemas/ImportJobSummaryResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" /admin/imports/{jobId}/execute: post: tags: [Imports] operationId: executeImport description: >- Administrator only. Re-verifies the source fingerprint and plan hash have not changed since preflight, then creates each planned page — parents before children — with full provenance for rollback. Never overwrites an existing page; an item already imported with unchanged content is skipped idempotently. parameters: - $ref: "#/components/parameters/ImportJobId" - name: sourceRelativePath in: query required: true description: The same relative path supplied to preflight, re-verified against its recorded fingerprint. schema: type: string responses: "200": description: Execution result content: application/json: schema: $ref: "#/components/schemas/ExecuteImportResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": description: The source or plan changed since preflight, or the job is not in a plan-ready state content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" /admin/imports/{jobId}/rollback: post: tags: [Imports] operationId: rollbackImport description: >- Administrator only. Removes exactly the pages/attachments this job's own provenance created — never a page a user has since edited, which is preserved instead (docs/17-leafwiki-migration.md). parameters: - $ref: "#/components/parameters/ImportJobId" responses: "200": description: Rollback result content: application/json: schema: $ref: "#/components/schemas/RollbackImportResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": description: There is nothing to roll back for this job content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" /admin/source-batches/purge/dry-run: post: tags: [Source batch purge] operationId: dryRunSourceBatchPurge description: >- Administrator only. Inspects an explicitly supplied, independently recorded page-ID list for one source batch. It blocks on missing or unlisted source pages, derived content outside the list, retained backlinks or project links. This endpoint never writes data. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/SourceBatchPurgeRequest" responses: "200": description: Immutable-scope dry-run and confirmation hash content: application/json: schema: $ref: "#/components/schemas/SourceBatchPurgeDryRunResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" /admin/source-batches/purge/execute: post: tags: [Source batch purge] operationId: executeSourceBatchPurge description: >- Administrator only. Permanently removes only the pages and attachment references proven by the preceding dry-run. The confirmation hash binds the request to the current page hashes and attachment graph; any change requires a new dry-run. Physical blobs are hash-verified and removed only after their database references have been committed as absent. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ExecuteSourceBatchPurgeRequest" responses: "200": description: Completed source-batch purge content: application/json: schema: $ref: "#/components/schemas/SourceBatchPurgeResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "409": description: The dry-run changed or the source scope is ambiguous content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" /projects: get: tags: [Projects] operationId: listProjects parameters: - $ref: "#/components/parameters/Cursor" - $ref: "#/components/parameters/Limit" - name: status in: query schema: $ref: "#/components/schemas/ProjectStatus" - name: priority in: query schema: $ref: "#/components/schemas/ProjectPriority" - name: ownerId in: query schema: type: string format: uuid - name: search in: query description: Case-insensitive match against project name or summary, applied before cursor pagination. schema: type: string - name: updatedAfter in: query description: Only projects updated at or after this ISO-8601 timestamp. schema: type: string format: date-time - name: sort in: query schema: type: string enum: [name, updatedAt] responses: "200": description: Project collection content: application/json: schema: $ref: "#/components/schemas/ProjectListResponse" "401": $ref: "#/components/responses/Unauthorized" post: tags: [Projects] operationId: createProject requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateProjectRequest" responses: "200": description: Created project (full bundle) headers: ETag: $ref: "#/components/headers/ETag" content: application/json: schema: $ref: "#/components/schemas/ProjectDetail" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" /projects/totals: get: tags: [Projects] operationId: getProjectCatalogTotals description: Authoritative workspace totals for dashboard KPI rendering; independent of cursor-paginated catalog pages. responses: "200": description: Total and active project counts content: application/json: schema: $ref: "#/components/schemas/ProjectCatalogTotalsResponse" "401": $ref: "#/components/responses/Unauthorized" /projects/{projectId}: parameters: - $ref: "#/components/parameters/ProjectId" get: tags: [Projects] operationId: getProject description: >- The project's bounded context bundle — structured data plus stable citations into linked pages, never a copy of their content (docs/15-project-hub.md "Project context bundle", M5-A05). responses: "200": description: Project detail bundle (documents, recent updates, milestones, decisions) headers: ETag: $ref: "#/components/headers/ETag" content: application/json: schema: $ref: "#/components/schemas/ProjectDetail" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" put: tags: [Projects] operationId: updateProject parameters: - $ref: "#/components/parameters/IfMatch" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UpdateProjectRequest" responses: "200": description: Updated project headers: ETag: $ref: "#/components/headers/ETag" content: application/json: schema: $ref: "#/components/schemas/ProjectDetail" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "412": $ref: "#/components/responses/PreconditionFailed" "428": $ref: "#/components/responses/PreconditionRequired" /projects/{projectId}/context: parameters: - $ref: "#/components/parameters/ProjectId" get: tags: [Projects] operationId: getProjectContext description: >- A bounded, single-call context bundle for automation and future MCP-server consumers (docs/09-api-contract.md "Project context", docs/24-mcp-readiness.md, M6-A04). Update/decision/document counts are clamped; the response reports what was truncated instead of ever growing unbounded. Distinct from the unbounded `GET /projects/{projectId}` detail bundle. responses: "200": description: Bounded project context bundle with stable citations content: application/json: schema: $ref: "#/components/schemas/ProjectContextResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" /projects/{projectId}/icon: parameters: - $ref: "#/components/parameters/ProjectId" post: tags: [Projects] operationId: uploadProjectIcon description: >- Replaces the current project icon using the attachment quarantine and byte-signature validation pipeline. Accepts only PNG, JPEG, GIF or WebP up to 2 MiB. The response points to same-origin, workspace-authorized attachment content. Requires the project's current ETag. parameters: - $ref: "#/components/parameters/IfMatch" requestBody: required: true content: multipart/form-data: schema: type: object required: [file] properties: file: type: string format: binary responses: "200": description: Project icon replaced headers: ETag: $ref: "#/components/headers/ETag" content: application/json: schema: $ref: "#/components/schemas/ProjectIconResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "412": $ref: "#/components/responses/PreconditionFailed" "413": description: Icon file exceeds 2 MiB content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "422": description: Unsupported or invalid image content content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "428": $ref: "#/components/responses/PreconditionRequired" /projects/{projectId}/status: parameters: - $ref: "#/components/parameters/ProjectId" post: tags: [Projects] operationId: changeProjectStatus description: Only the documented lifecycle transitions are accepted (docs/15-project-hub.md). parameters: - $ref: "#/components/parameters/IfMatch" requestBody: required: true content: application/json: schema: type: object required: [status] properties: status: $ref: "#/components/schemas/ProjectStatus" responses: "200": description: Updated project headers: ETag: $ref: "#/components/headers/ETag" content: application/json: schema: $ref: "#/components/schemas/ProjectDetail" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": description: The requested transition is not allowed from the project's current status content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "412": $ref: "#/components/responses/PreconditionFailed" "428": $ref: "#/components/responses/PreconditionRequired" /projects/{projectId}/documents: parameters: - $ref: "#/components/parameters/ProjectId" post: tags: [Projects] operationId: linkProjectDocument description: >- Links a page to the project with a role; the page remains canonical, only the relationship is stored (docs/15-project-hub.md). requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/LinkProjectDocumentRequest" responses: "200": description: Created link content: application/json: schema: $ref: "#/components/schemas/ProjectDocumentSummary" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" /projects/documents/{documentId}: parameters: - name: documentId in: path required: true schema: type: string format: uuid delete: tags: [Projects] operationId: unlinkProjectDocument description: Removes only the relationship — the linked page is never touched or deleted. responses: "200": description: Unlinked "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" /projects/{projectId}/updates: parameters: - $ref: "#/components/parameters/ProjectId" get: tags: [Projects] operationId: getProjectUpdates description: Ordered, attributable, linkable timeline (M5-A02). parameters: - $ref: "#/components/parameters/Cursor" - $ref: "#/components/parameters/Limit" responses: "200": description: Update timeline page content: application/json: schema: $ref: "#/components/schemas/ProjectUpdatesResponse" "401": $ref: "#/components/responses/Unauthorized" post: tags: [Projects] operationId: createProjectUpdate description: >- Immutable, append-only entry. Setting supersedesUpdateId marks the earlier update Superseded without rewriting it (docs/15-project-hub.md). requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateProjectUpdateRequest" responses: "200": description: Created update content: application/json: schema: $ref: "#/components/schemas/ProjectUpdateSummary" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" /projects/{projectId}/milestones: parameters: - $ref: "#/components/parameters/ProjectId" get: tags: [Projects] operationId: getProjectMilestones responses: "200": description: Milestones, in their stable sort order content: application/json: schema: type: array items: $ref: "#/components/schemas/ProjectMilestoneSummary" "401": $ref: "#/components/responses/Unauthorized" post: tags: [Projects] operationId: createProjectMilestone requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateProjectMilestoneRequest" responses: "200": description: Created milestone content: application/json: schema: $ref: "#/components/schemas/ProjectMilestoneSummary" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" /projects/milestones/{milestoneId}: parameters: - name: milestoneId in: path required: true schema: type: string format: uuid put: tags: [Projects] operationId: updateProjectMilestone parameters: - $ref: "#/components/parameters/IfMatch" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UpdateProjectMilestoneRequest" responses: "200": description: Updated milestone content: application/json: schema: $ref: "#/components/schemas/ProjectMilestoneSummary" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "412": $ref: "#/components/responses/PreconditionFailed" "428": $ref: "#/components/responses/PreconditionRequired" /decisions: get: tags: [Decisions] operationId: listDecisions parameters: - $ref: "#/components/parameters/Cursor" - $ref: "#/components/parameters/Limit" - name: projectId in: query schema: type: string format: uuid - name: status in: query schema: $ref: "#/components/schemas/DecisionStatus" responses: "200": description: Decision collection content: application/json: schema: $ref: "#/components/schemas/DecisionListResponse" "401": $ref: "#/components/responses/Unauthorized" post: tags: [Decisions] operationId: createDecision description: >- Creates one ArchitectureDecision page plus its lifecycle profile in one step (docs/16-runbooks-and-adrs.md "unique page profile"). requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateDecisionRequest" responses: "200": description: Created decision (full detail, including rendered content) headers: ETag: $ref: "#/components/headers/ETag" content: application/json: schema: $ref: "#/components/schemas/DecisionDetail" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" /decisions/{decisionId}: parameters: - name: decisionId in: path required: true schema: type: string format: uuid get: tags: [Decisions] operationId: getDecision description: Includes lineage (supersedes/supersededBy) — links are bidirectional and cycle-free. responses: "200": description: Decision detail with lineage headers: ETag: $ref: "#/components/headers/ETag" content: application/json: schema: $ref: "#/components/schemas/DecisionDetail" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" /decisions/{decisionId}/accept: parameters: - name: decisionId in: path required: true schema: type: string format: uuid post: tags: [Decisions] operationId: acceptDecision description: >- If this decision supersedes another, that decision must currently be Accepted and is atomically flipped to Superseded with bidirectional links (docs/16-runbooks-and-adrs.md). parameters: - $ref: "#/components/parameters/IfMatch" responses: "200": description: Accepted decision headers: ETag: $ref: "#/components/headers/ETag" content: application/json: schema: $ref: "#/components/schemas/DecisionDetail" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": description: Only a Proposed decision can be accepted content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "412": $ref: "#/components/responses/PreconditionFailed" "428": $ref: "#/components/responses/PreconditionRequired" /decisions/{decisionId}/reject: parameters: - name: decisionId in: path required: true schema: type: string format: uuid post: tags: [Decisions] operationId: rejectDecision parameters: - $ref: "#/components/parameters/IfMatch" responses: "200": description: Rejected decision content: application/json: schema: $ref: "#/components/schemas/DecisionDetail" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": description: Only a Proposed decision can be rejected content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "412": $ref: "#/components/responses/PreconditionFailed" "428": $ref: "#/components/responses/PreconditionRequired" /decisions/{decisionId}/withdraw: parameters: - name: decisionId in: path required: true schema: type: string format: uuid post: tags: [Decisions] operationId: withdrawDecision parameters: - $ref: "#/components/parameters/IfMatch" responses: "200": description: Withdrawn decision content: application/json: schema: $ref: "#/components/schemas/DecisionDetail" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "409": description: Only a Proposed or Accepted decision can be withdrawn content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" "412": $ref: "#/components/responses/PreconditionFailed" "428": $ref: "#/components/responses/PreconditionRequired" /runbooks: get: tags: [Runbooks] operationId: listRunbooks parameters: - $ref: "#/components/parameters/Cursor" - $ref: "#/components/parameters/Limit" - name: riskLevel in: query schema: $ref: "#/components/schemas/RunbookRiskLevel" - name: status in: query schema: $ref: "#/components/schemas/RunbookStatus" responses: "200": description: Runbook collection content: application/json: schema: $ref: "#/components/schemas/RunbookListResponse" "401": $ref: "#/components/responses/Unauthorized" post: tags: [Runbooks] operationId: createRunbook description: >- Creates one Runbook page plus its validation/risk profile in one step (docs/16-runbooks-and-adrs.md "unique page profile"). requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateRunbookRequest" responses: "200": description: Created runbook (full detail, including rendered content) headers: ETag: $ref: "#/components/headers/ETag" content: application/json: schema: $ref: "#/components/schemas/RunbookDetail" "400": $ref: "#/components/responses/BadRequest" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" /runbooks/{runbookId}: parameters: - name: runbookId in: path required: true schema: type: string format: uuid get: tags: [Runbooks] operationId: getRunbook description: >- isStale is true whenever the linked page has a newer revision than the last validation, or was never validated (docs/16-runbooks-and-adrs.md). responses: "200": description: Runbook detail headers: ETag: $ref: "#/components/headers/ETag" content: application/json: schema: $ref: "#/components/schemas/RunbookDetail" "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/NotFound" /runbooks/{runbookId}/validate: parameters: - name: runbookId in: path required: true schema: type: string format: uuid post: tags: [Runbooks] operationId: validateRunbook description: >- Records that a human reviewed the current revision — the platform never executes any step (docs/16-runbooks-and-adrs.md). parameters: - $ref: "#/components/parameters/IfMatch" responses: "200": description: Validated runbook headers: ETag: $ref: "#/components/headers/ETag" content: application/json: schema: $ref: "#/components/schemas/RunbookDetail" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "412": $ref: "#/components/responses/PreconditionFailed" "428": $ref: "#/components/responses/PreconditionRequired" /runbooks/{runbookId}/profile: parameters: - name: runbookId in: path required: true schema: type: string format: uuid put: tags: [Runbooks] operationId: updateRunbookProfile parameters: - $ref: "#/components/parameters/IfMatch" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UpdateRunbookProfileRequest" responses: "200": description: Updated runbook headers: ETag: $ref: "#/components/headers/ETag" content: application/json: schema: $ref: "#/components/schemas/RunbookDetail" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" "412": $ref: "#/components/responses/PreconditionFailed" "428": $ref: "#/components/responses/PreconditionRequired" /service-accounts: get: tags: [Service accounts] operationId: listServiceAccounts description: Administrator-only, cookie-authenticated — never reachable by a service token itself (docs/09-api-contract.md). responses: "200": description: Service accounts and their (secret-free) tokens content: application/json: schema: $ref: "#/components/schemas/ServiceAccountListResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" post: tags: [Service accounts] operationId: createServiceAccount requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateServiceAccountRequest" responses: "200": description: Service account and its one-time raw token — never retrievable again content: application/json: schema: $ref: "#/components/schemas/CreatedServiceAccountToken" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "422": description: One or more requested scopes are not recognized content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" /service-accounts/{serviceAccountId}/tokens: parameters: - name: serviceAccountId in: path required: true schema: type: string format: uuid post: tags: [Service accounts] operationId: issueServiceAccountToken description: >- Rotation pattern: issue a new token, update the client, then revoke the old one — a credential is never briefly unusable. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/IssueServiceAccountTokenRequest" responses: "200": description: New token — its one-time raw value content: application/json: schema: $ref: "#/components/schemas/CreatedServiceAccountToken" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" /service-accounts/tokens/{tokenId}/revoke: parameters: - name: tokenId in: path required: true schema: type: string format: uuid post: tags: [Service accounts] operationId: revokeServiceAccountToken description: Takes effect on the very next request using this token — tokens are verified live against the database, never cached as valid. responses: "200": description: Revoked "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "404": $ref: "#/components/responses/NotFound" /admin/backups: get: tags: [Backups] operationId: listBackups description: >- Administrator-only (docs/18-backup-restore.md). Never includes a filesystem path or connection string — only what an administrator needs to judge backup health. responses: "200": description: Completed backups, newest first content: application/json: schema: $ref: "#/components/schemas/BackupListResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" post: tags: [Backups] operationId: createBackup description: >- Synchronously creates and verifies a full backup (every table plus every content-addressed attachment blob), then applies retention. A failed backup never replaces the last known-good one. Restore is intentionally not exposed here — it runs as a separate maintenance-mode invocation of the same deployable image (docs/18-backup-restore.md; ADR-016 decision 4). responses: "200": description: The newly created, verified backup content: application/json: schema: $ref: "#/components/schemas/BackupSummaryResponse" "401": $ref: "#/components/responses/Unauthorized" "403": $ref: "#/components/responses/Forbidden" "409": description: A backup is already in progress content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" components: securitySchemes: cookieAuth: type: apiKey in: cookie name: ikh_auth bearerAuth: type: http scheme: bearer bearerFormat: IKH service token parameters: CorrelationId: name: X-Correlation-ID in: header required: false schema: type: string maxLength: 100 IdempotencyKey: name: Idempotency-Key in: header required: false schema: type: string minLength: 16 maxLength: 200 RequiredIdempotencyKey: name: Idempotency-Key in: header required: true schema: type: string minLength: 1 maxLength: 200 Cursor: name: cursor in: query required: false schema: type: string maxLength: 2048 Limit: name: limit in: query required: false schema: type: integer minimum: 1 maximum: 100 default: 25 PageId: name: pageId in: path required: true schema: type: string format: uuid SessionId: name: sessionId in: path required: true schema: type: string format: uuid RevisionNumber: name: revisionNumber in: path required: true schema: type: integer format: int64 minimum: 1 ImportJobId: name: jobId in: path required: true schema: type: string format: uuid ProjectId: name: projectId in: path required: true schema: type: string format: uuid UserId: name: userId in: path required: true schema: type: string format: uuid DraftKey: name: draftKey in: path required: true description: The page ID in edit mode, or a client-generated key for an unpublished new page. schema: type: string IfMatch: name: If-Match in: header required: true schema: type: string headers: ETag: description: Strong entity version tag schema: type: string responses: BadRequest: description: Invalid request content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" Unauthorized: description: Authentication required content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" Forbidden: description: Insufficient permission content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" NotFound: description: Resource not found or not disclosed content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" Conflict: description: Idempotency or domain conflict content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" PreconditionFailed: description: ETag does not match the current resource content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" PreconditionRequired: description: If-Match is required content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" ValidationFailed: description: Request failed validation content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" RateLimited: description: Request rate limited headers: Retry-After: schema: type: integer content: application/problem+json: schema: $ref: "#/components/schemas/ProblemDetails" schemas: CreateIngestionProposalRequest: type: object required: [manifest] properties: manifest: { type: string, minLength: 1, maxLength: 100000 } correlationId: { type: [string, "null"], maxLength: 200 } publication: { oneOf: [{ $ref: "#/components/schemas/IngestionPublicationRequest" }, { type: "null" }] } IngestionPublicationRequest: type: object required: [title, markdown, documentType, documentStatus, mutationKind] properties: title: { type: string, minLength: 1, maxLength: 200 } markdown: { type: string, minLength: 1, maxLength: 100000 } documentType: { $ref: "#/components/schemas/DocumentType" } documentStatus: { $ref: "#/components/schemas/DocumentStatus" } mutationKind: { type: string, enum: [CreatePage, UpdatePage] } parentPageId: { type: [string, "null"], format: uuid } targetPageId: { type: [string, "null"], format: uuid } expectedTargetETag: { type: [string, "null"], maxLength: 32 } PatchIngestionProposalRequest: type: object description: Human callers use manifest/rationale; the submitting service account uses analysis only. properties: manifest: { type: [string, "null"], maxLength: 100000 } rationale: { type: [string, "null"], maxLength: 2000 } analysis: { type: [string, "null"], maxLength: 100000 } IngestionProposalDecisionRequest: type: object properties: comment: { type: [string, "null"], maxLength: 2000 } IngestionProposalSourceDetail: type: object required: [id, fileName, mimeType, byteSize, digestSha256, scanResult, attachmentReferenceId] properties: id: { type: string, format: uuid } fileName: { type: string } mimeType: { type: string } byteSize: { type: integer, format: int64 } digestSha256: { type: string, pattern: '^[a-f0-9]{64}$' } scanResult: { type: string } attachmentReferenceId: { type: string, format: uuid } IngestionProposalRevisionDetail: type: object required: [revisionNumber, status, operation, actorId, occurredAt] properties: revisionNumber: { type: integer, format: int64 } status: { type: string } operation: { type: string } actorId: { type: string, format: uuid } occurredAt: { type: string, format: date-time } IngestionProposalSummary: type: object required: [id, correlationId, status, revisionNumber, sourceCount, submitterId, createdAt, destination, metadata, mutationKind] properties: id: { type: string, format: uuid } correlationId: { type: string } status: { type: string } revisionNumber: { type: integer, format: int64 } sourceCount: { type: integer } submitterId: { type: string, format: uuid } createdAt: { type: string, format: date-time } destination: { oneOf: [{ $ref: "#/components/schemas/IngestionProposalDestination" }, { type: "null" }] } metadata: { $ref: "#/components/schemas/IngestionProposalMetadata" } mutationKind: { type: [string, "null"], enum: [CreatePage, UpdatePage, null] } IngestionProposalListResponse: type: object required: [items, nextCursor, page, pageSize, total, correlationId] properties: items: { type: array, items: { $ref: "#/components/schemas/IngestionProposalSummary" } } nextCursor: { type: [string, "null"] } page: { type: integer, minimum: 1 } pageSize: { type: integer, minimum: 1, maximum: 100 } total: { type: integer, format: int64, minimum: 0 } correlationId: { type: string } IngestionProposalDestination: type: object required: [pageId, parentPageId, title] properties: pageId: { type: [string, "null"], format: uuid } parentPageId: { type: [string, "null"], format: uuid } title: { type: [string, "null"] } IngestionProposalMetadata: type: object description: Project and risk are currently unavailable persisted attributes and are therefore explicitly null. required: [documentType, documentStatus, projectId, risk] properties: documentType: { type: [string, "null"] } documentStatus: { type: [string, "null"] } projectId: { type: [string, "null"], format: uuid } risk: { type: [string, "null"], enum: [low, medium, high, null] } IngestionProposalCitation: type: object required: [label, pageId, excerpt, confidence] properties: label: { type: string } pageId: { type: [string, "null"], format: uuid } excerpt: { type: [string, "null"] } confidence: { type: [number, "null"] } IngestionProposalWarning: type: object required: [code, message, severity] properties: code: { type: string } message: { type: string } severity: { type: [string, "null"], enum: [low, medium, high, null] } IngestionProposalCandidate: type: object required: [pageId, title, kind] properties: pageId: { type: [string, "null"], format: uuid } title: { type: [string, "null"] } kind: { type: [string, "null"] } IngestionProposalDiff: type: object required: [before, after, summary] properties: before: { type: [string, "null"] } after: { type: [string, "null"] } summary: { type: [string, "null"] } IngestionProposalPermissions: type: object required: [canEdit, canDecide, canPublish] properties: canEdit: { type: boolean } canDecide: { type: boolean } canPublish: { type: boolean } IngestionProposalDetail: type: object description: Safe dossier metadata only; it never contains source bytes, paths, URLs, credentials, or tokens. required: [id, correlationId, status, revisionNumber, manifest, retryCount, maxPublishRetries, isRetryEligible, sources, revisions, etag, submitterId, createdAt, destination, metadata, mutationKind, attachments, expectedTargetETag, citations, warnings, duplicateCandidates, conflictCandidates, diff, permissions] properties: id: { type: string, format: uuid } correlationId: { type: string } status: { type: string } revisionNumber: { type: integer, format: int64 } manifest: { type: string } analysis: { type: [string, "null"] } reviewerComment: { type: [string, "null"] } retryCount: { type: integer } maxPublishRetries: { type: integer } isRetryEligible: { type: boolean } failedLifecycleStatus: { type: [string, "null"] } failedProcessingStage: { type: [string, "null"] } failureErrorType: { type: [string, "null"] } failureMessage: { type: [string, "null"] } sources: { type: array, items: { $ref: "#/components/schemas/IngestionProposalSourceDetail" } } revisions: { type: array, items: { $ref: "#/components/schemas/IngestionProposalRevisionDetail" } } etag: { type: string } publishedPageId: { type: [string, "null"], format: uuid } publishedPageRevisionNumber: { type: [integer, "null"], format: int64 } submitterId: { type: string, format: uuid } createdAt: { type: string, format: date-time } destination: { oneOf: [{ $ref: "#/components/schemas/IngestionProposalDestination" }, { type: "null" }] } metadata: { $ref: "#/components/schemas/IngestionProposalMetadata" } mutationKind: { type: [string, "null"], enum: [CreatePage, UpdatePage, null] } attachments: { type: array, items: { $ref: "#/components/schemas/IngestionProposalSourceDetail" } } expectedTargetETag: { type: [string, "null"] } citations: { type: array, items: { $ref: "#/components/schemas/IngestionProposalCitation" } } warnings: { type: array, items: { $ref: "#/components/schemas/IngestionProposalWarning" } } duplicateCandidates: { type: array, items: { $ref: "#/components/schemas/IngestionProposalCandidate" } } conflictCandidates: { type: array, items: { $ref: "#/components/schemas/IngestionProposalCandidate" } } diff: description: after contains the proposed publication Markdown. before is null when no current target snapshot is available. oneOf: [{ $ref: "#/components/schemas/IngestionProposalDiff" }, { type: "null" }] permissions: { $ref: "#/components/schemas/IngestionProposalPermissions" } SystemInfo: type: object required: [product, version, commit, schemaVersion, environment, primaryLocale, timeZone, uptimeSeconds, release] properties: product: type: string const: ItWorx Knowledge Hub version: type: string commit: type: string schemaVersion: type: string environment: type: string primaryLocale: type: string timeZone: type: string uptimeSeconds: type: integer release: $ref: "#/components/schemas/ReleaseIdentity" ReleaseIdentity: type: object description: >- The canonical, machine-readable release identity (docs/adr/ADR-029-v2-r5-final-hardening.md). Every field is honestly nullable — a value not actually known at build or deploy time is never fabricated. required: [semanticVersion, releaseTag, sourceCommit, imageRepository, imageTag, imageId, imageDigest, buildTimestamp, deploymentTimestamp, openApiVersion, openApiHash, frontendBuildHash, databaseMigrationRange, inventorySnapshotId, backupId] properties: semanticVersion: type: [string, "null"] releaseTag: type: [string, "null"] sourceCommit: type: [string, "null"] imageRepository: type: [string, "null"] imageTag: type: [string, "null"] imageId: type: [string, "null"] imageDigest: type: [string, "null"] buildTimestamp: type: [string, "null"] format: date-time deploymentTimestamp: type: [string, "null"] format: date-time openApiVersion: type: [string, "null"] openApiHash: type: [string, "null"] frontendBuildHash: type: [string, "null"] databaseMigrationRange: type: [string, "null"] description: "The first and last applied EF Core migration for the database this instance is actually connected to, formatted as 'first..last' (or a single migration name if only one has been applied)." inventorySnapshotId: type: [string, "null"] backupId: type: [string, "null"] format: int64 minimum: 0 DocumentType: type: string enum: - WikiPage - StudyNote - CourseModule - ProjectDocument - ArchitectureDecision - Runbook - Troubleshooting - Reference - ChangeLog - SessionNote - Checklist DocumentStatus: type: string enum: [Draft, Active, NeedsReview, Deprecated, Archived] PageSummary: type: object required: [id, parentPageId, title, slug, documentType, documentStatus, hasChildren, updatedAt] properties: id: type: string format: uuid parentPageId: type: [string, "null"] format: uuid title: type: string slug: type: string documentType: $ref: "#/components/schemas/DocumentType" documentStatus: $ref: "#/components/schemas/DocumentStatus" hasChildren: type: boolean updatedAt: type: string format: date-time PageChildrenResponse: type: object required: [items, nextCursor] properties: items: type: array items: $ref: "#/components/schemas/PageSummary" nextCursor: type: [string, "null"] BreadcrumbEntry: type: object required: [id, title] properties: id: type: string format: uuid title: type: string PageDetail: type: object required: - id - workspaceId - parentPageId - title - slug - markdown - renderedHtml - documentType - documentStatus - currentRevisionNumber - tags - breadcrumb - createdBy - updatedBy - createdAt - updatedAt - publishedAt - trashedAt - eTag properties: id: type: string format: uuid workspaceId: type: string format: uuid parentPageId: type: [string, "null"] format: uuid title: type: string slug: type: string markdown: type: string renderedHtml: type: string description: Server-rendered and sanitized. Never render untrusted Markdown as HTML client-side. documentType: $ref: "#/components/schemas/DocumentType" documentStatus: $ref: "#/components/schemas/DocumentStatus" currentRevisionNumber: type: integer format: int64 tags: type: array items: type: string breadcrumb: type: array items: $ref: "#/components/schemas/BreadcrumbEntry" createdBy: type: [string, "null"] format: uuid updatedBy: type: [string, "null"] format: uuid createdAt: type: string format: date-time updatedAt: type: string format: date-time publishedAt: type: [string, "null"] format: date-time trashedAt: type: [string, "null"] format: date-time description: Null unless the page is in the trash/retention model. eTag: type: string description: Also present as the response ETag header; included in the body so clients need not parse headers. CreatePageRequest: type: object required: [parentPageId, title, documentType, markdown, templateKey] description: >- Either supply documentType and markdown directly, or supply templateKey to seed the page from a starter template (in which case documentType/markdown are ignored and may be null). properties: parentPageId: type: [string, "null"] format: uuid title: type: string minLength: 1 maxLength: 300 documentType: oneOf: - $ref: "#/components/schemas/DocumentType" - type: "null" markdown: type: [string, "null"] maxLength: 2097152 templateKey: type: [string, "null"] tags: type: array items: type: string maxItems: 25 BeginPageCreationSessionRequest: type: object properties: parentPageId: type: [string, "null"] format: uuid PageCreationSessionResponse: type: object required: [sessionId, expiresAt] properties: sessionId: type: string format: uuid expiresAt: type: string format: date-time CommitPageCreationSessionRequest: type: object required: [title] description: >- No parentPageId (fixed at session-begin time) or templateKey (a template and pre-publish attachments are mutually exclusive, matching the existing product behavior). properties: title: type: string minLength: 1 maxLength: 300 documentType: oneOf: - $ref: "#/components/schemas/DocumentType" - type: "null" markdown: type: [string, "null"] maxLength: 2097152 tags: type: array items: type: string maxItems: 25 DiscardPageCreationSessionResponse: type: object required: [attachmentsRemoved] properties: attachmentsRemoved: type: integer UpdatePageRequest: type: object required: [title, markdown, documentType, documentStatus, changeSummary] properties: title: type: string minLength: 1 maxLength: 300 markdown: type: string maxLength: 2097152 documentType: $ref: "#/components/schemas/DocumentType" documentStatus: $ref: "#/components/schemas/DocumentStatus" changeSummary: type: [string, "null"] maxLength: 500 tags: type: array items: type: string maxItems: 25 description: Omitted or null leaves the page's tags unchanged. MovePageRequest: type: object required: [newParentPageId, afterSiblingId] properties: newParentPageId: type: [string, "null"] format: uuid afterSiblingId: type: [string, "null"] format: uuid description: Null means "first among the new siblings". TemplateSummary: type: object required: [key, name, documentType, description] properties: key: type: string name: type: string documentType: $ref: "#/components/schemas/DocumentType" description: type: string DraftResponse: type: object required: [draftKey, pageId, baseRevisionNumber, title, markdown, updatedAt, expiresAt, baseRevisionIsStale] properties: draftKey: type: string pageId: type: [string, "null"] format: uuid baseRevisionNumber: type: [integer, "null"] format: int64 title: type: string markdown: type: string updatedAt: type: string format: date-time expiresAt: type: string format: date-time baseRevisionIsStale: type: boolean description: True when the page has published a newer revision since this draft's base. SaveDraftRequest: type: object required: [pageId, baseRevisionNumber, title, markdown] properties: pageId: type: [string, "null"] format: uuid baseRevisionNumber: type: [integer, "null"] format: int64 title: type: string markdown: type: string PageRevisionSummary: type: object required: [revisionNumber, title, documentType, documentStatus, authoredBy, changeSummary, createdAt] properties: revisionNumber: type: integer format: int64 title: type: string documentType: $ref: "#/components/schemas/DocumentType" documentStatus: $ref: "#/components/schemas/DocumentStatus" authoredBy: type: [string, "null"] format: uuid changeSummary: type: [string, "null"] createdAt: type: string format: date-time PageRevisionDetailResponse: type: object required: [revisionNumber, title, markdown, renderedHtml, documentType, documentStatus, authoredBy, changeSummary, createdAt] properties: revisionNumber: type: integer format: int64 title: type: string markdown: type: string renderedHtml: type: string description: >- Server-rendered and sanitized without re-resolving internal links against today's link table (a historical snapshot's links are not live). documentType: $ref: "#/components/schemas/DocumentType" documentStatus: $ref: "#/components/schemas/DocumentStatus" authoredBy: type: [string, "null"] format: uuid changeSummary: type: [string, "null"] createdAt: type: string format: date-time BacklinkEntry: type: object required: [id, title, slug, documentType] properties: id: type: string format: uuid title: type: string slug: type: string documentType: $ref: "#/components/schemas/DocumentType" BacklinksResponse: type: object required: [items] properties: items: type: array items: $ref: "#/components/schemas/BacklinkEntry" TrashPurgeResponse: type: object required: [purgedCount] properties: purgedCount: type: integer AttachmentDeliveryPolicy: type: string enum: [InlinePreviewEligible, DownloadOnly, Unknown] AttachmentSummary: type: object required: [id, filename, mediaType, deliveryPolicy, byteLength, description, altText, createdAt] properties: id: type: string format: uuid filename: type: string description: The sanitized, stored display filename — never necessarily identical to what the client declared. mediaType: type: string description: The detected media type from the byte signature, not the caller-declared Content-Type. deliveryPolicy: $ref: "#/components/schemas/AttachmentDeliveryPolicy" byteLength: type: integer format: int64 description: type: [string, "null"] altText: type: [string, "null"] createdAt: type: string format: date-time AttachmentListResponse: type: object required: [items] properties: items: type: array items: $ref: "#/components/schemas/AttachmentSummary" ManualOverrideFieldCatalogEntry: type: object required: [fieldName, displayLabel, valueKind, maxStringLength, allowedEnumValues] properties: fieldName: type: string displayLabel: type: string valueKind: type: string enum: [Text, Enum] maxStringLength: type: integer description: 0 for an Enum-kind field, where length is governed by allowedEnumValues instead. allowedEnumValues: type: [array, "null"] items: type: string ManualOverrideFieldCatalogResponse: type: object required: [targetKind, fields] properties: targetKind: type: string fields: type: array items: $ref: "#/components/schemas/ManualOverrideFieldCatalogEntry" FactProvenance: type: object required: [sourceKey, confidence, lastSnapshotId, observedAt] properties: sourceKey: type: string confidence: type: string enum: [Observed, Inferred, Declared, Unknown] lastSnapshotId: type: string format: uuid observedAt: type: string format: date-time InventorySnapshotSummary: type: object required: [id, sourceKey, sourceHash, capturedAt, importedAt, hostCount, serviceCount] properties: id: type: string format: uuid sourceKey: type: string sourceHash: type: string capturedAt: type: string format: date-time importedAt: type: string format: date-time hostCount: type: integer serviceCount: type: integer InfrastructureHostSummary: type: object required: [id, sourceKey, hostname, operatingSystem, kernelVersion, architecture, logicalCpuCount, totalMemoryBytes, presence, lastSeenAt] properties: id: type: string format: uuid sourceKey: type: string hostname: type: string operatingSystem: type: string kernelVersion: type: string architecture: type: string logicalCpuCount: type: integer totalMemoryBytes: type: integer format: int64 presence: type: string enum: [Present, Missing] lastSeenAt: type: string format: date-time InfrastructureOverviewResponse: type: object required: [latestSnapshot, hosts, serviceCount, proposedServiceCount, missingServiceCount, unhealthyServiceCount, unprotectedServiceCount] properties: latestSnapshot: type: [object, "null"] allOf: - $ref: "#/components/schemas/InventorySnapshotSummary" hosts: type: array items: $ref: "#/components/schemas/InfrastructureHostSummary" serviceCount: type: integer proposedServiceCount: type: integer missingServiceCount: type: integer unhealthyServiceCount: type: integer unprotectedServiceCount: type: integer InfrastructureServiceSummary: type: object required: [id, sourceKey, name, detectedName, category, summary, runtimeKind, runtimeStatus, health, presence, confidence, imageReference, projectId, projectName, backupStatus, primaryEndpointUrl, isFavorite, isHidden, componentCount, endpointCount, volumeCount, databaseCount, lastSeenAt] properties: id: type: string format: uuid sourceKey: type: string name: type: string description: Editorial display name if set, otherwise the detected name. detectedName: type: string category: type: string summary: type: string runtimeKind: type: string runtimeStatus: type: string health: type: string enum: [Healthy, Degraded, Unhealthy, Unknown] presence: type: string enum: [Present, Proposed, Missing] confidence: type: string enum: [Observed, Inferred, Declared, Unknown] imageReference: type: string projectId: type: [string, "null"] format: uuid projectName: type: [string, "null"] backupStatus: type: string primaryEndpointUrl: type: [string, "null"] isFavorite: type: boolean isHidden: type: boolean componentCount: type: integer endpointCount: type: integer volumeCount: type: integer databaseCount: type: integer lastSeenAt: type: string format: date-time InfrastructureServiceListResponse: type: object required: [items, nextCursor, totalCount] properties: items: type: array items: $ref: "#/components/schemas/InfrastructureServiceSummary" nextCursor: type: [string, "null"] totalCount: type: integer description: >- How many services match the current `search`/`status`/`projectId`/ `includeHidden` filters in total (OUX-4). Counted server-side before cursor pagination, so it is the size of the filtered catalog rather than of the returned page, and stays stable while paging through it. InfrastructureStorageRow: type: object required: [id, serviceId, serviceName, name, mountPath, storageClass, readOnly, sizeBytes, provenance] properties: id: type: string format: uuid serviceId: type: string format: uuid serviceName: type: string name: type: string mountPath: type: string storageClass: type: string readOnly: type: boolean sizeBytes: type: [integer, "null"] format: int64 provenance: $ref: "#/components/schemas/FactProvenance" InfrastructureStorageListResponse: type: object required: [items, nextCursor] properties: items: type: array items: $ref: "#/components/schemas/InfrastructureStorageRow" nextCursor: type: [string, "null"] EditorialProfileResponse: type: object required: [id, displayName, summary, category, purpose, ownerId, projectId, repositoryUrl, liveUrl, backupStatus, monitoringUrl, maintenanceNotes, knownRisks, updateProcedure, rollbackProcedure, tags, isFavorite, isHidden, isArchived, updatedAt, etag] properties: id: type: string format: uuid displayName: type: string summary: type: string category: type: string purpose: type: string ownerId: type: [string, "null"] format: uuid projectId: type: [string, "null"] format: uuid repositoryUrl: type: string liveUrl: type: string backupStatus: type: string monitoringUrl: type: string maintenanceNotes: type: string knownRisks: type: string updateProcedure: type: string rollbackProcedure: type: string tags: type: array items: type: string isFavorite: type: boolean isHidden: type: boolean isArchived: type: boolean updatedAt: type: string format: date-time etag: type: string ManualOverrideResponse: type: object required: [id, fieldName, value, reason, createdBy, updatedAt] properties: id: type: string format: uuid fieldName: type: string value: description: The overridden value — any JSON scalar, array or object, matching the field's ManualOverrideFieldCatalogEntry.valueKind. reason: type: string createdBy: type: string format: uuid updatedAt: type: string format: date-time ServiceComponentResponse: type: object required: [id, name, kind, runtimeId, status, imageReference, provenance] properties: id: type: string format: uuid name: type: string kind: type: string runtimeId: type: string status: type: string imageReference: type: string provenance: $ref: "#/components/schemas/FactProvenance" InfrastructureEndpointResponse: type: object required: [id, scheme, host, publishedPort, targetPort, url, isPublic, provenance] properties: id: type: string format: uuid scheme: type: string host: type: string publishedPort: type: [integer, "null"] targetPort: type: [integer, "null"] url: type: string isPublic: type: boolean provenance: $ref: "#/components/schemas/FactProvenance" InfrastructureNetworkResponse: type: object required: [id, name, driver, scope, internal, provenance] properties: id: type: string format: uuid name: type: string driver: type: string scope: type: string internal: type: boolean provenance: $ref: "#/components/schemas/FactProvenance" InfrastructureVolumeResponse: type: object required: [id, name, mountPath, storageClass, readOnly, sizeBytes, provenance] properties: id: type: string format: uuid name: type: string mountPath: type: string storageClass: type: string readOnly: type: boolean sizeBytes: type: [integer, "null"] format: int64 provenance: $ref: "#/components/schemas/FactProvenance" InfrastructureDatabaseResponse: type: object required: [id, name, engine, version, host, port, hostPortPublished, provenance] properties: id: type: string format: uuid name: type: string engine: type: string version: type: string host: type: string port: type: [integer, "null"] hostPortPublished: type: boolean provenance: $ref: "#/components/schemas/FactProvenance" ServiceDependencyResponse: type: object required: [id, dependsOnServiceId, dependsOnSourceKey, kind, provenance] properties: id: type: string format: uuid dependsOnServiceId: type: [string, "null"] format: uuid dependsOnSourceKey: type: string kind: type: string provenance: $ref: "#/components/schemas/FactProvenance" HealthObservationResponse: type: object required: [id, state, checkKind, responseTimeMilliseconds, provenance] properties: id: type: string format: uuid state: type: string enum: [Healthy, Degraded, Unhealthy, Unknown] checkKind: type: string responseTimeMilliseconds: type: [integer, "null"] provenance: $ref: "#/components/schemas/FactProvenance" DeploymentReferenceResponse: type: object required: [id, repositoryUrl, commit, imageReference, imageDigest, deployedAt, provenance] properties: id: type: string format: uuid repositoryUrl: type: string commit: type: string imageReference: type: string imageDigest: type: string deployedAt: type: [string, "null"] format: date-time provenance: $ref: "#/components/schemas/FactProvenance" BackupCoverageResponse: type: object required: [id, method, schedule, lastSuccessfulAt, restoreTested, provenance] properties: id: type: string format: uuid method: type: string schedule: type: string lastSuccessfulAt: type: [string, "null"] format: date-time restoreTested: type: boolean provenance: $ref: "#/components/schemas/FactProvenance" InfrastructureServiceDetail: type: object required: [service, editorialProfile, editorialETag, overrides, components, endpoints, networks, volumes, databases, dependencies, healthObservations, deployments, backups] properties: service: $ref: "#/components/schemas/InfrastructureServiceSummary" editorialProfile: type: [object, "null"] allOf: - $ref: "#/components/schemas/EditorialProfileResponse" editorialETag: type: string overrides: type: array items: $ref: "#/components/schemas/ManualOverrideResponse" components: type: array items: $ref: "#/components/schemas/ServiceComponentResponse" endpoints: type: array items: $ref: "#/components/schemas/InfrastructureEndpointResponse" networks: type: array items: $ref: "#/components/schemas/InfrastructureNetworkResponse" volumes: type: array items: $ref: "#/components/schemas/InfrastructureVolumeResponse" databases: type: array items: $ref: "#/components/schemas/InfrastructureDatabaseResponse" dependencies: type: array items: $ref: "#/components/schemas/ServiceDependencyResponse" healthObservations: type: array items: $ref: "#/components/schemas/HealthObservationResponse" deployments: type: array items: $ref: "#/components/schemas/DeploymentReferenceResponse" backups: type: array items: $ref: "#/components/schemas/BackupCoverageResponse" UpdateEditorialProfileRequest: type: object required: [isFavorite, isHidden, isArchived] description: Every field besides the three booleans is optional — omit a field to leave it unchanged. properties: displayName: type: [string, "null"] summary: type: [string, "null"] category: type: [string, "null"] purpose: type: [string, "null"] ownerId: type: [string, "null"] format: uuid projectId: type: [string, "null"] format: uuid repositoryUrl: type: [string, "null"] liveUrl: type: [string, "null"] backupStatus: type: [string, "null"] monitoringUrl: type: [string, "null"] maintenanceNotes: type: [string, "null"] knownRisks: type: [string, "null"] updateProcedure: type: [string, "null"] rollbackProcedure: type: [string, "null"] tags: type: [array, "null"] items: type: string isFavorite: type: boolean isHidden: type: boolean isArchived: type: boolean SetManualOverrideRequest: type: object required: [fieldName, value] properties: fieldName: type: string description: Must be one of the field names GET /infrastructure/overrides/catalog returns for the target's object kind. value: description: Any JSON scalar, array or object, validated server-side against the field's catalog entry (valueKind, maxStringLength, allowedEnumValues). reason: type: [string, "null"] SetInfrastructureOverrideRequest: type: object required: [targetKind, targetId, fieldName, value] properties: targetKind: type: string enum: [Host, Service, Component, Endpoint, Network, Volume, Database, Deployment, Backup] targetId: type: string format: uuid fieldName: type: string description: Must be one of the field names GET /infrastructure/overrides/catalog?targetKind=... returns for this targetKind. value: description: Any JSON scalar, array or object, validated server-side against the field's catalog entry (valueKind, maxStringLength, allowedEnumValues). reason: type: [string, "null"] ReviewInfrastructureServiceRequest: type: object required: [decision] properties: decision: type: string enum: [Accept, Reject] reason: type: [string, "null"] ReviewInfrastructureServiceResponse: type: object required: [serviceId, presence] properties: serviceId: type: string format: uuid presence: type: string enum: [Present, Missing] InventoryChangeResponse: type: object required: [id, snapshotId, targetKind, targetId, sourceKey, changeKind, changedFields, recordedAt] properties: id: type: string format: uuid snapshotId: type: string format: uuid targetKind: type: string targetId: type: string format: uuid sourceKey: type: string changeKind: type: string enum: [Added, Updated, Missing, Unchanged] changedFields: type: array items: type: string recordedAt: type: string format: date-time InventoryHostRequest: type: object required: [sourceKey, hostname, logicalCpuCount, totalMemoryBytes] properties: sourceKey: type: string hostname: type: string operatingSystem: type: [string, "null"] kernelVersion: type: [string, "null"] architecture: type: [string, "null"] logicalCpuCount: type: integer totalMemoryBytes: type: integer format: int64 InventoryFactConfidence: type: string enum: [Observed, Inferred, Declared, Unknown] InventoryComponentRequest: type: object required: [sourceKey, name, confidence] properties: sourceKey: type: string name: type: string kind: type: [string, "null"] runtimeId: type: [string, "null"] status: type: [string, "null"] imageReference: type: [string, "null"] confidence: $ref: "#/components/schemas/InventoryFactConfidence" InventoryEndpointRequest: type: object required: [sourceKey, isPublic, confidence] properties: sourceKey: type: string scheme: type: [string, "null"] host: type: [string, "null"] publishedPort: type: [integer, "null"] targetPort: type: [integer, "null"] url: type: [string, "null"] isPublic: type: boolean confidence: $ref: "#/components/schemas/InventoryFactConfidence" InventoryNetworkRequest: type: object required: [sourceKey, name, internal, confidence] properties: sourceKey: type: string name: type: string driver: type: [string, "null"] scope: type: [string, "null"] internal: type: boolean confidence: $ref: "#/components/schemas/InventoryFactConfidence" InventoryVolumeRequest: type: object required: [sourceKey, name, readOnly, confidence] properties: sourceKey: type: string name: type: string mountPath: type: [string, "null"] storageClass: type: [string, "null"] readOnly: type: boolean sizeBytes: type: [integer, "null"] format: int64 confidence: $ref: "#/components/schemas/InventoryFactConfidence" InventoryDatabaseRequest: type: object required: [sourceKey, name, hostPortPublished, confidence] properties: sourceKey: type: string name: type: string engine: type: [string, "null"] version: type: [string, "null"] host: type: [string, "null"] port: type: [integer, "null"] hostPortPublished: type: boolean confidence: $ref: "#/components/schemas/InventoryFactConfidence" InventoryDependencyRequest: type: object required: [sourceKey, dependsOnSourceKey, confidence] properties: sourceKey: type: string dependsOnSourceKey: type: string kind: type: [string, "null"] confidence: $ref: "#/components/schemas/InventoryFactConfidence" InventoryHealthRequest: type: object required: [sourceKey, state, confidence] properties: sourceKey: type: string state: type: string enum: [Healthy, Degraded, Unhealthy, Unknown] checkKind: type: [string, "null"] responseTimeMilliseconds: type: [integer, "null"] confidence: $ref: "#/components/schemas/InventoryFactConfidence" InventoryDeploymentRequest: type: object required: [sourceKey, confidence] properties: sourceKey: type: string repositoryUrl: type: [string, "null"] commit: type: [string, "null"] imageReference: type: [string, "null"] imageDigest: type: [string, "null"] deployedAt: type: [string, "null"] format: date-time confidence: $ref: "#/components/schemas/InventoryFactConfidence" InventoryBackupRequest: type: object required: [sourceKey, restoreTested, confidence] properties: sourceKey: type: string method: type: [string, "null"] schedule: type: [string, "null"] lastSuccessfulAt: type: [string, "null"] format: date-time restoreTested: type: boolean confidence: $ref: "#/components/schemas/InventoryFactConfidence" InventoryServiceRequest: type: object required: [sourceKey, detectedName, health, readOnlyRootFilesystem, noNewPrivileges, privileged, confidence] properties: sourceKey: type: string hostSourceKey: type: [string, "null"] detectedName: type: string runtimeKind: type: [string, "null"] runtimeStatus: type: [string, "null"] health: type: string enum: [Healthy, Degraded, Unhealthy, Unknown] imageReference: type: [string, "null"] imageDigest: type: [string, "null"] runtimeUser: type: [string, "null"] readOnlyRootFilesystem: type: boolean noNewPrivileges: type: boolean privileged: type: boolean confidence: $ref: "#/components/schemas/InventoryFactConfidence" components: type: [array, "null"] items: $ref: "#/components/schemas/InventoryComponentRequest" endpoints: type: [array, "null"] items: $ref: "#/components/schemas/InventoryEndpointRequest" networks: type: [array, "null"] items: $ref: "#/components/schemas/InventoryNetworkRequest" volumes: type: [array, "null"] items: $ref: "#/components/schemas/InventoryVolumeRequest" databases: type: [array, "null"] items: $ref: "#/components/schemas/InventoryDatabaseRequest" dependencies: type: [array, "null"] items: $ref: "#/components/schemas/InventoryDependencyRequest" healthObservations: type: [array, "null"] items: $ref: "#/components/schemas/InventoryHealthRequest" deployments: type: [array, "null"] items: $ref: "#/components/schemas/InventoryDeploymentRequest" backups: type: [array, "null"] items: $ref: "#/components/schemas/InventoryBackupRequest" SynchronizeInventoryRequest: type: object required: [sourceKey, sourceHash, capturedAt] properties: sourceKey: type: string sourceHash: type: string capturedAt: type: string format: date-time hosts: type: [array, "null"] items: $ref: "#/components/schemas/InventoryHostRequest" services: type: [array, "null"] items: $ref: "#/components/schemas/InventoryServiceRequest" InventorySyncResponse: type: object required: [snapshotId, alreadyImported, addedServices, updatedServices, missingServices, preservedEditorialProfiles, preservedOverrides] properties: snapshotId: type: string format: uuid alreadyImported: type: boolean addedServices: type: integer updatedServices: type: integer missingServices: type: integer preservedEditorialProfiles: type: integer preservedOverrides: type: integer CuratedServiceProfileRequest: type: object required: [sourceKey, reviewed, archived] properties: sourceKey: type: string displayName: type: [string, "null"] summary: type: [string, "null"] category: type: [string, "null"] purpose: type: [string, "null"] repositoryUrl: type: [string, "null"] liveUrl: type: [string, "null"] tags: type: [array, "null"] items: type: string reviewed: type: boolean archived: type: boolean ImportCuratedInventoryRequest: type: object required: [inventory] properties: inventory: $ref: "#/components/schemas/SynchronizeInventoryRequest" services: type: [array, "null"] items: $ref: "#/components/schemas/CuratedServiceProfileRequest" ImportCuratedInventoryResponse: type: object required: [inventory, createdEditorialProfiles, confirmedServices, archivedServices] properties: inventory: $ref: "#/components/schemas/InventorySyncResponse" createdEditorialProfiles: type: integer confirmedServices: type: integer archivedServices: type: integer ActivityItemResponse: type: object required: [id, actorKind, actorId, source, action, targetType, targetId, summary, occurredAt] properties: id: type: integer format: int64 actorKind: type: string enum: [System, User, ServiceAccount] actorId: type: [string, "null"] format: uuid source: type: string action: type: string targetType: type: [string, "null"] targetId: type: [string, "null"] summary: type: string occurredAt: type: string format: date-time ActivityListResponse: type: object required: [items] properties: items: type: array items: $ref: "#/components/schemas/ActivityItemResponse" WorkspaceMemberSummary: type: object required: [id, displayName, email] properties: id: type: string format: uuid displayName: type: string email: type: string WorkspaceMemberListResponse: type: object required: [items] properties: items: type: array items: $ref: "#/components/schemas/WorkspaceMemberSummary" PreviewMarkdownRequest: type: object required: [markdown] properties: markdown: type: string PreviewMarkdownResponse: type: object required: [renderedHtml] properties: renderedHtml: type: string description: Sanitized HTML — safe to render directly, matching the same pipeline a published page revision uses. StagingCleanupRunSummary: type: object required: [sessionsExpired, sessionsClaimed, sessionsLostRace, attachmentReferencesRemoved, blobsRemoved, warnings] properties: sessionsExpired: type: integer sessionsClaimed: type: integer sessionsLostRace: type: integer description: Expired sessions this run observed but a concurrent run claimed first — never lost, just processed elsewhere. attachmentReferencesRemoved: type: integer blobsRemoved: type: integer warnings: type: array items: type: string StagingCleanupStatusResponse: type: object required: [activeSessionCount, expiredPendingCleanupCount, runInProgress, lastRunStartedAt, lastRunCompletedAt, lastRunTrigger, lastResult] properties: activeSessionCount: type: integer expiredPendingCleanupCount: type: integer runInProgress: type: boolean lastRunStartedAt: type: [string, "null"] format: date-time lastRunCompletedAt: type: [string, "null"] format: date-time lastRunTrigger: type: [string, "null"] enum: [scheduled, manual, null] lastResult: type: [object, "null"] allOf: - $ref: "#/components/schemas/StagingCleanupRunSummary" RunStagingCleanupRequest: type: object required: [dryRun] properties: dryRun: type: boolean RunStagingCleanupResponse: type: object required: [dryRun, result] properties: dryRun: type: boolean result: $ref: "#/components/schemas/StagingCleanupRunSummary" PreflightImportRequest: type: object required: [sourceRelativePath, idempotencyKey] properties: sourceRelativePath: type: string description: Resolved beneath the operator-mounted KnowledgeHub:Paths:Imports directory — never an arbitrary filesystem path. idempotencyKey: type: string ImportPlanItemResponse: type: object required: [sourceIdentity, kind, proposedTitle, proposedParentSourceIdentity, conflict, conflictDetail] properties: sourceIdentity: type: string kind: type: string enum: [Markdown, SqliteRow, Asset, Unsupported] proposedTitle: type: string proposedParentSourceIdentity: type: [string, "null"] conflict: type: string enum: [NoConflict, ExactRerunDuplicate, SamePathDifferentContent, ExistingUserPage, AmbiguousLink, Unsupported] conflictDetail: type: [string, "null"] ImportPreflightResponse: type: object required: [status, jobId, adapterName, discoveredCount, plannedCount, items, warnings, blockers] properties: status: type: string enum: [Success, NoApplicableAdapter, AlreadyPreflighted] jobId: type: [string, "null"] format: uuid adapterName: type: [string, "null"] discoveredCount: type: integer plannedCount: type: integer items: type: array items: $ref: "#/components/schemas/ImportPlanItemResponse" warnings: type: array items: type: string blockers: type: array items: type: string ExecuteImportResponse: type: object required: [status, imported, skipped, failed, sourceUnchangedAfterExecution, diagnostics] properties: status: type: string enum: [Success, JobNotFound, NotPlanReady, SourceChanged, PlanChanged] imported: type: integer skipped: type: integer failed: type: integer sourceUnchangedAfterExecution: type: boolean description: A re-fingerprint comparison proving the source was never mutated during import (M4-A08). diagnostics: type: array items: type: string RollbackImportResponse: type: object required: [status, removedPages, preservedPages, removedAttachments] properties: status: type: string enum: [Success, JobNotFound, NothingToRollBack] removedPages: type: integer preservedPages: type: integer description: Pages this import created but a user has since edited — never deleted by rollback. removedAttachments: type: integer ImportJobSummaryResponse: type: object required: [id, adapterName, status, discoveredCount, plannedCount, importedCount, skippedCount, failedCount, createdAt, startedAt, completedAt] properties: id: type: string format: uuid adapterName: type: string status: type: string enum: [Discovered, PlanReady, Executing, Completed, Failed, RolledBack] discoveredCount: type: integer plannedCount: type: integer importedCount: type: integer skippedCount: type: integer failedCount: type: integer createdAt: type: string format: date-time startedAt: type: [string, "null"] format: date-time completedAt: type: [string, "null"] format: date-time SourceBatchPurgeRequest: type: object required: [batchId, source, pages] properties: batchId: type: string minLength: 1 source: type: string pattern: "^[a-z0-9-]{1,64}$" pages: type: array minItems: 1 items: $ref: "#/components/schemas/SourceBatchPurgePageEvidence" SourceBatchPurgePageEvidence: type: object required: [pageId, contentHash] properties: pageId: type: string format: uuid contentHash: type: string pattern: "^[a-fA-F0-9]{64}$" ExecuteSourceBatchPurgeRequest: allOf: - $ref: "#/components/schemas/SourceBatchPurgeRequest" - type: object required: [confirmationHash] properties: confirmationHash: type: string pattern: "^[a-f0-9]{64}$" SourceBatchPurgeDryRunResponse: type: object required: [safe, confirmationHash, pages, descendantPages, revisions, drafts, links, attachmentUsages, attachmentReferences, orphanedBlobs, sharedBlobs, importJobs, importItems, provenanceRecords, sourceTagAssignments, searchIndexRecords, generatedSourceDerivedPages, retainedPages, blockers] properties: safe: type: boolean confirmationHash: type: string pages: type: integer descendantPages: type: integer revisions: type: integer drafts: type: integer links: type: integer attachmentUsages: type: integer attachmentReferences: type: integer orphanedBlobs: type: integer sharedBlobs: type: integer importJobs: type: integer importItems: type: integer provenanceRecords: type: integer sourceTagAssignments: type: integer searchIndexRecords: type: integer generatedSourceDerivedPages: type: integer retainedPages: type: integer blockers: type: array items: type: string SourceBatchPurgeResponse: type: object required: [status, dryRun, removedPages, removedRevisions, removedDrafts, removedLinks, removedAttachmentUsages, removedAttachmentReferences, removedImportJobs, removedImportItems, removedProvenanceRecords, removedBlobs] properties: status: type: string enum: [purged] dryRun: $ref: "#/components/schemas/SourceBatchPurgeDryRunResponse" removedPages: type: integer removedRevisions: type: integer removedDrafts: type: integer removedLinks: type: integer removedAttachmentUsages: type: integer removedAttachmentReferences: type: integer removedImportJobs: type: integer removedImportItems: type: integer removedProvenanceRecords: type: integer removedBlobs: type: integer SearchResultItem: type: object required: [id, parentPageId, title, slug, documentType, documentStatus, tags, updatedAt, snippet] properties: id: type: string format: uuid parentPageId: type: [string, "null"] format: uuid title: type: string slug: type: string documentType: $ref: "#/components/schemas/DocumentType" documentStatus: $ref: "#/components/schemas/DocumentStatus" tags: type: array items: type: string updatedAt: type: string format: date-time snippet: type: string description: >- May contain the safe server-generated highlight markers `**` around matched terms. Never raw HTML — clients must escape this text before rendering and only then interpret the `**` markers. SearchResponse: type: object required: [items, nextCursor] properties: items: type: array items: $ref: "#/components/schemas/SearchResultItem" nextCursor: type: [string, "null"] RagPublicationManifestItem: type: object description: >- docs/32-rag-publication-convention.md. `markdown` and `checksum` are always `null` when `retired` is `true`. required: - contentId - documentType - documentStatus - title - slug - canonicalUrl - markdown - revision - checksum - locale - space - updatedAt - publishedAt - retired - retiredReason properties: contentId: type: string format: uuid documentType: $ref: "#/components/schemas/DocumentType" documentStatus: $ref: "#/components/schemas/DocumentStatus" title: type: string slug: type: string canonicalUrl: type: string markdown: type: [string, "null"] revision: type: integer format: int64 checksum: type: [string, "null"] description: Lowercase-hex SHA-256 of `markdown` (Page.ContentHash). Null when retired. locale: type: string description: >- The page's own `locale:` tag, or the configured `KnowledgeHub:PrimaryLocale` when it declares none. Never silently `en`. space: type: [string, "null"] description: The page's own `rag:space:` tag suffix, if any. updatedAt: type: string format: date-time publishedAt: type: [string, "null"] format: date-time retired: type: boolean retiredReason: type: [string, "null"] enum: [null, trashed, draft, needs-review, deprecated, archived] RagPublicationManifestResponse: type: object required: [items, nextCursor] properties: items: type: array items: $ref: "#/components/schemas/RagPublicationManifestItem" nextCursor: type: [string, "null"] ProjectStatus: type: string description: docs/15-project-hub.md lifecycle diagram. Archived is terminal in v1 (ADR-014 decision 6). enum: [Concept, Planned, Active, Blocked, Maintenance, Paused, Completed, Archived] ProjectPriority: type: string enum: [Low, Medium, High, Critical] ProjectDocumentRole: type: string enum: - Overview - CurrentState - Architecture - Deployment - Infrastructure - Security - Testing - Troubleshooting - Release - UserDocumentation - Other ProjectUpdateCategory: type: string enum: [Progress, Feature, Fix, Deployment, Validation, Release, Decision, Risk, Blocker, Documentation, Maintenance] ProjectUpdateVerificationStatus: type: string description: The UI must distinguish reported claims from validated evidence (docs/15-project-hub.md). enum: [Reported, Verified, Superseded] ProjectMilestoneState: type: string enum: [Planned, InProgress, Completed, Cancelled] DecisionStatus: type: string enum: [Proposed, Accepted, Rejected, Superseded, Withdrawn] RunbookRiskLevel: type: string enum: [Low, Medium, High, Critical] RunbookStatus: type: string enum: [Draft, Active, Deprecated] ProjectSummary: type: object required: [id, name, slug, summary, status, priority, ownerId, repositoryUrl, liveUrl, updatedAt, iconAttachmentId] properties: id: { type: string, format: uuid } name: { type: string } slug: { type: string } summary: { type: string } status: { $ref: "#/components/schemas/ProjectStatus" } priority: { $ref: "#/components/schemas/ProjectPriority" } ownerId: { type: string, format: uuid } repositoryUrl: { type: [string, "null"], format: uri } liveUrl: { type: [string, "null"], format: uri } updatedAt: { type: string, format: date-time } iconAttachmentId: { type: [string, "null"], format: uuid, description: "Same-origin attachment content is served at /api/v1/attachments/{iconAttachmentId}/content." } ProjectListResponse: type: object required: [items, nextCursor, totalCount] properties: items: type: array items: $ref: "#/components/schemas/ProjectSummary" nextCursor: type: [string, "null"] totalCount: type: integer minimum: 0 description: >- How many projects match the current filters in total (OUX-4), counted server-side before cursor pagination. Not a substitute for `/projects/totals`, which answers the unfiltered dashboard question. ProjectCatalogTotalsResponse: type: object required: [totalCount, activeCount] properties: totalCount: { type: integer, minimum: 0 } activeCount: { type: integer, minimum: 0 } ProjectDocumentSummary: type: object required: [id, pageId, pageTitle, pageSlug, pageDocumentType, role] properties: id: { type: string, format: uuid } pageId: { type: string, format: uuid } pageTitle: { type: string } pageSlug: { type: string } pageDocumentType: { $ref: "#/components/schemas/DocumentType" } role: { $ref: "#/components/schemas/ProjectDocumentRole" } ProjectUpdateSummary: type: object required: [id, category, title, summaryMarkdown, verificationStatus, actorId, occurredAt] properties: id: { type: string, format: uuid } category: { $ref: "#/components/schemas/ProjectUpdateCategory" } title: { type: string } summaryMarkdown: { type: string } verificationStatus: { $ref: "#/components/schemas/ProjectUpdateVerificationStatus" } actorId: { type: string, format: uuid } occurredAt: { type: string, format: date-time } commitSha: { type: [string, "null"] } releaseTag: { type: [string, "null"] } issueIdentifier: { type: [string, "null"] } url: { type: [string, "null"] } pageId: { type: [string, "null"], format: uuid } decisionRecordId: { type: [string, "null"], format: uuid } milestoneId: { type: [string, "null"], format: uuid } attachmentReferenceId: { type: [string, "null"], format: uuid } supersedesUpdateId: { type: [string, "null"], format: uuid } ProjectUpdatesResponse: type: object required: [items, nextCursor] properties: items: type: array items: $ref: "#/components/schemas/ProjectUpdateSummary" nextCursor: type: [string, "null"] ProjectMilestoneSummary: type: object required: [id, name, state, summary, successCriteria, eTag] properties: id: { type: string, format: uuid } name: { type: string } state: { $ref: "#/components/schemas/ProjectMilestoneState" } targetDate: { type: [string, "null"], format: date } completionDate: { type: [string, "null"], format: date } summary: { type: string } successCriteria: { type: string } eTag: type: string description: Also present as the response ETag header. ProjectDecisionSummary: type: object required: [id, decisionNumber, status, summary] properties: id: { type: string, format: uuid } decisionNumber: { type: integer } status: { $ref: "#/components/schemas/DecisionStatus" } summary: { type: string } ProjectDetail: type: object required: - id - name - slug - summary - status - priority - currentStateMarkdown - ownerId - technologies - documents - recentUpdates - milestones - decisions - createdAt - updatedAt - eTag - iconAttachmentId properties: id: { type: string, format: uuid } name: { type: string } slug: { type: string } summary: { type: string } status: { $ref: "#/components/schemas/ProjectStatus" } priority: { $ref: "#/components/schemas/ProjectPriority" } currentStateMarkdown: { type: string } ownerId: { type: string, format: uuid } startDate: { type: [string, "null"], format: date } targetDate: { type: [string, "null"], format: date } completionDate: { type: [string, "null"], format: date } repositoryUrl: { type: [string, "null"] } liveUrl: { type: [string, "null"] } documentationUrl: { type: [string, "null"] } issueTrackerUrl: { type: [string, "null"] } technologies: type: array items: { type: string } documents: type: array items: $ref: "#/components/schemas/ProjectDocumentSummary" recentUpdates: type: array items: $ref: "#/components/schemas/ProjectUpdateSummary" milestones: type: array items: $ref: "#/components/schemas/ProjectMilestoneSummary" decisions: type: array items: $ref: "#/components/schemas/ProjectDecisionSummary" createdAt: { type: string, format: date-time } updatedAt: { type: string, format: date-time } eTag: type: string description: Also present as the response ETag header; included in the body so clients need not parse headers. iconAttachmentId: { type: [string, "null"], format: uuid } ProjectIconResponse: type: object required: [iconAttachmentId, eTag] properties: iconAttachmentId: { type: string, format: uuid } eTag: { type: string, description: Current project ETag after the icon change. } CreateProjectRequest: type: object required: [name, summary, priority, currentStateMarkdown, ownerId] properties: name: { type: string, minLength: 1, maxLength: 200 } summary: { type: string } priority: { $ref: "#/components/schemas/ProjectPriority" } currentStateMarkdown: { type: string } ownerId: { type: string, format: uuid } UpdateProjectRequest: type: object required: [name, summary, priority, currentStateMarkdown, ownerId] properties: name: { type: string, minLength: 1, maxLength: 200 } summary: { type: string } priority: { $ref: "#/components/schemas/ProjectPriority" } currentStateMarkdown: { type: string } ownerId: { type: string, format: uuid } startDate: { type: [string, "null"], format: date } targetDate: { type: [string, "null"], format: date } completionDate: { type: [string, "null"], format: date } repositoryUrl: { type: [string, "null"] } liveUrl: { type: [string, "null"] } documentationUrl: { type: [string, "null"] } issueTrackerUrl: { type: [string, "null"] } technologies: type: [array, "null"] items: { type: string } LinkProjectDocumentRequest: type: object required: [pageId, role] properties: pageId: { type: string, format: uuid } role: { $ref: "#/components/schemas/ProjectDocumentRole" } CreateProjectUpdateRequest: type: object required: [category, title, summaryMarkdown, verificationStatus] properties: category: { $ref: "#/components/schemas/ProjectUpdateCategory" } title: { type: string, maxLength: 200 } summaryMarkdown: { type: string } verificationStatus: { $ref: "#/components/schemas/ProjectUpdateVerificationStatus" } occurredAt: { type: [string, "null"], format: date-time } commitSha: { type: [string, "null"] } releaseTag: { type: [string, "null"] } issueIdentifier: { type: [string, "null"] } url: { type: [string, "null"] } pageId: { type: [string, "null"], format: uuid } decisionRecordId: { type: [string, "null"], format: uuid } milestoneId: { type: [string, "null"], format: uuid } attachmentReferenceId: { type: [string, "null"], format: uuid } supersedesUpdateId: { type: [string, "null"], format: uuid } CreateProjectMilestoneRequest: type: object required: [name, summary, successCriteria] properties: name: { type: string, minLength: 1 } targetDate: { type: [string, "null"], format: date } summary: { type: string } successCriteria: { type: string } UpdateProjectMilestoneRequest: type: object required: [name, summary, successCriteria, state] properties: name: { type: string, minLength: 1 } targetDate: { type: [string, "null"], format: date } summary: { type: string } successCriteria: { type: string } state: { $ref: "#/components/schemas/ProjectMilestoneState" } DecisionLineageEntry: type: object required: [id, decisionNumber, status, summary] properties: id: { type: string, format: uuid } decisionNumber: { type: integer } status: { $ref: "#/components/schemas/DecisionStatus" } summary: { type: string } DecisionDetail: type: object required: - id - workspaceId - pageId - decisionNumber - status - decisionDate - title - markdown - renderedHtml - summary - createdAt - updatedAt - eTag properties: id: { type: string, format: uuid } workspaceId: { type: string, format: uuid } projectId: { type: [string, "null"], format: uuid } pageId: { type: string, format: uuid } decisionNumber: { type: integer } status: { $ref: "#/components/schemas/DecisionStatus" } decisionDate: { type: string, format: date } title: { type: string } markdown: { type: string } renderedHtml: type: string description: Server-sanitized HTML — the same rendering pipeline as an ordinary Page. summary: { type: string } supersedes: oneOf: - $ref: "#/components/schemas/DecisionLineageEntry" - type: "null" supersededBy: oneOf: - $ref: "#/components/schemas/DecisionLineageEntry" - type: "null" createdAt: { type: string, format: date-time } updatedAt: { type: string, format: date-time } eTag: { type: string } DecisionSummary: type: object required: [id, decisionNumber, status, title, decisionDate, summary, updatedAt] properties: id: { type: string, format: uuid } projectId: { type: [string, "null"], format: uuid } decisionNumber: { type: integer } status: { $ref: "#/components/schemas/DecisionStatus" } title: { type: string } decisionDate: { type: string, format: date } summary: { type: string } updatedAt: { type: string, format: date-time } DecisionListResponse: type: object required: [items, nextCursor] properties: items: type: array items: $ref: "#/components/schemas/DecisionSummary" nextCursor: type: [string, "null"] CreateDecisionRequest: type: object required: [title, summary] properties: parentPageId: { type: [string, "null"], format: uuid } projectId: { type: [string, "null"], format: uuid } title: { type: string, minLength: 1 } markdown: { type: [string, "null"] } summary: { type: string } supersedesDecisionId: { type: [string, "null"], format: uuid } RunbookDetail: type: object required: - id - pageId - owner - riskLevel - status - requiresMaintenanceWindow - isStale - title - markdown - renderedHtml - currentRevisionNumber - createdAt - updatedAt - eTag properties: id: { type: string, format: uuid } pageId: { type: string, format: uuid } owner: { type: string, format: uuid } riskLevel: { $ref: "#/components/schemas/RunbookRiskLevel" } status: { $ref: "#/components/schemas/RunbookStatus" } requiresMaintenanceWindow: { type: boolean } estimatedDurationMinutes: { type: [integer, "null"] } reviewDueAt: { type: [string, "null"], format: date-time } lastValidatedAt: { type: [string, "null"], format: date-time } validatedBy: { type: [string, "null"], format: uuid } isStale: type: boolean description: True whenever the linked page has a newer revision than the last validation, or was never validated. title: { type: string } markdown: { type: string } renderedHtml: type: string description: Server-sanitized HTML — the same rendering pipeline as an ordinary Page. Never executed. currentRevisionNumber: { type: integer, format: int64 } createdAt: { type: string, format: date-time } updatedAt: { type: string, format: date-time } eTag: { type: string } RunbookSummary: type: object required: [id, pageId, title, riskLevel, status, isStale, updatedAt] properties: id: { type: string, format: uuid } pageId: { type: string, format: uuid } title: { type: string } riskLevel: { $ref: "#/components/schemas/RunbookRiskLevel" } status: { $ref: "#/components/schemas/RunbookStatus" } isStale: { type: boolean } lastValidatedAt: { type: [string, "null"], format: date-time } updatedAt: { type: string, format: date-time } RunbookListResponse: type: object required: [items, nextCursor] properties: items: type: array items: $ref: "#/components/schemas/RunbookSummary" nextCursor: type: [string, "null"] CreateRunbookRequest: type: object required: [title, riskLevel] properties: parentPageId: { type: [string, "null"], format: uuid } title: { type: string, minLength: 1 } markdown: { type: [string, "null"] } templateKey: { type: [string, "null"] } riskLevel: { $ref: "#/components/schemas/RunbookRiskLevel" } requiresMaintenanceWindow: { type: boolean } estimatedDurationMinutes: { type: [integer, "null"] } UpdateRunbookProfileRequest: type: object required: [owner, riskLevel, status, requiresMaintenanceWindow] properties: owner: { type: string, format: uuid } riskLevel: { $ref: "#/components/schemas/RunbookRiskLevel" } status: { $ref: "#/components/schemas/RunbookStatus" } requiresMaintenanceWindow: { type: boolean } estimatedDurationMinutes: { type: [integer, "null"] } reviewDueAt: { type: [string, "null"], format: date-time } CreateServiceAccountRequest: type: object required: [name, description, scopes] properties: name: type: string minLength: 1 maxLength: 200 description: type: string maxLength: 1000 scopes: type: array minItems: 1 uniqueItems: true items: type: string description: "One of the known scope constants, e.g. pages:read, pages:write, projects:read, runbooks:write (see docs/09-api-contract.md)." IssueServiceAccountTokenRequest: type: object required: [scopes] properties: scopes: type: array minItems: 1 uniqueItems: true items: type: string CreatedServiceAccountToken: type: object description: The raw token is only ever present in this one response — it is never persisted in the clear and is not retrievable again. required: [serviceAccountId, tokenId, rawToken] properties: serviceAccountId: type: string format: uuid tokenId: type: string format: uuid rawToken: type: string description: "Format ikh__. Present exactly once." ServiceAccountTokenSummary: type: object required: [id, publicId, scopes, createdAt] properties: id: type: string format: uuid publicId: type: string description: Safe, non-secret identifier — never the raw secret. scopes: type: array items: type: string createdAt: type: string format: date-time lastUsedAt: type: [string, "null"] format: date-time expiresAt: type: [string, "null"] format: date-time revokedAt: type: [string, "null"] format: date-time ServiceAccountSummary: type: object required: [id, name, description, enabled, createdAt, tokens] properties: id: type: string format: uuid name: type: string description: type: string enabled: type: boolean createdAt: type: string format: date-time tokens: type: array items: $ref: "#/components/schemas/ServiceAccountTokenSummary" ServiceAccountListResponse: type: object required: [items] properties: items: type: array items: $ref: "#/components/schemas/ServiceAccountSummary" CitationResponse: type: object description: A stable, citable pointer to a document or decision included in a project context bundle. required: [kind, id, title, canonicalPath, updatedAt] properties: kind: type: string enum: [Page, DecisionRecord, RunbookProfile] id: type: string format: uuid revision: type: [integer, "null"] format: int64 title: type: string canonicalPath: type: string updatedAt: type: string format: date-time ProjectContextProjectSummary: type: object required: [id, name, status, summary, currentStateMarkdown, updatedAt] properties: id: type: string format: uuid name: type: string status: type: string summary: type: string currentStateMarkdown: type: string updatedAt: type: string format: date-time ProjectContextTruncation: type: object required: [truncated, omittedCounts] properties: truncated: type: boolean omittedCounts: type: object additionalProperties: type: integer description: "Counts of items omitted per collection (e.g. updates, decisions), keyed by collection name." ProjectContextResponse: type: object description: "A bounded, permission-filtered bundle of the current project state, suitable for a single-call context fetch (docs/09-api-contract.md 'Project context', M6-A04). Unlike the unbounded project overview, this response always reports what was truncated rather than growing unbounded." required: [project, recentUpdates, activeMilestones, decisions, runbooks, documents, citations, truncation] properties: project: $ref: "#/components/schemas/ProjectContextProjectSummary" recentUpdates: type: array items: $ref: "#/components/schemas/ProjectUpdateSummary" activeMilestones: type: array items: $ref: "#/components/schemas/ProjectMilestoneSummary" decisions: type: array items: $ref: "#/components/schemas/ProjectDecisionSummary" runbooks: type: array items: $ref: "#/components/schemas/ProjectDocumentSummary" documents: type: array items: $ref: "#/components/schemas/ProjectDocumentSummary" citations: type: array items: $ref: "#/components/schemas/CitationResponse" truncation: $ref: "#/components/schemas/ProjectContextTruncation" BackupSummaryResponse: type: object description: Never includes a filesystem path, connection string or other private detail. required: [backupId, createdAt, totalSizeBytes, tableCount, totalRowCount, attachmentCount, schemaVersion, productVersion] properties: backupId: type: string createdAt: type: string format: date-time totalSizeBytes: type: integer format: int64 tableCount: type: integer totalRowCount: type: integer format: int64 attachmentCount: type: integer schemaVersion: type: string productVersion: type: string BackupListResponse: type: object required: [items] properties: items: type: array items: $ref: "#/components/schemas/BackupSummaryResponse" ProblemDetails: type: object required: [type, title, status, correlationId] properties: type: type: string format: uri-reference title: type: string status: type: integer detail: type: string instance: type: string format: uri-reference correlationId: type: string errors: type: object additionalProperties: type: array items: type: string SetupStatus: type: object required: [setupRequired] properties: setupRequired: type: boolean CompleteSetupRequest: type: object required: [workspaceName, displayName, email, password] properties: workspaceName: type: string minLength: 1 maxLength: 200 displayName: type: string minLength: 1 maxLength: 200 email: type: string format: email password: type: string minLength: 12 CompleteSetupResponse: type: object required: [workspaceId, userId] properties: workspaceId: type: string format: uuid userId: type: string format: uuid CsrfToken: type: object required: [token, headerName] properties: token: type: string headerName: type: string WorkspaceRole: type: string enum: [Reader, Editor, Administrator] SessionResponse: type: object required: [authenticated, setupRequired] properties: authenticated: type: boolean setupRequired: type: boolean userId: type: [string, "null"] format: uuid displayName: type: [string, "null"] email: type: [string, "null"] workspaceId: type: [string, "null"] format: uuid role: oneOf: - $ref: "#/components/schemas/WorkspaceRole" - type: "null" LoginRequest: type: object required: [email, password] properties: email: type: string format: email password: type: string RecoveryRedeemRequest: type: object required: [token, newPassword] properties: token: type: string newPassword: type: string minLength: 12 UserSummary: type: object required: [id, displayName, email, role, enabled, createdAt] properties: id: type: string format: uuid displayName: type: string email: type: string role: $ref: "#/components/schemas/WorkspaceRole" enabled: type: boolean createdAt: type: string format: date-time lastLoginAt: type: [string, "null"] format: date-time CreateUserRequest: type: object required: [displayName, email, password, role] properties: displayName: type: string minLength: 1 maxLength: 200 email: type: string format: email password: type: string minLength: 12 role: $ref: "#/components/schemas/WorkspaceRole" UpdateUserRequest: type: object properties: role: oneOf: - $ref: "#/components/schemas/WorkspaceRole" - type: "null" enabled: type: [boolean, "null"] IssueRecoveryTokenResponse: type: object required: [token, expiresAt] properties: token: type: string description: Shown once, at issuance. Never retrievable again. expiresAt: type: string format: date-time