Skip to content

About

A small FastAPI service exposing selected Craft operations as a generated OpenAPI 3.1 HTTP API. It is suitable for Open WebUI tool servers and other OpenAPI consumers.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

24 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Craft OpenAPI wrapper

Caution

This repo is purely vibe-coded. If you somehow find it, use at your own risk.

A small FastAPI service exposing selected Craft Space, Multi-Document, and Daily Notes operations as a generated OpenAPI 3.1 HTTP API. It is suitable for Open WebUI tool servers and other OpenAPI consumers.

Configure any combination of Space, Multi-Document, and Daily Notes. Each adapter uses its own server-configured Craft connection; it never falls back to another connection. Legacy mode uses one shared bearer token; optional experimental profiles use separate credentials and restricted write targets. The Craft client does not depend on FastAPI or Open WebUI.

For project context and agent handoff, start with PROJECT.md. Contributor instructions are in AGENTS.md.

Contents


Installation

Clone the repository and run the commands below from its root. Choose Python, Docker Compose, or Portainer according to how you want to manage the process. Supply connection secrets and the network name in your deployment configuration.

Python / local development

Requires Python 3.12+ and uv. The project defaults to Python 3.12; uv can install that interpreter if needed.

uv sync --locked --dev
cp .env.example .env

Edit .env with at least one Craft connection URL and a separately generated wrapper token. Use CRAFT_SPACE_BASE_URL for Space, CRAFT_DOCUMENTS_BASE_URL for Multi-Document, and CRAFT_DAILY_BASE_URL for Daily Notes; configure any one, pair, or all three. Remove unused connection settings entirely, including their placeholder values; blank supplied URLs are invalid. .env is ignored by Git and excluded from Docker. No credentials are included in the schema.

uv run --locked uvicorn craft_wrapper.main:create_app --factory --reload --no-access-log

The default listener is 127.0.0.1:8000. Generic Uvicorn access logging is disabled because it includes raw query strings; the wrapper logs request IDs, operation IDs, timings, status, and upstream error status instead.

  • GET /health: public process liveness, {"status":"ok"}; does not call Craft or verify credentials.
  • GET /openapi.json: public generated schema, exposing only enabled operations.
  • /docs: local interactive Swagger documentation.

Example reads below call the configured live Craft connection. Export your wrapper token as WRAPPER_API_TOKEN in the shell; .env is read by the application, not automatically by your shell. Use a filtered list for large spaces.

curl http://127.0.0.1:8000/health
curl http://127.0.0.1:8000/openapi.json
curl -H "Authorization: Bearer $WRAPPER_API_TOKEN" \
  'http://127.0.0.1:8000/v1/space/documents?location=unsorted'
curl -H "Authorization: Bearer $WRAPPER_API_TOKEN" \
  'http://127.0.0.1:8000/v1/space/blocks/DOCUMENT_ID/markdown?maxDepth=1'

Docker Compose

Requires Docker Engine and the Docker Compose plugin. Copy .env.example to .env if you have not already done so, configure at least one connection URL and the wrapper token, and set WRAPPER_NETWORK to an existing Docker network also attached to your tool client. You can find that network in the client's container configuration. Omit unused optional variables instead of setting them to empty strings.

cp .env.example .env  # First-time setup only; do not overwrite an existing .env.
# Edit .env: credentials and WRAPPER_NETWORK=YOUR_SHARED_NETWORK.
docker compose up --build -d
docker compose ps
docker compose logs --tail=50 craft-wrapper

docker-compose.yml builds from the clone and tags the image craft-openapi-wrapper:local. Both Compose files set the container name to craft-wrapper, rather than a generated stack/service/instance name. This name must be unique on the Docker host; change it if deploying another instance. It publishes no host ports. A client on the shared network reaches http://craft-wrapper:8000; the schema is /openapi.json. Use one service with this name per shared network to avoid DNS alias collisions. Host-browser requests to localhost:8000 do not reach this deployment.

Portainer with a cloned repository and local image

This method targets Docker Standalone. Clone the repository on the Docker host managed by Portainer, and build there:

./scripts/build-image.sh

The script builds craft-openapi-wrapper:local using the repository as its context, even when invoked from another directory. It uses the Docker builder's default platform, so build on the intended host. You can supply a different tag with ./scripts/build-image.sh IMAGE_TAG; also change the image in the Portainer Compose file if you do.

In Portainer:

  1. Select the same Docker environment where you built the image.
  2. Open Stacks → Add stack → Web editor, and paste docker-compose.portainer.yml from the clone. This file uses the local image with pull_policy: never; it contains no build context or host mounts.
  3. Add WRAPPER_NETWORK and the legacy WRAPPER_API_TOKEN (or profile rules and credentials), plus at least one of CRAFT_SPACE_BASE_URL, CRAFT_DOCUMENTS_BASE_URL, or CRAFT_DAILY_BASE_URL. Any combination is supported; omit unused connection variables entirely. The network must already exist and also be attached to the tool client. Optionally set WRAPPER_ENABLED_OPERATIONS to read_only, full, or an explicit operation-ID list. You can also add timeout settings or WRAPPER_PUBLIC_URL; omit unused optional settings entirely.
  4. Keep any Re-pull image option disabled and deploy. Check the container's health and logs.

The clone's .env is not automatically read by Portainer. Enter variables in Portainer or use its Load variables from .env file feature. No credential file is mounted into the container. See Portainer's stack and environment-variable documentation.

The ordinary docker-compose.yml is for builds from the local clone. Pasting it into Portainer's editor does not give Portainer access to that clone: build.context: . refers to the stack's own working directory. Use the prebuilt-image file above for this installation method.

Optional stack update after building

On Portainer Business Edition, enable Create a stack webhook in the stack's Editor and copy its URL. Stack webhooks are available on non-Edge environments. The build host must reach Portainer and build on the Docker environment where the stack runs. See Portainer's stack webhook documentation.

Store the copied URL in the clone's ignored .portainer-webhook file:

cp .portainer-webhook.example .portainer-webhook
chmod 600 .portainer-webhook
# Edit .portainer-webhook: replace its entire line with the copied webhook URL.
./scripts/build-image.sh

Use the bare URL without query parameters. It grants deployment access; do not commit or share it. The script appends pullimage=false so Portainer uses the locally rebuilt image rather than pulling from a registry. Keep pull_policy: never in the stack. The webhook redeploys Portainer's saved stack definition; changes to the clone's Compose file still need to be copied into the stack editor.

Alternatively, export PORTAINER_WEBHOOK_URL in the build shell. It overrides the file; setting it to an empty string disables the webhook for that invocation. The build script does not load .env or require this setting in the application container. With neither setting, the script only builds, as before.

The optional hook requires curl. Configuration is checked before building. After a successful build, the script sends one POST with a 10-second connection timeout and a 60-second overall timeout. It does not follow redirects, retry, or print the secret URL or response body. A failed build never triggers the hook. A failed/rejected webhook makes the script exit nonzero, although the image has already been built. Check stack state before retrying an uncertain request.

