Skip to main content
Wildo.ai Coming soon

Files and storage

Let someone supply a file without sharing your session

A short-lived, single-use grant authorizes one upload to a specific resource field through the existing upload route.

An expiring one-use ticket permits an integration to attempt one attachment upload.

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.

DeploymentHandoff available
Reachable backend and configured frontendDirect upload instructions and a browser drop-page URL
Reachable backend, no resolvable frontendDirect upload instructions; uploadPageUrl is null
Missing backend public URLThe 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.

StepValue to use
Upload from an agent host that can execute commandsThe returned uploadCommand, with the local file the command expects; bytes bypass the model channel
Upload through a client HTTP implementationuploadUrl plus the exact uploadHeader.name and uploadHeader.value, as in the REST example above
Ask a person to uploaduploadPageUrl, when non-null; continue from the returned statusUrl
Recover the produced IDUpload response or redeemed status file.fileId
Create or update the parentSubmit 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.

Building a B2B product or an internal tool?

Wildo is not self-service yet. Tell us what you have in mind and we will say plainly whether it fits, and what happens next.