artifacts

package
v1.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 10, 2026 License: Apache-2.0 Imports: 8 Imported by: 0

Documentation

Overview

Package artifacts defines Harbor's content-addressed blob store — the mandatory routing target for any output above the heavy-output threshold (default 32 KB; D-022, D-026, RFC §6.10).

The surface is a single mandatory `ArtifactStore` interface (eight methods including `Close`); there is NO `NoOp` fallback (brief 05 §1). V1 ships two drivers — an in-memory floor for dev/embedded use and a filesystem driver for single-binary production deployments. Phase 18 adds SQLite-blob + Postgres-blob; Phase 19 adds the S3-style driver; all four downstream drivers inherit Phase 17's conformance suite verbatim.

Identity model. `ArtifactScope` is a flat `(TenantID, UserID, SessionID, TaskID)` tuple — deliberately distinct from the runtime's `identity.Quadruple{Identity, RunID}` shape. RFC §6.10 keys artifacts on tasks (foreground OR background); for foreground tasks the consumer (tool dispatcher, Phase 26) maps `RunID → TaskID`. Keeping `ArtifactScope` as a flat-string struct lets the store stay dependency-free of `internal/identity` while still reading like the brief's wire shape.

Identity is mandatory at the API boundary. Empty tenant / user / session each return wrapped `ErrIdentityRequired` from Put*. Empty `TaskID` is acceptable for session-scoped artifacts (parallel to `state.StateStore`'s session-vs-run rule). `List` treats empty fields as wildcards: `ArtifactScope{TenantID: "A"}` lists every artifact under tenant A across users / sessions / tasks.

Get / GetRef return `(value, found, err)`. Found-false is NOT an error — the consumer pattern is "Exists → fetch." `ErrNotFound` is reserved for actual error contexts (e.g. corrupted indexing); the conformance suite tests the `(nil, false, nil)` shape explicitly.

Audit redaction is upstream (D-020). The store stores opaque bytes and never re-redacts; mixing redaction into a leaf would couple the store to the audit subsystem and split responsibility.

Index

Constants

View Source
const DefaultDriver = "inmem"

DefaultDriver is the Phase 17 floor — the in-memory driver. Phase 18 (SQLite-blob, Postgres-blob) and Phase 19 (S3-style) register additional names; `Open` switches on `cfg.Driver`.

Variables

View Source
var (
	// ErrNotFound — reserved for error contexts (e.g. corrupted
	// secondary index pointing at an absent primary). Get / GetRef
	// found-false is NOT this error; it is `(nil, false, nil)`.
	ErrNotFound = errors.New("artifacts: ref not found")
	// ErrScopeMismatch — `ScopedArtifacts` saw a returned ref whose
	// scope differs from the facade's fixed scope. Should be
	// impossible by construction; surfaced loudly when it isn't.
	ErrScopeMismatch = errors.New("artifacts: scope mismatch")
	// ErrIdentityRequired — Put*/Get/GetRef/Exists/Delete called
	// with a scope missing tenant/user/session.
	ErrIdentityRequired = errors.New("artifacts: identity required (tenant/user/session)")
	// ErrInvalidScope — scope failed structural validation outside
	// the identity-required dimension (reserved; not currently
	// returned by V1 drivers).
	ErrInvalidScope = errors.New("artifacts: invalid scope")
	// ErrUnknownDriver — Open was asked for a driver name no
	// registered factory handles.
	ErrUnknownDriver = errors.New("artifacts: unknown driver")
	// ErrStoreClosed — any method called after Close.
	ErrStoreClosed = errors.New("artifacts: store is closed")
)

Sentinel errors. Callers compare via errors.Is.

View Source
var ErrPresignUnsupported = errors.New("artifacts: presigned URLs not supported by this driver")

ErrPresignUnsupported is returned (wrapped) when a caller asks an `ArtifactStore` for presigned URLs via type-assertion to `Presigner` and the underlying driver does not implement the capability. The error's presence is part of the failure-loud contract — silent fallback to byte-streaming would mask backend configuration mistakes.

Functions

func Register

func Register(name string, factory Factory)

Register installs a driver factory under name. Drivers self-register from their package init(); cmd/harbor blank-imports the production drivers to trigger registration. Per AGENTS.md §4.4.

Re-registering the same name panics — the registration model is write-once-at-init and a duplicate signals a build mis-configuration.

func RegisteredDrivers

func RegisteredDrivers() []string

RegisteredDrivers returns a sorted list of driver names. Useful for boot-log output and surfacing in error messages.

func Validate

func Validate(scope ArtifactScope) error

Validate is the package-level helper that mirrors `ArtifactScope.Validate`. Returns wrapped `ErrIdentityRequired` when any of tenant / user / session is empty. Empty `TaskID` is accepted.

Types

type ArtifactRef

type ArtifactRef struct {
	ID        string
	MimeType  string
	SizeBytes int64
	Filename  string
	SHA256    string
	Scope     ArtifactScope
	Namespace string
	Source    map[string]any
}

ArtifactRef is the canonical reference returned by Put* and resolved by GetRef. `ID` is content-addressed: `{namespace}_{sha256_hex[:12]}`. Re-uploading identical bytes within the same scope returns the existing ref (no duplicate storage).

`SHA256` carries the full hex digest (64 chars). `SizeBytes` is the length of the stored bytes. `Source` is opaque caller metadata — drivers persist it as-is; for the FS driver, values must be JSON-encodable (non-encodable values cause Put to fail at marshal time).

type ArtifactScope

type ArtifactScope struct {
	TenantID  string
	UserID    string
	SessionID string
	TaskID    string
}

ArtifactScope identifies the (tenant, user, session, task) owner of an artifact. All four fields are flat strings; the consumer (tool dispatcher Phase 26+) is responsible for translating the runtime's `identity.Quadruple` (whose `RunID` becomes `TaskID` for foreground runs) into this shape.

Mandatory at the API boundary: `TenantID`, `UserID`, `SessionID` must be non-empty for Put*. Empty `TaskID` is acceptable for session-scoped artifacts. `List` treats empty fields as wildcards.

func (ArtifactScope) Equal

func (s ArtifactScope) Equal(other ArtifactScope) bool

Equal reports whether two scopes are field-for-field equal. Used by `ScopedArtifacts` for read-side scope checks and by drivers for cross-tenant isolation enforcement.

func (ArtifactScope) Validate

func (s ArtifactScope) Validate() error

Validate returns wrapped `ErrIdentityRequired` when any of tenant / user / session is empty. Empty `TaskID` is accepted.

Use the package-level `Validate(scope)` helper when you don't have an `ArtifactScope` value handy; both call sites converge on the same rule.

type ArtifactStore

type ArtifactStore interface {
	// PutBytes stores data under scope, returning the canonical ref.
	// The ref's `ID` is `{namespace}_{sha256_hex[:12]}`. Re-Put with
	// identical (scope, namespace, bytes) is a no-op that returns the
	// existing ref.
	PutBytes(ctx context.Context, scope ArtifactScope, data []byte, opts PutOpts) (ArtifactRef, error)

	// PutText is a thin wrapper over PutBytes that stores `text` as
	// UTF-8 bytes. Recovered via Get as bytes. MimeType defaults to
	// `text/plain; charset=utf-8` when opts.MimeType is empty.
	PutText(ctx context.Context, scope ArtifactScope, text string, opts PutOpts) (ArtifactRef, error)

	// Get returns the bytes for `id` within `scope`. Found-false
	// indicates the ref does not exist in this scope; it is NOT an
	// error. ErrNotFound is reserved for actual error contexts.
	Get(ctx context.Context, scope ArtifactScope, id string) ([]byte, bool, error)

	// GetRef returns the metadata-only ref for `id` within `scope`.
	// Same found-false semantics as Get.
	GetRef(ctx context.Context, scope ArtifactScope, id string) (*ArtifactRef, bool, error)

	// Exists reports whether `id` is stored in `scope`. Cheaper than
	// GetRef when the caller only needs presence.
	Exists(ctx context.Context, scope ArtifactScope, id string) (bool, error)

	// Delete removes `id` from `scope` and returns whether anything
	// existed before delete. Idempotent: Delete on absent returns
	// `(false, nil)`.
	Delete(ctx context.Context, scope ArtifactScope, id string) (bool, error)

	// List returns refs whose scope matches `filter`. Empty fields in
	// `filter` are wildcards: `ArtifactScope{TenantID: "A"}` lists
	// every artifact under tenant A across users / sessions / tasks.
	// Order is not specified; callers that need stability sort the
	// returned slice themselves.
	List(ctx context.Context, filter ArtifactScope) ([]ArtifactRef, error)

	// Close releases driver resources. Subsequent calls return
	// wrapped `ErrStoreClosed`. Implementations MUST honour ctx
	// during long teardowns (none of V1's drivers have any).
	Close(ctx context.Context) error
}

ArtifactStore is Harbor's mandatory content-addressed blob store. All eight methods are required; there is no `Supports*` capability ceremony (AGENTS.md §4.4). Implementations MUST be safe for N concurrent goroutines on a single shared instance (D-025); the conformance suite's `Concurrent_PutGet_NoRace` is the gate.

Identity is enforced at the API boundary: every Put*/Get/GetRef/ Exists/Delete validates `scope` before touching storage. `List` accepts a partial filter (empty fields are wildcards).

Get / GetRef return `(value, found, err)`. Found-false is NOT an error.

func Open

Open returns the ArtifactStore built by the factory whose name matches cfg.Driver (defaults to DefaultDriver when cfg.Driver is empty).

func OpenDriver

func OpenDriver(name string, cfg config.ArtifactsConfig) (ArtifactStore, error)

OpenDriver opens a specific driver by name; useful for tests that want to exercise the registry against a non-default driver.

type Factory

type Factory func(config.ArtifactsConfig) (ArtifactStore, error)

Factory builds an ArtifactStore from an ArtifactsConfig. Drivers expose one Factory each via init() → Register.

type Presigner

type Presigner interface {
	// PresignGet returns a time-bounded HTTPS URL the caller can hand
	// to a downstream consumer for direct download of the artifact's
	// bytes. Read-side only — there is no PresignPut / PresignDelete
	// counterpart at V1 (write-side presigned URLs are an attack
	// surface; documented in the Phase 19 plan).
	//
	// Returns a wrapped error when:
	//   - the scope fails identity validation (`ErrIdentityRequired`),
	//   - the artifact does not exist in this scope (`ErrNotFound`),
	//   - `expiry` is out of the `[1 minute, 7 days]` range,
	//   - the underlying signer fails.
	PresignGet(ctx context.Context, scope ArtifactScope, id string, expiry time.Duration) (string, error)
}

Presigner is an OPTIONAL capability interface that backends with native presigned-URL support implement. This is the explicit exception to the "no optional capabilities" rule in AGENTS.md §4.4: only S3-compatible object stores have presigned URLs natively, and the capability cannot be reasonably faked by the other V1 drivers (InMem / FS / SQLite-blob / Postgres-blob) without bolting a separate signing service onto Harbor — which is out of V1 scope.

Per RFC §6.10 + brief 05 §3, presigned URLs are the read-side hand-off path for media-class artifacts (D-021, D-022): the runtime hands a Console / Protocol client a time-bounded HTTPS URL the client downloads directly from object storage, bypassing the runtime's bytes path entirely.

Callers that need presigned URLs type-assert the `ArtifactStore` they hold to `Presigner`; absence is a typed error (`ErrPresignUnsupported`), not a silent fallback. The Phase 19 S3 driver is the only V1 driver implementing this capability.

Identity is mandatory at the Presigner boundary just like every other ArtifactStore method: implementations MUST validate the `scope` and reject with `ErrIdentityRequired` when any of tenant/user/session is empty.

`expiry` is bounded — implementations MUST reject expiries shorter than 1 minute or longer than 7 days (S3's documented limit). Out- of-range expiries return a clear error rather than being silently clamped (AGENTS.md §5: fail loudly).

type PutOpts

type PutOpts struct {
	MimeType  string
	Filename  string
	Namespace string
	Source    map[string]any
}

PutOpts carries optional metadata for Put* calls.

`Namespace` is a logical bucket that participates in `ID` computation, so the same bytes under different namespaces produce distinct refs. Callers SHOULD provide a namespace; drivers default to `"default"` when empty.

`Filename` is metadata only — never used in path construction. The FS driver's path-safety guard rejects traversal regardless.

`Source` values must be JSON-encodable when targeting the FS driver (it persists `Source` to a sibling `.meta.json`). Use Go primitives, slices, and maps; non-encodable values (functions, channels, cyclic graphs) cause Put to fail at marshal time.

type ScopedArtifacts

type ScopedArtifacts struct {
	// contains filtered or unexported fields
}

ScopedArtifacts is the immutable facade tools and runtime use to access the artifact store. It carries a fixed `ArtifactScope` (set at construction, never mutated) and:

  • Auto-stamps the scope on every Put*: callers do not pass scope at the facade boundary, so they cannot accidentally write to a different one.
  • Scope-checks on every read (Get, GetRef, Exists, Delete, List): if the underlying store somehow returned a ref whose scope differs, the facade returns wrapped `ErrScopeMismatch` rather than leaking cross-scope bytes.

Construction fails loud: `NewScoped` panics when the scope fails `Validate`. The facade's invariant is "every operation has a valid fixed scope"; rejecting at construction is the simplest, loudest failure mode (AGENTS.md §5: "fail loudly"). Acceptable per design — the alternative (returning an error from a constructor on every call site) accumulates boilerplate that masks the actual bug: callers that built a facade without a complete identity triple.

func NewScoped

func NewScoped(store ArtifactStore, scope ArtifactScope) *ScopedArtifacts

NewScoped wraps `store` with a fixed `scope`. Panics with a wrapped `ErrIdentityRequired` if scope is invalid (empty tenant/user/session).

Tools and runtime construct ScopedArtifacts at the consumer boundary (e.g. tool dispatcher, Phase 26+); they then never see the raw `ArtifactScope` again.

func (*ScopedArtifacts) Delete

func (s *ScopedArtifacts) Delete(ctx context.Context, id string) (bool, error)

Delete removes `id` from the facade's scope. Returns whether anything existed before delete; idempotent.

func (*ScopedArtifacts) Exists

func (s *ScopedArtifacts) Exists(ctx context.Context, id string) (bool, error)

Exists reports whether `id` is stored within the facade's scope.

func (*ScopedArtifacts) Get

func (s *ScopedArtifacts) Get(ctx context.Context, id string) ([]byte, bool, error)

Get returns the bytes for `id` within the facade's scope. Found-false is NOT an error.

If the underlying store returns bytes for an id whose stored scope doesn't equal the facade's, the facade collapses to `(nil, false, nil)` because the facade's invariant is "all reads are within scope" — a store that disregards scope is reported as `ErrScopeMismatch` via GetRef (which surfaces the mismatch loudly). Drivers that filter by scope (every V1 driver) will simply return found-false for cross-scope ids, so this branch is defensive.

func (*ScopedArtifacts) GetRef

func (s *ScopedArtifacts) GetRef(ctx context.Context, id string) (*ArtifactRef, bool, error)

GetRef returns the ref for `id` within the facade's scope. Found-false is NOT an error. If the underlying store returns a ref whose scope differs from the facade's, GetRef returns wrapped `ErrScopeMismatch` (defensive — V1 drivers filter by scope so this can only fire on a driver bug or future cross-scope driver).

func (*ScopedArtifacts) List

func (s *ScopedArtifacts) List(ctx context.Context) ([]ArtifactRef, error)

List returns every artifact under the facade's scope. The full scope is used as the filter (so this returns artifacts at the facade's task scope, NOT the broader session/user/tenant). Callers that need wildcard listings construct a separate ScopedArtifacts (or call the underlying store's List directly with a wildcard filter — appropriate at admin / observability layers, not tools).

func (*ScopedArtifacts) PutBytes

func (s *ScopedArtifacts) PutBytes(ctx context.Context, data []byte, opts PutOpts) (ArtifactRef, error)

PutBytes stores data under the facade's scope. The scope is stamped onto the returned ref automatically.

func (*ScopedArtifacts) PutText

func (s *ScopedArtifacts) PutText(ctx context.Context, text string, opts PutOpts) (ArtifactRef, error)

PutText stores text under the facade's scope.

func (*ScopedArtifacts) Scope

func (s *ScopedArtifacts) Scope() ArtifactScope

Scope returns the fixed scope this facade was constructed with. Useful for tests / diagnostics; the value is immutable.

Directories

Path Synopsis
Package conformancetest exposes the canonical correctness suite every artifacts.ArtifactStore driver must pass.
Package conformancetest exposes the canonical correctness suite every artifacts.ArtifactStore driver must pass.
drivers
fs
Package fs is Harbor's filesystem ArtifactStore driver.
Package fs is Harbor's filesystem ArtifactStore driver.
inmem
Package inmem is Harbor's V1 in-memory ArtifactStore driver.
Package inmem is Harbor's V1 in-memory ArtifactStore driver.
postgres
Package postgres is Harbor's V1 Postgres-backed ArtifactStore driver.
Package postgres is Harbor's V1 Postgres-backed ArtifactStore driver.
s3
Package s3 is Harbor's S3-compatible ArtifactStore driver.
Package s3 is Harbor's S3-compatible ArtifactStore driver.
sqlite
Package sqlite is Harbor's SQLite-backed `artifacts.ArtifactStore` driver (Phase 18).
Package sqlite is Harbor's SQLite-backed `artifacts.ArtifactStore` driver (Phase 18).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL