Rivulet LocalOps turns a written service policy into decisions that can be executed, checked and traced to a named owner. Its reference scenario is a local council handling waste and litter requests. Each request is diagnosed, tested against an approved policy, recorded in a tamper-evident ledger and reviewed by a second person.
This is an independent technical prototype. It is not an official City of Hobart, Tasmanian Government or council service. It contains no real resident requests and performs no real council-system writes.
Live bounded demo: Rivulet LocalOps assurance console — five server-owned synthetic scenarios only; no visitor free text or real resident data. The live bounded profile is release v0.8.0; internal support routes remain absent from that profile.
The v0.7.2 console after a synthetic request passes the policy check. Every figure carries its source class, and the policy rules, agent receipts and record hashes open from the result.
- At a glance
- The problem
- Who it is for
- One request, start to finish
- The screens
- What works today: v0.8.0
- Design principles
- Real data and synthetic data
- How it was built
- Problems that became rules
- Technology stack
- Run locally
- API surface
- Verify
- Security and data boundary
- Poster
- What is not finished
- What the project showed
| What it is | A civic workflow demonstrator: explainable triage, a policy Gate, a decision ledger, human review and assurance reporting in one reproducible workflow |
| Who it is for | Teams that handle public service requests under written policy: service managers, policy owners, reviewers, and the IT and data staff who support them |
| The problem | Automation can classify a request. It usually cannot show which rule allowed an action, what evidence was used, or who was accountable |
| Stack | Python 3.12, FastAPI, SQLite, React 19 and TypeScript, a Power BI project, Docker, Azure Container Apps and GitHub Actions |
| Evidence | 49 automated tests, 19 source-quality checks, five synthetic recovery exercises, a 316-event synthetic ledger and a live Azure deployment |
| Data | Real public reference data, and requests that are clearly labelled synthetic |
| Status | Release v0.8.0. A portfolio prototype. It has not been validated with a council and is not production-ready |
Automated classification is only one part of a trustworthy operational workflow. An organisation that answers to the public also has to be able to answer these questions about any single decision:
- Which approved rule allowed or blocked an action?
- What evidence and software version were used?
- Who was accountable for the decision?
- Can an independent reviewer accept, override or escalate it without erasing history?
- Can an agent use only the current case, approved data and allowlisted tools—and fail closed when its provider identity changes?
- Can automation be stopped safely when integrity or provider concerns arise?
Rivulet LocalOps makes those controls executable and testable.
| Role | The question they need answered | What Rivulet shows them |
|---|---|---|
| Service or operations manager | Which requests can go ahead and which need a person? | A Gate result of pass, reject or escalate, with the rule and the reason for each |
| Policy owner | Is the system applying the policy that was approved? | A versioned policy pack, with its version and content hash recorded on every decision |
| Reviewer or auditor | Can a decision be checked later without taking the software's word for it? | An append-only ledger, a replay of any decision and an integrity check of the hash chain |
| IT or security lead | What can the automated agent touch, and can it be stopped? | Allowlists for tools and data, a call budget, a time-limited grant and a provider kill switch |
| Data analyst | Which numbers are real and which are test data? | Source contracts, data-quality checks and a report that labels every figure by its source class |
These roles describe a reference scenario. No council and no resident took part in designing or testing the prototype.
flowchart LR
P[Versioned synthetic policy] --> R[Service request]
R --> D[Deterministic diagnosis]
C[Versioned agent capabilities] --> A[Read-only case-agent]
A --> D
A --> L
D --> G{Policy Gate}
G -->|pass / reject / escalate| L[Hash-chained event ledger]
L --> Q[Independent review queue]
Q --> H[Accept / override / escalate]
K[Provider kill switch] --> D
K --> M[Recorded manual fallback]
- A request arrives. For example, an overflowing public bin beside a footpath, with coordinates. Every request in the system is synthetic.
- Diagnosis. Rules place it in one of eight operational categories or a general fallback, and return the priority rule, a confidence value, the terms that matched and a list of missing information.
- Asset context. The nearest public bin is found in a 727-feature snapshot of City of Hobart open data.
- The Gate. A versioned policy decides pass, reject or escalate, and gives the rule behind each outcome. Only one action can ever pass: creating a draft work order.
- The ledger. The decision is appended as an event with request, policy and event hashes, chained with SHA-256.
- Review. A second person accepts, overrides or escalates. Their review is appended. The original Gate result is never overwritten.
- Replay and integrity. Any decision can be recomputed and compared with what was recorded, and the whole chain can be verified.
- Stopping. A kill switch disables the provider. Requests then take a manual route, and that fallback is recorded too.
![]() |
![]() |
| Power BI: Gate and decision assurance. Gate outcomes and the human-control paths | Power BI: data provenance and quality. Each source with its contract and its checks |
The web console is shown at the top of this page. A phone-width capture of a denied path is kept in evidence/F24.
| Capability | Implemented evidence |
|---|---|
| Explainable diagnosis | Eight operational categories, general fallback, priority rule, confidence, matched terms and missing-information list |
| Public asset context | Nearest-bin matching against a 727-feature City of Hobart public ArcGIS snapshot |
| Policy Gate | Versioned synthetic policy; deterministic pass, reject and escalate; rule-level reasons and policy hash |
| Action boundary | Only create_draft_work_order can pass; no connector or external write exists |
| Decision ledger | SQLite append-only events with request, policy and event hashes; provider/evidence/owner/time metadata |
| Human control | Pending queue; separate owner and reviewer; append-only accept/override/escalate history |
| Replay | Recomputes the decision and compares policy version/hash, provider version, category, priority and Gate rules |
| Safety control | Persistent provider kill switch, manual fallback event, integrity failure that stops automated processing |
| Bounded agent control | Server-selected case-agent, hash-bound capability registry, five-minute grant, four-call budget, tool/data/resource allowlists, pre-execution provider binding and auditable failure paths |
| Application support | Component health, correlation/request IDs, redacted bounded logs, checksum-protected Support Bundle, internal support page and five runbooks |
| Recovery evidence | APP-001/003/004/005 synthetic recovery passes; APP-002 local mismatch detection plus a hosted Azure rollback drill pass, with the first failed drill retained |
| Verification | 49 automated tests, dependency audits, a 12-case deterministic regression set, F18 capability, F22 container, F23 live Azure and APP-001–005 evidence |
| Windows reliability | SQLite read connections are explicitly closed; a disposable review runner verifies cleanup after the full review story |
| Public-source contracts | D1 Hobart assets and D2 Townsville monthly aggregate counts have publisher, URL, licence, retrieval, hash, schema, grain and limitation manifests |
| Data quality | 19 automated source checks: 15 pass, 4 documented warnings, 0 fail and 0 blocking |
| Synthetic analytics fixture | D6 deterministically reproduces 316 hash-chained events: 180 decisions, 120 reviews, 8 manual fallbacks and 8 provider-control changes |
| Analytics mart | 14 non-empty dimension/fact tables as versioned CSV plus a reproducible local SQLite build; 8 reconciliation checks pass |
| Metric governance | Numerators, denominators, grain, filters, null handling, truth class and caveats are defined; no unsupported target is invented |
| Power BI assurance | Deterministically generated two-page PBIP/PBIR/TMDL project: 7 Import tables, 29 measures, 8 relationships, 27 visuals, visible truth boundaries and versioned Desktop evidence |
| Cloud-safe web console | React/TypeScript overview console in plain language, backed by a separate FastAPI profile; visitors can run only five server-owned synthetic scenarios |
| Container delivery | Multi-stage Linux image, exact runtime lock, non-root UID/GID 10001, health probe, same-origin assets and public/internal route separation |
| Azure deployment | Live New Zealand North Container Apps Consumption revision: 0.25 vCPU/0.5 GiB, scale 0–1, public GHCR image and resource-group-scoped managed-identity OIDC with no client secret |
The current diagnosis engine and agent adapter are deliberately rules-based. They form a transparent control baseline for later model comparison and are not presented as generative AI.
- Rules first, models later. Diagnosis and the agent adapter are deterministic. They give a transparent baseline that a model can later be compared against.
- The Gate decides. The agent does not. The agent reads one case and approved public data. It cannot run the Gate, make the human decision or call a connector.
- One permitted action. Only a draft work order can pass. Nothing writes to an outside system.
- History is appended. A review adds to the record. It never replaces the original result.
- A second person reviews. The accountable owner and the reviewer are separate fields on every decision.
- Deny by default. Tools, data and resources are allowlisted, the grant lasts five minutes, the budget is four calls, and a change of provider identity fails closed before anything runs.
- Automation can be switched off. The kill switch is persistent, and the manual route it forces is itself recorded.
- Every number carries its source class. Real public reference data and synthetic operations are never mixed in one figure.
- Failures are kept. Failed hosted runs stay on the public record and release tags are not moved.
| Set | What it is | Source class |
|---|---|---|
| D1 | City of Hobart public litter-bin assets, 727 features | Real public reference |
| D2 | Townsville request-for-service counts, monthly aggregates | Real public reference. Context only, not Hobart demand |
| D6 | 316 hash-chained events from a fixed seed: 180 decisions, 120 reviews, 8 manual fallbacks and 8 provider-control changes | Synthetic operations |
| Policy | A versioned waste and litter policy written for the demonstration | Synthetic. Not a council policy |
Each public source has a manifest with its publisher, URL, licence, retrieval date, hash, schema, grain and limitations. Rates calculated from D6 exercise the calculations. They say nothing about council performance or model accuracy.
| Release | Date | What it added |
|---|---|---|
| v0.3.0, v0.3.1 | 9 Sep 2026 | Policy Gate, decision ledger, review queue, replay and kill switch |
| v0.4.0, v0.4.1 | 9-10 Sep 2026 | Contracted public sources, data-quality checks and the analytics mart |
| v0.5.0, v0.5.1 | 12 Sep 2026 | Power BI assurance report generated from code |
| v0.6.0 | 12 Sep 2026 | Bounded read-only case-agent with capability controls |
| v0.7.0 | 16 Sep 2026 | Web console and container image |
| v0.7.1 | 24 Sep 2026 | Live Azure deployment through OIDC, with no stored client secret |
| v0.7.2 | 10 Oct 2026 | Web console redesigned as a plain-language overview. The API, policy and controls are unchanged |
| v0.8.0 | 12 Oct 2026 | Internal application-support diagnostics, Support Bundle, recovery experiments and smoke-gated Azure rollback workflow |
The order was deliberate: the controls came before any model, and each sprint ended with a review against recorded evidence. See Sprint Evidence.
| What went wrong | What changed |
|---|---|
| A review run on Windows failed during cleanup because read-only SQLite connections were left open | v0.3.1 closes them explicitly and adds a reusable review runner |
| The first v0.4.0 hosted run failed: line-ending normalisation changed the bytes of hashed data snapshots | v0.4.1 marks those snapshots as non-text so a fresh checkout keeps the exact bytes |
| The v0.5.0 workflows failed on a fresh Windows checkout because generated report partitions embedded CRLF | v0.5.1 writes one canonical form, hashes that form and tests CRLF and LF equivalence |
| Two Azure deployment attempts failed before v0.7.1 was accepted | Both runs are kept as evidence beside the run that passed |
- Python 3.12
- FastAPI and Pydantic contracts
- SQLite event storage using the Python standard library
- SHA-256 canonical event hashing
- GeoJSON and public ArcGIS asset data
- Contracted public CSV data, deterministic JSONL events and a star-schema analytics mart
- Power BI Project (PBIP/PBIR/TMDL) with deterministic generation and Microsoft report validation
- React 19, TypeScript and Vite 8
- Docker multi-stage build and Azure Container Apps Bicep
- pytest and FastAPI TestClient
- exact development and runtime lockfiles
- GitHub Actions verification, OCI build/SBOM attestation and OIDC deployment workflow
- npm high-severity audit gate, Python dependency-graph check and post-deploy release/boundary smoke verification
See Technical Architecture for components, data contracts, controls and limitations. See Sprint Evidence for the completed acceptance checks and claim boundaries.
git clone https://github.com/Zeeekrom/rivulet-localops.git
cd rivulet-localops
py -3.12 -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r requirements-dev.lock
.\.venv\Scripts\python.exe -m uvicorn rivulet_localops.main:app --app-dir src --reloadOpen http://127.0.0.1:8000/docs for the interactive API.
To run the cloud-safe browser demo locally without exposing the internal API surface:
npm --prefix web ci
npm --prefix web run build
docker build -t rivulet-localops:local .
docker run --rm -p 127.0.0.1:8000:8000 rivulet-localops:localOpen http://127.0.0.1:8000. This profile disables API documentation and does not load internal /api/v1/* or
/ops/* routes. Its SQLite ledger is intentionally ephemeral and resets when the container is replaced.
Submit a synthetic request:
{
"request": {
"text": "The public bin is overflowing beside the footpath on Elizabeth Street",
"coordinates": {
"latitude": -42.88675,
"longitude": 147.31912
}
},
"proposed_action": "create_draft_work_order",
"accountable_owner_id": "owner.demo"
}Send it to POST /api/v1/submit. The response includes the original Gate result, ledger sequence and hashes, policy/provider references, evidence and review state.
| Method and path | Purpose |
|---|---|
GET /health |
Engine, policy, provider-control and ledger status |
POST /api/v1/diagnose |
Explainable diagnosis without creating a decision record |
POST /api/v1/submit |
Run diagnosis + Gate and append a decision event |
GET /api/v1/decisions |
Query recent decisions or the pending/reviewed queue |
GET /api/v1/decisions/{id} |
Read one decision and its review history |
POST /api/v1/decisions/{id}/reviews |
Append an independent accept/override/escalate review |
POST /api/v1/decisions/{id}/replay |
Compare a replay with recorded evidence |
GET /ops/v1/ledger/integrity |
Verify the event hash chain |
GET /ops/v1/providers/{id} |
Read provider-control status |
POST /ops/v1/providers/{id}/kill-switch |
Disable or re-enable the provider with operator and reason fields |
GET /ops/v1/agents/{id}/capabilities |
Inspect the server-selected read-only capability profile |
POST /api/v1/agents/case-agent/invoke |
Run one bounded synthetic-case diagnosis without Gate or connector execution |
GET /ops/v1/agents/invocations |
Query completed, denied and failed agent audit records |
GET /ops/v1/health |
Inspect API, ledger, policy, provider, configuration, dependency and build/version health |
GET /ops/v1/support/bundle |
Export allowlisted diagnostics with redacted logs and a SHA-256 checksum |
GET /ops/v1/operations/logs |
Read the bounded allowlist-only operational log buffer |
GET /ops/support |
Open the internal Operations & Support page |
The separately deployed public profile exposes only GET /healthz, GET /demo/v1/bootstrap, predefined scenario
execution under POST /demo/v1/scenarios/{scenario_id}/run, and static web assets. It accepts no visitor free text.
.\scripts\verify.ps1Expected release evidence:
- source profile: 19 checks, 0 fail, 0 blocking
- deterministic D6: 316 events with stable SHA-256 and a valid hash chain
- analytics mart: 14 tables and 8 successful reconciliations
- Power BI project: 51 controlled files, 88 structural checks and Microsoft validator 0 errors / 0 warnings
- Power BI Desktop: 7 of 7 Import partitions refreshed and 13 of 13 DAX reference values reconciled
49 passed- npm audit: 0 vulnerabilities at the recorded v0.8.0 verification; Python installed dependency graph valid
- APP-001 through APP-005 local synthetic controls: 5 of 5 pass; APP-002 hosted Azure rollback independently verified
- 12 deterministic regression cases with zero category/priority differences
- F18: 12 of 12 capability checks; 7 invocations = 1 completed, 5 denied and 1 failed before execution
- the Sprint 2 review runner completes on Windows without retaining the disposable SQLite files
The v0.6.0 release passed GitHub Actions on both main run 34688891241 and tag run 34688892353. The v0.7.0 release passed hosted main verify, tag verify and container/SBOM workflows. v0.7.1 then passed main verify 35993932294, tag verify 35993936061 and container/SBOM 35993936049. After two retained deployment-recovery failures, the workflow fix passed main verify 35995184675 and the complete Azure OIDC/deploy/boundary gate passed run 35995289399. v0.7.2 passed main verify 38045635848, tag verify 38045637107 and container/SBOM 38045637061. v0.8.0 then passed main verify 38205266531, tag verify 38205268239, container/SBOM 38205268224 and Azure deployment 38205416715. The first rollback drill 38205693603 exposed an invalid Azure CLI argument and is retained as failed evidence. Safety recovery 38205872737 restored v0.8.0; the corrected hosted drill 38206163289 then passed candidate detection, redeployment of the captured image/release and recovery verification.
Run the end-to-end review story separately with:
.\.venv\Scripts\python.exe scripts\run_sprint2_review.pyThe machine-readable review result and its claim boundary are in
evidence/F08/2026-09-09.
Source-quality and analytics evidence are in
F06 and F09. Power BI build, refresh,
reconciliation, screenshots and Desktop round-trip evidence are in
F10 initial Review and
F10 round-trip. Sprint 3 identity, tool, data, resource, budget and provider-binding
evidence is in F18. The v0.7.0 cloud-safe frontend, real-container negative-route
checks and rendered desktop/mobile screenshots are in F22. The v0.7.2 interface redesign, with its local checks and screenshots, is in F24. The v0.7.1 live
Azure endpoint, OIDC, scale, negative-route and recovery evidence is in F23.
Application-support runbooks and synthetic incident reports are in docs/application-support,
with machine evidence under evidence/APP-001 through evidence/APP-005. APP-002 separates the local mismatch fixture,
pre-change v0.7.1 observation, failed first drill, safety recovery and successful corrected hosted rollback.
To open the report, run the normal verification first, then open
analytics/powerbi/RivuletAssurance.pbip in Power BI Desktop and refresh. PBIP/PBIR are preview formats. After any
Desktop edit or refresh, rerun scripts/build_powerbi_project.py before validating or committing; the generator is
the repository's canonical serialization boundary.
The 12-case result is a regression check on self-authored, uncomplicated cases. It is not model-accuracy or real-world effectiveness evidence.
- The API schema accepts only records labelled
synthetic, but cannot prove entered prose is genuinely synthetic. Do not enter resident or other personal data. - Owner, reviewer and operator IDs are asserted strings, not authenticated identities. Authentication and RBAC are not yet implemented.
- The
case-agentidentity is selected by the server and its local capability envelope is enforced, but callers are unauthenticated. Current-case checks are a contract demonstration, not object authorization against a real case store. - The capability registry is versioned and hashed but not signed. Input hashes are not privacy-preserving commitments; only synthetic requests are permitted.
- No external model API or model credential is used. The agent runs the deterministic-rules baseline and does not execute the Gate, a human decision or a connector.
- The hash chain detects in-place modification, reordering and gaps. It is not an external signature, WORM store or production audit guarantee.
- The operations endpoint is logically separate but runs in the same application process; it is not yet an out-of-band control plane.
- No endpoint writes to a real business system.
- Townsville data is monthly aggregate reference context, not Hobart demand or individual requests.
- Every D6 decision, review, actor, time and operational metric is fixed-seed synthetic data. Its rates exercise calculations and are not council performance or model-accuracy evidence.
- The public demo ledger is local to one container replica and resets after scale-to-zero or revision replacement; it is demonstration trace state, not retained audit evidence.
- F23 proves one bounded Azure portfolio deployment and named negative-route checks; it does not establish production readiness, authenticated users, durable audit retention, actual idle scale-to-zero or zero cost.
A one-page summary of the project, made in October 2026. The screen on it is the v0.7.2 console, and the figures are the ones reported in this README.
The next slice is adversarial and secure-delivery verification: ambiguous/injection-style inputs, cross-case paths, SAST and container vulnerability gating. Authentication/RBAC, Windows enterprise lab integration and PostgreSQL remain later stages and are not represented as completed. The live Azure demo remains within the enabled Student spending limit, with no subscription upgrade or claim of remaining sponsorship credit.
For a real organisation to use something like this, it would also need authenticated identities and roles, a durable and independently protected audit store, connectors to its own systems, and its own approved policies in place of the demonstration policy. None of that is built.
- A decision is only as trustworthy as the record of how it was made.
- Putting the policy check outside the model keeps the outcome explainable whichever model is used later.
- Labelling every figure as real or synthetic prevents a test fixture from being read as performance.
- Keeping failed runs public costs little and makes the passing ones believable.