Portainer returns before deployment completes. A successful script reports that the request was accepted; check the stack's deployment status, container image, health, and logs to confirm the update finished. Removing .portainer-webhook disables the saved hook. Community Edition installations can continue using the manual update procedure below.

Docker image without Compose

For a host-local listener:

./scripts/build-image.sh
docker run --rm --env-file .env -p 127.0.0.1:8000:8000 craft-openapi-wrapper:local

The Dockerfile uses Python 3.12 and the lockfile, installs no dev dependencies, runs as a non-root user, and checks /health. .dockerignore excludes credentials, docs, scripts, and local caches from the build context. Supply credentials at runtime. Startup and health make no Craft request. TLS/remote hosting and deployment of the tool client are outside this project.

Updating

Pull changes from your clone's configured remote with git pull --ff-only. Keep your existing .env or Portainer variables; review .env.example for new settings without copying it over your credentials.

For Python, run uv sync --locked --dev and restart Uvicorn.

For local Compose, rebuild and apply the changes:

git pull --ff-only
docker compose up --build -d
docker compose ps

For Portainer, rebuild on the same Docker host:

git pull --ff-only
./scripts/build-image.sh

With the optional webhook configured, the build script automatically requests a stack update after building. Check Portainer to confirm deployment completed. If docker-compose.portainer.yml changed, apply those changes to the stack editor as well; the webhook does not read your clone.

Without the webhook, open the stack in Portainer, copy any changes from docker-compose.portainer.yml into its editor, and Update the stack with re-pulling disabled. Ensure the container is recreated from the rebuilt image; if it is retained, use the container's Recreate action with pulling disabled. Restarting an existing container alone does not apply a rebuilt image. Check health and logs after recreation. A locally built image is not updated by Portainer's registry-pull or GitOps settings.

For a standalone docker run container, rebuild with the script, stop the old container, and run the replacement with the same environment and port/network settings.


Versioning

The current release version is 0.4.0, adding Daily Notes support alongside Space and Multi-Document. The working tree adds unreleased experimental profiles, Space task discovery, and restricted deletion; the expected next capability release is 0.5.0. Releases use Git tags such as v0.4.0 and are recorded in CHANGELOG.md. Minor releases add capabilities; patch releases fix defects. While the project is below 1.0, any breaking changes must be called out in the changelog. The /v1/space, /v1/documents, and /v1/daily prefixes identify the HTTP contract independently of the project release number.

OpenAPI info.version comes from the installed package version. All three standalone Open WebUI tools include the release version in the metadata header. Docker images carry org.opencontainers.image.version; craft-openapi-wrapper:local remains the mutable tag used by the Portainer stack. Check the running container's release with:

docker inspect craft-wrapper --format '{{ index .Config.Labels "org.opencontainers.image.version" }}'

For an exact source release, check out its Git tag before building. Updating the wrapper image and updating the installed Open WebUI Python tool remain separate steps.

Identify development builds

Local builds have a separate identifier such as 0.4.0-dev.a1b2c3d4e5f6. The suffix is the first 12 hexadecimal characters of a SHA-256 source fingerprint, not a release counter. It includes uncommitted source changes, stays stable across repeated builds, and excludes credentials and private configuration. The base number is the last prepared release; it does not promote these changes to 0.5.0.

The image build script records the wrapper's build ID and Git revision as labels. Compare the local image and running container to check whether a rebuild was deployed:

./scripts/build-id.sh wrapper
docker image inspect craft-openapi-wrapper:local --format '{{ index .Config.Labels "io.craft-wrapper.build-id" }}'
docker inspect craft-wrapper --format '{{ index .Config.Labels "io.craft-wrapper.build-id" }}'
docker inspect craft-wrapper --format '{{ index .Config.Labels "org.opencontainers.image.revision" }}'

The fingerprint covers src/**/*.py, pyproject.toml, uv.lock, Dockerfile, and .dockerignore. The revision identifies the checkout's HEAD; the fingerprint also distinguishes uncommitted code. Direct docker build/Compose builds without the script leave the build labels unknown. Older images have no build label.

Installed tools: update-tools.sh automatically stamps each selected tool's version from that tool's own source. Preview/apply output shows the planned version; --list shows installed manifest versions when available. In a fresh chat, call the adapter's get_capabilities function to read toolVersion, which identifies the loaded Python tool independently of wrapper capabilities. Open WebUI receives that version in the source header/manifest when saving. Open WebUI update handling

For manual installation/update, export stamped copies first:

./scripts/export-tools.sh

Paste the corresponding build/openwebui/craft_*_tool.py into the tool editor. This directory is ignored; tracked sources keep their release headers. Copying the tracked file directly shows only the release version. Tool and wrapper fingerprints are independent and need not match. These shell commands need no Python or uv; they use the system's sha256sum (Linux) or shasum (macOS). No tags or releases are created.

Prepare a release locally

Keep reviewed changes under ## Unreleased in CHANGELOG.md. Choose the next version explicitly, then run from the repository root:

./scripts/prepare-release.sh 0.4.0 --dry-run
./scripts/prepare-release.sh 0.4.0

0.4.0 is an example, not an automatic version choice. The command moves those notes into a dated release entry, leaves an empty Unreleased section, and updates pyproject.toml, the Docker label, all three tool headers, and current-version references in README/PROJECT. It runs uv lock and uv sync --locked --dev to align the lockfile and installed package metadata. The default date is the local calendar date; override it with --date YYYY-MM-DD.

The dry run validates the inputs and lists affected files without changing them or running lock/sync. Empty notes, inconsistent current metadata, an existing local release tag, or a non-increasing version fail preparation. If lock/sync or final validation fails, release files are restored; run uv sync --locked --dev to reconcile the environment.

Review git diff and run the project checks before committing and tagging. The command never commits, tags, pushes, builds images, or deploys. Ordinary commits can continue without preparing a release.

GitHub releases

The release workflow runs on pushed v* tags and accepts only vMAJOR.MINOR.PATCH, such as v0.3.0. Ordinary branch pushes do not publish releases. It verifies the tagged package, lockfile, Docker label, and tool versions present in that tag, then publishes a GitHub release using that tag's matching dated CHANGELOG.md entry. Missing/mismatched metadata or empty notes fail the run. Existing releases are left unchanged when rerun.

After preparing a release commit and running the checks:

git tag -a v0.3.0 -m "Release 0.3.0"  # Example: only after setting version 0.3.0.
git push origin main
git push origin v0.3.0

Commit and push the workflow before relying on automatic publishing. For an older tag such as v0.2.0, push the tag if needed, then select Actions → Release → Run workflow, use the default branch, and enter the existing tag. This manual path reads release metadata/notes from the tag without moving it. Manual workflows must exist on the default branch; see GitHub's workflow trigger documentation.

