Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions .claude/rules/grpc.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,20 @@ dictated by the specs and MUST match exactly.
|---|---|---|---|
| Model | `StreamCompletion` | server-streaming + cancellation | `docs/specifications/model/README.md#transport--lifecycle` (explicitly *not* bidi) |
| Tool | `Invoke` | server-streaming | `docs/specifications/tool/protocol.md` |
| Frontend | `Attach` | **bidirectional** streaming | `docs/specifications/frontend/frontend-protocol.md` |
| Widget | `Attach` | server-streaming only | `docs/specifications/frontend/widget-protocol.md` |
| Frontend | *(no Attach)* | category triple only; I/O via callback channel | `docs/specifications/frontend/frontend-protocol.md` |
| Widget | *(no Attach)* | category triple only; metadata via callback | `docs/specifications/frontend/widget-protocol.md` |
| Slashcommand | `Invoke` | server-streaming | `docs/specifications/slashcommand/protocol.md` (same shape as Tool's `Invoke` — a direct-invoke command is a tool-shaped operation) |
| Kernel callback | `RunSession`, `CountTokens` | bidirectional (go-plugin's native plugin→kernel channel) | `docs/specifications/kernel-callbacks.md` |
| Kernel callback | `Subscribe` | server-streaming | `docs/specifications/kernel-callbacks.md#subscribe` (event-bus fan-out; see `docs/specifications/event-bus.md`) |
| Kernel callback | `ReadEvents` | server-streaming | `docs/specifications/kernel-callbacks.md#readevents` |

Frontend `Attach` and the kernel-callback channel are the **only** two
genuinely bidirectional RPCs in the whole protocol — that channel's own
`Subscribe`/`ReadEvents` additions are server-streaming, not a second bidi
RPC on it. Do not default a new RPC to bidi streaming because it "might need
it later" — pick the narrowest shape the spec calls for.
The kernel-callback channel is the **only** genuinely bidirectional
transport surface in the whole protocol (the plugin is the gRPC client on
that connection). Application RPCs on it — including `Subscribe`,
`ReadEvents`, and `StreamDeltas` — are unary or server-streaming, not bidi.
Frontend and widget no longer expose an `Attach` stream. Do not default a
new RPC to bidi streaming because it "might need it later" — pick the
narrowest shape the spec calls for.

- **A backend that has no real streaming to do still implements the
streaming RPC shape** and emits exactly one terminal message. Do not add a
Expand Down
44 changes: 44 additions & 0 deletions .dev/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Local dev run

Scaffolding for running the kernel against **locally built plugin binaries** instead of published releases. Nothing here is part of the build; it exists so the frontend-hosted path can actually be exercised end to end.

## How it resolves

Two files, two different roles:

| File | Role | Found via |
|---|---|---|
| `agent/config.hcl` | Global config — carries `dev_overrides` | `$XDG_CONFIG_HOME/agent/config.hcl` |
| `agent.hcl` | Project config — `required_providers`, settings, profile | `-config` |

The global config is normally per-user at `~/.config/agent/config.hcl`, and its path is not a flag — `internal/xdg.Resolve` computes it. But it reads `XDG_CONFIG_HOME`, so pointing that at this directory makes the whole run repo-local without any code change.

`dev_overrides` is the mechanism that matters: a name listed there resolves straight to a binary on disk, skipping registry resolution, the lock file, and checksum verification entirely. Identity comes from the plugin's own `Describe` RPC, which is what `dev_overrides` exists for.

## Run it

Build the plugins first, then:

```sh
XDG_CONFIG_HOME=$PWD/.dev \
XDG_STATE_HOME=$PWD/.dev/state \
XDG_CACHE_HOME=$PWD/.dev/cache \
XDG_DATA_HOME=$PWD/.dev/data \
./bin/agent -config .dev/agent.hcl 2>.dev/agent.log
```

All four XDG vars are redirected so a dev run writes no sessions, caches, or data into your real home.

**No `-prompt`.** That is what selects frontend-hosted mode: the kernel brings every provider up, installs the frontend host, and waits while the frontend drives sessions over the callback channel. It exits when the frontend's subprocess does — quitting the TUI ends the kernel — or on Ctrl-C.

With `-prompt` you get the old behavior instead: one non-interactive session, final message on stdout, exit. Useful for checking a model provider without involving a frontend.

## `2>.dev/agent.log` is not optional

The frontend owns the terminal — it opens `/dev/tty` directly, precisely because under go-plugin stdin/stdout belong to the handshake. The kernel logs to stderr. Point both at the same terminal and kernel log lines paint straight over the UI. Redirect stderr, then `tail -f .dev/agent.log` in a second terminal.

## Before it will work

Edit `agent/config.hcl` — the paths there are absolute and machine-specific — and set a real model id in `agent.hcl`.

A session cannot start without a model provider: `internal/session` resolves the profile's model chain against the live catalog and fails with `ErrNoDefaultModel` when nothing answers. A frontend alone brings the kernel up and gives you a UI, but the first submitted prompt will fail.
60 changes: 60 additions & 0 deletions .dev/agent.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Project config for a local dev run. Not the repository's own config —
# this repo is the kernel, not a project — so it lives under .dev/ and is
# passed explicitly with -config.
#
# Both providers resolve through dev_overrides in .dev/agent/config.hcl, so
# the source/version below are never fetched. They still have to be
# declared: required_providers is what creates the local name a
# dev_overrides entry, a provider block, and an agent_profile all refer to.

required_providers {
tui = {
source = "github.com/pluggableharness/plugin-frontend-tui"
version = "~> 0.1"
}
xai = {
source = "github.com/pluggableharness/plugin-provider-xai"
version = "~> 0.1"
}
}

settings {
default_frontend = "tui"
log_level = "debug"
telemetry = false
}

provider "xai" {
# Pinned off deliberately, and the reason is a real protocol gap rather
# than a preference.
#
# pluginhost fetches GetCapabilities at bring-up step 5, BEFORE Configure
# at step 8 — it has to, because the ConfigSchema that decoding the
# provider block needs arrives with that advertisement. So a provider
# whose roster depends on Configure-time state advertises its built-in
# roster and only then swaps in the remote one, leaving the kernel
# holding a catalog the provider itself no longer honors: the kernel
# offers "grok-4-3" while StreamCompletion rejects it as unknown, since
# the live catalog names it "grok-4.3".
#
# Capabilities are never re-fetched (providercatalog is built once, in
# bringUp), so nothing reconciles them later. Until a refresh path
# exists, keeping the roster static is what makes advertisement and
# enforcement agree.
fetch_models = "false"
}

agent_profile "default" {
model {
primary {
# In the compiled-in roster AND a valid API id, which is what makes
# this the one combination that works. xAI resolves it server-side
# to grok-4.3 and says so on the wire — the remap StreamMetadata's
# actual_model now captures. Note the roster's other ids are
# hyphenated (grok-4-3) and the API rejects them outright; the live
# catalog spells that model grok-4.3.
provider = "xai"
id = "grok-4"
}
}
}
21 changes: 21 additions & 0 deletions .dev/agent/config.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Global config for a local dev run — $XDG_CONFIG_HOME/agent/config.hcl,
# with XDG_CONFIG_HOME pointed at .dev/ (see .dev/README.md).
#
# dev_overrides maps a required_providers local name to a binary on disk.
# The kernel uses that binary directly and skips the whole registry path:
# no version constraint, no lock-file row, no checksum. Identity comes from
# the plugin's own Describe RPC instead, which is exactly what
# dev_overrides exists for
# (docs/specifications/configuration/lock-file.md#dev_overrides-and-identity-without-a-lock-entry).
#
# Paths must be absolute — edit these to match your checkouts.

dev_overrides {
tui = "/home/steven/pluggableharness/plugin-frontend-tui/bin/frontend_tui"

# A session cannot start without a model provider: internal/session
# resolves the profile's model chain against the live catalog and fails
# with ErrNoDefaultModel if nothing answers. This one authenticates from
# ~/.grok/auth.json, so a `grok login` session is enough — no API key.
xai = "/home/steven/pluggableharness/plugin-provider-xai/bin/provider_xai"
}
8 changes: 3 additions & 5 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
# --- Tier 1: top-level surfaces ---------------------------------------------
/api/ @pluggableharness/protocol-maintainers
/pkg/ @pluggableharness/sdk-maintainers
/examples/ @pluggableharness/sdk-maintainers
/internal/ @pluggableharness/kernel-maintainers
/cmd/ @pluggableharness/kernel-maintainers
/docs/ @pluggableharness/docs-maintainers
Expand All @@ -35,14 +34,13 @@
# The plugin-author surface. Third parties compile against it.
/docs/first-party/ @pluggableharness/sdk-maintainers

# The terminal shell: different skill set, and the TTY-ownership constraint
# that follows from a frontend being a go-plugin subprocess.
# The frontend-facing protocol surface. The reference terminal shell itself
# now lives in pluggableharness/plugin-frontend-tui, so what is left here is
# the contract every frontend builds against, not one implementation.
/docs/first-party/frontends/ @pluggableharness/frontend-maintainers
/pkg/frontend/ @pluggableharness/frontend-maintainers @pluggableharness/sdk-maintainers
/pkg/widget/ @pluggableharness/frontend-maintainers @pluggableharness/sdk-maintainers
/pkg/render/ @pluggableharness/frontend-maintainers @pluggableharness/sdk-maintainers
/internal/tui/ @pluggableharness/frontend-maintainers
/cmd/tui/ @pluggableharness/frontend-maintainers

# release.yml hands whatever a v* tag points at to GoReleaser.
/.github/workflows/release.yml @pluggableharness/release-engineering
Expand Down
11 changes: 4 additions & 7 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,13 @@
version: 2
updates:
# Every Go module in the tree needs its own entry — Dependabot resolves a
# directory, not a repository. examples/provider is a separate module by
# design (it exists to prove pkg/ builds from outside the main one), so a
# bump to the root go.mod leaves its pinned versions untouched. That is
# not cosmetic: CI runs `go mod tidy` there and fails on any diff, so
# omitting it turns every future root-module bump into a red main.
# Same failure mode the composite-action note below describes.
# directory, not a repository. The root module is currently the only one;
# adding a second module anywhere in the tree means adding its directory
# here too, or a root-module bump silently leaves its pinned versions
# untouched. Same failure mode the composite-action note below describes.
- package-ecosystem: "gomod"
directories:
- "/"
- "/examples/provider"
schedule:
interval: "weekly"
cooldown:
Expand Down
18 changes: 0 additions & 18 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,24 +96,6 @@ jobs:
exit 1
fi

# Builds the example provider from its own module, against this
# commit via a replace directive. This is the only check that proves
# pkg/ is genuinely usable from outside the main module: the
# depguard rule on internal/anthropic only simulates that isolation,
# and a simulation cannot catch an unexported type leaking through
# an exported signature.
- name: Build the standalone example provider
working-directory: examples/provider
run: |
go mod tidy
git diff --exit-code -- go.mod go.sum
go build ./...
go vet ./...
# Also runs the example's own conformance test, which proves
# pkg/model/modeltest is reachable and usable by a third party —
# the premise of shipping a conformance suite in pkg/ at all.
go test ./...

# ---------------------------------------------------------------------------
# test — race-enabled tests on every platform we release for.
#
Expand Down
19 changes: 16 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,15 @@ __debug_bin*
# the working directory, which the extension patterns above do not catch on
# Linux or macOS. These are the ones this repo can actually produce, listed
# by name because a broad pattern here would ignore real source files.
#
# Both shapes are covered on purpose: `go build ./cmd/agent` from the repo
# root lands at /agent, while `cd cmd/agent && go build` lands beside the
# main package. The second used to be uncovered, which is exactly how a
# 21 MB providerconform reached a commit.
/agent
/anthropic
/tui
/providerconform
examples/*/agent-example-provider
/cmd/agent/agent
/cmd/providerconform/providerconform

# --- Go: test, coverage & profiling artifacts --------------------------------
*.out
Expand Down Expand Up @@ -118,3 +122,12 @@ ehthumbs.db
Desktop.ini
$RECYCLE.BIN/
*.lnk

# --- Local dev run -----------------------------------------------------------
# .dev/ holds committed scaffolding for running the kernel against locally
# built plugins (.dev/README.md). The config files are committed as
# templates; everything the run *writes* is not.
/.dev/state/
/.dev/cache/
/.dev/data/
/.dev/*.log
27 changes: 0 additions & 27 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,33 +22,6 @@ linters:
- unconvert
- unparam
- wastedassign
- depguard
settings:
depguard:
rules:
# internal/anthropic is the reference model-provider plugin. It
# runs out of process and is meant to be indistinguishable from a
# plugin a third party could write against pkg/ alone, so it may
# import only pkg/... and the standard library. Reaching into any
# other internal/ package would make it a privileged in-tree
# shortcut rather than a proof that the published SDK is
# sufficient — and the whole point of building it was to find out
# whether pkg/ really is.
#
# Scoped to non-test files: the integration tier legitimately
# imports internal/pluginruntime to launch the built binary the
# way the kernel does, which is the kernel-launches-plugin
# direction, not a dependency of the plugin itself.
anthropic-plugin-isolation:
list-mode: lax
files:
- "**/internal/anthropic/**"
- "!$test"
allow:
- github.com/pluggableharness/agent/internal/anthropic
deny:
- pkg: github.com/pluggableharness/agent/internal
desc: internal/anthropic is a reference plugin — it may import only pkg/... and the standard library, never another internal/ package (see internal/anthropic/CLAUDE.md)
exclusions:
rules:
- path: _test\.go
Expand Down
22 changes: 2 additions & 20 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
version: 2

# Two binaries: the kernel and the reference Anthropic model provider.
# Both are stamped through -ldflags rather than reading their identity
# from a file at runtime, matching how internal/pluginhost's integration
# fixture is built (`-X main.fixtureName=...`) — a plugin's Describe RPC
# has to answer from the running process, since a dev_overrides binary has
# no lock-file entry to read identity from
# (configuration/lock-file.md#dev_overrides-and-identity-without-a-lock-entry).
# One binary: the kernel. Every model, tool, and frontend provider ships
# from its own repository, so nothing else is built here.
builds:
- id: agent
main: ./cmd/agent
Expand All @@ -20,19 +15,6 @@ builds:
- -s -w
- -X main.version={{ .Version }}

- id: anthropic
main: ./cmd/anthropic
binary: agent-provider-anthropic
env: [CGO_ENABLED=0]
goos: [linux, darwin, windows]
goarch: [amd64, arm64]
flags: [-trimpath]
ldflags:
- -s -w
# pluginVersion is what this plugin reports through Describe; the
# source stays the -X-able default in main.go for a checkout build.
- -X main.pluginVersion={{ .Version }}

archives:
- formats: [tar.gz]
format_overrides:
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ An AI coding harness built as a Go microkernel: the kernel owns plugin lifecycle

## Current state

Kernel-side packages in `internal/` and the `pkg/` SDK are real, tested Go. Three `cmd/` binaries exist: `agent` (the kernel, currently non-interactive — no REPL), `anthropic` (the reference model-provider plugin, and the template for any new plugin binary), and `tui` (the reference terminal shell, currently driven by a scripted demo source because no kernel-side frontend-attach path exists yet). Most other plugin categories exist only as spec. Implementation is spec-first: before writing code, confirm the relevant spec exists and is settled; if it has open questions bearing on the task, raise them instead of coding against an assumption. Don't start new implementation work without being asked.
Kernel-side packages in `internal/` and the `pkg/` SDK are real, tested Go. Two `cmd/` binaries exist: `agent` (the kernel) and `providerconform` (a CLI driving `pkg/model/modeltest` against a built provider binary). **No plugin ships from this repository** — every model, tool, and frontend provider lives in its own repo under the same org and is consumed through `pkg/`, exactly as a third-party plugin would be. Most plugin categories still exist only as spec. Implementation is spec-first: before writing code, confirm the relevant spec exists and is settled; if it has open questions bearing on the task, raise them instead of coding against an assumption. Don't start new implementation work without being asked.

The terminal shell's design — region layout, focus model, keymap layers, and the TTY-ownership constraint that follows from a frontend being a go-plugin subprocess — is [`docs/first-party/frontends/tui.md`](docs/first-party/frontends/tui.md). It is descriptive, not normative: the protocol deliberately leaves focus, keybindings, resize, and scrollback to each frontend.
The reference terminal shell is **not in this repository**. It lives at [`pluggableharness/plugin-frontend-tui`](https://github.com/pluggableharness/plugin-frontend-tui), versioned and released on its own cadence like any other plugin, and it is where a frontend's layout, focus model, keymap, and TTY-ownership decisions are documented. None of that is protocol: the spec deliberately leaves focus, keybindings, resize, and scrollback to each frontend, so a change there is not a change here.

## Toolchain, testing, and CI

Expand Down
19 changes: 19 additions & 0 deletions api/pluggableharness/content/v1/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,25 @@ message ImageBlock {

// The image's MIME type, e.g. "image/png".
string media_type = 2;

// How much detail the model should spend on this image, where the
// vendor exposes the choice.
//
// It is a cost control, not a rendering hint: vendors bill high-detail
// image input at a multiple of low, so a caller sending many
// screenshots for a coarse question has a real reason to say so.
// UNSPECIFIED leaves the vendor's own default.
ImageDetail detail = 3;
}

// ImageDetail names how much resolution a model should spend on an image.
enum ImageDetail {
// The vendor's own default applies.
IMAGE_DETAIL_UNSPECIFIED = 0;
// Prefer fewer tokens over fidelity.
IMAGE_DETAIL_LOW = 1;
// Prefer fidelity over token cost.
IMAGE_DETAIL_HIGH = 2;
}

// ThinkingBlock is the model's extended-reasoning output, when
Expand Down
Loading