Documentation
¶
Overview ¶
Package artifacts holds the cross-cutting versioning logic shared by the artifact_* meta tools, respond_to_user attachment resolution, and the oap artifact CLI. Memory is the source of truth (the artifact + artifact_revision Kinds); this package writes and reads that chain and stamps versioning intent onto ArtifactRender CRs so the prepare->await handoff can finalize a revision regardless of which tool sees Ready.
Index ¶
- Constants
- Variables
- func IsRenderName(s string) bool
- func NewRender(name, namespace, session string, sessionUID types.UID, artifactID string, ...) *spiceboxv1alpha1.ArtifactRender
- func RenderName(session string) string
- type ArtifactView
- type Option
- type RevisionResult
- type RevisionView
- type Service
- func (s *Service) FinalizeRevision(ctx context.Context, scope memory.Scope, cr *spiceboxv1alpha1.ArtifactRender) (RevisionResult, error)
- func (s *Service) GetHead(ctx context.Context, scope memory.Scope, headID string) (memartifact.Artifact, bool, error)
- func (s *Service) GetPreviewChild(ctx context.Context, scope memory.Scope, sourceRevID string) (renderName string, ok bool, err error)
- func (s *Service) GetRevision(ctx context.Context, scope memory.Scope, revID string) (memrev.Revision, bool, error)
- func (s *Service) ListArtifacts(ctx context.Context, scope memory.Scope) ([]ArtifactView, error)
- func (s *Service) NewArtifactID() string
- func (s *Service) NewRenderName(session string) string
- func (s *Service) PreviewChildID(sourceRevID string) string
- func (s *Service) ResolveRevisionTarget(ctx context.Context, scope memory.Scope, handle string) (headID, parentRevID, rendererKind string, err error)
- func (s *Service) ResolveToRender(ctx context.Context, scope memory.Scope, handle string) (string, error)
- func (s *Service) ResolveWidgetCSP(ctx context.Context, scope memory.Scope, handle string) (*spiceboxv1alpha1.WidgetCSP, error)
- func (s *Service) RevisionTree(ctx context.Context, scope memory.Scope, headID string) ([]RevisionView, error)
Constants ¶
const ( // LabelArtifactID groups every render of one logical artifact. Used by // FinalizeRevision and the live-view to List/watch. LabelArtifactID = "artifact.agentprimitives.authzed.com/id" AnnoParentRevision = "artifact.agentprimitives.authzed.com/parent-revision" AnnoChangeDescription = "artifact.agentprimitives.authzed.com/change-description" AnnoAppliedTags = "artifact.agentprimitives.authzed.com/applied-tags" // comma-separated AnnoArtifactName = "artifact.agentprimitives.authzed.com/name" AnnoArtifactDescription = "artifact.agentprimitives.authzed.com/description" // AnnoInternal ("true") marks the artifact head Internal at finalize time. AnnoInternal = "artifact.agentprimitives.authzed.com/internal" // AnnoPreviewOf / AnnoPreviewRevisionOf link a preview child to the source // bundled-only artifact head + the exact source revision it previews. AnnoPreviewOf = "artifact.agentprimitives.authzed.com/preview-of" AnnoPreviewRevisionOf = "artifact.agentprimitives.authzed.com/preview-revision-of" )
Label + annotation keys stamped on ArtifactRender CRs at create time. The render controller never reads these; they are runner-to-runner state that lets FinalizeRevision run from either artifact_prepare or artifact_await. Keys follow the feature-prefixed convention used elsewhere (e.g. channel.agentprimitives.authzed.com/...).
const TagLatest = "latest"
TagLatest is the reserved, system-maintained tag that always points at the newest revision. Authors may not set it explicitly.
Variables ¶
var ErrNotFound = errors.New("artifacts: artifact or revision not found")
ErrNotFound means the artifact handle or its head revision was not found in memory (e.g. the in-memory operator restarted and lost it). Callers can use errors.Is to distinguish a missing artifact (→ 404) from a backend/transport error (→ 500).
Functions ¶
func IsRenderName ¶
IsRenderName reports whether s is a name RenderName produced.
The inverse of the format above and deliberately adjacent to it. Matching is on the WHOLE string, and the session segment must be non-empty, so a bare "ar-abcdef" (no session) is not one.
Used by the replay's bundle format to recognize a render handle inside a recorded tool result, which is how a handle gets pinned rather than re-minted. It cannot be a prefix test: "ar-" is short enough to open plenty of prose, and the fixed-width hex tail is what makes the match specific.
func NewRender ¶
func NewRender(name, namespace, session string, sessionUID types.UID, artifactID string, spec spiceboxv1alpha1.ArtifactRenderSpec, annotations map[string]string) *spiceboxv1alpha1.ArtifactRender
NewRender builds the ArtifactRender CR every creator of one uses — the runner's artifact_prepare and its widget path, and the operator's workshop draft route — so the ownership shape is written once: owned by the session (by UID, as controller, blocking deletion) whenever the UID is known, the artifact id in LabelArtifactID, the head's intent in annotations. The ownership is what respond_to_user and artifact_await check before they will touch a render, so three hand-built copies of it were three places for the check to stop meaning the same thing.
name is minted by the caller: NewRenderName for a model-facing handle, the widget path's own format for a widget. A nil annotations map is left nil.
func RenderName ¶
RenderName mints the name for a new ArtifactRender CR belonging to session.
Random rather than derived: the CR is created before anything durable records it, so there is no earlier value to derive from, and two renders of the same artifact in one session must not collide.
Types ¶
type ArtifactView ¶
type ArtifactView struct {
ArtifactID string `json:"artifact_id"`
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
// RendererKind is the registered renderer producing this artifact's bytes.
RendererKind string `json:"renderer_kind"`
// RevisionCount is the highest revision seq the head has seen.
RevisionCount int `json:"revision_count"`
// Tags maps each tag name to the revision id it currently points at.
Tags map[string]string `json:"tags,omitempty"`
// CreatedAt is UTC, formatted "2006-01-02T15:04:05Z".
CreatedAt string `json:"created_at"`
}
ArtifactView is one logical artifact for listings.
type Option ¶
type Option func(*Service)
Option adjusts a Service at construction.
Variadic rather than more constructor parameters because every one of them is an id seam only a REPLAY sets — six of the seven construction sites in this repo pass nothing at all — and a growing positional signature would make each of those sites spell out two more nils it does not care about.
func WithClock ¶
WithClock overrides the clock a revision's and head's CreatedAt are stamped from. nil is production (time.Now, resolved in nowUTC).
The seam exists because CreatedAt is written into the entry the model reads back through artifact_history, so a deterministic fixture — or a whole-session replay — that leaves it on the wall clock produces bytes that differ run to run whenever two runs straddle a clock tick. That is a property of the clock, not of the tool. It is injected HERE, at the one component that stamps both the revision and the head, for the same reason the id seams are: a package-level hook on time.Now would be global mutable state that parallel tests could not hold apart.
func WithRenderNameMinter ¶
WithRenderNameMinter overrides how the name of a new ArtifactRender CR is minted. nil is production (artifacts.RenderName).
The seam exists for the reason NewArtifactID's does: the name is handed BACK to the model as artifact_prepare's `handle`, so a whole-session replay that re-mints it diverges from the recorded result on a value the run itself chose. Injected HERE rather than at each meta tool, because both tools that create a render CR share this one Service, and a package-level hook would be global mutable state that parallel tests could not hold apart.
Not reachable from a tool argument, same as NewArtifactID: a name the model could choose is a name it could point at another session's render.
func WithRevisionIDMinter ¶
WithRevisionIDMinter overrides how a revision id is derived from the render CR's UID. nil is production (sha256 of the UID, see revisionIDFor).
The contract fn MUST honour ¶
fn is a FUNCTION OF THE UID and is called every time an id is needed, never once per revision: equal uid MUST yield an equal id, for the life of the process. That is not a style preference — it is what makes FinalizeRevision idempotent across the prepare->await handoff, which is a designed flow rather than an error path (artifact_prepare returns `pending` and TELLS the agent to call artifact_await, which re-finalizes the SAME CR). A fn that handed back a different id on the second call would mint a SECOND revision of one render: the head's RevisionCount would double-count, `latest` would point at a revision holding the same bytes as the one before it, and the handle the agent was given would resolve to neither.
The signature is what carries the contract. It takes the uid precisely so an implementation has the key it must be stable across; a bare func() string could not be written correctly at all. bt.MintedIDSequence.KeyedMinter is the replay-side implementation, and it memoizes per key for exactly this reason.
type RevisionResult ¶
type RevisionResult struct {
RevisionID string
ArtifactID string
Seq int
Tags []string // tags now pointing at this revision (incl. "latest")
memrev.Revision
}
RevisionResult is the finalized-revision summary the tools return.
type RevisionView ¶
type RevisionView struct {
RevisionID string `json:"revision_id"`
// Seq is this revision's 1-based position in the artifact's order; higher is newer.
Seq int `json:"seq"`
ChangeDescription string `json:"change_description,omitempty"`
// ParentID is the revision this one revises; empty for the first revision.
ParentID string `json:"parent_id,omitempty"`
// RenderName is the ArtifactRender CR holding this revision's bytes.
RenderName string `json:"render_name"`
// OutputRef is the artifact-store key the rendered bytes live under.
OutputRef string `json:"output_ref"`
MIME string `json:"mime"`
// Size is the rendered output in bytes.
Size int64 `json:"size"`
Filename string `json:"filename,omitempty"`
// Tags are the tag names currently pointing at THIS revision (e.g. "latest").
Tags []string `json:"tags,omitempty"`
// CreatedAt is UTC, formatted "2006-01-02T15:04:05Z".
CreatedAt string `json:"created_at"`
}
RevisionView is one node of an artifact's revision tree. JSON tags keep the agent-facing contract (artifact_history) and any future JSON consumer consistent with artifact_prepare's snake_case result.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service is the cross-cutting artifact-versioning accessor over a memory.Memory.
func NewService ¶
NewService constructs the artifact service.
newID defaults to minting from memory.NewID if nil, which is what every binary passes. The seam exists so a whole-session REPLAY can hand back the very artifact ids the captured run returned to the model, keeping the recorded arguments literal and correct. It is injected HERE, at the component that mints, rather than at memory.NewID: that function is called from every kind's accessor, so a hook on it would be global mutable state and would not survive parallel tests.
It is deliberately not reachable from a tool argument — an id the model could choose is an id it could point at another session's artifact.
The nil default is applied in NewArtifactID rather than here, so that the ONE place deciding what nil means also covers a Service built as a bare &Service{} — the same construction lockForHead already tolerates.
func (*Service) FinalizeRevision ¶
func (s *Service) FinalizeRevision(ctx context.Context, scope memory.Scope, cr *spiceboxv1alpha1.ArtifactRender) (RevisionResult, error)
FinalizeRevision records the just-rendered CR as a revision in memory: the immutable revision is Put, then the head is converged onto it (RevisionCount, latest + applied tags).
It is idempotent AND convergent — different properties, both load-bearing. The two Puts share no transaction and no compare-and-set, so a runner killed between them (SIGTERM / OOM / eviction), or a head Put that errors, leaves the revision durable (RevisionTree reads the revision_of link, not the head) while the head still counts one fewer revision and still tags the PREVIOUS one as latest. A reader sees that half-finalized state directly: artifact-…#latest resolves to the older revision.
That window is not transient, because the retry IS the designed flow: artifact_prepare tells the agent to call artifact_await, which re-finalizes the SAME CR (same UID ⇒ same revisionIDFor). The second finalize must therefore REPAIR the head rather than early-return over it — an early return reports "ready" with empty tags and leaves the artifact permanently unreachable by handle. So: always read both, Put the revision only when absent, and merge into the head every time, writing the head only when the merge changed it.
Concurrency: the body is a read-modify-write of one head and memory.Entry carries no version, so there is no compare-and-set to make it atomic. It is serialized per (scope, head) via lockForHead, which suffices because this is the ONLY writer of the artifact head Kind anywhere and per session runs in exactly one process (the runner, through the single *artifacts.Service its loop and every artifact tool share); webd, `oap`, and the memory bundle handler only read. Without the lock, two artifact_prepare(revises:) calls in one assistant message (dispatchToolUses runs a turn's tool_uses on separate goroutines) both read RevisionCount=N, both mint seq=N+1, and the later head Put overwrites the earlier's applied tags — a revision vanishes from the head and its handle stops resolving.
The lock is deliberately held across both durable Puts: read and write must commit as one unit, and two revisions of one artifact are inherently serial. Different heads use different mutexes, so an unrelated render never waits.
func (*Service) GetHead ¶
func (s *Service) GetHead(ctx context.Context, scope memory.Scope, headID string) (memartifact.Artifact, bool, error)
GetHead loads a logical-artifact head by ID.
func (*Service) GetPreviewChild ¶
func (s *Service) GetPreviewChild(ctx context.Context, scope memory.Scope, sourceRevID string) (renderName string, ok bool, err error)
GetPreviewChild returns the RenderName of the latest revision of the preview child for sourceRevID, and ok=false when no preview has been generated yet (the live-viewer shows "generating preview…" in that case).
func (*Service) GetRevision ¶
func (s *Service) GetRevision(ctx context.Context, scope memory.Scope, revID string) (memrev.Revision, bool, error)
GetRevision loads a revision by ID. ok=false when absent.
func (*Service) ListArtifacts ¶
ListArtifacts returns every logical artifact in the session scope.
func (*Service) NewArtifactID ¶
NewArtifactID mints a fresh logical-artifact head ID. Call this BEFORE creating the ArtifactRender CR so the CR can carry it in LabelArtifactID.
This is the single place a nil newID means "production", which is why NewService just stores what it is handed: one definition covers every construction path, including a bare &Service{} — the same shape lockForHead already tolerates.
func (*Service) NewRenderName ¶
NewRenderName mints the name for a new ArtifactRender CR belonging to session, through the service's seam.
The seam exists for the same reason NewArtifactID's does, and is described in full on WithRenderNameMinter: a whole-session REPLAY hands back the very handles the captured run returned to the model. nil — every production binary — is RenderName.
func (*Service) PreviewChildID ¶
PreviewChildID returns the deterministic head ID of the internal preview artifact for a given source revision. Stable across calls so a re-offer reuses the same child head. It is a valid artifact head ID.
func (*Service) ResolveRevisionTarget ¶
func (s *Service) ResolveRevisionTarget(ctx context.Context, scope memory.Scope, handle string) (headID, parentRevID, rendererKind string, err error)
ResolveRevisionTarget maps a revises handle to the (headID, parent revisionID, rendererKind) used to create a new revision.
func (*Service) ResolveToRender ¶
func (s *Service) ResolveToRender(ctx context.Context, scope memory.Scope, handle string) (string, error)
ResolveToRender maps any handle form to the ArtifactRender CR name that holds its bytes. Callers then run the existing CR ownership + Ready checks against that name.
func (*Service) ResolveWidgetCSP ¶
func (s *Service) ResolveWidgetCSP(ctx context.Context, scope memory.Scope, handle string) (*spiceboxv1alpha1.WidgetCSP, error)
ResolveWidgetCSP maps any handle form to the widget's declared CSP (nil when the widget declared none, or the resolved revision predates CSP persistence) — mirrors ResolveToRender, reusing the same resolveRevision lookup so both agree on which revision a handle names.
func (*Service) RevisionTree ¶
func (s *Service) RevisionTree(ctx context.Context, scope memory.Scope, headID string) ([]RevisionView, error)
RevisionTree returns an artifact's revisions ordered by Seq ascending.