Skip to content
Open
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
12 changes: 12 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,15 @@ TELEGRAM_BOT_TOKEN=
# SECRET_KEY_BASE=
# PHX_HOST=
# TRINITY_AUTHORITY=local

# ---- Mattermost gateway, optional (slice 072) -----------------------------------------
# A server the operator names. Unset MATTERMOST_URL and no adapter starts. The callback URL
# is where that server reaches this node, for the approval dialog and the /trinity slash
# command; without it approvals are answered by command only. Under
# TRINITY_PROFILE=regulated the adapter must also be named on TRINITY_REGULATED_GATEWAYS
# (Trinity.Gateways.Mattermost) or the node refuses to boot.
MATTERMOST_URL=
MATTERMOST_CALLBACK_URL=
# The bot account's access token, and the token the server issued for the /trinity command.
MATTERMOST_BOT_TOKEN=
MATTERMOST_COMMAND_TOKEN=
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,32 @@ evidence for each increment is retained by the maintainers and summarised here.
tree, engineering rules consolidated into `docs/03-conventions.md`, and this changelog added as
the public record of delivered work.

## 2026-10-08

### `slice/072`: Trinity answers in Mattermost

The second messaging platform, after Telegram. A Mattermost adapter for a server the operator names: direct
messages and channel mentions are answered, each thread its own conversation, and the reply
streams into one post that is edited as the words arrive. An approval raised during the turn is
posted with a button that opens an interactive dialog; the answer goes back through the same
router, pairing check, rate limit and channel cap as a typed command, so a request above what a
channel may approve is refused there and receipted. A `/trinity` slash command carries the
commands, and the scheduler can deliver a task's result to a channel. Posts are cut to the size
the server says it accepts, counted the way the server counts.

Trinity opens the connection; the server is not assumed to be reachable from anywhere, and it only
calls back for the dialog and the slash command, when the operator gives it an address to call.
Every callback carries proof of where it came from or is refused unread. The bot token is read
when it is used and appears in no log line, checked on every failure path including a crash.
A node under the regulated profile refuses to start with the adapter configured unless it is named
on the gateway allow-list. Choosing Mattermost makes the channel authorizable; whether a given
deployment is authorized remains the operator's statement.

Along the way: a scheduled delivery the platform refused is no longer recorded as delivered, and
the session origin vocabulary names Mattermost in place of the dropped Discord slot.

Verified: gate green at exit 0; 1,065 tests passing (24 properties); coverage 80.49%.

### `slice/071`: Trinity answers on Telegram

A Telegram bot is now a channel Trinity can be reached from, alongside the console. It answers in
Expand Down
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ enforces it.
a bot that answers in private chats and in groups where it is mentioned, streams its reply by
editing one message, offers approvals as buttons (within the same cap), and passes a photo to a
model that can see. Set `TELEGRAM_BOT_TOKEN` and `TRINITY_GATEWAYS=telegram` to turn it on.
A Mattermost adapter ships too, for a server the operator names: direct messages and channel
mentions, replies streamed by editing one post, approvals answered in an interactive dialog
(within the same cap), a `/trinity` slash command, and scheduled results delivered to a channel.
Set `MATTERMOST_URL`, `MATTERMOST_BOT_TOKEN` and `TRINITY_GATEWAYS=mattermost` to turn it on.
- **Delegates.** The assistant can hand a bounded piece of work to a child session with a
`delegate` tool: its own context, its own history, and a result handed back as text. The
parent's conversation never contains the child's messages, which is the point:
Expand Down Expand Up @@ -174,9 +178,9 @@ enforces it.
curator that marks old memories stale and archives the untouched ones (never deleting)
run on the same queues; `/oban` shows the jobs.

Not there yet: the second messaging platform (072), the native desktop shell and signed releases
(the rest of M6), 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
Not there yet: the native desktop shell and signed releases (the rest of M6),
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.
Expand All @@ -191,7 +195,7 @@ Those are failures of the substrate, not of the product. The BEAM was built to r
of failure, so on it these properties can be structural rather than aspirational:

- A crash in one session, tool or gateway never affects another and never loses persisted state.
- One agent, seen on the desktop, in Telegram and in Discord at the same time, in real time.
- One agent, seen on the desktop, in Telegram and in Mattermost at the same time, in real time.
- Memory in tiers: a small always-on set of facts plus unlimited retrievable history.
- Every side-effecting action passes a permission gate, and dangerous ones need explicit approval.
- Any model or provider, cloud or local, switched by configuration.
Expand Down
3 changes: 2 additions & 1 deletion VERSIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,8 @@ never pin a version hex marks as retired or vulnerable.
| `luerl (+ sandbox)` | latest | 🔍 not a single package | Slice 110 only. Two packages, so no single lock key. |
| `burrito` | ~> 1.6 | ✅ in `mix.lock` | ⚠️ ERTS availability drives the OTP pin, and Slice 000 measured it: only the OTP 28 line is fetchable for macOS and Linux. Corrected 2026-09-05: this row previously read `~> 1.5 / 1.5.0 ✅`; that mark was not measured. Added at Slice 001. |
| `ex_tauri` | ~> 0.2 | ✅ in `mix.lock` | ⚠️ Declares `otp_release: "~> 27.0"`, and Slice 000's probe refutes the reason it gives: OTP 28 macOS universal returns 200 and OTP 27 returns 404. Whether it runs on the pinned OTP is Slice 001's first measurement. ⚠️ 439 downloads all-time, so the ADR-0004 fallback matrix carries real weight. |
| `nostrum` | ~> 0.10 | 🔍 not yet a dependency | Discord. ⚠️ No release in roughly 18 months. R11's trigger already fires. Check intents and components against the current gateway before Slice 072. |
| `nostrum` | not used (Slice 072 retargeted to Mattermost, 2026-10-07) | 🔍 not a single package | Discord. Was the planned library for Slice 072, which the owner retargeted to Mattermost on 2026-10-07: Discord has no authorization path for a regulated deployment, and the work is dropped rather than deferred. The row stays so the decision is visible where a reader would look for the package. |
| `websockex` | ~> 0.5.1 | ✅ in `mix.lock` | The Mattermost gateway's connection to its server's WebSocket (`Trinity.Gateways.Mattermost.Socket`, Slice 072). Was transitive through req_llm and already in every release; direct since Slice 072 because a module of ours calls it. ⚠️ Its own default is `insecure: true` (`verify: :verify_none`); the socket always passes verifying `ssl_options` and never relies on the default. |
| `telegex` | not used (measured at Slice 071, 2026-10-08) | 🔍 not a single package | Telegram. Measured at Slice 071: the latest release on hex is `1.9.0-rc.0` (2024-09-18) and the last stable `1.8.0` (2024-05-28); this file's own rule forbids pinning an `-rc`. Not used: `Trinity.Gateways.Telegram` speaks the Bot API with `req` (slice 071 NOTES, "The library, measured"). The row stays so the decision is visible where a reader would look for the package. |
| `ex_gram` | not used (measured at Slice 071, 2026-10-08) | 🔍 not a single package | Telegram. Measured at Slice 071 on `0.71.0` (2026-10-01): its poller keeps the `getUpdates` offset in process state and starts at `-1`, so a restarted poller reprocesses or skips unconfirmed updates (Slice 071's AC5 is that property); its file download host is a module attribute that cannot be pointed at a test server; Beerware licence; 18,054 lines for what is here a handful of JSON calls. Not used: `Trinity.Gateways.Telegram` speaks the Bot API with `req` and owns its poller. |
| `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. |
Expand Down
9 changes: 8 additions & 1 deletion config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,12 @@ config :logger, :default_formatter,
# Use Jason for JSON parsing in Phoenix
config :phoenix, :json_library, Jason

# Slice 072: request parameters whose names contain these are logged as [FILTERED]. Phoenix's
# default is "password" alone, and a Mattermost slash command arrives with its verification token
# in a parameter called `token` (and an action's signed context under `context.token`), and with a
# `response_url` that lets whoever holds it post into the channel for a time after the command.
config :phoenix, :filter_parameters, ["password", "token", "secret", "response_url"]

# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{config_env()}.exs"
Expand All @@ -246,4 +252,5 @@ config :trinity, :deliveries, %{"gateway" => Trinity.Scheduler.Delivery.Gateway}
# `config :trinity, :gateways, adapters: [...]` names modules, and `TRINITY_GATEWAYS`
# (config/runtime.exs) names adapters from this list by their name ("telegram", "console"), so an
# environment variable never mints an atom. A new platform is a module and a line here.
config :trinity, :gateways, available: [Trinity.Gateways.Console, Trinity.Gateways.Telegram]
config :trinity, :gateways,
available: [Trinity.Gateways.Console, Trinity.Gateways.Telegram, Trinity.Gateways.Mattermost]
18 changes: 18 additions & 0 deletions config/runtime.exs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,24 @@ if roots = System.get_env("TRINITY_FS_ROOTS") do
config :trinity, :fs, roots: String.split(roots, ":", trim: true)
end

# Slice 072: the Mattermost gateway's server, when the operator names one. It runs when it is
# named on TRINITY_GATEWAYS ("mattermost"); named without MATTERMOST_URL it starts idle and says
# why on /gateways. MATTERMOST_CALLBACK_URL, if set, is the base URL the server reaches this node at,
# for the approval dialog and the /trinity slash command. The bot token is MATTERMOST_BOT_TOKEN and
# the command's token MATTERMOST_COMMAND_TOKEN, both read when used (`Trinity.Config.secret/1`)
# and never here. Under TRINITY_PROFILE=regulated the adapter must also be named on
# TRINITY_REGULATED_GATEWAYS or the node refuses to boot.
if config_env() != :test do
mattermost =
[
url: System.get_env("MATTERMOST_URL"),
callback_url: System.get_env("MATTERMOST_CALLBACK_URL")
]
|> Enum.reject(fn {_key, value} -> value in [nil, ""] end)

if mattermost != [], do: config(:trinity, :mattermost, mattermost)
end

# Slice 022: the search provider's key is read at call time from BRAVE_SEARCH_API_KEY; the
# provider module is configuration so a fake can stand in.
if config_env() != :test do
Expand Down
Loading
Loading