The workflow uses GitHub's provided token with contents: write; no personal access token or Craft/Portainer credentials are required. Actions must be enabled and repository policy must permit this permission. It creates a published release with notes, using gh release create --verify-tag. It does not bump versions, create tags, run the application tests, publish Docker images/packages, or trigger deployments. Run the release checks before tagging. Prerelease suffixes are not supported by this initial workflow.


Configuration

Environment variables override .env. Startup rejects missing credentials, placeholders, invalid URLs, non-positive/non-finite timeouts, and invalid allowlists without printing supplied values.

Variable Default / meaning
CRAFT_SPACE_BASE_URL Optional Space connection: https://connect.craft.do/links/SECRET/api/v1 URL; the link itself is a credential. Only this HTTPS host/path is accepted; redirects and ambient HTTP proxies are disabled.
CRAFT_DOCUMENTS_BASE_URL Optional Multi-Document secret-link URL with the same validation. At least one connection is required; omit unused connections.
CRAFT_DAILY_BASE_URL Optional Daily Notes secret-link URL with the same validation. Omit to disable this adapter.
WRAPPER_API_TOKEN Required in legacy mode; ignored when experimental profiles are enabled. Nonempty independent bearer token without whitespace.
CRAFT_TIMEOUT_SECONDS 30; overall upstream deadline and read/write/pool phase limits.
CRAFT_CONNECT_TIMEOUT_SECONDS 5; upstream connection limit, also bounded by the overall deadline.
WRAPPER_ENABLED_OPERATIONS full or omitted: all operations on configured adapters (25 Space, 17 Multi-Document, 25 Daily Notes; 67 combined in the working tree). read_only: their reads (11 Space, 7 Multi-Document, 11 Daily Notes; 29 combined). Also accepts a comma-separated list of exact operation IDs below. Empty/unknown entries and IDs for an unconfigured adapter are errors; duplicates are harmless.
WRAPPER_PUBLIC_URL Optional non-secret HTTP(S) origin, without a path/query/credentials; sets OpenAPI servers. Otherwise consumers use the schema origin.
WRAPPER_PROFILES_JSON Optional experimental profile rules. Omitted preserves legacy mode; see profile configuration.
WRAPPER_READ_ONLY_TOKEN, WRAPPER_PLANNER_TOKEN, WRAPPER_MIGRATION_TOKEN Separate credentials for enabled profiles; never place them in the rules JSON.

Use a preset for common configurations:

WRAPPER_ENABLED_OPERATIONS=read_only

read_only includes discovery, content search, structured blocks, Markdown, collection schemas, and collection items for the configured adapters. Space also includes folder discovery; Daily Notes includes native task listing. All mutations, including task deletion, are disabled. full enables all implemented v1 operations, including writes; it does not enable deferred Craft capabilities.

Preset names are case-insensitive; Read Only and read-only also work. Set one preset or an explicit list, rather than mixing presets and operation IDs. Explicit operation IDs remain case-sensitive. For a narrower selection:

WRAPPER_ENABLED_OPERATIONS=craft_space_list_documents,craft_space_get_block,craft_space_read_markdown

Selection takes effect at startup. Recreate the Docker container after changing its environment, or restart the Python process. Globally disabled operations are absent from routing/OpenAPI and return 404, including when another enabled method shares the path. In legacy mode the token covers every enabled adapter; a prefix alone does not restrict that token. Opt-in experimental profiles provide separate credentials, operation permissions, and write-target restrictions; profile-disallowed operations/targets return 403.


Operations

Reads/updates return 200; creates/insertion return 201. All responses are JSON, including rendered Markdown.

The unreleased working tree exposes 67 Craft operations (25 Space, 17 Multi-Document, 25 Daily Notes), or 29 reads combined. Public routes are listed below; profile mounts prepend /profiles/{profileId}. Capability discovery is infrastructure and is excluded from these counts. See experimental profiles for permissions, write context, and setup.

Space

All paths below start with /v1/space.

Method and path Stable operation ID Behavior
GET /connection craft_space_get_connection_info Authenticated timezone/clock/native app-link template; no upstream credentials.
GET /tasks/{taskId} craft_space_get_task Verified native task; maxDepth=1, fetchMetadata=true by default; missing timestamps remain unknown.
GET /tasks craft_space_list_tasks Required scope active/upcoming/inbox/logbook/document/all; documentId required only for document scope.
POST /tasks craft_space_add_task One {documentId,markdown,scheduleDate?,deadlineDate?} task in a verified document; approved root required in profile mode.
PATCH /tasks/{taskId} craft_space_update_task Owning documentId query required; selected Markdown/state/dates only, no clearing or movement.
DELETE /tasks/{taskId} craft_space_delete_task Owning documentId query and fresh native/structural membership; leaf tasks only; Python confirmation required.
DELETE /blocks/{blockId} craft_space_delete_block Verified leaf-text deletion; owning documentId query required in every mode.
DELETE /collections/{collectionId}/items/{itemId} craft_space_delete_collection_item Delete one row and nested content; verify membership and require Python confirmation.
GET /folders craft_space_list_folders Hierarchy and document counts, {items:[...]}.
GET /documents craft_space_list_documents IDs/titles, optional metadata, {items:[...]}. Filter by location or folderId (direct documents only).
GET /documents/search craft_space_search_documents Required plain query; ranked content snippets, including substring matches. Optional one of location, folderId, documentId; fetchBlocks=false.
POST /documents craft_space_create_document One empty document: {title, folderId? OR location?}. Defaults to Unsorted; only Unsorted/Templates/folder destinations. Returns one document.
GET /blocks/{blockId} craft_space_get_block Structured root with nested blocks and IDs; maxDepth=1.
GET /blocks/{blockId}/markdown craft_space_read_markdown {blockId,markdown} using Craft rendering; maxDepth=1.
POST /blocks/{pageId}/content craft_space_insert_markdown {markdown,position?:"start"|"end"}; default end; returns inserted {items:[...]}.
PATCH /blocks/{blockId} craft_space_update_block_markdown {markdown} replaces one text block's Markdown; other fields preserved.
GET /collections craft_space_list_collections Existing collections, optionally filtered by one documentId.
GET /collections/{collectionId}/schema craft_space_get_collection_schema Native editable schema representation, with property keys/types/options.
GET /collections/{collectionId}/items craft_space_list_collection_items All items; maxDepth=0 defaults to properties without nested content.
POST /collections/{collectionId}/items craft_space_add_collection_item One {title,properties?:{key:"string"}} item.
PATCH /collections/{collectionId}/items/{itemId} craft_space_update_collection_item_properties Nonempty {properties:{key:"string"}}; omitted keys preserved.
PATCH /collections/{collectionId}/items/{itemId}/title craft_space_update_collection_item_title Rename with strict {title}; schema selects the native headline key, other fields preserved.
POST /collections/{collectionId}/items/{itemId}/content craft_space_insert_collection_item_markdown Append/prepend {markdown,position?} to a verified entry body.
PATCH /collections/{collectionId}/items/{itemId}/blocks/{blockId} craft_space_update_collection_item_block_markdown Edit {markdown} on one verified body text block.
DELETE /collections/{collectionId}/items/{itemId}/blocks/{blockId} craft_space_delete_collection_item_block Confirmed leaf-text deletion; entry root and nested collections protected.

Use the id returned by document discovery as the API root block ID, or documentId returned by content search. clickableLink is a navigation link: its embedded documentId can differ from the API ID and return 404 if used in an API request. Do not extract IDs from it for content reads or writes. A collection block's id can also be its collection ID; block and collection IDs are not disjoint namespaces. There is no separate document-detail endpoint. Creation does not implicitly insert content: call insertion separately with the returned ID. Insertions may create multiple blocks.

Both document list and search support createdDateGte/Lte, lastModifiedDateGte/Lte, and dailyNoteDateGte/Lte. Dates are real YYYY-MM-DD calendar dates or today/tomorrow/yesterday; relative dates pass to Craft unchanged. Reversed absolute ranges and conflicting filters are rejected. Location values for reads are unsorted, trash, templates, and daily_notes. IDs are opaque nonempty strings; URL path dot segments ./.. cannot be used as collection IDs.

Document listing requires location=daily_notes with either daily-note date bound; missing or incompatible scopes return 422. Live Craft listing otherwise returned an empty result despite matching daily notes. Document search accepts these date bounds without requiring that location. Listing by folderId returns direct documents only in observed Craft responses, contrary to the upstream documentation; query each descendant folder separately when needed. Folder-scoped search includes descendants. List ordering is unspecified; sort returned data client-side, requesting metadata when sorting by creation or modification dates.

Search terms can match inside words: end can match frontend, for example. Responses can contain multiple hits for one document. The upstream documentation describes a top-20 limit, but live responses exceeded 20 hits and 20 distinct documents; the wrapper preserves the returned results and makes no fixed result-count or completeness guarantee.

Depth 0 reads only the requested root (or collection properties), positive values limit descendants, and -1 requests all descendants. Finite depth is intentionally incomplete. Structured block reads preserve contentPreviewMd and itemsPreviewMd when Craft supplies them. Collection blocks can include rows under items, with each row's properties and nested content; use list_collection_items to read rows directly. Craft Markdown tags and link/scope markers are preserved.

Structured block/note reads accept fetchMetadata=false by default. Set it to true to request creation/modification timestamps. Recursive responses now preserve native listStyle and taskInfo state/dates. Task-detail reads verify actual task markers and the echoed ID, default metadata to true, and return 404 for a non-task block; Markdown checkbox text alone is not proof. Metadata omits authors, comments, and navigation links. Missing timestamps are unknown; the wrapper does not infer age from list order. Connection reads return space identity, timezone/time, UTC time, and the verified craftdocs block-link template, omitting space names and credentials. Live read-only Space checks passed; Daily variants are mock-tested because the local Daily connection was absent. Recurrence/location enrichment beyond existing task lists, movement, and template creation are not implemented by this checkpoint.

The implemented lists have no documented pagination; the wrapper adds no cursors, limits, or totals. Search is relevance-limited, not exhaustive. Prefer filters and finite depth: incoming bodies are capped at 1 MiB, upstream responses at 8 MiB, including streamed data. Oversized results produce an error instead of truncation.

Collection reads allow dynamic JSON property values. Untitled rows can omit title, and properties can be empty or omitted. Writes accept strings only and use keys/options discovered from the schema. Craft validates whether a string is suitable for that property's type. Relations, JSON numeric/boolean/complex writes, and null clearing are unsupported. Rename a row through the separate title-update operation; the properties-only operation remains unchanged. Stored views are not executed. A populated Date property was live-verified as a YYYY-MM-DD string, preserved by the wrapper. Existing-row Date updates using this string format were also live-verified with read-back and restoration of the original value. Other date representations remain unverified.

Collection entries have page-like bodies. Read one with get_block or read_markdown, using itemId as blockId and maxDepth=-1; finite depths return incomplete content/previews. Body insertion, single text-block updates, and confirmed leaf deletion use collection/item-scoped paths. Planner requires the owning collection as an approved target; fresh row membership and full structural checks run in all modes. Roots, nested collections/items, linked/property IDs, media and incomplete/ambiguous verification are protected. Title and regular properties remain separate from body edits; no whole-body replacement or transactional multi-call workflow is added.

Each collection-item read resolves the current schema before fetching rows and exposes its headline as public title, including named columns. Creation and mutation responses use the same mapping. Missing default-title metadata falls back to the documented title key; empty keys and keys that conflict with item-envelope fields fail closed. Schema lookups are uncached, consume Craft budgets, and are reused within a single write request for validation/membership/mapping. Row reads bound schema plus data retrieval by one upstream deadline; writes retain the combined verification/submission deadline. Rename inputs cannot replace the schema, update regular properties, or clear the title.

Schema option lists accept the documented string labels and observed {name,color?} objects, preserving the supplied representation. Title-property metadata (contentPropDetails) is optional because Craft may omit it. Property type strings such as the documented select and observed singleSelect are preserved without normalization. Single-select item values can be strings; multi-select values can be arrays. Both read shapes are preserved, while array writes remain unsupported. Existing row values are not a substitute for a schema: they do not reveal unused options or the full property contract.

Multi-Document

All paths below start with /v1/documents. These routes use only CRAFT_DOCUMENTS_BASE_URL, preserving the documents exposed by that connection. Craft filters links and relations; returned invalid:out_of_scope markers are preserved. Neither include/exclude filters nor resource IDs expand access.

Method and path Stable operation ID
GET /documents craft_documents_list_documents
GET /documents/search craft_documents_search_documents
GET /blocks/{blockId} craft_documents_get_block
GET /blocks/{blockId}/markdown craft_documents_read_markdown
POST /blocks/{pageId}/content craft_documents_insert_markdown
PATCH /blocks/{blockId} craft_documents_update_block_markdown
GET /collections craft_documents_list_collections
GET /collections/{collectionId}/schema craft_documents_get_collection_schema
GET /collections/{collectionId}/items craft_documents_list_collection_items
POST /collections/{collectionId}/items craft_documents_add_collection_item
PATCH /collections/{collectionId}/items/{itemId} craft_documents_update_collection_item_properties
PATCH /collections/{collectionId}/items/{itemId}/title craft_documents_update_collection_item_title
POST /collections/{collectionId}/items/{itemId}/content craft_documents_insert_collection_item_markdown
PATCH /collections/{collectionId}/items/{itemId}/blocks/{blockId} craft_documents_update_collection_item_block_markdown
DELETE /collections/{collectionId}/items/{itemId}/blocks/{blockId} craft_documents_delete_collection_item_block
DELETE /blocks/{blockId} craft_documents_delete_block
DELETE /collections/{collectionId}/items/{itemId} craft_documents_delete_collection_item

