Interop layer

NGL & interfaces

NGL (Native Glider Language) is the internal/ngl package. It contains a small extension point for each vendor. These extension points let claude, cursor-agent and agy operate with the shared code of Glider. The shared code never makes a decision from the name of a vendor. There are three of them: two Go interfaces, and one family of functions with a shared name pattern. Each one answers a different question, and the data moves in a different direction. This page shows the difference between them. To add a fourth CLI, you must know that difference.

Why there are three extension points

Each one answers a different question. The data moves in a different direction, and at a different point in the pipeline:

Extension pointThe questionThe data sourceThe direction
OriginAdapter Is this HTTP request the live traffic of a vendor CLI to its own backend? If Glider must reply as that backend, what is the format? A live HTTP request that Glider intercepted with MITM or with the gateway Into Glider, from the network
DelegateRenderer What text did a delegate run write? Remove all data except the final answer. The recorded stdout of a completed subprocess Out of Glider, from the subprocess to the chat reply
The ParseXTurn functions
(ParseClaudeTurn, ParseCursorAgentTurn, ParseAgyTurn)
What is the structure of one line of the stream-json output of a vendor? Is it text, a tool call, a tool result or reasoning? One event line of the stream output of a vendor Out of the subprocess, into a Turn and its Part items

There is a fourth interface one layer below, in internal/vendors. VendorAdapter controls what occurs when Glider runs a CLI. It finds the refusals, gets the session ID, gives a limited permission for the next run with ExtraResumeArgs or GrantResumePermission, reads the edits, and writes the resume prompt. It does not read a message format. Refer to the section below for the full list of methods, and to Delegation for the relay procedure. Do not confuse it with the three extension points above. It is a different boundary, because it controls the execution of a process and not a message format.

Turn and Part — the shared vocabulary

Each ParseXTurn function makes the same structure. turn.go defines it one time:

type Turn struct {
    Vendor string          // "claude" | "cursor-agent" | "agy" | a future vendor
    Raw    json.RawMessage // the native event of the adapter, with no changes
    Parts  []Part
}

Glider always keeps Raw. The Parts data is additional, and it does not replace Raw. Therefore Glider can always send a Turn back to the same vendor with no changes. Parts gives the type of each item: PartUserText, PartToolCall, PartToolResult, PartReasoning and PartOther. This layer does not change a name. The Name and Args of a ToolCall keep the words of the vendor. The alias table of a vendor pack changes the names for a caller that needs the same names for each vendor.

OriginAdapter — how Glider identifies the traffic of a CLI

Glider got this interface on 2026-07-27, after a live defect. The delegate handler in internal/mitm used r.URL.Path != "/v1/messages" as its entry test. That test is correct only when Claude Code is the front CLI. If you typed a delegate flag in the session of cursor-agent or agy, nothing occurred. The traffic of those CLIs does not use the Anthropic Messages format. Therefore the test refused the request before the flag parser operated.

type OriginAdapter interface {
    Vendor() string
    Matches(r *http.Request) bool
    ExtractUserInstruction(body []byte) (text, model string, stream, ok bool, err error)
    // header is written immediately, before replyText resolves — keeps a
    // slow delegate call (headless run of another vendor's CLI) from
    // idling the client out, and names what was delegated to whom.
    WriteReply(w http.ResponseWriter, model string, stream bool, header string, replyText <-chan string) error
}

internal/mitm/delegate_handler.go calls ngl.ResolveOriginAdapter(r). It never compares a host or a path with the name of a vendor. The Matches function of each adapter does that. In ExtractUserInstruction, ok=false is not an error. It means that the structure of the body is correct, but this adapter has no confirmed method to separate the human text from the data that the vendor adds. The caller must then send the request to the origin. The caller must not search the body for a substring. Glider used that method one time for Claude, before NGL existed, and it caused the injection defect that the package doc comment describes.

VendorThe message formatHow Glider knows
claudeThe Anthropic Messages API, POST /v1/messagesThe specification is public
agyREST in the Gemini format, POST .../v1internal:streamGenerateContent. The human text is inside <USER_REQUEST> tags.Glider recorded the live traffic and compared the bytes on 2026-07-27
cursor-agentConnect-RPC on HTTP/2, POST /agent.v1.AgentService/RunGlider recorded the live traffic. Each request field agrees with the public agent_v1.proto schema.

The completion host of cursor-agent always uses HTTP/2. A tool that expects HTTP/1.1 cannot see this traffic. The MITM proxy of Glider had this problem until 2026-07-28. Refer to MITM for the correction.

DelegateRenderer — a clean reply, not the full transcript

Glider got this interface on 2026-07-28. vendors.RunResult.Text holds the full recorded stdout of a delegate run. For claude and for cursor-agent, this is the complete stream-json NDJSON transcript. It contains each internal event line, and not only the answer. Glider asks for this format, because other code in the pipeline reads the refusals and the session ID from it. This full transcript is correct for a diagnostic task, but it is not correct for daily work.

