Documentation
¶
Overview ¶
Package remote is evolve's client for patchy's remote-evaluation service: the OIDC login flow and credential store, the HTTP/SSE client, the deterministic workspace bundler, the remote sweep orchestrator, and the bidirectional Reporter seam — EventReporter serializes run.Reporter calls onto the in-pod EVOLVE-EVENT stdout stream, ApplyEvent replays received events back onto a local Reporter, so a remote run's output is indistinguishable from a local one.
The wire types in wire.go are local copies of patchy's pkg/evaluation contract; the import swaps to the published package once patchy releases it (they must stay field-for-field identical until then).
Index ¶
- Constants
- Variables
- func ApplyEvent(rep run.Reporter, ev Event)
- func Bearer(ctx context.Context, store *Store, remoteURL string, info *AuthInfo) (string, error)
- func Login(ctx context.Context, store *Store, remoteURL string, noBrowser bool, ...) error
- func MarshalEvalEntry(e *results.EvalEntry) (json.RawMessage, error)
- func MarshalTriggerEntry(e *results.TriggerEntry) (json.RawMessage, error)
- func NormalizeRemote(u string) string
- func ReadBundle(b *Bundle) (map[string]string, error)
- func Sweep(ctx context.Context, opts SweepOptions) (failed bool, err error)
- func Truncate(s string, max int) string
- type AuthInfo
- type Bundle
- type CaseStatus
- type Client
- func (c *Client) Cancel(ctx context.Context, name string) error
- func (c *Client) Events(ctx context.Context, name string, onUnit func(UnitStatusWire), ...) (*EvaluationStatusWire, error)
- func (c *Client) HasWorkspace(ctx context.Context, digest string) (bool, error)
- func (c *Client) Status(ctx context.Context, name string) (*EvaluationStatusWire, error)
- func (c *Client) Submit(ctx context.Context, sub *Submission) (*SubmissionResponse, error)
- func (c *Client) UploadWorkspace(ctx context.Context, digest string, bundle []byte) error
- type Credential
- type EvaluationStatusWire
- type Event
- type EventReporter
- func (r *EventReporter) BaselineDone(u plan.UnitRef, item run.ItemResult)
- func (r *EventReporter) BaselineStarted(u plan.UnitRef, item run.ItemStart)
- func (r *EventReporter) Emit(ev Event)
- func (r *EventReporter) ItemDone(u plan.UnitRef, item run.ItemResult)
- func (r *EventReporter) ItemStarted(u plan.UnitRef, item run.ItemStart)
- func (r *EventReporter) UnitFinished(u plan.UnitRef, sum run.UnitSummary, _ string)
- func (r *EventReporter) UnitSkipped(u plan.UnitRef, reason string)
- func (r *EventReporter) UnitStarted(u plan.UnitRef, total, runs int, mode plan.Mode)
- func (r *EventReporter) Warn(format string, a ...any)
- type EventWire
- type HarnessOption
- type ItemEvent
- type ItemMetrics
- type JudgeSpec
- type ResultSummary
- type Store
- type Submission
- type SubmissionError
- type SubmissionResponse
- type SweepOptions
- type TokenUsage
- type UnitRef
- type UnitResult
- type UnitSpec
- type UnitStatusWire
- type UnitSummary
- type WorkspaceRef
Constants ¶
const ( EnvUnitFile = "EVOLVE_UNIT_FILE" EnvBundleDir = "EVOLVE_BUNDLE_DIR" )
Pod input environment: where the prepare container staged the unit spec (a UnitSpec JSON document) and the extracted workspace bundle.
const ( TypeUnitStarted = "unit_started" TypeUnitSkipped = "unit_skipped" TypeItemStarted = "item_started" TypeItemDone = "item_done" TypeBaselineStarted = "baseline_started" TypeBaselineDone = "baseline_done" TypeUnitFinished = "unit_finished" TypeWarn = "warn" TypeResult = "result" TypeFatal = "fatal" )
Event types. Patchy interprets only TypeResult and TypeFatal; every other type is progress, relayed verbatim to the monitoring client, which replays it onto its local reporter.
const ( SSEEventUnit = "unit" SSEEventEvent = "event" SSEEventEnd = "end" )
SSE event names on GET /api/v1/evaluations/{name}/events.
The stream is content-bearing with an explicit end event:
- On connect the server replays one "unit" event per child, so a reconnecting client rebuilds its view without Last-Event-ID; updates re-emit "unit" and the client applies them idempotently by name.
- "event" events relay raw in-pod progress lines tagged with the unit name. They are best-effort and degradable: a client must render correctly from "unit" events alone.
- "end" carries the final snapshot; the server closes after sending it.
const EventPrefix = "EVOLVE-EVENT: "
EventPrefix marks an event line on the runner's stdout; everything else the pod prints goes to stderr.
const EventVersion = 1
EventVersion is the current event schema version.
const MaxDetailLen = 4096
MaxDetailLen bounds human-facing detail strings on the wire (mirrors the CR status bound).
const MaxEntryBytes = 900 << 10
MaxEntryBytes bounds UnitResult.Entry. The pod truncates evidence fields until the marshalled entry fits — the entry is stored in a ConfigMap under Kubernetes' ~1MiB object cap, and this bound leaves headroom for the object's own metadata after gzip.
const MaxOutputLen = 16 << 10
MaxOutputLen bounds relayed item output snippets, so one chatty case cannot bloat the progress stream.
const SubmissionVersion = "v1"
SubmissionVersion is the accepted Submission.Version value.
Variables ¶
var ErrLoginExpired = errors.New("not logged in (or the session expired): run `evolve login`")
ErrLoginExpired means no usable credential exists for the remote — never stored, or its refresh token no longer works. The fix is `evolve login`.
Functions ¶
func ApplyEvent ¶
ApplyEvent replays one wire event onto a local run.Reporter: the monitor half of the seam. result and fatal events are not progress and are ignored here — the sweep handles them itself.
func Bearer ¶
Bearer returns a live bearer token for the remote, refreshing (and save-through persisting — dex rotates refresh tokens) when the stored ID token is stale. A hard refresh failure is ErrLoginExpired.
func Login ¶
func Login(ctx context.Context, store *Store, remoteURL string, noBrowser bool, out io.Writer) error
Login runs the OIDC authorization-code + PKCE flow against the remote's issuer and persists the credential in the store. The client is public (no secret); the redirect is an ephemeral localhost listener. noBrowser skips the best-effort browser open — the URL is always printed either way.
func MarshalEvalEntry ¶
func MarshalEvalEntry(e *results.EvalEntry) (json.RawMessage, error)
MarshalEvalEntry renders an eval entry within MaxEntryBytes, shedding weight in fidelity order: snapshot result arrays first (their summaries survive), then assertion evidence, then the assertions themselves.
func MarshalTriggerEntry ¶
func MarshalTriggerEntry(e *results.TriggerEntry) (json.RawMessage, error)
MarshalTriggerEntry renders a trigger entry within MaxEntryBytes, dropping the previous snapshot's per-query results first — the summary survives, and the wire's fidelity floor is the fresh results.
func NormalizeRemote ¶
NormalizeRemote canonicalizes a remote URL as the credential key: lowercase, no trailing slash.
func ReadBundle ¶
ReadBundle is a test helper: it lists a bundle's entries.
func Sweep ¶
func Sweep(ctx context.Context, opts SweepOptions) (failed bool, err error)
Sweep plans, submits, and monitors a remote run: enumerate units exactly as run.Sweep would, compute the case selection locally, bundle and dedupe workspaces, submit, then stream — progress events replay onto the Reporter, and every settled unit's entry merges into the local results file, saved with the same rotation a local run performs. failed reports graded failures (the --strict signal).
Types ¶
type AuthInfo ¶
type AuthInfo struct {
// Issuer is the OIDC issuer URL (discovery is derived from it).
Issuer string `json:"issuer"`
// ClientID of the public (PKCE, secret-less) client the login uses.
ClientID string `json:"clientID"`
// Scopes to request; the client unions in openid and offline_access.
Scopes []string `json:"scopes,omitempty"`
// Mode is the server's auth mode ("oidc" or "none"). Mode none needs no
// login at all.
Mode string `json:"mode"`
}
AuthInfo is GET /api/v1/auth/info: everything the client's login flow needs to run OIDC discovery and the PKCE authorization-code exchange, so a user configures nothing but the service URL.
type Bundle ¶
type Bundle struct {
// Digest is the hex sha256 of Data.
Digest string
// Data is the gzip tarball.
Data []byte
}
Bundle is one deterministic workspace tarball: identical inputs always produce identical bytes, so the sha256 digest doubles as the dedupe key.
func EvalsBundle ¶
EvalsBundle stages one skill's evals workspace: the skill directory plus its evals/<skill>/ tree (specs and fixtures; results files excluded).
func TriggersBundle ¶
TriggersBundle stages a plugin's triggers workspace: every sibling skill (the trigger surface is the whole skill roster) plus each skill's triggers spec — results files excluded. One bundle serves every trigger unit of the plugin.
type CaseStatus ¶
type CaseStatus struct {
// ID is the trigger query or eval id.
ID string `json:"id"`
// Passed reports the graded outcome.
Passed bool `json:"passed"`
}
CaseStatus is one case's graded outcome, compact enough for CR status.
type Client ¶
type Client struct {
// BaseURL of the service.
BaseURL string
// Store holds the login credential; Info the service's auth discovery.
Store *Store
Info *AuthInfo
// HTTPClient defaults to http.DefaultClient.
HTTPClient *http.Client
}
Client talks to a patchy remote-evaluation service, attaching the stored bearer credential (refreshed as needed) to every request.
func (*Client) Events ¶
func (c *Client) Events(ctx context.Context, name string, onUnit func(UnitStatusWire), onEvent func(EventWire)) (*EvaluationStatusWire, error)
Events follows the evaluation's SSE monitor until the end event: onUnit for every unit state, onEvent for relayed in-pod progress (best-effort — the server may not send any). A dropped stream reconnects with backoff, re-fetching the snapshot to reconcile; the final snapshot is returned.
func (*Client) HasWorkspace ¶
HasWorkspace reports whether the digest is already cached server-side.
func (*Client) Submit ¶
func (c *Client) Submit(ctx context.Context, sub *Submission) (*SubmissionResponse, error)
Submit posts the submission and returns the evaluation to monitor.
type Credential ¶
type Credential struct {
// IDToken is the raw ID token (the bearer the API verifies).
IDToken string `json:"idToken"`
// IDTokenExpiry is the verified token's expiry.
IDTokenExpiry time.Time `json:"idTokenExpiry"`
// Token is the OAuth2 access/refresh token pair.
Token *oauth2.Token `json:"token"`
}
Credential is one remote's stored login: the verified ID token presented as the bearer, and the OAuth2 token carrying the refresh token that renews it.
type EvaluationStatusWire ¶
type EvaluationStatusWire struct {
Name string `json:"name"`
// Phase is Pending|Running|Complete|Failed.
Phase string `json:"phase,omitempty"`
Submitter string `json:"submitter,omitempty"`
// Unit counters.
UnitsTotal int `json:"unitsTotal"`
UnitsComplete int `json:"unitsComplete"`
UnitsFailed int `json:"unitsFailed"`
// Units are the per-child states, index-ordered.
Units []UnitStatusWire `json:"units,omitempty"`
}
EvaluationStatusWire is the GET snapshot and the SSE "end" payload.
type Event ¶
type Event struct {
V int `json:"v"`
Type string `json:"type"`
Unit *UnitRef `json:"unit,omitempty"`
// Item carries the per-item payloads (see ItemEvent).
Item *ItemEvent `json:"item,omitempty"`
// Sum rides on unit_finished.
Sum *UnitSummary `json:"sum,omitempty"`
// Msg is the unit_skipped reason or the warn text.
Msg string `json:"msg,omitempty"`
// Result rides on type=result: the unit's outcome, exactly once per
// successful pod, as the last event before exit.
Result *UnitResult `json:"result,omitempty"`
// Error rides on type=fatal: the pod could not produce a result.
Error string `json:"error,omitempty"`
}
Event is one EVOLVE-EVENT line: the pod's progress and result stream.
type EventReporter ¶
type EventReporter struct {
// contains filtered or unexported fields
}
EventReporter implements run.Reporter by emitting EVOLVE-EVENT lines: the in-pod half of the Reporter seam. Emissions are mutex-serialized — ItemDone and Warn fire from the parallel agent-run goroutines — and everything else the process prints must go to stderr, keeping stdout pure event stream.
func NewEventReporter ¶
func NewEventReporter(w io.Writer, plugin string) *EventReporter
NewEventReporter builds an EventReporter writing to w (the pod's stdout). plugin tags every unit reference, since plan.UnitRef does not carry it.
func (*EventReporter) BaselineDone ¶
func (r *EventReporter) BaselineDone(u plan.UnitRef, item run.ItemResult)
BaselineDone implements run.Reporter.
func (*EventReporter) BaselineStarted ¶
func (r *EventReporter) BaselineStarted(u plan.UnitRef, item run.ItemStart)
BaselineStarted implements run.Reporter.
func (*EventReporter) Emit ¶
func (r *EventReporter) Emit(ev Event)
Emit writes one event line directly — the result/fatal channel exec-unit uses beside the Reporter methods.
func (*EventReporter) ItemDone ¶
func (r *EventReporter) ItemDone(u plan.UnitRef, item run.ItemResult)
ItemDone implements run.Reporter.
func (*EventReporter) ItemStarted ¶
func (r *EventReporter) ItemStarted(u plan.UnitRef, item run.ItemStart)
ItemStarted implements run.Reporter.
func (*EventReporter) UnitFinished ¶
func (r *EventReporter) UnitFinished(u plan.UnitRef, sum run.UnitSummary, _ string)
UnitFinished implements run.Reporter. savedRel is dropped: the pod's results path is meaningless to the monitor, which reports its own local save path when it lands the entry.
func (*EventReporter) UnitSkipped ¶
func (r *EventReporter) UnitSkipped(u plan.UnitRef, reason string)
UnitSkipped implements run.Reporter.
func (*EventReporter) UnitStarted ¶
UnitStarted implements run.Reporter.
func (*EventReporter) Warn ¶
func (r *EventReporter) Warn(format string, a ...any)
Warn implements run.Reporter.
type EventWire ¶
type EventWire struct {
// Unit is the EvaluationUnit name.
Unit string `json:"unit"`
// Event is the relayed in-pod event, verbatim.
Event Event `json:"event"`
}
EventWire is an SSE "event" payload: one relayed in-pod event, tagged with the unit it came from.
type HarnessOption ¶
type HarnessOption struct {
// Harness id ("claude", "codex", …).
Harness string `json:"harness"`
// ModelID is the harness-native model id the unit's model maps to.
ModelID string `json:"modelID,omitempty"`
}
HarnessOption is one acceptable harness for a unit, in preference order.
type ItemEvent ¶
type ItemEvent struct {
Index int `json:"index,omitempty"`
Label string `json:"label,omitempty"`
Runs int `json:"runs,omitempty"`
// Total and Mode ride on unit_started: the unit's case count and its
// run mode ("run" or "count-only").
Total int `json:"total,omitempty"`
Mode string `json:"mode,omitempty"`
// Status is pass|fail|skip|error (item_done only).
Status string `json:"status,omitempty"`
Detail string `json:"detail,omitempty"`
Output string `json:"output,omitempty"`
Metrics *ItemMetrics `json:"metrics,omitempty"`
}
ItemEvent carries the per-item payloads. unit_started uses Total/Runs/Mode; item_started uses Index/Label/Runs; item_done (and the baseline pair) uses Index/Label/Status/Detail/Output/Metrics. Local workspace and log paths never appear — they are meaningless outside the pod.
type ItemMetrics ¶
type ItemMetrics struct {
Hits *int `json:"hits,omitempty"`
Runs *int `json:"runs,omitempty"`
AvgRunSeconds *float64 `json:"avgRunSeconds,omitempty"`
InputTokens *int `json:"inputTokens,omitempty"`
CacheReadTokens *int `json:"cacheReadTokens,omitempty"`
CacheCreationTokens *int `json:"cacheCreationTokens,omitempty"`
OutputTokens *int `json:"outputTokens,omitempty"`
CostUSD *float64 `json:"costUSD,omitempty"`
AssertPassed *int `json:"assertPassed,omitempty"`
AssertTotal *int `json:"assertTotal,omitempty"`
}
ItemMetrics mirrors the client's plan.ItemMetrics: the per-item figures a live dashboard renders. All fields optional.
type JudgeSpec ¶
type JudgeSpec struct {
// Model is the canonical provider-qualified judge model id.
Model string `json:"model"`
// ModelID is its harness-native id on the unit's harness.
ModelID string `json:"modelID,omitempty"`
}
JudgeSpec binds the in-pod LLM judge. V1 constraint: the judge model must be runnable by the unit's own harness — the client validates this before submitting.
type ResultSummary ¶
type ResultSummary struct {
CasesPassed int `json:"casesPassed"`
CasesFailed int `json:"casesFailed"`
CasesErrored int `json:"casesErrored"`
// Cases lists per-case outcomes; the pod bounds the list (the CR caps
// it at 256 entries).
Cases []CaseStatus `json:"cases,omitempty"`
// TokenUsage summed over the unit's agent runs.
TokenUsage TokenUsage `json:"tokenUsage,omitempty"`
// ElapsedMS is the unit's wall-clock duration.
ElapsedMS int64 `json:"elapsedMS,omitempty"`
// Outcome is "ok" for a completed unit (graded failures included) or a
// short failure word ("error", "timeout") otherwise.
Outcome string `json:"outcome"`
}
ResultSummary is the typed, bounded portion of a unit result — everything patchy stamps into the EvaluationUnit status.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store persists credentials per normalized remote URL in one user-level file. UserConfigDir, not UserCacheDir: these are durable secrets, not a purgeable cache — evolve's first user-level file (everything else is repo-scoped configuration).
func NewStore ¶
NewStore opens the default user-level store (<UserConfigDir>/evolve/credentials.json).
func NewStoreAt ¶
NewStoreAt opens a store at an explicit path (tests).
type Submission ¶
type Submission struct {
// Version of this contract; must be SubmissionVersion.
Version string `json:"version"`
// Units to run.
Units []UnitSpec `json:"units"`
// TTLSeconds overrides the server's retention of the finished
// evaluation (0 keeps the server default).
TTLSeconds int64 `json:"ttlSeconds,omitempty"`
}
Submission is POST /api/v1/evaluations.
type SubmissionError ¶
type SubmissionError struct {
Error string `json:"error"`
MissingWorkspaces []string `json:"missingWorkspaces,omitempty"`
}
SubmissionError is a non-2xx body. MissingWorkspaces (with a 412) lists digests the client must upload before resubmitting.
type SubmissionResponse ¶
SubmissionResponse is the 201 body: the Evaluation name to monitor and the child unit names, index-ordered.
type SweepOptions ¶
type SweepOptions struct {
run.Options
Client *Client
Tiers plan.Tiers
// Runs per trigger query.
Runs int
// EvalFilter restricts evals to one id ("" = all).
EvalFilter string
// Per-tier timeouts, falling back to Options.Timeout.
TriggerTimeout time.Duration
EvalTimeout time.Duration
// Judge is the resolved judge model (zero = no judge); the pod binds it
// on the unit's own harness.
Judge model.Model
// ClientVersion recorded on every unit.
ClientVersion string
}
SweepOptions configures a remote sweep. Options carries the shared engine configuration (repo, selections, filters, per-run knobs); the sweep plans with AssumeRunnable — eligibility is the server's concern — and never executes anything locally.
type TokenUsage ¶
type TokenUsage struct {
InputTokens int64 `json:"inputTokens,omitempty"`
OutputTokens int64 `json:"outputTokens,omitempty"`
CacheReadTokens int64 `json:"cacheReadTokens,omitempty"`
CacheCreationTokens int64 `json:"cacheCreationTokens,omitempty"`
CostUSD float64 `json:"costUSD,omitempty"`
}
TokenUsage is a unit's token and cost accounting, summed over its agent runs. Cost stays a float on the wire; CR statuses render it as a decimal string.
type UnitRef ¶
type UnitRef struct {
Plugin string `json:"plugin,omitempty"`
Skill string `json:"skill"`
// Key is the provider-qualified model id.
Key string `json:"key"`
// Kind is "triggers" or "evals".
Kind string `json:"kind"`
}
UnitRef identifies the unit an event belongs to, mirroring the client's plan.UnitRef plus the plugin.
type UnitResult ¶
type UnitResult struct {
// Tier (1|2), Model, and Harness echo what actually ran, so the result
// is self-contained.
Tier int `json:"tier"`
Model string `json:"model"`
Harness string `json:"harness"`
// Failed reports whether any executed case failed — the client's
// --strict signal, not a scheduling failure.
Failed bool `json:"failed"`
// Summary is the bounded, typed digest patchy stores in CR status.
Summary ResultSummary `json:"summary"`
// Entry is the finished results entry (the client's TriggerEntry or
// EvalEntry, schema 5), OPAQUE to patchy: stored whole in the results
// ConfigMap and handed back to the client, which merges it into its
// local results file. At most MaxEntryBytes.
Entry json.RawMessage `json:"entry,omitempty"`
}
UnitResult is the type=result payload: the unit's outcome plus the finished results entry.
type UnitSpec ¶
type UnitSpec struct {
// Skill under evaluation and the plugin it belongs to.
Skill string `json:"skill"`
Plugin string `json:"plugin,omitempty"`
// Tier of the run: 1 = triggers, 2 = evals.
Tier int `json:"tier"`
// Model is the canonical provider-qualified model id.
Model string `json:"model"`
// Harnesses that can run the model, in preference order; the scheduler
// launches the first one enabled in the runner fleet.
Harnesses []HarnessOption `json:"harnesses"`
// Workspace bundle the unit runs against.
Workspace WorkspaceRef `json:"workspace"`
// TimeoutMS bounds one agent run's wall clock, per tier semantics.
TimeoutMS int64 `json:"timeoutMS,omitempty"`
// MaxTurns per agent run (0 = the runner default).
MaxTurns int `json:"maxTurns,omitempty"`
// RunsPerQuery repeats each trigger query (tier 1 only).
RunsPerQuery int `json:"runsPerQuery,omitempty"`
// Jobs is the in-pod case concurrency (0 = the runner default).
Jobs int `json:"jobs,omitempty"`
// Baseline also runs each executed eval without the skill (tier 2).
Baseline bool `json:"baseline,omitempty"`
// Judge grades llm assertions (tier 2); nil skips the judge.
Judge *JudgeSpec `json:"judge,omitempty"`
// Cases is the case allowlist (trigger queries or eval ids); nil runs
// all. The client computes --new/--failed/--modified selection locally
// and encodes the outcome here.
Cases []string `json:"cases,omitempty"`
// PriorEntry is the client's existing results entry for this unit,
// opaque to patchy. It seeds fingerprints, previous-run snapshots, and
// baselines in-pod, so those behave exactly as they do locally.
PriorEntry json.RawMessage `json:"priorEntry,omitempty"`
// ClientVersion is the submitting evolve's version, recorded for skew
// diagnostics (warned, never enforced).
ClientVersion string `json:"clientVersion,omitempty"`
}
UnitSpec is one evaluation unit: a skill evaluated on one model at one tier. The triggers/evals spec documents travel in the workspace bundle (the evals/<skill>/ tree), not here — the pod loads them with its own loaders, keeping submissions small and bundles self-contained.
type UnitStatusWire ¶
type UnitStatusWire struct {
// Name of the EvaluationUnit; the client's idempotency key.
Name string `json:"name"`
// Index within the submission, 0-based.
Index int `json:"index"`
Phase string `json:"phase,omitempty"`
// Harness resolved at launch.
Harness string `json:"harness,omitempty"`
// Reason a Failed unit failed:
// WorkspaceLost|HarnessUnavailable|JobFailed|Aborted|ResultTooLarge.
Reason string `json:"reason,omitempty"`
// Detail explains a failure for humans.
Detail string `json:"detail,omitempty"`
// Summary is the bounded digest once the unit settled.
Summary *ResultSummary `json:"summary,omitempty"`
// Result is the full unit result — Entry included — present once the
// unit settled with one.
Result *UnitResult `json:"result,omitempty"`
}
UnitStatusWire is one child's state, sent as an SSE "unit" event and embedded in snapshots. Phase values are Pending|Running|Complete|Failed.
type UnitSummary ¶
type UnitSummary struct {
Executed bool `json:"executed"`
Passed int `json:"passed"`
Failed int `json:"failed"`
Errored int `json:"errored"`
Total int `json:"total"`
AvgRunSeconds *float64 `json:"avgRunSeconds,omitempty"`
}
UnitSummary mirrors the client's run.UnitSummary — the rollup reported when a unit finishes.
type WorkspaceRef ¶
type WorkspaceRef struct {
// Digest is the hex sha256 of the deterministic gzip tarball.
Digest string `json:"digest"`
// SizeBytes of the tarball.
SizeBytes int64 `json:"sizeBytes,omitempty"`
}
WorkspaceRef names a content-addressed workspace bundle.