artifactref

package
v1.24.0 Latest Latest
Warning

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

Go to latest
Published: Jul 30, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

Documentation

Overview

Package artifactref carries Harbor's in-process pass-by-reference tool-argument routing: a tool declares an artifact-reference PARAMETER in its typed input struct, the model supplies an artifact id, and the runtime resolves that id to the stored bytes at DISPATCH so the tool function reads content the model never saw.

Because in-process tool input schemas are reflection-derived from the Go type (internal/tools/drivers/inproc.RegisterFunc -> internal/tools/schema.Derive), a reference parameter is a declared FIELD TYPE rather than a hand-written schema convention:

type SummarizeArgs struct {
    Doc      artifactref.Ref `json:"doc"`
    MaxWords int             `json:"max_words"`
}

func summarize(ctx context.Context, in SummarizeArgs) (Out, error) {
    body, err := in.Doc.Bytes() // the store's bytes, never the model's
    ...
}

The derived schema renders `doc` as a plain string, so the model authors `{"doc": "tool_ab12cd34ef56", "max_words": 200}` and never authors — or reads — content.

The substitution invariant

A resolved artifact value never re-enters the model's context or the observable record. When the runtime substitutes a resolved value into a dispatched tool argument, that value is DISPATCH-LOCAL: it does not appear in the trajectory, in the observation the runtime interleaves into the next chat thread, in any canonical event payload, in an audit payload, or in a log. The model authored an id and continues to see an id; the substitution is the runtime's and it ends at the tool boundary.

The invariant is held three ways, in descending order of durability:

  1. A bound on PRODUCTION. Substitute is the ONE call site at which a resolved value enters a dispatched argument, and ScanSubstitutionSites holds it to one mechanically. An invariant about where a value does not travel is enforced most durably by bounding where it is produced; enumerating every place it might arrive is the check that goes stale.

  2. A projection bound on the CARRIER. Ref keeps the resolved bytes in an unexported field and projects itself as its id through every serialisation door the runtime has: Ref.MarshalJSON emits the id, Ref.String returns the id, and Ref.LogValue renders the id. A `Ref` that reaches `json.Marshal`, `fmt.Sprintf` or `slog` therefore emits an id rather than content, by construction rather than by discipline.

  3. An arrival check at the LLM edge, which lives with the safety net rather than here (internal/llm.findContextLeak).

Extent

This is the IN-PROCESS arm. The consumer runs inside the Runtime, so resolution is a store read and a struct field: no URL, no third party, no egress. Handing a remote consumer something it can dereference is a separate, deliberately unbuilt design — see the routing notes in docs/decisions.md.

Concurrent reuse

Nothing in this package holds mutable package-level state. A Ref is decoded fresh per invocation and lives on the per-call argument value; the resolver rides the dispatch ctx, scoped to the dispatching run's identity, and is never read from a shared artifact.

Index

Constants

View Source
const EgressPkgPath = "github.com/hurtener/Harbor/internal/tools/artifactegress"

EgressPkgPath is the import path of the SIBLING package that holds the remote-egress encoder — the second writer that puts a resolved artifact value into a dispatched tool argument.

It is a STRING here rather than an import for two reasons, and both are load-bearing:

  1. This package must not import the egress package. The scan resolves an import path textually, so a string costs nothing and an import would create a dependency edge purely to name a constant.
  2. The scan returns early for a file that does not import the package it resolves. A file INSIDE the scanned package is therefore invisible to its own scan — so the egress encoder is deliberately NOT housed here, where it would have been unscanned by construction. The residual blind spot (a second call from inside the egress package itself) is bounded by a test asserting that package's non-test file set, so "the package is small enough to read" is checked rather than asserted.
View Source
const KindEgressValueRef = "egress_writer_value_reference"

KindEgressValueRef — the remote-egress encoder named as a VALUE rather than called. Same reasoning as KindSubstitutionValueRef: a bound counted in call positions is no bound once the function itself can travel.

View Source
const KindSubstitutionValueRef = "substitution_writer_value_reference"

KindSubstitutionValueRef — the substitution writer named as a VALUE rather than called: assigned to a variable or field, passed as an argument, returned, or stored in a map. The bound this scan holds is on call sites, and a function value carries the call anywhere the value goes — so a reference outside call position is reported wherever it appears, INCLUDING from a file on the reviewed list. The list registers a site that substitutes; it does not hand out the writer.

