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
- Variables
- func CallerClaims(ctx context.Context) resource.Claims
- func ScopePhrase(scope resource.Scope, scopeID string) string
- type Deps
- type Lander
- func (l *Lander) CheckResourceDestination(ctx context.Context, dest toolkit.ResourceDestination) error
- func (l *Lander) Land(ctx context.Context, dest toolkit.ResourceDestination, content io.Reader, ...) (*toolkit.ResourceLanding, error)
- func (l *Lander) LandResource(ctx context.Context, dest toolkit.ResourceDestination, content io.Reader, ...) (*toolkit.ResourceLanding, error)
- func (l *Lander) SetTableFollower(follow func(ctx context.Context, resourceID string, version int) []string)
- type LanderDeps
- type Ref
- func (r *Ref) Bind(l *Lander)
- func (r *Ref) CheckResourceDestination(ctx context.Context, dest toolkit.ResourceDestination) error
- func (r *Ref) Land(ctx context.Context, dest toolkit.ResourceDestination, content io.Reader, ...) (*toolkit.ResourceLanding, error)
- func (r *Ref) LandResource(ctx context.Context, dest toolkit.ResourceDestination, content io.Reader, ...) (*toolkit.ResourceLanding, error)
- type Writer
- func (w *Writer) Create(ctx context.Context, in resource.NewResource, claims resource.Claims) (*resource.Resource, error)
- func (w *Writer) Delete(ctx context.Context, id string, claims resource.Claims) (*resource.Resource, error)
- func (w *Writer) Get(ctx context.Context, id string, claims resource.Claims) (*resource.Resource, error)
- func (w *Writer) List(ctx context.Context, q toolkit.ResourceQuery, claims resource.Claims) (found []resource.Resource, total int, err error)
- func (w *Writer) Locate(ctx context.Context, addr toolkit.ResourceAddress, claims resource.Claims) (found *resource.Resource, uri string, err error)
- func (w *Writer) Replace(ctx context.Context, id string, up resource.RevisionUpload, ...) (*resource.Resource, int, error)
Constants ¶
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 ¶
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") // 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.
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.
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
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.
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
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
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 ¶
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.