Navigace
ELOWEN · Dokumentace

Poslední aktualizace: 27. října 2026

Procházet dokumentaci · Kanban data and service contracts

Tento článek zatím není přeložen. Je dostupný v angličtině.

Plugin development

Kanban data and service contracts

Boundaries

Kanban owns schema plugin_kanban. The entry exports migrate and register. The host migrates before registration; runners acquire existing state without migrating. createKanbanRuntime(ctx) returns the plugin database and the single KanbanAccess owner. Registration creates one service instance, registers member-scoped tools, and gives that same instance and its agent integration to every API route and lifecycle callback. The loader owns migration; runners acquire the existing schema. A handoff (by a person or their agent) prepares one conversation, binds its first owner, synchronizes participants and sends paid work. Native settlement is observational; missed events after a hard crash are not replayed.

The manifest requires shared API 16, browser API 74 and core 0.30.47. It declares complete method/path security projections, human account grants, conversation writes and event publication. The browser bundle registers page keys '', ':boardId' and ':boardId/:cardId'. These keys belong in the UI registration's pages, not manifest web.userPages: the latter is a separate-entry, exact-path contribution.

Wire contract

src/apiContract.ts owns Zod request, query, path and success-response schemas for every route in kanbanRoutes, exported as apiRoutes for the host's native manifest generator. That one descriptor carries input, summary, credential policy and transport kind; registration spreads it without reconstructing security. The host's projectApiRouteSecurity generates the 32 manifest declarations. Existing human and agent/API-token admission is unchanged; resource membership and founder/administrator checks remain in the service, not in discovery metadata. responseSchema owns successful wire validation, including bodyless 204 responses. Lazy parseParams, parseQuery and json(schema) consume the exact registered schemas after human-account admission, in the original params/query/body order. Existing bodyless operations still ignore the request body. Syntax and input-schema failures keep the native 400 envelope; service failures and invalid successful output still log locally and return 500. Unsupported methods and undeclared paths remain dispatcher 404s. No migration or browser change is required. All errors use errorResponseSchema; no server stack or SQL is public. src/types.ts derives RouteParams<K>, RouteBody<K>, RouteInput<K>, RouteQuery<K> and RouteResponse<K> from that authority. RouteInput is the browser input before defaults; RouteBody is the validated service input.

createBoard accepts optional initialColumnNames, a tuple of exactly four nonempty trimmed names in ordinary/ordinary/review/done order. The browser sends localized names as user data; omission keeps the English defaults for name-only API callers. No account-locale inference occurs.

listMemberCandidates is GET boards/:boardId/member-candidates, with {boardId} parameters and empty query/body. It returns {users: Member[]} for admitted members and administrators, in person or through their agent. The transaction-bound access owner excludes existing members and nonhuman accounts; whole-team boards have no candidates. No KanbanUpdate action calls this route; an agent reads it through ElowenApi.

listCards and shared card search accept archived: 'only' to select only archived cards before SQL count and pagination. That explicit mode overrides includeArchived; when omitted, the existing active/all behavior is unchanged.

Plugin entity IDs are UUID strings; account and Project IDs are positive integers. Dates are calendar YYYY-MM-DD, timestamps are ISO strings with offsets. Positions are nonnegative safe gap integers. A free-slot move updates one row. An occupied requested slot inserts before that item and shifts the remaining container positions without changing their order; bulk moves retain the supplied card-ID order. Position exhaustion is a conflict. Card descriptions carry HTML and plain text together. Rendering remains the shared UI's responsibility; this contract never authorizes raw HTML insertion.

Board reads return BoardSnapshot (board, members, columns, labels, active cards). Card reads return CardDetail, including the same board context and checklist, comments, attachments and activity. Cards carry aggregate counts for tiles. Filtered/archived card queries are paged separately. Board permissions are a presentation projection of the same access decision, not caller-supplied authority.

src/toolContract.ts defines the three agent tools' input schemas and the Update action type. They use the same card bodies as HTTP. Results use the typed KanbanService DTOs and RouteResponse contracts, not separate tool-output validators. Read adds bounded attachment content to the native tool result after serializing CardDetail. setLabels may atomically create newLabels and replace existing label IDs. Tool assignment never starts paid work or assigns Chetty; only the handoff route does. Removing checklist items is the explicitly supported checklist operation, not card/board/attachment deletion.

Attachment uploads contain filename, mime and contentBase64; decoded bytes must not exceed 5 MiB per file. The maximum base64 payload plus the bounded filename and MIME metadata fits within the host plugin API's unchanged 8 MiB JSON request cap. The request schema, attachment metadata and database size constraint share this decoded cap. The wire never exposes storage_key. Attachment GET returns binary, not a JSON envelope, with attachment disposition, X-Content-Type-Options: nosniff and Cache-Control: private, no-store. Only PNG, JPEG, GIF, WebP and PDF are previewable; other files download.

