Documentation
¶
Overview ¶
Package claudeproxy relays a CI/CD pipeline's Anthropic API calls through the hub, so the pipeline can run an agent harness without ever holding the hub's Claude credential.
The shape of the bargain ¶
A pipeline that has federated its OIDC identity (see pkg/ciauth) receives a session token and a base URL. It exports them as ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN and runs Claude Code, or any Anthropic SDK, unchanged: both send their requests to the base URL and their credential in the Authorization header, and both are satisfied by a server that answers the Messages API. Nothing in the runner knows it is not talking to Anthropic.
What it is actually talking to is a monitor. The session token is worth only what its policy says — these models, this many requests, this long — and it stops being worth anything the moment the hub closes the session. The real credential never leaves the hub process.
This is the same construction pkg/gitproxy applies to a GitHub token and pkg/kubeguard applies to a kubeconfig: take custody of the credential, give out something narrower that points at a thing that reads every request, and make the audit trail a record of decisions rather than of deliveries.
Runs, not only pipelines ¶
Since Task 20435 the same engine relays a sandboxed run's model calls (the hub's executors.model_relay). The shape is the same; three things differ. A run relay session carries its own upstream credential — the one the run's grant holds, which the hub withholds from the sandbox — rather than spending the proxy's (Options.SessionUpstreams). It stands on the run's secret lease and grant, is extended with the lease (Registry.Extend) and ends with it (CloseForLease, CloseForGrant). And its spend is the hub's to meter and to cap: Options.Admit holds the identity paying for the run to its budgets before a call is relayed, and Options.OnSpend books what the upstream reported after. Its tokens start "cloop_relay_", not "cloop_ci_".
What it deliberately does not do ¶
It does not inspect or rewrite prompts, and it does not cache responses. A proxy that reads the conversation is a proxy an operator must reason about as a data processor; this one moves bytes and decides whether it should. The exception is `max_tokens`, which is clamped because it is the one field that bounds spend, and the model name, which is checked because it is the one field that selects it.
Index ¶
- Constants
- Variables
- func IntersectModels(a, b []string) []string
- func SessionIDOf(token string) (string, bool)
- type Denial
- type DenyReason
- type Event
- type EventKind
- type MessageRequest
- type MintRequest
- type Minted
- type Options
- type Policy
- type Proxy
- type Registry
- func (r *Registry) Authenticate(token string) (*Session, error)
- func (r *Registry) BaseURL() string
- func (r *Registry) CheckRestore(rec SessionRecord) error
- func (r *Registry) Checkpoint() int
- func (r *Registry) Close(id, reason string) error
- func (r *Registry) CloseAll(reason string) int
- func (r *Registry) CloseByRule(ruleID, reason string) int
- func (r *Registry) CloseForGrant(leaseID, grantID, reason string) int
- func (r *Registry) CloseForLease(leaseID, reason string) int
- func (r *Registry) ClosedReason(token string) (string, bool)
- func (r *Registry) DropUnheld(ids []string, held map[string]bool) int
- func (r *Registry) DurableIDs() []string
- func (r *Registry) Extend(id string, until time.Time) (time.Time, error)
- func (r *Registry) Known(id string) bool
- func (r *Registry) LeaseSessions(leaseID string) []*Session
- func (r *Registry) Mint(req MintRequest) (*Minted, error)
- func (r *Registry) Prefix() string
- func (r *Registry) ReapExpired() int
- func (r *Registry) Restore(req RestoreRequest) (*Session, error)
- func (r *Registry) Session(id string) (*Session, bool)
- func (r *Registry) SessionIDOf(token string) (string, bool)
- func (r *Registry) Sessions() []*Session
- func (r *Registry) SetClock(now func() time.Time)
- func (r *Registry) Suspend(id, reason string) bool
- func (r *Registry) SuspendAll(reason string) []string
- func (r *Registry) SuspendForLease(leaseID, reason string) []string
- type RestoreRequest
- type Session
- func (s *Session) CloseReason() string
- func (s *Session) Closed() bool
- func (s *Session) Deadline() time.Time
- func (s *Session) Durable() bool
- func (s *Session) Expired(now time.Time) bool
- func (s *Session) Label() string
- func (s *Session) LastUsed() time.Time
- func (s *Session) RemainingRequests() int64
- func (s *Session) RenewedAt() time.Time
- func (s *Session) UpstreamKind() string
- func (s *Session) Usage() Usage
- type SessionRecord
- type SessionRenewer
- type SessionStore
- type Spend
- type Upstream
- type Usage
Constants ¶
const ( // DefaultMaxBodyBytes bounds one request. Conversations with large // pasted files are legitimately big; 24 MiB is past anything the // Messages API will accept anyway, so this is a memory guard rather // than a policy. DefaultMaxBodyBytes int64 = 24 << 20 // DefaultMaxOutputTokens clamps max_tokens when a policy sets none. // Generous enough for an agent turn, finite enough that a runaway loop // is bounded per call. DefaultMaxOutputTokens = 32000 )
Default body and output bounds.
const ( MinTTL = 1 * time.Minute MaxTTL = 6 * time.Hour MaxSessions = 4096 )
Session bounds. MaxSessions is a memory guard: each session is a few hundred bytes, so the bound exists to make an unbounded mint path impossible rather than because the memory matters.
const DefaultUpstream = "https://api.anthropic.com"
DefaultUpstream is the Anthropic API base. The proxy appends the path the client asked for, so this is the origin plus nothing.
const OAuthBeta = "oauth-2025-04-20"
OAuthBeta is the anthropic-beta value the API documents for requests authenticated with a claude.ai OAuth token.
const TokenPrefix = "cloop_ci_"
TokenPrefix identifies a CI session token.
The prefix is load-bearing, exactly as it is for pkg/apitoken: it is what lets a secret scanner recognise one of these in a log or a commit, and what lets the authentication path tell a session token apart from an Anthropic key a confused pipeline sent instead.
Variables ¶
var ( // ErrUnauthenticated covers every reason a presented token is not usable: // malformed, unknown, expired, closed. They are one error on purpose — // a caller probing for a valid session learns nothing from the // difference — while the audit trail records which it was. ErrUnauthenticated = errors.New("claudeproxy: session token not accepted") // ErrSessionNotFound is returned by management calls, not by // authentication. ErrSessionNotFound = errors.New("claudeproxy: no such session") // ErrNoUpstream means no Anthropic credential is configured, so there is // nothing to relay to. ErrNoUpstream = errors.New("claudeproxy: no upstream Anthropic credential is configured") )
Errors the hub distinguishes.
var ( // ErrSessionExists: the registry already holds a session with that id. ErrSessionExists = errors.New("claudeproxy: a session with that id is already live") // ErrSessionLapsed: the session's TTL ran out before it could be restored. ErrSessionLapsed = errors.New("claudeproxy: session lapsed before it could be restored") // ErrRecordGone is what a Store answers a checkpoint whose record is no // longer this process's to write: closed by another hub process — its // rule was withdrawn, it was revoked — or taken over by one. The // registry stops serving the session: the record, not this process's // memory, says whether a session stands. ErrRecordGone = errors.New("claudeproxy: the session's record is closed or held by another hub process") // ErrClosedHere: this registry closed the session — revoked, expired, // its rule withdrawn — and does not bring back a session it ended, even // when the write of that end to the record failed. ErrClosedHere = errors.New("claudeproxy: this hub process closed the session") // ErrTooManySessions: MaxSessions are live. It says nothing about the // session asked for, which may be minted or restored once one ends. ErrTooManySessions = errors.New("claudeproxy: the session limit is reached") )
Errors Restore distinguishes.
var ErrNotExtendable = errors.New("claudeproxy: session cannot be extended")
ErrNotExtendable is what Extend answers for a session it cannot move: not live here, already ended, or a deadline that does not move it forward.
Functions ¶
func IntersectModels ¶ added in v0.0.5
IntersectModels returns model patterns admitting only models both a and b admit, sorted. See Narrow for why it may drop a model both admit.
func SessionIDOf ¶ added in v0.0.5
SessionIDOf returns the id half of a session token without checking the secret half, and whether the token has the shape of one.
Types ¶
type Denial ¶
type Denial struct {
Reason DenyReason
Message string
Status int
}
Denial is a refusal with a reason and a message safe to return to the pipeline. The message names what is wrong with the request, never what the policy would have accepted instead: a caller mapping out an allowlist by probing it should learn nothing it did not already know.
func AllowsPath ¶
AllowsPath reports whether method and path are in the API surface, and returns a denial explaining which of the two failed.
Path and method are separated because they fail for different reasons and an operator reading the audit trail wants to know which: an unknown path is usually an SDK reaching for a feature the policy does not cover, while a wrong method on a known path is usually a client bug.
type DenyReason ¶
type DenyReason string
DenyReason is a stable machine-readable refusal code. Audit consumers match on these; the human-readable message beside them is free to change.
const ( DenyPathNotAllowed DenyReason = "path_not_allowed" DenyMethodNotAllowd DenyReason = "method_not_allowed" DenyModelNotAllowed DenyReason = "model_not_allowed" DenyBodyTooLarge DenyReason = "body_too_large" DenyBodyMalformed DenyReason = "body_malformed" DenyBudget DenyReason = "budget_exhausted" )
type Event ¶
type Event struct {
Kind EventKind `json:"kind"`
SessionID string `json:"session_id,omitempty"`
// Which rule admitted this pipeline, and which pipeline it was.
RuleID string `json:"rule_id,omitempty"`
RuleName string `json:"rule_name,omitempty"`
Project string `json:"project,omitempty"`
Subject string `json:"subject,omitempty"`
Repository string `json:"repository,omitempty"`
Ref string `json:"ref,omitempty"`
Workflow string `json:"workflow,omitempty"`
Actor string `json:"actor,omitempty"`
RunID string `json:"run_id,omitempty"`
// What a run relay session stands on (Task 20435): the lease and grant
// its credential came from, the executor its run was dispatched to, the
// identity its spend is charged to, and the kind of credential it spends
// ("oauth_token", "api_key") — never the credential.
LeaseID string `json:"lease_id,omitempty"`
GrantID string `json:"grant_id,omitempty"`
ExecutorID string `json:"executor_id,omitempty"`
Payer string `json:"payer,omitempty"`
UpstreamKind string `json:"upstream_kind,omitempty"`
// Request shape, for allowed and denied relays.
Method string `json:"method,omitempty"`
Path string `json:"path,omitempty"`
Model string `json:"model,omitempty"`
Status int `json:"status,omitempty"`
// Reason is set on a refusal.
Reason DenyReason `json:"reason,omitempty"`
// Spend attributable to this event.
InputTokens int64 `json:"input_tokens,omitempty"`
OutputTokens int64 `json:"output_tokens,omitempty"`
Detail string `json:"detail,omitempty"`
At time.Time `json:"at"`
}
Event is one thing the proxy decided, for the audit trail.
Events carry identifiers, model names, reasons and counts. They never carry a prompt, a completion, a session token or the upstream credential: an audit log that contains the conversation is a second copy of the data the proxy was built to avoid reading.
type EventKind ¶
type EventKind string
EventKind names what happened.
const ( // EventExchangeAccepted / EventExchangeRejected record the OIDC // federation step: a pipeline presented a token and either received a // session or did not. Both are emitted by the hub rather than by the // proxy, because that exchange happens before a session exists. EventExchangeAccepted EventKind = "ci.exchange.accepted" EventExchangeRejected EventKind = "ci.exchange.rejected" // EventSessionMinted / EventSessionClosed bracket a session's life. EventSessionMinted EventKind = "ci.session.minted" EventSessionClosed EventKind = "ci.session.closed" // EventSessionSuspended: a hub process stopping gracefully stopped // serving a recorded session without ending it, so the process that // receives the pipeline's next request can restore it (Task 20390). EventSessionSuspended EventKind = "ci.session.suspended" // EventSessionRestored: a hub process took over the record of a session // whose holder had stopped, held it to its rule as the rule stands now, // and serves it under its original id and token. EventSessionRestored EventKind = "ci.session.restored" // EventRelayAllowed is one forwarded request. It carries the token counts // the upstream reported, which is what makes per-pipeline spend // answerable. EventRelayAllowed EventKind = "ci.relay.allowed" // EventRelayDenied is one request the policy refused. The upstream never // saw it and the hub's credential was never attached. EventRelayDenied EventKind = "ci.relay.denied" // EventRejected is a request that never reached a session: an // unparseable, unknown, expired or revoked token. EventRejected EventKind = "ci.rejected" )
type MessageRequest ¶
type MessageRequest struct {
Model string `json:"model"`
MaxTokens *int `json:"max_tokens,omitempty"`
Stream bool `json:"stream,omitempty"`
}
MessageRequest is the handful of fields the policy reads out of a Messages API request. Everything else is passed through untouched.
func InspectBody ¶
func InspectBody(body []byte) (MessageRequest, error)
InspectBody decodes the policy-relevant fields of a request body.
It decodes into a struct with three fields rather than into a map, so a body carrying a 10 MB conversation is not materialised twice just to read the model name.
type MintRequest ¶
type MintRequest struct {
Policy Policy
RuleID string
RuleName string
Project string
Subject string
Repository string
Ref string
Workflow string
Actor string
RunID string
RunURL string
// TTL is the session lifetime, clamped to [MinTTL, MaxTTL].
TTL time.Duration
// Claims is the verified assertion the pipeline federated with, recorded
// with the session for the hub that restores it (Task 20390). Opaque
// here, never read, never retained in memory past the record's write.
Claims json.RawMessage
// A run relay session's binding (Task 20435); see Session.
LeaseID string
GrantID string
ExecutorID string
Payer string
// Upstream, when set, is the credential this session relays with in
// place of the proxy's. It must validate.
Upstream *Upstream
}
MintRequest describes the session to create.
type Minted ¶
type Minted struct {
Session *Session
// Token is the only copy. It is not stored, and a caller that loses it
// must mint again.
Token string
// BaseURL is what the pipeline sets ANTHROPIC_BASE_URL to.
BaseURL string
}
Minted is the one-time result of creating a session.
type Options ¶
type Options struct {
// Upstream is the credential and endpoint to relay to.
Upstream Upstream
// PathPrefix is stripped from the incoming path before the API surface
// is matched, e.g. "/api/ci/anthropic".
PathPrefix string
// Transport overrides the upstream HTTP transport. Tests point this at
// an httptest server.
Transport http.RoundTripper
// Now overrides the clock.
Now func() time.Time
// Fallback, when set, is offered every request whose credential names a
// session this registry has never heard of, before it is refused
// (Task 20354). Several hub processes may serve one control plane, each
// with its own registry, and a sandbox's request reaches whichever of
// them the network sends it to; the hub forwards it to the process that
// minted the session. It reports whether it answered the request.
// Credentials naming a session this registry knows — right or wrong
// token, live or expired — are always decided here.
Fallback func(w http.ResponseWriter, r *http.Request, sessionID string) bool
// Admit, when set, is asked before a Messages API request is relayed —
// after the policy accepted it, before its request is counted — and a
// denial it returns refuses the request as the policy's do. A run relay
// holds the identity paying for the run to its daily budgets here (Task
// 20435). Counting tokens and listing models spend nothing and are not
// asked about.
Admit func(s *Session, model string) *Denial
// OnSpend, when set, is told what each relayed response spent, as the
// upstream reported it, once the response has been relayed — on the
// request's goroutine, after the session's counters include it.
OnSpend func(s *Session, sp Spend)
// ExplainClosed answers a token whose session this registry closed —
// revoked, its lease ended, its run over — with a 403 naming why, rather
// than the 401 an unknown token gets (Task 20435). Measured against
// Claude Code, a 401 is retried 11 times over about three minutes before
// the harness gives up, and a 403 ends it at once. Only the holder of the
// session's token learns the reason: the token must hash to the closed
// session's.
ExplainClosed bool
// UserAgent replaces the client's User-Agent upstream. Empty uses
// "cloop-ci-proxy/1".
UserAgent string
// Unauthenticated is the message a request with no usable token gets.
// Empty uses the CI relay's, which tells a pipeline to federate again.
Unauthenticated string
// SessionUpstreams says every session carries the credential it relays
// with (MintRequest.Upstream) and the proxy holds none of its own (Task
// 20435): a run relay spends the credential the run's grant holds. The
// proxy's Upstream may then be empty, and a session without a credential
// of its own is refused rather than relayed with something else.
SessionUpstreams bool
}
Options configures a Proxy.
type Policy ¶
type Policy struct {
// Models is a glob allowlist of model IDs. Empty denies everything: the
// caller resolves "unset" to the hub default before minting.
Models []string `json:"models"`
// MaxOutputTokens clamps `max_tokens` on every request. Zero uses
// DefaultMaxOutputTokens.
MaxOutputTokens int `json:"max_output_tokens,omitempty"`
// MaxRequests caps the session's lifetime request count. Zero means
// unlimited, which the minting path does not produce.
MaxRequests int `json:"max_requests,omitempty"`
// MaxBodyBytes bounds one request body. Zero uses DefaultMaxBodyBytes.
MaxBodyBytes int64 `json:"max_body_bytes,omitempty"`
}
Policy is what one CI session may do.
func Narrow ¶ added in v0.0.5
Narrow holds a recorded policy to the one its rule grants now: it returns a policy no wider than either. A model is permitted only when both allowlists permit it, and every cap is the lower of the two. It refuses when no model the session was minted for is permitted any more — a session that could authenticate and do nothing.
The model intersection is computed on the patterns, conservatively: a pair of patterns whose common models cannot be proved to be one of them contributes nothing, so the result can be narrower than the exact intersection but never wider.
func (Policy) AllowsModel ¶
AllowsModel reports whether a model ID is in the allowlist.
func (Policy) DecideMessages ¶
func (p Policy) DecideMessages(body []byte) ([]byte, MessageRequest, *Denial)
DecideMessages applies the policy to a Messages API request and returns the body to forward.
The returned body differs from the input only when max_tokens had to be clamped. Rewriting rather than refusing is deliberate: a client that asked for more output than the policy allows is not misbehaving, it is using a default, and failing the request would make every stock SDK configuration unusable behind a policy that sets any cap at all.
type Proxy ¶
type Proxy struct {
// contains filtered or unexported fields
}
Proxy relays requests from CI sessions to the Anthropic API.
type Registry ¶
type Registry struct {
// OnEvent receives audit events. It is called on the calling goroutine,
// so a sink that blocks blocks a request.
OnEvent func(Event)
// Store, if set, records every session and keeps the record in step
// (durable.go), so another hub process can restore it. Nil keeps
// sessions in memory only, as every registry did before Task 20390.
Store SessionStore
// contains filtered or unexported fields
}
Registry holds live sessions.
func NewRegistry ¶
NewRegistry returns an empty registry. baseURL is the externally reachable mount point of the proxy, handed to pipelines as ANTHROPIC_BASE_URL.
func NewRegistryWithPrefix ¶ added in v0.0.7
NewRegistryWithPrefix returns an empty registry whose tokens start with prefix — a run relay's are not a CI pipeline's (Task 20435), and a secret scanner tells them apart by it. The prefix must start "cloop_", end in an underscore and hold nothing a token's id or secret could.
func (*Registry) Authenticate ¶
cannot tell an unknown session from an expired or revoked one by probing.
func (*Registry) CheckRestore ¶ added in v0.0.5
func (r *Registry) CheckRestore(rec SessionRecord) error
CheckRestore reports why Restore would refuse rec, or nil.
func (*Registry) Checkpoint ¶ added in v0.0.5
Checkpoint writes to the Store what every durable session has spent since its last checkpoint, and returns how many it wrote. The hub calls it on a timer — so a process that dies loses at most one interval's counts — and when it stops. A session whose counters have not moved is skipped, and one whose write fails is tried again next time.
func (*Registry) CloseAll ¶
CloseAll revokes every session, returning how many were live. Used on hub shutdown, and when an operator turns CI federation off: a policy change that leaves already-minted sessions relaying is a policy change that did not happen.
func (*Registry) CloseByRule ¶
CloseByRule revokes every session minted under a rule. Called when a rule is disabled or deleted, for the same reason CloseAll exists.
func (*Registry) CloseForGrant ¶ added in v0.0.7
CloseForGrant ends every session standing on lease leaseID whose credential came from grant grantID — the grant was revoked, or narrowed past what the session holds, and taken back from that lease. An empty leaseID ends the grant's sessions on every lease.
func (*Registry) CloseForLease ¶ added in v0.0.7
CloseForLease ends every session standing on lease leaseID — the lease was released, revoked or lapsed, so what it fed ends with it (Task 20435) — and returns how many were live.
func (*Registry) ClosedReason ¶ added in v0.0.7
ClosedReason reports why the session token names ended, when this registry closed it and token is that session's: a run's sandbox presenting a session its revocation ended learns why, where a caller merely holding the id learns nothing (Task 20435).
func (*Registry) DropUnheld ¶ added in v0.0.5
DropUnheld stops serving each session in ids whose record is not among held — the open records this process holds, read after ids were — and returns how many it dropped. A session whose record was closed elsewhere stops at once, whether or not its counters moved since the last checkpoint.
func (*Registry) DurableIDs ¶ added in v0.0.5
DurableIDs returns the ids of the recorded sessions this registry serves. With DropUnheld it is how the hub reconciles the registry with the records it holds: read the ids first, then the records, so a session minted between the two is not mistaken for one whose record went elsewhere.
func (*Registry) Extend ¶ added in v0.0.7
Extend moves a live session's deadline to until — never past MaxTTL from now, and never backwards — and records the renewal with the Store when the Store keeps renewals (SessionRenewer). A run relay session is minted for its lease's deadline and moved with it, as the run's keepalive extends the lease (Task 20435); a session whose lease is not extended lapses with it.
func (*Registry) Known ¶ added in v0.0.5
Authenticate resolves a presented token to a live session.
Every failure returns ErrUnauthenticated with no further detail, so a caller Known reports whether id names a session this registry holds, live or not. See Options.Fallback.
func (*Registry) LeaseSessions ¶ added in v0.0.7
LeaseSessions returns the live sessions standing on lease leaseID, sorted by id.
func (*Registry) Mint ¶
func (r *Registry) Mint(req MintRequest) (*Minted, error)
Mint creates a session and returns its one-time token.
func (*Registry) Prefix ¶ added in v0.0.7
Prefix is what every token this registry mints starts with.
func (*Registry) ReapExpired ¶
ReapExpired sweeps lapsed sessions and returns how many were removed. Expiry is enforced at authentication regardless, so this is hygiene.
func (*Registry) Restore ¶ added in v0.0.5
func (r *Registry) Restore(req RestoreRequest) (*Session, error)
Restore brings back a session another hub process minted, under the same id and token hash, with the counters it had reached. Its end is recorded in the Store like a minted session's.
It refuses a record it could not have minted — an id or a token hash of the wrong shape, a policy permitting no model, a deadline beyond MaxTTL from its issue — and a session already past its deadline or already live here: a restored session is checked as strictly as a new one, because the record it comes from was read back from a database rather than produced here.
func (*Registry) SessionIDOf ¶ added in v0.0.7
SessionIDOf returns the id half of a token of the shape this registry mints, without checking the secret half, and whether it has that shape.
func (*Registry) Suspend ¶ added in v0.0.5
Suspend stops serving a session without ending it: what it spent is written to its Store, it leaves the registry, and a request already authenticated against it finishes while any later one is refused here — but its record stays open, so the hub process that receives its next request can restore it. A session the Store holds no record of is closed instead, with reason: nothing could restore it. It reports whether the session was suspended.
func (*Registry) SuspendAll ¶ added in v0.0.5
SuspendAll suspends every session, as a hub stopping gracefully does, and returns the ids of the durable ones it suspended, sorted. Sessions with no record are closed with reason.
func (*Registry) SuspendForLease ¶ added in v0.0.7
SuspendForLease suspends every session standing on lease leaseID — the lease was taken over by the hub process that adopted its run, which restores them — and returns the ids of the recorded ones it suspended. One with no record is closed: nothing could restore it.
type RestoreRequest ¶ added in v0.0.5
type RestoreRequest struct {
// Record is the session as its Store recorded it, with the policy and
// deadline the caller holds it to now — never wider than recorded.
Record SessionRecord
// From names the hub process the session was taken over from, for the
// restored row.
From string
// Detail, when set, is added to the restored row: what the caller
// narrowed.
Detail string
// Upstream is the credential a run relay session relays with, re-derived
// by the caller from the lease it took over (Task 20435) — never read
// from the record, which holds none. Required when the record names an
// upstream kind, and must be of that kind.
Upstream *Upstream
}
RestoreRequest is a session to bring back.
type Session ¶
type Session struct {
ID string
Policy Policy
// Provenance: which rule admitted which pipeline. Recorded at mint so an
// audit entry describes the identity that was actually accepted, not
// whatever the rule says today.
RuleID string
RuleName string
Project string
Subject string
Repository string
Ref string
Workflow string
Actor string
RunID string
RunURL string
// What a run relay session stands on (Task 20435): the secret lease and
// grant its upstream credential came from, the executor the run was
// dispatched to, and the identity its spend is charged to. Empty for a
// CI session.
LeaseID string
GrantID string
ExecutorID string
Payer string
IssuedAt time.Time
// ExpiresAt is the deadline the session was minted or restored with.
// Extend moves the effective one; see Deadline.
ExpiresAt time.Time
// contains filtered or unexported fields
}
Session is one federated pipeline's authority to relay.
Counters are atomic and read without the registry lock, so a status page listing sessions cannot block a request in flight.
func (*Session) CloseReason ¶
CloseReason returns why the session was revoked, or "".
func (*Session) Deadline ¶ added in v0.0.7
Deadline is when the session lapses: ExpiresAt, or a later deadline an extension set.
func (*Session) Durable ¶ added in v0.0.5
Durable reports whether a Store holds a record of the session.
func (*Session) LastUsed ¶
LastUsed returns the last time a request authenticated against this session, or the zero time.
func (*Session) RemainingRequests ¶
RemainingRequests reports how many relayed calls the session has left, or -1 when its policy sets no cap.
func (*Session) RenewedAt ¶ added in v0.0.7
RenewedAt is when an extension last moved the deadline, zero for none.
func (*Session) UpstreamKind ¶ added in v0.0.7
UpstreamKind names the credential the session relays with — "api_key", "oauth_token", "auth_token" — or "" for one relaying with its proxy's.
type SessionRecord ¶ added in v0.0.5
type SessionRecord struct {
ID string
// TokenSHA256 is the hex SHA-256 of the session token.
TokenSHA256 string
Policy Policy
RuleID string
RuleName string
Project string
Subject string
Repository string
Ref string
Workflow string
Actor string
RunID string
RunURL string
// Claims is the verified assertion the session was minted for, as the
// hub chose to record it: opaque to this package, which never reads it.
// The hub holds a restored session to its rule against it. A run relay
// session's is the hub's account of the run it serves (Task 20435).
Claims json.RawMessage
// A run relay session's binding (Task 20435). UpstreamKind names the kind
// of credential it spends — never the credential, which the process
// restoring it re-derives from the lease.
LeaseID string
GrantID string
ExecutorID string
Payer string
UpstreamKind string
IssuedAt time.Time
ExpiresAt time.Time
// RenewedAt is when an extension last moved ExpiresAt, zero for none.
RenewedAt time.Time
// Usage and LastUsed are what the session had spent when it was last
// checkpointed.
Usage Usage
LastUsed time.Time
}
SessionRecord is what a Store keeps of a session: everything Restore needs. It has no field a token or an upstream credential could travel in.
type SessionRenewer ¶ added in v0.0.7
SessionRenewer is a SessionStore that records the extensions Extend makes (Task 20435), so a process restoring a renewed session bounds its deadline from the renewal rather than refusing it as one no mint could have set. An answer of ErrRecordGone ends the session here, as a checkpoint's does.
type SessionStore ¶ added in v0.0.5
type SessionStore interface {
// SaveSession records a session this process now serves.
SaveSession(rec SessionRecord) error
// CheckpointSession records what a session this process serves has
// spent so far.
CheckpointSession(id string, u Usage, lastUsed time.Time) error
// CloseSession records that a session this process served has ended,
// and why, with what it spent.
CloseSession(id, reason string, at time.Time, u Usage) error
}
SessionStore persists the record of every session and keeps it in step.
Errors are the implementation's to report: the registry cannot do anything useful with one beyond what it does — a session whose record could not be written is served all the same, and its minted row says what was lost.
type Spend ¶ added in v0.0.7
type Spend struct {
// Model is the model the upstream answered with, else the one asked for.
Model string
// Status is the upstream's HTTP status.
Status int
// Input, Output, CacheRead and CacheWrite are the response's token
// counts; CacheWrite1h is the part of CacheWrite written to the
// one-hour cache, which the API prices higher than the five-minute one.
Input, Output, CacheRead, CacheWrite, CacheWrite1h int64
}
Spend is what one relayed response spent, as the upstream reported it.
type Upstream ¶
type Upstream struct {
// BaseURL is the API origin. Empty uses DefaultUpstream.
BaseURL string
// APIKey is sent as x-api-key.
APIKey string
// AuthToken is sent as Authorization: Bearer.
AuthToken string
// OAuth marks AuthToken as a claude.ai OAuth token — what `claude
// setup-token` prints and CLAUDE_CODE_OAUTH_TOKEN carries — rather than a
// gateway's bearer. The relay then names the API's OAuth beta beside
// whatever betas the client asked for (OAuthBeta). Measured against the
// real API on 2026-10-16 (Task 20435), a token was accepted with and
// without it; the API's own documentation for OAuth callers asks for it,
// and a header the API ignores costs nothing where one it starts to
// require would cost every relayed run.
OAuth bool
}
Upstream is the credential the hub holds and the endpoint it relays to.
Exactly one of APIKey or AuthToken must be set. The two are different things at the API: an API key is a workspace credential presented in x-api-key, an auth token is an OAuth bearer. Guessing from the shape of the string would be a guess about billing, so the caller says which it has.
func (Upstream) Configured ¶
Configured reports whether there is a credential to relay with.
func (Upstream) Kind ¶ added in v0.0.7
Kind names the credential for logs, audit rows and the dashboard: "api_key", "oauth_token" or "auth_token" (a bearer that is not a claude.ai OAuth token, as a gateway issues), "" with none.
type Usage ¶
type Usage struct {
Requests int64 `json:"requests"`
Denied int64 `json:"denied"`
InputTokens int64 `json:"input_tokens"`
OutputTokens int64 `json:"output_tokens"`
CacheRead int64 `json:"cache_read_input_tokens,omitempty"`
CacheWrite int64 `json:"cache_creation_input_tokens,omitempty"`
BytesUp int64 `json:"bytes_up"`
BytesDown int64 `json:"bytes_down"`
}
Usage is what a session spent. Every field is cumulative for the session's lifetime.
func (Usage) TotalTokens ¶
TotalTokens is the sum a spend report cares about.