QUORUMRELAY / REFERENCE

QuorumRelay user manual

Candidate 0.1.0.dev8 · schema 2.0.0

QuorumRelay is a standalone engine for identifying configured civic-data sources, reading or parsing bounded captures, normalizing meeting records, searching captured material, preparing neutral exports, and executing a small set of explicitly authorized provider writes. It does not require Townlight, TownReporter, TownRecord, Brief, a hosted service, or a particular application database. Those projects are examples of consumers; the engine API is intended for other products as well.

This is a development candidate. The verified Python wheel is quorumrelay-0.1.0.dev8-py3-none-any.whl, SHA-256 bcfe691b4d1eb0de162f5f94985d0242a97b5d3d89bd5eeb9306d1ef28caf692, built from source commit 3862e5f4ad7064aee722e09bb438899c3566477f. It is not published to a Python package index. The candidate verification proves package contents and fixture/baseline behavior; it does not prove live provider acceptance, complete provider catalogs, or live write acceptance. See the development 08 receipt, the baseline equality receipt, and the capability matrix.

1. Install and verify the candidate

QuorumRelay requires Python 3.11 or newer. Use the public source checkout at the verified commit for this publication. The dev8 wheel is documented as verification evidence but is not a public download asset; use a wheel only if your operator already has the matching immutable artifact. Do not use a package-index install command for this candidate.

Install from the public Git repository

git clone https://github.com/townlight/quorumrelay.git C:\work\quorumrelay
Set-Location C:\work\quorumrelay
git checkout 3862e5f4ad7064aee722e09bb438899c3566477f
py -m venv .venv
.\.venv\Scripts\Activate.ps1
py -m pip install -e .

For the REST/HTTP and MCP service commands, install the optional service dependencies from the same checkout:

py -m pip install -e ".[service]"

The service extra installs the MCP, Starlette, Uvicorn, multipart and JSON-schema dependencies. The core SDK can be used without these service packages.

Install the verified wheel

This repository does not offer a public wheel download for the candidate. If your operator has already transferred the exact immutable wheel into an approved artifact store, verify its digest before installing it:

Get-FileHash .\quorumrelay-0.1.0.dev8-py3-none-any.whl -Algorithm SHA256
py -m pip install .\quorumrelay-0.1.0.dev8-py3-none-any.whl

Use only the immutable artifact whose hash is recorded above; rebuilding from source produces a different artifact identity. To run the HTTP or MCP service from source, install the matching checkout’s .[service] extra as shown above.

Verify the installed package

py -c "import quorumrelay; print(quorumrelay.__version__)"
'{}' | quorumrelay providers

The version command should print 0.1.0.dev8. The providers command returns the installed provider and format manifest as JSON. In an embedded deployment, also request GET /v1/info and compare the installed version and schema version with the application’s approved pin.

2. Choose a provider and input format

The package contains 31 canonical provider families. These are identifiers accepted by the engine; they are not claims that every tenant is reachable or every vendor feature is implemented.

Canonical provider IDs Canonical provider IDs Canonical provider IDs
legistar primegov granicus
novusagenda civicplus-select civicplus-agenda-center
municode-meetings boardbook escribe
simbli civicweb iqm2
swagit suiteone-media peak
utah-public-notice destiny-agendaquick town-hall-streams
telvue champ onbase
laserfiche-weblink towncloud digital-towpath
clerkbase first-party wistia
boardable gworks boarddocs
tyler-meeting-manager

Ask the installed engine for the authoritative provider-specific choices:

from quorumrelay import create_engine

engine = create_engine()
manifest = engine.execute("providers", {}).public_dict()
for provider in manifest["data"]["providers"]:
    print(provider["provider"], provider.get("formats", []),
          provider.get("additional_formats", []))

The fixed compatibility baseline contains 34 provider/input-format pairs. The additive parser hooks expose 28 more provider-specific captured formats in this candidate. A baseline format keeps its exact historical reader behavior; additive formats replay other captured details or API responses. An additive parser is a saved-capture capability and does not imply that QuorumRelay can discover that provider live. Use the exact input_format returned for the provider. Do not choose a parser by guessing from a filename.

Use capabilities for the provider’s operation-level implementation and acceptance information:

result = engine.execute("capabilities", {"provider": "legistar"})

