Skip to content

fix(skills): mount skills on a runtime the library does not know (#1634) - #1834

Merged
jhgaylor merged 3 commits into
mainfrom
stack/1634-3-skills
Sep 11, 2026
Merged

fix(skills): mount skills on a runtime the library does not know (#1634)#1834
jhgaylor merged 3 commits into
mainfrom
stack/1634-3-skills

Conversation

@jhgaylor

@jhgaylor jhgaylor commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

SandboxSkills.mount/3 forwarded a runtime string to
Managoat.Runtimes.Skills.install/3, which resolves it through
Managoat.Runtimes.for_runtime/1 — a closed map of the four packaged CLIs
that knows neither acp nor the deployed ACP fixture. It answered
{:error, "unsupported runtime: acp"}, and the sandbox came up with no skills
at all, including the two bundled ones, with nothing said about it anywhere.

  • A string is resolved through Fountain.RuntimeDispatch, the host dispatch
    that already knows every runtime Fountain has. A module is passed straight
    through as before.
  • A runtime nothing implements is logged and returned rather than swallowed. A
    missing skill is a degraded agent and not a broken one, which is the trade
    Managoat.Runtimes.Skills already makes for a single skill that will not
    install; this extends it to a runtime that cannot be resolved, without
    extending it to silence.

conversation_server.ex is line-pinned and only shrinks (#1369), so the note
about the mount being best effort folds into the comment already there rather
than adding a line to it. The pin stays at 2774.

The deployed fixture was reached by this too, and its own changeset refuses an
agent carrying any skills, so nothing observable changes there.


Stack for #1634 (merge top down; each PR is based on the one below it in the list):

PR What it is
1 #1832 Fountain.CommandRuntime and the host dispatch for acp
2 #1833 agents.runtime_command, and acp becomes a runtime you can name
3 #1834 skills mount on a runtime the library does not know
4 #1835 a turn on the acp runtime, end to end
5 #1836 refuse a turn whose agent no longer carries a command
6 #1837 the acp runtime in the agent form
7 #1838 a real ACP program through a whole turn
8 #1839 docs, CHANGELOG and SDK 1.25.0

Replaces #1677, which was the same feature as one 1,760-line branch.

🤖 Generated with Claude Code

https://claude.ai/code/session_01A2iMoNvSvrkQQfnP4RzVi2

@jhgaylor
jhgaylor force-pushed the stack/1634-2-runtime-command branch from 9f0ac07 to c7bdd83 Compare September 11, 2026 01:19
jhgaylor and others added 3 commits September 10, 2026 21:33
)

Every runtime Fountain has is an LLM coding agent. A deterministic program
that wants a warm machine, a vault, a thread, a schedule and a human who can
read what happened has nowhere to run: `Managoat.Runtimes` is a closed map of
four CLIs, each with a pinned adapter, a provider and an inference credential.

This adds the runtime module and the host dispatch that resolves it, and
nothing else. `"acp"` is not yet a value `Agent.changeset/2` accepts and the
`runtime_command` column it reads does not exist, so no request reaches this
code until the next PR in the stack opens the door.

- `Fountain.CommandRuntime` implements `Managoat.Runtimes` for a command the
  agent names. It writes no config, prepares no sandbox and builds no legacy
  command, so `Provisioning`'s `function_exported?` guards skip it. Its
  `default_env/2` ignores the credentials it is handed and exports one
  variable, `FOUNTAIN_SKILLS_DIR`.
- `Fountain.RuntimeDispatch` answers `acp` itself: `for_agent/1`,
  `acp_enabled?/1` and an `install/3` that has nothing to install. It gains
  `acp_model/2` (always nil for `acp`, so the pin path that reports
  `model`/`failed` is unreachable there), and the two predicates the schema
  needs next, `model_required?/1` and `command_required?/1`.
- `command/1` becomes `command/2`. The argv of this runtime is a property of
  the agent rather than of the runtime name, so the agent has to be in hand;
  every other clause ignores it. `TurnMachine.command/7` passes it through.

Stack for #1634 (merge top down):

1. **this PR** — the runtime module and the dispatch
2. `agents.runtime_command`, and `acp` becomes a runtime an agent can name
3. skills mount on the acp runtime
4. a turn on the acp runtime, end to end
5. refuse a turn whose agent no longer carries a command
6. the console form
7. the conformance fixture, a real OS process through a whole turn
8. docs, CHANGELOG and the SDK release

Refs #1634

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2iMoNvSvrkQQfnP4RzVi2
…an name (#1634)

Opens the door the previous PR built behind. `acp` joins the runtime enum, a
new `runtime_command` column carries the command it launches, and `model`
becomes optional for that one runtime.

- **The column.** `agents.runtime_command` is text: a value may be a whole
  shell line ("cd /srv/app && exec bin/agent acp"), and a length rule belongs
  in the changeset if one is ever wanted. `agents.model` loses its NOT NULL in
  the same migration, because whether a model is required is a rule about the
  pair of columns and belongs where both are in hand.
- **Both refusals are 422s naming the field.** A missing `runtime_command` on
  `acp`, or one carried by any other runtime, which resolves its own
  executable from a pinned table. A `runtime_command` that is stored and never
  run reads as configuration and is not.
- **A model is still checked when it is given.** Optional on `acp` and inert
  there, but it still has to parse and name a known provider, because a value
  that is stored and ignored is worse than one that is refused.
- **`model` is nullable on the wire**, in the response and in both request
  bodies, so an agent converted to `acp` can clear the model it no longer uses
  with `{"model": null}`. CastAndValidate runs before the changeset, so a
  non-nullable string there would reject the null with a 400 and leave the
  stale value on the row forever. `AgentRequest` drops `model` from `required`
  and the changeset enforces it instead, so a missing model is a 422 rather
  than a 400.
- Model suggestions return nothing for `acp`, and the export carries the field
  so an account export stays a restorable copy.

**The Swift SDK had to widen with the wire.** `FountainKit.Agent` declared
`public var model: String` on a `Decodable` struct, so an explicit null threw
`valueNotFound` — and because a page is decoded whole, one acp agent in the
account broke `agents.list()` for every Swift caller, not just a fetch of that
agent. `model` is now `String?`, `Agent` and `AgentInput` carry
`runtimeCommand`, and `Runtime.acp` joins the other four. The contract checker
does not compare a hand-written Swift model's optionality against a nullable
wire field, so nothing caught this: the two new decode tests do, and both fail
with the original `valueNotFound` when the widening is backed out.

`fountain apply` needed no change — the manifest's allowed spec keys derive
from `Agent.cast_fields/0` — but nothing pinned that, and the docs in the last
PR of this stack show a manifest using the field. Two tests in
`manifest_test.exs` now cover it, and the first fails if `runtime_command`
leaves `cast_fields/0`.

`sdk-no-release`: `contract.json`, the generated TypeScript types and the
Swift model move here; the TypeScript version bump is the last PR in the
stack, and the Swift entry goes under its existing `Unreleased` heading.

Refs #1634

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2iMoNvSvrkQQfnP4RzVi2
`SandboxSkills.mount/3` forwarded a runtime **string** to
`Managoat.Runtimes.Skills.install/3`, which resolves it through
`Managoat.Runtimes.for_runtime/1` — a closed map of the four packaged CLIs
that knows neither `acp` nor the deployed ACP fixture. It answered
`{:error, "unsupported runtime: acp"}`, and the sandbox came up with no skills
at all, including the two bundled ones, with nothing said about it anywhere.

- A string is resolved through `Fountain.RuntimeDispatch`, the host dispatch
  that already knows every runtime Fountain has. A module is passed straight
  through as before.
- A runtime nothing implements is logged and returned rather than swallowed. A
  missing skill is a degraded agent and not a broken one, which is the trade
  `Managoat.Runtimes.Skills` already makes for a single skill that will not
  install; this extends it to a runtime that cannot be resolved, without
  extending it to silence.

The deployed fixture was reached by this too, and its own changeset refuses an
agent carrying any skills, so nothing observable changes there.

Stack for #1634 (merge top down):

1. `Fountain.CommandRuntime`, the acp runtime's module
2. `agents.runtime_command`, and `acp` becomes nameable
3. **this PR** — skills mount on the acp runtime
4. a turn on the acp runtime, end to end
5. refuse a turn whose agent no longer carries a command
6. the console form
7. the conformance fixture, a real OS process through a whole turn
8. docs, CHANGELOG and the SDK release

Refs #1634

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2iMoNvSvrkQQfnP4RzVi2
@jhgaylor
jhgaylor force-pushed the stack/1634-2-runtime-command branch from c7bdd83 to d577e1f Compare September 11, 2026 01:41
@jhgaylor
jhgaylor changed the base branch from stack/1634-2-runtime-command to main September 11, 2026 01:56
@jhgaylor
jhgaylor merged commit ee6d551 into main Sep 11, 2026
23 checks passed
@jhgaylor
jhgaylor deleted the stack/1634-3-skills branch September 11, 2026 02:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants