Documentation
¶
Overview ¶
Package api wraps the Hadron GraphQL endpoint: a genqlient client for typed operations, a raw escape hatch for `hadron api`, and the mapping from transport/GraphQL errors to exit codes.
Index ¶
- Constants
- Variables
- func Capabilities(ctx context.Context, baseURL string) (map[string]bool, error)
- func CheckUpdateKinds(input *gen.UpdateNodeInput, cur NodeKindState) error
- func CollectAll[T any](fetch func(limit, offset int) ([]T, int, error)) ([]T, error)
- func CollectNodeBatch(refs []string, ...) ([]*gen.NodeBatchNodeBatchNodeBatchResultNodesNode, []string, error)
- func CollectPages[T any](pageSize int, fetch func(limit, offset int) ([]T, *int, error)) ([]T, error)
- func CollectUntil[T any](fetch func(limit, offset int) ([]T, int, error), done func(items []T) bool) ([]T, error)
- func CommentRecoveryDetails(err error) map[string]any
- func DescendantCount(err error) int
- func DocumentFromBatchNode(n *batchNode) *nodedoc.Document
- func DocumentHasMutation(doc string) bool
- func EndRefusedBeforeCommit(err error) bool
- func Endpoint(serverURL string) string
- func ExitCodeForExtension(code string) int
- func GovernedKindConflict(role *string, isRunnable bool) []string
- func HasErrorCode(err error, code string) bool
- func IsGraphQLValidationFor(err error, name string) bool
- func IsMCPOnlyCredential(err error) bool
- func IsUnknownGraphQLArgument(err error, argument, field string) bool
- func ListNotAdvancing(offset int) error
- func MapError(err error) error
- func MapWorkerReportingError(err error) error
- func NewClient(serverURL, token string, httpClient *http.Client) (graphql.Client, error)
- func PageNodes(pageSize int, fetch func(limit, offset int) ([]*ListNode, error), ...) error
- func RequireSecureURL(serverURL, token string) error
- func RoleInFamily(role *string, base string) bool
- func ScopeRefusalRemedy(kind ScopeRefusal) string
- func ServerMessage(err error) string
- func ServerMessages(err error) []string
- func ValidateOperationKey(key string) error
- func WithOperationKey(ctx context.Context, key string) context.Context
- func WithSession(ctx context.Context, sessionID string) context.Context
- func WorkerReportingDetails(err error) map[string]any
- type AuthoredNode
- func CreateNodeByKind(ctx context.Context, client graphql.Client, input *gen.CreateNodeInput) (*AuthoredNode, error)
- func CreateReviewNode(ctx context.Context, client graphql.Client, input *gen.CreateNodeInput) (*AuthoredNode, error)
- func CreateSpecNode(ctx context.Context, client graphql.Client, input *gen.CreateNodeInput) (*AuthoredNode, error)
- func UpdateNodeByKind(ctx context.Context, client graphql.Client, input *gen.UpdateNodeInput, ...) (*AuthoredNode, error)
- func UpdateNodeClearingRoleByKind(ctx context.Context, client graphql.Client, input *gen.UpdateNodeInput, ...) (*AuthoredNode, error)
- func UpdateSpecNode(ctx context.Context, client graphql.Client, input *gen.UpdateNodeInput) (*AuthoredNode, error)
- type FindNodesPage
- type HeldDetail
- type HoldStaleDetail
- type ListNode
- type NodeKindState
- type RawResult
- type ResponseWarning
- type ScopeRefusal
- type SearchHit
- type SearchNode
- type SearchPage
- type SearchScope
- type TakenDetail
- type WarningRegister
- type WarningSink
Constants ¶
const EnvAllowHTTP = urlsec.EnvAllowHTTP
EnvAllowHTTP opts out of the HTTPS-enforcement guard for a trusted local or self-hosted server (set to "1").
const MCPOnlyRemedy = "This key was issued for MCP clients only, and the CLI needs one with the `account` scope. " +
"Sign in again with `hadron auth logout && hadron auth login` (hadron v0.15.0 or later requests `account`), " +
"or create a key on the portal's API keys page (/app/account/api-keys) and run `hadron auth login --with-token` with it. " +
"If the key comes from HADRON_TOKEN, replace that variable instead."
MCPOnlyRemedy is appended wherever the CLI meets an MCP-only key. It names both ways back, because either may be the one that works: the browser login only helps from a CLI that requests `account` (v0.15.0+), and a portal key is the one route that needs no CLI upgrade.
const NodeBatchCap = 200
NodeBatchCap mirrors hadron-server's BATCH_READ_MAX_NODES (cor:api:040): a single nodeBatch call accepts at most 200 refs and fails loud above that, so bulk reads fan out in fixed-size chunks. The server also enforces a ~1 MB response cap that can return a partial page (truncated=true) with the spillover in `omitted`; CollectNodeBatch re-requests those.
const OperationKeyHeader = "X-Hadron-Operation-Key"
OperationKeyHeader supplies a memory data key for one App operation (x-hadron-operation-key, hadron-server#1817, the first #1796 slice). cor:sec:040:04 — "a key is never a field on a query or a mutation" — is the rule it implements: a key in GraphQL variables travels in the request body, where any body logging, APM capture or proxy log would hold key material. The server stages the header request-scoped (outside GraphQL Context), authenticates before parsing, and consumes it exactly once; installing it as a persistent client default is refused by design, so it must ride per call.
const PageLimit = 200
PageLimit is the server's hard cap on a single page of the uniform paginated list queries (hadron-server#473: cap 200; the server's own default is not relied on, cli#967).
const ReviewNodeRole = "review"
ReviewNodeRole is the root of the governed review role family. #1201 put `review` in the register for a SECURITY reason rather than a tidiness one: a review may be a safety check, so being able to remove one silently is the attack.
const SessionHeader = "X-Hadron-Session"
SessionHeader binds a request to a worker session (hadron-server#934, amending cor:aut:020:02). The server honors it only for a session the caller's own principal started and that is still active, and treats it as ATTRIBUTION, never a credential: it cannot widen what the caller may do.
const SpecNodeRole = "spec"
SpecNodeRole is the root of the governed spec role family. The server also governs its dotted descendants; the role is what the gate reads.
const UnsupportedScopeRemedy = "Sign in again with `hadron auth logout && hadron auth login`, " +
"or create a key on the portal's API keys page (/app/account/api-keys) and run `hadron auth login --with-token` with it. " +
"If the key comes from HADRON_TOKEN, replace that variable instead."
UnsupportedScopeRemedy is appended for a key whose OAuth grant carries a scope this server does not support: it is refused everywhere, so the only fix is a new credential (server#1306).
Variables ¶
var ErrRedirectPolicy = errors.New("redirect refused by policy")
ErrRedirectPolicy marks a refusal WE made about a redirect, as opposed to a network failure. net/http wraps a CheckRedirect error in *url.Error, which satisfies net.Error — so without this sentinel the transport classifier (#394) would read our own HTTPS-downgrade refusal as a lost request and report it as retryable exit 7. The server answered; retrying cannot help.
Functions ¶
func Capabilities ¶ added in v0.24.0
Capabilities reads GET /capabilities — hadron-server#1925, mounted by #1930: which deployment switches are ON, readable before sign-in. The answer is `{"capabilities":[...]}` with ruled token words ("node-signing", "passkey-registration"); an off switch is OMITTED, never false, so a deployment with the switches off reads exactly like a build without the features. Unauthenticated, ETag-cached, rate-limited, and OUTSIDE GraphQL.
Within one process the response is cached per server for its stated max-age; past the window it is revalidated with If-None-Match, where a 304 means unchanged (and renews the window). Callers treat every failure — non-200 (an older server's plain 404 is the expected one), transport error, unparsable body — as "no capabilities": no caller may pay a refused follow-up round trip (e.g. a signature query an older server would reject) just because discovery failed.
func CheckUpdateKinds ¶ added in v0.17.0
func CheckUpdateKinds(input *gen.UpdateNodeInput, cur NodeKindState) error
CheckUpdateKinds is UpdateNodeByKind's refusal without the write, for a caller that must know before it reports a plan or prompts (node import's --dry-run and overwrite prompt): nil when some door can make the update.
func CollectAll ¶ added in v0.5.1
CollectAll drains a uniform paginated { items, total } list query (hadron-server#473) to exhaustion, so "whole scope" commands never silently truncate at one server page. fetch receives (limit, offset) and returns one page's items plus the envelope total.
func CollectNodeBatch ¶ added in v0.2.0
func CollectNodeBatch( refs []string, fetch func([]string) (*gen.NodeBatchNodeBatchNodeBatchResult, error), ) ([]*gen.NodeBatchNodeBatchNodeBatchResultNodesNode, []string, error)
CollectNodeBatch fetches full nodes for refs in cap-sized chunks, re-queuing the spillover the server drops under its response-size cap. A ref is a node PK or a fully-qualified node URN (hadron-server#813) — `omitted` and `unavailable` come back as the refs were sent, so re-queuing and reporting both stay in the caller's own vocabulary. fetch is injected rather than calling gen.NodeBatch directly so the chunking/truncation loop is unit-testable without a server and so each caller controls the field projection. Returns the nodes (input order is not preserved across chunks), the union of refs the server reported unavailable, and the first error.
Shared by the whole-corpus fan-outs (`memory export`, `spec get --prefix`).
func CollectPages ¶ added in v0.27.0
func CollectPages[T any](pageSize int, fetch func(limit, offset int) ([]T, *int, error)) ([]T, error)
CollectPages drains a list whose total may be unknown (findNodes returns a nullable total; sessions and revisions return none). fetch receives (limit, offset) and returns one page plus the total when the server gave one. It stops only on an empty page or when a known total is reached, and advances by the rows actually received, so a server that clamps pages below pageSize can neither end the scan early nor skip rows (cor:api:120:04). A fetch error on any page fails the whole collection.
func CollectUntil ¶ added in v0.5.1
func CollectUntil[T any](fetch func(limit, offset int) ([]T, int, error), done func(items []T) bool) ([]T, error)
CollectUntil is CollectAll with an early exit: after each page, done receives the items accumulated so far, and returning true stops paging (the accumulated items are returned as-is). Use it when a caller can tell mid-scan that it already has what it needs — e.g. an exact match in a sorted list — without draining the remaining pages.
func CommentRecoveryDetails ¶ added in v0.21.0
CommentRecoveryDetails returns the bounded recovery fields published by comment refusals. Keep unrelated or arbitrary extensions out of curated output.
func DescendantCount ¶ added in v0.8.0
DescendantCount returns the descendant count carried by a NODE_HAS_DESCENDANTS error (server #661: its extensions.count), or -1 when err is not that error or carries no non-negative numeric count. JSON numbers decode to float64, but a few other numeric shapes are tolerated. A negative or non-numeric value is treated as "no count" (-1) so callers never render a nonsensical "-N descendant(s)". Call it BEFORE MapError wraps the error.
func DocumentFromBatchNode ¶ added in v0.3.0
DocumentFromBatchNode maps a bulk-read node into the neutral nodedoc.Document the markdown/JSON codecs consume: `memory export`'s. `node export` does not use it: the server renders that file with its own nodeExport, which does not yet emit role or isRunnable (cli#714). Type "" carries the server default (info), so it never serializes and a re-import defaults correctly. The projection only carries the memory id, not the URN, so callers that emit a standalone file resolve and set Document.MemoryURN themselves.
func DocumentHasMutation ¶ added in v0.9.0
DocumentHasMutation reports whether a GraphQL document contains a mutation operation, so `hadron api` can warn about idempotency only when a write was actually at stake.
String literals are skipped so a query selecting a field whose *value* mentions mutations is not misread. Beyond that the check is deliberately generous — a false positive prints a warning that is merely unnecessary, while a false negative withholds the one sentence that prevents a double-applied write. It errs toward warning.
func EndRefusedBeforeCommit ¶ added in v0.13.0
EndRefusedBeforeCommit reports whether an endSession failure PROVABLY changed nothing — the only case in which a client may tell its caller that a handoff was definitely not recorded.
It is deliberately ONE code, and not a taxonomy. `cor:agt:020:10` guarantees that a failed handoff write REFUSES the end, so `HANDOFF_WRITE_FAILED` is a SPEC-BACKED statement that nothing committed. Every other outcome is treated as unknown, including other GraphQL errors, because there is no way from here to tell a pre-commit refusal from a post-commit one:
- GraphQL returns `data` AND `errors` when a nested resolver fails after the mutation ran, and
- null-bubbling from a non-null child NULLS `data.endSession` even though the write happened,
so neither the presence of an error nor the absence of a payload proves a refusal (PR #528 review, Codex, twice).
The asymmetry is the whole design. Saying "may not have been recorded" when it definitely was not is harmless — the remedy is identical, since one stint records one handoff. Saying "was NOT recorded" when it WAS is the worst sentence this command can print, and it would make a later retry failure look like confirmation of data loss. So the definite wording is earned by a guarantee, not inferred from a shape.
Call it BEFORE MapError wraps.
func ExitCodeForExtension ¶ added in v0.18.0
ExitCodeForExtension also classifies typed refusal codes carried in a successful transfer preview's blockers, where there is no GraphQL error for MapError to inspect. The preview and apply paths must exit consistently.
func GovernedKindConflict ¶ added in v0.15.0
GovernedKindConflict reports the governed kinds a write would produce when there is MORE THAN ONE, and nil otherwise.
No door can write such a node. Each is exempt from its OWN kind only and stays fully subject to the others, so `isRunnable: true` + `role: "spec"` is refused by `createTaskNode` (for the role) and by `createSpecNode` (for the capability) alike. The server's refusal names one of them, which makes it actively misleading: it sends the caller to a door that will also refuse.
THIS IS ROUTING, NOT VALIDATION, and the distinction is the one @Ada drew on #615 — the CLI must not refuse a role VALUE, because the register is a server-side closed list that changes without this client knowing. It says nothing here about whether a value is governed in general; it reports that the mapping this client holds, which it needs anyway to pick a mutation, yields two doors and therefore none.
The degradation is safe and loud: if the server later un-governs a kind, this refuses a write that would now succeed — a visible Usage error naming `hadron api` as the way through, rather than a silent wrong result.
func HasErrorCode ¶ added in v0.5.1
HasErrorCode reports whether err carries a GraphQL error whose extensions.code equals code, including a non-200 HTTPError with a parsed GraphQL response. It also follows MapError's wrapping, though callers that need a special recovery path should branch before mapping the error — e.g. a guarded spec edit keeping its proposal on NODE_WRITE_CONFLICT.
func IsGraphQLValidationFor ¶ added in v0.18.0
IsGraphQLValidationFor reports a schema-validation refusal naming one field or argument. It reads the parsed GraphQL envelope, including the one inside an HTTPError, rather than matching HTTPError.Error's JSON-escaped rendering. Callers use it only when retrying a compatible operation is safe.
func IsMCPOnlyCredential ¶ added in v0.16.0
IsMCPOnlyCredential reports whether err is the server refusing an MCP-only key (#681). Call it on the RAW error, before MapError wraps it.
func IsUnknownGraphQLArgument ¶ added in v0.20.0
IsUnknownGraphQLArgument permits a compatibility retry only when every parsed error is the specific schema refusal, never a mixed business failure. #817/server#1633 pin gqlgen's unknown-argument wording; Diego also observed that exact envelope on production during the #819 blind review. A wording change fails closed rather than silently dropping session attribution.
func ListNotAdvancing ¶ added in v0.27.0
ListNotAdvancing is the error returned when a non-empty page brings nothing the scan has not already read: the server is not honouring the offset. The list cannot be certified complete, so this is a failed read, never a successful partial.
func MapError ¶
MapError converts transport and GraphQL errors into CodedErrors so the root command can derive the documented exit code. Codes come from hadron-server's Apollo resolvers (extensions.code).
It is also the ONE place the server's refusal is rendered for a human (#566). Every `exitcode.New` below wraps through `cleaned`, so the message that reaches `hadron: %s` — and the `--json` error envelope, which renders the same string — is what the server said, with none of genqlient's document-location scaffolding around it.
func MapWorkerReportingError ¶ added in v0.21.0
func NewClient ¶
NewClient returns a genqlient client for the given server, authenticating with token (may be empty for anonymous calls).
func PageNodes ¶ added in v0.27.0
func PageNodes(pageSize int, fetch func(limit, offset int) ([]*ListNode, error), visit func(*ListNode) bool) error
PageNodes drains a findNodes-shaped listing, calling visit once per node until visit returns false or the list is exhausted. fetch receives (limit, offset).
It stops only on an EMPTY page: the server may serve fewer rows than asked for (a clamp, a concurrent delete) and findNodes.total is nullable, so a short page proves nothing (cli#967, cor:api:120:04). It advances by the rows actually received, visits each node id once, and FAILS when a non-empty page brings nothing new (a server ignoring the offset) rather than looping or reporting a partial list as complete. A fetch error on any page is returned.
func RequireSecureURL ¶ added in v0.6.0
RequireSecureURL refuses to transmit the bearer token over a non-https server URL — cleartext credentials are trivially captured by an on-path attacker on the shared CI/dev machines this CLI runs on (#114). Carve-outs: a loopback host (local dev, incl. the test httptest servers) and HADRON_ALLOW_HTTP=1 (a trusted self-hosted backend). An empty token means no credential rides, so the check is a no-op — anonymous http is allowed.
func RoleInFamily ¶ added in v0.18.0
RoleInFamily matches the server's governed role boundary: the base role or one of its dotted descendants, never an ordinary prefix such as "special".
func ScopeRefusalRemedy ¶ added in v0.16.0
func ScopeRefusalRemedy(kind ScopeRefusal) string
ScopeRefusalRemedy is the recovery the CLI appends for a scope refusal.
func ServerMessage ¶ added in v0.14.0
ServerMessage returns the FIRST server message in err, or "" when there is none. For a caller rendering one line about one failure; MapError joins the whole list instead, because dropping the others would hide them.
func ServerMessages ¶ added in v0.14.0
ServerMessages returns the server's own message for each GraphQL error in err, with genqlient's location/path rendering left off. Empty when err carries no GraphQL errors at all — a transport failure or a raw HTTP body — which is the caller's signal to fall back to err.Error().
func ValidateOperationKey ¶ added in v0.21.0
ValidateOperationKey refuses everything the server's operation-key parser would refuse — so a malformed key is a LOCAL usage error before any request, not a BAD_UNLOCK_INPUT after a round-trip. Mirrors the server exactly: charset and length first, then the decode-and-reencode round-trip that rejects non-canonical spellings of valid bytes (the decoder silently drops the final character's low padding bits; re-encoding does not reproduce them). The value itself is never echoed back.
func WithOperationKey ¶ added in v0.21.0
WithOperationKey marks the GraphQL request made with ctx as carrying the memory data key in the OperationKeyHeader. Like WithSession it is deliberately per-CALL, never a client default: the key rides exactly the one mutation that needs it, and every other request a command makes — before or after — goes out without it. An empty key leaves ctx unchanged.
func WithSession ¶ added in v0.18.0
WithSession marks every GraphQL request made with ctx as driven by the worker session id. It is deliberately per-CALL, never a client default: the header changes server behaviour (a heartbeat, and under hadron-server#1353 the bound worker's own read state), so it rides only on the operations that ask for that, never every request a bound worktree makes. Chat reads use explicit advanceReadState:false (#817/server#1628); only the post-delivery mark may acknowledge what the CLI actually showed. Authored node/edge writes opt in through the command-local #821 hook; queries and maintenance operations keep their existing contexts. An empty id leaves ctx unchanged.
func WorkerReportingDetails ¶ added in v0.21.0
WorkerReportingDetails preserves the server's refusal code without forwarding arbitrary extension payloads. The numeric CLI exit code remains separate.
Types ¶
type AuthoredNode ¶ added in v0.14.0
type AuthoredNode struct {
Id string
MemoryId string
Loc string
Name string
NodeType string
Tags []string
Seq *int
IsRunnable *bool
Role *string
UpdatedAt string
}
AuthoredNode is the node an authoring write returns.
A CONCRETE struct rather than an alias onto one door's generated type, which is what it used to be. Six doors plus the two generic surfaces return eight structurally similar but DISTINCT genqlient types, and Go will not convert between them — so aliasing one of them forced every dispatcher to pick a winner and cast, which does not compile and should not. Mapping once here keeps the call sites off genqlient shapes, the same reason the command packages marshal their own DTOs.
func CreateNodeByKind ¶ added in v0.14.0
func CreateNodeByKind(ctx context.Context, client graphql.Client, input *gen.CreateNodeInput) (*AuthoredNode, error)
CreateNodeByKind writes a node through the door its KIND requires, or through the generic `createNode` when it is of no governed kind.
It exists because the gate is a property of the NODE, not of the command: any caller that can set `role` or `isRunnable` can produce a governed node, and `hadron node add --runnable` does exactly that. Before this, that command sent a runnable node to the generic surface and was refused — a live break wider than the authoring commands, found by @codex on #614 and reproduced against the server before being believed.
func CreateReviewNode ¶ added in v0.14.0
func CreateReviewNode(ctx context.Context, client graphql.Client, input *gen.CreateNodeInput) (*AuthoredNode, error)
CreateReviewNode writes a review check through the review door. As with CreateSpecNode, the caller sets `input.Role`.
func CreateSpecNode ¶ added in v0.14.0
func CreateSpecNode(ctx context.Context, client graphql.Client, input *gen.CreateNodeInput) (*AuthoredNode, error)
CreateSpecNode writes a spec node through the spec door.
The caller must set `input.Role` in the spec family — this wrapper does not do it, and that is on purpose: for a create the gate reads the state being written, so a node's kind is a property of the node being written, not of the function called to write it. Setting the role here would let a call site that forgot to think about the kind still produce a governed node, which is the wrong direction for a signal whose whole job is to be explicit.
func UpdateNodeByKind ¶ added in v0.14.0
func UpdateNodeByKind(ctx context.Context, client graphql.Client, input *gen.UpdateNodeInput, cur NodeKindState) (*AuthoredNode, error)
UpdateNodeByKind edits a node through the door that owns every governed kind the write TOUCHES — the kind it is now and the kind it will be.
THE SERVER GATE IS BEFORE ∪ AFTER, and that is the whole difficulty (hadron-server governedRole.ts, assertGovernedWrite). Two consequences:
- An update that touches neither `role` nor `isRunnable` still edits a governed node when the stored one is governed, so a plain `--description` edit of a review check or a task needs that kind's door. Hence the node's current kind is an argument: an omitted field preserves the stored value.
- REMOVING a kind needs that kind's door too. `isRunnable: false` on a task or a role change away from `spec` leaves an ungoverned node, but the generic surface refuses it ("removes"): the gate exists so the generic surface cannot quietly un-govern a node. Routing on the resulting state alone sent exactly those writes to updateNode (cli#714).
A write that touches two governed kinds — a task given `role: spec`, or a spec turned into a task — has no door, because each door is exempt from its OWN kind only. It is refused here as a Usage error rather than sent to be refused. WHO may use a door is hadron-server#1202, not this client's call.
func UpdateNodeClearingRoleByKind ¶ added in v0.25.0
func UpdateNodeClearingRoleByKind(ctx context.Context, client graphql.Client, input *gen.UpdateNodeInput, cur NodeKindState) (*AuthoredNode, error)
UpdateNodeClearingRoleByKind applies one role-null update, with all other explicit edits in the SAME mutation. A nil Role on the ordinary input means preserve, so use an empty role only in this local copy for routing the resulting ungoverned state; it is never sent on the wire. Server refusal is final: do not retry via an ordinary or generic writer.
func UpdateSpecNode ¶ added in v0.14.0
func UpdateSpecNode(ctx context.Context, client graphql.Client, input *gen.UpdateNodeInput) (*AuthoredNode, error)
UpdateSpecNode edits a spec node through the spec door — the counterpart the create half had to wait for (hadron-server #1192, then folded into #1201).
It is needed even when an edit touches neither `role` nor `isRunnable`, because the gate reads the kind the node is now as well as the kind it will be: editing a node that already carries `role: "spec.rule"` touches the spec kind, so the generic `updateNode` refuses it.
It keeps `updateNode`'s selectors and, crucially, its OMIT-TO-PRESERVE semantics — which is why routing edits through the create door with `upsert: true` was never the workaround it looked like: `CreateNodeInput` has no omit-to-preserve, and `spec edit` depends on it.
type FindNodesPage ¶ added in v0.5.0
FindNodesPage is the flattened result of one findNodes call: the hit nodes (hits[].node hoisted to a bare slice, the shape every old `nodes`/`nodeSearch` caller expects), plus the envelope's total and the degraded/reason notes that `spec find` surfaces on a vector-less memory. Nodes is always non-nil.
func FindNodes ¶ added in v0.5.0
func FindNodes( ctx context.Context, client graphql.Client, query *string, mode *gen.FindNodesMode, filter *gen.NodeFilter, sort *gen.NodeSort, sortProperty *gqltypes.NodePropertySort, limit, offset *int, ) (*FindNodesPage, error)
FindNodes runs the unified node search/list (cor:api:090) and flattens the hits[].node envelope into a bare node slice. Omit query for a filtered list in deterministic order (the old `nodes` semantics); pass query + mode to rank (the old `nodeSearch`). sortProperty orders by a properties/data JSON path and overrides sort when set (#719). All args are optional; nil pointers are omitted from the wire so the server applies no constraint. The caller maps GraphQL errors through MapError as usual.
func FindNodesProjected ¶ added in v0.14.0
func FindNodesProjected( ctx context.Context, client graphql.Client, query *string, mode *gen.FindNodesMode, filter *gen.NodeFilter, sort *gen.NodeSort, sortProperty *gqltypes.NodePropertySort, limit, offset *int, withProperties, withData bool, ) (*FindNodesPage, error)
FindNodesProjected is FindNodes with the two JSONB columns opt-in (#602). Pass withProperties / withData to select `properties` / `data` on the hit nodes; each is independent, so a caller that wants only the data envelope does not pay for properties. Every other caller should use FindNodes, whose projection is deliberately thin — a listing is an index.
The columns are separate arguments rather than one flag because they are separate payloads: this is the read half of `--where`'s `field` key, which names exactly these two columns, and the symmetry is the point (#602).
type HeldDetail ¶ added in v0.11.0
HeldDetail is the payload a WORKER_HELD error carries (hadron-server#1050): whose name it is, and since when.
TWO server paths raise this code and they do NOT carry the same fields. The pre-transaction check resolves the holder and sends workerId, heldBy, heldByName and heldAt; the compare-and-set inside the session-creating transaction — the one that refuses the loser of a race — sends only workerId and heldBy, because it has no holder read to spare. So HolderName and HeldAt are absent on a perfectly ordinary refusal, and a caller that renders them unconditionally prints a half-empty sentence on the path a concurrent bind takes. Every field is best-effort; only ok is a promise.
func WorkerHeldDetail ¶ added in v0.11.0
func WorkerHeldDetail(err error) (HeldDetail, bool)
WorkerHeldDetail extracts the WORKER_HELD payload from err's extensions; ok is false when err is not that error. Rendered from the extensions, not the message wording (cor:agt:020:09 is the contract). Call it BEFORE MapError wraps the error.
HELD is not TAKEN and the difference is the whole point: a held name is somebody's until they release it, so this refusal has no --force. Never pair it with a takeover suggestion.
func (HeldDetail) Holder ¶ added in v0.11.0
func (d HeldDetail) Holder() string
Holder names the person holding the name, preferring the handle the server resolved and falling back to the raw user id — which is what the race path leaves us with. Empty only when the server sent neither.
type HoldStaleDetail ¶ added in v0.13.0
HoldStaleDetail is the payload a WORKER_HOLD_STALE error carries (hadron-server#1084): the hold found NOW, at the moment the guarded write refused the caller's assertion.
HolderID is EMPTY when the name is currently held by NOBODY — the server sends `heldByUserId: null` for that, and it is a real answer rather than a missing field: it is what a caller who asserted a specific holder gets when the name was released underneath them. `Held` reports which of the two it is, because "" alone cannot: a JSON null and an absent key both decode to "".
The server deliberately does NOT say whose hold it is relative to the caller, nor whether releasing would be a force-release. It throws before comparing the holder to the caller, and the account now holding the name may well BE the caller — a caller asserting expectUnheld who turns out to hold it themselves is the plain case. The client knows its own id and can decide; rendering the server's neutrality as an accusation is the mistake this comment exists to prevent.
func WorkerHoldStaleDetail ¶ added in v0.13.0
func WorkerHoldStaleDetail(err error) (HoldStaleDetail, bool)
WorkerHoldStaleDetail extracts the WORKER_HOLD_STALE payload from err's extensions; ok is false when err is not that error. Call it BEFORE MapError wraps the error, like WorkerHeldDetail.
type ListNode ¶ added in v0.5.0
type ListNode = gen.FindNodesFindNodesFindNodesResultHitsNodeHitNode
ListNode is the shallow node projection the unified `findNodes` field returns under hits[].node — id/loc/name/type/tags/seq/isRunnable/updatedAt. Aliased here so callers don't spell the deeply-nested genqlient type name (and so a future projection change is a one-line edit), matching the batchNode alias pattern in nodedoc.go.
Properties and Data are the two JSONB columns, and they are OPT-IN (#602): they arrive only when FindNodesProjected was asked for them, and are nil otherwise. A nil here therefore means "not requested", NEVER "the node has none" — the two are indistinguishable at this type, which is the whole defect #602 was filed for. Read them only on the path that asked for them.
type NodeKindState ¶ added in v0.14.0
NodeKindState is what a node ALREADY is, which an update needs because the gate reads the kind the node is now as well as the kind it will be, and an omitted field preserves the stored one.
type RawResult ¶
type RawResult struct {
Body json.RawMessage
Errors []rawError
}
RawResult is the verbatim GraphQL response envelope.
func RawGraphQL ¶
func RawGraphQL(ctx context.Context, serverURL, token, query string, variables map[string]any, httpClient *http.Client) (*RawResult, error)
RawGraphQL posts an arbitrary query/mutation to the server and returns the raw response body. Used by `hadron api`. The returned error carries the mapped exit code when the response contains GraphQL errors.
type ResponseWarning ¶ added in v0.27.0
type ResponseWarning struct {
Path string
Code string
Text string
Register *WarningRegister // nil when the server sent none (a caller with no Agent)
}
ResponseWarning is one entry of a GraphQL response's `extensions.warnings` (server#2198/#2207): advice on an operation that SUCCEEDED, filed under the operation's response key (Path). Code and Text are the server's, verbatim; the code list is closed per server release and a client tolerates unknown ones.
type ScopeRefusal ¶ added in v0.16.0
type ScopeRefusal string
ScopeRefusal names why the server refused a credential by its OAuth scope. Its values are also `auth status` / `auth token validate`'s rejectedReason.
const ( // ScopeMCPOnly: the grant is `mcp` alone, valid for MCP clients only. ScopeMCPOnly ScopeRefusal = "mcp-only-scope" // ScopeUnsupported: the grant carries a scope this server does not // support, so it is refused everywhere. ScopeUnsupported ScopeRefusal = "unsupported-scope" )
func OAuthScopeRefusal ¶ added in v0.16.0
func OAuthScopeRefusal(err error) ScopeRefusal
OAuthScopeRefusal classifies a FORBIDDEN that refuses the CREDENTIAL, not the caller's permission. extensions.reason decides whenever the server sends it (server#1306), and an unknown reason is NOT a scope refusal. Only a server that sends no reason at all (one that predates #1306) is recognised by the MCP-only sentence, so the CLI works in either deploy order. Anything else returns "", and the error keeps its ordinary FORBIDDEN mapping. Call it on the RAW error.
type SearchHit ¶ added in v0.6.0
type SearchHit struct {
Score *float64
AbstractStale bool
Node *SearchNode
}
SearchHit is one scored search result. Score is nil when the server returned an unscored hit; AbstractStale reports the vector index's abstract-staleness flag (false when absent).
type SearchNode ¶ added in v0.6.0
type SearchNode = gen.SearchNodesFindNodesFindNodesResultHitsNodeHitNode
SearchNode is the search-shaped node projection (abstract included).
type SearchPage ¶ added in v0.6.0
type SearchPage struct {
Hits []*SearchHit
Total *int
Degraded *string
Reason *string
Scope *SearchScope
}
SearchPage is the scored counterpart of FindNodesPage: it preserves per-hit scores rather than flattening to bare nodes.
func SearchNodes ¶ added in v0.6.0
func SearchNodes( ctx context.Context, client graphql.Client, query string, mode *gen.FindNodesMode, filter *gen.NodeFilter, sortProperty *gqltypes.NodePropertySort, limit, offset *int, scope *string, orgID *string, appRef *string, ) (*SearchPage, error)
SearchNodes runs a ranked findNodes query (the `hadron search` backend), keeping per-hit score + vector metadata that FindNodes drops. sortProperty orders by a properties/data JSON path and overrides the mode ranking window when set (#719); nil pointers are omitted from the wire. scope names the search lens — a scope id, a bare name, `app` or `global` — and is resolved server-side; the resolution it reports comes back on SearchPage.Scope.
The two context arguments are for DIFFERENT scope forms and are not interchangeable: appRef is REQUIRED for `scope: "app"` and for a bare scope NAME (a name is unique only per owner), while orgID selects the member's "active organization" view (cor:api:100:01), which is what `scope: "global"` resolves against.
func SearchNodesProjected ¶ added in v0.14.0
func SearchNodesProjected( ctx context.Context, client graphql.Client, query string, mode *gen.FindNodesMode, filter *gen.NodeFilter, sortProperty *gqltypes.NodePropertySort, limit, offset *int, scope *string, orgID *string, appRef *string, withProperties, withData bool, ) (*SearchPage, error)
SearchNodesProjected is SearchNodes with the two JSONB columns opt-in (#602), so a ranked result can show the field a --where predicate selected on. Each column is independent; callers that want neither should use SearchNodes, whose projection is deliberately thin.
withProperties and withData are ADJACENT BOOLEANS, which is the same hazard the scope/appRef pair carries below: transposing them compiles and silently asks for the other column. The command tests assert each flag selects its own column and NOT its neighbour, which is what catches it.
type SearchScope ¶ added in v0.14.0
type SearchScope = gen.SearchNodesFindNodesFindNodesResultScopeSearchScopeInfo
SearchScope is the scope a search actually ran under, as the SERVER resolved it (spec 049). Nil when no scope applied.
Source is which rung of the resolution ladder won, and DroppedCount is how many of the scope's memories the caller may not read. Both are reported rather than derived: the ladder is the server's, and a client that inferred either would be a second implementation of it.
type TakenDetail ¶ added in v0.9.0
TakenDetail is the informed-takeover payload a WORKER_TAKEN error carries (hadron-server#940): everything the takeover prompt needs, in one round trip. Fields are empty when the server omitted them (lastDriver is null for an unattributed session).
func WorkerTakenDetail ¶ added in v0.9.0
func WorkerTakenDetail(err error) (TakenDetail, bool)
WorkerTakenDetail extracts the WORKER_TAKEN payload from err's extensions; ok is false when err is not that error. The MESSAGE also narrates the payload today, but the extensions are the documented contract (cor:agt:020:03) — render from these, not from message wording. Call it BEFORE MapError wraps the error.
type WarningRegister ¶ added in v0.27.0
WarningRegister says where the App's register maps a warning's code: State is mapped (TaskLoc, TaskURN set), unmapped or unresolved.
type WarningSink ¶ added in v0.27.0
type WarningSink struct {
// contains filtered or unexported fields
}
WarningSink collects the warnings of the requests made with the context WithWarningSink returned. It belongs to that context alone: a request made with any other context never reaches it.
func WithWarningSink ¶ added in v0.27.0
func WithWarningSink(ctx context.Context) (context.Context, *WarningSink)
WithWarningSink returns a context whose successful requests file their response warnings in the returned sink. Commands that never install one see no change.
func (*WarningSink) For ¶ added in v0.27.0
func (s *WarningSink) For(path string) []ResponseWarning
For returns the warnings filed under path (an operation's response key), in the server's order. It never returns nil.