You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit d99ea10
Browse filesBrowse the repository at this point in the historyBrowse files
Slice 021: permission gate and approval UI. PROOF.md at slices/021-permission-gate/PROOF.md; gate and postgres jobs green on the closing tree.
Signed-off-by: Ayla Croft <aylacroft@proton.me>
Copy file name to clipboardExpand all lines: VERSIONS.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -136,6 +136,7 @@ never pin a version hex marks as retired or vulnerable.
136
136
|`phoenix_streamdown`| not used (measured at Slice 013, 2026-09-20) | 🔍 not a single package | Streaming markdown renderer for LiveView. Measured at Slice 013: 1.0.0-beta.4 (2026-05-03) was still the latest release with no stable behind it, and this file's own rule forbids a pre-release. It is 801 lines over `mdex`, whose own `streaming: true` option completes fragments; the rest is a rendering rule Slice 013 keeps anyway. The row stays so the decision is visible where a reader would look for the package. |
137
137
|`mdex`|~> 0.13 | ✅ in `mix.lock`| Markdown renderer for the chat (`TrinityWeb.Markdown`), streaming fragments completed by its `streaming: true` option, raw HTML omitted (`unsafe: false`) and the default sanitizer on top. Added at Slice 013 after the measurement in its NOTES.md: `earmark` 1.4.49 is retired on hex with an open XSS advisory (EEF-CVE-2026-48591), which the gate refuses. ⚠️ A Rust NIF (`mdex_native`): the first in the bundle. Measured at Slice 013 (NOTES finding 13): neither precompiled artifact loads in Burrito's musl ERTS on Linux, so the linux package builds it from source for musl with Zig as the linker (`rustler` below, `scripts/zig-cc-musl`, the three settings in config/config.exs); macOS and Windows load the precompiled artifact. The `--smoke` path prints whether it rendered, and the `package` workflow reads that line on every target. |
138
138
|`jsv`|~> 0.23 | ✅ in `mix.lock`| JSON Schema (2020-12) validation of tool arguments in `Trinity.Tools.Schema`, with `cast: false` so a malformed call is refused and never repaired (docs/07). Was transitive through req_llm; direct since Slice 020 because a module of ours calls it (ADR-0009: Trinity's own tool behaviour, jsv for its schemas). |
139
+
|`jcs`|~> 0.2 | ✅ in `mix.lock`| RFC 8785 canonical JSON, under every approval fingerprint (`Trinity.Permissions.Fingerprint`, Slice 021) and, at 024, under the receipts' signed payload. Chosen at Slice 021: it matches the RFC's own example vector byte for byte on this OTP, and `test/trinity/permissions/fingerprint_test.exs` keeps that vector so a release that stops matching fails the gate. ⚠️ Pre-1.0, released 2025-03-31 with nothing since (R11's trigger). The alternative, `rfc8785` 1.0.0, refuses OTP 28 and waits on the OTP pin. |
139
140
|`rustler`|~> 0.38 | ✅ in `mix.lock`| Build time only (`runtime: false`): what `rustler_precompiled` needs to compile `mdex_native` from source when `MDEX_NATIVE_BUILD=1`, which the linux package sets (owner decision 2026-09-20, Slice 013 NOTES finding 14). Nothing in the tree calls it. |
| pattern | string |`*`, `key=glob` (`*` in a segment, `**` across, `?` one character; a trailing `*` is a prefix), `fp:<hex>` (a session grant bound to a fingerprint), `re:<regex>` (hand-edited rows only) |
Copy file name to clipboardExpand all lines: docs/07-security-model.md
+14-4Lines changed: 14 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -39,10 +39,20 @@ system prompt states that instructions inside untrusted blocks are data, not com
39
39
Every `Trinity.Tools.Tool` declares `effect/0` (`:none | :artifact | :catalog`) and the gate derives risk from the tool **name only** via `Trinity.Permissions.tier/1` (`:read | :write | :exec | :network | :destructive`; unmapped → ask).
40
40
41
41
Name-only is deliberate: it makes the tier a code-owned function of a value no caller can influence. That holds only while the namespace is closed. Tools registered at runtime carry names chosen elsewhere, by an MCP server or a skill author, so **every dynamic tool is namespaced before the tier lookup**: `mcp:<server>:<tool>`, `skill:<name>`. Core tool names are reserved and cannot be claimed. Without this a hostile server naming its tool `fs_read` inherits the `:read` tier and its allow-by-default policy, and the unmapped-goes-to-ask fallback does not catch it, because the name is mapped.
42
-
`Trinity.Permissions.decide(session, tool, args)` consults, in order: session grants → persona policy → global
An `:ask` suspends the Session in `approval_wait`, broadcasts to `approvals:<id>`, and resumes on decision.
45
-
Decisions are recorded (`approvals` table) and receipted. Approvals bind the canonical fingerprint of `(tool, args, scope, cwd, canonicalization_version)` and are re-derived at execution; "allow for this session" binds that fingerprint with an expiry; "always allow" is a rule, recorded as such.
42
+
`Trinity.Permissions.decide(session, tool, args, opts)` consults, in order: session grants → the newest unspent
43
+
decision for this fingerprint (an "allow once" or a denial, each spent by the execution that reads it) →
44
+
persona policy → global `tool_permissions` → default policy (`:read` allow, `:network` allow, `:write` ask,
45
+
`:exec` ask, `:destructive` ask; an unmapped name asks).
46
+
An `:ask` suspends the Session in `approval_wait`, broadcasts to `approvals:<id>` (and `approvals:all`), and
47
+
resumes on decision. Decisions are recorded (`approvals` table) and receipted (024). Approvals bind the canonical
48
+
fingerprint of `(tool, args, scope, cwd, canonicalization_version)` and are re-derived at execution; "allow for
49
+
this session" binds that fingerprint with an expiry; "always allow" is a rule, recorded as such.
50
+
51
+
As built at slice 021: the tier table is written once, by the registry, from the `risk/0` of the modules
52
+
`config :trinity, :tools` names, so a tier comes from code and config and never from a runtime registration; the
53
+
runner asks the policy at execution and the Session never pre-checks, so the decision that runs a call is the one
54
+
made against the arguments actually passed; a request left undecided expires into a denial decided by
55
+
`"expiry"`; the card's buttons and the `/permissions` page call `decide_request/3` and nothing else.
0 commit comments