arqtossdk

package module
v0.3.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 18, 2026 License: Apache-2.0 Imports: 0 Imported by: 0

README

arqtos-sdk-go

arqtos-sdk-go is the public Go contract SDK for arqtos connectors. It defines the interfaces, value types, and error taxonomy that a connector implements — and that a host uses to talk to a connector — without either side depending on the other's internals.

A connector is a small, focused adapter between arqtos and one external system (a secret store, a directory, a tracker, ...). This module defines the connector classes — CredentialLoader, Roster, CodeCI and Tracker — plus the shared building blocks every connector class is built from.

This module is dependency-light by design: the semantic contract itself is stdlib-only. Third-party dependencies are confined to the packages that need them — gopkg.in/yaml.v3 for the schemas, the gRPC/go-plugin stack for the Track-B wire layer, the official MCP Go SDK for the declarative-connector protocol surface, and github.com/vektah/gqlparser/v2 for gqlcheck.

Those are per-package costs, not per-consumer ones. Under Go's module-graph pruning, a consumer that imports only ref + cerr + credential compiles no MCP SDK package, records no MCP SDK line in its own go.sum, and never fetches the module — measured, not assumed. The SDK is visible to go list -m all as a graph node and nothing more.

Release versions — what to pin

The semver tag is the release. v0.1.0, v0.2.0 — that is what a consumer pins, what the release notes describe, and the only version this module claims. Pre-1.0, so a minor bump may break.

⚠️ GitHub milestones on this repo carry arqtos-cli train names (0.3.48, 0.3.49, …). Those are planning labels recording which cli train a piece of SDK work moves alongside — they are not SDK versions, and no consumer should ever pin or cite one. If you read 0.3.48 on an issue here, the shipped artefact for that work is still whichever vX.Y.Z tag it landed in.

Ratified 2026-07-27, after both schemes were found live in one repo.

Packages

