SafetyClaw
Technical architecture review
How SafetyClaw keeps AI helpful without making it an accountant with a write token.
This document is for engineers reviewing the codebase. It explains the trust boundary, tool layers, data flow, state transitions, review targets, safety invariants, and the places most likely to deserve careful attention before wider release.
Executive summary
The core boundary is proposal first, human publish last.
SafetyClaw is a Python and SQLite control plane between AI agents and QuickBooks Online. Its central rule is simple: AI may read bounded context and create draft work, but it may not approve, publish, roll back, enable live writes, or obtain unilateral QuickBooks write authority.
Every agent, upload, parser, model connection, and OpenClaw integration enters the same safety path: structured work becomes a SafetyClaw sandbox proposal. The proposal is visible in Review & Publish, where a human can reject it, approve it, and then perform a separate final publish step. The publish step is browser-session owned and rechecks policy, proposal freshness, QuickBooks realm, and live QuickBooks drift before writing.
The latest handoff build is
20260525-personal-topn-readback-scope. The latest
independent live-Chrome canonical measurement scored 9.5/10
raw and calibrated to 10/10 actionable after excluding
sandbox-data precondition rows. That is good evidence for limited
sandbox human testing, not a claim of production readiness.
Review scope
What a code reviewer should be trying to prove.
The code review should not only ask whether the app works. It should ask whether the safety boundary remains true under messy inputs, retries, stale browser tabs, missing QuickBooks entities, provider errors, and ambiguous human language.
- The agent can read only the bounded context it is supposed to read.
- The agent can create proposals but cannot publish or approve them.
- Every proposal contains enough evidence for a human to review it.
- Duplicate retries collapse instead of creating noisy repeated work.
- Policy is checked at creation, approval, and publish time.
- Publish fails closed if the approved proposal changed underneath the human.
- Publish fails closed if QuickBooks live state drifted after proposal creation.
- Rendered chat answers do not invent proposal IDs, queue contents, or publish status.
- High-risk and unsupported workflows remain proposal-only or out of scope.
Repository map
The codebase is a modular package with a compatibility shell.
The original MVP concentrated much of the product in
app/server.py. The current codebase has been decomposed
into app/safetyclaw/. server.py remains the
HTTP entrypoint and compatibility surface, but most behavior lives in
named subpackages.
app/
server.py entrypoint and legacy compatibility shell
assistant.py bounded upload and parser helpers
qbo_action_catalog.py action catalog constants
qbo_hold_reasons.py hold reason taxonomy
safetyclaw/
api/ HTTP routes and state assembly
agent/ persona and agent runtime glue
audit/ audit event writer
auth/ user and session lifecycle
llm/ classifier prompts, schemas, provider adapters
mcp/ MCP adapter
parsers/ deterministic invoice, bill, payment parsers
qbo/ QuickBooks OAuth, sync, resolver, secrets
render/ human-readable labels and prose helpers
runtime/ environment helper primitives
safety/ preflight, policy, capability registry
sandbox/ proposals, queue, publish, rollback, work orders
skills/ skill package registry
storage/ SQLite connection and DDL helpers
workflows/ dispatch executors and deterministic readbacks
app/static/
index.html single-page app shell and Help page
app.js browser behavior
styles.css app and document styles
whitepaper.html product/safety white paper
technical-architecture-review.html this document
qa/
architecture_fitness_safetyclaw.py architecture gate
safety_kernel_guards.py safety kernel regression gate
*.py focused deterministic regression gates
tester_kit/ live Chrome trust kit definitions
Four tool layers
Tool power increases one layer at a time.
SafetyClaw makes the data-manipulation boundary visible by splitting tools into layers. The names guide callers, and backend policy enforces the boundary even if a caller ignores the names.
Layer 1: read tools
Read tools answer questions from SafetyClaw state and the synced QuickBooks snapshot. Examples include snapshot summaries, aging reports, customer search, vendor search, item search, and recent invoice metadata. These tools are safe to expose to agents because they have no write path.
Examples: safetyclaw.read.qbo_snapshot_summary safetyclaw.read.report_aging safetyclaw.read.report_pl safetyclaw.read.search_customers safetyclaw.read.search_vendors safetyclaw.read.search_items safetyclaw.read.list_recent_invoices Allowed: read synced snapshot rows read queue summaries that belong to the user answer accounting context questions Forbidden: create proposal rows approve or reject proposals publish to QuickBooks alter policy or credentials
Layer 2: propose tools
Propose tools turn structured intent into a sandbox proposal. A proposal is SafetyClaw state, not a QuickBooks write. Proposals carry action type, source metadata, before and after state, amount, validation checks, source key, and the canonical write plan hash used later at approval and publish.
Examples: safetyclaw.propose.invoice safetyclaw.propose.bill safetyclaw.propose.customer_payment safetyclaw.propose.customer_create safetyclaw.propose.vendor_create safetyclaw.propose.delete_or_void Allowed: create sandbox_changes rows attach source labels and evidence hold rows that need clarification deduplicate active proposals by source key Forbidden: write to QuickBooks approve the proposal publish the proposal bypass Review & Publish
Layer 3: work-order tools
Work-order tools exist for messy work: a pasted invoice batch, a multi-row CSV, a long follow-up chain, or a batch with some complete rows and some held rows. The work-order ledger records expected rows, parsed rows, created proposals, held rows, duplicates, math holds, skipped rows, and unaccounted rows.
This layer is important because an AI assistant can otherwise say "done" after processing only part of a batch. The work-order ledger gives the renderer a durable truth source for counts and proposal IDs.
Examples: safetyclaw.work_order.status safetyclaw.work_order.list_records safetyclaw.propose.work_order_append_records Allowed: record batch progress link records to proposal IDs report missing or held rows detect unaccounted rows Forbidden: silently mark missing work as complete publish any row directly treat a work order as human approval
Layer 4: publish tools
Publish tools are not agent tools in practice. They are the live QuickBooks write boundary and are only reachable through the authenticated browser UI after a human has approved proposals and prepared a one-time final publish confirmation. Agent tokens, MCP callers, OpenClaw, and chat routes are refused.
Examples: safetyclaw.publish.invoice safetyclaw.publish.bill safetyclaw.publish.customer_payment_exact safetyclaw.publish.purchase_category_cleanup safetyclaw.publish.customer_create_unique safetyclaw.publish.vendor_create_unique safetyclaw.publish.item_create_unique Required before write: authenticated web session approved proposal one-time final publish confirmation current policy still allows it write_plan_hash still matches QuickBooks realm still matches live QuickBooks state has not drifted unexpectedly Forbidden: direct publish by AI direct publish by MCP or OpenClaw publish without final browser confirmation publish unsupported action types
Request flow
One user message becomes either an answer, a hold, or proposals.
The agent path is intentionally staged. The earliest layer catches unsafe direct-write instructions before an LLM call. The LLM classifier decides the broad route. Deterministic executors create proposals, answer readback questions, or ask for clarification.
Browser / external agent
-> HTTP route in app/safetyclaw/api/
-> authenticate user or bearer token
-> load user state, policy, snapshot, connection metadata
-> safety preflight
direct publish / approve / rollback bypass attempts stop here
-> dispatch classifier
bills_batch
payments_batch
invoice_batch
workflow_dispatch
verify_creation
state_query
general_answer
decline
-> executor
create proposals, answer readbacks, ask for more info, or decline
-> renderer
final answer plus streaming events and proposal chips
-> audit log
The important review point is that the LLM chooses an intent route, but it does not directly mutate QuickBooks. The business payload still passes through proposal creation, canonicalization, validation, source key idempotency, policy review, and Review & Publish.
Proposal lifecycle
The proposal is the data-manipulation surface.
SafetyClaw treats proposals as durable work objects. The proposal row stores what the source wanted to do, what action type it maps to, what references it depends on, what validation found, and what a human would be approving.
Normal lifecycle:
proposed
-> approved
-> publishing
-> committed
Other terminal or holding states:
blocked proposal exists, but depends on missing required information
rejected human rejected it
undone local queue undo, not a QuickBooks rollback
stale no longer publishable under current expected state
A held workflow should still become visible work where possible. For example, if an invoice depends on a missing customer, the customer creation proposal and the blocked invoice proposal can both be visible in the queue, with a dependency between them. That is better than hiding the financial work in chat memory.
Publish boundary
Approval is not publishing, and publishing is not an agent capability.
Publishing is deliberately the narrowest part of the system. A human first approves one or more proposals. Then the browser prepares a final publish confirmation. Then the browser submits that one-time token to perform the write. The backend rechecks everything before it calls QuickBooks.
Human browser only:
approve selected proposals
-> prepare final publish confirmation
stores selected IDs, expiration, and one-time token
-> submit final publish token
recheck user session
recheck policy
recheck write_plan_hash
recheck realm_id
re-read live QuickBooks state
claim row as publishing
call action-specific publisher
audit result
commit or fail closed
Reviewers should verify that no external agent surface exposes this boundary. External bearer-token tools can create proposals. They cannot approve them, publish them, roll them back, or enable live writes.
Account and authentication
The publish boundary assumes a real, verified human; here is how that human is authenticated.
The account layer lives in app/safetyclaw/auth/ and is
standard-library only — no third-party authentication, JWT, or
cryptography packages. Sessions, password hashes, verification and
reset tokens, and Google bindings are ordinary SQLite rows,
inspectable the same way as the rest of the control plane.
auth/__init__.py; a shared validate_new_password enforces a 12-character floor and an HIBP k-anonymity breach check (auth/breach.py, 5-char SHA-1 prefix, fail-open with an auth.breach_check_unavailable audit).
require_verified_user() gates connect, propose, and publish across nine routes; login itself stays open. Email verification uses single-use, hash-only tokens (auth/tokens.py).
auth/google.py: OAuth2 + PKCE/S256, a single-use state table. Identity is read from Google's UserInfo endpoint over TLS using an access token this client minted through its own authenticated code exchange (client secret + PKCE) — so the token is bound to this client, rather than verifying a local id_token signature (the no-crypto-dependency choice). Linking refuses an unverified-email match to close pre-registration takeover.
auth/throttle.py applies per-IP and per-account backoff ahead of PBKDF2 on login and signup. Client IP comes only from nginx's X-Real-IP / the rightmost X-Forwarded-For hop; client-supplied (leftmost) values are never trusted. State is in-memory per process (see Known risks).
?token= and OAuth code= values out of journald logs.
Reviewers and penetration testers: the design choices above
— UserInfo-over-TLS instead of local JWT verification, HIBP as
the only outbound dependency, fail-open breach checking — are
deliberate and documented in
docs/ACCOUNT_SYSTEM_V2_PLAN.md. The closed
response-header-injection finding in the Google ?next=
handler is recorded in docs/KNOWN_FINDINGS.md; the
in-memory throttle limitation is listed under Known risks below.
Safety invariants
These rules should survive every refactor.
- Proposal boundary: every state-changing accounting action must exist as a SafetyClaw proposal before human approval.
- No agent publish: LLM callers, MCP clients, OpenClaw, and bearer-token API callers cannot acquire approve, publish, or rollback capabilities.
-
Hash binding: approval binds the reviewed
write_plan_hash; publish recomputes it and fails closed if it changed. - Realm binding: the proposal realm must match the user's currently connected QuickBooks realm before publish.
- SyncToken drift check: supported publish and rollback adapters re-read live QuickBooks records and refuse unexpected drift.
- Policy recheck: policy is not just creation-time metadata; it is checked again at approval and publish.
- Queue truth: assistant answers must cite real active proposal IDs and must not report hidden stale or rejected rows as current queue work.
- One renderer contract: user-facing proposal prose should come through shared humanizers and render helpers, not duplicated string fragments.
Policy and capabilities
Safety is enforced by capabilities, not by trusting model behavior.
SafetyClaw has a capability registry and policy review layer. The policy layer distinguishes read, propose, approve, publish, rollback, and human-only capabilities. Agents can get read and propose surfaces. Human-only transitions require the authenticated browser path.
Review points:
app/safetyclaw/safety/preflight.py
direct unsafe prompt interception
app/safetyclaw/safety/capabilities.py
capability names and human_only flags
app/safetyclaw/safety/policy.py
user policy, live-write toggle, thresholds
app/safetyclaw/safety/policy_review.py
policy decisions and block details
app/safetyclaw/sandbox/commit.py
final publish confirmation and commit orchestration
app/safetyclaw/sandbox/publishers.py
action-specific live QuickBooks adapters
A useful review technique is to search for any path that reaches a
publisher without going through commit.py, final
confirmation, policy recheck, freshness check, and human session
ownership. There should not be one.
Entity resolution
Names are resolved conservatively because accounting ambiguity is expensive.
Many failures in accounting agents come from mapping a casual name to the wrong QuickBooks entity. SafetyClaw uses deterministic resolution against synced QuickBooks snapshots and active pending proposals. It can resolve unique shorthand in some cases, but it should hold and ask when the target is missing, ambiguous, or unsafe.
The paired-invariant to review is proposal-time canonicalization versus publish-time resolution. Proposal creation can be helpful and forgiving. Publish must be strict.
Readback system
Readbacks are deterministic because users audit the assistant through them.
A major recovery theme was readback honesty. Users ask questions like "what did I draft?", "did that invoice go through?", "what's in my queue?", or "top vendors I billed this year." These answers must be built from actual SafetyClaw and QuickBooks state. The system now has deterministic readback helpers for queue state, session state, calendar periods, entity activity, vendor history, customer AR, open AP, tax-season summaries, and broad SafetyClaw history.
Review points:
app/safetyclaw/workflows/state_query.py
queue/session/calendar/top-N/current-history rendering
app/safetyclaw/workflows/verify_creation.py
"did it happen?" answer path and ambiguity safety net
app/safetyclaw/workflows/ar_readback.py
customer AR details and payment/refund context
app/safetyclaw/workflows/vendor_history.py
vendor-side history and held bill context
app/safetyclaw/workflows/snapshot_aggregates.py
synced-QBO aggregate readbacks
app/safetyclaw/workflows/tax_summary.py
multi-query tax-season readback
The most important rule is that cited proposal IDs must be real and relevant. Hidden stale, rejected, committed, or unrelated rows should not appear as current queue work.
Test strategy
The tests mix unit-style gates with live browser trust measurement.
SafetyClaw uses several kinds of testing because AI accounting systems fail in several ways. Unit-style tests catch deterministic regressions. Architecture fitness catches forbidden implementation patterns. Browser tests catch real UI and conversation behavior. Live sandbox tests catch QuickBooks integration behavior.
The current canonical evidence is strong for limited human testing: two recent canonical runs above 9/10, no P0/P1 clusters in run-54, no forbidden behavior, clean cited-ID checks, and final queue cleanup. The remaining proof gap is fresh year-long endurance and chaos testing on the final build.
Useful local checks: python3 qa/architecture_fitness_safetyclaw.py python3 qa/safety_kernel_guards.py python3 -m py_compile app/server.py app/assistant.py node --check app/static/app.js Useful live checks: curl -sS https://aifdoggydog.openclawsystems.com:8443/api/health curl -sS https://ziosec.openclawsystems.com/api/health verify Review & Publish shows 0 pending / 0 approved in Chrome
Highest-value review targets
Where to spend review time first.
-
app/safetyclaw/llm/prompts.py: the dispatch classifier prompt is the planner. Review for overlapping rules, prompt accretion, and unclear schema obligations. -
app/safetyclaw/workflows/workflow_dispatch.py: maps structured action intent to proposal creation. Review for correct action splitting and safe follow-up behavior. -
app/safetyclaw/sandbox/proposals.py: proposal creation, canonicalization, source-key idempotency, and reference checks. This is a load-bearing boundary. -
app/safetyclaw/sandbox/canonical.py: write-plan hash and freshness logic. Review for stability and symmetry with publish. -
app/safetyclaw/sandbox/commit.py: approval-to-publish orchestration, final confirmation, stale guard, and publish claim behavior. -
app/safetyclaw/sandbox/publishers.py: QuickBooks write adapters. Review for drift checks, strict resolution, rollback metadata, and idempotent request IDs. -
app/safetyclaw/auth/: the Account System v2 layer and the imminent pentest target —google.py(OAuth/PKCE and the UserInfo identity step),tokens.py(single-use verification and reset tokens),throttle.py(brute-force limits), and therequire_verified_usergate. Review the account-linking matrix, token replay, and per-route gate coverage. -
app/safetyclaw/workflows/state_query.py: deterministic readback rendering. Review for scope drift and helper proliferation. -
app/safetyclaw/qbo/resolver.py: customer/vendor/item/account matching. Review for unsafe fuzzy matches and missing ambiguity holds. -
app/safetyclaw/safety/: preflight, policy, and capabilities. Review for any accidental grant of human-only capabilities to agents. -
scripts/deploy_vps.sh: deploy gate. Each test explains a past failure class. Review whether the gate still reflects the current risk profile.
Known risks
The architecture is strong, but not magic.
Release readiness
Current recommendation: limited sandbox human testing.
The current evidence supports limited human testing in sandbox QuickBooks accounts. It does not yet support broad customer rollout. Since this review was first written, the account and authentication layer described above has been built to production grade (Account System v2) and deployed, and an external penetration test of the live build is the next gate.
A concise meeting summary: SafetyClaw is doing the right safety thing. It lets AI prepare bookkeeping work while keeping actual QuickBooks writes behind a human-controlled review and publish gate. The next phase should validate durability, usability, security, and accounting judgment before production release.