Skip to content

v0.8.0

Latest

Choose a tag to compare

@cachafla cachafla released this 05 Nov 18:07
· 7 commits to main since this release
Immutable release. Only release title and notes can be modified.
77abaa3

Added

  • Tenants, for programs that embed Atryum and serve more than one
    organization from one Postgres database. Every table has a tenant_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.With in pkg/tenant; a context that names none
    acts for default). atryum.OpenPostgres opens such a pool for an
    embedder's own queries. Row-level security binds only a role that neither
    owns the tables nor has BYPASSRLS, 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 NULL with
    a column list).
  • A pool that skips Atryum's opener (a plain sql.Open("pgx", ...)) must set
    app.tenant_id before writing to Atryum's tables, or the insert fails: the
    column's default reads that setting. The *sql.DB handed over by
    WithDatabase already does. The connection under database/sql's Raw is
    no longer a *stdlib.Conn; it still has Conn() *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/agents is 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_connect likewise 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_id now share one
    session. Before, each could miss the lookup and mint its own ses_ row, so
    a parallel first tool call got a different session_id from its sibling
    and its judge history (and anything else keyed on session_id) was split.
    Sessions minted from client_session_id now get an ID derived from the
    agent binding and client_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 into float64, so an
    integer beyond 2^53 or a decimal with more digits than float64 holds
    changed: a transfer to account 9007199254740993 reached 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 with UseNumber now,
    so arguments hold json.Number. Rule conditions read an exact number as
    before (2.50 is 2.5 for eq and in), and any other as written;
    gt and lt compare as float64, as before. The AI summary of an
    invocation gets the same digits, so it no longer names …992.
  • policy.ExactNumber reports whether a json.Number survives float64,
    for an embedder that hashes or compares arguments: two calls that differ
    only in a digit float64 can't hold must not count as the same call.

What's Changed

Full Changelog: v0.7.0...v0.8.0