Documentation
¶
Overview ¶
Package ux is the CLI's audience-aware I/O layer (impl/3.1, impl/3.2). Commands never call fmt.Println / huh directly — they go through an Audience, so the CLI shape-shifts by mode (human vs agent/service-account) and every output byte, on BOTH stdout and stderr, passes the fail-closed redaction funnel in this file.
Index ¶
- Constants
- func IsSensitiveKey(key string) bool
- func MarshalForFile(data any) []byte
- func RedactBytes(data []byte) []byte
- func RegisterSensitiveFields(pkgPath string, table map[string][]string)
- func WithAudience(ctx context.Context, aud Audience) context.Context
- func WriteJSONLine(w io.Writer, v any) error
- type AgentError
- type AgentUX
- func (a *AgentUX) Ask(_, flagName string, _ bool) (string, error)
- func (a *AgentUX) AskConfirm(_ string) (bool, error)
- func (a *AgentUX) ForcesNoColor() bool
- func (a *AgentUX) IsFenced() bool
- func (a *AgentUX) Render(data any)
- func (a *AgentUX) ReportError(err error, step string)
- func (a *AgentUX) Theme() Palette
- type Audience
- type CodedError
- type Export
- type HumanUX
- func (h *HumanUX) Ask(question, flagName string, required bool) (string, error)
- func (h *HumanUX) AskConfirm(warning string) (bool, error)
- func (h *HumanUX) ForcesNoColor() bool
- func (h *HumanUX) IsFenced() bool
- func (h *HumanUX) Render(data any)
- func (h *HumanUX) ReportError(err error, step string)
- func (h *HumanUX) Theme() Palette
- type List
- type Page
- type Palette
- type Plan
- type Result
Constants ¶
const ( ExitOK = 0 // success (for execute: broker returned an upstream response, even a 4xx/5xx) ExitError = 1 // generic/local failure: transport, missing arg, fencing, CLI bug ExitDenied = 2 // Jentic said no: broker policy denial or resolve failure — change the ask, don't retry ExitTimeoutPending = 3 // --wait expired while still pending: retry later is meaningful ExitPartial = 4 // partial approval of a composite request: inspect per-item status )
Exit-code taxonomy (13 §4). core.Run maps any ExitCoder to these; nothing calls os.Exit directly (arch rule).
const ( CodeMissingArgument = "MISSING_ARGUMENT" CodeConfirmBlocked = "INTERACTIVE_CONFIRM_BLOCKED" CodeFenced = "FENCED_COMMAND" CodeNotAuthenticated = "NOT_AUTHENTICATED" CodePendingApproval = "PENDING_APPROVAL" CodeResolveFailed = "RESOLVE_FAILED" CodeBrokerDenied = "BROKER_DENIED" CodeTimeoutPending = "TIMEOUT_PENDING" CodePartialApproval = "PARTIAL_APPROVAL" CodeConfinementMissing = "CONFINEMENT_UNAVAILABLE" CodeTransportError = "TRANSPORT_ERROR" CodeInternalError = "INTERNAL_ERROR" // CodeMigrationRequired gates every command on an unmigrated V1 machine // (legacy ~/.jentic profiles, no MIGRATED marker): the V2 CLI no longer // reads the legacy store, so nothing can run until `jentic migrate` copies // it into the XDG context model. actionable_step is always "jentic migrate". CodeMigrationRequired = "MIGRATION_REQUIRED" )
error_code enum (13 §3a). Closed set: agents treat an unknown code as INTERNAL_ERROR-like ("stop and report"). New codes are additive; add them here AND to errorCodeExit.
const ( StatusCreated = "created" StatusAdded = "added" StatusUpdated = "updated" StatusSwitched = "switched" StatusDeleted = "deleted" StatusRegistered = "registered" StatusPending = "pending" )
Status verbs — the closed set of outcomes a Result may report. Named constants (not literals per call site) prevent the "added"/"created"/"switched" drift the map[string]string approach allowed. Mirrors 13 §2.
Variables ¶
This section is empty.
Functions ¶
func IsSensitiveKey ¶
IsSensitiveKey is the exported form of the redactor's secret-shaped-key predicate. It exists so the architecture sweep (tests/arch, Test1H) asserts against the ACTUAL runtime heuristic — allowlist included — rather than a hand-maintained copy that can silently drift (F8-35). It is the single source of truth for "does this property name look like a secret".
func MarshalForFile ¶
MarshalForFile is the exported, redacted, indented marshal for commands that write an envelope to a file (e.g. `history export -o out.json`) instead of through the Audience. It runs the SAME three-layer redaction funnel and schema-version stamping as stdout output, so a secret can never leak just because the destination is a file. Indented for human readability; still valid machine JSON.
func RedactBytes ¶
RedactBytes is the exported byte-level backstop for command code that emits a raw upstream/API body (e.g. the `jentic api` passthrough, `execute --raw`, `inspect`, `apis spec`) rather than a marshaled envelope. It applies the SAME redaction guarantee as every other output path so a secret in an API response can't leak to a machine parser.
When data is valid JSON it is parsed and run through the STRUCTURED key pass (redactValue), which redacts a sensitive key regardless of its value's JSON type — so a secret carried as a number, object, array, or bool is caught, not just a `"key":"string"` pair (the reKV byte regex alone matched string values only; review round-3 P0). Shape is preserved when NOTHING is redacted: the structured pass reports whether it changed anything, and if not, RedactBytes runs only the byte backstop over the ORIGINAL bytes — so shape-preserving callers like WriteJSON (which feed already-indented JSON through here) keep their exact layout and key order. Only when a sensitive value is actually redacted does the document get re-marshaled (matching the input's indentation); on that path key order may change, which is fine because the document already differs. The byte backstop then still runs to catch secrets embedded in free-form string values (bearer tokens, PEM blocks) that carry no sensitive key.
When data is NOT valid JSON (e.g. a Markdown inspect body, a YAML spec, a plain-text error) it falls back to the byte backstop alone — the same behaviour as before — because there is no structure to walk.
func RegisterSensitiveFields ¶
RegisterSensitiveFields merges a plane's generated SensitiveFields table (bare type name -> []json field names) into the redaction registry, qualifying every key with pkgPath (GEN-23) so lookups are collision-proof across planes and against app structs. Idempotent and additive; call once per plane at startup. pkgPath is the plane package's import path (control/broker), which redactTagged reads back from reflect.Type.PkgPath at redaction time.
func WithAudience ¶
WithAudience stores the resolved Audience in the context for downstream commands.
func WriteJSONLine ¶
WriteJSONLine writes ONE compact, redacted JSON document followed by a newline to w. It is the streaming primitive for tail-style commands (e.g. `events watch`) that emit an unbounded NDJSON sequence rather than a single terminal envelope — Render is for one final document, this is for a stream. It runs the same redaction funnel as every other output path, so a streamed event cannot leak a secret. Errors from the underlying writer are returned so the caller can stop the tail (e.g. on a closed pipe).
Types ¶
type AgentError ¶
type AgentError struct {
SchemaVersion string `json:"schema_version"`
ErrorCode string `json:"error_code"`
Error string `json:"error"`
Actionable string `json:"actionable_step,omitempty"`
Details map[string]any `json:"details,omitempty"`
}
AgentError is the stderr error envelope — the Go shape of the contract in 13 §3. That document is canonical; this struct mirrors it.
type AgentUX ¶
type AgentUX struct {
// contains filtered or unexported fields
}
AgentUX is the ruthlessly strict machine mode shared by `agent` and `service-account` (impl/3.1 §0): never prompts, no color, one JSON document per Render on stdout, structured error envelope on stderr.
func NewAgentUX ¶
NewAgentUX builds the agent audience. The palette is always no-color.
func (*AgentUX) AskConfirm ¶
AskConfirm returns true only if the global --yes pre-authorized; otherwise it returns an INTERACTIVE_CONFIRM_BLOCKED CodedError.
func (*AgentUX) ForcesNoColor ¶
ForcesNoColor reports that agent output must never carry ANSI (protects LLM parsers).
func (*AgentUX) ReportError ¶
ReportError writes the redacted structured error envelope to stderr.
type Audience ¶
type Audience interface {
// Ask prompts for missing information. Humans get a TUI prompt; agents get a
// typed CodedError explaining the missing flag (no interactive fallback).
Ask(question, flagName string, required bool) (string, error)
// AskConfirm prompts for a destructive action. Humans get [y/N]; agents
// auto-reject unless the global --yes was passed.
AskConfirm(warning string) (bool, error)
// Render writes a success payload to stdout. Intentionally VOID: the only
// realistic failure is a marshal error on an exotic value (channel/func) — a
// programmer bug — which marshalRedacted turns into a guaranteed-encodable error
// envelope, so stdout always carries exactly one valid JSON document. Callers
// treat Render as a terminal "this succeeded".
Render(data any)
// ReportError writes a failure to stderr (never stdout — stdout is reserved for
// Render). Humans get a red line; agents get the structured error envelope. Both
// pass the byte-level redaction backstop (review M6).
ReportError(err error, actionableNextStep string)
// Theme returns the active palette.
Theme() Palette
// IsFenced reports whether this mode is forbidden from state-mutating admin
// commands (the root interceptor enforces it — impl/3.2 §2).
IsFenced() bool
// ForcesNoColor reports whether ANSI must be suppressed regardless of theme, to
// protect downstream machine parsers.
ForcesNoColor() bool
}
Audience is the sole gatekeeper between a command's business logic and the caller's terminal. Commands must NEVER call fmt.Println / huh directly — they go through an Audience so the CLI shape-shifts by mode (impl/3.1 §1).
func FromContext ¶
FromContext retrieves the Audience. If none is present it FAILS CLOSED to strict agent mode (never a human prompt): reaching this branch means the root PersistentPreRunE didn't run — a wiring bug. Silently dropping a human into no-prompt agent mode is confusing to debug, so warn loudly on stderr (never stdout — that must stay machine-parseable).
type CodedError ¶
type CodedError struct {
Code string // e.g. "MISSING_ARGUMENT", "BROKER_DENIED"
Msg string // human/LLM-readable prose (redacted on output)
Actionable string // machine-runnable recovery step, when one exists
Details map[string]any // e.g. {"agent_directive": ..., "http_status": 403}
// contains filtered or unexported fields
}
CodedError is the typed error every fenced/validation/denial path returns so the error envelope can carry a closed-enum error_code. The enum values and their exit codes are owned by 13 §3a — do not invent codes here; add them to errorCodeExit in contract.go.
func (*CodedError) Error ¶
func (e *CodedError) Error() string
func (*CodedError) ExitCode ¶
func (e *CodedError) ExitCode() int
ExitCode makes every CodedError satisfy pkg/core's ExitCoder mechanically, so core.Run maps the closed enum to the exit taxonomy (13 §4) with no per-command wiring. Without it, a forgotten wrapper would exit 1 even for BROKER_DENIED (exit 2) or TIMEOUT_PENDING (exit 3). The code->exit table lives in contract.go.
func (*CodedError) IsReported ¶
func (e *CodedError) IsReported() bool
IsReported reports whether an Audience already rendered this error.
func (*CodedError) MarkReported ¶
func (e *CodedError) MarkReported()
MarkReported flags the error as already rendered by an Audience. Called by every ReportError implementation; command code never needs it.
type Export ¶
type Export struct {
SchemaVersion string `json:"schema_version"`
// TraceID is the correlation pivot the export was queried by (impl/5.0 §2).
TraceID string `json:"trace_id,omitempty"`
// Items is the exported records (kept as `any` so the same envelope serves any
// export; history passes []control.ExecutionResponse).
Items any `json:"items"`
// Count is the number of exported records after filtering.
Count int `json:"count"`
}
Export is the versioned envelope for bulk export commands (history export). It wraps a fully-walked, filtered result set (never a single page) plus the pivot it was queried by, so an agent can template a workflow from a run's real executions. SchemaVersion is stamped at render time like the other envelopes.
type HumanUX ¶
type HumanUX struct {
// contains filtered or unexported fields
}
HumanUX is the interactive mode: huh prompts, themed accents, indented JSON / formatted lines on stdout, a red error line on stderr.
func NewHumanUX ¶
NewHumanUX builds the human audience with the resolved palette and the global --yes setting.
func (*HumanUX) Ask ¶
Ask prompts the human for a value, or fails with a missing-flag CodedError when the session is not promptable.
func (*HumanUX) AskConfirm ¶
AskConfirm asks the human for y/N confirmation (auto-yes when --yes was set).
func (*HumanUX) ForcesNoColor ¶
ForcesNoColor reports that human mode respects theme/color preferences.
func (*HumanUX) Render ¶
Render writes data to stdout: Page footers, Result status lines, or indented JSON.
func (*HumanUX) ReportError ¶
ReportError writes a redacted, styled error line (and optional next step) to stderr.
type List ¶
type List struct {
SchemaVersion string `json:"schema_version"`
Data any `json:"data"`
// HasMore/NextCursor mirror the Jentic list APIs' cursor pagination. NextCursor
// is omitted when empty; HasMore is always present so an agent can branch on it
// without a null check.
HasMore bool `json:"has_more"`
NextCursor string `json:"next_cursor,omitempty"`
// Meta carries command-specific summary fields that are NOT the collection
// itself (e.g. catalog's catalog_total / manifest_age_seconds). Kept under one
// key so the top level stays a stable, closed shape across every list command;
// omitted entirely when a command has no summary.
Meta map[string]any `json:"meta,omitempty"`
}
List is the canonical, versioned envelope for data-plane list output (AGT-1/ AGT-5). Before this, list commands wrote ad-hoc maps: most used {data, has_more, next_cursor} but `endpoints` used {endpoints} and NONE carried schema_version, so an agent could not (a) detect the envelope version or (b) rely on a single collection/pagination key across commands. This struct makes every list emit the same three keys plus schema_version; marshalRedacted stamps the version like the other envelopes. Data is `any` so each command passes its concrete non-nil slice (an empty result serialises as [], not null).
type Page ¶
type Page struct {
SchemaVersion string `json:"schema_version"`
Items any `json:"items"`
// NextToken is the opaque cursor for the next page; empty on the last page.
NextToken string `json:"next_token,omitempty"`
}
Page is the pagination envelope for list commands (13 §2, impl/3.3). It carries the underlying items plus the opaque cursor for the next page. Cursor form only — Jentic list APIs are cursor-paginated (data + has_more + next_cursor); there is no page-number UX.
func NewPage ¶
NewPage builds a Page envelope from a typed item slice and the opaque next-page cursor (empty on the last page). It is the bridge from the SDK's client/paginate walk helper (whose Page[T].Next is the cursor) to the machine contract's rendered envelope: a command walks pages with paginate.All/ForEach, then hands the collected items here for Render. Kept generic so callers pass their concrete []T without an interface conversion at the call site.
type Plan ¶
type Plan struct {
SchemaVersion string `json:"schema_version"`
Operation string `json:"operation"`
DryRun bool `json:"dry_run"`
Payload any `json:"payload,omitempty"`
}
Plan is the machine-readable execution plan emitted for --dry-run/--export-plan (impl/5.0 §5): the operation a mutating command WOULD invoke and the hydrated payload it would send, without firing the call. It runs through the same redaction funnel as every other rendered value, so a plan can't leak a secret in its payload. SchemaVersion is stamped at render time like the other envelopes.
type Result ¶
type Result struct {
// SchemaVersion pins the envelope shape for agents (13 §2/§6). Call sites may
// leave it empty; marshalRedacted stamps the current version at render time.
SchemaVersion string `json:"schema_version"`
// Status is the outcome verb (StatusCreated, StatusSwitched, ...).
Status string `json:"status"`
// Resource is the kind of thing acted on ("environment", "context", "identity").
Resource string `json:"resource,omitempty"`
// Name is the human/identifier name of the affected resource, when it has one.
Name string `json:"name,omitempty"`
// ID is the server- or client-assigned identifier, when one was produced.
ID string `json:"id,omitempty"`
// Message is optional human-oriented context (e.g. "awaiting operator approval").
Message string `json:"message,omitempty"`
// Fields is the escape hatch for genuinely command-specific data. Prefer a
// first-class field above; use this only when nothing else fits.
Fields map[string]any `json:"fields,omitempty"`
}
Result is the canonical success payload for state-mutating commands (context use, env add, identity add, credential create, identity register, ...). It replaces ad-hoc map[string]string literals: those had no compile-time safety and drifted on key names and status verbs. Sensitive fields MUST use `redact:"true"` so layer 1 scrubs them — never put a raw secret in Fields.