Beamlet.Policy (Beamlet v0.1.0)

Copy Markdown View Source

A policy decides what a token's requests may do on your beamlet.

Declare policies in config, by name:

config :beamlet,
  policies: [
    explorer: [
      tools: [:eval],
      allow: [{Host.Code, except: [remove: 1]}],
      deny: [Host.HTTP]
    ]
  ]

Then give one to a token, with beamlet tokens.create laptop --policy explorer or on the consent page when a chat client connects. A token with no policy named gets default.

A policy keeps an agent to what you meant it to do. It is a guardrail, not a sandbox: code set on escaping it may succeed.

Keys

Every policy starts from default and changes only what it names.

  • :tools - The MCP tools the token gets: :eval, :define or both. :define brings the patch tool with it. Replaces the default's list, which has both.
  • :rules - Relaxes the rules on the code the token submits. There are two, allow_defmacro and allow_dynamic_dispatch, both off unless set to true.
  • :allow - Modules the token's code may call. A module alone grants all of it. {Mod, only: [fun: 1]} grants just the functions listed, and {Mod, except: [fun: 1]} all but those. An entry replaces whatever the default grants for that module, so allow: [Kernel] brings back apply/2 and the rest the default holds back.
  • :deny - Modules the token's code may not call at all. A module in both allow and deny is denied.

Tools and grants are separate. Without the except, explorer above could not define modules but could still delete them with Host.Code.remove/1.

Modules agents define need no allow. Every token can call them, whichever token defined them.

Applying and reading

Every module and function a policy names must exist, so a typo stops the boot instead of granting nothing. A change takes a restart. A policy in the data dir's config.exs replaces any policy of the same name declared elsewhere (Beamlet.Config.Provider).

beamlet policies.show explorer prints what a policy allows. Agent code reads its own with Host.Code.print_policy/0.

Relaxed rules reach every token

allow_defmacro lets the token write macros. A macro writes code into every module that uses it, so other tokens end up running code this token wrote.

allow_dynamic_dispatch lets code compute what it calls, as in mod.fun() with mod a variable. Beamlet cannot check such a call, so any module is within reach. Other tokens reach it too, through the modules this token defines.

Give either only to a token you would trust with the whole beamlet.

Summary

Types

A module's grant: everything, a closed list, or all but.

A function name/arity pair, the grants' granularity.

The grant table. Absence means denied.

t()

A built policy.

An MCP tool a policy may grant.

Functions

Builds a declared policy from its document, on top of the default.

The policy Beamlet ships: both tools, strict rules, the curated grants.

Renders the policy as an agent or operator reads it: name and tools, the rules in force, the deliberate denials it has not re-granted with their reasons, and the modules granted only in part.

The MCP tools a policy puts in a token's tool list, in name order.

Types

entry()

@type entry() :: :all | {:only, [fa()]} | {:except, [fa()]}

A module's grant: everything, a closed list, or all but.

fa()

@type fa() :: {atom(), arity()}

A function name/arity pair, the grants' granularity.

grants()

@type grants() :: %{required(module()) => entry()}

The grant table. Absence means denied.

t()

@type t() :: %Beamlet.Policy{
  grants: grants(),
  name: String.t(),
  rules: %Beamlet.Policy.Rules{
    allow_defmacro: term(),
    allow_dynamic_dispatch: term()
  },
  tools: [tool()]
}

A built policy.

tool()

@type tool() :: :define | :eval

An MCP tool a policy may grant.

Functions

build(name, document)

@spec build(String.t() | atom(), keyword()) :: {:ok, t()} | {:error, String.t()}

Builds a declared policy from its document, on top of the default.

The name is a string or atom. The document is a keyword list with any of tools, rules, allow and deny, as the moduledoc describes. An error names the policy and the key at fault.

default()

@spec default() :: t()

The policy Beamlet ships: both tools, strict rules, the curated grants.

render(policy)

@spec render(t()) :: String.t()

Renders the policy as an agent or operator reads it: name and tools, the rules in force, the deliberate denials it has not re-granted with their reasons, and the modules granted only in part.

The only listable answer to "what is disallowed", since everything absent from the grants is denied: the rendering covers the denials that carry teaching copy rather than the unbounded rest.

tool_list(policy)

@spec tool_list(t()) :: [:define | :eval | :patch]

The MCP tools a policy puts in a token's tool list, in name order.

define is two tools in one: a policy granting it lists the define and patch tools, since both write modules through the same pipeline under the same limit.