tessera-proxy 0.2.0__py3-none-any.whl

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.
tessera/__init__.py ADDED
@@ -0,0 +1,143 @@
1
+ """Tessera — a provenance control plane for tool-using agents.
2
+
3
+ Tessera contains the *blast radius* of a successful prompt injection. It does
4
+ not try to stop the model from being fooled (conceded as unsolvable in-band);
5
+ it ensures that when the model is fooled, the damage is bounded to actions that
6
+ are reversible, non-exfiltrating, or human-approved.
7
+
8
+ The v0.2 wedge — what this package implements — is a provenance-tracking MCP
9
+ proxy that:
10
+
11
+ 1. labels every tool result by its trust origin (:mod:`tessera.labels`),
12
+ 2. propagates those labels through an agent session (:mod:`tessera.session`),
13
+ 3. classifies tools by blast radius (:mod:`tessera.classification`), and
14
+ 4. enforces the single killer flow rule (:mod:`tessera.policy`):
15
+
16
+ Data that originated untrusted may not become an argument to an
17
+ exfiltration-capable or irreversible tool without passing a
18
+ declassifier or human approval.
19
+
20
+ Every label, decision, and escalation is written to an append-only audit
21
+ ledger (:mod:`tessera.ledger`).
22
+ """
23
+
24
+ from tessera.capabilities import (
25
+ Capability,
26
+ CapabilityEngine,
27
+ CapabilityResult,
28
+ Caveat,
29
+ arg_equals,
30
+ arg_in,
31
+ arg_matches,
32
+ expires_at,
33
+ max_uses,
34
+ tool_is,
35
+ )
36
+ from tessera.classification import (
37
+ BlastRadius,
38
+ Reversibility,
39
+ ToolProfile,
40
+ classify_tool,
41
+ )
42
+ from tessera.declassify import (
43
+ AllowlistDeclassifier,
44
+ BooleanDeclassifier,
45
+ Declassifier,
46
+ DeclassifyOutcome,
47
+ EnumDeclassifier,
48
+ IntegerDeclassifier,
49
+ IsoDateDeclassifier,
50
+ NumberDeclassifier,
51
+ PatternDeclassifier,
52
+ )
53
+ from tessera.labels import Origin, TrustLevel
54
+ from tessera.plan import (
55
+ Call,
56
+ Const,
57
+ Field,
58
+ Plan,
59
+ PlanInterpreter,
60
+ PlanRun,
61
+ Step,
62
+ Var,
63
+ call,
64
+ const,
65
+ plan,
66
+ step,
67
+ var,
68
+ )
69
+ from tessera.planner import (
70
+ ClaudePlanner,
71
+ Planner,
72
+ PlannerError,
73
+ ScriptedPlanner,
74
+ ToolSpec,
75
+ parse_plan,
76
+ plan_to_dict,
77
+ )
78
+ from tessera.policy import Decision, PolicyEngine, PolicyResult, Strictness
79
+ from tessera.provenance import LabeledValue, ProvenanceNode
80
+ from tessera.sdk import Blocked, Guard, protect, tool
81
+ from tessera.session import Session
82
+
83
+ __version__ = "0.2.0"
84
+
85
+ __all__ = [
86
+ "AllowlistDeclassifier",
87
+ "BlastRadius",
88
+ "Blocked",
89
+ "BooleanDeclassifier",
90
+ "Capability",
91
+ "CapabilityEngine",
92
+ "CapabilityResult",
93
+ "Caveat",
94
+ "Decision",
95
+ "Declassifier",
96
+ "DeclassifyOutcome",
97
+ "EnumDeclassifier",
98
+ "Guard",
99
+ "IntegerDeclassifier",
100
+ "IsoDateDeclassifier",
101
+ "LabeledValue",
102
+ "NumberDeclassifier",
103
+ "Call",
104
+ "Const",
105
+ "Field",
106
+ "ClaudePlanner",
107
+ "Origin",
108
+ "PatternDeclassifier",
109
+ "Plan",
110
+ "PlanInterpreter",
111
+ "PlanRun",
112
+ "Planner",
113
+ "PlannerError",
114
+ "PolicyEngine",
115
+ "PolicyResult",
116
+ "ScriptedPlanner",
117
+ "ToolSpec",
118
+ "parse_plan",
119
+ "plan_to_dict",
120
+ "ProvenanceNode",
121
+ "Reversibility",
122
+ "Session",
123
+ "Step",
124
+ "Strictness",
125
+ "ToolProfile",
126
+ "TrustLevel",
127
+ "Var",
128
+ "arg_equals",
129
+ "arg_in",
130
+ "arg_matches",
131
+ "call",
132
+ "classify_tool",
133
+ "const",
134
+ "expires_at",
135
+ "max_uses",
136
+ "plan",
137
+ "protect",
138
+ "step",
139
+ "tool",
140
+ "tool_is",
141
+ "var",
142
+ "__version__",
143
+ ]
@@ -0,0 +1,301 @@
1
+ """The capability engine — kill ambient authority.
2
+
3
+ A classic agent holds a credential that works for *any* call to a tool: read
4
+ the inbox, send mail to anyone, delete any file. That ambient authority is what
5
+ makes a hijacked agent dangerous. Tessera replaces it with **capabilities**:
6
+ unforgeable, just-in-time, narrowly-scoped grants derived from the plan —
7
+ "send_email to bob@ with this payload", never "send_email to anyone". They
8
+ expire fast, and they **attenuate down delegation chains**: when one capability
9
+ is narrowed for a sub-agent or a downstream tool, permissions only ever shrink.
10
+
11
+ The construction is macaroon-style. A capability carries a list of **caveats**
12
+ (constraints) and an HMAC signature chained over them:
13
+
14
+ sig0 = HMAC(root_key, capability_id)
15
+ sig_i = HMAC(sig_{i-1}, serialize(caveat_i))
16
+
17
+ From this one trick the security properties fall out:
18
+
19
+ * **Unforgeable** — only a holder of ``root_key`` can mint a valid root
20
+ signature, so an agent cannot fabricate a capability from nothing.
21
+ * **Attenuation needs no secret, and can only narrow** — appending a caveat
22
+ and extending the chain (``HMAC(current_sig, new_caveat)``) requires no key,
23
+ but every caveat is an *additional* restriction; you can never drop or
24
+ reorder one without breaking the signature.
25
+ * **Verifiable** — the engine, holding ``root_key``, recomputes the whole
26
+ chain and checks every caveat against the proposed call.
27
+
28
+ ``max_uses`` is inherently stateful, so the engine keeps a server-side use
29
+ counter keyed by capability id. Attenuated children keep their parent's id
30
+ (required so the HMAC chain still verifies from the root), which means a
31
+ capability lineage shares one use budget.
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ import hashlib
37
+ import hmac
38
+ import os
39
+ import re
40
+ import time
41
+ from dataclasses import dataclass, field
42
+ from typing import Any, Mapping
43
+
44
+
45
+ def _hmac(key: bytes, msg: str) -> bytes:
46
+ return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()
47
+
48
+
49
+ # --------------------------------------------------------------------------
50
+ # Caveats
51
+ # --------------------------------------------------------------------------
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class Caveat:
56
+ """A single restriction on what a capability authorizes.
57
+
58
+ ``kind`` selects the predicate; ``params`` carries its arguments. The
59
+ ``serialize`` form is what gets folded into the HMAC chain, so it must be
60
+ canonical and stable.
61
+ """
62
+
63
+ kind: str
64
+ params: tuple[tuple[str, Any], ...]
65
+
66
+ @classmethod
67
+ def make(cls, kind: str, **params: Any) -> "Caveat":
68
+ return cls(kind=kind, params=tuple(sorted(params.items())))
69
+
70
+ def get(self, key: str) -> Any:
71
+ for k, v in self.params:
72
+ if k == key:
73
+ return v
74
+ return None
75
+
76
+ def serialize(self) -> str:
77
+ body = ",".join(f"{k}={v!r}" for k, v in self.params)
78
+ return f"{self.kind}({body})"
79
+
80
+ def check(self, tool: str, args: Mapping[str, Any], ctx: "_VerifyContext") -> tuple[bool, str]:
81
+ checker = _CAVEAT_CHECKS.get(self.kind)
82
+ if checker is None:
83
+ return False, f"unknown caveat kind {self.kind!r}"
84
+ return checker(self, tool, args, ctx)
85
+
86
+ def describe(self) -> str:
87
+ return self.serialize()
88
+
89
+
90
+ def tool_is(name: str) -> Caveat:
91
+ """The call must target tool ``name``."""
92
+ return Caveat.make("tool_is", name=name)
93
+
94
+
95
+ def arg_equals(arg: str, value: Any) -> Caveat:
96
+ """``args[arg]`` must equal ``value`` (compared as strings)."""
97
+ return Caveat.make("arg_equals", arg=arg, value=str(value))
98
+
99
+
100
+ def arg_matches(arg: str, pattern: str) -> Caveat:
101
+ """``args[arg]`` must fully match the regex ``pattern``."""
102
+ return Caveat.make("arg_matches", arg=arg, pattern=pattern)
103
+
104
+
105
+ def arg_in(arg: str, values: list[str]) -> Caveat:
106
+ """``args[arg]`` must be one of ``values``."""
107
+ return Caveat.make("arg_in", arg=arg, values="|".join(sorted(str(v) for v in values)))
108
+
109
+
110
+ def expires_at(epoch_seconds: float) -> Caveat:
111
+ """The capability is invalid after ``epoch_seconds``."""
112
+ return Caveat.make("expires_at", ts=float(epoch_seconds))
113
+
114
+
115
+ def max_uses(n: int) -> Caveat:
116
+ """The capability (lineage) may authorize at most ``n`` calls."""
117
+ return Caveat.make("max_uses", n=int(n))
118
+
119
+
120
+ def _check_tool_is(c, tool, args, ctx):
121
+ want = c.get("name")
122
+ return (tool == want, f"tool must be {want!r}, got {tool!r}")
123
+
124
+
125
+ def _check_arg_equals(c, tool, args, ctx):
126
+ arg, want = c.get("arg"), c.get("value")
127
+ got = str(args.get(arg))
128
+ return (got == want, f"{arg} must equal {want!r}, got {got!r}")
129
+
130
+
131
+ def _check_arg_matches(c, tool, args, ctx):
132
+ arg, pattern = c.get("arg"), c.get("pattern")
133
+ got = str(args.get(arg, ""))
134
+ ok = re.fullmatch(pattern, got) is not None
135
+ return (ok, f"{arg}={got!r} must match /{pattern}/")
136
+
137
+
138
+ def _check_arg_in(c, tool, args, ctx):
139
+ arg = c.get("arg")
140
+ allowed = set(c.get("values").split("|")) if c.get("values") else set()
141
+ got = str(args.get(arg))
142
+ return (got in allowed, f"{arg}={got!r} must be one of {sorted(allowed)}")
143
+
144
+
145
+ def _check_expires_at(c, tool, args, ctx):
146
+ ts = c.get("ts")
147
+ return (ctx.now <= ts, f"capability expired at {ts} (now {ctx.now:.0f})")
148
+
149
+
150
+ def _check_max_uses(c, tool, args, ctx):
151
+ n = c.get("n")
152
+ return (ctx.uses < n, f"capability used {ctx.uses}/{n} times")
153
+
154
+
155
+ _CAVEAT_CHECKS = {
156
+ "tool_is": _check_tool_is,
157
+ "arg_equals": _check_arg_equals,
158
+ "arg_matches": _check_arg_matches,
159
+ "arg_in": _check_arg_in,
160
+ "expires_at": _check_expires_at,
161
+ "max_uses": _check_max_uses,
162
+ }
163
+
164
+ # Aliases so the convenience minter can build caveats without its keyword
165
+ # parameters shadowing the constructor functions of the same name.
166
+ _cav_arg_equals = arg_equals
167
+ _cav_arg_in = arg_in
168
+ _cav_arg_matches = arg_matches
169
+
170
+
171
+ # --------------------------------------------------------------------------
172
+ # Capability
173
+ # --------------------------------------------------------------------------
174
+
175
+
176
+ @dataclass(frozen=True)
177
+ class Capability:
178
+ """An unforgeable, attenuable grant. Identified by ``id``; the ``signature``
179
+ is the HMAC chain over ``caveats`` rooted at ``id``."""
180
+
181
+ id: str
182
+ caveats: tuple[Caveat, ...]
183
+ signature: bytes
184
+
185
+ def attenuate(self, *caveats: Caveat) -> "Capability":
186
+ """Return a strictly narrower capability with extra caveats appended.
187
+
188
+ Requires no secret: the chain is extended from the current signature.
189
+ The result can only be *more* restricted than ``self``.
190
+ """
191
+ sig = self.signature
192
+ for cav in caveats:
193
+ sig = _hmac(sig, cav.serialize())
194
+ return Capability(id=self.id, caveats=self.caveats + tuple(caveats), signature=sig)
195
+
196
+ def describe(self) -> str:
197
+ caveat_str = "; ".join(c.describe() for c in self.caveats) or "(no caveats)"
198
+ return f"cap {self.id[:8]} [{caveat_str}]"
199
+
200
+
201
+ @dataclass
202
+ class CapabilityResult:
203
+ """The outcome of checking a capability against a proposed call."""
204
+
205
+ authorized: bool
206
+ reason: str
207
+ capability_id: str | None = None
208
+
209
+ @property
210
+ def denied(self) -> bool:
211
+ return not self.authorized
212
+
213
+
214
+ @dataclass
215
+ class _VerifyContext:
216
+ now: float
217
+ uses: int
218
+
219
+
220
+ # --------------------------------------------------------------------------
221
+ # Engine
222
+ # --------------------------------------------------------------------------
223
+
224
+
225
+ @dataclass
226
+ class CapabilityEngine:
227
+ """Mints, attenuates, and verifies capabilities against a root key."""
228
+
229
+ root_key: bytes = field(default_factory=lambda: os.urandom(32))
230
+ _uses: dict[str, int] = field(default_factory=dict)
231
+
232
+ def mint(self, *caveats: Caveat, capability_id: str | None = None) -> Capability:
233
+ """Mint a fresh root capability with the given caveats (the secret path)."""
234
+ cap_id = capability_id or os.urandom(12).hex()
235
+ sig = _hmac(self.root_key, cap_id)
236
+ for cav in caveats:
237
+ sig = _hmac(sig, cav.serialize())
238
+ return Capability(id=cap_id, caveats=tuple(caveats), signature=sig)
239
+
240
+ def mint_for(
241
+ self,
242
+ tool: str,
243
+ *,
244
+ arg_equals: Mapping[str, Any] | None = None,
245
+ arg_in: Mapping[str, list[str]] | None = None,
246
+ arg_matches: Mapping[str, str] | None = None,
247
+ expires_in: float | None = None,
248
+ uses: int | None = None,
249
+ ) -> Capability:
250
+ """Convenience minter for the common 'one tool, scoped args' grant."""
251
+ caveats: list[Caveat] = [tool_is(tool)]
252
+ for k, v in (arg_equals or {}).items():
253
+ caveats.append(_cav_arg_equals(k, v))
254
+ for k, vals in (arg_in or {}).items():
255
+ caveats.append(_cav_arg_in(k, vals))
256
+ for k, pat in (arg_matches or {}).items():
257
+ caveats.append(_cav_arg_matches(k, pat))
258
+ if expires_in is not None:
259
+ caveats.append(expires_at(time.time() + expires_in))
260
+ if uses is not None:
261
+ caveats.append(max_uses(uses))
262
+ return self.mint(*caveats)
263
+
264
+ def _signature_valid(self, cap: Capability) -> bool:
265
+ expected = _hmac(self.root_key, cap.id)
266
+ for cav in cap.caveats:
267
+ expected = _hmac(expected, cav.serialize())
268
+ return hmac.compare_digest(expected, cap.signature)
269
+
270
+ def verify(
271
+ self,
272
+ cap: Capability,
273
+ tool: str,
274
+ args: Mapping[str, Any],
275
+ *,
276
+ now: float | None = None,
277
+ consume: bool = False,
278
+ ) -> CapabilityResult:
279
+ """Check that ``cap`` authorizes calling ``tool`` with ``args``.
280
+
281
+ Verifies the HMAC chain (unforgeability), then every caveat. With
282
+ ``consume=True``, increments the use counter on success — call it only
283
+ for the capability you actually use.
284
+ """
285
+ if not self._signature_valid(cap):
286
+ return CapabilityResult(False, "invalid signature (forged or tampered)", cap.id)
287
+ ctx = _VerifyContext(now=now if now is not None else time.time(), uses=self._uses.get(cap.id, 0))
288
+ for cav in cap.caveats:
289
+ ok, reason = cav.check(tool, args, ctx)
290
+ if not ok:
291
+ return CapabilityResult(False, reason, cap.id)
292
+ if consume:
293
+ self._uses[cap.id] = ctx.uses + 1
294
+ return CapabilityResult(True, "authorized by capability", cap.id)
295
+
296
+ def consume(self, cap: Capability) -> None:
297
+ """Record one use of a capability lineage."""
298
+ self._uses[cap.id] = self._uses.get(cap.id, 0) + 1
299
+
300
+ def uses_of(self, cap: Capability) -> int:
301
+ return self._uses.get(cap.id, 0)