Help us shape the future of Go! Take the 2026 Go Developer Survey

cloudcred

package
v1.0.2 Latest Latest
Warning

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

Go to latest
Published: Oct 29, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

Documentation

Overview

Package cloudcred is the PUBLIC seam between forge and a HOST APPLICATION that already holds the user's session — the credential-helper protocol.

── WHY THIS EXISTS: THE DEPENDENCY IS INVERTED ───────────────────────

A user signed in to an application that embeds or launches forge (Reliant) must not have to run `forge login` as well. The obvious way to get that — teach forge to go and find that application's credential — is the one thing forge must never do: internal/cloud's package header forbids importing control-plane or reliant, because a user deploying to hosted infra without the reliant harness must never have to install reliant first.

So forge publishes a PROTOCOL and the host implements it. When the environment variable HelperEnv names a command, forge runs it, writes a Request (which control plane, which scopes) to its stdin, and reads a Response (a token, or a structured refusal) from its stdout. It is the model kubectl's exec credential plugins, git's credential helpers and AWS's credential_process already use, for the same reason: the process that holds the session mints what the tool needs, at the moment it needs it.

── WHY A HELPER AND NOT A DEPOSIT ────────────────────────────────────

This package used to let a host DEPOSIT its own token into forge's credentials file. That copied a permanent, multi-purpose session credential (one that could also connect as the user's daemon) into a second file, and forge presented it to deploy endpoints that — correctly — refused it, because it did not hold deploy authority. A token may never grant authority it does not hold (forge/pkg/accesstoken). A helper fixes both halves: the session credential never leaves the host, and what forge receives is a short-lived token the host obtained for exactly the scopes forge asked for. RemoveLegacyHostDeposits is how a host cleans up what the old seam wrote.

── THE PROCESS BOUNDARY IS THE CONTRACT ─────────────────────────────

The helper is a separate process even when the host embeds forge in its own binary. One mechanism means one thing to test and one thing to document, and the same helper serves a forge the host launched directly, a forge an agent ran in a shell the host spawned, and a forge the host embeds. Exec is forge's half of the protocol; Serve is the host's.

Nothing here reads the environment (forge/pkg never does — see pkg/.golangci.yml). The application edge reads HelperEnv and passes the parsed command in.

Index

Constants

View Source
const HelperEnv = "FORGE_CREDENTIAL_HELPER"

HelperEnv is the environment variable that names the credential helper.

Its value is either a JSON array of strings — the helper's argv, which is what a host sets programmatically, so an executable path containing spaces survives on every OS — or a plain string naming a single executable, for a human who points it at a script. A plain string is never split on spaces: shell-style splitting is exactly the thing that differs between platforms.

View Source
const ProtocolVersion = 1

ProtocolVersion is the version of the Request/Response shapes. A Response carrying a different version is refused rather than half-understood.

Variables

View Source
var ErrNoSession = &HelperError{Code: CodeNoSession}

ErrNoSession matches (errors.Is) a refusal with CodeNoSession.

Functions

func FormatCommand

func FormatCommand(argv []string) string

FormatCommand renders argv as a HelperEnv value. Always the JSON-array form, so a host never has to think about quoting.

func ParseCommand

func ParseCommand(value string) ([]string, error)

ParseCommand turns HelperEnv's value into an argv. An empty or blank value means no helper is configured and returns (nil, nil).

func RemoveLegacyHostDeposits

func RemoveLegacyHostDeposits(path string) (int, error)

RemoveLegacyHostDeposits deletes every credential the retired deposit seam stored in the credentials file at path, and reports how many there were.

Those entries are copies of a host's permanent session credential. forge no longer reads them, so they are dead weight at best and a second copy of a bearer secret at worst. A host that used to deposit calls this (best-effort) when it next signs in or starts. Entries any other client wrote — including a human's `forge login` — are untouched. A missing file removes nothing.

func Serve

func Serve(ctx context.Context, r io.Reader, w io.Writer, mint func(context.Context, Request) (Token, error)) error

Serve is the host's half: read one Request from r, call mint, write one Response to w.

mint's error decides the answer. A *HelperError (errors.As) is sent with its code and message; any other error is sent as CodeUnavailable with its text — so a mint function must never put a secret in an error. Serve returns an error only when it cannot read the request or write the response; a refusal is a successful exchange, and the helper process should exit 0 after it.

Types

type ErrorCode

type ErrorCode string

ErrorCode classifies a helper's refusal, so forge can word the failure without parsing prose.

const (
	// CodeNoSession: the host holds no session that applies to this endpoint
	// (not signed in, or signed in somewhere else).
	CodeNoSession ErrorCode = "no_session"
	// CodeDenied: a session applies, but it cannot authorize this — for
	// example it was issued without deploy permission. Signing in again or
	// granting the permission is the fix, not retrying.
	CodeDenied ErrorCode = "denied"
	// CodeUnavailable: the helper could not reach whatever mints tokens.
	// Transient; retrying later may succeed.
	CodeUnavailable ErrorCode = "unavailable"
)

type HelperError

type HelperError struct {
	Code    ErrorCode
	Message string
	// Reason and Scopes carry the minting server's structured refusal, when
	// it gave one; see ResponseError. Optional, and never part of Is.
	Reason string
	Scopes []string
}

HelperError is a helper's well-formed refusal. Exec returns it (use errors.As); a host's mint function returns it from Serve's callback to choose the code.

func (*HelperError) Error

func (e *HelperError) Error() string

func (*HelperError) Is

func (e *HelperError) Is(target error) bool

Is makes errors.Is(err, &HelperError{Code: X}) match on the code alone.

type Request

type Request struct {
	Version int `json:"version"`
	// Endpoint is the control-plane origin the token will be PRESENTED to,
	// normalized (credentials.Normalize). A helper must refuse to answer for
	// an endpoint its session does not belong to: the endpoint comes from a
	// project's KCL, and a repository is not a trusted party.
	Endpoint string `json:"endpoint"`
	// Scopes are what forge would like. A helper may grant fewer — never more
	// than its session holds — and reports what it granted.
	Scopes []string `json:"scopes,omitempty"`
}

Request is what forge writes to the helper's stdin.

type Response

type Response struct {
	Version int    `json:"version"`
	Token   string `json:"token,omitempty"`
	// ExpiresAt is when Token stops being accepted; nil means the helper does
	// not know. forge re-runs the helper rather than present an expired token.
	ExpiresAt *time.Time `json:"expires_at,omitempty"`
	// Scopes the token carries, as the issuer reported them. Display only.
	Scopes []string `json:"scopes,omitempty"`
	// Source names where the token came from, for a human ("Reliant session
	// for https://api.example.com"). forge prints it; it must never contain
	// the token.
	Source string         `json:"source,omitempty"`
	Error  *ResponseError `json:"error,omitempty"`
}

Response is what the helper writes to stdout. Exactly one of Token and Error is set.

type ResponseError

type ResponseError struct {
	Code ErrorCode `json:"code"`
	// Message is actionable and safe to print; it never contains a token.
	Message string `json:"message"`
	// Reason is the stable reason the minting server gave, when it gave one
	// (its svcerr.ReasonHeader) — "scope_missing" for a denial over missing
	// authority. forge reports it in its --json documents so a host's UI
	// can route on it without parsing Message. Optional.
	Reason string `json:"reason,omitempty"`
	// Scopes are the scopes a "scope_missing" refusal names (the server's
	// accesstoken.ScopesHeader). Optional.
	Scopes []string `json:"scopes,omitempty"`
}

ResponseError is the wire form of a refusal.

type Token

type Token struct {
	Token     string
	ExpiresAt *time.Time
	Scopes    []string
	Source    string
}

Token is a successful helper answer.

func Exec

func Exec(ctx context.Context, argv []string, req Request) (Token, error)

Exec runs the helper argv with req and returns its token.

ctx bounds the run; give it a deadline. The helper inherits this process's environment. Errors:

  • *HelperError — the helper answered with a refusal; its Message is the host's own actionable advice and is safe to print.
  • anything else — the helper could not be run, failed, or answered with something that is not a protocol response.

stdout is NEVER included in an error: on a malformed answer it may still hold a token. A failing helper's stderr is included, bounded.

func (Token) Expired

func (t Token) Expired(now time.Time, margin time.Duration) bool

Expired reports whether the token expires within margin of now. A token with no known expiry never does.

Jump to

Keyboard shortcuts

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