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
- Variables
- func FormatCommand(argv []string) string
- func ParseCommand(value string) ([]string, error)
- func RemoveLegacyHostDeposits(path string) (int, error)
- func Serve(ctx context.Context, r io.Reader, w io.Writer, ...) error
- type ErrorCode
- type HelperError
- type Request
- type Response
- type ResponseError
- type Token
Constants ¶
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.
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 ¶
var ErrNoSession = &HelperError{Code: CodeNoSession}
ErrNoSession matches (errors.Is) a refusal with CodeNoSession.
Functions ¶
func FormatCommand ¶
FormatCommand renders argv as a HelperEnv value. Always the JSON-array form, so a host never has to think about quoting.
func ParseCommand ¶
ParseCommand turns HelperEnv's value into an argv. An empty or blank value means no helper is configured and returns (nil, nil).
func RemoveLegacyHostDeposits ¶
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" // 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 ¶
Token is a successful helper answer.
func Exec ¶
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.