openapi: 3.1.0 info: title: WeKnora extension protocol version: "1" description: | What WeKnora calls on a code plugin. Every POST takes an Envelope and answers {"output": ...}, or streams NDJSON Events. Failures answer a non-2xx status with an ErrorBody. Within a major version the protocol only grows; both sides ignore fields they do not know. Host plugins (started by the WeKnora plugin host) are called over the socket from their handshake with `Authorization: Bearer `. Remote plugins are called over HTTPS and verify `X-WeKnora-Signature`: hex HMAC-SHA256 of "." with the shared secret, rejecting timestamps more than 5 minutes off. servers: - url: http://plugin security: - hostToken: [] - signature: [] paths: /v1/manifest: get: summary: What this process is and serves responses: "200": description: The manifest content: application/json: schema: { $ref: "#/components/schemas/Manifest" } default: { $ref: "#/components/responses/Error" } /v1/health: get: summary: Liveness responses: "200": description: Healthy content: application/json: schema: type: object required: [status] properties: { status: { const: ok } } default: { $ref: "#/components/responses/Error" } /v1/config/validate: post: summary: Check the plugin's system and tenant configuration requestBody: { $ref: "#/components/requestBodies/Envelope" } responses: "200": { $ref: "#/components/responses/EmptyOutput" } default: { $ref: "#/components/responses/Error" } /v1/websearch/{id}/search: post: summary: Web search (WebSearchProvider.Search) parameters: [{ $ref: "#/components/parameters/ContributionID" }] requestBody: required: true content: application/json: schema: allOf: - $ref: "#/components/schemas/Envelope" - properties: { input: { $ref: "#/components/schemas/SearchInput" } } responses: "200": description: Results content: application/json: schema: type: object required: [output] properties: { output: { $ref: "#/components/schemas/SearchOutput" } } default: { $ref: "#/components/responses/Error" } /v1/connectors/{id}/validate: post: summary: Check a data source configuration (Connector.Validate) parameters: [{ $ref: "#/components/parameters/ContributionID" }] requestBody: { $ref: "#/components/requestBodies/Envelope" } responses: "200": { $ref: "#/components/responses/EmptyOutput" } default: { $ref: "#/components/responses/Error" } /v1/connectors/{id}/list-resources: post: summary: List syncable resources under a parent (Connector.ListResources) parameters: [{ $ref: "#/components/parameters/ContributionID" }] requestBody: required: true content: application/json: schema: allOf: - $ref: "#/components/schemas/Envelope" - properties: input: type: object properties: { parentId: { type: string } } responses: "200": description: Resources content: application/json: schema: type: object required: [output] properties: output: type: object required: [resources] properties: resources: { type: array, items: { $ref: "#/components/schemas/Resource" } } default: { $ref: "#/components/responses/Error" } /v1/connectors/{id}/resolve-ancestors: post: summary: Ancestors a lazy picker must expand to show a selection parameters: [{ $ref: "#/components/parameters/ContributionID" }] requestBody: required: true content: application/json: schema: allOf: - $ref: "#/components/schemas/Envelope" - properties: input: type: object properties: { resourceIds: { type: array, items: { type: string } } } responses: "200": description: Ancestor IDs content: application/json: schema: type: object required: [output] properties: output: type: object required: [ancestors] properties: { ancestors: { type: array, items: { type: string } } } default: { $ref: "#/components/responses/Error" } /v1/connectors/{id}/fetch: post: summary: Stream a full or incremental sync description: | Answers NDJSON: "item" events carry a FetchedItem, "checkpoint" events a Cursor (a complete, resumable snapshot), and the stream ends with "end" (data = final Cursor) or "error". A stream that stops without either is treated as interrupted. parameters: [{ $ref: "#/components/parameters/ContributionID" }] requestBody: required: true content: application/json: schema: allOf: - $ref: "#/components/schemas/Envelope" - properties: { input: { $ref: "#/components/schemas/FetchInput" } } responses: "200": description: Event stream content: application/x-ndjson: schema: { $ref: "#/components/schemas/Event" } default: { $ref: "#/components/responses/Error" } /v1/parsers/{id}/parse: post: summary: Parse a document into Markdown (DocReader.Read) description: | The file arrives as base64 content (or a URL). Answer Markdown and the images it references; WeKnora stores the images and chunks the text. A non-retryable error fails the document for good; a retryable one (unavailable, rate_limited) is retried later. parameters: [{ $ref: "#/components/parameters/ContributionID" }] requestBody: required: true content: application/json: schema: allOf: - $ref: "#/components/schemas/Envelope" - properties: { input: { $ref: "#/components/schemas/ParseInput" } } responses: "200": description: The parsed document content: application/json: schema: type: object required: [output] properties: { output: { $ref: "#/components/schemas/ParseOutput" } } default: { $ref: "#/components/responses/Error" } /v1/ui/request: post: summary: Answer a request from one of the plugin's pages description: | Plugin pages (pages, settingsSections, kbTabs) run in a sandboxed iframe and reach the plugin only through the WeKnora bridge. WeKnora checks the caller may open the page (its minRole) and forwards the request with the usual envelope. Method and path are the page's own routing. Plugins with pages that make no requests answer not_found. The manifest lists "ui": ["request"] under contributes when the plugin answers. requestBody: required: true content: application/json: schema: allOf: - $ref: "#/components/schemas/Envelope" - properties: { input: { $ref: "#/components/schemas/UIRequest" } } responses: "200": description: What the page receives content: application/json: schema: type: object required: [output] properties: { output: { $ref: "#/components/schemas/UIResponse" } } default: { $ref: "#/components/responses/Error" } /v1/events: post: summary: Receive an event the plugin subscribed to (permissions.events) description: | At least once: a retryable error or a timeout brings the same event back later with the same id and a higher attempt, so handle it idempotently. A non-retryable error drops it. Ignore unknown types. The manifest lists "events": ["handler"] under contributes when the plugin handles events. requestBody: required: true content: application/json: schema: allOf: - $ref: "#/components/schemas/Envelope" - properties: { input: { $ref: "#/components/schemas/EventDelivery" } } responses: "200": description: Handled content: application/json: schema: { type: object, required: [output], properties: { output: { type: object } } } default: { $ref: "#/components/responses/Error" } /v1/webhooks/{id}: post: summary: Answer an inbound call to webhook id (contributes.webhooks) description: | Third parties call a secret per-workspace URL of WeKnora; WeKnora relays the call here with that workspace's envelope. Verify the sender (e.g. a signature header) with the workspace configuration. parameters: [{ $ref: "#/components/parameters/ContributionID" }] requestBody: required: true content: application/json: schema: allOf: - $ref: "#/components/schemas/Envelope" - properties: { input: { $ref: "#/components/schemas/WebhookRequest" } } responses: "200": description: The answer to the caller content: application/json: schema: type: object required: [output] properties: { output: { $ref: "#/components/schemas/WebhookResponse" } } default: { $ref: "#/components/responses/Error" } components: securitySchemes: hostToken: { type: http, scheme: bearer } signature: { type: apiKey, in: header, name: X-WeKnora-Signature } parameters: ContributionID: name: id in: path required: true description: The contribution's local ID from plugin.yaml schema: { type: string } requestBodies: Envelope: required: true content: application/json: schema: { $ref: "#/components/schemas/Envelope" } responses: EmptyOutput: description: Accepted content: application/json: schema: type: object required: [output] properties: { output: { type: object } } Error: description: A failed call content: application/json: schema: { $ref: "#/components/schemas/ErrorBody" } schemas: Envelope: type: object required: [context, config] properties: context: type: object required: [tenantId] properties: tenantId: { type: integer } userId: { type: string } locale: { type: string } requestId: { type: string } deadline: { type: string, format: date-time } host: type: object description: Host API access; absent without granted scopes properties: url: { type: string } token: { type: string } webhooks: type: object description: This workspace's webhook URLs by webhook ID, when WeKnora knows its public address additionalProperties: { type: string } config: type: object description: Configuration scopes, secrets decrypted, never merged properties: system: { type: object } tenant: { type: object } instance: { type: object } input: {} Manifest: type: object required: [id, version, apiVersion, contributes] properties: id: { type: string } version: { type: string } apiVersion: { const: weknora.plugin/v1 } contributes: type: object additionalProperties: { type: array, items: { type: string } } SearchInput: type: object required: [query] properties: query: { type: string } maxResults: { type: integer } includeDate: { type: boolean } region: { type: string } freshness: { type: string } SearchOutput: type: object required: [results] properties: results: type: array items: type: object required: [title, url] properties: title: { type: string } url: { type: string } snippet: { type: string } content: { type: string } age: { type: string } publishedAt: { type: string, format: date-time } Resource: type: object required: [externalId, name] properties: externalId: { type: string } name: { type: string } type: { type: string } description: { type: string } url: { type: string } modifiedAt: { type: string, format: date-time } parentId: { type: string } hasChildren: { type: boolean } metadata: { type: object } FetchInput: type: object required: [mode] properties: mode: { enum: [full, incremental] } cursor: { $ref: "#/components/schemas/Cursor" } resourceIds: { type: array, items: { type: string } } Cursor: type: object properties: lastSyncTime: { type: string, format: date-time } state: { type: object } FetchedItem: type: object required: [externalId, title] properties: externalId: { type: string } title: { type: string } content: { type: string, contentEncoding: base64 } contentType: { type: string } fileName: { type: string } url: { type: string } updatedAt: { type: string, format: date-time } createdAt: { type: string, format: date-time } metadata: { type: object, additionalProperties: { type: string } } isDeleted: { type: boolean } sourceResourceId: { type: string } ParseInput: type: object properties: fileName: { type: string } fileType: { type: string, description: "Lower-case extension without the dot" } content: { type: string, contentEncoding: base64 } url: { type: string } title: { type: string } ParseOutput: type: object required: [markdown] properties: markdown: { type: string } images: type: array items: type: object required: [originalRef, data] properties: originalRef: { type: string } mimeType: { type: string } data: { type: string, contentEncoding: base64 } metadata: { type: object, additionalProperties: { type: string } } EventDelivery: type: object required: [id, type, occurredAt, attempt] properties: id: { type: string, description: Idempotency key; kept across retries } type: { enum: [knowledge.ingested, knowledge.failed, knowledge.deleted, chat.answered] } occurredAt: { type: string, format: date-time } attempt: { type: integer, minimum: 1 } data: oneOf: - $ref: "#/components/schemas/KnowledgeEventData" - $ref: "#/components/schemas/ChatEventData" KnowledgeEventData: type: object required: [knowledgeBaseId, knowledgeId] properties: knowledgeBaseId: { type: string } knowledgeId: { type: string } title: { type: string } fileName: { type: string } fileType: { type: string } source: { type: string } error: { type: string } ChatEventData: type: object required: [sessionId, messageId, answer] properties: sessionId: { type: string } messageId: { type: string } agentId: { type: string } userId: { type: string } question: { type: string } answer: { type: string } WebhookRequest: type: object required: [method, path] properties: method: { type: string } path: { type: string } query: { type: string } headers: { type: object, additionalProperties: { type: string } } body: { type: string, contentEncoding: base64 } WebhookResponse: type: object properties: status: { type: integer } contentType: { type: string } body: { type: string, contentEncoding: base64 } UIRequest: type: object required: [mount, method, path] properties: mount: { type: string, description: '"/" of the page, e.g. pages/links' } method: { type: string } path: { type: string } body: { description: JSON the page sent } role: { enum: [viewer, contributor, admin, owner] } UIResponse: type: object properties: status: { type: integer, description: HTTP-style status for the page; 200 when absent } body: { description: JSON returned to the page } Event: type: object required: [type] properties: type: { enum: [item, checkpoint, progress, log, error, end] } data: {} error: { $ref: "#/components/schemas/Error" } message: { type: string } level: { enum: [debug, info, warn, error] } Error: type: object required: [code, message] properties: code: { enum: [invalid_config, unauthorized, not_found, rate_limited, unavailable, bad_request, internal] } message: { type: string } retryable: { type: boolean } details: type: object properties: fields: { type: object, additionalProperties: { type: string } } retryAfter: { type: integer } ErrorBody: type: object required: [error] properties: error: { $ref: "#/components/schemas/Error" }