Read its status fields separately. A documented provider feature, an implemented operation, a fixture-tested parser, a bounded public read and authorized live acceptance are different evidence levels. The inventory and provider coverage map explain current limits.

3. Configure a source and caller

A source describes a provider and a durable HTTP(S) citation or route. It is not the consumer’s tenant identifier and must not contain credentials.

from quorumrelay import Bounds, OperationContext, Source

source = Source(
    provider="legistar",
    url="https://webapi.legistar.com/v1/example-city/events",
    namespace="//webapi.legistar.com/v1/example-city",
    input_format="legistar-events-json",
)

Replace the example tenant with a verified source for your organization. Keep the configured namespace equal to the provider-derived namespace. It is the stable identity used for records and source grants. The application’s principal, client and tenant identify the consumer and scope its stored state.

For an in-process integration, create a context with the current user’s established authorization and source restrictions:

context = OperationContext(
    principal="meeting-import-worker",
    client_id="your-application",
    tenant="your-configured-organization",
    allowed_operations=frozenset({
        "providers", "capabilities", "identify", "validate_source",
        "discover", "fetch", "normalize", "search", "import_preview",
        "import_bundle", "export", "status", "cancel",
    }),
    allowed_namespaces=frozenset({source.namespace}),
)

A default context is useful for local read-only SDK use. Production callers should set the real application principal/client/tenant and least-privilege operation and namespace policies. Provider credentials, when needed, belong in the private trusted OperationContext.credentials supplied by the application. Provider credential formats vary; follow the exact provider entry in the SDK guide and native mappings. Never put credentials in Source.url, operation JSON, a source option, or a citation.

Source.options contains only provider-documented configuration. For example, Tyler calendar requests require an observed tenant-specific API base; the configured API base must match that tenant’s observed client. An option is not an unrestricted request target. Set Source.body only where the provider mapping recognizes that body. Unmapped body assignments remain an explicit unknown instead of being guessed from a city or meeting label.

4. Read, parse and fetch

Identify and inspect a source

identify compares a URL or capture pattern with registered source identities. It does not log in or prove access. validate_source checks a configured source using the operation supported for that provider; do not treat it as a universal health check. Use providers and capabilities before choosing a live operation.

identified = engine.execute("identify", {"url": source.url}, context=context)
configured = engine.execute("validate_source", {"source": source.public_dict()}, context=context)

Discover bounded records

Discovery is a live or explicitly saved source read, depending on the provider adapter. Supply date and volume bounds:

result = engine.discover(
    source,
    bounds=Bounds(
        date_from="2026-11-01",
        date_to="2026-11-30",
        max_pages=5,
        max_records=200,
        deadline_seconds=30,
    ),
    context=context,
)

Dates are inclusive. The connector may support fewer filters than the shared model, may require explicit date or body information, or may report unknown dates. Check status, errors, and coverage before using any records. partial means the result may contain useful records with a known limit; it is not a successful-complete catalog. A provider returning an empty page does not by itself prove that the source has no records.

Normalize a saved capture

Normalization parses the exact bytes supplied by your application and does not persist them or contact the provider.

from pathlib import Path

capture_bytes = Path("captures/legistar-events.json").read_bytes()
preview = engine.normalize(
    source,
    capture_bytes,
    input_format="legistar-events-json",
    bounds=Bounds(max_records=250),
    context=context,
)

Or call the common operation explicitly:

preview = engine.execute(
    "normalize",
    {
        "source": source.public_dict(),
        "input_format": "legistar-events-json",
        "bounds": {"max_records": 250},
    },
    content=capture_bytes,
    context=context,
)

The returned data is a CaptureBundle containing source identity, normalized records, compatibility projection, raw capture metadata and coverage. Original capture bytes and unknown provider values are retained. The normalized fields are a convenient shared view, not a replacement for the source record.

Fetch one known record or a supplied attachment

Use a detail request only when the provider has a documented/evidenced detail adapter:

detail = engine.fetch_record(
    source, "7011",
    bounds=Bounds(deadline_seconds=20),
    context=context,
)

To retrieve a document already linked from a captured record, select its captured index. This operation does not accept an arbitrary caller URL:

