Raxol.AgentClientProtocol.MethodTable (Raxol Agent Client Protocol v0.1.0)

Copy Markdown View Source

Single source of truth for the ACP method vocabulary: every wire method string, its direction, its JSON-RPC kind, the callback atom used for both the behaviour @callback and the Router dispatch clause, its param/result schema modules, its capability gate, and its layer.

The method set follows the pinned ACP JSON Schema schema-v1.19.0 (meta.json), not the f1729 reference implementation — f1729 has session/fork/session/set_model (not in the pinned oracle) and lacks session/delete/session/close/logout/$/cancel_request (which the oracle has). The table is literal data in this module: the oracle copy at priv/schema-oracle/v1/meta.json is a dev/test file in the repository, not shipped in the package and not read at compile time; the package's test suite checks the table against it.

What lives here vs. what doesn't

This module owns rows/0, rows/1, rows_for_side/1, capability_for/1, the :capability column data, and the compile-time table invariants below (incl. invariant 7 guarding the capability paths). The negotiated-caps PREDICATE (Capabilities.negotiated?/2) and the Connection-side pre-filter for layer: :protocol/:session_control rows are consumers of this table, not implemented here.

Compile-time invariants (raise CompileError on violation)

  1. {direction, wire} is unique across all rows.
  2. kind: :notification implies result == nil; kind: :request implies result != nil.
  3. callback is unique per handling side (the set of callbacks visible to rows_for_side(:agent) has no duplicates, ignoring nil; ditto :client) — the anti-name-drift invariant.
  4. layer in [:protocol, :session_control] if and only if callback == nil.
  5. ext == nil rows must not start with "_"; ext == :raxol rows must start with "_raxol/"; a "$/" wire prefix is only allowed on layer: :protocol rows.

A sixth check (every params/result module referenced must exist and export from_json/1) runs in the package's test suite rather than at compile time, to avoid compile-order knots between this module and the Schema.* modules it references.

Summary

Types

How a row is capability-gated

Which side sends this row's wire method.

nil for the core v1.19.0 surface; :raxol for registered _raxol/* vendor rows.

JSON-RPC 2.0 shape: does this method get a reply?

:app — has an app-level @callback and Router dispatch clause. :protocol — protocol-level ($/cancel_request); Connection pre-filters before Router.decode/4 is ever called. :session_control — app-shaped wire method, but Connection-routed with NO app callback (currently only session/cancel).

Functions

The literal top-level field atoms of a side's capability struct (drift-guarded by invariant 7 + tests).

The capability gate for a wire method, normalized for Raxol.AgentClientProtocol.Capabilities.negotiated?/2

Register a compile-time dependency on this table's SOURCE FILE into the calling module.

All table rows, in declaration order (agent methods, then client methods, then protocol methods).

Rows whose direction matches exactly.

Rows a given handling side must be able to decode/dispatch: :agent sees everything sent :client_to_agent plus :both; :client sees everything sent :agent_to_client plus :both.

Types

capability()

@type capability() :: nil | :never | {:agent | :client, [atom()]}

How a row is capability-gated:

  • nil — baseline method, always available (no capability gate).
  • {:agent | :client, path} — a field path into the receiver's own negotiated capability struct (Schema.AgentTypes.AgentCapabilities for :agent, Schema.ClientTypes.ClientCapabilities for :client). Raxol.AgentClientProtocol.Capabilities.negotiated?/2 walks the path; a truthy leaf ⇒ negotiated, anything else (missing field, nil, false) ⇒ NOT negotiated (fail closed). The path uses Elixir struct field atoms, not the JSON wire keys, because the snapshot resolved against is the decoded struct — see invariant 7 and the ORACLE-DIVERGENCE note below.

  • :never — the oracle gates this method by a capability the ported Elixir capability structs cannot express, so it is unconditionally NOT negotiated (fail closed, -32601). No row currently uses it.

ORACLE-DIVERGENCE (schema-oracle v1.19.0 vs. ported f1729 structs)

The pinned oracle's capability tree is richer than the Elixir structs ported from the older f1729 snapshot; three oracle leaves that f1729 lacks are ported so their methods gate on a real path:

  • sessionCapabilities.{delete,close} — ported as Schema.AgentTypes.SessionDeleteCapabilities/SessionCloseCapabilities onto the SessionCapabilities struct's delete/close fields. session/delete/session/close gate on {:agent, [:session_capabilities, :delete | :close]}.
  • agentCapabilities.auth.logout — ported as Schema.AgentTypes.AgentAuthCapabilities/LogoutCapabilities onto the AgentCapabilities struct's auth field. logout gates on {:agent, [:auth, :logout]}.
  • JSON-key vs struct-field: paths name Elixir struct fields ([:session_capabilities, :list], [:file_system, :read_text_file], …), not oracle JSON keys ([:sessions, :list], [:fs, :read_text_file]), which resolve against neither the oracle JSON nor the struct; invariant 7 guards the first segment against drift.

direction()

@type direction() :: :client_to_agent | :agent_to_client | :both

Which side sends this row's wire method.

ext()

@type ext() :: nil | :raxol

nil for the core v1.19.0 surface; :raxol for registered _raxol/* vendor rows.

kind()

@type kind() :: :request | :notification

JSON-RPC 2.0 shape: does this method get a reply?

layer()

@type layer() :: :app | :protocol | :session_control

:app — has an app-level @callback and Router dispatch clause. :protocol — protocol-level ($/cancel_request); Connection pre-filters before Router.decode/4 is ever called. :session_control — app-shaped wire method, but Connection-routed with NO app callback (currently only session/cancel).

row()

@type row() :: %{
  wire: String.t(),
  direction: direction(),
  kind: kind(),
  callback: atom() | nil,
  params: module() | nil,
  result: module() | nil,
  capability: capability(),
  layer: layer(),
  ext: ext()
}

Functions

cap_fields(atom)

@spec cap_fields(:agent | :client) :: [atom()]

The literal top-level field atoms of a side's capability struct (drift-guarded by invariant 7 + tests).

capability_for(method)

@spec capability_for(String.t()) ::
  :none | :never | :unknown_method | {:agent | :client, [atom()]}

The capability gate for a wire method, normalized for Raxol.AgentClientProtocol.Capabilities.negotiated?/2:

  • :none — baseline method, always available (table capability: nil).
  • :never — unrepresentable in the ported caps model; fail closed.
  • {:agent | :client, path} — a field path into the receiver's own caps.

  • :unknown_method — method is not in the table (decode/4 will -32601 it; the caller MUST NOT treat this as a capability denial).

depend_on_source()

(macro)

Register a compile-time dependency on this table's SOURCE FILE into the calling module.

Any module that expands rows/0 data into its own compiled output — the Router decode/dispatch/result_marker clauses, the Agent/Client @callback surface — MUST call this in its module body. A plain rows/0 call is only an EXPORTS dependency (recompile when rows/0's API changes); a row-DATA edit does not change that signature, so without this an incremental build leaves the generated code stale (a new row present in the table but absent from Router/Agent until a mix clean). @external_resource forces the caller to recompile whenever this file changes.

rows()

@spec rows() :: [row()]

All table rows, in declaration order (agent methods, then client methods, then protocol methods).

rows(direction)

@spec rows(direction()) :: [row()]

Rows whose direction matches exactly.

rows_for_side(atom)

@spec rows_for_side(:agent | :client) :: [row()]

Rows a given handling side must be able to decode/dispatch: :agent sees everything sent :client_to_agent plus :both; :client sees everything sent :agent_to_client plus :both.