tide-runtime 0.2.5__tar.gz → 0.2.6__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.
- {tide_runtime-0.2.5 → tide_runtime-0.2.6}/PKG-INFO +12 -2
- {tide_runtime-0.2.5 → tide_runtime-0.2.6}/README.md +11 -1
- {tide_runtime-0.2.5 → tide_runtime-0.2.6}/pyproject.toml +4 -1
- {tide_runtime-0.2.5 → tide_runtime-0.2.6}/tests/test_runtime.py +57 -0
- {tide_runtime-0.2.5 → tide_runtime-0.2.6}/tide/__init__.py +1 -1
- tide_runtime-0.2.6/tide/cli.py +240 -0
- tide_runtime-0.2.6/tide/packs.py +70 -0
- {tide_runtime-0.2.5 → tide_runtime-0.2.6}/tide_runtime.egg-info/PKG-INFO +12 -2
- {tide_runtime-0.2.5 → tide_runtime-0.2.6}/tide_runtime.egg-info/SOURCES.txt +3 -0
- tide_runtime-0.2.6/tide_runtime.egg-info/entry_points.txt +2 -0
- {tide_runtime-0.2.5 → tide_runtime-0.2.6}/setup.cfg +0 -0
- {tide_runtime-0.2.5 → tide_runtime-0.2.6}/tide/policy.py +0 -0
- {tide_runtime-0.2.5 → tide_runtime-0.2.6}/tide/store.py +0 -0
- {tide_runtime-0.2.5 → tide_runtime-0.2.6}/tide_runtime.egg-info/dependency_links.txt +0 -0
- {tide_runtime-0.2.5 → tide_runtime-0.2.6}/tide_runtime.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tide-runtime
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6
|
|
4
4
|
Summary: Tide runtime policy, approvals, and earned-use ledger for AI agent tools.
|
|
5
5
|
Project-URL: Repository, https://github.com/rippletideco/rippletide-package
|
|
6
6
|
Requires-Python: >=3.8
|
|
@@ -13,9 +13,12 @@ earned-use logging.
|
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
15
|
pip install tide-runtime
|
|
16
|
-
tide
|
|
16
|
+
tide init --pack support-refunds --environment prod
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
`tide init --dry-run --pack <name>` prints the policy before writing it.
|
|
20
|
+
Built-in packs: `baseline`, `support-refunds`, `sql-analyst`, `mcp-baseline`.
|
|
21
|
+
|
|
19
22
|
```python
|
|
20
23
|
from tide import configure, controlled_tool, record_outcome
|
|
21
24
|
|
|
@@ -35,3 +38,10 @@ except Exception:
|
|
|
35
38
|
|
|
36
39
|
Default files live in `.tide/ledger/`. Override with `TIDE_LEDGER_DIR` or
|
|
37
40
|
`configure(project="...")`.
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
tide approvals
|
|
44
|
+
tide approve <id>
|
|
45
|
+
tide deny <id> --reason "not in scope"
|
|
46
|
+
tide report
|
|
47
|
+
```
|
|
@@ -5,9 +5,12 @@ earned-use logging.
|
|
|
5
5
|
|
|
6
6
|
```bash
|
|
7
7
|
pip install tide-runtime
|
|
8
|
-
tide
|
|
8
|
+
tide init --pack support-refunds --environment prod
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
`tide init --dry-run --pack <name>` prints the policy before writing it.
|
|
12
|
+
Built-in packs: `baseline`, `support-refunds`, `sql-analyst`, `mcp-baseline`.
|
|
13
|
+
|
|
11
14
|
```python
|
|
12
15
|
from tide import configure, controlled_tool, record_outcome
|
|
13
16
|
|
|
@@ -27,3 +30,10 @@ except Exception:
|
|
|
27
30
|
|
|
28
31
|
Default files live in `.tide/ledger/`. Override with `TIDE_LEDGER_DIR` or
|
|
29
32
|
`configure(project="...")`.
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
tide approvals
|
|
36
|
+
tide approve <id>
|
|
37
|
+
tide deny <id> --reason "not in scope"
|
|
38
|
+
tide report
|
|
39
|
+
```
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tide-runtime"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.6"
|
|
8
8
|
description = "Tide runtime policy, approvals, and earned-use ledger for AI agent tools."
|
|
9
9
|
requires-python = ">=3.8"
|
|
10
10
|
readme = "README.md"
|
|
@@ -12,5 +12,8 @@ readme = "README.md"
|
|
|
12
12
|
[project.urls]
|
|
13
13
|
Repository = "https://github.com/rippletideco/rippletide-package"
|
|
14
14
|
|
|
15
|
+
[project.scripts]
|
|
16
|
+
tide = "tide.cli:main"
|
|
17
|
+
|
|
15
18
|
[tool.setuptools]
|
|
16
19
|
packages = ["tide"]
|
|
@@ -6,10 +6,14 @@ import tempfile
|
|
|
6
6
|
import threading
|
|
7
7
|
import time
|
|
8
8
|
import unittest
|
|
9
|
+
from contextlib import redirect_stdout
|
|
10
|
+
from io import StringIO
|
|
9
11
|
|
|
10
12
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
11
13
|
|
|
12
14
|
import tide
|
|
15
|
+
from tide import cli as tide_cli
|
|
16
|
+
from tide import packs as tide_packs
|
|
13
17
|
from tide import policy as tide_policy
|
|
14
18
|
from tide import store as tide_store
|
|
15
19
|
|
|
@@ -179,6 +183,59 @@ class TideRuntimeTest(unittest.TestCase):
|
|
|
179
183
|
self.assertEqual(outcome["trace_id"], events(self.project)[0]["trace_id"])
|
|
180
184
|
self.assertEqual(outcome["status"], "success")
|
|
181
185
|
|
|
186
|
+
def test_packs_generate_reviewable_policy(self):
|
|
187
|
+
pol = tide_packs.get("support-refunds", environment="prod")
|
|
188
|
+
self.assertEqual(pol["pack"], "support-refunds")
|
|
189
|
+
self.assertEqual(pol["tools"]["stripe.refund"]["decision"], "require_approval")
|
|
190
|
+
self.assertEqual(pol["tools"]["stripe.customer.delete"]["decision"], "block")
|
|
191
|
+
|
|
192
|
+
def test_cli_init_writes_pack_policy(self):
|
|
193
|
+
out = StringIO()
|
|
194
|
+
with redirect_stdout(out):
|
|
195
|
+
code = tide_cli.main(["init", "--project", self.project, "--pack", "sql-analyst", "--force"])
|
|
196
|
+
self.assertEqual(code, 0)
|
|
197
|
+
pol = tide_store.load_policy(self.project)
|
|
198
|
+
self.assertEqual(pol["pack"], "sql-analyst")
|
|
199
|
+
self.assertEqual(pol["decisions"]["internal_write"]["prod"], "block")
|
|
200
|
+
|
|
201
|
+
def test_cli_dry_run_does_not_write_policy(self):
|
|
202
|
+
shutil.rmtree(self.project)
|
|
203
|
+
out = StringIO()
|
|
204
|
+
with redirect_stdout(out):
|
|
205
|
+
code = tide_cli.main(["init", "--project", self.project, "--pack", "mcp-baseline", "--dry-run"])
|
|
206
|
+
self.assertEqual(code, 0)
|
|
207
|
+
self.assertIn('"pack": "mcp-baseline"', out.getvalue())
|
|
208
|
+
self.assertFalse(os.path.exists(os.path.join(self.project, tide_store.POLICY_FILE)))
|
|
209
|
+
|
|
210
|
+
def test_cli_approves_pending_request(self):
|
|
211
|
+
@tide.controlled_tool(name="stripe.refund", risk_class="money_movement", capture=["charge_id"])
|
|
212
|
+
def refund(charge_id):
|
|
213
|
+
return "refunded"
|
|
214
|
+
|
|
215
|
+
with self.assertRaises(tide.LedgerPending) as ctx:
|
|
216
|
+
refund("ch_1")
|
|
217
|
+
prefix = ctx.exception.approval_id[:8]
|
|
218
|
+
out = StringIO()
|
|
219
|
+
with redirect_stdout(out):
|
|
220
|
+
code = tide_cli.main(["approve", prefix, "--project", self.project, "--reason", "ok"])
|
|
221
|
+
self.assertEqual(code, 0)
|
|
222
|
+
self.assertIn("one-shot", out.getvalue())
|
|
223
|
+
self.assertEqual(tide_store.approval_state(self.project, ctx.exception.approval_id), "approved")
|
|
224
|
+
|
|
225
|
+
def test_cli_report_prints_earned_use(self):
|
|
226
|
+
@tide.controlled_tool(name="crm.get_lead", risk_class="read_only")
|
|
227
|
+
def get_lead(lead_id):
|
|
228
|
+
return "lead"
|
|
229
|
+
|
|
230
|
+
get_lead("L-1")
|
|
231
|
+
tide.record_outcome("success")
|
|
232
|
+
out = StringIO()
|
|
233
|
+
with redirect_stdout(out):
|
|
234
|
+
code = tide_cli.main(["report", "--project", self.project])
|
|
235
|
+
self.assertEqual(code, 0)
|
|
236
|
+
self.assertIn("Earned use by tool", out.getvalue())
|
|
237
|
+
self.assertIn("crm.get_lead", out.getvalue())
|
|
238
|
+
|
|
182
239
|
|
|
183
240
|
if __name__ == "__main__":
|
|
184
241
|
unittest.main()
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""CLI for the local Tide ledger."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from . import __version__
|
|
9
|
+
from . import packs
|
|
10
|
+
from . import store
|
|
11
|
+
|
|
12
|
+
DEFAULT_PROJECT = os.environ.get("TIDE_LEDGER_DIR", ".tide/ledger")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _write_policy(args):
|
|
16
|
+
pol = packs.get(args.pack, args.environment)
|
|
17
|
+
if args.dry_run:
|
|
18
|
+
print(json.dumps(pol, indent=2, sort_keys=True))
|
|
19
|
+
return 0
|
|
20
|
+
store.ensure_project(args.project)
|
|
21
|
+
path = os.path.join(args.project, store.POLICY_FILE)
|
|
22
|
+
if os.path.exists(path) and not args.force:
|
|
23
|
+
print("tide: policy already exists at %s" % path, file=sys.stderr)
|
|
24
|
+
print("tide: use --force to replace it or --dry-run to preview a pack", file=sys.stderr)
|
|
25
|
+
return 1
|
|
26
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
27
|
+
json.dump(pol, f, indent=2, sort_keys=True)
|
|
28
|
+
f.write("\n")
|
|
29
|
+
print("tide: wrote %s policy -> %s" % (args.pack, path))
|
|
30
|
+
print("tide: wrap risky tools with @controlled_tool to enforce it.")
|
|
31
|
+
return 0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _approval_entries(project):
|
|
35
|
+
requests = {}
|
|
36
|
+
for r in store.read_jsonl(project, store.APPROVALS_FILE):
|
|
37
|
+
aid = r.get("approval_id")
|
|
38
|
+
if r.get("type") == "request":
|
|
39
|
+
requests[aid] = {"request": r, "state": "pending", "decision": None}
|
|
40
|
+
elif r.get("type") == "decision" and aid in requests:
|
|
41
|
+
requests[aid]["state"] = "approved" if r.get("decision") == "approved" else "denied"
|
|
42
|
+
requests[aid]["decision"] = r
|
|
43
|
+
elif r.get("type") == "consumed" and aid in requests:
|
|
44
|
+
requests[aid]["state"] = "consumed"
|
|
45
|
+
return requests
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _resolve_approval(project, prefix):
|
|
49
|
+
matches = [aid for aid in _approval_entries(project) if aid.startswith(prefix)]
|
|
50
|
+
if not matches:
|
|
51
|
+
raise ValueError("no approval found matching '%s'" % prefix)
|
|
52
|
+
if len(matches) > 1:
|
|
53
|
+
raise ValueError("'%s' is ambiguous; use more characters" % prefix)
|
|
54
|
+
return matches[0]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _approvals(args):
|
|
58
|
+
pending = [e["request"] for e in _approval_entries(args.project).values() if e["state"] == "pending"]
|
|
59
|
+
if not pending:
|
|
60
|
+
print("tide: no pending approvals")
|
|
61
|
+
return 0
|
|
62
|
+
print("tide: %d pending approval(s)\n" % len(pending))
|
|
63
|
+
for r in pending:
|
|
64
|
+
print(" %s %s@%s [%s]" % (
|
|
65
|
+
r["approval_id"][:8],
|
|
66
|
+
r.get("tool") or "?",
|
|
67
|
+
r.get("tool_version") or "?",
|
|
68
|
+
r.get("risk_class") or "?",
|
|
69
|
+
))
|
|
70
|
+
print(" agent=%s workflow=%s at %s" % (
|
|
71
|
+
r.get("agent_id") or "?",
|
|
72
|
+
r.get("workflow_id") or "?",
|
|
73
|
+
r.get("created_at") or "?",
|
|
74
|
+
))
|
|
75
|
+
if r.get("input_summary"):
|
|
76
|
+
print(" input: %s" % json.dumps(r["input_summary"], sort_keys=True))
|
|
77
|
+
print(" reason: %s" % (r.get("policy_reason") or "?"))
|
|
78
|
+
print(" approve: tide approve %s --project %s\n" % (r["approval_id"][:8], args.project))
|
|
79
|
+
return 0
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _decide(args, decision):
|
|
83
|
+
try:
|
|
84
|
+
aid = _resolve_approval(args.project, args.approval_id)
|
|
85
|
+
except ValueError as exc:
|
|
86
|
+
print("tide: %s" % exc, file=sys.stderr)
|
|
87
|
+
return 1
|
|
88
|
+
entry = _approval_entries(args.project)[aid]
|
|
89
|
+
if entry["state"] != "pending":
|
|
90
|
+
print("tide: approval %s is already %s" % (aid[:8], entry["state"]), file=sys.stderr)
|
|
91
|
+
return 1
|
|
92
|
+
store.append_jsonl(args.project, store.APPROVALS_FILE, {
|
|
93
|
+
"type": "decision",
|
|
94
|
+
"approval_id": aid,
|
|
95
|
+
"decision": decision,
|
|
96
|
+
"approver": os.environ.get("USER") or os.environ.get("USERNAME") or "unknown",
|
|
97
|
+
"reason": args.reason,
|
|
98
|
+
"created_at": store.now_iso(),
|
|
99
|
+
})
|
|
100
|
+
req = entry["request"]
|
|
101
|
+
print("tide: %s %s - %s (%s)" % (decision, aid[:8], req.get("tool"), req.get("risk_class")))
|
|
102
|
+
if decision == "approved":
|
|
103
|
+
print("tide: grant is one-shot and bound to the exact call.")
|
|
104
|
+
return 0
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _pct(n, d):
|
|
108
|
+
return "%d%%" % round((100 * n) / d) if d else "-"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _report(args):
|
|
112
|
+
events = store.read_jsonl(args.project, store.EVENTS_FILE)
|
|
113
|
+
outcomes = store.read_jsonl(args.project, store.OUTCOMES_FILE)
|
|
114
|
+
if not events and not outcomes:
|
|
115
|
+
print("tide: no events in %s - wrap tools with @controlled_tool and run your agent." % os.path.abspath(args.project))
|
|
116
|
+
return 1
|
|
117
|
+
|
|
118
|
+
by_decision = {}
|
|
119
|
+
for e in events:
|
|
120
|
+
key = e.get("policy_decision")
|
|
121
|
+
by_decision[key] = by_decision.get(key, 0) + 1
|
|
122
|
+
print("Tide Ledger - %s" % os.path.abspath(args.project))
|
|
123
|
+
print("\nTool calls: %d total (%s)" % (
|
|
124
|
+
len(events),
|
|
125
|
+
" / ".join("%s: %d" % (d, by_decision.get(d, 0)) for d in ("allow", "allow_with_log", "require_approval", "block")),
|
|
126
|
+
))
|
|
127
|
+
|
|
128
|
+
approvals = _approval_entries(args.project).values()
|
|
129
|
+
approval_counts = {"pending": 0, "approved": 0, "denied": 0, "consumed": 0}
|
|
130
|
+
for e in approvals:
|
|
131
|
+
approval_counts[e["state"]] = approval_counts.get(e["state"], 0) + 1
|
|
132
|
+
print("Approvals: %d requested (pending: %d / approved: %d / consumed: %d / denied: %d)" % (
|
|
133
|
+
sum(approval_counts.values()),
|
|
134
|
+
approval_counts.get("pending", 0),
|
|
135
|
+
approval_counts.get("approved", 0),
|
|
136
|
+
approval_counts.get("consumed", 0),
|
|
137
|
+
approval_counts.get("denied", 0),
|
|
138
|
+
))
|
|
139
|
+
|
|
140
|
+
trace_outcomes = {}
|
|
141
|
+
for o in outcomes:
|
|
142
|
+
bad = o.get("status") != "success" or bool(o.get("human_corrected"))
|
|
143
|
+
prev = trace_outcomes.get(o.get("trace_id"))
|
|
144
|
+
if not prev or bad:
|
|
145
|
+
trace_outcomes[o.get("trace_id")] = {"success": not bad, "corrected": bool(o.get("human_corrected"))}
|
|
146
|
+
print("Outcomes: %d recorded across %d trace(s)" % (len(outcomes), len(trace_outcomes)))
|
|
147
|
+
|
|
148
|
+
tools = {}
|
|
149
|
+
for e in events:
|
|
150
|
+
key = "%s@%s" % (e.get("tool"), e.get("tool_version") or "?")
|
|
151
|
+
t = tools.setdefault(key, {"calls": 0, "blocked": 0, "denied": 0, "errors": 0, "linked": 0, "helped": 0, "corrected": 0})
|
|
152
|
+
t["calls"] += 1
|
|
153
|
+
if e.get("policy_decision") == "block":
|
|
154
|
+
t["blocked"] += 1
|
|
155
|
+
if e.get("approval_status") == "denied":
|
|
156
|
+
t["denied"] += 1
|
|
157
|
+
if e.get("error_code") and e.get("error_code") not in ("blocked_by_policy", "approval_denied", "approval_timeout"):
|
|
158
|
+
t["errors"] += 1
|
|
159
|
+
oc = trace_outcomes.get(e.get("trace_id"))
|
|
160
|
+
if oc and e.get("output_hash"):
|
|
161
|
+
t["linked"] += 1
|
|
162
|
+
if oc["success"]:
|
|
163
|
+
t["helped"] += 1
|
|
164
|
+
if oc["corrected"]:
|
|
165
|
+
t["corrected"] += 1
|
|
166
|
+
|
|
167
|
+
print("\nEarned use by tool:")
|
|
168
|
+
print(" tool calls blocked denied errors outcome-linked success corrected")
|
|
169
|
+
for key, t in sorted(tools.items(), key=lambda item: item[1]["calls"], reverse=True):
|
|
170
|
+
print(
|
|
171
|
+
" %-40s%5d %7d %6d %6d %14d %7s %9s" % (
|
|
172
|
+
key[:40], t["calls"], t["blocked"], t["denied"], t["errors"],
|
|
173
|
+
t["linked"], _pct(t["helped"], t["linked"]), _pct(t["corrected"], t["linked"]),
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
return 0
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _packs(args):
|
|
180
|
+
if args.name:
|
|
181
|
+
print(json.dumps(packs.get(args.name), indent=2, sort_keys=True))
|
|
182
|
+
return 0
|
|
183
|
+
for name in packs.names():
|
|
184
|
+
print("%-16s %s" % (name, packs.PACKS[name].get("description", "")))
|
|
185
|
+
return 0
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def build_parser():
|
|
189
|
+
parser = argparse.ArgumentParser(description="Tide local policy and approvals for AI agent tools.")
|
|
190
|
+
parser.add_argument("--version", action="version", version="tide-runtime %s" % __version__)
|
|
191
|
+
sub = parser.add_subparsers(dest="command")
|
|
192
|
+
|
|
193
|
+
init = sub.add_parser("init", help="write .tide/ledger/policy.json")
|
|
194
|
+
init.add_argument("--project", default=DEFAULT_PROJECT)
|
|
195
|
+
init.add_argument("--pack", choices=packs.names(), default="baseline")
|
|
196
|
+
init.add_argument("--environment", choices=("dev", "prod"), default=None)
|
|
197
|
+
init.add_argument("--dry-run", action="store_true")
|
|
198
|
+
init.add_argument("--force", action="store_true")
|
|
199
|
+
init.set_defaults(func=_write_policy)
|
|
200
|
+
|
|
201
|
+
show_packs = sub.add_parser("packs", help="list packs or print one pack as JSON")
|
|
202
|
+
show_packs.add_argument("name", nargs="?", choices=packs.names())
|
|
203
|
+
show_packs.set_defaults(func=_packs)
|
|
204
|
+
|
|
205
|
+
approvals = sub.add_parser("approvals", help="list pending approval requests")
|
|
206
|
+
approvals.add_argument("--project", default=DEFAULT_PROJECT)
|
|
207
|
+
approvals.set_defaults(func=_approvals)
|
|
208
|
+
|
|
209
|
+
approve = sub.add_parser("approve", help="approve one exact call")
|
|
210
|
+
approve.add_argument("approval_id")
|
|
211
|
+
approve.add_argument("--project", default=DEFAULT_PROJECT)
|
|
212
|
+
approve.add_argument("--reason")
|
|
213
|
+
approve.set_defaults(func=lambda args: _decide(args, "approved"))
|
|
214
|
+
|
|
215
|
+
deny = sub.add_parser("deny", help="deny a request")
|
|
216
|
+
deny.add_argument("approval_id")
|
|
217
|
+
deny.add_argument("--project", default=DEFAULT_PROJECT)
|
|
218
|
+
deny.add_argument("--reason")
|
|
219
|
+
deny.set_defaults(func=lambda args: _decide(args, "denied"))
|
|
220
|
+
|
|
221
|
+
report = sub.add_parser("report", help="tool calls, approvals, outcomes, earned-use per tool")
|
|
222
|
+
report.add_argument("--project", default=DEFAULT_PROJECT)
|
|
223
|
+
report.set_defaults(func=_report)
|
|
224
|
+
return parser
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def main(argv=None):
|
|
228
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
229
|
+
if argv[:1] == ["ledger"]:
|
|
230
|
+
argv = argv[1:]
|
|
231
|
+
parser = build_parser()
|
|
232
|
+
args = parser.parse_args(argv)
|
|
233
|
+
if not hasattr(args, "func"):
|
|
234
|
+
parser.print_help()
|
|
235
|
+
return 1
|
|
236
|
+
return args.func(args)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
if __name__ == "__main__":
|
|
240
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Built-in starter policies."""
|
|
2
|
+
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
|
|
5
|
+
from .policy import DEFAULT_POLICY
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _policy(environment="prod", tools=None, decisions=None, description=None):
|
|
9
|
+
pol = deepcopy(DEFAULT_POLICY)
|
|
10
|
+
pol["environment"] = environment
|
|
11
|
+
if decisions:
|
|
12
|
+
pol["decisions"].update(decisions)
|
|
13
|
+
if tools:
|
|
14
|
+
pol["tools"].update(tools)
|
|
15
|
+
if description:
|
|
16
|
+
pol["description"] = description
|
|
17
|
+
return pol
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
PACKS = {
|
|
21
|
+
"baseline": _policy(
|
|
22
|
+
description="Default Tide policy: approve risky writes, block destructive prod actions.",
|
|
23
|
+
),
|
|
24
|
+
"support-refunds": _policy(
|
|
25
|
+
description="Support/refund agents: let reads and drafts through, require approval for refunds/customer-visible sends, block destructive customer deletes.",
|
|
26
|
+
tools={
|
|
27
|
+
"stripe.refund": {"decision": "require_approval", "reason": "refund moves money"},
|
|
28
|
+
"stripe.refunds.create": {"decision": "require_approval", "reason": "refund moves money"},
|
|
29
|
+
"shopify.refund": {"decision": "require_approval", "reason": "refund moves money"},
|
|
30
|
+
"shopify.order.refund": {"decision": "require_approval", "reason": "refund moves money"},
|
|
31
|
+
"zendesk.ticket.reply": {"decision": "require_approval", "reason": "customer-visible message"},
|
|
32
|
+
"intercom.message.send": {"decision": "require_approval", "reason": "customer-visible message"},
|
|
33
|
+
"stripe.customer.delete": {"decision": "block", "reason": "customer deletion is outside agent scope"},
|
|
34
|
+
"shopify.customer.delete": {"decision": "block", "reason": "customer deletion is outside agent scope"},
|
|
35
|
+
},
|
|
36
|
+
),
|
|
37
|
+
"sql-analyst": _policy(
|
|
38
|
+
description="SQL analyst agents: allow read-only queries, block write/destructive classes in every environment.",
|
|
39
|
+
decisions={
|
|
40
|
+
"internal_write": {"dev": "block", "prod": "block"},
|
|
41
|
+
"destructive": {"dev": "block", "prod": "block"},
|
|
42
|
+
},
|
|
43
|
+
),
|
|
44
|
+
"mcp-baseline": _policy(
|
|
45
|
+
description="MCP agents: approve shell/filesystem/GitHub writes, block common destructive calls.",
|
|
46
|
+
tools={
|
|
47
|
+
"filesystem.write_file": {"decision": "require_approval", "reason": "file write through MCP"},
|
|
48
|
+
"filesystem.delete_file": {"decision": "block", "reason": "file deletion through MCP"},
|
|
49
|
+
"shell.run": {"decision": "require_approval", "reason": "shell command through MCP"},
|
|
50
|
+
"github.merge_pull_request": {"decision": "require_approval", "reason": "repository state change"},
|
|
51
|
+
"github.delete_branch": {"decision": "require_approval", "reason": "repository state change"},
|
|
52
|
+
"kubernetes.delete": {"decision": "block", "reason": "destructive infrastructure action"},
|
|
53
|
+
"terraform.destroy": {"decision": "block", "reason": "destructive infrastructure action"},
|
|
54
|
+
},
|
|
55
|
+
),
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def names():
|
|
60
|
+
return sorted(PACKS)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get(name, environment=None):
|
|
64
|
+
if name not in PACKS:
|
|
65
|
+
raise KeyError(name)
|
|
66
|
+
pol = deepcopy(PACKS[name])
|
|
67
|
+
if environment:
|
|
68
|
+
pol["environment"] = environment
|
|
69
|
+
pol["pack"] = name
|
|
70
|
+
return pol
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tide-runtime
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6
|
|
4
4
|
Summary: Tide runtime policy, approvals, and earned-use ledger for AI agent tools.
|
|
5
5
|
Project-URL: Repository, https://github.com/rippletideco/rippletide-package
|
|
6
6
|
Requires-Python: >=3.8
|
|
@@ -13,9 +13,12 @@ earned-use logging.
|
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
15
|
pip install tide-runtime
|
|
16
|
-
tide
|
|
16
|
+
tide init --pack support-refunds --environment prod
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
`tide init --dry-run --pack <name>` prints the policy before writing it.
|
|
20
|
+
Built-in packs: `baseline`, `support-refunds`, `sql-analyst`, `mcp-baseline`.
|
|
21
|
+
|
|
19
22
|
```python
|
|
20
23
|
from tide import configure, controlled_tool, record_outcome
|
|
21
24
|
|
|
@@ -35,3 +38,10 @@ except Exception:
|
|
|
35
38
|
|
|
36
39
|
Default files live in `.tide/ledger/`. Override with `TIDE_LEDGER_DIR` or
|
|
37
40
|
`configure(project="...")`.
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
tide approvals
|
|
44
|
+
tide approve <id>
|
|
45
|
+
tide deny <id> --reason "not in scope"
|
|
46
|
+
tide report
|
|
47
|
+
```
|
|
@@ -2,9 +2,12 @@ README.md
|
|
|
2
2
|
pyproject.toml
|
|
3
3
|
tests/test_runtime.py
|
|
4
4
|
tide/__init__.py
|
|
5
|
+
tide/cli.py
|
|
6
|
+
tide/packs.py
|
|
5
7
|
tide/policy.py
|
|
6
8
|
tide/store.py
|
|
7
9
|
tide_runtime.egg-info/PKG-INFO
|
|
8
10
|
tide_runtime.egg-info/SOURCES.txt
|
|
9
11
|
tide_runtime.egg-info/dependency_links.txt
|
|
12
|
+
tide_runtime.egg-info/entry_points.txt
|
|
10
13
|
tide_runtime.egg-info/top_level.txt
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|