View Source
const KindUnregisteredEgress = "unregistered_egress_site"

KindUnregisteredEgress — a call to the remote-egress encoder from a site that is not on the reviewed list, or a list entry that names no such call.

View Source
const KindUnregisteredSubstitution = "unregistered_substitution_site"

KindUnregisteredSubstitution — a call to the substitution writer from a site that is not on the reviewed list, or a list entry that names no such call.

View Source
const PkgPath = "github.com/hurtener/Harbor/internal/tools/artifactref"

PkgPath is the import path of this package — the home of the substitution writer the scan holds to a reviewed list.

Variables

View Source
var ErrEmptyID = errors.New("artifactref: artifact reference id is empty")

ErrEmptyID is returned when an argument supplies an artifact reference whose id is empty. An omitted optional reference and an explicitly empty one are different facts, and only the second is a malformed argument.

View Source
var ErrNoResolver = errors.New("artifactref: no artifact resolver on the dispatch context")

ErrNoResolver is returned when a tool argument carries an artifact reference but the dispatch context seats no resolver. It is a loud failure on purpose: a silently unresolved reference would hand the tool function a zero value and let the call proceed against nothing, which is the silent-degradation shape Harbor forbids.

View Source
var ErrNotAddressable = errors.New("artifactref: substitution target is not addressable")

ErrNotAddressable is returned by Substitute when it is handed something it cannot write through — anything other than a non-nil pointer to the decoded argument value.

View Source
var ErrUnresolved = errors.New("artifactref: reference was not resolved at dispatch")

ErrUnresolved is returned by Ref.Bytes when the reference was never resolved — either the argument omitted it, or the value was built outside a dispatch that ran Substitute.

Functions

func Substitute

func Substitute(ctx context.Context, ptr any) error

Substitute is THE ONE call site at which a resolved artifact value enters a dispatched tool argument.

It walks the decoded argument value behind ptr, and for every supplied Ref it resolves the id through the resolver seated on ctx and binds the stored bytes onto that reference. Everything it writes is dispatch-local: the bytes live on the per-invocation argument value and are unreachable through any of Ref's serialisation doors.

It fails loudly rather than degrading: a supplied reference with no seated resolver is ErrNoResolver, an empty id is ErrEmptyID, and a resolver error is wrapped with the id that produced it. An UNSUPPLIED reference is left alone — an optional parameter the model omitted is not an error, and Ref.Bytes fails loudly if the tool reads it anyway.

Calls to this function are held to a reviewed list by ScanSubstitutionSites. Adding a second call site is a reviewed decision, not an edit.

func TypeContainsRef

func TypeContainsRef(t reflect.Type) bool

TypeContainsRef reports whether t can hold a Ref anywhere a reflective walk could reach and write. Callers precompute it once per registered tool so an argument type with no reference parameter never pays for the walk.

Interface-typed fields report false: a JSON decode cannot produce a Ref inside an `any`, and reflection cannot write through one. The schema deriver treats an empty interface as unconstrained for the same reason, so the two agree on what a reference parameter can be.

func WithResolver

func WithResolver(ctx context.Context, r Resolver) context.Context

WithResolver seats r on ctx for the dispatch that follows. The dispatching runtime is the only caller: the resolver it seats is closed over the run's identity scope, so seating one elsewhere would be handing a tool a reach its run does not have.

Types

type Ref

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

Ref is an artifact-reference tool PARAMETER.

On the wire (and in the derived JSON Schema) a Ref is a plain string: the artifact id the model authors. In the tool function it is a handle the runtime has already resolved — call Ref.Bytes for the stored content.

The resolved value is unexported and every serialisation door projects the id instead: Ref.MarshalJSON, Ref.String and Ref.LogValue. That is what makes "the resolved value never reaches the observable record" a property of the type rather than a rule contributors remember.

The zero Ref is a valid "not supplied" value: Ref.ID is empty and Ref.Bytes fails with ErrUnresolved.

func NewRef

func NewRef(id string) Ref

NewRef builds a Ref carrying id and no resolved value. It exists for callers assembling arguments in Go (tests, typed embedders) rather than decoding them from a model's JSON; the resulting Ref resolves at dispatch exactly as a decoded one does.

func (Ref) Bytes

func (r Ref) Bytes() ([]byte, error)

Bytes returns the stored artifact content the runtime resolved at dispatch. The returned slice is the tool's to read and MUST NOT be retained past the call or mutated — it is the per-invocation buffer, not a copy.