Persistence and access

Migration 1 creates boards, members, columns, cards, labels, joins, checklist, comments, mention IDs, attachment metadata, activity, handoff reservations and due-alert claims. Column roles have a partial per-board unique index. Composite foreign keys prevent cross-board columns and label assignments. Child rows cascade when a card or board is permanently removed. Account references deliberately have no foreign key to core: account-removal callbacks clear identity references while retaining authored content. Filesystem attachment deletion is owned by the backend mutation/lifecycle implementation, not by SQL cascades. Migration 1 also creates attachment_cleanup, without a child foreign key, so pending file deletion survives card/board removal. Upload reservations distinguish in-flight uploads from deletions: periodic cleanup handles deletions; authoritative startup also reaps uploads interrupted by the previous process. The default non-retrying transaction locks each reservation during its file write and metadata insertion; the reservation disappears only with committed metadata. See Backend for recovery and composition.

Due alerts claim (card_id, due_date, state, user_id), where state is due-tomorrow or overdue. Concurrent claim insertion uses ON CONFLICT; only the claimant sends. An expired claimed lease may be reacquired with a fresh token. Delivery marks delivered_at using that token. A stable host alert key derived from the composite identity is required so retry after send-before-mark does not duplicate a notice. Delivered claims survive interval reruns; changed due dates get distinct claims. The interval runs only in the authoritative process.

KanbanAccess reads live human identities. An agent acting for a person has exactly that person's rights, no more and no less: actor.source never widens or narrows a check. Administrators see every board. Unknown/invisible boards and mismatched card/board IDs return 404. Any live human account creates boards; that person is the founder. Only the founder or an administrator manages boards/columns, removes members or deletes cards. Members may add human accounts. Any member may also update/delete board labels; these actions do not depend on the founder/admin manage permission. Membership accepts only accounts whose host-projected type is human; chatbot accounts are excluded. This exact type check is the membership contract, without account-name heuristics.

The same rule holds for the agent's HTTP calls through ElowenApi and for KanbanUpdate. finish is the one agent-only action and additionally requires the card conversation's exact session ID.

Inside write transactions pass {sql: tx, users: tx.host.usersRead} to access methods. Never capture the root host facade in that callback. Writes and membership changes lock the board first; authorization is a separate statement after the lock so it observes membership removal committed while waiting. requireCard locks board before card. All implementations must preserve that order. members is an internal recipient/participant projection after access admission, not an unauthenticated user-facing read.

Service injection

Backend modules live under src/backend/: service, routes, events, alerts and lifecycle. Agent modules live under src/agent/: tools, handoff, hooks and participant synchronization. They extend src/index.ts composition without redefining the shared contracts. tests/helpers/kanbanRegistration.ts supplies the real core registry/host fixture; plugins/kanban/tests/harness.ts supplies PostgreSQL plus a mutable fake identity projection.

src/service.ts defines KanbanService, MutationCommand, ReadCommand, AgentTransition and KanbanAgent. The backend implementation owns the only card write path. HTTP routes and tools receive the identical service instance; tools translate their actions to mutate(actor, {route, params, body}), never implement card SQL again. The generic result is RouteResponse<typeof command.route>. Reads use read, search, readCard and download.

The agent integration uses reserveHandoff before calling the external conversation writer. It atomically checks human access, the plugin grant and linked Project, then stores one reservation per card in handoffs with a token, owner and previous card projection. A competing handoff cannot start paid work. transitionAgent applies start, refusal, settlement and finish through the same transaction/activity/event path. Start/refusal use the durable token; settlements use the recorded session ID. The prepared session and first owner are bound before participants or paid send and are retained on refusal. The previous projection remains until admission/refusal is settled, so a refusal does not overwrite a newer finish. Restart never retries a send or infers admission from a lost promise; stale card state is an accepted observation limit.

KanbanAgent.syncBoardParticipants(boardId) resolves the full current human membership and syncs all existing card conversations, excluding each conversation owner. The backend calls it after committed member changes, including user removal. Its failures propagate. It is not a second membership or card mutation authority.

Composition order is runtime, service, agent registration, then API/lifecycle registration. The API's handoff route delegates to KanbanAgent.handoff. All card changes and finish delegate to KanbanService. Post-commit event fan-out and alerts belong to the backend, not a parallel tool implementation.

The native writer's prepare is the awaited conversation-readiness boundary before setParticipants and send. The owner-only conversations.turn.settled hook carries session ID and answered/stopped/failed, not the original key or a paused outcome. Core commits the journal before observation, but does not replay missed observations after a hard crash. Kanban never automatically resends paid work to reconcile a stale card. See Agent integration for state, failure and pause semantics.