type DelegateRenderer interface {
    Vendor() string
    Render(raw []byte) (clean string, ok bool)
}

The renderers for claude and cursor-agent read the NDJSON and look for the last {"type":"result","result":"..."} line. That line holds the final answer of the vendor, and Glider does not make the answer again. If there is more than one such line, Glider uses the last one. LastUserInstruction makes the same choice for the incoming data. The renderer for agy makes almost no change, because agy has no --output-format flag and its stdout is already normal text. ok=false means that there is no result event, or that the event is empty. The caller then uses the raw text. Glider does not try to find an answer that can be absent.

vendors.ResolveDelegate uses this interface, and clean is the default. One control on the Vendors page sets the format: Settings → Delegated task replies. There is no flag for one message, because this is an operator setting. The raw mode always shows the full transcript. The clean mode uses the raw text when a renderer gives ok=false, but it also adds a note. Therefore you can always see that the format is different.

VendorAdapter — what occurs when Glider runs a CLI

This interface is in internal/vendors/adapter.go, and not in internal/ngl. Each difference between the CLIs in the execution layer must go through this one interface. RunWithOptions and ResolveDelegate find a VendorAdapter by the vendor name. They do not know which CLI they use. To add a fourth vendor, or to change how a vendor finds a refusal or gives a permission, you write or change one adapter here. You must not make a decision from vendor.Name in the shared code.

type VendorAdapter interface {
    // nil means "no denials found" — every adapter implements this, even a trivial no-op.
    DetectDenials(stdout, stderr []byte) []Denial
    // "" means none available (no such id in this vendor's format, or extraction failed).
    ExtractSessionID(stdout []byte) string
    // Scoped side effect OUTSIDE the resume argv itself; revert is always called after the
    // resume attempt, success or failure. cwd is the resolved workspace directory the resume
    // will run in (may be ""). Most vendors return a no-op revert — per-denial scoping happens
    // through ExtraResumeArgs instead (see below).
    GrantResumePermission(v Vendor, cwd string, denials []Denial) (revert func() error, err error)
    // Extra CLI args to append to the vendor's "resume" CommandTemplate for this specific set
    // of denials, or nil if the vendor has no such mechanism.
    ExtraResumeArgs(denials []Denial) []string
    // ok=false for a run that made no edit, or whose headless output carries no structured diff.
    ExtractEditViews(stdout []byte) (views ngl.EditViews, ok bool)
    // Lets a vendor reframe the resume prompt for its model's known behavior on a resumed call;
    // identity (returns prompt unchanged) for vendors whose resume already reliably completes.
    WrapResumePrompt(prompt string) string
}

A permission must change a condition before Glider runs the CLI again. If it does not, the new run stops at the same permission. Each CLI has a different mechanism. Therefore the interface has two methods for one function. ExtraResumeArgs gives the command-line arguments for the refused tools. For example, claude makes --allowedTools <comma-joined tool names> from the refusals. Glider knows the tool name only after the refusal, and thus the name cannot be in a fixed template. GrantResumePermission makes a change outside the command line. For example, agy writes a limited rule in its settings.json, and Glider then puts the original bytes back. Each vendor uses the method that its CLI supports. The shared code calls the two methods and does not know which one operates. cursor-agent has no argument for one tool. It has only -f or --yolo, which permit each tool, and Glider does not use them automatically. Therefore its adapter gives nil from ExtraResumeArgs and makes no change in GrantResumePermission. --resume [chatId] --trust is sufficient.

ExtractEditViews shows the file differences through NGL. It reads the raw stdout of a completed run and makes a standard ngl.EditViews structure. This is possible only when the output of the vendor has sufficient structure. agy writes normal text only, because it has no --output-format flag. Therefore its adapter always gives ok=false. This is a true limit of the format, and Glider does not hide it. WrapResumePrompt changes the resume prompt for a vendor whose model gets the permission but then gives a description of the directory in place of the work. agy has this behavior today. This method decreases the problem, but it does not prevent it. It is different from the two permission methods.

For a vendor name that has no adapter, Glider uses noopAdapter. Each method of that adapter is safe and makes no change. Therefore a caller does not have to test for an adapter before it calls one. Only the vendorAdapters map in adapter.go contains the full list of the vendor names for execution.

How to add a fourth vendor

This is the function of the three extension points. To add a vendor, you write new files. You do not change the shared code. A complete addition has these parts:

These five parts do not need a change to internal/mitm/delegate_handler.go, to vendors.ResolveDelegate, or to any other shared code.

The full history and the live-capture method are in planning/agent_cli_interop.md. For the delegate flag, refer to Delegation.