record = result.data["records"][0]
document = engine.fetch_attachment(
    source, record, index=0, kind="documents",
    capture=capture_bytes,
    bounds=Bounds(transfer_bytes=50 * 1024 * 1024),
    context=context,
)

If the bundle is stored, the helper can resolve the stored capture instead of receiving capture. Keep durable citation URLs separately from short-lived signed download URLs. Check the attachment result for availability, hash, byte length, content type and errors.

A captured media player URL may return a player page instead of media. Where a provider captured a direct video_url, audio_url or playback_url, select that existing field with kind="recordings" and url_field="video_url" (or the verified field). QuorumRelay does not let the caller substitute a URL. An HLS fetch returns the manifest, not all segments; it is not proof that a recording is playable.

5. Import, persist, search and export

Preview before persistence

Use import_preview to see which records would be new, unchanged or changed in the current QuorumRelay store. Preview does not write state.

preview_import = engine.execute(
    "import_preview",
    {"result": result.public_dict()},
    context=context,
)

To persist explicitly, configure storage and pass persist: true:

from quorumrelay.storage import SQLiteStorage

engine = create_engine(storage=SQLiteStorage("state/quorumrelay.sqlite3"))
saved = engine.execute(
    "import_bundle",
    {"result": result.public_dict(), "persist": True},
    context=context,
)

The import uses scoped atomic storage and reports conflicts rather than blindly replacing newer values. QuorumRelay storage is not your application’s canonical meeting database. Your application still owns destination record IDs, duplicate resolution, source transition decisions and user review. Do not infer that a source transition is safe from matching hostnames or similar IDs.

Search stored captures or a provider

By default, search searches records in the configured QuorumRelay store. It matches normalized metadata and explicitly supplied text; it does not add OCR, transcription or missing provider text.

from quorumrelay import Query

hits = engine.search(
    Query(terms=("budget",), phrases=("public comment",), limit=50),
    context=context,
)

A provider-specific native search is opt-in:

native = engine.execute(
    "search",
    {
        "native": True,
        "source": source.public_dict(),
        "query": {"terms": ["Council"], "limit": 20},
        "bounds": {"date_from": "2026-11-01", "date_to": "2026-11-30",
                   "max_pages": 2, "max_records": 100},
    },
    context=context,
)

Only use this when capabilities says that provider has an implemented native search. Provider search typically selects candidate records; QuorumRelay then applies exact local matching to fields that were actually supplied. Check the response’s coverage for searched fields, candidate limits, omitted rows and checkpoint information. A native result is not a promise of full-text or catalog coverage.

Export portable records

The neutral export operation emits JSON, JSONL or ZIP bundles. CSV and iCalendar are explicit projections and can lose unsupported meeting fields; the result reports losses. Export does not contact a vendor or mutate records.

exported = engine.execute(
    "export",
    {"bundle": saved.data["bundle"], "format": "zip", "stream": True},
    context=context,
)

Large streamed exports return a scoped artifact ID. Use the HTTP file endpoint, the MCP resource flow, or the SDK artifact helper for the selected interface. Do not persist a local artifact path or assume an artifact ID can be used by another caller. Provider-specific file preparation is a separate opt-in operation documented by that provider; it is not a vendor upload.

6. Write workflow

Writes are separate from read, import, search and export. The default engine registers evidenced native adapters but supplies no credentials or grants. Write access requires a supported mapping, trusted source/credential policy, durable atomic storage, the write:plan and write:commit grants, and approval of one exact plan.

  1. Ask plan_write to validate the proposed provider-native change.
  2. Review every planned entity, source namespace, field, parent/resource ID, file hash, expected state and mock/live transport marker.
  3. Approve the exact returned plan_id and plan_hash through your application’s authorization flow.
  4. Call commit_write with only that plan ID and hash.
  5. Read receipt for the durable outcome. If the response was lost or the receipt is uncertain, reconcile that receipt before deciding whether to retry.
from quorumrelay import OperationContext, create_engine
from quorumrelay.storage import SQLiteStorage

