Documentation
¶
Overview ¶
Package agentsession owns the internal credential-broker contract shared by bex-api and the isolated SSH gateway's Pod-bound Git smart-HTTP proxy. It deliberately exposes no raw credential to the sandbox and no public REST/GraphQL/MCP surface.
Index ¶
- Constants
- Variables
- func AgentProfileCommand(agent string) string
- func AgentProfileIDs() []string
- func AgentProfileRuntimeJSON(agent string) (argsJSON, envJSON string)
- func BindingLabels(sessionID, repository, branch string) (map[string]string, error)
- func IsModelKeyPlaceholder(value, sessionID string) bool
- func ModelKeyPlaceholder(sessionID string) string
- func ModelKeySecretPath(workspaceID string) string
- func ModelProxyURL(baseURL, namespace, sessionID string) (string, error)
- func NormalizeRepository(raw string) (string, error)
- func ParseModelProxyPath(path string) (namespace, sessionID, upstreamPath string, err error)
- func ProxyRepositoryURL(baseURL, namespace, sessionID, repository, branch string) (string, error)
- func RegisteredModelEndpoint(agent, requested string) (string, error)
- func ResolveAuthScheme(host, credential string) string
- func SessionObject(sessionID string) string
- func Sign(secret, body []byte, now time.Time) (timestamp, signature string)
- func ValidateBranch(branch string) error
- func Verify(secret, body []byte, timestamp, signature string, now time.Time, ...) error
- type AgentProfile
- type AgentProfileManifest
- type Client
- type ConnectionStore
- type GitHubClient
- type Handler
- type MintRequest
- type MintResponse
- type Minter
- type ModelClient
- type ModelHandler
- type ModelMintRequest
- type ModelMintResponse
- type ModelMinter
- type ModelProxyRoute
- type NonceClaimer
- type SessionStore
Constants ¶
const ( // InternalMintPath lives only on bex-api's cluster-internal :8091 listener. InternalMintPath = "/v1/agent-session-credentials" // GitProxyPath lives only on the gateway's cluster-internal :8082 listener. GitProxyPath = "/git/" SignatureHeader = "X-Bex-Agent-Credential-Signature" TimestampHeader = "X-Bex-Agent-Credential-Timestamp" NamespaceHeader = "X-Bex-Sandbox-Namespace" // Pod metadata is the authorization record until the durable session table // (w3/m39) lands. OpenSandbox copies create metadata to labels, so the repo and // branch use fixed-size digests while their exact values arrive in the request. LabelWorkspace = "bex.co/workspace" LabelRegime = "app.bex.co/regime" LabelSession = "bex.co/agent-session" LabelRepository = "bex.co/agent-repository" LabelBranch = "bex.co/agent-branch" RegimeSandbox = "sandbox" BranchPrefix = "bex-agent/" )
const ( AuditVerbMintCredential = "agentsessions.MintCredential" AuditVerbProxyCredential = "agentsessions.ProxyCredential" )
const ( // InternalModelMintPath lives only on bex-api's cluster-internal :8091 // listener, alongside InternalMintPath. Domain-separated by path; both hops // are authenticated by the same gateway-only HMAC secret. InternalModelMintPath = "/v1/agent-session-model-credentials" // ModelProxyPath is the gateway model proxy's listen path prefix. The agent's // base URL is ModelProxyURL(...) and every model API call lands under it; the // gateway strips the prefix and forwards the remainder to the vendor host. ModelProxyPath = "/model/" // ModelKeyField is the single map key a workspace's BYO model key is stored // under in OpenBao and read by the proxy mint. Keeping it here, in the // contract both bex-api and the gateway import, prevents key-name drift. ModelKeyField = "BEX_AGENT_MODEL_API_KEY" )
const ( // AuthSchemeAnthropicKey injects `x-api-key: <cred>` (Anthropic API keys). AuthSchemeAnthropicKey = "anthropic-key" // AuthSchemeAnthropicOAuth injects `Authorization: Bearer <cred>` for an // Anthropic OAuth token (`sk-ant-oat…`); claude-code already sends the // matching `anthropic-beta: oauth-…` header, which the proxy passes through. AuthSchemeAnthropicOAuth = "anthropic-oauth" // AuthSchemeBearer injects `Authorization: Bearer <cred>` (OpenAI-compatible). AuthSchemeBearer = "bearer" // AuthSchemeGoogleKey injects `x-goog-api-key: <cred>` (Gemini) and strips any // client-supplied `key` query parameter. AuthSchemeGoogleKey = "google-key" )
Injection schemes the ModelMinter selects and the gateway proxy applies. The gateway holds the credential only at request time, so the scheme (which the mint computes from the endpoint host + credential shape) travels in the mint response and the proxy switches on it. Keeping the vocabulary here holds both sides to one enum.
const AuditVerbMintModelCredential = "agentsessions.MintModelCredential"
AuditVerbMintModelCredential records credential issuance. The gateway mints once per provider exchange so every request revalidates the session lifecycle.
Variables ¶
var ( ErrInvalidRequest = errors.New("invalid agent credential request") ErrForbidden = errors.New("agent credential request forbidden") )
Functions ¶
func AgentProfileCommand ¶
AgentProfileCommand returns the operator-owned executable for a profile id.
func AgentProfileIDs ¶
func AgentProfileIDs() []string
AgentProfileIDs returns the supported public profile identifiers in stable order.
func AgentProfileRuntimeJSON ¶
AgentProfileRuntimeJSON renders args/env JSON for the sandbox driver environment.
func BindingLabels ¶
BindingLabels is the integration contract for the future session-create path: stamp these values into OpenSandbox create metadata. No secret is present.
func IsModelKeyPlaceholder ¶
IsModelKeyPlaceholder is the final admission guard at the sandbox lifecycle seam: only the exact session-bound fake credential may enter a Pod spec.
func ModelKeyPlaceholder ¶
ModelKeyPlaceholder is the deliberately-fake value the sandbox's agent-native credential env holds when the model proxy is active (ADR062 D2). It is useless off-pod by construction — the proxy authenticates the source Pod, not this string — and its obviously-non-credential shape makes an accidental direct use fail loudly rather than look like a real key.
func ModelKeySecretPath ¶
ModelKeySecretPath is the OpenBao KV v2 path a workspace's BYO agent-session model key is stored at (ADR047 D7). workspaceID (tea-<xid>) is opaque and unguessable, so folding it into the path achieves per-workspace isolation without the store's tenant-context mechanism (the agentsessions feature and the ModelMinter both read through this one definition).
func ModelProxyURL ¶
ModelProxyURL binds an agent's model base URL to the exact session identity the gateway verifies against the direct source Pod. It carries no credential; the per-provider path suffix (e.g. OpenAI's `/v1`) is added by the driver, which owns per-agent base-URL routing (ADR062 D5).
func NormalizeRepository ¶
NormalizeRepository accepts the owner/repo path Git passes to a credential helper and returns a stable GitHub repository identity. GitHub repository identities are case-insensitive; lowercasing prevents equivalent spellings from producing different pod-binding digests.
func ParseModelProxyPath ¶
ParseModelProxyPath extracts the claimed namespace, session id, and the vendor subpath (everything after the session segment, leading slash preserved) from a model-proxy request path. The namespace/session are claims only: the gateway still authorizes them against the resolved source Pod before minting.
func ProxyRepositoryURL ¶
ProxyRepositoryURL binds a Git smart-HTTP remote to the exact immutable session identity the gateway verifies against the direct source Pod. It contains no credential.
func RegisteredModelEndpoint ¶
RegisteredModelEndpoint resolves a supported agent profile server-side and rejects caller-supplied endpoint overrides. It is shared by create/rehydrate and the credential minter, so legacy rows cannot bypass the registration boundary after a new-create check is deployed.
func ResolveAuthScheme ¶
ResolveAuthScheme picks the injection scheme from the credential shape and the vendor host. It is deterministic and needs no agent name: an Anthropic OAuth token is unmistakable by prefix, and the remaining providers are keyed by host.
func SessionObject ¶
SessionObject is the OpenFGA object literal for one durable agent session (deploy/gitops/authz/model.fga's agent_session type). Shared by the bex-api sandbox-exec seam (round-13 #1) and the gateway's exec revalidator so both sides check the identical object; the agentsessions feature's own sessionObject helper is this same string.
func Sign ¶
Sign authenticates the gateway→bex-api hop with the already-shared sandbox exec secret, domain-separated from sandbox-exec tickets. The timestamp bounds capture/replay without adding any credential to the guest.
func ValidateBranch ¶
ValidateBranch enforces ADR047 D2's server-side mint gate. This is a policy check, not a claim that GitHub installation tokens encode a branch scope; tenants must still protect default branches.
Types ¶
type AgentProfile ¶
type AgentProfile struct {
ID string `json:"id"`
Executable string `json:"executable"`
Args []string `json:"args"`
Env map[string]string `json:"env"`
ModelEndpoint string `json:"modelEndpoint"`
ModelProxy ModelProxyRoute `json:"modelProxy"`
}
AgentProfile binds a public profile id to an operator-owned executable and the model-proxy routing metadata the gateway credential path expects.
func LookupAgentProfile ¶
func LookupAgentProfile(agent string) (AgentProfile, bool)
LookupAgentProfile resolves a public profile id to the release-locked manifest row.
func LookupAgentProfileOrEmpty ¶
func LookupAgentProfileOrEmpty(agent string) AgentProfile
type AgentProfileManifest ¶
type AgentProfileManifest struct {
Version int `json:"version"`
Profiles []AgentProfile `json:"profiles"`
}
AgentProfileManifest is the release-locked, non-secret runtime profile contract shared by bex-api and the in-pod driver (w5/m77).
type Client ¶
Client is the gateway's HMAC-authenticated caller for bex-api's internal mint verb. It never logs or persists the returned token.
func (*Client) Mint ¶
func (c *Client) Mint(ctx context.Context, req MintRequest) (MintResponse, error)
type ConnectionStore ¶
type ConnectionStore interface {
// GetGitConnectionByOwner resolves the workspace's connection for a repo's
// GitHub account (ADR078 §4). A workspace may hold several installations, so
// the session token must be minted from the one that owns the target repo.
GetGitConnectionByOwner(ctx context.Context, workspaceID, accountLogin string) (store.GitConnection, error)
}
type GitHubClient ¶
type Handler ¶
type Handler struct {
Secret []byte
Minter *Minter
// Nonce is the single-use replay guard (durable, cross-replica). Wired to the
// control-plane store in production; nil disables the check (dev/store-off).
Nonce NonceClaimer
Now func() time.Time
}
Handler authenticates the gateway→bex-api internal mint request. It is mounted only on :8091 and never under the public bex-api router.
type MintRequest ¶
type MintRequest struct {
SessionID string `json:"sessionId"`
Workspace string `json:"workspace"`
Repository string `json:"repository"`
Branch string `json:"branch"`
PodName string `json:"podName"`
PodUID string `json:"podUid"`
Nonce string `json:"nonce,omitempty"`
}
MintRequest contains no credential. The gateway derives Workspace, PodName, and PodUID from Kubernetes after matching the TCP source IP; the helper only supplies the non-secret session/repository/branch values bound by pod labels. Nonce is a per-request single-use token the client mints (Client.Mint) and the server claims once, riding inside the signed body so it is bound by the HMAC — it closes the ±skew replay window on this hop (security-audit run-1).
func AuthorizePod ¶
func AuthorizePod(namespace, podName, podUID string, labels map[string]string, req MintRequest) (MintRequest, error)
AuthorizePod binds a credential request to the source Pod resolved by the gateway. Namespace/workspace and every session target must match; a sibling sandbox can know these public values but its immutable labels will differ.
type MintResponse ¶
type MintResponse struct {
Username string `json:"username"`
Token string `json:"token"`
ExpiresAt string `json:"expiresAt"`
}
MintResponse is intentionally the minimal git credential response. It is always emitted with Cache-Control: no-store and must never be logged.
type Minter ¶
type Minter struct {
GitHub GitHubClient
Connections ConnectionStore
Sessions SessionStore
Audit core.AuditSink
Now func() time.Time
}
Minter is bex-api's internal-only credential verb. It trusts only a request authenticated by Handler's HMAC and still rechecks branch/repository policy.
func (*Minter) Mint ¶
func (m *Minter) Mint(ctx context.Context, req MintRequest) (response MintResponse, err error)
type ModelClient ¶
ModelClient is the gateway's HMAC-authenticated caller for bex-api's internal model-mint verb. It never logs or persists the returned credential.
func (*ModelClient) Mint ¶
func (c *ModelClient) Mint(ctx context.Context, req ModelMintRequest) (ModelMintResponse, error)
type ModelHandler ¶
type ModelHandler struct {
Secret []byte
Minter *ModelMinter
// Nonce is the single-use replay guard (durable, cross-replica). Wired to the
// control-plane store in production; nil disables the check (dev/store-off).
Nonce NonceClaimer
Now func() time.Time
}
ModelHandler authenticates the gateway→bex-api internal model-mint request. It is mounted only on :8091 and never under the public bex-api router.
func (*ModelHandler) ServeHTTP ¶
func (h *ModelHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
type ModelMintRequest ¶
type ModelMintRequest struct {
SessionID string `json:"sessionId"`
Workspace string `json:"workspace"`
PodName string `json:"podName"`
PodUID string `json:"podUid"`
Nonce string `json:"nonce,omitempty"`
}
ModelMintRequest carries only the verified session identity — no repository or branch (unlike the Git MintRequest); the model key is not repo-scoped. It contains no credential. Nonce is the per-request single-use replay guard (see MintRequest.Nonce), bound by the HMAC because it rides inside the signed body.
func AuthorizeSessionPod ¶
func AuthorizeSessionPod(namespace, podName, podUID string, labels map[string]string, sessionID string) (ModelMintRequest, error)
AuthorizeSessionPod binds a model-credential request to the source Pod the gateway resolved by TCP source IP. It is the session-scoped subset of AuthorizePod: namespace/workspace and the session label must match, but there is no repository/branch to check. A sibling sandbox knows the public session id but its immutable session label differs.
type ModelMintResponse ¶
type ModelMintResponse struct {
Credential string `json:"credential"`
EndpointHost string `json:"endpointHost"`
Scheme string `json:"scheme"`
}
ModelMintResponse is the minimal model-credential response. It is always emitted with Cache-Control: no-store and must never be logged. EndpointHost is the validated vendor host the proxy forwards to (host only — the vendor path comes from the agent's own request); Scheme names how to inject Credential.
type ModelMinter ¶
type ModelMinter struct {
Keys core.SecretKV
Sessions SessionStore
Audit core.AuditSink
Now func() time.Time
}
ModelMinter is bex-api's internal-only model-credential verb. It trusts only a request authenticated by ModelHandler's HMAC and re-checks the session's current lifecycle before releasing the workspace's BYO key from OpenBao — the same fail-closed gate as the Git minter (a terminal/canceling session, whose sandbox may survive the ADR054 grace window, gets nothing).
func (*ModelMinter) Mint ¶
func (m *ModelMinter) Mint(ctx context.Context, req ModelMintRequest) (response ModelMintResponse, err error)
type ModelProxyRoute ¶
type ModelProxyRoute struct {
BaseURLEnv string `json:"baseUrlEnv"`
BaseURLSuffix string `json:"baseUrlSuffix"`
CredentialEnv string `json:"credentialEnv"`
}
ModelProxyRoute describes how the driver points a provider SDK at the gateway model proxy for a profile.
type NonceClaimer ¶
type NonceClaimer interface {
ClaimShellNonce(ctx context.Context, nonce string, expiresAt time.Time) (bool, error)
}
NonceClaimer is the durable single-use guard for internal mint requests. Its production wiring is *store.PGStore.ClaimShellNonce — the same authoritative INSERT…ON CONFLICT single-use table the gateway's ticket NonceGuard uses, so a replayed mint request (identical timestamp+signature+body captured on the internal hop) is rejected exactly once across every bex-api replica. A nil claimer disables the check (store-off/dev; byte-identical to before the nonce existed). The claim is domain-prefixed ("agent-mint:") so a mint nonce and a gateway ticket nonce never collide in the shared table.
type SessionStore ¶
type SessionStore interface {
GetAgentSession(context.Context, string) (store.AgentSession, error)
}
SessionStore reads the durable agent-session lifecycle record. Mint consults it to refuse a credential for a session that is no longer active (codex F12).