# `Rho.Dyn`
[🔗](https://github.com/GenericJam/rho/blob/v0.1.0/lib/rho/dyn.ex#L1)

The Dyn layer: code the agent writes, compiles and runs on the device
itself (tools and screens), next to the fixed Core (the loop, sessions,
sign-in, the core tools, the approval gate and the repair path, which
ship in the app and which the agent can't change). The Core changes Dyn
only through this module, one generation at a time:

    stage_put/3 ... ──► propose/2 ──► activate/3 ──► probation ──► proven
     (staging copy)     check,         (approval)     │
                        compile G<n>,                 └─► reverted (crashes,
                        selftest                           failed launch, by hand)

1. **Stage**: edits go to a staging copy of the current generation's
   sources (`stage_reset/1`, `stage_put/3`, `stage_delete/2`).
2. **Propose** (`propose/2`): static check (`Rho.Dyn.Check`),
   compile into `Dyn.G<n>.*` next to the running generation
   (`Rho.Dyn.Compiler`), selftest each module in a contained
   process (`Rho.Dyn.Selftest`). Nothing live changes: the
   result is a proposal (generation, diff, rationale, test results) or a
   rejection saying which step refused and why.
3. **Activate** (`activate/3`) with a token from the approval seam
   (`request_approval/2`, `Rho.Dyn.Approval`): the human approves with
   the screen lock.
4. **Probation, revert, safe mode**: `Rho.Dyn.Keeper`. A new generation
   is unproven until it ran 60 s without a Dyn crash and survived an app
   restart; 3 crashes in 60 s, or a launch that dies before it is
   stable, revert it; two failed launches in a row start the next one in
   safe mode, with no Dyn code loaded, so the Core can always repair it.

**Generations, not files**: every accepted change is a generation, its
full source set, binaries, selftests and diff (`Rho.Dyn.Store`);
activation and revert flip a pointer. **Versioned modules**: the agent
writes `Dyn.Foo` (the namespace is the host's, `Rho.Dyn.Config`) and
generation `n` compiles it as `Dyn.G<n>.Foo`, so a candidate loads next
to the running code, never over it (no purge kills a running process);
an old generation is unloaded only once no process runs its code.
The registry maps logical names (`{:tool, "weather"}`, `{:screen,
"Notes"}`) to the current generation's modules (`Rho.Dyn.Registry`);
the loop sees Dyn tools through `Rho.ToolRegistry` (`Rho.Dyn.Source`),
from the next model call after a switch.

**Keeping Dyn code out of Core processes** (best effort) where Dyn
hands something over (`Rho.Untrusted`): crashes are formatted where
they happened (the tool's task, the selftest's or the compile's
process, bounded), and the loop, the Keeper and a proposal's caller
only describe a reason they get, without an exception's `message/1` or
a Dyn struct's protocols. A Dyn tool's answer is plain data before it
leaves its task, and its declaration is read in a bounded process
outside the loop and kept (see `Rho.Tool`). Log events that hold a Dyn
term are described before the logger formats them (`Rho.Dyn.LogGuard`).
A Dyn screen's own process runs its code, so whatever the host's screen
layer does with its crash is the host's (mob's) business.

**Threat model**: Dyn guards against the model's mistakes (a built tool
that crashes, hangs, or calls something it shouldn't by accident), not
against deliberately hostile code. The static check is a best-effort
deny-list, not a sandbox (`Rho.Dyn.Check` lists the known ways around
it); `Rho.Untrusted` and `Rho.Dyn.LogGuard` lower the chance that a Dyn
crash takes a trusted process down.

Dyn compiles Elixir on the device: the app must ship the Elixir compiler
(`:elixir`'s compiler modules and `:compiler`), so no slim build that
strips them.

The host's seams (`Rho.Dyn.Config`): the namespace, the Core's module
prefixes and what Dyn code may call (`Rho.Dyn.Check`), the screen
integration (`Rho.Dyn.Screens`), the approval (`Rho.Dyn.Approval`), the
launch's boot (`boot/1`) and `:on_stable` hook, and the seed it ships
(`Rho.Dyn.Seed`).

Every function takes the Keeper (its registered name, which is also the
registry table's) last, defaulting to the app's.

# `proposal`

```elixir
@type proposal() :: %{
  n: pos_integer(),
  parent: non_neg_integer(),
  rationale: String.t(),
  diff: String.t(),
  modules: [Rho.Dyn.Generation.module_entry()],
  selftests: [Rho.Dyn.Generation.selftest()],
  compile_ms: non_neg_integer(),
  reused: non_neg_integer(),
  warnings: [String.t()]
}
```

# `rejection`

```elixir
@type rejection() :: %{
  n: pos_integer() | nil,
  stage: :check | :compile | :selftest | :names | :superseded | :install,
  reason: String.t(),
  violations: [Rho.Dyn.Check.violation()],
  selftests: [Rho.Dyn.Generation.selftest()]
}
```

# `activate`

```elixir
@spec activate(pos_integer(), Rho.Dyn.Approval.token(), atom()) ::
  {:ok, Rho.Dyn.Generation.t()} | {:error, term()}
```

Activates candidate generation `n` (it goes on probation). Needs an approval token.

# `agent_guide`

```elixir
@spec agent_guide() :: String.t()
```

The system prompt section that explains the Dyn layer and its tools to
the agent: rho's rules, the screen rules of the host's
`Rho.Dyn.Screens`, then the host's own (`Rho.Dyn.Config`'s `:guide`).

# `await_rebuild`

```elixir
@spec await_rebuild(atom()) :: :ok
```

Returns once this launch's background rebuild is over (at once if none runs).

# `boot`

```elixir
@spec boot(keyword()) :: Rho.Dyn.Keeper.boot_report()
```

The Dyn step of the host's boot: loads the current generation (or
decides on boot probation / safe mode, see `Rho.Dyn.Keeper`) and has the
first rendered frame start the clock to "stable" (the host's
`c:Rho.Dyn.Screens.after_first_render/1`). `opts` go to
`Rho.Dyn.Keeper.boot/2` (`core:`, `core_rolled_back:`).

# `diff`

```elixir
@spec diff(non_neg_integer(), atom()) :: String.t()
```

Generation `n`'s unified diff against its parent.

# `discard`

```elixir
@spec discard(pos_integer(), atom()) :: :ok | {:error, :not_pending | :not_running}
```

Drops the pending candidate `n` (no approval needed: nothing that runs changes).

# `dyn_module?`

```elixir
@spec dyn_module?(module()) :: boolean()
```

Is `module` Dyn code: a generation's versioned module (`Dyn.G<n>.*`) or a
protocol implementation for one (`Rho.Dyn.Compiler.generation_of/1`)?

# `generation`

```elixir
@spec generation(non_neg_integer(), atom()) ::
  {:ok, Rho.Dyn.Generation.t()} | {:error, term()}
```

Generation `n`'s record (`Rho.Dyn.Generation`); 0 is the empty generation.

# `generations`

```elixir
@spec generations(atom()) :: [Rho.Dyn.Generation.t()]
```

Every generation, newest first, generation 0 last.

# `log`

```elixir
@spec log(pos_integer(), atom()) :: [map()]
```

The last `limit` reports (crashes, reverts, safe mode, load failures),
oldest first. Values are as stored (strings for atoms).

# `lookup`

```elixir
@spec lookup(Rho.Dyn.Registry.key(), atom()) :: {:ok, module()} | :error
```

The current generation's module for a logical name (`{:tool, "weather"}`,
`{:screen, "Notes"}`, `{:module, "Helpers"}`, see `Rho.Dyn.Registry`);
`:error` when it has none.

# `propose`

```elixir
@spec propose(String.t(), atom()) ::
  {:ok, proposal()}
  | {:error,
     rejection()
     | :no_changes
     | :rationale_required
     | :not_running
     | :proposal_limit
     | :rebuilding}
```

Turns the staged sources into a candidate generation without activating
it. `{:error, rejection}` names the step that refused (`:check`,
`:compile`, `:selftest`, `:names`) and why; a rejected generation's
modules are unloaded and the current one is untouched.
`{:error, :proposal_limit}`: this launch made too many proposals (see
`Rho.Dyn.Keeper`); the app has to restart first.
`{:error, :rebuilding}`: the current generation is being rebuilt for a
new Core in the background; propose once it's loaded.

# `rebuilding`

```elixir
@spec rebuilding(atom()) :: pos_integer() | nil
```

The generation this launch is rebuilding for a new Core in the
background (nothing of it is registered yet), or nil.

# `request_approval`

```elixir
@spec request_approval(Rho.Dyn.Approval.subject(), atom()) ::
  {:ok, Rho.Dyn.Approval.token()} | {:error, term()}
```

A token for `subject` from the configured approval (in the app: only
after `Rho.Dyn.Approval.Biometric.confirm/1`).

# `revert_to`

```elixir
@spec revert_to(non_neg_integer(), Rho.Dyn.Approval.token(), atom()) ::
  {:ok, Rho.Dyn.Generation.t()} | {:error, term()}
```

Makes generation `n` (one that was active once, or 0) current. Needs an approval token.

# `safe_mode?`

```elixir
@spec safe_mode?(atom()) :: boolean()
```

Did this launch start in safe mode (no Dyn module loaded)?

# `screens`

```elixir
@spec screens(atom()) :: [{String.t(), module()}]
```

The current generation's screens, `[{name, module}]`.

# `seed`

```elixir
@spec seed(
  (%{required(String.t()) =&gt; String.t()} -&gt;
     %{required(String.t()) =&gt; String.t()}),
  String.t(),
  atom()
) ::
  {:ok, pos_integer()}
  | {:error,
     :no_changes | :safe_mode | :proposal_pending | :not_running | rejection()}
```

Installs the seed (`Rho.Dyn.Seed`, the default front) or a
newer one: `merge` gets the current generation's sources and returns the
full source set to install (`Rho.Dyn.Seed.merge/3` keeps the
user's edits). It is checked, compiled and selftested like a proposal,
then made current and proven by `Rho.Dyn.Keeper.install_seed/3`
with no approval, because the seed ships with the Core and the merge
only replaces seed files nobody changed. Waits (an error, retried next
launch) in safe mode or while a proposal is pending; `{:error,
:no_changes}` when the merge changes nothing.

# `sources`

```elixir
@spec sources(non_neg_integer(), atom()) :: %{required(String.t()) =&gt; String.t()}
```

Generation `n`'s sources (what runs, for the current one).

# `stage_create`

```elixir
@spec stage_create(String.t(), String.t(), atom()) ::
  :ok | {:error, :bad_path | :exists | :not_running | File.posix()}
```

Creates `path` in staging without replacing an existing source.

# `stage_delete`

```elixir
@spec stage_delete(String.t(), atom()) :: :ok | {:error, :bad_path | :not_running}
```

Removes `path` from staging (`:ok` if it wasn't there).

# `stage_put`

```elixir
@spec stage_put(String.t(), String.t(), atom()) ::
  :ok | {:error, :bad_path | :not_running}
```

Writes `source` to `path` (relative, `.ex`) in staging.

# `stage_read`

```elixir
@spec stage_read(String.t(), atom()) ::
  {:ok, String.t()} | {:error, :bad_path | :not_found | :not_running}
```

Reads `path` from staging.

# `stage_reset`

```elixir
@spec stage_reset(atom()) :: :ok | {:error, :not_running}
```

Resets staging to the current generation's sources.

# `stage_update`

```elixir
@spec stage_update(String.t(), (String.t() -&gt; {:ok, String.t()} | term()), atom()) ::
  {:ok, String.t()} | term()
```

Reads `path` from staging, passes its source to `fun` and writes back what
`fun` returns as `{:ok, new_source}` (anything else is returned and nothing
is written), as one step: edits of the same file never interleave, so two
`dyn_edit` calls the model makes in parallel both land. Returns
`{:ok, old_source}` after writing.

# `staged`

```elixir
@spec staged(atom()) :: %{required(String.t()) =&gt; String.t()}
```

The staged sources, `%{path => source}` (empty when the Keeper isn't running).

# `status`

```elixir
@spec status(atom()) :: map()
```

The current generation, its status, its parent, the mode and the pending candidate.

# `subscribe`

```elixir
@spec subscribe(atom()) :: :ok
```

`{:rho_dyn, event}` for every crash, revert, activation, proof, candidate, safe mode.

# `tools`

```elixir
@spec tools(atom()) :: [{String.t(), module()}]
```

The current generation's tools, `[{name, module}]`.

# `unwatch`

```elixir
@spec unwatch(pid()) :: :ok
```

Stops watching `pid` (`watch/2`): its exit is no longer a crash, for a
host that stops a process it watched in an orderly way.

# `watch`

```elixir
@spec watch(pid(), module()) :: :ok
```

Has the app's Keeper watch `pid` if `module` is Dyn code, so its crash
counts against the generation (`Rho.ToolRunner` calls this for every
tool call, through `Rho.Dyn.Source`; a host calls it for a process of
its own that runs Dyn code, a screen say). Its abnormal exit is a
crash; exiting with `{:rho_crash, text}` (`Rho.Untrusted.crash/3`)
reports `text` as it is. A no-op for Core modules.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
