pywrit 0.2.3__tar.gz → 0.2.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pywrit-0.2.5/PKG-INFO ADDED
@@ -0,0 +1,216 @@
1
+ Metadata-Version: 2.4
2
+ Name: pywrit
3
+ Version: 0.2.5
4
+ Summary: Python client + CLI for Writ — the gate before the write
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/AvenueDAdmin/pywrit
7
+ Project-URL: Documentation, https://withwrit.com/docs
8
+ Requires-Python: >=3.9
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: httpx>=0.27
12
+ Provides-Extra: polyglot
13
+ Requires-Dist: tree-sitter>=0.23; extra == "polyglot"
14
+ Requires-Dist: tree-sitter-typescript>=0.23; extra == "polyglot"
15
+ Requires-Dist: tree-sitter-javascript>=0.23; extra == "polyglot"
16
+ Provides-Extra: keywords
17
+ Requires-Dist: ai-agents; extra == "keywords"
18
+ Requires-Dist: policy; extra == "keywords"
19
+ Requires-Dist: audit-log; extra == "keywords"
20
+ Requires-Dist: guardrails; extra == "keywords"
21
+ Dynamic: license-file
22
+
23
+ # pywrit
24
+
25
+ **Find the dangerous writes in your Python agent's code, then gate it.** `pywrit` is the Python
26
+ client and `writ` CLI for [Writ](https://withwrit.com): an allow/deny gate that sits
27
+ in front of your agent's consequential writes (database, HTTP, files, email, queues,
28
+ AWS) and records a hash-chained receipt for every decision.
29
+
30
+ ![writ scan finds 4 write sites in a Python agent (0/4 gated); writ scan --apply inserts gates; a re-scan shows 4/4 gated](https://raw.githubusercontent.com/AvenueDAdmin/pywrit/main/docs/assets/scan-python.gif)
31
+
32
+ `writ scan` is **local, deterministic, and free**: it parses your code with Python's
33
+ `ast` module, makes no network calls, and needs no API key.
34
+
35
+ ## Install
36
+
37
+ ```bash
38
+ pip install pywrit
39
+ ```
40
+
41
+ Requires Python 3.9+. Installs the `writ` command and the `pywrit` Python client.
42
+
43
+ ## 60-second quickstart: scan → apply → gate
44
+
45
+ **1. Scan.** See which functions write, and how many of them are gated.
46
+
47
+ ```bash
48
+ writ scan .
49
+ ```
50
+
51
+ ```text
52
+ writ scan: /path/to/support-agent
53
+ files scanned: 4 skipped: 0
54
+ write sites: 4 in 3 function(s)
55
+ gated: 0/4 (0%)
56
+ verbs discovered: 3
57
+ crm.update
58
+ payments.refund
59
+ tickets.close
60
+ ```
61
+
62
+ It also prints a **risk report** (0-100, weighted by risk tier), writes the discovered
63
+ verbs to `writ-policy.json`, and shows the instrumentation it would add as a unified
64
+ diff. Nothing in your code changes yet. `writ scan . --score` prints just the risk report.
65
+
66
+ **2. Apply.** Insert a gate at the top of each writing function.
67
+
68
+ ```bash
69
+ writ scan . --apply # shows the diff, then asks before writing
70
+ writ scan . --apply --yes # no prompt (e.g. in CI)
71
+ ```
72
+
73
+ Each gated function now asks Writ before it writes, and fails closed:
74
+
75
+ ```python
76
+ def issue_refund(charge_id, amount_cents):
77
+ if _writ_check("payments.refund") != "ALLOW":
78
+ raise PermissionError("writ denied payments.refund")
79
+ ...
80
+ ```
81
+
82
+ Re-run `writ scan .` and you'll see `gated: 4/4 (100%)`.
83
+
84
+ **3. Gate.** Get a free API key, load the discovered policy, and run your agent.
85
+
86
+ ```bash
87
+ writ key --email you@example.com # free API key + tenant
88
+ export WRIT_API_KEY=writ_... # read by the inserted gate
89
+ export WRIT_SPONSOR=acme WRIT_AGENT=support-agent # optional: who is acting
90
+ writ scan . --push-policy --key "$WRIT_API_KEY" # upload the discovered verb policy
91
+ ```
92
+
93
+ Every gated write now gets `ALLOW`, `DENY`, or `STEP_UP` (a human sponsor must
94
+ approve), and every decision is written to your tenant's tamper-evident audit log:
95
+
96
+ ```bash
97
+ writ receipts --key "$WRIT_API_KEY" # latest receipts
98
+ writ verify-chain --key "$WRIT_API_KEY" # verify the receipt hash chain
99
+ writ stream --key "$WRIT_API_KEY" # tail decisions live
100
+ writ report --key "$WRIT_API_KEY" # Agent Action Report
101
+ ```
102
+
103
+ ## What `writ scan` detects
104
+
105
+ Python (`.py`) files, parsed with the stdlib `ast` (no extra dependencies):
106
+
107
+ | Category | Examples |
108
+ |---|---|
109
+ | Database | `cursor.execute(...)` / `executemany` / `executescript` (write SQL only; `SELECT`/`WITH`/... skipped), `session.add` / `commit` / `delete` / `merge` / `flush` |
110
+ | HTTP | `requests.post` / `put` / `patch`, `client.delete(...)`, `session.request(...)` |
111
+ | Files | `open(..., "w"/"a"/"x"/"+")`, `Path.write_text` / `write_bytes` / `unlink` / `rename`, `os.remove` / `rename` / `makedirs`, `shutil.rmtree` / `move` / `copy` |
112
+ | Email | `sendmail`, `send_message`, `send_email` |
113
+ | Queues | `publish`, `produce`, `enqueue`, `queue.send(...)` |
114
+ | AWS SDK | `put_object`, `put_item`, `delete_item`, `upload_file`, `send_message`, `start_execution`, ... |
115
+
116
+ - Each write is mapped to a verb such as `payments.refund` or `crm.update`, inferred
117
+ from the file path and function name.
118
+ - A function that already calls `writ_check(...)` / `_writ_check(...)` counts as gated.
119
+ - Skipped: tests, hidden directories, virtualenvs, `node_modules`, `dist`, `build`.
120
+ Use `--exclude SUBSTR` (repeatable) to skip more.
121
+ - Not covered (review by hand): writes behind dynamically built SQL, third-party SDK
122
+ calls such as `stripe.Refund.create(...)`, deferred task queues, or shared clients
123
+ several layers down. The risk report lists these gaps every time.
124
+
125
+ ### TypeScript / JavaScript (scan-only)
126
+
127
+ `writ scan` also finds write sites in TypeScript and JavaScript. It needs the
128
+ optional extra (tree-sitter based):
129
+
130
+ pip install 'pywrit[polyglot]'
131
+ writ scan .
132
+
133
+ Without the extra, the scanner prints a one-line hint and keeps going —
134
+ Python scanning never needs it.
135
+
136
+ TS/JS is **scan-only**: findings are listed with verbs for your policy file,
137
+ but `writ scan --apply` never rewrites TS/JS files — gate those by hand.
138
+ Covered patterns: `fetch`/`axios` writes, `fs` writes, SQL through knex-style
139
+ clients, Prisma writes, and JS SDK calls such as `stripe.refunds.create(...)`.
140
+ (Still not covered: the *Python* SDK equivalents like `stripe.Refund.create(...)`
141
+ — see "Not covered" above.)
142
+
143
+ ![writ scan on a TypeScript agent finds 5 write sites, 1 of 5 gated](https://raw.githubusercontent.com/AvenueDAdmin/pywrit/main/docs/assets/scan-tsjs.gif)
144
+
145
+ ## Python client
146
+
147
+ ```python
148
+ from pywrit import WritClient
149
+
150
+ client = WritClient(api_key="writ_...")
151
+
152
+ result = client.check(
153
+ sponsor_id="acme",
154
+ agent_id="agent-7",
155
+ verb="db.write",
156
+ target="prod.customers",
157
+ purpose="backfill region field",
158
+ )
159
+
160
+ if result.decision == "ALLOW":
161
+ # result.auth_token is a short-lived token bound to this exact write
162
+ perform_write(...)
163
+ elif result.decision == "STEP_UP":
164
+ # a human sponsor must approve first: client.grant(...), then re-check
165
+ ...
166
+ else:
167
+ # DENY
168
+ ...
169
+ ```
170
+
171
+ No API key yet? Try the keyless sandbox:
172
+
173
+ ```python
174
+ client = WritClient()
175
+ client.sandbox({
176
+ "sponsorId": "acme",
177
+ "agentId": "agent-7",
178
+ "verb": "demo_write", # sandbox only allows demo_write ...
179
+ "target": "demo-customers", # ... on targets starting with demo-
180
+ "purpose": "trying the gate",
181
+ })
182
+ ```
183
+
184
+ What's covered:
185
+
186
+ - `check(...)`: the gate. `ALLOW` / `DENY` / `STEP_UP`, plus a receipt every time
187
+ - `verify_token(...)`: validate an `ALLOW` auth token (catches purpose drift)
188
+ - `grant(...)`: human-sponsor approval for the `STEP_UP` path
189
+ - `get_policy()` / `set_policy(...)`: manage the tenant policy
190
+ - `revoke(...)` / `reinstate(...)` / `revoked()`: the kill switch
191
+ - `receipts()` / `receipt(id)` / `verify_chain()` / `stream_receipts()`: the audit log
192
+ - `sandbox(...)`: keyless trial, no API key required
193
+
194
+ ## CLI reference
195
+
196
+ ```bash
197
+ writ check --key writ_... --sponsor acme --agent agent-7 \
198
+ --verb db.write --target prod.customers --purpose "backfill region field"
199
+ writ policy --key writ_... --set payments.refund require_grant
200
+ writ revoke --sponsor acme --agent agent-7 --reason "runaway loop" # kill switch (sponsor token)
201
+ writ grant --sponsor acme --agent agent-7 --verb payments.refund \
202
+ --target ch_123 --purpose "approved refund" # STEP_UP approval (sponsor token)
203
+ ```
204
+
205
+ Sponsor-token commands (`revoke`, `reinstate`, `revoked`, `grant`) read
206
+ `--sponsor-token` or `WRIT_SPONSOR_TOKEN`. Run `writ --help` for the full command list.
207
+
208
+ ## Docs
209
+
210
+ Full docs: [docs.withwrit.com](https://docs.withwrit.com) ·
211
+ Quickstart: [docs.withwrit.com/quickstart](https://docs.withwrit.com/quickstart) ·
212
+ Site: [withwrit.com](https://withwrit.com)
213
+
214
+ ## License
215
+
216
+ MIT
pywrit-0.2.5/README.md ADDED
@@ -0,0 +1,194 @@
1
+ # pywrit
2
+
3
+ **Find the dangerous writes in your Python agent's code, then gate it.** `pywrit` is the Python
4
+ client and `writ` CLI for [Writ](https://withwrit.com): an allow/deny gate that sits
5
+ in front of your agent's consequential writes (database, HTTP, files, email, queues,
6
+ AWS) and records a hash-chained receipt for every decision.
7
+
8
+ ![writ scan finds 4 write sites in a Python agent (0/4 gated); writ scan --apply inserts gates; a re-scan shows 4/4 gated](https://raw.githubusercontent.com/AvenueDAdmin/pywrit/main/docs/assets/scan-python.gif)
9
+
10
+ `writ scan` is **local, deterministic, and free**: it parses your code with Python's
11
+ `ast` module, makes no network calls, and needs no API key.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ pip install pywrit
17
+ ```
18
+
19
+ Requires Python 3.9+. Installs the `writ` command and the `pywrit` Python client.
20
+
21
+ ## 60-second quickstart: scan → apply → gate
22
+
23
+ **1. Scan.** See which functions write, and how many of them are gated.
24
+
25
+ ```bash
26
+ writ scan .
27
+ ```
28
+
29
+ ```text
30
+ writ scan: /path/to/support-agent
31
+ files scanned: 4 skipped: 0
32
+ write sites: 4 in 3 function(s)
33
+ gated: 0/4 (0%)
34
+ verbs discovered: 3
35
+ crm.update
36
+ payments.refund
37
+ tickets.close
38
+ ```
39
+
40
+ It also prints a **risk report** (0-100, weighted by risk tier), writes the discovered
41
+ verbs to `writ-policy.json`, and shows the instrumentation it would add as a unified
42
+ diff. Nothing in your code changes yet. `writ scan . --score` prints just the risk report.
43
+
44
+ **2. Apply.** Insert a gate at the top of each writing function.
45
+
46
+ ```bash
47
+ writ scan . --apply # shows the diff, then asks before writing
48
+ writ scan . --apply --yes # no prompt (e.g. in CI)
49
+ ```
50
+
51
+ Each gated function now asks Writ before it writes, and fails closed:
52
+
53
+ ```python
54
+ def issue_refund(charge_id, amount_cents):
55
+ if _writ_check("payments.refund") != "ALLOW":
56
+ raise PermissionError("writ denied payments.refund")
57
+ ...
58
+ ```
59
+
60
+ Re-run `writ scan .` and you'll see `gated: 4/4 (100%)`.
61
+
62
+ **3. Gate.** Get a free API key, load the discovered policy, and run your agent.
63
+
64
+ ```bash
65
+ writ key --email you@example.com # free API key + tenant
66
+ export WRIT_API_KEY=writ_... # read by the inserted gate
67
+ export WRIT_SPONSOR=acme WRIT_AGENT=support-agent # optional: who is acting
68
+ writ scan . --push-policy --key "$WRIT_API_KEY" # upload the discovered verb policy
69
+ ```
70
+
71
+ Every gated write now gets `ALLOW`, `DENY`, or `STEP_UP` (a human sponsor must
72
+ approve), and every decision is written to your tenant's tamper-evident audit log:
73
+
74
+ ```bash
75
+ writ receipts --key "$WRIT_API_KEY" # latest receipts
76
+ writ verify-chain --key "$WRIT_API_KEY" # verify the receipt hash chain
77
+ writ stream --key "$WRIT_API_KEY" # tail decisions live
78
+ writ report --key "$WRIT_API_KEY" # Agent Action Report
79
+ ```
80
+
81
+ ## What `writ scan` detects
82
+
83
+ Python (`.py`) files, parsed with the stdlib `ast` (no extra dependencies):
84
+
85
+ | Category | Examples |
86
+ |---|---|
87
+ | Database | `cursor.execute(...)` / `executemany` / `executescript` (write SQL only; `SELECT`/`WITH`/... skipped), `session.add` / `commit` / `delete` / `merge` / `flush` |
88
+ | HTTP | `requests.post` / `put` / `patch`, `client.delete(...)`, `session.request(...)` |
89
+ | Files | `open(..., "w"/"a"/"x"/"+")`, `Path.write_text` / `write_bytes` / `unlink` / `rename`, `os.remove` / `rename` / `makedirs`, `shutil.rmtree` / `move` / `copy` |
90
+ | Email | `sendmail`, `send_message`, `send_email` |
91
+ | Queues | `publish`, `produce`, `enqueue`, `queue.send(...)` |
92
+ | AWS SDK | `put_object`, `put_item`, `delete_item`, `upload_file`, `send_message`, `start_execution`, ... |
93
+
94
+ - Each write is mapped to a verb such as `payments.refund` or `crm.update`, inferred
95
+ from the file path and function name.
96
+ - A function that already calls `writ_check(...)` / `_writ_check(...)` counts as gated.
97
+ - Skipped: tests, hidden directories, virtualenvs, `node_modules`, `dist`, `build`.
98
+ Use `--exclude SUBSTR` (repeatable) to skip more.
99
+ - Not covered (review by hand): writes behind dynamically built SQL, third-party SDK
100
+ calls such as `stripe.Refund.create(...)`, deferred task queues, or shared clients
101
+ several layers down. The risk report lists these gaps every time.
102
+
103
+ ### TypeScript / JavaScript (scan-only)
104
+
105
+ `writ scan` also finds write sites in TypeScript and JavaScript. It needs the
106
+ optional extra (tree-sitter based):
107
+
108
+ pip install 'pywrit[polyglot]'
109
+ writ scan .
110
+
111
+ Without the extra, the scanner prints a one-line hint and keeps going —
112
+ Python scanning never needs it.
113
+
114
+ TS/JS is **scan-only**: findings are listed with verbs for your policy file,
115
+ but `writ scan --apply` never rewrites TS/JS files — gate those by hand.
116
+ Covered patterns: `fetch`/`axios` writes, `fs` writes, SQL through knex-style
117
+ clients, Prisma writes, and JS SDK calls such as `stripe.refunds.create(...)`.
118
+ (Still not covered: the *Python* SDK equivalents like `stripe.Refund.create(...)`
119
+ — see "Not covered" above.)
120
+
121
+ ![writ scan on a TypeScript agent finds 5 write sites, 1 of 5 gated](https://raw.githubusercontent.com/AvenueDAdmin/pywrit/main/docs/assets/scan-tsjs.gif)
122
+
123
+ ## Python client
124
+
125
+ ```python
126
+ from pywrit import WritClient
127
+
128
+ client = WritClient(api_key="writ_...")
129
+
130
+ result = client.check(
131
+ sponsor_id="acme",
132
+ agent_id="agent-7",
133
+ verb="db.write",
134
+ target="prod.customers",
135
+ purpose="backfill region field",
136
+ )
137
+
138
+ if result.decision == "ALLOW":
139
+ # result.auth_token is a short-lived token bound to this exact write
140
+ perform_write(...)
141
+ elif result.decision == "STEP_UP":
142
+ # a human sponsor must approve first: client.grant(...), then re-check
143
+ ...
144
+ else:
145
+ # DENY
146
+ ...
147
+ ```
148
+
149
+ No API key yet? Try the keyless sandbox:
150
+
151
+ ```python
152
+ client = WritClient()
153
+ client.sandbox({
154
+ "sponsorId": "acme",
155
+ "agentId": "agent-7",
156
+ "verb": "demo_write", # sandbox only allows demo_write ...
157
+ "target": "demo-customers", # ... on targets starting with demo-
158
+ "purpose": "trying the gate",
159
+ })
160
+ ```
161
+
162
+ What's covered:
163
+
164
+ - `check(...)`: the gate. `ALLOW` / `DENY` / `STEP_UP`, plus a receipt every time
165
+ - `verify_token(...)`: validate an `ALLOW` auth token (catches purpose drift)
166
+ - `grant(...)`: human-sponsor approval for the `STEP_UP` path
167
+ - `get_policy()` / `set_policy(...)`: manage the tenant policy
168
+ - `revoke(...)` / `reinstate(...)` / `revoked()`: the kill switch
169
+ - `receipts()` / `receipt(id)` / `verify_chain()` / `stream_receipts()`: the audit log
170
+ - `sandbox(...)`: keyless trial, no API key required
171
+
172
+ ## CLI reference
173
+
174
+ ```bash
175
+ writ check --key writ_... --sponsor acme --agent agent-7 \
176
+ --verb db.write --target prod.customers --purpose "backfill region field"
177
+ writ policy --key writ_... --set payments.refund require_grant
178
+ writ revoke --sponsor acme --agent agent-7 --reason "runaway loop" # kill switch (sponsor token)
179
+ writ grant --sponsor acme --agent agent-7 --verb payments.refund \
180
+ --target ch_123 --purpose "approved refund" # STEP_UP approval (sponsor token)
181
+ ```
182
+
183
+ Sponsor-token commands (`revoke`, `reinstate`, `revoked`, `grant`) read
184
+ `--sponsor-token` or `WRIT_SPONSOR_TOKEN`. Run `writ --help` for the full command list.
185
+
186
+ ## Docs
187
+
188
+ Full docs: [docs.withwrit.com](https://docs.withwrit.com) ·
189
+ Quickstart: [docs.withwrit.com/quickstart](https://docs.withwrit.com/quickstart) ·
190
+ Site: [withwrit.com](https://withwrit.com)
191
+
192
+ ## License
193
+
194
+ MIT
@@ -4,12 +4,21 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pywrit"
7
- version = "0.2.3"
7
+ version = "0.2.5"
8
8
  description = "Python client + CLI for Writ — the gate before the write"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
11
11
  requires-python = ">=3.9"
12
12
  dependencies = ["httpx>=0.27"]
13
+
14
+ [project.optional-dependencies]
15
+ # `pip install 'pywrit[polyglot]'`: lets `writ scan` parse TypeScript and
16
+ # JavaScript (scan only). Python scanning never needs it.
17
+ polyglot = [
18
+ "tree-sitter>=0.23",
19
+ "tree-sitter-typescript>=0.23",
20
+ "tree-sitter-javascript>=0.23",
21
+ ]
13
22
  keywords = ["ai-agents", "policy", "audit-log", "guardrails"]
14
23
 
15
24
  [project.urls]
@@ -27,7 +27,7 @@ from .errors import (
27
27
  WritError,
28
28
  )
29
29
 
30
- __version__ = "0.2.3"
30
+ __version__ = "0.2.5"
31
31
 
32
32
  __all__ = [
33
33
  "WritClient",