
Let someone supply a file without sharing your session
Sometimes an agent can prepare a record but needs a person to supply the file. Wildo can issue a temporary upload grant for that handoff without giving away the caller’s application session.
The grant names one field and one create or update operation. The file still passes through that field’s constraints and storage flow, and attaching it to the record remains a separate authorized write.
Example — An agent asks for the signed document
An agent prepares a record update and returns an upload page for the document field. The person drops the signed file there. The agent reads the grant status to obtain the file ID, then submits the normal record update using its own authority.
For engineers
Bind delegation to the intended write
The grant service stores the field, operation, minter and concrete upload URL in the token. An update grant also names the target row. This excerpt from file-upload-grant.backend.service.ts follows field validation; comments are omitted.
const constraint = await this.constraintResolver.resolve({ resourceType: target.resourceType, fieldName: target.fieldName });
this.assertDeclarationFitsConstraint(params.declared, constraint, executionContext, target);
const expiresInMinutes = params.expiresInMinutes ?? FILE_UPLOAD_GRANT_DEFAULT_MINUTES;
const metadataWithoutUrl: Omit<FileUploadGrantMetadata, 'uploadUrl' | 'producedFileId'> = {
fieldName: target.fieldName,
operation: target.operation,
minter: { entityType: minter.uploadedByEntityType, id: minter.uploadedBy },
};
const token = await this.consumableTokenService.createToken({
tokenType: CoreConsumableTokenTypes.FILE_UPLOAD_GRANT,
consumptionMode: ConsumableToken_ConsumptionMode.SINGLE_USE,
expiresIn: { value: expiresInMinutes, unit: DurationUnit.MINUTES },
organizationId: executionContext.initiatorIds?.organizationId,
userId: executionContext.initiatorIds?.userId,
resourceIdentifier: String(target.resourceType),
relatedId: target.resourceId,
roles: [...(executionContext.initiatorRoles ?? [])],
metadata: { ...metadataWithoutUrl, uploadUrl: params.uploadUrl },
});
Configure the addresses the recipient must reach
The HTTP mint route reads runtime.endPoints.main_backend_api.publicUrl from the application’s resolved configuration. It refuses minting when that address is absent; it does not construct a trusted upload destination from the incoming Host header. The returned upload and status URLs must be reachable by the party receiving the grant, not only from inside the application’s container network.
For a human drop page, the application must also declare a frontend service. The default frontend selected by configuration needs its own runtime.endPoints[frontendServiceName].publicUrl; the service must be present in frontendServices. The grant service builds uploadPageUrl from that address and the public upload-grant route. Configure deployment addresses through the normal application environment setup, then inspect the resolved values instead of adding a second URL authority inside a custom caller.
| Deployment | Handoff available |
|---|---|
| Reachable backend and configured frontend | Direct upload instructions and a browser drop-page URL |
| Reachable backend, no resolvable frontend | Direct upload instructions; uploadPageUrl is null |
| Missing backend public URL | The HTTP mint request is refused |
Check the returned uploadUrl and statusUrl, and inspect whether uploadPageUrl is present before offering a browser link. A headless deployment can accept the delegated upload through the backend contract; it does not acquire a hosted upload page merely by minting a grant.
Use the returned upload instructions
Both mint doors return the upload URL, a credential header, shell upload commands and a status URL. When a frontend is configured, the response also supplies the browser drop-page URL. Use those returned values so the client follows the exact bound route.
The default lifetime is 15 minutes with a framework ceiling of 60 minutes. Anonymous sessions and consumable-token callers cannot mint another grant. Ordinary operation authorization still applies; the grant does not create permission to read or modify other records.
Carry one file through the complete handoff
This illustrative browser JavaScript follows the Wonder Todos attachment test. recordUrl is the existing todo-list’s API URL, bearer belongs to a caller allowed to update it, and file is a selected File. Start with an empty attachments field: this example replaces its value with one file. An add-to-existing workflow must preserve the current IDs and handle concurrent edits.
async function attachFile({ recordUrl, bearer, file }) {
const readJson = async (response) => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
};
const authority = { authorization: `Bearer ${bearer}` };
const jsonHeaders = { ...authority, 'content-type': 'application/json' };
const grant = await readJson(await fetch(`${recordUrl}/files/attachments/grant`, {
method: 'POST',
headers: jsonHeaders,
body: JSON.stringify({ expiresInMinutes: 10 }),
}));
const form = new FormData();
form.append('file', file, file.name);
await readJson(await fetch(grant.uploadUrl, {
method: 'POST',
headers: { [grant.uploadHeader.name]: grant.uploadHeader.value },
body: form,
}));
const status = await readJson(await fetch(grant.statusUrl));
if (status.state !== 'redeemed' || !status.file?.fileId) {
throw new Error('The grant has not produced a file ID');
}
const attachments = {
fileIds: [status.file.fileId],
updatedAt: new Date().toISOString(),
};
await readJson(await fetch(recordUrl, {
method: 'PUT',
headers: jsonHeaders,
body: JSON.stringify({ attachments }),
}));
return readJson(await fetch(recordUrl, { headers: authority }));
}
The upload request carries only the returned grant header; the final write and read use the original Bearer. Let FormData set the multipart content type. For a human handoff, show uploadPageUrl and resume at the status read after upload instead of transferring bytes in this function. That browser URL requires a configured frontend.
Inspect the record in the final response (response.data for an enveloped response, otherwise the response itself): it should contain the same ID in attachments.fileIds. A single-file field instead uses { fileId, updatedAt }. A redeemed grant proves that bytes produced a file, not that scanning has finished or the parent accepted it: pending scanning can still delay attachment. Check file readiness and retain the produced ID while resolving a pending result; do not redeem the same grant again to retry the parent write.
Handle retries according to the claim point
Route matching and actionable file validation happen before the single-use token is consumed. Consumption occurs before the file row and byte write, so concurrent redemptions cannot both start an accepted upload. A refusal before that point can leave the grant usable; a failure after consumption requires a new grant.
The upload is attributed to the minter recorded in the grant. Possession of the link does not establish the identity of the person holding it. Status exposes the produced file ID when write-back succeeds, while the final resource create or update uses the caller’s normal operation permissions.
Give an MCP caller the same upload path
Opt the parent CREATE or UPDATE operation into MCP with mcp: { exposed: true, description: '…' } on its supported default URL-bearing variant. When that request contains a user-uploadable file field, Wildo derives the upload-grant tool and adds instructions to the parent’s description. There is no second upload tool implementation to author.
For a resource named todos with an exposed CREATE operation and an attachments field, the derived name is todos__create.upload_grant.attachments. Discover the actual name and input schema in the server’s tool list: UPDATE also requires the parent’s instance identifier, while CREATE does not.
This illustrative tools/call request declares the file before minting, so an impossible MIME type or size can be refused before upload:
{
"method": "tools/call",
"params": {
"name": "todos__create.upload_grant.attachments",
"arguments": {
"name": "inspection.jpg",
"mimeType": "image/jpeg",
"size": 184320,
"expiresInMinutes": 10
}
}
}
Read the grant object from the tool result, then use the returned instructions rather than constructing an upload URL yourself.
| Step | Value to use |
|---|---|
| Upload from an agent host that can execute commands | The returned uploadCommand, with the local file the command expects; bytes bypass the model channel |
| Upload through a client HTTP implementation | uploadUrl plus the exact uploadHeader.name and uploadHeader.value, as in the REST example above |
| Ask a person to upload | uploadPageUrl, when non-null; continue from the returned statusUrl |
| Recover the produced ID | Upload response or redeemed status file.fileId |
| Create or update the parent | Submit that ID in { fileId, updatedAt } or { fileIds: [...], updatedAt } under the parent field, using normal operation authorization |
A grant inherits the exposed parent operation’s gating; it does not grant the caller broader record access. Upload success, processing readiness and successful parent attachment remain separate steps. Treat the header and token-bearing page/status URLs as bearer credentials and keep them out of public logs.