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.
- {pywrit-0.1.7/pywrit.egg-info → pywrit-0.1.9}/PKG-INFO +1 -1
- {pywrit-0.1.7 → pywrit-0.1.9}/pyproject.toml +4 -1
- {pywrit-0.1.7 → pywrit-0.1.9/pywrit.egg-info}/PKG-INFO +1 -1
- {pywrit-0.1.7 → pywrit-0.1.9}/pywrit.egg-info/SOURCES.txt +5 -1
- {pywrit-0.1.7 → pywrit-0.1.9}/writ_cli/cli.py +319 -16
- pywrit-0.1.9/writ_cli/templates/audit-first.json +7 -0
- pywrit-0.1.9/writ_cli/templates/dev-smooth.json +7 -0
- pywrit-0.1.9/writ_cli/templates/prod-safe.json +7 -0
- pywrit-0.1.9/writ_cli/templates/strict-ai-defaults.json +7 -0
- {pywrit-0.1.7 → pywrit-0.1.9}/LICENSE +0 -0
- {pywrit-0.1.7 → pywrit-0.1.9}/README.md +0 -0
- {pywrit-0.1.7 → pywrit-0.1.9}/pywrit.egg-info/dependency_links.txt +0 -0
- {pywrit-0.1.7 → pywrit-0.1.9}/pywrit.egg-info/entry_points.txt +0 -0
- {pywrit-0.1.7 → pywrit-0.1.9}/pywrit.egg-info/requires.txt +0 -0
- {pywrit-0.1.7 → pywrit-0.1.9}/pywrit.egg-info/top_level.txt +0 -0
- {pywrit-0.1.7 → pywrit-0.1.9}/setup.cfg +0 -0
- {pywrit-0.1.7 → pywrit-0.1.9}/tests/test_scan_tsjs.py +0 -0
- {pywrit-0.1.7 → pywrit-0.1.9}/writ_cli/__init__.py +0 -0
- {pywrit-0.1.7 → pywrit-0.1.9}/writ_cli/config.py +0 -0
- {pywrit-0.1.7 → pywrit-0.1.9}/writ_cli/risk.py +0 -0
|
@@ -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.
|
|
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"]
|
|
@@ -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.
|
|
11230
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|