Document listing accepts only fetchMetadata=false; entries include id, title, and isDeleted. Deleted entries remain visible. Optional metadata includes createdAt, lastModifiedAt, and clickableLink. Use id for API reads, not an ID extracted from the navigation link.

Search accepts required query, optional documentId, documentFilterMode=include|exclude, and fetchBlocks=false. Collection discovery accepts optional documentId and the same filter mode. Supplying an ID defaults to include; supplying a mode without an ID returns 422. A singular public documentId maps to upstream documentIds. Search is relevance-limited (Craft documents top 20), not an exhaustive inventory; the wrapper preserves returned results without truncating them or inventing pagination.

Space-specific location, folder, and date filters are rejected. Multi-Document has no wrapper folder or document-creation route. Block depth defaults to 1, collection-item depth to 0, and -1 requests all descendants. Markdown insertion and block updates use the same request bodies as Space. Collection writes remain singleton, string-valued operations; complex reads and scope markers are preserved.

For a Multi-Document-only instance, remove CRAFT_SPACE_BASE_URL and set:

CRAFT_DOCUMENTS_BASE_URL=https://connect.craft.do/links/REPLACE_ME/api/v1
WRAPPER_API_TOKEN=REPLACE_ME
WRAPPER_ENABLED_OPERATIONS=read_only

Replace both placeholders. For combined access, configure both connection URLs instead. Example authenticated reads:

curl -H "Authorization: Bearer $WRAPPER_API_TOKEN" \
  'http://127.0.0.1:8000/v1/documents/documents?fetchMetadata=true'
curl -H "Authorization: Bearer $WRAPPER_API_TOKEN" \
  'http://127.0.0.1:8000/v1/documents/documents/search?query=example&documentId=DOCUMENT_ID&documentFilterMode=exclude'

Daily Notes

All paths below start with /v1/daily and use only CRAFT_DAILY_BASE_URL. Craft enforces scope across daily notes, the task inbox, and task logbook; out-of-scope links/relations remain filtered. Date-based calls pass today, tomorrow, and yesterday unchanged to Craft; the wrapper never applies its host timezone. Calendar dates must be valid YYYY-MM-DD; reversed absolute ranges are rejected.

Method/path Stable operation ID Behavior
GET /notes craft_daily_get_note Structured root by date=today; maxDepth=1. Returns editable IDs and hierarchy.
GET /notes/markdown craft_daily_read_note_markdown Rendered note with the same date/depth defaults; JSON {date, markdown} retains the requested selector.
POST /notes/content craft_daily_insert_note_markdown Insert {markdown, position} by query date=today; position defaults to end, or use start.
GET /blocks/{blockId} craft_daily_get_block Structured content by opaque ID; maxDepth=1.
GET /blocks/{blockId}/markdown craft_daily_read_markdown Rendered content by ID; JSON {blockId, markdown}, maxDepth=1.
POST /blocks/{pageId}/content craft_daily_insert_markdown Insert {markdown, position} into a specific page.
PATCH /blocks/{blockId} craft_daily_update_block_markdown Update one text block with {markdown}; empty Markdown is allowed.
GET /notes/search craft_daily_search_notes Required plain query; optional startDate, endDate, fetchBlocks=false.
GET /collections craft_daily_list_collections Discover collections with optional startDate/endDate; summaries identify dailyNoteDate.
GET /collections/{collectionId}/schema craft_daily_get_collection_schema Inspect property keys/types/options before writes.
GET /collections/{collectionId}/items craft_daily_list_collection_items Read rows with dynamic JSON properties; maxDepth=0.
POST /collections/{collectionId}/items craft_daily_add_collection_item Add one row with {title, properties?}; property values must be strings.
PATCH /collections/{collectionId}/items/{itemId} craft_daily_update_collection_item_properties Update a nonempty string-valued {properties} mapping.
PATCH /collections/{collectionId}/items/{itemId}/title craft_daily_update_collection_item_title Rename with strict {title}; schema selects the native headline key, other fields preserved.
POST /collections/{collectionId}/items/{itemId}/content craft_daily_insert_collection_item_markdown Append/prepend {markdown,position?} to a verified entry body.
PATCH /collections/{collectionId}/items/{itemId}/blocks/{blockId} craft_daily_update_collection_item_block_markdown Edit {markdown} on one verified body text block.
DELETE /collections/{collectionId}/items/{itemId}/blocks/{blockId} craft_daily_delete_collection_item_block Confirmed leaf-text deletion; entry root and nested collections protected.
GET /connection craft_daily_get_connection_info Authenticated timezone/clock/native app-link template; no upstream credentials.
GET /tasks/{taskId} craft_daily_get_task Verified native task; maxDepth=1, fetchMetadata=true by default; missing timestamps remain unknown.
GET /tasks craft_daily_list_tasks Required scope=active|upcoming|inbox|logbook.
POST /tasks craft_daily_add_task Create one native task, defaulting to the inbox.
PATCH /tasks/{taskId} craft_daily_update_task Update selected content, state, schedule, or deadline fields.
DELETE /tasks/{taskId} craft_daily_delete_task Delete one native task; return {id} with status 200.
DELETE /blocks/{blockId} craft_daily_delete_block Delete one verified leaf text block; owning date context required.
DELETE /collections/{collectionId}/items/{itemId} craft_daily_delete_collection_item Delete one row and its nested content; verify collection membership.

Date reads send only upstream date; ID reads send only id. Use the root ID from get_note and returned child IDs for exact page/block targeting. Date-based insertion chooses Craft's most recently updated note when duplicate daily notes share a date; use a specific pageId to target another copy. Finite depth is deliberately incomplete; -1 requests all descendants. Missing notes return Craft's error without a wrapper-generated document or implicit read-side insertion.

Daily-note search returns relevance-ranked snippets with dailyNoteDate, markdown, blockIds, and optional matched blocks; Craft documents a top-20 limit. This is not a daily-note inventory. No document-listing endpoint, regex, or pagination is exposed; the upstream reference's discovery hint does not establish a /documents contract for this connection. Space-specific folder/location/document filters are rejected.

Task creation accepts markdown, target=inbox|daily_note (default inbox), optional date, scheduleDate, and deadlineDate. date is valid only for daily_note and defaults to today for that target. For example:

{"markdown":"Review example","target":"daily_note","date":"today","scheduleDate":"tomorrow"}

Task updates accept at least one of markdown, state=todo|done|canceled, scheduleDate, or deadlineDate. Omitted fields are preserved; explicit nulls, date clearing, nested taskInfo write inputs, and task movement are rejected. The client translates the flat write fields into Craft's taskInfo structure. Responses can be partial, so only id is required; returned state strings are preserved.