Package Purpose
ref The op://<vault>/<item>/<field> secret-reference type (Ref, Parse). A Ref is a reference to a secret — connectors never receive raw credential material as input, only refs.
cerr The connector error taxonomy: a closed Kind vocabulary (NotFound / Unauthorized / Unavailable / RateLimited / Unsupported / Invalid / Timeout / ContractViolation / Unknown), Error, New, KindOf, Classified, Retryable, TripsBreaker. Callers classify errors by kind, never by string-matching — and Unknown deliberately does not trip a breaker.
connector The base contract every connector implements regardless of class: Class (with Classes() / Class.Valid(), the closed class set the manifest's implements enum is derived from), Capability / Capabilities, Health / HealthStatus, and the Connector interface (Implements, Capabilities, Health, Close).
credential The CredentialLoader connector class: Resolve / List / Lease / Renew / Revoke, plus Resolution (a resolve result in which a credential that did not resolve cannot be read as an empty one — an empty value is expressible, but only by asserting it with ResolvedEmpty()), Material (redacted, revealable-on-demand, wipeable secret bytes), Lease, and the optional BatchResolver operation behind CapBatchResolve.
credconform The conformance harness for a CredentialLoader: run it in your own CI to check the contract properties a compiler cannot — no empty-success, typed failures, and a manifest whose declared capabilities match the running connector.
roster The Roster connector class: a read-only view of a directory's Principals, Groups and Memberships, reported as vendor-neutral facts with no arqtos org model in them. Carries Resolution[T] (a list result in which a directory that was not read cannot be read as a directory of nobody — genuine emptiness is expressible, but only by asserting it with EmptyRoster()), the host-side guards CheckResolution / CheckPrincipals / CheckMemberships, and the optional Watcher operation behind CapWatch.
rosterconform The conformance harness for a Roster: no unresolved-as-empty, a deactivated principal reported rather than dropped, memberships that match the group requested, typed failures, and declared capabilities that match both the running connector and the data it returns. RunOutOfProcess runs all of it against a spawned provider binary, across a real gRPC boundary — the only way the marshalling failures are visible at all.
codeci The CodeCI connector class: pull/merge-request lifecycle (CreatePR / ListPRs / MergePR / GetDiff), ListBranches, WhoAmI, CI status (GetCheckRuns / GetWorkflowRun) and two board-agnostic issue operations (GetIssue / CloseIssue), plus the optional RerunWorkflow / CancelWorkflow behind CapCIControl. CreatePR takes a CreatePRRequest (so a draft can be opened, and the next field added breaks no caller) and MUST validate it before opening anything; WhoAmI returns {Login, Authenticated} and MUST NOT report an authenticated identity with an empty login — CheckIdentity is the host-side guard, because an Identity is a plain struct whose fields read fine. Carries Resolution[T] (the same fail-closed list shape as roster.Resolution, reimplemented here with CI-appropriate wording rather than aliased, so a failed ListPRs is never reported as "roster of nobody"), and requires MergePR to validate its MergeMethod and refuse a draft before attempting a merge. The issue pair is addressed as (fullName, number) with no board, which is what the Tracker class cannot express (ItemRef requires a BoardRef, so an issue on no board has no address there) — GetIssue returns an IssueState closed vocabulary rather than an Open bool, because a bool's zero value asserts closed and an unresolvable blocker read as closed tells an operator to start work that is still blocked; an issue that is missing or invisible is a typed failure, never a state. CloseIssue validates its CloseReason before anything else and is idempotent — closing an already-closed issue succeeds. ⚠️ Deliberately no issue listing and no arbitrary field writes: two operations, both by explicit address, so a second work tracker cannot grow inside this class. Distinct from a code-host-administration class: same vendor, different contract.
codeciconform The conformance harness for a CodeCI: no empty-success across ListPRs/ListBranches/GetCheckRuns/GetDiff, typed fail-closed reads, MergePR refusing an unspecified method and a draft and CreatePR refusing an incomplete request (each exercised against a real fixture, never merging or opening one), Branch.Protected measured in both directions so neither constant passes, every PR carrying a URL, WhoAmI refused when it reports an empty login as a success, GetIssue measured against both an open and a closed fixture so neither constant passes and required to fail — typed — on an issue that cannot be resolved, CloseIssue refusing an unspecified reason before it resolves the issue (driven at an issue that does not exist, so the ordering is observable and nothing can be closed to discover the guard is missing) and succeeding on an already-closed one, and declared capabilities that match both the manifest and the running connector's actual CIController implementation — checked by type assertion, never derived from Capabilities() itself. Its own suite asserts that every check has a connector built to violate it, and that no such connector breaks a neighbouring check.
tracker The Tracker connector class: one work tracker — one board on one instance of one provider — as five batch-first, name-keyed operations (Catalogue / Scan / GetItems / Create / Apply). No backend identity crosses the boundary: every field, option, label, train and item type is addressed by NAME, and the Catalogue that resolves names is valid for one call chain and must not be stored — on the tracker this class was designed against, editing one option of a single-select field regenerates the identity of every option in it. Every address is fully qualified (BoardRef, ItemRef), because an estate runs several trackers at once and a board number alone cannot route. Apply is not transactional and says so: ApplyReport's arithmetic is the contract, and CheckApplyReport is the host-side guard that a report accounting for fewer changes than were asked for does not read as a success. Carries Resolution[T] aliased from roster rather than re-implemented, the optional TrainAdmin (behind CapTrains) and SchemaAdmin (behind CapSchemaAdmin), and the two TrainAdmin guards the contract holds rather than leaving to each backend — CheckTrainSets (a scope that could not be read comes back with Err set, never as a scope with no trains: the set is a union over scopes, so the less of it a caller can see the greener a replan looks) and CheckTrainsCreated (a create is verified by re-reading, because a create loop that iterated once still returns successfully for every name it was given).
trackerconform The conformance harness for a Tracker, 15 named checks: the seven every class in this estate carries, plus a scan that pages to exhaustion under both the cheapest and the fullest Selection, unknown never reported as empty, the Selection echoed back on every item, Apply attributing an outcome to every change, a cross-tracker parent refused before any network call, a Catalogue re-read rather than held — and the two TrainAdmin properties, driven through the same guards a host runs. Every write check is driven through a refusal the contract requires, so against a conformant connector the run is safe to repeat against a live board; every check has a stub built to violate it, and TestRun_EveryCheckHasAViolatingStub fails if one ever loses its falsifier.
githubratelimit GitHub's three rate-limit mechanisms handled as three separate things: MechanismPrimary (the hourly quota, in x-ratelimit-* headers), MechanismSecondary (abuse detection — a retry-after, a 429, or a body naming a secondary limit), and MechanismGraphQLCost (a point budget in the response body, refused on HTTP 200). Carries Classify, the fail-closed PrimaryBudget / PointBudget (whose zero value is unknown, never healthy), and a concurrency-safe Gate with Admit (pre-emptive, so a multi-step sweep never half-applies), Do (which discards a value that arrived alongside a rate-limit refusal), jittered backoff and an injected Clock. Deliberately vendor-named — see Handling GitHub rate limits.
gqlcheck Validates the GraphQL a connector sends against the schema the backend actually serves — LoadSchema / LoadSchemaFile, then ValidateDocument, ValidateRootSelection (a run-time-assembled root selection, wrapped in an operation whose variable declarations are inferred from the schema rather than guessed), ValidateVariables (the values a document is sent with, which the document itself says nothing about) and EnumValues. A Report is Valid / Invalid / Unknown / NotChecked and only the first is a pass — a document that would not parse has not been checked. Every Finding carries the rule, the message, the position and the selection path that reaches the offending node. Vendor-free: the SDL is the caller's to supply and to pin — see Validating GraphQL documents.
manifest The connector.yml schema: name, implements, kind, typed capabilities, refs-only auth, min_host_version. Strict parse, closed enums.
mcpconform The MCP protocol surface for declarative connectors: checks that an MCP server can be driven by arqtos — including SessionIndependence, which POSTs a tools/list carrying no initialize and no Mcp-Session-Id and requires a result, so a server that needs a session it will not get is rejected.
skillspec The skill.yml schema (Skill, Parse, Validate) that rides along with the connector SDK. Standalone — not imported by the connector packages.
scaffold / cmd/create-arqtos-connector Generates a Track-B Roster connector skeleton — see Scaffolding a new connector.

See docs/CONTRACT.md for the full method-by-method semantics and docs/SECURITY.md for the security rules every connector implementation MUST honour.

Install

go get github.com/arqtiqa/arqtos-sdk-go

Writing a CredentialLoader connector

A CredentialLoader resolves ref.Ref secret references to credential.Material, and optionally supports leased (time-bounded, renewable) material for dynamic secrets. It embeds the base connector.Connector interface, so every CredentialLoader also reports its Class, Capabilities, Health, and supports Close.

Below is a minimal skeleton — a stub that compiles against the real credential.CredentialLoader interface. It returns cerr.KindUnsupported for everything; a real connector replaces each method body with calls to its backing store.

package myconnector

import (
	"context"

	"github.com/arqtiqa/arqtos-sdk-go/cerr"
	"github.com/arqtiqa/arqtos-sdk-go/connector"
	"github.com/arqtiqa/arqtos-sdk-go/credential"
	"github.com/arqtiqa/arqtos-sdk-go/ref"
)

// Loader is a minimal CredentialLoader skeleton. Swap the bodies for calls
// into your backing secret store.
type Loader struct{}

func (Loader) Implements() connector.Class { return connector.ClassCredentialLoader }

func (Loader) Capabilities() connector.Capabilities {
	return connector.Capabilities{credential.CapRead}
}

func (Loader) Health(ctx context.Context) (connector.Health, error) {
	return connector.Health{Status: connector.Healthy}, nil
}

func (Loader) Close() error { return nil }

// Resolve returns a credential.Resolution, not a *Material. A credential
// that did not resolve cannot be read as an empty one: credential.Resolved
// refuses material with no bytes, and the zero Resolution is unreadable. A
// backend that answers a signed-out read with empty output and a success
// exit code therefore surfaces as a failure, with no emptiness check written
// here. A secret that really is stored empty is still expressible — by
// asserting it with credential.ResolvedEmpty().
func (Loader) Resolve(ctx context.Context, r ref.Ref) (credential.Resolution, error) {
	// A real connector: return credential.Resolved(credential.NewMaterial(b))
	return credential.Resolution{}, cerr.New(cerr.KindUnsupported, "Resolve", nil)
}

func (Loader) List(ctx context.Context, scope string) ([]ref.Ref, error) {
	return nil, cerr.New(cerr.KindUnsupported, "List", nil)
}

func (Loader) Lease(ctx context.Context, r ref.Ref) (credential.Resolution, credential.Lease, error) {
	return credential.Resolution{}, credential.Lease{}, cerr.New(cerr.KindUnsupported, "Lease", nil)
}

func (Loader) Renew(ctx context.Context, l credential.Lease) (credential.Lease, error) {
	return credential.Lease{}, cerr.New(cerr.KindUnsupported, "Renew", nil)
}

func (Loader) Revoke(ctx context.Context, l credential.Lease) error {
	return cerr.New(cerr.KindUnsupported, "Revoke", nil)
}

// Compile-time proof Loader satisfies the contract.
var _ credential.CredentialLoader = Loader{}

Writing a Roster connector

A Roster is a read-only adapter over one directory — an identity provider, a workspace directory, a code host's teams, a flat file. It reports what the directory says about principals, groups and memberships, and nothing about arqtos: no org, no team, no role. Mapping directory facts onto arqtos's own model is the host's job, on the host's side of the boundary.

Three properties do most of the work, and all three exist because of what the host does with the answer:

  • An unresolved roster is unreadable, not empty. The list operations return roster.Resolution[T], not a slice. roster.Resolved refuses an empty list and the zero Resolution cannot be read, so a directory read that came back with nothing — unauthenticated, throttled, misdirected — surfaces as a failure. It has to: an offboarding sweep over "the read failed and I returned a zero value" deprovisions the whole estate. A directory that genuinely holds nobody is still expressible, by asserting it with roster.EmptyRoster[T]().
  • A truncated read is unreadable too, unless you say it isn't. roster.Resolved(items, completeness) takes a roster.Completeness on every call — roster.Complete or roster.Partial — with no default. A real directory of any size means ListPrincipals paginates internally, and a real pagination loop fails partway at least once in production: a 429 on page 7 of 250. The natural line to write at that point is roster.Resolved(itemsSoFar, roster.Partial) — and Resolved refuses it, exactly as it refuses an empty list: a page fetched before the failure is not a smaller success. Return a typed cerr error instead (cerr.KindUnavailable, cerr.KindTimeout, ...), the same way a wholesale read failure already must. roster.Complete is the ordinary case: an atomic read, or pagination that ran to its own natural end.
  • Suspended is not absent. Report a deactivated identity, with Active: false. Omitting it tells the host the person left the organisation, and the host revokes everything belonging to somebody on parental leave.
package mydirectory

import (
	"context"

	"github.com/arqtiqa/arqtos-sdk-go/cerr"
	"github.com/arqtiqa/arqtos-sdk-go/connector"
	"github.com/arqtiqa/arqtos-sdk-go/roster"
)

// Directory is a minimal Roster skeleton. Swap the bodies for calls into your
// backing directory.
type Directory struct{}

func (Directory) Implements() connector.Class { return connector.ClassRoster }

// Declare only what this directory can actually do. Each capability is a
// measured vendor difference, and rosterconform checks every declaration
// against the running connector AND against the data it returns.
func (Directory) Capabilities() connector.Capabilities {
	return nil
}

func (Directory) Health(ctx context.Context) (connector.Health, error) {
	return connector.Health{Status: connector.Healthy}, nil
}

func (Directory) Close() error { return nil }

// ListPrincipals reports EVERY identity in the directory, including
// deactivated ones (Active: false). Omission means "not in the directory".
//
// If your read paginates internally and a page fails partway through, do NOT
// return roster.Resolved(principalsReadSoFar, roster.Complete) — that is a
// truncated page reported as a whole directory, and an offboarding sweep run
// against it revokes everyone past the failure point. Return a typed cerr
// error instead (cerr.KindUnavailable, cerr.KindTimeout, ...), the same way
// you would for a read that never got a first page.
func (Directory) ListPrincipals(ctx context.Context) (roster.Resolution[roster.Principal], error) {
	// A real connector that read the WHOLE directory: return
	// roster.Resolved(principals, roster.Complete) — never roster.Partial;
	// see the comment above.
	return roster.Resolution[roster.Principal]{}, cerr.New(cerr.KindUnsupported, "ListPrincipals", nil)
}

func (Directory) ListGroups(ctx context.Context) (roster.Resolution[roster.Group], error) {
	return roster.Resolution[roster.Group]{}, cerr.New(cerr.KindUnsupported, "ListGroups", nil)
}

// ListMemberships answers for ONE group. Every returned Membership.GroupID
// must equal groupID, and a group that does not exist is KindNotFound — never
// an empty roster, which a reconcile loop reads as "this group lost everyone".
func (Directory) ListMemberships(ctx context.Context, groupID string) (roster.Resolution[roster.Membership], error) {
	return roster.Resolution[roster.Membership]{}, cerr.New(cerr.KindUnsupported, "ListMemberships", nil)
}

// Compile-time proof Directory satisfies the contract.
var _ roster.Roster = Directory{}

Then gate your own CI on rosterconform:

rep, err := rosterconform.Run(ctx, myRoster, rosterconform.Options{
	Manifest:           myManifest,
	Group:              populatedGroupID,        // must HAVE members
	AbsentGroup:        noSuchGroupID,           // must NOT exist
	SuspendedPrincipal: deactivatedPrincipalID,  // must be deactivated
})
if err != nil {
	return err // the check could not be run at all
}
if err := rep.Err(); err != nil {
	return err // the connector ran, and is not conformant
}

Every Options field is required, and each fixture has to be the real thing: a run given an empty group, a group that merely has no members, or no deactivated principal cannot exercise the check it was meant to drive, and a report that is green because nothing looked is worse than no report.

If you ship your roster as an out-of-process provider (kind: provider), that run is not enough. Run takes a roster.Roster and cannot tell a native connector from a host stub talking to a subprocess, so it records TransportUnrecorded rather than claiming the wire was exercised. Add rosterconform.RunOutOfProcess, which launches your built binary, dials it the way a host does — min_host_version negotiation included — and runs every check across a real gRPC boundary:

rep, err := rosterconform.RunOutOfProcess(ctx, rosterconform.Provider{
	Path:        pathToYourBuiltProviderBinary,
	HostVersion: yourHostContractVersion,
}, opts)

An entire class of failure is invisible in-process, because in-process nothing is marshalled: an unresolved read arriving as an empty directory, a suspended principal losing its Active flag, a membership arriving for a group nobody asked about. The spawning lives in the SDK rather than in your tests because a connector repository forbids os/exec in its own package tree — see examples/roster-provider/roundtrip_test.go for the whole pattern.

Scaffolding a new connector

Rather than copying the skeleton above by hand, cmd/create-arqtos-connector generates a complete, buildable Track-B (out-of-process) Roster connector project — the shape a third-party connector actually ships, and the one rosterconform.RunOutOfProcess is the gate for:

go run ./cmd/create-arqtos-connector \
  -name okta-roster \
  -module github.com/you/okta-roster-connector \
  -out ./okta-roster-connector
cd ./okta-roster-connector
go build ./...
go test ./...

The generated project — go.mod pinned to this module at v0.2.0 (the tag carrying the Roster wire protocol and the out-of-process harness; no GOPRIVATE or credential setup, because this module is public), main.go, connector.yml, and an in-process conformance test — compiles and passes rosterconform immediately, against a fixed placeholder directory, before a single line of real logic exists. main.go's comments carry the two mistakes this contract has already made someone pay for once each: an unresolved read must never arrive as an empty directory, and Capabilities() must be hardcoded honest rather than derived from connector.yml. It also does not, and cannot, declare watch — see plugin/roster.go.

Verifying a connector against the contract

Run credconform against your own connector, in your own CI, before arqtos ever loads it:

rep, err := credconform.Run(ctx, myLoader, credconform.Options{
	Manifest:     myManifest,                    // what you publish
	Resolvable:   []ref.Ref{presentRef},         // must resolve
	Unresolvable: absentRef,                     // must fail, and typed
})
if err != nil {
	return err // the check could not be run at all
}
if err := rep.Err(); err != nil {
	return err // the connector ran, and is not conformant
}
Check What it requires
manifest/valid the manifest validates and declares only capabilities this class defines
capability/manifest-matches-runtime the manifest and Capabilities() declare the same set
batch/declared-is-implemented batch_resolve is declared exactly when credential.BatchResolver is implemented
resolve/no-empty-success a resolvable reference comes back carrying material — not a success carrying nothing, and not a ResolvedEmpty() assertion either
failure/typed an unresolvable reference fails with a classified cerr.Kind, not vendor prose
batch/results-match-request batch results correspond one-for-one, in order, with the references requested

Every one of those checks has a test that drives it with a connector built to violate exactly the property it checks — a resolve that returns empty with no error, a failure carrying only the backend's own wording, a manifest declaring a batch operation that is not there. A harness only ever run against compliant input proves nothing about what it would catch.

The remaining conformance surface — full contract shape, secret-handling (no material to logs, disk or wire; dies-with-session), and protocol-version negotiation — lands alongside these checks.

Validating GraphQL documents

A connector that talks GraphQL sends text. A fixture server answers whatever it is asked, so a suite built on one is green for a document the real backend rejects outright — measured: a leaf added to an inline fragment on an interface that has no such field, every read through that document broken in production, and not one test red.

gqlcheck is the check a fixture server cannot stand in for. It asks document-versus-schema, which is a different question from document-versus-decode (does the selection request every leaf my struct carries?) and neither implies the other.

schema, err := gqlcheck.LoadSchemaFile("testdata/schema.graphql") // YOUR pinned SDL
if err != nil {
	return err
}

r := schema.ValidateDocument("catalogueQuery", catalogueQuery)
if !r.OK() {
	t.Fatalf("%s", r) // names the rule, the message and the selection path
}

Three things about it are load-bearing:

  • Only Valid is a pass. Unknown is what a document that would not parse gets, and NotChecked is what a caller records when it did not look at all. Report.OK() is false for both — a checker that reports the unchecked as clean is how the defect it exists for ships behind a green suite.
  • This module vendors no schema. A vendored SDL is a vendor artefact with a refresh policy attached, and it belongs with whoever depends on that backend. ⚠️ Where two modules check against the same backend, they must read one pinned copy: two pins drift, and the second one drifts silently.
  • It does not find your documents. Something has to hand it the text. Driving every document a package contains — a Go-source sweep, a wire gate that walks back from the transport — is caller-side machinery bound to a package loader, and it is deliberately not in this module. gqlcheck takes a schema and text and returns a verdict; see the package doc for the three corrections recorded against that machinery, kept there for anyone building the finding half.

Handling GitHub rate limits

githubratelimit is the one package here named after a vendor, and that is deliberate. Every other package is vendor-neutral because an ABC derived from a single backend encodes that backend's accidents as contract. These three mechanisms are GitHub's, down to the header names and the units, so the name says so — nobody should reach for this as "the rate limiter" and quietly inherit GitHub's shape for a backend that does not have it.

It lives in the SDK because a host reaches GitHub through more than one surface (a tracker and a PR/CI surface at least) and each needs the identical discipline. Two implementations are two implementations that drift, and the drift is invisible until one of them under-waits in production.

The three mechanisms are not one mechanism
Signal Response
Primary — the hourly quota x-ratelimit-remaining / x-ratelimit-reset, on every response including successful ones wait until the reset; and because the budget arrives before exhaustion, Gate.Admit waits before spending it
Secondary — abuse detection (burst rate, concurrency) a retry-after header, a 429, or a 403 whose body names a secondary limit honour retry-after; jittered exponential backoff when it names none. Waiting out the primary reset does not clear this
GraphQL point cost data.rateLimit in the response body — no header at all — and refused on HTTP 200 with a RATE_LIMITED entry in errors wait until resetAt; accounted separately, because one query can cost hundreds of points while the REST request count barely moves

On a secondary refusal the primary budget typically reads healthy, so a handler that derived its wait from the budget computes a wait of nothing and retries straight back into the limit. That is the failure this package exists to prevent, and Classify reports which mechanism refused so the two never collapse.

Fail closed, in two places

Do returns the zero value alongside any rate-limit error — even when the attempt reported no error. GitHub refuses a point-exhausted GraphQL query with HTTP 200 and partial data, so a caller decoding that body has a value, a nil error, and half an answer; a truncated result indistinguishable from a complete one is exactly the defect being discarded here.

PrimaryBudget and PointBudget have an unknown zero value, not a healthy one. An unmeasured budget is never Exhausted() (no evidence of exhaustion, so nothing to wait for) and has no Headroom() (no evidence of room, so nothing to admit a sequence against).

A multi-step sequence completes, or does not start

The guarantee is completes-after-waiting, and it is bought by not letting the sequence begin until the whole of it fits:

gate := githubratelimit.New(githubratelimit.Options{
    Reserve: 50, // keep room for the final status read and the error report
    Notify: func(w githubratelimit.Wait) {
        log.Printf("waiting %s on the github %s limit until %s (attempt %d/%d)",
            w.Delay, w.Mechanism, w.Until.Format(time.RFC3339), w.Attempt, w.Attempts)
    },
})

// One call, before the first mutation, for the whole sequence.
if err := gate.Admit(ctx, len(mutations)); err != nil {
    return err // typed, and it names when the limit clears
}
for _, m := range mutations {
    apply(m)
}

A sequence larger than the entire quota is refused as cerr.KindInvalid without waiting: no amount of waiting makes it fit, and a wait that can never succeed is a hang with extra steps. The gate cannot roll back a mutation that already happened — admitting the whole sequence up front is the mechanism that keeps there from being one.

One request
pr, err := githubratelimit.Do(ctx, gate, "GetPR",
    func(ctx context.Context) (PR, githubratelimit.Observation, error) {
        resp, err := client.Do(req.WithContext(ctx))
        if err != nil {
            return PR{}, githubratelimit.Observation{}, err
        }
        defer resp.Body.Close()
        body, err := io.ReadAll(resp.Body)
        if err != nil {
            return PR{}, githubratelimit.FromHTTP(resp, nil), err
        }
        return decode(body), githubratelimit.FromHTTP(resp, body), nil
    })

Do retries rate-limit refusals only. A 500, a 404, a 403 for a missing scope, a transport error — each is returned verbatim on the first attempt. A wrapper that also retried those would turn a broken backend into a slow one and a missing permission into a five-attempt stall.

Waits are visible, and time is injected

Options.Notify receives a Wait before each pause, carrying the mechanism, the delay, the wall-clock time it ends, and whether the mechanism dictated the wait or the gate computed a jittered backoff. A sweep that pauses for eleven minutes in silence is indistinguishable from a hung one, and an operator kills the second — turning a wait that would have completed into the half-applied sweep the wait existed to prevent.

Options.Clock is the only source of now and of sleeping, so tests for a package whose whole job is waiting do not wait: they assert the computed backoff and the number of attempts, both exact. The package's own test suite is gated against reading the real clock at all — a test asserting elapsed time is asserting a property of the CI runner, and under -race with coverage instrumentation that runner is slow enough to make such a test flake rather than fail honestly.

Options.Resource scopes a gate to one quota bucket. Search is 30 requests a minute against core's 5000 an hour, so a gate that folded them into one number would stall every core request for the rest of the hour the first time a search was refused. A host reaching more than one bucket builds more than one gate.

Three sharp edges the API deliberately encodes

The header constants are canonical, not as GitHub documents them. githubratelimit.HeaderRemaining is "X-Ratelimit-Remaining" — lower-case l. http.Header is a map whose keys Get/Set canonicalize, so the documented spelling "X-RateLimit-Remaining" is invisible to Get when a consumer builds or reads the map directly. Use the constants and both routes work.

A retry-after is clamped at MaxRetryAfter (24h). time.Duration counts nanoseconds in an int64, so retry-after: 18446744074 multiplied out overflows and comes back as a plausible-looking 290 ms — a silent under-wait dressed as compliance. Nothing longer than a day is a rate limit GitHub has, so the value is clamped down rather than rejected: the mechanism is still reported, and only the duration is capped.

A backoff is never zero. Options.Jitter of 1 (full jitter) has a window floor of zero by definition, so an unlucky draw would compute no delay at all — a tight retry straight back into the limit that just refused the request. Gate.Backoff floors at MinBackoff.

A zero Gatevar g Gate, or one embedded in another struct — defaults itself on first use rather than panicking on its nil clock or reporting a rate limit for a request it never made.

Declarative connectors and MCP

A declarative connector ships no Go code: it points arqtos at an MCP server and maps connector operations onto that server's tools. Its contract is the MCP protocol, and arqtos tracks that protocol through the official MCP Go SDK (github.com/modelcontextprotocol/go-sdk) — pinned here at the same version the host uses. There is deliberately no arqtos dual-protocol shim: compatibility across protocol revisions comes from the SDK's wrapper.

Two things bind such a connector, both checkable before arqtos ever dials it:

  • Do not depend on a session. arqtos does not promise a long-lived connection, and the specification is moving to a stateless core.
  • Statelessness is configured, not inferred. Serving over streamable HTTP with the Go SDK requires setting Stateless: true explicitly; the default handler rejects a client that skipped the handshake.

mcpconform turns those into a check you can run in your own CI:

report, err := mcpconform.Run(ctx, mcpconform.StreamableHTTP(endpoint), &mcpconform.Options{
	RequireTools: []string{"list_items", "create_item"},
})
if err != nil {
	return err // the check could not be run at all
}
if err := report.Err(); err != nil {
	return err // the server answered, and is not conformant
}

The check that decides it is session-independent: a raw tools/list POST with no initialize and no Mcp-Session-Id, which must come back with a result. It has to speak the wire rather than use the SDK's Go client, because that client always performs the handshake — every connection it opens is a session, so a check built on it can only compare sessions to each other. It is exported as mcpconform.SessionIndependence for pointing at a deployed endpoint on its own:

if res := mcpconform.SessionIndependence(ctx, endpoint, nil); !res.Pass {
	return fmt.Errorf("%s: %s", res.Name, res.Detail)
}

A server built with mcp.StreamableHTTPOptions{Stateless: false} — the SDK's default — fails this check, which is the point.

See docs/CONTRACT.md for the full check list and the protocol-version compatibility notes.

Versioning and the wire protocol

This module is the Go semantic contract: interfaces, types, and error taxonomy. That contract applies identically whether a CredentialLoader is compiled into the host (native) or runs out-of-process as a separate provider binary (Track-B), talked to over gRPC via hashicorp/go-plugin.

This module also ships the Track-B wire layer built on top of that same contract, for both connector classes: the .proto/generated stubs (proto/, connectorpb/), the marshalling and error-mapping helpers (transport/), the go-plugin handshake and dispense wiring (plugin/), and the provider manifest schema (manifest/) — whose min_host_version is negotiated at dial time, not merely declared. See docs/CONTRACT.md for the full layer-by-layer breakdown, and examples/credentialloader-provider/ or examples/roster-provider/ for a complete, vendor-free reference provider to copy as a starting point for a real one.

The one rule a provider author must not get wrong is the same for both classes: a read that resolved nothing must not arrive at the host looking like an empty secret or an empty directory. Emptiness is asserted on the wire, never inferred from an absent field — because a protobuf repeated field and a zero-length bytes field are both simply absent, so the encoding a hurried author emits by accident has to mean "unresolved". For a roster the cost of getting it wrong is an estate-wide deprovision.

Out of scope here (a separate, later contract): the host-side connector registry — discovery, lifecycle and the broker wiring for managing many connectors at once — and a secrets.Provider adapter.

CI: the private-content firewall

Every pull request and every push to main scans the tracked tree against .github/scripts/private-content-denylist.txt. A match fails the build; a missing denylist exits 2 (misconfigured) rather than reporting a clean scan.

⚠️ This repository is public, so it is the one where the scan matters most — and it was the one running without it (arqtos-sdk-go#38). A repo with no gate is not "ungated pending work": the green check is already there and nothing is producing it.

⚠️ The denylist here omits one rule its siblings carry: the op:// locator shape. The ref package exists to parse those references, so its tests contain them by necessity — and an op://vault/item/field reference is an address, carrying no credential material. Credential-material rules are unaffected: a real token planted in ref/ref_test.go is still refused, by name. The omission is argued in full in the denylist's own header, along with why it must not be widened.

License

Apache-2.0. See LICENSE.

Documentation

Overview

Package arqtossdk is the public contract SDK for arqtos connectors.

It defines the connector-class interfaces (starting with credential.CredentialLoader), the op:// secret-reference type (ref), the error taxonomy (cerr), and the skill schema (skillspec). Third parties build connectors against THIS module and never against arqtos-cli.

Directories

Path Synopsis
Package authconform is the conformance harness for the Authenticator connector class: it checks the parts of the contract a compiler cannot enforce.
Package authconform is the conformance harness for the Authenticator connector class: it checks the parts of the contract a compiler cannot enforce.
Package authenticator defines the Authenticator connector-class contract: establishing WHO IS DRIVING THIS SESSION, interactively and verifiably, against an identity provider.
Package authenticator defines the Authenticator connector-class contract: establishing WHO IS DRIVING THIS SESSION, interactively and verifiably, against an identity provider.
Package cerr is the connector error taxonomy.
Package cerr is the connector error taxonomy.
cmd
create-arqtos-connector command
Command create-arqtos-connector scaffolds a new out-of-process (Track-B) arqtos Roster connector project — the shape a third-party connector author actually ships, and the one rosterconform.RunOutOfProcess is the gate for.
Command create-arqtos-connector scaffolds a new out-of-process (Track-B) arqtos Roster connector project — the shape a third-party connector author actually ships, and the one rosterconform.RunOutOfProcess is the gate for.
Package codeci defines the CodeCI connector-class contract: pull/merge request lifecycle, diffs, branch listing, CI check/workflow-run operations, and the two issue operations below, against ONE code host's PR/CI surface.
Package codeci defines the CodeCI connector-class contract: pull/merge request lifecycle, diffs, branch listing, CI check/workflow-run operations, and the two issue operations below, against ONE code host's PR/CI surface.
Package codehost is the CodeHost connector-class contract: one adapter over one code host's repositories, its git transport, its branches and its change-request lifecycle.
Package codehost is the CodeHost connector-class contract: one adapter over one code host's repositories, its git transport, its branches and its change-request lifecycle.
Package codehostconform checks a codehost.CodeHost connector against the parts of the contract a compiler cannot enforce.
Package codehostconform checks a codehost.CodeHost connector against the parts of the contract a compiler cannot enforce.
Package connector is the base contract every arqtos connector implements, independent of connector-class.
Package connector is the base contract every arqtos connector implements, independent of connector-class.
Package credconform checks a CredentialLoader connector against the parts of the contract a compiler cannot enforce.
Package credconform checks a CredentialLoader connector against the parts of the contract a compiler cannot enforce.
Package credential defines the CredentialLoader connector-class contract: resolve op:// references to secret material, with lease/renew for dynamic secrets.
Package credential defines the CredentialLoader connector-class contract: resolve op:// references to secret material, with lease/renew for dynamic secrets.
examples
authenticator-provider command
Command authenticator-provider is a vendor-free, out-of-process (Track-B) reference implementation of the authenticator.Authenticator connector class.
Command authenticator-provider is a vendor-free, out-of-process (Track-B) reference implementation of the authenticator.Authenticator connector class.
credentialloader-provider command
Command credentialloader-provider is a vendor-free, out-of-process (Track-B) reference implementation of the credential.CredentialLoader connector class.
Command credentialloader-provider is a vendor-free, out-of-process (Track-B) reference implementation of the credential.CredentialLoader connector class.
roster-provider command
Command roster-provider is a vendor-free, out-of-process (Track-B) reference implementation of the roster.Roster connector class.
Command roster-provider is a vendor-free, out-of-process (Track-B) reference implementation of the roster.Roster connector class.
Package githubratelimit handles GitHub's THREE rate-limit mechanisms — primary quota, secondary (abuse-detection) limits, and the GraphQL point budget — as three separate things, because they are three separate things.
Package githubratelimit handles GitHub's THREE rate-limit mechanisms — primary quota, secondary (abuse-detection) limits, and the GraphQL point budget — as three separate things, because they are three separate things.
Package gqlcheck validates GraphQL a client SENDS against the schema the backend actually serves.
Package gqlcheck validates GraphQL a client SENDS against the schema the backend actually serves.
Package manifest is the connector manifest schema (a connector.yml a connector author ships alongside their code) declaring the connector's name, the connector-class it Implements, its runtime Kind (declarative | provider | native), capabilities/supports, its refs-only Auth wiring, and — for out-of-process providers — the minimum host version it requires.
Package manifest is the connector manifest schema (a connector.yml a connector author ships alongside their code) declaring the connector's name, the connector-class it Implements, its runtime Kind (declarative | provider | native), capabilities/supports, its refs-only Auth wiring, and — for out-of-process providers — the minimum host version it requires.
Package mcpconform checks that an MCP server can be driven by arqtos as a declarative connector backend, using the official MCP Go SDK (github.com/modelcontextprotocol/go-sdk) as the client.
Package mcpconform checks that an MCP server can be driven by arqtos as a declarative connector backend, using the official MCP Go SDK (github.com/modelcontextprotocol/go-sdk) as the client.
This file is the Track-B (out-of-process) wiring for the Authenticator connector class, symmetric with the CredentialLoader wiring in plugin.go and the Roster wiring in roster.go: a provider serves an implementation via goplugin.Serve using AuthenticatorPluginMap; a host dials it with AuthenticatorHostPluginMap and Dispense()s a value that itself satisfies authenticator.Authenticator.
This file is the Track-B (out-of-process) wiring for the Authenticator connector class, symmetric with the CredentialLoader wiring in plugin.go and the Roster wiring in roster.go: a provider serves an implementation via goplugin.Serve using AuthenticatorPluginMap; a host dials it with AuthenticatorHostPluginMap and Dispense()s a value that itself satisfies authenticator.Authenticator.
Package ref is the op:// secret-reference used across connector contracts.
Package ref is the op:// secret-reference used across connector contracts.
Package roster defines the Roster connector-class contract: a READ-ONLY view of a directory's principals, its groups, and the memberships between them.
Package roster defines the Roster connector-class contract: a READ-ONLY view of a directory's principals, its groups, and the memberships between them.
Package rosterconform checks a Roster connector against the parts of the contract a compiler cannot enforce.
Package rosterconform checks a Roster connector against the parts of the contract a compiler cannot enforce.
Package scaffold generates a Track-B (out-of-process) arqtos Roster connector skeleton — a spawnable provider binary — from the same proven shape as arqtos-sdk-go's examples/roster-provider.
Package scaffold generates a Track-B (out-of-process) arqtos Roster connector skeleton — a spawnable provider binary — from the same proven shape as arqtos-sdk-go's examples/roster-provider.
Package skillspec is the skill.yml schema shipped alongside the connector SDK (one repo holds both the connector contract and the skill format).
Package skillspec is the skill.yml schema shipped alongside the connector SDK (one repo holds both the connector contract and the skill format).
Package tracker is the Tracker connector-class contract: one adapter over one work tracker — one board on one instance of one provider — and its items, their fields, their hierarchy and their lifecycle.
Package tracker is the Tracker connector-class contract: one adapter over one work tracker — one board on one instance of one provider — and its items, their fields, their hierarchy and their lifecycle.
Package transport marshals the SDK's semantic contract types (ref.Ref, credential.Resolution, credential.Lease, cerr.Error) to and from the generated connectorpb wire types and gRPC status, so a Track-B provider and host can exchange them over the CredentialLoader gRPC service.
Package transport marshals the SDK's semantic contract types (ref.Ref, credential.Resolution, credential.Lease, cerr.Error) to and from the generated connectorpb wire types and gRPC status, so a Track-B provider and host can exchange them over the CredentialLoader gRPC service.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL