Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .sobelow-skips
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
Config.CSP: Missing Content-Security-Policy,lib/trinity_web/router.ex:14,1DC4768
Traversal.FileModule: Directory Traversal in `File.mkdir_p`,lib/trinity/data_dir/lock.ex:60,4ECF02C
Traversal.FileModule: Directory Traversal in `File.read`,lib/trinity/data_dir/lock.ex:146,5EFB666
Traversal.FileModule: Directory Traversal in `File.read`,lib/trinity/sandbox/skill.ex:75,30481DE
Traversal.FileModule: Directory Traversal in `File.rm`,lib/trinity/data_dir/lock.ex:85,7D2B826
Traversal.FileModule: Directory Traversal in `File.write`,lib/trinity/data_dir/lock.ex:135,42D2817
1 change: 1 addition & 0 deletions .sobelow-skips.reasons
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@
5EFB666 Rekeyed at slice 013 (the moduledoc grew three lines for the macOS and Windows liveness check). Slice 010, Trinity.DataDir.Lock read/1: reads <data_dir>/LOCK, a path this module composes from the data directory and a constant file name; no caller supplies it.
7D2B826 Rekeyed at slice 013 (the moduledoc grew three lines for the macOS and Windows liveness check). Slice 010, Trinity.DataDir.Lock.release/2: removes <data_dir>/LOCK only when the file carries this process's own token; same composed path as the read.
42D2817 Rekeyed at slice 013 (the moduledoc grew three lines for the macOS and Windows liveness check). Slice 010, Trinity.DataDir.Lock take_over/2: rewrites <data_dir>/LOCK after the recorded holder's pid is found dead; same composed path, same constant name.
30481DE Slice 110, the skill script runner: the path sobelow cannot trace is the return of `Trinity.Sandbox.Skill.script_path/2`, which resolves it with `Path.expand/2` and refuses anything landing outside the skill's own `scripts/` directory. sobelow sees a function parameter and stops there; it cannot follow the containment into the previous call. The check is public precisely so it can be tested rather than trusted, and `test/trinity/sandbox/skill_test.exs` asserts the traversal case, the absolute-path case and the pathless-skill case against the real function rather than a copy of it. This is the only finding sobelow reports in the tree.
35 changes: 35 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,41 @@ evidence for each increment is retained by the maintainers and summarised here.

## 2026-09-24

### `slice/110` — Executable skills in an in-process sandbox
Skills that can execute need a real programming surface. Trinity now has one that costs no trust: a
`run_lua` tool and skill-owned scripts run Lua inside this process, with no operating system
process, no filesystem handle and no socket. A model asked to total a column produces a number that
looks right; a script produces one that is right and leaves the script in the transcript.

The limits are the virtual machine's rather than a promise. The runner sets its own heap ceiling and
the machine kills it at the boundary; a wall clock bounds the run; the pool refuses an over-limit run
by name instead of queueing it invisibly, because a run that waits an unbounded time to start has no
wall-clock bound at all.

**One limit is reported rather than claimed, and the measurement is published.** The library offers a
reduction cap enforced by polling every hundred milliseconds, so a cap of a thousand and a cap of a
million both stop a tight loop at twenty to thirty-four million reductions. Reductions are therefore
a statistic here and the bound that stops a runaway script is time. `docs/sandbox.md` carries the
numbers.

What a script wants *done* it asks for through the same executor a model's own tool call uses, so it
is decided by the same permission gate and leaves the same receipts. There is no second door, and a
test asserts a denied call gives the script nothing. A call needing a person fails immediately rather
than holding the sandbox open, because a run is bounded in seconds and a person is not: letting a
script block would turn every approval into a timeout.

A skill may ship a script of its own. Where that script may read is checked by resolving the path
rather than by trusting it, so a traversal is caught by where it lands rather than by what it looks
like. The check is public so the test exercises the real function rather than a copy of it.

The skill scanner gained a rule for scripts reaching at what the sandbox removes. It is a reviewer's
signal and the changelog says so: the control is that those globals are absent at run time, and a
regular expression over source text would be a poor last line.

`docs/sandbox.md` states what this does not protect against as plainly as what it does, including
that the sandbox constrains the script and the permission gate constrains the effect, and that
reading the first as a guarantee about the second is the mistake the page exists to prevent.

### `slice/014` — The prompt window is the recent conversation
A session that held more than five hundred messages was building every subsequent request from its
**first** five hundred. The query that fetched the window ordered oldest-first and then took a
Expand Down
34 changes: 24 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,13 @@ desktop application. Apache-2.0, developed in the open from the first commit.

## Status

Pre-alpha, and usable from source. Milestones M0 to M4 are approved, **M5a Automates** with them
(050, the scheduler; 059, the MCP measurement; 060, the MCP client; 061, the MCP server; 062, MCP
authorization), **M5b Reaches** in part (070, the gateway core; 080, subagents) and the first slice
of **M6 Ships** (090, observability and the cost ledger), with 002 (the supply chain slice) beside
them: 29 slices, each merged with a merge commit and tagged `slice/NNN`
(`git tag -l 'slice/*' | wc -l` → 30, on 2026-09-24: one more tag than slices, because
Pre-alpha, and usable from source. Milestones **M0** to **M4** are approved, **M5a Automates** with
them (050, the scheduler; 059, the MCP measurement; 060, the MCP client; 061, the MCP server; 062,
MCP authorization), **M5b Reaches** in part (070, the gateway core; 080, subagents), the first slice
of **M6 Ships** (090, observability and the cost ledger), **M7 Sandboxed** (110, executable skills in
an in-process sandbox), and the first slice of **M9 Donatable** (120, the open-source hygiene audit),
with 002 (the supply chain slice) beside them: 36 slices, each merged with a merge commit and tagged
`slice/NNN` (`git tag -l 'slice/*' | wc -l` → 37, on 2026-09-24: one more tag than slices, because
`slice/090` was pushed at the wrong commit before its merge landed and this repository's ruleset
forbids moving or deleting a tag; the correct one is `slice/090.1`, and `git tag -n20 slice/090.1`
says so in its own annotation rather than leaving a reader to work it out). What that means in
Expand Down Expand Up @@ -134,6 +135,16 @@ enforces it.
by model, by session, by persona, against budgets that warn and, when you ask them to, refuse.
`/activity` shows the live stream with the spend beside it; a LiveDashboard page lists the
running sessions, asking each process its state rather than trusting a row.
- **Computes, rather than estimates.** A `run_lua` tool runs a Lua script inside this process: no
operating system process, no filesystem handle, no socket. A model asked to total a column
produces a number that looks right; a script produces one that is right and leaves the script in
the transcript for anyone to check. The limits are the virtual machine's rather than a promise:
the runner sets its own heap ceiling and is killed at it, a wall clock bounds the run, and the
pool refuses an over-limit run by name instead of queueing it invisibly. What a script wants
*done* it asks for through `trinity.tool`, which is decided by the same permission gate and leaves
the same receipts as any other call, so there is no second door. A skill can ship a script of its
own, and where that script may read is checked by resolving the path rather than by trusting it.
`docs/sandbox.md` states what this does **not** protect against as plainly as what it does.
- **Runs on a schedule.** Tasks on the `/tasks` page: a prompt, a persona, the skills to hint,
and when (a cron expression, a one-shot time, or a phrase like "every weekday at 9am" the
model turns into cron). Each run is a fresh conversation you can open, its result waits on
Expand All @@ -144,8 +155,9 @@ enforces it.

Not there yet: the messaging platforms themselves (071 Telegram and 072 Discord, which need a bot
token the project does not yet hold), the native desktop shell and signed releases (the rest of M6),
executable skills in a sandbox (M7). The [Milestones](#milestones) section below sets out the order
the remaining work is being built in.
store-and-forward receipts for disconnected operation (026, waiting on an answer from the external
authority plane). The [Milestones](#milestones) section below sets out the order the remaining work
is being built in.

The interface is a local web page; the desktop shell exists as a packaging spike, not a product.
If you want to follow along, watch the roadmap and the tags.
Expand Down Expand Up @@ -244,7 +256,8 @@ rather than described by it.
| Auditability | Decisions and effects are recorded in an Ed25519-signed hash chain with checkpoints and a verifier, so an operator can reconstruct what was done, by whom and under what decision. |
| Untrusted content | Anything arriving from outside the machine is marked untrusted at the boundary and is never treated as instruction. |
| Approved cryptography | A dedicated CI leg builds from source and runs the cryptographic properties inside a FIPS-mode container, so statements about approved algorithms are measured on that leg rather than asserted. |
| Per-file licensing | Every one of the 609 files in this repository carries copyright and licence information, following the REUSE Specification 3.3. `reuse lint` exits 0 and runs in CI. What is not this project's says so: the desktop shell's icons are Tauri's template icons and are attributed to Tauri, not to us. |
| Exercised by generated input | The canonicalisers, the argument validator and the receipt chain are tested with property-based testing as well as by example, because code that answers "is this the same thing" fails on the spellings nobody thought of. The suite carries a planted defect it catches and the example tests miss, so a property that has never failed is not mistaken for one that cannot. |
| Per-file licensing | Every one of the 630 files in this repository carries copyright and licence information, following the REUSE Specification 3.3. `reuse lint` exits 0 and runs in CI. What is not this project's says so: the desktop shell's icons are Tauri's template icons and are attributed to Tauri, not to us. |
| Supply chain | Dependency and licence audits run on every commit; dependency versions are pinned in `VERSIONS.md` and verified against the lock file by the gate. A CycloneDX bill of materials is generated by the gate on every commit and ships beside every packaged binary, so whoever holds the artifact holds the list of what is in it; the bill states its own coverage inside the document, including what it does not cover. |
| Provenance | Every commit carries a Developer Certificate of Origin sign-off, enforced by a hook and independently by CI. Every packaged binary carries build provenance attested through the workflow's own identity and recorded in a public transparency log, verified in the same run that produced it, so a holder can check where the bytes were built without trusting this page. |
| Independent self-certification | The project holds the [OpenSSF Best Practices **silver** badge](https://www.bestpractices.dev/projects/14772) (awarded 2026-09-23), assessed against the Open Source Security Foundation's published criteria at two tiers. Every required criterion at both is met. What is not met is stated rather than stretched: at passing, three suggested criteria (semantic versioning, which begins at the first supported release, and two concerning dynamic analysis tooling the project does not run); at silver, a bus factor of two and an accessibility assessment, both suggested-tier, and signed version tags. |
Expand All @@ -266,7 +279,7 @@ rather than described by it.
| M7 Sandboxed | Executable skills in an in-VM sandbox | 110 approved |
| M9 Donatable | Open-source hygiene audited, supply chain signed, shared libraries extracted | 002 and 120 to 123 approved |

M0 to M5a are approved as of 2026-09-22. Of M5b, 070 and 080 are approved as of 2026-09-23; 071 and 072 wait on a messaging bot token the project does not yet hold. Of M6, 090 is approved as of 2026-09-24. Slice numbers have gaps on purpose (000, 001, 010, 011 and so on) so that a slice can be inserted
M0 to M5a are approved as of 2026-09-22. Of M5b, 070 and 080 are approved as of 2026-09-23; 071 and 072 wait on a messaging bot token the project does not yet hold. Of M6, 090 is approved as of 2026-09-24. **M7** is approved as of 2026-09-24 (110). Of M9, 120 is approved as of 2026-09-24. Four slices merged on 2026-09-24 were not on the plan and are recorded as what they were: 004 (property-based testing, taken because it closes two OpenSSF gold criteria), 014 (a defect: a session past 500 messages built its prompt from the *first* 500), and 027, 028 and 029 from the connectome research. Slice numbers have gaps on purpose (000, 001, 010, 011 and so on) so that a slice can be inserted
later without renumbering anything.

**[`ROADMAP.md`](ROADMAP.md)** sets out what the project intends to do and what it intends not to
Expand All @@ -283,6 +296,7 @@ do over the next year, and why the order is what it is.
| `THIRD_PARTY_LICENSES.md` | Every dependency and its licence, generated from the CycloneDX bill. A dependency whose licence neither the bill nor a declared entry supplies fails the gate |
| `LICENSES/` | The full text of every licence the tree uses, as the REUSE Specification requires |
| `docs/effects-catalog.md` | The published effect catalogue: every tool that can cause an effect, its risk tier and effect class, with a version that is a hash of its own rows. Generated, and the gate fails when it and the tree disagree |
| `docs/sandbox.md` | What the Lua sandbox protects against, what it does not, and which of its limits are enforced by the virtual machine rather than sampled |
| `docs/telemetry.md` | The telemetry event catalogue: every event this tree emits, its measurements and metadata, and the rule about what never appears in one |
| `docs/adr/` | Architecture decision records. One is added whenever a decision changes |
| `docs/10-assurance-case.md` | The structured argument that the security claims hold, with the evidence for each and the assumptions and limits named |
Expand Down
3 changes: 2 additions & 1 deletion THIRD_PARTY_LICENSES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
hand: the gate regenerates it and fails when this file and the bill disagree. -->
# Third-party licences

133 components, derived from `sbom.cdx.json`, the CycloneDX bill this project's
134 components, derived from `sbom.cdx.json`, the CycloneDX bill this project's
quality gate generates on every commit. This file and that bill are the same data rendered
twice, so they cannot disagree.

Expand Down Expand Up @@ -75,6 +75,7 @@ it, and a reader should not infer from an entry here that the component is in a
| `lazy_html` | `0.1.12` | Apache-2.0 | bom |
| `llm_db` | `2026.9.4` | Apache-2.0 | bom |
| `logger` | `1.20.4` | Apache-2.0 | bom |
| `luerl` | `1.5.1` | Apache-2.0 | bom |
| `makeup` | `1.2.2` | BSD-2-Clause | bom |
| `makeup_elixir` | `1.0.1` | BSD-2-Clause | bom |
| `makeup_erlang` | `1.1.0` | BSD-2-Clause | bom |
Expand Down
1 change: 1 addition & 0 deletions VERSIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ never pin a version hex marks as retired or vulnerable.
| `pgvector` | ~> 0.4.1 | ✅ in `mix.lock` | Vectors on the Postgres path: `memories.embedding_vector vector(384)` under an HNSW cosine index, searched by `Trinity.Memory.VectorStores.Pgvector` (Slice 032). The postgres job runs on the `pgvector/pgvector:pg17` image. Split from the postgrex row at Slice 010. |
| `oban` | ~> 2.24 | ✅ in `mix.lock` | Uses `Oban.Engines.Lite` on SQLite and `Oban.Engines.Basic` on Postgres (the adapter is compile time, ADR-0002, so the engine is too). ⚠️ Oban Pro Workflows/Smart engine are Postgres-only. Added at Slice 050 (2.24.1 measured on hex.pm 2026-09-22, Apache-2.0). |
| `sbom` | ~> 0.11 | ✅ in `mix.lock` | CycloneDX software bill of materials, generated by the gate from mix.lock (`mix sbom.cyclonedx`). Maintained by erlef, the Erlang Ecosystem Foundation; BSD-3-Clause, CC0-1.0, MIT and Apache-2.0; 0.11.0 measured on hex.pm 2026-09-23, released 2026-09-22. Dev and test only: it ships in no artifact, and the gate validates its output, so a defect in it fails the build rather than producing a quietly wrong bill. Added at Slice 002. |
| `luerl` | ~> 1.5 | ✅ in `mix.lock` | Lua 5.3 implemented in Erlang, for the in-VM sandbox: a skill's script runs inside this node with no OS process, no file handle and no socket. Apache-2.0, **no dependencies of its own**; 1.5.1 measured on hex.pm 2026-09-24, released 2025-12-03. `:luerl_sandbox` is a module of this package and not a package of its own, which slice 110's SLICE.md assumed wrongly. Its `max_reductions` is polled with a 100 ms sleep between checks, so it cannot bound a tight loop below tens of millions of reductions; Trinity runs the interpreter under its own process with `max_heap_size` and a timeout instead, and reports reductions as a statistic (slice 110 NOTES F1). Added at Slice 110. |
| `stream_data` | ~> 1.4 | ✅ in `mix.lock` | Property-based testing: generated input for `test/property`. Apache-2.0; 1.4.0 measured on hex.pm 2026-09-24, released 2026-07-14. Test only, so it ships in no artifact. Elixir has no focused fuzzing library and property-based testing is what the OpenSSF gold criterion `dynamic_analysis` accepts in its place, which is why this is a dependency rather than a habit. Added at Slice 004. |
| `jose` | ~> 1.11 | ✅ in `mix.lock` | JOSE (JWK, JWS, JWKS) over OTP's crypto, for the MCP resource server's token validation and the personal profile's embedded authorization server. MIT; 1.11.12 measured on hex.pm 2026-09-22. Not joken, a layer over it this tree does not need. Added at Slice 062. |
| `oban_web` | ~> 2.13 | ✅ in `mix.lock` | The Oban dashboard, a LiveView mounted at /oban (dev, and behind `config :trinity, :oban_web, true` elsewhere). Apache-2.0 on hex.pm since the 2.12 line (2.13.0 measured 2026-09-22); the plan of 2026-09-05 wrote it in when it was commercial. Added at Slice 050. |
Expand Down
4 changes: 3 additions & 1 deletion config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,9 @@ config :trinity, :tools,
Trinity.Tools.Memory,
Trinity.Tools.Shell.Run,
# Slice 080: delegation to a child session.
Trinity.Tools.Delegate
Trinity.Tools.Delegate,
# Slice 110: a Lua script in the in-VM sandbox.
Trinity.Sandbox.Tool
],
toolsets: %{
fs: ["fs_read", "fs_write", "fs_edit", "fs_list", "fs_glob", "fs_grep"],
Expand Down
Loading
Loading