pywrit 0.1.7__tar.gz → 0.1.9__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pywrit
3
- Version: 0.1.7
3
+ Version: 0.1.9
4
4
  Summary: Writ — the gate before the write. Allow/deny gate with a tamper-evident audit log
5
5
  Project-URL: Homepage, https://withwrit.com
6
6
  Project-URL: Documentation, https://docs.withwrit.com
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
  # Distribution name: pywrit (approved by Chad 2026-09-27).
7
7
  # The bare name "writ" is taken on PyPI.
8
8
  name = "pywrit"
9
- version = "0.1.7"
9
+ version = "0.1.9"
10
10
  description = "Writ — the gate before the write. Allow/deny gate with a tamper-evident audit log"
11
11
  readme = "README.md"
12
12
  requires-python = ">=3.9"
@@ -39,3 +39,6 @@ writ = "writ_cli.cli:main"
39
39
  [tool.setuptools.packages.find]
40
40
  where = ["."]
41
41
  include = ["writ_cli*"]
42
+
43
+ [tool.setuptools.package-data]
44
+ writ_cli = ["templates/*.json"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pywrit
3
- Version: 0.1.7
3
+ Version: 0.1.9
4
4
  Summary: Writ — the gate before the write. Allow/deny gate with a tamper-evident audit log
5
5
  Project-URL: Homepage, https://withwrit.com
6
6
  Project-URL: Documentation, https://docs.withwrit.com
@@ -11,4 +11,8 @@ tests/test_scan_tsjs.py
11
11
  writ_cli/__init__.py
12
12
  writ_cli/cli.py
13
13
  writ_cli/config.py
14
- writ_cli/risk.py
14
+ writ_cli/risk.py
15
+ writ_cli/templates/audit-first.json
16
+ writ_cli/templates/dev-smooth.json
17
+ writ_cli/templates/prod-safe.json
18
+ writ_cli/templates/strict-ai-defaults.json
@@ -24,6 +24,7 @@ BASE = os.environ.get(
24
24
  "WRIT_BASE",
25
25
  config.base_url() or "https://api.withwrit.com",
26
26
  )
27
+ _JSON_OUTPUT = False
27
28
 
28
29
 
29
30
  def stream_cmd(args):
@@ -11226,14 +11227,12 @@ def request(method, path, payload=None, key=None, token=None):
11226
11227
  def _request_and_print(method, path, payload=None, key=None, token=None):
11227
11228
  """Call request(), print the response, and return a clean exit code.
11228
11229
 
11229
- Success bodies go to stdout; error bodies go to stderr. This is the
11230
- helper all simple dispatchers should use so the CLI exits 0 on success.
11230
+ Success bodies go to stdout; error bodies go to stderr. With --json,
11231
+ everything goes to stdout as raw JSON so agents can parse it cleanly.
11231
11232
  """
11232
11233
  code, text = request(method, path, payload, key=key, token=token)
11233
- if code == 0:
11234
- print(text)
11235
- return 0
11236
- print(text, file=sys.stderr)
11234
+ out = sys.stdout if _JSON_OUTPUT else (sys.stdout if code == 0 else sys.stderr)
11235
+ print(text, file=out)
11237
11236
  return code
11238
11237
 
11239
11238
 
@@ -11258,6 +11257,28 @@ def _load_json_file(path):
11258
11257
  return json.loads(p.read_text(encoding="utf-8"))
11259
11258
 
11260
11259
 
11260
+ def _load_policy_template(name):
11261
+ """Find a policy template JSON file by name or path."""
11262
+ candidates = []
11263
+ if "/" in name or name.endswith(".json"):
11264
+ candidates.append(_pathlib.Path(name))
11265
+ candidates.append(_pathlib.Path.home() / ".writ" / "templates" / (name + ".json"))
11266
+ # when run as an installed package
11267
+ try:
11268
+ import writ_cli as _wc
11269
+ candidates.append(_pathlib.Path(_wc.__file__).parent / "templates" / (name + ".json"))
11270
+ except Exception:
11271
+ pass
11272
+ # when running from the repo
11273
+ candidates.append(_pathlib.Path(__file__).parent / "templates" / (name + ".json"))
11274
+ repo = _pathlib.Path(__file__).parent.parent.parent / "docs" / "templates" / "policies" / (name + ".json")
11275
+ candidates.append(repo)
11276
+ for p in candidates:
11277
+ if p.exists():
11278
+ return json.loads(p.read_text(encoding="utf-8"))
11279
+ raise FileNotFoundError("template not found: %s (looked in %s)" % (name, ", ".join(str(c) for c in candidates)))
11280
+
11281
+
11261
11282
  # ---------------------------------------------------------------------------
11262
11283
  # Agent Action Report
11263
11284
  # ---------------------------------------------------------------------------
@@ -11547,6 +11568,10 @@ def report_cmd(args):
11547
11568
 
11548
11569
  def main():
11549
11570
  parser = argparse.ArgumentParser(description="Ask Writ before an agent writes.")
11571
+ json_parent = argparse.ArgumentParser(add_help=False)
11572
+ json_parent.add_argument("--json", action="store_true",
11573
+ help="Output raw JSON for machine consumption")
11574
+
11550
11575
  sub = parser.add_subparsers(dest="cmd", required=True)
11551
11576
 
11552
11577
  key = sub.add_parser("key", help="Get a free API key + tenant")
@@ -11565,16 +11590,18 @@ def main():
11565
11590
  cfg.add_argument("name", default="", help="Config key")
11566
11591
  cfg.add_argument("value", default="", help="Config value (for set)")
11567
11592
 
11568
- me = sub.add_parser("me", help="Show tenant, key meta, and policy")
11593
+ me = sub.add_parser("me", parents=[json_parent], help="Show tenant, key meta, and policy")
11569
11594
  me.add_argument("--key", default=config.api_key())
11570
11595
 
11571
11596
  MODES = ("allow", "deny", "require_grant", "step_up")
11572
- policy = sub.add_parser("policy", help="Get or set tenant verb policy")
11597
+ policy = sub.add_parser("policy", parents=[json_parent], help="Get or set tenant verb policy")
11573
11598
  policy.add_argument("--key", default=config.api_key())
11574
11599
  policy.add_argument("--set", nargs=2, metavar=("VERB", "MODE"), action="append", default=[])
11575
11600
  policy.add_argument("--env", default="", help="Environment override to get/set: dev, staging, or prod (default: tenant policy)")
11601
+ policy.add_argument("--principal", default="", help="Principal id for a per-principal policy override")
11602
+ policy.add_argument("--template", default="", help="Apply a starter template by name (audit-first, prod-safe, dev-smooth, strict-ai-defaults) or path")
11576
11603
 
11577
- check = sub.add_parser("check", help="ALLOW or DENY a write")
11604
+ check = sub.add_parser("check", parents=[json_parent], help="ALLOW or DENY a write")
11578
11605
  check.add_argument("--key", default=config.api_key())
11579
11606
  check.add_argument("--sponsor", required=True)
11580
11607
  check.add_argument("--agent", required=True)
@@ -11603,7 +11630,8 @@ def main():
11603
11630
  revoked = sub.add_parser("revoked", help="List revoked principals (sponsor token)")
11604
11631
  revoked.add_argument("--sponsor-token", default=config.sponsor_token())
11605
11632
 
11606
- receipts = sub.add_parser("receipts", help="Latest receipts for your tenant")
11633
+ receipts = sub.add_parser("receipts", parents=[json_parent], help="Latest receipts for your tenant, or one receipt by id")
11634
+ receipts.add_argument("receipt_id", nargs="?", default="", help="Fetch a single receipt by id; omit to list")
11607
11635
  receipts.add_argument("--key", default=config.api_key())
11608
11636
 
11609
11637
  stream = sub.add_parser("stream", help="Tail the live audit log (SSE stream)")
@@ -11615,7 +11643,7 @@ def main():
11615
11643
  verify.add_argument("--key", default=config.api_key())
11616
11644
  verify.add_argument("--limit", type=int, default=200, help="Max receipts to verify (1-1000)")
11617
11645
 
11618
- invite = sub.add_parser("invite", help="Create a team invite code for your tenant")
11646
+ invite = sub.add_parser("invite", parents=[json_parent], help="Create a team invite code for your tenant")
11619
11647
  invite.add_argument("--key", default=config.api_key())
11620
11648
 
11621
11649
  grant = sub.add_parser("grant", help="Sponsor approval: grant one write (sponsor token)")
@@ -11665,7 +11693,7 @@ def main():
11665
11693
  scan.add_argument("--repo-id", default="",
11666
11694
  help="Repository identifier for the manifest (default: directory name)")
11667
11695
 
11668
- report = sub.add_parser("report", help="Agent Action Report from receipts")
11696
+ report = sub.add_parser("report", parents=[json_parent], help="Agent Action Report from receipts")
11669
11697
  rsrc = report.add_mutually_exclusive_group(required=True)
11670
11698
  rsrc.add_argument("--key", default=config.api_key(), help="API key — fetch receipts from the Writ API")
11671
11699
  rsrc.add_argument("--file", help="Read receipts from a local JSON file (array or JSON-lines)")
@@ -11680,7 +11708,7 @@ def main():
11680
11708
  manifest_key = sub.add_parser("manifest-key", help="Fetch the tenant's CI manifest HMAC key")
11681
11709
  manifest_key.add_argument("--key", default=config.api_key())
11682
11710
 
11683
- manifests = sub.add_parser("manifests", help="List, fetch, or submit CI attestation manifests")
11711
+ manifests = sub.add_parser("manifests", parents=[json_parent], help="List, fetch, or submit CI attestation manifests")
11684
11712
  manifests_sub = manifests.add_subparsers(dest="manifests_cmd", required=True)
11685
11713
  ms_list = manifests_sub.add_parser("list", help="List repos with their latest manifest summary")
11686
11714
  ms_list.add_argument("--key", default=config.api_key())
@@ -11691,7 +11719,7 @@ def main():
11691
11719
  ms_push.add_argument("file")
11692
11720
  ms_push.add_argument("--key", default=config.api_key())
11693
11721
 
11694
- github = sub.add_parser("github", help="Manage the Writ GitHub App integration")
11722
+ github = sub.add_parser("github", parents=[json_parent], help="Manage the Writ GitHub App integration")
11695
11723
  github_sub = github.add_subparsers(dest="github_cmd", required=True)
11696
11724
  gh_inst = github_sub.add_parser("installations", help="List connected installations")
11697
11725
  gh_inst.add_argument("--key", default=config.api_key())
@@ -11718,7 +11746,105 @@ def main():
11718
11746
  gh_disc.add_argument("--totp-code", default="", help="TOTP step-up code")
11719
11747
  gh_disc.add_argument("--key", default=config.api_key())
11720
11748
 
11749
+ principals = sub.add_parser("principals", parents=[json_parent], help="Manage the tenant identity directory")
11750
+ pr_sub = principals.add_subparsers(dest="principals_cmd", required=True)
11751
+ pr_list = pr_sub.add_parser("list", help="List principals")
11752
+ pr_list.add_argument("--key", default=config.api_key())
11753
+ pr_create = pr_sub.add_parser("create", help="Create a principal")
11754
+ pr_create.add_argument("name")
11755
+ pr_create.add_argument("--key", default=config.api_key())
11756
+ pr_rename = pr_sub.add_parser("rename", help="Rename a principal")
11757
+ pr_rename.add_argument("principal_id")
11758
+ pr_rename.add_argument("new_name")
11759
+ pr_rename.add_argument("--key", default=config.api_key())
11760
+ pr_disable = pr_sub.add_parser("disable", help="Disable a principal (pauses its keys)")
11761
+ pr_disable.add_argument("principal_id")
11762
+ pr_disable.add_argument("--key", default=config.api_key())
11763
+ pr_enable = pr_sub.add_parser("enable", help="Re-enable a principal")
11764
+ pr_enable.add_argument("principal_id")
11765
+ pr_enable.add_argument("--key", default=config.api_key())
11766
+ pr_del = pr_sub.add_parser("delete", help="Delete a principal")
11767
+ pr_del.add_argument("principal_id")
11768
+ pr_del.add_argument("--force", action="store_true", help="Revoke remaining keys first")
11769
+ pr_del.add_argument("--key", default=config.api_key())
11770
+ pr_rev = pr_sub.add_parser("revoke-keys", help="Revoke all API keys for a principal")
11771
+ pr_rev.add_argument("principal_id")
11772
+ pr_rev.add_argument("--key", default=config.api_key())
11773
+
11774
+ verbs = sub.add_parser("verbs", parents=[json_parent], help="Manage the tenant verb vocabulary")
11775
+ vb_sub = verbs.add_subparsers(dest="verbs_cmd", required=True)
11776
+ vb_list = vb_sub.add_parser("list", help="List custom verbs")
11777
+ vb_list.add_argument("--key", default=config.api_key())
11778
+ vb_add = vb_sub.add_parser("add", help="Add a custom verb")
11779
+ vb_add.add_argument("name")
11780
+ vb_add.add_argument("--description", default="")
11781
+ vb_add.add_argument("--resolved-verb", default="", help="Canonical verb this aliases")
11782
+ vb_add.add_argument("--key", default=config.api_key())
11783
+ vb_rename = vb_sub.add_parser("rename", help="Rename or redescribe a custom verb")
11784
+ vb_rename.add_argument("name")
11785
+ vb_rename.add_argument("--new-name", default="")
11786
+ vb_rename.add_argument("--description", default="")
11787
+ vb_rename.add_argument("--key", default=config.api_key())
11788
+ vb_retire = vb_sub.add_parser("retire", help="Retire a custom verb")
11789
+ vb_retire.add_argument("name")
11790
+ vb_retire.add_argument("--key", default=config.api_key())
11791
+
11792
+ keys = sub.add_parser("keys", parents=[json_parent], help="Manage tenant API keys")
11793
+ keys_sub = keys.add_subparsers(dest="keys_cmd", required=True)
11794
+ keys_list = keys_sub.add_parser("list", help="List key metadata")
11795
+ keys_list.add_argument("--key", default=config.api_key())
11796
+ keys_create = keys_sub.add_parser("create", help="Create a new API key (TOTP-gated)")
11797
+ keys_create.add_argument("--label", default="", help="Optional label")
11798
+ keys_create.add_argument("--principal-id", default="", help="Assign to a principal")
11799
+ keys_create.add_argument("--totp-code", default="", help="TOTP step-up code")
11800
+ keys_create.add_argument("--key", default=config.api_key())
11801
+ keys_revoke = keys_sub.add_parser("revoke", help="Revoke an API key by id")
11802
+ keys_revoke.add_argument("key_id")
11803
+ keys_revoke.add_argument("--key", default=config.api_key())
11804
+
11805
+ badge = sub.add_parser("badge", parents=[json_parent], help="Dynamic README badge tokens")
11806
+ badge_sub = badge.add_subparsers(dest="badge_cmd", required=True)
11807
+ badge_create = badge_sub.add_parser("create", help="Create a public badge token")
11808
+ badge_create.add_argument("--label", default="gated")
11809
+ badge_create.add_argument("--key", default=config.api_key())
11810
+ badge_revoke = badge_sub.add_parser("revoke", help="Revoke a badge token")
11811
+ badge_revoke.add_argument("token")
11812
+ badge_revoke.add_argument("--key", default=config.api_key())
11813
+
11814
+ vault = sub.add_parser("vault", parents=[json_parent], help="Credential vault")
11815
+ vault_sub = vault.add_subparsers(dest="vault_cmd", required=True)
11816
+ vault_list = vault_sub.add_parser("list", help="List enrolled credentials")
11817
+ vault_list.add_argument("--key", default=config.api_key())
11818
+
11819
+ stats = sub.add_parser("stats", parents=[json_parent], help="24-hour receipt decision counters")
11820
+ stats.add_argument("--key", default=config.api_key())
11821
+
11822
+ lease = sub.add_parser("lease", parents=[json_parent], help="Create a single-use gated-execution lease")
11823
+ lease.add_argument("--verb", required=True)
11824
+ lease.add_argument("--target", required=True)
11825
+ lease.add_argument("--purpose", required=True)
11826
+ lease.add_argument("--ttl", type=int, default=60, help="Lease TTL in seconds")
11827
+ lease.add_argument("--key", default=config.api_key())
11828
+
11829
+ execute = sub.add_parser("execute", parents=[json_parent], help="Execute a gated upstream request using a lease")
11830
+ execute.add_argument("lease_id")
11831
+ execute.add_argument("--key", default=config.api_key())
11832
+
11833
+ doctor = sub.add_parser("doctor", help="Validate local config and gate connectivity")
11834
+ doctor.add_argument("--key", default=config.api_key())
11835
+ doctor.add_argument("--sponsor-token", default=config.sponsor_token())
11836
+ doctor.add_argument("--base", default=config.base_url())
11837
+
11838
+ mcp_setup = sub.add_parser("mcp-setup", help="Write MCP client config for Claude Desktop or Cursor")
11839
+ mcp_setup.add_argument("--key", default=config.api_key())
11840
+ mcp_setup.add_argument("--base", default=config.base_url())
11841
+ mcp_setup.add_argument("--server-path", default="", help="Path to mcp-server/server.py; omit to use uvx writ-mcp")
11842
+ mcp_setup.add_argument("--claude", action="store_true", help="Write Claude Desktop config (default)")
11843
+ mcp_setup.add_argument("--cursor", action="store_true", help="Write Cursor config")
11844
+
11721
11845
  args = parser.parse_args()
11846
+ global _JSON_OUTPUT
11847
+ _JSON_OUTPUT = bool(getattr(args, "json", False))
11722
11848
  if args.cmd == "key":
11723
11849
  payload = {"email": args.email}
11724
11850
  if args.tenant_name.strip():
@@ -11764,7 +11890,8 @@ def main():
11764
11890
 
11765
11891
  _KEY_COMMANDS = {"me", "policy", "check", "receipts", "stream",
11766
11892
  "verify-chain", "invite", "token-verify", "manifest-key",
11767
- "manifests", "github"}
11893
+ "manifests", "github", "principals", "verbs", "keys",
11894
+ "badge", "vault", "stats", "lease", "execute"}
11768
11895
  if args.cmd in _KEY_COMMANDS and not _require_key(args.key):
