Skip to content

Add support for more harnesses - #26

Merged
armstrongl merged 15 commits into
mainfrom
larah/more-harnesses
Sep 17, 2026
Merged

armstrongl merged 15 commits into
mainfrom
larah/more-harnesses

Conversation

@armstrongl

@armstrongl armstrongl commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Aperture CLI supported five coding agents, and the steps to add more were not written down. This adds three agents and a contributor guide. This branch is rebased on main, so the new agents use the current supported-endpoint API.

Features and notable additions/changes:

  • Adds the Pi client. It speaks OpenAI Responses, Anthropic Messages, OpenAI Chat Completions, and Google Vertex. It writes one extension for each launch and removes it at exit, so the user's Pi settings and sessions stay unchanged.
  • Adds the Oh My Pi client. It uses the same extension API and the same four protocols.
  • Adds the Hermes Agent client. It speaks OpenAI Chat Completions through CUSTOM_BASE_URL, so the user's Hermes configuration stays unchanged.
  • Adds docs/adding-a-client.md and three skills in .agents/skills/. They record the client contract, the menu and replay rules, the CI gates, and the live routing checks.
  • Moves the three new clients to the supported-endpoint API from main. The clients now call SupportsEndpoint with the config.Endpoint* constants. The Compatibility map no longer exists.
  • Keeps the recorded LastBackendType values unchanged. These strings are in the user's launcher.json. A change to them makes the quick-select record invalid.
  • Installs Hermes with bash -o pipefail. A curl failure in a pipe gives an exit status of 0 without this option, and the TUI then reports an installation that did not occur.
  • Updates the guide and the skills for GET /v1/models. The old GET /api/providers path gives status 403. All commands now send the User-Agent: aperture-cli header, because Aperture filters the model list for Claude Code user agents.
  • Corrects the settings test on macOS. The test used XDG_CONFIG_HOME, but os.UserConfigDir ignores this variable on macOS. The test failed on macOS and passed in Linux CI.
  • Adds README.md entries for the three agents and links to the contributor documentation.

How to test:

  1. Install the branch with make install. An older binary calls GET /api/providers and cannot connect.
  2. Run gofmt -l ., go vet ./..., make test, and make build. Use Go 1.26.6 and Go 1.27.
  3. Start aperture and make sure the agent menu opens.
  4. Select Pi, Oh My Pi, and Hermes Agent in turn. Select a provider, a backend, and a model for each one. Make sure the agent starts and completes a request.
  5. Quit each agent. Make sure the [0] quick-select row shows the agent, the model, and the endpoint.
  6. Change to a different endpoint in Settings. Make sure the quick-select row is not shown. Change back and make sure it returns.
  7. Make sure no file stays in ~/Library/Application Support/aperture/clients/pi/ after Pi exits.

Adds docs/adding-a-client.md, a step-by-step contributor guide covering the
clients.Client interface, both routing variants (environment variables and a
generated config file), the menu flow, replay, and the CI gates.

Also ignores .artifacts/, which holds local session scratch output.
Pi has no environment variable for a custom API base URL, so routing is
expressed as a provider definition. This client generates a per-launch
extension and loads it with 'pi -e', leaving the user's own ~/.pi/agent
directory (settings, logins, session history) untouched.

Supports the four wire protocols Aperture serves: OpenAI Responses,
Anthropic Messages, OpenAI Chat Completions, and Google Vertex. Bedrock is
omitted because Pi's bedrock API type fails at request time.

Registers the client in cmd/aperture/main.go and lists it in the README.
Add Oh My Pi as a registered Aperture CLI client with discovery, Bun-based installation, provider and backend selection, model selection, YOLO mode, and quick-select replay.

Generate a temporary per-launch OMP extension instead of replacing user configuration. The extension registers Aperture-backed providers for OpenAI Responses, Anthropic Messages, OpenAI Chat Completions, and Google Vertex, then removes itself when the launched process exits.

Document Oh My Pi as a supported agent and cover endpoint construction, provider filtering, generated extension content and permissions, launch arguments, replay validation, and install metadata. This means users can launch OMP through compatible Aperture providers while retaining their existing OMP settings, credentials, and sessions.
The provider capability model changed underneath these clients while this
branch was in flight. config.ProviderInfo.Compatibility was replaced by
SupportedEndpoints, keyed on canonical API paths that are now centrally
defined as constants in internal/config/providers.go.

Each client's backend table now holds config.Endpoint* constants instead of
compat-key strings, and capability checks go through p.SupportsEndpoint.

The backend id strings are deliberately unchanged. They are persisted in
LaunchState.LastBackendType, so rewriting them would silently invalidate
existing users' quick-select state. Hermes previously used one constant for
both its capability check and its persisted backend type; those are now
separate, with the persisted value pinned to its original string.

Hermes also pipes curl into bash to install, so its install plan moves to
bash -o pipefail, matching claudecode and opencode. Without it a failing
curl is masked by the succeeding bash and the TUI reports a successful
install of nothing.
The contributor guide was written against the pre-refactor API and had
become actively misleading: its curl recipes hit GET /api/providers,
which no longer exists, and its Go samples used the removed
Compatibility map.

