Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
76a18ac
Lift the SSRF guard into Raxol.Core.Outbound
DROOdotFOO Sep 14, 2026
14bb724
Add the shared bounded response accumulator
DROOdotFOO Sep 14, 2026
20e6646
Add a remote transport for the MCP client
DROOdotFOO Sep 14, 2026
9133a48
Gate MCP header refs on configuration provenance
DROOdotFOO Sep 14, 2026
546f296
Add raxol_web3: guarded client and EVM backend
DROOdotFOO Sep 14, 2026
08594d0
Decide the required backend callback set (#1026)
DROOdotFOO Sep 14, 2026
745996d
Add the raw JSON-RPC partial source (#1026)
DROOdotFOO Sep 14, 2026
07a66cd
Add the Solana backend (#1028)
DROOdotFOO Sep 14, 2026
cb72a86
Add the Tron backend (#1027)
DROOdotFOO Sep 14, 2026
7eb1b10
Add the Canton backend (#1029)
DROOdotFOO Sep 14, 2026
ea95764
Add the Aztec backend (#1030)
DROOdotFOO Sep 14, 2026
a1a0731
Record the web3 surface in the docs
DROOdotFOO Sep 14, 2026
c3e103a
Accept two on-chain identifiers in the scan
DROOdotFOO Sep 14, 2026
5701d78
Refresh the root lockfile for phoenix 1.8.14
DROOdotFOO Sep 14, 2026
a173a80
Allow the MCP client table owner as a singleton
DROOdotFOO Sep 14, 2026
47e4359
Classify raxol_web3 in the release catalog
DROOdotFOO Sep 14, 2026
27786f1
Lock the new optional MCP deps in the consumers
DROOdotFOO Sep 14, 2026
786cdaa
Allowlist the two on-chain identifiers by value
DROOdotFOO Sep 14, 2026
5f3050f
Accept the two identifiers by fingerprint again
DROOdotFOO Sep 14, 2026
5a4ee4f
Escape Canton update ids against unreserved
DROOdotFOO Sep 15, 2026
518a751
Key the Aztec cache on its base URL digest
DROOdotFOO Sep 15, 2026
10cf7b7
Bound a Tron address before Base58 decoding
DROOdotFOO Sep 15, 2026
cfa323b
Intern tool argument names at compile time
DROOdotFOO Sep 15, 2026
055d7ea
Document the account-reference family prefixes
DROOdotFOO Sep 15, 2026
62c6da2
Check a task reference before applying its outcome
DROOdotFOO Sep 15, 2026
d795cb5
Clear reprobed? on a successful round trip
DROOdotFOO Sep 15, 2026
fdfccca
Run the era re-probe in a monitored task
DROOdotFOO Sep 15, 2026
f250c8e
Gate the serialized cap on state, not a clock
DROOdotFOO Sep 15, 2026
13eba29
Validate and encode Blockscout path segments
DROOdotFOO Sep 15, 2026
3b6baad
Merge master for the lockfile CI fix
DROOdotFOO Sep 15, 2026
2606357
Bound the web3 response cache
DROOdotFOO Sep 15, 2026
3c772ca
Document what bounds the origins table
DROOdotFOO Sep 15, 2026
f6c2f0d
Say what .gitguardian.yaml can and cannot do
DROOdotFOO Sep 15, 2026
7301e39
Name the ETS function the comment describes
DROOdotFOO Sep 15, 2026
8a30922
Harden MCP and Web3 boundaries
DROOdotFOO Sep 16, 2026
2d061c5
Refresh resolved dependency lock
DROOdotFOO Sep 16, 2026
570951d
Repair MCP package CI
DROOdotFOO Sep 16, 2026
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
49 changes: 46 additions & 3 deletions .gitguardian.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,32 @@
# GitGuardian configuration.
# GitGuardian configuration -- for `ggshield` only. It does NOT affect the
# `GitGuardian Security Checks` check run on a pull request.
#
# What reads this file: the ggshield CLI, which looks for `~/.gitguardian.yaml`
# and then `./.gitguardian.yaml` in the repository root ("Configuration files",
# docs.gitguardian.com/ggshield-docs/configuration). Nothing in CI runs
# ggshield; this is for a contributor running `ggshield secret scan` or the
# pre-commit hook locally.
#
# What does not read this file: the check run. That comes from the GitGuardian
# GitHub App, and "secret scans in the context of GitHub Check Runs will run
# server-side on the VCS, at the post-receive stage"
# (docs.gitguardian.com/internal-monitoring/prevent/detect-secrets-in-real-time-in-github)
# -- from the platform, not from a checkout, so there is no repository file to
# read. That page's "customizing check runs on a per-repository basis" section
# lists GitHub custom properties, and labels on GHES < 3.13, as the only
# repository-level overrides; a committed config file is not among them.
#
# So an entry below cannot turn a red check run green, and this repository
# change does not claim to clear any server-side incident. For every existing
# false-positive incident, a GitGuardian dashboard user must open the incident
# and close it with the `False positive` resolution. The check run then stops
# raising it. The check run's own `Skip: false positive` action is the equivalent
# per-check alternative; `Skip: test credential` and `Skip: low risk` are
# different classifications and must not be used for these incidents.
#
# Keys use the documented v2 spelling, with underscores. ggshield accepts the
# hyphenated form but warns that it is deprecated (`replace_dash_in_keys` and
# `_warn_about_dash_keys` in ggshield/core/config).
#
# The stealth conformance fixture and its generator contain deterministic
# secp256k1 test vectors (including the well-known ethers/web3 documentation key
Expand All @@ -8,13 +36,28 @@
# vectors do not trip secret detection.
version: 2
secret:
ignored-paths:
ignored_paths:
- "packages/raxol_payments/test/fixtures/stealth_vectors.json"
- "packages/raxol_payments/scripts/gen_reverse_stealth_vectors.exs"
ignored-matches:
ignored_matches:
# Tron USDT (TRC-20) public contract address. Used as the relay destination
# in scripts/run_live_gates.sh and as the @usdt_tron constant across the repo.
# A public on-chain contract address, tied to no wallet -- not a credential.
# The high-entropy base58 form trips the generic detector; ignore the value.
- name: Tron USDT public contract address
match: TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t
# Solana SPL mint pubkey, asserted by value in the Solana backend test: the
# token a real mainnet account holds. Public on-chain identifier tied to no
# wallet, and base58 trips the generic detector on entropy alone. Matched by
# value rather than by path because the same pubkey is inside the recorded
# fixture test/fixtures/solana/rpc_token_accounts.json, and excluding that
# whole file would exclude everything else recorded in it too.
- name: Solana SPL mint public address
match: NkfbyG7feH9aJhdnSs4d7D7kWCEnxrmNUbM7dbpC6MM
# Tron TRC-20 contract address, asserted by value in the Tron backend test
# to prove a TRC-20 balance is distinguishable from a TRC-10 one. Same
# class as the USDT entry above: a published contract address. Also inside
# test/fixtures/tron/tronscan_account_tokens.sse, so matched by value for
# the same reason as the mint.
- name: Tron TRC-20 public contract address
match: TFczxzPhnThNSqr5by8tvxsdCFRRz6cPNq
8 changes: 7 additions & 1 deletion .github/workflows/ci-unified.yml
Original file line number Diff line number Diff line change
Expand Up @@ -398,10 +398,16 @@ jobs:
# have shipped unrun. raxol_agent is the package that consumes both, so
# both join together.
#
# raxol_web3 joins with its first module. It is the guarded outbound
# client of ADR-0038, so every test it has is a security property: the
# SNI and the Host header a pinned dial presents, and the transport
# options it refuses. Landing it ungated would repeat exactly the gap
# the packages above were added here to close.
#
# Keep on ONE line: test/raxol/formatter_delegation_test.exs parses this
# matrix with `^\s*package: \[...\]` to prove every gated package is also
# delegated by the root .formatter.exs. A block sequence parses as absent.
package: [raxol_core, raxol_agent, raxol_mcp, raxol_gateway, raxol_telegram, raxol_cli, raxol_payments, raxol_earn, raxol_agent_client_protocol, raxol_console, raxol_symphony]
package: [raxol_core, raxol_agent, raxol_mcp, raxol_gateway, raxol_telegram, raxol_cli, raxol_payments, raxol_earn, raxol_agent_client_protocol, raxol_console, raxol_symphony, raxol_web3]

steps:
- uses: actions/checkout@v7
Expand Down
9 changes: 5 additions & 4 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -170,10 +170,10 @@ jobs:
# hit, which also disabled the "Create issue for critical findings"
# step gated on `needs.secrets-scan.result == 'failure'`.
#
# Blocking is only safe because measured historical false positives are
# accepted by exact fingerprint in `.gitleaksignore`. Public values and
# vendored assets use narrow allowlists in `.gitleaks.toml`; no test tree
# is exempt by path. A new key-shaped value therefore fails this job.
# Blocking is only safe because historical test-vector findings remain
# bound to exact commit/path/rule/line fingerprints. Published values and
# vendored assets use narrow rule-scoped allowlists. The scanner version
# is pinned below so that allowlist syntax is part of the enforced gate.
#
# `--only-verified` on TruffleHog for the same reason: unverified
# high-entropy matches in signing test vectors are not credentials, and
Expand Down Expand Up @@ -227,6 +227,7 @@ jobs:
uses: gitleaks/gitleaks-action@v3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITLEAKS_VERSION: 8.30.1
# Repo-scoped config; without it this job cannot pass. See above.
GITLEAKS_CONFIG: .gitleaks.toml

Expand Down
20 changes: 20 additions & 0 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,23 @@ paths = [
'''priv/static/assets/vendor/.*''',
]

[[allowlists]]
description = """
Two public on-chain identifiers asserted by value in the web3 backend tests: an
SPL mint pubkey and a TRC-20 contract address, each recorded from a mainnet
read on 2026-09-14 and resolvable in any block explorer. `generic-api-key`
matches them on entropy alone, because base58 is what both a mint address and
a key look like.

Allowlisted BY VALUE rather than by fingerprint or path, for two reasons. The
tests assert these exact strings, since the property under test is that a token
is identified correctly, so they cannot be shortened or faked away. And a
fingerprint binds to a commit sha, so it goes stale the moment the branch is
rebased and the gate fails again for a reason that has nothing to do with
secrets. Any OTHER key-shaped string in those files still fails.
"""
targetRules = ["generic-api-key"]
regexes = [
'''NkfbyG7feH9aJhdnSs4d7D7kWCEnxrmNUbM7dbpC6MM''',
'''TFczxzPhnThNSqr5by8tvxsdCFRRz6cPNq''',
]
6 changes: 4 additions & 2 deletions docs/PACKAGES.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Packages

Raxol ships as a main package plus 17 focused subsystems. Use the main `raxol` package for the full framework, or take individual packages for narrower needs.
Raxol ships as a main package plus 18 focused subsystems. Use the main `raxol` package for the full framework, or take individual packages for narrower needs.

## Main

Expand Down Expand Up @@ -31,6 +31,7 @@ Raxol ships as a main package plus 17 focused subsystems. Use the main `raxol` p
| `raxol_gateway` (pre-alpha) | `path: "packages/raxol_gateway"` | Unified messaging gateway (multi-platform) |
| `raxol_cli` (pre-alpha) | `path: "packages/raxol_cli"` | The `raxol` command (`code`, `p`, `acp`, `agent`, `playground`, `new`), packaged as a self-contained Burrito binary |
| `raxol_console` (pre-alpha) | `path: "packages/raxol_console"` | Boots a Virtuals ACP Console agent package onto the gateway stack |
| `raxol_web3` (pre-alpha) | `path: "packages/raxol_web3"` | Guarded outbound client, six chain backends, indexer-agnostic reads (ADR-0033, ADR-0038, ADR-0039) |

The **coding agent** layers as: `Backend.Selector` (LLM backend adapter) -> the **Harness** engine (the event/command contract `Raxol.Agent.Contract` and the durable journal `Raxol.Agent.Journal` in `raxol_agent`, the projections and surface widgets `Raxol.Harness.*` in main `raxol`) -> the product surfaces `mix raxol.code` (interactive TUI, also over SSH), `mix raxol.p` (headless one-shot), and `mix raxol.acp` (ACP on stdio, for editors), all three in `raxol_agent` -> `raxol_symphony`, which orchestrates many agent runs above them. The Harness is the engine; the `mix raxol.harness.*.bless` tasks only regenerate its golden/fixture test snapshots.

Expand All @@ -53,13 +54,14 @@ raxol_mcp --> raxol_core
raxol_liveview --> raxol_core (+ phoenix_live_view optional)
raxol_plugin --> raxol_core

raxol_agent --> raxol + raxol_mcp
raxol_agent --> raxol + raxol_mcp (+ raxol_web3 optional, for the `web3` tool)
raxol_payments --> raxol_agent (compile-time only)
raxol_earn --> raxol_payments (runtime), raxol_mcp + raxol_agent (compile-time only)
raxol_agent_client_protocol --> (none; jason only, zero raxol deps)
raxol_symphony --> raxol_core, raxol_agent, raxol_mcp (all optional)
raxol_cli --> raxol, raxol_agent (+ raxol_agent_client_protocol for `raxol acp`)
raxol_console --> raxol_agent, raxol_gateway, raxol_earn
raxol_web3 --> raxol_core, raxol_mcp (+ mint, castore)

raxol_speech --> raxol_core (+ bumblebee/nx/exla optional for STT)
raxol_telegram --> raxol_core (+ raxol/telegex/raxol_gateway optional)
Expand Down
36 changes: 21 additions & 15 deletions docs/adr/0033-web3-data-surface.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,16 @@ Four modules move down, unchanged in behaviour, with deprecated delegating shims

### 3. The backend contract

**Amended by [ADR-0039](0039-required-callback-set-partial-sources.md), 2026-09-14.** The
required set below is two rather than six: `get_transaction/2`, `account_info/2`,
`list_transactions/3` and `token_balances/2` are optional. The set here was derived from what
chains can answer, and the contract binds sources. A raw JSON-RPC node has no method that
lists by account; probes on 2026-09-14 found no account resource at all on Aztec's public
surface; and SQD Portal, the Solana and Tron primary, answers queries over block ranges, so
it reports no current balance and cannot look a Solana transaction up by signature. What
survives as required is what a source knows about the chain rather than about anything in it:
which chain this is, and how far it has got. Everything else in this decision stands.

`Raxol.Web3.Backend` follows the repo's established adapter shape: a `{module, state}` handle
as in `ChainReader`, a capability-declaring callback as in `Raxol.Earn.ProviderAdapter`'s
`supported_chain_ids/1`, and a self-identifying zero-arity callback as in
Expand Down Expand Up @@ -258,36 +268,32 @@ package in which failover silently degrades to retrying a hard-down primary on e
Reference implementations ship in `lib/`, not `test/`, matching `Adapter.InMemory`,
`ProviderAdapter.Mock`, and `ChainReader.Stub`. No mocking library is introduced.

### 4. The served surface is read-only by construction
### 4. The served surface is read-only and authorization-gated

Agent Actions are the primary surface, and MCP `tool_def` maps are written directly. The
existing `Raxol.MCP.AgentBridge` is not used: it has no caller anywhere in the repo, it drops
the `sensitive` flag instead of emitting an annotation, and it formats results with
`inspect/2` rather than JSON.

Every tool in this package is a read, so the server runs under a nil authorizer on stdio. The
enforcement that makes that safe lives in this package rather than in the server.
`Raxol.MCP.Server.refuse_unguarded_sensitive_tools!/2` raises at boot only when a registered
tool is annotated sensitive through `ToolDef.sensitive?/1` and no `:authorizer` is configured,
and the `tools/call` backstop keys off the same predicate. An unannotated tool passes both
checks unimpeded, so the annotation records an intent and enforces nothing against a write
tool that omits it.
Every served operation is a chain read, but it is still an outbound capability: it discloses
addresses, names and query intent to an upstream and can consume provider quota. Every Web3
MCP tool therefore carries both `readOnlyHint: true` and `sensitive: true`. The server MUST
have an authorizer; `Raxol.MCP.Server.refuse_unguarded_sensitive_tools!/2` refuses to boot
otherwise, and the `tools/call` backstop enforces the same predicate.

Three things carry the constraint instead:
Three things carry the read-only constraint:

- Every callback in the backend contract is a read, and `raw_request/2` is bounded by a
per-backend compile-time allowlist of read methods, so the passthrough is structurally
incapable of reaching `eth_sendRawTransaction`.
- The registered tool set is asserted rather than assumed. A test enumerates the registry
after `register_all/2` and fails on any tool this package did not declare as a read, which
is the check the server does not perform.
after `register_all/2` and fails on any tool this package did not declare as a read.
- Aggregated upstream tools are filtered against a per-backend allowlist before registration,
since an upstream server's own annotations are untrusted (section 7).

Admitting a write tool later is therefore a deliberate act with a visible cost: it must carry
the `sensitive` annotation, which forces an `:authorizer` onto the whole server and changes
the deployment story for every consumer. Writes stay where they already are, behind the
spend-gated Actions in `raxol_payments`.
Admitting a write tool later remains a deliberate contract change, not a way to opt into the
authorization gate: the whole Web3 surface is already sensitive. Writes stay where they
already are, behind the spend-gated Actions in `raxol_payments`.

### 5. Chain coverage and data sources

Expand Down
Loading
Loading