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
- Variables
- func Register(name string, factory Factory)
- func RegisteredDrivers() []string
- func Validate(scope ArtifactScope) error
- type ArtifactRef
- type ArtifactScope
- type ArtifactStore
- type Factory
- type Presigner
- type PutOpts
- type ScopedArtifacts
- func (s *ScopedArtifacts) Delete(ctx context.Context, id string) (bool, error)
- func (s *ScopedArtifacts) Exists(ctx context.Context, id string) (bool, error)
- func (s *ScopedArtifacts) Get(ctx context.Context, id string) ([]byte, bool, error)
- func (s *ScopedArtifacts) GetRef(ctx context.Context, id string) (*ArtifactRef, bool, error)
- func (s *ScopedArtifacts) List(ctx context.Context) ([]ArtifactRef, error)
- func (s *ScopedArtifacts) PutBytes(ctx context.Context, data []byte, opts PutOpts) (ArtifactRef, error)
- func (s *ScopedArtifacts) PutText(ctx context.Context, text string, opts PutOpts) (ArtifactRef, error)
- func (s *ScopedArtifacts) Scope() ArtifactScope
Constants ¶
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 ¶
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.
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 ¶
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 ¶
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 ¶
func Open(_ context.Context, cfg config.ArtifactsConfig) (ArtifactStore, error)
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 ¶
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 ¶
Delete removes `id` from the facade's scope. Returns whether anything existed before delete; idempotent.
func (*ScopedArtifacts) Get ¶
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). |