11769
11896
  return 2
11770
11897
 
@@ -11789,7 +11916,19 @@ def main():
11789
11916
  if args.cmd == "me":
11790
11917
  return _request_and_print("GET", "/v1/me", key=args.key)
11791
11918
  if args.cmd == "policy":
11792
- url = "/v1/policy" if not args.env else f"/v1/policies/environments/{args.env}"
11919
+ if args.principal:
11920
+ url = "/v1/policies/principals/" + urllib.parse.quote(args.principal, safe="")
11921
+ if args.env:
11922
+ url += "?environment=" + urllib.parse.quote(args.env, safe="")
11923
+ else:
11924
+ url = "/v1/policy" if not args.env else f"/v1/policies/environments/{args.env}"
11925
+ if args.template:
11926
+ try:
11927
+ template = _load_policy_template(args.template)
11928
+ except (FileNotFoundError, ValueError) as exc:
11929
+ print("error: %s" % exc, file=sys.stderr)
11930
+ return 2
11931
+ return _request_and_print("PUT", url, template, key=args.key)
11793
11932
  if args.set:
11794
11933
  for verb, mode in args.set:
11795
11934
  if mode not in MODES:
@@ -11798,6 +11937,8 @@ def main():
11798
11937
  return _request_and_print("PUT", url, {verb: mode for verb, mode in args.set}, key=args.key)
