resourcewrite

package
v1.131.0 Latest Latest
Warning

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

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

Documentation

Overview

Package resourcewrite lets something other than a browser write a managed resource (#1487).

A managed resource is the only kind of file an asset can reference, and until now the only way to put one there was a person at an upload form. That left the data half of a referencing asset unrefreshable by the platform itself: an agent could rewrite the report on a schedule and could not rewrite the CSV the report reads.

This is the same two writes the REST surface makes, against the same store, the same blob client and the same version trail, with the same scope permission rule applied to the caller's own claims. A revision written here is indistinguishable from one uploaded through the portal: same id, same canonical URI, same filename, same history, same retention -- which is what keeps every citation, prompt attachment and asset reference pointing at it resolving across the write.

Index

Constants

View Source
const DefaultListLimit = 100

DefaultListLimit is the page size a listing takes when its caller names none, and the largest page it will answer with. It is a tool's page rather than a browser's: a model reading a folder listing pays for every row in context, and a folder with more rows than this is one to narrow by path.

Variables

View Source
var (
	// ErrRefused is a permission decision: the caller may not write where they
	// asked. The message it wraps states what to change.
	ErrRefused = errors.New("managed-resource write refused")
	// ErrNoSuchResource is a replacement naming a file that is not there, or
	// one the caller cannot see. The two are deliberately the same answer: a
	// caller who should not learn the resource exists must not be able to tell
	// them apart.
	ErrNoSuchResource = errors.New("no such managed resource")
	// ErrUnavailable is the deployment lacking the layer the write needs. It is
	// reported as itself rather than as a failure, so a caller is never told a
	// write happened that could not have.
	ErrUnavailable = errors.New("managed-resource write unavailable")
)

The three ways a write does not happen, told apart because each is the caller's to act on differently.

View Source
var ErrNoLibrary = errors.New("this deployment has no managed-resource library to land in: it needs a database " +
	"and an S3 connection for resource storage. Ask an administrator to configure one, or export to a portal " +
	"asset instead. Nothing was written")

ErrNoLibrary is what an export naming a resource destination is told on a deployment that has no managed-resource library: a database and an S3 connection for resource storage. It names the missing piece rather than reporting a failure, and it is said instead of a write, never after one.

View Source
var ErrTooLarge = errors.New("content exceeds the managed-resource upload ceiling")

ErrTooLarge reports content that passed the managed-resource library's upload ceiling. It is its own answer because it is the caller's to act on: the file is too big for this library, and no retry of the same export changes that.

Functions

func CallerClaims added in v1.131.0

func CallerClaims(ctx context.Context) resource.Claims

CallerClaims derives the managed-resource identity of whoever a tool call belongs to: the principal that made it, the persona it was authorized under, and the address of the person an unattended caller acts for.

It is here, rather than at the composition root, so the derivation a landing is authorized by is the one a test of a landing exercises. A context with no platform identity yields empty claims, which reach only what an anonymous caller reaches.

func ScopePhrase

func ScopePhrase(scope resource.Scope, scopeID string) string

ScopePhrase names a scope the way a refusal has to name it: the thing the caller must change, in words they can act on, never the id of a record they may not be allowed to know exists.

Types

type Deps

type Deps struct {
	Store       resource.Store
	Blobs       resource.S3Client
	Bucket      string
	URIScheme   string
	MaxVersions int
	Registered  func(*resource.Resource)
	// Unregistered takes a deleted resource out of the MCP resource list,
	// keyed on the URI it was registered under. Nil leaves a deleted file
	// listed until the next reload.
	Unregistered func(uri string)
	// Producers records what wrote each resource (#1569). Nil records nothing.
	Producers producedby.Store
}

Deps is what a writer is assembled from: the record store, the blob client the content lives in, and the callback that tells connected clients the resource list moved.

type Lander added in v1.131.0

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

Lander lands an export's bytes in the managed resource at a path, creating the file the first time and recording a new version of it every time after (#1663).

It is the one implementation of toolkit.ResourceLander, shared by every export tool, and it is the Writer's own two writes reached by address instead of by id. That is the whole of what it adds: an export names where the file goes, the platform decides whether that is a create or a revision, and the file's id, its canonical URI and the tables registered over it are the same ones across every run.

The bytes are streamed. Nothing in this path holds the content whole, which is what lets an ingestion script land a file far larger than anything it could pass through its own runtime.

func NewLander added in v1.131.0

func NewLander(d LanderDeps) *Lander

NewLander builds the lander, or nil when there is no writer to land through. A nil lander is what a deployment with no managed-resource library has, and the export tools bound to it report that reason rather than accepting a destination that goes nowhere.

func (*Lander) CheckResourceDestination added in v1.131.0

func (l *Lander) CheckResourceDestination(ctx context.Context, dest toolkit.ResourceDestination) error

CheckResourceDestination validates the destination and the caller's authority over it, writing nothing.

It exists so a refusal lands before the upstream call that produces the bytes: an export that cannot be stored anywhere should not have asked an upstream for anything, least of all through a method that changes something there.

func (*Lander) Land added in v1.131.0

func (l *Lander) Land(
	ctx context.Context, dest toolkit.ResourceDestination,
	content io.Reader, contentType string, claims resource.Claims,
) (*toolkit.ResourceLanding, error)

Land is LandResource for a caller that holds the acting identity itself rather than carrying it on the context: a managed-script run, whose writes go through the platform's own funnels rather than across the MCP middleware that would have put a principal on the context.

func (*Lander) LandResource added in v1.131.0

func (l *Lander) LandResource(
	ctx context.Context, dest toolkit.ResourceDestination, content io.Reader, contentType string,
) (*toolkit.ResourceLanding, error)

LandResource streams content into the destination under the identity the request carries.

func (*Lander) SetTableFollower added in v1.131.0

func (l *Lander) SetTableFollower(follow func(ctx context.Context, resourceID string, version int) []string)

SetTableFollower binds the callback that moves the tables registered over a replaced file. It is a setter rather than a constructor parameter because the table registrar is assembled after the export tools it serves, the same sequencing the asset toolkit's registrar has.

type LanderDeps added in v1.131.0

type LanderDeps struct {
	Writer *Writer
	// Claims derives the acting identity from a request context. Empty takes
	// CallerClaims, which is what every request-driven surface uses; a test
	// substitutes its own.
	Claims func(ctx context.Context) resource.Claims
	// MaxUploadBytes is resources.managed.max_upload_bytes; non-positive takes
	// the library's default.
	MaxUploadBytes int64
}

LanderDeps is what a lander is assembled from.

type Ref added in v1.131.0

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

Ref is the resource destination a toolkit is wired to before the lander behind it exists.

The export tools are assembled with the portal layer, which is built before the managed-resource layer that lands a resource: a direct lander would be nil at the only moment it could be passed. A Ref is handed over instead and bound once the library exists, so a deployment that has one gains the destination and a deployment that has none answers the reason rather than failing in a way that reads as a fault.

func (*Ref) Bind added in v1.131.0

func (r *Ref) Bind(l *Lander)

Bind publishes the lander every export tool holding this Ref will land through. Binding nil leaves the destination unavailable, which is what a deployment with no managed-resource library has.

func (*Ref) CheckResourceDestination added in v1.131.0

func (r *Ref) CheckResourceDestination(ctx context.Context, dest toolkit.ResourceDestination) error

CheckResourceDestination validates the destination, or reports that this deployment has no managed-resource library to land in.

func (*Ref) Land added in v1.131.0

func (r *Ref) Land(
	ctx context.Context, dest toolkit.ResourceDestination,
	content io.Reader, contentType string, claims resource.Claims,
) (*toolkit.ResourceLanding, error)

Land streams the content through the bound lander under an identity the caller holds itself, which is what a managed-script run has: its principal is on its MCP session, not on the context its own output writes cross.

func (*Ref) LandResource added in v1.131.0

func (r *Ref) LandResource(
	ctx context.Context, dest toolkit.ResourceDestination, content io.Reader, contentType string,
) (*toolkit.ResourceLanding, error)

LandResource streams the content through the bound lander.

type Writer

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

Writer creates managed resources and replaces their content.

func New

func New(d Deps) *Writer

New builds the writer, or nil when the deployment has no managed-resource layer to write into: no record store, or nowhere to put the bytes. A nil writer is what a deployment without managed resources has, and the surfaces bound to it report that reason rather than accepting a write that goes nowhere.

A store with no version trail still yields a writer. Creating does not need one -- the trail is recorded when it is there and skipped when it is not, exactly as the upload route treats it -- while replacing does, and says so.

func (*Writer) Create

func (w *Writer) Create(
	ctx context.Context, in resource.NewResource, claims resource.Claims,
) (*resource.Resource, error)

Create files new content as a managed resource under the caller's identity.

The scope permission is checked before anything is written, and the refusal names the scope rather than the file: what the caller has to change is where they filed it, not what they filed.

func (*Writer) Delete added in v1.131.0

func (w *Writer) Delete(ctx context.Context, id string, claims resource.Claims) (*resource.Resource, error)

Delete removes a managed resource and the objects its content lives in, returning the record that was removed (#1665).

The authority is the authority to change the file, which is the same rule replacing its content meets: the person who uploaded it, or an administrator of the library it is filed in. Deleting is not a stronger authority than overwriting -- a replacement already leaves nothing of the previous content beyond the version trail this delete takes with it.

What still points at the file is deliberately not consulted here. That question is the caller's to ask and to put to whoever is deleting, because the answer is a warning rather than a rule: the surface that asks it decides what to do with the answer, and this is the one place the delete itself happens for every surface that reaches it.

func (*Writer) Get

func (w *Writer) Get(ctx context.Context, id string, claims resource.Claims) (*resource.Resource, error)

Get reads a resource the caller may see, so a surface can settle what a replacement is about to change before it changes it -- the stored filename a replacement must keep, and whether the file is there at all, before a payload is decoded.

A resource the caller cannot see reads as absent: those two are deliberately one answer, because a caller who should not learn the resource exists must not be able to tell them apart. A read that FAILED is a third answer and stays one, for the reason given at the check below.

func (*Writer) List added in v1.131.0

func (w *Writer) List(
	ctx context.Context, q toolkit.ResourceQuery, claims resource.Claims,
) (found []resource.Resource, total int, err error)

List returns the resources filed under a folder that the caller may see, newest first, with the total the page was cut from.

The visible libraries are derived from the caller's claims and never from what they asked for: naming a library they cannot reach narrows the answer to nothing rather than widening it.

func (*Writer) Locate added in v1.131.0

func (w *Writer) Locate(
	ctx context.Context, addr toolkit.ResourceAddress, claims resource.Claims,
) (found *resource.Resource, uri string, err error)

Locate resolves an address to the resource filed there, and reports the canonical mcp:// URI the address names whether or not anything is filed at it.

It is the lookup a caller holding a path rather than an id needs, and it is the same resolution a landing performs: the alias trail a move left behind is followed, so a file somebody refiled is still the file that path names, and the record that comes back reports the address it actually lives at.

Nothing being there is an answer rather than an error, which is what lets one lookup serve both callers: a create that may replace asks whether to create or to revise, and a read reports the address as empty. A resource the caller may not see reads as nothing, for the reason Get states. A read that FAILED is a third answer and stays an error, because acting on "nothing is there" when the store could not say is how a second file gets filed at an address that already has one.

The URI is returned beside the record rather than only inside it so a caller can name the address it asked about when the answer is that nothing is filed there.

func (*Writer) Replace

func (w *Writer) Replace(
	ctx context.Context, id string, up resource.RevisionUpload, claims resource.Claims,
) (*resource.Resource, int, error)

Replace records new content as the resource's next revision. The id, the canonical mcp:// URI and the filename are unchanged by contract -- the revision path keys the new blob on a fresh per-revision directory and moves the head onto it -- so every asset referencing the resource resolves to the new bytes without being re-saved.

It returns the version number the content was recorded as, which is what makes the write checkable from the version history rather than only from the bytes.

Jump to

Keyboard shortcuts

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