# Load this from protected operator configuration; its shape is provider-specific.
write_context = OperationContext(
    principal="authorized-write-worker",
    client_id="your-application",
    tenant="authorized-test-organization",
    authorization=frozenset({"write:plan", "write:commit"}),
    allowed_operations=frozenset({"plan_write", "commit_write", "receipt"}),
    allowed_namespaces=frozenset({source.namespace}),
    credentials=trusted_provider_credentials,
)
write_engine = create_engine(storage=SQLiteStorage("state/quorumrelay-writes.sqlite3"))
planned = write_engine.plan_write(
    source,
    {
        "changes": [{
            "key": "meeting-2026-11-11",
            "entity": "event",
            "operation": "create",
            "fields": {"EventDate": "2026-11-11T00:00:00", "EventBodyId": 4},
        }],
    },
    context=write_context,
)
# Review planned.data["plan"] before obtaining the application's exact approval.
approval = {key: planned.data[key] for key in ("plan_id", "plan_hash")}
committed = write_engine.commit_write(approval, context=write_context)
receipt = write_engine.receipt({"plan_id": approval["plan_id"]}, context=write_context)

The native fields are provider-specific. A neutral meeting record is not automatically a writable vendor request. Unknown fields, unsupported entities, deletion, arbitrary URLs and generic administration are rejected. An ambiguous create is not blindly repeated: the write coordinator persists intent and reconciles against readback. Completed siblings remain visible in a partial batch; no automatic remote rollback occurs.

Live write acceptance for this candidate is NOT_RUN. The mappings have deterministic fixture checks, but no authorized provider test tenant and credentials have been supplied. Synthetic or mock receipts are not vendor acceptance. Do not use a public city’s production data as a test environment. See the write contract for the currently mapped operations and their exact fields.

7. Run the CLI or service

CLI

The console command accepts a single JSON request from stdin or --request. Add --jsonl for one request per nonblank line. --content supplies saved source bytes; --file-root confines local input paths. Stdout contains JSON result envelopes; diagnostics go to stderr.

$Request = @{
  source = @{
    provider = "legistar"
    url = "https://webapi.legistar.com/v1/example-city/events"
    namespace = "//webapi.legistar.com/v1/example-city"
  }
  bounds = @{ date_from = "2026-11-01"; date_to = "2026-11-30"; max_pages = 3 }
} | ConvertTo-Json -Depth 12

$Request | quorumrelay discover --database C:\app\state\connectors.sqlite

For file output, use --output only with one export request. The file is created exclusively; an existing file is not overwritten. --write-file CHANGE PART PATH supplies one declared metadata-only write-plan file part. --export-file INDEX PATH supplies a declared export file. Both require the selected local file to remain under --file-root if that option is set.

Exit codes are 0 for completed, 2 partial, 3 cancelled, 4 failed, 5 unsupported and 64 invalid request/configuration. For JSONL, the CLI returns the highest observed code and retains every result.

REST/HTTP

Install the service extra and run the process under your service manager:

quorumrelay serve --config C:\app\connector-service.json --database C:\app\state\connectors.sqlite

The default listener is 127.0.0.1:8765. A minimal token-protected, loopback-only read policy is shown below. Store a generated token in an operator-controlled secret store and render the configuration with restrictive file permissions. This example has no upstream credentials and grants no provider writes.

{
  "service": {
    "host": "127.0.0.1",
    "port": 8765,
    "tokens": {
      "replace-with-a-random-secret-at-least-16-characters": {
        "principal": "meeting-reader",
        "client_id": "your-application",
        "tenant": "your-organization",
        "operations": ["providers", "capabilities", "discover", "fetch", "normalize", "search", "import_preview", "import_bundle", "export", "receipt", "status", "cancel"],
        "allowed_namespaces": ["//webapi.legistar.com/v1/example-city"]
      }
    }
  }
}

For an account API, configure provider-specific credentials only in trusted private process configuration and bind them to the exact origin, tenant and namespace required by that adapter. The source namespace is still separate from the consumer tenant. examples/service.mock.json uses synthetic values and is not deployment configuration.

Check GET /v1/info and GET /openapi.json. Submit a shared engine request as JSON:

$Source = @{ provider = "legistar"; url = "https://webapi.legistar.com/v1/olympia/Events"; input_format = "legistar-events-json" }
$Headers = @{ Authorization = "Bearer $env:QUORUMRELAY_TOKEN" }
$Body = @{ source = $Source; bounds = @{ max_pages = 2; max_records = 50 } } | ConvertTo-Json -Depth 12
Invoke-RestMethod -Method Post -Uri "$env:QUORUMRELAY_URL/v1/operations/discover" -Headers $Headers -ContentType "application/json" -Body $Body