Discovery is now GET /v1/models, an OpenAI-style list of model rows that
config.ParseProviders aggregates into providers, so every recipe is
rewritten for that shape. The recipes now also send User-Agent:
aperture-cli, which is load-bearing: Aperture serves a filtered model list
to Claude Code user agents, so omitting it returns a shorter list than the
launcher itself sees.

Also folded in: InstallPlan.SkipInstalledCheck and the post-install binary
verification, the bash -o pipefail installer requirement, Codex moving off
CODEX_HOME to --config overrides, and quick-select's new rule that a
replay is only offered when the recorded endpoint is the active one.

All 54 line-number anchors in the guide were re-resolved against the
current tree, and the shell and python recipes were executed rather than
eyeballed. The claim that providers can report models: null was tested
against ParseProviders, found impossible under the new shape, and removed.
TestSettings_InvalidJSONReturnsErrorWithoutReplacingFile built its path
from XDG_CONFIG_HOME, but os.UserConfigDir ignores that variable on darwin
and resolves under HOME/Library/Application Support. The malformed file
was written where LoadSettings never looks, so the call hit the
missing-file path, returned defaults, and the test failed locally while
passing in Linux CI.

Ask os.UserConfigDir for the directory instead, matching how settingsPath
resolves it.

@guygrigsby guygrigsby left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found one bug re: the vertex case and a couple other suggestions.

Comment thread internal/clients/omp/omp.go Outdated
Comment thread internal/clients/pi/extension.go Outdated
Comment thread README.md Outdated
Comment thread internal/clients/pi/extension.go Outdated
The .agents/skills/ path does not exist on this branch or on main, so
the link is broken as merged. It returns with the PR that lands the
directory.
internal/clients/omp was a near copy of internal/clients/pi, so every
change to the shared design had to be made twice, and the copy dropped
the comments explaining the non-obvious parts of it: why every model
field must be populated, why the provider ID is namespaced, why routing
uses an extension instead of models.json, and why --approve is not a
yolo flag.

Extract the common client into internal/clients/pilike, parameterized by
a Variant carrying the eight things that actually differ: display name,
binary name, config directory, install command, uninstall argv, common
binary paths, yolo arguments, and the generated extension's header
comment. pi and omp become thin packages holding only their Variant, an
init() that registers it, and their own commonBinaryPaths. Every comment
from the pi package comes across, generalized only where the wording
named pi-the-binary, and omp now carries the note that Pi's --approve is
project-file trust rather than tool approval while its own
--auto-approve is a real approval bypass.

The persisted-state contract is unchanged: LastClientName stays "Pi" and
"Oh My Pi", and the backend IDs stay openai_responses, anthropic,
openai_chat, and vertex, so an existing state.json still replays. Two
new tests pin those values so a rename fails the build rather than
silently dropping every recorded launch.

The bulk of pi_test.go moves to pilike as table tests driven by a
Variant, covering both the no-yolo and yolo shapes. Each client keeps a
small test for its own identity, binary paths, install argv, and yolo
arguments.
The cleanup function returned by writeProviderExtension runs only after
the child exits normally, so a crash, a kill, or a panic strands a
tmp_aperture_provider_*.js file in the client config directory. Nothing
swept them, and each one embeds the tailnet hostname.

Glob the directory and remove every match before creating the new file.
Removal errors are ignored: a file another process holds open is not a
reason to fail the launch. The sweep is safe against a launch that is
still running, because the harness reads the extension once at startup
and never reopens it.

os.CreateTemp and its unique suffix stay, so two overlapping launches
still never write the same path.
The guide cited Pi as the plugin-shape example and pointed at
extension.go inside the Pi package, which now lives in
internal/clients/pilike.

Repoint both references, and add a note that two harnesses sharing a
plugin API should share a package rather than fork one, with pilike and
its Variant as the worked example. The plugin-shape paragraph also gains
the reminder that a crash never runs the cleanup function, so a plugin
client sweeps stale files on the next launch.
The vertex backend roots its base URL at publishers/google, and Aperture's
Vertex router picks the wire protocol from that publisher segment alone,
never from the model name. Claiming EndpointVertexClaude therefore offered
the backend to providers it cannot serve: Claude on Vertex is reached over
publishers/anthropic with :rawPredict, which no API type in the harness's
union can produce.

This narrows the claim to EndpointVertexGemini. A provider advertising only
the Anthropic endpoint no longer sees a Google Vertex backend that would
fail at request time, which matches the reasoning already used to exclude
Bedrock.

The backend id is unchanged, so no recorded launch is invalidated.

A Vertex provider that advertises both endpoints still matches, and still
lists Claude models that fail once launched. Filtering the model list by
family is the remaining fix and lands with the same fix in opencode, which
has carried this bug since #33.

@guygrigsby guygrigsby left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. One suggestion, but approved either way. Thank you!

Comment thread internal/clients/pilike/extension.go Outdated
Co-authored-by: Guy J Grigsby <guy@tailscale.com>
@armstrongl
armstrongl merged commit f260e7f into main Sep 17, 2026
2 checks passed
@armstrongl
armstrongl deleted the larah/more-harnesses branch September 17, 2026 19:42
guygrigsby added a commit that referenced this pull request Sep 18, 2026
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