active and upcoming classify open tasks by schedule date when present, otherwise deadline date. inbox selects the task inbox; logbook selects completed/canceled tasks. Marking tasks done/canceled moves them to Craft's logbook. Space-only all/document scopes are rejected. Native tasks differ from collection rows, and task dates do not create timed reminders. Deletion removes a task rather than completing it; no rollback is promised. Lost or malformed write/deletion responses report outcomeUnknown; inspect state before repeating.

Example reads for a Daily Notes connection:

curl -H "Authorization: Bearer $WRAPPER_API_TOKEN" \
  'http://127.0.0.1:8000/v1/daily/notes?date=today&maxDepth=1'
curl -H "Authorization: Bearer $WRAPPER_API_TOKEN" \
  'http://127.0.0.1:8000/v1/daily/tasks?scope=inbox'

Errors and write outcomes

Every error has an error object containing code, message, requestId, and optional upstreamStatus, upstreamCode, retryAfterSeconds, outcomeUnknown, or validation details:[{field,message}]. X-Request-Id matches the envelope.

Status Code / meaning
400 craft_rejected_request: upstream 400/422 or an unsafe path identifier.
401 unauthorized: missing/invalid wrapper token.
403 permission_denied or profile_expired for experimental profiles.
404 craft_not_found or not_found: missing Craft resource or wrapper route.
409 craft_conflict.
413 request_too_large.
422 validation_error: invalid body/query; input values are not echoed.
429 craft_rate_limited; valid upstream Retry-After is returned as seconds.
500 internal_error; exception strings/tracebacks are not returned.
502 craft_access_error for upstream 401/403; craft_response_too_large; or craft_upstream_error for other failures, redirects, invalid JSON/content types, or unexpected response shapes.
503 craft_unavailable: upstream connection/network error.
504 craft_timeout.

There are no automatic retries on any operation. Craft budgets are shared across connections, clients, MCP, and public-IP users; a local limiter cannot reserve them. Safe upstream codes/messages are bounded and redacted; unrecognized bodies receive a fixed message. No raw upstream body or secret URL is returned in an error.

Distinguish a 502 with an upstream failure and retry guidance from an unexpected response shape after a successful Craft response. A schema parsing mismatch is deterministic until the model or upstream shape changes; backoff does not repair it. For transient reads, callers can honor retryAfterSeconds before retrying. The wrapper does not assume every upstream 502 is throttling or convert it into 429.

A write failure after submission can leave its outcome uncertain, including malformed success responses. Such errors set outcomeUnknown=true. Inspect the target before retrying; the wrapper provides neither transactions nor rollback. Conservatively, upstream write rejection responses also carry this flag because the docs provide no atomicity guarantee.


Open WebUI

Workspace Python tool

Choose the standalone Space tool (25 Craft operations), Multi-Document tool (17), or Daily Notes tool (25). Each also has a capability-discovery function. Each explicitly calls this HTTP wrapper; none connects directly to Craft or requires the wrapper package inside Open WebUI. Their structures follow Open WebUI's tool development conventions.

  1. Run ./scripts/export-tools.sh. Open Workspace → Tools in Open WebUI and create a tool. Paste the entire corresponding stamped file from build/openwebui/ into the editor and save it. The metadata declares its HTTPX dependency; Pydantic is supplied by Open WebUI.
  2. Open the tool's Valves settings and configure the values below. The token field uses Open WebUI's password input convention; masking the input does not itself encrypt stored values. Restrict tool access to the intended users.
  3. Enable the tool in a chat with a model that supports tool calling. Native function calling is recommended. Ask Space to list folders, Multi-Document to list documents, or Daily Notes to read today’s note. Successful reads return 200; a missing daily note can return Craft’s 404.
Valve Value
WRAPPER_URL Wrapper origin reachable from Open WebUI, such as http://craft-wrapper:8000 on a shared Docker network. No adapter path suffix such as /v1/space, /v1/documents, or /v1/daily.
WRAPPER_API_TOKEN Legacy wrapper token or the selected profile's token, without Bearer . Never use the Craft connection URL here.
WRAPPER_PROFILE Empty for legacy mode, or read-only, planner, migration; see profile setup.
TIMEOUT_SECONDS Default 45, covering the wrapper's default 30-second upstream deadline plus network overhead.

The Space source was renamed from craft_wrapper_tool.py to craft_space_tool.py; existing installed tools keep working without changing their functions or Valves. Use the renamed source for subsequent installation or updates.

Each tool has named functions matching the wrapper's operation IDs. Query arguments go inside parameters; write payloads go inside body. IDs remain separate arguments. For example:

{
  "parameters": {
    "query": "example",
    "documentId": "example-document",
    "fetchBlocks": true
  }
}

These are arguments for craft_space_search_documents. A block update uses craft_space_update_block_markdown with {"blockId":"example-text-block","body":{"markdown":"Updated text"}}. Collection writes accept string-valued properties only; the wrapper validates dates, depths, scopes, and payloads.

Nested query/body keys are forwarded without silently removing unknown fields. For instance, craft_space_list_documents with {"parameters":{"documentId":"example-document"}} reaches the wrapper and returns 422, because that listing filter is unsupported. Open WebUI can still discard unsupported top-level arguments; put query fields inside parameters as documented.

Each result contains request (method, path, serialized query), statusCode, requestId, and the unchanged JSON response. This distinguishes a wrapper rejection from a tool connection/decoding failure. Authentication headers and write bodies are not included in request evidence. Wrapper error codes, retry guidance, and uncertain-write flags are preserved. The tool does not follow redirects or retry requests; failed writes after submission are marked uncertain. Results exceeding 9 MiB are rejected rather than truncated.

Each Python tool keeps a static function list: 26 for Space, 18 for Multi-Document, and 26 for Daily Notes, including one capability-discovery function each. Install adapters separately as needed. Global disabled operations return 404; profile denials return 403 on the server. Native Python tools require a live deletion confirmation dialog; unattended calls fail closed. See profiles for Valves and document/date write context. Choose this tool or OpenAPI registration for a chat to avoid duplicate operations.

To update manually, run ./scripts/export-tools.sh, replace the installed tool's code in the Open WebUI editor with its stamped copy in build/openwebui/, and save it. Alternatively, use the script below. Rebuilding the wrapper image does not update the separately installed tools. Automated tests cover mocked HTTP calls and calls through an in-process wrapper; the installed Open WebUI UI/model still needs the discovery smoke check above.

Update installed Python tools

Run scripts/update-tools.sh from the clone. It requires only curl and jq; on Debian/Raspberry Pi OS, install missing commands with sudo apt install curl jq. No Python, uv, or Docker runtime is needed for the updater. It updates existing tools from local repository sources; it does not create tools or fetch remote Python. Select IDs explicitly so Test and everyday tools can be updated separately. After pulling, deploy the matching wrapper first.

Configure this once: copy .openwebui-tools.env.example to .openwebui-tools.env, protect it, and edit its URL/token in your text editor. Keep an existing file when updating the repository.

