Skip to content
Open
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
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,22 @@ Built with [Tauri](https://tauri.app/), [SQLite](https://www.sqlite.org/index.ht
[Invoice editing](https://www.upcount.app/screenshots/invoice-edit.png)
[Invoice settings](https://www.upcount.app/screenshots/settings.png)

## Governed AI access (optional)

Upcount is local-first with no API, so the only ways an AI assistant could *operate* it today are
screen-scraping or raw SQLite access — both ungoverned. The optional [`kriya-mcp/`](kriya-mcp/)
integration instead exposes Upcount's existing actions to an assistant (e.g. Claude Desktop) as a
**governed MCP server**:

- **Reads** (clients, invoices, time entries) run freely.
- **Routine writes** (create/update clients, tax rates, tags, projects, time entries) are recorded in a signed audit log.
- **Money & destructive** actions (create/update/issue an invoice, and any `delete_*`) pause for **human approval**.
- A per-minute **budget** caps a runaway agent; anything not allow-listed is **denied by default**.

It's **off by default** and changes nothing in the app: the `kriya_exec` helper binary reuses the
exact async `Database` methods the UI already calls, and all governance lives in the external
`kriya-mcp` process. See [`kriya-mcp/README.md`](kriya-mcp/README.md) to enable it.

## Download

Upcount is available for Mac, Linux & Windows and can be downloaded from Github releases.
Expand Down
20 changes: 20 additions & 0 deletions kriya-mcp/.mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"//": "TEMPLATE — copy the mcpServers.upcount block into your assistant's MCP config and replace the /ABSOLUTE/PATH placeholders. For Claude Desktop on macOS that file is ~/Library/Application Support/Claude/claude_desktop_config.json. See kriya-mcp/README.md.",
"mcpServers": {
"upcount": {
"command": "kriya-mcp",
"args": [
"--persistent",
"--name", "upcount",
"--exec", "/ABSOLUTE/PATH/TO/upcount/src-tauri/target/release/kriya_exec",
"--tools", "/ABSOLUTE/PATH/TO/upcount/kriya-mcp/tools.json",
"--policy", "/ABSOLUTE/PATH/TO/upcount/kriya-mcp/agent-policy.yaml",
"--approval", "gui",
"--actor", "claude-desktop"
],
"env": {
"UPCOUNT_DB": "/ABSOLUTE/PATH/TO/Library/Application Support/com.upcount.dev/sqlite.db"
}
}
}
}
118 changes: 118 additions & 0 deletions kriya-mcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Governed AI access for Upcount (optional)

This folder lets an AI assistant (Claude Desktop, Cursor, …) safely **operate** Upcount — not by
screen-scraping or opening the raw SQLite file, but by exposing Upcount's *existing* actions as a
**governed [MCP](https://modelcontextprotocol.io) server**.

It is **off by default** and **changes nothing in the app**. If you never wire it into an
assistant, none of this runs.

> Why this exists: Upcount is local-first with no API, so until now there was no *safe* way to let
> an assistant do things in it. This gives the agent a narrow, permissioned, audited door instead
> of ungoverned database access.

---

## What you get

- 🔒 **Permissions** — the agent can only call the actions you allow-list (everything else denied).
- ✋ **Human approval** — financial-document actions (create/update/issue an invoice) and anything
destructive (`delete_*`) pause for a one-click yes/no before they touch your books.
- 🧾 **Signed audit log** — every executed agent action is an Ed25519-signed receipt you can replay.
- 💸 **Budget cap** — at most N actions per rolling minute, so a looping agent can't run away.

## How it works

```
Claude Desktop ──MCP/stdio──▶ kriya-mcp ──one JSON line per action──▶ kriya_exec
(the agent) (governor) (Upcount's data layer)
│ │
policy ▸ approval ▸ budget ▸ audit reuses Upcount's async
(agent-policy.yaml) Database methods → sqlite.db
```

- **`kriya_exec`** (`src-tauri/src/bin/kriya_exec.rs`) is a small second binary in *this* crate.
It does **no** business logic of its own — it pulls in the very same `db` module the Tauri
commands use, so an AI tool-call runs the **identical async `Database` method** a human action
does. No second implementation, nothing new to trust.
- **`kriya-mcp`** is the external governor (from the open-source [`kriya`](https://crates.io/crates/kriya)
crate). It speaks MCP to the assistant and, for every call, enforces policy → approval → budget →
signed-audit **before** forwarding the cleared action to `kriya_exec`.

## Enable it

1. **Build the executor** (release):
```bash
cd src-tauri
cargo build --release --bin kriya_exec # → src-tauri/target/release/kriya_exec
```
2. **Install the governor**:
```bash
cargo install kriya # provides the `kriya-mcp` binary on your PATH
```
3. **Point it at your data.** By default `kriya_exec` opens the same database the app uses
(`<app data dir>/com.upcount.dev/sqlite.db`). To target a copy, set `UPCOUNT_DB` (a path or a
full `sqlite://` URL) or pass `--db <path>`.
4. **Wire it into your assistant.** Copy the `mcpServers.upcount` block from [`.mcp.json`](.mcp.json)
into your assistant's MCP config (Claude Desktop on macOS:
`~/Library/Application Support/Claude/claude_desktop_config.json`), replacing the
`/ABSOLUTE/PATH/...` placeholders. Restart the assistant.
5. Ask something read-only first, e.g. *"List my unpaid invoices for <org>."* Then try an invoice
create and watch the approval prompt appear.

## Governance model

| Tier | Actions | Policy |
|---|---|---|
| Read | `get_*` | allow (no prompt) |
| Routine write | `create_*` / `update_*` for clients, organizations, tax rates, tags, time entries, projects | allow + audit |
| Money / financial document | `create_invoice`, `update_invoice`, `update_invoice_state` | **human approval** + audit |
| Destructive | `delete_*` (client, invoice, organization, tax rate, tag, time entry) | **human approval** + audit |
| Anything else | — | **denied** |

Edit [`agent-policy.yaml`](agent-policy.yaml) to tighten or loosen this (e.g. set an action to
`allow: false` to forbid it outright, or drop `require_approval` to let it run audited).

## Notes for agents

- **IDs are optional on create** — `kriya_exec` mints a nanoid when you omit `id`. (You may still
pass an explicit `id`.) Foreign keys are enforced, so create an organization and client before an
invoice, and pass their ids as `organizationId` / `clientId`.
- **Money is integer minor units (cents)** — `total`, `taxTotal`, `subTotal`, and line-item
`unitPrice` are cents (e.g. `1000` = 10.00).
- **Invoice `number` is caller-supplied** — Upcount's numbering/format logic lives in the UI, so an
agent-created invoice must pass `number` itself.
- Field names follow Upcount's own schema: mostly camelCase (`organizationId`, `clientId`,
`dueDate`, …) with a few snake_case (`registration_number`, `bank_name`, `date_format`). See
[`tools.json`](tools.json) for each action's exact shape.

## Approval on each OS

`--approval gui` is a native macOS dialog that works even though Claude Desktop has no terminal.
On **Linux/Windows** there is no GUI gate yet, so either run `kriya-mcp` from a terminal with
`--approval tty`, or keep the default and know that approval-required actions will simply be
**denied** when there's no way to ask a human — the safe failure mode. Reads and routine writes are
unaffected.

## Audit log

Executed actions are appended as signed JSONL receipts to `$TMPDIR/kriya-audit.jsonl` (override with
`kriya-mcp --audit-log <path>`). Each receipt is attributable to the `--actor` you set.

## Adding an action

1. Add a `match` arm in `dispatch()` in `src-tauri/src/bin/kriya_exec.rs` calling the relevant
`db` method.
2. Add a tool entry to [`tools.json`](tools.json).
3. Add a rule to [`agent-policy.yaml`](agent-policy.yaml) deciding its tier.

## What this does **not** do

- It adds **no new dependency** to the app and changes nothing in the app binary/runtime —
`kriya_exec` is a separate binary and the governor is a separate process.
- It makes no network calls (beyond the local stdio MCP pipe) and adds no telemetry.
- It exposes nothing not listed in `tools.json`; the policy denies everything else. `backup_database`
/ `restore_database` are intentionally **not** exposed (they need the Tauri GUI dialog).

This integration is contributed under Upcount's GPL-3.0 license; `kriya-mcp` runs as a separate
process and is not linked into the app.
41 changes: 41 additions & 0 deletions kriya-mcp/agent-policy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Governance policy for Upcount's optional governed-MCP access. kriya-mcp enforces this on every
# tools/call from the external agent — the agent can only *propose*; this policy decides.
#
# Posture: reads flow freely; financial documents (invoices) and anything destructive pause for
# on-device human approval; routine record-keeping is allowed but audited; everything else is
# denied by default; and a per-minute cap stops a runaway agent. First matching rule wins.
rules:
# Reads — safe to run unattended.
- action: "get_*"
allow: true

# Money / financial documents — pause for a human's go-ahead, every time.
- action: "create_invoice"
allow: true
require_approval: true
- action: "update_invoice"
allow: true
require_approval: true
- action: "update_invoice_state" # issuing / marking sent|paid|void is a financial-state change
allow: true
require_approval: true

# Anything destructive — pause for approval.
- action: "delete_*"
allow: true
require_approval: true

# Routine record-keeping (clients, organizations, tax rates, tags, time entries, projects) —
# allowed, and every one is recorded in the signed audit log.
- action: "create_*"
allow: true
- action: "update_*"
allow: true

# Everything else: denied (deny-by-default). An agent cannot invoke anything not listed above.
- action: "*"
allow: false

budget:
# A looping agent can't hammer your books: at most 30 actions per rolling minute.
max_actions_per_minute: 30
Loading