agentsession

package
v0.0.0-...-7892194 Latest Latest
Warning

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

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

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

View Source
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/"
)
View Source
const (
	AuditVerbMintCredential  = "agentsessions.MintCredential"
	AuditVerbProxyCredential = "agentsessions.ProxyCredential"
)
View Source
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"
)
View Source
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.

View Source
const AuditVerbMintModelCredential = "agentsessions.MintModelCredential"

AuditVerbMintModelCredential records credential issuance. The gateway mints once per provider exchange so every request revalidates the session lifecycle.

Variables

View Source
var (
	ErrInvalidRequest = errors.New("invalid agent credential request")
	ErrForbidden      = errors.New("agent credential request forbidden")
)

Functions

func AgentProfileCommand

func AgentProfileCommand(agent string) string

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

func AgentProfileRuntimeJSON(agent string) (argsJSON, envJSON string)

AgentProfileRuntimeJSON renders args/env JSON for the sandbox driver environment.

func BindingLabels

func BindingLabels(sessionID, repository, branch string) (map[string]string, error)

BindingLabels is the integration contract for the future session-create path: stamp these values into OpenSandbox create metadata. No secret is present.

func IsModelKeyPlaceholder

func IsModelKeyPlaceholder(value, sessionID string) bool

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

func ModelKeyPlaceholder(sessionID string) string

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

func ModelKeySecretPath(workspaceID string) string

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

func ModelProxyURL(baseURL, namespace, sessionID string) (string, error)

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

func NormalizeRepository(raw string) (string, error)

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

func ParseModelProxyPath(path string) (namespace, sessionID, upstreamPath string, err error)

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

func ProxyRepositoryURL(baseURL, namespace, sessionID, repository, branch string) (string, error)

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

func RegisteredModelEndpoint(agent, requested string) (string, error)

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

func ResolveAuthScheme(host, credential string) string

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

func SessionObject(sessionID string) string

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

func Sign(secret, body []byte, now time.Time) (timestamp, signature string)

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

func ValidateBranch(branch string) error

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.

func Verify

func Verify(secret, body []byte, timestamp, signature string, now time.Time, maxSkew time.Duration) error

Verify checks a signed internal request within maxSkew of now.

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

type Client struct {
	URL    string
	Secret []byte
	HTTP   *http.Client
	Now    func() time.Time
}

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 GitHubClient interface {
	MintSessionInstallationToken(context.Context, int64, string) (github.InstallationToken, error)
}

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.

func (*Handler) ServeHTTP

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request)

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

type ModelClient struct {
	URL    string
	Secret []byte
	HTTP   *http.Client
	Now    func() time.Time
}

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

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).

Jump to

Keyboard shortcuts

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