Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@

All notable changes to **flycanon** are documented here.

## [26.10.4] - 2026-10-14

### Fixed

- Reject workspace creation when the body `id` differs from `X-Workspace-Id`. The service returns `400 workspace_scope_mismatch` before any storage or lifecycle event write, rather than exposing a PostgreSQL RLS failure as HTTP 500. Matching requests retain their existing `201` response.
- Correct the workspace reference: lookup returns `workspace_not_found`, and the list endpoint accepts no filter or pagination arguments. A separate configured admin connection permits tenant-wide listing; otherwise the request connection's RLS scope controls visibility.

### Distribution

- Align the server and Python/Java SDK versions at `26.10.4`. SDK methods, wire schemas, dependency pins and RLS policies are unchanged. Configure the SDK's workspace header to match the workspace being created; the published Python SDK `26.10.3` remains compatible with the corrected server.
- Native local checks cover creation, RFC 7807 errors, SDK propagation, foreign-scope visibility and credential denial. Lifecycle event delivery remains best-effort after a successful workspace write; this patch adds no delivery guarantee or tenant-bound platform-key grant.

## [26.10.3] - 2026-10-14

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ over whole documents with citations — all behind a single HTTP service.
[![agentic](https://img.shields.io/badge/genai-fireflyframework--agentic-purple)](https://github.com/fireflyframework/fireflyframework-agentic)
[![OpenAPI](https://img.shields.io/badge/api-openapi%203.1-green)](docs/api-reference.md)
[![vector store](https://img.shields.io/badge/vector--store-pgvector%20%7C%20qdrant%20%7C%20chroma-336791)](docs/architecture.md#retrieval-backend-pluggable-dense-store)
[![Version](https://img.shields.io/badge/version-26.10.3-green.svg)](#)
[![Version](https://img.shields.io/badge/version-26.10.4-green.svg)](#)
[![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE)

</div>
Expand Down
27 changes: 16 additions & 11 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,23 +164,28 @@ The `/api/v1/ingest-jobs/*` surface parallels flyradar's

## Workspaces

CRUD over the `canon_workspaces` table. All six endpoints
require the standard tenant headers; list / get / update / close /
purge scope by `X-Tenant-Id`.
CRUD over the `canon_workspaces` table. All six endpoints require
the standard tenant and workspace headers. For creation, the body
`id` must equal `X-Workspace-Id`. A mismatch returns
`400 workspace_scope_mismatch` before any row or event is written.
Production RLS restricts row visibility to the tenant and workspace
in those headers; the tenant-wide list exception is described below.

| Method | Path | Description |
|--------|------|-------------|
| `POST` | `/api/v1/workspaces` | Create. Body: `WorkspaceCreate`. Returns `WorkspaceSummary`. |
| `GET` | `/api/v1/workspaces` | List within tenant. Query: `status`, `limit`, `offset`. |
| `GET` | `/api/v1/workspaces/{id}` | Fetch one. 404 → `resource_not_found`. |
| `POST` | `/api/v1/workspaces` | Create. Body: `WorkspaceCreate`; `id` must match `X-Workspace-Id`. Returns `201` with `WorkspaceSpec`. |
| `GET` | `/api/v1/workspaces` | List visible workspaces within the tenant, newest first. No filter or pagination arguments. |
| `GET` | `/api/v1/workspaces/{id}` | Fetch one. 404 → `workspace_not_found`. |
| `PATCH` | `/api/v1/workspaces/{id}` | Sparse update. Body: `WorkspaceUpdate`. |
| `POST` | `/api/v1/workspaces/{id}:close` | Close (idempotent; sets `status=closed` + `closed_at`). Data stays. |
| `POST` | `/api/v1/workspaces/{id}:purge` | Erase everything the workspace holds, then close it: every source through the full removal pipeline (vectors, chunks, stored originals, audit + event each), then knowledge items / versions / citations / relations, candidates, conversations / turns, ingest jobs / events and cost events in one transaction; emits `WorkspaceDeleted`; audit rows are kept and a `workspace.purged` row records the counts. `X-Workspace-Id` must equal the path id (`400 workspace_scope_mismatch`). Returns `WorkspacePurgeResult` -- every counter is what this call erased, `originals_deleted` only objects actually found in the store, `closed` only when this call closed the row; a repeat is all zeros, `closed: false`, no event, no audit row. (26.7.1) |

`GET /api/v1/workspaces` is a tenant-wide read and runs on the
BYPASSRLS engine named by `FLYCANON_ADMIN_DATABASE_URL`; under the
production `flycanon_app` role without that setting it can only return
the workspace named in the caller's own `X-Workspace-Id` header.
With a separate BYPASSRLS connection configured through
`FLYCANON_ADMIN_DATABASE_URL`, `GET /api/v1/workspaces` lists the
tenant's workspaces on that connection. Otherwise it uses the request
connection; under the production non-bypass role, RLS limits the
result to the workspace named in `X-Workspace-Id`. Both tenant and
workspace headers remain required in either configuration.

## Billing

Expand Down Expand Up @@ -351,7 +356,7 @@ Field-level validation details land under `errors[]`.
| 400 | `url_fetch_forbidden_host` / `url_fetch_unsupported_scheme` | `uri` targets a refused host / a non-http scheme. |
| 413 | `url_fetch_too_large` | The origin's document exceeds `FLYCANON_MAX_BYTES`. |
| 502 | `url_fetch_http_error` / `url_fetch_failed` / `url_fetch_too_many_redirects` | The origin answered 4xx/5xx, could not be reached, or redirected more than five times. |
| 400 | `workspace_scope_mismatch` | `POST /workspaces/{id}:purge` with an `X-Workspace-Id` that differs from the path id. |
| 400 | `workspace_scope_mismatch` | Workspace creation with a body `id` different from `X-Workspace-Id`, or `POST /workspaces/{id}:purge` with a different path id. |
| 404 | `source_not_found` | `DELETE`/`PUT` on an unknown source id (either tier). |
| 400 | `invalid_request` | Pydantic validation, missing-field `ValueError` raised inside a controller helper. |
| 404 | `resource_not_found` | Generic missing resource (knowledge item, source, conversation, candidate, job, workspace). |
Expand Down
6 changes: 3 additions & 3 deletions docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ For a host installation using the released wheel, follow [Install the released s

```bash
docker pull ghcr.io/fireflyframework/flycanon:latest # latest main
docker pull ghcr.io/fireflyframework/flycanon:26.10.3 # specific release
docker pull ghcr.io/fireflyframework/flycanon:26.10.4 # specific release
docker pull ghcr.io/fireflyframework/flycanon:26.10 # latest 26.10.x
```

Expand Down Expand Up @@ -681,12 +681,12 @@ The recommended upgrade dance:

```bash
# 1. Pull the new image
docker pull ghcr.io/fireflyframework/flycanon:26.10.3
docker pull ghcr.io/fireflyframework/flycanon:26.10.4

# 2. Run migrations against the live database
docker run --rm \
--env-file .env \
ghcr.io/fireflyframework/flycanon:26.10.3 migrate
ghcr.io/fireflyframework/flycanon:26.10.4 migrate

# 3. Roll the API + worker deployments
docker compose --env-file .env up -d
Expand Down
7 changes: 6 additions & 1 deletion docs/operations-runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,14 @@ mutation emits an event on `canon.workspaces.v1` so consumers

### Create a workspace

Set `X-Workspace-Id` to the same value as the body `id`. A mismatch
returns `400 workspace_scope_mismatch` before any row or event is
written. This check preserves the production RLS scope; it does not
grant access to a tenant or workspace.

```bash
curl -X POST https://canon.example.com/api/v1/workspaces \
-H "Authorization: Bearer $OPERATOR_JWT" \
-H "X-API-Key: $FLYCANON_API_KEY" \
-H "X-Tenant-Id: acme" \
-H "X-Workspace-Id: ws-prod" \
-H "Content-Type: application/json" \
Expand Down
14 changes: 7 additions & 7 deletions docs/released-server-install.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ Use the released source archive for the operator configuration and migration fil

## Download and verify

Download `flycanon-26.10.3-py3-none-any.whl`, `flycanon-26.10.3.tar.gz`, `runtime-requirements-26.10.3.txt`, and `SHA256SUMS` from the [26.10.3 release](https://github.com/fireflyframework/flycanon/releases/tag/v26.10.3). Verify the downloaded files against their matching checksum entries before extracting or installing them.
Download `flycanon-26.10.4-py3-none-any.whl`, `flycanon-26.10.4.tar.gz`, `runtime-requirements-26.10.4.txt`, and `SHA256SUMS` from the [26.10.4 release](https://github.com/fireflyframework/flycanon/releases/tag/v26.10.4). Verify the downloaded files against their matching checksum entries before extracting or installing them.

```bash
tar -xzf flycanon-26.10.3.tar.gz
cd flycanon-26.10.3
tar -xzf flycanon-26.10.4.tar.gz
cd flycanon-26.10.4
uv venv --python 3.13 .venv
uv pip install --python .venv/bin/python -r ../runtime-requirements-26.10.3.txt
uv pip install --python .venv/bin/python -r ../runtime-requirements-26.10.4.txt
```

The requirements file selects the released server wheel and these canonical framework references:
Expand All @@ -26,7 +26,7 @@ The equivalent installation without a requirements file is:

```bash
uv pip install --python .venv/bin/python \
'flycanon @ https://github.com/fireflyframework/flycanon/releases/download/v26.10.3/flycanon-26.10.3-py3-none-any.whl' \
'flycanon @ https://github.com/fireflyframework/flycanon/releases/download/v26.10.4/flycanon-26.10.4-py3-none-any.whl' \
'pyfly @ git+https://github.com/fireflyframework/fireflyframework-pyfly.git@v26.09.04' \
'fireflyframework-agentic @ https://github.com/fireflyframework/fireflyframework-agentic/releases/download/v26.10.1/fireflyframework_agentic-26.10.1-py3-none-any.whl'
```
Expand All @@ -39,11 +39,11 @@ This release retains pyfly `v26.09.04` and the existing PostgreSQL EDA outbox. A

## Configure the runtime

Keep `pyfly.yaml`, `alembic.ini`, `migrations/`, and `env_template` from the source archive in the working directory. The wheel does not supply the root-level `pyfly.yaml`. The release also provides a byte-identical `pyfly-26.10.3.yaml` copy for operators who already have a migration scaffold; install it as `pyfly.yaml` in the runtime working directory.
Keep `pyfly.yaml`, `alembic.ini`, `migrations/`, and `env_template` from the source archive in the working directory. The wheel does not supply the root-level `pyfly.yaml`. The release also provides a byte-identical `pyfly-26.10.4.yaml` copy for operators who already have a migration scaffold; install it as `pyfly.yaml` in the runtime working directory.

The configuration selects the PostgreSQL event adapter and separate API/worker consumer groups. Starting without it can activate framework defaults, including an attempted Kafka connection to `localhost:9092` and a separate management listener. Do not rely on those defaults for the released service.

Set the database URL, embedding model and dimensions, provider credentials, platform API keys, object-storage location, and optional Redis settings for your environment. Use [`env_template`](https://github.com/fireflyframework/flycanon/blob/v26.10.3/env_template) and the [deployment guide](https://github.com/fireflyframework/flycanon/blob/v26.10.3/docs/deployment.md) for the exact settings. Store real secrets outside published configuration and supply them through your operator's environment or secret manager.
Set the database URL, embedding model and dimensions, provider credentials, platform API keys, object-storage location, and optional Redis settings for your environment. Use [`env_template`](https://github.com/fireflyframework/flycanon/blob/v26.10.4/env_template) and the [deployment guide](https://github.com/fireflyframework/flycanon/blob/v26.10.4/docs/deployment.md) for the exact settings. Store real secrets outside published configuration and supply them through your operator's environment or secret manager.

Provision the PostgreSQL database, required extensions and roles using the deployment guide. Apply migrations as a separate operator step before serving a new database:

Expand Down
4 changes: 2 additions & 2 deletions openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -5103,7 +5103,7 @@
"url": "https://github.com/fireflyframework/flycanon/blob/main/LICENSE"
},
"title": "flycanon",
"version": "26.10.3"
"version": "26.10.4"
},
"openapi": "3.1.0",
"paths": {
Expand Down Expand Up @@ -8719,7 +8719,7 @@
]
},
"post": {
"description": "The body carries the caller-chosen workspace id (slug,\n``<= 64`` chars). Tenant id comes from the ``X-Tenant-Id``\nheader -- the body intentionally does not accept it so a\ncaller cannot create workspaces under another tenant.\n\nReturns ``201 Created`` with the persisted :class:`WorkspaceSpec`.",
"description": "The body carries the caller-chosen workspace id (slug,\n``<= 64`` chars). Tenant id comes from the ``X-Tenant-Id``\nheader. The body id must equal ``X-Workspace-Id``; a mismatch\nreturns ``400 workspace_scope_mismatch`` before any write.\n\nReturns ``201 Created`` with the persisted :class:`WorkspaceSpec`.",
"operationId": "create",
"parameters": [
{
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
name = "flycanon"
# CalVer YY.MM.PP -- bumped per release. Note that PEP 440 normalises
# ``26.05.04`` -> ``26.5.4`` in the built wheel filename.
version = "26.10.3"
version = "26.10.4"
description = "Operational Knowledge Repository -- versioned, provenance-tracked canonical knowledge with hybrid retrieval and RAG, exposed as a standalone HTTP microservice over fireflyframework-pyfly and -agentic. Part of Firefly Framework, platform-agnostic by design."
readme = "README.md"
requires-python = ">=3.13,<3.14"
Expand Down
8 changes: 4 additions & 4 deletions sdks/java/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Compatible with **flycanon service version `26.10.x`**.

| SDK | Service |
|-----|---------|
| `26.10.3` | `26.10.x` (unchanged HTTP contract; Agentic dependency adoption) |
| `26.10.4` | `26.10.x` (same request/response schemas; workspace creation requires matching scope) |
| `26.5.7` | `26.5.x` |

## Install
Expand All @@ -50,7 +50,7 @@ Compatible with **flycanon service version `26.10.x`**.
<dependency>
<groupId>com.firefly</groupId>
<artifactId>flycanon-sdk</artifactId>
<version>26.10.3</version>
<version>26.10.4</version>
</dependency>
```

Expand Down Expand Up @@ -181,7 +181,7 @@ at the boundary as a typed exception.

| Method | Description |
|--------|-------------|
| `createWorkspace(WorkspaceCreate spec) -> WorkspaceSpec` | Create a workspace under the caller's tenant. |
| `createWorkspace(WorkspaceCreate spec) -> WorkspaceSpec` | Create a workspace under the caller's tenant. Configure the client's workspace header to equal `spec.id()`; a mismatch returns `400 workspace_scope_mismatch` before any write. |
| `listWorkspaces() -> List<WorkspaceSummary>` | List every workspace owned by the caller's tenant (`created_at DESC`). |
| `getWorkspace(String workspaceId) -> WorkspaceSpec` | Fetch a single workspace by id. |
| `updateWorkspace(String workspaceId, WorkspaceUpdate patch) -> WorkspaceSpec` | Sparse PATCH; only present fields are applied. |
Expand Down Expand Up @@ -418,7 +418,7 @@ use).
## Versioning

The SDK pins its version to the service's CalVer (`YY.MM.PP`), so
`flycanon-sdk@26.10.3` is the matching client for service version
`flycanon-sdk@26.10.4` is the matching client for service version
`26.10.x`. Upgrade the SDK in lockstep with the service. Both
clients (`CanonClient`, `ReactiveCanonClient`) ship from the same
artifact -- no separate dependency to manage.
Expand Down
2 changes: 1 addition & 1 deletion sdks/java/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

<groupId>com.firefly</groupId>
<artifactId>flycanon-sdk</artifactId>
<version>26.10.3</version>
<version>26.10.4</version>
<packaging>jar</packaging>

<name>flycanon-sdk</name>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public final class CanonClient {
static final String HEADER_IDEMPOTENCY_KEY = "Idempotency-Key";

/** SDK version. Mirrors {@code pom.xml}. */
public static final String SDK_VERSION = "26.10.3";
public static final String SDK_VERSION = "26.10.4";

private final RestClient restClient;
private final ObjectMapper mapper;
Expand Down
8 changes: 4 additions & 4 deletions sdks/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,17 @@ Compatible with **flycanon service version `26.10.x`**.

| SDK | Service |
|-----|---------|
| `26.10.3` | `26.10.x` (unchanged HTTP contract; Agentic dependency adoption) |
| `26.10.4` | `26.10.x` (same request/response schemas; workspace creation requires matching scope) |
| `26.8.0` | `26.8.x` (embedding sets, `flycanon reindex`, `ChunkStats.by_embedding_model`) |
| `26.7.1` | `26.7.x` (routes realigned: `?mode=async`, `/query/stream`, `/conversations/{id}/turn`, `/query/suggest`, `?after_id=`; `X-API-Key`; `delete_source`, `purge_workspace`) |
| `26.5.7` | `26.5.x` |

## Install

```bash
uv add https://github.com/fireflyframework/flycanon/releases/download/v26.10.3/flycanon_sdk-26.10.3-py3-none-any.whl
uv add https://github.com/fireflyframework/flycanon/releases/download/v26.10.4/flycanon_sdk-26.10.4-py3-none-any.whl
# or
pip install https://github.com/fireflyframework/flycanon/releases/download/v26.10.3/flycanon_sdk-26.10.3-py3-none-any.whl
pip install https://github.com/fireflyframework/flycanon/releases/download/v26.10.4/flycanon_sdk-26.10.4-py3-none-any.whl
```

## Quick start
Expand Down Expand Up @@ -110,7 +110,7 @@ boundary.

| Method | Description |
|--------|-------------|
| `create_workspace(spec: WorkspaceCreate) -> WorkspaceSpec` | Create a workspace under the caller's tenant. |
| `create_workspace(spec: WorkspaceCreate) -> WorkspaceSpec` | Create a workspace under the caller's tenant. The client's `workspace_id` must equal `spec.id`; a mismatch returns `400 workspace_scope_mismatch` before any write. |
| `list_workspaces() -> list[WorkspaceSummary]` | List every workspace owned by the caller's tenant (`created_at DESC`). |
| `get_workspace(workspace_id) -> WorkspaceSpec` | Fetch a single workspace by id. |
| `update_workspace(workspace_id, patch: WorkspaceUpdate) -> WorkspaceSpec` | Sparse PATCH; only present fields are applied. |
Expand Down
2 changes: 1 addition & 1 deletion sdks/python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# SPDX-License-Identifier: Apache-2.0
[project]
name = "flycanon-sdk"
version = "26.10.3"
version = "26.10.4"
description = "Async-first Python client for the flycanon Operational Knowledge Repository service."
readme = "README.md"
requires-python = ">=3.11"
Expand Down
2 changes: 1 addition & 1 deletion sdks/python/src/flycanon_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@
WorkspaceUpdated,
)

__version__ = "26.10.3"
__version__ = "26.10.4"

__all__ = [
"CANON_WORKSPACES_TOPIC",
Expand Down
6 changes: 4 additions & 2 deletions sdks/python/src/flycanon_sdk/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -877,8 +877,10 @@ async def create_workspace(self, spec: WorkspaceCreate) -> WorkspaceSpec:
"""Create a workspace under the caller's tenant.

The id comes from ``spec.id`` (caller-chosen slug); the tenant
comes from the ``X-Tenant-Id`` header. Returns ``201 Created``
with the persisted :class:`WorkspaceSpec`.
comes from the ``X-Tenant-Id`` header. Configure the client's
``workspace_id`` to equal ``spec.id``. A mismatch raises an
SDK error with ``400 workspace_scope_mismatch`` before any write.
Returns ``201 Created`` with the persisted :class:`WorkspaceSpec`.
"""
body = await self._request(
"POST",
Expand Down
4 changes: 2 additions & 2 deletions sdks/python/tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -592,7 +592,7 @@ async def test_answer_returns_typed_response() -> None:


def test_sdk_version_declares_current_release() -> None:
assert __version__ == "26.10.3"
assert __version__ == "26.10.4"


def test_canon_workspaces_topic_constant() -> None:
Expand Down Expand Up @@ -674,7 +674,7 @@ async def test_user_agent_advertises_sdk_version() -> None:
}
)
await client.version()
assert "flycanon-sdk-python/26.10.3" in route.calls.last.request.headers["User-Agent"]
assert "flycanon-sdk-python/26.10.4" in route.calls.last.request.headers["User-Agent"]


# ----------------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion sdks/python/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading