Add support for more harnesses - #26
Merged
Merged
Conversation
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.
armstrongl
force-pushed
the
larah/more-harnesses
branch
from
September 15, 2026 15:06
5b1b27f to
601fadf
Compare
armstrongl
force-pushed
the
larah/more-harnesses
branch
from
September 15, 2026 22:52
601fadf to
db59a8a
Compare
guygrigsby
reviewed
Sep 16, 2026
guygrigsby
left a comment
Contributor
There was a problem hiding this comment.
I found one bug re: the vertex case and a couple other suggestions.
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
approved these changes
Sep 17, 2026
guygrigsby
left a comment
Contributor
There was a problem hiding this comment.
LGTM. One suggestion, but approved either way. Thank you!
Co-authored-by: Guy J Grigsby <guy@tailscale.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
CUSTOM_BASE_URL, so the user's Hermes configuration stays unchanged.docs/adding-a-client.mdand three skills in.agents/skills/. They record the client contract, the menu and replay rules, the CI gates, and the live routing checks.main. The clients now callSupportsEndpointwith theconfig.Endpoint*constants. TheCompatibilitymap no longer exists.LastBackendTypevalues unchanged. These strings are in the user'slauncher.json. A change to them makes the quick-select record invalid.bash -o pipefail. Acurlfailure in a pipe gives an exit status of 0 without this option, and the TUI then reports an installation that did not occur.GET /v1/models. The oldGET /api/providerspath gives status 403. All commands now send theUser-Agent: aperture-cliheader, because Aperture filters the model list for Claude Code user agents.XDG_CONFIG_HOME, butos.UserConfigDirignores this variable on macOS. The test failed on macOS and passed in Linux CI.README.mdentries for the three agents and links to the contributor documentation.How to test:
make install. An older binary callsGET /api/providersand cannot connect.gofmt -l .,go vet ./...,make test, andmake build. Use Go 1.26.6 and Go 1.27.apertureand make sure the agent menu opens.[0]quick-select row shows the agent, the model, and the endpoint.Settings. Make sure the quick-select row is not shown. Change back and make sure it returns.~/Library/Application Support/aperture/clients/pi/after Pi exits.