11799
11938
  return _request_and_print("GET", url, key=args.key)
11800
11939
  if args.cmd == "receipts":
11940
+ if args.receipt_id:
11941
+ return _request_and_print("GET", "/v1/receipts/" + urllib.parse.quote(args.receipt_id, safe=""), key=args.key)
11801
11942
  return _request_and_print("GET", "/v1/receipts", key=args.key)
11802
11943
  if args.cmd == "stream":
11803
11944
  return stream_cmd(args)
@@ -11849,6 +11990,168 @@ def main():
11849
11990
  if args.github_cmd == "disconnect":
11850
11991
  return _request_and_print("POST", "/v1/github/disconnect",
11851
11992
  {"installationId": args.installation_id, "totpCode": args.totp_code}, key=args.key)
11993
+ if args.cmd == "principals":
11994
+ if args.principals_cmd == "list":
11995
+ return _request_and_print("GET", "/v1/principals", key=args.key)
11996
+ if args.principals_cmd == "create":
11997
+ return _request_and_print("POST", "/v1/principals", {"name": args.name}, key=args.key)
11998
+ if args.principals_cmd == "rename":
11999
+ return _request_and_print("PATCH", "/v1/principals/" + urllib.parse.quote(args.principal_id, safe=""),
12000
+ {"name": args.new_name}, key=args.key)
12001
+ if args.principals_cmd == "disable":
12002
+ return _request_and_print("PATCH", "/v1/principals/" + urllib.parse.quote(args.principal_id, safe=""),
12003
+ {"disabled": True}, key=args.key)
12004
+ if args.principals_cmd == "enable":
12005
+ return _request_and_print("PATCH", "/v1/principals/" + urllib.parse.quote(args.principal_id, safe=""),
12006
+ {"disabled": False}, key=args.key)
12007
+ if args.principals_cmd == "delete":
12008
+ qs = "?force=1" if args.force else ""
12009
+ return _request_and_print("DELETE", "/v1/principals/" + urllib.parse.quote(args.principal_id, safe="") + qs, key=args.key)
12010
+ if args.principals_cmd == "revoke-keys":
12011
+ return _request_and_print("POST", "/v1/principals/" + urllib.parse.quote(args.principal_id, safe="") + "/revoke",
12012
+ {}, key=args.key)
12013
+ if args.cmd == "verbs":
12014
+ if args.verbs_cmd == "list":
12015
+ return _request_and_print("GET", "/v1/verbs", key=args.key)
12016
+ if args.verbs_cmd == "add":
12017
+ body = {"name": args.name}
12018
+ if args.description:
12019
+ body["description"] = args.description
12020
+ if args.resolved_verb:
12021
+ body["resolvedVerb"] = args.resolved_verb
12022
+ return _request_and_print("POST", "/v1/verbs", body, key=args.key)
12023
+ if args.verbs_cmd == "rename":
12024
+ body = {}
12025
+ if args.new_name:
12026
+ body["name"] = args.new_name
12027
+ if args.description:
12028
+ body["description"] = args.description
12029
+ return _request_and_print("PATCH", "/v1/verbs/" + urllib.parse.quote(args.name, safe=""), body, key=args.key)
12030
+ if args.verbs_cmd == "retire":
12031
+ return _request_and_print("DELETE", "/v1/verbs/" + urllib.parse.quote(args.name, safe=""), key=args.key)
12032
+ if args.cmd == "keys":
12033
+ if args.keys_cmd == "list":
12034
+ return _request_and_print("GET", "/v1/keys", key=args.key)
12035
+ if args.keys_cmd == "create":
12036
+ body = {"totpCode": args.totp_code}
12037
+ if args.label:
12038
+ body["label"] = args.label
12039
+ if args.principal_id:
12040
+ body["principalId"] = args.principal_id
12041
+ return _request_and_print("POST", "/v1/keys", body, key=args.key)
12042
+ if args.keys_cmd == "revoke":
12043
+ return _request_and_print("DELETE", "/v1/keys/" + urllib.parse.quote(args.key_id, safe=""), key=args.key)
12044
+ if args.cmd == "badge":
12045
+ if args.badge_cmd == "create":
12046
+ return _request_and_print("POST", "/v1/badge/tokens", {"label": args.label}, key=args.key)
12047
+ if args.badge_cmd == "revoke":
12048
+ return _request_and_print("POST", "/v1/badge/tokens/revoke", {"token": args.token}, key=args.key)
12049
+ if args.cmd == "vault":
12050
+ if args.vault_cmd == "list":
12051
+ return _request_and_print("GET", "/v1/vault", key=args.key)
12052
+ if args.cmd == "stats":
12053
+ return _request_and_print("GET", "/v1/receipts/stats", key=args.key)
12054
+ if args.cmd == "lease":
12055
+ return _request_and_print("POST", "/v1/leases",
12056
+ {"verb": args.verb, "target": args.target, "purpose": args.purpose, "ttlSeconds": args.ttl}, key=args.key)
12057
+ if args.cmd == "execute":
12058
+ return _request_and_print("POST", "/v1/execute", {"leaseId": args.lease_id}, key=args.key)
12059
+ if args.cmd == "doctor":
12060
+ def _mask(s, n=8):
12061
+ if not s or len(s) <= n * 2:
12062
+ return s
12063
+ return s[:n] + "..." + s[-n:]
12064
+ base = args.base or BASE
12065
+ print("Writ doctor")
12066
+ print(" config path: %s" % config._config_path())
12067
+ cfg = config.read_config()
12068
+ if cfg:
12069
+ print(" config values: %s" % ", ".join(
12070
+ "%s=%s" % (k, _mask(v) if "token" in k or "Key" in k else v)
12071
+ for k, v in sorted(cfg.items())
12072
+ ))
12073
+ else:
12074
+ print(" config: (none)")
12075
+ print(" env WRIT_API_KEY: %s" % ("set" if os.environ.get("WRIT_API_KEY") else "not set"))
12076
+ print(" env WRIT_SPONSOR_TOKEN: %s" % ("set" if os.environ.get("WRIT_SPONSOR_TOKEN") else "not set"))
12077
+ print(" env WRIT_BASE: %s" % os.environ.get("WRIT_BASE", "(default)"))
12078
+ print(" resolved base URL: %s" % base)
12079
+ ok = True
12080
+ code, text = request("GET", "/health")
12081
+ if code == 0:
12082
+ print(" /health: OK (%s)" % text.replace("\n", " "))
12083
+ else:
12084
+ print(" /health: FAIL (exit %s)" % code, file=sys.stderr)
12085
+ ok = False
12086
+ if args.key:
12087
+ code, text = request("GET", "/v1/me", key=args.key)
12088
+ if code == 0:
12089
+ print(" /v1/me: OK")
12090
+ else:
12091
+ print(" /v1/me: FAIL (exit %s) — check your API key" % code, file=sys.stderr)
12092
+ ok = False
12093
+ else:
12094
+ print(" /v1/me: skipped (no API key)")
12095
+ if args.sponsor_token:
12096
+ code, text = request("GET", "/v1/revoked", token="Bearer " + args.sponsor_token)
12097
+ if code == 0:
12098
+ print(" /v1/revoked: OK")
12099
+ else:
12100
+ print(" /v1/revoked: FAIL (exit %s) — check your sponsor token" % code, file=sys.stderr)
12101
+ ok = False
12102
+ else:
12103
+ print(" /v1/revoked: skipped (no sponsor token)")
12104
+ if not ok:
12105
+ print("\nFix: run `writ login --email you@example.com` or set WRIT_API_KEY / WRIT_BASE.", file=sys.stderr)
12106
+ return 1
12107
+ return 0
12108
+ if args.cmd == "mcp-setup":
12109
+ def _mcp_config_block(key, base, server_path):
12110
+ env = {"WRIT_API_KEY": key}
12111
+ if base:
12112
+ env["WRIT_BASE"] = base
12113
+ if server_path:
12114
+ return {
12115
+ "command": sys.executable,
12116
+ "args": [server_path],
12117
+ "env": env,
12118
+ }
12119
+ return {
12120
+ "command": "uvx",
12121
+ "args": ["writ-mcp"],
12122
+ "env": env,
12123
+ }
12124
+ key = args.key or config.api_key()
12125
+ if not key:
12126
+ print("error: no API key. Run `writ login` or pass --key.", file=sys.stderr)
12127
+ return 2
12128
+ do_claude = args.claude or not args.cursor
12129
+ do_cursor = args.cursor
12130
+ base = args.base or config.base_url()
12131
+ written = []
12132
+ if do_claude:
12133
+ config_dir = _pathlib.Path.home() / "Library" / "Application Support" / "Claude"
12134
+ config_path = config_dir / "claude_desktop_config.json"
12135
+ if not config_dir.exists():
12136
+ config_dir = _pathlib.Path.home() / ".config" / "Claude"
12137
+ config_path = config_dir / "claude_desktop_config.json"
12138
+ os.makedirs(config_dir, exist_ok=True)
12139
+ data = json.loads(config_path.read_text(encoding="utf-8")) if config_path.exists() else {}
12140
+ data.setdefault("mcpServers", {})["writ"] = _mcp_config_block(key, base, args.server_path)
12141
+ config_path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
12142
+ written.append(str(config_path))
12143
+ if do_cursor:
12144
+ cursor_dir = _pathlib.Path.home() / ".cursor"
12145
+ cursor_path = cursor_dir / "mcp.json"
12146
+ os.makedirs(cursor_dir, exist_ok=True)
12147
+ data = json.loads(cursor_path.read_text(encoding="utf-8")) if cursor_path.exists() else {}
12148
+ data.setdefault("mcpServers", {})["writ"] = _mcp_config_block(key, base, args.server_path)
12149
+ cursor_path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
12150
+ written.append(str(cursor_path))
12151
+ print("Wrote MCP config for Writ to:")
12152
+ for p in written:
12153
+ print(" %s" % p)
12154
+ return 0
11852
12155
  if args.cmd in ("revoke", "reinstate", "revoked"):
11853
12156
  if not _require_sponsor_token(args.sponsor_token):
11854
12157
  return 2
@@ -0,0 +1,7 @@
1
+ {
2
+ "verify": "require_grant",
3
+ "refund": "require_grant",
4
+ "provision": "require_grant",
5
+ "admit": "require_grant",
6
+ "prescribe": "require_grant"
7
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "verify": "allow",
3
+ "refund": "require_grant",
4
+ "provision": "require_grant",
5
+ "admit": "allow",
6
+ "prescribe": "require_grant"
7
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "verify": "require_grant",
3
+ "refund": "step_up",
4
+ "provision": "step_up",
5
+ "admit": "require_grant",
6
+ "prescribe": "require_grant"
7
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "verify": "require_grant",
3
+ "refund": "deny",
4
+ "provision": "deny",
5
+ "admit": "require_grant",
6
+ "prescribe": "require_grant"
7
+ }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes