Documentation
¶
Overview ¶
Package relay wires the agenthooks runtime to the Gram hooks backend. It builds an agenthooks.Runner whose handlers translate every coding-agent hook event into the canonical Gram ingest contract, POST it to the authenticated /rpc/hooks.ingest endpoint, and honor the server's allow/deny verdict. The server remains the sole authority on blocking; this package only relays events and enforces the returned decision.
Index ¶
- Constants
- Variables
- func NewRunner(cfg Config) *agenthooks.Runner
- func RunDrain(ctx context.Context, out io.Writer) int
- func RunSkillFeedbackMCP(ctx context.Context, cfg Config) error
- func RunSkillUpload(ctx context.Context, args []string, credential io.Reader) int
- func WritePlugin(ctx context.Context, provider, dir string, cfg PluginConfig) error
- type Config
- type DrainSummary
- type FileConfig
- type PluginConfig
- type Relay
Constants ¶
const DefaultServerURL = "https://app.getgram.ai"
DefaultServerURL is the production Gram endpoint used when no override is configured in the environment.
Variables ¶
var BinaryVersion = "dev"
BinaryVersion is stamped by the main package with the release version so device telemetry can attribute behavior to the exact binary build.
Functions ¶
func NewRunner ¶
func NewRunner(cfg Config) *agenthooks.Runner
NewRunner constructs the agenthooks Runner: gating events (prompt.submitted, tool.requested) POST synchronously and honor deny; every other event is relayed as fire-and-forget telemetry. Handler failures fail open — a broken hook must never wedge the agent — and the credential ratchet governs the unauthenticated case.
func RunDrain ¶
RunDrain is the `speakeasy-hooks drain` entrypoint. Exit 1 only when the control plane proved unreachable mid-run — the one outcome a retry fixes — so a supervising caller (the device agent's recovery trigger) logs it as incomplete. Skipped-only leftovers (no credential, newer entry schema) exit 0: retrying can't deliver them until the machine's state changes, and a permanent exit-1 would train operators to ignore the signal. The counts on stdout carry the detail either way.
func RunSkillFeedbackMCP ¶
RunSkillFeedbackMCP serves the speakeasy-skill-feedback MCP server over stdio. Generated plugin packages point their .mcp.json at this subcommand through the bootstrap script, so the agent-facing feedback tool authenticates with the same credential chain as hook ingestion instead of a key baked into the MCP config.
func RunSkillUpload ¶
RunSkillUpload reads one bounded credential, reopens one manifest from a trusted parent-supplied root, and uploads it only if its full raw hash still matches the accepted activation.
func WritePlugin ¶
func WritePlugin(ctx context.Context, provider, dir string, cfg PluginConfig) error
WritePlugin renders a provider hook package under dir that drives the speakeasy-hooks binary. provider is the agenthooks slug (claude-code, cursor, codex, opencode). For claude-code and cursor, dir is a plugin directory; for codex, which has no plugin layout for hooks, dir is the Codex home the config installs into; for opencode, dir receives an .opencode/plugin shim usable either as a project directory or referenced from an OpenCode config's plugin list.
Types ¶
type Config ¶
type Config struct {
// ServerURL is the Gram API base, e.g. https://app.getgram.ai.
ServerURL string
// SiteURL is the dashboard origin the browser sign-in opens. Production
// serves the API and dashboard from one domain, so this stays empty and
// ServerURL is used; local development splits them across ports and needs
// the override.
SiteURL string
// ProjectSlug routes events to a project via the Gram-Project header.
ProjectSlug string
// OrgID scopes the cached credential so a key minted for another org can
// never authenticate this plugin (shared "default" slugs would otherwise
// cross project boundaries).
OrgID string
// HooksAPIKey is the shared org-wide hooks key baked into a published
// plugin. It is a fallback behind explicit and cached per-user keys and is
// never persisted locally.
HooksAPIKey string
// BrowserLogin controls whether fresh machines may mint a per-user hooks
// key through the dashboard. Published plugins default this off and use the
// shared org key unless the organization opts in.
BrowserLogin bool
// Nonblocking is the legacy observability-mode flag still baked into
// plugins published before the feature was removed. It is honored only as
// the fail-open posture (unreachable/5xx allow); explicit deny decisions
// and credential failures enforce regardless. New plugins never set it.
Nonblocking bool
// DebugLog, when set, appends one diagnostic line per event. It travels as a
// command flag so it survives providers that scrub the hook environment.
DebugLog string
// ConfigPath records the speakeasy.json the config was loaded from, so the
// login nudge can point the sign-in command at the same deployment identity
// instead of the production defaults.
ConfigPath string
// ConfigError records a failure to read an explicitly referenced
// speakeasy.json. The deployment identity is unknown, so events must not
// fall back to the default server where a cached key could route them to
// another workspace.
ConfigError string
}
Config holds the deployment identity baked into a generated plugin. Every field has an environment override so a single binary can serve any project/org without recompilation, mirroring the env knobs the legacy bash senders read.
func LoadConfig ¶
LoadConfig resolves the effective config from the environment, layering the GRAM_HOOKS_* overrides over the provided defaults.
func SplitInlineFlags ¶
SplitInlineFlags extracts the deployment flags from a hook command's argv and returns the config they carry plus the remaining args to hand to agenthooks. The install packaging bakes only --config=<path>: deployment values must never ride argv because providers parse and filter hook commands (Cursor drops the whole hooks.json over a scheme:// URL, agenthooks quirk #29) and Codex trust-hashes the command definition. The remaining flags are manual overrides layered on top of the file.
type DrainSummary ¶
type DrainSummary struct {
// Replayed entries were accepted by the server and deleted.
Replayed int
// Dropped entries were deleted without acceptance: the server rejected
// them definitively (4xx — a replay would fail identically), or the
// file was corrupt and could never replay.
Dropped int
// Expired entries aged past spoolMaxAge and were deleted unsent.
Expired int
// Skipped entries were left in place for a later run: a newer schema
// version this binary doesn't understand, or a deployment we currently
// hold no credential for.
Skipped int
// Remaining is the entry count left on disk when the run ended.
Remaining int
// Aborted is true when the control plane proved unreachable mid-drain;
// the rest of the backlog is kept rather than hammered.
Aborted bool
}
DrainSummary reports one drain run's outcome.
func Drain ¶
func Drain(ctx context.Context) DrainSummary
Drain replays the spool under the drain lock. Concurrent drains serialize; on Windows, where the lock is a no-op, a racing double-send is harmless because both carry the same Idempotency-Key.
type FileConfig ¶
type FileConfig struct {
ServerURL string `json:"server_url"`
SiteURL string `json:"site_url,omitempty"`
Project string `json:"project"`
Org string `json:"org,omitempty"`
HooksAPIKey string `json:"hooks_api_key,omitempty"`
BrowserLogin bool `json:"browser_login,omitempty"`
Nonblocking bool `json:"nonblocking,omitempty"`
}
FileConfig is the on-disk schema of a plugin's speakeasy.json. Evolution is strictly additive: readers ignore unknown keys and default missing ones, so a binary and a config file can skew in either direction. Never rename or repurpose a key.
type PluginConfig ¶
type PluginConfig struct {
ServerURL string
SiteURL string
ProjectSlug string
OrgID string
HooksAPIKey string
BrowserLogin bool
BinaryPath string
}
PluginConfig carries the deployment identity written into a provider package.