Fails with ErrUnresolved when the reference was never resolved, rather than returning an empty slice a caller could mistake for an empty artifact.

func (Ref) ID

func (r Ref) ID() string

ID returns the artifact reference id the argument carried. It is the only part of a Ref that is safe to log, emit or persist.

func (Ref) LogValue

func (r Ref) LogValue() slog.Value

LogValue renders the reference as its id, so `slog.Any("args", in)` over a tool's argument struct logs ids and never content.

func (Ref) MarshalJSON

func (r Ref) MarshalJSON() ([]byte, error)

MarshalJSON emits the reference id. A Ref that reaches an event payload, a trajectory step, an audit view or a re-encoded argument map therefore serialises as the id the model authored — the substitution invariant expressed as a type property.

func (Ref) Resolved

func (r Ref) Resolved() bool

Resolved reports whether the runtime bound stored content to this reference at dispatch.

func (Ref) Size

func (r Ref) Size() int

Size returns the byte length of the resolved content, or 0 when the reference is unresolved.

func (Ref) String

func (r Ref) String() string

String renders the reference as its id, so a `%v` or `%s` verb over a tool's argument struct prints ids and never content.

func (Ref) Supplied

func (r Ref) Supplied() bool

Supplied reports whether the argument carried this reference at all. An omitted optional reference reports false.

func (*Ref) UnmarshalJSON

func (r *Ref) UnmarshalJSON(b []byte) error

UnmarshalJSON accepts the artifact id as a JSON string. A JSON null leaves the reference unsupplied.

type Resolver

type Resolver interface {
	ResolveArtifact(ctx context.Context, id string) ([]byte, error)
}

Resolver resolves an artifact reference id to its stored bytes. The dispatching runtime seats one on the ctx, closed over the run's own identity scope, so a tool reaches the bytes its own run's identity reaches and no others — there is no identity logic in any tool.

A resolver reports a missing artifact as an error, never as empty content: a reference the model authored against nothing is a fact the tool must be told about.

func ResolverFrom

func ResolverFrom(ctx context.Context) (Resolver, bool)

ResolverFrom returns the resolver seated on ctx, if any.

type ResolverFunc

type ResolverFunc func(ctx context.Context, id string) ([]byte, error)

ResolverFunc adapts a function to Resolver.

func (ResolverFunc) ResolveArtifact

func (f ResolverFunc) ResolveArtifact(ctx context.Context, id string) ([]byte, error)

ResolveArtifact implements Resolver.

type Violation

type Violation struct {
	File   string
	Line   int
	Kind   string
	Detail string
}

Violation is one breach found by the scan: the offending file (relative to the scanned root), the 1-based line, the kind of breach, and a detail that names the fix rather than only the mismatch.

func ScanEgressSites added in v1.24.0

func ScanEgressSites(root string, allow map[string]string) ([]Violation, int, error)

ScanEgressSites walks the Go source under root and flags every call to the remote-egress encoder that is not on the reviewed list.

It is a SEPARATE scan from ScanSubstitutionSites rather than an extension of it, because the two writers live in two packages and the scan returns early for a file that imports neither. Widening the substitution scan's allow-list would have covered nothing on this path anyway: the MCP arm does not and cannot call Substitute, which walks a Go type tree that a remote-authored input schema does not have.

What this bounds, honestly: where the encoder is CALLED, not where its output travels. Downstream of the encoder the value is data, and an AST walk over call sites says nothing about data flow. It is one of three mechanisms — the others being the carrier's projection bound (a Payload serialises as a reference at every door but MarshalJSON) and an arrival check over every sink the raw arguments reach.

allow maps a repo-relative file path to the reason that file is permitted to encode. Blank reasons and entries matching no call site are both reported, so the list stays a description of the code.

Returns the violations and the number of Go files read.

func ScanSubstitutionSites

func ScanSubstitutionSites(root string, allow map[string]string) ([]Violation, int, error)

ScanSubstitutionSites walks the Go source under root and flags every call to the substitution writer that is not on the reviewed list.

allow maps a repo-relative file path to the reason that file is permitted to substitute. The list is short by construction: binding a resolved artifact value into a dispatched argument is the runtime's act at the tool boundary, and it happens in one place.

Returns the violations and the number of Go files read.

func (Violation) String

func (v Violation) String() string

String renders a Violation as `file:line: kind: detail`.

Jump to

Keyboard shortcuts

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