Repository navigation
Added
- Tenants, for programs that embed Atryum and serve more than one
organization from one Postgres database. Every table has atenant_id, and
every primary, unique and foreign key starts with it, so names are unique
per tenant and no row can point at another tenant's. Postgres row-level
security shows a connection only the rows of the tenant it set in
app.tenant_id. Atryum's pool sets it before each statement from the
request's context (tenant.Withinpkg/tenant; a context that names none
acts fordefault).atryum.OpenPostgresopens such a pool for an
embedder's own queries. Row-level security binds only a role that neither
owns the tables nor hasBYPASSRLS, so an embedder serving several tenants
connects as one. A standalone Atryum, and every SQLite database, stays one
tenant,default, and needs no change.
Changed
- Migration 034 needs Postgres 15 or later (it uses
ON DELETE SET NULLwith
a column list). - A pool that skips Atryum's opener (a plain
sql.Open("pgx", ...)) must set
app.tenant_idbefore writing to Atryum's tables, or the insert fails: the
column's default reads that setting. The*sql.DBhanded over by
WithDatabasealready does. The connection underdatabase/sql'sRawis
no longer a*stdlib.Conn; it still hasConn() *pgx.Conn. - Atryum's connections keep the tenant as a session setting, so they need a
session that stays one connection. A pooler in transaction mode, such as
PgBouncer's, would carry one request's tenant into another's.
Security
- Any signed-in user could take over another agent's Claude Managed Agent.
POST /api/v1/agentsis open to every user, and Atryum resolves an
identity by an agent's aliases (agent_ids) before its Claude Managed
Agent bindings, so an alias on a Claude agent ID bound to another agent took
that agent's calls, which then ran under the alias holder's rules and
charter. This happened whether the alias came first or the binding did.
force_claude_managed_agent_connectlikewise let any user move a binding
off another agent. Now an alias on a Claude agent ID bound to another agent,
and a binding of a Claude agent ID that is another agent's alias, are both
refused (409, for admins too), and only an admin may force connect (403
otherwise). An admin's force connect also removes the Claude agent ID from
any other agent's aliases, so an alias can't lock admins out of binding it.
The checks run before the write, not in one transaction, so two concurrent
requests can still both pass; the startup warning below finds those. - A 409 for an identity another agent holds names that agent only to an
admin. Members see only their own agents, so they're told the ID is
claimed by another agent. - At startup Atryum logs a warning for each identity that one agent has as an
alias while another has it bound, made before these checks existed. Its
calls go to the alias holder until it's removed from one of them.
Fixed
- Submits that arrive together for a new
client_session_idnow share one
session. Before, each could miss the lookup and mint its ownses_row, so
a parallel first tool call got a differentsession_idfrom its sibling
and its judge history (and anything else keyed onsession_id) was split.
Sessions minted fromclient_session_idnow get an ID derived from the
agent binding andclient_session_id, and a duplicate insert is a no-op. - A tool call's arguments keep their numbers as the agent wrote them. The
MCP proxy,POST /api/v1/invocations,POST /api/v1/external/invocations
and the managed-agents bridge decoded arguments intofloat64, so an
integer beyond 2^53 or a decimal with more digits thanfloat64holds
changed: a transfer to account9007199254740993reached the upstream
tool, the decider, the stored invocation and its events as
9007199254740992, and a hook's own check refused the call because the
invocation no longer matched it. They are decoded withUseNumbernow,
so arguments holdjson.Number. Rule conditions read an exact number as
before (2.50is2.5foreqandin), and any other as written;
gtandltcompare asfloat64, as before. The AI summary of an
invocation gets the same digits, so it no longer names…992. policy.ExactNumberreports whether ajson.Numbersurvivesfloat64,
for an embedder that hashes or compares arguments: two calls that differ
only in a digitfloat64can't hold must not count as the same call.
What's Changed
- Keep a tool call's numbers as the agent wrote them by @kam-validmind in #183
- api: an alias can't take another agent's Claude Managed Agent; only admins force connect by @kam-validmind in #185
- Concurrent first submits share one external session by @cachafla in #181
- Session race follow-ups from review on #181 by @cachafla in #188
- Tenants, kept apart by row-level security on Postgres by @cachafla in #189
- release: changelog for 0.8.0 by @cachafla in #190
Full Changelog: v0.7.0...v0.8.0