Documentation
¶
Overview ¶
Package credentials implements governance R9 — Just-In-Time credential dispensing for per-action least privilege.
The design is a two-tier plugin system: a Provider knows how to mint short-lived Credentials for a particular backend (AWS STS, HashiCorp Vault, RFC 8693 token exchange, etc.); a Credential yields a concrete Materialization (env vars, headers) valid for exactly one tool invocation.
The runner calls Provider.NewCredential once per (skill, tool) pair at startup, then calls Credential.Materialize on every BeforeToolExec hook fire. This lets a provider batch expensive setup (e.g. AWS credential resolution) once while still giving each tool call a fresh scope-down.
See docs/security/least-privilege-credentials.md for the operator side.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var DefaultRegistry = NewRegistry()
DefaultRegistry is the package-level registry. Providers that live in `credentials/*` subpackages register into this via init(), so importing the subpackage is the only wiring an operator has to do.
var ErrUnknownProvider = errors.New("credentials: unknown provider")
ErrUnknownProvider is returned when a CredentialSpec references a plugin that wasn't in the Registry.
Functions ¶
This section is empty.
Types ¶
type AuditSink ¶
AuditSink is the narrow interface Injector uses to emit credential_issued / credential_revoked / credential_failed events. Kept minimal (map-based fields) so the credentials package doesn't depend on the runtime package (avoiding an import cycle — forge-core/runtime/audit.go already imports agentspec which imports this package).
Runner startup wires a small adapter that implements this against the real *runtime.AuditLogger.
type Credential ¶
type Credential interface {
// Materialize mints one JIT credential for the given tool call.
// `args` is the raw JSON the LLM is about to pass to the tool —
// providers may inspect it to further scope down (e.g. read the
// S3 key path and constrain the STS session policy). Providers
// that don't care about args should ignore it.
Materialize(ctx context.Context, tool string, args json.RawMessage) (Materialization, error)
// Kind returns the provider name (e.g. "sts_assume_role"). Used
// on audit events so operators can filter by credential source.
Kind() string
}
Credential is what a provider hands back from NewCredential — a reusable factory the runner calls once per tool invocation.
Implementations MUST be safe for concurrent Materialize calls when the same skill's tool is invoked from multiple goroutines. Each Materialize call SHOULD produce a distinct credential; providers that support caching (STS creds valid for 15m) MAY reuse a materialization within its TTL but MUST NOT return one past expiration.
type CredentialSpec ¶
type CredentialSpec struct {
Tool string `json:"tool,omitempty" yaml:"tool,omitempty"`
Binary string `json:"binary,omitempty" yaml:"binary,omitempty"`
Provider string `json:"provider" yaml:"provider"`
Spec json.RawMessage `json:"spec,omitempty" yaml:"spec,omitempty"`
}
CredentialSpec is the declarative shape a skill's config uses to describe one JIT credential.
Tool + Binary route the credential to a specific tool call:
- Tool empty → credential applies to every tool.
- Tool set → credential applies only to that tool.
- Binary set → additionally scoped to cli_execute invocations of that binary (ignored for non-cli_execute tools).
Provider names the plugin (e.g. "sts_assume_role", "static"). Spec is opaque JSON the provider decodes into its own config struct.
func (CredentialSpec) MatchesTool ¶
func (spec CredentialSpec) MatchesTool(tool, binary string) bool
MatchesTool reports whether spec applies to the given tool + binary pair. Used by the runner to select the right CredentialSpec from a skill's list on each BeforeToolExec fire.
func (*CredentialSpec) UnmarshalYAML ¶
func (c *CredentialSpec) UnmarshalYAML(node *yaml.Node) error
UnmarshalYAML lets `CredentialSpec` round-trip through `gopkg.in/yaml.v3`. Without this, `Spec json.RawMessage` fails to decode from any YAML shape (`cannot unmarshal !!map into json.RawMessage`) — that broke `forge.yaml` config loading of the `credentials:` block, flagged in @initializ-mk's #236 second review.
Strategy: decode the wrapping fields via a type-alias to avoid recursion; then decode the `spec` sub-node into a generic Go value and re-encode as JSON so providers keep receiving canonical `json.RawMessage` bytes. Pattern mirrors `forge-skills/contract/types.go`'s `BinRequirement.UnmarshalYAML`.
type Duration ¶
type Duration string
Duration is a JSON-friendly time.Duration wrapper. Values marshal as strings like "15m" / "1h" so config files stay readable and audit events stay grep-friendly. We deliberately do not export a full time.Duration to avoid tying the audit schema to Go's nanosecond string form ("15m0s").
type Handle ¶
type Handle struct {
// contains filtered or unexported fields
}
Handle is the per-invocation grip on a materialized credential. The caller invokes Close() when the tool has finished so the injector can revoke (if applicable) and emit the credential_revoked audit event.
func (*Handle) Close ¶
Close revokes the credential (if the provider supports it) and emits credential_revoked. Idempotent — safe to defer.
The emitted event carries a `revoked` bool distinguishing:
- `revoked: true` — the provider had a Revoke callback and it was invoked; the credential is invalidated at the source.
- `revoked: false` — the provider has no Revoke path (`self_expiring: true`); the credential remains live until its TTL. Applies to STS + static providers today.
Reviewer @initializ-mk asked for this on #236 — pre-fix, credential_revoked was emitted even when nothing was invalidated, so operators couldn't distinguish hard-revoke from tool-finished.
func (*Handle) Env ¶
Env returns the env vars to inject into a subprocess. Never returns nil — an empty map is fine to append to a slice.
type Injector ¶
type Injector struct {
// contains filtered or unexported fields
}
Injector materializes JIT credentials at tool-exec time. Tools that need credentials (currently cli_execute) call Materialize(...) and merge the resulting env into their subprocess env; the returned Handle is used to schedule revocation after the tool completes.
Injector is a thin coordinator over a set of Credential instances resolved at startup — the resolution → mint → audit path lives here so individual tools don't each duplicate provider lookup + logging.
func NewInjector ¶
func NewInjector(ctx context.Context, reg *Registry, specs []CredentialSpec, audit AuditSink) (*Injector, error)
NewInjector resolves each CredentialSpec against reg and returns an Injector. Any spec that references an unregistered provider fails startup — runners want a loud config error, not silent omission that would leave a tool running without credentials.
func (*Injector) Empty ¶
Empty reports whether the injector has any resolved specs. Tools can use this to short-circuit the materialize call in the common no-JIT-configured case.
func (*Injector) Materialize ¶
func (i *Injector) Materialize(ctx context.Context, tool, binary string, args json.RawMessage) (*Handle, error)
Materialize looks up the first spec whose Tool+Binary matches, mints a fresh Credential via Provider, and returns a Handle carrying the Materialization plus a Close func to revoke it. Nil Handle when no spec matched — the caller carries on without JIT env.
`args` is the raw JSON the LLM passed the tool; providers can inspect it to further scope down (e.g. read the S3 key path).
type Materialization ¶
type Materialization struct {
Env map[string]string
Headers map[string]string
TTL Duration
Revoke func(context.Context) error
}
Materialization is what a Credential produces at tool-call time.
Env holds environment variables to inject into a subprocess (for cli_execute) or otherwise pass to the tool. Headers is for HTTP tool calls that sign or authenticate outbound requests. TTL is the operator-facing lifetime of the underlying secret — used for audit and to schedule revocation. Revoke, when non-nil, is called by the runner after the tool completes.
type Provider ¶
type Provider interface {
// Name returns the plugin name matched against CredentialSpec.Provider.
Name() string
// NewCredential decodes spec (the plugin-specific JSON payload)
// and returns a reusable Credential. Called once per matching
// CredentialSpec at runner startup.
NewCredential(ctx context.Context, spec CredentialSpec) (Credential, error)
}
Provider is the plugin that mints Credentials. One provider instance per registered plugin name — the runner looks it up once at startup and reuses it for every skill.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry holds Provider instances by name. A single Registry per runtime; providers register themselves at init time by calling Register() on the package-level default, or a fresh Registry can be constructed for tests.
func (*Registry) Get ¶
Get returns the provider by name. Returns nil when the operator referenced a provider that wasn't wired — the caller (runner startup) reports this as a config error.
func (*Registry) Names ¶
Names returns the sorted set of registered provider names. Used on startup logs so operators can confirm which plugins are wired.
func (*Registry) Register ¶
Register adds p to the registry. Panics on duplicate name — a startup misconfiguration bug that should not survive to production.
func (*Registry) ResolveSpec ¶
func (r *Registry) ResolveSpec(ctx context.Context, spec CredentialSpec) (Credential, error)
ResolveSpec looks up spec.Provider in r and constructs the Credential. Returns ErrUnknownProvider (wrapped) when not registered.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package static is the reference no-op Credential provider for governance R9.
|
Package static is the reference no-op Credential provider for governance R9. |
|
Package sts is the AWS STS AssumeRole reference provider for governance R9 (JIT credential dispensing).
|
Package sts is the AWS STS AssumeRole reference provider for governance R9 (JIT credential dispensing). |