cp -n .openwebui-tools.env.example .openwebui-tools.env
chmod 600 .openwebui-tools.env
nano .openwebui-tools.env
./scripts/update-tools.sh --list

Use the listed IDs, not display names. Example Test-tool update:

./scripts/update-tools.sh --tool space=craft_space_test --tool daily=craft_daily_test --dry-run
./scripts/update-tools.sh --tool space=craft_space_test --tool daily=craft_daily_test --apply

The updater automatically reads .openwebui-tools.env from the repository root, including when launched from another directory. The file is ignored by Git and excluded from the Docker image. Set OWUI_URL to the Open WebUI base URL (including any subpath, without /api/v1) and OWUI_API_TOKEN to a write-capable Open WebUI token, distinct from wrapper/profile tokens. Exported OWUI_* environment variables take precedence; no token prompt is needed. The wrapper's .env is not loaded by this command.

Omitting --apply previews only. --tool documents=YOUR_TOOL_ID selects Multi-Document; repeat an adapter for multiple installed copies. Unchanged sources are skipped. The script preflights all selected tools, preserves names/descriptions, omits sharing grants and Valves from writes, and verifies saved source/metadata/sharing. Open WebUI regenerates manifest/function specs. Credentials are parsed as data, never executed as shell code; use plain or simply quoted KEY=value lines, with comments on separate lines. No private source, token, or response body is printed.

This contract was checked against Open WebUI v0.11.4's router and tool models. Saving executes Python on Open WebUI and can install declared dependencies, as a UI save does. Use trusted local source and protect the credential. Redirects and retries are disabled; OWUI_TIMEOUT_SECONDS defaults to 60. On an ambiguous save or verification failure, inspect the installed tool before retrying. Updates are sequential, can partially succeed, and are not atomic with concurrent editor changes. Start a fresh chat, check Valves and run a capability/read smoke test after updating; cross-worker reload and live deployment remain separate checks.

OpenAPI tool server

Register this as a backend/global OpenAPI tool server, using the wrapper origin and /openapi.json in legacy mode, or /profiles/{profileId}/openapi.json and its matching token in profile mode, with Authorization: Bearer <WRAPPER_API_TOKEN>. Restrict access to the intended user in Open WebUI. OpenAPI calls do not receive the Python tools' deletion dialogs; use the Python tools for this safeguard. Enable selected tools in the chat. The server-side allowlist remains authoritative even if a consumer cached an older schema.

Use an address reachable from the Open WebUI backend. When both services are containers on the same Docker network, use the wrapper container's service name and port. A backend container's localhost is that container; on Docker Desktop, host.docker.internal can reach a host listener bound to a reachable interface. A listener bound to 127.0.0.1 is intended for host-local access. CORS is not enabled because this integration uses backend calls.

Open WebUI can import the OpenAPI 3.x schema and call ordinary HTTP operations. Tool results are complete responses, and this wrapper does not depend on streaming or interactive confirmation events. See tool-server support and backend vs. browser integration. The running Open WebUI installation still needs its own integration smoke check; the test suite does not claim to have verified its UI or model.


Tests and checks

uv run --locked pytest -q
uv run --locked ruff check src tests integrations scripts
uv run --locked ruff format --check src tests integrations scripts
uvx basedpyright

pyproject.toml sets standard type checking for Pyright and basedpyright, using the project's .venv and Python 3.12. The editor and CLI use the same configuration; basedpyright's stricter annotation/style rules are not enabled by this preset. uvx runs the checker separately from application dependencies. See basedpyright configuration.

All tests use HTTPX MockTransport, fixture data, or an in-process ASGI app, with no live credentials or requests to Craft. They verify exact mappings for all 67 Craft routes across three adapters, discovery/editing, collection, and native task workflows, validation, auth, deadlines, size bounds, no retries, secret-safe errors/logs, allowlist enforcement, client shutdown, and OpenAPI validity.

Space response fixtures are extracted from the first response example for implemented operations in craft-docs/space-api-docs.md. Singleton mutation tests narrow example batches; focused error/workflow tests use additional synthetic data. Multi-Document tests use generic synthetic examples of documented responses, including deleted documents and scoped links. Daily Notes tests use generic synthetic fixtures for date-based content, search, collections, and partial task responses. Fixture generation never edits the source docs.

Verification observations

Direct Craft HTTP probes confirmed:

  • Collection schemas can return option objects and omit default-title metadata. Populated Date values were calendar-date strings.
  • A string-valued Date update persisted on read-back, and restoration preserved all original properties.
  • Default and named row headlines updated through their actual top-level schema keys. Returned row fields were restored exactly, with properties/nested content preserved. A hardcoded title key was invalid for the named column; the wrapper now maps the current headline key and exposes a separate title-update route. Space Test create/rename/read-back checks passed for both default and named headlines; each newly created test row was removed with confirmation and the original rows retained their properties/nested content.
  • Secret-link-only authentication worked. Select-property writes and additional authentication mechanisms were not verified.

Open WebUI execution reports and read-back checks cover Space collection/content planning, Space document-task and Daily inbox-task lifecycles, canceled/approved deletion, and unapproved-target denial. One uncertain Daily creation was found exactly once during inspection and was not repeated. The latency cause and visual rendering of the no-deletion status remain unconfirmed. See profile observations for the evidence and limits.

A direct HTTP probe created one disposable row, inserted/edited/read its body Markdown, deleted the new leaf block, and removed the probe row. Existing collection rows matched their prior returned state. The new scoped wrapper/tool body workflow is mock-tested; its installed-tool check remains pending.

Automated regression fixtures contain synthetic values; private IDs, credentials, and captured content are excluded.


Architecture

api/ owns HTTP models, validation, auth, error translation, and tool descriptions. craft/transport.py owns bounded asynchronous HTTP, decoding, deadlines, and sanitized failures. craft/space/client.py, craft/documents/client.py, and craft/daily/client.py own their upstream query names and adapter methods. craft/operations.py shares matching block/collection mappings and response parsing; adapter-specific document/date summaries, task models, and filters stay separate. config.py owns environment settings and operation selection.

All three local docs were compared before design. Space has 44 documented method/path pairs, Multi-Document 33, Daily Notes 36; 31 are shared. Overlap does not imply matching semantics:

  • Space includes daily-note access and tasks; it is not disjoint from the Daily Notes API.
  • Scoped connections filter links and relations; never fall back to a broader Space connection.
  • Single-document search selectors are blockId (Space), documentId (Multi-Document), and date (Daily Notes).
  • Collection discovery uses document filters, document include/exclude filters, or date ranges respectively.
  • Daily Notes has date defaults/search results and narrower task scopes; Space task all is not the union of active/upcoming/inbox/logbook.
  • Views are stored definitions, not executed queries. Reminders have conditional availability/ownership and cursor pagination; neither is exposed in v1.

All three adapters reuse transport/errors and matching block/collection helpers and models. Keep scope-specific discovery and filters in their adapters; share additional helpers only when contracts match. Do not introduce a universal Craft interface or capability framework. Existing Space operation IDs stay stable.

The unreleased Space adapter adds document-targeted native task creation, updates and leaf-task deletion. Space writes verify the approved root and native-task membership; inbox/daily-note writes remain on the Daily adapter. Space task movement and date clearing remain unsupported. Singleton collection-item deletion and protected leaf-text deletion are also available in the working tree. Broader deletion/movement operations, folder writes, collection creation/schema mutation/views, comments, reminders, uploads, and whiteboards remain deferred. Other documented gaps remain explicit: examples use inconsistent identifier terminology and collection-type representations; array query serialization and regex claims are ambiguous; list pagination and mutation atomicity are unspecified. The wrapper follows documented example field names, accepts single-value filters, preserves read type strings, and omits those uncertain capabilities.


Roadmap

Future additions are listed below. Implemented experimental permission profiles are documented separately; release metadata remains 0.4.0 until preparation.

Additional Craft operations

Target coverage of every currently documented operation across the Space, Multi-Document, and Daily Notes APIs. Daily Notes content, existing collection items, and native task operations are available in 0.4.0. Remaining additions include:

  • Extend Space task targets and movement beyond approved documents if needed; document-targeted creation/updates and leaf-task deletion are implemented in the working tree.
  • Document deletion, broader block deletion, and movement. Singleton collection-item deletion and leaf-text deletion are implemented in the working tree.
  • Space folder creation, deletion, and movement.
  • Collection creation and schema replacement; collection-view listing, creation, updates, deletion, and active-view selection. Views remain stored configuration, not executed queries.
  • Connection information and search within a document/page (GET /blocks/search), with each adapter’s documented selectors.
  • Comments, reminders (including their explicit cursor pagination), uploads, and whiteboard creation and element operations.

Implement each operation only for adapters that document it. Preserve connection isolation and scope markers; do not expose Daily Notes task behavior through Space implicitly. Keep existing paths and operation IDs stable, and update operation presets, generated OpenAPI, and the corresponding Open WebUI tools as capabilities are added.

Maintain the API coverage matrix against the local API references, recording the reference date, each adapter’s upstream method/path, wrapper operation IDs, supported parameters and request/response shapes, verification, and remaining limitations. Classify entries as implemented, partial, deferred, or not applicable. Refresh the references against Craft’s official documentation before implementation; new upstream operations require a new coverage review.

Track operation coverage separately from complete contract support. Existing endpoints intentionally limit block writes to Markdown-focused inputs, collection-property writes to strings, mutations to single targets, search filters, and collection-schema output formats. Evaluate missing documented inputs and representations explicitly; retain and document useful tool-oriented adaptations. Resolve ambiguous contracts through verification rather than inventing behavior.

Operation coverage is complete when every applicable documented method/path has a tested wrapper equivalent. Full contract support additionally requires verifying all documented input/output variants and resolving partial entries; endpoint presence alone is insufficient. Use mocked mapping, validation, scope-isolation, error, and OpenAPI tests, with separately authorized live checks for uncertain upstream behavior.

Permission-based routes for LLM tools

Experimental read-only, planner, and expiring copy-only migration profiles are implemented in the working tree. They share existing connections, expose separate schemas, bind credentials to profiles, and restrict writable collections/document roots. Python tools add capability discovery and fail-closed deletion dialogs. See profile setup and limitations. This is an application safeguard, not atomic upstream authorization or server-verified human approval.

Future work includes arbitrary named profiles, multiple named Multi-Document connections, upstream read isolation, and profile management. Preserve the global operation allowlist as an upper bound and existing paths/operation IDs. Never fall back to a broader connection.

The standalone Python tools still expose a predefined function list. Capability discovery reports permissions but does not hide unavailable functions. Evaluate generating reduced tools from a profile's OpenAPI during installation/update; avoid modifying Open WebUI internals or maintaining a second permission list. The server remains authoritative.

Open WebUI profile factory and management (evaluation)

Build the factory for existing profiles first: an authorized user selects a profile, enters a tool name, previews its document scope and enabled operations, and clicks Create tool. Generate a dedicated tool from a trusted base definition using the profile's OpenAPI schema; expose only its enabled operations and configure its URL and credential separately from the generated source. Support regenerating the tool when the base definition or profile changes.

Current upstream Open WebUI provides dynamic Valves dropdowns for profile selection and authenticated tool creation/update and Valves endpoints for saving tool instances. These establish the building blocks for a custom factory; verify compatibility with the installed Open WebUI version before implementation.

Add profile creation and management in a later step, including assigning a Craft Multi-Document connection and choosing permissions. This requires a wrapper administration API and persistent server-side configuration. Evaluate a user-triggered Open WebUI Action with an interactive interface; embedded interfaces are sandboxed and must not assume access to the Open WebUI login session. If direct integration is impractical, use a small wrapper-hosted management interface that can be opened from Open WebUI, avoiding configuration-file edits on the deployment host.

Keep profile administration separate from ordinary LLM-callable tools, with explicit administrative authorization and durable server-side configuration. Keep connection secrets and wrapper credentials out of model context, generated tool source, logs, and exported definitions. Ordinary tool requests must never supply arbitrary upstream URLs or create profiles. The wrapper remains responsible for enforcing document scope and operation permissions.

Acceptance checks should cover selecting an existing profile, creating a tool with the matching operation set, creating a profile through the management flow, and updating a generated tool. Verify that read-only tools cannot write and that credentials cannot cross profiles, including when two profiles share a Craft connection. Profile revocation must stop access even if a tool retains an older definition.

Reusable block and collection shapes (evaluation)

Evaluate named, versioned definitions for repeatable content structures, starting with a tasks collection. The aim is for creation through an LLM tool or a direct HTTP API call to use the same server-owned definition, defaults, and validation. Proposed task fields could include a title, status with explicit options, and a due date; settle the actual property keys, Craft types, and supported write values before defining the contract.

Keep block content structure, collection column schemas, and collection-item values distinct. A tasks collection is a collection of rows; it does not by itself establish native Craft task behavior. Verify the documented creation contracts and returned shapes before promising task integration or accepting additional property types.

Start with one concrete definition and evaluate how clients discover its shape and select its version when creating content. The API should apply that definition, and the Open WebUI tool should call the same API rather than recreate its rules. Acceptance checks should compare creation through both entry points and confirm consistent columns, options, defaults, and item validation. Collection creation is currently deferred; implementing this feature depends on adding the relevant creation operations and permissions.

Do not replace schemas on existing collections implicitly. Report incompatible shapes and require an explicit migration decision. Broader templates, dynamic per-collection OpenAPI generation, and a general schema registry are outside the initial evaluation.

About

A small FastAPI service exposing selected Craft operations as a generated OpenAPI 3.1 HTTP API. It is suitable for Open WebUI tool servers and other OpenAPI consumers.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages