# Publish runtime artifacts

Upload generated sandbox files as downloadable Salambo run artifacts.

Runtime artifacts are explicit. Salambo does not upload every file in the sandbox automatically; publish only the files that are safe and useful for the user.

A sandbox receives these runtime-scoped environment variables during run startup:

| Variable                      | Purpose                                                                |
| ----------------------------- | ---------------------------------------------------------------------- |
| `SALAMBO_ARTIFACT_UPLOAD_URL` | Run-scoped upload endpoint for generated artifacts.                    |
| `SALAMBO_ARTIFACT_TOKEN`      | Bearer token with the `artifact.write` capability for the current run. |
| `SALAMBO_OUTPUT_DIR`          | Recommended output root, defaulting to `/workspace/outputs`.           |

The token is scoped to the current account/run and does not expose storage provider credentials.

## Upload a file

Use a small helper in your sandbox code instead of hardcoding an endpoint:

```ts
import { createReadStream } from 'node:fs';
import { stat } from 'node:fs/promises';
import { basename } from 'node:path';

export async function uploadArtifact(
  filePath: string,
  options: { path: string; contentType?: string },
) {
  const uploadUrl = process.env.SALAMBO_ARTIFACT_UPLOAD_URL;
  const token = process.env.SALAMBO_ARTIFACT_TOKEN;

  if (!uploadUrl || !token) {
    throw new Error('Artifact publishing is not available for this run.');
  }

  const file = await stat(filePath);
  const request = {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': options.contentType ?? 'application/octet-stream',
      'Content-Length': String(file.size),
      'x-display-path': options.path,
      'x-file-name': basename(filePath),
    },
    body: createReadStream(filePath),
    duplex: 'half',
  } satisfies RequestInit & { duplex: 'half' };
  const response = await fetch(uploadUrl, request);

  if (!response.ok) {
    const message = await response.text();
    throw new Error(`Artifact upload failed (${response.status}): ${message}`);
  }
}
```

Example:

```ts
await uploadArtifact('/workspace/report.pdf', {
  path: '/reports/report.pdf',
  contentType: 'application/pdf',
});
```

## Path and size rules

* Use logical artifact paths such as `/report.txt` or `/reports/summary.json`.
* `/workspace/outputs/...` remains accepted for compatibility and is normalized to a logical path.
* Do not publish secrets, credentials, or temporary files.
* Re-uploading the same logical path replaces the previous artifact metadata for the run.
* The default maximum artifact size is 100 MB unless the deployment is configured otherwise.

Published files appear in the run UI and are downloadable through the Files API.