The token shown here must come from your trusted deployment configuration. Do not place it in a saved request or log it. The service returns the same complete result envelope used by the SDK. Long operations may use POST /v1/jobs/{operation}; poll with the shared status operation and request cooperative cancellation with cancel.

File upload uses POST /v1/files and returns an opaque caller-scoped handle, not a path. Retrieve it through GET /v1/files/{handle}. Use file handles for large imports/exports instead of embedding large base64 content in JSON. A handle is not a credential and is not portable across caller scopes.

MCP

Run local MCP stdio with quorumrelay mcp. An operator-owned client can also use Streamable HTTP at /mcp when the service is explicitly configured for MCP. Both use the same engine operations and result envelope. For supported protocol versions, resource delivery, authentication and cancellation behavior, see the MCP reference. The service does not issue OAuth tokens or expose provider credentials to model tool arguments.

8. Bounds, privacy and operational ownership

Default shared bounds are 1,000 records, 25 pages, 16 MiB parser input, 200 MiB transfer, and a 45-second engine deadline. Providers or service configuration may impose lower limits. HTTP/CLI JSON framing defaults to 24 MiB; the default HTTP operation timeout is 60 seconds. The parser limit and file-transfer limit are separate. Large downloadable bytes do not imply a large parser input can be processed.

The service requires explicit configuration before binding beyond loopback: enable network access, configure authenticated tokens and a non-wildcard Host allowlist, and set allowed origins for your deployment. Put it behind an operator-managed HTTPS reverse proxy. MCP network use with service tokens also needs the configured public resource URL, issuer URL and a verifier suitable for the deployment. Follow the interface security notes and MCP reference.

Use a private database path and restrict its filesystem permissions. SQLite uses WAL and full synchronous commits; the consumer owns backup, retention, restore testing, service lifecycle and file storage. Storage keys are scoped by principal, client and consumer tenant. Keep those three values stable while using existing records, checkpoints and write receipts. A new identity scope cannot see the old scope’s state.

Provider responses and document text are untrusted input. Preserve original captures and their hashes, validate dates/source identities, and apply your own editorial or records-management review. The engine does not publish articles, send notices, host an application’s canonical meeting records, or decide that an imported change is editorially approved.

9. Bounds, continuation and failure recovery

Always read all of OperationResult: status, data, errors, coverage and operation_id. The status values are:

Status Meaning
completed The requested bounded operation completed. It does not promise an entire provider archive.
partial Some data may be usable; inspect errors, truncation, unknown fields and scope.
cancelled Cooperative cancellation stopped future work; prior completed work or receipts remain.
failed The operation could not complete under the given request or policy.
unsupported No evidenced operation is registered for that request. This is not an empty result.

A continuation token is opaque state. Supply the exact returned checkpoint under bounds.checkpoint only with the same source, query, caller scope, date/body/type filters and current grants. Some adapters keep private cursor state in the current process, so a service restart or expiry may require restarting the bounded read. Do not edit a checkpoint, convert it into a URL or reuse it across callers. If the current page changes, restart according to that adapter’s coverage information.

Symptom What to check
unsupported or operation_unavailable Check the canonical provider ID, operation and exact saved format in providers/capabilities. Lack of implementation is not proof the vendor lacks that feature.
partial, no error Read coverage for page/record limits, unknown date/body, provider candidate limits, unavailable text and catalog scope. Do not report it as complete.
invalid_capture Confirm the raw bytes, provider and exact input_format; stay within parser limit.
permission_denied Confirm the authenticated principal’s operation and namespace grants. Caller identity, credentials and grants cannot be supplied in request JSON.
Provider 401/403 or missing account records Check provider-specific credential binding, tenant/origin/namespace and upstream account permission in trusted configuration. Do not retry by changing source origins without evidence.
Expired/unavailable checkpoint Restart the read with the same configured source and current filters. Use the returned partial coverage to decide whether another bounded read is appropriate.
File handle missing or rejected Confirm the same authenticated principal/client/tenant, file purpose, expiry and hash/length. A handle is scoped and temporary.
Write response timeout or uncertain receipt Read the existing receipt for the exact plan. Do not issue a new create until reconciliation determines the prior effect.